Skip to content

Latest commit

 

History

History
132 lines (92 loc) · 41.6 KB

README.md

File metadata and controls

132 lines (92 loc) · 41.6 KB

This demo provides examples of how the command pattern works and how you might use this package. It is made up of various UI panels each of which demonstrate different use cases for this architecture. While many of the examples in this scene could be implemented without the command pattern, it is intended to so the versatility of this pattern and its ability to separate the encapsulate the logic of a method into an object. We will explain the demos here in order from least to most complicated. Note that the demo scene is intended to be viewed in the editor's game window in the free aspect display, you may need to adjust the window slightly if panels are cut off. Using a large/maximized game window may leave empty space on the sides.

Logger Panel

The panel in the top left is the simplest example included in this demo intended to showcase how this package and the command pattern works without any additional overhead. In the inspector for the parent object of the panel you will find two components, a LoggerCommandStream, and a LoggerCommandFactory. The LoggerCommandStream is the wrapper object for the invoker of the command pattern, the CommandStream object, discussed in the package README. It can act as a layer where you include information specific to your implementation of the command pattern that the inner invoker isn't aware of. In this example the method that exposes the "QueueCommand" method of the inner invoker only excepts LoggerCommand arguments, whereas the inner invoker will except any ICommand object. This is also where we determine when to execute the queued commands. We could expose the TryExecuteNext method to the rest of the client to have more control of when exactly this happens; however, for simplicity in this demo (as well as the rest of them) we just execute commands in the wrapper components update method. We also make this object a singleton. This is a very useful pattern to use for this object as it allows you to queue commands into the CommandStream without needing a reference to he CommandStream's wrapper. You will find all of the wrappers in this demo use this pattern.

The LoggerCommandFactory simulates the client sending commands to the invoker through its wrapper. It will store your text entry and construct a logger command from it when one of the buttons is selected, which it will then send to the invoker. A LoggerCommand object is constructed from a string to log and an enum indicating wether it should be logged as a message, warning, or error. Once the command is executed, it prints the message in the desired manner. This demo is certainly overkill for a simple logger; however it hopefully clarifies what the command pattern is doing if it isn't already obvious.

Health Panel

This panel shows how you could use commands to modify some parameter of an entity in your game through an interface abstraction. It also demonstrates some of the ways you can extend CommandStream by reacting to command failures in its wrapper.

IHealth

This Interface acts as an abstraction for any object that would implement health. It has two properties it requires of its implementer, a get only property for Maximum health, as well as a property supporting both get and set for Health. It also has an event OnHealthChanged that should be invoked in the set of Health. We simulate an monobehaviour implementing this Interface through the HealthBar component. This component responds to the OnHealthChanged event by updating a UI element in the demo scene to show the new health value.

ModifyHealthCommand

This is the command we use to modify the Health parameter. The receiver of this command is an object implementing IHealth, which is provided in its constructor along with an amount to modify it by. It implements IUndoable, which would return a command acts on the same receiver object with the opposite magnitude. If you look at the code for this Command you will notice we cache the undo command in a field of the original command object upon the first invocation of GetUndoCommand. While this is not explicitly required by the IUndoable interface it is frequently a good idea to do so for a few reasons. Most broadly, this is good practice as it is more space efficient since we are not creating a new command every time we get the undo command. It also allows us to compare other commands to see if they are equal to the IUndoable's undo command. It is also IFailable and returns true for WouldFail if it would modify Health below zero or above MaximumHealth. While our implementation of IHealth clamps changes to the health property anyway, this is still necessary as if we try to undo a ModifyHealthCommand that would reduce the health property below zero, it would think it had modified the health property by its full magnitude even though the actual change was clamped to zero. This would cause executing its undo command to increase the health to greater than it was when the original command was executed.

CommandStream Wrapper (HealthCommandStream)

This class primarily just wraps a CommandStream in a Monobehaviour and exposes its QueueCommand method while executing commands in the update method. The demo simulates the client sending this wrapper commands to execute through UI elements and a HealthCommandFactory that creates the appropriate ModifyHealthCommands based on those elements. One additional (and generally useful) piece of functionality this wrapper adds on top of the CommandStream is maintaining an undo stack. This is more useful when the command stream mixes commands that are IUndoable with commands that aren't. It also showcases one of the benefits of caching an IUndoable's undo command, as if we simply added each IUndoable executed to the undo stack we would enter a loop of continually undo-ing and redo-ing the same Command. Instead, we can store a reference to the last IUndoable we queued and undo command for and check if an executed command is equal to that undo command before adding it to the undo stack. One other extension worth commenting on here is how this wrapper responds to command failures. While the internal invoker doesn't know the implementation details of the commands its executing, in your wrapper you will now how you are implementing commands and can react to their failure accordingly. In this case we do so by queueing a new command that will modify the Health of the command's receiver to precisely zero or MaxHealth depending on whichever is appropriate for the command.

