你说得对,但是EmiteInnaACTSystemFramework是一款由EmiteInna自主研发,适用于PC平台的Unity第三人称上帝视角3D动作游戏代码框架。您将在游戏里扮演一个名叫游戏程序的角色,通过跳跃,冲刺和各种各样的丝滑小连招击败你的对手,修复程序中的bug,找回失散多年的代码能力,发掘未知的设计模式和模拟算法,揭开游戏行业无法入行、毕业即失业的真相。

本篇是ACT系列之一,github仓库位于:https://github.com/EmiteInna/EmiteInnaActSystem

简述

一个类型设计图

图片

最后具体的设计是这样的

在GameObject侧,根Transform是一个空物体,它挂载了Controller类,当然,根据不同的角色挂的肯定是Controller的派生类,而CharacterEvent、CharacterAnimation、StateMachine等等,都在Controller里注册,同时,Controller也会从根Transform的子物体中找到带有Animator的那个作为显示层。

Input接口被彻底取代,通过Controller身上挂载的Configure提供快捷键,直接在代码里判断,整个类设大概如下,虽然在github也能看到啦。

代码
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
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EmiteInnaACT
{
/// <summary>
/// 控制类,角色模块的实际中枢,负责各个模块的通信,需要重载的基本上只有Initialize。
/// 物理暂时还是用Collider和Rigidbody来实现了,后面如果需要自己写物理再上手写组件吧。
/// 具体角色的方法在状态机中实现。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent (typeof(Collider))]
public class CharacterControllerBase : MonoBehaviour
{
public Rigidbody rb;
public CharacterAnimationController animationController;
public CharacterConfigureBase configure;
public CharacterEvent characterEvent=new CharacterEvent();
public StateMachineBase currentState=null;
public CharacterSpellBase spell=new CharacterSpellBase();
public Dictionary<string,StateMachineBase> states = new Dictionary<string,StateMachineBase>();

#region Initialize Functions
/// <summary>
/// 角色的初始化,在awake阶段使用。
/// </summary>
public virtual void Initialize()
{
rb = GetComponent<Rigidbody>();
BindAnimationController();
spell.InitializeSpellBase(this);
}
/// <summary>
/// 由于动画机在View层,所以要遍历子节点来发现。
/// </summary>
public virtual bool BindAnimationController()
{
for(int i = 0; i < transform.childCount; i++)
{
CharacterAnimationController c = transform.GetChild(i).gameObject.GetComponent<CharacterAnimationController>();
if (c != null)
{
c.Initialize(this);
return true ;
}
}
return false;
}
/// <summary>
/// 从配置文件中获得技能,记得这个要在configure绑定之后用。
/// </summary>
public virtual void GetSpellsFromConfigure()
{
if (configure == null) return;
foreach(SpellConfigureBase i in configure.spellList)
{
spell.RegisterSpell(i.Name,i);
Debug.Log("成功读取技能" + i.SpellName+" 使用名为"+i.Name);
}

}
#endregion


#region StateMachine
/// <summary>
/// 注册状态机到角色。
/// </summary>
/// <param name="name"></param>
/// <param name="state"></param>
public void RegisterStateMachine(string name,StateMachineBase state)
{
if (!states.ContainsKey(name))
{
states.Add(name, state);
state.controller = this;
}
}
/// <summary>
/// 进入到某个状态。
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool EnterState(string name)
{
if (states.ContainsKey(name))
{
if (currentState != null) currentState.OnLeftState();
StateMachineBase state = states[name];
state.OnEnterState();
currentState = state;
}
return false;
}
/// <summary>
/// 移除某个状态。
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool DeleteState(string name)
{
if (states.ContainsKey(name))
{
states.Remove(name);
return true;
}
return false;
}
#endregion

#region Event
/// <summary>
/// 注册事件
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
/// <returns></returns>
public bool RegisterCharacterEvent(string name, Action action)
{
return characterEvent.RegisterCharacterEvent(name, action);
}
/// <summary>
/// 启用事件
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool EnableCharacterEvent(string name)
{
return characterEvent.EnableCharacterEvent(name);
}
/// <summary>
/// 禁用事件
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public bool DisableCharacterEvent(string name)
{
return characterEvent.DisableCharacterEvent(name);
}
/// <summary>
/// 启用一个事件,如果没有则注册。
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
public void UseCharacterEvent(string name, Action action)
{
characterEvent.UseCharacterEvent(name, action);
}
/// <summary>
/// 删除一个事件
/// </summary>
/// <param name="name"></param>
public void DeleteCharacterEvent(string name)
{
characterEvent.DeleteCharacterEvent(name);
}
/// <summary>
/// 调用事件的实际入口。
/// </summary>
/// <param name="name"></param>
public void OnApplyCharacterEvent(string name)
{
characterEvent.ApplyCharacterEvent(name);
}
#endregion

#region Animation
/// <summary>
/// 播放动画。
/// </summary>
/// <param name="clip"></param>
/// <param name="transition"></param>
/// <param name="speed"></param>
public void PlayAnimation(AnimationClip clip,float transition=0f,float speed = 1f)
{
animationController.PlayAnimation(clip,transition,speed);

}
#endregion

#region Functions
public void CharacterMove(Vector3 direction, float movementSpeed, float rotationSpeed, float accelerate = 0.5f)
{
EChara.CharacterMove(rb, direction, movementSpeed, rotationSpeed);
}
public void CharacterJump(float jumpHeight)
{
EChara.CharacterJump(rb,jumpHeight);
}
#endregion

#region Spell
/// <summary>
/// 使用技能。
/// </summary>
/// <param name="name"></param>
public void CharacterApplySpell(string name)
{
spell.ApplySpell(name);
}
/// <summary>
/// 设置某个技能是否启用
/// </summary>
/// <param name="name"></param>
/// <param name="value"></param>
public void CharacterSetSpellActive(string name, bool value)
{
spell.SetSpellActive(name, value);
}
/// <summary>
/// 取消当前释放的技能(通过事件名)
/// </summary>
/// <param name="eventName"></param>
public void CharacterCancelSpell(string eventName)
{
if (spell.currentSpell != null)
spell.currentSpell.OnSpellCancel(this, eventName);
}
/// <summary>
/// 取消当前释放的技能(通过命令名)
/// </summary>
/// <param name="eventName"></param>
public void CharacterCancelSpellWithCommand(string commandName)
{
if (spell.currentSpell != null)
spell.currentSpell.OnSpellCancelWithCommandName(this, commandName);
}
/// <summary>
/// 启用可打断事件。
/// </summary>
/// <param name="e"></param>
public void CharacterStartInterruptableCoroutine(IEnumerator e)
{
if (spell.currentSpell != null)
spell.currentSpell.StartInterrupatbleCoroutine(this, e);
}
#endregion
public virtual void Awake()
{
Initialize();
}
public virtual void Update()
{
if(currentState != null)currentState.OnUpdateState();
if (spell != null) spell.UpdateCharacterSpell();
}
public virtual void FixedUpdate()
{
if (currentState != null) currentState.OnFixedUpdateState();
if (spell != null) spell.FixedUpdateCharacterSpell();
}
public virtual void OnDestroy()
{
currentState = null;
configure = null;
characterEvent = null;
states.Clear();
states = null;
animationController.Destroy();
animationController = null;
spell.OnDestroy();
}
/// <summary>
/// 技能debug用的框框
/// </summary>
public void OnDrawGizmos()
{
if (spell != null)
{
if(spell.spells != null)
{
foreach(KeyValuePair<string,SpellBase> sp in spell.spells)
{
if (sp.Value.config.attackEvents != null)
{
foreach(SpellAttackEvent e in sp.Value.config.attackEvents)
{
if (e.showDebugGizmos == false) continue;
if (e.areaType == AreaType.CUBE)
{
Vector4 center = e.centerOffset;
Vector4 p1 = center + new Vector4(e.extends.x, e.extends.y, e.extends.z,1);
Vector4 p2 = center + new Vector4(-e.extends.x, e.extends.y, e.extends.z, 1);
Vector4 p3 = center + new Vector4(e.extends.x,- e.extends.y, e.extends.z, 1);
Vector4 p4 = center + new Vector4(e.extends.x, e.extends.y, -e.extends.z, 1);
Vector4 p5 = center + new Vector4(-e.extends.x,- e.extends.y, e.extends.z, 1);
Vector4 p6 = center + new Vector4(-e.extends.x, e.extends.y,- e.extends.z, 1);
Vector4 p7 = center + new Vector4(e.extends.x, -e.extends.y, -e.extends.z, 1);
Vector4 p8 = center + new Vector4(-e.extends.x,- e.extends.y,- e.extends.z, 1);
p1 = transform.localToWorldMatrix * p1;
p2 = transform.localToWorldMatrix * p2;
p3 = transform.localToWorldMatrix * p3;
p4 = transform.localToWorldMatrix * p4;
p5 = transform.localToWorldMatrix * p5;
p6 = transform.localToWorldMatrix * p6;
p7 = transform.localToWorldMatrix * p7;
p8 = transform.localToWorldMatrix * p8;
Gizmos.color = Color.green;
Gizmos.DrawLine(p1, p2);
Gizmos.DrawLine(p1, p3);
Gizmos.DrawLine(p1, p4);
Gizmos.DrawLine(p2, p6);
Gizmos.DrawLine(p2, p5);
Gizmos.DrawLine(p3, p5);
Gizmos.DrawLine(p3, p7);
Gizmos.DrawLine(p4, p6);
Gizmos.DrawLine(p4, p7);
Gizmos.DrawLine(p5, p8);
Gizmos.DrawLine(p6, p8);
Gizmos.DrawLine(p7, p8);
}else if (e.areaType == AreaType.ELLIPSE)
{
Gizmos.color = Color.green;
int delta = 10;
Vector4 center = e.centerOffset;
if (e.angle.y >= 180)
{
for (int i = 0; i < 360; i+=delta)
{
float degnow = (float)i / 360 * 2 * Mathf.PI;
float degnext = (float)(i + delta) / 360 * 2 * Mathf.PI;
Vector4 pnow = center + new Vector4(-e.extends.x * Mathf.Sin(degnow), e.extends.y, e.extends.z * Mathf.Cos(degnow), 1);
Vector4 pnext = center + new Vector4(-e.extends.x * Mathf.Sin(degnext), e.extends.y, e.extends.z * Mathf.Cos(degnext), 1);
pnow = transform.localToWorldMatrix * pnow;
pnext = transform.localToWorldMatrix * pnext;
Gizmos.DrawLine(pnow, pnext);
pnow = center + new Vector4(-e.extends.x * Mathf.Sin(degnow), -e.extends.y, e.extends.z * Mathf.Cos(degnow), 1);
pnext = center + new Vector4(-e.extends.x * Mathf.Sin(degnext), -e.extends.y, e.extends.z * Mathf.Cos(degnext), 1);
pnow = transform.localToWorldMatrix * pnow;
pnext = transform.localToWorldMatrix * pnext;
Gizmos.DrawLine(pnow, pnext);
}
}
else
{
delta = (int)(e.angle.y/18);
if (delta == 0) delta++;
Vector4 pcBtm = center + new Vector4(0, -e.extends.y, 0, 1);
Vector4 pcTop = center + new Vector4(0, e.extends.y, 0, 1);
pcBtm = transform.localToWorldMatrix * pcBtm;
pcTop = transform.localToWorldMatrix * pcTop;
bool flg = false;
Vector4 psBtm=Vector4.one, psTop = Vector4.one, peBtm = Vector4.one, peTop = Vector4.one;
for (int i =(int)(e.angle.x-e.angle.y); i < (int)(e.angle.x + e.angle.y); i+=delta)
{
// float down = e.angle.x - e.angle.y;
//// if (down < 0) down += 360;
// float up = e.angle.x + e.angle.y;
//// if (up >= 360) up -= 360;
// if ((i < down || i + delta >up)&&(i-360<down||i+delta-360>up)) continue;


float degnow = (float)i / 360 * 2 * Mathf.PI;
float degnext = (float)(i + delta) / 360 * 2 * Mathf.PI;


Vector4 pnow = center + new Vector4(-e.extends.x * Mathf.Sin(degnow), e.extends.y, e.extends.z * Mathf.Cos(degnow),1);
Vector4 pnext = center + new Vector4(-e.extends.x * Mathf.Sin(degnext), e.extends.y, e.extends.z * Mathf.Cos(degnext),1);
pnow = transform.localToWorldMatrix * pnow;
pnext = transform.localToWorldMatrix * pnext;
if (!flg)
{
psTop = pnow;
}
peTop = pnext;
Gizmos.DrawLine(pnow, pnext);
pnow = center + new Vector4(-e.extends.x * Mathf.Sin(degnow), -e.extends.y, e.extends.z * Mathf.Cos(degnow),1);
pnext = center + new Vector4(-e.extends.x * Mathf.Sin(degnext), -e.extends.y, e.extends.z * Mathf.Cos(degnext),1);
pnow = transform.localToWorldMatrix * pnow;
pnext = transform.localToWorldMatrix * pnext;
if (!flg)
{
psBtm = pnow;
flg = true;
}
peBtm = pnext;
Gizmos.DrawLine(pnow, pnext);

}
//Debug.Log(psTop + " " + psBtm + " " + peTop + " " + peBtm);
Gizmos.DrawLine(pcTop, pcBtm);
Gizmos.DrawLine(pcTop, psTop);
Gizmos.DrawLine(psTop, psBtm);
Gizmos.DrawLine(pcTop, peTop);
Gizmos.DrawLine(pcBtm, psBtm);
Gizmos.DrawLine(pcBtm, peBtm);
Gizmos.DrawLine(peTop, peBtm);
}

}
}
}
}
}
}
}
}
}

