Skip to content

A small package providing tools for quickly implementing the command pattern in unity with support for commands that are optionally failable or undoable.

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
LICENSE.meta
Notifications You must be signed in to change notification settings

cmwedin/CommandPattern

Repository files navigation

Command Pattern

This package contains a collection of classes and interfaces to allow for quick implementation of the command pattern across projects with ground up support for commands that are optionally undoable, failable, composite, or asynchronous. An out-of-the-box implementation is provided through the SingletonCommandManager component, for more information see Simple Implementation.

Installation

Through GitHub (Recommended)

Stable Installation

To install this package in your Unity project, select the "window/Package Manager" entry in the Unity Inspector toolbar. Then, select the "+" icon in the upper left corner of the opened window, and select "Add package from git url." Paste the following:

https://github.com/cmwedin/CommandPattern.git

Once you see this package show up in the Package Manager window, it has been successfully installed.

You can automatically update the package by clicking the "update," button in the bottom right corner of the package manager window. This will retrieve any changes on main branch on this repository since installation or your last update.

Bleeding-Edge Installation

You can install the experimental version of this package instead by adding #Development-Branch to the end of the Github url above. This will make your installation use the most up to date version possible, even before changes are merged onto the main branch.

Through Itch.io

When installing this package through Itch.io you will need to update the package manually. There are two methods to download the package through Itch, you can either install it with the rest of your packages, leaving your asset folder less cluttered, but preventing modifications to the packages files; or you can install it as an asset, placing all of the scripts within your asset folder and allowing you to modify them. Modifications to this packages scripts are recommended only for user very confident in what they are doing.

Installing as a Package

Download this package's archive from its Itch.io page. Once downloaded, extract the "CommandPattern" folder contained within to your desired installation location. Note that deleting this folder will break your package installation, even after adding the package to a Unity project.

After you have downloaded and unzipped the package, open the Unity project you wish to add this package too. Open the package manager window, and select the "+" icon in the upper left corner. Then, select the "Add package from disk" option. Navigate to your installation location and select the "package.json" file. Once you see this package show up in the Package Manager window, it has been successfully installed.

Installing as an Asset

Download the .unitypackage file from this packages Itch.io page. Once downloaded, open the Unity project you wish to add this package to. Select "Assets" from the Unity Editor's toolbar, an from the "Import Package" menu select "Custom Package". In the window that pops up navigate to the .unitypackage file you downloaded and select it. The package will be added to your assets folder in the "/Packages/CommandPattern/" directory.

Updating the Package

If you installed the package through GitHub, you can automatically update it to its newest version by clicking the "update" button in the bottom right corner of its entry in the package manager window. If not, you will need to update it manually. In addition, there may be steps needed to migrate your code to the newer version of the package. If there are, these will be added to this section when the update has been released.

Manual Updating as a package

If you installed the package using the local package installation through the Unity package manager to update it you will need to download the archive for the updated version from the packages Itch.io page. Once you've done this, remove the old package in the package manager, delete the old installation and extract the inner folder from the new archive. If you plan to install the updated package with the same path, you may be able to skip removing the old package, but it is recommended you do so regardless. Once you have completed this, add the updated version of the package through the same installation progress you followed above.

Manual Updating as an Asset

If you installed the package as an asset using the .unitypackage file, simply download the updated .unitypackage file, delete you old installation in your assets folder, and follow the installation process above using the new .unitypackage file.

Installing the Package Demo

This package includes a demo showcasing how to use the command pattern along with many scripts you may find helpful to reference when implementing your own commands. If you installed the package as an asset the demo will be included by default within the "Demos/SimpleDemo" directory. Once the package is installed, this demo can easily be added by opening the "Samples" dropdown, and clicking the "import" button next to the "Simple Demo" entry. Once this demo has been imported, you can open it by navigating to the newly created "Samples/CommandPattern/[Current Version]/SimpleDemo" folder and opening the "SimpleDemo.Unity" scene. You may need to enter play-mode and resize your game window for the scene's ui elements to display properly.

Using this package

Full documentation of this package can be found here