Simple Async Panel

Moving on to some of the more complex panel's, we have first a demonstration of one of the more advanced command types, AsyncCommands. These commands are created by inheriting from the AsyncCommand class rather than the normal Command class. To illustrate AsyncCommands better, the commands in this panel are very simple and just wait for a certain number of seconds before completing. To see how they work, open the console window in the Unity Editor, and then in play-mode click on one of the "run" buttons on this panel. You will notice three messages printing in the console. The first is from the command and indicates how long of a wait it will preform, the second is from the CommandStream wrapper and indicates that TryExecuteNext as returned, and the third indicates that the wait has completed. If you aren't familiar with asynchronous programming you may be surprised by this, we have a return value from TryExecuteNext, but the command isn't done executing yet! This is the strength of asynchronous methods, to see whats happening better lets open up the WaitForSecondsCommand class and take a closer look at what is happening.

WaitForSecondsCommand & AsyncCommand

Looking through the methods in this class one of the first things that might jump out to you is that there is no Execute method in this class, instead the logic of the command is implemented in a separate method ExecuteAsync, which unlike the standard Execute method has a return value of Task (you may also notice that despite having a return value the method never actually returns anything), and has the additional keyword async. In fact, when you inherit from AsyncCommand you are explicitly prevented from overriding Execute, as its implementation within AsyncCommand is sealed as the final override in this chain of inheritance. Lets take a look at the base class of AsyncCommand to see why this is the case.

Taking a look at the AsyncCommand implementation of Execute, you can see that it does a few things. We will focus on the two methods it invokes first and circle back to the book-keeping it preforms in a moment. The first method it invokes is the ExecuteAsync, where concrete implementations of AsyncCommand place their logic. It stores the return value of this method in the CommandTask property. But recall, we never return anything from ExecuteAsync in our implementation. What going on here? When you declare a method as async you indicate to the compiler that there is some portion of it that needs to wait on something else before it can be completed. Rather than block the main thread from executing while it waits, upon reaching an "await" statement, the method will return a task representing the rest of its execution, than resume that task on a separate thread once its done waiting. In the ExecuteAsync method of WaitForSecondsCommand that Execute will invoke we can see that this method prints the first debug log we saw in the console, then it enters a for loop, each iteration increasing i by a certain time-step, and leaves once i is greater then the total wait in milliseconds. At the end of each iteration of the loop is the line await Task.Delay(timeStep);. As soon as we reach this line for the first time, control of the main thread will be returned to the invoker of the ExecuteAsync, along with the task for its completion, which will resume on a separate thread each time the time-step has elapsed.

