基于消息机制的Unity框架

思考并回答以下问题:

前言

我们使用Unity能快速开发一款小游戏。但是如果涉及到稍微大型的游戏,就不是那么轻松了。因为大型游戏会包含很多模块,例如游戏模块,UI模块,角色模块,网络模块等等。面临的问题之一就是模块之间的耦合性,就是一个模块如何调用另一个模块。

当然也有一些框架摆在我们面前。比如单例模式框架,每个模块都使用Instance单例模式,通过模块.Instance.方法可以让脚本之间访问调用。缺点也很明显,就是耦合度很高,一个脚本修改了,会影响其他的脚本。

另一种是MVC式框架,类似于通信的接收发送消息,由中间的命令层来控制逻辑。优点就是脚本之间独立。缺点是受到框架的限制,要遵守框架的规则。

在这里给大家介绍的就是一种基于消息机制的框架。每个脚本都有发送消息和处理消息的功能。

**什么是消息机制?**

就好比人类一样,两个人要交流必须要事先沟通好。比如,我们在打仗我一喊“撤退”,其他人就走。这就是一个发送者,发出“撤退”这个消息,还有一个接收者接收到“撤退”这个消息。当然这个消息也有可能有很多接收者,每个接收者也可以根据自己的状态,不处理消息。比如,断腿的人虽然听到了,但是也无法做出“撤退”这个动作。在程序里,一个脚本A发出一个消息,另外一个脚本B或多个脚本接收这个消息,并处理自身要执行的事件。

**为什么要用消息机制?**

很简单,就是为了解耦合。开发线上游戏,需求变更是非常频繁的。基于消息机制的框架可以很轻松的扩展功能。

创建MsgCenter和AreaCode

MonoBase.cs

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

/// <summary>
/// 为什么要写这个脚本?
/// 我们想扩展MonoBehaviour,但是不能直接修改
/// 定义一个MonoBase作为父类,然后其他类继承MonoBase
/// </summary>
public class MonoBase : MonoBehaviour
{
public virtual void Execute()
{
}
}

AreaCode.cs

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

/// <summary>
/// 区域码 代表 模块
/// </summary>
public class AreaCode
{
// UI模块
public const int UI = 0;

// GAME模块
public const int GAME = 1;

// CHARACTER模块
public const int CHARACTER = 2;

// NET模块
public const int NET = 3;

// AUDIO模块
public const int AUDIO = 4;
}

MsgCenter.cs

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
using UnityEngine;
using System.Collections;

/// <summary>
/// 消息处理中心
/// 只负责消息的转发
/// UI -> MsgCenter -> Character
/// </summary>
public class MsgCenter : MonoBase
{
/// <summary>
/// 处理消息
/// </summary>
public override void Execute()
{
// todo
}

/// <summary>
/// 发送消息 系统里面所有的发消息 都通过这个方法发
/// 怎么转发?根据不同的模块来发给不同的模块
/// 怎么识别模块呢?通过areaCode
///
/// 第二个参数:事件码 用来区分做什么事情的
///
/// 比如说 第一个参数识别到是角色模块,但是角色模块有很多功能,例如移动,攻击,逃跑,死亡等
/// 就需要第二个参数来识别具体是做哪一个动作。
///
/// 第三个参数是事件的参数。
///
/// </summary>
///
public void Dispatch(int areaCode, int eventCode, object message)
{
switch (areaCode)
{
case AreaCode.UI:
break;

case AreaCode.GAME:
break;

case AreaCode.CHARACTER:
break;

case AreaCode.NET:
break;

case AreaCode.AUDIO:
break;

default:
break;
}
}
}

ManagerBase

具体的每一个模块需要做什么还不知道,需要开发一个模块的基类ManagerBase来统一管理模块能做的事情。以一个字典为核心,进行字典的增删,foreach操作。

