状態に基づいてUnity3Dを再生するためのメニューを作成する

すべての人に良い一日を! 小さなゲームプロジェクトでゲームUIシステムを実装した方法についてお話ししたいと思います。 このアプローチは、必要なすべての面で最も最適で便利なように思えました。



システム全体は、非決定的有限状態マシンのかなり些細な表現です。

実装には、状態セット、状態表現のセット、 これらの状態を切り替える 状態 スイッチャーが必要です。



メニュー管理サービスの実装



先ほど言ったように、システム全体は3〜4クラスで説明できます。状態、状態の視覚的表現、およびこれらの状態を切り替えるオートマトンです。



状態インターフェイスについて説明します。



IState
public interface IState { void OnEnter(params object[] parameters); void OnEnter(); void OnExit(); }
      
      







OnEnterメソッドは、状態への遷移時に呼び出され、そのオーバーロードは、パラメーターセットを状態に渡すために作成されます。 したがって、状態の内部で使用されるオブジェクト(イベントの引数、または特定のイベントの状態から呼び出されるデリゲートなど)を渡すことができます。 順番に、状態を終了するとOnExitが呼び出されます。



ステータスビュー:



各状態にはビューが必要です。 プレゼンテーションの目的は、UI要素に情報を表示し、UIに関連するユーザーアクションの状態を通知することです(インターフェースの特定のページで提供されている場合)。



IUIShowableHidable
 public interface IUIShowableHidable { void ShowUI(); void HideUI(); }
      
      







ShowUIは、現在のメニューページに関連するUI要素を表示(アクティブ化)するメソッドをカプセル化するメソッドです。



HideUIは、たとえば別のページに移動する前に、すべての要素を非表示にできるメソッドです。



状態とその表現の実現:



IStateIUIShowableHidableは連動して動作することが理解されています-OnEnterの呼び出し時に、すでに存在するIUIShowableHidableは既にスタックにあります。 状態に遷移するとShowUIが呼び出され、終了するとHideUIが呼び出されます。 ほとんどの場合、これはまさに状態間の遷移が機能する方法です。 前のページのHideUIと新しいページのShowUIの間の遅延を必要とする長い遷移アニメーションなどの例外は、さまざまな方法で対処できます。



上記の事実を考えると、新しい状態を作成する便利さと速度のために、「ビュー」を持つフィールドを持つ抽象クラスを作成し、遷移メソッドでUIの表示と非表示をカプセル化します。



UIState
 public abstract class UIState : IState { protected abstract IUIShowableHidable ShowableHidable { get; set; } protected abstract void Enter(params object[] parameters); protected abstract void Enter(); protected abstract void Exit(); public virtual void OnEnter() { ShowableHidable.ShowUI(); Enter(); } public virtual void OnExit() { ShowableHidable.HideUI(); Exit(); } public virtual void OnEnter(params object[] parameters) { ShowableHidable.ShowUI(); Enter(parameters); } }
      
      







対応するIUIShowableHidableメソッドを呼び出した後に呼び出される抽象EnterおよびExitメソッドもあります。 OnEnterOnExitの単純なオーバーライドで取得できたため、実際の利点はありませんが、必要に応じて満たされる空のメソッドをストーリー内に保持しておくと便利だと思われました。



簡単にするために、 UIShowableHidableクラスが実装されました。これにより、 IUIShowableHidableが実装され、毎回ShowUIとHideUIを実装する必要がなくなります。 また、アウェイクでは、要素が非アクティブになります。これは、最初に、インスタンスを取得するためにすべてのUI要素が含まれるという理由で行われます。



UIShowableHidable
 public class UIShowableHidable : CachableMonoBehaviour, IUIShowableHidable { protected virtual void Awake() { gameObject.SetActive(false); } public virtual void ShowUI() { gameObject.SetActive(true); } public virtual void HideUI() { gameObject.SetActive(false); } protected bool TrySendAction(Action action) { if (action == null) return false; action(); return true; } }
      
      







ゲームメニューの「ハート」の設計を始めましょう。



次の3つの主な方法が必要です。





IMenuService
 public interface IMenuService { void GoToScreenOfType<T>() where T : UIState; void GoToScreenOfType<T>(params object[] parameters) where T : UIState; void GoToPreviousScreen(); void ClearUndoStack(); }
      
      







次に、状態を切り替えることができるメカニズムを実装する必要があります。



状態切り替え器
 public class StateSwitcher { private IState currentState; private readonly List<IState> registeredStates; private readonly Stack<StateSwitchCommand> switchingHistory; private StateSwitchCommand previousStateSwitchCommand; public StateSwitcher() { registeredStates = new List<IState>(); switchingHistory = new Stack<StateSwitchCommand>(); } public void ClearUndoStack() { switchingHistory.Clear(); } public void AddState(IState state) { if (registeredStates.Contains(state)) return; registeredStates.Add(state); } public void GoToState<T>() { GoToState(typeof(T)); } public void GoToState<T>(params object[] parameters) { GoToState(typeof(T), parameters); } public void GoToState(Type type) { Type targetType = type; if (currentState != null) if (currentState.GetType() == targetType) return; foreach (var item in registeredStates) { if (item.GetType() != targetType) continue; if (currentState != null) currentState.OnExit(); currentState = item; currentState.OnEnter(); RegStateSwitching(targetType, null); } } public void GoToState(Type type, params object[] parameters) { Type targetType = type; if (currentState != null) if (currentState.GetType() == targetType) return; foreach (var item in registeredStates) { if (item.GetType() != targetType) continue; if (currentState != null) currentState.OnExit(); currentState = item; currentState.OnEnter(parameters); RegStateSwitching(targetType, parameters); } } public void GoToPreviousState() { if (switchingHistory.Count < 1) return; StateSwitchCommand destination = switchingHistory.Pop(); previousStateSwitchCommand = null; if (destination.parameters == null) { GoToState(destination.stateType); } else { GoToState(destination.stateType, destination.parameters); } } private void RegStateSwitching(Type type, params object[] parameters) { if (previousStateSwitchCommand != null) switchingHistory.Push(previousStateSwitchCommand); previousStateSwitchCommand = new StateSwitchCommand(type, parameters); } private class StateSwitchCommand { public StateSwitchCommand(Type type, params object[] parameters) { stateType = type; this.parameters = parameters; } public readonly Type stateType; public readonly object[] parameters; } }
      
      







