元某人也元某人也

Unity 程序基础小框架

Unity 教学向小框架:单例、对象池、事件中心、Mono 桥接与资源加载。

本文目录

Unity 程序基础小框架

常用教学向模块化架构:单例 → 缓存池 / 事件中心 / Mono 桥接 / 资源加载。
目标:降低耦合、复用对象、减少 GC、让非 MonoBehaviour 类也能用 Update / 协程。

目录


框架总览

image-20260520092711147

模块基类作用
普通单例BaseManager<T>纯 C# 管理类,可 new
Mono 单例SingletonMono<T> / SingletonMonoAuto<T>挂场景或自动创建常驻物体
对象池SingletonMonoAuto<PoolMgr>复用 GameObject,少 Instantiate/Destroy
事件中心BaseManager<EventCenter>字符串事件 + 委托,观察者模式
Mono 管理BaseManager<MonoMgr>非 Mono 类注册 Update、开协程
资源加载BaseManager<ResMgr>Resources 同步/异步加载

Unity 中常用的 Inspector 特性

特性作用
[Header("text")]分组标题
[Space] / [Space(h)]空白间距
[Tooltip("text")]悬停提示
[SerializeField]私有字段显示在 Inspector
[HideInInspector]隐藏 public 字段
[Range(min, max)]数值滑条
[TextArea] / [Multiline]多行字符串
[ColorUsage(showAlpha)]颜色选择器是否含 Alpha
[EnumFlags]枚举多选
[ContextMenu("name")]组件右键菜单调用方法
[RequireComponent(typeof(T))]自动添加依赖组件
[ExecuteInEditMode]编辑模式下也执行
[GUIColor(r,g,b,a)]属性行颜色(Odin 等扩展常见)
[LabelText("name")]自定义显示名(Odin)

第 12 条 [InspectorButton] 为第三方(如 Odin)特性,Unity 内置无此 Attribute。
第 16–18 条为旧版 Image Effect 相关,URP/HDRP 项目中较少使用。

自定义类要在 Inspector 显示:

1
2
[System.Serializable]
public class PlayerConfig { public int hp; }

单例模式基类模块

思路

  • 纯 C# 管理器:static + 泛型 BaseManager<T>
  • 需挂场景 / 用协程:MonoBehaviour 单例

注意:继承 MonoBehaviour 的脚本不能 new,只能 AddComponent 或拖挂载。

1. 普通单例(非 Mono)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class BaseManager<T> where T : new()
{
    private static T _instance;

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

使用:class EventCenter : BaseManager<EventCenter> { } → EventCenter.Instance

2. Mono 单例(场景内唯一)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance => instance;

    protected virtual void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this as T;
    }
}

子类重写 Awake 时需 base.Awake()。

3. Mono 单例(自动创建 + 常驻)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SingletonMonoAuto<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T instance;

    public static T Instance
    {
        get
        {
            if (instance == null)
            {
                GameObject obj = new GameObject(typeof(T).Name);
                instance = obj.AddComponent<T>();
                DontDestroyOnLoad(obj);
            }
            return instance;
        }
    }

    protected virtual void Awake()
    {
        if (instance != null && instance != this)
        {
            Destroy(gameObject);
            return;
        }
        instance = this as T;
        DontDestroyOnLoad(gameObject);
    }
}
类型适用
BaseManager<T>EventCenter、MonoMgr、ResMgr
SingletonMono<T>场景中预先放好一个物体
SingletonMonoAuto<T>PoolMgr 等全局自动创建

缓存池模块

作用

  • 减少反复 Instantiate / Destroy 带来的 GC 与卡顿
  • 对象停用后放入池,需要时再取出激活
  • 代价:长期占用一块内存

GC 简述

创建对象申请内存;Destroy 往往先断开引用,由 GC 在内存紧张时回收。池化可避免频繁分配。

PoolMgr 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections.Generic;
using UnityEngine;

public class PoolMgr : SingletonMonoAuto<PoolMgr>
{
    private Dictionary<string, List<GameObject>> poolDic =
        new Dictionary<string, List<GameObject>>();

    /// <summary>从池中取物体;池空则 Resources 实例化</summary>
    public GameObject GetObj(string name)
    {
        GameObject obj = null;

        if (poolDic.TryGetValue(name, out var list) && list.Count > 0)
        {
            obj = list[0];
            list.RemoveAt(0);
        }
        else
        {
            obj = Instantiate(Resources.Load<GameObject>(name));
        }

        obj.SetActive(true);
        return obj;
    }

