6.1 命令模式+NodeCanvas行为树实现Ai

1
2
3
4
5
6
7
public interface ICommand

{

public void onExcute(IAiAction Ai);

}``

实现了命令接口ICommand,不同的Ai命令类继承该接口

如后退命令类:

1
2
3
4
5
6
7
8
1.   public class MoveBackCmd : ICommand
2. {
3. public void onExcute(IAiAction Ai)
4. {
5. Ai.MoveBack();
6. }
7. }
8.

命令下达者只需要知道命令实施者的行为,而不需要知道实施者的属性等其他信息,所以实现了实施者的行为接口,如果角色需要被Ai控制,则需要角色脚本继承实现该接口

1
2
3
4
5
6
7
8
9
10
11
12
13
1.   public interface IAiAction
2. {
3. public void RunForward();
4. public void RunBackward();
5. public void MoveForward();
6. public void MoveBack();
7. public void StopMove();
8. public void GuardUp();
9. public void GuardBottom();
10. public void StopGuard();
11. public void Dodge();
12. }
13.

Ai脚本:

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
1.   public class Ai :MonoBehaviour
2. {
3. public Enemy Puppet;
4. public ICommand command;
5. public ICommand MoveForwardCmd;
6. public ICommand MoveBackCmd;
7. public ICommand StopMoveCmd;
8. public ICommand UpGuardCmd;
9. public ICommand DownGuardCmd;
10. public ICommand StopGuardCmd;
11. public ICommand DodgeCmd;
12. public ICommand RunFwdCmd;
13. public ICommand RunBwdCmd;
14. public ICommand[] AiCommands;
15. private void Start()
16. {
17. inputCode = 0;
18. MoveForwardCmd = new MoveForwardCmd();
19. MoveBackCmd = new MoveBackCmd();
20. StopMoveCmd=new StopMoveCmd();
21. UpGuardCmd=new UpGuardCmd();
22. DownGuardCmd = new GuardBottomCmd();
23. StopGuardCmd = new StopGuardCmd();
24. DodgeCmd=new DodgeCmd();
25. RunFwdCmd=new RunFwdCmd();
26. RunBwdCmd=new RunBwdCmd();
27. AiCommands = new ICommand[]{MoveForwardCmd,MoveBackCmd,StopGuardCmd,StopMoveCmd,UpGuardCmd,DownGuardCmd,DodgeCmd ,RunFwdCmd,RunBwdCmd};
28. }
29. public void RunForWard(IAiAction Puppet) => RunFwdCmd.onExcute(Puppet);
30. public void RunBackWard(IAiAction Puppet)=> RunBwdCmd.onExcute(Puppet);
31. public void MoveForward(IAiAction Puppet)=> MoveForwardCmd.onExcute(Puppet);
32. public void MoveBack(IAiAction Puppet) => MoveBackCmd.onExcute(Puppet);
33. public void StopMove(IAiAction Puppet)=>StopMoveCmd.onExcute(Puppet);
34. public void GuardUp(IAiAction Puppet)=> UpGuardCmd.onExcute(Puppet);
35. public void GuardDown(IAiAction Puppet) => DownGuardCmd.onExcute(Puppet);
36. public void StopGuard(IAiAction Puppet) => StopGuardCmd.onExcute(Puppet);
37. public void Dodge(IAiAction Puppet) => DodgeCmd.onExcute(Puppet);
38. }
39.

img

6.2 角色出招机实现(搓招,连招,预输入)

