Unity game basic design pattern

1. Combination mode

introduce:

Group objects into tree structures to represent "part-whole" hierarchies. Composite mode enables users to use single objects and composite objects with consistency.

accomplish:

Use different scripts to implement different functions, and then use drag and drop to freely combine to achieve different purposes.

Because in Unity, all objects can be used as components (including scripts), which is very beneficial to realize the combination mode.

Purpose:

Improve the reusability of code and reduce the cost of adding objects.

2. Singleton mode

introduce:

Ensure that there is only one instance of a class, and provide a global access point to access it. For systems that need to record the status of each file modification, only one instance is very important, otherwise multiple instances may record multiple states, and the system will lose its role of recording and supervision.

accomplish:

private void Awake(){
    //If there is an instance, and it's not me, delete
    
    if(Instance != null && Instance != this){
        Destroy(this);
    }
    else{
        Instance = this;
    }
}

Explanation: In order to make it easier for each system to access this type of data, it needs to be a global variable, so use static to make the instance static, and use keywords to realize its global access function.

public static Singleton Instance{get; private set;}

advantage:

1. There is only one instance in the memory, which reduces the memory overhead, does not need to experience frequent instance creation and destruction, and saves performance.

2. Through the single case, the modules can unconditionally access each other's data, and it is easy to link the various modules of the game.

shortcoming:

1. Increased code coupling and difficult maintenance

For example: If the data in a certain singleton needs to be modified, but due to the excessive use of singletons, there are many files linked to this singleton, and it is necessary to find every file that may have modified this data, which will increase the difficulty of maintenance.

2. Difficulty in scaling

In the singleton mode, if you want to expand, you can only modify the code in the original class, and there is no second method other than this. Make it difficult to extend functionality.

3. Command mode

introduce:

Encapsulates requests as an object, allowing you to parameterize clients with different requests, queue or log requests, and support undoable operations.

Implementation (taking object movement as an example):

If the module for inputting commands and the module for object movement are all written in the same class, then the two modules will be coupled.

private void PlayerInputHandler(){
    if(Input.GetKeyDown(KeyCode.W)) MoveForWard();
}

private void MoveForWard(){
    player.transform.Translate(Vector3.forward);
}

1. Create a base class

public abstract class Command{
    //此方法中的参数是我们要控制的物体
    public abstract void Execute(GameObject player);
}

2. Implement subclasses that move forward, backward, left, and right

public class MoveForWard : Command{
    private GameObject _player;
    //实现父类的Execute方法
    public override void Execute(GameObject player){
        _player = player;
        player.transform.Translate(Vector3.forward);
    }
}

According to the above method, encapsulate the other three moving directions.

3. Instantiate these four classes

private readonly MoveForWard _moveForWard = new MoveForWard();

4. Re-modify PlayerInputHandler

private void PlayerInputHandler(){
    if(Input.GetKeyDown(KeyCode.W)){
        //此行用于传入移动物体
    	_moveForWard.Execute(_playerCube);
        //此行用于记录指令
        CommandManager.Instance.AddCommands(_moveForWard);
	}

	//实现所有操作撤销
    if(Input.GetKeyDown(KeyCode.B)){
        StartCoroutine(CommandManager.Instance.UndoStart());
    }
}

At this time, the InputHandler module is only responsible for input, while the action processing is placed in another module to achieve decoupling between modules

5. Implement the undo function

public abstract class Command{
    //此方法中的参数是我们要控制的物体
    public abstract void Execute(GameObject player);
    //实现撤销
    public abstract void Undo();
}

6. Implement the undo function in the subclass

public class MoveForWard : Command{
    private GameObject _player;
    //实现父类的Execute方法
    public override void Execute(GameObject player){
        _player = player;
        player.transform.Translate(Vector3.forward);
    }
    //反向移动,实现撤销功能
    public override void Undo(){
        player.transform.Translate(Vector3.back);
    }
}

7. Use List to record the commands issued

public class CommandManger : MonoBehaviour
{
	public static CommandManager Instance;
	private readonly List<Command> _commandList = new List<Command>();

	private void Awake()
	{
    	if(Instance) Destroy(Instance);
    	else Instance = this;
    }

	public void AddCommands(Command command)
	{
    	_commandList.Add(command);
    }

	public IEnumerator UndoStart()
	{
    	//将List中的命令全部倒转,来实现正常的撤销顺序
    	_commandList.Reverse();
    	foreach(Command command in _commandList)
		{
        	yield return new WaitForSecond(.2f);
        	//执行Undo撤销操作
        	command.Undo();
        }
    	_commandList.Clear();
    }
	
}

4. Explanation and analysis of the concepts of entrustment, Event, Action, and Func

introduce:

Relationship between the four

Delegate entrusts:

First, you need to define a function template. All functions conforming to this type of template can be registered through +=. When the container is executed, all methods in the container will be executed.

//定义一个函数模板
public DelegateDemo DelegateDemo(int a);

public class GameManager : Singleton<GameManager>{
	//实例化模板
	public DelegateDemo DelegateDemo; 
    
    void Start(){
        //注册
        DelegateDemo += DebugSomething;
        //通过Invoke执行,输出结果为1
        DelegateDemo ?.Invoke(1);
	}
	//创建符合模板的方法
    public void DebugSomething(int a){
        Debug.Log(a);
    }
}

