《GDScript 从浅入深》

《GDScript 从浅入深》

完全教程指南

—— 从零基础到游戏开发高手


前言

为什么选择 GDScript?

在开始这段学习旅程之前,我想先和你聊聊为什么 GDScript 值得你投入时间学习。

Godot 引擎近年来在游戏开发领域的崛起势不可挡。作为一个完全开源、免费且功能强大的游戏引擎,Godot 正在被越来越多的独立开发者、教育机构甚至小型工作室采用。而 GDScript,作为 Godot 的原生脚本语言,是通往这个强大引擎的最直接入口。

GDScript 的设计哲学可以用四个词概括:简单、优雅、集成、高效

它的语法深受 Python 影响,如果你有过 Python 编程经验,上手 GDScript 几乎没有任何障碍。即使你完全不懂编程,GDScript 简洁的语法和 Godot 友好的学习曲线,也会让你比学习其他游戏开发语言更快产出成果。

这本书的目标是什么?

这是一本真正带你“从浅入深”理解 GDScript 的教程书。我们会从最基础的概念开始,比如“什么是变量”、“什么是函数”,逐步深入到信号机制、场景树架构、性能优化,乃至自定义资源与模块开发。

全书分为四个阶段:

  • 入门篇(1-4章):掌握核心语法,能够编写简单的游戏逻辑
  • 进阶篇(5-8章):理解 Godot 特有机制,学会场景组织和信号通信
  • 高级篇(9-12章):性能优化、设计模式、工具脚本与自动化
  • 实战篇(13-16章):通过完整游戏项目,巩固并综合运用所学知识

每一章都配有可运行的代码示例和课后练习。书中的示例基于 Godot 4.x 版本,所有代码都经过实际测试。

你需要准备什么?

  • 一台可以运行 Godot 引擎的电脑(Windows / macOS / Linux 均可)
  • Godot 4.x 版本(建议从官网下载最新稳定版)
  • 一个趁手的代码编辑器(Godot 自带的脚本编辑器已经很不错,你也可以使用 VSCode 并安装 GDScript 插件)
  • 最重要的:好奇心和耐心

现在,让我们开始吧。


第一部分:入门篇

第1章:初识 GDScript

1.1 你好,世界

打开 Godot,创建一个新项目。让我们从最经典的程序开始。

步骤:

  1. 新建场景,选择 2D 场景
  2. 在场景中添加一个 Label 节点
  3. 选中 Label 节点,点击右侧的“添加脚本”按钮(看起来像一本书的图标)
  4. 在脚本编辑器中输入以下代码:
extends Label