一:招式信息类

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
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
1.   //连招类
2. public class NextCombo : ComboData
3. {
4. //连招判定帧长
5. public float delayFrame;
6. public NextCombo() { }
7. }
8. //招式类
9. public class ComboData
10. {
11. //匹配
12. public bool unmatch;
13. //招式名称
14. public string name;
15. //优先级
16. public int priority;
17. //冲击力等级
18. public int wallopLevel;
19. //顿帧时长
20. public int slomoFrame;
21. //震屏时长
22. public int camShakeFrame;
23. //动画攻击检测开始帧
24. public int beginHitFrame;
25. //攻击检测持续帧
26. public int effectiveFrame;
27. //该招式的指令集
28. public CommandData[] commands;
29. //后续连招
30. public List<NextCombo> nextCombo;
31.
32. public ComboData(string name, int priority, int beginHitFrame, int effectiveFrame, CommandData[] commands, List<NextCombo> nextCombo){}
33. public ComboData() { }
34. }
35.
36. //指令类
37. public class CommandData
38. {
39. //指令
40. public int code;
41. //到下一个指令的输入等待时间
42. public float delayFrame;
43. public CommandData(int code, float delaytime){}
44. public CommandData() { }
45.
46. }
47. //招式信息结构体,出招回调函数参数
48. public struct ComboInfo
49. {
50. private string name;
51. private int priority;
52. private int wallopLevel;
53. private int beginHitFrame;
54. private int effectiveFrame;
55. private int slomoFrame;
56. private int camShakeFrame;
57. public string Name { get { return name; } }
58. public int Priority { get { return priority; } }
59. public int WallopLevel { get { return wallopLevel; } }
60. public int SlomoFrame { get { return slomoFrame; } }
61. public int CamShakeFrame { get { return camShakeFrame; } }
62. public int BeginHitFrame { get { return beginHitFrame; } }
63. public int EffectiveFrame { get { return effectiveFrame; } }
64. public ComboInfo(string _name,int _priority,int _wallopLevel,int _slomoFrame,int _camShakeFrame,int _beginFrame,int _effectFrame);
65. }
1. public class ComboMachine
2. {
3. //当前命令符索引
4. int current;
5. //上次按键的时间
6. int elapsedFrameFromLastPress;
7. //一次输入判定里是否有指令匹配
8. bool match;
9. //是否暂停
10. bool stop;
11. //是否正在出招
12. public bool isActing;
13. //动作施放后经过时间用于判断动作播放是否完毕
14. private float actElapsedTime;
15. private AnimatorStateInfo clipInfo;
16. public StringBuilder clipAnimName;
17. //等待连招
18. bool WaitForNextCombo;
19. //连招检测经过帧
20. private int elapsedFrame;
21. //动作状态
22. ComboData comboState;
23. ComboInfo? lastComboInfo;
24. public ComboInfo? LastComboInfo { get { return lastComboInfo; }set { lastComboInfo = value; } }
25. //攻击碰撞盒
26. BoxCollider hitBox;
27. public ComboData ComboState { get { return comboState; } }
28. //基本出招表
29. private List<ComboData> Combos;
30. int unmatchNum;
31. //连招表
32. private List<ComboData> combineCombo;
33. //由该状态机管理动画状态机里的搓招攻击动画
34. Animator animator;
35. //攻击盒
36. public bool Stop { get { return stop; } set { stop = value; } }
37. public ComboMachine(string path, Animator animator,BoxCollider hitBox)
38. {
39.
40. //读取Json招数配置表
41. string str = File.ReadAllText(path);
42. combineCombo = new List<ComboData>(10);
43. Combos = JsonMapper.ToObject<List<ComboData>>(str);
44.
45. }
46. //添加招式
47. public void AddCombo(ComboData newCombo){}
48. /// 移除招式
49. public void RemoveCombo(string comboName){}
50. //是否存在招式
51. public bool ExistCombo(string comboName){}
52. //获取招式信息
53. public ComboData GetComboData(string comboName){}
54. public ComboInfo GetComboInfo(string comboName){}
55. //攻击盒开启与关闭
56. async UniTaskVoid DisableHitAsync(int beiginFrame, int continueTime)
57. {
58. if(continueTime==0) return
59. await UniTask.DelayFrame(beiginFrame,PlayerLoopTiming.FixedUpdate);
60. hitBox.enabled = true;
61. await UniTask.DelayFrame(continueTime,PlayerLoopTiming.FixedUpdate);
62. if (hitBox.enabled)
63. hitBox.enabled = false;
64. }
65. //出招
66. public ComboInfo? PlayCombo(ComboData combo)
67. {
68. if (combo == null)
69. return null;
70. animator.Play(combo.name, -1, 0);
71. isActing = true;
72. clipAnimName.Append(combo.name);
73. DisableHitAsync(combo.beginHitFrame, combo.effectiveFrame).Forget();
74. if (combo.nextCombo==null||combo.nextCombo.Count==0)
75. MachineReset();
76. else
77. NextComboReset();
78. return new ComboInfo(combo);
79. }
80. /// <summary>
81. /// 判断能否进入下一个搓招命令
82. /// </summary>
83. void NextCommand()
84. {
85. if (match)
86. {
87. match = false;
88. current++;
89. }
90. }
91. //重置此次出招按键判定
92. public void MachineReset()
93. {
94. current = 0;
95. unmatchNum = 0;
96. match = false;
97. WaitForNextCombo = false;
98. comboState = null;
99. elapsedFrame = 0;
100. foreach(var combo in Combos)
101. {
102. combo.unmatch = false;
103. }
104. }
105. //用于连招的重置
106. private void NextComboReset()
107. {
108. current = 0;
109. unmatchNum = 0;
110. match = false;
111. WaitForNextCombo = true;
112. foreach(ComboData combo in comboState.nextCombo)
113. {
114. combo.unmatch = false;
115. if(!combineCombo.Exists((t)=>t.name==combo.name))
116. {
117. combineCombo.Add(combo);
118. }
119. }
120. comboState = null;
121. }
122. //指令判定
123. private void CheckCommand(int code,bool nextCombo)
124. {
125. if (!nextCombo)
126. {
127. //循环所有招式判断能否进入
128. for (int i = 0; i < Combos.Count; i++)
129. {
130. if(Combos[i].unmatch)
131. {
132. continue;
133. }
134. //如果已进入某个招数状态则判断该出招单个指令输入时间是否超时
135. if (current != 0)
136. {
137. if (elapsedFrameFromLastPress > Combos[i].commands[current 1].delayFrame)
138. {
139. //Debug.Log(Combos[i].name + "超时");
140. Combos[i].unmatch = true;
141. unmatchNum++;
142. }
143. }
144. //如果有输入且当前招式仍在匹配队列
145. if (code != 0)
146. {
147. //指令配对成功
148. if (Combos[i].commands[current].code == code)
149. {
150. // Debug.Log(Combos[i].name + "招的第" + current + "次指令匹配成功");
151. int temp = current;
152. match = true;
153. temp++;
154. //判断该搓招的指令集是否全部配对成功
155. if (temp > Combos[i].commands.Length - 1)
156. {
157. //成功后判断优先级,保留优先级高的动作
158. if (comboState == null || Combos[i].priority > comboState.priority)
159. {
160. //Debug.Log(Combos[i].name + "可释放");
161. comboState = Combos[i];
162. Combos[i].unmatch = true;
163.
164. }
165. }
166. }
167. //指令配对失败,移除
168. else
169. {
170. //Debug.Log(Combos[i].name + "输入错误");
171. Combos[i].unmatch = true;
172. unmatchNum++;
173. }
174. }
175. }
176. }
177. else
178. {
179. elapsedFrame++;
180. //前摇和攻击判定帧是否结束
181. if (elapsedFrame > lastComboInfo.Value.BeginHitFrame)
182. {
183. for (int i = 0; i < combineCombo.Count; i++)
184. {
185. if (combineCombo[i].unmatch)
186. {
187. continue;
188. }
189. //该连招的连招判定帧是否结束
190. if (elapsedFrame < (combineCombo[i] as NextCombo).delayFrame + lastComboInfo.Value.BeginHitFrame)
191. {
192. //Debug.Log(lastComboData.name + "的连招判定开启");
193. if (code != 0)
194. {
195. //指令配对成功
196. if (combineCombo[i].commands[current].code == code)
197. {
198. //Debug.Log(combineCombo[i].name + "招的第" + current + "次指令匹配成功");
199. int temp = current;
200. match = true;
201. temp++;
202. //判断该搓招的指令集是否全部配对成功
203. if (temp > combineCombo[i].commands.Length - 1)
204. {
205. comboState = combineCombo[i];
206. combineCombo[i].unmatch = true;
207. }
208. }
209. //指令配对失败,移除
210. else
211. {
212. //Debug.Log(combineCombo[i].name + "输入错误");
213. combineCombo[i].unmatch = true;
214. }
215. }
216. if (current != 0 && elapsedFrameFromLastPress > combineCombo[i].commands[current - 1].delayFrame)
217. {
218. //Debug.Log(combineCombo[i].name + "超时");
219. combineCombo[i].unmatch = true;
220. }
221. }
222. else
223. {
224. combineCombo[i].unmatch = false;
225. }
226. }
227. }
228. }
229. NextCommand();
230. }
231. //如果有招式指令达成则释放,全部失败则重置
232. private ComboInfo? GetCombo(int code,bool nextCombo)
233. {
234. CheckCommand(code, nextCombo);
235. if (comboState != null)
236. {
237. lastComboInfo = PlayCombo(comboState);
238. return lastComboInfo;
239. }
240. if(nextCombo)
241. {
242. if (unmatchNum >= combineCombo.Count)
243. {
244. MachineReset();
245. return null;
246. }
247. }
248. else
249. {
250. if (unmatchNum >= Combos.Count)
251. {
252. MachineReset();
253. return null;
254. }
255. }
256. return null;
257. }
258. /// <summary>
259. /// 运行
260. /// </summary>
261. /// <param name="code">玩家输入</param>
262. /// <returns>返回最近动作的基本信息(可空结构体)</returns>
263. public ComboInfo? RunMachine(int code)
264. {
265. if (code!=0)
266. {
267. elapsedFrameFromLastPress = 0;
268. }
269. elapsedFrameFromLastPress++;
270. //Debug.Log(Stop);
271. if (stop)
272. {
273. //Debug.Log(Stop);
274. return null;
275.
276. }
277. //连招检测
278. if(WaitForNextCombo)
279. {
280. ComboInfo? info= GetCombo(code, WaitForNextCombo);
281. if (info != null)
282. return info;
283. }
284. if(isActing)
285. {
286.
287. actElapsedTime += Time.deltaTime;
288. clipInfo = animator.GetCurrentAnimatorStateInfo(0);
289. if ((actElapsedTime >= clipInfo.length / animator.speed)||!clipInfo.IsName(clipAnimName.ToString()))
290. {
291. clipAnimName.Clear();
292. isActing = false;
293. actElapsedTime = 0;
294. if(comboState!=null)
295. {
296. lastComboInfo = PlayCombo(comboState);
297. return lastComboInfo;
298. }
299. else
300. {
301. MachineReset();
302. }
303. }
304. else
305. { //预输入检测
306. CheckCommand(code,false);
307. if(comboState==null)
308. {
309. if (unmatchNum >= Combos.Count)
310. {
311. MachineReset();
312. return null;
313. }
314. }
315. return null;
316. }
317. }
318. return GetCombo(code, false);
319. }
320.
321. public void RunVoidMachine()
322. {
323. if (isActing)
324. {
325. actElapsedTime += Time.deltaTime;
326. clipInfo = animator.GetCurrentAnimatorStateInfo(0);
327. if ((actElapsedTime >= clipInfo.length / animator.speed) || !clipInfo.IsName(clipAnimName.ToString()))
328. {
329. clipAnimName.Clear();
330. isActing = false;
331. actElapsedTime = 0;
332. MachineReset();
333. }
334. }
335. }
336.
337. //供Ai使用,只进行攻击动作是否播放完毕的检测
338. public void RunAiMachine()
339. {
340.
341. if (isActing)
342. {
343. actElapsedTime += Time.deltaTime;
344. clipInfo = animator.GetCurrentAnimatorStateInfo(0);
345. if (actElapsedTime >= clipInfo.length / animator.speed)
346. {
347. isActing = false;
348. actElapsedTime = 0;
349. }
350. }
351. } C
352. }
353.

