ECS结合CocosCreator讨论
-
Creator 版本: 2.4.3
-
讨论一下关于ECS框架和Creator结合的问题。
-
个人想要尝试ECS框架的原因
- 通过脚本继承的方式会增加团队合作的成本。
- 维护父类的脚本会违背编程的四大原则,且工作量较大。
- 策划更改需求的时候就会更改之前的脚本或者继承重写方法,导致继承混乱。
- 维护好脚本后,通过排列组合的方式可以组合出很多不一样的功能。灵活度和复用性比较高。
- 方便之后接手项目的人阅读和维护。
- 以上都是个人遇到的问题(当然很大部分是因为我菜,嘿嘿嘿)
-
个人发帖的观点(个人哦)
- 技术在于讨论和分享,互联网的意义也在于此。不怕错,敢于提问题和分享技术才是互联网的良性发展。
-
参考文章:
什么是ECS框架?
E : 表示的是 Entity 也就是实例(负责关联组件)。
C : 表示的是 Component 也就是组件,和实例的关系是一个实例对应多个组件,只负责数据的记录,不进行任何逻辑运算,这很重要。
S : 表示的是 System 也就是系统, 负责整个游戏的运作,数据的运算。类似于,我只要关心我要修改的组件,改了就完事儿了,其他我不管。
以上是个人看法,如果有错误的地方,希望大佬指出。
个人理解的ECS框架的用法
- 如果我们要设计一个飞行射击的游戏,那么我们可能会需要以下几个组件
- 血量,移动,AI,攻击, 敌人,玩家,系统输入
- 那么我们就可以这样进行组合
- 玩家(血量,移动,攻击,玩家,系统输入)
- 敌人(血量,移动,AI,敌人)
- BOSS(血量,移动,AI,敌人,其他特殊组件)
- 我们可能需要以下几个系统
- 玩家控制系统,普通敌人AI控制系统,BOSS敌人AI控制系统。
- 实际使用
通过这种组件组合的形式,我们就能够实现很多的角色功能。我们维护好多个组件后,之后的开发任务重心就可以放到系统脚本的维护和编写上来,策划增加新的功能,我们就增加新的组件和系统脚本,之后动态修改绑定的组件。这样,就不需要我们再次编写和修改之前的脚本。举个例子,如果策划这个时候和我们说,需要敌人会飞。那我们只需要增加一个飞行的组件,再增加一个飞行的系统控制。然后这么做:- 敌人(血量,移动,AI,敌人,飞行)
- BOSS(血量,移动,AI,敌人,其他特殊组件,飞行)
这样的话,我们的敌人是不是就会飞了,哈哈哈。如果有一个敌人是不会飞并且不会移动的,那么我们只要这样组合: - 敌人(血量,AI,敌人)
没错,把移动和飞行的脚本去掉就可以了(理论上来说,我们去掉移动脚本就可以了 滑稽脸)。
以上是个人观点,如果说的有问题,望大家矫正。
一个简单的ECS框架结合Creator的例子(抛砖引玉)
- ECSContextManager(ECSWorld,不一定是要单例,我做的是单例)
- ECSEntityMananger : 控制实例,和组件的增删减。
- ECSSystemManager : 控制系统的运行,和系统的结束开启。
- ECSComponent(组件)
- ECSInputComponent : 接收玩家的输入。
_mark: number = ECS_COMP_ENUM.ECS_INPUT;
value: number = 0; // 用来记录按键
- ECSVelocityComponent : 玩家的速度和方向。
export default class ECSVelocityComponent extends ECSComponent {
_mark: number = ECS_COMP_ENUM.ECS_VELOCITY;
private _value: cc.Vec2 = cc.v2(0, 0);
private _speed: number = 5;
private _maxSpeed: number = 10;
private _minSpeed: number = 0;
public get maxSpeed(): number {
return this._maxSpeed;
}
public set maxSpeed(value: number) {
this._maxSpeed = value;
}
public get minSpeed(): number {
return this._minSpeed;
}
public set minSpeed(value: number) {
this._minSpeed = value;
}
public get value(): cc.Vec2 {
return this._value;
}
public set value(value: cc.Vec2) {
this._value = value;
}
public get speed(): number {
return this._speed;
}
public set speed(value: number) {
this._speed = Math.max(Math.min(value, this.maxSpeed), this.minSpeed);
}
constructor(speed?: number, maxSpeed?: number, minSpeed?: number, direction?: cc.Vec2) {
super();
this.value = direction || this.value;
this.speed = speed || this.speed;
this.maxSpeed = maxSpeed || this.maxSpeed;
this.minSpeed = minSpeed || this.minSpeed;
}
- ECSPositionComponent : 角色的位置。
_mark: number = ECS_COMP_ENUM.ECS_POSITION;
public value: cc.Vec2 = cc.v2(0, 0);
- ECSNodeComponent : 实例是否挂载节点。(这个脚本我还在优化)
_mark: number = ECS_COMP_ENUM.ECS_NODE; // 节点标志
private _pool: cc.NodePool; // 节点对象池容器
private _tiledLayer: cc.TiledLayer; // 节点地图容器
private _node: cc.Node; // 包含的节点
public get pool(): cc.NodePool {
return this._pool;
}
public set pool(value: cc.NodePool) {
this._pool = value;
}
public get tiledLayer(): cc.TiledLayer {
return this._tiledLayer;
}
public set tiledLayer(value: cc.TiledLayer) {
this._tiledLayer = value;
}
public get node(): cc.Node {
return this._node;
}
public set node(value: cc.Node) {
this._node = value;
}
constructor(node: cc.Node, pool?: cc.NodePool, tiledLayer?: cc.TiledLayer) {
super();
this.node = node;
this.pool = pool;
this.tiledLayer = tiledLayer;
}
destroy(): void {
if (this.tiledLayer != undefined && this.tiledLayer != null) {
this.tiledLayer.removeUserNode(this.node);
}
if (this.pool != null && this.pool != undefined) {
this.node.removeFromParent();
this.pool.put(this.node);
return;
}
this.pool = null;
this.tiledLayer = null;
this.node.removeFromParent();
this.node.destroy();
this.node = null;
}
- ECSHealthComponent : 玩家是否有血量组件。(暂时没有用)
- 暂时不举例子
- 主游戏脚本(挂载在主场景上的脚本)
MainComp
@property({
type: cc.Prefab,
displayName: '角色预制体'
})
role: cc.Prefab = null;
heroId: string = 'hero_01';
private _systemManager: ECSSystemManager = null;
public get systemManager(): ECSSystemManager {
return this._systemManager;
}
public set systemManager(value: ECSSystemManager) {
this._systemManager = value;
}
async start() {
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE); // 屏幕设置为横屏
GameController.InitGameController(() => { }, async () => {
let contextManager: ECSContextManager = ECSContextManager.getInstance(); // 获取世界实例
this._systemManager = this.CreateSystem(contextManager); // 创建系统管理
// cc.log(this._systemManager.systemList[0].__proto__.constructor.name); // 获得函数方法名
let playerStr = 'prefab/role';
let res = await NodeController.getInstance().createNode(playerStr);
let player = contextManager.game.createEntity(new ECSPlayerEntity());
let hero = RoleController.getInstance().getHeroById(this.heroId);
this.node.addChild(res);
// 这里给实例绑定我们想要绑定的脚本(比如一个玩家可以控制的实例需要 速度组件 位置组件 节点组件 和操作输入组件 以及血量组件)
contextManager.game.addComponent(new ECSInputComponent(), ECS_COMP_ENUM.ECS_INPUT, player)
.addComponent(new ECSVelocityComponent(hero.moveSpeed, hero.maxMoveSpeed, hero.minMoveSpeed), ECS_COMP_ENUM.ECS_VELOCITY, player)
.addComponent(new ECSPositionComponent(), ECS_COMP_ENUM.ECS_POSITION, player)
.addComponent(new ECSNodeComponent(res), ECS_COMP_ENUM.ECS_NODE, player)
.addComponent(new ECSHealthComponent(hero), ECS_COMP_ENUM.ECS_HEALTH, player);
// 没什么的,可以忽视,这是我对状态机的尝试
let test = new ECSStateComponent();
StateMachine.apply(test, { // 这是我对状态机的尝试
init: 'A',
transitions: [
{ name: 'step', from: 'A', to: 'B' },
{ name: 'step', from: 'B', to: 'B' },
]
});
}, this);
}
testCallFunc(): void {
cc.log('test callFunc.');
}
update(dt: number): void { // 这个很关键嗷
if (this.systemManager != undefined && this.systemManager != null) {
this._systemManager.Execute(dt);
this._systemManager.CleanUp();
}
}
onDestroy(): void {
this._systemManager.TearDown();
}
private CreateSystem(contextManager: ECSContextManager): ECSSystemManager {
let sysMgr = new ECSSystemManager();
sysMgr.addSystem(new ECSChangePlayerVelocitySystem()) // 添加角色移动位置系统
.addSystem(new ECSChangePositionSystem(contextManager)) // 添加改变位置系统
.addSystem(new ECSPlayerAtkSystem(contextManager)) // 添加角色攻击系统
.addSystem(new ECSInputSystem(contextManager)) // 添加输入监听系统;
return sysMgr;
}
-
接下来就是对system的控制
这是我的对系统输入的监控脚本RockerComp
start = () => { this.moveListenOpen(); } initData = () => { } update(dt) { this.moveController(dt); } // 开启移动监听 moveListenOpen = () => { this.node.on(cc.Node.EventType.TOUCH_START, this.onMoveTouchStartCallback, this); this.node.on(cc.Node.EventType.TOUCH_MOVE, this.onMoveTouchMoveCallback, this); this.node.on(cc.Node.EventType.TOUCH_END, this.onMoveTouchEndCallback, this); this.node.on(cc.Node.EventType.TOUCH_CANCEL, this.onMoveTouchCancelCallback, this); } // 关闭移动监听 moveListenClose = () => { this.node.off(cc.Node.EventType.TOUCH_START, this.onMoveTouchStartCallback, this); this.node.off(cc.Node.EventType.TOUCH_MOVE, this.onMoveTouchMoveCallback, this); this.node.off(cc.Node.EventType.TOUCH_END, this.onMoveTouchEndCallback, this); this.node.off(cc.Node.EventType.TOUCH_CANCEL, this.onMoveTouchCancelCallback, this); } // 移动开始触摸回调 onMoveTouchStartCallback = (event) => { if (this.moveControlId < 0) { // 控制单个移动事件监听 this._controllerTrigger = true; this.node.opacity = 255; this.moveControlId = event.getID(); let location = event.target.convertToNodeSpaceAR(event.getLocation()); this.rockerPositionHandler(this.rocker, location, true); } } // 移动触摸移动回调 onMoveTouchMoveCallback = (event) => { if (event.getID() === this.moveControlId) { let location = event.target.convertToNodeSpaceAR(event.getLocation()); this.rockerPositionHandler(this.rocker, location); } } // 移动触摸结束回调 onMoveTouchEndCallback = (event) => { if (event.getID() === this.moveControlId) { this.moveControlId = -1; this._controllerTrigger = false; // this.moveEndCallFunc(this); this.rockerReset(this.rocker); // 虚拟操控感复位 } } // 移动取消触摸回调 onMoveTouchCancelCallback = (event) => { if (event.getID() === this.moveControlId) { this.moveControlId = -1; this._controllerTrigger = false; // this.moveEndCallFunc(this); this.rockerReset(this.rocker); // 虚拟操控感复位 } } // 虚拟摇杆 // 摇杆位置操作 rockerPositionHandler(node, touchPos, isFirst = false): void { let moveVector = touchPos.sub(node.position); // 移动向量 let touchToCenterDistance = moveVector.mag(); // 触摸点距离摇杆中心距离 let moveRadian = Math.atan2(moveVector.x, moveVector.y); // 移动向量弧度 let moveAngle = moveRadian * 180 / Math.PI; // 移动向量角度 if (isFirst && touchToCenterDistance > this.rockerMoveRadius) { // 第一次触摸调整摇杆位置 let beyondDistance = touchToCenterDistance - this.rockerMoveRadius; let newPos = cc.v2(node.position.x + beyondDistance * Math.sin(moveRadian), node.position.y + beyondDistance * Math.cos(moveRadian)); node.setPosition(newPos); moveVector = touchPos.sub(node.position); // 移动向量 touchToCenterDistance = moveVector.mag(); // 触摸点距离摇杆中心距离 } let directorPos = cc.v2(this.directorRadius * Math.sin(moveRadian), this.directorRadius * Math.cos(moveRadian)); if (touchToCenterDistance > this.joyStickMoveRadius) { let newPos = cc.v2(this.joyStickMoveRadius * Math.sin(moveRadian), this.joyStickMoveRadius * Math.cos(moveRadian)); this.joyStick.setPosition(newPos); } else { let newPos = cc.v2(touchToCenterDistance * Math.sin(moveRadian), touchToCenterDistance * Math.cos(moveRadian)); this.joyStick.setPosition(newPos); } if (this.rockerDirector) { this.rockerDirector.active = true; this.rockerDirector.setPosition(directorPos); this.rockerDirector.angle = -moveAngle; } } // 操控杆恢复初始设置 rockerReset(node): void { this.rocker.setPosition(this.rockerInitPos); this.joyStick.setPosition(cc.v2(0, 0)); this.node.opacity = 100; if (this.rockerDirector) { this.rockerDirector.active = false; } } // 控制单位移动(这里可以自己实现不同的节点移动效果) moveController(dt): void { let moveVector = this.joyStick.position.sub(cc.v2(0, 0)); // 操控杆移动向量 // let moveLength = moveVector.mag(); // 操控杆移动距离 CKNotification.getInstance().notify(NOTIFY_EVENT_ENUM.JOY_STICK, moveVector); // 这个通知很重要 // if (moveLength > 0) { // } } /** 移动开始回调 */ moveStartCallFunc(self): void { if (!self.beControlledNode) return; self.beControlledNode.ins.moveStartCallFunc(self.joyStick.position); } /** 移动结束回调 */ moveEndCallFunc(self): void { if (!self.beControlledNode) return; self.beControlledNode.ins.moveEndCallFunc(self.joyStick.position); }这是监听这个事件的脚本ECSInputSystem
_flag = ECS_SYSTEM_ENUM.ECS_INPUT_SYSTEM; private _contextManager: ECSContextManager; public get contextManager(): ECSContextManager { return this._contextManager; } public set contextManager(value: ECSContextManager) { this._contextManager = value; } constructor(contextManager: ECSContextManager) { super(); CKNotification.getInstance().on(NOTIFY_EVENT_ENUM.INPUT_OPERATION, this.onInputCallFunc, this); CKNotification.getInstance().on(NOTIFY_EVENT_ENUM.CANCEL_OPERATION, this.onInputCancelCallFunc, this); CKNotification.getInstance().on(NOTIFY_EVENT_ENUM.JOY_STICK, this.onJoyStick, this); this.contextManager = contextManager; } execute(dt?: number) { } onInputCallFunc(notify, [flag, target]): void { cc.log(flag, ' was be clicked.'); this.addInputCode(flag); } onInputCancelCallFunc(notify, [flag, target]): void { cc.log(flag, ' was be cancel.'); this.removeInputCode(flag); } addInputCode(flag: number): void { let inputCompList = this.contextManager.game.getComponentListByComponentType(ECS_COMP_ENUM.ECS_INPUT); for (let comp of inputCompList) { comp.value |= flag; cc.log(comp); } } removeInputCode(flag: number): void { let inputCompList = this.contextManager.game.getComponentListByComponentType(ECS_COMP_ENUM.ECS_INPUT); for (let comp of inputCompList) { comp.value ^= flag; cc.log(comp); } } onJoyStick(notify, [vec]): void { let contextManager: ECSContextManager = ECSContextManager.getInstance(); // 获取世界实例 let entityList: Array<ECSEntity> = contextManager.game.getEntityHasAllCompTypes(ECS_COMP_ENUM.ECS_INPUT, ECS_COMP_ENUM.ECS_VELOCITY); //获取需要的实例 for (let entity of entityList) { let comp: ECSVelocityComponent = contextManager.game.getComponent(ECS_COMP_ENUM.ECS_VELOCITY, entity); let moveVec: cc.Vec2 = vec.normalizeSelf(); comp.value = moveVec.mulSelf(comp.speed); } }然后,关系位置组件的系统就改变了实例位置 ECSChangePositionSystem
_flag = ECS_SYSTEM_ENUM.ECS_CHANGE_PLAYER_POS_SYSTEM; private _contextManager: ECSContextManager; public get contextManager(): ECSContextManager { return this._contextManager; } public set contextManager(value: ECSContextManager) { this._contextManager = value; } constructor(contextManager: ECSContextManager) { super(); this.contextManager = contextManager; } execute(dt?: number): void { let entityList: Array<ECSEntity> = this.contextManager.game.getEntityHasAllCompTypes(ECS_COMP_ENUM.ECS_NODE, ECS_COMP_ENUM.ECS_POSITION, ECS_COMP_ENUM.ECS_VELOCITY); for (let entity of entityList) { let velocityComp = entity.getComponent(ECS_COMP_ENUM.ECS_VELOCITY); let posComp = entity.getComponent(ECS_COMP_ENUM.ECS_POSITION); let nodeComp = entity.getComponent(ECS_COMP_ENUM.ECS_NODE); posComp.value.addSelf(velocityComp.value); nodeComp.node.position = posComp.value; } }
结束
- 这是我第三次发帖,主要是自己不太明白想和大家交流一下这个关于ECS和Cocos结合的方案
- 代码参考5楼的大佬GitHub就可以了~
- 目前对于ECS就是感觉维护和整理很方便。
- 希望有大佬可以指点迷津。


