第十章:类与继承

第十章:类与继承

"继承让我们站在巨人的肩膀上,复用已有的代码来构建新的功能。"

继承是面向对象编程的核心机制之一,它允许我们基于已有的类创建新类,实现代码复用和多态。本章将深入探讨GDScript中的继承机制。


10.1 继承基础

10.1.1 extends关键字

# 继承内置类
extends Node2D
extends CharacterBody2D
extends Control

# 继承自定义类(通过路径)
extends "res://scripts/base_enemy.gd"

# 继承命名类
extends BaseEnemy  # 如果BaseEnemy有class_name声明

10.1.2 继承层次

# 所有类的继承链
# Object → RefCounted/Node → ... → 你的类

# 示例继承链
# Object → Node → CanvasItem → Node2D → Sprite2D → YourSprite

Godot类层次示例:

Object
├── RefCounted
│   ├── Resource
│   │   ├── Texture2D
│   │   └── PackedScene
│   └── 自定义数据类
└── Node
    ├── CanvasItem
    │   ├── Node2D
    │   │   ├── Sprite2D
    │   │   └── CharacterBody2D
    │   └── Control
    │       ├── Button
    │       └── Label
    └── Node3D
        ├── MeshInstance3D
        └── Camera3D

10.1.3 简单继承示例

# base_character.gd
class_name BaseCharacter
extends CharacterBody2D

var health: int = 100
var max_health: int = 100
var speed: float = 100.0

func take_damage(amount: int) -> void:
    health = max(0, health - amount)
    if health == 0:
        die()

func die() -> void:
    queue_free()

func move(direction: Vector2) -> void:
    velocity = direction * speed
    move_and_slide()

# player.gd - 继承BaseCharacter
class_name Player
extends BaseCharacter

var score: int = 0

func _ready():
    max_health = 150  # 玩家有更多生命
    health = max_health

func collect_item(value: int) -> void:
    score += value

10.2 方法重写

10.2.1 重写父类方法

# 基类
class_name Enemy
extends CharacterBody2D

func attack() -> void:
    print("敌人攻击")

func die() -> void:
    print("敌人死亡")
    queue_free()

# 派生类
class_name Goblin
extends Enemy

# 重写attack方法
func attack() -> void:
    print("哥布林使用匕首攻击!")

# 重写die方法
func die() -> void:
    print("哥布林发出尖叫倒下了!")
    drop_loot()
    queue_free()

func drop_loot() -> void:
    print("掉落金币")

10.2.2 调用父类方法(super)

class_name Boss
extends Enemy

var phase: int = 1

# 使用super调用父类方法
func attack() -> void:
    super.attack()  # 先执行父类的attack
    if phase >= 2:
        special_attack()

func die() -> void:
    if phase < 3:
        phase += 1
        health = max_health  # 进入下一阶段
        print("Boss进入第", phase, "阶段!")
    else:
        super.die()  # 真正死亡

func special_attack() -> void:
    print("Boss释放特殊攻击!")

10.2.3 重写内置回调

class_name CustomNode
extends Node2D

func _ready() -> void:
    # 重写_ready,可以调用super
    super._ready()  # 如果父类有实现
    print("CustomNode准备就绪")

func _process(delta: float) -> void:
    # 重写_process
    super._process(delta)
    custom_update(delta)

func _physics_process(delta: float) -> void:
    super._physics_process(delta)
    physics_update(delta)

func custom_update(delta: float) -> void:
    pass

func physics_update(delta: float) -> void:
    pass

10.3 多态

10.3.1 多态概念

# 基类定义接口
class_name Shape
extends RefCounted

func get_area() -> float:
    return 0.0

func get_perimeter() -> float:
    return 0.0

# 圆形
class_name Circle
extends Shape

var radius: float

func _init(r: float):
    radius = r

func get_area() -> float:
    return PI * radius * radius

func get_perimeter() -> float:
    return 2 * PI * radius

# 矩形
class_name Rectangle
extends Shape

var width: float
var height: float

func _init(w: float, h: float):
    width = w
    height = h

func get_area() -> float:
    return width * height

func get_perimeter() -> float:
    return 2 * (width + height)

10.3.2 多态使用

# 使用多态
func print_shape_info(shape: Shape) -> void:
    print("面积:", shape.get_area())
    print("周长:", shape.get_perimeter())

# 测试
var circle = Circle.new(5.0)
var rect = Rectangle.new(4.0, 6.0)

print_shape_info(circle)  # 使用Circle的实现
print_shape_info(rect)    # 使用Rectangle的实现

# 在数组中存储不同类型
var shapes: Array[Shape] = [
    Circle.new(3.0),
    Rectangle.new(2.0, 4.0),
    Circle.new(1.0)
]

for shape in shapes:
    print_shape_info(shape)

10.3.3 类型检查与转换

func process_entity(entity: Node) -> void:
    # 使用is检查类型
    if entity is Player:
        print("这是玩家")
        var player = entity as Player
        player.score += 10
    elif entity is Enemy:
        print("这是敌人")
        var enemy = entity as Enemy
        enemy.take_damage(10)
    elif entity is NPC:
        print("这是NPC")