func _ready():
    text = "你好,世界!"
    print("你好,控制台!")
  1. 保存场景(比如命名为 Main.tscn
  2. 点击运行(F5)

你会看到屏幕上显示“你好,世界!”,同时 Godot 的输出控制台会打印出“你好,控制台!”。

恭喜!你已经写出了第一个 GDScript 程序。

代码解读:

  • extends Label — 说明这个脚本附着在 Label 节点上,并且继承了 Label 的所有功能
  • func _ready() — 这是一个特殊的函数,当节点进入场景树时会自动执行
  • text = "你好,世界!" — 将 Label 的 text 属性修改为我们指定的字符串
  • print() — 一个内置函数,用于在控制台输出信息

1.2 什么是脚本?

在 Godot 中,脚本是赋予节点“生命”的代码。一个节点本身只有基本功能(比如一个 Label 只能显示文字,一个 Sprite2D 只能显示图片),而脚本让你可以定义这个节点应该做什么。

你可以把节点想象成一个空白的“角色”,脚本就是它的“剧本”。

1.3 脚本的构成

一个典型的 GDScript 脚本包含以下几个部分(按常见顺序):

# 1. 继承声明(必须)
extends Node2D

# 2. 类名声明(可选,用于全局访问)
class_name MyCustomNode

# 3. 信号声明(第7章会详细讲解)
signal health_depleted

# 4. 常量定义
const GRAVITY = 9.8

# 5. 枚举定义
enum Direction {UP, DOWN, LEFT, RIGHT}

# 6. 变量定义
var health = 100
@export var speed = 200

# 7. 内置回调函数
func _ready():
    pass

func _process(delta):
    pass

# 8. 自定义函数
func take_damage(amount):
    health -= amount
    if health <= 0:
        health_depleted.emit()

1.4 Godot 脚本工作流程

理解 Godot 如何执行你的脚本非常重要:

  1. 场景加载:当你加载一个场景,Godot 会创建场景树中的所有节点
  2. 脚本附加:每个节点的脚本被加载并附加到对应节点上
  3. _init():最先执行,相当于构造函数
  4. _enter_tree():节点进入场景树时调用
  5. _ready():节点及其所有子节点都进入场景树后调用
  6. _process(delta):每一帧都会调用
  7. _physics_process(delta):每个物理帧调用(默认每秒60次)
  8. _exit_tree():节点离开场景树时调用

这个执行顺序很重要,我们在后续章节会反复用到。

1.5 代码注释

注释是写给人类(包括未来的自己)看的说明,Godot 会完全忽略它们。

# 这是单行注释

# 也可以使用
# 多个单行注释
# 来模拟多行注释

# 推荐在注释前加一个空格,更易读

# TODO: 这是一个待办事项注释,方便标记未完成的工作
# FIXME: 标记需要修复的问题
# NOTE: 重要的说明信息

1.6 常用的编辑器快捷键

快捷键 功能
Ctrl + / 注释/取消注释当前行或选中的代码块
Ctrl + D 复制当前行
Tab 缩进
Shift + Tab 反向缩进
Ctrl + F 查找
Ctrl + R 重命名符号
Ctrl + Shift + F 全局查找
F1 打开上下文相关的文档

1.7 第一个练习

练习1.1:创建一个新的 Sprite2D 节点,附加脚本,在 _ready() 函数中修改它的 modulate 属性(颜色)为红色。

提示:颜色可以用 Color.REDColor(1, 0, 0) 表示。

第2章:变量、数据类型与运算符

2.1 变量:存储信息的盒子

变量是程序中用来存储数据的“容器”。在 GDScript 中,你不需要声明变量的类型(但可以声明,我们稍后会讲到)。

# 创建变量使用 var 关键字
var score = 0
var player_name = "Hero"
var is_alive = true

# 变量可以在创建时不赋值(此时为 null)
var item_count

# 变量的值可以被改变
score = 100
player_name = "Legendary Hero"

变量命名规则:

  • 必须以字母或下划线开头
  • 只能包含字母、数字和下划线
  • 区分大小写(myVarmyvar 是不同的)
  • 不能使用 GDScript 的保留字(如 varfuncif 等)

命名约定:

  • 变量名使用 snake_case(全小写,单词间用下划线):player_healthcoin_count
  • 常量使用 SHOUTING_SNAKE_CASEMAX_SPEEDDEFAULT_HP
  • 私有变量(仅在当前脚本内使用)建议以单下划线开头:_internal_state

2.2 数据类型

GDScript 是动态类型语言,但具有可选的静态类型注解。让我们先了解内置的基本数据类型:

2.2.1 整数(int)

var age = 25
var hex_value = 0xFF    # 十六进制,值为255
var binary = 0b1010     # 二进制,值为10
var negative = -42

2.2.2 浮点数(float)

var pi = 3.14159
var speed = 5.0
var scientific = 1.2e10    # 1.2 × 10^10

2.2.3 布尔值(bool)

var is_running = true
var has_key = false

# 布尔运算结果也是布尔值
var is_greater = 10 > 5    # true

2.2.4 字符串(String)

var greeting = "Hello"
var name = 'Alice'          # 单引号也可以
var multi_line = """
这是多行字符串
可以跨越多个行
"""

# 字符串拼接
var full = greeting + " " + name    # "Hello Alice"

# 字符串插值(推荐)
var message = "你好,%s!你的分数是 %d。" % [name, score]

# 更现代的插值方式(Godot 4)
var modern = "你好,{name}!分数:{score}".format({"name": name, "score": score})

2.2.5 类型推断与显式类型

GDScript 可以自动推断变量类型,你也可以显式声明:

# 自动推断
var auto_int = 10        # 推断为 int
var auto_float = 10.0    # 推断为 float
var auto_string = "hi"   # 推断为 String

# 显式类型声明(推荐,有助于编辑器提供更好的代码补全)
var explicit_int: int = 10
var explicit_float: float = 3.14
var explicit_string: String = "hello"
var explicit_bool: bool = true

# 类型声明后,赋值为其他类型会导致错误
var health: int = 100
# health = "full"    # 这行会报错!

2.3 运算符

2.3.1 算术运算符

运算符 名称 示例 结果
+ 加法 5 + 3 8
- 减法 5 - 3 2
* 乘法 5 * 3 15
/ 除法 5 / 2 2.5
// 整数除法 5 // 2 2
% 取模(余数) 5 % 2 1
** 幂运算 2 ** 3 8
var a = 10
var b = 3

print(a + b)    # 13
print(a - b)    # 7
print(a * b)    # 30
print(a / b)    # 3.333...
print(a // b)   # 3
print(a % b)    # 1
print(2 ** 4)   # 16

2.3.2 比较运算符

运算符 名称 示例
== 等于 5 == 5 → true
!= 不等于 5 != 3 → true
> 大于 5 > 3 → true
< 小于 5 < 3 → false
>= 大于等于 5 >= 5 → true
<= 小于等于 3 <= 5 → true

重要:注意 =(赋值)和 ==(相等比较)的区别!

var x = 10    # 赋值:把10放到x里

if x == 10:   # 比较:x等于10吗?
    print("x is 10")

2.3.3 逻辑运算符

运算符 名称 描述
and 逻辑与 两边都为真时结果为真
or 逻辑或 至少一边为真时结果为真
not 逻辑非 反转真假值
var age = 18
var has_license = true

# 可以开车吗?(年满16岁并且有驾照)
var can_drive = age >= 16 and has_license    # true

# 可以免费入场吗?(5岁以下或者65岁以上)
var age = 70
var is_free = age < 5 or age > 65            # true

# 不能开车(没有驾照)
var cannot_drive = not has_license           # false

2.3.4 复合赋值运算符

var score = 10

score += 5    # 等价于 score = score + 5 → 15
score -= 3    # 等价于 score = score - 3 → 12
score *= 2    # 等价于 score = score * 2 → 24
score /= 4    # 等价于 score = score / 4 → 6
score %= 3    # 等价于 score = score % 3 → 0

2.4 常量

使用 const 关键字定义不会改变的值:

const MAX_HP = 100
const GAME_TITLE = "冒险之旅"
const PI = 3.14159

# 尝试修改常量会报错
# MAX_HP = 200    # 错误!

常量在编译时就确定了,性能比变量更好。对于不会改变的值,应该使用常量。

2.5 枚举

枚举让代码更易读、更安全:

# 定义枚举
enum Direction {
    UP,      # 默认值为0
    DOWN,    # 1
    LEFT,    # 2
    RIGHT    # 3
}

# 可以手动指定值
enum PowerUp {
    HEALTH = 1,
    SPEED = 2,
    DAMAGE = 4,   # 可以是任意整数
}

# 使用枚举
var current_direction = Direction.RIGHT

match current_direction:
    Direction.UP:
        print("向上移动")
    Direction.DOWN:
        print("向下移动")
    # ...

# 枚举也像字典一样可以获取名称
print(Direction.keys()[Direction.UP])    # "UP"

2.6 @export 变量:在编辑器中调整数值

这是一个非常实用的功能。通过在变量前添加 @export,你可以在 Godot 编辑器的检视面板中直接修改这个变量的值:

extends CharacterBody2D

@export var speed: int = 200          # 在编辑器中可见并可修改
@export var jump_force: float = 400.0
@export var player_name: String = "Hero"
@export var start_position: Vector2 = Vector2(100, 200)

# 导出范围限制
@export_range(1, 100) var health: int = 50
@export_range(0.0, 1.0) var opacity: float = 1.0

# 导出文件路径
@export_file("*.tscn") var next_scene: String
@export_dir var save_directory: String

# 导出颜色
@export_color_no_alpha var tint: Color = Color.WHITE

这使得你可以快速调整游戏参数而不需要修改代码,极大地提高了迭代效率。

2.7 静态类型的好处

虽然 GDScript 是动态类型语言,但强烈推荐使用静态类型注解:

好处:

  1. 编辑器能提供更好的代码补全
  2. 在编译时发现类型错误,而不是运行时
  3. 代码更易读、自文档化
  4. 性能略有提升
# 不推荐(但可以工作)
var damage = calculate_damage()

# 推荐
var damage: int = calculate_damage()

# 函数参数和返回值也推荐添加类型
func add_numbers(a: int, b: int) -> int:
    return a + b

2.8 类型转换

有时你需要将一个类型转换为另一个类型:

# 显式转换
var int_value: int = 42
var float_value: float = float(int_value)    # 42.0

var float_num: float = 3.14
var int_num: int = int(float_num)            # 3(截断,不是四舍五入)

# 字符串转换
var num_str: String = "123"
var num: int = int(num_str)                  # 123
var bad: int = int("hello")                  # 0(转换失败返回0)

var number: int = 456
var str_num: String = str(number)            # "456"

# 使用 as 关键字进行安全的类型转换(针对对象类型)
var node: Node = $MyNode
var label: Label = node as Label
if label:
    label.text = "转换成功"

2.9 练习

练习2.1:编写一个脚本,定义以下变量:玩家姓名(字符串)、等级(整数)、经验值(浮点数)、是否在线(布尔值)。给它们赋初值,然后用 print() 输出一条包含所有信息的消息。

练习2.2:创建一个计算器脚本,定义两个整数变量 ab,计算并输出它们的和、差、积、商、整数商和余数。

练习2.3:使用 @export 创建一个角色属性设置器,包含:生命值(1-500)、移动速度(50-800)、角色颜色(带透明度)、初始武器名称(字符串)。在编辑器中观察它们如何出现在检视面板中。


第3章:流程控制

程序默认是按顺序从上到下执行的。流程控制语句让你能够改变这个执行顺序。

3.1 条件判断:if / elif / else

条件判断让程序能够根据不同的情况执行不同的代码:

var health = 30

if health <= 0:
    print("玩家死亡")
elif health < 30:
    print("血量低,小心!")
elif health < 70:
    print("血量中等")
else:
    print("血量充足")

# 输出:"血量低,小心!"

语法要点:

  • if 后面跟条件表达式,以冒号 : 结尾
  • 条件为真时执行的代码块必须缩进(推荐4个空格)
  • elif 是 "else if" 的缩写,可以有多个
  • else 是可选的,处理所有其他情况

简洁写法(单行):

# 只有一行代码时可以写在同一行
if game_over: print("游戏结束")

# 三元运算符(条件 ? 真值 : 假值)
var status = "成人" if age >= 18 else "未成年"

3.2 循环

3.2.1 for 循环

for 循环用于遍历一个范围(range)或集合中的每个元素:

# 遍历数字范围
for i in range(5):      # 0, 1, 2, 3, 4
    print(i)

for i in range(2, 5):   # 2, 3, 4
    print(i)

for i in range(0, 10, 2):  # 0, 2, 4, 6, 8(步长为2)
    print(i)

# 倒序遍历
for i in range(5, 0, -1):  # 5, 4, 3, 2, 1
    print(i)

# 遍历数组
var fruits = ["苹果", "香蕉", "橙子"]
for fruit in fruits:
    print(fruit)

# 遍历字典
var player = {"name": "勇者", "hp": 100, "mp": 50}
for key in player:
    print(key, ":", player[key])

# 同时获取索引和值
for index in range(fruits.size()):
    print(index, fruits[index])

3.2.2 while 循环

while 循环在条件为真时重复执行:

var count = 0
while count < 5:
    print("计数:", count)
    count += 1

# 无限循环(小心使用!)
var running = true
var timer = 0
while running:
    timer += 1
    if timer >= 100:
        running = false   # 退出循环

# 通常更安全的做法是使用 _process 或 Timer 节点

3.2.3 循环控制:break 和 continue

  • break:立即退出整个循环
  • continue:跳过当前迭代,继续下一次循环
# 查找第一个偶数
for i in range(10):
    if i % 2 == 0:
        print("找到偶数:", i)
        break   # 找到后就退出,不再继续

# 跳过奇数
for i in range(10):
    if i % 2 == 1:
        continue   # 奇数跳过
    print(i)       # 只打印偶数

3.3 match 语句(模式匹配)

match 是 GDScript 中的强大功能,类似于其他语言的 switch,但更强大:

var command = "HEAL"

match command:
    "ATTACK":
        print("执行攻击")
    "DEFEND":
        print("执行防御")
    "HEAL":
        print("执行治疗")
    _:                     # 下划线是默认情况
        print("未知命令")

# match 支持多种模式
var value = 42

match value:
    0:
        print("是零")
    1, 2, 3:               # 多个值匹配同一个分支
        print("1到3之间")
    10..20:                # 范围匹配
        print("10到20之间(包含边界)")
    var v:                 # 绑定到变量
        print("其他值: ", v)

# 匹配类型
var node = get_node("SomeNode")
match typeof(node):
    TYPE_LABEL:
        print("这是一个 Label")
    TYPE_BUTTON:
        print("这是一个 Button")

3.4 常见模式与实践

3.4.1 保护子句(提前返回)

使用 if 提前处理边界情况,减少嵌套:

# 不推荐:深层嵌套
func process_player(player):
    if player != null:
        if player.is_alive:
            if player.has_weapon:
                player.attack()
            else:
                print("没有武器")
        else:
            print("玩家已死亡")
    else:
        print("玩家不存在")

# 推荐:使用保护子句
func process_player(player):
    if player == null:
        print("玩家不存在")
        return
    
    if not player.is_alive:
        print("玩家已死亡")
        return
    
    if not player.has_weapon:
        print("没有武器")
        return
    
    player.attack()

3.4.2 使用枚举代替魔法数字

# 不推荐
if state == 0:    # 0代表什么?
    jump()
elif state == 1:  # 1呢?
    run()

# 推荐
enum PlayerState {IDLE, RUN, JUMP, FALL}

if state == PlayerState.JUMP:
    jump()
elif state == PlayerState.RUN:
    run()

3.5 练习

练习3.1:写一个判断成绩等级的程序。输入一个分数(0-100),输出对应的等级:90分以上为A,80-89为B,70-79为C,60-69为D,60分以下为F。

练习3.2:用循环计算 1 到 100 之间所有偶数的和。

练习3.3:用 match 语句实现一个简单的计算器,根据操作符("+"、"-"、"*"、"/")执行相应的运算。


第4章:函数与方法

4.1 定义和调用函数

函数是组织代码的基本单元。它们让你能够将重复使用的代码打包,并赋予一个名字。

# 基本函数定义
func say_hello():
    print("Hello!")

# 带参数的函数
func greet(name: String):
    print("Hello, ", name)

# 带返回值的函数
func add(a: int, b: int) -> int:
    return a + b

# 使用默认参数
func create_player(name: String = "Newbie", health: int = 100) -> Dictionary:
    return {"name": name, "health": health}

# 调用函数
say_hello()                      # Hello!
greet("Alice")                   # Hello, Alice
var result = add(5, 3)           # result = 8
var player = create_player()     # 使用默认值
var custom = create_player("战神", 500)

4.2 返回值与 return

return 语句有两重作用:

  1. 从函数返回一个值
  2. 立即结束函数的执行
func check_health(health: int) -> String:
    if health <= 0:
        return "dead"      # 这里就返回了,下面的代码不会执行
    elif health < 30:
        return "critical"
    
    return "healthy"       # 如果没进任何分支,到这里返回

# 没有显式 return 的函数返回 null
func do_nothing():
    pass

var nothing = do_nothing()   # nothing = null

4.3 函数是一等公民

在 GDScript 中,函数可以被赋值给变量,作为参数传递:

# 将函数赋值给变量
var my_func = say_hello
my_func.call()    # 调用函数

# 更常见的做法:使用 Callable
var callable: Callable = say_hello
callable.call()

# 将函数作为参数传递
func apply_operation(a: int, b: int, operation: Callable) -> int:
    return operation.call(a, b)

func multiply(x: int, y: int) -> int:
    return x * y

var result = apply_operation(5, 3, multiply)   # 15

# 匿名函数(Lambda)
var add_lambda = func(a, b): return a + b
print(add_lambda.call(2, 3))   # 5

4.4 内置的常用函数

GDScript 提供了许多内置函数,可以直接使用:

# 类型转换
int("123")      # 123
float("3.14")   # 3.14
str(42)         # "42"

# 数学函数
abs(-5)         # 5
min(10, 3)      # 3
max(10, 3)      # 10
clamp(15, 0, 10)  # 10(限制在0-10之间)
round(3.6)      # 4
floor(3.9)      # 3
ceil(3.1)       # 4
sqrt(16)        # 4
pow(2, 3)       # 8

# 随机数
randi()              # 随机整数
randf()              # 0.0 到 1.0 之间的随机浮点数
randi_range(1, 10)   # 1到10之间的随机整数

# 设置随机种子,使随机结果可重现
seed(12345)

# 类型检查
is_instance_of(node, Label)   # 检查 node 是否是 Label 的实例
typeof(value)                  # 返回值的类型

4.5 静态函数

使用 static 关键字创建属于类本身而不是实例的函数:

class_name MathHelper

static func square(x: float) -> float:
    return x * x

static func is_even(x: int) -> bool:
    return x % 2 == 0

# 调用静态函数(不需要实例)
var result = MathHelper.square(5)   # 25
var even = MathHelper.is_even(10)   # true

4.6 函数的递归

函数调用自身称为递归:

# 计算阶乘
func factorial(n: int) -> int:
    if n <= 1:
        return 1
    return n * factorial(n - 1)

print(factorial(5))   # 120

# 小心:深度递归可能导致栈溢出
# 对于大量迭代,使用循环通常更好

4.7 练习

练习4.1:编写一个函数 is_prime(n),判断一个整数是否为质数。然后在 _ready() 中测试 1 到 100 之间的所有整数。

练习4.2:编写一个函数 fibonacci(n),返回斐波那契数列的第 n 项(使用循环实现,不是递归)。

练习4.3:创建一个 Calculator 类,包含静态方法 addsubtractmultiplydivide,以及一个计算平方根的静态方法 sqrt


第5章:数组与字典

5.1 数组(Array)

数组是存储有序元素集合的数据结构。在 GDScript 中,数组可以混合存储不同类型的元素。

# 创建数组
var empty_array = []                    # 空数组
var numbers = [1, 2, 3, 4, 5]          # 整数数组
var mixed = [1, "hello", 3.14, true]   # 混合类型

# 类型化数组(性能更好,类型安全)
var typed_array: Array[int] = [1, 2, 3]
var strings: Array[String] = ["a", "b", "c"]

# 访问元素(索引从0开始)
var fruits = ["苹果", "香蕉", "橙子"]
print(fruits[0])    # "苹果"
print(fruits[2])    # "橙子"
print(fruits[-1])   # "橙子"(负数从末尾开始)

# 修改元素
fruits[1] = "葡萄"
print(fruits)       # ["苹果", "葡萄", "橙子"]

# 数组大小
print(fruits.size())   # 3

# 遍历数组
for fruit in fruits:
    print(fruit)

for i in range(fruits.size()):
    print(i, fruits[i])

5.2 数组常用方法

var arr = [1, 2, 3]

# 添加元素
arr.append(4)           # [1, 2, 3, 4]
arr.append(5)           # [1, 2, 3, 4, 5]
arr.push_front(0)       # [0, 1, 2, 3, 4, 5](效率较低)

# 插入元素
arr.insert(3, 99)       # 在索引3处插入99 → [0, 1, 2, 99, 3, 4, 5]

# 移除元素
arr.remove_at(3)        # 移除索引3 → [0, 1, 2, 3, 4, 5]
arr.pop_back()          # 移除最后一个元素,返回它 → 5, arr变为[0,1,2,3,4]
arr.pop_front()         # 移除第一个元素 → 0, arr变为[1,2,3,4]

# 查找元素
var idx = arr.find(3)   # 返回3的索引(2)
var has_5 = 5 in arr    # false(检查是否包含)

# 排序
var unsorted = [3, 1, 4, 1, 5]
unsorted.sort()         # [1, 1, 3, 4, 5](原地排序)

# 反转
unsorted.reverse()      # [5, 4, 3, 1, 1]

# 复制数组
var copy = arr.duplicate()   # 浅复制
var deep_copy = arr.duplicate(true)  # 深复制(复制嵌套的数组/字典)

# 清空
arr.clear()             # []

# 切片
var slice = [0, 1, 2, 3, 4, 5]
var sub = slice.slice(2, 5)    # [2, 3, 4](索引2到5,不包括5)

# 连接
var combined = [1, 2] + [3, 4]   # [1, 2, 3, 4]

5.3 字典(Dictionary)

字典存储键值对。键可以是任何类型,但通常使用字符串。

# 创建字典
var empty_dict = {}
var player = {
    "name": "勇者",
    "hp": 100,
    "mp": 50,
    "inventory": ["剑", "盾", "药水"],
    "position": Vector2(100, 200)
}

# 访问值
print(player["name"])        # "勇者"
print(player.name)           # 也可以使用点号(如果键是有效标识符)

# 添加/修改值
player["level"] = 5          # 添加新键值对
player.hp = 80               # 修改现有值
player["mp"] -= 10           # 减少mp

# 检查键是否存在
if player.has("inventory"):
    print("有背包")

# 或者使用 in 操作符
if "level" in player:
    print("等级:", player.level)

# 删除键值对
player.erase("mp")           # 删除mp键

# 遍历字典
for key in player:
    print(key, ":", player[key])

# 同时遍历键和值
for key in player.keys():
    print(key)

for value in player.values():
    print(value)

# 获取键列表或值列表
var keys = player.keys()       # ["name", "hp", ...]
var values = player.values()

# 字典大小
print(player.size())           # 键值对的数量

# 合并字典
var dict1 = {"a": 1, "b": 2}
var dict2 = {"b": 3, "c": 4}
dict1.merge(dict2)             # dict1变成{"a":1, "b":3, "c":4}(重复键被覆盖)

# 复制
var dict_copy = player.duplicate()
var deep_copy = player.duplicate(true)

5.4 多维数组与嵌套字典

# 二维数组(网格)
var grid = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
]

print(grid[1][2])   # 6(第2行,第3列)

# 访问所有元素
for row in grid:
    for cell in row:
        print(cell)

# 嵌套字典
var game_data = {
    "player": {
        "name": "英雄",
        "stats": {
            "strength": 10,
            "dexterity": 8,
            "intelligence": 7
        }
    },
    "world": {
        "name": "中土",
        "difficulty": "normal"
    }
}

print(game_data.player.stats.strength)   # 10

5.5 数组与字典的性能考虑

# 数组查找是 O(n)(线性时间)
var items = ["sword", "shield", "potion", ...]  # 可能有1000个元素
if "excalibur" in items:    # 需要遍历整个数组才能确定
    print("找到")

# 字典查找是 O(1)(常数时间)
var item_dict = {
    "sword": true,
    "shield": true,
    "potion": true,
    # ...
}
if item_dict.has("excalibur"):   # 直接哈希查找,非常快
    print("找到")

# 所以如果需要频繁查找,使用字典更好
# 如果需要保持顺序,使用数组

5.6 练习

练习5.1:创建一个待办事项管理器。使用数组存储任务(字符串),实现添加任务、完成(删除)任务、列出所有任务的功能。

练习5.2:用字典表示一个游戏角色,包含以下属性:名称、等级、生命值、魔法值、装备列表(数组)、技能字典(技能名→等级)。然后编写函数显示角色的完整信息。

练习5.3:写一个函数,接受一个整数数组,返回一个新数组,其中只包含原数组中的偶数,并按升序排列。


第6章:节点与场景

这是 GDScript 区别于普通编程语言的核心内容。理解节点和场景是掌握 Godot 的关键。

6.1 节点树

在 Godot 中,每个游戏都是由节点(Node)组成的树状结构:

Scene (根节点)
├── Player (CharacterBody2D)
│   ├── Sprite2D
│   └── CollisionShape2D
├── Enemy (CharacterBody2D)
│   ├── Sprite2D
│   └── CollisionShape2D
└── UI (CanvasLayer)
    ├── Label
    └── Button

6.2 获取节点

要从脚本中访问其他节点,你需要获取对它们的引用:

extends Node2D

func _ready():
    # 方法1:使用 $ 语法(推荐)
    var player = $Player
    var sprite = $Player/Sprite2D
    
    # 方法2:使用 get_node()
    var enemy = get_node("Enemy")
    var ui = get_node("../UI")   # 父目录
    
    # 方法3:使用路径
    var absolute = get_node("/root/Main/Player")  # 从根节点开始的绝对路径
    
    # 方法4:使用 % 和唯一名称
    # 在节点上启用"Access as Unique Name"后,可以使用 % 快速访问
    var health_label = %HealthLabel  # 节点唯一名称为"HealthLabel"
    
    # 方法5:查找子节点
    var child = find_child("Sprite2D")   # 递归查找
    var children = get_children()         # 获取所有直接子节点

# 检查节点是否存在
if has_node("Player"):
    var player = $Player
    player.move()

# 安全的节点获取
var node = get_node_or_null("MaybeNotExist")
if node:
    node.do_something()

6.3 节点生命周期函数

这些是 Godot 中最重要的内置函数,记住它们的调用顺序:

extends Node

# 1. 构造函数(不能访问其他节点,因为节点还未加入场景树)
func _init():
    print("1. 初始化")
    # 这里不能使用 $ 或 get_node()!

# 2. 进入场景树时调用
func _enter_tree():
    print("2. 进入场景树")
    # 现在可以使用 get_node() 了,但子节点可能还没准备好

# 3. 所有子节点都进入场景树后调用(最常用)
func _ready():
    print("3. 准备就绪")
    # 最安全的地方:所有子节点都已准备好

# 4. 每帧调用(帧速率相关)
func _process(delta: float):
    # delta 是上一帧到这一帧经过的时间(秒)
    # 用于实现与帧率无关的移动
    pass

# 5. 每个物理帧调用(固定时间间隔,默认60次/秒)
func _physics_process(delta: float):
    # 用于物理相关代码(移动、碰撞等)
    pass

# 6. 输入事件(键盘、鼠标、触摸)
func _input(event: InputEvent):
    if event.is_action_pressed("ui_accept"):
        print("按下了确认键")

# 7. 离开场景树时调用
func _exit_tree():
    print("7. 离开场景树")

6.4 _process() vs _physics_process()

这是初学者最容易混淆的地方:

extends CharacterBody2D

@export var speed = 300

# _process: 每帧调用,帧率变化时调用频率也会变化
func _process(delta):
    # 不适合处理物理移动,因为帧率波动会导致移动不平滑
    # 但适合处理动画、UI更新、非物理相关逻辑
    update_ui()

# _physics_process: 固定频率(默认60Hz),与物理引擎同步
func _physics_process(delta):
    # 适合处理角色移动、碰撞检测等物理相关逻辑
    var velocity = Vector2.ZERO
    
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
        
    velocity = velocity.normalized() * speed
    move_and_collide(velocity * delta)
    # 或使用 move_and_slide()

选择指南:

  • 使用 _physics_process:角色移动、物理交互、碰撞检测
  • 使用 _process:动画更新、UI 闪烁效果、非关键计时、Camera 平滑跟随

6.5 场景切换

游戏通常由多个场景组成(主菜单、游戏关卡、设置界面等):

extends Node

# 切换到另一个场景
func go_to_game():
    # 方法1:直接切换(推荐)
    get_tree().change_scene_to_file("res://scenes/Game.tscn")
    
    # 方法2:先加载,再切换(可以显示加载界面)
    ResourceLoader.load_threaded_request("res://scenes/Game.tscn")
    var game_scene = await ResourceLoader.load_threaded_get("res://scenes/Game.tscn")
    get_tree().change_scene_to_packed(game_scene)

# 重新加载当前场景
func restart_scene():
    get_tree().reload_current_scene()

# 添加场景作为子节点(而不是替换)
func add_pause_menu():
    var pause_scene = load("res://scenes/PauseMenu.tscn").instantiate()
    add_child(pause_scene)

# 场景切换时的数据传递
var player_data = {
    "score": 1000,
    "lives": 3
}

# 在切换前将数据保存到某个地方(比如单例)
func save_and_switch():
    Global.player_data = player_data
    get_tree().change_scene_to_file("res://scenes/NextLevel.tscn")

# 在新场景的 _ready() 中读取数据
func _ready():
    if Global.player_data:
        score = Global.player_data.score
        lives = Global.player_data.lives

6.6 节点创建与删除

动态创建和销毁节点是常见需求:

# 创建节点
func spawn_enemy():
    # 加载场景
    var enemy_scene = preload("res://scenes/Enemy.tscn")
    # 实例化
    var enemy = enemy_scene.instantiate()
    # 设置属性
    enemy.position = Vector2(100, 200)
    enemy.name = "Enemy_" + str(get_child_count())
    # 添加到场景树
    add_child(enemy)

# 使用代码创建基本节点
func create_custom_node():
    var label = Label.new()
    label.text = "动态创建的标签"
    label.position = Vector2(50, 50)
    add_child(label)

# 删除节点
func remove_enemy(enemy: Node):
    enemy.queue_free()    # 安全删除(在当前帧结束时删除)
    # enemy.free()        # 立即删除(可能导致错误,不推荐)

# 删除所有子节点
func clear_children():
    for child in get_children():
        child.queue_free()

# 等待一帧后再删除(有时需要)
func delayed_remove(node: Node, delay: float = 0.1):
    await get_tree().create_timer(delay).timeout
    if is_instance_valid(node):
        node.queue_free()

6.7 练习

练习6.1:创建一个场景,包含一个玩家(CharacterBody2D 带 Sprite2D 子节点)和几个金币(Area2D)。编写脚本,当玩家碰到金币时,金币消失并增加分数。使用 _physics_process 处理玩家移动。

练习6.2:实现一个简单的场景切换系统:按空格键切换到另一个场景,按 R 键重新开始当前场景。创建两个不同的场景来测试。

练习6.3:创建一个弹幕发射器。使用 _process 每0.5秒动态创建一个子弹节点(场景实例),子弹自动向前移动,到达屏幕边缘后自动销毁。


第7章:信号(Signals)

信号是 Godot 中最优雅的通信机制。它们实现了松耦合的组件间通信,让你无需知道接收者是谁就能发送消息。

7.1 理解信号

想象一下:你按下一个按钮,想要更新屏幕上的分数。按钮不需要知道分数显示在哪里,它只需要发出“我被按下了”的信号。任何关心这个信号的节点都可以连接并响应。

这就是信号的哲学:发送者不关心接收者,接收者决定关心什么事件

7.2 使用内置信号

大多数 Godot 节点都提供了内置信号:

extends Node

func _ready():
    # 连接按钮的 pressed 信号
    var button = $Button
    button.pressed.connect(_on_button_pressed)
    
    # 连接计时器的 timeout 信号
    var timer = $Timer
    timer.timeout.connect(_on_timer_timeout)
    timer.start(2.0)  # 2秒后触发

func _on_button_pressed():
    print("按钮被按下了!")

func _on_timer_timeout():
    print("2秒钟过去了!")

更简洁的写法:在编辑器中连接信号

  1. 选中节点
  2. 转到“节点”面板(Node)
  3. 找到信号
  4. 双击或拖拽到目标节点
  5. 选择或创建连接函数

Godot 会自动生成类似这样的代码:

# 编辑器会自动生成连接和回调函数
func _on_button_pressed():
    # 你的代码
    pass

7.3 自定义信号

你可以定义自己的信号来满足特定需求:

extends CharacterBody2D

# 定义信号
signal health_changed(new_health: int, max_health: int)
signal player_died
signal score_updated(new_score: int)
signal item_collected(item_name: String, value: int)

var health: int = 100
var max_health: int = 100
var score: int = 0

func take_damage(amount: int):
    health -= amount
    # 发出信号
    health_changed.emit(health, max_health)
    
    if health <= 0:
        die()

func add_score(points: int):
    score += points
    score_updated.emit(score)

func collect_item(item_name: String, value: int):
    item_collected.emit(item_name, value)
    add_score(value)

func die():
    player_died.emit()
    queue_free()

7.4 连接信号的方式

有几种方式连接信号:

# 方式1:使用 .connect() 方法
func _ready():
    $Button.pressed.connect(_on_button_pressed)
    $Player.health_changed.connect(_on_health_changed)

# 方式2:使用 Callable 绑定参数
func _ready():
    $Button.pressed.connect(_on_button_pressed.bind("额外参数"))

func _on_button_pressed(extra_param: String):
    print("按钮被按下,额外参数:", extra_param)

# 方式3:连接带参数的信号
signal damage_dealt(amount: int, target: String)

func _ready():
    damage_dealt.connect(_on_damage_dealt)

func _on_damage_dealt(amount: int, target: String):
    print("对 ", target, " 造成了 ", amount, " 点伤害")

# 方式4:使用 lambda 表达式(不推荐用于重要逻辑)
func _ready():
    $Button.pressed.connect(func(): print("按钮被按下"))

7.5 信号的高级用法

7.5.1 断开连接

# 断开特定连接
if $Button.pressed.is_connected(_on_button_pressed):
    $Button.pressed.disconnect(_on_button_pressed)

# 断开所有连接(谨慎使用)
$Button.pressed.disconnect_all()

7.5.2 信号连接状态

# 检查是否已连接
if $Button.pressed.is_connected(_on_button_pressed):
    print("已经连接了")

# 获取连接数量
print($Button.pressed.get_connection_count())

# 获取所有连接信息
var connections = $Button.pressed.get_connections()
for conn in connections:
    print("连接到: ", conn.callable)

7.5.3 等待信号

使用 await 关键字等待信号:

# 等待计时器
func wait_example():
    print("开始等待")
    await get_tree().create_timer(2.0).timeout
    print("2秒后执行这里")

# 等待动画完成
func play_and_wait():
    var animation = $AnimationPlayer
    animation.play("jump")
    await animation.animation_finished
    print("动画播放完毕")

# 等待任意信号
func wait_for_button():
    print("等待按钮按下...")
    await $Button.pressed
    print("按钮被按下了!")
    
# 等待多个信号中的任意一个
func wait_for_any():
    var button_pressed = $Button.pressed
    var timer_timeout = $Timer.timeout
    
    var result = await await_first(button_pressed, timer_timeout)
    print("最先发生的是: ", result)

func await_first(signal1: Signal, signal2: Signal):
    var result = await signal1
    return "signal1"
    # 注意:这个实现不完美,实际需要更复杂的逻辑

7.5.4 信号的常见模式

观察者模式:

# GameManager.gd (自动加载单例)
extends Node

signal game_started
signal game_over(score: int)
signal level_completed(level: int)

var current_score: int = 0

func start_game():
    current_score = 0
    game_started.emit()

func end_game():
    game_over.emit(current_score)

# UI.gd
func _ready():
    GameManager.game_started.connect(_on_game_started)
    GameManager.game_over.connect(_on_game_over)

func _on_game_started():
    show_hud()
    reset_display()

func _on_game_over(score: int):
    show_game_over_screen(score)

中介者模式:

# LevelManager.gd
extends Node

signal all_enemies_defeated

var enemy_count: int = 0

func register_enemy(enemy: Node):
    enemy_count += 1
    enemy.died.connect(_on_enemy_died)

func _on_enemy_died():
    enemy_count -= 1
    if enemy_count <= 0:
        all_enemies_defeated.emit()

7.6 信号 vs 直接调用

场景 推荐方式 原因
父子节点通信 直接调用 关系明确,性能更好
兄弟节点通信 信号 避免耦合,更灵活
任意节点通信 信号 发送者不需要知道接收者
频繁调用(每帧) 直接调用 信号有轻微开销
跨场景通信 信号(通过单例) 解耦,易于维护
编辑器内连接 信号 可视化,直观

7.7 练习

练习7.1:创建一个健康系统。玩家节点有一个 health 变量,当受到伤害时发出 damage_taken 信号。创建一个 UI 节点连接到这个信号,更新生命条显示。

练习7.2:实现一个成就系统。当玩家收集特定数量的金币(发出 coin_collected 信号)时,发出 achievement_unlocked 信号。创建一个成就弹窗节点监听此信号并显示通知。

练习7.3:使用 await 创建一个序列:玩家碰到门 → 等待1秒 → 播放传送动画 → 等待动画完成 → 切换场景。


第8章:基础游戏组件

8.1 CharacterBody2D:角色控制

CharacterBody2D 是制作可移动角色的专用节点:

extends CharacterBody2D

@export var speed: float = 300.0
@export var jump_velocity: float = -400.0

# 获取重力(从项目设置)
var gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta):
    # 应用重力
    if not is_on_floor():
        velocity.y += gravity * delta

    # 处理跳跃
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = jump_velocity

    # 处理水平输入
    var direction = Input.get_axis("ui_left", "ui_right")
    if direction:
        velocity.x = direction * speed
    else:
        velocity.x = move_toward(velocity.x, 0, speed)

    move_and_slide()

