3D 移动端横屏割草 Demo。主角操控摇杆移动、自动攻击,击杀野猪群掉落肉块并堆在背后,死亡后进入结算与本地最高伤害榜。
主菜单 → Loading(Additive 加载战斗场景)→ 战斗 → 死亡结算 → 回主菜单,可连续再开一局。
交付物不只是玩法本身,还包含一条完整的工程化链路:配置表 Excel → C# 的导出工具链、Addressables 资源动态加载、可复用的 _Core 基础设施、覆盖全流程的自动化 Play Mode 验收、以及一键出包。
怪物 AI 未使用 NavMesh / NavMeshAgent,寻路、转向与避让自研。
未做的部分见文末「已知限制」。
- Unity 6000.3.21f1(URP)
- 打开
UnityClient/,入口场景Assets/Scenes/MainScene.unity,直接 Play - 配置表如需重新导出:运行
Design/gen.bat ci,产物写入Assets/Scripts/Gen/ - 已打包体:
Build/
| 渲染 | URP(移动端配置) |
| 资源 | Addressables 2.9.1 |
| 配置表 | Luban,Excel → C# + 二进制 |
| 相机 | Cinemachine 3.1.7 |
| 输入 | Input System 1.20.0 + 自研动态摇杆 |
| 异步 | UniTask |
| 其他 | Odin Inspector、DOTween |
Design/ Luban 配置表工程(Excel + luban.conf + gen.bat)
Luban/ Luban 工具本体
UnityClient/ Unity 工程
Build/ 打包产物
Unity 工程内:
Assets/
├ Scripts/ 全部代码,分层见下
├ Scenes/ MainScene(常驻入口)、BattleScene(Additive 加载)
├ Prefabs/ Actor(主角与两种野猪)、Loot(肉块)、UI(四面板 + 跳字 + 血条)
├ Art/ Player Monster Loot Environment UI Materials Animators
├ GameRes/Config/ Luban 导出的二进制配置,运行时经 Addressables 加载
├ Plugins/ Odin、DOTween、UniTask
├ Settings/ URP 管线资产(移动端 / PC 两套)
└ AddressableAssetsData/
Assets/Scripts/ 分三层:
_Core/ 框架层:Config Event Resource Pool Save UI Camera Scene Singleton
+ GameManager(组装根)
UI/ 业务层·表现:四个面板、面板注册表、跳字与血条、虚拟摇杆
Gameplay/ 业务层·玩法
├ Player/ 主角三层 MVC
├ Monster/ 怪物三层 MVC + MonsterManager
├ Combat/ 主角与怪物共用的战斗契约
├ Loot/ 掉落、拾取、背后堆叠
└ World/ 战斗边界
Gen/ Luban 生成产物,不手改
Editor/ 编辑器工具:自动化验收驱动器、打包工具、地图工具
分层规则只有一条:_Core 不认识业务类型,业务依赖框架是单向的。 唯一例外是 GameManager——它是组装根,必须认识被组装的东西;代价是把 _Core 复用到下个项目时需要排除它。
三层 MVC(PlayerData / PlayerView / PlayerController)+ 自研动态摇杆,摇杆由 GameManager 在生成主角时注入,不在场景里裸挂。
攻击范围与拾取半径都在配置表里,跟角色走。
三层 MVC(MonsterData / MonsterView / MonsterController),场上所有怪由 MonsterManager 统一持有与驱动。
状态机 待机 → 追击 → 攻击 → 死亡 → 重生:主角进入感知半径转追击,进入攻击距离转攻击,死亡后延迟一段时间由 MonsterManager 重生补位,保持场上数量恒定。动画由状态与移动速度参数驱动,不在状态切换处手动播片段。
两种野猪(普通 / 精英)共用同一套逻辑,血量、攻击力、移速、感知半径、攻击距离、掉落数量全部来自配置表;精英是普通野猪的预制体变体。
每帧只有 MonsterManager 一个 Update,逐只调 Tick。几十个同构对象各挂一个 Update 多付的是几十次托管/native 边界调用,而它们要做的事完全一样。死亡走「标记 + 帧末统一清理」,不在遍历中途改名单。
需求约束:「怪物之间要有略微的碰撞效果 防止怪物重叠 不可以使用Navmesh」。NavMeshAgent 的 ObstacleAvoidance 不可用。
算法是最朴素的加权分离,即「鸟群算法」:近处的同类互相排斥,在 Unity 中即是通过公式计算周围同类斥力形成的飘移向量,再与追击方向向量进行计算形成真实位移方向。一个细节:away 必须先归一化再乘权重——向量长度本身携带距离信息,直接相乘等于把距离算两遍,近处推力平方级增长。
没用 collider 推挤。 那是「先移动、再被物理推一次」,位置在两处被改,正是它抖动与跑偏的根因。
没做 RVO,也没做 AOI。 实测邻居查询的交叉点在 n=100~200,场上 15 只;此时用 AOI 反而影响性能。
IDamageable / CombatQuery / AnimationEventRelay / DamageDealtEvent 四件由主角与怪物共用。范围判定全项目只有 CombatQuery 一个出口,攻击与拾取都从这里出,判据不会分叉。
伤害结算后的分发走事件总线,即 EventManager 事件中心:主角与怪物各自结算完命中后发出同一个 DamageDealtEvent(携带受击者、伤害值、双方阵营),订阅者——CombatUIManager 管理跳字对象池并响应事件冒伤害跳字,GameManager 用它累计本局玩家总伤害供结算排行榜。
跳字与血条各自走对象池,战斗结束统一回收。
参考视频里的表现,需求文字未要求。
野猪死亡后爆出肉块,沿抛物线四散落地;主角走近后肉块飞向背后,堆成一根跟随移动的柱子。掉落数量走配置表,拾取半径跟角色走。
肉块全程走对象池,一局结束统一回收。每帧只有 LootManager 一个 Update,与怪物同一条口径。
拾取判定用 Physics.OverlapSphereNonAlloc,与攻击判定同一个 API,开销与场上肉块数量无关。肉块的 Collider 只在落地静止期间启用:飞行中的 Collider 会持续搅动物理场景的空间索引,而堆在主角背上的肉块离拾取球心几乎零距离,不禁用会被反复拾取。
柱子的高度维护是 O(1):锚点的世界旋转恒为 identity,每块肉入栈时写一次 localPosition 即固定,之后整根柱子由锚点一个 Transform 带着走,不逐块刷新。
背包总数无上限,背后显示的块数有上限(可配),避免 GameObject 数量随一局时长线性增长。
Luban 工具链:Design/ 下 Excel 定义,gen.bat 一键导出 C# 类型与二进制数据。角色与怪物拆成两张表,字段差异明显。运行时经 ConfigManager 全局只读访问,配置缺失直接抛异常——那是接线错误,不该静默降级成默认值。
数值全部数据驱动:血量、攻击力、移速、感知半径、攻击距离、掉落数量、拾取半径,改表即改手感,不动代码。
MainScene 常驻,战斗场景 Additive 加载并上报进度,一局结束卸载,可连续再开。GameManager 是组装根,按固定顺序初始化全部 Manager 并驱动整条流程。
资源经 ResourceManager 统一走 Addressables,是项目里唯一的资源出口;预制体、配置、UI 面板都从这里按地址加载与释放。
按可复用到下个项目的标准写,不掺业务:
EventManager |
强类型同步事件总线,readonly struct + Action<T>,无装箱 |
GameObjectPool |
单 Prefab 对象池 |
MonoSingleton / Singleton |
唯一实例与跨场景保留 |
ResourceManager |
Addressables 唯一出口 |
SaveManager |
JSON 本地存档与最高伤害榜,写盘走原子替换 |
UIManager |
UI 分层框架、面板生命周期 |
GameSceneManager |
场景 Additive 加载与进度 |
CameraManager |
相机唯一访问点 |
- 自动化 Play Mode 验收:一套 Editor 驱动器,从真实入口推进主菜单 → 战斗 → 结算 → 回菜单全流程,采集玩家、怪物、掉落与场景状态,并统计进入 Play 后与停播后的运行时错误。调试开关在 Player 构建下编译为常量
false。 - 一键打包:
BuildTool出 Android / Windows,打包前自动同步配置表。 - 地图工具:地形尺寸参数化,一次调用同步空气墙、地形、栅栏与刷怪半径。
- 热更新未做。 Addressables 已经接好,但只做本地加载层,没有远端 Catalog——没有可用的 CDN,做出来也无法验证真实下发。资源加载走的是同一套地址与释放接口,接远端只是换 Profile 与打包方式。
- CI 未做。 需要真实的构建机与分发通道才有意义,本地一键打包(
BuildTool)已覆盖出包这一步。