强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Godot 3 GDScript 教程 / 函数与信号

函数与信号

函数基础

函数是 GDScript 中组织代码的基本单元。每个函数都属于某个脚本(类),可以有参数和返回值。

函数定义

# 无参数,无返回值
func say_hello() -> void:
    print("Hello!")

# 有参数,无返回值
func greet(name: String) -> void:
    print("Hello, %s!" % name)

# 有参数,有返回值
func add(a: int, b: int) -> int:
    return a + b

# 多个参数
func create_enemy(type: String, pos: Vector2, level: int) -> void:
    print("创建 %s 敌人,位于 %s,等级 %d" % [type, pos, level])

# 返回复杂类型
func get_nearest_enemy(pos: Vector2) -> Node2D:
    var nearest: Node2D = null
    var min_dist: float = INF
    for enemy in get_tree().get_nodes_in_group("enemies"):
        var dist = pos.distance_to(enemy.position)
        if dist < min_dist:
            min_dist = dist
            nearest = enemy
    return nearest

void 返回值

# void 表示不返回任何值
func do_nothing() -> void:
    pass  # pass 是空语句

# 也可以省略 void(不推荐)
func implicit_void():
    print("没有类型注解")

⚠️ 注意:建议始终使用类型注解,有助于编辑器自动补全和错误检测。

参数详解

默认参数

# 默认参数必须在非默认参数之后
func spawn_bullet(pos: Vector2, speed: float = 500.0, damage: int = 10) -> void:
    print("子弹位置: %s, 速度: %f, 伤害: %d" % [pos, speed, damage])

# 调用时可以省略有默认值的参数
spawn_bullet(Vector2(100, 200))           # 使用全部默认值
spawn_bullet(Vector2(100, 200), 800.0)    # 自定义速度
spawn_bullet(Vector2(100, 200), 800.0, 25)  # 全部自定义

# 默认值可以是表达式
func create_effect(pos: Vector2, color: Color = Color.white, lifetime: float = 1.0) -> void:
    pass

参数传递方式

# 基本类型(int, float, bool, String)是值传递
func modify_value(x: int) -> void:
    x = 100  # 不影响调用者

var a = 10
modify_value(a)
print(a)  # 10(未改变)

# 对象类型(Node, Array, Dictionary)是引用传递
func modify_array(arr: Array) -> void:
    arr.append(4)  # 会影响调用者

var my_array = [1, 2, 3]
modify_array(my_array)
print(my_array)  # [1, 2, 3, 4](已改变)

⚠️ 注意:Array 和 Dictionary 是引用传递!如果不想修改原数据,需先 .duplicate()

可变参数模拟

GDScript 不直接支持可变参数,但可以用数组实现:

func calculate_sum(numbers: Array) -> float:
    var total: float = 0.0
    for n in numbers:
        total += n
    return total

# 调用
var result = calculate_sum([1, 2, 3, 4, 5])  # 15.0

参数展开(unpack)

# 使用 call() 动态调用函数
func attack(target: String, damage: int, element: String) -> void:
    print("%s%s 造成 %d%s 伤害" % ["Hero", target, damage, element])

var args = ["Slime", 25, "Fire"]
attack(args[0], args[1], args[2])  # 手动展开

# 使用 callv() 自动展开
attack.callv(args)  # 自动传入数组参数

静态函数(static)

extends Node

# 静态函数不依赖实例,不能访问成员变量
static func add(a: int, b: int) -> int:
    return a + b

static func clamp_value(value: float, min_val: float, max_val: float) -> float:
    if value < min_val:
        return min_val
    if value > max_val:
        return max_val
    return value

# 不能在静态函数中使用 self 或成员变量
static func broken() -> void:
    # print(self)  # ❌ 错误!静态函数中不能使用 self
    pass

工具类示例

# math_utils.gd
class_name MathUtils

static func lerp_angle(a: float, b: float, t: float) -> float:
    return a + (b - a) * t

static func vector_from_angle(angle: float) -> Vector2:
    return Vector2(cos(angle), sin(angle))

static func random_in_circle(radius: float) -> Vector2:
    var angle = randf() * TAU
    var r = sqrt(randf()) * radius
    return Vector2(cos(angle), sin(angle)) * r

使用:

var pos = MathUtils.random_in_circle(100.0)

信号(Signal)

信号是 Godot 的观察者模式实现,用于节点间松耦合通信。

定义信号

extends Node

# 基本信号定义
signal died
signal health_changed

# 带参数的信号
signal took_damage(amount: int)
signal healed(amount: int)
signal level_up(new_level: int)
signal position_changed(new_pos: Vector2)