# 安全转换
func try_attack(node: Node) -> void:
    var enemy = node as Enemy  # 如果不是Enemy返回null
    if enemy:
        enemy.take_damage(10)
    else:
        print("目标不是敌人")

10.4 抽象类与接口

10.4.1 模拟抽象类

GDScript没有正式的抽象类语法,但可以模拟:

# 抽象基类
class_name AbstractEnemy
extends CharacterBody2D

func _init():
    # 防止直接实例化
    assert(get_script() != AbstractEnemy, "不能直接实例化AbstractEnemy")

# 抽象方法 - 子类必须实现
func get_attack_damage() -> int:
    assert(false, "子类必须实现get_attack_damage()")
    return 0

func get_attack_range() -> float:
    assert(false, "子类必须实现get_attack_range()")
    return 0.0

# 具体方法
func attack(target: Node2D) -> void:
    if position.distance_to(target.position) <= get_attack_range():
        if target.has_method("take_damage"):
            target.take_damage(get_attack_damage())

10.4.2 接口模式

# 使用has_method模拟接口
class_name Damageable
# 这是一个"接口"类,定义方法签名

# 方法签名
# func take_damage(amount: int) -> void
# func get_health() -> int
# func is_alive() -> bool

# 使用接口
func apply_damage_to_all(entities: Array, damage: int) -> void:
    for entity in entities:
        if entity.has_method("take_damage"):
            entity.take_damage(damage)

# 实现接口的类
class_name Player
extends CharacterBody2D

var health: int = 100

func take_damage(amount: int) -> void:
    health -= amount

func get_health() -> int:
    return health

func is_alive() -> bool:
    return health > 0

10.4.3 使用组合替代继承

# 组件类
class_name HealthComponent
extends RefCounted

signal health_changed(new_health: int)
signal died

var health: int
var max_health: int

func _init(p_max_health: int = 100):
    max_health = p_max_health
    health = max_health

func take_damage(amount: int) -> void:
    health = max(0, health - amount)
    health_changed.emit(health)
    if health == 0:
        died.emit()

func heal(amount: int) -> void:
    health = min(max_health, health + amount)
    health_changed.emit(health)

# 使用组合而非继承
class_name Player
extends CharacterBody2D

var health_component: HealthComponent

func _ready():
    health_component = HealthComponent.new(150)
    health_component.died.connect(_on_died)

func take_damage(amount: int) -> void:
    health_component.take_damage(amount)

func _on_died() -> void:
    print("玩家死亡")
    queue_free()

10.5 Godot节点继承

10.5.1 继承节点类

# 继承Sprite2D创建自定义精灵
class_name AnimatedCharacter
extends Sprite2D

@export var animation_speed: float = 0.1
var current_frame: int = 0
var timer: float = 0.0

func _process(delta: float) -> void:
    timer += delta
    if timer >= animation_speed:
        timer = 0.0
        current_frame = (current_frame + 1) % hframes
        frame = current_frame

10.5.2 继承场景脚本

# base_weapon.gd - 附加到weapon.tscn
class_name BaseWeapon
extends Node2D

@export var damage: int = 10
@export var attack_speed: float = 1.0

signal attack_performed

func attack() -> void:
    # 基础攻击逻辑
    attack_performed.emit()

# sword.gd - 继承BaseWeapon,附加到sword.tscn(继承自weapon.tscn)
class_name Sword
extends BaseWeapon

func _ready():
    damage = 15  # 剑的伤害更高
    attack_speed = 0.8

func attack() -> void:
    super.attack()
    play_slash_effect()

func play_slash_effect() -> void:
    $AnimationPlayer.play("slash")

10.5.3 扩展内置节点

# 扩展Button添加自定义功能
class_name SoundButton
extends Button

@export var click_sound: AudioStream
@export var hover_sound: AudioStream

func _ready():
    pressed.connect(_on_pressed)
    mouse_entered.connect(_on_mouse_entered)

func _on_pressed() -> void:
    if click_sound:
        var player = AudioStreamPlayer.new()
        player.stream = click_sound
        add_child(player)
        player.play()
        player.finished.connect(player.queue_free)

func _on_mouse_entered() -> void:
    if hover_sound:
        # 播放悬停音效
        pass

10.6 继承最佳实践

10.6.1 何时使用继承

# 好的继承使用场景

# 1. "是一个"关系
# Player是一个Character
class_name Player
extends Character

# 2. 扩展内置类型
class_name CustomButton
extends Button

# 3. 共享大量代码
class_name Enemy
extends CharacterBody2D
# Goblin、Orc、Boss都继承Enemy

# 不好的继承使用场景

# 1. 仅为复用少量代码
# 应该使用组合

# 2. 多重继承需求
# GDScript不支持多重继承,使用组合

# 3. "有一个"关系
# Player"有一个"武器,不应该继承Weapon