The Basics

If you are already familiar with the command pattern or don't care about the abstract design concepts behind it skip ahead to the implementation sections.

In broad strokes the command pattern is a behavioral design pattern in which objects encapsulate the performance of an action. A command is in a sense a reified (or object/thing-ified if you aren't familiar with the term) function call, and only necessarily has a single method among its members, Command.Execute().

More technically we can describe four distinct objects associated with the pattern. The Command, the Receiver, the Invoker, and the Client. The Command is the fundamental unit of this pattern. A Command has a Receiver object, which during its Execute method it acts upon it in some way (such as invoking the Receiver's public method or modifying its fields). The Invoker knows how to execute command (but does not know the specific implementation of the command itself such as it's receiver) and can do bookkeeping such as tracking the history of executed commands. The client object knows about the Invoker, specific implementations of Commands, and the Receiver objects that those Commands could act on. It tells the invoker what Commands to executed and when.

For a more concrete example we can consider a toy model of object we will call tickers, these objects will be the receivers of our model and have an int whose value can be increased or decreased. Our command objects have a ticker to modify, and an amount to modify it by. The Invoker doesn't know or care what Tickers are or how these commands are changing them, it just has references to commands and knows that they have an Execute method it will invoke when told too. The client is less concrete and essentially the wider project, in which determine when to tell the invoker to execute commands and what those commands are, what receivers they act on and how.

Considering this toy model further we can see an extension of this pattern by observing that given a ticker-command object, we can create a new ticker-command object that acts on the same ticker with the opposite magnitude to precisely counter the changes of the first ticker-command. More broadly we can encapsulate any command which has a separate command that is its precise opposite in the IUndoable Interface. In our toy example it was possible to represent a ticker-command and its opposite through the same type, but this may not be true generally, as long as they are both adhere to the command abstraction (implementing an Execute() method).

Another extension implemented in this library can be arrived at by considering the fact that if the Invoker does not know the details about what any given command is doing, as far as it is concerned there is no difference between executing a single command and executing a group of commands in succession. This is an example of a broader design pattern called composition, and is reflected in the CompositeCommand class.

Simple Implementation

For the quickest Implementation of this pattern create an empty game object in your scene and attach the "SingletonCommandManager" component to it. You will be able to access this object from anywhere in your project by writing SingletonCommandManager.Instance. Then you need to define what you command objects are. To do this create a class inheriting from the Command class, implementing the abstract method.

public override void Execute() {...}

How exactly this class should work is entirely up to you based on the needs of your project. Once you have defined a commands, whenever you need to execute it you can pass an instance of the command object into the QueueCommand(Command command) method of your command manager instance. You can create a new instance of the command object each time you need one or use same object multiple times.

Once you have commands Queued in your command manager it will execute them every frame. This can be turned off or on by calling ToggleCommandExecution(), or ToggleCommandExecution(bool onoff) to set a specific value (true to start executing, false to stop). You can access the history of executed command by through the command manager instance's GetCommandHistory() method. The entries at the end of the list are the most recently executed commands.

This implementation is useful for introducing yourself to this pattern, but onces you understand it we strongly recommend moving on to the advanced implementation section and creating your own class wrapping the invoker object this package provides.

Advanced Implementation

For more complicated implementation rather than simply using the SingletonCommandManager you should create your own wrapper of the CommandStream class. This class can be interacted with in many of the same ways as the SingletonCommandManager instance can, which functions simply by exposing the pubic methods of its internal CommandStream object. You can create a CommandStream using the constructor new CommandStream([historyDepth = PositiveInfinity]) which has an optional argument to limit the depth to which the CommandStream stores its previously executed commands. The default value of this argument is Single.PositiveInfinity, indicating to the CommandStream to record all of its history no matter how many commands it executes. Note that setting historyDepths to values between Int.MaxValue and Single.PositiveInfinity may cause unexpected behavior. If you want to limit the history depth at all the limit should be below Int.MaxValue, otherwise leave the constructor empty. If you set HistoryDepth to zero the process of recording commands will be skipped entirely for some slight performance enhancements. Also worth noting is that if history is recorded at a limited depth, once that depth is reached access the history of the command stream becomes a linear time operation.

Commands queued into the CommandStream can be executed through the CommandStream's TryExecuteNext() method. There is also a version of this method that returns the nextCommand through an out parameter, TryExecuteNext(out Command topCommand), that is recommended for more advanced implementations (such as tracking the execution of IUndoable to maintain an Undo stack). There is a method TryPeekNext(out ICommand nextCommand) to examine the next command in the queue without executing it or removing it from the queue. TryExecuteNext returns an ExecuteCode enum value (the definition of which can be found at the top of the CommandStream.cs file) that indicates what happened when attempting to execute the next command. This enum can have the following values

  • ExecuteCode.Success indicating the top command in the queue was executed successfully
  • ExecuteCode.Failure indicating the top command was not executed because it was IFailable and WouldFail() returned true
  • ExecuteCode.QueueEmpty indicates no command was execute because there where none in the command queue
  • ExecuteCode.AwaitingCompletion indicates the top command was an AsyncCommand that is awaiting something to finish running - for more information see AsyncCommands
  • ExecuteCode.AlreadyRunning indicates the top command was an AsyncCommand who currently has a running CommandTask (an AsyncCommand cannot be executed again until the task from its pervious execution completes)
  • ExecuteCode.CompositeFailure If you see this something went wrong, It indicates the top command was a composite that started execution but one of its children failed; however, it was able to undo its executed children otherwise the CommandStream would throw an exception. This occurring means either the CompositeCommand that was executed needs to implement IFailable or alternatively its WouldFail() method contains an error. See this section for more info.

If you don't wish to use a queue, but rather want to tell the CommandStream to execute a Command and have it do so immediately you can use the TryExecuteImmediate(ICommand command) method.

Example Implementation

For examples of various implementations of this package please import the demo as explained in the installation section and reference its README file (found in this repository here). This file will go into detail about the various approaches you can take to using this package for different use cases.

Advanced Features

IUndoable Commands

Once you have these basics down you can create more advanced commands by implementing the IUndoable interface. This interface requires the

public Command GetUndoCommand()

method. Which should return a Command that if executed you negate the changes cause by the Command implementing this interface's Execute() method. While not required by the interface oftentimes you will find it useful to store a reference to the UndoCommand in a field of the IUndoable command. Within the GetUndoCommand method, you can create the appropriate command to undo the IUndoable if this field is null, and then return that field. This ensures that every time you get the undo command of an IUndoable you are working with the same object, which can be very useful to know (examples of why this is useful can be found in the demo).

You can queue an IUndoable command's undo into a CommandStream using the CommandStream.QueueUndoCommand(IUndoable commandToUndo) method. This method will not let you undo a command if it does not exist it the CommandStream's history. You can bypass this restriction by using the CommandStream.ForceUQueueUndoCommand(IUndoable commandToUndo) method, which would be equivalent to directly calling CommandStream.QueueCommand(commandToUndo.GetUndoCommand()). If you don't want to use a queue these method also have TryExecuteImmediate variants as well.

IFailable Commands

It may be the case that a Command you need for your project could potentially fail to execute properly. In this situation you should implement the IFailable Interface. This interface requires that your command implement the

public bool WouldFail()

method, which should determine if the command would be able to execute or not based on current state. When invoking TryExecuteNext() of a CommandStream, it will first check if the next command in it's queue is IFailable, and if so if it would fail if attempted to be executed. If it is IFailable and WouldFail() returns true, the TryExecuteNext() will return ExecuteCode.Failure and the command will be removed from the queue without being executed or recorded.

CompositeCommands & SimpleComposites

Another more advanced type of command you can use is CompositeCommands. This abstract subclass of Command encapsulates multiple commands into a single object and can be thought of as an Invoker that is also a Command. When its Execute() method is invoked it queues all of the commands in its subCommands list into an internal CommandStream and executes commands from that CommandStream until it is empty. If the internal CommandStream ever fails to execute its next command before it is empty the CompositeCommand will throw a exception indicating that it couldn't execute all of its children (In some case's, if all of the executed children are undoable, the package will be able to handle this situation for you, if not it will be thrown for your CommandStream wrapper to handle).

