Godot 4 平台跳跃实战:瓦片地图、敌人AI与角色手感
2026/9/1 18:00:47 网站建设 项目流程

这次我们直接看一个很实际的选题:用 Godot 4 从零做一个平台跳跃小游戏,并且把瓦片地图、敌人 AI、角色手感这三块一次打通。很多新手学 Godot 时会发现,单看每一个功能的 Demo 都能跑通,但把它们组合到一个项目里就会出现各种问题:瓦片碰撞没生效、敌人撞墙不回头、跳跃手感发飘。这篇文章的定位就是“组合实战”,按两小时左右的节奏,把一条完整流程走完,最后得到一个可以实际玩的小关卡,而不是停留在碎片化测试。

Godot 4.x 是这次使用的引擎版本。它是开源免费的 2D/3D 游戏引擎,支持 Windows、Linux、macOS,导出目标覆盖 PC、Web、Android、iOS。平台跳跃属于 2D 横版玩法,对硬件门槛很低,普通办公电脑即可完成编辑和测试。整个项目使用 GDScript 编写,语法接近 Python,没有 C++ 基础也能看懂。核心节点集中在 CharacterBody2D(角色物理)、TileMapLayer(瓦片地图)、Area2D(敌人检测与玩家受击)。

本文会带大家完成这些内容:创建 Godot 项目、配置输入映射;用 TileSet 搭建瓦片地图并添加物理碰撞;实现玩家移动、跳跃、落地判定和跳跃手感;实现敌人 AI 的巡逻、追击与返回;加入简单的死亡判定和重开逻辑;最后测试运行和导出。如果你已经会 Godot 基础界面,可以直接跳到瓦片地图和敌人 AI 部分;如果你是从零开始,建议按顺序完整执行。

1. Godot 平台跳跃实战核心能力速览

项目说明
项目类型2D 横版平台跳跃游戏开发实战
引擎版本Godot 4.x(示例按 Godot 4 语法编写)
脚本语言GDScript
核心功能瓦片地图绘制、TileMap 碰撞、角色移动与跳跃、敌人 AI 巡逻追击
硬件要求普通办公电脑即可,2D 场景编辑对 GPU 要求很低
资源占用2D 项目整体占用低,具体取决于素材分辨率与场景复杂度
启动方式Godot 编辑器打开项目,运行当前场景
导出平台Windows / Linux / macOS / Web / Android / iOS
API 能力项目本身不涉及接口服务;Godot 支持 HTTP 请求,可按需扩展
批量任务不涉及批量任务;素材批量导入和重命名可以通过文件系统批量处理
适合场景独立游戏原型、横版闯关、游戏开发教学、角色手感实验

这一张表可以帮助你在动手前快速判断:这个项目的重点不是复杂系统,而是把 2D 平台跳跃最常见的技术点串成完整闭环。如果你只是想学习单个功能,可以只看对应章节;如果你希望做出一个能玩的小 Demo,建议按文章顺序操作。

2. 平台跳跃游戏适用场景与使用边界

先说清楚这个项目适合谁。独立游戏开发者想快速验证横版玩法,学生需要用不超过两个小时的流程理解游戏状态机与物理系统,或者从零开始做毕设、作品集,都很适合用这套流程。平台跳跃这个品类,核心体验是角色移动、跳跃手感、关卡设计和敌人行为,工程量比 RPG、动作冒险要小很多,作为游戏开发的第一款完整 Demo 非常合适。

同时也要说清楚不适合什么场景。如果你要做的是一款大型 3D 动作游戏、多人联机游戏,或者以复杂剧情和 UI 交互为核心的产品,平台跳跃这套流程只能作为入门基础,架构上还需要大量扩展。教程里的敌人 AI 是简单的状态机,不包含寻路、避障、Boss 战等高级行为;瓦片地图也以手工绘制为主,没有接入自动生成算法。

使用边界方面,最需要留意的是素材合规问题。做游戏开发练习时很容易从网上下载瓦片素材、音效、字体,如果只是本地学习,问题不大;但一旦要发布到 Steam、Itch.io 或参加游戏比赛,就必须检查素材的授权协议。使用 AI 生成的素材时,也要看对应平台的商用条款。教程本身的代码可以随意修改使用,素材则要单独确认来源。

3. Godot 平台跳跃开发环境准备

3.1 下载与安装 Godot

平台跳跃项目不依赖复杂的编译环境,只需要一个 Godot 编辑器。从 Godot 官方网站下载对应操作系统的标准版即可,Windows 下解压后直接双击编辑器程序启动,不需要安装,也不需要配置环境变量。建议下载 4.x 分支的稳定版本。如果后续要做导出,还需要到编辑器内部下载对应平台的导出模板,这个可以等做到导出环节再处理。

