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

devtools/2024/9/25 8:34:39/

概述

继承组合是编程中常用的两种策略,旨在尽可能多地重用代码。继承应用得非常广泛,但我认为组合在很多场景下会更加合适一些。
基于组合,游戏开发前辈们专门设计出了实体组件模式(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.0var health:floatfunc _ready():health = MAX_HEALTHfunc damage(attack:Attack):health -= attack.attack_damageif health <=0:get_parent().queve_free()
extends Area2D
class_name HitboxComponent@export var health_component:HealthComponentfunc damage(attack:Attack):if health_component:health_component.damage(attack)

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

总结

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

http://www.ppmy.cn/devtools/43361.html

相关文章

数据结构——不相交集(并查集)

一、基本概念 关系&#xff1a;定义在集合S上的关系指对于a&#xff0c;b∈S&#xff0c;若aRb为真&#xff0c;则a与b相关 等价关系&#xff1a;满足以下三个特性的关系R称为等价关系 (1)对称性&#xff0c;aRb为真则bRa为真&#xff1b; (2)反身性,aRa为真; (3)传递性,aRb为真…

数据结构与算法之线性表01

数组是一种线性数据结构&#xff0c;把相同数据类型的元素存储在连续的内存空间中&#xff0c;数组的索引&#xff08;元素在数组中的位置&#xff09;从0开始。 一、常用操作&#xff1a; 1、初始化 # 给定初始值 arr:list[int] [0] * 5 nums:list[int] [1, 2, 3, 4, 5] …

Leetcode 力扣95. 不同的二叉搜索树 II (抖音号:708231408)

给你一个整数 n &#xff0c;请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同 二叉搜索树 。可以按 任意顺序 返回答案。 示例 1&#xff1a; 输入&#xff1a;n 3 输出&#xff1a;[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null…

GO语言 linux部署

https://blog.csdn.net/wangye135/article/details/136177171 一、简述 1. 可以直接在服务器上运行编译好的二进制文件&#xff0c;不需要在服务器上下载语言环境。 2. 内置运行时环境&#xff1a;可执行文件中内置了运行时环境&#xff0c;包括垃圾回收、调度器等&#xff…

从计划到行动:BI项目中PDCA闭环思维如何转化为实践?

在当今快节奏的商业环境中&#xff0c;项目管理的质量直接影响到企业的运营效率和竞争力。为了确保项目能够高效、顺利地进行&#xff0c;并达到预期目标&#xff0c;企业需要采用系统化的管理方法来指导项目的实施。PDCA&#xff08;Plan-Do-Check-Act&#xff09;循环&#x…

芯片固定uv胶有什么优点?

芯片固定uv胶有什么优点&#xff1f; 芯片固定UV胶具有多种优点&#xff0c;这些优点使得它在半导体封装和芯片固定等应用中成为理想的选择。以下是芯片固定UV胶的一些主要优点&#xff1a; 固化速度快&#xff1a;UV胶在紫外线照射下能迅速固化&#xff0c;通常在几秒到几十秒…

QT C++ 模型视图结构 QTableView 简单例子

在Qt中&#xff0c;MVC模式被广泛使用于各种用户界面框架中&#xff0c;包括Qt的模型视图结构。Qt的模型视图结构是基于MVC模式设计的&#xff0c;其中包括了Model、View和Delegate三个部分。 QTableView是Qt模型视图结构中的一种视图&#xff0c;它用于以表格形式显示数据。 …

Java如何将tif格式图片转为jpg格式图片

在Java中&#xff0c;将TIFF&#xff08;.tif&#xff09;格式的图片转换为JPEG&#xff08;.jpg&#xff09;格式的图片&#xff0c;通常需要使用图像处理库&#xff0c;如Apache Commons Imaging&#xff08;之前称为Sanselan&#xff09;或Java Advanced Imaging (JAI)。但是…