☰
Godot 4平台跳跃游戏实战:角色控制器、瓦片地图与敌人AI
2026/10/21 14:08:19 网站建设 项目流程

做平台跳跃游戏,最容易被卡住的并不是“让角色跳起来”这件事,而是地图是怎么铺的、敌人是怎么动起来的、角色碰到地面为什么不会被弹飞。很多新手照着别人的视频敲代码,角色明明能跑能跳,一旦想自己做一关,就发现 TileMap 怎么都碰撞不上,敌人只会原地抖,最后只能放弃。

这篇文章的主角是 Godot。我会用一个 2 小时能跑通的平台跳跃实战流程,把三件事彻底讲明白:玩家控制器怎么写、瓦片地图怎么铺才不会穿模、敌人 AI 怎么从“只会走”变成“会追踪”。整个过程基于 Godot 4.x 的实际工程,代码可以直接复制到你的项目里运行。

先给一个明确判断:Godot 是目前做 2D 平台跳跃游戏性价比最高的引擎之一。它不是“勉强能用来教学”,而是连物理、动画、场景管理、导出发布都做成了完整闭环。你不需要在代码里手写碰撞检测,也不需要自己管理游戏循环,这2小时真正要学的是节点怎么组织、信号怎么通信、有限状态机怎么落地这三件通用能力。

读完本文你会得到:一个可运行的平台跳跃最小工程、一套瓦片地图关卡搭建流程、一个带巡逻与追踪逻辑的敌人 AI 模板。这些代码不是一次性 demo,而是可以直接往正式项目迁移的骨架。

1. 先想清楚:2 小时做完,到底做完什么

很多人打开 Godot 会懵,是因为编辑器里全是面板和按钮,不知道从哪里开始。在动手之前,我建议把“2小时完成目标”拆成四个验收点:

  1. 玩家角色可以左右移动、跳跃、下落,碰到地面不会穿透。
  2. 关卡地图是用瓦片画出来的,而不是用一个个 Sprite 拼的。
  3. 地图上的平台有真实碰撞,角色能站在上面。
  4. 有一个敌人,平时左右巡逻,玩家靠近后切换成追踪状态。

这四个点对应的是四个核心系统:角色控制器、输入映射、瓦片地图 + 碰撞层、敌人 AI 状态机。把这四个系统打通,你的项目骨架就立住了。

用 2 小时做完的含义,不是“2 小时写完全部代码”,而是“2 小时理解并跑通最小闭环”。后面你要加双段跳、加攻击、加机关,都是在最小闭环上做增量而已。

为什么要选平台跳跃?因为它是一个“麻雀虽小,五脏俱全”的游戏类型。角色运动、物理碰撞、地图编辑、敌人行为、动画切换、音效触发,这些商业游戏里最常见的问题,在一款平台跳跃里全都会出现。你在 2 小时里踩过的坑,后面做任何 2D 游戏都绕不开。

2. 基础概念:节点、场景、信号,Godot 的三大支柱

写代码之前,先建立一套正确的认知模型。Godot 的一切都由三件事构成:节点(Node)、场景(Scene)、信号(Signal)。

节点是最小功能单元。它有类型,比如CharacterBody2D表示“一个可以被物理移动的角色”,Sprite2D表示“显示一张图片”,CollisionShape2D表示“一个碰撞形状”。你在场景里看到的每一个东西都是节点。

场景就是节点的组合。你可以把一个角色做成一个场景,里面包含Sprite2D、CollisionShape2D、脚本节点,甚至子节点,然后用一个.tscn文件保存,需要的时候实例化。这个“场景即预制体”的思路,让代码复用变得非常自然。

信号是节点之间的通信机制。当一个事件发生时(比如按钮被按下、敌人撞到墙),节点会发射信号,其他节点可以监听并做出反应。它比直接对象引用更解耦,是 Godot 组织代码的核心方式。

以下面这个角色身体节点为例:

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

CharacterBody2D负责物理移动,Sprite2D负责显示外观,CollisionShape2D决定碰撞范围,Camera2D让镜头跟随。你不需要把这些代码写在一个类里,只需要把节点挂在场景树上,事情就自然分工了。

这种设计带来的好处是:你改动画时不碰物理代码,改碰撞时不碰逻辑代码。作为新手,一开始就建立这种分层意识,后面代码不会变成一坨浆糊。

3. 环境准备:下载 Godot 4.x 并创建项目

第一步是下载引擎。打开 Godot 官网,下载 4.x 最新稳定版。建议选择标准版,不需要 .NET 版,因为本文代码使用 GDScript,不涉及 C#。解压后双击可执行文件就能打开编辑器,不需要安装依赖,也不向系统写额外配置,这点比很多游戏引擎清爽得多。

