0%

Unity游戏设计模式

10.25目标

单例模式

懒汉式与饿汉式

Unity中一般也就懒汉式,因为本来就是单程,很难出现线程不安全

原理:

  • static静态属性——不会被GC,存在类型对象中而不是实例化出来的对象中
  • 私有构造函数、私有静态对象、公有静态函数或属性——方便其他对象获取

 

不过我在写这篇文章前写的倒是并不像这样规范

public class Singleton : MonoBehaviour
{
    public static Singleton instance;
    private void Awake()
    {
        instance = this;
    }
}

这个单例继承于MonoBehaviour,是挂载在对象上的,在对象初始化的时候便instance=this,意味着这个单例的初始化是挂载到unity的生命周期上的。

 

比较规范化的饿汉式单例模式

public class Singleton
{
    private Singleton() { }
    private static Singleton instance;
    public static Singleton Instance 
    {
        get
        {
            if (instance == null)
            {
                instance = new Singleton();
            }
            return instance;
        }
    }
}

为什么说是饿汉式,因为没有继承于MonoBehaviour,需要被继承过MonoBehaviour的对象调用过之后才算真正创建了,如果需要线程安全,那就在公有对象的地方加锁,但是其实unity没啥必要。

常在Get区域DontDestroyOnLoad(obj)避免单例被场景变换而销毁

 

通用单例类

实际运用过程中其实会用到非常非常多单例类,什么对象池什么商店类什么玩家类,可以把单例操作简化到一个类中来避免重复编写——但其实也没啥必要,看起来更好看罢了

namespace Common
{
public class Singleton<T> where T : class, new()
{
    private static T _instance;
    private static readonly object syslock = new object();

public static T Instance
{
    get
    {
        if (_instance == null)
        {
            lock (syslock)
            {
                if (_instance == null)
                {
                    _instance = new T();
                }
            }
        }
        return _instance;
    }
}

}
}

public class UseSingleton : Singleton<UseSingleton>
{
    public string Name;
    public int Age;
}

缓冲池优化(留存?)

 

事件中心

观察者模式、事件发布-订阅模式、Unity自带的UnityEvent

游戏游玩过程中会触发非常多的事件,这些事件往往关联非常多的游戏对象与游戏系统,单独其设置逻辑会造成代码的高耦合,例如“让对象A触碰到对象B“会发生一些小事件,这个时候”对象A“与”对象B“与”事件对象“就会形成一个比较尴尬的局面——相互引用。如果两个对象只是单例那可能还算清晰,但更多情况下游戏的事件要么是”击杀n个敌人“这种带有多个实例化对象的事件、要么是”玩家死亡“这种比较单纯的事件等等。。事件错综繁杂而难以理清,暴力地相互引用必然是不正当的做法,这个时候我们需要布置一些”特殊的岗位“来特定地办这一系列事。

最典型的能描述代码中事件中心的就是游戏的成就系统或者图鉴系统

 

观察者模式

在进入正题之前,我们最好先看准我们的需求,然后从需求中一点点迭代到代码规格当中。

我们首先最需要的,就是实现

  • 对象发生一些”事件“的时候,不需要去管理自己与其他对象需要做什么

就像玩手机一样,我们身体发出了”肚子饿“的指令,那么我们不需要去特地做饭,而是打开手机点外卖;当我们想要找人聊天的时候,我们不需要特地去朋友家中,而是直接打开聊天软件。。。明明是两者截然不同的的事件,发生的对象也不一样,但我们只需要使用手机(事件系统)就能解决。

于是从上面的例子我们渐渐有了灵感

  • 对象发生一些”事件“的时候,只需要向外界发出信息就行

这样的”信息“在C#中,就像生活中各自事件一样,是完全不一样的类型的,我们需要将这些信息,或者说发出信息的对象归并到一个列表当中——Subject,也可以叫做”抽象被观察者“。对应的,Observer就是”抽象观察者“。”对象发出事件让观察者接收“这个事情,完全可以统一归一为“抽象被观察者发出信息让抽象观察者来订阅”。

  • 观察者接收到信息的时候,需要知道信息的来源

“抽象被观察者”需要持有抽象观察者的列表,这样一来就可以让“被观察者”来决定消息发布给谁。而消息要如何发布呢?我们其实完全可以将这个“列表”直接定义为一个接口,然后让程序自己来决定将消息发布给谁,就像面向对象中的多态一样——毕竟一个个定义列表这件事是极其荒谬的。同样,被观察者也需要拥有统一接口——添加观察者,移除观察者,以及通知观察者

  • 观察者接收到信息的时候,进行一些事件实现