其它组件的代码就不贴了,跟着Controller里找就能找到分别的设计。

PlayerController的状态机模块设计

其实Statemachine其实就是四个函数,分别是进入状态、离开状态、Update状态、FixedUpdate状态。

那么为什么不用委托事件呢?可以灵活很多不是吗?实际上考虑到有些状态可以复用,用类写反而省了一些事,而且分开文件写也还是清晰很多,用委托事件虽然灵活,但也容易出锅,属于个人的一种偏好了,我宁可在一些地方出现相同的代码,也不愿意被一堆各个地方出现的委托关系绕晕。

然后在操作上的逻辑判断里我采用了这样的手法,用分层状态机来维护操作,所有的操作直接写在状态里,也就是说按键移动、跳跃等等逻辑都是放在对应的状态机里的,实际上当我做完简单的移动、冲刺、跳跃、释放一个简单技能的部分的时候,我就已经有了9个状态,其中有7个目前是叶子状态,分别是“地面且可操作”——包含“Idle静止、Walk行走、Sprint冲刺、Landed落地”,“空中可操作”——包含“Rising升空、Landing下降”,释放技能Casting。

依靠这些状态,我实现了这样的逻辑:当玩家处在Idle状态,它按下了horizontal或者vertical(用过unity的应该知道是什么意思),这会让他进入walk状态,而在walk状态下玩家可以依靠horizontal和vertical进行行走,如果他松开了这两个输入键,他将回到idle状态。

