单例模式的引入
在游戏开发中,单例模式会被经常使用。特别是跨模块模块访问时,我们会发现特别的方便。
单例模式的核心就是对象只能创建一个,所以我们在第一次获取对象的时候去创建对象,不是第一次的时候就直接返回第一次访问创建的对象。
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即可。也就提高了数据=底层 的复用性。换一个环境也可以使用。
模块化
什么是模块化?
我的理解就是功能独立的业务。比如:登录模块、战斗模块、商店模块。
评论区