lundi 8 mai 2023

What is the best way to implement a return to the "previous" state in a state machine for the UI?

I use a state pattern for user input in the UI.

Let's say a user may have a modal dialog box, a chat window and a menu open. The dialog box can only be opened with an external trigger (network packet), chat can be opened with a key, the menu also opens with a key.

Chat can not be opened from the menu, the menu can not be opened if the chat is open (in reality there are more conditions, but it does not matter), etc.

I have a base class State, from which IdleState, DialogState, ChatState, MenuState are inherited. They override State* ProcessInput(Input& input) method which returns new state depending on input or nullptr if it remains the same.

The problem is that because the dialog is opened and closed by an external trigger, not by user input, the dialog must be opened as soon as the menu or chat is closed. But how does the state of the chat or menu know that they must switch to the DialogState instead of the IdleState?

At this point I added a flag to solve this problem. But I don't really like this solution because it is not universal. I mean the following:

bool DialogMustBeOpen = false;

State* ChatState::ProcessInput(Input& input)
{
    if (input.IsCloseButtonPressed())
    {
        if (DialogMustBeOpen)
            return DialogStateInstance;
            
        return IdleStateInstance;
    }

    return nullptr;
}

State* MenuState::ProcessInput(Input& input)
{
    if (input.IsCloseButtonPressed())
    {
        if (DialogMustBeOpen)
            return DialogStateInstance;
            
        return IdleStateInstance;
    }

    return nullptr;
}

void NetworkHandler::OnDialog(bool enable)
{
    if (!enable && CurrentState->GetType() == StateType::Dialog)
        SetState(IdleStateInstance);

    DialogMustBeOpen = enable;
}

I also had an idea to make an additional manager that would have a GetStateForClose method. Then it would look like this:

class UIManager
{
private:
    bool DialogMustBeOpen = false;

public:
    State* GetStateForClose() const
    {
        if (DialogMustBeOpen)
            return DialogStateInstance;

        return IdleStateInstance;
    }

    void ToggleDialog(const bool enable)
    {
        if (!enable && CurrentState->GetType() == StateType::Dialog)
            SetState(IdleStateInstance);
            
        DialogMustBeOpen = enable;
    }
} UIManagerInstance;

State* ChatState::ProcessInput(Input& input)
{
    if (input.IsCloseButtonPressed())
        return UIManagerInstance.GetStateForClose();

    return nullptr;
}

State* MenuState::ProcessInput(Input& input)
{
    if (input.IsCloseButtonPressed())
        return UIManagerInstance.GetStateForClose();

    return nullptr;
}

void NetworkHandler::OnDialog(bool enable)
{
    UIManagerInstance.ToggleDialog(enable);
}

Even though it seems like a good solution, I still feel like it could be done better, but I don't know how.

I also forgot to mention the fact that we can switch from the DialogState to the ChatState. In this case also when closing the chat, the DialogState must be returned, not IdleState.

So this flag is also important here. It looks like this:

class UIManager
{
private:
    bool DialogMustBeOpen = false;

    friend DialogState;

public:
    State* GetStateForClose() const
    {
        if (DialogMustBeOpen)
            return DialogStateInstance;

        return IdleStateInstance;
    }

    void ToggleDialog(const bool enable)
    {
        if (!enable && CurrentState->GetType() == StateType::Dialog)
            SetState(IdleStateInstance);
            
        DialogMustBeOpen = enable;
    }
} UIManagerInstance;

State* DialogState::ProcessInput(Input& input)
{
    if (input.IsChatButtonPressed())
    {
        UIManagerInstance.DialogMustBeOpen = true;

        return ChatStateInstance;
    }

    return nullptr;
}

I really don't like this strong dependency between states, but I repeat I don't know how it can be done differently. Thanks to all OOP experts who can help.

The processing of the "visual", for clarity, looks like this:

State* newState = CurrentState->ProcessInput(input);

if (newState)
{
    CurrentState->HideVisual();

    newState->ShowVisual();

    CurrentState = newState;
}

Aucun commentaire:

Enregistrer un commentaire