ET框架的基础逻辑
ECS思想和OOP思想的区别
以传统RPG游戏为例,游戏中可能有Player,NPC,Monster等角色。传统的OOP思想主要是大量运用继承抽象多态,用来实现不同对象的需求。
而ECS则是将所有对象设看作一个实体,所有功能都看作组件,不同功能的对象其实就是挂载了不同组件的实体。所有实体都是等价的,不会出现实体间继承的状况,这样可以很轻松的完成功能多样的不同实体,并增强组件的复用性。
ECS下简易的逻辑的分发
仍用上述举例,介绍一下简单的逻辑分发过程。
-
Model
Model层负责定义实体和组件,在这里要定义实体Unit,以及一些组件(MoveComponent,CombatComponent等)以及还需定义UnitType的枚举,以便后续逻辑的分发处理
注意:
Model下的实体不能调用UnityAPI,与Unity交互的组件实体只能放在ModelView下
Model中只能存在数据,例如position,UnitType不能有对数据的操作
-
ModelView
负责定义与Unity交互的实体和组件,有一些诸如动画组件,GameObject组件用到的数据由Unity提供,就需要将这些实体组件放到这个下面。
-
Hotfix
Hotfix负责System行为的定义,提供创建实体和为实体挂载组件的功能。此时定义的UnitFactory工厂就应为不同的实体提供不同的创建方法,例如CreatePlayer,就应先将UnitType置为Player,为其添加MoveComponent组件等待操作。再如CreateNPC,UnitType设置为NPC后,由于npc一般不会移动则无需添加移动组件,可以添加对话组件等待。
注意:
Hotfix下只能定义行为,不能包含数据状态,只能对Model提供的数据状态进行相关操作
Hotfix下也不能使用Unity Api 对于一些需要Unity才能挂载的组件,需要放到view中执行
-
HotfixView
HotfixView负责需要与Unity交互的System行为,例如加载模型prefab,实例化游戏对象GameObject,若实体中需要用到GameObject对象,则还应为Unit实体添加GameObject组件,里面存有gameObject。若Unit实体还需要在场景中播放动画,还需要为其添加使用了UnityApi的AnimatorComponent。
同理,在处理与Unity交互的System行为时,不同类型的Unit也可能行为有所不同,在此需要针对UnitType提供不同的行为。
ET框架下实体的生命周期
在Hotfix下编写实体生命周期的行为,创建相应的类实现特定生命周期接口即可,xxxSystem<>
,注意对应实体需实现IAwake等接口
Computer.cs
namespace ET
{
/// <summary>
/// 实体:主机
/// </summary>
public class Computer:Entity,IAwake,IUpdate,IDestroy
{
}
}
ComputerSystem.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ET
{
public class ComputerAwakeSystem:AwakeSystem<Computer>
{
public override void Awake(Computer self)
{
Log.Debug("ComputerAwake!!!!");
}
public class ComputerUpdateSystem : UpdateSystem<Computer>
{
public override void Update(Computer self)
{
Log.Debug("Computer Update !!!");
}
}
public class ComputerDestroySystem:DestroySystem<Computer>
{
public override void Destroy(Computer self)
{
Log.Debug("Computer Destroy !!");
}
}
}
public static class ComputerSystem
{
public static void Start(this Computer self)
{
//启动前先开启电源
self.GetComponent<PCCaseComponent>().StartPower();
Log.Debug("Computer Start!!!!");
//启动后开启显示器
self.GetComponent<MonitorsComponent>().Display();
}
}
}
触发实体的Awake生命周期一般是用 xxxScene.AddChild<实体名>()进行触发,表示将一个实体添加到一个场景的子节点中,即实例化过程,返回的对象即为实例化对象。 可通过实体对象.Dispose()释放掉,触发Destroy生命周期
ET框架的Scene树
在ET框架下,Scene即为场景作为根节点,根节点下可以存放多个实体或组件。但Scenen本质也是实体,所以Scene之间也会有层次关系。
游戏客户端的Scene层次结构
-
GameScene
游戏客户端全局的Scene根节点,用于提供游戏客户端全局且必要的基础功能组件(资源加载管理组件,计时器组件等)
-
ZoneScene
用于提供玩家全局游戏业务功能逻辑组件(例如基础UI,背包界面等)
-
CurrentScene
代表玩家当前所在的地图场景,一般用于挂载当前场景相关的组件,切换或释放场景时回收所有实体及组件。
游戏服务端Scene层次结构
-
GameScene
类似客户端,其用来挂载全局服务端所需的基础功能必备组件
-
ZoneScene
可以创建多个不同功能的ZoneScene, 每个不同功能的ZoneScene下挂载其应该具有的功能组件,例如网关下的NetKcpComponent,定位服务器的LocationComponent等等,一般通过SceneType的枚举对其进行逻辑分发。
不同ZoneScene可以存在一个进程上面,也可以每个都ZoneScene运行在一个单独的进程上,不同ZoneScene进程甚至可以分布在服务器集群上,大大提高了运行效率。
Scene可以动态创建和销毁(用于制作副本等临时场景)
创建Scene的一般流程
-
创建一个未挂载任何组件的Scene对象
Scene scene = EntitySceneFactory.CreateScene(id, instanceId, zone, sceneType, name, parent);
-
添加服务器下Scene的公共组件mailBox(用于接收Actor消息),
scene.AddComponent<MailBoxComponent, MailboxType>(MailboxType.UnOrderMessageDispatcher);
-
根据scene.SceneType针对不同服务器添加相应的组件,做相应的逻辑处理。
switch (scene.SceneType) { case SceneType.Realm: scene.AddComponent<NetKcpComponent, IPEndPoint, int>(startSceneConfig.OuterIPPort, SessionStreamDispatcherType.SessionStreamDispatcherServerOuter); break; case SceneType.Gate: scene.AddComponent<NetKcpComponent, IPEndPoint, int>(startSceneConfig.OuterIPPort, SessionStreamDispatcherType.SessionStreamDispatcherServerOuter); scene.AddComponent<PlayerComponent>(); scene.AddComponent<GateSessionKeyComponent>(); break; case SceneType.Map: scene.AddComponent<UnitComponent>(); scene.AddComponent<AOIManagerComponent>(); break; case SceneType.Location: scene.AddComponent<LocationComponent>(); break; }
创建自定义场景的基本步骤
-
在SceneType枚举中添加自定义名称
-
在SceneFactory中添加自定义场景类型所应该处理的相关逻辑
-
在Excel配置表中添加自定义场景信息,并生成相应cs文件
具体有关Excel的配置,笔者会在后续的博客中详细介绍