支持Json文件导入C

img

(连招数量多的情况下json文件写与读显得尤为繁琐,需要改进)

加上[Serializable]也支持在Inspector上序列化

6.3 打击反馈

编写了打击和受击接口,再根据ComboInfo参数来动态调整效果

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
public interface IHit

{

/// <summary>

/// 关闭攻击碰撞盒

/// </summary>

public void ShutHitBox();

/// <summary>

/// 开启顿帧

/// </summary>

/// <param name="isAi">是否AI</param>

/// <param name="slomoTime">顿帧时长</param>

public void SlomoNotify(int slomoFrame);



/// <summary>

/// 镜头抖动

/// </summary>

/// <param name="shakeTime">抖动时间</param>

public void CamShake(int camShakeFrame);

//攻击特效

public void HitEffect(int wallopLevel, Vector3 pos);

/// <summary>

/// 施加力

/// </summary>

/// <param name="a">是否AI</param>

/// <param name="b">是否被防御</param>

/// <param name="wallopLevel">攻击冲击力级别</param>

public void HitForce(bool b, int wallopLevel);



}



/// <summary>

/// 受伤,打击感实现

/// </summary>

public interface IHurt

{

/// <summary>

/// 根据受击部位和受击冲击级别加分

/// </summary>

/// <param name="isAi">受击者是否是AI</param>

/// <param name="wallopLevel">受击动作的冲击力级别</param>

public void AddScore(int wallopLevel);

/// <summary>

/// 健康损耗

/// </summary>

/// <param name="a">我是不是AI</param>

/// <param name="wallopLevel">受击冲击级别</param>

public void HealthLoss(int wallopLevel);

/// <summary>

/// 根据受击冲击级别播放受击动画并开启结束顿帧

/// </summary>

/// <param name="a">我是否是AI</param>

/// <param name="wallopLevel">冲击力级别</param>

/// <param name="slomoTime">顿帧时间</param>

public void PlayHurtAnim(int wallopLevel,int slomoFrame);

/// <summary>

/// 根据受击冲击级别施加后退力

/// </summary>

/// <param name="a">我是否是AI</param>

/// <param name="wallopLevel">受击冲击力级别</param>

public void HurtForce(int wallopLevel,bool a,string name);

}