3.2 创建项目

打开 Godot 后,点击项目管理器中的“新建项目”:

  • 项目名称:PlatformerTutorial
  • 项目路径:选择本地任意目录
  • 渲染器:选择 Forward+、Mobile 或 Compatibility 均可。2D 项目建议使用 Compatibility,兼容性更好,内存占用也更低。

创建完成后,项目会自动生成 project.godot 文件和空目录。后续所有场景、脚本、素材都放在这个目录里。由于平台跳跃的核心是 2D 物理,不需要额外启用 3D 设置,在项目创建阶段保持默认项即可。

3.3 配置输入映射

平台跳跃需要三个输入动作:move_left、move_right、jump。在“项目设置 → 输入映射”中手动添加:

  • move_left:绑定 A 键和左方向键
  • move_right:绑定 D 键和右方向键
  • jump:绑定空格键和 W 键

添加完之后,代码中就可以直接通过Input.get_axis("move_left", "move_right")读取水平输入,通过Input.is_action_just_pressed("jump")读取跳跃键。这里不推荐直接在 project.godot 里手写事件序列化文本,因为格式很容易写错,编辑器界面更可靠。配置完成这一步,后面所有脚本才能正常控制角色。

3.4 准备瓦片素材

瓦片地图需要一个素材图。可以使用 Godot 官方示例素材,也可以自己用像素编辑器画一张。建议选择 16x16 或 32x32 的地面、砖块、平台素材,保存为 PNG。不需要额外安装工具,Godot 的 TileSet 编辑器可以直接从单张图片切割瓦片。素材本身可以是整个关卡共用的一张图集,只要你在导入时设置好切片大小,Godot 会自动把图片分割成多个独立瓦片。

4. 玩家角色:平台跳跃的物理与手感实现

4.1 创建玩家场景

在场景面板中新建一个 CharacterBody2D,命名为 Player:

  • 添加 Sprite2D 作为视觉显示
  • 添加 CollisionShape2D,使用 RectangleShape2D 或 CapsuleShape2D
  • 添加脚本 player.gd

Player 节点结构:

Player (CharacterBody2D) ├── Sprite2D └── CollisionShape2D

CharacterBody2D 是 Godot 中处理移动角色的标准节点,它不会自动产生碰撞反应,而是通过move_and_slide()方法让角色沿墙壁和地板滑动。平台跳跃的物理逻辑都写在_physics_process里。这个节点自带is_on_floor()方法,可以判断角色是否站在地面,这是跳跃判定最核心的依据。

4.2 核心移动代码

player.gd:

extends CharacterBody2D @export var speed: float = 300.0 @export var jump_velocity: float = -400.0 @export var gravity: float = 980.0 @export var coyote_time: float = 0.1 @export var jump_buffer_time: float = 0.1 var coyote_timer: float = 0.0 var jump_buffer_timer: float = 0.0 func _physics_process(delta: float) -> void: # 重力:不在空中时加速下落 if not is_on_floor(): velocity.y += gravity * delta # 土狼时间:离开平台后短时间内仍可跳跃 if is_on_floor(): coyote_timer = coyote_time else: coyote_timer -= delta # 水平移动 var direction := Input.get_axis("move_left", "move_right") velocity.x = direction * speed # 跳跃缓冲:按下跳跃键后短时间内仍可生效 if Input.is_action_just_pressed("jump"): jump_buffer_timer = jump_buffer_time else: jump_buffer_timer -= delta # 执行跳跃 if jump_buffer_timer > 0.0 and coyote_timer > 0.0: velocity.y = jump_velocity jump_buffer_timer = 0.0 coyote_timer = 0.0 move_and_slide()

这段代码的关键点有三个:重力累加、水平方向乘以速度、move_and_slide()负责实际移动。后面两个 Timer 分别实现了土狼时间和跳跃缓冲,这两项对平台跳跃手感影响非常大。没有它们,角色每次落地都必须像素级准确才跳得起来,玩起来会非常僵。这里的核心逻辑量已经覆盖了平台跳跃最常见的物理问题,后续扩展二段跳、冲刺、爬坡都可以在此基础上叠加。

4.3 动画朝向与简单状态

如果不想用动画,至少要让角色根据移动方向转头。最简单的方式是翻转 Sprite2D 的 scale.x。这里把朝向逻辑单独抽成一个函数,方便在移动后调用:

func _flip_sprite(direction: float) -> void: if direction != 0.0: $Sprite2D.scale.x = abs($Sprite2D.scale.x) * sign(direction)

这个函数只做视觉翻转,不影响碰撞体。在_physics_process中调用时,注意要在速度更新之后再执行,否则可能出现方向已经改变但视觉还没跟上的情况。如果需要跑步动画,建议把 AnimationPlayer 加入 Player 节点,用速度和is_on_floor()作为状态条件切换 idle、run、jump 三个动画。

