思考并回答以下问题:
本章涵盖:
- MVC代码模块设计
- 事件代码实现案例
- 窗体基类的实现案例
- 窗体子类代码实现案例
- 控制类实现案例
- 状态类设计实现
- 窗体管理类实现案例
- MVC案例分享
- 小结
前言
分工协作
因为程序的逻辑是与美术直接打交道的,这就要求程序给美术制定一定的规范,比如命名规范,资源大小规范包括材质和模型以及动作数量等。模型资源上的脚本要采用动态加载的方式,这样美术调整好资源可以直接运行查看其效果。特别强调命名规范是非常重要的。
框架
框架的设计主要是分为两部分:一部分是针对UI的框架设计,一部分是针对游戏场景中的设计。UI设计框架我们采用的是MVC设计模式,架构如下所示:
对于窗口之间的切换采用的是状态机模式,这样开发扩展只需要针对具体的逻辑编写就可以了,状态的变换都是按照同样的逻辑,扩展起来非常方便,这里面要注意一个问题,UI上的脚本都是动态绑定的。
接下来是游戏玩法架构:
游戏玩法架构采用的是模块化开发,把通用的模块抽离出来,对于一些APRG游戏角色会有战斗也是游戏的核心玩法,该架构采用的是有限状态机设计也就是FSM设计。有限状态机主要是根据动作状态的改变而设计的,这样策划配置不同的动作或者技能组合,FSM架构如下图所示:
其他模块的设计可以采用工厂模式的设计理念,比如技能系统的设计架构:
其他系统的设计跟这个类似,就不一一列举了。对于一些通用的在游戏编程必可少的,直接以模块化封装:
它们可以重用到其他项目开发中。
我们公司对代码都有自己的代码库,开发游戏的时候去随意组装就可以了,非常方便。我们的游戏对象除了一些不变化的脚本可以直接挂上去,其他的都要采用动态挂接的方式,动态挂接的方式主要目的是对美术和程序做一个分离,二者互不干涉,程序只需要事先对美术的命名规范要做一定的要求,美术必须严格执行这样的操作,特别是针对用户逻辑的资源,这样做可以极大的提升开发效率。我们团队利用该架构设计开发了两款游戏,开发游戏速度非常快。
当然任何东西都不是绝对的,开发适用于自己的框架才是最好的框架。
热更新
首先要明白热更新的含义,好多人以为只更新资源就属于热更新,其实不然,热更新的含义是在不改变原有包的基础上做出的既包括资源也包括逻辑代码的更新。大家以前玩端游的时候,经常在游戏启动时提示你需要更新,有加载进度条,其实它们就是做的热更新。移动端做热更新通常采用的方式是采用ulua做热更新,网上有这方面的资料。平台的SDK经常需要更新,出现这种情况就需要客户端强制更新,或者是出现大版本更新,也需要对客户端进行强制更新。
包体优化
开发包体优化主要是从代码方面,资源方面进行的。代码方面主要是从这几方面考虑:一是资源的清理,比如在加载进度条时把以前加载的资源释放掉,二是可以对资源进行预加载,增加游戏运行的流畅度,避免动态加载的时候出现卡顿情况,类似对象池的概念。
影响包体大小的主要是模型和贴图,模型的面数要适当的减少,材质要有自己的游戏材质库,这样一旦美术风格定下后,相应的材质库也要建立,这样避免美术不断的增加材质。贴图的格式采用的是,RGB格式采用的是jpg格式,RGBA采用的是png格式。
防破解、文本文件加密
现在很多破解软件,可以绕过计费系统或者屏蔽计费,把包体变成白包,面对这种问题,可以使用类似360加固这样的软件,避免计费文件被修改,这种是针对不破解包体就可以屏蔽计费的。还有一种是修改配置文件的,一般对于基础数据都是放到本地的,保护本地文件采用的方法是对其进行二进制加密,也可以对其文本文件进行SHA1算法压缩加密,然后在程序中对其进行解压读取。当然后一种方式只能防君子不能防小人。
移动端调试
在开发中经常会遇到在PC端没有任何问题,但是在移动端会出现各种问题,比如崩掉,卡顿,显示不全等。这就需要程序能够直接使用移动端调试,必须要学会在移动端调试,调试的方式有很多种:比如:eclipse的DDMS,可以查看其错误的位置;还有一个是手机端下载 Unity Remote.apk,然后通过mono编辑器或者是vs编辑器加断点调试,二者都可以通过profiler查看其内存占用情况。还有一种方式可以打印出log,用adb命令,在cmd中输入adblogcat后面加一些参数再加一个log.txt可以把log文件打印出来。
多语言
多语言版本这个也是开发游戏常用的,我们的多语言版本是把UI上的除了有限的图标外,所有的文字信息都放在一个文本文件中,把所有的语言都放进去,文件读取的时候一次性加入,程序根据UI的命名赋值给对应的UI,判断哪个国家的语言,可以通过读取系统语言获取,然后对应着加载不同的配置文件内容。
一键打包
一键打包通常运用在多渠道发行的时候,不同的渠道要求不同,对于只修改后缀的可以批量打成jar包将其对应的资源拷贝到Plugins对应的Android文件夹下面,这可以编写一个小工具实现文件的拷贝和编译,这些在Unity中都有接口,对于加品宣的需要单独处理。
MVC
MVC在游戏架构设计时经常用到,这也是作为开发者必须要掌握的技能,MVC全称是Model View Controller,中文含义是模型、视窗、控制器。它是非常经典的设计模式,已经在游戏设计中广泛运用,尤其是在UI界面架构设计中。MVC在游戏开发中的解释分别是:Model称为模型,它的作用是针对数据变化的,比如在网络游戏中,角色等级升级了,角色的经验值增加了,技能等级提升了等都涉及数据更新,这些都需要用到Model。View,顾名思义是视窗,也就是窗口显示,在游戏的UI中表示的是UI界面的显示。Controller是控制器,控制界面的显示以及数据的更新,比如玩家单击某个按钮的响应操作。MVC架构设计图如图1所示。
图1 MVC架构设计图
Controller模块可以与网络进行对接,用于控制数据Model在View上显示的数据,Controller也可以直接控制与View之间的切换。
MVC代码模块设计
在游戏开发中拿到策划需求后,不要急于编写代码,要先通过模块化架构设计理顺思路。开发者需要拿出时间仔细思考如何去做架构设计,如果只是简单的实现功能它不是产品开发,游戏产品中程序也是一个团队开发公用的,做的框架需要给其他程序员编写逻辑使用,所以前期考虑问题一定要全面。做架构设计时可以把UI架构和游戏玩法架构分别做架构设计,本节主要针对UI做架构设计,运用MVC思想设计UI架构。首先对View模块进行架构设计。众所周知,游戏中UI窗口很多,每个窗口都有一些独特的属性,但是它们也有一些共性。把共性抽离出来可以作为所有窗体的父类使用,后面的窗体逻辑继承于父类即可。View的架构设计如图2所示。
图2 窗体模块架构
以上架构只列举了几个窗体,对于游戏的窗体来说远远不够,但是游戏的窗体的实现原理是一样的。接下来继续在架构上做文章,初步的架构已经设计完成了,但是对于这么多窗体,我们是否还需要一个管理类来对它们进行统一管理呢?答案是肯定的。否则的话,每次编写代码调用每个具体的窗体操作是很麻烦的事情,一旦出现问题查找起来也是比较麻烦的,因此需要设计一个管理类对这些窗体进行统一管理。接下来在图2架构的基础上继续完善,增加窗体的管理类,架构如图3所示。
图3 窗体管理类架构
有读者可能会问,架构设计是否完成了?还没有完成,图3架构设计的只是关于窗体的模块,还没有将它们联系起来,我们要考虑一个问题就是模块之间的耦合性。降低模块之间的耦合性是很重要的,在游戏开发中经常遇到的问题就是解决模块之间的耦合性,关于解耦合可通过事件机制去处理,接下来先给大家介绍事件代码设计。
事件代码实现案例
事件机制主要用于模块之间解耦合,关于事件的封装在前面有过介绍,在本节封装的是改进版,只用一个文件实现所有事件功能。事件机制将各个模块联系在一起,并且它们之间的耦合性非常低,特别适用于游戏产品的模块化开发。事件的类型可以通过枚举或者字符串的形式表示,本节使用的是枚举。事件机制的实现原理是将表示不同事件的枚举与其对应的回调函数通过AddListener函数加到定义好的表中,触发回调函数通过Broadcast函数对已定义的事件枚举进行分类分发,从而触发已加载到内存的事件回调函数。如果不需要监听事件可通过RemoveListener函数移除监听事件。事件机制实现的完整代码如下所示。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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314using System;
using System.Collections.Generic;
using UnityEngine;
using Game;
static internal class EventCenter
{
static public Dictionary<EGameEvent, Delegate> mEventTable = new Dictionary<EGameEvent, Delegate>();
//消息处理程序不应该被移除
static public List< EGameEvent > mPermanentMessages = new List< EGameEvent > ();
//做一个永久性标记
static public void MarkAsPermanent(EGameEvent eventType)
{
Debug.Log("Messenger MarkAsPermanent \t\"" + eventType + "\"");
mPermanentMessages.Add( eventType );
}
static public void Cleanup()
{
Debug.Log("MESSENGER Cleanup. Make sure that none of necessary listeners are removed.");
List< EGameEvent > messagesToRemove = new List<EGameEvent>();
foreach (KeyValuePair<EGameEvent, Delegate> pair in mEventTable)
{
bool wasFound = false;
foreach (EGameEvent message in mPermanentMessages)
{
if (pair.Key == message)
{
wasFound = true;
break;
}
}
if (!wasFound)
messagesToRemove.Add( pair.Key );
}
foreach (EGameEvent message in messagesToRemove)
{
mEventTable.Remove( message );
}
}
static public void PrEGameEventEventTable()
{
foreach (KeyValuePair<EGameEvent, Delegate> pair in mEventTable)
{
Debug.Log("\t\t\t" + pair.Key + "\t\t" + pair.Value);
}
Debug.Log("\n");
}
static public void OnListenerAdding(EGameEvent eventType, Delegate listenerBeingAdded)
{
if (!mEventTable.ContainsKey(eventType))
{
mEventTable.Add(eventType, null );
}
Delegate d = mEventTable[eventType];
if (d != null && d.GetType() != listenerBeingAdded.GetType())
{
throw new ListenerException(string.Format("Attempting to add listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being added has type {2}", eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
}
}
static public void OnListenerRemoving(EGameEvent eventType, Delegate listenerBeingRemoved)
{
if (mEventTable.ContainsKey(eventType))
{
Delegate d = mEventTable[eventType];
if (d == null)
{
throw new ListenerException(string.Format("Attempting to remove listener with for event type \"{0}\" but current listener is null.", eventType));
} else if (d.GetType() != listenerBeingRemoved.GetType())
{
throw new ListenerException(string.Format("Attempting to remove listener with inconsistent signature for event type {0}. Current listeners have type {1} and listener being removed has type {2}", eventType, d.GetType().Name, listenerBeingRemoved.GetType().Name));
}
} else
{
throw new ListenerException(string.Format("Attempting to remove listener for type \"{0}\" but Messenger doesn't know about this event type.", eventType));
}
}
static public void OnListenerRemoved(EGameEvent eventType)
{
if (mEventTable[eventType] == null)
{
mEventTable.Remove(eventType);
}
}
static public void OnBroadcasting(EGameEvent eventType)
{
if (!mEventTable.ContainsKey(eventType))
{
}
}
static public BroadcastException CreateBroadcastSignatureException(EGameEvent eventType)
{
return new BroadcastException(string.Format("Broadcasting message \"{0}\" but listeners have a different signature than the broadcaster.", eventType));
}
public class BroadcastException : Exception
{
public BroadcastException(string msg)
: base(msg)
{
}
}
public class ListenerException : Exception
{
public ListenerException(string msg)
: base(msg)
{
}
}
//无参数增加监听
static public void AddListener(EGameEvent eventType, Callback handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback)mEventTable[eventType] + handler;
}
//单个参数增加监听
static public void AddListener<T>(EGameEvent eventType, Callback<T> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T>)mEventTable[eventType] + handler;
}
//两个参数增加监听
static public void AddListener<T, U>(EGameEvent eventType, Callback<T, U> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] + handler;
}
//三个参数增加监听
static public void AddListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] + handler;
}
//四个参数增加监听
static public void AddListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] + handler;
}
//无参数移除监听
static public void RemoveListener(EGameEvent eventType, Callback handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//单个参数移除监听
static public void RemoveListener<T>(EGameEvent eventType, Callback<T> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//两个参数移除监听
static public void RemoveListener<T, U>(EGameEvent eventType, Callback<T, U> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//三个参数移除监听
static public void RemoveListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//四个参数移除事件监听
static public void RemoveListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//无参数分发
static public void Broadcast(EGameEvent eventType)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d))
{
Callback callback = d as Callback;
if (callback != null)
{
callback();
}
else
{
throw CreateBroadcastSignatureException(eventType);
}
}
}
static public void SendEvent(CEvent evt)
{
Broadcast<CEvent>(evt.GetEventId(), evt);
}
//单个参数分发
static public void Broadcast<T>(EGameEvent eventType, T arg1)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d))
{
Callback<T> callback = d as Callback<T>;
if (callback != null)
{
callback(arg1);
}
else
{
throw CreateBroadcastSignatureException(eventType);
}
}
}
//两个参数分发
static public void Broadcast<T, U>(EGameEvent eventType, T arg1, U arg2)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d))
{
Callback<T, U> callback = d as Callback<T, U>;
if (callback != null)
{
callback(arg1, arg2);
}
else
{
throw CreateBroadcastSignatureException(eventType);
}
}
}
//三个参数分发
static public void Broadcast<T, U, V>(EGameEvent eventType, T arg1, U arg2, V arg3)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d))
{
Callback<T, U, V> callback = d as Callback<T, U, V>;
if (callback != null)
{
callback(arg1, arg2, arg3);
}
else
{
throw CreateBroadcastSignatureException(eventType);
}
}
}
//四个参数分发
static public void Broadcast<T, U, V, X>(EGameEvent eventType, T arg1, U arg2, V arg3, X arg4)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d))
{
Callback<T, U, V, X> callback = d as Callback<T, U, V, X>;
if (callback != null)
{
callback(arg1, arg2, arg3, arg4);
}
else
{
throw CreateBroadcastSignatureException(eventType);
}
}
}
}
整个事件代码已完成,再给大家解释一下代码中的常用接口部分,下面的代码是需要经常调用的监听函数,从无参数的监听函数到有四个参数的监听函数的实现,函数代码如下所示。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//无参数的监听函数
static public void AddListener(EGameEvent eventType, Callback handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback)mEventTable[eventType] + handler;
}
//一个参数的监听函数
static public void AddListener<T>(EGameEvent eventType, Callback<T> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T>)mEventTable[eventType] + handler;
}
//二个参数的监听函数
static public void AddListener<T, U>(EGameEvent eventType, Callback<T, U> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] + handler;
}
//三个参数的监听函数
static public void AddListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] + handler;
}
//四个参数的监听函数
static public void AddListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler)
{
OnListenerAdding(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] + handler;
}
可以根据自己的实际情况调用不同的监听函数,下面的代码是移除监听,移除代码的作用是如果事件已经监听完成,且不再需要监听,那么可以通过函数RemoveListener将其移除,它与Addlistener函数是一一对应的,调用函数接口如下。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//无参数的移除监听函数
static public void RemoveListener(EGameEvent eventType, Callback handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//单个参数的移除监听函数
static public void RemoveListener<T>(EGameEvent eventType, Callback<T> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//两个参数的移除监听函数
static public void RemoveListener<T, U>(EGameEvent eventType, Callback<T, U> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//三个参数的移除监听函数
static public void RemoveListener<T, U, V>(EGameEvent eventType, Callback<T, U, V> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
//四个参数的移除监听函数
static public void RemoveListener<T, U, V, X>(EGameEvent eventType, Callback<T, U, V, X> handler)
{
OnListenerRemoving(eventType, handler);
mEventTable[eventType] = (Callback<T, U, V, X>)mEventTable[eventType] - handler;
OnListenerRemoved(eventType);
}
最后是关于事件的消息分发,也就是触发监听事件的回调函数,先调用监听函数AddListener后,才可以使用Broadcast对事件消息进行分发操作。二者有前后关系,函数代码如下。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//无参数的消息事件广播函数
static public void Broadcast(EGameEvent eventType)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d)) {
Callback callback = d as Callback;
if (callback != null) {
callback();
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
// 发送事件
static public void SendEvent(CEvent evt)
{
Broadcast<CEvent>(evt.GetEventId(), evt);
}
// 单个参数的消息事件广播函数
static public void Broadcast<T>(EGameEvent eventType, T arg1) {
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d)) {
Callback<T> callback = d as Callback<T>;
if (callback != null) {
callback(arg1);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
//两个参数的消息事件广播函数
static public void Broadcast<T, U>(EGameEvent eventType, T arg1, U arg2)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d)) {
Callback<T, U> callback = d as Callback<T, U>;
if (callback != null) {
callback(arg1, arg2);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
// 三个参数的消息事件广播函数
static public void Broadcast<T, U, V>(EGameEvent eventType, T arg1, U arg2, V arg3)
{
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d)) {
Callback<T, U, V> callback = d as Callback<T, U, V>;
if (callback != null) {
callback(arg1, arg2, arg3);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
//四个参数的消息事件广播函数
static public void Broadcast<T, U, V, X>(EGameEvent eventType, T arg1, U arg2, V arg3, X arg4) {
OnBroadcasting(eventType);
Delegate d;
if (mEventTable.TryGetValue(eventType, out d)) {
Callback<T, U, V, X> callback = d as Callback<T, U, V, X>;
if (callback != null) {
callback(arg1, arg2, arg3, arg4);
} else {
throw CreateBroadcastSignatureException(eventType);
}
}
}
在分发消息的函数中增加了static public void SendEvent(CEvent evt)函数用于消息的发送,它内部是调用BroadCast函数,所以其实现方式与BroadCast原理是一样的,接下来介绍窗体基类的实现案例。
窗体基类的实现案例
对于游戏中的窗体,从它们身上可以看到很多共性,最基本的功能是窗体的初始化、窗体显示、窗体隐藏,这些功能对于所有窗体都是需要的,因此可以将它们抽离出来,单独放到一个类里面作为窗体的父类,游戏中所有的窗体都继承这个父类。窗体父类实现的是所有窗口的共性方法或者属性,子类只负责实现自己独有的方法,共性的方法直接继成父类即可,基类的完整代码如下。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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177using UnityEngine;
using System.Collections;
using Game;
using GameDefine;
namespace Game.View
{
public abstract class BaseWindow
{
protected Transform mRoot;
protected EScenesType mScenesType; //场景类型
protected string mResName; //资源名
protected bool mResident; //是否常驻
protected bool mVisible = false; //是否可见
//类对象初始化
public abstract void Init();
//类对象释放
public abstract void Realse();
//窗口控制初始化
protected abstract void InitWidget();
//窗口控件释放
protected abstract void RealseWidget();
//游戏事件注册
protected abstract void OnAddListener();
//游戏事件注销
protected abstract void OnRemoveListener();
//显示初始化
public abstract void OnEnable();
//隐藏处理
public abstract void OnDisable();
//每帧更新
public virtual void Update(float deltaTime) { }
//取得所有场景类型
public EScenesType GetScenseType()
{
return mScenesType;
}
//是否已打开
public bool IsVisible() { return mVisible; }
//是否常驻
public bool IsResident() { return mResident; }
//显示
public void Show()
{
if (mRoot == null)
{
if (Create())
{
InitWidget();
}
}
if (mRoot && mRoot.gameObject.activeSelf == false)
{
mRoot.gameObject.SetActive(true);
mVisible = true;
OnEnable();
OnAddListener();
}
}
//隐藏
public void Hide()
{
if (mRoot && mRoot.gameObject.activeSelf == true)
{
OnRemoveListener();
OnDisable();
if (mResident)
{
mRoot.gameObject.SetActive(false);
}
else
{
RealseWidget();
Destroy();
}
}
mVisible = false;
}
//预加载
public void PreLoad()
{
if (mRoot == null)
{
if (Create())
{
InitWidget();
}
}
}
//延时删除
public void DelayDestory()
{
if (mRoot)
{
RealseWidget();
Destroy();
}
}
//创建窗体
private bool Create()
{
if (mRoot)
{
Debug.LogError("Window Create Error Exist!");
return false;
}
if (mResName == null || mResName == "")
{
Debug.LogError("Window Create Error ResName is empty!");
return false;
}
if (GameMethod.GetUiCamera.transform== null)
{
Debug.LogError("Window Create Error GetUiCamera is empty! WindowName = " + mResName);
return false;
}
GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);
if (obj == null)
{
Debug.LogError("Window Create Error LoadRes WindowName = " + mResName);
return false;
}
mRoot = obj.transform;
mRoot.gameObject.SetActive(false);
return true;
}
//销毁窗体
protected void Destroy()
{
if (mRoot)
{
LoadUiResource.DestroyLoad(mRoot.gameObject);
mRoot = null;
}
}
//取得根节点
public Transform GetRoot()
{
return mRoot;
}
}
}
在窗体封装的父类代码中,显示函数Show、隐藏函数Hide对于任何窗体都是通用的。因此可以在父类中将其方法实现出来,子类继承该方法可以直接调用,子类无须重新写。当然还有一些公用的函数定义是需要子类自己实现的,父类使用了一些抽象的函数声明,这些抽象函数是针对子类的。先介绍通用函数Show,其作用就是将窗体先创建出来,并对窗体中的控件进行初始化操作,同时增加了消息监听函数的接口。函数代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//显示
public void Show()
{
if (mRoot == null)
{
if (Create())
{
InitWidget();
}
}
if (mRoot && mRoot.gameObject.activeSelf == false)
{
mRoot.gameObject.SetActive(true);
mVisible = true;
OnEnable();
OnAddListener();
}
}
在上文的函数中调用Create函数,用于资源的加载创建,同时将创建的窗体设为不可见,这种处理方式跟预加载机制非常像。该函数实现代码如下所示。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//创建窗体
private bool Create()
{
if (mRoot)
{
Debug.LogError("Window Create Error Exist!");
return false;
}
if (mResName == null || mResName == "")
{
Debug.LogError("Window Create Error ResName is empty!");
return false;
}
if (GameMethod.GetUiCamera.transform== null)
{
Debug.LogError("Window Create Error GetUiCamera is empty! WindowName = " + mResName);
return false;
}
GameObject obj = LoadUiResource.LoadRes(GameMethod.GetUiCamera.transform, mResName);
if (obj == null)
{
Debug.LogError("Window Create Error LoadRes WindowName = " + mResName);
return false;
}
mRoot = obj.transform;
mRoot.gameObject.SetActive(false);
return true;
}
下面介绍一下UI界面的隐藏。隐藏分为两种,一种是将其破坏掉,另一种是将其真正隐藏起来。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21//隐藏
public void Hide()
{
if (mRoot && mRoot.gameObject.activeSelf == true)
{
OnRemoveListener();
OnDisable();
if (mResident)
{
mRoot.gameObject.SetActive(false);
}
else
{
RealseWidget();
Destroy();
}
}
mVisible = false;
}
为了方便开发者调用,实现了一个预加载函数,对于一些资源,如果要预先加载到内存里可以调用该函数,函数代码如下所示。1
2
3
4
5
6
7
8
9
10
11//预加载
public void PreLoad()
{
if (mRoot == null)
{
if (Create())
{
InitWidget();
}
}
}
以上是关于窗体父类核心函数的讲解,窗体父类设计完成,接下来继续窗体子类代码讲解。
窗体子类代码实现案例
为了加深印象,在这里重复一遍父类和子类的关系:“父类窗体实现了所有窗体的共性,子类的实现就需要继承父类,子类自己独有的方法或者属性在其子类中实现”。在写子类之前大家可以思考两个问题,子类的窗体需要有哪些信息?如何去编写子类的代码?这两个问题是架构设计时必须要解决的。
下面分析一下子类的编写,首先子类窗体是一个assetbundle资源或者说是实例化的物体,这样是为了便于程序动态的加载。资源加载完成后,这个资源是否可以常驻内存,可以为此加个标记,因为有的界面要经常使用,对于这样的界面设计时就要考虑不要将其卸载掉。接下来运用已经封装好的事件系统,以及用于窗体的显示和隐藏接口。另外子类中需要窗体的初始化操作,以及单击按钮时的回调触发函数,因为脚本不继承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
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using GameDefine;
using UICommon;
using Game;
using Game.GameData;
using Game.Network;
using System.Linq;
using Game.Ctrl;
namespace Game.View
{
public class LoginWindow : BaseWindow
{
public LoginWindow()
{
mScenesType = EScenesType.EST_Login;
mResName = GameConstDefine.LoadGameLoginUI;
mResident = false;
}
////////////////////////////继承接口/////////////////////////
//类对象初始化
public override void Init()
{
EventCenter.AddListener(EGameEvent.eGameEvent_LoginEnter, Show);
EventCenter.AddListener(EGameEvent.eGameEvent_LoginExit, Hide);
}
//类对象释放
public override void Realse()
{
EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginEnter, Show);
EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginExit, Hide);
}
//窗口控件初始化
protected override void InitWidget()
{
mLoginParent = mRoot.FindChild("Server_Choose");
mLoginInput = mRoot.FindChild("Server_Choose/Loginer");
mLoginSubmit = mRoot.FindChild("Server_Choose/Button");
mPlayParent = mRoot.Find("LoginBG");
mPlaySubmitBtn = mRoot.Find("LoginBG/LoginBtn");
UIEventListener.Get(mPlaySubmitBtn.gameObject).onClick += OnPlaySubmit;
UIEventListener.Get(mPlayServerBtn.gameObject).onClick += OnPlayServer;
UIEventListener.Get(mReLoginSubmit.gameObject).onClick += OnReLoginSubmit;
UIEventListener.Get(mLoginSubmit.gameObject).onClick += OnLoginSubmit;
}
//窗口控件释放
protected override void RealseWidget()
{
}
//游戏事件注册
protected override void OnAddListener()
{
EventCenter.AddListener(EGameEvent.eGameEvent_LoginSuccess, LoginSuceess);
}
//游戏事件注销
protected override void OnRemoveListener()
{
EventCenter.RemoveListener(EGameEvent.eGameEvent_LoginSuccess, LoginSuceess);
}
//显示
public override void OnEnable()
{
}
//隐藏
public override void OnDisable()
{
}
////////////////////////////////UI事件响应////////////////////////////////////
void OnPlaySubmit(GameObject go)
{
mWaitingParent.gameObject.SetActive(true);
UIEventListener.Get(mPlaySubmitBtn.gameObject).onClick -= OnPlaySubmit;
LoginCtrl.Instance.GamePlay();
}
void OnPlayServer(GameObject go)
{
ShowServer(LOGINUI.SelectServer);
}
void OnChangeAccount(GameObject go)
{
LoginCtrl.Instance.SdkLogOff();
}
void OnReLoginSubmit(GameObject go)
{
mReLoginParent.gameObject.SetActive(false);
LoginCtrl.Instance.SdkLogOff();
}
void OnLoginSubmit(GameObject go)
{
mWaitingParent.gameObject.SetActive(true);
LoginCtrl.Instance.Login(mLoginAccountInput.value, mLoginPassInput.value);
}
//登录成功
void LoginSuceess()
{
UIEventListener.Get(mPlaySubmitBtn.gameObject).onClick -= OnPlaySubmit;
}
}
}
在LoginWindow类中实现了一些逻辑的调用,大部分都是用封装好的事件机制实现的。下面给大家介绍一下关于代码的实现,构造函数主要实现了资源的加载,以及对是否常驻内存做了一个标记,函数代码如下所示。1
2
3
4
5
6public LoginWindow()
{
mScenesType = EScenesType.EST_Login;
mResName = GameConstDefine.LoadGameLoginUI;
mResident = false;
}
第一行表示的是资源类型,资源类型主要分为两种:Login和Play,使用的是定义好的枚举。1
2
3
4
5
6public enum EScenesType
{
EST_None,
EST_Login,
EST_Play,
}
Init函数主要是实现监听函数功能,它继承于它的父类BaseWindow,添加了监听消息函数AddListener,通过监听函数实现窗体显示Show和隐藏Hide的回调。1
2
3
4
5
6//类对象初始化
public override void Init()
{
EventCenter.AddListener(EGameEvent.eGameEvent_LoginEnter, Show);
EventCenter.AddListener(EGameEvent.eGameEvent_LoginExit, Hide);
}
InitWidget函数主要作用是初始化需要操作界面的各个按钮回调的响应,这样单击界面按钮时会触发响应的回调函数。它继承了父类的InitWidget函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//窗口控件初始化
protected override void InitWidget()
{
mLoginParent = mRoot.FindChild("Server_Choose");
mLoginInput = mRoot.FindChild("Server_Choose/Loginer");
mLoginSubmit = mRoot.FindChild("Server_Choose/Button");
mPlayParent = mRoot.Find("LoginBG");
mPlaySubmitBtn = mRoot.Find("LoginBG/LoginBtn");
UIEventListener.Get(mPlaySubmitBtn.gameObject).onClick += OnPlaySubmit;
UIEventListener.Get(mPlayServerBtn.gameObject).onClick += OnPlayServer;
UIEventListener.Get(mReLoginSubmit.gameObject).onClick += OnReLoginSubmit;
UIEventListener.Get(mLoginSubmit.gameObject).onClick += OnLoginSubmit;
}
InitWidget函数利用UIEventListener进行窗体Button事件监听,这么做的好处是不需要把脚本挂接到UI上。该函数对应的UI界面如图4所示。
图4 UI界面
如果Login界面不涉及数据的变化处理,则不需要Model模块。子类窗体的实现都可以采用上述脚本的实现方式,这里就不一一举例了,其他窗体按照这个模式编写就可以了,接下来开始MVC模式的Controller模块编写。
控制类实现案例
本节讲解MVC中的Controller控制类设计,控制类是负责控制View窗体显示的,它的控制方式是通过消息事件的监听和分发实现的,这也有效地降低了模块之间的耦合性,它的实现方式是继承单例模式,它不继承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
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
55using UnityEngine;
using System;
using System.Collections;
using Game;
using Game.GameData;
using Game.Network;
using System.IO;
using System.Linq;
using Game.Model;
namespace Game.Ctrl
{
public class LoginCtrl : Singleton<LoginCtrl>
{
public void Enter()
{
EventCenter.Broadcast(EGameEvent.eGameEvent_LoginEnter);
}
public void Exit()
{
EventCenter.Broadcast(EGameEvent.eGameEvent_LoginExit);
}
//登录
public void Login(string account, string pass)
{
SelectServerData.Instance.SetServerInfo((int)SdkManager.Instance.GetPlatFrom(), account, pass);
NetworkManager.Instance.canReconnect = false;
NetworkManager.Instance.Close();
NetworkManager.Instance.Init(GameLogic.Instance.LoginServerAdress, 49996, NetworkManager.ServerType.LoginServer);
}
//登录错误反馈
public void LoginError(int code)
{
MsgInfoManager.Instance.ShowMsg(code);
EventCenter.Broadcast<EErrorCode>(EGameEvent.eGameEvent_LoginError, (EErrorCode)code);
}
//登录失败
public void LoginFail()
{
NetworkManager.Instance.canReconnect = false;
EventCenter.Broadcast(EGameEvent.eGameEvent_LoginFail);
}
//开始游戏
public void GamePlay()
{
}
}
}
在该函数中读者可能注意到,它的分发消息是在Enter函数中,移除消息监听是在Exit函数中,因为它不继承mono,所以必须有其他类来调用Controller控制模块的方法才能运行。那如何去调用Controller控制类呢?请看下一节节状态类设计实现。
状态类设计实现
上一节讲述了MVC中的View和Controller模块都不继承于mono,方便扩展,接下来设计的状态切换模式也是不继承mono的,用于View窗体的切换。下面开始状态模式的框架设计,首先设计一个状态的父类,这个父类将其定义为一个抽象类。抽象类架构设计如图5所示。
图5 游戏抽象类架构设计
图5中iGameState作为所有状态的父类,状态模式是设计模式中的一种,它是根据物体状态的改变而改变的行为,所以在设计父类时,需要考虑到以下几点:设置某个状态,获取某个状态,进入某个状态,状态更新,状态停止,等等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using UnityEngine;
using System.Collections;
namespace Game.GameState
{
public interface IGameState
{
GameStateType GetStateType();
void SetStateTo(GameStateType gsType);
void Enter();
GameStateType Update(float fDeltaTime);
void FixedUpdate(float fixedDeltaTime);
void Exit();
}
}
父类有了以后,接下来开始设计子类。以LoginState为例,LoginState是登录窗体的具体状态,它继承上述写的父类,状态主要是通过MVC中的Controller去控制窗体的显示,完整的代码如下所示。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
83using UnityEngine;
using System.Collections;
using GameDefine;
using Game.Resource;
using Game.Ctrl;
namespace Game.GameState
{
class LoginState : IGameState
{
GameStateType stateTo;
GameObject mScenesRoot;
GameObject mUIRoot;
public LoginState()
{
}
public GameStateType GetStateType()
{
return GameStateType.GS_Login;
}
public void SetStateTo(GameStateType gs)
{
stateTo = gs;
}
public void Enter()
{
SetStateTo(GameStateType.GS_Continue);
ResourceUnit sceneRootUnit = ResourcesManager.Instance.loadImmediate(GameConstDefine.GameLogin, ResourceType.PREFAB);
mScenesRoot = GameObject.Instantiate(sceneRootUnit.Asset) as GameObject;
LoginCtrl.Instance.Enter();
ResourceUnit audioClipUnit = ResourcesManager.Instance.loadImmediate(AudioDefine.PATH_UIBGSOUND, ResourceType.ASSET);
AudioClip clip = audioClipUnit.Asset as AudioClip;
AudioManager.Instance.PlayBgAudio(clip);
EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
}
public void Exit()
{
EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
EventCenter.RemoveListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
LoginCtrl.Instance.Exit();
GameObject.DestroyImmediate(mScenesRoot);
}
public void FixedUpdate(float fixedDeltaTime)
{
}
public GameStateType Update(float fDeltaTime)
{
return stateTo;
}
public void OnEvent(CEvent evt)
{
UIPlayMovie.PlayMovie("cg.mp4", Color.black, 2/* FullScreenMovieControlMode.Hidden*/, 3/*FullScreenMovieScalingMode.Fill*/);
switch (evt.GetEventId())
{
case EGameEvent.eGameEvent_InputUserData:
SetStateTo(GameStateType.GS_User);
break;
case EGameEvent.eGameEvent_IntoLobby:
GameStateManager.Instance.ChangeGameStateTo(GameStateType.GS_Lobby);
break;
}
}
}
}
我把类中的主要函数给读者介绍一下。在该类中使用了Enter函数的定义,在设计Controller时也定义了一个函数Enter,该函数在该类中具有非常重要的作用,它实现的内容包括:设置当前状态,调用Controller的Enter接口显示View层的UI,因为View层的UI显示面板需要将其实例化出来。面板的实例化也是在State状态类中去实现的,同时也可以包括声音、音效的加载以及其他逻辑的实现,函数代码如下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public void Enter()
{
SetStateTo(GameStateType.GS_Continue);
ResourceUnit sceneRootUnit = ResourcesManager.Instance.loadImmediate(GameConstDefine.GameLogin, ResourceType.PREFAB);
mScenesRoot = GameObject.Instantiate(sceneRootUnit.Asset) as GameObject;
//调用Controller控制类的Enter函数
LoginCtrl.Instance.Enter();
ResourceUnit audioClipUnit = ResourcesManager.Instance.loadImmediate(AudioDefine.PATH_UIBGSOUND, ResourceType.ASSET);
AudioClip clip = audioClipUnit.Asset as AudioClip;
AudioManager.Instance.PlayBgAudio(clip);
EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_InputUserData, OnEvent);
EventCenter.AddListener<CEvent>(EGameEvent.eGameEvent_IntoLobby, OnEvent);
}
通过类的继承关系知道,它也不是继承mono的,所以它有自己的构造函数,它继承了父类的Enter函数。这个函数主要是处理状态的改变,以及将窗体的实例化,同时调用MVC的Controller模块中的Enter函数进行View窗体的显示。当然从逻辑上说在该函数中可以添加事件的监听。前面已经提过State没有继承mono,所以它不能挂到对象上直接使用。它调用与它相关的类挂到对象上,每个窗体都有自己的State状态,这么多状态也需要一个状态管理类去管理控制,下面实现GameStateManager类的设计。其架构设计和上面讲述的窗体设计的管理类类似,这里就不展示架构图了,先把完整的代码实现写出来。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
113
114using UnityEngine;
using System.Collections;
using System.Collections.Generic;
namespace Game.GameState
{
public enum GameStateType
{
GS_Continue,
GS_Login,
GS_User,
GS_Lobby,
GS_Room,
GS_Hero,
GS_Loading,
GS_Play,
GS_Over,
}
public class GameStateManager : Singleton<GameStateManager>
{
Dictionary<GameStateType, IGameState> gameStates;
IGameState currentState;
public GameStateManager()
{
gameStates = new Dictionary<GameStateType, IGameState>();
IGameState gameState;
gameState = new LoginState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new UserState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new LobbyState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new RoomState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new HeroState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new LoadingState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new PlayState();
gameStates.Add(gameState.GetStateType(), gameState);
gameState = new OverState();
gameStates.Add(gameState.GetStateType(), gameState);
}
public IGameState GetCurState()
{
return currentState;
}
public void ChangeGameStateTo(GameStateType stateType)
{
if(currentState!=null&¤tState.GetStateType()!= GameStateType.GS_Loading && currentState.GetStateType() == stateType)
return;
if (gameStates.ContainsKey(stateType))
{
if (currentState != null)
{
currentState.Exit();
}
currentState = gameStates[stateType];
currentState.Enter();
}
}
public void EnterDefaultState()
{
ChangeGameStateTo(GameStateType.GS_Login);
}
public void FixedUpdate(float fixedDeltaTime)
{
if (currentState != null)
{
currentState.FixedUpdate(fixedDeltaTime);
}
}
public void Update(float fDeltaTime)
{
GameStateType nextStateType = GameStateType.GS_Continue;
if (currentState != null)
{
nextStateType = currentState.Update(fDeltaTime);
}
if (nextStateType > GameStateType.GS_Continue)
{
ChangeGameStateTo(nextStateType);
}
}
public IGameState getState(GameStateType type)
{
if (!gameStates.ContainsKey(type))
{
return null;
}
return gameStates[type];
}
}
}
通过上面代码可以看出,状态管理类的实现功能通过枚举变量public enum GameStateType,把游戏中涉及的所有状态列出来,状态的变换是通过设置的枚举值作为标记实现的。状态管理类首先要做的是把所有的状态在管理类的构造函数中加入到字典Dictionary中,便于统一管理。声明语句如下所示。
Dictionary
在构造函数public GameStateManager()中将所有的状态存储到已声明的字典中便于读取。在该类中最重要的函数是:public void ChangeGameStateTo(GameStateType stateType),这个是对外提供的,可以单独使用,也可以在函数public voidupdate(float timeDelta)中去设置。该函数的主要作用是改变游戏状态,它也是状态管理类中最重要的函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16public void ChangeGameStateTo(GameStateType stateType)
{
if(currentState!=null&¤tState.GetStateType()!= GameStateType.GS_Loading && currentState.GetStateType() == stateType)
return;
if (gameStates.ContainsKey(stateType))
{
if (currentState != null)
{
currentState.Exit();
}
currentState = gameStates[stateType];
currentState.Enter();
}
}
该函数在窗口切换中非常重要,凡是涉及状态变换都要调用这个函数,这样就为逻辑实现了统一的接口调用,下面开始窗体管理类的案例讲解。
窗体管理类实现案例
窗体管理类的主要作用是管理游戏页面中的所有UI面板。在现实生活中,买汽车都要到车辆管理所登记,方便对车进行管理,将其思想应用到程序里面就是所有在游戏中使用的UI都要在窗体管理类中注册。当然管理类不只是负责注册,游戏UI要互相切换,编写逻辑不能把UI之间的切换写死,要有个关于UI切换的统一流程,这样才能真正地实现切换功能。下面把窗口管理类完整的代码给大家展示一下。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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Game;
using Game.GameState;
namespace Game.View
{
public enum EScenesType
{
EST_None,
EST_Login,
EST_Play,
}
public enum EWindowType
{
EWT_LoginWindow, //登录
EWT_UserWindow, //用户
EWT_LobbyWindow,
EWT_BattleWindow,
EWT_RoomWindow,
EWT_HeroWindow,
EWT_BattleInfoWindow,
EWT_MarketWindow,
EWT_MarketHeroListWindow,
EWT_MarketHeroInfoWindow,
EWT_MarketRuneListWindow,
EWT_MarketRuneInfoWindow,
EWT_SocialWindow,
EWT_GamePlayWindow,
EWT_InviteWindow,
EWT_ChatTaskWindow,
EWT_ScoreWindow,
EWT_InviteAddRoomWindow,
EWT_RoomInviteWindow,
EWT_TeamMatchWindow,
EWT_TeamMatchInvitationWindow,
EWT_TeamMatchSearchingWindow,
EWT_MailWindow,
EWT_HomePageWindow,
EWT_PresonInfoWindow,
EWT_ServerMatchInvitationWindow,
EWT_SoleSoldierWindow,
EWT_MessageWindow,
EWT_MiniMapWindow,
EWT_VIPPrerogativeWindow,
EWT_RuneEquipWindow,
EWT_DaliyBonusWindow,
EWT_EquipmentWindow,
EWT_SystemNoticeWindow,
EWT_TimeDownWindow,
EWT_RuneCombineWindow,
EWT_HeroDatumWindow,
EWT_RuneRefreshWindow,
EWT_GamePlayGuideWindow,
EMT_PurchaseSuccessWindow,
EMT_GameSettingWindow,
EMT_AdvancedGuideWindow,
EMT_ExtraBonusWindow,
EMT_EnemyWindow,
EMT_HeroTimeLimitWindow,
EMT_SkillWindow,
EMT_SkillDescribleWindow,
EMT_RuneBuyWindow,
EMT_DeathWindow,
}
public class WindowManager : Singleton<WindowManager>
{
public WindowManager()
{
mWidowDic = new Dictionary<EWindowType, BaseWindow>();
mWidowDic[EWindowType.EWT_LoginWindow] = new LoginWindow();
mWidowDic[EWindowType.EWT_UserWindow] = new UserInfoWindow();
mWidowDic[EWindowType.EWT_LobbyWindow] = new LobbyWindow();
mWidowDic[EWindowType.EWT_BattleWindow] = new BattleWindow();
mWidowDic[EWindowType.EWT_RoomWindow] = new RoomWindow();
mWidowDic[EWindowType.EWT_HeroWindow] = new HeroWindow();
mWidowDic[EWindowType.EWT_BattleInfoWindow] = new BattleInfoWindow();
mWidowDic[EWindowType.EWT_MarketWindow] = new MarketWindow();
mWidowDic[EWindowType.EWT_MarketHeroListWindow] = new MarketHeroListWindow();
mWidowDic[EWindowType.EWT_MarketHeroInfoWindow] = new MarketHeroInfoWindow();
mWidowDic[EWindowType.EWT_SocialWindow] = new SocialWindow();
mWidowDic[EWindowType.EWT_GamePlayWindow] = new GamePlayWindow();
mWidowDic[EWindowType.EWT_InviteWindow] = new InviteWindow();
mWidowDic[EWindowType.EWT_ChatTaskWindow] = new ChatTaskWindow();
mWidowDic[EWindowType.EWT_ScoreWindow] = new ScoreWindow();
mWidowDic[EWindowType.EWT_InviteAddRoomWindow] = new InviteAddRoomWindow();
mWidowDic[EWindowType.EWT_RoomInviteWindow] = new RoomInviteWindow();
mWidowDic[EWindowType.EWT_TeamMatchWindow] = new TeamMatchWindow();
mWidowDic[EWindowType.EWT_TeamMatchInvitationWindow] = new TeamMatchInvitationWindow();
mWidowDic[EWindowType.EWT_TeamMatchSearchingWindow] = new TeamMatchSearchingWindow();
mWidowDic[EWindowType.EWT_MailWindow] = new MailWindow();
mWidowDic[EWindowType.EWT_HomePageWindow] = new HomePageWindow();
mWidowDic[EWindowType.EWT_PresonInfoWindow] = new PresonInfoWindow();
mWidowDic[EWindowType.EWT_ServerMatchInvitationWindow] = new ServerMatchInvitationWindow();
mWidowDic[EWindowType.EWT_SoleSoldierWindow] = new SoleSoldierWindow();
mWidowDic[EWindowType.EWT_MessageWindow] = new MessageWindow();
mWidowDic[EWindowType.EWT_MarketRuneListWindow] = new MarketRuneListWindow();
mWidowDic[EWindowType.EWT_MiniMapWindow] = new MiniMapWindow();
mWidowDic[EWindowType.EWT_MarketRuneInfoWindow] = new MarketRuneInfoWindow();
mWidowDic[EWindowType.EWT_VIPPrerogativeWindow] = new VIPPrerogativeWindow();
mWidowDic[EWindowType.EWT_RuneEquipWindow] = new RuneEquipWindow();
mWidowDic[EWindowType.EWT_DaliyBonusWindow] = new DaliyBonusWindow();
mWidowDic[EWindowType.EWT_EquipmentWindow] = new EquipmentWindow();
mWidowDic[EWindowType.EWT_SystemNoticeWindow] = new SystemNoticeWindow();
mWidowDic[EWindowType.EWT_TimeDownWindow] = new TimeDownWindow();
mWidowDic[EWindowType.EWT_RuneCombineWindow] = new RuneCombineWindow();
mWidowDic[EWindowType.EWT_HeroDatumWindow] = new HeroDatumWindow();
mWidowDic[EWindowType.EWT_RuneRefreshWindow] = new RuneRefreshWindow();
mWidowDic[EWindowType.EWT_GamePlayGuideWindow] = new GamePlayGuideWindow();
mWidowDic[EWindowType.EMT_PurchaseSuccessWindow] = new PurchaseSuccessWindow();
mWidowDic[EWindowType.EMT_GameSettingWindow] = new GameSettingWindow();
mWidowDic[EWindowType.EMT_AdvancedGuideWindow] = new AdvancedGuideWindow();
mWidowDic[EWindowType.EMT_ExtraBonusWindow] = new ExtraBonusWindow();
mWidowDic[EWindowType.EMT_EnemyWindow] = new EnemyWindow();
mWidowDic[EWindowType.EMT_HeroTimeLimitWindow] = new HeroTimeLimitWindow();
mWidowDic[EWindowType.EMT_SkillWindow] = new SkillWindow();
mWidowDic[EWindowType.EMT_SkillDescribleWindow] = new SkillDescribleWindow();
mWidowDic[EWindowType.EMT_RuneBuyWindow] = new RuneBuyWindow();
mWidowDic[EWindowType.EMT_DeathWindow] = new DeathWindow();
}
public BaseWindow GetWindow(EWindowType type)
{
if (mWidowDic.ContainsKey(type))
return mWidowDic[type];
return null;
}
public void Update(float deltaTime)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (pWindow.IsVisible())
{
pWindow.Update(deltaTime);
}
}
}
public void ChangeScenseToPlay(EScenesType front)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (pWindow.GetScenseType() == EScenesType.EST_Play)
{
pWindow.Init();
if(pWindow.IsResident())
{
pWindow.PreLoad();
}
}
else if ((pWindow.GetScenseType() == EScenesType.EST_Login) && (front == EScenesType.EST_Login))
{
pWindow.Hide();
pWindow.Realse();
if (pWindow.IsResident())
{
pWindow.DelayDestory();
}
}
}
}
public void ChangeScenseToLogin(EScenesType front)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (front == EScenesType.EST_None && pWindow.GetScenseType() == EScenesType.EST_None)
{
pWindow.Init();
if (pWindow.IsResident())
{
pWindow.PreLoad();
}
}
if (pWindow.GetScenseType() == EScenesType.EST_Login)
{
pWindow.Init();
if (pWindow.IsResident())
{
pWindow.PreLoad();
}
}
else if ((pWindow.GetScenseType() == EScenesType.EST_Play) && (front == EScenesType.EST_Play))
{
pWindow.Hide();
pWindow.Realse();
if (pWindow.IsResident())
{
pWindow.DelayDestory();
}
}
}
}
/// <summary>
/// 隐藏所有的窗体
/// </summary>
/// <param name="front"></param>
public void HideAllWindow(EScenesType front)
{
foreach (var item in mWidowDic)
{
if (front == item.Value.GetScenseType())
{
Debug.Log(item.Key);
item.Value.Hide();
//item.Value.Realse();
}
}
}
public void ShowWindowOfType(EWindowType type)
{
BaseWindow window;
if(!mWidowDic.TryGetValue(type , out window))
{
return;
}
window.Show();
}
private Dictionary<EWindowType, BaseWindow> mWidowDic;
}
}
窗体管理类的实现思路和状态管理类的实现思路类似,也需要先通过枚举把所有的窗体列出来,然后把窗体在已经定义好的字典Dictionary中注册,并且提供了获取某个窗体函数public BaseWindow GetWindow(EWindowType type)和场景跳转函数接口public void ChangeScense ToPlay(EScenesType front)以及场景跳转到UI函数接口public void ChangeScenseToLogin(EScenesType front),因为在游戏中我们只设计了登录场景login和游戏场景play,所以提供这两个接口就可以实现场景之间的跳转。两个跳转函数功能类似,下面我们以ChangeSceneToPlay为例,将其函数实现内容给大家展示一下。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25public void ChangeScenseToPlay(EScenesType front)
{
foreach (BaseWindow pWindow in mWidowDic.Values)
{
if (pWindow.GetScenseType() == EScenesType.EST_Play)
{
pWindow.Init();
if(pWindow.IsResident())
{
pWindow.PreLoad();
}
}
else if ((pWindow.GetScenseType() == EScenesType.EST_Login) && (front == EScenesType.EST_Login))
{
pWindow.Hide();
pWindow.Realse();
if (pWindow.IsResident())
{
pWindow.DelayDestory();
}
}
}
}
该函数实现了窗体的初始化、窗体的更新、窗体的隐藏、窗体的破坏等功能,这样关于MVC的架构设计到此就全部讲完了,接下来通过案例的方式告诉大家怎么去使用它。
MVC案例分享
首先需要把用到的UI资源做好,UI界面使用的是NGUI。如果使用UGUI,那么原理也是一样的。案例中只做了三个界面之间的切换,来应用MVC框架。首先要做的是实时检测State状态的改变,这需要将其代码放置到Update函数中,该脚本需要挂接到对象上,我们可以自己编写一个继承mono的脚本,直接挂到对象上,并且该对象不被销毁,函数代码如下所示。1
2
3
4
5
6
7
8void Update ()
{
//更新游戏状态机
GameStateManager.Instance.Update(Time.deltaTime);
//UI更新
WindowManager.Instance.Update(Time.deltaTime);
}
在Awake函数和Start函数中要设置默认的状态函数,也就是说首先切换场景,然后设置默认状态,代码如下所示。1
2
3
4
5
6
7
8
9void Awake()
{
WindowManager.Instance.ChangeScenseToLogin(EScenesType.EST_None);
}
void Start()
{
GameStateManager.Instance.EnterDefaultState();
}
用NGUI建三个面板窗口并将它们实例化出来,效果如图6所示。
图6 UI实例化面板
这些实例化的UI是可以动态加载创建出来的,接下来就可以直接运行程序,动态加载的代码大家可以自己去实现,UI的第一个面板是登录面板,效果如图7所示。
图7 UI登录面板
UI的第二个面板是创建角色面板,效果如图8所示。
图8 UI创建角色面板
UI的第三个面板是创建关卡面板,界面效果如图9所示。
图9 UI关卡面板
利用该架构实现了界面的搭建以及各个界面之间的切换功能。
小结
MVC对于UI的架构设计是非常方便的,我已经在多款游戏中使用,证明该架构设计对大部分游戏来说都是适用的。当然任何事情都不是绝对的,希望该架构能对开发者有所启发,并在未来能够做出更适合团队开发的架构系统。