镜头抖动:

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
1.   public class CameraShake : MonoBehaviour
2. {
3. [Header("抖动时间")]
4. public int ShakeTime=5;
5. [Header("偏移距离")]
6. public float ShakeAmount=0.2f;
7. [Header("偏移速度")]
8. public float ShakeSpeed=1.5f;
9.
10. public async UniTaskVoid Shake(int shakeFrame)
11. {
12. //存下当前相机位置
13. Vector3 OrigPosition = transform.localPosition;
14. //记下运行时间
15. int ElapsedFrame = 0;
16. while (ElapsedFrame < shakeFrame)
17. {
18. Vector2 offset = Random.insideUnitCircle * ShakeAmount;
19. //在单位圆面上随机取点
20. Vector3 RandomPoint = OrigPosition + new Vector3(offset.x,offset.y);
21. //更新相机位置
22. transform.localPosition = Vector3.Lerp(transform.localPosition, RandomPoint, Time.deltaTime * ShakeSpeed);
23. await UniTask.Yield(PlayerLoopTiming.PostLateUpdate);
24. //更新时间
25. ElapsedFrame += 1;
26. }
27. //恢复相机位置
28. transform.localPosition = OrigPosition;
29. }
30. }

DoTween****动画增强打击感和动作感

//序列人物抛物线斜上受击动画

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
public void Sequence()

