背景
以前写过一些cocos小游戏,但是每次做得稍微复杂一点,就不知道代码得写哪里了。直到在遇到oops框架,接触了ecs框架才发现原来开发不只是mvc。然后学习了一波ecs,但是oops看上去有些复杂,如果要写软著,还得去除框架,所以打算让chatgpt给整一个。
ecs理解参考
目标
结合已使用过的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来调取系统的静态方法实现
}