A note on overriding a CompositeCommand's execute method: this can be useful to do when implementing more complicated Composite's in which a step by step approach is needed, but it can also be dangerous. While it is possible to bypass the internal CommandStream and invoke each child's Execute method directly, this is not recommended as using the CommandStream gives you more control over how the subCommands are executed. As such when overriding this method the first thing it is recommended that you do is invoke internalStream.QueueCommands(subCommands). After this you can execute the queue however is needed for your purpose. Keep in mind, overriding a CompositeCommand's execute method will bypass its exception handling if it is possible for one of the children to fail you will need to handle this situation yourself (and as well if it is you should be implementing IFailable to prevent the composite from being executed in the first place).

A SimpleComposite is a concrete type of CompositeCommand's that can be created from any collection of Commands which does not contain a Command that is IFailable. If the reasoning for this restriction is not obvious an explanation can be insightful into the concepts behind the command pattern.


Sidebar: Why cant SimpleComposite's contain IFailable children? A command upon execution changes some aspect of the state of its receiver object, and by extension the state of the client containing those objects. A SimpleComposite, does not know the implementation details of its child objects. This means that if one of its command might fail, it can only determine if that Command would fail (as the IFailable interface exposes this information to it) based on the state if its receiver object before any of its children have been executed. It is possible that one of its children might modify the state of the receiver in such a way the failable command is no longer able to succeed. The SimpleComposite would have no way to determine if this would be the case. It could check if the IFailable child would fail after executing each of its other children; however, these Commands may not be IUndoable. This would be incompatible with a Composite executing either all of its children or none of them, as once it executes the command and realizes its IFailable child can no longer be executed, it has no way to revert the changes of the executed command. The simplest solution to this is to require that the children of a SimpleComposite never be IFailable in the first place.

