news 2026/10/1 15:34:55

Godot 4 投射物平台游戏教程:TileMapLayer与子弹系统实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Godot 4 投射物平台游戏教程:TileMapLayer与子弹系统实战

大家好,我是你们的老朋友。之前一直在折腾 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 编辑器中操作:

  1. 在文件系统中选中图片资源。
  2. 右键选择“新建 TileSet”。
  3. 在 TileSet 资源中设置 Tile Size 为 16x16。
  4. 添加图集,选择瓦片图片,设置切片大小。
  5. 在 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_leftA左箭头
move_rightD右箭头
jump空格W

添加完成后,脚本中的"move_left"、"move_right"、"jump"才能被正确识别。

4.3 地面检测与碰撞设置

Player 的CollisionShape2D可以是一个矩形,尺寸根据你的角色大小设置。注意碰撞层默认是第 1 层,掩码也默认是第 1 层。TileMapLayer 的瓦片碰撞默认在第 1 层。

所以多数情况下默认值就能让角色站在瓦片上。如果在运行后角色直接掉穿地面,请检查:

  1. TileSet 中是否给瓦片画了物理多边形。
  2. TileMapLayer 是否开启了物理层。
  3. 播放场景后,在调试菜单中打开“可见碰撞形状”查看碰撞多边形。

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。

添加子节点:

  • Sprite2D
  • CollisionShape2D
  • Timer(用于反向巡逻)

敌人脚本:

# 文件路径: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: Node2D

7.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.0

9.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 投射物的生成、移动、碰撞检测。
  • 信号在跨场景通信中的使用。
  • 物理层规划与常见性能优化思路。

如果想继续深入,可以按下面的路线学习:

  1. 添加更多武器类型:霰弹、三连发、蓄力攻击。
  2. 增加敌人 AI:巡逻、追击、攻击动画。
  3. 制作 BOSS 战:攻击模式、状态机、伤害阶段。
  4. 引入动画播放器和动画树,让角色动作更丰富。
  5. 添加音效、粒子和屏幕震动,提升打击感。

对于实际项目来说,最重要的是先保证核心循环稳定,也就是“移动、跳跃、射击、反馈”这套基础体验没问题,再去扩展内容。否则在复杂的敌人 AI 和多层地图下,排错会指数级增加。

如果你在跟着本文实践时遇到问题,建议打开 Godot 编辑器的“调试 -> 可见碰撞形状”,先确认所有碰撞体都在正确位置。这是一个被低估但极其有效的调试技巧。希望这篇 Godot 4 投射物平台游戏实战笔记能帮到你,也欢迎在评论区交流你遇到的坑,我会根据反馈继续整理更多 Godot 4 实战内容。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 15:34:36

AI搜索推荐逻辑是什么?深度拆解生成式引擎的答案生成机制

AI搜索推荐逻辑是什么&#xff1f;深度拆解生成式引擎的答案生成机制AI搜索推荐逻辑的本质&#xff0c;是大语言模型基于语义理解、信源权重与实时检索三重机制&#xff0c;从海量内容中筛选、整合并生成一段"最像标准答案"的回复。与传统搜索引擎返回10个蓝色链接不…

作者头像 李华
网站建设 2026/10/1 15:34:35

程序到了Linux才出错?用CLion把调试接到目标环境

一段C程序&#xff0c;在开发电脑上运行正常&#xff0c;部署到Linux服务器后却读取不到配置、加载不了依赖库&#xff0c;甚至突然退出。查看日志、修改代码、编译上传&#xff0c;反复操作后&#xff0c;仍可能无法确定问题出在代码还是环境。对于Linux服务、设备端程序和工业…

作者头像 李华
网站建设 2026/10/1 15:33:24

马德拉群岛旅行全攻略:徒步、美酒与大西洋秘境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 15:32:45

【Unity UGUI源码深度解析】 19|输入模块源码解析:鼠标、触摸与导航如何转换为UI事件

《UGUI源码深度解析》第 19 篇 界面小组工作日志 基准:Unity 2022.3.62f2c1 / 本地 UGUI 1.0.0,跟踪传统 StandaloneInputModule。 人物与项目情节为虚构;源码机制以本地实现为准。 一、按下背包格,抬起时已经在外面 阿澈准备实现拖动装备,却遇到一个设计问题:“鼠标按…

作者头像 李华
网站建设 2026/10/1 15:32:03

锗废料回收实战:红外锗片、光纤锗、锗切屑怎么检测和计价

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注…

作者头像 李华
网站建设 2026/10/1 15:31:54

全能型 AI论文工具梯队划分(2026 精选)

基于综合性能、学术适配度、用户口碑和功能完整性&#xff0c;以下是当前主流 AI 论文写作工具的权威排名&#xff0c;按综合推荐指数从高到低排列&#xff0c;并标注核心优势与适用场景。&#x1f3c6; 第一梯队&#xff1a;全流程学术解决方案&#xff08;★★★★★&#xf…

作者头像 李华