8.2 Area2D:区域检测

Area2D 用于检测重叠(不需要物理碰撞响应):

extends Area2D

signal coin_collected

func _ready():
    # 连接信号(也可以在编辑器中连接)
    body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node2D):
    if body.is_in_group("player"):
        coin_collected.emit()
        queue_free()

8.3 Timer:计时器

extends Node

@onready var timer = $Timer

func start_countdown(seconds: float):
    timer.wait_time = seconds
    timer.one_shot = true  # 单次触发
    timer.start()
    await timer.timeout
    print("倒计时结束!")

func start_repeating():
    timer.wait_time = 1.0
    timer.one_shot = false  # 重复触发
    timer.start()

func _on_timer_timeout():
    print("每一秒执行一次")

8.4 练习

练习8.1:创建一个完整的平台跳跃角色,包含移动、跳跃、二段跳和爬墙功能。

练习8.2:创建一个收集品系统,有不同价值的金币(1分、5分、10分),使用 Area2D 检测玩家,更新 UI 显示总分。


第二部分:进阶篇

第9章:场景树与自动加载

9.1 场景树的深层理解

Godot 的场景树不仅仅是节点的容器,它是一个活动对象的层次结构:

# 获取场景树的根节点
var root = get_tree().root

# 获取当前场景
var current_scene = get_tree().current_scene

