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

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

简述

因为jkframe的原因,慕名而来看了一下ARPG的3,发现其实他的技能编辑器把我一直以来去做的某件事情明确了,现在我仔细想想这件事情,其实就是技能各个逻辑的分离。

这个也就是我一直在做的事情,为什么要这么做呢,是因为归根结底,在ACT中,技能这个东西,它可以总结为:“我释放某个技能→进入某个动画→在这个动画的某个时间我触发了某个效果”,而这句话中效果这个东西它当然是包含了音效和特效的,那么为什么我要把它和动作分开呢?因为动画是一个持续的过程,而效果是一个瞬间的过程(哪怕是协程类的持续性效果,它也是瞬间发生的),那么仔细想想瞬间的过程和持续的过程它的主要区别在哪呢?主要区别在于是否需要判断打断的逻辑,现在我们已经通过分层状态机把一些琐碎的判断去优化掉了,那么到底怎么样才能算是一个完整的技能,答案就只是加入了几句,变为了在如此的状态下-我释放某个技能-在技能的时长的某个时间中我播放某个动画-在技能时长的某个时间中我产生了什么效果-在技能时长的某个时间中我被打断了,这个打断使我进入了一个怎样的后续-在技能时长结束之后我进入下一个状态-或是释放下一个技能。

回到一开始,一切皆技能这个说法真的对吗?需要选择技能目标的技能难道不可以看成是本身进入了一个状态,然后选定完目标之后我进入下一个技能,这的确不需要制作一个新的类型来处理。但是走路呢?走路也可以是技能,因为没有人说在技能的过程中玩家就不能进行操作,走路完全可以是一个持续施法类型的技能。

那么技能需要的真的是可视化编辑吗?不,它其实不需要可视化编辑,它只是需要可视化而已。

那么我们能把走路也变成技能吗?可以,但没必要。