发射信号

extends KinematicBody2D

signal died()
signal health_changed(current: int, maximum: int)

var health: int = 100
var max_health: int = 100

func take_damage(amount: int) -> void:
    health -= amount
    health = max(0, health)
    
    # 发射信号(emit_signal 或 emit)
    emit_signal("health_changed", health, max_health)
    
    if health <= 0:
        emit_signal("died")
        queue_free()

# Godot 3.5+ 简写
func heal(amount: int) -> void:
    health = min(health + amount, max_health)
    emit_signal("health_changed", health, max_health)

连接信号

方式一:代码连接(推荐)

extends Node

func _ready() -> void:
    # 连接信号到函数
    $Player.connect("health_changed", self, "_on_player_health_changed")
    $Player.connect("died", self, "_on_player_died")

func _on_player_health_changed(current: int, maximum: int) -> void:
    $HealthBar.value = current
    $HealthLabel.text = "%d / %d" % [current, maximum]

func _on_player_died() -> void:
    $GameOverScreen.show()
    get_tree().paused = true

方式二:编辑器连接

  1. 选中发射信号的节点
  2. 在 Inspector → Node 面板中选择信号
  3. 点击 “Connect” → 选择目标节点 → 选择方法
  4. 编辑器自动生成回调函数

方式三:Lambda(Godot 3.x 不支持)

⚠️ 注意:Godot 3.x 不支持 Lambda 连接信号,需在 Godot 4.x 中使用。

connect() 语法详解

# 完整语法
emitter.connect("signal_name", target_object, "method_name", [], flags)

# 示例
$Enemy.connect("died", self, "_on_enemy_died", [], CONNECT_ONESHOT)

# CONNECT_ONESHOT - 触发一次后自动断开
# CONNECT_DEFERRED - 延迟到空闲帧处理
# CONNECT_PERSIST - 持久化连接(编辑器用)

# 断开信号
$Enemy.disconnect("died", self, "_on_enemy_died")

# 检查信号是否已连接
if $Enemy.is_connected("died", self, "_on_enemy_died"):
    print("已连接")

# 检查对象是否有某信号
if $Enemy.has_signal("died"):
    print("有 died 信号")

信号参数传递

# 信号定义时声明参数类型
signal item_picked(item_name: String, quantity: int)
signal enemy_spawned(enemy: Node2D, pos: Vector2)

# 发射时传入参数
emit_signal("item_picked", "Health Potion", 3)
emit_signal("enemy_spawned", enemy_instance, Vector2(100, 200))

# 接收时参数要匹配
func _on_item_picked(item_name: String, quantity: int) -> void:
    print("拾取了 %d%s" % [quantity, item_name])

# 连接
$Item.connect("item_picked", self, "_on_item_picked")

⚠️ 注意:接收回调的参数数量必须和信号定义的参数数量一致,否则报错。

协程与 yield

Godot 3 中使用 yield 实现协程(异步等待)。

yield 基础

# 等待信号
func attack_sequence() -> void:
    print("蓄力中...")
    yield(get_tree().create_timer(1.0), "timeout")  # 等待1秒
    print("攻击!")
    yield(get_tree().create_timer(0.5), "timeout")
    print("后摇结束")

# 等待信号的一般形式
func wait_for_signal(emitter: Object, signal_name: String):
    return yield(emitter, signal_name)

yield 常用场景

# 等待时间
func flash_red() -> void:
    $Sprite.modulate = Color.red
    yield(get_tree().create_timer(0.2), "timeout")
    $Sprite.modulate = Color.white

# 等待动画播放完毕
func play_attack_anim() -> void:
    $AnimationPlayer.play("attack")
    yield($AnimationPlayer, "animation_finished")
    print("攻击动画完成")

# 等待输入
func wait_for_input() -> void:
    yield(self, "input_received")  # 自定义信号

# 等待帧结束
func _ready() -> void:
    yield(get_tree(), "idle_frame")  # 等待一帧(所有 _ready 完成后)
    print("所有节点已就绪")

yield 的注意事项

# ⚠️ yield 返回一个 GDScriptFunctionState 对象
# 如果持有该对象,可以稍后恢复协程
var state = some_coroutine()
# state.resume()  # 手动恢复

# 如果协程中的对象被销毁,协程会自动取消
# 可以用 is_valid() 检查
var state2 = some_coroutine()
if state2 is GDScriptFunctionState and state2.is_valid():
    state2.resume()

⚠️ 注意

  • yield 后的代码在恢复前不会执行
  • 如果节点被 queue_free(),挂起的协程会自动取消
  • 不要在 _process_physics_process 中使用 yield

