本节摘要:完整复盘"深井矿工"开发的下半场之前半程——新建项目与目录规划、搭建测试关卡、实现角色控制与跳跃手感、接入输入映射、配置跟随镜头。目标是达到"手感正确"这一里程碑,全程保留真实的开发顺序与中途返工。
复盘的价值在于真实,所以这一节按实际的工时顺序写,包括第一次做得不满意推倒重来的部分。目标是让读者照着走一遍后,得到一个能在测试关卡里跑跳自如的矿工角色——这是整个游戏的第一块压舱石。
新建项目的动作本身只有几分钟:指定项目文件夹、选渲染后端(2D 项目选兼容或移动端后端即可,第 6 章讲后端差异)。真正花时间且值得花时间的是目录规划。第一版我们随手把所有场景堆在根目录,半小时后就尝到苦头——找文件靠眼力,团队协作必打架。推倒重来,按下表立规矩:
scenes 主场景与关卡模板 scenes player enemies items ui scripts 全局脚本与工具类 assets art audio fonts
(示意结构,实际以团队约定为准。)原则只有一条:按"是什么"分,不按"什么时候做的"分。此后三个月的每次保存都不再需要思考放哪。
第一小时的另一件正事是输入映射。在项目设置里定义动作:左右移动、跳跃、挖掘、交互。键位先随便配,重点是动作名先于键位存在——代码从此只认动作不认键位,将来加手柄支持、改键、跨平台适配都是配置问题而不是代码问题(第 7 章展开输入系统)。
不急着画正式美术。用两三种纯色方块瓦片铺一个十米宽、三层楼深的小坑,加上两段平台——这就是测试关卡,它的使命是让手感参数的每次调整都能立刻被验证。
角色骨架按下表组装,每个选择都有理由:
| 子节点 | 职责 | 选择理由 |
|---|---|---|
| 角色根(运动学身体) | 移动与碰撞 | 5.2 谱系:手感归代码,碰撞归引擎 |
| 精灵 | 显示 | 纯色占位图即可,后期换 |
| 碰撞形状 | 物理轮廓 | 长方形起步,比角色视觉略窄(防挂墙角) |
| 灯光 | 头顶矿灯 | 5.1 光照:一口主光源定氛围 |
| 射线 | 脚下坡度采样 | 挖掘判定要用,先挂上 |
碰撞形状"比视觉略窄"是平台游戏的通用经验:视觉是角色,物理是一根略瘦的柱子,角色贴墙时像素重叠半格看不出来,物理挂角则极其硌手。
第一版移动代码就是 5.2 的那段模板——重力、跳跃、水平速度、移动与滑动四件套。跑起来"能用",但手感立刻暴露两个问题:起跳黏滞(按下跳跃到离地有可感延迟)与下落飘(上升下落同速,像气球不像人)。
第二版针对手感做三处手术,这就是 5.2 埋下的三招:
const JUMP_CUT := 0.5 # 松键截断系数 const FALL_GRAVITY_MULT := 1.6 # 下落重力倍率 const COYOTE_TIME := 0.12 # 离地宽限秒数 var _coyote_timer := 0.0 func _physics_process(delta: float) -> void: var g := GRAVITY if velocity.y < 0.0: if Input.is_action_just_released(&"jump"): velocity.y *= JUMP_CUT # 招式一:短按小跳 else: g *= FALL_GRAVITY_MULT # 招式二:下落更重 if not is_on_floor(): _coyote_timer -= delta velocity.y += g * delta else: _coyote_timer = COYOTE_TIME # 招式三:土狼时间充值 if Input.is_action_just_pressed(&"jump") and _coyote_timer > 0.0: velocity.y = JUMP_VELOCITY _coyote_timer = 0.0 # 起跳即清零,防止二连跳 velocity.x = Input.get_axis(&"move_left", &"move_right") * SPEED move_and_slide()
三招的原理分别说一句。短按截断:松开跳跃键时把上升速度乘一个小于一的系数,按得越短跳得越矮,玩家获得"控制感"。下落加重:下落相的重力加大,跳跃曲线变成"缓升骤降",这是主流平台跳的共同手感密码。土狼时间:离开平台边缘的一小段时间内仍允许起跳——玩家"明明按了却没跳"的挫败感大半来自离地瞬间,宽限窗口默默吞掉这类误判。
第三版只改了一处:把三个手感系数从常量改成导出变量,编辑器面板上边玩边调。十分钟的参数打磨后,矿工的跳跃从"正确"变成"舒服"——这个差别玩家说不出,但手知道。
镜头是手感的隐形一半。方案一"硬绑定":镜头做角色子节点,位置完全同步——最简单,跳起来镜头跟着颠,十秒头晕。方案二"每帧线性插值"(5.1 提过的写法)开始平顺,但急转弯时角色总贴着屏幕边。方案三"目标点偏移加平滑":镜头追的不是角色本体,而是角色前方的一个偏移点,移动方向决定偏移方向——玩家看得到"要去的地方"而不是"正在的地方"。
# 方案三:前瞻镜头 const LOOK_AHEAD := 96.0 # 前瞻像素距离 const SMOOTH := 4.0 # 平滑系数 var _look_dir := 0.0 func _process(delta: float) -> void: var axis := Input.get_axis(&"move_left", &"move_right") if absf(axis) > 0.2: _look_dir = axis # 记住最后朝向 var target := player.global_position + Vector2(_look_dir * LOOK_AHEAD, -48.0) global_position = global_position.lerp(target, SMOOTH * delta)
最终采用方案三。复盘的教训值得记下:镜头方案的选择要在"跑起来"之后做——静止读代码永远体会不到晕与不晕的差别。手感工程的一切决策,都以真实操控为准绳。

四小时的成绩单:目录立规、输入抽象、测试关卡、角色三版跳跃、前瞻镜头。遗留三件事给下节:瓦片挖掘还没接(采样射线空挂着)、没有任何反馈(挖到东西没声音没数字)、没有存档。这三件事恰好构成下一节的全部内容——它们也是"能玩的玩具"与"是款游戏"之间的分界线。
💡 复盘心法:每个小时结束跑一次游戏,是本次开发节奏里最好的决定。手感问题在"热"的时候修是一分钟的事,攒到后期变成"总觉得哪里不对"的玄学,就是一天的事。
角色活起来了,下一节给它一个完整的世界:挖掘、收集、计分、存档,把玩具变成游戏。