函数是写代码时第一个需要认真掌握的"组织工具"。把一段逻辑封装成函数,以后就能反复调用,不用复制粘贴。GDScript 的函数语法比 C# 简洁很多,没有访问修饰符,类型也可以省略,但提供了足够的灵活性。这篇笔记把我自己学习函数时的经验和踩过的坑整理出来,尽量详细一点。
一、函数的基本定义
GDScript 里定义函数用 func 关键字,后面跟函数名和括号,参数放在括号里,最后用冒号结束,函数体缩进。
gdscript
func 函数名(参数1, 参数2):
# 函数体
return 返回值 # 可选
最简单的函数:
gdscript
func say_hello():
print("你好")
调用的时候直接写函数名加括号:
gdscript
say_hello()
命名习惯
我个人习惯函数名用蛇形命名法(snake_case),也就是所有小写字母,单词之间用下划线连接,比如 get_player_health()、spawn_enemy()。Godot 官方示例也是这个风格,保持统一就好。函数名最好以动词开头,让调用者一眼知道它是干什么的。
二、参数与返回值
函数可以接收参数,也可以返回结果。参数和返回值都可以不写类型,但强烈建议写上(后面细说)。
参数
参数写在括号里,多个参数用逗号分隔:
gdscript
func greet(name):
print("你好," + name)
greet("Tom")
如果参数没有类型注解,你可以传任何类型的值进去,GDScript 会动态处理。但这样容易在运行时出错,因为你可能传了一个数字给一个期望字符串的函数。
返回值
用 return 返回结果。函数可以没有返回值,此时可以省略 return,或者写 return 不带任何值。如果函数中途需要提前退出,也可以直接用 return。
gdscript
func add(a, b):
return a + b
var result = add(3, 5) # 8
三、类型注解
GDScript 4 支持给函数的参数和返回值加上类型注解,写法是参数名后加冒号和类型,函数末尾用 -> 类型 指定返回类型。
gdscript
func add(a: int, b: int) -> int:
return a + b
如果不返回值,用 -> void:
gdscript
func log_message(msg: String) -> void:
print(msg)
为什么建议写类型注解?
- 编辑器补全更智能 :类型明确后,当你写
a.时编辑器能列出int类型的方法和属性,不至于瞎猜。 - 提前发现错误:如果你在函数里返回了错误类型,或者调用时传了错误类型,编辑器会在保存时就报错,而不是等运行到那一行才崩。
- 性能更好:静态类型在 GDScript 中运行更快,因为引擎不需要在运行时反复检查类型。
但也要注意,类型注解不是万能的。有些复杂类型(比如返回一个节点引用)写起来比较长,如果嫌麻烦,关键的地方写注解就够了。
四、默认参数值
GDScript 支持给参数设置默认值。调用时如果不传这个参数,就使用默认值。这个特性很常用,可以减少函数重载的需求。
gdscript
func spawn_enemy(type: String = "normal", count: int = 1):
for i in range(count):
print("生成敌人:" + type)
spawn_enemy() # 两个参数都用默认值
spawn_enemy("elite") # type 为 "elite",count 默认 1
spawn_enemy("boss", 1) # 都指定
注意:带默认值的参数必须放在参数列表的最后面。下面的写法会直接报错:
gdscript
func bad_func(a = 1, b): # 错误:带默认值的参数不能在前面
pass
五、可变参数
有时候你不确定函数会接收多少个参数,比如写一个求和函数,想传任意个数字。GDScript 支持可变参数,在参数名前加 ...,函数内部这些参数会被打包成一个数组。
gdscript
func sum_all(...numbers: int) -> int:
var total = 0
for n in numbers:
total += n
return total
print(sum_all(1, 2, 3)) # 6
print(sum_all(10, 20)) # 30
print(sum_all()) # 0,一个参数都没传时数组为空
注意:
- 可变参数必须放在参数列表最后。
- 一个函数只能有一个可变参数。
- 类型注解写在
...参数名后面,表示每个参数都应该是该类型。如果省略类型注解,可以传任意类型。
六、局部变量与作用域
函数内部定义的变量是局部变量,只在函数内部有效,函数执行完就销毁。外面访问不到。
gdscript
func my_func():
var local_var = 10
print(local_var) # 正常
# 在这里写 local_var 会报错:未定义
局部变量和成员变量同名的问题
脚本中,在函数外面定义的变量是成员变量,整个脚本都能访问。但如果在函数里给一个和成员变量同名的变量赋值,GDScript 默认会创建一个新的局部变量,而不是修改成员变量。
gdscript
var health = 100 # 成员变量
func take_damage(amount):
var health = 50 # 这是新的局部变量,不是上面的成员变量
self.health -= amount # 用 self. 明确指定成员变量
func _ready():
take_damage(10)
print(health) # 输出 90,因为成员变量被正确修改了
如果直接写 health = 50,它只会改变局部变量,成员变量的 100 依然保留。所以我的习惯是:不要让局部变量和成员变量同名 ,避免混淆。如果一定要操作成员变量,加上 self. 前缀更明确。
七、函数重载的替代方案
C# 支持方法重载:同一个函数名可以定义多个版本,参数不同。GDScript 不支持这种重载,但有几种替代方式。
1. 使用默认参数
如果只是参数数量不同,用默认参数最方便。
gdscript
func attack(damage: int = 10, critical: bool = false):
# 不传参数时使用默认值
2. 在函数内部判断类型
如果参数类型不同,可以在函数体内用 is 判断类型,分别处理。
gdscript
func process_value(value):
if value is int:
print("处理整数:", value)
elif value is String:
print("处理字符串:", value)
elif value is Array:
print("处理数组:", value)
3. 使用可变参数
如果参数数量变化很大,可变参数可以接收任意数量的参数,然后在函数里遍历处理。
不过这些方法都会让函数变得臃肿。如果需要处理的情况确实很多,不如拆成几个不同名字的函数,比如 process_int()、process_string(),更清晰。
八、函数的传递
在 GDScript 中,函数可以被赋值给变量、作为参数传递、存储在数组里。这类似 C# 的委托,但语法更直接。
函数变量
gdscript
func add(a, b):
return a + b
func subtract(a, b):
return a - b
var op = add
print(op(3, 5)) # 8
op = subtract
print(op(3, 5)) # -2
注意,把函数赋值给变量时,函数名后面不要加括号 ,加括号会立即调用函数并把结果赋给变量。op = add 是把函数本身赋给 op,然后 op(3,5) 调用它。
函数作为参数传递
函数也可以作为参数传给另一个函数,然后在内部调用。
gdscript
func apply_operation(func_ref: Callable, a: int, b: int) -> int:
return func_ref.call(a, b)
var result = apply_operation(add, 3, 5) # 8
GDScript 4 中,函数引用类型是 Callable,用 call() 方法调用。不过这种用法在 GDScript 中相对少见,因为还有信号和 lambda 可以做类似的事。知道有这回事就行。
九、lambda 匿名函数
GDScript 4 支持 lambda 表达式,可以创建匿名函数,即没有名字的函数。语法是 func(参数): 返回值。
gdscript
var square = func(x): return x * x
print(square(5)) # 25
lambda 常作为参数传递给其他函数,比如数组的 map 和 filter。
gdscript
var numbers = [1, 2, 3, 4]
var doubled = numbers.map(func(n): return n * 2)
print(doubled) # [2, 4, 6, 8]
var evens = numbers.filter(func(n): return n % 2 == 0)
print(evens) # [2, 4]
lambda 也可以有多行,但为了可读性,尽量保持简短。如果要写复杂逻辑,还是定义普通函数更好。
十、内置回调函数
在 Godot 中,每个节点脚本都有一些由引擎自动调用的特殊函数,这些也是函数,但我们不需要手动调用,只需要在脚本里实现它们即可。
最常用的几个:
| 函数名 | 调用时机 |
|---|---|
_ready() |
节点进入场景树时调用一次,常用于初始化 |
_process(delta) |
每帧调用,delta 是上一帧到这一帧的时间间隔(秒) |
_physics_process(delta) |
固定物理帧调用,适合物理相关逻辑 |
_input(event) |
有输入事件时调用 |
_unhandled_input(event) |
输入事件未被其他节点处理时调用 |
_exit_tree() |
节点离开场景树时调用 |
示例:
gdscript
extends Node2D
func _ready():
print("节点准备好了")
func _process(delta):
# 每帧执行
position.x += 100 * delta # 以每秒 100 像素的速度向右移动
这些回调函数是 Godot 脚本的基础,后面讲节点和场景时会更深入。
十一、写函数的几个建议
- 函数名用动词开头 :
get_health()、set_position()、spawn_enemy(),一眼就知道功能。 - 函数尽量短小:一个函数只做一件事,方便测试和复用。如果一个函数超过 30 行,考虑拆分成多个小函数。
- 参数不要太多:如果参数超过四五个,考虑把它们封装成一个字典或自定义类型,否则调用时容易传错顺序。
- 及时写类型注解:重要的函数,参数和返回值都写类型,减少出错,也方便别人阅读。
- 善用默认参数:让函数在大多数情况下都能方便地调用,减少重复代码。
十二、总结
函数是组织代码的基本单位,GDScript 的函数语法简单但功能完整。这篇笔记详细介绍了定义、参数、返回值、默认值、可变参数、作用域、重载替代方案、函数作为一等公民、lambda 以及内置回调函数。掌握这些,日常开发基本够用了。建议多写一些自己的工具函数,慢慢积累起来,写游戏逻辑会越来越快。
下一篇我们进入数组的详细用法,看看 GDScript 的数组有哪些实用操作,包括添加、删除、查找、遍历和切片。