Unity之ACT游戏开发记录

之前一直想做一款ACT游戏demo,可惜市面上没找到合适的教程。求人求神不如求己,于是我决定自己开发一款ACT游戏demo,于是记录一下自己的思路。


游戏目标

游戏打算从简单开始设计、慢慢变得更复杂,我的目标就是做出类似于《巫兔》这种,仿《只狼》的ACT游戏。

开发记录

1.相机部分。首先是关于相机部分,配上InputSystem,写一个GameInputManager类用于管理我们的输入,把GameInputManager写成一个单例,通过鼠标移动控制相机旋转,同时用athf.Clamp设置相机上下看的范围,写一个方法通过鼠标滚轮控制相机与角色距离,实现玩家移动方向朝向相机。
我觉得后续应该会再写一套相机系统,一套索敌用的。因为《巫兔》里打Boss时,相机是不能旋转的,是固定在角色身后的某一方位的,后续再写这个功能。应该再写一个脚本,动态添加相机系统(因为我要写两套)。

2.角色移动。角色移动我采用的八方向移动+RootMotion,之前踩的坑已经在博客里写过,主要是处理旋转上的逻辑。同时给Animator组件写了个拓展函数,拓展方法AnimationAtTag(string name ,int layerIndex = 0),如果当前动画的标签是name就返回true,不是就返回false,主要用于处理一些逻辑,比如角色在受伤动画下、是不能随相机旋转的,只有AnimationAtTag(“Move”)才能实现跟随相机旋转。

3.角色闪避。当idle状态下,按下shift会向前翻滚,Run状态下配合方向键做出不同翻滚动作。判断方向就是看我们输入的Vector2向量的x、y值,比如向右翻滚,需要HorizontalSpeed >0f,如果是按下WD+Shift呢,也有考虑,做出向前方翻滚的动作,控制好Vector2的变量就好。

动画状态机

连招系统。连招实际上是不同动画的组合,这里我借鉴了up主鬼鬼鬼ii的部分连招思路,首先创建两个类,一个是ComboData,配置以下数据: 招式动画名称、招式动画伤害、敌人被这个招式打到的受伤动画的名字、敌人对这个动画格挡动画的名字。接着创建一个类叫Combo,提供游戏方法对ComboData进行管理。

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
using UnityEngine;

[CreateAssetMenu(fileName = "ComboData", menuName = "Scriptable Objects/ComboData")]
public class ComboData : ScriptableObject
{
[SerializeField,Header("招式名称")]private string comboName;
[SerializeField,Header("招式伤害")]private int comboDamage;
[SerializeField,Header("被招式受击名称")]private string hitName;
[SerializeField,Header("被招式格挡名称")]private string parryName;

public string ComboName => comboName;
public int ComboDamage => comboDamage;
public string HitName => hitName;
public string ParryName => parryName;
}

using NUnit.Framework;
using System;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "Combo", menuName = "Scriptable Objects/Combo")]
public class Combo : ScriptableObject
{
//存储招式
public List<ComboData> combo = new List<ComboData>();

//获得招式名字
public string TryGetComboName(int index)
{
if (combo.Count == 0 || combo.Count < index + 1)
{
return null;
}
else
{
return combo[index].ComboName;
}
}

//获得受伤动画名称
public string TryGetHitName(int index)
{
if (combo.Count == 0 || combo.Count < index + 1)
{
return null;
}
else
{
return combo[index].HitName;
}
}

//获得格挡动画名称
public string TryGetParryName(int index)
{
if (combo.Count == 0 || combo.Count < index + 1)
{
return null;
}
else
{
return combo[index].ParryName;
}
}


//获得招式伤害
public int TryGetComboDamage(int index)
{
if (combo.Count == 0 || combo.Count < index + 1)
{
return 0;
}
else
{
return combo[index].ComboDamage;
}
}

//获得招式动画数量
public int TryGetComboCount() => combo.Count;
}

对于连招这方面的处理,先声明一个index作为招式的索引,再声明一个bool类型的方法CanAttackInput(),用于判断是否可以进行攻击输入。如果为true的话,当按下鼠标左键(攻击键)时,index会++,播放下一段动画。

那么CanAttackInput()这个方法怎么写?首先,当AnimationAtTag(“Move”)——角色在移动时候可以攻击(我把Idle动画的标签也改成了Move);然后,动画在过渡状态时不能攻击(animator.IsInTransition)——因为我发现,角色在待机动画向翻滚动画过渡时进行攻击,会发生抽搐现象。最后呢,当前动画播放进度超过60%就能攻击了。

最后处理连招索引的问题,比如角色攻击了一下,长时间不操作、下次攻击也应该从第一段攻击开始,currentIndex需要归为0了。我的思路是,当动画播放完、索引值就归为0;连招是只能在动画的60%-100%这个区间触发的。

展示代码:

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
using GGG.Tool;
using UnityEngine;

public class PlayerCombo : MonoBehaviour
{
private Animator animator;
[SerializeField,Header("角色组合技")]private Combo currentCombo;
[SerializeField, Header("技能失效时间")] private float outTime = 1.4f;
//当前招式索引值
private int currentIndex = 0;

private void Awake()
{
animator = GetComponent<Animator>();
}

// Update is called once per frame
void Update()
{
Attack();
Reset();
}

//是否可以攻击输入
private bool CanAttackInput()
{
if (animator.AnimationAtTag("Move") && !animator.IsInTransition(0))
return true;
if(animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.6f)
return true;
return false;
}

/// <summary>
/// 攻击逻辑书写
/// </summary>
private void Attack()
{
//不能攻击就别执行逻辑了
if (CanAttackInput() == false)
return;
if(GameInputManager.Instance().attack)
{
animator.CrossFade(currentCombo.TryGetComboName(currentIndex), 0.1f);
currentIndex++;
outTime = 999.4f;
}
if(currentIndex == currentCombo.TryGetComboCount())
{
currentIndex = 0;
}
}

private void Reset()
{
if(animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.95f)
currentIndex = 0;
}
}

12.14优化移动逻辑。
优化
这里把状态机优化了一下,把原来的翻滚动画融合成一个混合树,idle和run也融合成一个混合树,把HasInput参数取消掉了,相机旋转部分的逻辑稍微改变,当动画在move状态且输入不等于Vector2.zero时可以跟随相机旋转。