10.6.2 继承 vs 组合

# 继承:Player是一个Character
class_name Player
extends Character

# 组合:Player有一个Weapon
class_name Player
extends Character

var weapon: Weapon

func equip(new_weapon: Weapon) -> void:
    weapon = new_weapon

func attack() -> void:
    if weapon:
        weapon.attack()

10.6.3 里氏替换原则

# 子类应该能够替换父类
# 不好的设计
class_name Bird
extends RefCounted

func fly() -> void:
    print("飞行")

class_name Penguin
extends Bird

func fly() -> void:
    # 企鹅不会飞!违反里氏替换原则
    assert(false, "企鹅不会飞")

# 更好的设计
class_name Bird
extends RefCounted

func move() -> void:
    pass

class_name FlyingBird
extends Bird

func move() -> void:
    fly()

func fly() -> void:
    print("飞行")

class_name Penguin
extends Bird

func move() -> void:
    swim()

func swim() -> void:
    print("游泳")

10.6.4 继承深度控制

# 避免过深的继承层次
# 不推荐:Object → Node → Node2D → Enemy → MeleeEnemy → Goblin → EliteGoblin → GoblinChief

# 推荐:保持2-3层继承
# Object → Node2D → Enemy
# 使用组合添加其他功能
class_name Enemy
extends Node2D

var behavior: EnemyBehavior
var combat: CombatComponent
var movement: MovementComponent

func _init(behavior_type: String):
    match behavior_type:
        "melee":
            behavior = MeleeBehavior.new()
        "ranged":
            behavior = RangedBehavior.new()

10.7 实际案例

10.7.1 敌人系统

# base_enemy.gd
class_name BaseEnemy
extends CharacterBody2D

signal died(enemy: BaseEnemy)

@export var max_health: int = 100
@export var move_speed: float = 100.0
@export var damage: int = 10

var health: int
var target: Node2D

func _ready():
    health = max_health
    add_to_group("enemies")

func take_damage(amount: int) -> void:
    health -= amount
    flash_red()
    if health <= 0:
        die()

func die() -> void:
    died.emit(self)
    drop_loot()
    queue_free()

func drop_loot() -> void:
    pass  # 子类实现

func flash_red() -> void:
    modulate = Color.RED
    await get_tree().create_timer(0.1).timeout
    modulate = Color.WHITE

# goblin.gd
class_name Goblin
extends BaseEnemy

func _ready():
    super._ready()
    max_health = 50
    health = max_health
    move_speed = 120.0
    damage = 8

func drop_loot() -> void:
    # 掉落金币
    var gold = preload("res://scenes/gold.tscn").instantiate()
    gold.position = position
    get_parent().add_child(gold)

# orc.gd
class_name Orc
extends BaseEnemy

func _ready():
    super._ready()
    max_health = 150
    health = max_health
    move_speed = 80.0
    damage = 20

func drop_loot() -> void:
    # 掉落武器
    var weapon = preload("res://scenes/axe.tscn").instantiate()
    weapon.position = position
    get_parent().add_child(weapon)

10.7.2 UI组件系统

# base_ui_component.gd
class_name BaseUIComponent
extends Control

signal visibility_changed(visible: bool)

func show_animated() -> void:
    modulate.a = 0
    visible = true
    var tween = create_tween()
    tween.tween_property(self, "modulate:a", 1.0, 0.3)
    visibility_changed.emit(true)

func hide_animated() -> void:
    var tween = create_tween()
    tween.tween_property(self, "modulate:a", 0.0, 0.3)
    await tween.finished
    visible = false
    visibility_changed.emit(false)

# dialog_box.gd
class_name DialogBox
extends BaseUIComponent

@onready var label: Label = $Label
@onready var portrait: TextureRect = $Portrait

func show_dialog(text: String, character_portrait: Texture2D = null) -> void:
    label.text = text
    if character_portrait:
        portrait.texture = character_portrait
    show_animated()

# inventory_panel.gd
class_name InventoryPanel
extends BaseUIComponent

@onready var grid: GridContainer = $Grid

func refresh(items: Array) -> void:
    for child in grid.get_children():
        child.queue_free()
    
    for item in items:
        var slot = preload("res://scenes/ui/slot.tscn").instantiate()
        slot.set_item(item)
        grid.add_child(slot)

本章小结

本章我们深入学习了GDScript的类与继承机制:

  1. 继承基础:extends关键字、继承层次
  2. 方法重写:覆盖父类方法、super关键字
  3. 多态:不同类型的统一接口、类型检查
  4. 抽象类与接口:模拟抽象类、接口模式
  5. Godot节点继承:扩展内置节点、场景继承
  6. 最佳实践:何时使用继承、继承vs组合、设计原则
  7. 实际案例:敌人系统、UI组件系统

继承是强大的代码复用工具,但要谨慎使用。下一章我们将学习Godot独特的信号系统,这是实现对象间通信的优雅方式。


上一章:面向对象编程

下一章:信号系统

← 返回目录