{
body.DOKill();
sequence = DOTween.Sequence();

if(isRight)

{

sequence.Append(body.DOMove(new Vector3(transform.position.x + 1.5f, transform.position.y + 3, transform.position.z), 0.2f).SetEase(Ease.OutCirc));

sequence.Append(body.DOMove(new Vector3(transform.position.x + 1.5f, transform.position.y - 3, transform.position.z), 0.3f).SetEase(Ease.InCirc));

​ }

else

​ {

​ sequence.Append(body.DOMove(new Vector3(transform.position.x - 1.5f, transform.position.y + 3, transform.position.z), 0.2f).SetEase(Ease.OutCirc));

​ sequence.Append(body.DOMove(new Vector3(transform.position.x - 1.5f, transform.position.y - 3, transform.position.z), 0.3f).SetEase(Ease.InCirc));

​ }

}

//受大冲击力的受击动画

public void Tweener()

{



if (isRight)

​ tweener = body.DOMoveX(transform.position.x + 8f, 1f).SetEase(Ease.OutQuart);

else

​ tweener = body.DOMoveX(transform.position.x - 8f, 1f).SetEase(Ease.OutQuart);

​ oldpos = body.position;

​ tweener.OnUpdate(() =>

​ {

float dis = Vector3.Distance(oldpos, body.position);

​ Vector3 dir = (body.position - oldpos).normalized;

​ raycastHits = Physics.RaycastAll(oldpos, dir, dis);

for (int a = 0; a < raycastHits.Length; a++)

​ {

if (raycastHits[a].collider.CompareTag("Fence"))

​ {

​ tweener.Pause();

if (isRight)

​ body.DOMoveX(transform.position.x - 3f, 0.2f);

else

​ body.DOMoveX(transform.position.x + 3f, 0.2f);

​ }

​ }

​ oldpos = body.position;

​ });

}



//飞踢动画

public void Tweener2()

{

if (isRight)

​ {

​ oldpos = new Vector3(body.position.x-3f, body.position.y-2.8f, body.position.z);

​ tweener = body.DOMove(new Vector3(transform.position.x - 10f, transform.position.y - 6.5f, transform.position.z), 0.5f).SetEase(Ease.OutQuad);

​ }



else

​ {

​ oldpos = new Vector3(body.position.x + 3f, body.position.y - 2.8f, body.position.z);

​ tweener = body.DOMove(new Vector3(transform.position.x + 10f, transform.position.y - 6.5f, transform.position.z), 0.5f).SetEase(Ease.OutQuad);

​ }

​ oldpos = body.position;

​ tweener.OnUpdate(() =>

​ {

float dis = Vector3.Distance(oldpos, body.position);

​ Vector3 dir = (body.position - oldpos).normalized;

​ raycastHits = Physics.RaycastAll(oldpos, dir, dis);

for (int a = 0; a < raycastHits.Length; a++)

​ {

if (raycastHits[a].collider.CompareTag("Player"))

​ {

​ tweener.Pause();

if (isRight)

​ body.DOMoveX(transform.position.x - 3f, 0.2f);

else

​ body.DOMoveX(transform.position.x + 3f, 0.2f);

​ }

​ }

​ oldpos = body.position;

​ });

}

6.4 镜头变化

由于本作视角和美术风格设定(3D人物,2D背景,人物和前景为正交视角,中景后景为透视视角),自定义了一个摄像机脚本控制游戏时变化

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
public class CameraController : MonoBehaviour

