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

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

简述

发现一件事情,那就是我忘了设置带参的事件,这使得整个系统显得非常蛋疼,我们决定去解决这个问题。

但是这依然会有其它蛋疼的地方,其中之一就是在面板里填参数的时候和这个系统产生的矛盾。

所以补了一下带参事件,主要是更新了CharacterEvent

代码
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

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

namespace EmiteInnaACT
{
//TODO:事件的对象池回收机制
public interface ICharacterEvent { };

public class CharacterEventBase : ICharacterEvent
{
public Action action;
}
#region 7种参数类型
public class CharacterEventBase<E> : ICharacterEvent
{
public Action<E> action;
}
public class CharacterEventBase<E,M> : ICharacterEvent
{
public Action<E,M> action;
}
public class CharacterEventBase<E, M,I> : ICharacterEvent
{
public Action<E, M,I> action;
}
public class CharacterEventBase<E, M, I,T> : ICharacterEvent
{
public Action<E, M, I,T> action;
}
public class CharacterEventBase<E, M, I, T,N> : ICharacterEvent
{
public Action<E, M, I, T,N> action;
}
public class CharacterEventBase<E, M, I, T, N,A> : ICharacterEvent
{
public Action<E, M, I, T, N,A> action;
}
#endregion
public struct CharacterEventStructer
{
public bool enabled;
public Action action;

}
/// <summary>
/// 角色事件类,每个controller挂一个就行。
/// </summary>
public class CharacterEvent {
public Dictionary<string, ICharacterEvent> eventList=new Dictionary<string, ICharacterEvent>();
/// <summary>
/// 注册一个角色事件
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
/// <returns></returns>
public bool RegisterCharacterEvent(string name,Action action)
{
if(eventList.TryGetValue(name,out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase c = new CharacterEventBase();
c.action = action;
eventList.Add(name, c);
return true;
}
}
#region 不同版本
public bool RegisterCharacterEvent<E>(string name, Action<E> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E> c = new CharacterEventBase<E>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
public bool RegisterCharacterEvent<E,M>(string name, Action<E, M> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E, M> c = new CharacterEventBase<E, M>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
public bool RegisterCharacterEvent<E,M,I>(string name, Action<E, M, I> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E, M, I> c = new CharacterEventBase<E, M, I>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
public bool RegisterCharacterEvent<E,M,I,T>(string name, Action<E, M, I, T> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E, M, I, T> c = new CharacterEventBase<E, M, I, T>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
public bool RegisterCharacterEvent<E, M, I, T,N>(string name, Action<E, M, I, T, N> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E, M, I, T, N> c = new CharacterEventBase<E, M, I, T, N>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
public bool RegisterCharacterEvent<E, M, I, T, N,A>(string name, Action<E, M, I, T, N, A> action)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
return false;
}
else
{
CharacterEventBase<E, M, I, T, N, A> c = new CharacterEventBase<E, M, I, T, N, A>();
c.action = action;
eventList.Add(name, c);
return true;
}
}
#endregion
/// <summary>
/// 启用一个事件
/// 已经废弃
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
//public bool EnableCharacterEvent(string name)
//{
// if(eventList.TryGetValue(name,out ICharacterEvent ces))
// {
// ces.enabled = true;
// eventList[name] = ces;
// return true;
// }
// else
// {
// return false;
// }
//}
/// <summary>
/// 禁用一个事件,已经废弃
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
//public bool DisableCharacterEvent(string name)
//{
// if (eventList.TryGetValue(name, out CharacterEventStructer ces))
// {
// ces.enabled = false;
// eventList[name] = ces;
// return false;
// }
// else
// {
// return false;
// }
//}
/// <summary>
/// 启用一个事件,如果没有则注册。
/// 已经废弃,直接用register
/// </summary>
/// <param name="name"></param>
/// <param name="action"></param>
//public void UseCharacterEvent(string name, Action action)
//{
// if (EnableCharacterEvent(name)) return;
// RegisterCharacterEvent(name, action);
// EnableCharacterEvent(name);
//}
/// <summary>
/// 删除一个事件
/// </summary>
/// <param name="name"></param>
public void DeleteCharacterEvent(string name)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
eventList.Remove(name);
}
}
/// <summary>
/// 调用一个事件,没有或者未启用则返回false。
/// </summary>
/// <param name="name"></param>
public bool ApplyCharacterEvent(string name)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase)) return false;
CharacterEventBase c = ces as CharacterEventBase;
c.action?.Invoke();
return true;
}
return false;
}
#region 不同版本
public bool ApplyCharacterEvent<E>(string name,E arg1)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E>)) return false;
CharacterEventBase<E> c = ces as CharacterEventBase<E>;
c.action?.Invoke(arg1);
return true;
}
return false;
}
public bool ApplyCharacterEvent<E,M>(string name, E arg1,M arg2)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E, M>))
return false;
CharacterEventBase<E, M> c = ces as CharacterEventBase<E, M>;
c.action?.Invoke(arg1,arg2);
return true;
}
return false;
}
public bool ApplyCharacterEvent<E, M,I>(string name, E arg1, M arg2,I arg3)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E, M, I>))
return false;
CharacterEventBase<E, M,I> c = ces as CharacterEventBase<E, M,I>;
c.action?.Invoke(arg1, arg2,arg3);
return true;
}
return false;
}
public bool ApplyCharacterEvent<E, M,I,T>(string name, E arg1, M arg2,I arg3,T arg4)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E, M, I, T>))
return false;
CharacterEventBase<E, M, I, T> c = ces as CharacterEventBase<E, M, I, T>;
c.action?.Invoke(arg1, arg2,arg3,arg4);
return true;
}
return false;
}
public bool ApplyCharacterEvent<E, M, I, T,N>(string name, E arg1, M arg2, I arg3, T arg4,N arg5)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E, M, I, T, N>))
return false;
CharacterEventBase<E, M, I, T, N> c = ces as CharacterEventBase<E, M, I, T, N>;
c.action?.Invoke(arg1, arg2, arg3, arg4,arg5);
return true;
}
return false;
}
public bool ApplyCharacterEvent<E, M, I, T, N,A>(string name, E arg1, M arg2, I arg3, T arg4, N arg5,A arg6)
{
if (eventList.TryGetValue(name, out ICharacterEvent ces))
{
if (!(ces is CharacterEventBase<E, M, I, T, N, A>))
return false;
CharacterEventBase<E, M, I, T, N, A> c = ces as CharacterEventBase<E, M, I, T, N, A>;
c.action?.Invoke(arg1, arg2, arg3, arg4, arg5,arg6);
return true;
}
return false;
}
#endregion
}

}

