为什么把 Awake(和 Update)声明为 virtual
如果你不写 virtual,仍然可以在子类里写自己的 Awake。但这会用 方法隐藏(new) 的方式覆盖父类版本,而 Unity 仍然分别调用两份 Awake——父类的、子类的。
一旦你的父类 Awake 里有 必须执行的初始化(拿 CharacterBase 来说:缓存 Animator 和 CharacterController),就需要确认子类永远不会忘记这一步。把它声明为 virtual + 要求子类 override 并手动 base.Awake(),可以在编译期强制提醒。
protected override void Awake()
{
base.Awake(); // 保证父类初始化
/* 子类自己的初始化 */
}
如果不用 virtual 而让子类写 new void Awake(),你稍不注意就会漏掉 base.Awake()——编译器不会给任何警告,运行时才发现 controller = null 之类的问题
Lambda表达式
参数列表规则:
无参数:必须写空括号
() => DoSomething();
一个参数:可以省略括号(但显式写类型/属性时不能省略)
x => x * 2
(int x) => x * 2 // 显式类型 → 需要括号
([NotNull] string s) => s.Length // 带属性 → 需要括号(C# 10+ 支持参数属性)
多个参数:必须加括号
(x, y) => x + y
(int x, int y) => x + y
方法体规则
单表达式:可以省略花括号与 return
Func<int,int> f = x => x + 1; // 返回值 = 表达式的值
Action a = () => Console.WriteLine("Hi"); // 表达式为 void → 推断为 Action
语句块:用 { ... }
;返回非 void 时必须写 return
Func g = x => { var y = x + 1; return y; };
=>
1. 方法定义的简写语法
包括普通方法/属性/构造函数/析构函数 的方法体简写
=> 右侧必须是单个表达式;需要多语句/校验就回到 { … } 写法
void A() => Console.Write("A");
//等价于
void A()
{
Console.Write("A");
}
有返回值时,表达式的值就是返回值:
int Sq(int x) => x * x;
//等价于传统写法:
int Sq(int x)
{
return x * x;
}
属性的访问器get/set
int Count => _list.Count; // 只读属性
int Count { get => _count; set => _count = value; } // 访问器
2.委托和Lambda表达式的关系
首先记住Lambda表达式的上一个版本叫匿名方法,所以本质还是一个没有名字的函数,是函数
委托(delegate)就是“带有签名的类型安全函数指针
所以两者的关系是:lambda表达式作为委托的简易实现
编译器把 x => x + 1
这种语法转换成与某个委托类型匹配的可调用对象。
注意第一种用法 =>的左侧是函数签名而不是参数列表,而用在委托的简化实现时,左侧是参数列表
属性的简单写法
private int _health;
public int Health
{
get { return _health; }
set { _health = value; }
}
写法类型 | 示例 | 说明 |
---|---|---|
自动属性 | public int HP { get; set; } | 最常用,编译器自动处理字段 |
只读属性 | public int Level { get; } | 只能在构造器赋值 |
表达式属性 | public float Speed => 10f; | 适合只读并简单返回值 |
私有设置 | public int Score { get; private set; } | 外部只读,内部可写 |
手动属性 | 见完整写法 | 可在 get/set 中写逻辑 |
表达式属性的等价写法:
public float Speed
{
get { return 10f; }
}
public float Speed
{
get => 10f; //其实是有返回值的方法的简写(见上)
}
带默认值的自动属性(auto-property initializer)语法:
public BindableProperty<Transform> lockTarget { get; set; }
= new BindableProperty<Transform>();
委托
调用
直接调用(像方法一样)
Action<string> log = Console.WriteLine;
log("hello"); // 直接像方法那样调用 —— 最常用、最快
空条件调用 ?.Invoke(...)
(防空)
Action<string> log = null;
log?.Invoke("safe"); // log 为 null 时什么也不做;常用于触发事件
常见内置委托类型
委托 | 签名形态 | 作用 | 典型用法/API |
---|---|---|---|
Action | void 返回;Action() 、Action<T1> ... Action<T1,...,T16> | 做事但不返回结果(副作用) | 事件/回调、List.ForEach(Action<T>) 、按钮点击等 |
Func | 最后一个泛型参数是返回值;Func<TResult> 、Func<T1,TResult> … | 有输入、有返回值(变换/计算) | Select(Func<T,R>) 、Aggregate 、工厂方法等 |
Predicate | 等价于 Func<T,bool> | 断言/条件判断(返回布尔) | List.Find/Exists/RemoveAll(Predicate<T>) 、Where |
委托的Target属性
委托的Target指“委托要调用到的那个对象”——也就是接收者/响应者(receiver)。
不是事件的拥有者/发布者(publisher)。发布者通常会作为事件回调的第一个参数 sender
传进来,而不是 delegate.Target
- 实例方法回调:
delegate.Target == 方法所属实例
(响应者)。 - 静态方法回调:
delegate.Target == null
(无接收者)。 - 捕获变量的 lambda:
delegate.Target == 闭包对象
(编译器生成的隐藏对象)。 - 扩展方法:本质静态方法,
Target == null
。 - 多播委托:每个订阅各有自己的
Target
。
委托之间的 ==
- 闭包对象:编译器生成、承载被捕获变量的隐藏对象,是委托的
Target
。 - 委托相等:必须
Method
与Target
同时相同;捕获 lambda 往往方法不同(即使 Target 相同),或作用域不同导致 Target 不同,所以比较常为false
。
Action
/委托虽然是引用类型,但 .NET 专门为委托实现了 ==
/!=
(在 MulticastDelegate
里)。
比较规则不是“同一引用”,而是比较调用列表(invocation list):
- 单播委托:相等当且仅当方法相同 + 目标对象(Target)相同。
- 静态方法:
Target == null
,只要方法相同就相等。 - 实例方法:方法相同但目标不同就不相等。
- 静态方法:
- 多播委托:两边的调用列表长度相同且逐项(方法+目标)一一相等且顺序一致才相等。
- 类型必须相同:
Action<object>
不能和Action<string>
用==
比。 null
规则:两个都是null
→ 相等;一边null
一边不是 → 不相等。
// 静态方法:方法相同 => 相等
Action a1 = Console.Beep;
Action a2 = Console.Beep;
Console.WriteLine(a1 == a2); // True
// 实例方法:方法相同但目标不同 => 不相等
var s1 = "hi";
var s2 = "hi";
Func<int> f1 = s1.GetHashCode;
Func<int> f2 = s2.GetHashCode;
Console.WriteLine(f1 == f2); // False(目标 s1 vs s2)
// 捕获的 lambda:通常不同(各自有不同的闭包对象)
int n = 0, m = 0;
Action l1 = () => Console.WriteLine(n);
Action l2 = () => Console.WriteLine(n);
Action l3 = () => Console.WriteLine(m);
Console.WriteLine(l1 == l2); // False(闭包对象不同)
Console.WriteLine(l1 == l3); // False(方法体不同)
// 不捕获的 lambda/方法组:编译成同一静态方法 => 相等
Action p1 = () => Console.WriteLine("X");
Action p2 = () => Console.WriteLine("X");
Console.WriteLine(p1 == p2); // True(方法相同、Target 为 null)
// 多播委托:调用列表要完全一致
Action x = Console.Beep;
Action y = Console.Beep;
Action xy1 = x + y;
Action xy2 = x + y;
Action yx = y + x;
Console.WriteLine(xy1 == xy2); // True
Console.WriteLine(xy1 == yx); // False(顺序不同)
闭包对象
当 lambda/匿名方法捕获了外部变量(如 n
)时,编译器会生成一个隐藏的“显示类/闭包类”(display class),把被捕获的局部变量变成这个类的字段;该类的一个实例就是“闭包对象”。捕获的 lambda 会被编译成这个类上的实例方法,因此委托的 Target
就是这个闭包对象;
int n = 0;
// 两个捕获同一外部变量 n 的 lambda
Action l1 = () => Console.WriteLine(n);
Action l2 = () => Console.WriteLine(n);
Console.WriteLine(object.ReferenceEquals(l1.Target, l2.Target)); // True:同一闭包对象
Console.WriteLine(l1.Method == l2.Method); // False:方法不同
Console.WriteLine(l1 == l2); // False:Method 不同→不相等
如果来自不同作用域/不同调用(闭包对象也不同):
Action Make()
{
int x = 0;
return () => Console.WriteLine(x); // 捕获 x → 生成新的闭包对象
}
var a = Make();
var b = Make();
Console.WriteLine(object.ReferenceEquals(a.Target, b.Target)); // False:不同闭包对象
Console.WriteLine(a.Method == b.Method); // True/False 取决于编译器实现
Console.WriteLine(a == b); // False:至少 Target 不同
多播委托
多播委托指的是“一个委托实例里绑定了多个响应方法,调用该委托时会按顺序依次调用这些方法
构造与调用顺序
void A() => Console.Write("A");
void B() => Console.Write("B");
Action d = null; // 空委托
d += A; // 现在是单播: [A]
d += B; // 多播: [A, B]
d += A; // 多播: [A, B, A]
d(); // 输出:ABA (按添加顺序依次调用)
相等性:多播委托相等当且仅当调用列表长度相同,且每一项(方法+目标)顺序一致:
Action d1 = A; d1 += B; // [A, B]
Action d2 = A; d2 += B; // [A, B]
Action d3 = B; d3 += A; // [B, A]
Console.WriteLine(d1 == d2); // True 列表完全一致
Console.WriteLine(d1 == d3); // False 顺序不同
移除监听
Action d = A; d += B; d += A; // [A, B, A]
d -= A; // 移除尾部那个 A → [A, B]
全局唯一单例(Mono Singleton)模板
泛型单例基类,用于保证某个组件在整个游戏生命周期中只存在一个实例,并且自动持久化到场景之间。
// 约束 T 必须是继承自 MonoSingleton<T> 的类型本身
public class MonoSingleton<T>:MonoBehaviour where T : MonoSingleton<T>
{
private static T instance;
public static T Instance
{
get
{
if (instance == null)
{
//先尝试在场景中查找现有的 T 类型对象;
instance = GameObject.FindAnyObjectByType<T>();
if (instance == null)
{
//如果找不到,就创建一个新的空物体并挂上该组件,这保证了程序中一定有一个有效的 T
//适合XXmanager,毕竟一般我们也是创建一个空物体放置在原点然后挂载上XXmanager.cs
var go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
}
return instance;
}
}
protected virtual void Awake()
{
//默认放到DontDestroyScene中
//如果当前实例是第一个,就设置为单例,并且让它在场景切换时不销毁(DontDestroyOnLoad)
if (instance == null)
{
instance = (T)this;
DontDestroyOnLoad(instance);
}
else
{
//否则销毁这个重复对象(保持唯一性)
//在物体销毁时访问次对象最好加一个Null的判断,防止程序退出次物体被销毁
Destroy(instance);
}
}
}
使用类单例
public class XXManager : MonoSingleton<AudioManager>
{
public void PlaySound(string name) { ... }
}
然后你可以在任何地方使用:
XXManager.Instance.PlaySound("click");
where关键字
where
用于规定泛型类型参数 T
必须具备某些特性,如继承某个类、实现某接口、有无参构造函数等。
where T : class
public class MyClass<T> where T : class
限制 T 必须是引用类型(比如 string
、GameObject
、自定义类等)。
where T : struct
限制 T 必须是值类型(如 int, float, Vector3,不能为 Nullable)
where T : new()
public class MyClass<T> where T : new()
限制 T 必须有一个公共的无参构造函数,这样你可以 new T()
。
where T : SomeBaseClass
public class MyClass<T> where T : Enemy
限制 T 必须是 Enemy
类或其子类。这样你可以使用 Enemy
中的方法。
where T : IMyInterface
public class MyClass<T> where T : IAttackable
限制 T 必须实现某个接口,如 IAttackable
多个约束组合使用
public class MyClass<T> where T : MonoBehaviour, new()
限制 T 必须继承自 MonoBehaviour
并且有无参构造函数
单例模板类的递归泛型约束(CRTP,Curiously Recurring Template Pattern)
保证从 MonoSingleton<T>
中返回的 Instance
是派生类类型,不是基类
I
当你希望一个类型能够被比较(用于排序、查找等),你可以让它实现这个接口
namespace System
{
public interface IComparable<in T>
{
int CompareTo(T other);
}
}
可观察属性(ObservableProperty)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
/// <summary>
/// 让一个变量属性的值发生变化时,自动触发 ValueChanged 事件
/// </summary>
/// <typeparam name="T"></typeparam>
public class BindableProperty<T>
{
// 事件委托:当 Value 值发生变化时,会调用所有订阅的方法,并传入新的值
public Action<T> ValueChanged;
private T m_value;
// 公共属性,外部可以读写它的值
public T Value
{
// 当设置新值时(Value = xxx)
set
{
// 如果新值和当前值不相等
if (!Equals(m_value,value))
{
// 更新内部存储的值
m_value = value;
// 触发事件通知
ValueChanged?.Invoke(m_value);
}
}
get { return m_value; }
}
}
它是一个可观察的泛型值容器:把某个值包装成 Value
属性,外部读写;当你给 Value
赋新值时,如果新旧值不相等(用 Equals
比较),就更新内部值并触发 ValueChanged
事件,把新值通知给所有订阅者。
适合做数据绑定/状态同步(值变→UI/逻辑自动响应),并避免重复通知。
public BindableProperty<Transform> lockTarget { get; set; } = new BindableProperty<Transform>();
当你给 lockTarget.Value
赋值时,如果新旧引用不相等,就触发 ValueChanged(newValue)
。
Private & Readonly Readonly | const
private
只是限制可见性(只能在这个类里访问),不限制你在类内部重新赋值。readonly
则是限制赋值次数/时机:这个字段只能在声明处或构造函数里赋值;离开构造函数后就不能再被重新指到别的对象。
private readonly Dictionary<T, List<Action<object, object>>> eventDic = new();
- 这张字典只能在本类里用(private);
- 它的引用一经建立就不允许被换成另一张字典(readonly)。
你仍然可以对内容增删改(Add/Remove/Clear
),但不能写eventDic = new Dictionary<...>()
或eventDic = null
。
为什么有用(即使已经 private):
- 防误操作:避免以后维护时在某个方法里把
eventDic
重新 new/置空,导致已注册的监听丢失。 - 意图表达:告诉读代码的人“这个集合的身份固定不变,只有内容会变”
特性 | const | readonly |
---|---|---|
何时确定 | 编译期 | 运行期(构造时/静态构造时) |
是否内联到调用方 | ✅ 是(有版本兼容风险) | ❌ 否 |
是否隐式 static | ✅ 是(不能再写 static ) | ❌ 否(可选:可写成 static readonly ) |
允许的类型 | 数值/bool /char /string /enum /decimal ,或引用类型的 null | 任意类型 |
赋值位置 | 只能在声明处 | 声明处或构造函数(static readonly 可在静态构造函数) |
是否可因实例不同而不同 | ❌ 否(所有地方同一值) | ✅ 可以(每个实例的只读字段可不同) |
能否指向可变对象并修改其内容 | 不适用(必须是常量) | 引用不能换,但对象能改(内容可变) |
内联
内联(Inlining)就是编译器/运行时把“对某个东西的引用”直接替换为它的内容,从而省掉一次间接访问或函数调用。
两种常见语境:
- 常量内联(C#
const
):调用方在编译时把Lib.Pi
直接写成数值3.1415…
,以后即使库把常量改了,未重新编译的调用方仍用旧值。 - 函数内联(JIT/优化):把一次小函数调用替换成函数体本身,减少调用开销(是否内联由 JIT/优化器决定,可受
MethodImplOptions.AggressiveInlining
等影响)。
const
是编译期常量(隐式 static
、会被内联,只能是编译期可确定的字面量/枚举/字符串/数字等)
而 readonly
是运行期只读字段(仅能在声明或构造函数/静态构造函数赋值,不会内联,可用于任意类型——引用不可换但对象内容可变)
事件系统
EventHandler<T>
首先需要知道事件的ID,依此订阅者类在OnEnable中调用AddEventListening,在DisEnable中调用RemoveEventListeningByTarget
成员功能函数中创建EventMessage类对象,传递并调用 SendMessage / SentMessageByQue,两者都最终会触发TriggerEvent,一个立刻调用TriggerEvent(eventMessage),另一个会先将消息放入Queue中,在Updata中只要检测存放EventMessage的Queue.Count大于0才调用TriggerEvent(eventMessage)
总的来说EventHandler定义了事件系统给外界提供了订阅,解订阅,发送信息,创建信息之间的逻辑,而底层实现放在了EventHandlerBase类中,下面我们来分析这个类
using System; // 引用系统基础命名空间
using System.Collections.Generic; // 使用泛型集合(List、Dictionary、Queue 等)
using System.Linq; // (未使用)LINQ 扩展
using System.Text; // (未使用)文本相关
using System.Threading.Tasks; // (未使用)任务并行库
using Unity.VisualScripting; // (未使用)Bolt/Visual Scripting 相关
public class EventHandler<T> // 事件派发器(带队列,可同步或分帧派发)
{
private readonly EventHandlerBase<T> eventHandlerBase = new EventHandlerBase<T>(); // 底层注册表:事件ID -> 监听列表
private readonly Queue<EventMessage> eventQue = new Queue<EventMessage>(); // 事件消息队列(分帧派发用),用queue是为了防止谁先发谁先处理
public void OnEventInit() // 初始化(通常在系统启动时调用)
{
eventQue.Clear(); // 清空队列,避免残留消息
}
public void OnEventUpdate() // 每帧驱动(由外部 Update 调用)
{
if (eventQue.Count > 0) // 若队列中有待处理事件
{
var eventMessage = eventQue.Dequeue(); // 出队一个事件消息
TriggerEvent(eventMessage); // 立即触发该消息绑定的回调
}
// 提示:当前一帧只处理一条,如事件量大可能积压;可按需改为 while 循环 + 帧预算
}
/// <summary>
/// 比如UI类中:void OnEnable() => EventService.Instance.AddEventListening(EventID.HpChanged, OnHpChanged);
/// </summary>
/// <param name="id"></param>
/// <param name="action"></param>
public void AddEventListening(T id, Action<object, object> action) // 对外:按事件ID注册监听
{
eventHandlerBase.AddEventHandler(id, action); // 委托给底层注册表
}
public void RemoveEventListeningByID(T id) // 对外:按事件ID移除所有监听
{
eventHandlerBase.RemoveEventByID(id); // 委托给底层注册表
}
/// <summary>
/// 比如UI类中:void OnDisable() => EventService.Instance.RemoveEventListeningByTarget(this);
/// </summary>
/// <param name="target"></param>
public void RemoveEventListeningByTarget(object target) // 对外:按对象(回调所属目标)批量移除
{
eventHandlerBase.RemoveEventByTarget(target); // 委托给底层注册表
}
//在修改数据,事件系统外需要通知时调用,订阅方(UI/音效/逻辑都可各自订阅
/*
* public void TakeDamage(int dmg)
{
int old = hp;
hp = Mathf.Max(0, hp - dmg);
EventService.Instance.SendMessage(
EventID.HpChanged,
sender: this,
param2: new HpChangedArgs(old, hp)
);
}*/
public void SentMessage(T t, object param1, object param2) // 同步派发:立刻触发所有监听
{
List<Action<object, object>> actions = eventHandlerBase.GetEvent(t); // 取出该事件ID的监听列表
if (actions == null) { return; } // 若无人监听,直接返回
foreach (var action in actions) // 遍历所有监听回调
{
EventMessage eventMessage = new EventMessage(action, param1, param2); // 封装一条事件消息
TriggerEvent(eventMessage); // 立即触发(同步执行)
}
}
public void SentMessageByQue(T t, object param1, object param2) // 入队派发:分帧处理
{
List<Action<object, object>> actions = eventHandlerBase.GetEvent(t); // 取出监听列表
// 风险提示:actions 可能为 null,这里没有判空;可按需加 if (actions == null) return;
foreach (var action in actions) // 遍历所有监听
{
EventMessage eventMessage = new EventMessage(action, param1, param2); // 封装消息
//new EventMessage(...) 创建的是托管堆上的对象。你把它 Enqueue 到 Queue 后,队列持有它的引用,所以即使方法返回、局部变量离开作用域,
//它也不会被回收;等到后来 OnEventUpdate() 里 Dequeue 出来并用完、且不再被任何地方引用时,它才变成 GC 可回收(至于什么时候回收,由 GC 决定、不可预测)
eventQue.Enqueue(eventMessage); // 入队,等待 OnEventUpdate 在后续帧触发
}
}
private void TriggerEvent(EventMessage eventMessage) // 实际触发回调
{
eventMessage.action(eventMessage.param1, eventMessage.param2); // 执行委托,把两个参数传入
// 建议:为了健壮性,可 try/catch 捕获异常,避免一个监听崩溃影响后续
}
}
public class EventMessage // 单条事件消息(绑定一个委托与两个参数)
{
public Action<object, object> action; // 回调委托(签名固定为两个 object 参数)
public object param1; // 第一个参数(任意类型,外部按约定强转)
public object param2; // 第二个参数(任意类型)
public EventMessage(Action<object, object> action, object param1, object param2) // 构造函数
{
this.action = action; // 赋值回调
this.param1 = param1; // 赋值参数1
this.param2 = param2; // 赋值参数2
}
}
EventHandlerBase
只负责“谁监听了什么” 的存储与管理,不负责触发。是EventHandler的底层实现
using System;
using System.Collections.Generic;
// 事件注册表:管理“事件ID <-> 监听回调”与“目标对象 <-> 事件ID”映射
public class EventHandlerBase<T>
{
// 事件ID -> 回调函数列表,支持一个事件名(ID)下挂多个监听;监听的回调签名统一为 Action<object, object>且事件的传参依据各类事件函数来选择或者不使用
private readonly Dictionary<T, List<Action<object, object>>> eventDic = new Dictionary<T, List<Action<object, object>>>();
//目标对象 -> 注册过的事件ID列表 (反向表)
private readonly Dictionary<object,List<T>> targetEventDic = new Dictionary<object, List<T>>();
/// <summary>
/// 注册监听
/// </summary>
/// <param name="t">事件的“ID/键</param>
/// <param name="action"></param>
public void AddEventHandler(T t, Action<object, object> action)
{
if (!eventDic.ContainsKey(t)) // 如果该事件ID还没有列表
{
//eventDic.Add(t, new List<Action<object, object>>());使用索引器:
eventDic[t]=new List<Action<object, object>>(); // 创建一个新的回调列表
}
//避免从重复注册相同的事件(这就是自己实现时间系统的好处,可以规避Unity事件系统的问题)
List<Action<object,object>> eventList = eventDic[t]; // 取出回调列表
//Predicate<T>是参数类型T,返回bool 的函数的委托类型 public delegate bool Predicate<in T>(T obj);
Action<object, object> checkAction = eventList.Find(i => i == action); // 查找是否已存在该委托
if (checkAction != null)
{
//说明该事件已绑定过该回调函数,则不给该事件重复添加回调函数
return;
}
//事件未绑定过该回调函数,则给事件绑定该回调函数
eventList.Add(action);
//更新从属者信息
// 获取委托的响应者(实例方法为对象,静态方法为 null) - Unity自带
object target = action.Target;
if (!targetEventDic.ContainsKey(target))
{
//如果响应者不在反向表中,则为该响应者在反向表中创建一个列表
//targetEventDic.Add(target, new List<T>());
targetEventDic[target] = new List<T>();
}
//把事件名添加到这个从属者列表中
targetEventDic[target].Add(t);
}
// 按事件ID移除所有监听
public void RemoveEventByID(T t)
{
if (eventDic.ContainsKey(t))
{
// 先不着急移除 eventDic 的键,先更新 targetEventDic
//更新target
// 在事件ID - 回调函数列表字典中根据键也就是事件ID,取出对应的回调函数列表
List<Action<object,object>> actions = eventDic[t];
// 遍历该列表
foreach (var action in actions)
{
//找到该回调函数的所属者
object target = action.Target;
if (target != null && targetEventDic.ContainsKey(target))
{
//这里可能存在一个位置注册多个同名的事件(list会存在重复元素)
List<T> idList = targetEventDic[target];
idList.RemoveAll(id => id.Equals(t));//可以用拉姆达表达式
if (idList.Count == 0)
{
targetEventDic.Remove(target);
}
}
}
eventDic.Remove(t);
}
}
public void RemoveEventByTarget(object target)
{
if (targetEventDic.ContainsKey(target))
{
List<T> idList = targetEventDic[target];
foreach (var id in idList)
{
if (eventDic.ContainsKey(id))
{
List<Action<object, object>> actions = eventDic[id];
actions.RemoveAll(action => action.Target == target);
if (actions.Count == 0)
{
eventDic.Remove(id);
}
}
}
targetEventDic.Remove(target);
}
}
public List<Action<object, object>> GetEvent(T t)
{
if (eventDic.ContainsKey(t))
{
return eventDic[t];
}
return null;
}
}
EventID
就是在一个公共的枚举里集中定义/分配,要用的事件名加到这个枚举里,编译后任何脚本都能用 EventID.XXX
调用。对Event Handler的第二层封装,只是该类是单例。外层通过单例访问Event Handler
using System;
public enum EventID//这里添加事件ID
{
OnDoneStateUIInit
}
public class EventService : MonoSingleton
{
private EventHandler EventHandler = new EventHandler();
protected override void Awake()
{
//不同ID的Event都有自己唯一的EventHandler
base.Awake();
//初始化,清空queen
EventHandler.OnEventInit();
}
private void Update()
{
//如果queen中的事件消息数量大于0则调用
EventHandler?.OnEventUpdate();
}
private void OnDestroy()
{
EventHandler = null;
}
/// <summary>
/// 添加事件,参数超过2个的,封装个类传递参数
/// </summary>
/// <param name="eventID"></param>
/// <param name="action"></param>
public void AddEventListening(EventID eventID, Action<object, object> action)
{
//先回调Event Handler,再回调给Event HandlerBase给事件ID绑定响应方法
EventHandler.AddEventListening(eventID, action);
}
/// <summary>
/// 通过ID注销某一类事件:这会将所有地方注册的该类事件都给注销
/// </summary>
/// <param name="eventID"></param>
public void RemoveEventListeningByID(EventID eventID)
{
EventHandler.RemoveEventListeningByID(eventID);
}
/// <summary>
/// 清除该对象所有注册的事件
/// </summary>
/// <param name="target"></param>
public void RemoveEventListeningByTarget(object target)
{
EventHandler.RemoveEventListeningByTarget(target);
}
/// <summary>
/// 立即处理事件
/// </summary>
/// <param name="eventID"></param>
/// <param name="param1"></param>
/// <param name="param2"></param>
public void SendMessage(EventID eventID, object param1, object param2)
{
EventHandler.SentMessage(eventID, param1, param2);
}
/// <summary>
/// 分帧处理事件
/// </summary>
/// <param name="eventID"></param>
/// <param name="param1"></param>
/// <param name="param2"></param>
public void SendMessageByQue(EventID eventID, object param1, object param2)
{
EventHandler.SentMessageByQue(eventID, param1, param2);
}
}
field:SerializeField
[field: SerializeField] public StringAsset standValueParameter { get; set; }
Unity 只能序列化字段;这些是自动属性({ get; set; }
),因此用 [field: SerializeField]
标注到幕后字段,让它们在 Inspector 可见/可保存。