📜 ⬆️ ⬇️

Learn to command

I wanted to share a wonderful development process, which I recently met. I have not seen such an approach before, and people, as soon as they get acquainted with it, cannot long understand and accept this method of building games. And, honestly, I myself did not understand everything in the first week. But after some mastering, I already forgot how to make games differently. There are plans to write a series of articles, but we will start small and gradually we will increase our understanding of what and why and with what it is.

As someone could already guess, today I will tell you about the “Command” pattern and how to use it to develop games using the Unity 3D engine. This is one of the key patterns in this approach. The code will be simplified, but working and should give an understanding of the process.

Prologue


You probably sometime already saw articles in which developers tell how to use Actor-s in Unity? If not, I will now quickly explain the essence with an example: in your game there are a dozen game characters who should, for example, jump in different ways. Of course, the task can be solved through everyone’s favorite polymorphism: make a base unit and simply overload the virtual Jump method for each unit.

Something like this
public class UnitController : MonoBehaviour { public Rigidbody AttachedRigidbody; //... public virtual void Jump() { rigidbody.velocity = new Vector3 (0, 10, 0); } //... } 

 public class RabitUnitController : UnitController { //... public override void Jump () { //very high jump } //... } 


But in this case, if you need to have several ready-made units, which jumped in different ways, began to jump the same way, then you have to either slightly correct the class hierarchy, or just copy the appropriate piece of code into all the necessary classes (which is terrifying).
')
With the help of Actor-ov this problem is solved differently. Applying this approach ^ it would also be necessary to write a class of a unit, only now, instead of the virtual Jump method, write a series of separate UnitJumper components and simply hook a suitable component onto the correct unit. And at the moment of the jump, call the Jump method on the attached component:

Actor code
 public class UnitJumper : MonoBehaviour { public virtual void Jump(Rigidbody rigidbody) {} } public class RegularJumper : UnitJumper { public override void Jump (Rigidbody rigidbody) { base.Jump (rigidbody); rigidbody.velocity = new Vector3 (0, 10, 0); } } public class MajesticAFJumper : UnitJumper { public override void Jump (Rigidbody rigidbody) { base.Jump (rigidbody); rigidbody.velocity = new Vector3 (0, 15, 10); /* * some magic here */ } } 


And so was the controller
 public class UnitController : MonoBehaviour { [SerializeField] private UnitJumper _unitJumper; public Rigidbody AttachedRigidbody; //... public virtual void Jump() { if (_unitJumper != null) _unitJumper.Jump (AttachedRigidbody); else Debug.Log("UnitJumper Component is missing"); } //... } 


Now everything is simple and beautiful. There are fewer problems with hierarchy, the jump code is in a separate small class, which makes it easy to change. Each jump method can have any number of parameters and you will be sure that changing them will not break, for example, running. Also, the modification of the jump mode for the unit is now also very simple. In addition, the environment itself pushes us to follow such an architecture, and with the help of the [RequireComponent ()] attribute it is possible to tinker with the editor even less. Now you have to ask why I am telling all this and what is the connection. So it's time for a logical transition to the Command pattern.

Logical transition


We have already moved away from writing all the jump code in our example into one class, but what if we need the units to not only jump themselves in different ways, but also, for example, to change the jump method depending on the circumstances ( do somersaults, run along the wall)? This is where we will need a team.

The essence remains the same - to carry out all the elementary actions in separate classes. Only now we will add the necessary component to the unit immediately before use and this will allow us to change the unit's behavior at any time and there will not be such a strong connection as in the case with Actors. Let's write a small base class for a command, which so far will only serve to call a command on a given object.

Basic team
 public class Command : MonoBehaviour { public static T ExecuteOn<T>(GameObject target) where T : Command { return target.AddComponent <T>(); } private void Start() { OnStart (); } protected virtual void OnStart() {} } 


The above code serves only to conveniently add components to an object, and the OnStart () method so far (but only so far) exclusively for intellisense.

Now that we have a base class, we can implement a simple jump.

Approximate class jump
 public class RegularJumpCommand : Command { protected override void OnStart () { base.OnStart (); gameObject.GetComponent <Rigidbody>().velocity = new Vector3(0, 10, 0); } } 


And now, in order to make the unit jump, we only need to execute the command on it:

Command call
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject); } } } 


The first thing that catches your eye is that the velocity values ​​are constant. So just to make the jump a little higher will not work. Previously, we would solve this by passing arguments to the jump method, and let's do it here. Let's rewrite our beautiful team:

Command with arguments
 public class Command : MonoBehaviour { private object[] _args; public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { OnStart (_args); } protected virtual void OnStart(object[] args) {} } 


