I'm designing a menu system in Unity and need help validating my approach to ensure it follows the SOLID principles, particularly the Liskov Substitution Principle.
The system consists of:
- A
BaseMenuabstract class for shared functionality. - Two specific types of menus:
StaticMenu: Predefined buttons assigned in the Unity Editor.DynamicMenu: Buttons created dynamically at runtime based on game context (e.g., dialogue choices).
My Questions:
- Does my
BaseMenudesign properly support the needs of bothStaticMenuandDynamicMenu? - Are there any potential violations of SOLID principles in this setup?
- Is this the correct approach, or should I consider using an interface or another pattern?
BaseMenu as abstract class
public abstract class BaseMenu
{
public bool IsActive { get; set; }
public RectTransform Panel { get; protected set; }
public List<Button> Buttons { get; protected set; }
public int CurrentIndex { get; protected set; }
public virtual void Initialize()
{
CurrentIndex = 0;
Buttons = new List<Button>(); // Ensures Buttons is always initialized
SetupButtons(); // Abstract method to allow flexibility
}
public virtual void Show()
{
Panel.gameObject.SetActive(true);
IsActive = true;
RegisterWithMenuManager();
}
public virtual void Hide()
{
Panel.gameObject.SetActive(false);
IsActive = false;
DeregisterWithMenuManager();
}
public virtual void SelectNext()
{
CurrentIndex = (CurrentIndex + 1) % Buttons.Count;
// Add visual selection logic here
}
public virtual void SelectPrevious()
{
CurrentIndex = (CurrentIndex - 1 + Buttons.Count) % Buttons.Count;
// Add visual selection logic here
}
public abstract void HandleInput(); // Subclass defines input logic (e.g., vertical vs horizontal)
protected abstract void SetupButtons(); // Subclass defines how buttons are set up
public void RegisterWithMenuManager()
{
MenuManager.Instance.Register(this); // Add this menu to a stack of active menus
}
public void DeregisterWithMenuManager()
{
MenuManager.Instance.Deregister(this); // Remove this menu from the stack
}
}
StaticMenu Implementation
Uses predefined buttons dragged into a SerializedField in the Unity Editor.
public class StaticMenu : BaseMenu
{
[SerializeField] private List<Button> serializedButtons;
protected override void SetupButtons()
{
Buttons = serializedButtons; // Use predefined buttons
}
public override void HandleInput()
{
if (Input.GetKeyDown(KeyCode.Up))
{
SelectPrevious();
}
else if (Input.GetKeyDown(KeyCode.Down))
{
SelectNext();
}
}
}
DynamicMenu Implementation
Dynamically populates buttons based on a list of options at runtime.
public class DynamicMenu : BaseMenu
{
private List<string> options;
public DynamicMenu(List<string> options)
{
this.options = options;
}
protected override void SetupButtons()
{
foreach (var option in options)
{
Button newButton = InstantiateButton(option);
Buttons.Add(newButton);
}
}
private Button InstantiateButton(string text)
{
GameObject buttonPrefab = Resources.Load<GameObject>("ButtonPrefab");
Button button = Instantiate(buttonPrefab, Panel).GetComponent<Button>();
button.GetComponentInChildren<Text>().text = text;
return button;
}
public override void HandleInput()
{
if (Input.GetKeyDown(KeyCode.Up))
{
SelectPrevious();
}
else if (Input.GetKeyDown(KeyCode.Down))
{
SelectNext();
}
}
}
I'm new to Unity and SOLID, so feedback is welcome. Thank you!
virtualany child class could just not call the base implementation and therefore miss thingspublic List<Button> Buttons { get; protected set; } = new();and all there is left is toSetupButtons()which you can call in the constructorMenuManagercan break the I, if its interface exposes more methods than justRegister/Deregister. It also breaks the D: in general, you can't achieve the D principle with singletons. BtwInput,Button,Resourcesetc also do break the I & the D principles, but it is not practical to abstract Unity APIs.