除此之外,移除了事件需要启用的鸡肋机制。

继续进化技能系统

然后有考虑要不要给普攻连段单独去做一个技能配置,后来想想没有必要,毕竟不算常用。

所以现在普攻连段的实现方式就是:第一段普攻的后半段开启一个InterrupatableCoroutine(之前有记录),然后在一段时间内再次按攻击键可以进入第二段普攻,会通过”SecondAttack”这个命令来Cancel原来的技能,以此类推。

然后基于原来设计的SpellAttack来进行一个范围检测

代码
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

public static bool TargetInAttackRange(Transform transform,Collider col,SpellAttackEvent eventData)
{
Vector4 pos = col.transform.position;
pos.w = 1;
pos = transform.worldToLocalMatrix * pos;
bool inrange = false;
if (eventData.areaType == AreaType.CUBE)
{
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "AreaAttackDebug").Value)
Debug.Log("Type1 at " + Time.realtimeSinceStartup);
Vector3 pv3 = pos;
Vector3 off = pv3 - eventData.centerOffset;
if (Mathf.Abs(off.x) <= eventData.extends.x &&
Mathf.Abs(off.y) <= eventData.extends.y &&
Mathf.Abs(off.z) <= eventData.extends.z)
inrange = true;
}
else if (eventData.areaType == AreaType.ELLIPSE)
{
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "AreaAttackDebug").Value)
Debug.Log("Type2 at " + Time.realtimeSinceStartup);
Vector3 pv3 = pos;
Vector3 off = pv3 - eventData.centerOffset;

if(Mathf.Abs(off.y)<=eventData.extends.y)
{

off.y = 0;
float cos = Vector3.Dot(off.normalized, new Vector3(0, 0, 1));
float ang=Mathf.Acos(cos)*180/Mathf.PI;
if (off.x > 0) ang = - ang;
if (ang < 0) ang += 360;
if(ang>=eventData.angle.x-eventData.angle.y&&ang<=eventData.angle.x+eventData.angle.y||
ang-360 >= eventData.angle.x - eventData.angle.y && ang-360 <= eventData.angle.x + eventData.angle.y)
{
if(Mathf.Abs(off.z)*Mathf.Abs(off.z)/eventData.extends.z/eventData.extends.z+
Mathf.Abs(off.x) * Mathf.Abs(off.x) / eventData.extends.x / eventData.extends.x<= 1){
inrange = true;
}
}
}
}
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "AreaAttackDebug").Value)
Debug.Log(inrange);
return inrange;
}

这个东西他目前还没有出锅,但我不太相信我第一次就能写对,所以把它留在这,看看什么时候会出锅w

这个函数的用处是检测目标collider是否在这个范围技能的范围内,而一开始会通过Physics.OverlapBox()来把一定范围内的东西都放进去,再用这个函数去检测,来实现不规则形状的范围检测,算是一个小计算几何。