    /// <summary>放回池中(Deactivate)</summary>
    public void PushObj(string name, GameObject obj)
    {
        obj.SetActive(false);

        if (!poolDic.ContainsKey(name))
            poolDic[name] = new List<GameObject>();

        poolDic[name].Add(obj);
    }
}

原笔记 bug:else 分支只 Add 了空 List,没有把 obj 放进列表,已在上文修正。

使用示例

1
2
3
GameObject bullet = PoolMgr.Instance.GetObj("Bullet");
// ...
PoolMgr.Instance.PushObj("Bullet", bullet);

可扩展:父节点 poolRoot、预加载数量、超时清理、与 ResMgr 配合 Addressables。


事件中心模块

作用

  • 避免脚本互相 GetComponent 强引用
  • 观察者模式:发布 / 订阅分离,降低耦合

事件中心结构

  • 容器:Dictionary<string, UnityAction<object>>
  • 支持:注册、移除、触发、清空

EventCenter 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class EventCenter : BaseManager<EventCenter>
{
    private Dictionary<string, UnityAction<object>> eventDic =
        new Dictionary<string, UnityAction<object>>();

    public void AddEventListener(string eventName, UnityAction<object> action)
    {
        if (eventDic.ContainsKey(eventName))
            eventDic[eventName] += action;
        else
            eventDic.Add(eventName, action);
    }

    public void EventTrigger(string eventName, object info = null)
    {
        if (eventDic.TryGetValue(eventName, out var action))
            action?.Invoke(info);
    }

    public void RemoveEventListener(string eventName, UnityAction<object> action)
    {
        if (!eventDic.ContainsKey(eventName)) return;

        eventDic[eventName] -= action;
        if (eventDic[eventName] == null)
            eventDic.Remove(eventName);
    }

    /// <summary>场景切换时建议清空,避免野引用</summary>
    public void Clear()
    {
        eventDic.Clear();
    }
}

原方法名 EventTigger 建议改为 EventTrigger。

发布者(Monster)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class Monster : MonoBehaviour
{
    void Start() => StartCoroutine(DeadCoroutine());

    public void Dead()
    {
        EventCenter.Instance.EventTrigger("MonsterDead", this);
    }

    IEnumerator DeadCoroutine()
    {
        yield return new WaitForSeconds(3f);
        Dead();
    }
}

顺序:若广播时还没有订阅者,回调不会执行。应先 AddEventListener 再 EventTrigger,或用延迟/初始化阶段统一注册。

订阅者(Player)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Player : MonoBehaviour
{
    void Start()
    {
        EventCenter.Instance.AddEventListener("MonsterDead", OnMonsterDead);
    }

    void OnMonsterDead(object info)
    {
        Debug.Log("Monster dead: " + info);
    }

    void OnDestroy()
    {
        EventCenter.Instance.RemoveEventListener("MonsterDead", OnMonsterDead);
    }
}

必须在 OnDestroy 里移除监听,否则对象销毁后仍被委托引用 → 内存泄漏。

无参事件(扩展)

可用 UnityAction 或泛型 EventCenter<T>,避免处处 (object) 强转。


公共 Mono 模块

作用

  • 让不继承 Mono 的类也能:
    • 注册 Update(统一在一个 MonoBehaviour 里跑)
    • 开启 / 停止协程
  • 减少满场景都是 Update 的空转(仍是一个 Update 分发,按需注册)

MonoController(挂在场景物体上)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using UnityEngine.Events;

public class MonoController : MonoBehaviour
{
    private event UnityAction updateEvent;

    void Start()
    {
        DontDestroyOnLoad(gameObject);
    }

    void Update()
    {
        updateEvent?.Invoke();
    }

    public void AddUpdateListener(UnityAction fun) => updateEvent += fun;
    public void RemoveUpdateListener(UnityAction fun) => updateEvent -= fun;
}

MonoMgr(对外接口)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Collections;
using UnityEngine;
using UnityEngine.Events;

public class MonoMgr : BaseManager<MonoMgr>
{
    private MonoController controller;

    public MonoMgr()
    {
        GameObject obj = new GameObject("MonoController");
        controller = obj.AddComponent<MonoController>();
        Object.DontDestroyOnLoad(obj);
    }

    public void AddUpdateListener(UnityAction fun) =>
        controller.AddUpdateListener(fun);

    public void RemoveUpdateListener(UnityAction fun) =>
        controller.RemoveUpdateListener(fun);

  public Coroutine StartCoroutine(IEnumerator routine) =>
        controller.StartCoroutine(routine);