接下来创建项目。

  1. 打开 Godot,点击“新建项目”(New Project)。
  2. 项目名称填GodotPlatformer。
  3. 项目路径选择一个你能找到的目录。
  4. 渲染器保持默认(Forward Plus),点击“创建”。

创建完成后,你会看到 Godot 的主编辑器。建议先把界面大致过一遍:左侧是场景面板,中间是视口(游戏画面),右侧是检查器面板,下方是输出面板。现在不需要记所有按钮,只需要知道你要的资源在哪里可以找到。

在编写代码之前,先配置输入映射。平台跳跃最少需要三个输入动作:move_left、move_right、jump。

打开菜单栏的“项目” -> “项目设置”,切到“输入映射”(Input Map)标签页。在底部输入动作名,点击“添加”。然后为每个动作分配快捷键:

  • move_left:键盘 A 或左方向键。
  • move_right:键盘 D 或右方向键。
  • jump:空格键或 W 或上方向键。

输入映射做得好不好,直接影响手感。建议把多个按键映射到同一个动作上,避免玩家只能用一个键操作。

4. 玩家控制器:从移动、跳跃到下落的完整实现

现在创建玩家角色场景。

在文件系统面板右键点击res://,选择“新建场景”,选择根节点为CharacterBody2D,命名为Player。保存为player.tscn。

选中Player节点,在检查器中添加子节点:

  1. Sprite2D:显示角色外观。课堂上为了方便,你可以创建一个 32x32 的ColorRect临时替代,或者准备一张小图片。
  2. CollisionShape2D:添加一个RectangleShape2D,尺寸设置成 32x32,位置稍微偏移到角色中心。
  3. Camera2D:让镜头跟随玩家。

新建脚本,命名为player.gd,附加到Player节点上。代码如下:

# 文件路径:player.gd extends CharacterBody2D @export var speed: float = 300.0 @export var jump_velocity: float = -400.0 @export var gravity: float = 980.0 func _physics_process(delta: float) -> void: # 重力:只有不在地面时才累加 if not is_on_floor(): velocity.y += gravity * delta # 水平方向输入:返回 -1.0 到 1.0 之间的值 var direction := Input.get_axis("move_left", "move_right") if direction != 0: velocity.x = direction * speed else: # 没有输入时平滑减速,防止角色滑冰 velocity.x = move_toward(velocity.x, 0, speed) # 跳跃:只允许在地面时起跳 if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_velocity move_and_slide()

这段代码的核心是move_and_slide()。它做了三件事:根据velocity更新位置、检测碰撞、自动把角色推到地面或墙上。这也是CharacterBody2D和普通Node2D最大的区别——你不需要手写碰撞检测,引擎已经帮你处理了。

gravity用@export导出,意味着你可以在检查器里直接调数值。手感调优时,这几个参数非常关键:

  • speed决定移动速度,建议 250 到 350。
  • jump_velocity决定跳跃高度,负数表示向上,-350 到 -450 比较合适。
  • gravity决定下落加速度,900 到 1200 会让角色“有重量感”。

把脚本保存后,切回场景,把Player节点拖到视口中,按 F6 运行当前场景。你会发现角色已经能在地面上移动和跳跃了。

真正容易踩坑的地方是:为什么我加了重力,角色却一直往下掉?答案通常是场景里没有地面,或者地面没有碰撞形状。所以下一步就是做瓦片地图。

5. 瓦片地图搭建:TileSet 与 TileMapLayer 实战

Godot 4.3 之后,原来的TileMap节点被拆分成了TileMapLayer。每个TileMapLayer只负责一层瓦片,结构更清晰。如果你的 Godot 版本还是 4.2,命名仍是TileMap,但概念几乎一致。

瓦片地图的核心是TileSet资源。它定义了“有哪些瓦片”“每个瓦片多大”“哪些瓦片有碰撞”。

5.1 创建 TileSet

  1. 在文件系统面板中右键 -> 新建资源,选择TileSet,保存为world_tileset.tres。
  2. 选中这个TileSet资源,在检查器面板中,点击“添加图块集”(Add Atlas Source)。
  3. 点击新出现的 sources 下的空位,选择你的瓦片图集图片。如果你没有现成素材,可以先用一张纯色 PNG 或者截图软件做一张简单的 16x16 地面图。
  4. 设置Tile Size为 16x16 或者你的素材实际像素尺寸。