那么又到了最喜欢的类设环节(bushi

图片

这个雏形当然后面可能有所更改,但是它满足了我对技能的理解之一:同类型业务的分离。

再次强调一遍,为什么我要这么单独地对待技能编辑器,还是在已经理解了“一切皆技能”这个感觉的前提下,为了效率,我希望能够以最快的速度去制作一个技能,让它能够很好地运行,并且我能够很好地去测试它。

在这个设计中,我把动画、逻辑、音效、特效(甚至以后还会有其它东西)分离开来,用结构体的方法进行管理,然后在Spell的Use方法中,我用一堆迭代器来推理它们,然后通过事件字符串的方法和Character的Controller类进行沟通,实现它们各自应有的逻辑,完成整个技能的生命周期,这看起来和单纯写个协程相比反而更加困难。

但是这些结构体都可以被可视化,用技能编辑器或者Gizmos的方式来debug,而且现成的组件越多,构建一个新的技能就越容易。

至少直到我后悔这么干之前是这样的,就像我后悔在gamejam上给一个单例写1500行代码来控制整个游戏的进程一样。

贴一个Spell的代码

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

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

namespace EmiteInnaACT
{
/// <summary>
/// 技能的基类,提供一些技能对象的方法,需要传入CharacterController才能进行使用。
/// 感觉也不会有派生类了,这个东西。
/// </summary>
public class SpellBase
{
public float currentCD;
public bool active;
public Stack<Coroutine> stack = new Stack<Coroutine>();
public SpellConfigureBase config;
/// <summary>
/// 构造函数,对一个技能来说,技能配置是必须具备的。
/// </summary>
/// <param name="config"></param>
public SpellBase(SpellConfigureBase config)
{
this.config = config;
currentCD = 0;
active = false;
}
/// <summary>
/// 设置技能是否启用,只有启用的技能才能被使用,以及更新CD。
/// </summary>
/// <param name="value"></param>
public void SetActive(bool value)
{
active = value;
}
/// <summary>
/// 技能的fixupdate,默认功能是更新CD。
/// </summary>
public virtual void OnSpellFixedUpdate()
{
if (currentCD > 0) currentCD -= Time.fixedDeltaTime;
}
/// <summary>
/// 技能的update。
/// </summary>
public virtual void OnSpellUpdate()
{

}
/// <summary>
/// 进入CD,注意调用的时机。
/// </summary>
public void EnterCooldown()
{
currentCD = config.SpellCoolDown;
}
/// <summary>
/// 使用技能。
/// </summary>
/// <param name="ch"></param>
public virtual void OnSpellUse(CharacterControllerBase ch)
{
if (!active) return;
if (currentCD > 0) return;
ClearCoroutineStack();
StartInterrupatbleCoroutine(ch,(DoSpellUse(ch)));
//TODO:判断蓝耗
}
/// <summary>
/// 使用事件列表里的事件来打断技能。
/// </summary>
/// <param name="ch"></param>
/// <param name="commandName"></param>
public virtual void OnSpellCancelWithCommandName(CharacterControllerBase ch,string commandName){
if (config.cancelEvents.TryGetValue(commandName, out string eventName)) {
OnSpellCancel(ch, eventName);
}
}
/// <summary>
/// 技能被打断时触发的方法。
/// </summary>
/// <param name="ch"></param>
/// <param name="cancelCommand"></param>
public virtual void OnSpellCancel(CharacterControllerBase ch,string cancelCommand = "Stun")
{
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "SpellDebug").Value)
Debug.Log("打断技能" + config.SpellName);
while (stack.Count > 0)
{
Coroutine co=stack.Peek();
ch.StopCoroutine(co);
stack.Pop();
}
ch.OnApplyCharacterEvent(cancelCommand);
}
/// <summary>
/// 清空协程栈。
/// </summary>
public void ClearCoroutineStack()
{
while (stack.Count > 0) stack.Pop();
}
/// <summary>
/// 开始一个会被打断的协程,当技能被打断时,这些协程也会被打断。
/// </summary>
/// <param name="ch"></param>
/// <param name="c"></param>
public void StartInterrupatbleCoroutine(CharacterControllerBase ch, IEnumerator c)
{
Coroutine co= ch.StartCoroutine(c);
stack.Push(co);
}
/// <summary>
/// 实际使用的协程,注意调用顺序。
/// </summary>
/// <param name="ch"></param>
/// <returns></returns>
IEnumerator DoSpellUse(CharacterControllerBase ch)
{
float timer = 0;
int idx_Animation = 0;
int idx_Script = 0;
int idx_Audio = 0;
if (ConfigureInstance.GetValue<EmiteInnaBool>("uniform", "SpellDebug").Value)
Debug.Log("使用技能"+config.SpellName);
while (timer <= config.duration*config.timeMultiplier)
{
while (idx_Animation < config.animationEvent.Count)
{
if (timer >= config.animationEvent[idx_Animation].happenTime * config.timeMultiplier)
{
ch.PlayAnimation(config.animationEvent[idx_Animation].clip);
idx_Animation++;
}
}
while (idx_Script < config.scriptEvents.Count)
{
if (timer >= config.scriptEvents[idx_Script].happenTime * config.timeMultiplier)
{
ch.OnApplyCharacterEvent(config.scriptEvents[idx_Script].eventName);
idx_Script++;
}
}
while (idx_Audio < config.audioEvents.Count)
{
if (timer >= config.audioEvents[idx_Audio].happenTime * config.timeMultiplier)
{
ESoundInstance.PlaySFX(config.audioEvents[idx_Audio].clip, ch.transform.position +
config.audioEvents[idx_Audio].offset, config.audioEvents[idx_Audio].clip.length,
config.audioEvents[idx_Audio].playVolume, config.audioEvents[idx_Audio].playPitch/config.timeMultiplier);
idx_Audio++;
}
}
timer += Time.fixedDeltaTime;
yield return new WaitForFixedUpdate();
}
}
}
}