2441 字
12 分钟
Godot C# 零基础教程 04:2D 平台跳跃

教程一 · 第 4 章:2D 平台跳跃——重力、跳跃、动画、摄像机#

Pong 是”完全自己控制位移”,平台跳跃引入真正的物理:重力、地面检测、跳跃缓冲、土狼时间。做完你就具备了独立做 2D 动作游戏原型的能力。

4.1 输入动作#

项目设置 → 输入映射:

动作按键
move_left / move_rightA / 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 帧动画它更方便):

  1. Sprite Frames 资源:检查器 → 新建 SpriteFrames,底部帧面板建 idle/run/jump/fall 四个动画,把序列帧拖进去,设帧率(一般 8–12)和循环
  2. 代码调用 API 一样:_sprite.Play("run")
  3. 朝向翻转:_sprite.FlipH = Velocity.X < 0f;(只在 |X|>10 时设,别每帧赋 false 造成抖动)

4.6 关卡:TileMapLayer 画地面#

Godot 4.3+ 用 TileMapLayer(旧的 TileMap 节点已弃用,老教程注意区分)。

  1. 拖一张 tileset 图(Kenney.nl 有免费平台跳跃素材包;没有就先下载任意 16×16 格子图)到文件系统
  2. 场景加 TileMapLayer 节点,检查器 Tile Set → 新建 TileSet
  3. 拖纹理到 TileSet 面板,按提示设置格子大小(如 16×16)
  4. 关键一步——加碰撞:TileSet 编辑器底部选 “Physics Layers”,加一层;点 Tile Set 编辑器上方的瓦片,逐块(或框选一片)画碰撞矩形
  5. 回到主视口,用画笔在 TileMapLayer 上画地面。TileMapLayer 自带碰撞,不需要再摆 StaticBody2D
  6. 多画几个浮空平台,高度差用跳跃能力验证:最高跳跃高度 ≈ 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 调试武器库#

  1. 可见碰撞形状:视口顶部”调试 → 可见碰撞形状”,蓝色是碰撞体,立刻知道”为什么撞不到/为什么被空气挡住”
  2. 远程场景树:游戏运行时,编辑器左侧场景面板切到”远程”,能看到运行时真实节点树、检查器实时改属性
  3. GD.Print:打印任意对象,GD.Print($"{Position} {Velocity.Y}") 字符串插值(教程二第 3 章)
  4. Debugger 标签:报错带堆栈,双击跳到出错行;GD.PrintErr 红字
  5. Debug 绘制:DrawLine/DrawCircle 配合 QueueRedraw 画调试射线、仇恨范围;或用 _Draw()

4.11 本章作业与扩展方向#

作业:

  1. 加 move_down(S)动作,实现单向平台跳下
  2. 金币数显示到 HUD(CanvasLayer + Label,参考 Pong)
  3. 做 3 个浮空平台,验证土狼时间和跳跃缓冲确实救了你几次

想继续深化时的方向(后面章节会覆盖):

  • 敌人:巡逻 + 踩踏判定(Area2D 放在脚下检测玩家)
  • 状态机:idle/run/jump/fall/attack/hurt,第 6 章用 C# 状态模式重构
  • 粒子:起跳尘土、落地烟尘、金币闪光(GPUParticles2D,第 8 章)
  • 音效:跳跃、拾取、受伤(AudioStreamPlayer,第 8 章)

下一章暂停做游戏,系统讲透 信号与节点通信——这是从小脚本走向架构清晰的项目的分水岭。


《Godot C# 零基础》系列导航

⬅️ 上一篇:Godot C# 零基础教程 03:第一个完整游戏 Pong 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:Godot C# 零基础教程 05:信号与节点通信

Godot C# 零基础教程 04:2D 平台跳跃
https://021028.xyz/posts/default/164/
作者
021028
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0