这里的关键是:图集素材里的瓦片,必须是等尺寸的格子图。如果你的原图是 64x64 像素的 4x4 素材,那么每个瓦片尺寸应该是 16x16。

5.2 为瓦片设置物理碰撞

瓦片地图如果没有物理形状,角色会直接穿过去。选中TileSet资源后,在Physics Layers部分点击“添加图层”,然后回到瓦片选择的视图中,选中需要碰撞的瓦片,点击“添加碰撞多边形”,在瓦片画布上框选碰撞区域。

这一步非常容易漏。如果你发现地图画出来了但角色掉下去,先检查这个瓦片是否被分配了Physics Layer和Collision Polygon。

5.3 创建 TileMapLayer 并绘制关卡

回到主场景,新建一个节点,选择TileMapLayer,命名为Ground。把刚才的world_tileset.tres拖到它的Tile Set属性中。

选中Ground节点,在工具栏上会多出瓦片绘制工具。你可以用铅笔工具在画布中画出地面、平台、墙体。

绘制时建议遵循一条原则:同一层的瓦片只画同一种用途。地面层画的是物理碰撞的砖块,装饰层可以画花花草草,碰撞层放的是尖刺等伤害地形。不要把所有东西堆在一层,否则后面做玩法逻辑时会很难处理。

如果场景需要一个双层结构,你可以再添加一个TileMapLayer,一上一下,互不干扰。

# 脚本方式创建 TileSet 的参考写法(可用于程序化生成) var tile_set := TileSet.new() var source := TileSetAtlasSource.new() source.texture = preload("res://assets/tileset.png") source.texture_region_size = Vector2i(16, 16) tile_set.add_source(source, 0)

这段代码不是必须写在项目里的,但它说明了TileSet的底层结构:一个TileSetAtlasSource对应一张图集,texture_region_size决定瓦片切割尺寸。理解这一点,你就知道为什么素材必须是规整网格图了。

5.4 验证碰撞效果

回到Player场景,把Player节点保存后,在主场景中实例化一个Player。再给地面TileMapLayer添加一个StaticBody2D下的CollisionShape2D?其实不需要:瓦片地图的 Physics Layer 会自动产生碰撞体,引擎会把瓦片上的碰撞多边形合并成静态碰撞体。这正是瓦片地图比逐块摆StaticBody2D高效的原因。

按 F5 运行主场景,玩家应该能站在瓦片地面上,跳跃后能落回地面。如果你发现角色的脚部位置不对,微调CollisionShape2D的位置即可。

6. 敌人 AI:从巡逻到追踪的状态机实现

平台跳跃里的敌人,不需要像格斗游戏 AI 那么复杂,但至少要有一个可扩展的行为框架。这里我用一个简单的有限状态机(FSM)来组织敌人行为。

状态机的思想是:敌人的行为可以被划分为若干个不重叠的状态,每个状态有自己的一套行为逻辑,状态之间通过条件切换。平台跳跃里最常见的是两个状态:PATROL(巡逻)和CHASE(追踪)。

创建敌人场景,根节点同样选用CharacterBody2D,保存为enemy.tscn。新建enemy.gd,代码如下:

# 文件路径:enemy.gd extends CharacterBody2D enum State { PATROL, CHASE } @export var patrol_speed: float = 80.0 @export var chase_speed: float = 150.0 @export var detection_range: float = 200.0 @export var wall_check_distance: float = 8.0 var state: State = State.PATROL var direction: int = 1 var player: Node2D func _ready() -> void: # 寻找玩家,建议玩家节点加入 "player" 分组 player = get_tree().get_first_node_in_group("player") func _physics_process(delta: float) -> void: # 判断是否切换状态 if player and global_position.distance_to(player.global_position) < detection_range: state = State.CHASE else: state = State.PATROL match state: State.PATROL: patrol(delta) State.CHASE: chase(delta) func patrol(delta: float) -> void: velocity.x = direction * patrol_speed # 检测前方是否有墙壁 if is_on_wall(): direction *= -1 # 检测前方是否为悬崖 var target_position := global_position + Vector2(direction * 20, 0) var space_state := get_world_2d().direct_space_state var query := PhysicsRayQueryParameters2D.create( global_position, target_position ) query.exclude = [self] var result := space_state.intersect_ray(query) # 如果前方没有地面,就掉头 if result.is_empty(): direction *= -1 move_and_slide() func chase(delta: float) -> void: if not player: return var to_player: Vector2 = player.global_position - global_position direction = 1 if to_player.x > 0 else -1 velocity.x = direction * chase_speed # 简单跳跃:如果前方有障碍,尝试跳一下 if is_on_wall(): velocity.y = -300.0 move_and_slide()

