Undo and Redo in Unity

Redo and Undo action is everywhere, but how it’s done? In most cases, developers use the Command Design Pattern which we implemented in Unity last time. So how can we extend it with undo and redo action? Let’s find out! ?

Because we did a lot in the last post about the Command Design Pattern, we will use it as our base. If you didn’t see it yet, it’s a great time to do so! I can wait. ?

Ready? So let’s get into that!

There is a lot of places where undo and redo action can come handy. Maybe it’s not that popular in games, but almost every app has it!

Implementation

The first thing that we will do is to extend our Command class with the Undo method. For Redo we can use Execute method, it’s not a problem. ?

using UnityEngine;

/// <summary>
/// Abstract class for commands.
/// </summary>
public abstract class Command
{
    /// <summary>
    /// Method called to execute command.
    /// </summary>
    public abstract void Execute();

    /// <summary>
    /// Method called to undo command.
    /// </summary>
    public abstract void Undo();
}

Great! As we have that, now we can move to our CommandInvoker to implement Undo and Redo logic!

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Command invoker. Collects command into buffer to execute them at once.
/// </summary>
public class CommandInvoker : MonoBehaviour
{
    // Command history
    protected List<Command> commandHistory = new List<Command>();
    protected int executionIndex = 0;

    /// <summary>
    /// Method used to execute current command.
    /// </summary>
    /// <returns>The command index.</returns>
    public virtual int ExecuteCommand()
    {
        if (executionIndex < commandHistory.Count)
        {
            commandHistory[executionIndex].Execute();
            executionIndex++;
        }

        return executionIndex;
    }

    /// <summary>
    /// Method used to add and execute command.
    /// </summary>
    /// <returns>The command index.</returns>
    /// <param name="command">New command.</param>
    public virtual int ExecuteCommand(Command command)
    {
        for (int i = commandHistory.Count - 1; i >= executionIndex; i--)
        {
            commandHistory.RemoveAt(i);
        }

        commandHistory.Add(command);
        return ExecuteCommand();
    }

    /// <summary>
    /// Method used to undo command.
    /// </summary>
    /// <returns>The command index.</returns>
    public virtual int UndoCommand()
    {
        if (executionIndex > 0)
        {
            executionIndex--;
            commandHistory[executionIndex].Undo();
        }

        return executionIndex;
    }

    /// <summary>
    /// Method used to clear command buffer.
    /// </summary>
    public virtual void ClearCommandHistory()
    {
        commandHistory.Clear();
        executionIndex = 0;
    }

    /// <summary>
    /// Method used to get list of command names.
    /// </summary>
    /// <returns>The command names.</returns>
    public virtual string[] GetCommandNames()
    {
        var commandNames = new string[commandHistory.Count];
        for (int i = 0; i < commandHistory.Count; i++)
        {
            commandNames[i] = commandHistory[i].ToString();
        }

        return commandNames;
    }

}

Thanks to the nature of the Command Design Pattern, we can make a list of commands history on which we can easily navigate forward and backward. ?

Example

In the last post, we have built a simple game where we collect move commands to navigate the character to the destination. Now we will extend it with Undo and Redo actions.

What do we need to change? Actually not that much! Let’s start with our MoveCommand.

using UnityEngine;

/// <summary>
/// Command that moves player in provided direction.
/// </summary>
public class MoveCommand : Command
{
    // Reference to the player movement.
    public PlayerMovement player;

    // Direction value.
    public Vector2Int direction;

    public override void Execute()
    {
        player.MovePlayer(direction);
    }

    public override void Undo()
    {
        player.MovePlayer(direction * -1);
    }
}

Now we need to change our logic a little bit. We will execute MoveCommand immediately after clicking on the direction button, but we will add Undo and Redo buttons as well.

UI displays list of current commands.
New controls with Undo and Redo buttons.

With that, we need to update the UI class to show the command list with an indication at which command we are currently.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using TMPro;

/// <summary>
/// Collecting Command view with events for buttons and showing command list.
/// </summary>
public class UICommandsView : UIView
{
    // Reference to the label with command list.
    [SerializeField]
    private TextMeshProUGUI label;

    // List of command names.
    private List<string> commandList = new List<string>();

    public override void ShowView()
    {
        base.ShowView();

        ShowCommands(new string[0]);
    }

    // Adding new command to the command list label.
    public void ShowCommands(string[] commands)
    {
        commandList.Clear();
        commandList.Add("Command List:");
        foreach (var c in commands)
        {
            commandList.Add(c);
        }
        commandList.Add("End");

        UpdateCommandList(0);
    }

    /// <summary>
    /// Method updates position of the pointer at command list.
    /// </summary>
    /// <param name="commandPointer">Index of current command.</param>
    public void UpdateCommandList(int commandPointer)
    {
        commandPointer++;

        label.text = string.Empty;
        for (int i = 0; i < commandList.Count; i++)
        {
            label.text += string.Format("{0}\t{1}\n", i == commandPointer ? ">" : "", commandList[i]);
        }
    }

    // Event called when Undo Button is clicked.
    public UnityAction OnUndoClicked;

    /// <summary>
    /// Method called by Undo Button.
    /// </summary>
    public void UndoClick()
    {
        OnUndoClicked?.Invoke();
    }

    // Event called when Redo Button is clicked.
    public UnityAction OnRedoClicked;

    /// <summary>
    /// Method called by Redo Button.
    /// </summary>
    public void RedoClick()
    {
        OnRedoClicked?.Invoke();
    }

