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

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 解释器

脚本创建与附加

创建脚本的三种方式

  1. 菜单创建:选中节点 → Inspector 底部 → “Attach Script”
  2. 快捷键:选中节点 → 按 Ctrl+Shift+N
  3. 文件系统:在 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]

扩展阅读