单例模式的引入

在游戏开发中,单例模式会被经常使用。特别是跨模块模块访问时,我们会发现特别的方便。

单例模式的核心就是对象只能创建一个,所以我们在第一次获取对象的时候去创建对象,不是第一次的时候就直接返回第一次访问创建的对象。

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

但是如果每一个想要实现单例模式的类都这样写的话,那么会有很多重复的代码。所以我们可以将上面的代码提取出来作为一个工具类。只要类继承这个工具类,就可以让类拥有单例性质。

因为单例模式不能够通过构造函数去创建,我们可以把它作为一个抽象类。可以通过 “泛型+继承” 拿到子类。

public abstract class Singleton<T> where T : Singleton<T>

这个泛型T一定是子类。

为了让子类T能够被new,我们需要给子类T添加一个约束,必须保证子类有无参构造函数

public abstract class Singleton<T> where T : Singleton<T>, new()

接着我们可以在调用Instance的时候,先判断对象是否被创建,如果没有被创建,则创建子类对象,否则,返回之前创建的对象。

private static T _instance;

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

或者如果知道懒加载Lazy类的话,那么会更加的简单

private static Lazy<T> _instance = new(() => new T());
public static T Instance => _instance.Value;

完整的代码如下

using System;

namespace DYFramework.Util
{
    public abstract class Singleton<T> where T : Singleton<T>, new()
    {
        private static Lazy<T> _instance = new(() => new T());
        public static T Instance => _instance.Value;
    }
}

单例模式的弊端

我们如何去使用单例模式呢?只要继承单例工具类,那么这个类就具备单例模式

public class SingletonExample : Singleton<SingletonExample>{}

我们可以通过SingletonExample.Instance 去获得单例模式

也就是说,任何类都可以通过类.Instance访问单例模式,没有任何的限制。这就意味如果这个单例模式被许多代码引用的话,在其它类不知道的情况下,一些类会破坏这个类的原有状态。如果代码过多的话,就会出现单例模式难以管理。

所以说我们必须要控制单例模式的访问权限。有些模块可以访问,有些模块不能访问。

IOC容器的引入

IOC容器就是由字典构成,key存储类型,value存储实例。通过注册的方式将实例存放在字典中,当访问的时候通过类的类型去字典中找到对应的实例,代码比较简单。

using System;
using System.Collections.Generic;

namespace DYFramework.IOC
{
    public class IocContainer
    {
        private Dictionary<Type, object> _instances = new();

        public void Register<T>(T instance)
        {
            var key = typeof(T);
            _instances[key] = instance;
        }
        
        public T Get<T>() where T : class
        {
            var key = typeof(T);
            if (_instances.TryGetValue(key, out var instance))
            {
                return instance as T;
            }
            return null;
        }
    }
}

测试

接着我们写一个类来测试一下。准备要给BluetoothManager类,将这个类注入到IOC容器中,如果得到的hash值是一样的话,并且打印"蓝牙连接",那么测试成功。

using DYFramework.IOC;
using UnityEngine;

namespace DYFramework.Examples
{
    public class IocExample : MonoBehaviour
    {
        private void Start()
        {
            IocContainer iocContainer = new IocContainer();
            iocContainer.Register(new BluetoothManager());
            Debug.Log( iocContainer.Get<BluetoothManager>().GetHashCode());
            Debug.Log( iocContainer.Get<BluetoothManager>().GetHashCode());
            Debug.Log( iocContainer.Get<BluetoothManager>().GetHashCode());
            iocContainer.Get<BluetoothManager>().Connect();
        }
    }

    public class BluetoothManager
    {
        public void Connect()
        {
            Debug.Log("蓝牙连接");
        }
    }
}

简单示例引入IOC容器

搭建一个场景

准备CountApp容器

using DYFramework.Examples.Model;
using DYFramework.IOC;

namespace DYFramework.Examples
{
    public class CountApp
    {
        private static IocContainer _iocContainer;

        public static void Register<T>(T instance) where T : class
        {
            _iocContainer.Register(instance);
        }

        private static void Init()
        {
            Register(new CountModel());
        }

        public static T Get<T>() where T : class
        {
            if (_iocContainer == null)
            {
                _iocContainer = new IocContainer();
                Init();
            }
            return _iocContainer.Get<T>();
        }

    }
}

准备数据

using DYFramework.BindableProperty;

namespace DYFramework.Examples.Model
{
    public interface ICountModel
    {
        BindableProperty<int> Count { get; set; }
    }
    
