更新记录
- 2026-08-05:开始跟练 Metroid
- 2026-08-06:实现如下功能
- 角色水平移动
- 加入跳跃和重力
- 添加可调节的跳跃力度和重力
- 添加射击以及冷却功能
- 实现固定方向射击
- 实现发射方向跟随鼠标移动变化,标准化向量
- 实现子弹生成
- 设置子弹初始位置
- 实现子弹移动
资源管理
-
文件名和节点的命名
通常建议:
- 文件名:
lower_snake_case - 节点名:
PascalCase - 脚本变量:
snake_case - 类名:
PascalCase
- 文件名:
.tscn和实例化
把 player.tscn 实例化到关卡后,关卡里保存的是对玩家场景的引用。把脚本挂在 player.tscn的根节点 Player 上,那么这个场景实例化出来的所有玩家,默认都会运行同一个脚本。
但是,每个实例的运行状态是独立的。虽然它们共用同一份脚本代码,但不是共用同一个对象。
一般会把“所有玩家都应该具备的功能”写进 player.tscn 的脚本,把“这个玩家单独的参数”在关卡实例上调整。
Vector2
二维数据内置类型。
表示速度时。
velocity = Vector2(100, 0)
表示每秒向右移动一定距离。
更常见的写法是:
velocity = direction * speed
例如:
var direction = Vector2.RIGHT
var speed = 200.0
velocity = direction * speed
结果就是:
velocity = Vector2(200, 0)
也就是向右移动,速度为 200。
为什么由 Level 创建子弹
玩家按下射击键后,子弹需要被添加到游戏场景中。
如果把子弹添加为 Player 的子节点:
Level
└── Player
└── Bullet
子弹仍然属于 Player,可能受到玩家移动、旋转或删除的影响。
更合理的结构是:
Level
├── Player
└── Bullet
子弹和玩家都属于 Level。这样子弹射出后可以在关卡中独立移动,不会继续跟随玩家。
各节点的职责
Player:检测玩家是否按下射击键。Level:接收射击请求,并把子弹添加到关卡中。Bullet:负责自己的移动、碰撞和销毁。
射击流程
玩家按下射击键
↓
Player 发出射击请求
↓
Level 收到请求
↓
Level 创建 Bullet
↓
Bullet 在关卡中独立移动
为什么要在 Level 上挂脚本
因为 Level 是 Player 和 Bullet 的共同父节点,它适合负责创建和管理关卡中的物体。
可以简单理解为:
Player 负责扣动扳机,Level 负责容纳射出去的子弹。
这不是唯一的实现方法,但可以避免子弹成为 Player 的子节点,也方便以后统一管理玩家子弹、敌人子弹和其他关卡物体。
信号
Godot 的“信号”可以理解为一种节点之间发送通知的机制。
某个节点发生了一件事,它不直接命令其他节点做什么,而是发出一个信号。对这个信号感兴趣的节点,通过“连接”接收通知并执行对应函数。
一个完整的信号过程包括:
声明信号 → 连接信号 → 发出信号 → 接收并处理
signal = 定义一个通知 emit = 发出通知 connect = 指定谁接收通知 回调函数 = 收到通知后执行的代码
在射击功能中,只需要这样理解:
Player 按下射击键
→ 发出 shoot 信号
→ Level 收到信号
→ Level 创建子弹
信号可以连接多个接收者
一个信号不只能连接一个函数。
例如 Player 射击时:
Player.shoot
├── Level 创建子弹
├── AudioManager 播放声音
├── Camera 产生震动
└── UI 减少弹药数量
Player 只需要发出一次:
shoot.emit(pos, dir)
多个接收者都可以收到通知。
这也是信号的重要作用:发送者不需要知道有多少接收者。
子弹位置和鼠标方向的传递路径
1. Player 定义射击信号
signal shoot(pos: Vector2, dir: Vector2)
shoot 信号携带两个数据:
pos:子弹的生成位置dir:子弹的飞行方向
2. Player 获取鼠标方向并发出信号
if Input.is_action_just_pressed("shoot") and $ReloadTimer.time_left == 0:
# 获取并标准化鼠标方向,保证子弹速度固定
shoot.emit(position, get_local_mouse_position().normalized())
$ReloadTimer.start()
Player 发出的两个值是:
position → Player 当前的位置
get_local_mouse_position().normalized() → Player 指向鼠标的方向
get_local_mouse_position() 获取鼠标相对于 Player 的位置向量。
normalized() 将向量长度变成 1,只保留方向,避免鼠标距离影响子弹速度。
例如:
鼠标在右边 → 方向接近 (1, 0)
鼠标在左边 → 方向接近 (-1, 0)
鼠标在右上 → 方向接近 (0.7, -0.7)
ReloadTimer 用于控制射击间隔,冷却没有结束时不能再次射击。
3. Level 接收信号
func _on_player_shoot(pos: Vector2, dir: Vector2) -> void:
var bullet = bullet_scene.instantiate() as Area2D
$Bullets.add_child(bullet)
bullet.setup(pos, dir)
数据按照参数顺序传递:
Player.position
→ Level.pos
Player 获取到的鼠标方向
→ Level.dir
Level 根据 bullet.tscn 创建一颗子弹,然后把位置和方向继续传给 Bullet。
4. Bullet 接收位置和方向
var direction: Vector2
func setup(pos: Vector2, dir: Vector2):
position = pos + dir * 16
direction = dir
这里:
position = pos + dir * 16:让子弹在玩家前方 16 像素处生成。direction = dir:保存鼠标方向,供移动代码使用。
5. Bullet 按照方向移动
func _physics_process(delta: float) -> void:
position += direction * 30 * delta
子弹每个物理帧都会沿着 direction 移动。
因为方向已经使用了 normalized(),所以鼠标离玩家的距离不会改变子弹速度。
完整传递路径
Player 获取自己的位置
↓
Player 获取并标准化鼠标方向
↓
shoot.emit(position, mouse_direction)
↓
Level._on_player_shoot(pos, dir)
↓
Level 创建 Bullet
↓
bullet.setup(pos, dir)
↓
Bullet 设置初始位置并保存方向
↓
Bullet 每帧沿鼠标方向移动
虽然 Player、Level 和 Bullet 中都出现了 pos 和 dir,但它们是不同函数中的局部参数。
它们通过参数顺序传递同一组数据:
Player 的第一个参数 → Level 的第一个参数 → Bullet 的第一个参数
Player 的第二个参数 → Level 的第二个参数 → Bullet 的第二个参数