【Unity】 节奏类游戏的表盘卡点功能
目录
1:前言
2:开始 ---(方案一根据音频数据自动生成节奏点)
2.1:功能实现选择---音频可视化
2.2:结论-(结果不准确)
3:游戏表盘的实现----(方案二自给自足,自动输入用时自动读取)
3.1:游戏节奏表盘类型选取
3.2:游戏功能分析
4:表盘功能的详细实现
4.1:表盘的输入功能
4.2:表盘的读取功能
5:总结
6:项目源码及参考资料
1:前言
最近迷上音乐游戏,发现随着带感的背景音乐和满屏的节奏点,让我很兴奋,游戏让我耳目手同时行动,产生了非常强烈的代入感,不过音游类的游戏最注重的还是节奏点的卡点,如果能够闭着眼睛玩下去并且连续Combo那绝对称得上是一款好游戏。所以我决定复刻一个简单的音游卡点功能!
(图片来自网络侵删)
2:开始 ---(方案一根据音频数据自动生成节奏点)
2.1:功能实现选择---音频可视化
一开始我构想音游节奏点随着背景音乐的高低频率变化,那么可以用unity自带的音频数据分析来实现,说干就干!
先上效果图:
首先我们先分析频率较高的点在什么时候出现?
- 系统会分析每一帧的音频并转化为一个数组。
- 我们截取需要的音乐并记录下来,该期间出现每一个组数据的平均值。
- 根据平均值的60%设置阈值,并输出打印出来,观察可行性。
1. 分析结束后我们开始写代码,首先是音频分析部分,用unity自带的API。
float[] spectrum = new float[256];
//将音频片段转化为数据部分AudioListener.GetSpectrumData(spectrum, 0, FFTWindow.Rectangular);timeCount -= Time.deltaTime;if (timeCount <= 0){for (int i = 0; i < cubes.Count; i++){cubes[i].transform.localScale = new Vector3(cubes[i].localScale.x, spectrum[i] * StepCount, cubes[i].localScale.z);}//每隔0.1s检测一次timeCount = 0.1f;}
2.然后就是分析每次的片段数组平均值。
private void OnDisable(){
//该片段总体的平均值print(sum / m);}
//每0.1s数据的平均值float Pingjun(float[] data){float a = 0;foreach (var item in data){a += item;}return a / data.Length;}
3.设置阈值再次遍历
if (Pingjun(spectrum) > 0.0035f){
//记录点,当前平均值大于阈值时在该时刻输出print(1);}sum += Pingjun(spectrum);
2.2:结论-(结果不准确)
结果并没有我想象的那么好,有时对得上有时对不上,音乐的频率和音高和我们听到的是有差别的,可能音乐内部数据比我们想象的更复杂,所以我要换一种方法。
3:游戏表盘的实现----(方案二自给自足,自动输入用时自动读取)
3.1:游戏节奏表盘类型选取
第一个是传统瀑布型节奏点分别从不同轨道下落并跟着背景音乐同步下落。
(图片源于网络侵删)
第二种则是比较小众的表盘式节奏点,伴随着音乐节奏不断更新自己的表盘点,玩家可以在根据音乐节奏去击点,然后配合一些动作类游戏也会让玩家有很强的代入感。本文选择此种类型来展开讨论和分析其功能如何实现 。大概类型为下图
3.2:游戏功能分析
先放出结果,带着结果去分析。
表盘应包含哪些元素
- 拥有一个指针并且能每隔一段时间去围绕中心旋转。
- 表盘上包含许多节奏点(下文称点)。
表盘应包含哪些功能
- 指针和点之间有触发条件。
- 指针能随着表盘动态更新,以波为单位。
- 点拥有3种状态一种为失活不显示,一种为激活显示为黑色,一种为激活但没有卡上为红色圆圈。
4:表盘功能的详细实现
4.1:表盘的输入功能
首先需要实现指针转动的功能 ,指针每隔0.5s旋转一次,旋转一次为10°正好对应表盘点数36个,一波为18s。
IEnumerator pointerCoroutine(){while (true){//时间间隔默认为0.5syield return waitForSeconds;//speed为旋转的度数默认为10°transform.Rotate(new Vector3(0, 0, Speed));}}
其次需要实现表盘波数的数据结构,我们需要一个数据存储波数,一个数组存储当前表盘应出现的点,数组长度默认为36对应表盘36个点,其中数组值只有0和1,0表示点隐藏,1表示点显示。由于我们需要在游戏运行时记录数据并且退出后保存数据,所以我们采用ScriptableObject类。
[CreateAssetMenu(fileName ="newitem_data",menuName ="CreateData/Create New item_Data")]
public class item_Data : ScriptableObject
{//表盘上的点数集合public List<ItemData> itemDatas;
}[System.Serializable]
public class ItemData
{//波数public int wave;//时间暂时未用上public float time;//该波数的状态表0表示隐藏,1表示应该出现。public int[] statetable;public ItemData(){}public ItemData(int wave,float time,int[] statetable){this.wave=wave;this.time=time;this.statetable=statetable;}
}
最后就是如何去输入了,大概思路就是指针旋转一次对应该波数数据结构中的数组索引值自增一表示该位置的点(36个点),如果按下空格键则证明此处为节奏点,将该处的数组值改为1。并在指针组件Z轴为0时默认进入下一波的录制,并且会将数组重新初始化,数组值全为0。
public class PrinfScript : MonoBehaviour
{[SerializeField] float Speed = 10;WaitForSeconds waitForSeconds;[SerializeField] float time = 0.5f;public item_Data items;public int wave = 0;float wavenexttime;float wavetime = 0;int statetable_count = 0;public int[] statetable = new int[36];public AudioSource bgm;public Slider bgmslider;private void Awake(){Array.Clear(statetable, 0, statetable.Length - 1);waitForSeconds = new WaitForSeconds(time);wavenexttime = wavetime;}void Start(){transform.rotation = Quaternion.Euler(Vector3.zero);StartCoroutine(nameof(pointerCoroutine));StartCoroutine(nameof(StatetAction));}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){if (statetable[statetable_count] != 1){statetable[statetable_count] = 1;}}}/// <summary>/// 指针索引动态更新/// </summary>/// <returns></returns>IEnumerator pointerCoroutine(){while (true){yield return waitForSeconds;statetable_count += 1;statetable_count = statetable_count % 35;transform.Rotate(new Vector3(0, 0, Speed));}}/// <summary>/// 每经过一轮自动更新/// </summary>/// <returns></returns>IEnumerator StatetAction(){while (true){if (TransformRotation.Instance.GetInspectorRotationValueMethod(transform).z == 0){wavetime = wavenexttime;ItemData pnode = new ItemData();pnode.wave = wave;pnode.time = wavetime;int[] pnodeInt = new int[statetable.Length];for (int i = 0; i < statetable.Length; i++){pnodeInt[i] = statetable[i];}pnode.statetable=pnodeInt;items.itemDatas.Add(pnode);wavenexttime = bgm.time;wave++;Array.Clear(statetable, 0, statetable.Length - 1);yield return new WaitForSeconds(0.7f);}yield return null;}}
录取时先创建一个ScriptableObject文件,并拖入该脚本中。
(如何创建数据文件)
(数据文件样式)
4.2:表盘的读取功能
先不说表盘如何读取,我们先解决如何判定指针和点什么时候接触什么时候离开,起初有三种方案:
- 碰撞检测,在OnCollisionEnter2D时开始执行接触时的逻辑,在OnCollisionExit2D执行结束时的逻辑,在OnCollisionStay2D处理逻辑。
- 运用Physics2D.IgnoreLayerCollision频繁的忽略层级来判断是否进入检测范围。
- 触发检测时OnCollisionEnter2D开启触发逻辑处理协程,OnCollisionExit2D关闭逻辑处理协程。
经过多轮测试比较发现第三种是最稳定的,前两种相对于不太稳定有时能检测到有时则不能检测到。
检测的问题解决了,下一步就是检测逻辑应包含那些?观察下图
- 当检测时按下空格键会播放指针伸缩动画。
- 检测成功点消失,检测失败点变为红色。
将该脚本挂在36个点上。
public class Item : MonoBehaviour
{bool IsClick = false;//输入正确的图片显示[SerializeField] Sprite Trueimage;//输入错误的图片显示[SerializeField] Sprite Falseimage;//获得图片组件SpriteRenderer idlesprit;//指针[SerializeField] GameObject pointer;Animator animator;private void Awake(){idlesprit = GetComponent<SpriteRenderer>();animator = GetComponent<Animator>();}private void OnEnable(){idlesprit.sprite = Trueimage;animator.Play("itemAnimation");}private void OnDisable(){StopAllCoroutines();}/// <summary>/// 指针进入开始执行协程/// </summary>public void StartCoroutine(){StartCoroutine(nameof(GetSpaceCoroutine));}/// <summary>/// 指针退出执行携程/// </summary>public void StopCoroutine(){//如果状态为未点击则更改图片的样式if (IsClick == false){idlesprit.sprite = Falseimage;}StopCoroutine(nameof(GetSpaceCoroutine));}/// <summary>/// 指针和点数重叠的逻辑处理/// </summary>/// <returns></returns>IEnumerator GetSpaceCoroutine(){while (true){if (Input.GetKeyDown(KeyCode.Space) && gameObject.activeSelf){IsClick = true;//播放伸缩动画pointer.GetComponent<bornpoint>().PlayerAnimation();gameObject.SetActive(false);}yield return null;}}
下一步就是表盘逻辑处理,怎样动态读取并更新表盘上的点呢?
- 将指针的Rotation-Z轴 值作为波数更新和开启下一波数的条件。
- 动态读取录制好的文件以波数为索引读取里面的数组。
- 遍历数组若为1显示为0则隐藏。
这里再解释一下,其中有一个GameObject数组包含面板上的36个点,还有一个数组是数据文件中的数组,只是包含36个0或1。其中这两个数组相同索引表示面板上哪一个点,所以结合数据数组可以便捷的显示或隐藏面板的点。
还有就是在指针Z轴旋转等于-10时预备更新下一波,要做的是清空表盘上的点(全部失活),在Z等于0的时候为下一波开始的时候,根据数据文件对应的波数更新表盘上的点。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ItemManager : MonoBehaviour
{public int WaveCount = 1;public int waveCountNext;[SerializeField] Transform pointer;//包含的指针集合[SerializeField] List<GameObject> items;//指针回放的ScriptObject文件[SerializeField] item_Data itemdata;private void Awake(){for (int i = 0; i < transform.childCount; i++){items.Add(transform.GetChild(i).gameObject);}//更新波数逻辑waveCountNext = WaveCount + 1;}void Start(){StartCoroutine(nameof(WaveCountCoroutine));StartCoroutine(nameof(StartBornCoroutine));for (int i = 0; i < itemdata.itemDatas[1].statetable.Length; i++){if (itemdata.itemDatas[1].statetable[i] == 1){if (!items[i].gameObject.activeSelf){items[i].gameObject.SetActive(true);}}}}private void OnDisable(){StopAllCoroutines();}/// <summary>/// 即将结束时要将表盘上的指针清除/// </summary>/// <returns></returns>IEnumerator WaveCountCoroutine(){while (true){if (TransformRotation.Instance.GetInspectorRotationValueMethod(pointer).z == -10){WaveCount = waveCountNext;//清场foreach (var item in items){if (item.activeSelf){item.SetActive(false);}}yield return new WaitForSeconds(0.7f);}yield return null;}}/// <summary>/// 加载本次表盘上相应位置出现的点数/// </summary>/// <returns></returns>IEnumerator StartBornCoroutine(){while (!(WaveCount >= itemdata.itemDatas.Count)){if (TransformRotation.Instance.GetInspectorRotationValueMethod(pointer).z == 0){for (int i = 0; i < itemdata.itemDatas[WaveCount].statetable.Length; i++){if (itemdata.itemDatas[WaveCount].statetable[i] == 1){items[i].gameObject.SetActive(true);}yield return new WaitForSeconds(0.1f);}waveCountNext++;}yield return new WaitUntil(() => WaveCount == waveCountNext);}}}
5:总结
这次想法主要是要参加一个游戏比赛(打打广告多为我们加加油!演示视频链接会在文末给出),尝试着将节奏类卡点游戏结合动作射击。核心功能虽然实现了但是表盘更新会随着游戏帧率产生误差,音乐播放并不会受到帧率的影响,解决方案就是将众多的协程返回一帧改为一帧真实时间(WaitForSecondsRealtime),不过有一些功能也会差生影响,虽然完成了但并不是那么完美!自己还得努力提升。
项目做的不太完善,希望大佬有什么意见或建议评论区留言或者私信,另外又看不懂的小伙伴也可以私信或者留言问我。感谢大家希望大家能够点赞喜欢。
6:项目源码及参考资料
项目源码:GitHub - JAM893736346/ClockMusic: 节奏类游戏功能启发
项目演示视频:游戏演示_演示 (bilibili.com)
项目参考:【Unity】ScriptableObject的介绍_妈妈说女孩子要自立自强的博客-CSDN博客_unityscriptobject
【Unity】 节奏类游戏的表盘卡点功能相关推荐
- Unity 剧情类游戏基础脚本
本文主要用于给新人提供实现剧情类游戏的基本思路. using System.Collections; using System.Collections.Generic; using UnityEngi ...
- 用Unity实现LOL游戏中聊天对话框的功能
基于Socket,用UGUI实现游戏场景中玩家的聊天 如图,聊天UI分为两部分:对话框.输入框. 实现的功能如下 1.默认情况下,对话框不可见,按下回车键显示对话框和输入框,鼠标光标在输入框 2.再按 ...
- 用Unity开发2D消除类游戏的素材资源精选
本文精选了一些用Unity制作2D消除类游戏的UI素材.音频资源和完整项目. 常见的消除类游戏种类有:三消.六边形三消.点点消.连连消.泡泡龙类型消除.连连看.1024类型消除等.也有各种各样和其他元 ...
- fps射击HTML网页游戏,关于Unity中FPS第一人称射击类游戏制作(专题十)
当前Unity最新版本5.6.3f1,我使用的是5.5.1f1 FPS第一人称射击类游戏实例 场景搭建 1.创建Unity项目工程和文件目录,保存场景 2.导入人物模型和子弹碎片的资源包charact ...
- unity跑酷怎么添加金币_【Unity3D实战】零基础一步一步教你制作跑酷类游戏(填坑完整版)...
在两个月前曾写了一篇<[Unity3D实战]零基础一步一步教你制作跑酷类游戏(1)>,里面一步一步演示了制作跑酷类游戏,然而由于时间原因,只写到了让角色往前移动为止.这个坑一直没有时间去填 ...
- 当年的三国java游戏_三国卡牌类手游塞班 分享问几年前玩的一个三国
分享问几年前玩的一个三国卡牌手机游戏,好像是塞班...分享问几年前玩的一个三国卡牌手机游戏,好像是塞班,或者安卓的,单机的...是塞班的,之前也玩过.里面有霹雳车,攻城云梯,什么的.骑兵,枪兵,盾兵, ...
- unity期末作业--迷宫寻宝类游戏(附下载链接)
unity期末作业–迷宫寻宝类游戏 上下左右移动,空格攻击可以获取各种装备宝物,钥匙,开门需要钥匙和体力,击败怪物需攻击值,击败怪物会掉落金币,拾取完所有物体后可以去下一层继续游戏 源文件和导出文件下 ...
- 【优化】Unity游戏加载卡顿原因之一:冗余组件的挂载问题
游戏加载卡顿这个问题严重吗? 一.背景 在游戏开发中游戏的卡顿不是某个单一的问题导致,是由诸多问题的量变在一起导致的:可能是资源层面的,也可能是逻辑层面的,也可能是网络层面的-;我这里要说的是Unit ...
- 设计一款博弈类游戏的人机对战算法、策略_卡牌游戏八合一,华人团队开源强化学习研究平台RLCard...
雷锋网 AI 科技评论按:在过去的两三年中,我们经常听说人工智能在棋牌类游戏(博弈)中取得新的成果,比如基于深度强化学习的 AlphaGo 击败了人类世界冠军,由 AlphaGo 进化而来的 Alph ...
最新文章
- Please set spring.main.web-application-type=reactive or remove spring-boot-starter-web dependency
- C# 10 新特性 —— CallerArgumentExpression
- Android studio之提示Failed to resolve: com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.46
- 破解数字游戏 —— 概率篇
- Adblock Plus无法屏蔽CSDN右下角广告解决
- 怎样给家庭组计算机授权,steam如何设置家庭共享?steam设置家庭共享方法
- 人工智能AI - 学习/实践
- zoom下载官网android最新,Zoom下载安卓最新版_手机app官方版免费安装下载_豌豆荚...
- 极光设置一级二级标题
- 如何清除redis缓存
- 读余文森《有效评课》
- Z-blogPHP蜘蛛访问日志统计插件+自动收集死链
- 二.android 12 修改文件夹背景透明度
- Android 禁止adb reboot recovery进入recovery模式
- 时空猎人无尽之塔初级玩法解析攻略
- 计算机组成 vhdl cpu 实验 西安交大,基于FPGA的VHDL计算机组成实验平台的设计与实现...
- 泉州dns服务器无响应,泉州联通dns服务器地址
- PyCharm Plugins
- mysql占用服务器cpu过高的原因以及解决办法
- 非常养眼的元代青花风格的pad ui 界面设计
热门文章
- ThingsBoard资产设备总数/离线数/在线数统计
- 计算机D盘无法读取,电脑d盘打不开怎么办_解决电脑d盘无法打开的方法
- Cobar分布式关系数据库访问代理
- c语言报名入口,考试报名入口
- linux分区方案6,linux (centos 6.4)安装自定义分区方案(转载)
- utf-8中一个汉字是3个字节,你知道吗?
- 七夕王者服务器维护什么时间结束,王者荣耀2020七夕活动什么时候结束?七夕情人节活动结束时间[多图]...
- 基于 Arduino 高精度简易桌面钟(Oled/DS3231)
- mansory使用与UIScrollView
- JWT如何解析过期的token中的信息