静态缓存页面 · 查看动态版本 · 登录
智柴网 登录 | 注册
← 返回话题
Q
QianXun @QianXun · 2026-09-26 16:40

> 本篇把前三级串成一条完整的 2D 主线:移动手感、瓦片地图、相机、碰撞分层。读完你会有一个能玩的小游戏雏形——这是整个系列最重要的里程碑。

一、2D 的世界单位:像素即真理

Godot 2D 的单位就是像素,默认 gravity 980(像素/秒²),一个 128×128 的窗口一格就能放下角色。3D 才是「米」的世界。这个「所见即像素」的直白,是 Godot 2D 工作流口碑好的根本原因。

第一步永远是项目设置三连:

  • 显示 → 窗口 → 视口宽高:设成你想要的逻辑分辨率(如 640×360,像素风标配);
  • 拉伸模式:canvas_items、宽高比 keep——窗口怎么缩放,游戏内像素都清晰锐利;
  • 默认纹理过滤:像素风选 Nearest(4.7 还给 3D 视口加了最近邻缩放选项,复古味 3D 也能锐利)。

二、移动手感:CharacterBody2D 的调参艺术

第一级那个方块已经会跑会跳了,但「会动」和「手感好」差着十倍距离。手感三要素:

  • Coyote Time(土狼时间):离开平台后仍可跳窗的宽限期,通常 0.1 秒。人类反应有延迟,没这个宽限的跳跃永远「差半拍」。
  • Jump Buffer(跳跃缓冲):落地前 0.1 秒按跳,落地瞬间自动起跳。手感廉价感的最大来源就是「按了没跳」。
  • 可变跳跃高度:松开跳跃键就削减上升速度,短按小跳、长按大跳。
extends CharacterBody2D

const SPEED := 300.0
const ACCEL := 1500.0
const FRICTION := 2200.0
const JUMP_VELOCITY := -420.0
const COYOTE_TIME := 0.1
const JUMP_BUFFER := 0.1

var coyote_counter := 0.0
var buffer_counter := 0.0

func _physics_process(delta: float) -> void:
    if is_on_floor():
        coyote_counter = COYOTE_TIME
    else:
        coyote_counter -= delta
        velocity += get_gravity() * delta

    if Input.is_action_just_pressed("jump"):
        buffer_counter = JUMP_BUFFER
    else:
        buffer_counter -= delta

    if buffer_counter > 0.0 and coyote_counter > 0.0:
        velocity.y = JUMP_VELOCITY
        buffer_counter = 0.0
        coyote_counter = 0.0
    if Input.is_action_just_released("jump") and velocity.y < 0.0:
        velocity.y *= 0.5        # 可变跳跃高度

    var dir := Input.get_axis("move_left", "move_right")
    if dir:
        velocity.x = move_toward(velocity.x, dir * SPEED, ACCEL * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, FRICTION * delta)
    move_and_slide()

这三个常量没有任何一个「正确值」——手感是调出来的,把上面六个参数全 @export,运行时在远程检查器里现场拧,是打磨手感的标准姿势。

三、TileMapLayer:一张图铺一个世界

4.x 的瓦片系统由 TileSet(资源:图集 + 物理形状 + 地形规则)与 TileMapLayer(节点:一层瓦片)组成。4.0 曾用 TileMap,4.3 改为多层 TileMapLayer,官方明确建议一瓦片层一个节点。

关键进化在 4.5:TileMapLayer 的物理从「一格一刚体」改成了相邻格碰撞形状自动合并——一整片地面在物理引擎里不再是几千个小刚体,而是少数几个大形状。官方公告原话是重写(reworked from the ground up),对依赖物理的 2D 场景是量级级优化。升级注意:get_coords_for_body_rid() 返回值会变,依赖旧值的老代码要看迁移指南。

TileSet 编辑器里的高频操作:

  • 给图集勾选物理层,逐格画碰撞多边形(地形可以只给顶面碰撞,做「从下往上穿过」的平台);
  • 地形集(Terrain Set)画好连接规则后,铅笔刷一笔刷出自动拼接的地块;
  • 4.6 起场景瓦片可以 90 度旋转,动画宝箱、火把直接当瓦片摆。

四、碰撞分层:矩阵决定谁撞谁

项目设置 → 层名称(Layer Names → 2D Physics),把 1-5 层命名成 player / enemy / world / pickup / hazard,之后每个物理节点的 Collision → Layer/Mask 直接勾语义名。

规则一句话:Layer 是「我是谁」,Mask 是「我关心谁」。玩家 Layer=player、Mask=world|enemy|pickup;拾取物改成 Area2D + Mask=player,敌我互撞就永远不会发生。新人把 Layer 和 Mask 搞反是 2D 篇第一事故,遇到「怎么都检测不到」先查这里。

五、相机跟随:平滑不是弹簧越软越好

Camera2D 设为 Player 子节点即可自动跟随,但直接挂上去是「像素级死锁」,画面硬邦邦。调法:

  • Position Smoothing 开启(速度 5-8 之间开始试);
  • Drag Margins 开启后,画面只在角色拖到边缘时才动,适合大场景;
  • Limits 四项必设(左/右/上/下边界),否则关卡边缘会露出「世界之外的虚空」——平台游戏的经典穿帮。
再进阶一步是给相机加「朝向前瞻」:角色朝右跑时视野先偏右 40 像素,用 Tween 或 lerp 平滑过去,游戏立刻有了「镜头语言」。这个效果 30 行内可写完,建议自己试。

六、把方块换成角色:Sprite2D 与 AnimatedSprite2D

  • 单图角色:Sprite2D 够用,翻转用 flip_h;
  • 帧动画角色:AnimatedSprite2D + SpriteFrames 资源,在编辑器里把精灵表切成帧、命名 idle/run/jump/fall 动画;
  • 联动逻辑:在移动脚本里按状态切换播放——if not is_on_floor(): play("jump" if velocity.y < 0 else "fall")。
第五级会讲重量级的 AnimationPlayer 与状态机,AnimatedSprite2D 是轻量方案,两者不冲突——像素游戏的帧动画走 AnimatedSprite2D,骨骼/属性/位移混合动画走 AnimationPlayer。

七、本篇交付物清单

完成本篇,你手上应该有:

  • 一个带土狼时间、跳跃缓冲、可变跳跃的玩家场景;
  • 一张用 TileMapLayer 铺的关卡,含自动拼接地形与单向平台;
  • 一个带平滑、边界限制、前瞻偏移的相机;
  • 碰撞层语义化命名、敌人场景雏形。
下一篇,第五级台阶:动画与状态机——AnimationPlayer 的轨道、AnimationTree 的状态机与混合空间,让角色从「能播放动画」进化到「动画与逻辑互不打架」。

*本篇配图:小方块跳跃平台与碰撞层。*

第四级 · 2D 实战:平台跳跃

暂无表态