Unity游戏基本设计模式

一.组合模式

介绍:

将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。

实现:

将不同的功能用不同的脚本实现,然后使用拖拽的方式自由组合,来实现不同的目的。

因为在Unity中,一切物体都可当做组件(包括脚本),这就极有利于实现组合模式。

目的:

提高代码的复用性,降低增加对象成本。

二.单例模式

介绍:

保证一个类只有一个实例,并提供一个·访问它的全局访问点。对于需要记录文件每一次修改状态的系统来说,只有一个实例非常重要,否则多个实例可能会记录多个状态,系统就会失去记录和监督的作用。

实现:

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

说明:为了便于各个系统都能访问此类数据,需要它是一个全局变量,故使用static将实例静态化,使用关键词实现其全局访问功能。

扫描二维码关注公众号,回复: 14796429 查看本文章
public static Singleton Instance{get; private set;}

优点:

1.内存里只有一个实例,减少了内存的开销,不用经历频繁的实例创建和销毁,节省性能。

2.通过单例,模块之间可以无条件互相访问数据,轻松实现链接游戏的各个模块。

缺点:

1.代码耦合度增加,维护困难

比如:若某一单例中的数据需要修改,但是由于单例过多使用,与此单例链接的文件数量众多,需要查找每一个可能修改了此数据的文件,导致维护难度增加。

2.扩展困难

在单例模式中,若要进行扩展则只能修改原本类中的代码,除此之外不存在第二种方法。使扩展功能变得困难。

三.命令模式

介绍:

将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,将请求排队或记录,以及支持可撤销操作。

实现(以物体移动为例):

若将输入命令的模块和物体移动的模块全部写在同一类中,则两模块就会发生耦合。

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

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

1.创造一个基类

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

2.实现前后左右移动的子类

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

依照以上方法,封装其他三个移动方向。

3.实例化这四个类

private readonly MoveForWard _moveForWard = new MoveForWard();

4.重新修改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());
    }
}

此时,在InputHandler模块中只负责输入,而动作处理则被放在了另一模块中,实现了模块间的解耦

5.实现撤销功能

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

6.在子类中实现撤销功能

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.用List来记录发出的命令

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();
    }
	
}

四.委托,Event,Action,Func的概念讲解与解析

介绍:

四者关系图

Delegate委托:

首先需要定义函数模板,所有符合此类模板的函数都可以通过+=来进行注册,当执行容器时,容器内的所有方法都会执行。

//定义一个函数模板
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);
    }
}

但是委托存在风险:它可以被直接赋值,这样就会导致,容器内的函数存在丢失的风险。假设delegate1中含有多个函数,但是delegate2中没有函数,若将delegate2直接赋值给delegate1,则会导致delegate1的注册函数被清空。此时可以使用Event。

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

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

Event可以使delegate的直接赋值权限变为private,其他功能不变。只需要在定义前加上even的关键词t就可以。

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

Action:Delegate的简写版,与delegate没有区别,同时,Action最多支持16个参数的模板

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

Functionn:可以得到函数的返回值,其括号中最后一个类型就是返回值类型

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;
    }
}

五.观察者模式

介绍:

当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。

优点:

1.起到解耦的作用;

2.建立一套触发机制;

缺点:

1.如果一个被观察者对象有很多的直接和间接的观察者的话,通知到所有的观察者会花费很多时间;

2.如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃;

3.观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。

为什么需要用到观察者模式:

在一般的游戏设计中,常常存在这样的情况,当玩家HP = 0时,需要通过if条件判断来执行一系列操作

但是这样的话,我们就会将五个独立的系统捆绑在一起,并且如果有一处引用丢失,就会出现bug,这样查找引用的写法也比较麻烦。

上述问题解决办法:

我们可以创建一个死亡事件,当玩家HP = 0 时,执行这个死亡事件。然后我们将每一个需要对玩家死亡产生反应的系统确定为观察者,因为这些系统的行为是通过观察玩家是否死亡来进行的,所以我们可以通过注册的方式,将这些行为与角色死亡这个事件绑定在一起。每当角色事件触发时,与该事件绑定在一起的其他事件同步触发。这样的话,在代码层面,每个系统都在该系统内部执行自己的内部事件,而在执行层面,每个系统都很好的协同了起来。

六.对象池模式

介绍:

当我们需要经常频繁的创建和销毁某些元素时,可预先定义一个包含可重用对象的池子,在初始化池子的时候就创建好对象并全设置为非激活状态,当我们需要创建物体时,激活池子中的对象,当我们要销毁物体时,就将物体的状态设置为非激活。

优点:

与传统的反复创建和销毁物体相比,对象池模式在CPU和内存的消耗上会有性能的极大提升

为什么会这种区别:

1.当我们使用传统的创建方法时,系统会根据大小为我们分配对应的内存,但由于创建是不连续的,分配的内存也是不连续的,当我们销毁其中的部分内存空间时,就会空出来一部分的内存空间,如果这些空出来的空间没有合适的新任务进行占用(新任务的内存大小和空出来的内存空间不适配),就会产生内存碎片,随着内存碎片的增加,占用的无效内存就会越来越多。而对象池则是一次性分配好一长串内存,不会产生内存碎片。

2.创建和销毁操作的计算量是远远大于单纯的激活和失活物体的,所以当创建和销毁的物体达到一定的规模时,在游戏性能上的开销就会变得十分巨大。

实现(以实现角色跑动拖影为例):

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;
    }
}

猜你喜欢

转载自blog.csdn.net/m0_63673681/article/details/128002499
今日推荐