top of page
  • Writer's pictureLingheng Tao

Unity Engine #3 Patterns


This article mainly introduces the design patterns of various types of code. In GoF's book "Design Patterns: The Foundation for Reusable Object-Oriented Software", the author introduces a total of 23 design patterns. Among them, the patterns that are very widely and heavily used in Unity are mainly singleton pattern, observer pattern and factory pattern in this note.


Singleton Pattern


Singleton mode means that when a class has only one instance in the scene, we can provide it with a global access point. Commonly used cases are various types of Manager. The following is a conventional singleton mode writing method for GameManager Singleton:

public class GameManager : MonoBehaviour
{
    // private singleton
    private static GameManager _instance;
    
    // public singleton for others to get;
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = FindObjectOfType<GameManager>();
                if (_instance == null)
                {
                    _instance = new GameObject("Spawned GameManager", typeof(GameManager)).GetComponent<GameManager>();
                }
            }
            return _instance;
        }
    }

    private void Awake()
    {
        // since we are using the Singleton pattern, we should always assume that we only have the current instance in this scene
        if (_instance != null && _instance != this)
        {
            // if another instance is found, we destroy it;
            Destroy(this.gameObject);
        }
        else
        {
            _instance = this;
            DontDestroyOnLoad(this.gameObject);
        }
    }
    
    public void SomeFunction() {
        // ... some implementation ...
        return;
    }
}

After that, we can use it in other C# scripts

GameManager.Instance.SomeFunction();

To call public functions and variables in GameManager, there is no need to separately set a SerializedField or find the GameManager instance of the current scene.


Advantages of singleton mode

  1. Control the number of instances: The singleton pattern ensures that a class has only one instance during the life cycle of the application.

  2. Global access point: Singleton instances can be accessed globally, so they can be accessed anywhere in the application, and there is no need to repeatedly create objects or pass objects through parameters;

  3. Saving state: Singletons can maintain state throughout the life cycle of the application. This is useful for storing data that is shared across multiple systems or components, such as configuration data, caching, logging, etc.

  4. Reduced resource consumption: Because a large number of objects no longer need to be created (or destroyed), GC calls can be reduced.

  5. Unified Resource Management: When you have a shared resource that needs to be used in multiple places (such as a database connection or file system access), a singleton can provide a unified point to manage these Resources, ensuring resource usage is synchronized and consistent.

  6. Lazy Initialization: A singleton can implement Lazy Initialization (Lazy Initialization), that is, the object is not created until it is actually needed. This helps reduce application startup time and resource consumption.


Problems with singleton mode

Although the singleton pattern has many advantages, it also needs to be aware of its potential problems. For example, it can introduce problems with global state, which can make code difficult to test and maintain. Additionally, if used incorrectly, singletons can cause resource synchronization issues, especially in multi-threaded environments.


When using the singleton pattern, you need to weigh the convenience and potential risks it brings, and use it with caution in appropriate scenarios.


Observer Pattern


Observer Pattern defines a one-to-many dependency relationship between objects. When the state of an Object/Observed (Subject) changes, all dependencies Its Observers are notified and automatically updated.


When the observed changes, it will require changing other objects. At the same time, the observed does not not know how many We use the Observer pattern when Observeris observing itself.


The following is the conventional way of writing the observer pattern.

// 观察者接口
public interface IObserver
{
    void Update();
}

// 被观察者接口
public interface ISubject
{
    // 注册新的 Observer
    void RegisterObserver(IObserver observer);
    
    // 移除已有的 Observer
    void RemoveObserver(IObserver observer);
    
    // 通知目前所有的 Observer
    void NotifyObservers();
}

// 具体主体
public class ConcreteSubject : ISubject
{
    private List<IObserver> observers = new List<IObserver>();
    private float someState;

    public void RegisterObserver(IObserver observer)
    {
        observers.Add(observer);
    }

    public void RemoveObserver(IObserver observer)
    {
        observers.Remove(observer);
    }

    public void NotifyObservers()
    {
        foreach (var observer in observers)
        {
            observer.Update();
        }
    }

    public void ChangeState(float state)
    {
        someState = state;
        NotifyObservers();
    }
}

// 具体观察者
public class ConcreteObserver : IObserver
{
    private ConcreteSubject subject;
    private float observerState;

    public ConcreteObserver(ConcreteSubject subject)
    {
        this.subject = subject;
        this.subject.RegisterObserver(this);
    }

    // 更新操作
    public void Update()
    {
        observerState = subject.SomeState; 
    }
}

Here, we just compare the differences between interface / struct / class.


Interface interface

  • Definition: An interface is a specification or contract that defines a set of methods and properties but does not provide an implementation. The interface only declares members and does not contain the implementation of the members.

  • Purpose: Used to define the methods and properties that an object should have, but not implement these methods. It allows unrelated classes to implement the same interface and be treated in the same way.

  • Features:

    • Cannot contain data fields.

    • Can contain declarations of methods, properties, indexers, and events.

    • Interface members are public by default.

    • A class can implement multiple interfaces.

    • Interface supports multiple inheritance.

