之前写过一个系列《HTML5 2D平台游戏开发》,在此过程中发现有很多知识点没有掌握,而且用纯JavaScript来开发一个游戏效率极低,因为调试与地图编辑都没有可视化的工具,开发起来费时费力,加上业余时间有限,我决定暂且中止开发。为了弥补缺少的知识点,我打算先学习和借鉴一下Unity的开发思路,于是把原先的游戏素材移植了过来。首先还是先从人物的动作开始,Unity的动画与之前开发时的思路有很大不同,Unity没有“帧”这一概念,也就是说没有办法获取到当前动画播放到第几帧,只能通过normalizedTime
来获取动画播放的百分比进度,一下子让适应这种模式有些困难。先不考虑代码实现细节,整理一下思路,人物实现三连击的状态机大致如下:
- Idle ⇢ attack_a ⇢ Idle
- Idle ⇢ attack_a ⇢ attack_b ⇢ Idle
- Idle ⇢ attack_a ⇢ attack_b ⇢ attack_c ⇢ Idle
在Idle状态下按下攻击键,过渡到attack_a,如果没有下一步操作,attack_a动画播放完毕后还原到Idle状态。如果在attack_a状态下再次按下攻击键,则过渡到attack_b,如果在attack_b状态下无操作,动画播放完毕后还原到Idle,依此类推,多段连击也是一样的。为了表示攻击的状态,需要为Animator添加一个attack
参数:
attack等于0表示处于非攻击状态,attack等于1表示处于attack_a,2和3分别表示处于attack_b、attack_c。
接下来一步一步分析各个状态间的过渡。
Idle ⇢ attack_a
Idle状态可以随时通过按下攻击键打断并过渡到attack_a,故没有 Has Exit Time
,其它项也都置为0。
attack_a ⇢ Idle
最初我是这样考虑的,给这个过渡设置 Has Exit Time
,如果没有任何操作,让其还原到Idle,于是有了
但最终运行时攻击总是会卡在最后一帧一段时间,只有把Exit Time设置为0.1才流畅。也许是动画播放速度设置太快的缘故,但我总觉得通过Exit Time的方式来实现不太好。在查阅一番资料后,我觉得给动作添加Behaviour是比较好的方式。选中attack_a,为其添加一个名为SetNormalizeTime
的Behaviour:
public class SetNormalizedTime : StateMachineBehaviour {
private string targetParameter = "Normalized Time";
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.SetFloat(targetParameter, 0);
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.SetFloat(targetParameter, stateInfo.normalizedTime);
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.SetFloat(targetParameter, 0);
}
}
每次进入该状态,先将Normalized Time
重置为0,表示动画从头开始,然后在OnStateUpdate
中更新它,最后退出时再置为0。同时也别忘了在Animator中添加一个名为Normalized Time
的参数(float类型):
现在可以把attack_a ⇢ Idle的 Has Exit Time
去掉了,同时添加一个 Conditions
:
表示当动画播放完毕时(NormalizeTime > 1.0f) 过渡到 Idle。同样的,也给attack_b ⇢ Idle 和 attack_c ⇢ Idle 附加上这个 Behaviour。
代码实现
常规操作:
private Animator anim;
private Rigidbody2D myRigidbody;
private AnimatorStateInfo stateInfo;
public int hitCount = 0; //0:表示idle状态。 1:表示当前正在进行attack_a。 2:attack_b。 3:attack_c。
void Start ()
{
anim = GetComponent<Animator>(); //获取动画组件
myRigidbody = GetComponent<Rigidbody2D>(); //获取刚体组件
}
void Update()
{
stateInfo = anim.GetCurrentAnimatorStateInfo(0);
HandleInput();
}
下面实现HandleInput方法:
void HandleInput()
{
//若动画为三种状态之一并且已经播放完毕
if ((stateInfo.IsName("attack_a") || stateInfo.IsName("attack_b") || stateInfo.IsName("attack_c")) && stateInfo.normalizedTime > 1.0f)
{
hitCount = 0; //将hitCount重置为0,即Idle状态
anim.SetInteger("attack", hitCount);
attack = false;
}
//按下键盘J键攻击
if (Input.GetKeyDown(KeyCode.J))
{
HandleAttack();
}
}
(这里踩了一个坑,实现这部分逻辑时我是远程操作完成的,发送的指令实际上有一定的延迟,这样就导致按住键盘J键不放可以连续触发攻击,也就是连发。让我误以为GetKeyDown是连续触发的,实际上GetKeyDown只触发一次。)
HandleAttack的实现:
void HandleAttack()
{
//若处于Idle状态,则直接打断并过渡到attack_a(攻击阶段一)
if (stateInfo.IsName("Idle") && hitCount == 0)
{
hitCount = 1;
anim.SetInteger("attack", hitCount);
}
//如果当前动画处于attack_a(攻击阶段一)并且该动画播放进度小于80%,此时按下攻击键可过渡到攻击阶段二
else if(stateInfo.IsName("attack_a") && hitCount == 1 && stateInfo.normalizedTime < 0.8f)
{
hitCount = 2;
}
//同上
else if(stateInfo.IsName("attack_b") && hitCount == 2 && stateInfo.normalizedTime < 0.8f)
{
hitCount = 3;
}
}
这里要注意,比如在触发第二段攻击时需要满足条件 normalizedTime < 0.8f ,但此时按下攻击键是不会马上播放第二段攻击动画的,如果马上播放就显得动作非常不协调了,应该等到第一阶段的攻击动画播放到一定阶段才播放第二段攻击动画。所以需要给关键帧添加一个方法,告诉动画系统在这一帧要执行某个指令。
void GoToNextAttackAction()
{
anim.SetInteger("attack", hitCount);
}
给第7帧添加一个事件,指向 GoToNextAttackAction 这个方法,动画将在第7帧的时候被打断并进入下一个攻击动画。如果hitCount没有改变,SetInteger("attack",hitCount) 不会影响当前正在播放的动画,动画会持续播放完毕(至第9帧)。
P.S. 虽然费了一些周折,但还是把效果实现出来了,Unity的开发效率比JS高出太多,大概100倍左右吧,这是在开发过程中的感觉。不知道离游戏成品还有多遥远,但我还是会继续学习。