# 场景树遍历
func traverse_tree(node: Node, indent: int = 0):
    print("  ".repeat(indent), node.name)
    for child in node.get_children():
        traverse_tree(child, indent + 1)

# 调用场景树中的所有节点
get_tree().call_group("enemies", "take_damage", 10)

# 设置组
func _ready():
    add_to_group("enemies")
    
# 暂停游戏
func toggle_pause():
    get_tree().paused = not get_tree().paused

# 在暂停状态下让某个节点继续工作
func _ready():
    process_mode = PROCESS_MODE_ALWAYS  # 即使游戏暂停也继续处理

9.2 自动加载(单例模式)

自动加载的脚本会在游戏启动时加载,并且全局可用:

设置方法:

  1. 项目设置 → 自动加载(AutoLoad)
  2. 添加脚本或场景
  3. 命名(如 "Global")

Global.gd:

extends Node

# 全局变量
var player_score: int = 0
var current_level: int = 1
var player_data: Dictionary = {}

# 全局信号
signal score_changed(new_score: int)
signal level_completed

# 全局函数
func add_score(points: int):
    player_score += points
    score_changed.emit(player_score)

func save_game():
    var save_dict = {
        "score": player_score,
        "level": current_level,
        "player_data": player_data
    }
    var file = FileAccess.open("user://savegame.save", FileAccess.WRITE)
    file.store_string(JSON.stringify(save_dict))

