【Godot4.2】Godot中的继承与组合

概述

继承组合是编程中常用的两种策略,旨在尽可能多地重用代码。继承应用得非常广泛,但我认为组合在很多场景下会更加合适一些。
基于组合,游戏开发前辈们专门设计出了实体组件模式(EC模式)和进阶的ECS模式。本篇所提及的Godot中的“组合”可能更倾向于UML类图中描述类与类关系的“组合”,而并不完全是EC模式或ECS模式所提到的严格的形式,尤其是在Godot中,更是以一种父子节点的关系来实现。
但是这对理解什么是“组合”和“继承”,以及两者的区别和优劣,并为进一步学习ECS模式提供基础。

参考视频

在笔者搜寻已有教程资料的过程中,发现B站这个搬运视频是最简单和容易理解的。
【中文语音】如何在 Godot 4 中轻松简化代码_哔哩哔哩_bilibili
所以我就在扒字幕的基础上,进行了扩充和修改,让其变成了一篇更容易阅读和理解组合模式的文章。建议在看原视频的基础上,将本文当做是一个笔记和总结来看。希望对Godoter们的学习发挥一点小小的作用。

继承

假设我们有这样一个设定:

  • 我们有一个玩家类Player,它能够攻击,有生命值,并且有一个HitBox(攻击范围盒),同时还能处理用户输入。
  • 相应地,我们也有一个敌人类Enemy,它同样能够攻击,拥有生命值和HitBox,以及一些敌人特有的东西,比如AI。

在这里插入图片描述
如果我们使用继承的思想和原则,就可以将Player类和Enemy类中重复和相同的部分helth属性、attack()方法和hitbox属性提取出来,放到一个单独的类Entity中,并让Player类和Enemy继承自Entity

在这里插入图片描述

这样做的好处是:

  • 敌人和玩家的代码得到简化
  • 而且将重复性的内容放到单独的类,可以提高代码的可重用性
  • 除了敌人和玩家,我们可以基于Entity创建其他具有类似特征(属性)和行为(方法)的类
  • 修改Entity类,会自动将修改传递到子类

继承的缺陷

如果故事到这里就结束,继承将是完美的解决方案。因为这一切看起来十分自然,也很容易理解。
但是如果让我们引入另一个类Tree来代表游戏中的树。
和玩家及敌人一样,树也可以受到伤害,也应该有生命值。

在这里插入图片描述

我们可以让Tree类也继承自Entity,但是Tree在继承Entityhealth属性的同时也继承了attack()方法和hitbox属性,这意为着树拥有了发起攻击的能力。
除非你做的是魔幻类的游戏和类似指环王里的树人角色,否则作为一般意义的“树”是不应该具有攻击能力的。


注意
这里的Tree类名在Godot中肯定是不能使用的,因为Tree控件已经占用了这个类名。这里只做理解,实际中可以起其他名称。


可以发现单纯的抽离父类和通过继承实现子类的方法,会存在父类某些属性或方法子类用不上的情况。此时,就表明你的父类需要进行更细致的拆分,并增加继承的层级。而这就会增加继承的复杂度,并让修改变得牵一发而动全身,甚至引发不可控的错误。
在Godot中,还有一个更深层次的问题:

  • 玩家(Player类)和敌人(Enemy类)都继承自CharacterBody2D
  • Tree则应该继承自StaticBody2D
  • 在Godot中,你无法同时继承多个类,也就是“即是…又是…”。

在这里插入图片描述

通过上面的类图其实已经可以发现:

  • 玩家(Player类)和敌人(Enemy类)已经出现多重继承的问题,也就是既是Entity类的子类,又是CharacterBody2D的子类,而这在目前Godot4.2为止的GDScript中是无法实现的
  • 树(Tree类)也是相同的多重继承的问题,既是Entity类的子类,又是StaticBody2D的子类

如果我们将Entity设为CharacterBody2D的子类:

  • 则可以瞬间解决玩家(Player类)和敌人(Enemy类)的多重继承问题
  • 但是树(Tree类)的多重继承问题则继续存在,Tree不仅是Entity,同时还是StaticBody2D,而且还是CharacterBody2D,这在Godot中是无法实现的