Putting this all together, when the CommandStream has its TryExecuteNext method invoked, it will invoke the Execute method of WaitForSecondsCommand, which is defined by its base AsyncCommand class. This method will first check if the CommandTask has already had its value assigned, and if it has check if it is still running. If so, it will throw an exception that is handled by the CommandStream. Otherwise, it will invoke ExecuteAsync, which will run to the end of the first iteration of the for loop before returning its task to be stored in the CommandTask field. Execute then invokes another async method, InvokeWhenTaskComplete. Looking at this method we see it await's CommandTask before invoking one of three events depending on how it finished (the 3 ways an AsyncCommand can finish its task are successfully, by being cancelled, or by the task throwing an exception). At this point, the Execute method completes, and control returns to TryExecuteNext, this method sees that it just executed a IAsyncCommand, preforms some bookkeeping (more information about this is in the package README), and then returns the ExecuteCode "AwaitingCompletion." Control has now been returned to the Update method of the CommandStream's wrapper, which prints the second debug log we saw. While all of this is happening, the CommandTask is still running on a separate thread. Each time the time-step elapses, it resumes its execution and moves on to the next iteration of the loop. It reports its new progress, checks if it has been canceled (more on this in a moment), and then suspends the task for a time-step again. Once the total desired number of time-steps has elapsed, the method breaks out of the loop, prints the third debug log we saw, and finally we reach the end of the method. Rather than returning control to the invoker (since the main thread has long since returned to the CommandStream's wrapper by now), this causes CommandTask to be marked as complete. This in turn causes the invokeCompletionEventTask, which has been waiting for CommandTask to complete all this time, to resume, and the InvokeWhenTaskComplete method to finish by raising the OnTaskComplete event. Canceling CommandTask, or ExecuteAsync throwing an exception will also cause this to happen, although InvokeWhenTaskComplete responds to these sections differently.

On the right underneath this panel are some settings you can adjust to visualize how the different values for the WaitForSecondsCommand affect all of this. You can try pressing multiple "Run" buttons at once, or the "Run All" button, and see how the update cycle of the CommandStream wrapper continues to execute newly queued commands while the previous ones run in the background. You can also increase the time-step to visually see how the tasks stop and start as the time-steps go by. Try running a short WaitForSecondsCommand immediately after a long one and notice how even though the long one was started first the short one records its final debug log before the long one does.

AsyncCommand Cancellation

Lets circle back to the command cancellation now, as this is important to understand when implementing your own AsyncCommands. You may have already experimented with the cancellation button. If you did, you probably noticed how it takes a moment for the cancellation to actually occur, especially if you experimented with longer time-steps. This is because cancelling asynchronous method doesn't happen automatically, but needs to be deliberately implemented. To understand this, lets zoom out and look at how canceling async tasks work's in general.

Canceling a task first involves creating a CancellationTokenSource object. This object, as its name implies, has a CancellationToken as well. The CancellationTokenSource is used to indicate that a task should be cancelled. The task, than uses the CancellationToken to determine if the CancellationTokenSource has been told that it should be cancelled. When a CommandStream see's that it has executed a command implementing IAsyncCommand, it will create a new CancellationTokenSource as assign the source's token to the CancellationToken property of the IAsyncCommand. This CancellationTokenSource can than be retrieved using the CommandStream's GetRunningTaskCTS method. When a CTS is done being used it is important to dispose of it lest it lead to a memory leak (as there are un-managed resources associated with the CTS that will not be cleaned up in garbage collection), this is also handled by the CommandStream for you. When you want to indicate a task should be canceled, you do so by using the Cancel method of its token's source. You do not need to retrieve the CTS from the CommandStream every time you want to do this, it as a CancelRunningTask method to do this for you, while also verifying the task is still running (both GetRunningTaskCTS and CancelRunningTask can be invoked using either the task itself or the AsyncCommand that created it as an argument).

But remember, all this is doing is indicating to the token that the task should be canceled. You need to respond to that indication withing your ExecuteAsync method, otherwise using the CancelRunningTask method will do nothing. In WaitForSecondsCommand we do this in the conditional: if (CancellationToken.IsCancellationRequested) { Debug.Log($"{millisecondsToWait} ms wait cancelled after {i} milliseconds"); CancellationToken.ThrowIfCancellationRequested(); }

We could also simply invoke CancellationToken.ThrowIfCancellationRequested(); as this will do nothing unless CancellationToken.IsCancellationRequested is true, however there is often clean-up you will want to do before canceling the task in order for it to cancel gracefully. This is why it takes a moment for cancellation to occur. If you cancel while the Task is await'ing the time-step, it will still finish that await and not cancel until its next loop after it updates its progress. You may be wondering, what happens in the first loop before the CommandStream has had the opportunity to create the CancellationTokenSource? How are we able to access members of the CancellationToken before its value has been assigned? The reason for that is the type CancellationToken is actually a value-type struct, not a reference-type class. This means that before CancellationToken has been assigned it has the value CancellationToken.None, which is also what the CommandStream will set its value back to after it dispose's between executions.

Reporting Progress

The last thing to discuss is how these commands report their progress to update the bars displayed on the ui. This is not a feature that is implemented by default in the AsyncCommand abstract class as what reporting progress would entail can differ wildly between implementations, and in some cases isn't even necessary (for example in AsyncCommands that wait for some user input). In our case reporting progress was as simple as sending a float to the listening game objects for what the new progress was; however, in other cases this process may be more complicated. The general pattern for creating an AsyncCommand that reports its progress is to define an object that implements the IProgress interface, and give the Command an instance of this object. If you want multiple different AsyncCommand to report their progress through the same object, such as different AsyncCommands loading different assets into the scene, you can pass the object through which their progress should be reported into their constructors. Alternatively if each command should report its progress inherently, as is the case in this demo, each can create its own instance of the object upon construction.

When you inherit from the IProgress interface you will need to specify a generic type argument and define a Report method that takes an argument of the type you specified. This method is what you will invoke to indicate the progress has changed. In our case the type we reports is a simple float indicating what portion of the wait has been completed, however this can be any type the you wish. to use the example above of async commands loading assets you might append a string id of each asset that is loaded to display a message of what the a loading screen is currently doing. It is also recommended you use an event that is invoked when the Report method is called, and then bubble that event up through the AsyncCommand object as well. This will allow you to subscribe to the event to display total progress made as it changes.

Looking at the implementation of this in the demo, we use the WaitProgress object, which passes the argument of its Report method through the OnProgressChanged event, which is the bubbled up into the OnProgressChanged event of the WaitForSecondsCommand. In the WaitForSecondsFactory meant to simulate the client creating these commands and sending them to the CommandStream, we subscribe the ProgressBar's SetFilledPortion method to this event. In the WaitForSeconds command each time we enter a new loop we send the current ratio between the total number of time-steps we have awaited, and the total number of time-step's the wait will take, into the command's WaitProgress object.

This is not the only way of reporting progress out of a AsyncCommand, and is likely overkill for this demo as a simple event you probably work for its case; however, it hopefully demonstrates a pattern that can be used to implement reporting progress in an extensible manner that will suit the needs of even more complicated use cases of AsyncCommands.

Tween Panel

A common pattern that is found in implementing simple animation in games, in particular of ui elements, is called "tweening." The name for this pattern comes from the fact that rather than a complex animation it simply moves the object between two states, wether that be two positions, two scales, or two rotations. It is typically done through linear interpolation (or lerp-ing), however more complex forms of interpolation are possible. In this panel we showcase how you might implement this using the CommandPattern. You can left click anywhere within the panel to preform a Tween command to move the white square from its current position to the position you clicked on. You can also preform scale and rotate tweens using the buttons below the panel or the right or middle mouse buttons respectively. You can adjust what factor or angle the scale and rotate commands use with the sliders below the panel as well.

Based on the messages in the debug log for this panel, you may think it also works through AsyncCommands as well. In fact, this panel actual uses synchronous commands; however, they preform their execution by running coroutines. The difference between coroutines and asynchronous methods is subtle but significant. We will discuss this more after we explain what these commands are doing exactly.

TweenCommands

This is an abstract base class of all of the tween commands used to implement what they have in common. The length of time the Tween will take, and the time that it started, as well a property to get the difference between the current time and the start time. The game object being 'tween-ed, and an implementation of the Execute method. This implementation uses a monobehaviour component of the game object its moving to start the TweenCoroutine method, which it leaves abstract to be defined by specific types of TweenCommands. This pattern is very similar to the AsyncCommand pattern, but unlike an async method a coroutine returns an IEnumerator, and does not run on a separate thread. Instead it spreads the execution of the coroutine over several frames. To see what is meant by this, lets look at the simplest concrete tween command, the MoveTweenCommand.

MoveTweenCommand

In addition to the fields of the AbstractTweenCommand it inherits from, a MoveTweenCommand also has fields for its start position vector and its end position vector. The end position is defined in its constructor, and the start position is set at the beginning of its TweenCoroutine implementation. Within the TweenCoroutine implementation we also communicate to the TweenCommandStream wrapper that a move tween has started. Then, until the the time elapsed since the tween started is greater than the tween's time-span, the coroutine lerps the position of its game object between the start and target position based on the portion of the time-span that has elapsed, and then has the line yield return null. This line is similar in function to the await statements of AsyncCommands. When a coroutine executes a yield return statement it suspends the execution of the coroutine based on what is being returned. yield return null will suspend the execution until the next frame. Once the total time-span has elapsed, we communicate to the wrapper that the coroutine is done, and print a message in the debug log as well.

Scale and Rotate TweenCommands

The other two types of concrete tween commands are very similar to the MoveTweenCommand, so we will not be explaining them in detail. You can look at their implementation yourself if you would like. All tween commands broadly lerp some vector element of their gameObject's transform from a start position to and end position. The key difference in scale and rotate tweens is instead of defining the target in the constructor they calculate the target at the beginning of their coroutines. The reason for this is if we want to scale the object by a factor or rotate it by and angle, the target vector depends on the start vector, so we need to know what exactly the start vector is before defining the target vector otherwise it might behave unpredictably as the lerp would depend on the game objects transform when the command was creates, rather then when the lerp was started.

Tween CommandStream wrapper

Taking a look at the wrapper of the CommandStream for this panel the most notable thing that sets it apart from the wrappers of other panels is that it actually wraps multiple CommandStreams. This can be useful if you want to keep different types of commands separated from each other, as we do in this case. Rather than providing a single method that publicly exposes the QueueCommand method of its CommandStream, this wrapper provides three different overloads for its QueueCommand method. Each of these accepts a different type of the concrete tween commands as its argument and queues them all into their own CommandStream. Then, in the update loop we check if each TweenType has a coroutine running (which we communicated to the wrapper in the implementations of the TweenCoroutines). If it doesn't, and its CommandStream's queue isn't empty, we invoke that CommandStream's TryExecuteNext method. The reason for this is if we start a Coroutine while one is currently running, unexpected behaviours can happen. Say We have a MoveTweenCommand running its coroutine. Halfway through we execute a different MoveTweenCommand. This second command would set its start position based on where the object was in its first command when the second was executed. Both of these coroutines would then run, although visually the object will continue along its first coroutines path until it finished. Once it did, it would suddenly jump to wherever its second coroutine thinks its should be based on where it was when that coroutine started, and finish that one.

The reason we maintain separate CommandStream's for different TweenTypes is because is not true for tween's of different types. In fact we might want to be able to combine these different types of tween to create interesting animations. So, use a different CommandStream for each type of tween, which allows us not to execute a command from, say, the moveCommandStream, and still execute a command from the scaleCommandStream while the previous MoveTweenCommand was still running its coroutine. You can see this by executing multiple types of tweens in a row in the demo, this may be easier using the mouse buttons rather than the ui buttons. If you queue two movement tweens in a row the object will complete the first one before starting the second. If you queue a move tween then a scale tween, the object will smoothly preform the scale tween while if preforms the move tween without them conflicting with each other.

What if we wanted to tween multiple objects

The implementation in this panel admittedly takes advantage of the simplicity of only having one object we want to preform tweens for. This is unlikely to be the case in a real use-case, so lets take a moment to talk about how you might extend this. The simplest method would be to replace the boolean for wether or not a TweenType has a coroutine running with a list of game object that are currently being tween by AbstractTweenCommands that are currently running. When a tween command starts its coroutine, it would add its game object to the list for its type, and remove it after it finishes (although while we are discussing improvements that could be made to this implementation, it would be better to separate this concerns using an OnCoroutineStart and OnCoroutineFinished event). We could then, before executing the next command, use the CommandStream's TryPeekNext method to examine what game object the next command would act on (We would need to make the game object property of AbstractTweenCommand publicly readable to do this). If it is currently in the list of game objects that are currently being tweened by a command of that TweenType, skip executing the next command of that CommandStream this frame. Alternatively, if we didn't want to wait till the command at the front of the queue could run before executing any of the commands behind it, we could use the CommandStream's RequeueNextCommand method to move the next command to the back of the queue.

Why use coroutines over AsyncCommands

There are many reasons that one might chose to implement something through coroutines rather than through an asynchronous method, not all of which we will be discussing here. This talk provides an excellent overview of the difference between the two. Instead, we will focus on the primary reason why this demo is implemented using coroutines rather than AsyncCommands. That reason being, there are significant limitations to what Unity components can be accessed from outside the main thread. Basically anything using the Unity API runs on a single thread (this is ignoring the jobs system which is beyond the scope of this demo). Time.time, gameObject.transform, and transform.position, would all be inaccessible from the thread ExecuteAsync ran in. This would make these command significancy more complicated to implement. It is much easier to preform our tween's by using a coroutine instead. As mentioned above, coroutines are all evaluated on the main thread and simply periodically have their evaluation suspended for a frame or more. A general guideline would be if you have a command that needs to spread its execution over time, and you have to use any of the Unity API functionality in that command, you should implement it using coroutines. If you can separate anything involving the Unity API from the command itself, you can use AsyncCommand. For example (an example that will be seen in practice in the next demo), if you have a command that does something in response to user input, you could separate the logic that listens for input into a monobehaviour that invokes an event when the input is received. Your command can then subscribe to that event and respond to the input and finish its execution without having to touch the Unity API. In this case AsyncCommand would be an excellent option for your implementation. In this demo, significant workarounds would be needed to execute the command without touching any Unity API functions, and it is much easier to just use coroutines instead. \

Input Panel

This final panel is an example of more realistic use case of this package as the input system for an actual game. This panel can be activated by toggling the switch beneath it. While active, you can move the player within the panel box through the WASD keys and fire projectiles by left or right clicking (although the mouse must be in the input panel if the fire keys are bound to mouse buttons). These inputs can be rebound using the panel to the left as well, which also utilizes the command pattern and will be discussed shortly.

Input Types

there are 8 different types of inputs that are supported in this demo, represented in the InputType enum. First are the 4 directional movement inputs: up, down, left, and right, along with a sprint input that modifies the speed of movement. Then there are 2 fire inputs, fire and alt fire. And lastly is the undo input, which causes the player to retrace their movement backwards until returning to their original position (or the oldest position recorded in the very unlikely situation over one million commands are executed in a single playmode session).

Player

When the demo is active this monobehaviour will check for key stroke's matching the KeyCode's in the InputCommandStream's InputKeybinds dictionary for each input type in its update method (or the fixed update method for movement inputs). If any of them are detected, it will queue the appropriate of the two command types in this demo into the InputCommandStream to be executed. Two of the fields modifiable in this components inspector will affect how it responds to movement input, its speed, which will determine fast the player moves, and the sprint factor, which the speed will be multiplied by if the sprint input is down while moving.

MovePlayerCommand

The receiver of this command is the Player provided to it in its constructor. It moves its receiver in the (normalized) direction provided in its constructor with a given speed, also provided in the constructor. It implements IFailable and IUndoable, the implementation of the later of which should be obvious. It will return true for would fail if it would move the player outside of its bounding box (assigned in the inspector of the player). It can also be created by explicitly defining the vector to move the player by in the constructor, rather that provide a direction and speed and letting the command calculate the actual movement vector.

SpawnProjectileCommand & Projectile scriptable objects

This command creates its own receiver when it is executed. It uses a ProjSO scriptable object, provided in its constructor to determine how it does so. Before we explain how this command works lets discuss how these scriptable objects work. ProjSO is an abstract class that has different concrete implementations for projectiles with different behaviours. All ProjSO assets defines a speed for its projectiles to move at and a prefab game object to use for the projectiles visuals. The also have a implementation of ProjSO's public abstract Vector3 UpdatePosition(Vector3 currentPos, Vector3 origin, Vector3 prevDirection, out Vector3 newDirection) method. Not every implementation of this uses all of the data included as arguments. This method determines how the projectiles position changes over time. When a SpawnProjectileCommand executes, it instantiates the prefab from the scriptable object's visuals field. It then attaches the projectile component to this game object, and sets that component's data field to be equal to the command ProjSO. The projectile component in its FixedUpdate method uses its data's UpdatePosition method to determine what position to move the game object to next. It also stores the out parameter of the UpdatePosition method as the previous direction it was moved in to use as the prevDirection argument in its next FixedUpdate. T

This approach allows the same command and monobehaviour component to be used for a variety of different projectile behaviours. You can see this by changing between the different possible projectile types in the "Select Projectile" panel to the right of the input panel. These different projectile types use there different subtypes of ProjSO, which all calculate new positions from the arguments of their UpdatePosition method in different ways. Different projectiles that use the same subtype of ProjSO create different behavior by using different value for the serialized fields of their scriptable object asset. If you are curious how this implementation works we recommend you look at the different ProjSO assets in the "Projectiles" folder withing the assets of the demo, as well as the different implementations of "UpdatePosition" in the subtypes of ProjSO.

Rebinding Inputs

The keys to queue each command can be rebound using the buttons on the panel to the left of the input demo. As was said above, these buttons also work using the architecture provided by this package. When one of these buttons is pressed command will be queued which creates a game object that will listen for a keystroke. If the keystroke detected is not currently used for a different input command, the command that created the object will change the key for the selected input type to the detected key. Currently the keybindings are reset upon leaving play-mode; however, this could be circumvented using a scriptable object or json to store the changes and reload them upon reentering play-mode; however, that is beyond the scope of this demo.

RebindKeyManager

This object stores references to each of the rebind buttons, sets their text, and sets the delegate's invoked when they are pressed in its start method. It also maintains a CommandStream the commands to rebind inputs are queued into, as well as a collection of commands to use to rebind keys. Since only one command to rebind a key can be running at a time, we use the same command every time we rebind a certain input type. These Commands are also created in the start method of the RebindKeyManager.

RebindKeyCommand

If you read the section on the simple async panel, you can probably already guess that the RebindKeyCommand is an example of an AsyncCommand. If not, we strongly recommend you read that section first. Unlike the simple async panel this demo is a much more practical use case of AsyncCommand's you are more likely to see used in an actual game. A RebindKeyManager is simply creates by providing its constructor with a an InputType enum entry to rebind. This is part of the reason we can reuse the same command every time we execute one in this panel, as there are now parameters in the constructor that would need to be changed between uses. This class also adds an OnRebindStart event to the standard task end events provided by the base AsyncCommand class. When the Command is constructed it sets up delegate to its turn off the input demo while the command is running, as well as to deactivate the switch to turn it back on, and revert both of these to their previous state after the command finishes. The delegate we subscribe to the OnAnyTaskEnd event also destroys the object we us to listen for the key the user wants to rebind the input to. The RebindKeyManager creating the commands also subscribes some delegates to these events. When it creates a Command to rebind a certain InputType, it subscribes a delegate to its OnRebindStart event to clear the text on that InputTypes button to just display what the input is. This is to provide feedback to the user that they are in the process of rebinding the input. It subscribes a delegate to update the buttons text to the OnAnyTaskEnd event. This will either update the text to the new key the user has selected, or return it to the old text in the event the rebind was cancelled.

Looking at the ExecuteAsync method of this commands, we can see it starts by invoking the OnRebindStart event we discussed above. Then it defines a bool to indicate wether the rebind has been completed, and constructs a game object with the KeystrokeListener component. Lets take a moment here to take a look at this component, as it is fairly simple. We can see it simply contains a reference to the RebindKeyCommand that created (set as part of its creation), an event to invoke when a keycode is detected, and a keycode to use as an indication to cancel the rebind. Within its updates loop it loops through all of the possible keycodes, and checks if they are pressed, if it is pressed, and its the cancelKeyCode, the component destroys itself, which causes its OnDestroy method to cancel the RebindKeyCommand that created it (this will also be invoked if this object is destroyed through other means such as deleting it in the inspector). If the pressed key isn't the cancel key, it passes it into the KeystrokeDetected event. Back in the RebindKeyCommand, we subscribe a delegate to preform the actual rebind to this event. Within this delegate we verify this key isnt being used for any other input currently. If it is, we reject the input and wait for the event to be raised again. Unless, that is, the button was already being used for this key. In this case we just mark the rebind as done without doing anything, effectively a soft cancellation of the rebind (as the RebindKeyCommand still completes successfully but functionally it is the same as if it had been cancelled.) If the key isnt currently being used for any other input, we update the InputCommandStream's keybinding dictionary's entry for this commands InputType to be equal to the entered key, and marks the rebind as done. After this delegate has been set, the command then enters a while loop, which it stays in until the rebind has been marked as done. Within each iteration of this loop is a await Task.Yield() statement, which is functionally very similar to Yield return null. Alternatively you could use await Task.Delay(1) to suspend the task for a single millisecond. After this await, it cancels the task if requested to, then restarts the loop.

RebindKeyCommand is an example of why its important to support canceling tasks and consider what circumstances it is appropriate to cancel them in. If we didn't support canceling RebindKeyCommands, or only did so directly in response to the cancel key being pressed instead of in the KeyStroke listeners OnDestroy method, consider what would happen if we started a RebindKeyCommand then destroyed its keystroke listener. The command would continue running but it would never have the event it is listening for invoked as the object that would invoke it has been destroyed. Even if we allowed other RebindKeyCommands to run while a different was running, this command would continually be running in the background wasting resources and slowing the game down. Even worse, it we didnt properly dispose of the KeystrokeListener a command created in its OnEnyTaskEnd event, this object would continually be looping through every possible KeyCode (of which there are very many) each frame and checking if they are down. The only reason this is an acceptable implementation performance wise is there is a KeystrokeListener in the scene so rarely and they are destroyed as soon as they are done being used.