“灵剑出鞘“小游戏开发日志(2)---- 引入ecs框架

背景

以前写过一些cocos小游戏,但是每次做得稍微复杂一点,就不知道代码得写哪里了。直到在遇到oops框架,接触了ecs框架才发现原来开发不只是mvc。然后学习了一波ecs,但是oops看上去有些复杂,如果要写软著,还得去除框架,所以打算让chatgpt给整一个。

ecs理解参考

云风的 BLOG: 浅谈《守望先锋》中的 ECS 构架

ECS框架初识_esc框架_=黄木木=的博客-CSDN博客

目标

结合已使用过的oops架构的理解,用chatgpt4,写一个符合自己游戏运行的ecs框架

开发过程

开始用chatgpt3.5,给出的代码。后来让chatgpt4重新优化整理了。

一个简单的ecs架构(这个是第一版,后面有第二版)

export abstract class Component {

public entity: Entity | null = null;



onAttach(entity: Entity): void {

// Override this method to handle component attachment

}



onDetach(entity: Entity): void {

// Override this method to handle component detachment

}

reset(): void { }

}




export class Entity {
    private components: Map<new () => any, Component> = new Map();
    public readonly id: number;

    constructor(entityManager: EntityManager) {
        this.id = entityManager.generateEntityId();
        this.components = new Map();
    }

    attachComponent<T extends Component>(component: T): T {
        const ctor = component.constructor as unknown as new () => any;
        this.components.set(ctor, component);
        component.onAttach(this);
        component.entity = this;
        return component;
    }
    detachComponent<T extends Component>(componentClass: new () => T): void {
        const component = this.components.get(componentClass);
        if (component) {
            this.components.delete(componentClass);
            component.onDetach(this);
            component.entity = null;
        }
    }

    getComponent<T extends Component>(componentClass: new () => T): T | undefined {
        return this.components.get(componentClass) as T;
    }
}



export class EntityManager {
    private _entities: Map<number, Entity> = new Map();
    private nextEntityId = 0;
    // 添加一个公共的 getter 方法来获取 entities
    get entities(): Map<number, Entity> {
        return this._entities;
    }
    createEntity(): Entity {
        const entity = new Entity(this);
        this.entities.set(entity.id, entity);
        return entity;
    }

    removeEntity(entity: Entity): void {
        this.entities.delete(entity.id);
    }

    getEntity(entityId: number): Entity | undefined {
        return this.entities.get(entityId);
    }

    generateEntityId(): number {
        return this.nextEntityId++;
    }
}



export abstract class PooledComponent extends Component {
    private static objectPool: Map<new () => any, PooledComponent[]> = new Map();

    public static createPool<T extends PooledComponent>(compCls: { new(): T }, initCount: number): void {
        const pool: T[] = [];
        for (let i = 0; i < initCount; ++i) {
            const comp = new compCls() as T;
            comp.reset();
            pool.push(comp);
        }
        this.objectPool.set(compCls, pool);
    }

    public static requestComponent<T extends PooledComponent>(compCls: { new(): T }): T {
        const pool = this.objectPool.get(compCls);
        if (pool) {
            const comp = pool.find(comp => !comp.entity) as T;
            if (comp) {
                comp.reset();
                return comp;
            }
        }
        const newComp = new compCls();
        if (pool) {
            pool.push(newComp);
        } else {
            this.objectPool.set(compCls, [newComp]);
        }
        return newComp;
    }
}



export class SystemManager {
    systems: { apply: (entity: Entity) => void }[] = [];

    addSystem(system: { apply: (entity: Entity) => void }): void {
        this.systems.push(system);
    }

    removeSystem(system: { apply: (entity: Entity) => void }): void {
        const index = this.systems.indexOf(system);
        if (index >= 0) {
            this.systems.splice(index, 1);
        }
    }

    applySystems(entityManager: EntityManager): void {
        for (let system of this.systems) {
            for (let entity of entityManager.entities.values()) {
                system.apply(entity);
            }
        }
    }
}


 

使用疑惑

在使用oops时,技能也作为了一个实体,但我问gpt的时候,它给出技能算组件

技能通常不算是一个实体,而是一个组件,每个技能都可以由一个或多个技能组件组成。技能组件可以包含技能的属性、效果、范围等信息,而实体则是通过拥有多个组件来描述一个游戏对象。

