Unity下落式音游实现——(4)鼓盘动画及敲击判定
前期准备
导入资源及布置UI,创建脚本DrumController.cs
思路
由于有多个鼓盘,需要为每个鼓盘创建一个Animator,每个鼓盘有错误敲击和正确敲击两个Animation。另外正确敲击时在鼓盘的上方会有额外的动画,由于美术那边将额外动画和正确敲击的鼓盘分开给我,而且出于复用性考虑,我将额外动画配置鼓盘动画保存为一个prefab,在显示正确动画的同时在对应位置创建(事实证明这是个错误…)。配置动画难度不高,但会有很多很烦人的坑
正常的音游判定思路应该是这样的:滑块与鼓盘的距离小于一定范围时判定敲击成功(可以进一步在范围中细分距离改变敲击等级,good,perfect…),若在范围外敲击鼓盘或滑块到达鼓盘处时仍未被敲击则判定为敲击失败;但该需求中的判定有所不同:滑块到达鼓盘位时并不会销毁,而是在原地待机,若后一个滑块到达鼓盘时上一个滑块仍未销毁,则销毁上一个滑块,敲击时鼓盘处有滑块时判定敲击成功,若无滑块则敲击失败。虽然需求不同,但思路是近似的
实现过程
鼓盘动画
(这部分确实不是很熟悉,如果有错误麻烦指出)
在上一节中我们提到Drum属于UI部分,因此需要创建Canvas,在里面创建好Drum并放在空对象下管理
注意我们还给每个Drum添加一个Audio Source组件,用于播放敲击鼓声
// DrumController.cs
public class DrumController : MonoBehaviour
{
[SerializeField] private Sprite DrumPic;
[SerializeField] private Sprite rightDrumPic;
[SerializeField] private Animator anim;
[SerializeField] private GameObject RightHitAniPrefab;// 附加动画预置体,显示完删除
private AudioSource audio;
private GameObject curSlider = null; // 离终点最近的滑块
private GameObject secondSlider = null; // 第二个滑块
}
配置Animator Controller
在controller中增加参数ifHit、ifRightHit用于后续敲击判断
注意从NULL过渡到动画状态时需要将Has Exit Time取消勾选,不然的话会出现动画播放太快的问题
配置Animation
大概就是Add Property,然后剩下的慢慢凹吧,太繁琐了,略
tip:你是否经历过做好Animation后想预览想过,却发现Preview那栏的播放键点不开,那是因为你可能直接在Project的资源文件夹里打开Animation了,但预览需要在Hiearchy选中(左键单击Animation所在的gameobject上)所以正确的预览动画方式应该是先打开Animation窗口,选中对应gameobject,最后在preview下方选取你想要看的Animation
接受击鼓信号
我们上一节实现了观察者模式,可以在DrumController中使用,监听击鼓消息
private void Awake()
{
// 接受一个编号,在hitDrum判断是不是自己被敲
Messenger<int>.AddListener(GameEvent.DRUM_HIT, hitDrum);
}
private void OnDestroy()
{
Messenger<int>.RemoveListener(GameEvent.DRUM_HIT, hitDrum);
}
实现hitDrum
// 判断敲击是否正确
bool judgeHit()
{
if (curSlider)
{
bool t = curSlider.GetComponent<SliderController>().judgeHit();
// 击鼓正确则销毁
if (t)
Destroy(curSlider);
return t;
}
else
return false;
}
void hitDrum(int durmNum)
{
// show状态不能击鼓
if (("Drum" + durmNum) == name)
{
audio.Play();
if (status == MissionStatus.game)
{
bool t = judgeHit();
anim.SetBool("ifHit", true);
anim.SetBool("ifRightHit", t);
if (t)
{
// 生成额外动画
GameObject rightHitAni = Instantiate(RightHitAniPrefab, transform) as GameObject;
rightHitAni.transform.position = new Vector3(transform.position.x + 30, transform.position.y + 300, transform.position.z);
StartCoroutine(DestoryAni(rightHitAni));
}
}
}
}
关于生成额外动画部分,我们在鼓盘附近生成了预先配置好的prefab,动画自动播放,然后用协程在播放完之后删除
敲击判定
SenceController需要在生成滑块时通知对应的drum
// SenceController.cs
private Transform[] Drums;
void Start()
{
...
Drums = Drum.GetComponentsInChildren<Transform>();
}
void genSlider(int OrbitNum)
{
...
// 设置slider初始条件
...
// 传给鼓,用于判断正确率和销毁滑块
Drums[OrbitNum + 1].SendMessage("setNewSlider", slider);
}
在DrumController更新滑块信息,判断是否销毁滑块。回忆一下我们的需求,在展示阶段(show)滑块可视且碰到鼓盘后销毁,在游戏阶段(game)滑块不可视且要根据轨道上的其他滑块来判定是否销毁当前滑块,这就意味着鼓盘和滑块不能解耦,滑块不能单独决定是否销毁自身,必须从鼓盘处获得信息(是否有上一个滑块?)
我们给每个鼓盘维护一个队列,记录在这个轨道上的滑块,并在状态切换时清除所有还留在鼓盘处的滑块
// 被SenceController调用
void setNewSlider(GameObject slider)
{
// 如果这条轨道上之前有滑块,就入队列
if (curSlider == null)
curSlider = slider;
else
sliderPool.Enqueue(slider);
}
void ifDestoryCurrentSlider()
{
if (curSlider)
if (curSlider.GetComponent<SliderController>().status == MissionStatus.show)
{
if (curSlider.GetComponent<SliderController>().ifDestory)
{
Destroy(curSlider);
if (sliderPool.Count > 0)
curSlider = sliderPool.Dequeue();
showRightDrumPic();
}
}
else
{
if (sliderPool.Count > 0)
{
// 如果下一个滑块到了,那就销毁前一个滑块
if (Vector3.Distance(curSlider.transform.position,sliderPool.Peek().transform.position)<0.001f)
{
Destroy(curSlider);
curSlider = sliderPool.Dequeue();
}
}
}
}
void Update()
{
ifDestoryCurrentSlider();
}
Q:在Update里调用ifDestoryCurrentSlider()会不会有点浪费,毕竟只有在敲鼓或第二个滑块出现时才有可能会销毁
A:你说得对,但是没必要。而且滑块出现频率还挺高,优化不了多少
注意到在ifDestoryCurrentSlider函数中我们调用了curSlider.GetComponent().ifDestory,打开SliderController
// SliderController.cs
public bool ifDestory = false;
private void FixedUpdate()
{
// 移动滑块
...
// game状态下销毁交给DrumController处理
// show状态下自己通知DrumController销毁
if (status == MissionStatus.show && targetIndex == target.Length)
{
ifDestory = true;
}
}
Q:为什么不能在DrumController中(或者让slider自己)统一处理slider销毁?
A:从设计上slider和Drum是分离的(slider在gameobject层,Drum在UI层),但是需求中game状态下slider的销毁不能单独完成,这就破坏了分离。如果是传统音游,就直接全部在DrumController解决就好
总结
我们现在已经能生成滑块,敲击鼓盘并根据滑块信息判断敲击是否合法,显示对应的动画。接下来实现如何“正确地生成滑块”
在上述部分中省略了部分不重要的信息,如怎么样接受输入等,这一部分暂时是用键盘接受输入,可以自己实现(从硬件处接受输入的方案后续会提)