Struct structure

  • Definition: A structure is a value type used to encapsulate small, lightweight objects.

  • Purpose: Suitable for representing data structures, such as point, color, etc.

  • Features:

    • is a value type and allocates memory on the stack.

    • Inheritance is not supported, but interfaces can be implemented.

    • Structure members can be data, methods, events, etc.

    • A structure can have a constructor, but it cannot have a default no-argument constructor.

    • Structures are more lightweight than classes and are suitable for small objects.

    • When you copy a structure, a copy of it is created.

Class class

  • Definition: A class is a reference type that is used to create a template for objects.

  • Purpose: Used to encapsulate data and behavior, which is the basis of object-oriented programming.

  • Features:

    • is a reference type and allocates memory on the heap.

    • Supports inheritance, one class can only inherit from another class.

    • Class members can be data, methods, constructors, events, etc.

    • A class can have multiple constructors.

    • When copying a class object, only the reference is copied.

    • Classes can be declared abstract or sealed.

  • Abstract class:

Definition

An abstract class is a class that cannot be instantiated. It usually serves as a base class, defining and partially implementing common functionality, but also containing at least one abstract method. These abstract methods must be implemented in derived classes.


Features

  • Abstract classes cannot be instantiated directly.

  • Abstract classes can contain abstract methods and concrete methods.

  • Abstract methods are methods that have no implementation, only declarations.

  • Any subclass that inherits from an abstract class must implement all of its abstract methods unless the subclass is also abstract.

  • Abstract classes are often used to provide a basic framework that can be extended and implemented by subclasses.

  • sealed class:

Definition

A sealed class is a class that cannot be inherited. In other words, other classes cannot derive from a sealed class. This is typically used for security and performance optimization.


Features

  • Once a class is declared sealed, it cannot be inherited.

  • Sealed classes can be instantiated and used just like ordinary classes.

  • Sealed classes are mainly used to prevent further inheritance, especially when designing a framework or library, to ensure that core functionality is not modified.

  • In some cases, sealed classes can improve runtime performance because they make certain types of optimizations possible (such as JIT compiler optimizations).


Compare Class and Struct

  • Memory allocation: Struct is a value type and is allocated on the stack; Class is a reference type and is allocated on the heap.

  • Inheritance: Class supports inheritance; Struct does not support inheritance; Interface is used to implement multiple inheritance.

  • Default constructor: Class can have a custom parameterless constructor; Struct always has a default parameterless constructor.

  • Instantiation: Class uses the new keyword when instantiating it; Struct does not need to use the new keyword.

  • Assignment behavior: When Struct is assigned, its value is copied; when Class is assigned, the reference is copied.


Factory Mode


Factory pattern is a creational design pattern that is used to create objects without exposing the logic of object creation to the client. It provides an encapsulation mechanism for creating instances of objects, which can make the system more modular and increase the maintainability and flexibility of the code.


When a class does not know the class of the object it must create, or when a class expects its subclasses to specify the class that creates the object, or when the class delegates the responsibility for creating the object to more than one helper When one of the classes locally wants to hide who the delegate is as information, we can use the factory pattern.


Type


There are three common types of branches in the factory pattern.

  1. Simple Factory Pattern: 23 design patterns that are not part of GoF. It uses a centralized factory class to create all instances.

  2. Factory Method Pattern: Define an interface for creating objects, but let subclasses decide which class to instantiate. Factory methods enable deferring instantiation of a class to its subclasses.

  3. Abstract Factory Pattern: Provides an interface for creating a family of related or dependent objects without explicitly specifying a concrete class.

Code


Taking the Factory Method pattern as an example, suppose we have a factory for creating different types of loggers.


// 抽象产品
public interface ILogger
{
    void Log(string message);
}

// 具体产品
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        // 写日志到文件
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        // 输出日志到控制台
    }
}

// 抽象工厂
public abstract class LoggerFactory
{
    public abstract ILogger CreateLogger();
}

// 具体工厂
public class FileLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        // 返回 FileLogger 实例
        return new FileLogger();
    }
}

public class ConsoleLoggerFactory : LoggerFactory
{
    public override ILogger CreateLogger()
    {
        // 返回 ConsoleLogger 实例
        return new ConsoleLogger();
    }
}

// 客户端代码
class Client
{
    static void Main(string[] args)
    {
        LoggerFactory factory = new FileLoggerFactory();
        ILogger logger = factory.CreateLogger();
        logger.Log("This is a log message.");
    }
}

In this case, LoggerFactory is an abstract factory that defines methods for creating objects.


The specific factory classes are FileLoggerFactory and ConsoleLoggerFactory here, which implement the methods defined above and are used to create specific products (such as FileLogger and ConsoleLogger). The client code only interacts with the abstract factory and product interface, so that adding new types of loggers does not require modification of the client code.

5 views0 comments

Recent Posts

See All
bottom of page