    public class CountModel : ICountModel
    {
        public BindableProperty<int> Count { get; set; } = new BindableProperty<int>()
        {
            Value = 0
        };
    }
}

我们给GamePanel添加一个脚本,为了不让CountViewController类代码拥挤,可以使用命令模式。

using DYFramework.Examples.Command;
using DYFramework.Examples.Model;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace DYFramework.Examples
{
    public class CountViewController : MonoBehaviour
    {
        [SerializeField]private Button addBtn;
        [SerializeField]private Button reduceBtn;
        [SerializeField]private TextMeshProUGUI goldText;

        private void Start()
        {
            var countModel = CountApp.Get<CountModel>();
            countModel.Count.OnValueChanged += UpdateGoldUI;
            
            addBtn.onClick.AddListener(() =>
            {
                new AddCommand().Execute();
            });
            
            reduceBtn.onClick.AddListener(() =>
            {
                new ReduceCommand().Execute();
            });
        }

        public void UpdateGoldUI(int count)
        {
            goldText.text = count.ToString();
        }

        private void OnDestroy()
        {
            CountApp.Get<CountModel>().Count.OnValueChanged -= UpdateGoldUI;
        }
    }
}

namespace DYFramework.Command
{
    public interface ICommand
    {
        void Execute();
    }
}
using DYFramework.Command;
using DYFramework.Examples.Model;

namespace DYFramework.Examples.Command
{
    public class AddCommand : ICommand
    {
        public void Execute()
        {
            CountApp.Get<CountModel>().Count.Value++;
        }
    }
}
using DYFramework.Command;
using DYFramework.Examples.Model;

namespace DYFramework.Examples.Command
{
    public class ReduceCommand : ICommand
    {
        public void Execute()
        {
            CountApp.Get<CountModel>().Count.Value--;
        }
    }
}

提取架构

如果每一个模块都要写一遍CountApp类中的方法,就有一点麻烦,其实可以提取

树结构思维

提到树结构,最先想到的就是思维导图。从一个根结点开始,慢慢的向外延申。每一个结点都是一个很好的归纳总结(也可以理解为一个分类)。这么做的好处就是,结构清晰明了。一个大型的项目,有着各种各样的模块,如果每个模块都是分散的,那么项目将是难以管理的。

而树形结构能够很好的梳理模块与模块之间的关系。

如何拥有树结构思维?

对象之间的交互

方法调用

父节点调用子节点可以直接方法调用

什么是对象之间的交互呢?举一个简单的例子,玩家攻击了敌人,敌人受到了伤害。

玩家和敌人就进行了交互,以伪代码来实现的话,应该是这样子的

public class Player{
    public void Attack(Enemy enemy){
      enemy.Wound();
    }
}

public class Enemy {
    private int hp = 5;
    public void Wound(){
        hp--;
    }
}

Player player = new Player();
Enemy enemy = new Enemy();
player.Attack(enemy);

在上面的代码中,我们在Player脚本中的Attack方法中,竟然调用了Enemy类中的方法。这显然有者非常高的耦合度。

因为Player类直接依赖了Enemy的具体实现,如果出现了其他的Enemy,Wound方法的逻辑不一样,就必须修改Player的代码,违背了开闭原则。

而且这种应用场景非常的多,如何去避免和设计就是一门技术了。

就比如说:点击开始按钮,需要隐藏主界面,显示游戏界面

public class GameStartPanel : MonoBehavior{
    public void Hide(){
        this.gameObject.SetActive(false);
    }
}

public class GamePanel: MonoBehavior{
    public void Show(){
        this.gameObject.SetActive(true);
    }
}

public class BtnStartGame : MonoBehavior {
    public GameStartPanel gameStartPanel;
    public GamePanel gamePanel;
    public void Enter(){
        gameStartPanel.Hide();
        gamePanel.Show();
    }

}

委托&回调

一个共识:父节点可以引用子节点,但子节点不能引用父节点。

如果子节点通知父节点用应该这么办?答案是是由委托回调

界面监听子按钮点击事件:一个非常常见的案例就是,当我们在页面中点击关闭按钮,就会隐藏这个页面。

CloseBtn需要通知SettingPanel父节点。只需要让SettingPanel监听CloseBtn按钮即可。

public class SettringPanel : MonoBehaviour
{
    // 通过拖拽的方式,拿到它的子节点
    public Button CloseBtn;
    
    private void Start(){
        CloseBtn.onClick.AddListener(()=> gameObject.SetActive(false););
    }
}

