教程一 · 第 4 章:2D 平台跳跃——重力、跳跃、动画、摄像机
Pong 是”完全自己控制位移”,平台跳跃引入真正的物理:重力、地面检测、跳跃缓冲、土狼时间。做完你就具备了独立做 2D 动作游戏原型的能力。
4.1 输入动作
项目设置 → 输入映射:
| 动作 | 按键 |
|---|---|
move_left / move_right | A / D(顺带加上 ←/→) |
jump | 空格(顺手加 W、↑) |
4.2 玩家场景 player.tscn
Player(CharacterBody2D)├── Sprite2D(先用 Polygon2D 占位:32×48 蓝方块,或用素材图)├── CollisionShape2D(CapsuleShape2D,半径 12、高度 44;胶囊比盒子卡墙少)└── AnimationPlayer(4.5 节做动画)为什么用胶囊碰撞:方角在瓷砖缝上会”卡边”,胶囊底部圆弧能顺滑滑过接缝。
4.3 移动脚本(含所有平台跳跃标准手感技巧)
scripts/Player.cs:
using Godot;
public partial class Player : CharacterBody2D{ [Export] public float MoveSpeed = 280f; [Export] public float JumpVelocity = -520f; [Export] public float Gravity = 1600f;
// 手感参数(业界标准的两个"时间宽容"设计) [Export] public float CoyoteTime = 0.1f; // 土狼时间:离开平台后仍可跳的窗口 [Export] public float JumpBufferTime = 0.12f; // 跳跃缓冲:落地前提前按也生效
private float _coyoteTimer; private float _jumpBufferTimer;
public override void _PhysicsProcess(double delta) { float dt = (float)delta;
// ① 重力:没站在地面上就持续加重力 if (!IsOnFloor()) Velocity = Velocity with { Y = Velocity.Y + Gravity * dt };
// ② 水平移动(直接设速度 = 立刻加速立刻停,手感偏"街机";想要惯性见 4.4) float dir = Input.GetAxis("move_left", "move_right"); Velocity = Velocity with { X = dir * MoveSpeed };
// ③ 计时器 if (IsOnFloor()) _coyoteTimer = CoyoteTime; else _coyoteTimer -= dt;
if (Input.IsActionJustPressed("jump")) _jumpBufferTimer = JumpBufferTime; else _jumpBufferTimer -= dt;
// ④ 跳跃:缓冲期内 + 土狼期内才起跳 if (_jumpBufferTimer > 0f && _coyoteTimer > 0f) { Velocity = Velocity with { Y = JumpVelocity }; _jumpBufferTimer = 0f; _coyoteTimer = 0f; }
// ⑤ 可变跳高:松开跳跃键,上升速度立刻截断 → 短按小跳、长按大跳 if (Input.IsActionJustReleased("jump") && Velocity.Y < 0f) Velocity = Velocity with { Y = Velocity.Y * 0.5f };
MoveAndSlide(); }}逐个讲透:
IsOnFloor():CharacterBody2D 在 MoveAndSlide() 后自动记录碰撞信息——脚下有碰撞体时返回 true。它依赖”谁是地板”的判定,默认要求碰撞法线朝上约 45° 以内,所以斜坡、倒立天花板不会被误判。
重力不是引擎白送的:CharacterBody2D 叫”角色身体”,意思是位移完全由你的代码决定,引擎只负责碰撞响应。想要重力,自己加。这和 RigidBody2D(刚体,引擎施力)是两种哲学。平台跳跃、银河城、格斗全部用 CharacterBody2D,因为手感要 100% 可控。
Velocity with { X = ... }:C# 的 with 表达式,复制一个 Vector2 结构体只改指定字段,等价于 new Vector2(dir * MoveSpeed, Velocity.Y)。比手写短且不容易改错轴。
土狼时间(Coyote Time):名字来自卡通片里土狼跑出悬崖还能在空中跑两步。玩家走下平台边缘的 0.1 秒内按跳仍然生效——消灭”我明明按了跳”的挫败感。
跳跃缓冲(Jump Buffer):落地前 0.12 秒按跳,落地瞬间自动起跳——消灭”我明明在落地瞬间按了”的挫败感。
这两个是所有优秀平台游戏(蔚蓝、超级肉肉哥、马力欧)的标配,参数窗口一般 0.08–0.15 秒。
可变跳高:现实中跳跃高度不该是固定值。松开键截断上升速度是最常用的实现;也可以用”按住时重力减半”。
4.4 可选:加速度手感
把水平移动从”瞬时”改成”加减速”,手感更顺滑:
[Export] public float Acceleration = 2400f;[Export] public float Friction = 2000f;
float targetX = dir * MoveSpeed;float rate = Mathf.Abs(targetX) > 0.01f ? Acceleration : Friction;Velocity = Velocity with{ X = Mathf.MoveToward(Velocity.X, targetX, rate * dt)};Mathf.MoveToward(当前值, 目标值, 每步最大变化量) 是游戏手感万能函数,旋转、镜头跟随都用它。
4.5 动画:AnimationPlayer 入门
先占位也行,但 Godot 动画系统值得现在学。两种情况:
A. 暂时没有美术素材(用代码翻方块)
加两个 Polygon2D 子节点也行,更推荐直接熟悉 AnimationPlayer 流程:给 Player 加 AnimationPlayer 子节点。底部动画面板 → 新建动画 idle:
- 点”时间轴钥匙”录制,或手动加轨道:选中 Sprite2D/Polygon2D 的 Modulate 属性打关键帧,做个颜色呼吸
- 新建
run:给 Position 加两帧上下小幅震动(0.06 秒循环),模拟跑动 - 动画属性开 Autoplay on Load 的是 idle;循环开关在动画面板右下角
代码里切动画:
private AnimationPlayer _anim = null!;
public override void _Ready() => _anim = GetNode<AnimationPlayer>("AnimationPlayer");
// _PhysicsProcess 末尾:if (!IsOnFloor()) _anim.Play("jump");else if (Mathf.Abs(Velocity.X) > 10f) _anim.Play("run");else _anim.Play("idle");B. 有精灵图(sprite sheet)
用 AnimatedSprite2D 节点(不是 AnimationPlayer,2D 帧动画它更方便):
- Sprite Frames 资源:检查器 → 新建 SpriteFrames,底部帧面板建
idle/run/jump/fall四个动画,把序列帧拖进去,设帧率(一般 8–12)和循环 - 代码调用 API 一样:
_sprite.Play("run") - 朝向翻转:
_sprite.FlipH = Velocity.X < 0f;(只在 |X|>10 时设,别每帧赋 false 造成抖动)
4.6 关卡:TileMapLayer 画地面
Godot 4.3+ 用 TileMapLayer(旧的 TileMap 节点已弃用,老教程注意区分)。
- 拖一张 tileset 图(Kenney.nl 有免费平台跳跃素材包;没有就先下载任意 16×16 格子图)到文件系统
- 场景加 TileMapLayer 节点,检查器 Tile Set → 新建 TileSet
- 拖纹理到 TileSet 面板,按提示设置格子大小(如 16×16)
- 关键一步——加碰撞:TileSet 编辑器底部选 “Physics Layers”,加一层;点 Tile Set 编辑器上方的瓦片,逐块(或框选一片)画碰撞矩形
- 回到主视口,用画笔在 TileMapLayer 上画地面。TileMapLayer 自带碰撞,不需要再摆 StaticBody2D
- 多画几个浮空平台,高度差用跳跃能力验证:最高跳跃高度 ≈
JumpVelocity² / (2 × Gravity)= 520²/3200 ≈ 84 像素;水平跳跃距离 ≈2 × |JumpVelocity| / Gravity × MoveSpeed≈ 182 像素。按这个尺度设计平台间距
4.7 摄像机:Camera2D
给 Player 加子节点 Camera2D,勾选 Position Smoothing Enabled(值 8 左右),摄像机带阻尼跟随,不晕。
加 限制(Limits):检查器 Limit → Left/Top/Right/Bottom 填关卡边界(如 Right=2000),镜头不会把关卡外的虚空照出来。
摄像机是 Player 的子节点 = 天然跟随;想要”前瞻”(朝移动方向多看一点),给 Camera2D 加个 Marker2D 孙子节点或代码偏移:
// Camera2D 挂个小脚本即可public override void _PhysicsProcess(double delta){ var player = GetParent<CharacterBody2D>(); Position = Position with { X = Mathf.MoveToward(Position.X, player.Velocity.X * 0.3f, 10f) };}4.8 收集物与死亡:Area2D 登场
做个金币:
Coin(Area2D)├── CollisionShape2D(CircleShape2D,半径 12)└── Polygon2D(小黄圆)Area2D 不产生物理阻挡,只报告”谁进来了”,是触发器、拾取物、伤害区、感应器的统一选择。
scripts/Coin.cs:
using Godot;
public partial class Coin : Area2D{ [Signal] public delegate void CollectedEventHandler(Coin coin);
public override void _Ready() { // 信号可以连信号:Area2D 内置 BodyEntered → 自己的处理方法 BodyEntered += OnBodyEntered; }
private void OnBodyEntered(Node2D body) { if (body is Player) { EmitSignal(SignalName.Collected, this); QueueFree(); // 销毁自己 } }}检测条件要点:
BodyEntered默认只对 Physics 层(CharacterBody/RigidBody/StaticBody)触发;监测另一个 Area 要用AreaEntered- 谁能和谁碰撞/监测,由检查器里 Collision → CollisionLayer / CollisionMask 控制。层是”我是谁”,掩码是”我和谁打交道”。金币:层可留空或单独一层,Mask 勾玩家所在层
QueueFree()是销毁节点的标准方法(不是 C# 的 null——引擎对象要用它回收)
Main 收集金币信号(金币多时用组或由 Coin 直接调用 autoload,第 7 章讲 GameManager):
// 关卡脚本里foreach (Node child in GetNode("Coins").GetChildren()) if (child is Coin coin) coin.Collected += OnCoinCollected;
private int _coins;private void OnCoinCollected(Coin coin){ _coins++; GD.Print("金币:", _coins);}死亡区:关卡底部放一个铺满宽度的 Area2D(脚本判断进入者是 Player 就让它重生):
private void OnBodyEntered(Node2D body){ if (body is Player p) p.GlobalPosition = _spawnPoint; // 简单重生;正式项目用场景重载}4.9 单双向平台与跳穿平台
- 普通地面:TileSet physics 层默认,四面都挡
- 单向平台(可以从底下跳上去、站在上面、按跳下):TileSet physics 层把碰撞形状的 One Way 勾选,或平台 StaticBody2D 用 OneWayCollision 形状
- 按下跳:代码检测
jump键 +move_down动作同时按时,SetCollisionMaskValue(层, false)短暂关闭再开(0.2–0.3 秒计时器恢复)
4.10 调试武器库
- 可见碰撞形状:视口顶部”调试 → 可见碰撞形状”,蓝色是碰撞体,立刻知道”为什么撞不到/为什么被空气挡住”
- 远程场景树:游戏运行时,编辑器左侧场景面板切到”远程”,能看到运行时真实节点树、检查器实时改属性
GD.Print:打印任意对象,GD.Print($"{Position} {Velocity.Y}")字符串插值(教程二第 3 章)Debugger标签:报错带堆栈,双击跳到出错行;GD.PrintErr红字- Debug 绘制:
DrawLine/DrawCircle配合QueueRedraw画调试射线、仇恨范围;或用_Draw()
4.11 本章作业与扩展方向
作业:
- 加
move_down(S)动作,实现单向平台跳下 - 金币数显示到 HUD(CanvasLayer + Label,参考 Pong)
- 做 3 个浮空平台,验证土狼时间和跳跃缓冲确实救了你几次
想继续深化时的方向(后面章节会覆盖):
- 敌人:巡逻 + 踩踏判定(Area2D 放在脚下检测玩家)
- 状态机:idle/run/jump/fall/attack/hurt,第 6 章用 C# 状态模式重构
- 粒子:起跳尘土、落地烟尘、金币闪光(GPUParticles2D,第 8 章)
- 音效:跳跃、拾取、受伤(AudioStreamPlayer,第 8 章)
下一章暂停做游戏,系统讲透 信号与节点通信——这是从小脚本走向架构清晰的项目的分水岭。
《Godot C# 零基础》系列导航
⬅️ 上一篇:Godot C# 零基础教程 03:第一个完整游戏 Pong 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:Godot C# 零基础教程 05:信号与节点通信