自定义信号事件系统

事件总线模式(EventBus)

创建一个全局 Autoload 作为事件总线:

# res://scripts/EventBus.gd
extends Node

# 定义所有全局信号
signal player_damaged(amount: int, source: Node2D)
signal player_healed(amount: int)
signal player_died()
signal enemy_killed(enemy: Node2D, points: int)
signal item_collected(item_name: String)
signal level_changed(new_level: int)
signal boss_defeated(boss_name: String)
signal game_paused()
signal game_resumed()
signal score_changed(new_score: int)

Project Settings → Autoload 中添加 EventBus

使用方式:

# 玩家脚本
extends KinematicBody2D

func take_damage(amount: int, source: Node2D) -> void:
    health -= amount
    EventBus.emit_signal("player_damaged", amount, source)
    if health <= 0:
        EventBus.emit_signal("player_died")

# 敌人脚本
extends KinematicBody2D

func die() -> void:
    EventBus.emit_signal("enemy_killed", self, score_value)
    queue_free()

# UI 脚本
extends Control

func _ready() -> void:
    EventBus.connect("score_changed", self, "_on_score_changed")
    EventBus.connect("player_damaged", self, "_on_player_damaged")

func _on_score_changed(new_score: int) -> void:
    $ScoreLabel.text = "Score: %d" % new_score

func _on_player_damaged(amount: int, source: Node2D) -> void:
    $DamageFlash.play("flash")

信号与观察者模式

# 可观察属性(Observable Property)
extends Node

signal property_changed(property_name: String, old_value, new_value)

var _health: int = 100 setget set_health, get_health

func set_health(value: int) -> void:
    var old = _health
    _health = value
    emit_signal("property_changed", "health", old, _health)

func get_health() -> int:
    return _health

# 外部使用
$Player.health = 80  # 自动触发信号

高级信号技巧

一次性信号

# 只触发一次后自动断开连接
$Timer.connect("timeout", self, "_on_timer_timeout", [], CONNECT_ONESHOT)

func _on_timer_timeout() -> void:
    print("只执行一次")

延迟信号

# CONNECT_DEFERRED - 回调延迟到空闲帧执行
# 适合在物理处理中安全地修改场景树
$Area.connect("body_entered", self, "_on_body_entered", [], CONNECT_DEFERRED)

信号链

# 将信号转发给其他对象
func _ready() -> void:
    $Child.connect("some_signal", self, "_relay_signal")

signal relay(data)

func _relay_signal(data) -> void:
    emit_signal("relay", data)  # 转发信号

游戏开发场景

场景:Boss 战阶段系统

Boss 战中,Boss 在不同血量阶段切换行为模式,需要解耦各系统的响应。

# Boss.gd
extends KinematicBody2D

signal phase_changed(new_phase: int)
signal health_changed(current: int, maximum: int)
signal boss_defeated()

enum Phase { PHASE_1, PHASE_2, PHASE_3 }

var health: int = 500
var max_health: int = 500
var current_phase: int = Phase.PHASE_1

func take_damage(amount: int) -> void:
    health = max(0, health - amount)
    emit_signal("health_changed", health, max_health)
    
    # 阶段切换检查
    var health_percent = float(health) / max_health
    if health_percent <= 0.3 and current_phase != Phase.PHASE_3:
        set_phase(Phase.PHASE_3)
    elif health_percent <= 0.6 and current_phase == Phase.PHASE_1:
        set_phase(Phase.PHASE_2)
    
    if health <= 0:
        emit_signal("boss_defeated")
        queue_free()

func set_phase(new_phase: int) -> void:
    current_phase = new_phase
    emit_signal("phase_changed", new_phase)
    match new_phase:
        Phase.PHASE_1:
            $AnimationPlayer.play("idle")
        Phase.PHASE_2:
            $AnimationPlayer.play("enraged")
        Phase.PHASE_3:
            $AnimationPlayer.play("desperate")

# BossUI.gd
extends Control

func _ready() -> void:
    var boss = get_node("/root/Game/Boss")
    boss.connect("health_changed", self, "_on_health_changed")
    boss.connect("phase_changed", self, "_on_phase_changed")
    boss.connect("boss_defeated", self, "_on_boss_defeated")

func _on_health_changed(current: int, maximum: int) -> void:
    $HealthBar.value = float(current) / maximum * 100

func _on_phase_changed(new_phase: int) -> void:
    var names = ["Phase 1", "Phase 2", "Phase 3"]
    $PhaseLabel.text = names[new_phase]

func _on_boss_defeated() -> void:
    $VictoryScreen.show()

扩展阅读