📜 ⬆️ ⬇️

Creating a menu for playing on Unity3D based on states

Good day to all! I would like to talk about how I implemented the game UI system in a small game project. This approach seemed to me the most optimal and convenient in all required aspects.

The whole system is a rather trivial representation of a non-deterministic finite automaton.
To implement, we need: a set of states , a set of state representations, a state switch that switches these states .

Implementing a menu management service


As I have already said, the whole system can be described by 3-4 classes: a state, a visual representation of a state, and an automaton that switches between these states.

We describe the state interface:
')
Istate
public interface IState { void OnEnter(params object[] parameters); void OnEnter(); void OnExit(); } 


The OnEnter method will be called at the moment of transition to the state, and its overload is created in order to transfer to the state a set of parameters. Thus, it is possible to pass objects that will be used inside the state — event arguments or, for example, delegates that will be called from the state in a particular event. In turn, OnExit will be called upon exiting the state.

State view:

Each state must have a presentation. The task of the presentation is to display information in the UI elements and notify the state of user actions related to the UI (if such are provided for a specific interface page).

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


ShowUI is a method that encapsulates the methods that implement the display (activation) of UI elements related to the current menu page.

HideUI is a method that allows you to hide all the elements, for example, before moving to another page.

The implementation of states and their representations:

It is understood that IState and IUIShowableHidable work in conjunction - at the time of the OnEnter call in the stack, IUIShowableHidable is already there. When switching to the state, ShowUI is called, on exit - HideUI. In most cases, this is how the transition between states will work. Exceptions, such as lengthy transition animations that require a delay between the previous page's HideUI and the new page's ShowUI, can be solved in various ways.

Taking into account the fact described above, it was decided by me for the convenience and speed of creating new states to make an abstract class that would have a field with a view and encapsulate the display and concealment of the UI in the transition methods.

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); } } 


There are also abstract methods Enter and Exit , which will be called after calling the appropriate IUIShowableHidable methods. There is no real benefit from them, since it was possible to do with the simple override OnEnter and OnExit , if necessary , but it seemed to me convenient to keep empty methods that would be filled in if necessary.

For greater simplicity, the UIShowableHidable class has been implemented, which implements IUIShowableHidable and saves us from the need to implement ShowUI and HideUI every time. Also, in Awake, the element will be deactivated, it is made from considerations that initially, all UI elements are included, in order to get their instances.

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; } } 


Let's start designing the “heart” of the game menu:

We need three basic methods:


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


Next, you need to implement a mechanism to switch between states.

StateSwitcher
 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; } } 


Everything is simple: AddState adds a state to the list of steates, GoToState checks for the required state in the list, if it finds it, it exits the current state and enters the required state, and also registers the state change, representing the transition by the StateSwitchCommand class, adding it to the stack transitions, which will allow us to return to the previous screen.

It remains to add the implementation 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(); } } 


The constructor accepts a set of IState's that will be used in your game.

Using


A simple example of use:

State example
 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; } } 

The constructor requires input IUIShowableHidable and, in fact, GameEndUI itself - a state view.

State view example
 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; } } 


Initialization and Transitions
  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()); } 


Conclusion


The result is a fairly practical, in my opinion, and easily expandable user interface management system.

Thanks for attention. I would be happy for comments and suggestions that can improve the method described in the article.

Source: https://habr.com/ru/post/310236/


All Articles