ManagerBase.cs

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 每个模块的基类
/// 1。保存每个模块自身注册的一系列消息
/// </summary>
public class ManagerBase : MonoBase
{
/// <summary>
/// 存储 消息的事件码 和 哪个脚本 关联 的字典
///
/// 角色模块 有一个动作是 移动
/// 移动模块需要关心这个事件 控制角色位置 进行移动
/// 动画模块 也需要关心 控制角色播放动画
/// 音效模块 也需要关心 控制角色移动的音效播放 比如走路声
/// </summary>
private Dictionary<int, List<MonoBase>> dict = new Dictionary<int, List<MonoBase>>();

/// <summary>
/// 处理自身的消息
/// </summary>
/// <param name="eventCode">Event code</param>
/// <param name="message">Message</param>
public override void Execute(int eventCode, object message)
{
if (!dict.ContainsKey(eventCode))
{
Debug.LogWarning("没有注册 : " + eventCode);
return;
}

// 一旦注册过这个消息 给所有的脚本 发过去
List<MonoBase> list = dict[eventCode];
for (int i = 0; i < list.Count; i++)
{
list[i].Execute(eventCode, message);
}
}

/// <summary>
/// 添加单个事件
/// </summary>
/// <param name="eventCode">Event code</param>
/// <param name="mono">Mono</param>
public void Add(int eventCode, MonoBase mono)
{
List<MonoBase> list = null;

// 之前没有注册过
if (!dict.ContainsKey(eventCode))
{
list = new List<MonoBase>();
list.Add(mono);
dict.Add(eventCode, list);
return;
}

// 之前注册过
list = dict[eventCode];
list.Add(mono);
}

/// <summary>
/// 添加多个事件
/// 一个脚本关心多个事件
/// </summary>
/// <param name="eventCode">Event code</param>
public void Add(int[] eventCodes, MonoBase mono)
{
for (int i = 0; i < eventCodes.Length; i++)
{
Add(eventCodes[i], mono);
}
}

/// <summary>
/// 移除单个事件
/// </summary>
/// <param name="eventCode">Event code</param>
/// <param name="mono">Mono</param>
public void Remove(int eventCode, MonoBase mono)
{
// 没注册过 没法移除 报个警告
if (!dict.ContainsKey(eventCode))
{
Debug.LogWarning("没有这个事件" + eventCode + "注册");
return;
}

List<MonoBase> list = dict[eventCode];

if (list.Count == 1)
dict.Remove(eventCode);
else
list.Remove(mono);
}

/// <summary>
/// 移除多个事件
/// </summary>
/// <param name="eventCode">Event code</param>
/// <param name="mono">Mono</param>
public void Remove(int[] eventCodes, MonoBase mono)
{
for (int i = 0; i < eventCodes.Length; i++)
{
Remove(eventCodes[i], mono);
}
}
}

使用

原来的写法

1
2


存在的问题

使用框架的写法

AudioEvent.cs

1
2
3
4
5
6
7
using System;

public class AudioEvent
{
public const int PLAY_AUDIO = 0;

}

AudioManager.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 声音模块管理器
/// </summary>
public class AudioManager : ManagerBase
{
public const int PLAY_AUDIO = 0;

public static AudioManager Instance = null;

void Awake()
{
Instance = this;
}
}

AudioBase.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AudioBase : MonoBase
{
/// <summary>
/// 自身关心的消息集合
/// </summary>
List<int> list = new List<int>();

/// <summary>
/// 绑定一个或多个消息
/// </summary>
/// <param name="eventCodes">Event codes.</param>
protected void Bind(params int[] eventCodes)
{
list.AddRange(eventCodes);
AudioManager.Instance.Add(list.ToArray(), this);
}

/// <summary>
/// 接触绑定的消息
/// </summary>
protected void UnBind()
{
AudioManager.Instance.Remove(list.ToArray(), this);
list.Clear();
}

/// <summary>
/// 自动移除绑定的消息
/// </summary>
public virtual void OnDestroy()
{
if (list != null)
UnBind();
}

/// <summary>
/// 发消息
/// </summary>
/// <param name="areaCode">Area code</param>
/// <param name="eventCode">Event code</param>
/// <param name="message">Message</param>
public void Dispatch(int areaCode, int eventCode, object message)
{
MsgCenter.Instance.Dispatch(areaCode, eventCode, message);
}
}

MainAudioCtrl.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MainAudioCtrl : AudioBase
{

void Awake()
{
Bind(AudioEvent.PLAY_AUDIO);
}

public override void Execute(int eventCode, object message)
{
switch (eventCode)
{
case AudioEvent.PLAY_AUDIO:
Debug.Log("已经播放音乐 :" + message.ToString());
break;

default:
break;
}
}
}

UI

UIManager.cs

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

public class UIManager : ManagerBase
{
public static UIManager Instance = null;

void Awake()
{
Instance = this;
}
}

新建一个UI Panel,在其下创建一个按钮和Text,实现点击按钮Text内容改变。新建一个脚本TestPanel.cs。

TestPanel.cs

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TestPanel : UIBase
{
[SerializeField]
private Button btn;

[SerializeField]
private Text txt;

void Start()
{
btn.onClick.AddListener(onClick);
}

void onClick()
{
txt.text += "1";

// 还能播放声音
Dispatch(AreaCode.AUDIO, AudioEvent.PLAY_AUDIO, "audioName1");
Dispatch(AreaCode.UI, AudioEvent.PLAY_AUDIO, "audioName1");
}
}
0%