Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
86 lines (72 sloc) 7.6 KB

组件设计

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

1. 数据组织耦合性及强

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

2. 接口逻辑难以复用,难以热插拔。

面向对象处理相同行为所使用的方法是继承相同的父类或者接口。问题是接口并没有实现代码,而是需要其子类自己去写相关实现。很显然相同的功能,每个子类都可能写一份相似的代码。这导致接口的实现代码无法复用。还有个问题,一个类实现了一个接口,那么这个接口就永远粘在了这个类身上,你想甩掉她都不行,还是以骑马为例,玩家Player可以进行骑行,那么可能继承一个骑行的接口,问题是,当我这个Player从坐骑上下来时,玩家Player身上还是有骑行的接口,根本没法动态删掉这个接口!可能例子举得不是很对,但是道理表述的应该很清楚了。

3. 使用面向对象可能导致灾难性后果

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

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

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

目前只有极少有服务端使用了组件的设计,守望先锋服务端应该是使用了组件的设计,守望先锋的开发人员称之为ECS架构,其实就是组件模式的一个变种,E就是Entity,C就是Component,S是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也可以施放技能,那么给它挂上SpellComponent,NPC不需要背包,那么就不用挂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转发),那么很自然的只需要挂载一个内网组件即可。

// 内网网络组件NetInnerComponent处理对内网连接
Game.Scene.AddComponent<NetInnerComponent, string, int>(innerConfig.Host, innerConfig.Port);

类似Unity3d的组件,ET框架也提供了组件事件,例如Awake,Start,Update等。要给一个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 Start,Update方法以及其它方法都可以放到热更层中。只有数据放在model层,方便热更修复逻辑bug。

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