    public void StopCoroutine(Coroutine routine) =>
        controller.StopCoroutine(routine);

    public void StopAllCoroutines() =>
        controller.StopAllCoroutines();
}

使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Test : MonoBehaviour
{
    void Start()
    {
        var logic = new TestLogic();
        MonoMgr.Instance.AddUpdateListener(logic.OnUpdate);
        MonoMgr.Instance.StartCoroutine(logic.Run());
    }
}

public class TestLogic
{
    public void OnUpdate() => Debug.Log("Update from non-Mono class");

    public IEnumerator Run()
    {
        yield return new WaitForSeconds(1f);
        Debug.Log("Coroutine from non-Mono class");
    }
}

资源加载模块

概念

方式特点
同步 Resources.Load阻塞主线程,加载完立刻可用
异步 ResourceRequest不卡帧,完成后在回调里使用资源

正式项目更推荐 Addressables / AssetBundle;教学框架常用 Resources 文件夹。

Lambda 与委托(回调写法)

1
2
3
4
5
// 无返回值
UnityEngine.Events.UnityAction<GameObject> onLoaded = (obj) => { /* 使用 obj */ };

// 有返回值
Func<string, int> fn = (value) => 1;

异步加载必须在回调里实例化或赋值,不能 LoadAsync 后立刻 Instantiate(资源尚未完成)。

ResMgr 实现

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
using Object = UnityEngine.Object;

public class ResMgr : BaseManager<ResMgr>
{
    /// <summary>同步加载 Resources 下资源</summary>
    public T Load<T>(string path) where T : Object
    {
        return Resources.Load<T>(path);
    }

    /// <summary>同步加载并实例化预制体</summary>
    public GameObject LoadGameObject(string path)
    {
        GameObject prefab = Load<GameObject>(path);
        return prefab != null ? Object.Instantiate(prefab) : null;
    }

    /// <summary>异步加载资源</summary>
    public void LoadAsync<T>(string path, UnityAction<T> callback) where T : Object
    {
        MonoMgr.Instance.StartCoroutine(LoadAsyncCoroutine(path, callback));
    }

    private IEnumerator LoadAsyncCoroutine<T>(string path, UnityAction<T> callback)
        where T : Object
    {
        ResourceRequest request = Resources.LoadAsync<T>(path);
        yield return request;

        callback?.Invoke(request.asset as T);
    }

    /// <summary>异步加载并实例化</summary>
    public void LoadGameObjectAsync(string path, UnityAction<GameObject> callback)
    {
        LoadAsync<GameObject>(path, prefab =>
        {
            if (prefab != null)
                callback?.Invoke(Object.Instantiate(prefab));
            else
                callback?.Invoke(null);
        });
    }
}

使用示例

1
2
3
4
5
6
7
8
9
// 同步
GameObject go = ResMgr.Instance.LoadGameObject("Prefabs/Enemy");

// 异步 + Lambda
ResMgr.Instance.LoadGameObjectAsync("Prefabs/Enemy", obj =>
{
    if (obj != null)
        obj.transform.position = Vector3.zero;
});

与对象池配合

1
2
3
4
5
// 异步加载预制体 → 首次 Instantiate → 之后走 PoolMgr
ResMgr.Instance.LoadAsync<GameObject>("Bullet", prefab =>
{
    // 预热:预先 Push 若干发
});

模块协作与注意点

推荐初始化顺序

  1. 场景放置或自动创建 PoolMgr / MonoController(MonoMgr 构造时创建)
  2. EventCenter、ResMgr 首次访问 Instance 时创建
  3. 各玩法脚本在 Start 中注册事件、Update
  4. 场景切换:EventCenter.Instance.Clear(),按需清理池

常见问题

问题处理
事件不触发订阅晚于广播;检查事件名拼写
内存泄漏OnDestroy 移除事件;池与常驻单例慎引用已销毁对象
异步资源为空路径不含扩展名;文件在 Resources 下;等回调再使用
单例重复Awake 里 Destroy 重复实例
Mono 双份 Update既在自己 Update 里写逻辑,又注册 MonoMgr,避免重复

可扩展方向

模块说明
SceneMgr封装 SceneManager.LoadScene + 加载 UI + 进度
UIMgr面板栈、Resources/Addressables 加载 UI 预制体
MusicMgrBGM / SFX 池化 AudioSource
TimerMgr基于 MonoMgr.Update 的倒计时、间隔调用
存档 DataMgrJSON / PlayerPrefs,与事件中心解耦 UI
仅用于交流学习,记录学习心得