这段代码解释几个重要点:

  1. 状态切换条件:玩家与敌人距离小于detection_range时,进入追踪。这个写法适合作为最小原型,但实际项目中你可能需要“视野检测”,也就是说敌人在玩家背后时不应该被看到。
  2. is_on_wall()和move_and_slide()的配合:当敌人碰到墙壁时,is_on_wall()会返回true,因此可以安全地翻转方向。这是平台跳跃敌人最常用的巡逻方式。
  3. 射线检测悬崖:通过PhysicsRayQueryParameters2D,向敌人前方打一条射线。如果前方没有地面,说明前面是悬崖,敌人掉头。这段代码是巡逻 AI 的“防坠崖”关键。
  4. 追踪状态的简单跳跃:当追踪时撞到墙,直接给一个向上的velocity.y,让敌人尝试跳过障碍。这个逻辑很粗糙,但它足以应对简单地形。

如果你希望敌人碰到玩家时对玩家造成伤害,还需要给敌人添加一个Area2D或检查碰撞体。最简单的做法是给敌人加一个子节点Area2D,覆盖敌人身体范围,然后监听body_entered信号。这样就把“敌人”和“伤害判定”解耦了。

# 在 enemy.gd 中添加 func _on_damage_area_body_entered(body: Node2D) -> void: if body.has_method("take_damage"): body.take_damage(1)

这个on_damage_area_body_entered函数可以在编辑器的信号页签中自动连接,也可以在代码中手动连接。当敌人触碰玩家角色时,就调用玩家身上的take_damage方法。通过这种方式,你新增任何“可受伤体”时,只要给它加一个take_damage函数,敌人逻辑完全不用改。

把敌人场景拖入主场景,按 F5 运行。玩家靠近敌人时,敌人会切换成追踪;离远了,敌人会恢复巡逻。如果你发现敌人从平台边缘掉下去,多半是patrol()里的射线逻辑没有执行——检查一下射线query.exclude = [self],防止射线打到敌人自己的碰撞体。

7. 运行效果与验证路径

写完代码后,你需要有一套判断“是否成功”的验证方法,而不是只看画面“好像能动”。

第一层验证:按住右键,观察角色是否向右移动;按空格,角色是否跳跃,下落是否流畅。如果角色跳得很高或很飘,调低jump_velocity;如果角色贴地滑行,调高gravity或减小friction相关参数。

第二层验证:在地图不同位置测试。跑过整个关卡,检查角色是否会在某些瓦片边缘卡住,跳跃后是否能落在窄平台上。瓦片碰撞边缘卡人,是平台跳跃最常见的手感问题。遇到这种情况,可以把瓦片的碰撞多边形稍微内缩一点,或者在TileSet中设置瓦片的y_sort_origin和碰撞边距。

第三层验证:把敌人放在一个平台上,看它是否来回巡逻;把玩家放在detection_range内,看敌人是否会切换追踪。如果敌人只在原地抖动,检查direction是否有多次翻转。注意is_on_wall()只在move_and_slide()之后才更新,所以不要在同一帧内连续翻转两次方向。

第三层验证中,最推荐的方式是利用 Godot 的调试面板。按 F5 运行游戏后,按 F12 打开底部调试器,切到“远程场景树”,你可以实时看到场景中每个节点的属性。用这种方式检查敌人的velocity数值是否正常、检测范围是否覆盖到了玩家。很多“看不见的 bug”,在远程场景树里一眼就能看出来。

8. 常见问题与排查方法

我把新手在做这个项目时最常遇到的问题整理成一张表。遇到问题时,先按这个表逐项排查。

问题现象可能原因排查方式解决方案
角色一直往下掉,穿破地图瓦片没有物理碰撞层选中 TileSet,检查 Physics Layers 是否添加,瓦片上是否有碰撞多边形为地面瓦片添加 Physics Layer 和 Collision Polygon
角色跳跃后浮空、下不来重力没有生效检查_physics_process里是否调用了velocity.y += gravity * delta补充重力累加逻辑
角色左右滑冰、惯性太大没有水平减速或减速值太小观察松开按键后velocity.x是否归零用move_toward(velocity.x, 0, speed),调大减速速度
敌人只往一个方向走,不回头掉头逻辑没触发打印is_on_wall()和射线结果检查碰撞形状是否正常,射线exclude是否错误包含敌人自身
敌人追踪时速度极快或极慢chase_speed值不合理检查chase_speed导出值根据游戏设计调参,或者增加加速度
玩家和敌人重叠时没有伤害没有伤害判定区检查敌人是否有Area2D,是否连接了body_entered添加Area2D并连接信号
瓦片边缘卡住角色瓦片碰撞多边形和图形像素不完全贴合缩放查看瓦片物理形状在 TileSet 中微调碰撞多边形,或让平台边缘使用半瓦设计

