【持续更新中】Music99:项目开发日记

【持续更新中】Music99:项目开发日记

Music99(吃鸡音游demo)的开发过程记录。



回顾了下聊天记录,这个demo是8月26日和朋友吹水时决定开始做的。到现在两周的时间,虽说基本功能都做完了,但本身就不是很复杂的东西,开发效率仍待提高。

开发过程里踩了不少坑,新东西嘛一个都没提出来,可以说是抄都抄不利索。为了给自己警示,照样会写开发日记。

To-Do List

  1. 改进commit规则,不要全量提交。

  2. 支持本地二人对打。

  3. 加入攻击交互,实现-1攻击和-3攻击两种攻击技能。

  4. 加入结算画面。

  5. 已经不打算做了,但姑且放在这里,提醒自己有多垃圾,跑通大厅、房间和多人联网功能。


原型设计

打算另写一篇文章,写完把链接贴在这里


配置说明

https://uynad.github.io/2021/09/04/devlog/20210905-music99-designer/

不断更新中,下次一定。


开发日志


2021.09.04

今天要开发的是②将Note统一加入对应队列进行管理。看似简单,实际上确实很简单,写的过程中遇到一堆问题。


1. <队列名>.Peek()为什么会报错?

在Note物体上,用<队列名>.Peek()获取队列的第一位物体并和自己比对,如果是自己才可以按,不是就不能按。这是这个写法的核心思路。

但是写了个 if(gameObject == <队列名>.Peek())后就开始报错了,大意是对象是empty。音符被按掉后会出队列,在最后一个音符被按掉时一定会进入empty的状态。update()会卡死在这个报错行,不再向下执行。

为了规避这个报错,又在外面套了个if,判断Peek()的对象是否存在。没想到这样改完依然报错,只是报错行从刚刚那行变成了这一行(…)。

稍微做了下功课,发现queue支持包含的元素是null,应该就是这个特性导致了报错。把第一个if判断Peek()是否存在改为了<队列名>.count是否大于0,保证队列里还有元素。


2. 逻辑写好了,单次按键还是会消掉多个

每个音符上挂着一个NoteObject的脚本,在里面Update不断判断:我是不是队列的第一个?是,则继续判断是否进入了判定区,是否触发了按键,一旦触发后就会计算hit类型,然后触发对应类型的得分事件。

在实际运行时,单次按键却还是会消除掉所有在判定区里的音符。通过修改特定配置、打印DebugLog等方法,最后捋清了这一过程里发生的事。

问题出在所有音符物体都在Update()里调用Input.GetKeyDown().

队列第一个和第二个音符同时进入判定区,第一个音符是可以触发按键的状态,在Input.GetKeyDown()后执行得分事件,并将自己退队;在同一帧内,第二个音符成为了队列第一名,由于它也在Update里监听Input.GetKeyDown(),同样得到了TRUE结果,结果自己也执行了得分事件并退队。

简单来说,Input.GetKeyDown()会在一帧内返回同样的结果。对于多个监听这一事件的物体,可以用记得自己之前也用过的变量来控制。

在NoteHolder上public了四个布尔变量,分别是key1~4是否按下。队列的第一个音符进入判定区时,只有该变量为False时才能触发按键事件。而一旦按键,该变量就会变为TRUE。同时,在Update里单独写一个if判断,如果Input.GetKeyUp(),对应变量就会变成FALSE。

我承认这个写法相当不优雅,且沾点脑瘫,但它跑起来了。


3. 解决问题后,再往前走几步

1和2处理妥当后,需求①和②已经完成了。但人不能只满足于解决问题,于是做了点额外工作。

EffectObject:音符触发按键事件时的受击特效,单独封装成了一个prefab。这个prefab下有一个sprite图片表明是啥,还有一个粒子系统表明打击感。挂了一个EffectObject脚本,让它在float lifeTime后自行销毁,lifeTime公开出来可配置。在NoteObject的按键事件里加了一行,在当前的位置(即被按键的位置)生成对应的受击特效。

生成配置参数修改:按之前的写法,NoteGroup里配置的是每个Note的初始Position.y,这一数字非常反人类,无法做谱。怎么看都是用多少秒后到达判定线更合理吧!于是,改了。改起来其实很简单,NoteScroller的移动逻辑是每秒 -= beatTempo * Time.DeltaTime,其实beatTempo就是移动速度。修改了foreach里生成音符时传入的参数,从postion_y变成了time*beatTempo,其中time是策划配置,beatTempo是读取的noteScroller的数值。

完美,终于可以回家了。



2021.09.03

昨天看项目时,一度萌生了“不然直接用这个好了”的邪恶想法,但最终还是作罢。

本身这个项目也只是为了做玩法实验,如果因为“别人的程序写得比我好”就换,反而会拖慢实现想法的速度。好的东西是好,但学习不能一蹴而就,真是残念。

昨天的研究过程里,发现了能解决问题的方案,C#的队列

每一条轨道上的音符都是先进先出的关系,每次只对队列的第一位进行判定,判定成功后就将其踢出队伍。后来的音符加入队尾,等前面都被判定完再轮到自己。这不就是完美的音游次序解决方案吗?

