“灵剑出鞘“小游戏开发日志(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 方法来获取 entitiesget 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的时候,它给出技能算组件

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

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

来个demo

首先运行系统各个系统

    timer: number = 0; // hdprivate 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 = 0this.speedY = 0this.speedZ = 0}onAttach(entity) {this.entity = entityconsole.log('实体', entity, '挂载了MoveComp')}onDetach(entity) {this.entity = nullconsole.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 = 1entity1.attachComponent(_MoveComp);// logconsole.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 = entitysetTimeout(() => {comp.onAttach(entity); // 延迟0,防止外部数据未挂载就已经执行}, 0)return comp}static removeComp(comp: Comp) {comp.onDetach(comp.entity);comp.entity = nullcomp.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 方法来获取 entitiesstatic 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来调取系统的静态方法实现
}

本文链接:https://my.lmcjl.com/post/10402.html

展开阅读全文

4 评论

留下您的评论.