{

[Header("玩家一")]

public Transform player1;

[Header("玩家2")]

public Transform player2;

[Header("玩家最大距离")]

public float maxDistance;

[Header("视角广度变化玩家最小距离")]

public float minDistance;

[Header("最大视角广度")]

public float maxSize;



[Header("最小视角广度")]

public float minSize;

[Header("最大视角时Y轴偏移量")]

public float yOffsetMax;

[Header("最小视角时Y轴偏移量")]

public float yOffsetMin;

[Header("X轴偏移量")]

public float xOffset;

[Header("是否均衡插值")]

public bool isLerpAverage;

public Camera cameraOth;

public float MaxView;

public float MinView;

private float OthCoefficient;

//视角广度变化系数

private float SOFCoefficient;

//Y轴偏移值变化系数

private float YOFCoefficient;

private Vector3 pos;

private float Dist;

private float horizonalDist;

private float verticalDist;

private float lerpTime;



// Start is called before the first frame update

void Start()

{

​ SOFCoefficient = (maxSize - minSize) / (maxDistance - minDistance);

​ YOFCoefficient = (yOffsetMax - yOffsetMin) / (maxDistance - minDistance);

​ OthCoefficient = (MaxView - MinView) / (maxDistance - minDistance);

​ pos.z = transform.position.z;

}

private void Update()

{

​ horizonalDist = Mathf.Abs(player1.position.x - player2.position.x);

//超过视角最大距离停止人物反方向移动

if(horizonalDist>maxDistance)

​ {

​ player1.GetComponent<BasePlayer>().stop = true;

​ player2.GetComponent<BasePlayer>().stop=true;1

​ }

else

​ {

​ player1.GetComponent<BasePlayer>().stop = false;

​ player2.GetComponent<BasePlayer>().stop = false;

​ }

}

// Update is called once per frame

void LateUpdate()

{

​ Dist = Vector2.Distance(player1.position, player2.position);

​ pos.x = horizonalDist / 2 + xOffset+(player1.position.x > player2.position.x ? player2.position.x : player1.position.x);

​ Dist = Mathf.Clamp(Dist, minDistance, maxDistance);

​ horizonalDist = Mathf.Clamp(horizonalDist, minDistance, maxDistance);

​ verticalDist = Mathf.Abs(player1.position.y - player2.position.y);

​ pos.y = (horizonalDist - minDistance) * YOFCoefficient + yOffsetMin+ verticalDist / 2 + (player1.position.y > player2.position.y ? player2.position.y: + player1.position.y);

//是否进行平均插值

if (isLerpAverage)

​ {

​ lerpTime = lerpTime + Time.deltaTime;

if (lerpTime >= 1) lerpTime = 0;

​ transform.position = Vector3.Slerp(transform.position, pos, lerpTime);

​ Camera.main.fieldOfView= Mathf.Lerp(Camera.main.fieldOfView, (Dist-minDistance) * SOFCoefficient + minSize, lerpTime);



​ }

else

​ {

​ transform.position = Vector3.Slerp(transform.position, pos, Time.deltaTime);

​ Camera.main.fieldOfView = Mathf.Lerp(Camera.main.fieldOfView, (Dist - minDistance) * SOFCoefficient + minSize, Time.deltaTime*1000);

​ }

​ cameraOth.orthographicSize = (Dist - minDistance) * OthCoefficient + MinView;

}

}

Ui镜头动画,开场为TimeLine,其他就是普通的DoTween动画就不多赘述了

6.5 自定义InputSystem Interaction

因为要复刻拳皇双击或者多击之后按住一个键保持状态,松开则取消该状态的效果,最规范同时效果最好的就是自定义Interaction

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
\#if UNITY_EDITOR

[InitializeOnLoad]

\#endif

public class TapHoldInteraction : IInputInteraction

{

//按键枚举

private enum TapPhase

{

​ None,

​ WaitingForNextRelease,

​ WaitingForNextPress,

​ Hold

}

[Tooltip("hold所需时长")]

public float holdTime;



[Tooltip("一次press和release之间允许的时间")]

public float duration;



[Tooltip("tap之间允许的时间")]

public float pressSpacing;



[Tooltip("Tap次数")]

public int tapCount=2;



public float pressPoint;



private TapPhase m_CurrentPhase;



private int m_CurrentTapCount;



private double m_CurrentTapStartTime;



private double m_LastTapReleaseTime;



//如果没有赋予变量值则值为Inputsystem里默认大小

private float holdTimeOrDefault=> ((double)holdTime > 0.0) ? duration : InputSystem.settings.defaultHoldTime;

private float tapTimeOrDefault => ((double)duration > 0.0) ? duration : InputSystem.settings.defaultTapTime;



internal float tapDelayOrDefault => ((double)pressSpacing > 0.0) ? pressSpacing : InputSystem.settings.multiTapDelayTime;



private float pressPointOrDefault => (pressPoint > 0f) ? pressPoint : InputSystem.settings.defaultButtonPressPoint;



private float releasePointOrDefault => pressPointOrDefault * InputSystem.settings.buttonReleaseThreshold;



public void Process(ref InputInteractionContext context)

{

//如果超时

if (context.timerHasExpired)

​ {

if(m_CurrentPhase!=TapPhase.Hold)

​ {

​ context.Canceled();

​ }

else

​ {

​ context.PerformedAndStayPerformed();

​ }

return;

​ }



switch (m_CurrentPhase)

​ {

case TapPhase.None:

if (context.ControlIsActuated(pressPointOrDefault))

​ {

​ m_CurrentPhase = TapPhase.WaitingForNextRelease;

​ m_CurrentTapStartTime = context.time;

//context.Started();

float num = tapTimeOrDefault;

float num2 = tapDelayOrDefault;

float num3 = holdTimeOrDefault;

​ context.SetTimeout(num);

//context.SetTotalTimeoutCompletionTime(num * (float)(tapCount-1) + (float)(tapCount - 1) * num2+num3);

​ }

break;

case TapPhase.WaitingForNextRelease:

if (context.ControlIsActuated(releasePointOrDefault))

​ {

break;

​ }



if (context.time - m_CurrentTapStartTime <= (double)tapTimeOrDefault)

​ {

​ m_CurrentTapCount++;

​ m_CurrentPhase = TapPhase.WaitingForNextPress;

​ m_LastTapReleaseTime = context.time;

​ context.SetTimeout(tapDelayOrDefault);

​ }

else

​ {

​ context.Canceled();

​ }



break;

case TapPhase.WaitingForNextPress:

if (!context.ControlIsActuated(pressPointOrDefault))

​ {

break;

​ }

if (context.time - m_LastTapReleaseTime <= (double)tapDelayOrDefault)

​ {

​ m_CurrentTapStartTime = context.time;

if (m_CurrentTapCount + 1 >= tapCount)

​ {

//m_CurrentPhase = TapPhase.Hold;

​ context.Started();

​ m_CurrentTapCount++;

​ context.SetTimeout(holdTimeOrDefault);

break;

​ }

​ m_CurrentPhase = TapPhase.WaitingForNextRelease;

​ context.SetTimeout(tapTimeOrDefault);

​ }

else

​ {

​ context.Canceled();

​ }

break;

case TapPhase.Hold:

//Debug.Log("Hold");

//if (context.time - m_CurrentTapStartTime >= (double)holdTimeOrDefault)

//{

// context.PerformedAndStayPerformed();

//}

//if (!context.ControlIsActuated())

//{

// context.Canceled();

//}

break;

​ }

switch(context.phase)

​ {

case InputActionPhase.Started:

if (context.time - m_CurrentTapStartTime >= (double)holdTimeOrDefault)

​ {

​ context.PerformedAndStayPerformed();

​ }



if (!context.ControlIsActuated())

​ {

​ context.Canceled();

​ }

break;

case InputActionPhase.Performed:

if (!context.ControlIsActuated(pressPointOrDefault))

​ {

​ context.Canceled();

​ }

break;

​ }

}

public void Reset()

{

​ m_CurrentPhase = TapPhase.None;

​ m_CurrentTapCount = 0;

​ m_CurrentTapStartTime = 0.0;

​ m_LastTapReleaseTime = 0.0;

}

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]

