第九章:面向对象编程

第九章:面向对象编程

"面向对象编程不是关于类和继承,而是关于如何组织和管理复杂性。"

面向对象编程(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的面向对象编程:

  1. OOP基础:封装、继承、多态、抽象的概念
  2. 类定义:class_name、内部类、构造函数
  3. 成员变量:实例变量、导出变量、@onready、静态变量
  4. 方法:实例方法、静态方法、虚方法、getter/setter
  5. 访问控制:命名约定、封装实践
  6. 对象生命周期:创建、销毁、生命周期回调
  7. 组合与聚合:不同的对象关系、依赖注入
  8. 设计模式:单例、工厂、观察者

面向对象编程是组织复杂代码的强大工具。下一章我们将深入学习继承机制,这是OOP最重要的特性之一。


上一章:函数与方法

下一章:类与继承

← 返回目录