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

2023-10-29 21:10

本文主要是介绍[Unity]类似节奏地牢的音游旋律系统的搭建记录,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

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

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

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

    • [Unity]类似节奏地牢的音游节奏系统的搭建
  • 前言
  • 一、最初的方案(不太可行,缺点多)
    • 1. 需求
    • 2.实现与缺点
  • 二、对上述方案的改进
    • 1. 需求
    • 2.实现与缺点
    • 3.发现的小问题


前言

该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 APIpublic 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;elsetapLevel = TapLevel.Good;//TODO:隐藏节点 isShow = false;return tapLevel;}#endregion#region Helperprivate 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 APIpublic 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拍音频,然后在进入奏乐模式时循环播放这个音频。
    缺点
    音频时间要足够精确,不然会出现延迟问题
    该游戏考虑变速问题,音频的变速会导致失真

二、对上述方案的改进

1. 需求

对旧方案的改进

  1. 将浪费性能的部分集成到RyhthmManager中
  2. 更改音乐播放的模式,用每秒播放一拍代替4秒播放一次四拍音频,以增加精度

新需求

  1. 加入减速、提速效果(如节奏地牢)

2.实现与缺点

缺点
通过设置Time.deltaTime的方式来实现减速提速效果虽然很方便,但用这种方法在减速的时候会出现降低帧数的情况,因为fixedUpdate是指定的帧数,在默认50帧的情况下,如果我们将Time.deltaTime/=2,那么除2后游戏的1秒等于正常下游戏中的0.5秒,但fixedUpdate仍然不变,就是说此时的帧数按照现实是2秒50帧,而正常情况下是1秒50帧,帧数是和除的系数成反比的。为了更好理解,在极端情况下,我们把系数设为50,那么现实中每秒就是1帧,这显然不是我们想要的。所以我考虑用复杂的方式,为所以和减速增速相关的地方乘除这个增量。

3.发现的小问题

  1. 有部分切换状态和播放动画的代码被我分开在了Update和FixedUpdate中,导致出现了动画没有播放的问题,估计是因为在Update中的一帧较FixedUpdate中的一帧早,正巧使得这部分内容被覆盖了,经过修改到Update中,问题得到解决。

这篇关于[Unity]类似节奏地牢的音游旋律系统的搭建记录的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/303364

相关文章

更改linux系统的默认Python版本方式

《更改linux系统的默认Python版本方式》通过删除原Python软链接并创建指向python3.6的新链接,可切换系统默认Python版本,需注意版本冲突、环境混乱及维护问题,建议使用pyenv... 目录更改系统的默认python版本软链接软链接的特点创建软链接的命令使用场景注意事项总结更改系统的默

Java 与 LibreOffice 集成开发指南(环境搭建及代码示例)

《Java与LibreOffice集成开发指南(环境搭建及代码示例)》本文介绍Java与LibreOffice的集成方法,涵盖环境配置、API调用、文档转换、UNO桥接及REST接口等技术,提供... 目录1. 引言2. 环境搭建2.1 安装 LibreOffice2.2 配置 Java 开发环境2.3 配

基于Spring Boot 的小区人脸识别与出入记录管理系统功能

《基于SpringBoot的小区人脸识别与出入记录管理系统功能》文章介绍基于SpringBoot框架与百度AI人脸识别API的小区出入管理系统,实现自动识别、记录及查询功能,涵盖技术选型、数据模型... 目录系统功能概述技术栈选择核心依赖配置数据模型设计出入记录实体类出入记录查询表单出入记录 VO 类(用于

在Linux系统上连接GitHub的方法步骤(适用2025年)

《在Linux系统上连接GitHub的方法步骤(适用2025年)》在2025年,使用Linux系统连接GitHub的推荐方式是通过SSH(SecureShell)协议进行身份验证,这种方式不仅安全,还... 目录步骤一:检查并安装 Git步骤二:生成 SSH 密钥步骤三:将 SSH 公钥添加到 github

java中pdf模版填充表单踩坑实战记录(itextPdf、openPdf、pdfbox)

《java中pdf模版填充表单踩坑实战记录(itextPdf、openPdf、pdfbox)》:本文主要介绍java中pdf模版填充表单踩坑的相关资料,OpenPDF、iText、PDFBox是三... 目录准备Pdf模版方法1:itextpdf7填充表单(1)加入依赖(2)代码(3)遇到的问题方法2:pd

Python极速搭建局域网文件共享服务器完整指南

《Python极速搭建局域网文件共享服务器完整指南》在办公室或家庭局域网中快速共享文件时,许多人会选择第三方工具或云存储服务,但这些方案往往存在隐私泄露风险或需要复杂配置,下面我们就来看看如何使用Py... 目录一、android基础版:HTTP文件共享的魔法命令1. 一行代码启动HTTP服务器2. 关键参

Linux系统中查询JDK安装目录的几种常用方法

《Linux系统中查询JDK安装目录的几种常用方法》:本文主要介绍Linux系统中查询JDK安装目录的几种常用方法,方法分别是通过update-alternatives、Java命令、环境变量及目... 目录方法 1:通过update-alternatives查询(推荐)方法 2:检查所有已安装的 JDK方

Linux系统之lvcreate命令使用解读

《Linux系统之lvcreate命令使用解读》lvcreate是LVM中创建逻辑卷的核心命令,支持线性、条带化、RAID、镜像、快照、瘦池和缓存池等多种类型,实现灵活存储资源管理,需注意空间分配、R... 目录lvcreate命令详解一、命令概述二、语法格式三、核心功能四、选项详解五、使用示例1. 创建逻

Zabbix在MySQL性能监控方面的运用及最佳实践记录

《Zabbix在MySQL性能监控方面的运用及最佳实践记录》Zabbix通过自定义脚本和内置模板监控MySQL核心指标(连接、查询、资源、复制),支持自动发现多实例及告警通知,结合可视化仪表盘,可有效... 目录一、核心监控指标及配置1. 关键监控指标示例2. 配置方法二、自动发现与多实例管理1. 实践步骤

使用Python构建一个高效的日志处理系统

《使用Python构建一个高效的日志处理系统》这篇文章主要为大家详细讲解了如何使用Python开发一个专业的日志分析工具,能够自动化处理、分析和可视化各类日志文件,大幅提升运维效率,需要的可以了解下... 目录环境准备工具功能概述完整代码实现代码深度解析1. 类设计与初始化2. 日志解析核心逻辑3. 文件处