第十章:类与继承
"继承让我们站在巨人的肩膀上,复用已有的代码来构建新的功能。"
继承是面向对象编程的核心机制之一,它允许我们基于已有的类创建新类,实现代码复用和多态。本章将深入探讨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的类与继承机制:
- 继承基础:extends关键字、继承层次
- 方法重写:覆盖父类方法、super关键字
- 多态:不同类型的统一接口、类型检查
- 抽象类与接口:模拟抽象类、接口模式
- Godot节点继承:扩展内置节点、场景继承
- 最佳实践:何时使用继承、继承vs组合、设计原则
- 实际案例:敌人系统、UI组件系统
继承是强大的代码复用工具,但要谨慎使用。下一章我们将学习Godot独特的信号系统,这是实现对象间通信的优雅方式。
上一章:面向对象编程
下一章:信号系统