PROJECT-v M1:角色能动闭环实现拆解
M1 不是一个“项目进度汇报”节点,而是 PROJECT-V 的第一条可运行实现链路:玩家输入进入 Unity,新 Input System 被消费,Rigidbody2D 接收移动与跳跃,状态机更新视觉状态,相机跟随,测试场景验证角色可以从起点平台移动到目标平台。
本文所有文件路径均以 PROJECT-V 项目根目录为基准。
关联提交和依据
这篇 M1 记录对应两类提交:
| commit | 用途 |
|---|---|
576d81e |
feat(m1): implement player movement milestone,M1 功能主体实现 |
94a125c |
docs: add phase completion blog hook,阶段完成后的博客钩子说明 |
主要参考产物是:
-
reports/M1_delivery_report.html -
qa/reports/M1_test_report.md -
qa/reports/T-DEV01_compile_verified.md -
qa/test_cases/M1_smoke_checklist.md -
qa/test_cases/M1_player_movement.md -
qa/test_cases/M1_visual_validation.md -
qa/test_cases/M1_performance_baseline.md
M1 的实现边界
M1 只做 character agency,也就是“角色能动性”。这个边界很窄,但它是后续战斗、敌人、关卡、Build 和表现层的共同底座。
M1 纳入实现的内容:
-
Unity 工程初始化与 Core 模板接入。
-
玩家输入、移动、跳跃、短跳、土狼时间和跳跃缓冲。
-
Player Prefab 及其物理组件配置。
-
横向 2D 相机跟随。
-
M1 测试场景和自动化验证。
M1 明确不做的内容:
-
战斗、受击、死亡、敌人、Boss。
-
程序化房间、关卡选择、Roguelite Build。
-
Steam 集成、正式 UI、正式角色动画和正式美术资产。
这个边界直接决定了实现方式:M1 没有急着堆玩法系统,而是先把“输入到物理反馈”的主链路做成可复现、可测试、可继续扩展的工程结构。
工程生成入口
M1 的工程落地入口不是手动在 Unity 里拖一堆对象,而是 unity-project/Assets/_Project/Editor/M1ProjectBuilder.cs。
这个 Editor 脚本负责一次性生成和校验 M1 需要的基础内容:
-
创建
Ground、Player物理 Layer。 -
创建玩家与地块占位 Sprite。
-
写入
PlayerStatsConfig.asset。 -
生成
Player.prefab。 -
生成
_Init.unity和M1_PlayerMovement.unity两个测试场景。 -
把 M1 场景加入 Build Settings。
-
验证 Prefab、场景、配置资产和核心组件是否存在。
这一步的关键价值不是“省事”,而是避免 M1 依赖某一次本地 Unity 场景手工操作。Player Prefab、测试场景和基础配置可以由脚本稳定重建,后续如果场景被误删或配置漂移,也能通过同一个入口恢复。
M1 生成和验证涉及的关键产物路径如下:
| 类别 | 路径 |
|---|---|
| Unity 工程 | unity-project/ |
| Core 脚本 | unity-project/Assets/_Project/Scripts/Core/ |
| Player 脚本 | unity-project/Assets/_Project/Player/Scripts/ |
| 输入资产 | unity-project/Assets/_Project/Player/InputActions/DefaultInputActions.inputactions |
| Stats 资产 | unity-project/Assets/_Project/Player/SO/PlayerStatsConfig.asset |
| Player Prefab | unity-project/Assets/_Project/Player/Prefabs/Player.prefab |
| M1 测试场景 | unity-project/Assets/Scenes/TestScenes/M1_PlayerMovement.unity |
| Windows Development Build | unity-project/Builds/Windows/PROJECT-V.exe |
Player Prefab 的组成
玩家对象落在 unity-project/Assets/_Project/Player/Prefabs/Player.prefab,它把 M1 需要的几类职责拆成明确组件:
| 组件 | 职责 |
|---|---|
SpriteRenderer |
显示当前运动状态的占位 Sprite |
Rigidbody2D |
承接水平速度、跳跃速度和下落速度 |
BoxCollider2D |
提供玩家碰撞体和地面检测尺寸来源 |
PlayerInputHandler |
读取移动与跳跃输入 |
PlayerStatsComponent |
暴露移动参数配置 |
PlayerController |
组织输入、物理、状态、事件和视觉切换 |
Prefab 的物理设置偏保守:Rigidbody2D 使用动态刚体,冻结旋转,并开启更稳定的碰撞与插值设置。M1 只做横板基础移动,所以不让角色因为物理碰撞产生旋转,是一个必要的约束。
输入层:缓存意图,不直接改物理
输入实现位于 unity-project/Assets/_Project/Player/Scripts/PlayerInputHandler.cs。
M1 没有把输入读取塞进 FixedUpdate,而是在输入层缓存“玩家意图”:
-
Move读取水平轴。 -
Jump记录按下与松开。 -
ConsumeJumpPressed()和ConsumeJumpReleased()用一次性消费的方式交给控制器。
这样做的好处是,跳跃这种瞬时输入不会因为物理帧和渲染帧不同步而丢失。玩家在两次 FixedUpdate 之间按下跳跃键,Update 仍能记录下来,随后由控制器在物理帧里处理。
M1 支持的输入来源:
| 行为 | 键鼠 | 手柄 |
|---|---|---|
| 水平移动 | A/D、方向键 |
左摇杆、D-pad |
| 跳跃 | Space、W、上方向键 |
south button |
工程里同时保留 unity-project/Assets/_Project/Player/InputActions/DefaultInputActions.inputactions,自动化测试会检查 Player/UI map 和关键绑定是否存在。这给后续把输入资产完全纳入编辑器流程留下空间。
控制器:Update 处理状态,FixedUpdate 处理物理
核心控制逻辑位于 unity-project/Assets/_Project/Player/Scripts/PlayerController.cs。
M1 的控制器分两条节奏运行:
-
Update:消费跳跃按下/松开,记录跳跃缓冲,处理短跳请求,更新运动状态。 -
FixedUpdate:刷新接地状态,更新土狼时间和跳跃缓冲计时,执行跳跃,应用水平速度,应用短跳,限制最大下落速度。
这个拆分比较重要。输入和状态是“玩家感知层”,应该跟随渲染帧及时响应;速度、重力、接地检测是“物理层”,应该放在固定时间步里更新,避免不同机器帧率下行为漂移。
移动参数来自 ScriptableObject
玩家移动数值由 unity-project/Assets/_Project/Player/Scripts/PlayerStatsConfigSO.cs 定义,并由 unity-project/Assets/_Project/Player/SO/PlayerStatsConfig.asset 承载。
M1 当前核心参数如下:
| 参数 | 当前值 | 实现含义 |
|---|---|---|
moveSpeed |
5 |
地面水平移动速度 |
airControlMultiplier |
0.8 |
空中横向控制倍率 |
jumpForce |
12 |
起跳时的纵向速度 |
jumpCutMultiplier |
0.5 |
松开跳跃后的短跳削减倍率 |
maxJumps |
1 |
M1 禁止二段跳 |
coyoteTime |
0.1s |
离地后仍允许跳跃的宽限窗口 |
jumpBufferTime |
0.1s |
落地前提前按跳的缓存窗口 |
gravity |
-25 |
M1 控制器施加的重力 |
maxFallSpeed |
20 |
最大下落速度限制 |
这些值没有写死在控制器里,是为了让后续手感调参不需要改代码。M1 的代码负责解释参数,参数本身留给配置资产。
跳跃手感的四个细节
M1 的跳跃不是简单的“按键就给一个 Y 速度”,而是补了横板动作游戏最基本的容错。
第一,接地检测使用基于 Collider 尺寸的 BoxCast,检测目标是 Ground Layer。这样平台宽度、角色碰撞体变化时,检测区域仍能跟着 Collider 走。
第二,土狼时间通过离地后的计时器实现。角色刚离开平台边缘时,coyoteTime 还没有归零,玩家仍能跳。这解决的是“玩家肉眼觉得还在边缘,但物理上已经离地”的手感落差。
第三,跳跃缓冲通过 jumpBufferTime 实现。玩家落地前提前按跳,控制器会短暂保留这个输入;只要缓存窗口内重新接地,跳跃就会被消费。
第四,短跳通过松开跳跃键后的速度削减实现。当玩家提前松开跳跃,PlayerController 会把上升速度乘以 jumpCutMultiplier,从而让长按和轻点产生不同高度。
在测试中,理论跳高按 jumpForce^2 / (2 * abs(gravity)) 估算:12^2 / (2 * 25) = 2.88m。这个结果落在 M1 预期的 2.8m 到 3.0m 区间内。
状态机目前只做视觉切换
M1 已接入 Core 状态机模板,但没有提前把移动逻辑拆散到多个复杂状态里。现在的状态集合很小:
-
Idle -
Move -
Jump
对应文件包括:
-
unity-project/Assets/_Project/Player/Scripts/PlayerMotionState.cs -
unity-project/Assets/_Project/Player/Scripts/PlayerStateMachine.cs -
unity-project/Assets/_Project/Player/Scripts/States/PlayerIdleState.cs -
unity-project/Assets/_Project/Player/Scripts/States/PlayerMoveState.cs -
unity-project/Assets/_Project/Player/Scripts/States/PlayerJumpState.cs
这些状态在 M1 阶段主要调用 SetVisualState(),切换占位 Sprite、颜色和朝向。真正的移动和跳跃仍集中在 PlayerController,这是有意保守的实现:M1 的状态机先验证接入方式,不急着把物理行为拆到状态类里,避免在玩法尚未展开时制造过度抽象。
事件只暴露起跳与落地
M1 里已经把起跳和落地作为事件抛出:
-
OnPlayerJump -
OnPlayerLand
这两个事件不是为了 M1 立刻做完整音效和特效,而是给后续反馈层留接口。到 M2/M3 引入音频、粒子、受击或战斗反馈时,不需要再回头侵入跳跃主逻辑,只要订阅事件即可。
相机跟随保持简单
相机逻辑位于 unity-project/Assets/_Project/Player/Scripts/CameraFollow.cs。
M1 没有引入 Cinemachine 虚拟相机配置,而是用一个轻量脚本做目标跟随:
-
在
LateUpdate中读取玩家位置。 -
添加固定 offset。
-
用
Vector3.SmoothDamp平滑移动。
这个实现足够验证横板移动和跳跃场景。更复杂的镜头边界、房间锁定、Boss 战震屏和镜头权重,都应该留到关卡和战斗系统出现之后再做。
M1 测试场景怎么验证移动
测试场景是 unity-project/Assets/Scenes/TestScenes/M1_PlayerMovement.unity。
场景里不是随便摆几个方块,而是按移动验证拆了平台:
| 平台 | 作用 |
|---|---|
A_Start_Wasteland |
起点平台,验证站立、起步和基础移动 |
B_Cyberpunk_Rise |
上升平台,验证跳跃高度和空中控制 |
C_Biohazard_Rise |
第二段上升平台,验证连续平台移动 |
D_Landing_Return |
落点平台,验证落地状态回切 |
Safety_Floor |
兜底地面,避免角色永久掉出场景 |
unity-project/Assets/_Project/Tests/EditMode/M1SetupTests.cs 会检查配置默认值、Prefab 组件、输入资产、场景存在性和平台间距。这里的测试重点不是“玩家主观手感已经完美”,而是证明 M1 的工程结构没有断:Prefab 没丢组件、场景能被找到、关键平台在可达范围内、核心参数没有被误改。
运行探针用于短时工程验证
M1 还提供了 unity-project/Assets/_Project/Player/Scripts/M1RuntimeProbe.cs。它会在运行时按目标帧率采样,并把结果写入日志文件。
完整的本地验证结果如下:
| 验证项 | 证据 | 结果 |
|---|---|---|
| Unity 资源生成 | logs/unity_setup_m1_final.log |
Pass |
| M1 setup verification | logs/unity_verify_m1_final.log |
Pass |
| EditMode tests | logs/editmode-results-final.xml |
5 passed / 0 failed |
| Windows Development Build | logs/unity_build_windows_final.log |
Success |
| Runtime probe | logs/m1-runtime-probe-final.json |
平均 59.943 FPS |
短时探针结果显示:
| 指标 | 数值 |
|---|---|
| 测量帧数 | 480 |
| 平均 FPS | 59.943 |
| 最低 FPS | 49.590 |
| 最高 FPS | 62.887 |
| 目标 FPS | 60 |
这只能说明 M1 简单场景的短时运行没有明显性能问题,不能替代 30 分钟 soak,也不能代表后续战斗场景性能。qa/reports/M1_test_report.md 已明确记录:30 分钟 1080p 长时性能 soak 没有执行,它仍然是后续 release-grade QA gate。
M1 之后真正留下了什么
M1 最重要的产物不是一段能移动的角色代码,而是一条清楚的实现主线:
1 | Input System |
这条链路让 PROJECT-V 从“文档和设定”进入“可运行工程”。它仍然很小,但已经有了后续扩展需要的关键接口:参数资产、状态机、事件、Prefab、场景生成脚本、自动化测试和短时运行探针。
下一步如果进入 M2,应该沿着这条链路增加生命值、受击、伤害入口和最小敌人,而不是另起一套控制框架。