func load_game():
    if FileAccess.file_exists("user://savegame.save"):
        var file = FileAccess.open("user://savegame.save", FileAccess.READ)
        var content = file.get_as_text()
        var data = JSON.parse_string(content)
        player_score = data["score"]
        current_level = data["level"]
        player_data = data["player_data"]

在任何脚本中使用:

# 直接使用,无需获取引用
Global.add_score(100)
Global.save_game()
print(Global.player_score)

9.3 资源的预加载与加载

# 预加载:在编译时加载(速度快,但增加启动时间)
const EnemyScene = preload("res://scenes/Enemy.tscn")

# 运行时加载(稍慢,但更灵活)
var scene_path = "res://scenes/Level" + str(level_num) + ".tscn"
var level_scene = load(scene_path)

# 异步加载(避免卡顿)
func load_level_async(level_path: String):
    var load_thread = ResourceLoader.load_threaded_request(level_path)
    # 可以做其他事情
    await get_tree().create_timer(0.1).timeout
    var scene = await ResourceLoader.load_threaded_get(level_path)
    get_tree().change_scene_to_packed(scene)

9.4 练习

练习9.1:创建一个 SaveSystem 自动加载单例,实现完整的游戏存档功能,包括保存玩家的位置、分数和背包物品。

练习9.2:实现一个暂停菜单,使用 get_tree().paused,并确保暂停菜单本身仍然可以响应输入。