When inheriting from CompositeCommand you can also inherit from IUndoable as well. When doing so it is best practice to ensure all of the child commands also implement IUndoable. Adhering to this practice allows the undo command of a composite command to be simply constructed with the following linq query

var undoComposite = new SimpleComposite(
    from com in subCommands.Cast<IUndoable>()
    select com.GetUndoCommand()
)

Although this assumes that none of the undo commands are IFailable. If this is not the case a more nuanced approach to constructing the Undo command will be needed.

Best Practice in IFailable CompositeCommands

While a SimpleComposite cannot contain any IFailable children, this is not necessarily true of all CompositeCommands. While a CompositeCommand should be IUndoable only if all of its children are IUndoable, the opposite is true of IFailable (that is, a CompositeCommand should be IFailable if any of its children are IFailable). However, IFailable CompositeCommands require special care to avoid introducing errors where the CompositeCommand becomes failable partway through execution (as discussed above this will cause an exception to occur). In short, if a CompositeCommand is IFailable, any IFailable child it contains must be independent of all other children. By Independent here we mean that the execution of one command does not interfere in any way with the execution of another. They act on different receiver objects, or they modify different aspect of the same receiver. Following this practice means that the composite can check if its IFailable children would fail and be certain that executing any one of its children will not cause that to change, therefore it will be able to execute all of them. While it may be possible to create CompositeCommands that breaks this rule without introducing errors through clever use of IUndoable commands, this is not recommended unless you are very confident in what you are doing.

AsyncCommands

