ECS系列一:简介

简介

这是一个Typescript语言版的Entity-Component-System框架。框架参考了Unity的Entitas框架。

ecs地址

使用说明

组件

自定义组件必须继承ecs.IComponent,并且需要使用ecs.register注册组件。

@ecs.register('Hello')
export class HelloComponent extends ecs.IComponent {
    info: string;
    data: number;

    // 组件被回收前会调用这个方法。
    reset() {
        this.info = '';
        this.data = 0;
    }
}

ecs.register功能

  • 能通过entity.Hello获得组件对象;
  • 将组件的构造函数存入ecs上下文中,并且给该类组件分配一个组件id。

实体

为了能利用Typescript的类型提示机制,在使用实体的时候需要用户自己继承ecs.Entity。

export class HelloEntity extends ecs.Entity {
    Hello: HelloComponent; // 这里的Hello要和ecs.register中填入的参数一致
}

添加组件:

entity.add(HelloComponent); // 添加组件时会优先从组件缓存池中获取无用的组件对象,如果没有才会新创建一个组件对象

删除组件:

entity.remove(HelloComponent); // 组件对象会从实体身上移除并放入组件缓存池中

获得组件对象:

1、entity.Hello; // 见上方自定义实体操作

2、entity.get(HelloComponent);

判断是否拥有组件:

1、entity.has(HelloComponent);

2、!!entity.Hello;

销毁实体:

entity.destroy() // 销毁实体时会先删除实体身上的所有组件,然后将实体放入实体缓存池中

实体筛选

目前提供了四种类型的筛选能力,但是这四种筛选能力可以组合从而提供更强大的筛选功能。

  • anyOf: 用来描述包含任意一个这些组件的实体;
  • allOf: 用来描述同时包含了这些组件的实体;
  • onlyOf: 用来描述只包含了这些组件的实体;不是特殊情况不建议使用onlyOf,因为onlyOf会监听所有组件的添加和删除事件;
  • excludeOf: 表示不包含所有这里面的组件(“与”关系);

使用方式:

  • 表示同时拥有多个组件
ecs.allOf(AComponent, BComponent, CComponent);
  • 表示拥有任意一个组件
ecs.anyOf(AComponent, BComponent);
  • 表示拥有某些组件,并且不包含某些组件
// 不包含CComponent或者DComponent
ecs.allOf(AComponent, BComponent).excludeOf(CComponent, DComponent);

// 不同时包含CComponent和DComponent
ecs.allOf(AComponent, BComponent).excludeOf(CComponent).excludeOf(DComponent);

系统

  • ecs.System: 用来组合某一功能所包含的System;
  • ecs.RootSystem: System的root;
  • ecs.ComblockSystem: 抽象类,组合式的System。默认情况,如果该System有实体,则每帧都会执行update方法;
  • ecs.IEntityEnterSystem: 实现这个接口表示关注实体的首次进入;
  • ecs.IEntityRemoveSystem: 实现这个接口表示关注实体的移除;

怎么使用

1、声明组件

@ecs.register('Node')
export class NodeComponent extends ecs.IComponent {
    val: cc.Node = null;

    reset() {
        this.val = null;
    }
}

@ecs.reigster('Move')
export class MoveComponent extends ecs.IComponent {
    heading: cc.Vec2 = cc.v2();
    speed: number = 0;

    reset() {
        this.heading.x = 0;
        this.heading.y = 0;
        this.speed = 0;
    }
}

@ecs.register('Transform')
export class TransformComponent extends ecs.IComponent {
    position: cc.Vec2 = cc.v2();
    angle: number;
    reset() {
    
    }
}

export class AvatarEntity extends ecs.Entity {
    Node: NodeComponent;
    Move: MoveComponent;
    Transform: TransformComponent;
}

2、创建系统

export class RoomSystem extends ecs.RootSystem {
    constructor() {
        super();
        this.add(new MoveSystem());
        this.add(new RenderSystem());
    }
}

export class MoveSystem extends ecs.ComblockSystem<AvatarEntity> {

    init() {
    
    }

    filter(): ecs.Matcher {
        return ecs.allOf(MoveComponent, TransformComponent);
    }
    
    // 每帧都会更新
    update(entities: AvatarEntity[]) {
        for(let e of entities) {
            let moveComp = e.Move; // e.get(MoveComponent);
            lel position = e.Transform.position;
            
            position.x += moveComp.speed.x * moveComp.speed * this.dt;
            position.y += moveComp.speed.y * moveComp.speed * this.dt;
            
            e.Transform.angle = cc.misc.lerp(e.Transform.angle, Math.atan2(moveComp.speed.y, moveComp.speed.x) * cc.macro.DEG, dt);
        }
    }
}