Now the height and direction of our jump can be changed by passing arguments to the team (don't forget to quote). Since Start () is called a little after the creation of the object, the arguments are passed correctly to our OnStart (object [] args) method.

The jump command will remain almost unchanged, but now we can use in it the arguments passed from the outside:

Use of arguments in the command
 public class RegularJumpCommand : Command { protected override void OnStart (object[] args) { base.OnStart (args); gameObject.GetComponent <Rigidbody> ().velocity = (Vector3)args [0]; } } 


The command call changes a bit more:

Call command with arguments
 public class SomeController : MonoBehaviour { // don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{new Vector3(0, 10, 0)}); } } } 


After the manipulations, the teams became flexible and a separate class will now only be needed for flips. But to initialize the parameters, you will only need to use the OnStart (object [] args) method.

The second problem that we still have is that every time we jump, we will call the expensive GetComponent () method. To solve this, let us remember that we still have a controller with Actors, which holds links to all important components and in the team we will ask him everything we need. We can also pass the controller to the arguments, and I suggest making it a little more formalized. Let's write for the team a child class with a controller:

Command with controller
 public class CommandWithType<T> : Command where T : MonoBehaviour { protected T Controller { get; private set; } protected override void OnStart (object[] args) { base.OnStart (args); Controller = args [0] as T; } } 


In the command itself, after this, only the number of the argument that we use has changed, but do not forget about that either. But there was a convenient way to get the controller without resorting to GetComponent (). And be sure to call base.OnStart (args), otherwise we will not be able to use the controller:

Controller use
 public class RegularJumpCommand : CommandWithType<UnitController> { protected override void OnStart (object[] args) { base.OnStart (args); Controller.AttachedRigidbody.velocity = (Vector3)args [1]; } } 


Calling the team also became a little different:
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{_targetUnit ,new Vector3(0, 10, 0)}); } } } 


Now everything has become quite good: we have teams that can do without a controller (show advertising, post something somewhere) and commands that need a controller (to go, run, fly). Commands with a controller are sharpened for work with a family of classes and will not be available for other families, which introduces additional orderliness. And also we still have the advantages of Actor-in. And you could not help but notice how small and neat they are. The controllers also benefited from this: they became the same laconic receptacles for references to the components we need (later, of course, we will give them more weight). But this is still only the beginning and with such functionality we will not go very far.

In order not to go far from the jump, let's see what else we missed. We bring this awkward, but useful example to a logical conclusion.

The first thing that catches your eye: after ten jumps, ten absolutely useless teams will hang on the object. The second thing to notice is that the velocity changes at the start and this does not guarantee the correct operation of the physics engine.

Let's focus on the first, go second and decide on its own, as it happens. The most logical thing for the team to clean itself is as soon as it fits. Let's add the command completion functionality and two flags to know if the command completed its execution (more on that later). And after small transformations, the team evolves (but this is not even its last form).

Team cleaning
 public class Command : MonoBehaviour { private object[] _args; private bool _started = false; private bool _isReleased = false; public bool IsRunning { get{ return _started && !_isReleased;} } public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { _started = true; OnStart (_args); } protected virtual void OnStart(object[] args) {} private void OnDestroy() { if (!_isReleased) OnReleaseResources (); } protected virtual void OnReleaseResources() { _isReleased = true; } protected void FinishCommand() { OnReleaseResources (); Destroy (this, 1f); } protected virtual void OnFinishCommand(){} } 


Now, at the right moment, the team, as a decent citizen and a member of society, will self-destruct, just call the FinishCommand () method after all the necessary manipulations. Destroy () is slightly delayed so that everyone who needs it can use the command before the disappearance (take data from it, but more on that later), and the IsRunning flag is needed by the team itself so that it does not start working ahead of time and does not continue after complete. All unsubscribing from events and freeing resources can be easily done in OnReleaseResources () or OnFinishCommand (). And do not be afraid that you accidentally write On Destory () and you will suffer for a long time (as I once did).

Now with this all we can solve the second problem:

Change in velocity in FixedUpdate
 public class RegularJumpCommand : CommandWithType<UnitController> { private Vector3 _velocity; protected override void OnStart (object[] args) { base.OnStart (args); _velocity = (Vector3)args [1]; } private void FixedUpdate() { if (!IsRunning) return; Controller.AttachedRigidbody.velocity = _velocity; FinishCommand (); } } 


Now the value of velocity will change at the moment of the first iteration of the physics engine after Start-a. The team at this stage of its development will wonderfully cope with tasks like casting spells, running, various jumps and visual effects.

But how ?!