在游戏中,使用技能时通常会创建一个技能对象,该技能对象会负责持有对应的技能组件,并在需要时实例化或销毁其他相关的组件来完成技能的运作。在这个过程中,可以考虑使用组件池技术来优化各个组件对象的创建和销毁。而对于技能对象本身,因为技能的创建和移除都比较少,所以一般情况下不需要使用池来优化。

好吧,这下给我重构增加难度了,原始那套大部分都一下套用不了了。

接着我就问“游戏中属于实体的存在”

游戏中通常会有很多游戏对象,如玩家、敌人、NPC、道具、障碍物等,这些对象都可以被视为实体。具体来说,以下是一些游戏中常见的实体:

  • 玩家对象
  • 敌人对象
  • NPC对象
  • 物品对象(如武器、装备、道具等)
  • 障碍物对象
  • 攻击特效对象(如剑光、火球等)
  • 音效对象
  • 其他各种需要被处理和渲染的游戏对象

每个实体可能会包含多个组件,每个组件描述实体的不同特性,比如位置、速度、血量等。在 ECS 框架中,实体是由多个组件组成,而不是一个单独的游戏对象。

来个demo

首先运行系统各个系统

    timer: number = 0; // hd
    private interval: number = 0.2; // 单位秒

    update(deltaTime: number) {
        this.timer += deltaTime;
        if (this.timer >= this.interval) {
            ooxh.systemManager.applySystems(ooxh.entityManager);
            this.timer = 0;
        }
    }

这里选用了每秒5次调用

先来个移动组件

export class MoveSystem {

    apply(entity: Entity) {
        const moveComponent = entity.getComponent(MoveComp);
        if (moveComponent) {
            const speedX = moveComponent.speedX;
            const speedY = moveComponent.speedY;
            const speedZ = moveComponent.speedZ;

            console.log(`${entity.id}执行了一次MoveSystem${new Date()}`)

            const nodeViewComp = entity.getComponent(NodeViewComp);
            const { x, y, z } = nodeViewComp.node.position;
            nodeViewComp.node.setPosition(x + speedX, y + speedY, z + speedZ);
        }
    }
}

export class MoveComp extends PooledComponent {
    speedX = 0; // X 轴移动速度
    speedY = 0; // Y 轴移动速度
    speedZ = 0; // Z 轴移动速度

    reset() {
        this.speedX = 0
        this.speedY = 0
        this.speedZ = 0
    }

    onAttach(entity) {
        this.entity = entity
        console.log('实体', entity, '挂载了MoveComp')
    }

    onDetach(entity) {
        this.entity = null
        console.log('实体', entity, '移除了MoveComp')
    }
}

在入口文件main.ts去启动

        

        // 新增我们的系统
        ooxh.systemManager.addSystem(new MoveSystem());
        ooxh.systemManager.addSystem(new NodeViewSystem());
        // 创建一个实体
        const entity1 = ooxh.entityManager.createEntity();
        // 加入视图组件
        let _NodeViewComp = PooledComponent.requestComponent(NodeViewComp)
        _NodeViewComp.prefabPath = 'prefabs/sprite'
        entity1.attachComponent(_NodeViewComp);
        // 加入移动组件
        let _MoveComp = PooledComponent.requestComponent(MoveComp)
        _MoveComp.speedX = 1
        entity1.attachComponent(_MoveComp);
        // log
        console.log(entity1)

运行测试记录  

效果预期的达成情况

基本符合我的预期。

但是目前各个系统都是每秒执行5次的样子,我想每个系统都有自己的执行间隔,且可以根据事件触发。(看看)

下一个开发计划

登录页

 PS:2023年6月7日补充:

上面的第一版,作为开发记录暂时就不删除了,在后面开发使用中不断调整。已经改为了下面的

第二版

/**
 * 组件
 */