小坑:说起Physics.OverlapBox(),我们知道unity的物理系统实际执行是在fixedupdate之后,也就是说这个函数执行的时候当前帧的物理是还没有执行的,用的是上一帧的物理,然后在这一帧里,如果有些东西是通过脚本进行了移动,我们无法控制这个移动是发生在这个函数前还是之后的,这可能会导致函数判断产生一定程度的同步问题。

解决方法是我们尽量少地使用脚本进行大范围的位移,或者说尽量不要用移动来实现一些需要精细判断的功能。

加入木桩进行测试

图片

大家好啊,我是木桩。

记录一下构建木桩这个基础单位的过程,它有一个受击状态,其它的状态只有一个常规Idle,在Idle状态下如果它脱离了原本的位置,它会回到原本的位置,而在受伤状态不会,受伤状态在进入后在一段时间后会回到idle状态,为什么这么设计懂的都懂吧,经典的挨打木桩。

然后整个攻击检测的逻辑是——attack事件的攻击事件产生效果→controller中发动相应函数,获取攻击区域的碰撞体列表→对列表中符合条件的碰撞体触发事件→目标执行事件。

面向对象中强调行为,我们知道大部分character都有被打和被晕两种状态,它们分别对应一个状态(按照原来的设定),但是这真的有必要吗?

当然有必要,别想偷懒。

因为每个角色处在哪怕是类似状态下的逻辑也是不同的,让每个角色的状态独立出来并不是增加工作量,反而是降低了上级继承互相影响带来的困难,反之,如果用同一个上级继承来写,反而会在定制这个上级的时候畏畏缩缩,最后发现这个上级等同于白做了……这也算是个人的一种理解吧,当然,优势和劣势并存,优势如上所述,劣势是如果真的需要修改大量重复的代码,会显得非常坐牢(当然,一般来讲不至于到这个地步)。

所以这里并没有把怪物的类似状态都做模板,只是做了一个全Enemy通用的静止、移动、受伤和眩晕状态,以及在CharacterController里加入了通用的EnterIdle、EnterHurt、EnterStun方法。

代码
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

#region 自带事件
/// <summary>
/// 进入静置状态
/// </summary>
public void EnterIdle()
{
EnterState("Idle");
}
/// <summary>
/// 进入眩晕状态
/// </summary>
public void EnterCasting()
{
EnterState("Casting");
}
/// <summary>
/// 进入受伤状态,time秒后回到idle
/// </summary>
/// <param name="time"></param>
public void EnterHurt(float time)
{
EnterState("Hurt");
if(currentState is UniformHurtState)
{
(currentState as UniformHurtState).timer = time;
(currentState as UniformHurtState).starttocount = false;
}
}
/// <summary>
/// 进入受伤状态,持续time秒
/// </summary>
/// <param name="time"></param>
public void EnterStun(float time)
{
EnterState("Stun");
if (currentState is UniformStunState)
{
(currentState as UniformStunState).timer = time;
(currentState as UniformStunState).starttocount = false;
}
}
public virtual void ApplyAreaAttack(SpellAttackEvent eventData)
{

}
#endregion

当然,除此之外这里有些原因也是具体写怪物AI的时候还是会考虑行为树,这个时候怎么设计状态就不重要了(?)

不过,这个项目主要还是3C为主,可能暂时不会推进到那个地方吧。

然后是范围技能伤害的处理,这个同样是一个charactercontroller带的函数,但是是虚函数,必须重载才有用,相当于一个接口,但是考虑到有些怪可能不会攻击,所以没做接口。

攻击逻辑

这段代码以后可能会改,或者回头参考,所以也留下来。

代码
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

#region AttackAbout
public override void ApplyAreaAttack(SpellAttackEvent eventData)
{
base.ApplyAreaAttack(eventData);
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "AreaAttackDebug").Value)
Debug.Log("攻击了");
Vector3 center = eventData.centerOffset + transform.position;
float maxBound =12f* Mathf.Max(eventData.extends.x, Mathf.Max(eventData.extends.y, eventData.extends.z));
Debug.Log(transform.position +" "+maxBound+GameObject.Find("Golem").transform.position);
Collider[] cols = Physics.OverlapBox(transform.position,new Vector3(maxBound, maxBound, maxBound));
// Debug.DrawLine(center, center + new Vector3(maxBound, maxBound, maxBound),Color.red, 5f);
foreach (Collider col in cols)
{
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "AreaAttackDebug").Value)
Debug.Log("区域内有"+col.gameObject.name);
EnemyController e = col.gameObject.GetComponent<EnemyController>();
if (e == null) continue;
if (EChara.TargetInAttackRange(transform, col, eventData))
{
e.OnApplyCharacterEvent<float>("LightAttack", 0);
}
}
}
#endregion

以及

接下来一段时间可能没这么闲了,而且做到这个地方已经可以稍微用用了,先发个包,然后可能会摸一两天。