export class RenderSystem extends.ecs.ComblockSystem<AvatarEntity> implements ecs.IEntityEnterSystem, ecs.IEntityRemoveSystem {
    filter(): ecs.Matcher {
        return ecs.allOf(NodeComponent, TransformComponent);
    }
    
    // 实体第一次进入MoveSystem会进入此方法
    entityEnter(entities: AvatarEntity[]) {
        for(e of entities) {
            e.Node.val.active = true;
        }
    }
    
    entityRemove(entities: AvatarEntity[]) {
        for(let e of entities) {
            // Global.avatarNodePool.put(e.Node.val);
        }
    }
    
    update(entities: AvatarEntity[]) {
        for(let e of entities) {
            e.Node.val.setPosition(e.Transform.position);
            e.Node.val.angle = e.Transform.angle;
        }
    }
}

3、驱动ecs框架

const { ccclass, property } = cc._decorator;
@ccclass
export class GameControllerBehaviour extends cc.Component {
    rootSystem: RootSystem = null;

    onLoad() {
        this.rootSystem = new RootSystem();
        this.rootSystem.init();
    }
    
    createAvatar(node: cc.Node) {
        let entity = ecs.createEntityWithComps<AvatarEntity>(NodeComponent, TransformComponent, MoveComponent);
        entity.Node.val = node;
        entity.Move.speed = 100;
    }

    update(dt: number) {
        this.rootSystem.execute(dt);
    }
}

调试

添加如下代码

windows['ecs'] = ecs;

在chrome浏览器的console中输入ecs可看到

其中红框内为ecs上下文数据。

相关ecs框架

https://github.com/dualface/ecs-typescript

https://github.com/nomos/lokas-ts

https://github.com/darkoverlordofdata/entitas-ts

https://github.com/NateTheGreatt/bitecs

https://github.com/ecsyjs/ecsy

https://github.com/dannyfritz/flock-ecs

https://github.com/ddmills/geotic

https://github.com/fireveined/perform-ecs

https://github.com/ayebear/picoes

https://github.com/bvalosek/tiny-ecs

5赞

为啥单独弄一套组件系统,creator不是自带一份组件系统么

此组件不是彼组件

ECS限制很大啊,破坏OOP。。。
在js上连它的最大优势(并发)都没了。。

提个醒,使用前最好充分了解ECS优缺点,你要做的是选择最适合的,而不是感觉最牛逼的。

那个字那句话让你感觉我在吹牛逼。。。 :sweat_smile:

最近看到各种各样的ecs框架出来了。之前也有一个ecs框架看起来比你这个还完整一点

https://github.com/esengine/ecs-framework

我没说这是万能的框架,不要自行脑补。后面会写帖子说明它的使用环境。

1赞

额,我也没认为你将它当成万能的。。。

只是说,在unity上,ecs最大的优点是和jobs system结合起来使用,
可以处理大量物体且有各自逻辑的情况。。
js天然没多线程,等于就放弃了这个最大的优点。

至于它的数据拆分,我认为不是优点。
因为拆分本来就不直观、反人类的;
它拆分的目的是为了并发,不是为了方便好用。

要做到向unity那样的ecs需要官方去做。

至于是不是优点,在我看来是要分使用环境,后面我会发帖说说我自己的使用感受,欢迎继续探讨。
:smile:

厉害厉害,我居然没看到过。

在实作ECS之前, 要先调研好javascript的内存分配机制. 目前js很难做到cache友好, 主要还是js无法对物件配置连续的空间, js的物件无法被确保分配到使用者决定好的位置上, 即使组织成数组, 数组里的元素也只是某个物件的引用, 物件真正的位置是由VM决定的

1赞

不明觉厉,先Mark

应该说下ecs适用场景,并不是所有项目都可以用,比如你用ecs是为了解决什么问题,高并发还是。。。 为了解决计算性能还是? 其实目前来看oop还是能解决所有问题 抛开3A不说

改动有点大.把之前多种类的system精简成一种了

是的,感觉逻辑重复,给优化掉了。

ECS最大的优势不是各司其职吗。数据驱动。
我个人感觉ECS不适合的原因是消耗比较大。
我不记得unity对多线程友好……

我就觉得楼主ecs写得最好一个,特别有这个缓存机制,很好解决了这个性能问题!

:grinning:识货。这个框架后面还是会持续优化、完善。

之前在github上也看到了楼主的项目,对楼主的昵称印象深刻。。
ECS系统是守望先锋出来后流行的,数据分离后方便做回滚逻辑。我也正在做一些DEMO试试效果。确实js上可能ECS发挥的空间弱一些