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 需要的基础内容:

  • 创建 GroundPlayer 物理 Layer。

  • 创建玩家与地块占位 Sprite。

  • 写入 PlayerStatsConfig.asset

  • 生成 Player.prefab

  • 生成 _Init.unityM1_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
跳跃 SpaceW、上方向键 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
2
3
4
5
6
7
Input System
-> PlayerInputHandler
-> PlayerController
-> Rigidbody2D / Collider2D
-> PlayerMotionState
-> CameraFollow
-> M1SetupTests / M1RuntimeProbe

这条链路让 PROJECT-V 从“文档和设定”进入“可运行工程”。它仍然很小,但已经有了后续扩展需要的关键接口:参数资产、状态机、事件、Prefab、场景生成脚本、自动化测试和短时运行探针。

下一步如果进入 M2,应该沿着这条链路增加生命值、受击、伤害入口和最小敌人,而不是另起一套控制框架。