すべてが単純です: AddState状態のリストに状態を追加し、 GoToStateは必要な状態がリストにあるかどうかを確認し、見つかった場合は現在の状態を終了して必要な状態に入りStateSwitchCommandクラスで遷移を表す状態変更を登録し、スタックに追加しますこれにより、前の画面に戻ることができます。



IMenuServiceの実装を追加するために残ります



MenuManager
 public class MenuManager : IMenuService { private readonly StateSwitcher stateSwitcher; public MenuManager() { stateSwitcher = new StateSwitcher(); } public MenuManager(params UIState[] states) : this() { foreach (var item in states) { stateSwitcher.AddState(item); } } public void GoToScreenOfType<T>() where T : UIState { stateSwitcher.GoToState<T>(); } public void GoToScreenOfType(Type type) { stateSwitcher.GoToState(type); } public void GoToScreenOfType<T>(params object[] parameters) where T : UIState { stateSwitcher.GoToState<T>(parameters); } public void GoToScreenOfType(Type type, params object[] parameters) { stateSwitcher.GoToState(type, parameters); } public void GoToPreviousScreen() { stateSwitcher.GoToPreviousState(); } public void ClearUndoStack() { stateSwitcher.ClearUndoStack(); } }
      
      







コンストラクターは、ゲームで使用される一連のIStateを受け入れます。



使用する



簡単な使用例:



ステータスの例
 public sealed class GameEndState : UIState { protected override IUIShowableHidable ShowableHidable { get; set; } private readonly GameEndUI gameEndUI; private Action onRestartButtonClicked; private Action onMainMenuButtonClicked; public GameEndState(IUIShowableHidable uiShowableHidable, GameEndUI gameEndUI) { ShowableHidable = uiShowableHidable; this.gameEndUI = gameEndUI; } protected override void Enter(params object[] parameters) { onRestartButtonClicked = (Action) parameters[0]; onMainMenuButtonClicked = (Action)parameters[1]; gameEndUI.onRestartButtonClicked += onRestartButtonClicked; gameEndUI.onMainMenuButtonClicked += onMainMenuButtonClicked; gameEndUI.SetGameEndResult((string)parameters[2]); gameEndUI.SetTimeText((string)parameters[3]); gameEndUI.SetScoreText((string)parameters[4]); } protected override void Enter() { } protected override void Exit() { gameEndUI.onRestartButtonClicked -= onRestartButtonClicked; gameEndUI.onMainMenuButtonClicked -= onMainMenuButtonClicked; } }
      
      





コンストラクターには、入力IUIShowableHidableおよび実際にはGameEndUI自体、 つまり状態表現が必要です。



状態表示の例
 public class GameEndUI : UIShowableHidable { public static GameEndUI Instance { get; private set; } [SerializeField] private Text gameEndResultText; [SerializeField] private Text timeText; [SerializeField] private Text scoreText; [SerializeField] private Button restartButton; [SerializeField] private Button mainMenuButton; public Action onMainMenuButtonClicked; public Action onRestartButtonClicked; protected override void Awake() { base.Awake(); Instance = this; restartButton.onClick.AddListener(() => { if(onRestartButtonClicked != null) onRestartButtonClicked(); }); mainMenuButton.onClick.AddListener(() => { if (onMainMenuButtonClicked != null) onMainMenuButtonClicked(); }); } public void SetTimeText(string value) { timeText.text = value; } public void SetGameEndResult(string value) { gameEndResultText.text = value; } public void SetScoreText(string value) { scoreText.text = value; } }
      
      







初期化と遷移
  private IMenuService menuService; private void InitMenuService() { menuService = new MenuManager ( new MainMenuState(MainMenuUI.Instance, MainMenuUI.Instance, playmodeService, scoreSystem), new SettingsState(SettingsUI.Instance, SettingsUI.Instance, gamePrefabs), new AboutAuthorsState(AboutAuthorsUI.Instance, AboutAuthorsUI.Instance), new GameEndState(GameEndUI.Instance, GameEndUI.Instance), playmodeState ); } ... private void OnGameEnded(GameEndEventArgs gameEndEventArgs) { Timer.StopTimer(); scoreSystem.ReportScore(score); PauseGame(!IsGamePaused()); Master.GetMenuService().GoToScreenOfType<GameEndState>( new Action(() => { ReloadGame(); PauseGame(false); }), new Action(() => { UnloadGame(); PauseGame(false); }), gameEndEventArgs.gameEndStatus.ToString(), Timer.GetTimeFormatted(), score.ToString()); }
      
      







おわりに



私の意見では、結果はかなり実用的で、簡単に拡張可能なユーザーインターフェイス管理システムです。



ご清聴ありがとうございました。 この記事で説明されている方法を改善できるコメントや提案を喜んでいます。



All Articles