第九章:面向对象编程
"面向对象编程不是关于类和继承,而是关于如何组织和管理复杂性。"
面向对象编程(OOP)是现代软件开发的基础范式之一。GDScript是一门面向对象的语言,与Godot的节点系统完美结合。本章将深入探讨OOP在GDScript中的实现。
9.1 面向对象基础
9.1.1 什么是面向对象
面向对象编程的核心概念:
| 概念 | 说明 | GDScript示例 |
|---|---|---|
| 封装 | 将数据和操作数据的方法绑定在一起 | 类和成员变量 |
| 继承 | 从已有类创建新类 | extends关键字 |
| 多态 | 不同对象对同一消息的不同响应 | 虚函数重写 |
| 抽象 | 隐藏复杂性,暴露简单接口 | 私有成员,公共API |
9.1.2 GDScript中的对象
在GDScript中,几乎所有东西都是对象:
# 节点是对象
var player = get_node("Player")
# 资源是对象
var texture = load("res://icon.png")
# 内置类型也有对象的特性
var vector = Vector2(10, 20)
var length = vector.length() # 调用方法
9.1.3 脚本即类
每个GDScript文件本质上就是一个类定义:
# player.gd - 这个文件定义了一个类
extends CharacterBody2D
var health: int = 100
var speed: float = 200.0
func move(direction: Vector2):
velocity = direction * speed
move_and_slide()
func take_damage(amount: int):
health -= amount
9.2 类的定义
9.2.1 基本类定义
# 简单类(不继承任何节点)
# item.gd
class_name Item
extends RefCounted # 或省略extends,默认继承RefCounted
var name: String
var value: int
var stackable: bool
func _init(p_name: String = "", p_value: int = 0):
name = p_name
value = p_value
stackable = true
func get_description() -> String:
return "%s (价值: %d)" % [name, value]
9.2.2 class_name关键字
# 定义全局类名
class_name Player
extends CharacterBody2D
# 现在可以在任何地方使用Player类型
var player: Player
var new_player = Player.new() # 创建实例(仅限非节点类)
9.2.3 内部类
# 在脚本内定义类
class_name Inventory
extends RefCounted
# 内部类
class Slot:
var item: Item
var count: int
func _init():
item = null
count = 0
func is_empty() -> bool:
return item == null
# 主类的成员
var slots: Array[Slot] = []
func _init(size: int = 10):
for i in range(size):
slots.append(Slot.new())
9.2.4 构造函数
class_name Character
extends RefCounted
var name: String
var level: int
var stats: Dictionary
# 构造函数
func _init(p_name: String = "Unknown", p_level: int = 1):
name = p_name
level = p_level
stats = {
"health": 100 + level * 10,
"attack": 10 + level * 2
}
print("创建角色:", name)
# 使用
var hero = Character.new("Hero", 5)
var npc = Character.new("Villager") # 使用默认level
9.3 成员变量
9.3.1 实例变量
extends Node
# 实例变量 - 每个实例都有自己的副本
var health: int = 100
var position: Vector2 = Vector2.ZERO
var inventory: Array = []
func _ready():
print(health) # 每个实例可以有不同的值
9.3.2 导出变量
extends Node
# 基本导出
@export var speed: float = 100.0
@export var player_name: String = "Player"
# 带范围的导出
@export_range(0, 100) var health: int = 100
@export_range(0.0, 10.0, 0.1) var damage_multiplier: float = 1.0
# 枚举导出
@export_enum("Easy", "Normal", "Hard") var difficulty: int = 1
# 文件路径导出
@export_file("*.png") var texture_path: String
@export_dir var save_directory: String
# 颜色导出
@export var tint_color: Color = Color.WHITE
# 节点路径导出
@export var target_node: NodePath
# 资源导出
@export var character_texture: Texture2D
@export var audio_effect: AudioStream
# 数组导出
@export var items: Array[String] = []
@export var spawn_points: Array[Vector2] = []
# 分组导出
@export_group("Movement")
@export var walk_speed: float = 100.0
@export var run_speed: float = 200.0
@export_group("Combat")
@export var attack_damage: int = 10
@export var defense: int = 5
# 子分组
@export_subgroup("Advanced")
@export var critical_chance: float = 0.1
9.3.3 @onready变量
extends Node2D
# @onready在_ready()之前、所有@export之后初始化
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var animation_player: AnimationPlayer = $AnimationPlayer
# 复杂初始化
@onready var max_health: int = calculate_max_health()
@onready var screen_size: Vector2 = get_viewport_rect().size
func _ready():
# 此时所有@onready变量都已初始化
sprite.texture = load("res://player.png")
9.3.4 静态变量
class_name GameManager
extends Node
# 静态变量 - 所有实例共享
static var instance_count: int = 0
static var high_score: int = 0
func _init():
instance_count += 1
static func get_instance_count() -> int:
return instance_count
static func set_high_score(score: int):
if score > high_score:
high_score = score
9.4 方法
9.4.1 实例方法
class_name Player
extends CharacterBody2D
var health: int = 100
var max_health: int = 100
# 实例方法 - 可以访问self和实例变量
func take_damage(amount: int) -> void:
health = max(0, health - amount)
if health == 0:
die()
func heal(amount: int) -> void:
health = min(max_health, health + amount)
func die() -> void:
print("玩家死亡")
queue_free()
# 获取器和设置器
func get_health_percent() -> float:
return float(health) / float(max_health)
9.4.2 静态方法
class_name Utils
# 静态方法 - 不需要实例即可调用
static func clamp_vector(v: Vector2, max_length: float) -> Vector2:
if v.length() > max_length:
return v.normalized() * max_length
return v
static func random_point_in_circle(radius: float) -> Vector2:
var angle = randf() * TAU
var r = sqrt(randf()) * radius
return Vector2(cos(angle), sin(angle)) * r
static func format_time(seconds: float) -> String:
var minutes = int(seconds / 60)
var secs = int(seconds) % 60
return "%02d:%02d" % [minutes, secs]
# 使用
var pos = Utils.random_point_in_circle(100.0)
var time_str = Utils.format_time(125.5) # "02:05"
9.4.3 虚方法
# base_enemy.gd
class_name BaseEnemy
extends CharacterBody2D
var health: int = 100
# 虚方法 - 可以被子类重写
func attack():
print("基础攻击")
func take_damage(amount: int):
health -= amount
on_damage_taken(amount) # 调用可重写的钩子
if health <= 0:
die()
# 钩子方法 - 期望被重写
func on_damage_taken(amount: int):
pass # 默认什么都不做
func die():
queue_free()
# goblin.gd
class_name Goblin
extends BaseEnemy
func attack():
print("哥布林猛击!")
# 可以调用父类方法
# super.attack()
func on_damage_taken(amount: int):
print("哥布林受到", amount, "点伤害,发出尖叫!")
9.4.4 getter和setter
extends Node
var _health: int = 100
# 使用属性语法
var health: int:
get:
return _health
set(value):
_health = clampi(value, 0, max_health)
health_changed.emit(_health)
var max_health: int = 100
# 只读属性
var is_alive: bool:
get:
return _health > 0
# 计算属性
var health_percent: float:
get:
return float(_health) / float(max_health)
signal health_changed(new_health: int)
9.5 访问控制
9.5.1 命名约定
GDScript没有严格的访问修饰符,使用命名约定:
class_name Player
extends Node
# 公共成员 - 可以从外部访问
var health: int = 100
var position: Vector2
# 私有成员 - 约定以下划线开头
var _internal_state: int = 0
var _cached_data: Dictionary = {}
# 公共方法
func move(direction: Vector2):
_update_position(direction)
_check_collision()
# 私有方法
func _update_position(direction: Vector2):
position += direction
func _check_collision():
pass
9.5.2 封装实践
class_name BankAccount
extends RefCounted
var _balance: float = 0.0
var _transaction_history: Array = []
# 公共接口
func deposit(amount: float) -> bool:
if amount <= 0:
return false
_balance += amount
_record_transaction("deposit", amount)
return true
func withdraw(amount: float) -> bool:
if amount <= 0 or amount > _balance:
return false
_balance -= amount
_record_transaction("withdraw", amount)
return true
func get_balance() -> float:
return _balance
func get_statement() -> Array:
return _transaction_history.duplicate()
# 私有方法
func _record_transaction(type: String, amount: float):
_transaction_history.append({
"type": type,
"amount": amount,
"timestamp": Time.get_unix_time_from_system()
})
9.6 对象创建与销毁
9.6.1 创建对象
# 创建非节点对象(RefCounted或Object子类)
var item = Item.new()
var character = Character.new("Hero", 10)
# 创建节点对象
var node = Node.new()
var sprite = Sprite2D.new()
add_child(sprite) # 添加到场景树
# 实例化场景
var enemy_scene = preload("res://scenes/enemy.tscn")
var enemy = enemy_scene.instantiate()
add_child(enemy)
# 运行时加载
var scene = load("res://scenes/player.tscn")
var player = scene.instantiate()
9.6.2 销毁对象
# RefCounted对象 - 自动垃圾回收
var item = Item.new()
item = null # 引用计数归零后自动释放
# 节点对象 - 需要手动释放
var sprite = Sprite2D.new()
sprite.queue_free() # 在帧结束时安全删除
# 或立即删除(谨慎使用)
sprite.free()
# 删除所有子节点
for child in get_children():
child.queue_free()
9.6.3 对象生命周期
extends Node
func _init():
# 最早调用,对象刚创建
print("1. _init")
func _enter_tree():
# 进入场景树
print("2. _enter_tree")
func _ready():
# 节点和子节点都准备好
print("3. _ready")
func _exit_tree():
# 退出场景树
print("4. _exit_tree")
# 对于RefCounted对象
func _notification(what):
if what == NOTIFICATION_PREDELETE:
print("对象即将被删除")
9.7 组合与聚合
9.7.1 组合模式
# 组合:部分不能脱离整体存在
class_name Character
extends Node2D
var _health_component: HealthComponent
var _movement_component: MovementComponent
func _init():
_health_component = HealthComponent.new()
_movement_component = MovementComponent.new()
func take_damage(amount: int):
_health_component.take_damage(amount)
func move(direction: Vector2):
_movement_component.move(self, direction)
# 组件类
class HealthComponent:
var health: int = 100
var max_health: int = 100
func take_damage(amount: int):
health = max(0, health - amount)
func is_alive() -> bool:
return health > 0
class MovementComponent:
var speed: float = 100.0
func move(entity: Node2D, direction: Vector2):
entity.position += direction * speed
9.7.2 聚合模式
# 聚合:部分可以独立于整体存在
class_name Team
extends RefCounted
var _members: Array[Player] = []
func add_member(player: Player):
if player not in _members:
_members.append(player)
func remove_member(player: Player):
_members.erase(player)
func get_total_health() -> int:
var total = 0
for member in _members:
total += member.health
return total
# Player可以独立存在,可以加入或离开Team
9.7.3 依赖注入
# 不好的做法:硬编码依赖
class_name GameManager
extends Node
var _audio: AudioManager
func _ready():
_audio = AudioManager.new() # 紧耦合
# 好的做法:依赖注入
class_name GameManager
extends Node
var _audio: AudioManagerInterface
func setup(audio: AudioManagerInterface):
_audio = audio
func play_sound(sound_name: String):
if _audio:
_audio.play(sound_name)
# 接口/基类
class AudioManagerInterface:
func play(sound_name: String):
pass
9.8 设计模式简介
9.8.1 单例模式
# 使用autoload实现单例
# 在项目设置中添加为autoload,命名为GameManager
class_name GameManager
extends Node
var score: int = 0
var high_score: int = 0
func add_score(points: int):
score += points
if score > high_score:
high_score = score
# 使用
func _ready():
GameManager.add_score(100)
print(GameManager.score)
9.8.2 工厂模式
class_name EnemyFactory
static func create(type: String, pos: Vector2) -> Enemy:
var enemy: Enemy
match type:
"goblin":
enemy = Goblin.new()
"orc":
enemy = Orc.new()
"boss":
enemy = Boss.new()
_:
enemy = BasicEnemy.new()
enemy.position = pos
return enemy
# 使用
var goblin = EnemyFactory.create("goblin", Vector2(100, 200))
9.8.3 观察者模式
# 使用信号实现观察者模式
class_name Subject
extends Node
signal value_changed(new_value)
var _value: int = 0
var value: int:
get:
return _value
set(v):
_value = v
value_changed.emit(v)
# 观察者
class_name Observer
extends Node
func _ready():
var subject = get_node("/root/Subject")
subject.value_changed.connect(_on_value_changed)
func _on_value_changed(new_value: int):
print("值变为:", new_value)
本章小结
本章我们深入学习了GDScript的面向对象编程:
- OOP基础:封装、继承、多态、抽象的概念
- 类定义:class_name、内部类、构造函数
- 成员变量:实例变量、导出变量、@onready、静态变量
- 方法:实例方法、静态方法、虚方法、getter/setter
- 访问控制:命名约定、封装实践
- 对象生命周期:创建、销毁、生命周期回调
- 组合与聚合:不同的对象关系、依赖注入
- 设计模式:单例、工厂、观察者
面向对象编程是组织复杂代码的强大工具。下一章我们将深入学习继承机制,这是OOP最重要的特性之一。
上一章:函数与方法
下一章:类与继承