Unity简单程序框架
Unity设计模式
单例基类
像是一些xxManager,xxController等等的管理器,控制器,一般都会选择成为单例。为了减少代码量,可设计出一个基类以供继承使用。单例基类分为无继承与继承MonoBehaviour多种
无继承
//确保T具有一个无参构造函数以供new使用
public class BaseSingleton<T> where T : new()
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
_instance = new T();
return _instance;
}
}
}
继承MonoBehaviour
实际使用中,可以不手动将脚本挂载在物体上,让单例被首次调用时,由代码自行创建
public class BaseSingletonWithMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance = null;
public static T Instance
{
get
{
//可能已经挂载到场景中,先查找
if (_instance == null)
_instance = FindObjectOfType<T>();
//查找不到,则自行创建一个
if (_instance == null)
{
GameObject obj = new GameObject(typeof(T).ToString());
_instance = obj.AddComponent<T>();
}
return _instance;
}
}
}
继承ScritableObject(待更新)
其实UnityEditor
中时是有该类的,叫做ScriptableSingleton<T>
,使用方法如下
//写一个GameSetting的测试类
using UnityEditor;
[CreateAssetMenu(menuName = "ScriptableSingleton/GameSetting")]
public class GameSetting : ScriptableSingleton<GameSetting>
{
public string DefaultPlayerName;
}
这个时候若我们在项目中中创建多个GameSetting
,则编译器将会报错
但是问题没有这么简单,如果我们不手动在场景中创建,而是通过代码间接操作
using UnityEditor;
public class TestLoadSO
{
[MenuItem("Tools/Read")]
public static void Read()
{
Debug.Log(GameSetting.instance.DefaultPlayerName);
}
[MenuItem("Tools/Write")]
public static void Write()
{
GameSetting.instance.DefaultPlayerName = "Player555";
}
}
然后在上方栏目中点击Tools/Write,再点击Tools/Read
结果符合我们的预期,但是这个时候我们关闭,(不管你有没有Save)重新进入后点Tools/Read,会发现打印出来的是空,因为Unity内部创建的ScriptableSingleton<T>
是不会帮我们保存的
//检测到instance为null时会执行到的一部分代码
ScriptableObject.CreateInstance<GameSetting>().hideFlags = HideFlags.HideAndDontSave;
那么这个时候我们再自己手动创建一个GameSetting
,编译器仍然会报错
故在使用Unity编辑器自带的单例SO时,最好保证你的ScriptableObject已经事前被你创建出来
下面提供两种自行写单例基类的代码
第一种
public class ScriptableObjectSingleton<T> : ScriptableObject where T : ScriptableObject
{
private static T _instance = null;
public static T Instance
{
get
{
if (_instance == null)
{
T[] _instances = Resources.FindObjectsOfTypeAll<T>();
if (_instances.Length == 1)
_instance = _instances[0];
else if (_instances == null || _instances.Length == 0)
Debug.LogError($"You need to create at least one {typeof(T)}");
else
Debug.LogError($"{typeof(T)} has more than one entity!");
}
return _instance;
}
}
}
使用这种做法的话,我们需要在场景中创建一个挂在着类似SingletonScriptableObjectReference脚本的空物体,然后再在脚本中引用单例ScriptableObject,以供查找
第二种
public class ScriptableObjectSingleton<T> : ScriptableObject where T : ScriptableObject
{
private static T _instance = null;
public static T Instance
{
get
{
if (_instance == null)
{
string[] findAssets = AssetDatabase.FindAssets(typeof(T).Name);
if (findAssets == null || findAssets.Length == 0)
Debug.LogError($"You need to create at least one {typeof(T)}");
else if (findAssets.Length > 1)
Debug.LogError($"{typeof(T)} has more than one entity!");
else
_instance = AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(findAssets[0]));
}
return _instance;
}
}
}
对象池
在FPS游戏中,子弹是一个频繁被创建,频繁被销毁的物体。不断的重复创建销毁,会加快GC,导致内存回收时更频繁的卡顿。对于这种类型的物体,我们可以创建一个对象池,在用户需要的时候检测池中是否含有该对象,若有则拿出来用,若没有则在池中创建一个新的。在完成其功能后,我们将其重新投入池中而不是Destroy
掉。
举个例子,把一个衣柜看作是对象池,当我们外出时则从衣柜中取出衣服穿上,若发现衣柜里头已经没衣服穿了,就需要买新衣服加进衣柜里。忙完一天回家后,再把衣服脱下放回到衣柜中。(不会有人一天换一件衣服用完就直接扔掉吧不会吧不会吧)
当然了衣柜也可以有很多个抽屉,一个抽屉放外套,一个抽屉放裤子...也就是说一个对象池其实可以有一个List
,存放各种不同的物体
除了子弹之外,棋牌类游戏中的牌,又或者是图片集中的图片等各种会重复创建同一类型的物体的情况,都可以使用到对象池。以下是一种简单对象池的实现
public class PoolData
{
public GameObject dataParents;
public List<GameObject> dataList;
public PoolData(GameObject gameObject, GameObject poolObj)
{
dataParents = new GameObject($"{gameObject.name}s");
dataList = new List<GameObject>();
dataParents.transform.parent = poolObj.transform;
PushGameObject(gameObject);
}
public GameObject PopGameObject()
{
//取出后重置父节点
GameObject popOne = dataList[0];
popOne.transform.parent = null;
dataList.RemoveAt(0);
return popOne;
}
public void PushGameObject(GameObject gameObject)
{
//放入后归位
dataList.Add(gameObject);
gameObject.transform.parent = dataParents.transform;
}
}
[CreateAssetMenu(fileName = "Frame/PoolManger")]
public class PoolManager : ScriptableObjectSingleton<PoolManager>
{
[SerializeField] private List<GameObject> presInpool; //池子中的预制体
[SerializeField] private string poolName = "MyPool"; //总节点Name
private GameObject poolObj; //总节点GameObject
private Dictionary<string, PoolData> poolDic = new Dictionary<string, PoolData>(); //池中对象
public GameObject PopGameObject(string objectName, Transform trans)
{
GameObject popOne = null;
//可在类内初始化物体坐标,或返回GameObject后再修改
if (poolDic.ContainsKey(objectName) && poolDic[objectName].dataList.Count > 0)
popOne = poolDic[objectName].PopGameObject();
else
popOne = (GameObject)Instantiate(FindInList(objectName), trans.position, trans.rotation);
if (popOne)
{
//重设物体的名字,活跃状态
popOne.name = objectName;
popOne.SetActive(true);
}
return popOne;
}
public void PushGameObject(GameObject gameObject)
{
//检测总节点是否为空
if (poolObj == null)
poolObj = new GameObject(poolName);
gameObject.SetActive(false);
//存在则直接放入,若不存在则先创建再放入
if (poolDic.ContainsKey(gameObject.name))
poolDic[gameObject.name].PushGameObject(gameObject);
else
poolDic.Add(gameObject.name, new PoolData(gameObject, poolObj));
}
public void ClearPool()
{
//切换场景时主动调用,清空对象池
poolDic.Clear();
poolObj = null;
}
public GameObject FindInList(string objectName)
{
//寻找池中是否存在需要创建的对象
foreach (var preInpool in presInpool)
{
if (preInpool.name.Equals(objectName))
return preInpool;
}
return null;
}
}
中心事件管理模块
作为一个单例管理模块,集各种事件于一体。接受无参与单参数的无返回值事件,通过调用Raise
实现事件调用
interface IEventInfo {}
public class EventInfo<T> : IEventInfo
{
public EventInfo(UnityAction<T> action)
{
actions += action;
}
public UnityAction<T> actions;
}
public class EventInfo : IEventInfo
{
public EventInfo(UnityAction action)
{
actions += action;
}
public UnityAction actions;
}
public class CenterEvent : BaseSingletonWithMono<CenterEvent>
{
//储存所有事件
private Dictionary<string, IEventInfo> eventDir = new Dictionary<string, IEventInfo>();
public void AddListener<T>(string eventName, UnityAction<T> action)
{
//有参版本
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo<T>).actions += action;
else
eventDir.Add(eventName, new EventInfo<T>(action));
}
public void AddListener(string eventName, UnityAction action)
{
//无参版本
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo).actions += action;
else
eventDir.Add(eventName, new EventInfo(action));
}
//RemoveListener一般在OnDestroy中调用
public void RemoveListener<T>(string eventName, UnityAction<T> action)
{
//有参版本
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo<T>).actions -= action;
}
public void RemoveListener(string eventName, UnityAction action)
{
//无参版本
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo).actions -= action;
}
public void Raise<T>(string eventName, T info)
{
//如果存在,则依次调用所有事件
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo<T>).actions?.Invoke(info);
}
public void Raise(string eventName)
{
//如果存在,则依次调用所有事件
if (eventDir.ContainsKey(eventName))
(eventDir[eventName] as EventInfo).actions?.Invoke();
}
public void Clear()
{
//跳转场景前手动调用清除不必要的内存占用
eventDir.Clear();
}
}
基脚事件管理模块(待更新)
基脚与中心的区别就是基脚将一个单例要干的事情拆分成很多个小模块去分配,由多个ScriptableObject去替换掉中间的大单例
主要思想就是把一个一个事件做成ScriptableObject,然后在场景中创建空物件,最后把各个监听器挂载在空物件上
//VoidGameEvent.cs
[CreateAssetMenu(menuName = "GameEvent/Void")]
public class VoidGameEvent : ScriptableObject
{
public UnityAction unityAction;
public void Raise()
{
unityAction?.Invoke();
}
}
//VoidEventListener.cs
public class VoidEventListener : MonoBehaviour
{
public VoidGameEvent voidGameEvent;
public UnityEvent unityEvent;
public void OnEnable()
{
if (voidGameEvent == null)
return;
voidGameEvent.unityAction += Respond;
}
public void OnDisable()
{
if (voidGameEvent == null)
return;
voidGameEvent.unityAction -= Respond;
}
public void Respond()
{
unityEvent?.Invoke();
}
}
公共Mono模块
使类虽然没有继承MonoBehaviour,但是仍可以通过往模块中添加事件,可实现Update,协程等功能
public class MonoEvent : BaseSingletonWithMono<MonoEvent>
{
private UnityAction UpdateEvent;
private void Update()
{
if (UpdateEvent == null)
return;
UpdateEvent.Invoke();
}
public void AddUpdateEvent(UnityAction _action) { UpdateEvent += _action; }
public void RemoveUpdateEvent(UnityAction _action) { UpdateEvent -= _action; }
}
//在其他类中调用实例
MonoEvent.Instance.AddUpdateEvent(PrintHello);
MonoEvent.Instance.StartCoroutine(SayHello());
资源加载模块
先讲解一下使用Resources.Load
的一些规则。使用该方式加载资源,需要将资源存放在Asset目录下的Resources文件夹(需自行新键)中
//限定类型调用方法 二级路径也可以使用Cube直接读取
Resources.Load<GameObject>("Cube");
Resources.Load("Cube", typeof(GameObject));
若文件处于二级目录,例如位于Resources/Bullet文件夹中,则需传入的字符串为:"Bullet/Cube"
。需要注意的是,Resources文件夹并不是一定要处于Asset目录的下一级,即使是Asset/Res/Resources这样的二级路径,也能被Resources.Load
识别到
这里讲一下UnityEngine.Object
与System.Object
的区别。若使用大写的Object
且同时引用了两个名称空间,则会发生冲突,UnityEngine.Object是Unity中所有Component以及GameObject的父类,然后UnityEngine.Object
又继承自System.Object
。小写的object
默认是System.Object
,不会发生冲突
public class ResourcesLoader : BaseSingleton<ResourcesLoader>
{
/// <summary>
/// 预设列表 储存资源镜像
/// </summary>
private Hashtable resCache;
public ResourcesLoader()
{
resCache = new Hashtable();
}
/// <summary>
/// 同步加载资源
/// </summary>
/// <param name="objectPath">资源路径</param>
/// <param name="isCache">是否将资源缓存</param>
/// <typeparam name="T">资源类型</typeparam>
/// <returns>资源实体</returns>
public T Load<T>(string objectPath, bool isCache = false) where T : UnityEngine.Object
{
T res = null;
if (resCache.ContainsKey(objectPath))
res = resCache[objectPath] as T;
else
{
res = Resources.Load<T>(objectPath);
if (isCache)
resCache.Add(objectPath, res);
}
if (res is GameObject)
return GameObject.Instantiate(res);
else
return res;
}
/// <summary>
/// 异步加载资源
/// </summary>
/// <param name="objectPath">资源路径</param>
/// <param name="callBack">函数回调</param>
/// <typeparam name="T">资源类型</typeparam>
public void LoadAsync<T>(string objectPath, UnityAction<T> callBack) where T : UnityEngine.Object
{
MonoEvent.Instance.StartCoroutine(IEload<T>(objectPath, callBack));
}
private IEnumerator IEload<T>(string Path, UnityAction<T> callBack) where T : UnityEngine.Object
{
ResourceRequest res = Resources.LoadAsync<T>(Path);
yield return null;
//资源动态加载完毕之后调用回调
if (res.asset is GameObject)
callBack(GameObject.Instantiate(res.asset) as T);
else
callBack(res.asset as T);
}
}
输入控制模块
将游戏中的输入检测汇集到一个地方,降低项目的耦合性
public class KeyTypeManager
{
public static readonly string WKeyDown = "WKeyDown";
public static readonly string EKeyDown = "EKeyDown";
public static readonly string RKeyDown = "RKeyDown";
}
public class InputManager : BaseSingleton<InputManager>
{
private bool isListen = false;
//构造析构时增加删除监听对象
public InputManager() { MonoEvent.Instance.AddUpdateEvent(InputUpdate); }
~InputManager() { MonoEvent.Instance.RemoveUpdateEvent(InputUpdate); }
//需要使用时开启监听
public void StartListen(bool _isListen) { isListen = _isListen; }
public void InputUpdate()
{
if (!isListen)
return;
if (Input.GetKeyDown(KeyCode.W))
CenterEvent.Instance.Raise(KeyTypeManager.WKeyDown);
if (Input.GetKeyDown(KeyCode.E))
CenterEvent.Instance.Raise(KeyTypeManager.EKeyDown);
}
}
public class TestInput : MonoBehaviour
{
private void Start()
{
InputManager.Instance.StartListen(true); //开启监听,同时若首次调用则创建单例
CenterEvent.Instance.AddListener(KeyTypeManager.WKeyDown, GetWDown);
CenterEvent.Instance.AddListener(KeyTypeManager.EKeyDown, Open);
}
public void Forward() { Debug.Log("向前走"); }
public void Open() { Debug.Log("打开"); }
}