第10章:输入处理

10.1 Input Map(输入映射)

在项目设置中定义输入动作,让输入处理与具体按键解耦:

设置输入动作:

移动_左: [A, 左箭头]
移动_右: [D, 右箭头]
跳跃: [Space, 上箭头]
攻击: [鼠标左键, J]

使用输入动作:

func _process(delta):
    # 检查按住
    var left = Input.is_action_pressed("移动_左")
    var right = Input.is_action_pressed("移动_右")
    
    # 检查按下(单次)
    if Input.is_action_just_pressed("跳跃"):
        jump()
    
    # 检查释放
    if Input.is_action_just_released("攻击"):
        stop_attacking()

10.2 自定义 InputEvent 处理

func _input(event: InputEvent):
    # 处理鼠标
    if event is InputEventMouseButton:
        if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
            print("鼠标点击位置: ", event.position)
    
    # 处理键盘
    if event is InputEventKey:
        if event.pressed and event.keycode == KEY_ESCAPE:
            toggle_pause_menu()
    
    # 处理游戏手柄
    if event is InputEventJoypadButton:
        if event.button_index == JOY_BUTTON_A:
            print("A 按钮被按下")
    
    # 手势(触摸屏)
    if event is InputEventScreenTouch:
        if event.pressed:
            print("触摸点: ", event.position)