这些问题的共同点是:它们大多不是代码逻辑错,而是资源、碰撞、信号连接这些“工程细节”错了。所以排查时不要一上来就盯着代码,先检查节点结构,再检查碰撞形状,最后看打印输出。

9. 最佳实践与工程建议

到这里,核心功能已经跑通了。但要把 demo 变成真正的项目,还需要建立几个工程习惯。

9.1 用分组管理玩家和敌人

在脚本中用get_tree().get_first_node_in_group("player")查找玩家,比硬编码场景引用更灵活。在 Godot 中,点击玩家节点,在检查器中设置Groups为player,之后任何脚本都能通过分组找到它。同理,敌人也可以统一放到enemies分组,方便批量处理。

9.2 把数值抽成导出变量

@export var speed: float = 300.0允许你在检查器中直接调整速度、重力、检测范围等参数,不必为了调手感反复修改代码。团队协作时,策划也可以直接在编辑器中调参,而不用碰代码。这是 Godot 工作流的重要体验。

9.3 用 Autoload 管理全局状态

当你在多个场景之间切换(比如从主菜单进入关卡),玩家血量、金币、关卡进度这些数据不能随着场景销毁而丢失。这时可以创建一个 Autoload 脚本(比如GameState.gd),在项目设置 -> Autoload 中注册它,然后你在任何脚本中都能用GameState访问全局变量。平台跳跃只是起点,这个模式在后续做存档、计分、设置界面时非常关键。

9.4 物理层与碰撞层分开

Godot 的物理引擎支持多个碰撞层和碰撞掩码。建议把玩家、敌人、地面、伤害区域分别放到不同的物理层。这样你可以在“伤害区域”上只响应玩家,而不响应敌人;在地面上只响应所有实体,而不响应装饰。刚开始也许不会出错,但场景复杂后,没有合理的物理层规划,碰撞组会互相干扰。

9.5 素材与代码分离

这一点很多新手会忽略。把瓦片素材、角色素材、音频素材分别放在assets目录下的子目录中。脚本放在scripts目录,场景放在scenes目录。文件命名全部用小写加下划线。项目规模变大后,你会感谢这个分类习惯。

9.6 使用版本控制

Godot 项目本质上就是文本文件加素材文件,非常适合用 Git 管理。在项目根目录初始化 Git,并注意把.godot/目录加入.gitignore,这个目录是编辑器缓存,不应该提交。版本控制不是为了“用高级工具”,而是为了让你频繁改动时能随时回到“能运行的状态”。

10. 总结与后续学习方向

现在你已经跑通了一个最小可玩的 Godot 平台跳跃项目:角色可以移动、跳跃、下落;地图是用 TileSet 和 TileMapLayer 搭建的,带物理碰撞;敌人拥有巡逻、转向、追踪和简单跳跃的有限状态机行为。这套东西放到任何一个 2D 平台跳跃项目里,都是最基础的底座。

接下来有三条进阶路线,按你自己的兴趣选:

第一条是深化动作系统。给玩家添加 Coyote Time(离开平台后短暂允许跳跃)、Jump Buffer(跳跃键预输入)、可变跳跃高度,这些细节会明显提升手感。手感优化是平台跳跃的核心竞争力,值得花时间研究。

第二条是扩展关卡玩法。在 TileMapLayer 上增加更多地形类型,比如移动平台、单向平台、可破坏砖块,结合信号机制制作机关和触发器。平台跳跃的关卡设计空间很大,你可以用本文的瓦片系统快速搭建多个关卡原型。

第三条是完善敌人 AI。给敌人增加视野锥、攻击状态、投掷物行为,或者引入导航系统做更复杂的巡逻路径。当你真正理解状态机之后,扩展新的敌人类型只需要增加一个状态分支,而不是推倒重写。

最后提醒一句:如果你在实现过程中遇到 TileSet 物理碰撞不起作用的问题,不要急着怀疑引擎。Godot 的瓦片碰撞设计得很稳定,绝大多数情况是漏了某个物理层设置。把今天的排查表收藏好,下次遇到类似问题直接对照。

现在,打开 Godot,把玩家角色先跑起来。剩下的事情,代码会告诉你下一步该做什么。建议收藏备用,也需要动手实践。

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

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

立即咨询