But there is a risk in delegation: it can be directly assigned, which will lead to the risk of loss of functions in the container. Assuming that delegate1 contains multiple functions, but delegate2 has no functions, if delegate2 is directly assigned to delegate1, the registered functions of delegate1 will be cleared. Event can be used at this time.

//在另一个文件中创建一个空委托,并通过单例赋值给上面创建的委托
//此时进行Invoke,则没有输出,因为第一个容器内的函数被清空了
public class PlayerController : Monobehavvoiur{
    private DelegateDemo _demo2;

    private void Start(){
        GameManager.Instance.DelegateDemo = _demo2;
    }
}

Event can make the direct assignment permission of the delegate private, and other functions remain unchanged. Just add the even keyword t before the definition.

//为防止容器被覆盖,使用event实例化模板
public event DelegateDemo DelegateDemo;
private void Start(){
    //若使用event,则这句语句会报错
    //因为赋值权限变成了private,不可跨文件直接赋值
    GameManager.Instance.DelegateDemo = _demo2;
}

Action: Shorthand version of Delegate, which is no different from delegate. At the same time, Action supports templates with up to 16 parameters

//public DelegateDemo DelegateDemo;
//使用Action
public Action<int> DelegateDemo;

Functionn: You can get the return value of the function, and the last type in the brackets is the return value type

public class GameManager : singleton<GameManager>{
    public Func<int, int> funDemo;

    void Start(){
        //注册多个函数,返回值取最后一个注册的
        funDemo += DebugSomething2;
        funDemo += DebugSomething;
        //最终输出的结果为1
        Debug.Log(funDemo.Invoke(1));
    }

    public int DebugSomething(int a){
        return 1;
    }
    public int DebugSomething2(int a){
        return 2;
    }
}

Five. Observer mode

introduce:

When there is a one-to-many relationship between objects, the observer pattern (Observer Pattern) is used. For example, when an object is modified, objects that depend on it are automatically notified. The Observer pattern is a behavioral pattern.

advantage:

1. Play the role of decoupling;

2. Establish a trigger mechanism;

shortcoming:

1. If an observed object has many direct and indirect observers, it will take a lot of time to notify all observers;

2. If there is a circular dependency between the observer and the observation target, the observation target will trigger a circular call between them, which may cause the system to crash;

3. The observer mode has no corresponding mechanism to let the observer know how the observed target object changes, but only knows that the observed target has changed.

Why use the Observer pattern:

In general game design, there is often such a situation, when the player's HP = 0, it is necessary to perform a series of operations through the if condition judgment

But in this case, we will bundle five independent systems together, and if a reference is missing, a bug will appear, so it is more troublesome to find the reference.

Solutions to the above problems:

We can create a death event that will be executed when the player's HP = 0. Then we identify every system that needs to respond to the player's death as an observer, because the behavior of these systems is carried out by observing whether the player is dead, so we can bind these behaviors to the event of the character's death by registering set together. Whenever a character event fires, other events bound to that event fire synchronously. In this way, at the code level, each system executes its own internal events within the system, and at the execution level, each system is well coordinated.

Six. Object pool mode

introduce:

When we need to create and destroy certain elements frequently, we can pre-define a pool containing reusable objects. When the pool is initialized, the objects are created and all are set to inactive. When we need to create objects, activate The object in the pool, when we want to destroy the object, set the state of the object to inactive.

advantage:

Compared with the traditional repeated creation and destruction of objects, the object pool mode will greatly improve the performance of CPU and memory consumption

Why this difference:

1. When we use the traditional creation method, the system will allocate corresponding memory for us according to the size, but since the creation is discontinuous, the allocated memory is also discontinuous. When we destroy part of the memory space, it will A part of the memory space is vacated. If the vacated space is not occupied by suitable new tasks (the memory size of the new task does not match the vacated memory space), memory fragmentation will occur. With the increase of memory fragmentation, More and more invalid memory will be occupied. The object pool allocates a long list of memory at one time without memory fragmentation.

2. The calculation of creating and destroying operations is far greater than simply activating and deactivating objects, so when the created and destroyed objects reach a certain scale, the overhead in game performance will become very large.

Implementation (take the implementation of character running smear as an example):

public class ShadowPool : MonoBehaviour
{
    public static ShadowPool instance;
	//要重复产生物体的预制体
    public GameObject shadowPrefab;
	//产生的数量
    public int shadowCount;
	//用队列的方式存储物体
    private Queue<GameObject> availableObjects = new Queue<GameObject>();

    void Awake()
    {
        instance = this;

        //初始化对象池
        FillPool();
    }

    public void FillPool()
    {
        for (int i = 0; i < shadowCount; i++)
        {
            var newShadow = Instantiate(shadowPrefab);
            newShadow.transform.SetParent(transform);

            //取消启用,返回对象池
            ReturnPool(newShadow);
        }
    }

    public void ReturnPool(GameObject gameObject)
    {
    	//将物体设为非激活状态,以作销毁
        gameObject.SetActive(false);
    	//向队列为添加一个元素
        availableObjects.Enqueue(gameObject);
    }

    public GameObject GetFromPool()
    {
        if (availableObjects.Count == 0)
        {
            FillPool();
        }
    	//从队列头取出一个元素
        var outShadow = availableObjects.Dequeue();

        outShadow.SetActive(true);

        return outShadow;
    }
}

Guess you like

Origin blog.csdn.net/m0_63673681/article/details/128002499