Some of these templates will be discussed below, and we’ll describe each of them briefly. We tried to choose the most used templates both in our projects and around the world.
We will analyze each template separately, for each template we will try to provide the most easy-to-understand code.
As we have mentioned above: one of the behavioral design patterns that help us encapsulate some action as an object and perform when we need to.
Sample code for this behavior:
public abstract class Command { public abstract void Execute(); public abstract void Undo(); public abstract void Redo(); } public class ConcreteCommand : Command { private Receiver receiver; public ConcreteCommand(Receiver r) { receiver = r; } public override void Execute() { receiver.Action(); } public override void Undo() {} public override void Redo() {} } public class Receiver { public void Action() { } } public class Invoker { private Command command; public void SetCommand(Command c) { command = c; } public void Run() { command.Execute(); } public void Cancel() { command.Undo(); } public void Restart() { command.Redo(); } } public class Client { void Main() { Invoker invoker = new Invoker(); Receiver receiver = new Receiver(); ConcreteCommand command=new ConcreteCommand(receiver); invoker.SetCommand(command); invoker.Run(); } }
Let’s describe each participant of this template:
Thus, the client who makes a request knows nothing about who will execute the command. In addition, we can extend the command set by simply creating several new classes that inherit the Command class.
In the gaming field, this design pattern can often be found in user input locations. For example:
private void Update () { if (Input.GetKeyDown(KeyCode.UpArrow)) { invoker.SetCommand(new MoveUp(this)); } else if (Input.GetKeyDown(KeyCode.DownArrow)) { invoker.SetCommand(new MoveDown(this)); } else if (Input.GetKeyDown(KeyCode.LeftArrow)) { invoker.SetCommand(new MoveLeft(this)); } else if (Input.GetKeyDown(KeyCode.RightArrow)) { invoker.SetCommand(new MoveRight(this)); } invoker.Run(); }
As a result, we get a system with reverse action functions in response to certain actions, as well as the ability to stop and restart commands. Also with this template, we can significantly expand the logging of our program for better error handling.
One of the most useful patterns in game design development.
Basically consists of two main parts:
Let’s say we have a task: when some event occurs in the Subject class, we need to destroy all objects that subscribe to this object.
Based on the description above, this problem is solved quite simply using the Observer design pattern.
public class Subject { private List<Observer> observers = new List<Observer>(); public void Notify() { for (int i = 0; i < observers.Count; i++) { observers[i].OnNotify(); } } public void AddObserver(Observer observer) { observers.Add(observer); } public void RemoveObserver(Observer observer) { observers.Remove(observer); } } public abstract class Observer : MonoBehaviour { public abstract void OnNotify(); } public class Box : Observer { public override void OnNotify() { Destroy(gameObject); } }
This design pattern became so popular that some languages began to introduce it as part of the standard library. For example, in the context of the C# language, the event keyword exists and, in conjunction with delegates, they actually implement the template.
In conclusion, we get a flexible system of alerts for subject observers, which allows us to significantly improve the architecture of applications and remove unnecessary “spaghetti” code.
This design pattern is considered from the point of view of a common game problem.
A player during a game can be in different States (eg. Running, Shooting, Death). This problem is best solved with the help of this template. We delegate the logic of each state to a separate class derived from State.
Look at the example given below:
class Program { static void Main() { Player player = new Player(new State1()); player.DoSmth(); player.CurrentState = new State2(); player.DoSmth(); } } public abstract class State { public abstract void Do(Player player); } public class State1 : State { public override void Do(Player player) { context.State = new StateB(); } } public class State2 : State { public override void Do(Player player) { context.State = new StateA(); } } public class Player { public State CurrentState {get; set;} public Player(State state) { currentState = state; } public void DoSmth() { currentState.Do(this); } }
This design reduces the number of branch operators and makes the code cleaner and more extensible.
Everyone who has faced with game development sooner or later will face the task of saving and restoring the state of the object, such as the level or the state of the player at its beginning. This is where Memento comes to the rescue. Illustrate its essence immediately in the UML diagram.
For more clarity, we will describe each component:
This listing shows the simplest implementation of this design pattern. As a result of the listing, we save the changed state of the object to the Caretaker class object, which we can later unload at any time.
public struct State { public int valueToSave; } public class Memento { public State State { get; private set;} public Memento(State state) { this.State = state; } } public class Caretaker { public Memento Memento { get; set; } } public class Originator { public State State { get; set; } public void SetMemento(Memento memento) { State = memento.State; } public Memento CreateMemento() { return new Memento(State); } } class Program { static void Main(string[] args) { State state1; state1.valueToSave = 5; State state2; state2.valueToSave = 10; Originator originator = new Originator(); originator.State = state1; originator.State.valueToSave = 0; Caretaker care = new Caretaker(); care.Memento = originator.CreateMemento(); originator.State = state2; } }
In conclusion, we have a fairly simple way, without breaking the encapsulation, to maintain the state of an object.
The most useful in terms of performance gain generating design pattern. The main idea of the template is to reuse game objects.
For example, take the situation when you have a Match-3 game and a large number of new crystals are created, which the player periodically destroys. Without an object pool, creating each crystal will allocate memory, which can affect performance (and it will) for the worse. Such actions often increase memory fragmentation much and, as a result, the garbage collector spends more time freeing memory.
What is the solution to this problem? Instead of creating objects and allocating memory for them, we simply hide them from the user’s eyes, as long as they are again not needed. Statistically, resetting parameters is a much easier operation than allocating memory, so we can significantly improve the efficiency of the application by adding a simple object pool mechanism.
There are a lot of implementations of the object pool, but the main requirements are reduced to a short list:
A good article was given in the blog catlikecoding.
We’ve observed a lot of design patterns, each of them can significantly improve your life throughout the development of the game, but at the same time do not abuse them, because it can significantly reduce the readability of your code. Perhaps the most effective and almost ultimatum solution for any project is a pool of objects (Object Pooling) along with the Observer.
Comments are closed.