队列要挂在一直存在且和音符有关系的物体上,最后就挂在了NoteHolder上,为此单独写了个NoteManager脚本。为了将场上所有音符都自动加入队列,鄙人做出了个艰难的决定:

不再在场上直接摆音符,而是通过NoteManager脚本自动化生成。

这样NoteManager就要承载两个功能:①根据事先的配置自动生成Note;②将Note统一加入对应队列进行管理。

先进行了①的开发。

unity的instantiate函数相当好用。对于四个轨道的音符,只需要修改position.x进行对应;传参position.y进去生成对应距离(时间)的音符;.parent来指定母物体。因为不想用绝对路径去指定音符,就在前面public了四个GameObject,然后分别把四种音符物体都拖进来。通过这一方法生成的音符,只是原版的clone。

最后,再在最外面套一个音符1队列名.Enqueue(生成音符1) ,生成的音符物体就会自动进入该队列了。

public virtual void Enqueue( object obj );
向 Queue 的末尾添加一个对象。


2021.09.02

秉着能抄就抄不能换个的开发原则,当然是要去找找更成熟的项目了。在Github上搜索,最后选中了这个unity项目作为参考对象。

不下不知道,一下吓一跳。看完之后,鄙人陷入了自卑。

在开发项目前,就和朋友讨论过音乐游戏的谱面编辑器,以及如果要做有key音的高端音游,编辑器要怎么设计才方便。但当时的想法只停留在设计层面,并没考虑过具体执行。毕竟鄙人不是真正的程序员,而且实在太菜了毫无头绪。

这个小项目麻雀虽小五脏齐全,不仅完美实现了鄙人的基本诉求,而且附带一个完整的谱面编辑器,且游戏内自带了几张制作完整的谱面,玩起来完全没问题。音符甚至轨道全都是游戏开始后初始化生成的,不用狗策划自己手摆。

除去功能问题,整体框架也显得比我现在的更好些,拓展性更强,写死的地方更少。

断断续续看了一天。



2021.09.01

教学视频里的工程存在一个非常大的问题:每个Note单独挂一个脚本,自己判定自己的,如果多个Note一起进了判定区,按一下会同时消除掉多个。

先复盘下当前工程的结构——

GameManager上挂了游戏规则相关的几乎所有参数,并写了三种类型(Perfect、Good和Miss)的得分计算事件。GM被静态公开,以便被其他脚本引用。

Note上挂了NoteObject脚本,这一脚本会检测是否进入判定区,以及按下时判定为哪种类型的hit。判定结束后执行引用自GM的对应得分事件。

NoteHolder是所有Note的母物体,上面有BeatScroller脚本,会根据配置的bpm进行移动。也就是说,Note本身没有在移动,只是它们共同的亲妈母物体在移动而已。

Button(判定线)上挂的脚本没有程序功能,只是在按下和放开时切换sprite,来做出动态效果。

Canvas上挂了一切需要给玩家看的UI,本身没有程序功能,被GameManager引用并不断更新文本。


可以看出,Note没有一个程序功能上的管理器,控制所有Note的状态,每个Note单独进行自己的判定,因此才出现了按一次消多个的情况。

当时思考到这一点,突然整个人就漏气儿了(迫真)。问题定在了结构上,但结构本来就是最难改的地方。而且话说回来,到底要改成什么样的结构,才能满足这个需求呢?



2021.08.31

第一周马上实现了和视频里效果一致的原型,很快啊。但就是从这一周开始问题频出。

一是这个教学视频本身结构就偏简单,后面拓展起来容易出问题;二是鄙人是有一套完整的玩法构思的,在人家的基础上改造也可能出事儿。

要改造的第一个点就是——判定算分机制。

原视频里,Perfect、Good和Miss三个区间是直接写死在函数里的,不利于拓展;而且我希望实现的效果是,Perfect和Miss之间,会根据精准度来计算出一个百分比,再根据百分比计算得分。

最后的算法是,用Good距离减去Miss距离并除以9,结果再被按下时的Note距离所整除,使用Math.f函数进行取整,最后除以10,就得到了percent数值。

对于Good的分数算法,会将上面的percent传入,例如good总得分是200,当精准度percent为0.8(80%)时,得分就只有200*percent=160。

为了突出perfect和good的区别,perfect单独进行了赋值,日后可以通过配置perfect基础值为300,而good为200,进一步强调90%与100%的质变差异。



2021.08.26 - 2021.08.30

和朋友吹水,聊到这个吃鸡音游的想法,决定抓住机会马上完善下,不要让吹水只停留在吹水阶段。

毕竟怎么想都是2D游戏,打算使用Unity开发。但近一年玩的都是UE4,对Unity已经开始记忆模糊,实际上当初用也没用得多熟练。秉着能抄就抄不能换个的开发理念,火速在YouTube上找了个符合需求的开发教学 ,然后开始全量照抄。

国外是没有Music Game这一细分概念的,需要搜索Rhythm Game。

做着做着,Unity的记忆也开始复苏了,真好。


【持续更新中】Music99:项目开发日记

https://uynad.github.io/2021/09/09/devlog/20210910-music99-program/

作者

UyNad

发布于

2021-09-10

更新于

2026-03-12

许可协议

CC BY-NC-SA 4.0

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×