TH1/Book/4.1组件式设计.md
2025-07-17 18:26:28 +08:00

8.8 KiB
Raw Blame History

组件式设计

在代码复用和组织数据方面,面向对象可能是大家第一反应。面向对象三大特性继承,封装,多态,在一定程度上能解决不少代码复用,数据复用的问题。不过面向对象不是万能的,它也有极大的缺陷:

1. 数据结构耦合性极强

一旦父类中增加或删除某个字段可能要影响到所有子类影响到所有子类相关的逻辑。这显得非常不灵活在一套复杂的继承体系中往父类中改变字段会变得越来越麻烦比方说ABC是D的子类某天发现需要增加一个AB都有的数据但是C没有那么这个数据肯定不好放到父类中只能将AB抽象出来一个父类EE继承于DAB共有的字段加到E中一旦继承结构发生了变化可能接口也要改变比方说之前有个接口传入参数类型是E当AB不再需要共用的那个字段那么需要调整继承关系让AB重新继承D那么这个接口的传入参数类型需要改成D其中的逻辑代码很可能也要发生调整。更可怕的是游戏逻辑变化非常复杂非常频繁可能今天加了个字段明天又删掉了假如每次都要去调整继承结构这简直就是噩梦。继承结构面对频繁的数据结构调整感觉很无力。

2. 难以热插拔

继承结构无法运行时增加删除字段比如玩家Player平常是走路使用坐骑后就骑马。问题是坐骑的相关信息就需要一直挂在Player对象上面。这就显得很不灵活我不骑马的时候内存中为啥要有马的数据接口也有同样的问题一个类实现了一个接口那么这个接口就永远粘在了这个类身上你想甩掉她都不行还是以骑马为例玩家Player可以进行骑行那么可能继承一个骑行的接口问题是当我这个Player从坐骑上下来时玩家Player身上还是有骑行的接口根本没法动态删掉这个接口可能例子举得不是很对但是道理表述的应该很清楚了。

使用面向对象可能导致灾难性后果游戏开发中有新人有老人有技术好的有技术差的。人都是喜欢偷懒的当你发现调整继承关系麻烦的时候有可能AB中增加一个字段为了省事直接就放到父类D中去了。导致C莫名奇妙的多了一个无用的字段。关键还没法发现最后导致父类D越来越大到最后有可能干脆就不用ABC了直接让所有对象都变成D方便嘛是的很多游戏就是这么干的开发到最后根本就不管继承关系了因为想管也管不了了。

3. 方法与数据耦合

传统面向对象都是class中带有方法并且特别提倡虚函数多态。方法跟数据放在一起带来了特别多耦合的问题。为了解决这些耦合大家想出了大量的设计模式比如依赖接口依赖转置。说实话这就是脱裤子放屁为了解耦合把类做成接口然后继承接口难道这就不叫依赖了这些做法导致代码中到处是接口代码阅读极其困难。写起代码来也没有个标准高手跟菜鸡写出来的代码完全是两回事。大部分码农都是逻辑仔谁有时间天天想这个类要怎么设计啊随着逻辑越来越复杂类里面的方法将越来越庞大可怕的是这是这个类的方法极其难以重构很多项目中能看到类里面存在上万行代码的虚函数。天哪

面向对象在面对复杂的游戏逻辑时很无力所以很多游戏开发者又倒退了回去使用面向过程进行开发游戏面向过程简单粗暴不考虑复杂的继承不考虑抽象不考虑多态是开发届的freestyle挽起袖子就开撸但同时代码逻辑的复用性数据的复用性也大大降低。面向过程也不是一种好的游戏开发模式。

组件模式很好的解决了面向对象以及面向过程的种种缺陷在游戏客户端中使用非常广泛Unity3d虚幻4等等都使用了组件模式。组件模式的特点 1.高度模块化,一个组件就是一份数据加一段逻辑
2.组件可热插拔,需要就加上,不需要就删除
3.类型之间依赖极少,任何类型增加或删除组件不会影响到其它类型。

