RunNode 接入游戏主流程

- NodeMapForm:节点地图
    - NodeItem:节点入口
This commit is contained in:
SepComet
2026-03-08 11:27:51 +08:00
parent c81bf97903
commit 5c6f9bf3a4
94 changed files with 6145 additions and 651 deletions
@@ -174,7 +174,7 @@ namespace GeometryTD.CustomComponent
EnsureCombatRuntimeInitialized();
}
public void StartCombat()
public void StartCombat(int levelId = 0)
{
if (!EnsureCombatRuntimeInitialized())
{
@@ -182,9 +182,14 @@ namespace GeometryTD.CustomComponent
return;
}
if (!TrySelectRandomLevel(out DRLevel selectedLevel))
bool hasLevel = levelId > 0
? TryGetLevelById(levelId, out DRLevel selectedLevel)
: TrySelectRandomLevel(out selectedLevel);
if (!hasLevel)
{
Log.Warning("CombatNodeComponent has no level cache. Call OnInit(levelThemeType) first.");
Log.Warning(
"CombatNodeComponent start failed. LevelId={0}. Missing cached level data or invalid level id.",
levelId);
return;
}
@@ -348,6 +353,17 @@ namespace GeometryTD.CustomComponent
return _levelsById.TryGetValue(selectedLevelId, out level);
}
private bool TryGetLevelById(int levelId, out DRLevel level)
{
level = null;
if (levelId <= 0)
{
return false;
}
return _levelsById.TryGetValue(levelId, out level);
}
private int CountPhases()
{
int count = 0;
@@ -15,7 +15,6 @@ namespace GeometryTD.CustomComponent
private readonly List<EventItem> _eventItems = new List<EventItem>();
private EventFormUseCase _eventFormUseCase;
private EventFormController _eventFormController;
private bool _initialized;
public void OnInit()
@@ -42,11 +41,7 @@ namespace GeometryTD.CustomComponent
_eventFormUseCase = new EventFormUseCase();
}
if (_eventFormController == null)
{
_eventFormController = new EventFormController();
_eventFormController.BindUseCase(_eventFormUseCase);
}
GameEntry.UIRouter.BindUIUseCase(UIFormType.EventForm, _eventFormUseCase);
_initialized = true;
Log.Info("EventNodeComponent initialized with {0} events.", _eventItems.Count);
@@ -68,14 +63,14 @@ namespace GeometryTD.CustomComponent
int index = Random.Range(0, _eventItems.Count);
EventItem randomEvent = _eventItems[index];
if (_eventFormUseCase == null || _eventFormController == null)
if (_eventFormUseCase == null)
{
Log.Warning("EventNodeComponent StartEvent failed. Event form is not initialized.");
return;
}
_eventFormUseCase.SetCurrentEvent(randomEvent);
_eventFormController.OpenUI();
GameEntry.UIRouter.OpenUI(UIFormType.EventForm);
GameEntry.Event.Fire(this, NodeEnterEventArgs.Create());
}
@@ -177,4 +172,4 @@ namespace GeometryTD.CustomComponent
return effects.ToArray();
}
}
}
}
+6 -42
View File
@@ -1,19 +1,5 @@
//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2021 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------
// 此文件由工具自动生成,请勿直接修改。
// 生成时间:2021-06-16 21:54:35.652
//------------------------------------------------------------
using GameFramework;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO;
using System.Text;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.DataTable
@@ -28,49 +14,27 @@ namespace GeometryTD.DataTable
/// <summary>
/// 获取界面编号。
/// </summary>
public override int Id
{
get
{
return m_Id;
}
}
public override int Id => m_Id;
/// <summary>
/// 获取资源名称。
/// </summary>
public string AssetName
{
get;
private set;
}
public string AssetName { get; private set; }
/// <summary>
/// 获取界面组名称。
/// </summary>
public string UIGroupName
{
get;
private set;
}
public string UIGroupName { get; private set; }
/// <summary>
/// 获取是否允许多个界面实例。
/// </summary>
public bool AllowMultiInstance
{
get;
private set;
}
public bool AllowMultiInstance { get; private set; }
/// <summary>
/// 获取是否暂停被其覆盖的界面。
/// </summary>
public bool PauseCoveredUIForm
{
get;
private set;
}
public bool PauseCoveredUIForm { get; private set; }
public override bool ParseDataRow(string dataRowString, object userData)
{
@@ -37,15 +37,20 @@
/// </summary>
RepoForm = 111,
/// <summary>
/// 节点地图界面。
/// </summary>
NodeMapForm = 112,
/// <summary>
/// 道具详细信息界面。
/// </summary>
ItemDescForm = 112,
ItemDescForm = 113,
/// <summary>
/// 奖励三选一界面。
/// </summary>
RewardSelectForm = 113,
RewardSelectForm = 114,
/// <summary>
/// 事件节点界面。
@@ -77,4 +82,4 @@
/// </summary>
TestMenuForm = 200,
}
}
}
@@ -0,0 +1,26 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public sealed class NodeMapNodeClickEventArgs : GameEventArgs
{
public static int EventId => typeof(NodeMapNodeClickEventArgs).GetHashCode();
public override int Id => EventId;
public int SequenceIndex { get; private set; }
public static NodeMapNodeClickEventArgs Create(int sequenceIndex)
{
NodeMapNodeClickEventArgs args = ReferencePool.Acquire<NodeMapNodeClickEventArgs>();
args.SequenceIndex = sequenceIndex;
return args;
}
public override void Clear()
{
SequenceIndex = -1;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f16aabf150fd6f640ad6e2ab01705d4f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using GameFramework;
using GameFramework.Event;
using GeometryTD.Procedure;
namespace GeometryTD.CustomEvent
{
public sealed class NodeMapNodeEnterRequestedEventArgs : GameEventArgs
{
public static int EventId => typeof(NodeMapNodeEnterRequestedEventArgs).GetHashCode();
public override int Id => EventId;
public string RunId { get; private set; }
public int NodeId { get; private set; }
public RunNodeType NodeType { get; private set; }
public int SequenceIndex { get; private set; }
public static NodeMapNodeEnterRequestedEventArgs Create(
string runId,
int nodeId,
RunNodeType nodeType,
int sequenceIndex)
{
NodeMapNodeEnterRequestedEventArgs args = ReferencePool.Acquire<NodeMapNodeEnterRequestedEventArgs>();
args.RunId = runId;
args.NodeId = nodeId;
args.NodeType = nodeType;
args.SequenceIndex = sequenceIndex;
return args;
}
public override void Clear()
{
RunId = null;
NodeId = 0;
NodeType = RunNodeType.None;
SequenceIndex = -1;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef66951469c024d49a35903ac4c6eb13
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6892232c51df8e1439ccc908fb93307b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class MenuExitRequestedEventArgs : GameEventArgs
{
public static int EventId => typeof(MenuExitRequestedEventArgs).GetHashCode();
public override int Id => EventId;
public static MenuExitRequestedEventArgs Create()
{
return ReferencePool.Acquire<MenuExitRequestedEventArgs>();
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6b431184ddeace4489032ecdd5996b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class MenuSettingsRequestedEventArgs : GameEventArgs
{
public static int EventId => typeof(MenuSettingsRequestedEventArgs).GetHashCode();
public override int Id => EventId;
public static MenuSettingsRequestedEventArgs Create()
{
return ReferencePool.Acquire<MenuSettingsRequestedEventArgs>();
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b9028de1ea48854caaafc4c9022319a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,21 @@
using GameFramework;
using GameFramework.Event;
namespace GeometryTD.CustomEvent
{
public class MenuStartRequestedEventArgs : GameEventArgs
{
public static int EventId => typeof(MenuStartRequestedEventArgs).GetHashCode();
public override int Id => EventId;
public static MenuStartRequestedEventArgs Create()
{
return ReferencePool.Acquire<MenuStartRequestedEventArgs>();
}
public override void Clear()
{
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b6605592d400a4b478c9380e0c97ad3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d17e5e91c45a8814c954edf31b063100
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -86,7 +86,7 @@ namespace GeometryTD.Procedure
ChangeState<ProcedureMenu>(procedureOwner);
break;
case SceneType.Main:
ChangeState<ProcedureTest>(procedureOwner);
ChangeState<ProcedureMain>(procedureOwner);
break;
default:
Log.Debug($"Scene {sceneType.ToString()} don't configure a procedure");
@@ -146,4 +146,4 @@ namespace GeometryTD.Procedure
ne.DependencyAssetName, ne.LoadedCount.ToString(), ne.TotalCount.ToString());
}
}
}
}
@@ -0,0 +1,213 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
namespace GeometryTD.Procedure
{
public class ProcedureMain : ProcedureBase
{
public override bool UseNativeDialog => false;
private RepoFormUseCase _repoFormUseCase;
private NodeMapFormUseCase _nodeMapFormUseCase;
private RunState _currentRunState;
#region FSM
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn);
GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Subscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
GameEntry.PlayerInventory?.OnInit();
_currentRunState = RunStateFactory.CreateFixedRun(
LevelThemeType.Plain,
GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null);
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
_nodeMapFormUseCase = new NodeMapFormUseCase();
_nodeMapFormUseCase.SetRunState(_currentRunState);
GameEntry.UIRouter.BindUIUseCase(UIFormType.NodeMapForm, _nodeMapFormUseCase);
OpenHubUI();
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
float realElapseSeconds)
{
base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds);
GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(NodeMapNodeEnterRequestedEventArgs.EventId, OnNodeMapNodeEnterRequested);
GameEntry.Event.Unsubscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn);
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.UIRouter.CloseUI(UIFormType.RepoForm);
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
_repoFormUseCase = null;
_nodeMapFormUseCase = null;
_currentRunState = null;
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs))
{
return;
}
GameEntry.UIRouter.CloseUI(UIFormType.NodeMapForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
private void OnNodeComplete(object sender, GameEventArgs e)
{
if (!(e is NodeCompleteEventArgs args))
{
return;
}
BackpackInventoryData snapshot = args.InventorySnapshotAfterNode;
if (snapshot == null && GameEntry.PlayerInventory != null)
{
snapshot = GameEntry.PlayerInventory.GetInventorySnapshot();
}
AdvanceRunState(true, snapshot);
OpenHubUI();
}
private void OnCombatFailureReturn(object sender, GameEventArgs e)
{
if (!(e is CombatFailureReturnEventArgs))
{
return;
}
BackpackInventoryData snapshot = GameEntry.PlayerInventory != null
? GameEntry.PlayerInventory.GetInventorySnapshot()
: null;
AdvanceRunState(false, snapshot);
OpenHubUI();
}
private void OnNodeMapNodeEnterRequested(object sender, GameEventArgs e)
{
if (!(sender is NodeMapForm) || !(e is NodeMapNodeEnterRequestedEventArgs args))
{
return;
}
RunNodeState currentNode = _currentRunState?.CurrentNode;
if (_currentRunState == null || !_currentRunState.CanEnterCurrentNode || currentNode == null)
{
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Current run has no enterable node.");
return;
}
if (args.SequenceIndex != currentNode.SequenceIndex || args.NodeType != currentNode.NodeType)
{
Log.Warning(
"ProcedureMain.OnNodeMapNodeEnterRequested() ignored. Requested={0}#{1}, CurrentNode={2}#{3}.",
args.NodeType,
args.SequenceIndex,
currentNode.NodeType,
currentNode.SequenceIndex);
return;
}
switch (currentNode.NodeType)
{
case RunNodeType.Combat:
case RunNodeType.BossCombat:
if (!HasAvailableParticipantTower())
{
Log.Warning("ProcedureMain blocked combat start. No participant tower is available.");
return;
}
GameEntry.CombatNode.StartCombat(currentNode.LinkedLevelId);
return;
case RunNodeType.Event:
GameEntry.EventNode.StartEvent();
return;
case RunNodeType.Shop:
GameEntry.ShopNode.StartShop();
return;
default:
Log.Warning("ProcedureMain.OnNodeMapNodeEnterRequested() encountered unsupported node type: {0}.", currentNode.NodeType);
return;
}
}
private void AdvanceRunState(bool succeeded, BackpackInventoryData snapshot)
{
if (_currentRunState == null)
{
return;
}
if (!RunStateAdvanceService.TryCompleteCurrentNode(_currentRunState, succeeded, snapshot))
{
return;
}
if (_currentRunState.IsCompleted)
{
Log.Info("ProcedureMain run completed. RunId={0}", _currentRunState.RunId);
return;
}
RunNodeState nextNode = _currentRunState.CurrentNode;
if (nextNode != null)
{
Log.Info(
"ProcedureMain advanced run. RunId={0}, NextNodeType={1}, SequenceIndex={2}, LevelId={3}.",
_currentRunState.RunId,
nextNode.NodeType,
nextNode.SequenceIndex,
nextNode.LinkedLevelId);
}
}
private static bool HasAvailableParticipantTower()
{
return GameEntry.PlayerInventory != null &&
GameEntry.PlayerInventory.GetParticipantTowerSnapshot().Count > 0;
}
private void OpenHubUI()
{
_nodeMapFormUseCase?.SetRunState(_currentRunState);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
GameEntry.UIRouter.OpenUI(UIFormType.NodeMapForm);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e6f092ddfab48b74d8e8cc338e4742e4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,7 +1,5 @@
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.UI;
using UnityGameFramework.Runtime;
@@ -14,7 +12,7 @@ namespace GeometryTD.Procedure
public bool GameStart { get; set; }
private RepoFormUseCase _repoFormUseCase;
private MenuFormUseCase _menuFormUseCase;
#region FSM
@@ -27,22 +25,11 @@ namespace GeometryTD.Procedure
{
base.OnEnter(procedureOwner);
GameEntry.Event.Subscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn);
GameEntry.Event.Subscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
GameEntry.Event.Subscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameStart = false;
GameEntry.EventNode.OnInit();
GameEntry.CombatNode.OnInit(LevelThemeType.Plain);
GameEntry.ShopNode.OnInit();
GameEntry.PlayerInventory?.OnInit();
_repoFormUseCase = new RepoFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.RepoForm, _repoFormUseCase);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
_menuFormUseCase = new MenuFormUseCase();
GameEntry.UIRouter.BindUIUseCase(UIFormType.MenuForm, _menuFormUseCase);
GameEntry.UIRouter.OpenUI(UIFormType.MenuForm);
}
protected override void OnUpdate(IFsm<IProcedureManager> procedureOwner, float elapseSeconds,
@@ -57,44 +44,16 @@ namespace GeometryTD.Procedure
return;
}
GameEntry.CombatNode.OnUpdate(elapseSeconds, realElapseSeconds);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.CombatNode.OnShutdown();
GameEntry.Event.Unsubscribe(CombatFailureReturnEventArgs.EventId, OnCombatFailureReturn);
GameEntry.Event.Unsubscribe(NodeEnterEventArgs.EventId, OnNodeEnter);
GameEntry.Event.Unsubscribe(NodeCompleteEventArgs.EventId, OnNodeComplete);
_repoFormUseCase = null;
GameEntry.UIRouter.CloseUI(UIFormType.MenuForm);
_menuFormUseCase = null;
base.OnLeave(procedureOwner, isShutdown);
}
#endregion
private void OnNodeEnter(object sender, GameEventArgs e)
{
if (!(e is NodeEnterEventArgs)) return;
GameEntry.UIRouter.CloseUI(UIFormType.TestMenuForm);
GameEntry.UIRouter.CloseUI(UIFormType.MainForm);
}
private void OnNodeComplete(object sender, GameEventArgs e)
{
if (!(e is NodeCompleteEventArgs)) return;
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
private void OnCombatFailureReturn(object sender, GameEventArgs e)
{
if (!(e is CombatFailureReturnEventArgs)) return;
GameEntry.UIRouter.OpenUI(UIFormType.TestMenuForm);
GameEntry.UIRouter.OpenUI(UIFormType.MainForm);
}
}
}
@@ -0,0 +1,16 @@
using GeometryTD.Procedure;
namespace GeometryTD.UI
{
public sealed class NodeItemContext : UIContext
{
public int NodeId;
public int SequenceIndex;
public RunNodeType NodeType;
public bool IsLocked;
public bool IsCurrent;
public bool IsCompleted;
public bool IsFailed;
public bool CanClick;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da34e789554bd744e913db9fa3615322
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,11 @@
namespace GeometryTD.UI
{
public sealed class NodeMapFormContext : UIContext
{
public string Title;
public string ProgressText;
public string CurrentNodeText;
public bool IsRunCompleted;
public NodeItemContext[] NodeItems;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bb4720747caac9641b0bf06abbdad70f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,162 @@
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GameFramework.Event;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public sealed class NodeMapFormController : UIFormControllerCommonBase<NodeMapFormContext, NodeMapForm>
{
private NodeMapFormUseCase _useCase;
protected override UIFormType UIFormTypeId => UIFormType.NodeMapForm;
protected override void RefreshUI(NodeMapForm form, NodeMapFormContext context)
{
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(NodeMapNodeClickEventArgs.EventId, OnNodeClicked);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(NodeMapNodeClickEventArgs.EventId, OnNodeClicked);
}
public override int? OpenUI(object userData = null)
{
if (userData is NodeMapFormContext context)
{
return OpenUIInternal(context);
}
if (userData is NodeMapFormRawData rawDataFromUserData)
{
return OpenUI(rawDataFromUserData);
}
if (userData != null)
{
Log.Warning("NodeMapFormController.OpenUI() userData type is invalid.");
return null;
}
if (_useCase == null)
{
Log.Error("NodeMapFormController.OpenUI() useCase is null.");
return null;
}
return OpenUI(_useCase.CreateInitialModel());
}
public int? OpenUI(NodeMapFormRawData rawData)
{
return OpenUIInternal(BuildContext(rawData));
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is NodeMapFormUseCase nodeMapFormUseCase))
{
Log.Error("NodeMapFormController.BindUseCase() useCase is invalid.");
return;
}
_useCase = nodeMapFormUseCase;
}
private void OnNodeClicked(object sender, GameEventArgs e)
{
if (!IsEventFromCurrentForm(sender) || !(e is NodeMapNodeClickEventArgs args) || _useCase == null)
{
return;
}
if (!_useCase.TryRequestEnterNode(args.SequenceIndex, out NodeMapNodeRawData selectedNode, out NodeMapFormRawData latestRawData))
{
if (latestRawData != null)
{
SetContext(BuildContext(latestRawData));
RefreshCurrentUI();
}
return;
}
if (latestRawData != null)
{
SetContext(BuildContext(latestRawData));
RefreshCurrentUI();
}
if (Form != null && selectedNode != null)
{
GameEntry.Event.Fire(Form, NodeMapNodeEnterRequestedEventArgs.Create(
selectedNode.RunId,
selectedNode.NodeId,
selectedNode.NodeType,
selectedNode.SequenceIndex));
}
}
private bool IsEventFromCurrentForm(object sender)
{
if (Form == null)
{
return false;
}
if (ReferenceEquals(sender, Form))
{
return true;
}
if (sender is Component component)
{
NodeMapForm ownerForm = component.GetComponentInParent<NodeMapForm>();
return ownerForm == Form;
}
return false;
}
private static NodeMapFormContext BuildContext(NodeMapFormRawData rawData)
{
NodeItemContext[] nodeItemContexts = System.Array.Empty<NodeItemContext>();
if (rawData?.Nodes != null && rawData.Nodes.Count > 0)
{
nodeItemContexts = new NodeItemContext[rawData.Nodes.Count];
for (int i = 0; i < rawData.Nodes.Count; i++)
{
NodeMapNodeRawData node = rawData.Nodes[i];
nodeItemContexts[i] = new NodeItemContext
{
NodeId = node?.NodeId ?? 0,
SequenceIndex = node?.SequenceIndex ?? i,
NodeType = node?.NodeType ?? RunNodeType.None,
IsLocked = node != null && node.Status == RunNodeStatus.Locked,
IsCurrent = node != null && node.IsCurrentNode,
IsCompleted = node != null && node.Status == RunNodeStatus.Completed,
IsFailed = node != null && node.Status == RunNodeStatus.Failed,
CanClick = node != null && node.CanEnter
};
}
}
return new NodeMapFormContext
{
Title = rawData?.Title ?? "节点地图",
ProgressText = rawData?.ProgressText ?? "0 / 0",
CurrentNodeText = rawData?.CurrentNodeText ?? "当前节点: 无",
IsRunCompleted = rawData != null && rawData.IsRunCompleted,
NodeItems = nodeItemContexts
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e24f8a016c2dd9b4b9e7aa34a24185a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System.Collections.Generic;
using GeometryTD.Procedure;
namespace GeometryTD.UI
{
public sealed class NodeMapFormRawData
{
public string RunId;
public string Title;
public string ProgressText;
public string CurrentNodeText;
public bool IsRunCompleted;
public List<NodeMapNodeRawData> Nodes;
}
public sealed class NodeMapNodeRawData
{
public string RunId;
public int NodeId;
public int SequenceIndex;
public RunNodeType NodeType;
public RunNodeStatus Status;
public bool IsCurrentNode;
public bool CanEnter;
public string Title;
public string StatusText;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 933dea9978c15484aab9df1bd8f32276
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,156 @@
using System.Collections.Generic;
using GeometryTD.Procedure;
namespace GeometryTD.UI
{
public sealed class NodeMapFormUseCase : IUIUseCase
{
private RunState _runState;
public void SetRunState(RunState runState)
{
_runState = runState;
}
public NodeMapFormRawData CreateInitialModel()
{
return BuildRawData(_runState);
}
public bool TryRequestEnterNode(int sequenceIndex, out NodeMapNodeRawData selectedNode, out NodeMapFormRawData latestRawData)
{
latestRawData = BuildRawData(_runState);
selectedNode = null;
if (_runState == null || _runState.IsCompleted)
{
return false;
}
RunNodeState currentNode = _runState.CurrentNode;
if (currentNode == null || currentNode.SequenceIndex != sequenceIndex || !_runState.CanEnterCurrentNode)
{
return false;
}
RunNodeState targetNode = _runState.GetNodeBySequenceIndex(sequenceIndex);
if (targetNode == null || targetNode.Status != RunNodeStatus.Available)
{
return false;
}
selectedNode = BuildNodeRawData(_runState.RunId, targetNode, currentNode.SequenceIndex);
return selectedNode != null && selectedNode.CanEnter;
}
private static NodeMapFormRawData BuildRawData(RunState runState)
{
if (runState == null)
{
return new NodeMapFormRawData
{
Title = "节点地图",
ProgressText = "0 / 0",
CurrentNodeText = "当前节点: 无",
IsRunCompleted = true,
Nodes = new List<NodeMapNodeRawData>()
};
}
List<NodeMapNodeRawData> nodes = new List<NodeMapNodeRawData>(runState.NodeCount);
int currentSequenceIndex = runState.CurrentNode != null ? runState.CurrentNode.SequenceIndex : -1;
foreach (RunNodeState node in runState.Nodes)
{
NodeMapNodeRawData nodeRawData = BuildNodeRawData(runState.RunId, node, currentSequenceIndex);
if (nodeRawData != null)
{
nodes.Add(nodeRawData);
}
}
string currentNodeText = runState.IsCompleted
? "当前节点: 已完成"
: $"当前节点: {BuildNodeTitle(runState.CurrentNode)}";
return new NodeMapFormRawData
{
RunId = runState.RunId,
Title = "节点地图",
ProgressText = $"{runState.CompletedNodeCount} / {runState.NodeCount}",
CurrentNodeText = currentNodeText,
IsRunCompleted = runState.IsCompleted,
Nodes = nodes
};
}
private static NodeMapNodeRawData BuildNodeRawData(string runId, RunNodeState node, int currentSequenceIndex)
{
if (node == null)
{
return null;
}
bool isCurrentNode = node.SequenceIndex == currentSequenceIndex;
bool canEnter = isCurrentNode && node.Status == RunNodeStatus.Available;
return new NodeMapNodeRawData
{
RunId = runId,
NodeId = node.NodeId,
SequenceIndex = node.SequenceIndex,
NodeType = node.NodeType,
Status = node.Status,
IsCurrentNode = isCurrentNode,
CanEnter = canEnter,
Title = BuildNodeTitle(node),
StatusText = BuildStatusText(node.Status, canEnter)
};
}
private static string BuildNodeTitle(RunNodeState node)
{
return node == null ? "未知节点" : BuildNodeTitle(node.NodeType);
}
private static string BuildNodeTitle(RunNodeType nodeType)
{
switch (nodeType)
{
case RunNodeType.Combat:
return "战斗";
case RunNodeType.Event:
return "事件";
case RunNodeType.Shop:
return "商店";
case RunNodeType.BossCombat:
return "Boss";
default:
return "未知节点";
}
}
private static string BuildStatusText(RunNodeStatus status, bool canEnter)
{
if (canEnter)
{
return "当前";
}
switch (status)
{
case RunNodeStatus.Locked:
return "未解锁";
case RunNodeStatus.Available:
return "可进入";
case RunNodeStatus.Completed:
return "已完成";
case RunNodeStatus.Failed:
return "失败";
case RunNodeStatus.Skipped:
return "跳过";
default:
return string.Empty;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e35c4c20ae9d48a4c8e9878e79b7d346
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,121 @@
using UnityEngine;
using UnityEngine.UI;
using GeometryTD.CustomEvent;
using GeometryTD.Procedure;
namespace GeometryTD.UI
{
public class NodeItem : MonoBehaviour
{
private const string CombatIconAssetName = "Combat";
private const string EventIconAssetName = "Event";
private const string ShopIconAssetName = "Shop";
private const string BossIconAssetName = "BossCombat";
[SerializeField] private Image _icon;
[SerializeField] private Image _bg;
[SerializeField] private CommonButton _button;
[SerializeField] private Color _lockedColor = Color.gray;
[SerializeField] private Color _activeColor = Color.cyan;
[SerializeField] private Color _passedColor = Color.green;
[SerializeField] private Color _failedColor = Color.red;
[SerializeField] private Color _defaultIconColor = Color.white;
[SerializeField] private Color _lockedIconColor = Color.gray;
private NodeItemContext _context;
private string _requestedIconAssetName;
public void RefreshUI(NodeItemContext context)
{
_context = context;
if (_button != null)
{
_button.Interactive = context != null && context.CanClick;
}
Color targetColor = _lockedColor;
if (context == null)
{
targetColor = _lockedColor;
}
else if (context.IsCompleted)
{
targetColor = _passedColor;
}
else if (context.IsFailed)
{
targetColor = _failedColor;
}
else if (context.IsCurrent)
{
targetColor = _activeColor;
}
if (_bg != null)
{
_bg.color = targetColor;
}
if (_icon != null)
{
RefreshIcon(context != null ? context.NodeType : RunNodeType.None);
_icon.color = context != null && !context.IsLocked ? _defaultIconColor : _lockedIconColor;
}
}
public void OnClick()
{
if (_context == null || !_context.CanClick)
{
return;
}
GameEntry.Event.Fire(this, NodeMapNodeClickEventArgs.Create(_context.SequenceIndex));
}
private void RefreshIcon(RunNodeType nodeType)
{
if (_icon == null)
{
return;
}
string assetName = ResolveIconAssetName(nodeType);
_requestedIconAssetName = assetName;
if (string.IsNullOrWhiteSpace(assetName) || GameEntry.SpriteCache == null)
{
_icon.sprite = null;
return;
}
GameEntry.SpriteCache.GetSprite(assetName, sprite =>
{
if (_icon == null || _requestedIconAssetName != assetName)
{
return;
}
_icon.sprite = sprite;
});
}
private static string ResolveIconAssetName(RunNodeType nodeType)
{
switch (nodeType)
{
case RunNodeType.Combat:
return CombatIconAssetName;
case RunNodeType.Event:
return EventIconAssetName;
case RunNodeType.Shop:
return ShopIconAssetName;
case RunNodeType.BossCombat:
return BossIconAssetName;
default:
return null;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d370c24f0bca1cd49b1163c3765e0f4c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using TMPro;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class NodeMapForm : UGuiForm
{
[SerializeField] private TMP_Text _titleText;
[SerializeField] private TMP_Text _progressText;
[SerializeField] private TMP_Text _currentNodeText;
[SerializeField] private NodeItem[] _nodeItems;
private NodeMapFormContext _context;
public void RefreshUI(NodeMapFormContext context)
{
_context = context;
if (_titleText != null)
{
_titleText.text = context?.Title ?? string.Empty;
}
if (_progressText != null)
{
_progressText.text = context?.ProgressText ?? string.Empty;
}
if (_currentNodeText != null)
{
_currentNodeText.text = context?.CurrentNodeText ?? string.Empty;
}
if (_nodeItems == null)
{
return;
}
for (int i = 0; i < _nodeItems.Length; i++)
{
NodeItemContext itemContext =
context?.NodeItems != null && i < context.NodeItems.Length ? context.NodeItems[i] : null;
_nodeItems[i]?.RefreshUI(itemContext);
}
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (userData is NodeMapFormContext context)
{
RefreshUI(context);
return;
}
Log.Warning("NodeMapForm requires NodeMapFormContext as userData.");
}
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
if (_titleText != null)
{
_titleText.text = string.Empty;
}
if (_progressText != null)
{
_progressText.text = string.Empty;
}
if (_currentNodeText != null)
{
_currentNodeText.text = string.Empty;
}
if (_nodeItems != null)
{
for (int i = 0; i < _nodeItems.Length; i++)
{
_nodeItems[i]?.RefreshUI(null);
}
}
base.OnClose(isShutdown, userData);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 16f09225744e9ed45ba3ed24c27f9602
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,13 @@
namespace GeometryTD.UI
{
public class MenuFormContext : UIContext
{
public string TitleText { get; set; }
public string StartButtonText { get; set; }
public string SettingsButtonText { get; set; }
public string ExitButtonText { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b608c268739c144990195981f1c4465
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,9 @@
using GeometryTD.Procedure;
namespace GeometryTD.UI
{
public class TestMenuFormContext : UIContext
{
public RunNodeType CurrentNodeType { get; set; }
}
}
@@ -0,0 +1,129 @@
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GeometryTD.Procedure;
using GameFramework.Event;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class MenuFormController : UIFormControllerCommonBase<MenuFormContext, MenuForm>
{
private MenuFormUseCase _useCase;
protected override UIFormType UIFormTypeId => UIFormType.MenuForm;
protected override void RefreshUI(MenuForm form, MenuFormContext context)
{
form.RefreshUI(context);
}
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(MenuStartRequestedEventArgs.EventId, OnStartRequested);
GameEntry.Event.Subscribe(MenuSettingsRequestedEventArgs.EventId, OnSettingsRequested);
GameEntry.Event.Subscribe(MenuExitRequestedEventArgs.EventId, OnExitRequested);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(MenuStartRequestedEventArgs.EventId, OnStartRequested);
GameEntry.Event.Unsubscribe(MenuSettingsRequestedEventArgs.EventId, OnSettingsRequested);
GameEntry.Event.Unsubscribe(MenuExitRequestedEventArgs.EventId, OnExitRequested);
}
public override int? OpenUI(object userData = null)
{
if (userData is MenuFormContext context)
{
return OpenUIInternal(context);
}
if (userData is MenuFormRawData rawDataFromUserData)
{
return OpenUI(rawDataFromUserData);
}
if (userData != null)
{
Log.Warning("MenuFormController.OpenUI() userData type is invalid.");
return null;
}
if (_useCase == null)
{
Log.Error("MenuFormController.OpenUI() useCase is null.");
return null;
}
return OpenUI(_useCase.CreateInitialModel());
}
public int? OpenUI(MenuFormRawData rawData)
{
return OpenUIInternal(BuildContext(rawData));
}
public override void BindUseCase(IUIUseCase useCase)
{
if (!(useCase is MenuFormUseCase menuFormUseCase))
{
Log.Error("MenuFormController.BindUseCase() useCase is invalid.");
return;
}
_useCase = menuFormUseCase;
}
private void OnStartRequested(object sender, GameEventArgs e)
{
if (!IsCurrentFormEvent(sender) || !(e is MenuStartRequestedEventArgs))
{
return;
}
if (GameEntry.Procedure.CurrentProcedure is ProcedureMenu procedureMenu)
{
procedureMenu.GameStart = true;
return;
}
Log.Warning("MenuFormController received start request outside ProcedureMenu.");
}
private void OnSettingsRequested(object sender, GameEventArgs e)
{
if (!IsCurrentFormEvent(sender) || !(e is MenuSettingsRequestedEventArgs))
{
return;
}
Log.Warning("Menu settings button click is TODO.");
}
private void OnExitRequested(object sender, GameEventArgs e)
{
if (!IsCurrentFormEvent(sender) || !(e is MenuExitRequestedEventArgs))
{
return;
}
UnityGameFramework.Runtime.GameEntry.Shutdown(ShutdownType.Quit);
}
private bool IsCurrentFormEvent(object sender)
{
return Form != null && ReferenceEquals(sender, Form);
}
private static MenuFormContext BuildContext(MenuFormRawData rawData)
{
return new MenuFormContext
{
TitleText = rawData?.TitleText ?? string.Empty,
StartButtonText = rawData?.StartButtonText ?? string.Empty,
SettingsButtonText = rawData?.SettingsButtonText ?? string.Empty,
ExitButtonText = rawData?.ExitButtonText ?? string.Empty
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3ec06b97e6bc9d743a6340377ab7cd8b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,6 +1,6 @@
using GeometryTD.CustomEvent;
using GeometryTD.Definition;
using GameFramework.Event;
using GeometryTD.Procedure;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
@@ -16,12 +16,10 @@ namespace GeometryTD.UI
protected override void SubscribeCustomEvents()
{
GameEntry.Event.Subscribe(TestMenuNodeClickEventArgs.EventId, OnNodeClick);
}
protected override void UnsubscribeCustomEvents()
{
GameEntry.Event.Unsubscribe(TestMenuNodeClickEventArgs.EventId, OnNodeClick);
}
public override int? OpenUI(object userData = null)
@@ -50,36 +48,10 @@ namespace GeometryTD.UI
private static TestMenuFormContext BuildDefaultContext()
{
return new TestMenuFormContext();
return new TestMenuFormContext
{
CurrentNodeType = RunNodeType.None
};
}
private void OnNodeClick(object sender, GameEventArgs e)
{
if (!(sender is TestMenuForm form))
{
return;
}
if (!(e is TestMenuNodeClickEventArgs args))
{
return;
}
switch (args.NodeType)
{
case TestMenuNodeType.Combat:
GameEntry.CombatNode.StartCombat();
return;
case TestMenuNodeType.Event:
GameEntry.EventNode.StartEvent();
return;
case TestMenuNodeType.Shop:
GameEntry.ShopNode.StartShop();
return;
default:
return;
}
}
}
}
}
@@ -0,0 +1,13 @@
namespace GeometryTD.UI
{
public class MenuFormRawData
{
public string TitleText { get; set; }
public string StartButtonText { get; set; }
public string SettingsButtonText { get; set; }
public string ExitButtonText { get; set; }
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5dc8f63e892ff6745aed855ae42cfd88
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,16 @@
namespace GeometryTD.UI
{
public class MenuFormUseCase : IUIUseCase
{
public MenuFormRawData CreateInitialModel()
{
return new MenuFormRawData
{
TitleText = "GeometryTD",
StartButtonText = "Start",
SettingsButtonText = "Settings",
ExitButtonText = "Exit"
};
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1fc5bfbd1fcf533448f2669b0eda64bf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using GeometryTD.CustomEvent;
using TMPro;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class MenuForm : UGuiForm
{
[SerializeField] private TMP_Text _titleText;
[SerializeField] private TMP_Text _startButtonText;
[SerializeField] private TMP_Text _settingsButtonText;
[SerializeField] private TMP_Text _exitButtonText;
private MenuFormContext _context;
public void RefreshUI(MenuFormContext context)
{
_context = context;
if (_context == null)
{
return;
}
SetText(_titleText, _context.TitleText);
SetText(_startButtonText, _context.StartButtonText);
SetText(_settingsButtonText, _context.SettingsButtonText);
SetText(_exitButtonText, _context.ExitButtonText);
}
public void OnStartButtonClick()
{
GameEntry.Event.Fire(this, MenuStartRequestedEventArgs.Create());
}
public void OnSettingsButtonClick()
{
GameEntry.Event.Fire(this, MenuSettingsRequestedEventArgs.Create());
}
public void OnExitButtonClick()
{
GameEntry.Event.Fire(this, MenuExitRequestedEventArgs.Create());
}
protected override void OnOpen(object userData)
{
base.OnOpen(userData);
if (userData is MenuFormContext context)
{
RefreshUI(context);
return;
}
Log.Warning("MenuForm requires MenuFormContext as userData.");
}
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
base.OnClose(isShutdown, userData);
}
private static void SetText(TMP_Text textComponent, string value)
{
if (textComponent == null)
{
return;
}
textComponent.text = value ?? string.Empty;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fdf317aa70fe94f44a332b6ee6ef5bbc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,10 +1,16 @@
using GeometryTD.CustomEvent;
using GeometryTD.Procedure;
using UnityEngine;
using UnityGameFramework.Runtime;
namespace GeometryTD.UI
{
public class TestMenuForm : UGuiForm
{
[SerializeField] private GameObject combatButtonObject;
[SerializeField] private GameObject eventButtonObject;
[SerializeField] private GameObject shopButtonObject;
private TestMenuFormContext _context;
public void RefreshUI(TestMenuFormContext context)
@@ -12,8 +18,17 @@ namespace GeometryTD.UI
_context = context;
if (_context == null)
{
SetButtonVisible(combatButtonObject, false);
SetButtonVisible(eventButtonObject, false);
SetButtonVisible(shopButtonObject, false);
return;
}
SetButtonVisible(
combatButtonObject,
_context.CurrentNodeType == RunNodeType.Combat || _context.CurrentNodeType == RunNodeType.BossCombat);
SetButtonVisible(eventButtonObject, _context.CurrentNodeType == RunNodeType.Event);
SetButtonVisible(shopButtonObject, _context.CurrentNodeType == RunNodeType.Shop);
}
public void OnCombatButtonClick()
@@ -47,8 +62,19 @@ namespace GeometryTD.UI
protected override void OnClose(bool isShutdown, object userData)
{
_context = null;
SetButtonVisible(combatButtonObject, false);
SetButtonVisible(eventButtonObject, false);
SetButtonVisible(shopButtonObject, false);
base.OnClose(isShutdown, userData);
}
private static void SetButtonVisible(GameObject buttonObject, bool visible)
{
if (buttonObject != null)
{
buttonObject.SetActive(visible);
}
}
}
}
}