LOADING

加载过慢请开启缓存 浏览器默认开启

车万空寂花系列一:从可玩成品倒推 Unity 弹幕游戏架构

前言

“车万空寂花”最开始只是想做一个仿东方 Project 的弹幕游戏。项目往后做,角色与难度选择、完整关卡、Boss、分数记录、PC 和移动端控制都加了进来,原本单独运行的弹幕代码也要和关卡、输入、资源、UI 接起来。

最近正好在整理这个项目的资料,于是先把目前已经跑通的流程写下来。本文主要看整体结构,后面两篇再分别展开弹幕管理和关卡加载。

项目源码:qian488/Danmaku;试玩页面:itch.io

这个系列

  1. 本文:从可玩成品倒推整体架构
  2. 集中更新、对象池与碰撞判定
  3. 时间轴关卡、资源预热与加载流程

这个项目目前做到了什么

项目当前的完整游戏循环如下:

主菜单选择角色、章节和难度
        ↓
加载关卡资源并预热高频对象
        ↓
玩家移动、射击、擦弹、收集道具
        ↓
时间轴生成敌人、波次和 Boss
        ↓
胜利或失败结算,记录最高分

为了支撑这条循环,项目需要处理六项工作:推进关卡;生成、更新和回收弹幕;把主菜单选择传入战斗场景;准备首次使用的贴图、音效和预制体;统一键盘与触摸输入;集中管理玩法数值。下面的五条链路说明这些工作在项目中的归属。

我现在整理出的五条运行链路

1. 一局游戏的选择数据

主菜单选择角色、章节和难度后,将本局需要的数据写入 DanmakuRunSettings。战斗场景读取这些会话数据,再创建对应玩家、加载关卡并应用难度倍率。

DanmakuRunSettings 只保存本局选择。默认数值由配置提供,历史成绩由持久化模块保存。三类数据分开后,各自的创建和清理时机更清楚。

2. 键盘和触摸输入

PlayerController 只消费统一的移动、低速、射击和炸弹意图。键盘与触摸组件分别采集输入,再转换成相同语义。

键盘 / 触摸
     ↓
移动向量、低速状态、炸弹请求
     ↓
PlayerController
     ↓
移动、射击、受伤和 Bomb

这样做的好处是,修改虚拟摇杆不会影响受伤逻辑,增加手柄输入也不需要复制玩家控制器。

3. 关卡时间轴

StageRunner 推进关卡时间,读取 StageData,在时间到达时触发敌人波次、弹幕或 Boss。具体怎样发射,则交给 shooter 或 pattern。

时间轴提供任务触发时间,发射器计算弹幕参数,BulletManager 更新生成后的子弹。调整关卡节奏时主要修改任务时间与 pattern 参数。

4. 弹幕的生成与回收

敌弹进入 BulletManager 后,由统一的活跃列表更新位置、做碰撞和判断越界。对象从 GameObjectPool 取得,结束后回池,而不是频繁 Instantiate/Destroy

集中管理后,当前活跃弹幕、判定顺序和回收规则都可以从 BulletManager 追踪。排查未消失的子弹时,检查范围也随之收敛。

5. 进入战斗前的加载

主菜单与战斗场景分别维护预加载清单。DanmakuLauncher 和关内控制器异步加载资源、更新进度,并在开战前预热核心对象池和子弹贴图索引。

与其让第一次开火随机卡顿,不如在玩家已经接受等待的 Loading 阶段完成资源准备。

数值放在哪里

弹幕游戏的手感来自许多相互影响的数值:

  • 玩家高速与低速移动速度
  • 受击半径与擦弹半径
  • 子弹速度和难度倍率
  • 火力档位与升级经验
  • 敌人生命、Boss 倍率和掉落权重
  • 击坠、擦弹、时间奖励的分数组成

项目用 DanmakuGameplayBalance 一类的配置承载这些调节项。同一套平衡规则因此有明确入口,关卡逻辑也能读取配置,减少散落在脚本中的魔法数字。

判断一个字段是否应该进入配置,可以问三个问题:

  1. 它是否可能在不改玩法代码的情况下调整?
  2. 它是否需要在多个系统中保持一致?
  3. 它是否属于会被反复调整的设计规则?

三者满足其一,就值得考虑从代码常量中抽离。

框架和玩法是怎么接起来的

工程中还有 UI、音频、场景、资源、存档和对象池等通用模块。我采用以下边界:

  • 通用层提供加载、回收、页面切换和数据保存能力;
  • 弹幕层定义玩家、敌人、子弹、关卡和计分语义;
  • 入口负责把两者装配起来。

例如 BulletManager 可以使用通用对象池,但对象池不需要理解“擦弹”;UI 框架可以打开结算窗口,但不负责计算最终分数。

目前还没解决好的地方

项目已经能完成一局游戏,但不代表架构已经到终点:

  • 资源仍以 Resources 为主,依赖和卸载粒度有限;
  • GameObject 弹幕适合当前规模,再向数千级扩展会受 Transform 与 Renderer 成本限制;
  • BulletManager 集中管理后容易继续膨胀,需要警惕变成新的 God Object;
  • 关卡配置需要更直观的编辑器预览,单看时间数字不利于调节节奏;
  • 公开仓库还缺少稳定的性能基准,现阶段只能说明代码采用了对象池和集中更新,尚无可公开的性能数据。

下一步应先建立可重复的性能场景,再根据测量结果决定是否迁移 Addressables、数据导向弹幕或批量渲染。

写在最后

重新整理一遍以后,我觉得这个项目目前有四个比较清楚的点:

  1. 完整游玩闭环决定了系统的拆分范围。
  2. 时间轴管时机,发射器管模式,BulletManager 管高频对象生命周期。
  3. 平台输入、游戏规则和通用框架通过明确接口连接。
  4. 对象池和预加载将一部分运行时成本转移到可观察、可控制的流程中。

下一篇接着看 BulletManager。这部分代码更具体,包括活跃弹幕列表、对象池状态重置、受击和擦弹判定。