但是目前只有极少有服务端使用了组件的设计守望先锋服务端应该是使用了组件的设计守望先锋的开发人员称之为ECS架构其实就是组件模式的一个变种E就是EntityC就是ComponentS是System其实就是将组件Component的逻辑与数据剥离逻辑部分叫System话题扯远了还是回到ET框架来把。

ET框架使用了组件的设计。一切都是Entity和Component任何类继承于Entity都可以挂载组件例如玩家类

public sealed class Player : Entity
{
    public string Account { get; private set; }
    public long UnitId { get; set; }
	
    public void Awake(string account)
    {
        this.Account = account;
    }
	
    public override void Dispose()
    {
        if (this.Id == 0)
        {
            return;
        }
        base.Dispose();
    }
}

给玩家对象挂载个移动组件MoveComponent这样玩家就可以移动了给玩家挂上一个背包组件玩家就可以管理物品了给玩家挂上技能组件那么玩家就可以施放技能了加上Buff组件就可以管理buff了。

player.AddComponent<MoveComponent>();
player.AddComponent<ItemsComponent>();
player.AddComponent<SpellComponent>();
player.AddComponent<BuffComponent>();

组件是高度可以复用的比如一个NPC他也可以移动给NPC也挂上MoveComponent就行了有的NPC也可以施放技能那么给它挂上SpellComponentNPC不需要背包那么就不用挂ItemsComponent了

ET框架模块全部做成了组件的形式一个进程也是由不同的组件拼接而成。比方说Loginserver需要对外连接也需要与服务器内部进行连接那么login server挂上

// 内网网络组件NetInnerComponent处理对内网连接  
Game.Scene.AddComponent<NetInnerComponent, string, int>(innerConfig.Host, innerConfig.Port);
// 外网网络组件NetOuterComponent处理与客户端连接
Game.Scene.AddComponent<NetOuterComponent, string, int>(outerConfig.Host, outerConfig.Port);

比如battle server就不需要对外网连接外网消息由gateserver转发那么很自然的只需要挂载一个内网组件即可。 类似Unity3d的组件ET框架也提供了组件事件例如AwakeStartUpdate等。要给一个Component或者Entity加上这些事件必须写一个辅助类。比如NetInnerComponent组件需要Awake跟Update方法那么添加一个这样的类即可

[ObjectEvent]
public class NetInnerComponentEvent : ObjectEvent<NetInnerComponent>, IAwake, IUpdate
{
    public void Awake()
    {
        this.Get().Awake();
    }

    public void Update()
    {
        this.Get().Update();
    }
}

这样NetInnerComponent在AddComponent之后会调用其Awake方法并且每帧调用Update方法。 ET没有像Unity使用反射去实现这种功能因为反射性能比较差而且这样实现的好处是这个类可以放到热更dll中这样组件的Awake StartUpdate方法以及其它方法都可以放到热更层中。将Entity和Component做成没有方法的类方法都放到热更层方便热更修复逻辑bug。

组件式开发最大的好处就是不管菜鸟还是高手开发一个功能都能很快的知道怎么组织数据怎么组织逻辑。可以完全放弃面向对象。使用面向对象开发最头疼的就是我该继承哪个类呢之前做过最恐怖的就是虚幻三虚幻三的继承结构非常多层完全不知道自己需要从哪里开始继承。最后可能导致一个非常小的功能继承了一个及其巨大的类这在虚幻三开发中屡见不鲜。所以虚幻4改用了组件模式。组件模式的模块隔离性非常好技术菜鸟某个组件写得非常差也不会影响到其它模块大不了重写这个组件就好了。

ET的组件设计有所创新方法跟数据分离完全解除耦合不用绞尽脑汁去想怎么解除耦合随意写静态方法即可根本不存在耦合即使是菜鸟写的代码也很容易重构。

正是因为ET使用了可拆卸的组件模式ET可以将所有服务器组件都装到同一个进程上那么这一个进程就可以当作一组分布式服务器使用。从此用vs调试分布式服务器成为了可能。正因为这样平常开发只使用一个进程发布的时候发布成多个进程就行了。说实在的不是吹牛这是一个伟大的发明这一发明解决了分布式游戏服务器开发中的大大大难题极大的提高了开发效率。