10.3 练习

练习10.1:创建一个可配置的控制设置界面,允许玩家修改按键绑定,并将配置保存到文件中。


第11章:性能优化

11.1 避免在 _process 中重复操作

# 不推荐
func _process(delta):
    var children = get_children()  # 每帧都获取
    for child in children:
        child.update()

# 推荐:缓存引用
var children_cache: Array

func _ready():
    children_cache = get_children()

func _process(delta):
    for child in children_cache:
        child.update()

11.2 使用对象池

class_name ObjectPool

var pool: Array = []
var scene: PackedScene

func _init(scene_path: String, initial_size: int = 10):
    scene = preload(scene_path)
    for i in range(initial_size):
        var obj = scene.instantiate()
        obj.visible = false
        add_child(obj)
        pool.append(obj)

func get_object():
    if pool.is_empty():
        var obj = scene.instantiate()
        add_child(obj)
        return obj
    else:
        var obj = pool.pop_back()
        obj.visible = true
        return obj

func return_object(obj: Node):
    obj.visible = false
    pool.append(obj)

11.3 练习

练习11.1:实现一个子弹对象池,支持回收和复用,对比使用对象池前后的性能差异。


第12章:设计模式在 GDScript 中的应用

12.1 状态模式

extends CharacterBody2D

var state_machine: StateMachine