private static void Initialize()

{

​ InputSystem.RegisterInteraction<TapHoldInteraction>();

}

}

6.6 对象池模块和事件中心模块

对象池:

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
public class Drawers

{

private GameObject father;

private Queue<GameObject> drawer;

public Queue<GameObject> Drawer { get { return drawer; } }

public Drawers(GameObject obj,Transform wardrobe)

{

​ father = new GameObject(obj.name+"'s"+"屉子");

​ father.transform.SetParent(wardrobe);

​ obj.transform.SetParent(father.transform);

​ drawer = new Queue<GameObject>() ;

​ drawer.Enqueue(obj);

}

/// <summary>

/// 取衣服

/// </summary>

/// <returns></returns>

public GameObject GetObject()

{

​ drawer.Peek().SetActive(true);

​ drawer.Peek().transform.parent = null;

return drawer.Dequeue();

}

public void GetObject(Vector3 pos)

{

​ drawer.Peek().SetActive(true);

​ drawer.Peek().transform.parent = null;

​ drawer.Peek().transform.position = pos;

​ drawer.Dequeue();

}

/// <summary>

/// 存衣服

/// </summary>

/// <param name="obj">要存入的衣服</param>

public void ReturnObject(GameObject obj)

{

​ drawer.Enqueue(obj);

​ obj.transform.SetParent(father.transform);

}

}

/// <summary>

/// 衣柜

/// </summary>

public class Wardrobe : BasicManager<Wardrobe>