在这里插入图片描述

此时,你或许会想到将Entity类拆分为StaticEntityCharacterEntity,分别用来代表不能自主移动的“静态实体”和可以自由移动的“角色实体”。
并让Tree继承自StaticEntityPlayerEnemy继承自CharacterEntity
并分别设计StaticEntityCharacterEntity的成员。

在这里插入图片描述

这样做的好处,确实是解决了多重继承的问题,但是肉眼可见的增加了类设计的难度,并且无法消除重复代码的问题。
这还只是引入了一个Tree类,如果你需要创建一个具有很多游戏实体类型的复杂游戏,则每添加一两个类,就要想方设法增加一些基类用于继承。
这便是使用继承存在的问题。

组合

好在面向对象设计中,除了继承我们还可以使用其他的方式,而其中类的组合关系,可以解决一部分继承关系带来的问题。
我们换个角度思考一下我们面临的问题:我们只是需要将不同的特征(属性)和行为(方法)附加到不同的类上而已。而这就是“组合”思想。
我们将所需要添加的特征(属性)和行为(方法)称为是“组合”方法中的“组件”。
在UML类图中,用一条带实心菱形箭头的连线代表类之间的组合关系。
我们将之前继承关系改为组合关系:

  • 首先我们将需要添加到各个类的特征(属性)和行为(方法)单独封装为一个类,并以“xxxComponent”形式命名,比如:AttackComponentHealthComponentHitboxComponent
  • 接着我们使用组合关系,将这些组件按需求添加到需要它们的类之上

在这里插入图片描述

可以看到类的关系一下子清晰起来,数量也有所减少,而且每个组件都是可以重用于之后的类的。
而且因为组件不是某个游戏实体的子类,所以不受继承影响。实体本身是什么类型,组件也不关心。并且重复代码的问题也消失了。
而这就是“组合”的魅力。

在Godot中实现组合

在Godot中,2D游戏中的“组件”是用Node2D节点扩展而来的。它们通常作为某个“游戏实体”,比如PlayerCharacterBody2D类型)节点、EnemyCharacterBody2D类型)节点或TreeStaticBody2D类型)节点的子节点而添加和发挥作用。
image.png
作为组件,我们可以按需添加到任何游戏实体上,比如我们新增了一个代表子弹的类Bullet,则我们可以将AttackComponentHitboxComponent添加到Bullet上,这将让它自动拥有发动攻击攻击碰撞检测的能力。而为子弹设定血量是没有必要的,因为一般来说子弹与某些可被射中的游戏实体接触后,便会自动销毁。因此不需要给Bullet添加HealthComponent组件。

实际案例

extends Node2D
class_name HealthComponent
@export var MAX_HEALTH := 10.0

var health:float

func _ready():
    health = MAX_HEALTH

func damage(attack:Attack):
    health -= attack.attack_damage
    if health <=0:
        get_parent().queve_free()
extends Area2D
class_name HitboxComponent

@export var health_component:HealthComponent

func damage(attack:Attack):
    if health_component:
        health_component.damage(attack)

上面定义了两个简单的组件:HealthComponentHitboxComponent,我们便可以在任何需要相应功能的实体上添加组件,并调用其中的方法。
【原视频有使用组件的演示】

总结

  • 游戏开发中难免使用OOP面向对象的思想,也难免落入父类、子类、继承和扩展的惯性思维
  • 但是一味的使用继承,可能会导致设计的类越来越多,类与类之间的关系越来越复杂,最后导致可扩展性下降,并出现不可消除的冗余和重复代码
  • 我们可以使用组合思维,将游戏中的类分为“实体”和“组件”,实体可以根据游戏的复杂度不断增加,组件也可以按需开发和增加,而实体的功能由添加的组件定义,而且可以随时增删。
  • “组合”形式,大大降低了类之间的关系的复杂度,并避免了复杂继承关系,父类修改导致子类崩坏的可能性。
  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巽星石

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值