按照上面的例子,我们可以将“抽象观察者”定义为一个总的接口,然后具体实现由其他观察者来实现,可以理解成面向对象中的“多态”,在具体运行过程中,由程序来决定运行哪一个实现。

 

这样一来,对象A(抽象被观察者)就不需要进行纷乱繁杂的引用,它直接持有一个抽象观察者列表,并在发起通知时直接遍历抽象者列表中的Update。

public class Player : ISubject{
    private List<IObeserver> observers=new List<IObeserver>();
    ...
}

其实做到现在还是抽象太多了,哪怕是我自己,在刚得知这种设计模式时仍然会感到疑惑——这种看起来更麻烦复杂的方式为什么就能解耦呢。

让我们回到之前的需求中来,我们会发现至少作为一个对象,消息处理机制已经得到了规范——也就是说实现了“发生事件时提供消息”以及“消息到来时调用的方法和对象本身没有关系“

 

发布-订阅模式

想要将观察者与被观察者完全解耦,那就必须要设置一个调度中心来统一对”观察者、被观察者、消息“进行管理。

和之前的玩手机一样的原理:如果你点外卖,那么理论上你和店家是不存在交互关系的,因为你只和外卖平台在进行交互,而店家也只是在处理外卖平台提供的订单通知而已。

观察者接口:

public interface IObserver 
{
    public void ResponseToNotify();
}

“调度中心”:

public class GameManager 
{
    //单例模式应用
    private static GameManager instance;
    public static GameManager Instance
    {
        get
        {
            if (instance == null)
                instance = new GameManager();
            return instance;
        }
    }
    private List<IObserver> observers = new List<IObserver>();
    //添加观察者
    public void AddObserver(IObserver observer)
    {
        observers.Add(observer);
    }
    //移除观察者
    public void RemoveObserver(IObserver observer)
    {
        observers.Remove(observer);
    }
    //发送通知给观察者
    public void Notify()
    {
        for (int i = 0; i < observers.Count; i++)
        {
            observers[i]?.ResponseToNotify();
        }
    }
}

玩家死亡的时候:

public class NewPlayer : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.J))
        {
            GameManager.Instance.Notify(); //触发玩家死亡通知
        }
    }
}

敌人接收事件消息:

public class NewEnemy :MonoBehaviour, IObserver
{
    private void Start()
    {
        GameManager.Instance.AddObserver(this);
    }
    private void OnDestroy()
    {
        GameManager.Instance.RemoveObserver(this);
    }
    public void ResponseToNotify()
    {
        print($"{gameObject.name}停止移动");
    }
}

UI接收消息:

public class NewGameOverUI : MonoBehaviour,IObserver
{
    public void ResponseToNotify()
    {
        print("游戏结束");
    }
    void Start()
    {
        GameManager.Instance.AddObserver(this);
    }
    private void OnDestroy()
    {
        GameManager.Instance.RemoveObserver(this);
    }
}

我们会惊喜地发现这种模式确确实实实现了对象只需要发布消息就能得到回馈的功能,而且对象间不需要进行引用,只需要对调度中心进行交互即可,哪怕是不用的观察者也是一样的。

代码借鉴于观察者模式(结合C#,Unity)_unity 观察者模式-CSDN博客

 

观察者的生命周期

在上面的例子中,观察者定义了事件发生后的事情,但这个条件是由调度中心全权来操作的。

按照我们以往的编码流程,“自己的事情自己做”就是理所应当的,所以函数的条件与实现都会放在一起。

在这个模式中我们将他分开了,那么就有可能会产生一个严重的后果——调度中心尝试操作一个未知状态的观察者。

毕竟上面流程的代码是没有添加任何安全锁,对于调度中心来说,为了解耦,它甚至不用理会观察者到底是上面,因为他只是不停地在遍历接口列表而已。

于是我们设想一个很诡异的场景:观察者因为场景变化或者其他因素消失了,但调度中心列表中仍然存在该观察者的接口,于是调度中心一次又一次地按下那个“虚空”操作按钮。。。

所以最好的解决方案就是将观察者的控制权与其生命周期相匹配——也就是当观察者销毁的时候及时将其移出观察者列表。

  • 在 Awake/Start 方法中把观察者添加到列表中,在 OnDestroy 方法中把观察者从列表中移除。
  • 在 OnEnable 方法中把观察者添加到列表中,在 OnDisable 方法中把观察者从列表中移除。

 

UIManage

 

DOTween

 

 

UI的状态栈

 

 

面向对象设计原则