func _ready():
    state_machine = StateMachine.new(self)
    state_machine.add_state("idle", IdleState.new())
    state_machine.add_state("run", RunState.new())
    state_machine.add_state("jump", JumpState.new())
    state_machine.change_to("idle")

func _physics_process(delta):
    state_machine.update(delta)

# 状态基类
class State:
    var character: CharacterBody2D
    func enter(): pass
    func exit(): pass
    func update(delta): pass

# 具体状态
class IdleState extends State:
    func enter():
        character.velocity = Vector2.ZERO
    func update(delta):
        if Input.get_axis("ui_left", "ui_right") != 0:
            state_machine.change_to("run")
        if Input.is_action_just_pressed("ui_accept"):
            state_machine.change_to("jump")

12.2 观察者模式

# 使用信号实现观察者模式
class Subject:
    signal value_changed(new_value)
    var _value: int = 0
    
    func set_value(v: int):
        _value = v
        value_changed.emit(v)

class Observer:
    func on_value_changed(v: int):
        print("值变为: ", v)

# 使用
var subject = Subject.new()
var observer = Observer.new()
subject.value_changed.connect(observer.on_value_changed)
subject.set_value(42)

12.3 练习

练习12.1:使用状态模式实现一个敌人 AI,包含巡逻、追逐、攻击和逃跑四种状态。


第三部分:高级篇

第13章:自定义资源

13.1 创建自定义资源

# ItemResource.gd
extends Resource
class_name ItemResource

@export var item_name: String = ""
@export var description: String = ""
@export var icon: Texture2D
@export var value: int = 0
@export var item_type: ItemType

enum ItemType {CONSUMABLE, EQUIPMENT, QUEST, MATERIAL}

func use(target: Node) -> bool:
    # 具体使用效果由子类实现
    return false

13.2 练习

练习13.1:创建一个完整的物品系统,包括武器、防具、消耗品等自定义资源,并实现背包系统。


第14章:工具脚本与编辑器集成

14.1 @tool 脚本

@tool
extends Node2D

@export var radius: float = 50.0:
    set(value):
        radius = value
        queue_redraw()

func _draw():
    draw_circle(Vector2.ZERO, radius, Color.YELLOW)

func _ready():
    if Engine.is_editor_hint():
        print("运行在编辑器中")
    else:
        print("运行在游戏中")

14.2 练习

练习14.1:创建一个自定义的路径绘制工具,在编辑器中可视化显示 NPC 巡逻路径。


第15章:网络编程

15.1 基础网络

extends Node

var peer: ENetMultiplayerPeer

func create_server():
    peer = ENetMultiplayerPeer.new()
    peer.create_server(4242)
    multiplayer.multiplayer_peer = peer
    multiplayer.peer_connected.connect(_on_player_connected)
    print("服务器已启动")

func join_server(ip: String = "127.0.0.1"):
    peer = ENetMultiplayerPeer.new()
    peer.create_client(ip, 4242)
    multiplayer.multiplayer_peer = peer

@rpc
func send_message(content: String):
    print("收到消息: ", content)

15.2 练习

练习15.1:实现一个简单的多人游戏,玩家可以移动和发射子弹,其他玩家实时看到。


第16章:完整游戏项目实战

16.1 项目规划

让我们创建一个完整的“吸血鬼幸存者”风格的俯视角射击游戏:

核心功能:

  • 玩家移动和射击
  • 敌人 AI(追逐玩家)
  • 经验值和升级系统
  • 多种武器和技能
  • Boss 战

16.2 实现要点

# 玩家控制器
extends CharacterBody2D

@export var speed: float = 300.0
var health: int = 100
var experience: int = 0
var level: int = 1

func _physics_process(delta):
    var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
    velocity = input_dir * speed
    move_and_slide()
    
    # 面向鼠标方向
    look_at(get_global_mouse_position())

func take_damage(amount: int):
    health -= amount
    if health <= 0:
        die()
    else:
        # 无敌帧和闪白效果
        modulate = Color.RED
        await get_tree().create_timer(0.1).timeout
        modulate = Color.WHITE

16.3 完整项目结构

res://
├── scenes/
│   ├── Main.tscn
│   ├── Player.tscn
│   ├── Enemy.tscn
│   ├── Weapon.tscn
│   └── UI.tscn
├── scripts/
│   ├── Player.gd
│   ├── Enemy.gd
│   ├── Weapon.gd
│   ├── ExperienceGem.gd
│   └── UpgradeSystem.gd
├── resources/
│   ├── weapons/
│   ├── enemies/
│   └── upgrades/
├── assets/
│   ├── sprites/
│   └── sounds/
└── autoload/
    ├── Global.gd
    └── GameManager.gd

附录

附录A:常见错误与解决方案

错误 原因 解决方法
"Invalid call. Nonexistent function" 函数名拼写错误或作用域错误 检查函数名,确认对象类型
"Attempt to call function in base" 节点类型转换错误 使用 asis_instance_of() 检查
"Resourceloader: Cannot load file" 路径错误 使用 res:// 绝对路径
"Queue_free() on null instance" 对象已被删除 使用 is_instance_valid() 检查

附录B:GDScript 2.0 新特性

  • @export 变量的改进
  • 匿名函数(Lambda)
  • await 关键字替代 yield
  • 类型化数组 Array[Type]
  • 字符串格式化增强

附录C:推荐的资源

  • 官方文档:docs.godotengine.org
  • GDQuest 教程:gdquest.com
  • Godot 演示项目:github.com/godotengine/godot-demo-projects
  • Reddit 社区:r/godot

结语

恭喜你完成了《GDScript 从浅入深》的学习之旅!

从第一行的 "Hello World",到复杂的网络游戏架构,你已经掌握了使用 GDScript 开发游戏的核心技能。但请记住,编程是一项实践技能——只有不断地编写代码、犯错、调试、改进,才能真正的进步。

GDScript 的设计哲学是让你专注于游戏逻辑本身,而不是与语言搏斗。随着你的深入,你会发现它的简洁背后蕴藏着强大的表现力。

现在,是时候去创造你自己的游戏了。从一个小项目开始,比如一个简单的平台跳跃游戏,或者一个休闲的益智游戏。使用你学到的知识:用信号来解耦组件,用状态机来管理复杂的角色行为,用自动加载来管理全局状态。

如果你在开发中遇到问题,Godot 社区非常友好和乐于助人。不要害怕提问,也不要害怕犯错——每一个错误都是学习的机会。

最后,记住:游戏开发的终极目标是创造乐趣。技术只是实现这个目标的工具。享受编码的过程,也享受创造的乐趣。

祝你在 Godot 和 GDScript 的世界中玩得开心!

posted @ 2026-04-05 20:41  Ceyase  阅读(1)  评论(0)    收藏  举报