    // Event called when Up Button is clicked.
    public UnityAction OnUpClicked;

    /// <summary>
    /// Method called by Up Button.
    /// </summary>
    public void UpClick()
    {
        OnUpClicked?.Invoke();
    }

    // Event called when Down Button is clicked.
    public UnityAction OnDownClicked;

    /// <summary>
    /// Method called by Down Button.
    /// </summary>
    public void DownClick()
    {
        OnDownClicked?.Invoke();
    }

    // Event called when Left Button is clicked.
    public UnityAction OnLeftClicked;

    /// <summary>
    /// Method called by Left Button.
    /// </summary>
    public void LeftClick()
    {
        OnLeftClicked?.Invoke();
    }

    // Event called when Right Button is clicked.
    public UnityAction OnRightClicked;

    /// <summary>
    /// Method called by Right Button.
    /// </summary>
    public void RightClick()
    {
        OnRightClicked?.Invoke();
    }
}

The last thing we need to change is CollectCommandsState, which we will rename to CommandingState. It will implement new logic for executing commands with the option to undo or redo the action! ?

using UnityEngine;

/// <summary>
/// Game state in which we are executing / undoing commands.
/// </summary>
public class CommandingState : BaseState
{
    public override void PrepareState()
    {
        base.PrepareState();

        // Attaching events to the UI buttons.
        owner.UI.CommandsView.OnUndoClicked += OnUndoClicked; ;
        owner.UI.CommandsView.OnRedoClicked += OnRedoClicked;

        owner.UI.CommandsView.OnUpClicked += OnUpClicked;
        owner.UI.CommandsView.OnDownClicked += OnDownClicked;
        owner.UI.CommandsView.OnLeftClicked += OnLeftClicked;
        owner.UI.CommandsView.OnRightClicked += OnRightClicked;

        owner.UI.CommandsView.ShowView();
    }

    public override void DestroyState()
    {
        owner.UI.CommandsView.HideView();

        // Detaching events from the UI buttons.
        owner.UI.CommandsView.OnUndoClicked -= OnUndoClicked;
        owner.UI.CommandsView.OnRedoClicked -= OnRedoClicked;

        owner.UI.CommandsView.OnUpClicked -= OnUpClicked;
        owner.UI.CommandsView.OnDownClicked -= OnDownClicked;
        owner.UI.CommandsView.OnLeftClicked -= OnLeftClicked;
        owner.UI.CommandsView.OnRightClicked -= OnRightClicked;

        base.DestroyState();
    }

    /// <summary>
    /// Method attached to the Redo button.
    /// Executes/Redo current command.
    /// </summary>
    private void OnRedoClicked()
    {
        var idx = owner.CommandInvoker.ExecuteCommand();
        owner.UI.CommandsView.UpdateCommandList(idx);
    }

    /// <summary>
    /// Method attached to the Undo button.
    /// Reverts/Undo current command.
    /// </summary>
    private void OnUndoClicked()
    {
        var idx = owner.CommandInvoker.UndoCommand();
        owner.UI.CommandsView.UpdateCommandList(idx);
    }

    /// <summary>
    /// Method attached to the Up button.
    /// Adds move command to the buffer and updates command list in the UI.
    /// </summary>
    private void OnUpClicked()
    {
        var command = new MoveCommand() { direction = Vector2Int.up, player = owner.Player };
        AddCommand(command);
        CheckEndCondition();
    }

    /// <summary>
    /// Method attached to the Down button.
    /// Adds move command to the buffer and updates command list in the UI.
    /// </summary>
    private void OnDownClicked()
    {
        var command = new MoveCommand() { direction = Vector2Int.down, player = owner.Player };
        AddCommand(command);
        CheckEndCondition();
    }

    /// <summary>
    /// Method attached to the Left button.
    /// Adds move command to the buffer and updates command list in the UI.
    /// </summary>
    private void OnLeftClicked()
    {
        var command = new MoveCommand() { direction = Vector2Int.left, player = owner.Player };
        AddCommand(command);
        CheckEndCondition();
    }

    /// <summary>
    /// Method attached to the Right button.
    /// Adds move command to the buffer and updates command list in the UI.
    /// </summary>
    private void OnRightClicked()
    {
        var command = new MoveCommand() { direction = Vector2Int.right, player = owner.Player };
        AddCommand(command);
        CheckEndCondition();
    }

    /// <summary>
    /// Adds command to the invoker and UI.
    /// </summary>
    /// <param name="command">Command.</param>
    private void AddCommand(Command command)
    {
        var idx = owner.CommandInvoker.ExecuteCommand(command);

        owner.UI.CommandsView.ShowCommands(owner.CommandInvoker.GetCommandNames());
        owner.UI.CommandsView.UpdateCommandList(idx);
    }

    /// <summary>
    /// Checks the end condition.
    /// </summary>
    private void CheckEndCondition()
    {
        // check if player is at the same position as destination.
        if (owner.Player.CurrentPosition == owner.Destination.CurrentPosition)
        {
            var gameOver = new GameOverState();
            gameOver.won = true;
            owner.ChangeState(gameOver);
        }
    }
}

Awesome! I think we have all the things needed to play!

Working example of Undo and Redo commands.

Success! With new buttons and added logic, we can enjoy undo and redo mechanic in our Unity project!

As always, the whole project and code are available at my public repository. ?

Hope you liked it, and see you next time! ?

5 1 vote
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x