Game Development Patterns in C#

art1
In the gaming industry, you need to solve a lot of complex tasks all the time, such as performance degradation or “spaghetti” code. The industry has now developed many practices and generalized architectural solutions to the problems often encountered in game projects around the globe. At the moment, many architectural patterns are included in high-level programming languages as part of them. For example “Decorator” in Kotlin or “Observer” in C#/Java.

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.

  • Command is one of the behavioral design patterns that help us encapsulate some action as an object and execute when we need to. Also, this template provides the ability to cancel and repeat the command.
  • Observer is one of the most popular patterns at the moment. It provides an interface for receiving alerts for a class object that implements this design pattern from objects of other classes, thereby “observing” them
  • State is a fairly common behavioral pattern. It changes the behavior of the internal state and settings.
  • Memento allows you to save the state of the object without breaking its encapsulation, so you can easily restore the saved state.
  • Object Pooling is a popular generative design pattern that allows you to save performance when creating objects.
We will analyze each template separately, for each template we will try to provide the most easy-to-understand code.

Command

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.1_vmfsu9vhmkanfjhjxrjg2gSample 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:
  • Command: the interface that encapsulates the target action. Usually defines Execute(), Undo(), Redo () methods.
  • ConcreteCommand: an implementation of the command that realizes the Command methods. In the Execute() part, the Action() method of the Receiver class object is called.
  • Receiver: the recipient of the command. Specifies the actions to be performed as a result of the query.
  • Invoker: Makes a command call to execute a specific request
  • Client: The root part of the template. Creates a command and sets its recipient using the SetCommand() method.
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.

Observer

One of the most useful patterns in game design development. Basically consists of two main parts:
  • Subject contains a list of “observers” who will react to some action from the subject.
  • Observers are objects that “monitor” the subject and in response to its alerts perform certain actions.
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.

State

This design pattern is considered from the point of view of a common game problem.1_9auixcbpr8-iibw3-qydwqA 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.

Memento

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.1_tsep8ammnegrqcdp11xz4gFor more clarity, we will describe each component:
  • Memento is a guardian that stores the state of the Originator object.
  • Originator provides an interface for creating a guardian object to save your state
  • Caretaker performs only the storage function of the Memento object, and it is important to note that it no longer has access to anything and should not perform any manipulations on the Memento object, except its storage.
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.

Object Pooling

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:
  • Performance of pulling objects out of the pool
  • Ability to create multiple pools
  • The ability to pre-create a number of instances of a class.
A good article was given in the blog catlikecoding.

Conclusion

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.
Please, estimate my article. I did my best!
1 Star2 Stars3 Stars4 Stars5 Stars (3 votes, average: 4.67 out of 5)
Loading…

Comments are closed.

Share
Share