[Unity]类似节奏地牢的音游节奏系统的搭建
近期发现之前写的一些文章看的人还是有不少,但大部分都是找了解决方案然后转述了一遍罢了,心想既然有人看不如做点使用的东西出来,于是结合现在正好在做音游的demo,而音游节奏一块正好是我没接触过的,所以可以记录下来并给大家一同参考下。如果有人能发现有不足之处可以评论指出,我看到了会尽快更改。
前言
该demo借鉴了啪嗒砰、节奏地牢的相关玩法即设定。demo的游戏类型是横版+rpg+roguelite+音游元素。
一、最初的方案(不太可行,缺点多)
我对于常见的音游的认知就几处,跟着节奏,在到达节奏的那个点附近的时候按下Tap键触发。想起来很简单,好,于是上手做。
1. 需求
- 类似于节奏地牢的节奏条,在开启节奏模式时可以在Start位置生成音符Note并匀速运动到End位置,在经过TapLine附近时,按下即可触发Node的事件,并记录下按键的keycode。在距离TapLine不同距离(位置)按下Tap键触发反馈对应的Tap品质, 对象池回收音符Note。如下图
- 音乐/节奏,我们选了后者,循环播放一段节拍音频,可以和音符到达TapLine的时间对上
2.实现与缺点
- 实现
为了实现节奏条,我创建了Note类和RhythmManager,如下,基本思路是在按下Space进入奏乐模式MusicMode时为noteQueue入队noteIdx所对应集合里的Note并激活和显示(nodeIdx++),激活后的NoteremainTime随时间减少,并在对应的阶段按下Tap键时调用API取出队首Note调用它的API,来检测Tap品质和隐藏Note。(注:仅为隐藏,在移动到end点时不会重置,因为激活下一个Note需要当前NoteremainTime等于1f,为了保证后续Note的持续出现只可采用这种方法。)在重置阶段将把Note的位置重新定位到start点,remainTime重设为duration,bool值全部归为默认值,这样就可在下次激活时继续使用,实现了对象池的功能。而记录Tap的按键由string来记录。 遇到的问题 记录按键功能我一开始用了List集合,后续理所应当的比较是总为false,原因在于两个集合的值虽然一样但地址不同,所以两个集合不相等,可以实际举例,由a集合赋值给b集合,那b集合就等于a集合,对b集合的修改就相当于修改a集合(简单的数据结构,但当时脑抽了),后续用string来记录虽解决了比较问题,但仍留下了不同按键对应Tap品质的储存问题,这部分就留在后续更新中探讨吧。 缺点 性能的浪费:很多属性和功能可以集中在RhythmManager中,而非单个Note中,部分单例可由回调替代。 时间精度问题:暂时发现不出问题,但是肯定存在
public class Note : MonoBehaviour
{
private float remainTime;
private float duration = 2;
private float perfectTime = 1;
private float perfectTimeOffset = 0.05f;
private float greatTimeOffset = 0.15f;
private float moveSpeed = 1f;
private RectTransform startLine;
private RectTransform endLine;
private RectTransform rectTrans;
public bool isActivated;
public bool isShow;
public bool hasStartedNextNote;
public bool hasTapped;
private void Update()
{
if (isActivated)
{
remainTime -= Time.deltaTime;
if (remainTime < perfectTime - greatTimeOffset && isShow && !hasTapped)
{
HideNote();
RhythmManager.Instance.NoteDeQueue();
RhythmManager.Instance.BreakCombo();
}
if(remainTime <= 0.001f )
{
Reset();
}
if (remainTime <= perfectTime + 0.01f && remainTime >= perfectTime - 0.01f && !hasStartedNextNote)
{
hasStartedNextNote = true;
RhythmManager.Instance.StartNextNote();
}
}
}
private void FixedUpdate()
{
if (isActivated)
{
rectTrans.anchoredPosition += new Vector2((endLine.anchoredPosition.x - startLine.anchoredPosition.x) * Time.deltaTime / 2, 0);
}
}
#region public API
public void Reset()
{
transform.position = startLine.transform.position;
remainTime = duration;
isActivated = false;
hasStartedNextNote = false;
hasTapped = false;
HideNote();
}
public void SetActive()
{
isActivated = true;
ShowNote();
}
public TapLevel BeTapped()
{
hasTapped = true;
HideNote();
RhythmManager.Instance.NoteDeQueue();
TapLevel tapLevel = TapLevel.Miss;
if (remainTime > perfectTime + greatTimeOffset || remainTime < perfectTime - greatTimeOffset)
tapLevel = TapLevel.Miss;
else if (remainTime <= perfectTime + perfectTimeOffset && remainTime >= perfectTime - perfectTimeOffset)
tapLevel = TapLevel.Perfect;
else
tapLevel = TapLevel.Good;
return tapLevel;
}
#endregion
#region Helper
private void ShowNote()
{
isShow = true;
GetComponent<CanvasGroup>().alpha = 1;
}
private void HideNote()
{
isShow = false;
GetComponent<CanvasGroup>().alpha = 0;
}
#endregion
}
public class RhythmManager : UnitySingleton<RhythmManager>
{
public Note[] notes;
public Queue<Note> noteQueue;
public int noteIdx = 0;
public string keyCodes;
public bool inMusicMode;
private void Awake()
{
initialization();
}
void initialization()
{
notes = GameObject.Find("Notes_U").GetComponentsInChildren<Note>();
noteQueue = new Queue<Note>();
keyCodes = "";
}
#region Public API
public bool InAndOutMusicMode()
{
if (inMusicMode)
{
inMusicMode = false;
AudioManager.Instance.OutMusicMode();
for (int idx = 0; idx < notes.Length; idx++)
{
notes[idx].Reset();
}
noteQueue.Clear();
}
else
{
inMusicMode = true;
AudioManager.Instance.InMusicMode();
noteQueue.Enqueue(notes[noteIdx]);
notes[noteIdx].SetActive();
return false;
}
}
public void StartNextNote()
{
noteIdx = (noteIdx + 1) % notes.Length;
noteQueue.Enqueue(notes[noteIdx]);
notes[noteIdx].SetActive();
}
public void NoteDeQueue()
{
noteQueue.Dequeue();
}
public void BreakCombo()
{
keyCodes = "";
UIManager.Instance.GetPanelBase("ComboPanel").GetComponent<ComboPanel>().BreakCombo();
}
public void Tap(KeyCode keyCode)
{
if (noteQueue.Count == 0) return;
TapLevel tapLevel = noteQueue.Peek().BeTapped();
UIManager.Instance.GetPanelBase("TapLevelPanel").GetComponent<TapLevelPanel>().ShowTapLevelText(tapLevel);
switch (tapLevel)
{
case TapLevel.Miss:
keyCodes = "";
UIManager.Instance.GetPanelBase("ComboPanel").GetComponent<ComboPanel>().BreakCombo();
break;
case TapLevel.Good:
keyCodes += keyCode.ToString();
UIManager.Instance.GetPanelBase("ComboPanel").GetComponent<ComboPanel>().RefreshCombo();
break;
case TapLevel.Perfect:
keyCodes += keyCode.ToString();
UIManager.Instance.GetPanelBase("ComboPanel").GetComponent<ComboPanel>().RefreshCombo();
break;
default:
Debug.LogError("传入的TapLevel有问题");
break;
}
}
#endregion
}
- 实现
为了实现节拍能对上Note移动到TapLine真的苦了我这种对音乐一窍不通的人了,所以我用了最简单粗暴的办法,录制了4拍音频,然后在进入奏乐模式时循环播放这个音频。 缺点 音频时间要足够精确,不然会出现延迟问题 该游戏考虑变速问题,音频的变速会导致失真
未完
|