子节点通知父节点用委托或事件

消息&事件

不同模块之间的通信使用事件。

数据与表现分离

交互逻辑和表现逻辑

通过视图影响数据就是交互逻辑。

比如:有一个按钮,每次点击,经验值就加1。点击按钮触发数据的改变就是交互逻辑。

数据改变,视图也会改变就是表现逻辑

比如:每次金币增加,就会出现视图上金币的动态增加。

使用属性,当数据改变时,自动触发表现逻辑

现在有一个按钮,每次点击它时,金币就会+1

using System;

namespace CountApp
{
    public static class GoldModel
    {
        private static int _gold;

        public static Action<int> OnCountChanged;

        public static int Gold
        {
            get => _gold;
            set
            {
                if (_gold != value)
                {
                    _gold = value;
                    OnCountChanged?.Invoke(value);
                }
            }
        }
    }
}

using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace CountApp
{
    public class CountViewController : MonoBehaviour
    {
        [SerializeField]private Button addBtn;
        [SerializeField]private TextMeshProUGUI goldText;

        private void Start()
        {
            GoldModel.OnCountChanged += OnCountChanged;
            
            addBtn.onClick.AddListener(() =>
            {
                // 表现逻辑
                GoldModel.Gold++;
            });
        }

        private void OnCountChanged(int goldNum)
        {
            goldText.text = goldNum.ToString();
        }

        private void OnDestroy()
        {
            GoldModel.OnCountChanged -= OnCountChanged;
        }
    }
}

封装属性

using System;

namespace DYFramework.BindableProperty
{
    public class BindableProperty<T> where T : IEquatable<T>
    {
        public Action<T> OnValueChanged;

        private T _value;

        public T Value
        {
            get => _value;
            set
            {
                if (!_value.Equals(value))
                {
                    _value = value;
                    OnValueChanged?.Invoke(value);
                }
            }
        }

    }
}

使用

using DYFramework.BindableProperty;
namespace CountApp
{
    public static class GoldModel
    {

        public static BindableProperty<int> Gold = new()
        {
            Value = 0
        };
        
    }
}

using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace CountApp
{
    public class CountViewController : MonoBehaviour
    {
        [SerializeField]private Button addBtn;
        [SerializeField]private TextMeshProUGUI goldText;

        private void Start()
        {
            GoldModel.Gold.OnValueChanged += OnCountChanged;
            
            addBtn.onClick.AddListener(() =>
            {
                GoldModel.Gold.Value++;
            });
        }

        private void OnCountChanged(int goldNum)
        {
            goldText.text = goldNum.ToString();
        }

        private void OnDestroy()
        {
            GoldModel.Gold.OnValueChanged -= OnCountChanged;
        }
    }
}

交互逻辑优化

在上面的代码中CountViewController处理数据与视图的交互,如果只是一行代码的话,GoldModel.Gold.Value++;并没有上面大的问题,但是这里面的逻辑特别的复杂又庞大,那么CountViewController类将会变得特别臃肿,如果通过增加方法的方式来扩展功能,随着方法的增加,整体代码将会变得难以维护。

通过命令模式将调用者和执行者分离。

namespace DYFramework.Command
{
    public interface ICommand
    {
        void Execute();
    }
}
using DYFramework.Command;

namespace CountApp
{
    public class AddGoldCommand : ICommand
    {
        public void Execute()
        {
            GoldModel.Gold.Value += 1;
        }
    }
}
using TMPro;
using UnityEngine;
using UnityEngine.UI;

namespace CountApp
{
    public class CountViewController : MonoBehaviour
    {
        [SerializeField]private Button addBtn;
        [SerializeField]private TextMeshProUGUI goldText;

        private void Start()
        {
            GoldModel.Gold.OnValueChanged += OnCountChanged;
            
            addBtn.onClick.AddListener(() =>
            {
                new AddGoldCommand().Execute();
            });
        }

        private void OnCountChanged(int goldNum)
        {
            goldText.text = goldNum.ToString();
        }

        private void OnDestroy()
        {
            GoldModel.Gold.OnValueChanged -= OnCountChanged;
        }
    }
}

数据是项目的底层

自顶向上用委托和事件

自顶向下用方法调用

数据与表现分离的好处

如果换一种表现也是可以实现的,只需要调用数据相关的api即可。也就提高了数据=底层 的复用性。换一个环境也可以使用。

模块化

什么是模块化?

我的理解就是功能独立的业务。比如:登录模块、战斗模块、商店模块。