4.4 调整手感的参数顺序

第一次调手感时,建议按这个顺序来:

  1. 先调 speed,让水平移动速度符合预期。
  2. 再调 jump_velocity,跳跃高度太高会导致关卡和敌人都要重新设计。
  3. 然后调 gravity,重力越小,跳跃越飘;重力越大,跳跃越干脆。
  4. 最后调 coyote_time 和 jump_buffer_time,这两个值通常在 0.08 到 0.15 秒之间。

每次修改参数后按 F6 运行当前场景,反复跳跃十几次再继续调。不要一次改多个参数,否则很难定位是哪一项影响最大。平台跳跃的手感没有标准答案,不同像素游戏、不同角色大小、不同平台宽度,需要的参数组合都不一样。最稳的做法是先把基础数值跑通,再根据自己的手感需要微调。

5. 瓦片地图实战:从 TileSet 到关卡绘制

5.1 创建 TileSet 并导入瓦片

在文件系统面板选中准备好的瓦片素材,右键“新建 → 资源 → TileSet”,然后在 Inspector 中打开 TileSet 编辑器。Godot 4 中 TileSet 的资源类型是 TileSet,在资源面板中创建后,指定 source 为素材图片,再设置切片大小(例如 16x16 或 32x32)。

瓦片创建完成后,编辑器会自动把图片切割成多个瓦片。在这里可以给每个瓦片添加物理层:

  • 选择某个地面瓦片
  • 在 Inspector 中展开“物理层”
  • 点击“添加多边形”
  • 在瓦片上绘制一个矩形碰撞形状

这里建议给所有会阻挡角色通过的瓦片都添加物理层,例如地面、砖块、柱子,而不是只给部分瓦片添加。否则角色可能在某个看起来很实的砖块上直接穿模掉下去。物理多边形可以画得比瓦片边缘略小一点,这样角色踩在几块瓦片接缝处时更不容易被卡住。

5.2 添加 TileMapLayer 节点

Godot 4 中推荐使用 TileMapLayer 节点来放置瓦片地图。如果你使用的是 Godot 4.3 或更新版本,可以直接创建 TileMapLayer;旧版本中使用的 TileMap 节点仍然可以打开,但新项目建议直接用 TileMapLayer。操作步骤:

  1. 在场景根节点下添加一个 TileMapLayer 节点,命名为 GroundLayer。
  2. 在 Inspector 的 Tile Set 属性里选择刚才创建的 TileSet。
  3. 进入瓦片绘制模式,选择对应瓦片,在场景视图中点击绘制地图。

图层设计建议:

  • GroundLayer:地面与实体砖块,包含碰撞
  • BackgroundLayer:背景装饰,可以放在最底层,不包含碰撞
  • ForegroundLayer:前景挡板或装饰,放在角色前面,显示用

如果只需要一张平台跳跃地图,用 GroundLayer 加 BackgroundLayer 两层就够。这里的关键思路是“碰撞层和装饰层分离”。装饰层不参与碰撞,可以放心画各种花花草草和远景;碰撞层只保留真正能站上去的实体瓦片。这样后期调整外观时不用重新配碰撞,出现碰撞问题也更好排查。

5.3 碰撞边界与出生点

在场景中放置一个 StaticBody2D 作为场景边界,或者在 GroundLayer 外画一圈深坑。平台跳跃通常不需要物理墙,直接让角色掉落到边界外即可,这样反而省事。为了让玩家掉落时不会无限下坠,可以在场景最下方放一个不可见的 Area2D,作为死亡检测区域。

出生点可以是一个 Node2D,标记为 SpawnPoint。玩家角色在游戏开始时被实例化到该位置。如果后续要做多关卡,这个出生点也可以作为关卡检查点使用,角色死亡后从最近检查点重新开始。

5.4 瓦片地图绘制细节

绘制时注意几个细节:

  • 平台宽度至少要有 2-3 个瓦片,否则角色跳跃落点太难掌握。
  • 敌人活动区域尽量平整,避免 AI 反复卡在台阶边缘。
  • 高空敌人路线需要预留足够的检测空间。
  • 绘制完地图后,打开调试面板,开启“可见碰撞体”,检查每块地面是否有碰撞框。

检查碰撞是瓦片地图最容易出错的一步。如果绘制完发现角色直接穿模,大概率是瓦片没有加物理层,或者 CollisionShape2D 的位置发生偏移。瓦片地图本身不限制 TileSet 里可以放多少瓦片,一个关卡里几十块瓦片足够覆盖常见的平台结构,重点是物理层要配得干净,不要出现细碎的碰撞缝隙。

