IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> [Unity]类似节奏地牢的音游旋律系统的搭建记录(持续更新) -> 正文阅读

[游戏开发][Unity]类似节奏地牢的音游旋律系统的搭建记录(持续更新)

[Unity]类似节奏地牢的音游节奏系统的搭建

近期发现之前写的一些文章看的人还是有不少,但大部分都是找了解决方案然后转述了一遍罢了,心想既然有人看不如做点使用的东西出来,于是结合现在正好在做音游的demo,而音游节奏一块正好是我没接触过的,所以可以记录下来并给大家一同参考下。如果有人能发现有不足之处可以评论指出,我看到了会尽快更改。


前言

该demo借鉴了啪嗒砰、节奏地牢的相关玩法即设定。demo的游戏类型是横版+rpg+roguelite+音游元素。

一、最初的方案(不太可行,缺点多)

我对于常见的音游的认知就几处,跟着节奏,在到达节奏的那个点附近的时候按下Tap键触发。想起来很简单,好,于是上手做。

1. 需求

  1. 类似于节奏地牢的节奏条,在开启节奏模式时可以在Start位置生成音符Note并匀速运动到End位置,在经过TapLine附近时,按下即可触发Node的事件,并记录下按键的keycode。在距离TapLine不同距离(位置)按下Tap键触发反馈对应的Tap品质, 对象池回收音符Note。如下图节奏条示意图
  2. 音乐/节奏,我们选了后者,循环播放一段节拍音频,可以和音符到达TapLine的时间对上

2.实现与缺点

  1. 实现
    为了实现节奏条,我创建了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();
                //TODO: combo断连
                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;
        //TODO:隐藏节点 isShow = false;

        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
}
  1. 实现
    为了实现节拍能对上Note移动到TapLine真的苦了我这种对音乐一窍不通的人了,所以我用了最简单粗暴的办法,录制了4拍音频,然后在进入奏乐模式时循环播放这个音频。
    缺点
    音频时间要足够精确,不然会出现延迟问题
    该游戏考虑变速问题,音频的变速会导致失真

未完

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-12-13 13:11:29  更:2021-12-13 13:12:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/27 20:59:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码