而如果在walk状态下玩家按住了(其实就是GetKey())sprint键,那么他会进入冲刺状态,这个时候他的行走速度是walk时的两倍,松开sprint键会让玩家回到walk状态,而松开horizontal和vertical输入键会让他直接回到Idle状态。

在这三个状态任意一个——也就是处在“地面且可操作状态”时,玩家可以按空格键进行跳跃,这将给玩家一个y方向的正速度,并让他处在Rising状态,而当他的y方向速度<零,也就是经过了跳跃顶点时,它将进入Landing状态,在这两个状态中,玩家依然可以通过horizontal和vertical输入来进行移动,但是会稍微慢一点。随后当玩家的速度>=0时,我认为玩家已经落地,他将进入landed状态,并播放一个帅气的着陆动画,然后在3秒后回到Idle状态,当然,这3秒内玩家能进行的操作和Idle状态是相同的,并且他可以通过行走和跳跃来提前进入其它状态。

此外当玩家处在”空中可操作”状态时如果按下了sprint,玩家会触发一个迅速落地(打桩)的技能,这会使他进入casting状态,在casting状态下玩家无法进行任何操作,直到他接触地面并回到idle状态。当然这部分内容是后面技能相关的东西了。

为什么我要花这么多文字来描述现在的这个状态机,而不是用一张图来表示这个过程?因为状态机的一些局限性可能会导致我最终采用其它的控制方案,到时候这段文字可能还更有用一点了。

音效播放器

差点忘了今天还做了这个,有对象池的基础上这个很好实现,只是一些封装而已。