6. 敌人 AI:巡逻、追击与状态切换

6.1 敌人节点结构

创建一个敌人类,使用 CharacterBody2D 作为根节点,这样敌人也能使用move_and_slide()和物理碰撞:

Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── RayCast2D(检测前方墙壁或悬崖) └── Area2D(检测玩家)

Area2D 用于检测玩家是否进入追击范围,生成一个“看到玩家”的信号;RayCast2D 用于防止敌人走到墙壁或悬崖外。也可以用简单的距离判断代替 RayCast2D,看具体关卡复杂度。如果关卡地形简单的巡逻路径,距离判断已经够用;如果敌人需要翻越台阶或者绕开障碍,就要加 RayCast2D 甚至寻路系统。

6.2 敌人状态机代码

enemy.gd:

extends CharacterBody2D enum State { IDLE, PATROL, CHASE } @export var patrol_speed: float = 80.0 @export var chase_speed: float = 160.0 @export var patrol_range: float = 120.0 @export var detection_range: float = 200.0 var state: State = State.PATROL var start_x: float = 0.0 var direction: int = 1 func _ready() -> void: start_x = global_position.x func _physics_process(delta: float) -> void: match state: State.IDLE: pass State.PATROL: _patrol(delta) State.CHASE: _chase(delta) move_and_slide() func _patrol(delta: float) -> void: if global_position.x < start_x - patrol_range: direction = 1 elif global_position.x > start_x + patrol_range: direction = -1 velocity.x = direction * patrol_speed _update_facing() func _chase(delta: float) -> void: var player := get_tree().get_first_node_in_group("player") if player == null: return var diff := player.global_position.x - global_position.x if abs(diff) > 8.0: velocity.x = sign(diff) * chase_speed else: velocity.x = 0.0 _update_facing() func _on_detection_area_body_entered(body: Node2D) -> void: if body.is_in_group("player"): state = State.CHASE func _on_detection_area_body_exited(body: Node2D) -> void: if body.is_in_group("player"): state = State.PATROL func _update_facing() -> void: if direction != 0: $Sprite2D.scale.x = abs($Sprite2D.scale.x) * direction

这段代码描述了一个常见敌人 AI 行为:在起点附近巡逻,发现玩家后追击,玩家离开检测范围后恢复巡逻。状态迁移由 Area2D 信号触发。需要说明的是,这个 AI 没有处理障碍寻路,追击时如果敌人和玩家之间隔着墙壁,敌人会直接撞墙。对于平台跳跃关卡,通常建议在敌人前方加一个 RayCast2D,检测到墙壁时沿墙向上或向下移动,或者干脆限制敌人的追击区域。用状态机管理 AI 的好处是后续加新行为时,只需要在枚举里增加一个状态,再补一个处理函数,不需要改动整个控制流。

6.3 敌人与玩家碰撞

敌人碰撞分两种情况:

  • 玩家踩到敌人头顶:敌人死亡,玩家反弹。
  • 玩家从侧面碰到敌人:玩家受伤或死亡。

最简单实现:在玩家脚本中判断is_on_floor()和玩家与敌人的相对位置,如果是下压,则调用敌人死亡;否则玩家死亡。也可以用 Area2D 区分攻击区域和伤害区域。如果要做更精确的判定,建议给敌人头顶单独加一个 Area2D 作为攻击判定,其他地方作为伤害判定。这样玩家踩到头顶正上方时触发敌人死亡,碰到敌人侧面时触发玩家死亡,逻辑更清晰。

7. 游戏流程与胜负判定

7.1 玩家信号与主场景

把主场景分为三层:

  • Main:根节点,控制游戏流程
  • World:包含瓦片地图、敌人生成点与背景
  • Player:玩家角色

在 Main 中写一个简单的流程控制:

extends Node2D @onready var player: CharacterBody2D = $Player func _ready() -> void: player.add_to_group("player") player.died.connect(_on_player_died) func _on_player_died() -> void: get_tree().call_deferred("reload_current_scene")

玩家脚本中需要发射 died 信号。当玩家血量归零或掉落地图时,调用emit_signal("died")。这种“信号驱动”的写法比在敌人脚本里直接操作玩家节点更清晰,后续加 UI、加商店都比较方便。主场景只关心“玩家死了要重新加载”,至于玩家怎么死的,由玩家自己通过信号通知,职责划分比较明确。

7.2 胜利与终点

在有多个关卡时,可以增加一个 Area2D 作为终点检测区,玩家进入后切换到下一关。切关代码:

func _on_finish_area_body_entered(body: Node2D) -> void: if body.is_in_group("player"): get_tree().change_scene_to_file("res://level_2.tscn")

如果只做一个

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询