在 Unity 中,每个脚本都绑定在一个独立的对象上,而对象与对象之间不可避免地需要彼此之间进行沟通和调用,其中包括平级的对象之间,以及父对象与子对象之间,都会存在各种不同的数据传递或者调用,来实现事件的响应等。

参考链接:【详解Unity】脚本数据传递与事件通知 | 方法总结_哔哩哔哩_bilibili

1. 数据传递

先来看下对象之间的数据传递,大致有如下几种方法:

  • 定义静态字段
  • 定义公开属性、Get 方法
  • PlayerPrefs
  • 单例模式

a. 定义静态字段

一般适用于一些固定的常量,或者实例之间共享的变量。和 C++ 类似,定义的静态字段可以直接通过"类名.静态变量名"的方式来获取调用。定义方式如下

1
2
public static int number = 999;
public static readonly int NUMBER = 888;

b. 定义公开属性、Get 方法

如果是跨脚本传输数据的话,需要获得该脚本的实例对象才能去访问公开变量,至于如何获取该实例对象见下方代码

脚本 A

1
2
3
4
5
6
7
8
9
10
11
12
13
using UnityEngine;

public class ItemA : MonoBehavior
{
// a. 定义私有变量
private int Hp = 1000

// b. 定义该变量的公开属性
public int Hp{get{return Hp;}}

// c. 定义该变量的公开方法
public int GetHp() { return Hp; }
}

脚本 B

1
2
3
4
5
6
7
8
9
// 1. 必须首先通过拖拽赋值
public ItemA A;
private void Start() {
// 通过公有成员函数获取
Debug.Log("Hp= " + A.GetHp());

// 2. 通过游戏运行时查找
A = GameObject.Find("对象名").GetComponent<ItemA>();
}

c. PlayerPrefs

PlayerPrefs 本质上是用于数据本地持久化保存和读取的一个 Unity 内置静态类,但也可以用于脚本通讯。多用于存档使用。

原理:以 Key-Value 的形式将数据保存在本地,然后在代码中可以写入、读取、更新数据。以下以整形数据存储读取举例

1
2
3
4
5
6
7
8
// 存储整形数据
PlayerPrefs.SetInt("intKey", 999);

// 取出 key 为 "intKey" 的整形数据
int intVal = PlayerPrefs.GetInt("intKey");

// 查找是否存在 key 为 "intKey" 的数据
bool exist = PlayerPrefs.HasKey("intKey");

d. 单例模式

一旦使用单例模式,数据就可以很方便地被调用,无需先获取实例,因为类的内部已经写好了获取方法。但是首先要保证,这个类是独一份的,才适用于这种模式。这种有点类似于静态变量的定义,已经保证在整个系统中只存在这样一个变量,那么就可以共享该数据。

1
2
3
4
5
6
7
8
9
10
class Test 
{
private int number = 889;
Test static test = null;
public static Test GetInstance() {
if (test == null)
Test test = new Test();
return test;
}
}

2. 消息通知

实现:SendMessage;

这个方法是由 Unity 提供的,可用于游戏对象自身的脚本之间的通知、父级对子级的通知、子级对父级的通知等。但并不支持两个游戏对象之间的消息通知。具体有这么三种方法可以使用

  • SendMessage("接收函数", 需传递的参数) -> 发送给自身的所有脚本
  • SendMessageUpwards("接收函数", 需传递的参数) -> 发送给自身的所有脚本以及自身父物体、父、父物体等身上的所有脚本
  • BroadcaseMessage("接收函数", 需传递的参数) -> 发送给自身的所有脚本以及自身子物体、子、子物体等身上的所有脚本

3. 定义委托(事件回调函数机制)

事件中心的实现代码如下,通过这个事件中心来实现回调函数的注册,注销和触发。个人觉得可以把它理解成一个函数指针的字典,并且是单例实现的。前面也说过通过单例实现可以类似静态变量一样来实现被全局调用,于是在整个工程中,通过该事件中心来实现所有需要交互调用的函数的注册使用以及触发。当某一个函数需要被其他对象调用时,可以先将其注册到事件中心。然后在需要调用处来触发实现调用。

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
49
50
51
52
53
54
55
56
57
58
using UnityEngine;
using System.Collections.Generic;
public class EventCenter
{
private EventCenter() { }
private static EventCenter eventCenter= null;
public static EventCenter GetInstance()
{
if (eventCenter == null)
eventCenter = new EventCenter();
return eventCenter;
}
public delegate void processEvent(Object obj, int param1, int param2);
//把委托当成一个指针来理解
private Dictionary<string, processEvent> eventMap = new Dictionary<string, processEvent>();
//那么这里存贮的就是一个字符串,对应一个函数地址
//声明一个自定义委托,使用字典键值对存贮,

//注册
public void Regist(string name, processEvent func)
{
if (eventMap.ContainsKey(name))
eventMap[name] += func;
else
eventMap[name] = func;
}
//注销
public void UnRegist(string name, processEvent func)
{
if (eventMap.ContainsKey(name))
eventMap[name] -= func;
}
//触发
public void Trigger(string name, Object obj, int param1, int param2)
{
if (eventMap.ContainsKey(name))
eventMap[name].Invoke(obj, param1, param2);
}

//运行逻辑:当触发 触发函数时,判断字典内是否有对应注册好的字符串,若有,则执行字符串对应的函数地址的函数,则完成一次触发
}

/*
//注册方面
private void Start()
{
EventCenter.GetInstance().Regist("LookItem", OnLookItem);
}
private void OnLookItem(Object obj, int param1, int param2)
{
Debug.Log(obj.name + "被发现了");
}


//触发方面:
EventCenter.GetInstance().Trigger("LookItem", hit.collider.gameObject, 0, 0);

*/

4. 总结

  • 声明一个静态字段的可用范围较小,但使用通过类名获取很方便;
  • 声明一个公开属性或 Get 方法很方便,但需要手动给脚本添加实例才能使用;
  • PlayerPrpfs 技术是全局通用的,多用于存档备份;
  • 单例模式是一种设计模式,但也可以很方便地访问数据,因为它不需要获取当前实例;
  • SendMessage 虽然用于消息通知,不过通知的同时也可以传递参数,但仅能传递同对象脚本,不常用;
  • 定义委托则是全局通用,通过事件处理机制来协调各个脚本之间的数据传递和消息通知;