This is not going to take advantage of wherever they want. If you need to download the config or validate the user's actions (do not let the children buy buns for parental money) or simply use the commands as part of running in zigzags. Quite often, when it is necessary to know if the team has completed, if it has completed successfully, and if so, take the necessary data from it (for which the destruction was delayed). In short: you can't figure out here without callbacks. Personally, I like the new Unity UI system because of them, but only when adding or deleting them comes from the code (it’s a sin to do this in the editor, don’t do so).

You can do everything on events, you don’t even need to think, but you don’t really want to unsubscribe at the right moment to keep references to all running commands. And the pain just below the back, in the case when I forgot to unsubscribe, very few people can bring pleasure. Let's first stipulate what we need to do in order to start using the commands in full force and so that there will be no changes. The main task is to make a callback for successful and not successful completion of the command. Make them convenient for signing and without having to follow the unsubscribe. It will also be convenient to transfer the command itself to the callback argument, in order not to keep it as a separate field in the class. And we have not implemented a way to stop the command from the outside.

First: let's make a small wrapper for those same callbacks. No sooner said than done, we are programmers - simple people. It turned out like this:

Callback
  public class Callback<T> where T : Command { public readonly Action<T> Succeed; public readonly Action<T> Fault; public Callback (Action<T> succeed) { this.Succeed = succeed; } public Callback (Action<T> succeed, Action<T> fault) { this.Succeed = succeed; this.Fault = fault; } } 


Simple and convenient. Note that by default if the callback is one, then we automatically assume that we are only interested in the successful completion of the command and it will be called only in this case. The next logical step is to make a container for these same callbacks, because one will always be small. And we got this:

Callbacktoken
  public class CallbackToken <T> where T : Command { private List<Callback<T>> _callbacks; private T _command; public CallbackToken (T _command) { this._command = _command; _callbacks = new List<Callback<T>>(); } public void AddCallback(Callback<T> callback) { _callbacks.Add (callback); } public void RemoveCallback(Callback<T> callback) { _callbacks.Remove (callback); } public void FireSucceed() { foreach (Callback<T> calback in _callbacks) { calback.Succeed(_command); } } public void FireFault() { foreach (Callback<T> callback in _callbacks) { if (callback.Fault != null) { callback.Fault (_command); } } } } 


It remains only to add CallbackToken to our team and call it at the right moment. And do not forget to make the ability to complete the command successfully, not successfully from the outside. And immediately the final code:

Team with Callback
 public class Command : MonoBehaviour { private object[] _args; private bool _started = false; private bool _isReleased = false; public CallbackToken<Command> CallbackToken { get; private set; } public Command () { CallbackToken = new CallbackToken<Command> (this); } public bool IsRunning { get{ return _started && !_isReleased;} } public static T ExecuteOn<T>(GameObject target, params object[] args) where T : Command { T result = target.AddComponent <T>(); result._args = args; return result; } private void Start() { _started = true; OnStart (_args); } protected virtual void OnStart(object[] args) {} private void OnDestroy() { if (!_isReleased) OnReleaseResources (); } protected virtual void OnReleaseResources() { _isReleased = true; } protected void FinishCommand(bool result = true) { if (!IsRuning) return; OnReleaseResources (); OnFinishCommand (); if (result) CallbackToken.FireSucceed (); else CallbackToken.FireFault (); Destroy (this, 1f); } protected virtual void OnFinishCommand(){} public void Terminate(bool result = false) { FinishCommand (result); } 


Now the FinishCommand () method will accept the success argument, and the Terminate () method will be used to terminate the command from the outside.

Now let's see what a subscription looks like:

Subscribe to Callback
 public class SomeController : MonoBehaviour { //don't forget to set this in editor public UnitController _targetUnit; private void Start() { if (_targetUnit != null) { Command.ExecuteOn<RegularJumpCommand> (_targetUnit.gameObject, new object[]{_targetUnit ,new Vector3(0, 10, 0)}) .CallbackToken.AddCallback (new Callback<Command>(OnJumpFinish)); } } private void OnJumpFinish (Command command) { Debug.Log(string.Format("{0}", "Successfully jumped")); } } 


Now we can easily solve the second task: pick up the data from the command (after all, we get it in the callback method), simply by making a public get-er for the necessary information and - voila.

The end!


In conclusion, I would like to say that the approach is very good for developing games on Unity. Everything is simple and beautiful, easy to modify, and utilitarian commands (download, post) are easily transferred to any project. If you like this article, I will not only be pleased, but also will have an incentive to talk about state machines, MVC, strategies and how it lives and coexists in one project.

PS: do not forget that the code here is for informational purposes only and has been tested only in the editor.

Source: https://habr.com/ru/post/280306/


All Articles