大家好,我是你们的老朋友。之前一直在折腾 Godot 3.x 的 2D 项目,最近把目光转向了 Godot 4,开始尝试做一款带投射物机制的 2D 平台游戏。说实话,从 3.x 切到 4.x 确实有不少新东西要重新适应,比如 TileMapLayer 的引入、物理系统的调整以及动画播放器的变化。正好有朋友也在问 Godot 4 中投射物平台游戏应该怎么做,所以我就把这一段时间的踩坑和实操经验整理成一篇完整的教程。这篇文章适合有一定 Godot 基础、想从零开始做一个完整小游戏的开发者,也适合刚接触 Godot 4、想了解 TileMapLayer 和投射物系统的同学。读完你可以掌握角色移动、跳跃、TileMap 关卡搭建、投射物生成与碰撞检测,以及敌人和生命值系统的完整写法。
1. 背景与核心概念
1.1 为什么选择 Godot 4 做 2D 平台游戏
Godot 4 是 Godot 引擎的一次大版本升级,带来了全新的渲染器、物理系统、资源导入管线以及场景组织方式。对于 2D 平台游戏来说,Godot 4 的 2D 渲染更高效,光照和粒子效果也更强,同时新增了 TileMapLayer 节点,让瓦片地图的使用方式发生了明显变化。
在 Godot 3.x 中,TileMap 是单独一个节点,所有瓦片数据直接挂在同一个节点下;到 Godot 4 之后,官方把 TileMap 升级为 TileMapLayer,每个 TileMapLayer 专注于一层瓦片,这样可以让地面、装饰、碰撞层、道具层分开管理,结构更清晰,也更容易做分层渲染和动态修改。刚开始从 3.x 转过来的时候,很多人会在 TileMap 和 TileMapLayer 之间困惑,后面我们会专门讲解。
1.2 什么是投射物平台游戏
投射物平台游戏指的是玩家角色在 2D 平台场景中移动、跳跃,同时可以发射子弹、飞刀、能量球等投射物,用于击败敌人或触发机关。典型的例子包括经典的魂斗罗风格关卡,或是一些恶魔城like游戏中的远程攻击设定。
这种游戏的核心玩点通常集中在三方面:
- 角色移动手感:左右移动、跳跃、重力、空中控制。
- 投射物系统:发射方向、速度、生命周期、碰撞检测。
- 关卡与敌人:瓦片地图、敌人 AI、伤害判定、生命值系统。
本文会通过一个简单的“主角 + 发射子弹 + 瓦片关卡 + 敌人”的小项目,把这些系统完整串起来。用一个最简单的项目演示多个系统之间的协作,比单独拆开讲更有参考价值。
1.3 本文使用的技术栈
| 模块 | 技术方案 |
|---|---|
| 引擎 | Godot 4.x(以 Godot 4.2 为主,低版本略有差异) |
| 语言 | GDScript |
| 瓦片地图 | TileMapLayer 节点 |
| 场景组织 | Node2D / CharacterBody2D / Area2D |
| 动画 | AnimatedSprite2D 或 Sprite2D + 脚本控制 |
| 物理碰撞 | 碰撞形状、物理图层、碰撞掩码 |
版本说明:Godot 4 目前已经发布多个 4.x 小版本,例如 4.0、4.1、4.2、4.3。本文示例以较为稳定的 Godot 4.2 为主写代码,但大部分功能在 4.0 以上都能运行。如果你使用的是 4.3 或 4.4,接口基本兼容,直接照着写通常不会有大问题。如果某个属性名字报错,优先检查引擎版本和文档。
2. 环境准备与项目创建
2.1 安装 Godot 4
从 Godot 官网下载 Godot 4.x 标准版即可。Windows 用户可以直接下载 exe 文件,macOS 用户下载 dmg 或 zip 版,Linux 用户选择对应发行版。
下载完成后无需安装,直接把编辑器打开就行。Godot 4 项目与 Godot 3 项目不通用,所以不要用 3.x 版本直接打开 4.x 工程文件。
2.2 创建项目
打开 Godot 4,点击“新建项目”,填写项目名称(例如Godot4ProjectilePlatformer),选择项目路径,渲染器保持默认的 Forward+ 或 Mobile 都可以。2D 平台游戏对渲染要求不高,使用默认设置即可。
创建完成后得到如下结构:
Godot4ProjectilePlatformer/ ├── project.godot ├── icon.svg ├── scenes/ ├── scripts/ ├── assets/其中scenes放场景文件,scripts放 GDScript 脚本,assets放图片和音频。可以先手动创建这些文件夹,也可以在后续保存场景时自动创建。
2.3 准备素材
为了不涉及版权问题,本文不依赖外部图片素材,直接用 Godot 内置的图标和颜色节点代替。你可以用ColorRect做临时角色,或者用Sprite2D设置一张简单的 16x16 像素图片。
如果你自己有原始素材包,也可以把图片导入assets目录。本文重点不在资源导入,所以用简单色块来表示角色、敌人和投射物。
3. TileMapLayer 的使用详解
3.1 TileMap 与 TileMapLayer 的区别
刚接触 Godot 4 的朋友很容易掉进一个坑:在节点创建列表里找不到旧版的 TileMap 节点,只有 TileMapLayer。实际上这是 Godot 4 的重构结果。
在 Godot 3.x 中,TileMap 节点同时承担了瓦片存储、渲染、碰撞、坐标转换等功能。但当一个 TileMap 非常大、图层很多的时候,节点会变得臃肿,而且不方便动态控制某一层的显示和遮挡关系。
Godot 4 拆出了 TileMapLayer:
- 每个 TileMapLayer 只负责一层。
- 多个 TileMapLayer 可以控制不同的绘制顺序。
- 碰撞、渲染、脚本访问都拆分得更清晰。
如果你从 3.x 迁移项目,在 Godot 4 中新建 TileMapLayer 后,需要重新设置 TileSet,旧项目不能直接无缝迁移。
3.2 创建 TileSet 和 TileMapLayer
首先准备一张类似下图的地图图集。假设我们有一张 128x128 的瓦片图集,每个瓦片是 16x16。
在 Godot 4 编辑器中操作:
- 在文件系统中选中图片资源。
- 右键选择“新建 TileSet”。
- 在 TileSet 资源中设置 Tile Size 为 16x16。
- 添加图集,选择瓦片图片,设置切片大小。
- 在 TileSet 的“物理层”中添加一个物理层,并在需要的瓦片上绘制碰撞多边形。
如果没有现成图片,也可以直接在 TileSet 中用空瓦片创建几个方块地形。
创建完 TileSet 之后,新建一个 TileMapLayer 节点,把 TileSet 资源拖到节点的 TileSet 属性中,然后在编辑器中像画笔一样刷出地面、墙壁和平台地形。
这里要注意:Godot 4 的瓦片刷法比旧版更细腻,右下角可以选择不同 Tile,快捷键D切换绘制模式,E切换橡皮擦模式。默认情况下,刷出来的瓦片会同时使用你定义的物理层碰撞。
3.3 使用脚本操作 TileMapLayer
为了后面的游戏逻辑,除了手动刷地图,有时我们也需要通过代码动态修改地图,比如生成随机平台、破坏地形、清除障碍物。在 Godot 4 中,TileMapLayer 提供了set_cell和get_cell等接口。
# 文件路径:scripts/tilemap_helper.gd extends TileMapLayer # 在指定格子坐标放置瓦片 func place_tile(cell: Vector2i, tile_id: int): # 第一个参数是瓦片位置(格子坐标),第二个参数是图集ID,第三个是图集坐标 set_cell(cell, 0, Vector2i(1, 0)) # 清除指定格子 func clear_tile(cell: Vector2i): set_cell(cell, -1) # -1 表示清空这里需要注意,set_cell(cell: Vector2i, source_id: int, atlas_coords: Vector2i)是 Godot 4 TileMapLayer 的标准写法。如果你在旧教程中看到set_cellv或tilemap.set_cell(x, y, ...),那是 Godot 3 的用法,不能直接照搬。
另外,在场景中引用 TileMapLayer 时:
$TileMapLayer如果节点名字是TileMapLayer,直接在脚本里$TileMapLayer.place_tile(...)就能调用。
3.4 碰撞层与图层顺序
在 2D 平台游戏中,我们通常有三层瓦片:
- 背景层:只渲染不碰撞,例如远景、天空。
- 地面层:既渲染又有物理碰撞。
- 装饰层:只渲染,但绘制顺序在地面之上。
每一个 TileMapLayer 都有自己的z_index和y_sort_enabled属性。多层之间通过调整节点顺序来控制遮挡关系,节点在上面的会绘制在更前面。
碰撞方面,TileMapLayer 默认会把瓦片上的物理多边形加入刚体静态碰撞。玩家场景使用 CharacterBody2D,敌人和投射物用 Area2D 或 CharacterBody2D 时,需要确保碰撞层和掩码匹配,否则会出现“穿墙”或者“打不到敌人”的问题。这一点会在后面实战中细讲。
4. 主角移动与跳跃实现
4.1 创建玩家场景
在scenes目录下新建场景,根节点选择CharacterBody2D,命名为Player。
为 Player 添加以下子节点:
Sprite2D:显示角色外观。CollisionShape2D:添加一个矩形碰撞形状。RayCast2D:用于地面检测(可选,也可以直接用is_on_floor())。Muzzle:Marker2D,表示发弹点。
先给玩家一个简单脚本player.gd,实现水平移动和跳跃。
# 文件路径:scripts/player.gd extends CharacterBody2D @export var speed: float = 180.0 @export var jump_velocity: float = -300.0 @export var gravity: float = 600.0 func _physics_process(delta: float) -> void: # 获取输入方向,A/D 或 左/右 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 * 0.2) # 重力 if not is_on_floor(): velocity.y += gravity * delta # 跳跃 if Input.is_action_just_pressed("jump") and is_on_floor(): velocity.y = jump_velocity move_and_slide()这段代码是平台游戏最基础的移动模板。Input.get_axis("move_left", "move_right")会返回 -1 到 1 之间的值,用于判断方向。move_toward让速度在停止输入时平滑减到 0,避免滑冰感。
注意,gravity这里使用正数,叠加到velocity.y时角色会向下加速。jump_velocity为负数,表示向上跳。
访问函数说明:
is_on_floor()依赖move_and_slide()自动计算着地状态。- 如果跳跃失效,检查物理层和掩码,确保玩家碰撞层能检测到地面瓦片。
jump动作需要预先在输入映射中配置。
4.2 配置输入映射
点击编辑器顶部菜单“项目 -> 项目设置 -> 输入映射”,添加以下动作:
| 动作名称 | 按键1 | 按键2 |
|---|---|---|
| move_left | A | 左箭头 |
| move_right | D | 右箭头 |
| jump | 空格 | W |
添加完成后,脚本中的"move_left"、"move_right"、"jump"才能被正确识别。
4.3 地面检测与碰撞设置
Player 的CollisionShape2D可以是一个矩形,尺寸根据你的角色大小设置。注意碰撞层默认是第 1 层,掩码也默认是第 1 层。TileMapLayer 的瓦片碰撞默认在第 1 层。
所以多数情况下默认值就能让角色站在瓦片上。如果在运行后角色直接掉穿地面,请检查:
- TileSet 中是否给瓦片画了物理多边形。
- TileMapLayer 是否开启了物理层。
- 播放场景后,在调试菜单中打开“可见碰撞形状”查看碰撞多边形。
4.4 角色动画翻转
如果没有动画素材,可以用简单的左右翻转来模拟朝向:
# 在 _physics_process 里补充 if direction != 0: $Sprite2D.flip_h = direction < 0如果使用AnimatedSprite2D,翻转逻辑相同。
5. 投射物系统设计
投射物平台游戏的核心之一就是“打子弹”。这里我们用 Area2D 做子弹,因为它可以检测进入碰撞区域的物体,不会像 CharacterBody2D 那样受物理移动限制。
5.1 投射物场景
新建场景,根节点为Area2D,命名为Bullet。
添加子节点:
Sprite2D:显示子弹外观。CollisionShape2D:圆形或矩形碰撞区域。VisibleOnScreenNotifier2D:用于飞出屏幕后自动删除。
脚本如下:
# 文件路径:scripts/bullet.gd extends Area2D @export var speed: int = 500 var direction := Vector2.RIGHT func _ready() -> void: # 连接信号 body_entered.connect(_on_body_entered) area_entered.connect(_on_area_entered) func setup(dir: Vector2) -> void: direction = dir.normalized() func _physics_process(delta: float) -> void: # 子弹移动方式一:直接使用全局位移 position += direction * speed * delta func _on_body_entered(body: Node2D) -> void: # 碰到玩家或墙壁时,删除子弹 if body is Player: return queue_free() func _on_area_entered(area: Area2D) -> void: # 碰到敌人伤害区域时,让敌人扣血并删除子弹 if area.has_method("take_damage"): area.take_damage(1) queue_free()需要注意的点:
body_entered处理子弹碰到 CharacterBody2D 或其他碰撞体的场景。area_entered处理子弹碰到 Area2D 敌人的场景。queue_free()会延迟到帧结束时删除节点,比直接free()更安全。setup(dir)用于初始化方向,不要在_ready里直接写死。
另一种移动方式是使用move_and_collide,但 Area2D 没有内建移动方法,需要手动操作position或使用PhysicsServer2D。对大多数 2D 平台游戏来说,直接改position简单也够用。如果你想更精确,可以继承RigidBody2D,但那种方案后续可控性会差一些。
5.2 玩家发射逻辑
回到player.gd,添加发射逻辑:
# 在 Player 场景中添加发射点 Marker2D,命名为 Muzzle # 在 player.gd 中补充 @onready var muzzle := $Muzzle @export var bullet_scene: PackedScene var facing_direction := Vector2.RIGHT func _physics_process(delta: float) -> void: # ...之前的移动代码... # 更新朝向 if direction != 0: facing_direction = Vector2.RIGHT if direction > 0 else Vector2.LEFT # 发射 if Input.is_action_just_pressed("shoot"): shoot() func shoot() -> void: if bullet_scene == null: return var bullet := bullet_scene.instantiate() get_parent().add_child(bullet) bullet.global_position = muzzle.global_position bullet.setup(facing_direction)这里有个重要的做法:把子弹实例加到玩家的父节点,而不是加到玩家节点下。因为如果子弹作为玩家的子节点,它会跟随玩家的位移和旋转,子弹的坐标系会变得混乱。放到同一个父节点下,子弹就在世界坐标系中独立运动。
输入映射中还需要添加自定义动作shoot,例如按键J或鼠标左键。
5.3 子弹碰到墙壁的处理
如果地图瓦片有碰撞,子弹作为 Area2D,默认并不会被墙壁挡住,因为它没有物理体。所以我们需要在子弹的脚本中额外检测是否碰到了静态瓦片。
简单方案是使用PhysicsRayQuery2D做检测,但更直观的是在子弹内部加一个RayCast2D指向运动方向,并且设置collision_mask为瓦片层。
还有一种常见做法:利用body_entered信号。当子弹是 Area2D,瓦片是静态碰撞体时,body_entered是可以触发的,但前提是瓦片所在的物体确实作为“body”存在。TileMapLayer 的碰撞会被引擎作为不可见静态体处理,通常可以触发 body_entered。
如果你发现子弹穿墙,可以按以下顺序排查:
- 检查子弹的
CollisionShape2D是否覆盖了当前位置。 - 检查子弹的
monitoring是否开启。 - 检查子弹的碰撞掩码是否包含地面所在的物理层。
- 检查瓦片物理层是否只对特定层生效。
为了更稳定,我会推荐在子弹脚本中增加一个简单的射线检测:
# 文件路径:scripts/bullet.gd 添加射线检测 @onready var ray: RayCast2D = $RayCast2D func _ready() -> void: ray.target_position = direction * 16 ray.force_raycast_update() func _physics_process(delta: float) -> void: position += direction * speed * delta ray.target_position = direction * 16 ray.force_raycast_update() if ray.is_colliding(): queue_free()这种方式比较直接,但要注意RayCast2D的target_position是相对自身节点的局部坐标,所以方向向量要乘以一个长度,这个长度决定检测距离。如果子弹速度很快,建议把检测距离设置得比你单帧移动距离更大,不然可能漏检测。更好的方案是使用move_and_collide做扫掠检测,但本文先不过度复杂化。
6. 敌人与伤害系统
6.1 创建敌人场景
敌人同样可以使用CharacterBody2D或者Area2D。这里用一个简单的CharacterBody2D敌人,它能左右巡逻,碰到玩家会让玩家扣血。
新建场景,根节点CharacterBody2D,命名Enemy。
添加子节点:
Sprite2DCollisionShape2DTimer(用于反向巡逻)
敌人脚本:
# 文件路径:scripts/enemy.gd extends CharacterBody2D @export var speed: float = 40.0 var direction := Vector2.RIGHT func _physics_process(delta: float) -> void: velocity = direction * speed move_and_slide() # 碰到墙壁反向 if is_on_wall(): direction *= -1 func take_damage(_amount: int) -> void: queue_free()这里用is_on_wall()判断是否撞墙。如果敌人卡在边缘不掉落,可以加一个 RayCast2D 向下检测,但简单演示就先用撞墙反向。
6.2 玩家受击逻辑
在player.gd中添加take_damage方法:
@export var max_health: int = 3 var health: int func _ready() -> void: health = max_health func take_damage(amount: int) -> void: health -= amount print("玩家生命值:", health) if health <= 0: # 死亡处理 queue_free()然后连接敌人与玩家的碰撞。在敌人脚本中:
func _on_body_entered(body: Node2D) -> void: if body.has_method("take_damage"): body.take_damage(1)但这里有个细节:CharacterBody2D的body_entered信号需要通过move_and_slide()触发,所以敌人场景里需要连接信号。你可以在_ready()中连接:
func _ready() -> void: body_entered.connect(_on_body_entered)如果你的敌人也是 Area2D,可以用area_entered或body_entered处理。
6.3 子弹与敌人的伤害连接
在bullet.gd中处理敌人伤害时,需要保证敌人确实有take_damage方法。如果你用的敌人脚本是enemy.gd,并且继承CharacterBody2D,那么子弹的area_entered无法触发,因为敌人不是 Area2D。
所以更通用的做法是在子弹中同时连接body_entered和area_entered,并统一判断:
# 文件路径:scripts/bullet.gd func _physics_process(delta: float) -> void: position += direction * speed * delta func _on_body_entered(body: Node2D) -> void: if body.has_method("take_damage"): body.take_damage(1) queue_free() func _on_area_entered(area: Area2D) -> void: if area.has_method("take_damage"): area.take_damage(1) queue_free()这里需要注意的是,玩家也属于CharacterBody2D,如果玩家也有take_damage方法,子弹打到玩家自己身上就会误伤。所以需要在碰撞检测中排除玩家:
if body != owner and body is Player: return更好的做法是设置子弹的collision_mask,不让子弹检测玩家所在的层,或者通过碰撞层区分。工程上推荐使用碰撞层来避免误伤,但演示项目里也可以直接通过节点类型判断。
6.4 生命值 UI
为了能看到伤害效果,可以用一个简单的Label显示生命值。
新建场景HUD,添加Label节点,然后在主场景中实例化 HUD。
HUD 脚本:
# 文件路径:scripts/hud.gd extends CanvasLayer @onready var health_label := $HealthLabel func update_health(value: int) -> void: health_label.text = "生命值: %d" % value玩家在受伤时调用 HUD 的更新方法即可:
# 在主场景中获取 HUD 实例 @onready var hud := $HUD func take_damage(amount: int) -> void: health -= amount hud.update_health(health)7. 主场景整合与运行
7.1 主场景结构
在主场景Main.tscn中组织以下节点:
Main (Node2D) ├── Background (TileMapLayer) ├── Ground (TileMapLayer) ├── Player (CharacterBody2D) ├── Enemy (CharacterBody2D) ├── BulletContainer (Node2D) └── HUD (CanvasLayer)将Player场景实例拖入Player节点,Enemy场景实例拖入Enemy节点。BulletContainer 是一个普通 Node2D,用于存放所有子弹实例,方便统一管理。
在player.gd中设置bullet_scene为子弹场景资源,发射时把子弹加到BulletContainer下而不是Player下。
修改后的发射代码:
@onready var bullet_container: Node2D = get_node("/root/Main/BulletContainer")如果场景路径固定,可以直接这样写。更稳妥的方式是导出一个节点引用,然后在编辑器中拖入:
@export var bullet_container: Node2D7.2 运行前检查清单
在按 F5 运行前,逐项检查:
- 输入映射是否添加了
move_left、move_right、jump、shoot。 - 玩家场景中是否有
CollisionShape2D。 - 子弹场景中
CollisionShape2D的形状尺寸是否合理。 - TileMapLayer 的 TileSet 是否包含物理碰撞信息。
- 主场景是否设置了当前场景。
- 子弹脚本是否连接了信号,或直接在
_ready中连接。
很多新手在运行时发现“按下按键没反应”,大概率是输入映射没配置好。如果发现“角色可以走动但不能发射”,多半是bullet_scene没有赋值。
7.3 预期效果
运行后,你应该能实现:
- 左右移动和跳跃。
- 按自定义射击键发射子弹。
- 子弹在碰到敌人后消失,敌人扣血并消失。
- 子弹碰到墙壁后消失。
- 敌人碰撞玩家后,玩家扣血。
- 生命值 UI 实时刷新。
如果出现子弹没有碰到敌人就穿过去,优先检查子弹和敌人的碰撞层是否在同一层。Godot 的物理层默认只有第一层启用,如果你给敌人手动设置了第 2 层,子弹的掩码也要包含第 2 层。这一点是很多平台游戏“打不到怪”的常见原因。
8. 常见问题与排查思路
8.1 角色无法移动
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 按方向键无反应 | 输入映射未配置 | 检查“项目设置 -> 输入映射” |
| 角色移动非常慢 | speed 太小或 delta 处理错误 | 调整 speed 数值 |
| 角色一直朝一个方向走 | 输入映射冲突 | 检查按键是否重复绑定 |
8.2 跳跃失效或无法落地
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 无法跳跃 | is_on_floor()一直为 false | 检查碰撞层、瓦片物理多边形 |
| 跳跃后无法控制 | 空中没有水平摩擦 | 使用move_toward或允许空中控制 |
| 角色穿地 | TileSet 未设置物理层 | 打开 TileSet 添加物理多边形 |
8.3 子弹未生效
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 无法发射 | bullet_scene为空 | 在导出变量中指定子弹场景 |
| 子弹发射但没移动 | direction未初始化 | 调用setup(dir)设置方向 |
| 子弹穿墙 | 射线检测距离太短 | 增加RayCast2D或使用扫掠检测 |
| 子弹打不中敌人 | 碰撞层不匹配 | 检查子弹和敌人的碰撞层掩码 |
8.4 敌人无法碰撞玩家
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 玩家穿过敌人 | 敌人没有碰撞形状 | 给敌人添加 CollisionShape2D |
| 敌人不触发伤害 | body_entered未连接 | 在_ready中连接信号 |
| 敌人撞到玩家不掉血 | 玩家没有take_damage方法 | 给玩家脚本添加该方法 |
8.5 运行时性能问题
如果地图较大,子弹数量多,建议限制同时存在的子弹数量。增加一个最大子弹数判断:
const MAX_BULLETS := 20 func shoot() -> void: if bullet_container.get_child_count() >= MAX_BULLETS: return这样可以避免短时间内生成大量 Area2D 节点导致卡顿。
9. 最佳实践与工程建议
9.1 物理图层规划
建议从一开始就规划好物理层,不要所有物体都放在第 1 层。一个清晰的规划可以避免很多误伤和穿透问题。
例如可以这样分配:
| 物理层 | 内容 | 用途 |
|---|---|---|
| 1 | 地面/墙壁 | 静态碰撞 |
| 2 | 玩家 | 角色碰撞 |
| 3 | 敌人 | 敌人碰撞 |
| 4 | 子弹 | 投射物碰撞 |
然后在每个节点的Collision属性中设置对应 layer 和 mask。这个方法在大项目中尤其重要,可以显著减少“子弹打到自己”“敌人重叠”之类的问题。
9.2 场景与脚本分离
不要把全部代码堆到player.gd或main.gd中。建议:
- 每个实体独立场景:玩家、敌人、子弹、HUD。
- 每个场景有自己的脚本,职责单一。
- 跨场景通信通过信号或直接引用有限度使用。
这样后续扩展武器系统、Boss 战、道具掉落时,不需要改动原有基础代码。
9.3 使用信号解耦
在player.gd中可以定义信号,让 HUD 或其他系统响应,而不需要玩家直接持有 HUD 引用:
signal health_changed(current: int) func take_damage(amount: int) -> void: health -= amount health_changed.emit(health)然后在主场景中把 HUD 绑定到信号上:
$Player.health_changed.connect($HUD.update_health)这种设计更符合 Godot 场景树解耦的思想,也更容易维护。
9.4 关于跳跃手感
平台游戏中跳跃手感非常影响体验。调整几个参数:
jump_velocity:负数绝对值越大跳得越高。gravity:越大下落越快,跳跃更“重”。- 空中水平控制:可以添加
air_control系数,让空中转向更灵活或更迟钝。 - 跳跃缓冲:在按下跳跃后的一小段时间内,即使角色还没落地也预存跳跃动作,手感会更好。
这里给出一个简单的跳跃缓冲示例:
var jump_buffer_time := 0.0 func _physics_process(delta: float) -> void: if Input.is_action_just_pressed("jump"): jump_buffer_time = 0.1 else: jump_buffer_time -= delta if is_on_floor() and jump_buffer_time > 0.0: velocity.y = jump_velocity jump_buffer_time = 0.09.5 代码注释与命名
Godot 的 GDScript 不需要强制类型标注,但推荐给关键节点和方法添加注释。项目越往后越大,注释和命名清晰能节省大量调试时间。例如:
# 朝朝向方向发射子弹 func shoot() -> void:9.6 版本控制与备份
使用 Git 管理项目,.gitignore建议忽略.godot/文件夹。这个目录是编辑器本地缓存,提交到仓库容易造成冲突。Godot 4 项目中,project.godot是核心配置文件,需要纳入版本控制。
10. 总结与学习路线
本文围绕 Godot 4 的投射物平台游戏,从 TileMapLayer 的基本用法开始,完整实现了角色移动、跳跃、投射物发射、敌人碰撞、生命值 UI 等核心系统。你可以通过这个小项目掌握:
- Godot 4 的 TileMapLayer 与 TileSet 配置方式。
- CharacterBody2D 的移动与物理交互。
- Area2D 投射物的生成、移动、碰撞检测。
- 信号在跨场景通信中的使用。
- 物理层规划与常见性能优化思路。
如果想继续深入,可以按下面的路线学习:
- 添加更多武器类型:霰弹、三连发、蓄力攻击。
- 增加敌人 AI:巡逻、追击、攻击动画。
- 制作 BOSS 战:攻击模式、状态机、伤害阶段。
- 引入动画播放器和动画树,让角色动作更丰富。
- 添加音效、粒子和屏幕震动,提升打击感。
对于实际项目来说,最重要的是先保证核心循环稳定,也就是“移动、跳跃、射击、反馈”这套基础体验没问题,再去扩展内容。否则在复杂的敌人 AI 和多层地图下,排错会指数级增加。
如果你在跟着本文实践时遇到问题,建议打开 Godot 编辑器的“调试 -> 可见碰撞形状”,先确认所有碰撞体都在正确位置。这是一个被低估但极其有效的调试技巧。希望这篇 Godot 4 投射物平台游戏实战笔记能帮到你,也欢迎在评论区交流你遇到的坑,我会根据反馈继续整理更多 Godot 4 实战内容。