The final advanced topic we will cover in this readme is asynchronous commands. While implementing a command you might find that you need to wait for some over piece of code to complete before you can finish executing the command, but don't want to hang your program while you do so. In this situation it is best to use the AsyncCommand abstract class over the base Command abstract class. When you inherit from this class you will no longer be able to override Execute(), and instead will need to implement your command's logic in an override of the public abstract Task ExecuteAsync() method (remember to mark your override as async, that isn't part of a methods signature and can't be included in and abstract method definition). The reason for this is that Execute() return's void, and in general it is a bad idea to make a method that returns void async (if you are unclear as to why this is bad consider reviewing best practices in asynchronous programing before implementing this type of command). Instead, when a CommandStream executes and AsyncCommand it's Execute method will invoke your override for ExecuteAsync, this will run up to your first await in the method, at which point it will return a Task, representing the execution of the reminder of method after it is ready to continue. At this point, control has returned back to the Execute method even though the ExecuteAsync method has yet to fully complete, this is the point of asynchronous code. The execute method will store the Task returned by ExecuteAsync in the backing field of a publicly getable property. Then, it will continue by invoking a private method that will await the completion of said Task and invoke an event depending on how the task finished (the ways an AsyncCommand's task can finished are discussed below), each possible event will also invoke the OnAnyTaskEnd event to avoid the need to subscribe the same delegate to multiple events. This method will again, upon being told to await the completion of the ExecuteAsync's return task, give control back to the Execute method.

At this point, the Execute method will complete, and control will be returned to CommandStream.TryExecuteNext(); this is why its important not to make methods that return void async, because there is important information we need in the Task returned by ExecuteAsync that if we returned void we would be discarding. When TryExecuteNext regains control, it will move on to its bookkeeping phase, where it will see that the Command it just executed is IAsyncCommand and (assuming the Task hasn't already completed) add the CommandTask to its list of currently running tasks. It will then subscribe a cleanup delegates to the AsyncCommands completion events (we will discuss these in more detail shortly). The wrapper of the CommandStream, or any other object with a reference to it, can get a read only copy of its list of currently running tasks and examine their status (note that until the await statement is finished the status of these tasks will be awaiting activation, they represent the remainder of ExecuteAsync after the await, not the method in its entirety) by invoking CommandStream.GetRunningCommandTasks().

Sidebar: Isn't it bad to invoke asynchronous methods from synchronous ones? If you read the article on best practices in asynchronous programing linked above, or are already familiar with asynchronous programing, you may be concerned that this implementation doesn't conform to the practice listed in the article to "async all the way." It is true that when possible it is best to let an asynchronous method "grow" through the codebase; however, allow me to present a couple of arguments here explaining the reasoning behind why we do not do so in this package.
  • Compatibility with synchronous commands

    We want a CommandStream to be able to execute Commands that are both synchronous and asynchronous. Because of this, the CommandStream requires objects it is given as Commands to conform to the ICommand interface, meaning they have a method with the signature public void Execute(), which it will invoke when it is time to execute that object. To make ExecuteAsync() method "async all the way," we would need to it invoking method, Execute() async as well. The only way to do this while also conforming to the ICommand interface you be to make the Execute() method async void, which is arguably worse practice. Alternatively, we would have to implement separate overloads for queueing AsyncCommands as well as executing them separately from synchronous commands as to truly be "async all the way" public bool TryExecuteNext() would need to become public async Task<bool> TryExecuteNextAsync()..

  • We aren't blocking synchronous methods based on asynchronous tasks

    one of the primary reasons to allow async methods to grow throughout the codebase is not doing so can cause deadlocks to occur where the synchronous code cannot continue until the asynchronous code has run, such as waiting for its result, and the asynchronous code cannot resume while the synchronous code is blocked (this is due to more complicated aspects of how async methods resume their execution in certain environments beyond the scope of this readme but explained in the article linked above). However in our case, we are not blocking any of the synchronous methods that eventually invoke ExecuteAsync. When we reach the point in our asynchronous code where we need to await some condition TryExecuteNext will simply do some bookkeeping and return ExecuteCode.AwaitingCompletion, allowing the CommandStream object to continue normally. Furthermore, because the Execute method of a command represents an action with no return value, we wont encounter situations where we need to wait on a task to give us its result in synchronous code. If a situation arises where we need to prevent commands from being executed while async commands are running we can add a conditional in our CommandStream wrapper to not invoke TryExecuteNext if the runningCommandTask count isn't zero without introducing any blocking (simply by returning early or skipping the code that would invoke it.)


Types of task completion & task exceptions

There are 3 different completion events that an AsyncCommand can invoke once it is done running its task, corresponding to the three states of the Task for which Task.IsCompleted can return true. Either the task fully ran (i.e. it was successfully executed), in which case the OnTaskCompleted event will be raised, the task was cancelled, In which case the OnTaskCanceled event is raised, or the task faulted (i.e. an exception was thrown), in which case the OnTaskFaulted event will be raised. All of these events will also cause a separate event, OnAnyTaskEnd as discussed above.

It is worth noting here a distinction here in how async methods handle exceptions versus how synchronous methods handle them. When an async method throws an exception, rather than the typical behaviours where the exception is thrown to the invoking method for it to catch, instead the exception is captured within the exception property of the Task return value of the async method. This is one of the reasons it is dangerous to make an async void method, as you have no way of knowing if the async method threw an exception. In the event that an exception occurs before reaching the first await of execute async this package will replicate the typical behaviours expected of exceptions (throwing it upward for the CommandStream's wrapper to handle, although note that the type of this will be an AggregateException containing the exception that was originally thrown). This is because at this point control has yet to be returned back to the CommandStream to set up its cleanup delegates; however the task has already faulted and thus OnTaskFaulted has already been raised. Otherwise, if the task faults after its first await (when the CommandStream has had the opportunity to set up its delegates) we will continue in the spirit of the typical async behavior of tasks. That is to say, the exception will not be thrown, but instead the user will be warned (note that this does not happen by default when an async task faults) and the information regarding the fault (such as what exception was thrown) will be stored in the faultedCommandTasks field of the executing CommandStream. If this behavior is undesirable, we provide the tools to throw the exception instead. Whenever a task's OnTaskFaulted event is raised, the CommandStream that executed its AsyncCommand will raise its own OnTaskFaulted event. You can subscribe to this event in your CommandStream wrapper to throw the exception the task caused rather than let the CommandStream store it in the faultedCommand Tasks field.

Canceling AsyncCommands tasks

It may become necessary to cancel a task before it has finished. While we provide some tools to do that in this package there are also steps you must take to enable it yourself in your implementation of ExecuteAsync. There are two important objects to understand when it comes to cancelling tasks, the CancellationTokenSource and the CancellationToken itself (which the CancellationTokenSource has an instance of). The CancellationTokenSource is used to indicate that the task should be cancelled from outside the task, whereas the CancellationToken is used inside the task to determine if it has been cancelled. An object inheriting AsyncCommand contains a reference to a CancellationToken. As CancellationToken is a struct, prior to being assigned the reference will have the default value of CancellationToken.None. Upon being executing an IAsyncCommand as part of its book-keeping the CommandStream will create a new CancellationTokenSource and set the IAsyncCommand's CancellationToken field to equal this CancellationTokenSource's CancellationToken. The CancellationTokenSource can be retrieved from the CommandStream using the GetRunningTaskCTS method. You can indicate that an AsyncCommand's task should be cancelled by either directly invoking the Cancel method of its CancellationTokenSource, or using the CancelRunningCommandTask method of CommandStream (this is the recommended method as it will verify the task is still running). However there is an important caveat here, this will only tell the CancellationToken that the task should be cancelled. You will still need to respond to this within the ExecuteAsync() method. You will ultimately do this by invoking the ThrowIfCancellationRequested method of the CancellationToken, which will cause both the task to be cancelled and the package to raise the AsyncCommands OnTaskCancelled event. If there is anything you need to do before cancellation you can enclose this within a conditional block using the CancellationToken's IsCancellationRequested boolean property. CancellationToken also have un-managed resources that need to be disposed of or else risk causing a memory leak. The CommandStream will take care of this for you. As part of its book-keeping is to subscribe a delegate to the OnAnyTaskEnd event which disposes of the CancellationTokenSource, removes the task from its running tasks collection, and sets the AsyncCommands CancellationToken back to CancellationToken.None.

About

A small package providing tools for quickly implementing the command pattern in unity with support for commands that are optionally failable or undoable.

Topics

Resources

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
LICENSE.meta

Stars

Watchers

Forks

Packages

No packages published

Languages