Godot 3 GDScript 教程 / GDScript 基础语法
GDScript 基础语法
GDScript 概述
GDScript 是 Godot 引擎的内置脚本语言,语法受 Python 启发,专为游戏开发优化。它与 Godot 的节点系统深度集成,语法简洁、学习曲线平缓。
GDScript vs Python
| 特性 | GDScript | Python |
|---|---|---|
| 缩进语法 | ✅ 使用 Tab | ✅ 使用空格 |
| 类型系统 | 可选类型注解 | 可选类型注解 |
| 继承 | extends | class MyClass(Base) |
| 构造函数 | _init() | __init__() |
| 空值 | null | None |
| 字典 | {} | {} |
| 数组 | [] | [] |
| 字符串格式化 | "Hello %s" % name | f"Hello {name}" |
| 运行环境 | Godot 引擎 | CPython 解释器 |
脚本创建与附加
创建脚本的三种方式
- 菜单创建:选中节点 → Inspector 底部 → “Attach Script”
- 快捷键:选中节点 → 按
Ctrl+Shift+N - 文件系统:在 FileSystem 面板右键 → New → GDScript
脚本结构模板
extends Node # 继承自哪个节点类型
# 类变量
var health: int = 100
var speed: float = 200.0
# 生命周期函数
func _ready() -> void:
pass # 节点进入场景树时调用
func _process(delta: float) -> void:
pass # 每帧调用
func _physics_process(delta: float) -> void:
pass # 每个物理帧调用
⚠️ 注意:每个 .gd 文件只能有一个 extends 声明,且必须在文件开头。
变量声明
var 关键字
# 基本变量声明
var health = 100 # 类型推断为 int
var name = "Player" # 类型推断为 String
var position = Vector2(100, 200) # 类型推断为 Vector2
# 带类型注解的声明(推荐)
var health: int = 100
var speed: float = 250.0
var is_alive: bool = true
var player_name: String = "Hero"
var direction: Vector2 = Vector2.ZERO
# 无初始值(默认为 null)
var target: Node2D
var score: int
const 常量
# 常量必须在声明时初始化,之后不可修改
const MAX_HEALTH: int = 100
const GRAVITY: float = 980.0
const PLAYER_SPEED: float = 300.0
# 常量可以是复杂类型
const ENEMY_SCENES = {
"slime": preload("res://scenes/Slime.tscn"),
"goblin": preload("res://scenes/Goblin.tscn"),
}
# 常量表达式
const HALF_MAX: int = MAX_HEALTH / 2 # = 50
⚠️ 注意:const 仅限基本类型和 preload 的资源,不能使用运行时计算的值。
变量作用域
extends Node
var class_var: int = 10 # 类变量(成员变量),整个脚本可访问
func _ready() -> void:
var local_var: int = 20 # 局部变量,仅在 _ready 内可用
print(class_var) # ✅ 可访问
print(local_var) # ✅ 可访问
func _process(delta: float) -> void:
print(class_var) # ✅ 可访问
# print(local_var) # ❌ 错误!local_var 在此作用域不存在
数据类型详解
基本类型
| 类型 | 说明 | 示例 |
|---|---|---|
int | 整数 | 42, -10, 0xFF |
float | 浮点数 | 3.14, -0.5, 1e10 |
bool | 布尔值 | true, false |
String | 字符串 | "Hello", 'World' |
向量与数学类型
| 类型 | 说明 | 示例 |
|---|---|---|
Vector2 | 2D 向量 | Vector2(1, 2) |
Vector3 | 3D 向量 | Vector3(1, 2, 3) |
Rect2 | 2D 矩形 | Rect2(0, 0, 100, 100) |
Transform2D | 2D 变换 | 位移+旋转+缩放 |
Transform | 3D 变换 | 位移+旋转+缩放 |
Color | 颜色 | Color(1, 0, 0), Color.red |
容器类型
# 数组 Array
var items: Array = ["sword", "shield", "potion"]
items.append("bow") # 添加元素
items.erase("shield") # 删除元素
var first = items[0] # 访问元素
var count = items.size() # 获取大小
items.sort() # 排序
items.shuffle() # 随机打乱
# 类型化数组
var scores: Array[int] = [100, 200, 300]
# 字典 Dictionary
var player = {
"name": "Hero",
"health": 100,
"level": 1,
}
player["health"] = 80 # 修改值
player["attack"] = 25 # 添加新键
var hp = player.get("health", 0) # 安全获取,带默认值
player.erase("level") # 删除键
# 遍历字典
for key in player:
print("%s: %s" % [key, player[key]])
# 检查键是否存在
if player.has("health"):
print("生命值: ", player["health"])
类型推断与类型注解
类型推断
# Godot 自动推断类型
var x = 10 # 推断为 int
var y = 3.14 # 推断为 float
var name = "Godot" # 推断为 String
显式类型注解(推荐)
# 明确声明类型有助于代码可读性和错误检查
var health: int = 100
var speed: float = 200.0
var name: String = "Player"
var direction: Vector2 = Vector2.RIGHT
# 函数参数和返回值类型
func take_damage(amount: int) -> bool:
health -= amount
return health <= 0
💡 提示:开启类型注解后,编辑器可以提供更好的自动补全和错误检测。
运算符
算术运算符
var a = 10
var b = 3
var sum = a + b # 13 加法
var diff = a - b # 7 减法
var prod = a * b # 30 乘法
var quot = a / b # 3 整数除法
var quot_f = 10.0 / 3 # 3.33 浮点除法
var rem = a % b # 1 取余
var power = pow(2, 10) # 1024 幂运算
比较运算符
var x = 5
x == 5 # true 等于
x != 3 # true 不等于
x > 3 # true 大于
x < 10 # true 小于
x >= 5 # true 大于等于
x <= 4 # false 小于等于
逻辑运算符
var a = true
var b = false
a and b # false 逻辑与
a or b # true 逻辑或
not a # false 逻辑非
# 短路求值
if health > 0 and is_alive: # 如果 health <= 0,不会检查 is_alive
print("角色存活")
位运算符
var flags = 0b1010 # 10
flags | 0b0001 # 0b1011 = 11 按位或
flags & 0b1100 # 0b1000 = 8 按位与
flags ^ 0b1111 # 0b0101 = 5 按位异或
~flags # 按位取反
flags << 2 # 左移2位
flags >> 1 # 右移1位
赋值运算符
var x = 10
x += 5 # x = x + 5 = 15
x -= 3 # x = x - 3 = 12
x *= 2 # x = x * 2 = 24
x /= 4 # x = x / 4 = 6
x %= 4 # x = x % 4 = 2
向量运算
var a = Vector2(1, 2)
var b = Vector2(3, 4)
var sum = a + b # Vector2(4, 6)
var diff = a - b # Vector2(-2, -2)
var scaled = a * 2.0 # Vector2(2, 4)
var length = a.length() # 2.236
var normalized = a.normalized() # 单位向量
var dist = a.distance_to(b) # 距离
var dot = a.dot(b) # 点积
字符串格式化
% 格式化(类似 C 的 printf)
var name = "Hero"
var level = 10
var hp = 85.5
# %s - 字符串
# %d - 整数
# f - 浮点数
# %02d - 补零的整数
print("玩家: %s, 等级: %d, 生命: %.1f%%" % [name, level, hp])
# 输出: 玩家: Hero, 等级: 10, 生命: 85.5%
var time = 125
print("%02d:%02d" % [time / 60, time % 60])
# 输出: 02:05
字符串常用方法
var text = "Hello, Godot!"
text.length() # 13
text.to_upper() # "HELLO, GODOT!"
text.to_lower() # "hello, godot!"
text.begins_with("Hello") # true
text.ends_with("!") # true
text.find("Godot") # 7
text.replace("Godot", "World") # "Hello, World!"
text.substr(0, 5) # "Hello"
text.split(", ") # ["Hello", "Godot!"]
# 字符串连接
var greeting = "Hello" + ", " + "World" # 拼接
var greeting2 = "Hello, %s" % "World" # 格式化
# 多行字符串
var dialogue = """这是一段
跨越多行的
对话文本。"""
注释
# 这是单行注释
"""
这是多行注释(文档字符串)
通常用于函数说明
"""
# === 区域标记 ===
# 可以在编辑器中折叠
# region 敌人逻辑
func spawn_enemy() -> void:
pass
func kill_enemy() -> void:
pass
# endregion
文档注释
# 计算伤害值
# @param base_damage 基础伤害
# @param multiplier 伤害倍率
# @return 最终伤害值
func calculate_damage(base_damage: int, multiplier: float) -> int:
return int(base_damage * multiplier)
代码规范
命名约定
| 元素 | 约定 | 示例 |
|---|---|---|
| 变量 | snake_case | player_health |
| 常量 | SCREAMING_SNAKE_CASE | MAX_HEALTH |
| 函数 | snake_case | calculate_damage() |
| 信号 | snake_case | health_changed |
| 类名 | PascalCase | EnemyController |
| 枚举 | PascalCase | EnemyState |
| 枚举值 | SCREAMING_SNAKE_CASE | IDLE, WALK |
代码风格
# ✅ 推荐风格
extends KinematicBody2D
var speed: float = 200.0
var health: int = 100
signal died()
signal health_changed(new_health: int)
func _ready() -> void:
pass
func take_damage(amount: int) -> void:
health -= amount
emit_signal("health_changed", health)
if health <= 0:
die()
func die() -> void:
emit_signal("died")
queue_free()
脚本生命周期
Godot 节点有一系列内置的生命周期回调函数:
节点创建
│
├── _init() # 构造函数
│
├── _enter_tree() # 进入场景树
│ └── _ready() # 子节点都已就绪(从下往上)
│
├── _process(delta) # 每帧调用(逻辑更新)
├── _physics_process(delta) # 每物理帧调用(物理更新)
├── _input(event) # 输入事件
│
├── _exit_tree() # 离开场景树
└── _notification() # 通知回调
各回调详解
extends Node2D
# 构造函数(场景实例化时调用)
# 此时节点还未加入场景树,无法访问子节点
func _init() -> void:
print("_init: 节点已创建")
# 进入场景树时调用(每次加入树都会调用)
func _enter_tree() -> void:
print("_enter_tree: 进入场景树")
# 子节点的 _ready 已全部完成
# 适合初始化逻辑(最常用的初始化位置)
func _ready() -> void:
print("_ready: 节点就绪")
# 此时可以安全地使用 $ChildNode 访问子节点
# 每帧调用(参数 delta 是上一帧到这一帧的时间差,单位秒)
func _process(delta: float) -> void:
# 用于非物理相关的逻辑:动画、UI更新、AI决策等
position.x += 100 * delta
# 每个物理帧调用(默认每秒60次)
func _physics_process(delta: float) -> void:
# 用于物理相关的逻辑:移动、碰撞检测
move_and_slide(Vector2(200, 0))
# 输入事件回调
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"):
print("按下了确认键")
# 离开场景树时调用
func _exit_tree() -> void:
print("_exit_tree: 离开场景树")
# Godot 通知回调
func _notification(what: int) -> void:
match what:
NOTIFICATION_PAUSED:
print("游戏暂停")
NOTIFICATION_UNPAUSED:
print("游戏恢复")
delta 的使用
func _process(delta: float) -> void:
# ❌ 错误:帧率不同时移动速度不一致
position.x += 5
# ✅ 正确:乘以 delta 使移动速度与帧率无关
# 无论 60fps 还是 144fps,每秒都移动 300 像素
position.x += 300 * delta
⚠️ 注意:delta 单位是秒(如 1/60 ≈ 0.0167),乘以速度后得到每帧的位移量。
_process vs _physics_process
| 特性 | _process | _physics_process |
|---|---|---|
| 调用频率 | 与帧率相同(可变) | 固定(默认 60fps) |
| 用途 | 视觉效果、UI、动画 | 物理、移动、碰撞 |
| 受帧率影响 | ✅ 是 | ❌ 否 |
| delta | 变化 | 固定 |
💡 提示:
- 纯视觉逻辑(动画、UI 更新)用
_process - 物理相关逻辑(移动、碰撞)用
_physics_process - 两者都可以使用,但职责要分离
游戏开发场景
场景:一个简单的计时器系统
在许多游戏中,需要计时功能,如关卡时间限制、技能冷却、Boss 战阶段切换等。
extends Node
# 计时器变量
var elapsed_time: float = 0.0
var is_running: bool = false
# 信号
signal time_updated(current_time: float)
signal time_up()
# 配置
export var countdown_time: float = 60.0 # 倒计时秒数
export var is_countdown: bool = true # 是否倒计时模式
func _process(delta: float) -> void:
if not is_running:
return
if is_countdown:
elapsed_time -= delta
if elapsed_time <= 0.0:
elapsed_time = 0.0
is_running = false
emit_signal("time_up")
else:
elapsed_time += delta
emit_signal("time_updated", elapsed_time)
func start_timer() -> void:
if is_countdown:
elapsed_time = countdown_time
else:
elapsed_time = 0.0
is_running = true
func stop_timer() -> void:
is_running = false
func get_time_string() -> String:
var minutes = int(elapsed_time) / 60
var seconds = int(elapsed_time) % 60
return "%02d:%02d" % [minutes, seconds]