export abstract class Comp {
    /**
     * 组件池
     */
    private static compsPool: Map<new () => any, Comp[]> = new Map();
    /**
     * 创建组件
     * @param compClass 
     * @returns 
     */
    public static createComp<T extends Comp>(compClass: new () => T, entity: Entity): T {
        // 获取对应组件类的池子
        let pool = this.compsPool.get(compClass);
        // 如果池子不存在,为组件类创建一个新的空池子
        if (!pool) {
            pool = [];
            this.compsPool.set(compClass, pool);
        }
        // 如果池子中有实例,则取出并返回;否则创建一个新实例并返回
        let comp = pool.length > 0 ? pool.pop() as T : new compClass();
        comp.entity = entity
        setTimeout(() => {
            comp.onAttach(entity); // 延迟0,防止外部数据未挂载就已经执行
        }, 0)
        return comp
    }
    static removeComp(comp: Comp) {
        comp.onDetach(comp.entity);
        comp.entity = null
        comp.reset();
        // 获取组件实例的构造函数
        const compClass = comp.constructor as new () => Comp;
        // 从组件池中找到对应的构造函数对应的池子
        const pool = this.compsPool.get(compClass);
        // 如果池子存在,将组件实例放回池子中
        if (pool) {
            pool.push(comp);
        } else {
            // 如果池子不存在,创建一个新的池子并将组件实例放入
            this.compsPool.set(compClass, [comp]);
        }
        // console.log('回收组件,当前组件池', this.compsPool)
    }
    /**
     * 单体组件的实体
     */
    public entity: Entity | null = null;
    /** 
     * 组件完成后的回调
     */
    abstract callback: Function;
    /** 监听挂载到实体 */
    abstract onAttach(entity: Entity)
    /** 监听从实体卸载 */
    abstract onDetach(entity: Entity)
    /** 重置 */
    abstract reset()
}



export class Entity {
    private static _entities: Map<number, Entity> = new Map();
    private static nextEntityId = 0;
    // 添加一个公共的 getter 方法来获取 entities
    static get entities(): Map<number, Entity> {
        return this._entities;
    }
    static createEntity(): Entity {
        const entity = new Entity();
        this.entities.set(entity.id, entity);
        return entity;
    }

    static removeEntity(entity: Entity): void {
        // console.log('卸载实体', entity)
        for (let component of entity.components.values()) {
            Comp.removeComp(component)
        }
        this.entities.delete(entity.id);
    }

    static getEntity(entityId: number): Entity | undefined {
        return this.entities.get(entityId);
    }

    static generateEntityId(): number {
        return this.nextEntityId++;
    }

    /** 单实体上挂载的组件 */
    public components: Map<new () => any, Comp> = new Map();
    /** 实体id */
    public readonly id: number;

    constructor() {
        this.id = Entity.generateEntityId();
        this.components = new Map();
    }

    /** 单实体上挂载组件 */
    attachComponent<T extends Comp>(componentClass: new () => T): T {
        const hascomponent = this.components.get(componentClass) as T;
        if (hascomponent) {
            console.error('已存在组件,不会触发挂载事件')
            return hascomponent;
        } else {
            const component = Comp.createComp(componentClass, this);
            this.components.set(componentClass, component);
            // console.log('实体挂载了组件', this.components, this)
            return component;
        }
    }

    /** 单实体上卸载组件 */
    detachComponent<T extends Comp>(componentClass: new () => T): void {
        const component = this.components.get(componentClass);
        if (component) {
            this.components.delete(componentClass);
            Comp.removeComp(component)
            // console.log('实体卸载了组件', this.components, this)
        }
    }

    getComponent<T extends Comp>(componentClass: new () => T): T | undefined {
        return this.components.get(componentClass) as T;
    }

}




export abstract class System {
    // 为了简化系统,系统内方法都是静态方法,直接调用不需new
    // 如果系统有 定时性的 需要 借用 views 里cocos中Component的update来调取系统的静态方法实现
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
hexdump -e 是一个在Linux命令行中使用的命令,它用于以自定义格式显示文件的十六进制内容。 通过使用不同的选项和参数,可以指定输出的格式,并且可以以不同的方式处理二进制数据。例如,可以使用 -e fmt 选项指定自定义的格式字符串来显示文件的特定部分。 在一个名为 golang-hex-dumper 的库例程中,也有一个简单的十六进制转储工具,用于诊断需要深入研究二进制文件的情况。这个工具可以显示字节的十六进制表示。 更多关于hexdump命令的详细信息,可以参考原文链接:Linux命令--hexdump(以16进制查看文件内容)_IT利刃出鞘的博客。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Linux命令--hexdump(以16进制查看文件内容)](https://blog.csdn.net/feiying0canglang/article/details/125569804)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [golang-hex-dumper](https://download.csdn.net/download/weixin_42099814/18297740)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值