{

private Dictionary<string, Drawers> objectPool = new Dictionary<string,Drawers>();

public GameObject wardrobe;

//字典容量

private int? poolCapacity=10;

//队列容量

private int? queueCapacity = 10;



/// <summary>

/// 取衣服

/// </summary>

/// <param name="name">既是路径又是名字</param>

/// <returns></returns>

public GameObject GetObject(string name)

{

if(objectPool.ContainsKey(name)&&objectPool[name].Drawer.Count>0)

​ {

return objectPool[name].GetObject();

​ }

else

​ {

​ GameObject obj = GameObject.Instantiate(Resources.Load<GameObject>(name));

​ obj.name = name;

return obj;

​ }

}

public void GetObject(string name,Vector3 startPos)

{

if (objectPool.ContainsKey(name) && objectPool[name].Drawer.Count > 0)

​ {

​ objectPool[name].GetObject(startPos);

​ }

else

​ {

​ GameObject obj = GameObject.Instantiate<GameObject>(Resources.Load<GameObject>(name),startPos,Quaternion.identity);

​ obj.name = name;

​ }

}



/// <summary>

/// 存衣服

/// </summary>

/// <param name="name">名字</param>

/// <param name="obj">要存入的衣服</param>

public void ReturnObject(string name,GameObject obj)

{

​ obj.SetActive(false);

if (wardrobe == null)

​ wardrobe = new GameObject("衣柜");

//衣柜是否有这个抽屉

if (objectPool.ContainsKey(name))

​ {

//抽屉是否有空间若无删除

if (objectPool[name].Drawer.Count < queueCapacity)

​ objectPool[name].ReturnObject(obj);

else

​ GameObject.Destroy(obj);

​ }

//没有该抽屉

//衣柜没有多余空间直接删除

else if(objectPool.Count>=poolCapacity)

​ {

​ GameObject.Destroy(obj);

​ }

//衣柜还有空间创造新的抽屉储存

else

​ {

​ Drawers draw = new Drawers(obj, wardrobe.transform);

​ objectPool.Add(name, draw);

​ }

}

/// <summary>

/// 预加载(放好要穿的衣服)

/// 场景加载时预加载最好

/// </summary>

/// <param name="name">物体名</param>

/// <param name="preNum">预加载数量</param>

public void PreLoad(string name,int preNum)

{

​ wardrobe = new GameObject("衣柜");

​ Queue<GameObject> gameObjects = new Queue<GameObject>();

for(int i=0;i<preNum;i++)

​ {

​ gameObjects.Enqueue(Resources.Load<GameObject>(name));

​ }

}



/// <summary>

/// 清空

/// </summary>

public void Clear()

{

​ objectPool.Clear();

​ GameObject.Destroy(wardrobe);

}

}

事件中心:

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
public interface IEventInfo{}

public class EventInfo<T>: IEventInfo

{

public UnityAction<T> actions;

public EventInfo( UnityAction<T> action)

{

​ actions += action;

}

}



public class EventInfo : IEventInfo

{

public UnityAction actions;



public EventInfo(UnityAction action)

{

​ actions += action;

}

}

public class EventCenter : BasicManager<EventCenter>

{

//key事件名

//value委托函数

private Dictionary<string, IEventInfo> eventDic = new Dictionary<string, IEventInfo>();



/// <summary>

/// 添加事件监听

/// </summary>

/// <param name="name">事件的名字</param>

/// <param name="action">准备用来处理事件 的委托函数</param>

public void AddEventListener<T>(string name, UnityAction<T> action)

{

//有没有对应的事件监听

//有的情况

if( eventDic.ContainsKey(name) )

​ {

​ (eventDic[name] as EventInfo<T>).actions += action;

​ }

//没有的情况

else

​ {

​ eventDic.Add(name, new EventInfo<T>( action ));

​ }

}



/// <summary>

/// 监听不需要参数传递的事件

/// </summary>

/// <param name="name"></param>

/// <param name="action"></param>

public void AddEventListener(string name, UnityAction action)

{

//有没有对应的事件监听

//有的情况

if (eventDic.ContainsKey(name))

​ {

​ (eventDic[name] as EventInfo).actions += action;

​ }

//没有的情况

else

​ {

​ eventDic.Add(name, new EventInfo(action));

​ }

}





/// <summary>

/// 移除对应的事件监听

/// </summary>

/// <param name="name">事件的名字</param>

/// <param name="action">对应之前添加的委托函数</param>

public void RemoveEventListener<T>(string name, UnityAction<T> action)

{

if (eventDic.ContainsKey(name))

​ (eventDic[name] as EventInfo<T>).actions -= action;

}



/// <summary>

/// 移除不需要参数的事件

/// </summary>

/// <param name="name"></param>

/// <param name="action"></param>

public void RemoveEventListener(string name, UnityAction action)

{

if (eventDic.ContainsKey(name))

​ (eventDic[name] as EventInfo).actions -= action;

}



/// <summary>

/// 事件触发

/// </summary>

/// <param name="name">哪一个名字的事件触发了</param>

public void EventTrigger<T>(string name, T info)

{

//有没有对应的事件监听

//有的情况

if (eventDic.ContainsKey(name))

​ {

//eventDic[name]();

if((eventDic[name] as EventInfo<T>).actions != null)

​ (eventDic[name] as EventInfo<T>).actions.Invoke(info);

//eventDic[name].Invoke(info);

​ }

}



/// <summary>

/// 事件触发(不需要参数的)

/// </summary>

/// <param name="name"></param>

public void EventTrigger(string name)

{

//有没有对应的事件监听

//有的情况

if (eventDic.ContainsKey(name))

​ {

//eventDic[name]();

if ((eventDic[name] as EventInfo).actions != null)

​ (eventDic[name] as EventInfo).actions.Invoke();

//eventDic[name].Invoke(info);

​ }

}



/// <summary>

/// 清空事件中心

/// 主要用在 场景切换时

/// </summary>

public void Clear()

{

​ eventDic.Clear();

}

}

6.7 简单ShaderGraphs

描边:

img

水雾:

img

云:

img