From c25c1554f344f54a95b1f472d5f6b28153b723a1 Mon Sep 17 00:00:00 2001 From: gongxh Date: Wed, 4 Jun 2025 22:19:20 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8B=86=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 18 +- README.md | 31 +- docs/Asset.md | 102 ------ docs/BehaviorTree.md | 141 ------- docs/EC.md | 524 --------------------------- docs/QuadTree.md | 132 ------- image/image-entity-editor.png | Bin 143553 -> 0 bytes package.json | 7 +- src/asset/AssetLoader.ts | 336 ----------------- src/asset/AssetPool.ts | 170 --------- src/asset/AssetUtils.ts | 52 --- src/behaviortree/Agent.ts | 48 --- src/behaviortree/BTNode/Action.ts | 189 ---------- src/behaviortree/BTNode/BaseNode.ts | 113 ------ src/behaviortree/BTNode/Composite.ts | 206 ----------- src/behaviortree/BTNode/Condition.ts | 24 -- src/behaviortree/BTNode/Decorator.ts | 367 ------------------- src/behaviortree/BehaviorTree.ts | 61 ---- src/behaviortree/Blackboard.ts | 105 ------ src/behaviortree/Ticker.ts | 55 --- src/behaviortree/header.ts | 27 -- src/cocos/CocosEntry.ts | 9 - src/ecmodule/Component.ts | 112 ------ src/ecmodule/ComponentManager.ts | 290 --------------- src/ecmodule/ComponentPool.ts | 99 ----- src/ecmodule/ECDataHelper.ts | 109 ------ src/ecmodule/ECDecorator.ts | 141 ------- src/ecmodule/ECManager.ts | 172 --------- src/ecmodule/ECType.ts | 90 ----- src/ecmodule/Entity.ts | 276 -------------- src/ecmodule/EntityManager.ts | 442 ---------------------- src/ecmodule/ObjectBase.ts | 17 - src/ecmodule/ObjectFactory.ts | 65 ---- src/fgui/WindowBase.ts | 8 +- src/kunpocc.ts | 32 -- src/net/http/HttpManager.ts | 15 +- src/quadtree/Box.ts | 38 -- src/quadtree/Circle.ts | 42 --- src/quadtree/IShape.ts | 32 -- src/quadtree/Polygon.ts | 92 ----- src/quadtree/QuadTree.ts | 372 ------------------- src/quadtree/Shape.ts | 75 ---- 42 files changed, 48 insertions(+), 5188 deletions(-) delete mode 100644 docs/Asset.md delete mode 100644 docs/BehaviorTree.md delete mode 100644 docs/EC.md delete mode 100644 docs/QuadTree.md delete mode 100644 image/image-entity-editor.png delete mode 100644 src/asset/AssetLoader.ts delete mode 100644 src/asset/AssetPool.ts delete mode 100644 src/asset/AssetUtils.ts delete mode 100644 src/behaviortree/Agent.ts delete mode 100644 src/behaviortree/BTNode/Action.ts delete mode 100644 src/behaviortree/BTNode/BaseNode.ts delete mode 100644 src/behaviortree/BTNode/Composite.ts delete mode 100644 src/behaviortree/BTNode/Condition.ts delete mode 100644 src/behaviortree/BTNode/Decorator.ts delete mode 100644 src/behaviortree/BehaviorTree.ts delete mode 100644 src/behaviortree/Blackboard.ts delete mode 100644 src/behaviortree/Ticker.ts delete mode 100644 src/behaviortree/header.ts delete mode 100644 src/ecmodule/Component.ts delete mode 100644 src/ecmodule/ComponentManager.ts delete mode 100644 src/ecmodule/ComponentPool.ts delete mode 100644 src/ecmodule/ECDataHelper.ts delete mode 100644 src/ecmodule/ECDecorator.ts delete mode 100644 src/ecmodule/ECManager.ts delete mode 100644 src/ecmodule/ECType.ts delete mode 100644 src/ecmodule/Entity.ts delete mode 100644 src/ecmodule/EntityManager.ts delete mode 100644 src/ecmodule/ObjectBase.ts delete mode 100644 src/ecmodule/ObjectFactory.ts delete mode 100644 src/quadtree/Box.ts delete mode 100644 src/quadtree/Circle.ts delete mode 100644 src/quadtree/IShape.ts delete mode 100644 src/quadtree/Polygon.ts delete mode 100644 src/quadtree/QuadTree.ts delete mode 100644 src/quadtree/Shape.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f8a751f..945009f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,4 +21,20 @@ ## 1.0.35 - 修复未配置GameEntiry中的ecConfig时报错的问题 ## 1.0.38 -- 修复适配器设计尺寸设置错误的问题 \ No newline at end of file +- 修复适配器设计尺寸设置错误的问题 + +## 1.1.0 模块拆分 +- 拆分资源管理模块,使用 `npm install kunpocc-assets` 安装 + * 仓库地址: https://github.com/Gongxh0901/kunpocc-assets + +- 拆分ec模块,使用 `npm install kunpocc-ec` 安装 + * 仓库地址: https://github.com/Gongxh0901/kunpo-ec + +- 拆分ecs模块,使用 `npm install kunpocc-ecs` 安装 + * 仓库地址: https://github.com/Gongxh0901/kunpo-esc + +- 拆分四叉树模块,使用 `npm install kunpocc-quadtree` 安装 + * 仓库地址: https://github.com/Gongxh0901/kunpo-quadtree + +- 拆分行为树模块,使用 `npm install kunpocc-behaviortree` 安装 + * 仓库地址: https://github.com/Gongxh0901/kunpocc-behaviortree \ No newline at end of file diff --git a/README.md b/README.md index 148b8b7..ee5b16e 100644 --- a/README.md +++ b/README.md @@ -40,21 +40,22 @@ npm set registry https://npm.aliyun.com 2. [项目配置](./docs/Basic.md) 3. [UI模块](./docs/UI.md) -4. [实体组件模块](./docs/EC.md) -5. [网络模块](./docs/HTTP.md) -6. [四叉树](./docs/QuadTree.md) -7. [行为树](./docs/BehaviorTree.md) -8. [资源管理](./docs/Asset.md) -9. [条件显示节点 (一般用于UI上的红点)](./docs/Condition.md) -10. [全局事件](./docs/Event.md) -11. [全局计时器](./docs/Timer.md) -12. [平台工具](./docs/Platform.md) -13. [屏幕尺寸](./docs/Screen.md) -14. [小工具](./docs/Tools.md) -15. [时间](./docs/Time.md) -16. [socket网络模块](./docs/Socket.md) -17. [小游戏接口封装](./docs/MiniGame.md) -18. [热更新](./docs/HotUpdate.md) +4. [ec模块](https://github.com/Gongxh0901/kunpo-ec) +5. [ecs模块](https://github.com/Gongxh0901/kunpo-esc) +6. [网络模块](./docs/HTTP.md) +7. [四叉树](https://github.com/Gongxh0901/kunpo-quadtree) +8. [行为树](https://github.com/Gongxh0901/kunpocc-behaviortree) +9. [资源管理](https://github.com/Gongxh0901/kunpocc-assets) +10. [条件显示节点 (一般用于UI上的红点)](./docs/Condition.md) +11. [全局事件](./docs/Event.md) +12. [全局计时器](./docs/Timer.md) +13. [平台工具](./docs/Platform.md) +14. [屏幕尺寸](./docs/Screen.md) +15. [小工具](./docs/Tools.md) +16. [时间](./docs/Time.md) +17. [socket网络模块](./docs/Socket.md) +18. [小游戏接口封装](./docs/MiniGame.md) +19. [热更新](./docs/HotUpdate.md) ## 类型支持 该库完全使用 TypeScript 编写,提供完整的类型定义文件。 diff --git a/docs/Asset.md b/docs/Asset.md deleted file mode 100644 index 85cc457..0000000 --- a/docs/Asset.md +++ /dev/null @@ -1,102 +0,0 @@ -## 资源加载 -> !!! 注意:资源加载多次和一次效果一样 - -### 特点 - * 可通过路径或者uuid获取资源 - - * 只适合手动管理资源,单无论加载多少次,卸载一次后删除 - - * 可根据 `new kunpo.AssetLoader("batchName")` 传入的 `batchName`批量卸载资源 - - > 比如进入战斗时,创建了多个new kunpo.AssetLoader("batchName") 来加载资源,传入的batchName相同 - > - > 等退出战斗后,可以通过 AssetPool.releaseBatchAssets("batchName") 一键释放所有等于batchName的资源 - -### 使用 -```typescript - let paths: kunpo.IAssetConfig[] = [ - { path: "ui/manual", type: cc.Asset }, - { path: "prefab", type: cc.Prefab }, - { path: "icon", type: cc.SpriteFrame }, - { path: "texture/6101/spriteFrame", type: cc.SpriteFrame, isFile: true }, - { path: "pet", type: cc.SpriteFrame, bundle: "bundle_res" }, - ]; - let loader = new kunpo.AssetLoader("batchName"); - loader.start({ - configs: paths, - complete: () => { - console.log("加载完成"); - }, - fail: (msg: string, err: Error) => { - console.log("加载失败", msg, err); - }, - progress: (percent: number) => { - console.log("加载进度", percent); - } - }); -``` - -### 接口 -#### *资源加载器* - -```typescript -interface IAssetConfig { - /** 资源类型 */ - type: typeof Asset; - /** 资源路径 */ - path: string; - /** 是否是单个文件 默认是文件夹 */ - isFile?: boolean; - /** 资源包名 默认 resources */ - bundle?: string; -} - -/** - * 开始加载资源 - * @param {IAssetConfig[]} res.configs 资源配置 - * @param {number} res.parallel 并行加载数量 默认 10 - * @param {number} res.retry 失败重试次数 默认 3 - * @param {Function} res.complete 加载完成回调 - * @param {Function} res.progress 加载进度回调 - * @param {Function} res.fail 加载失败回调 - */ -public start(res: { configs: IAssetConfig[], parallel?: number, retry?: number, complete: () => void, fail: (msg: string, err: Error) => void, progress?: (percent: number) => void }): void - -/** 重试 重新加载失败的资源 */ -public retry(): void -``` - -#### *资源池* - -```typescript -/** 资源是否已加载 */ -public static has(path: string, bundlename: string = "resources"): boolean - -/** 获取资源 */ -public static get(path: string, bundlename: string = "resources"): T - -/** 按 uuid 判断资源是否已加载 */ -public static hasUUID(uuid: string): boolean - -/** 按 uuid 获取资源 */ -public static getByUUID(uuid: string): T - -/** 按资源路径释放资源 */ -public static releasePath(path: string, bundlename: string = "resources"): void - -/** 按 bundle 和 文件夹释放资源 */ -public static releaseDir(dir: string, bundlename: string = "resources", asset: typeof Asset): Promise - -/** 按 uuid 释放资源 */ -public static releaseUUID(uuid: string): void - -/** 释放所有加载的资源 */ -public static releaseAll(): void - -/** - * 按资源加载批次释放资源 - * @param batchName 资源加载批次名 对应 AssetLoader 实例化时传入的 name - */ -public static releaseBatchAssets(batchName: string): void; -``` - diff --git a/docs/BehaviorTree.md b/docs/BehaviorTree.md deleted file mode 100644 index 1b15e04..0000000 --- a/docs/BehaviorTree.md +++ /dev/null @@ -1,141 +0,0 @@ -## 行为树 - -> 行为树是一种强大的 AI 决策系统,用于实现复杂的游戏 AI 行为。 - -#### 基本概念 - -1. 节点状态 -```typescript -enum Status { - SUCCESS, // 成功 - FAILURE, // 失败 - RUNNING // 运行中 -} -``` - -2. 节点类型 -- **动作节点 (Action)**:执行具体行为的叶子节点 -- **组合节点 (Composite)**:控制子节点执行顺序的节点 -- **条件节点 (Condition)**:判断条件的节点 -- **装饰节点 (Decorator)**:修饰其他节点行为的节点 - -#### 使用示例 - -```typescript -import { - BehaviorTree, - Sequence, - Selector, - Parallel, - Success, - Failure, - WaitTime, - Agent, - Blackboard -} from 'kunpocc'; - -// 1. 创建行为树 -const tree = new BehaviorTree( - new Sequence( // 顺序节点:按顺序执行所有子节点 - new WaitTime(2), // 等待2秒 - new Selector( // 选择节点:选择一个可执行的子节点 - new Success(() => { - console.log("执行成功动作"); - }), - new Failure(() => { - console.log("执行失败动作"); - }) - ) - ) -); - -// 2. 创建代理和黑板 -const agent = new Agent(); // AI代理 -const blackboard = new Blackboard(); // 共享数据黑板 - -// 3. 执行行为树 -tree.tick(agent, blackboard); -``` - -#### 常用节点 - -1. 组合节点 - - ```typescript - // 顺序节点:按顺序执行所有子节点,直到遇到失败或运行中的节点 - new Sequence(childNode1, childNode2, childNode3); - - // 选择节点:选择第一个成功或运行中的子节点 - new Selector(childNode1, childNode2, childNode3); - - // 并行节点:同时执行所有子节点 - new Parallel(childNode1, childNode2, childNode3); - - // 记忆顺序节点:记住上次执行的位置 - new MemSequence(childNode1, childNode2, childNode3); - - // 记忆选择节点:记住上次执行的位置 - new MemSelector(childNode1, childNode2, childNode3); - - // 随机选择节点:随机选择一个子节点执行 - new RandomSelector(childNode1, childNode2, childNode3); - ``` - -2. 动作节点 - - ```typescript - // 成功节点 - new Success(() => { - // 执行动作 - }); - - // 失败节点 - new Failure(() => { - // 执行动作 - }); - - // 运行中节点 - new Running(() => { - // 持续执行的动作 - }); - - // 等待节点 - new WaitTime(2); // 等待2秒 - new WaitTicks(5); // 等待5个tick - ``` - -3. 使用黑板共享数据 - - ```typescript - // 在节点中使用黑板 - class CustomAction extends Action { - tick(ticker: Ticker): Status { - // 获取数据 - const data = ticker.blackboard.get("key"); - - // 设置数据 - ticker.blackboard.set("key", "value"); - - return Status.SUCCESS; - } - } - ``` - - -#### 注意事项 - -1. 节点状态说明: - - `SUCCESS`:节点执行成功 - - `FAILURE`:节点执行失败 - - `RUNNING`:节点正在执行中 -2. 组合节点特性: - - `Sequence`:所有子节点返回 SUCCESS 才返回 SUCCESS - - `Selector`:任一子节点返回 SUCCESS 就返回 SUCCESS - - `Parallel`:并行执行所有子节点 - - `MemSequence/MemSelector`:会记住上次执行位置 -3. 性能优化: - - 使用黑板共享数据,避免重复计算 - - 合理使用记忆节点,减少重复执行 - - 控制行为树的深度,避免过于复杂 - - diff --git a/docs/EC.md b/docs/EC.md deleted file mode 100644 index 0c0a97e..0000000 --- a/docs/EC.md +++ /dev/null @@ -1,524 +0,0 @@ -## 实体组件模块 -> 实体组件系统是一种用于游戏开发的架构模式,它将游戏对象(实体)的数据(组件)和行为分离。 - -### 特点 - * 不同实体上的组件更新顺序管理(`只根据注册的组件更新顺序更新,跟实体无关`) - * 灵活的EC装饰器 (配合插件 `kunpo-ec` 使用,配置实体组件信息,一键导出) - * 支持多世界(多战斗场景,互不影响) - * 区分数据组件和逻辑组件,只更新逻辑组件 - -### 插件链接 - -* **kunpo-ec**: [https://store.cocos.com/app/detail/7311](https://store.cocos.com/app/detail/7311) - -### 使用 - -#### *creator插件`kunpo-ec`* -> `kunpo-cc`可以方便创建、配置、导出实体,操作界面如下图: - -![image-entity-editor](../image/image-entity-editor.png#pic_left) - -#### *使用* - -1. 组件类型声明 - - ```typescript - /** - * @Author: gongxh - * @Date: 2025-01-23 - * @Description: 组件枚举 - */ - - import { cc } from "../header"; - - /** 数据组件类型 */ - enum DataComponentType { - Health, - Transform, - RootNode, - LimitMove, - /** 渲染组件 (多个) */ - Render, - } - - /** 逻辑组件类型 (组件更新数据从上到下) */ - export enum SystemComponentType { - Move = 100000, - ScreenRebound, - - /** 位置更新系统 */ - PositionUpdateSystem = 120000, - } - - export const ComponentType = { - ...DataComponentType, - ...SystemComponentType - }; - export type ComponentType = DataComponentType | SystemComponentType; - - /** 自定义组件更新顺序列表 */ - export const componentUpdateOrderList = cc.Enum.getList(cc.Enum(SystemComponentType)).map(item => item.value).sort((a, b) => a - b); - ``` - -2. 编写组件脚本 - - ```typescript - import { AnimationClip, Asset, AudioClip, Color, Enum, JsonAsset, ParticleAsset, Prefab, Size, Skeleton, SpriteFrame, Vec2, Vec3 } from "cc"; - import { _ecdecorator, Component } from "kunpocc"; - import { ComponentType } from "../../ComponentTypes"; - const { ecclass, ecprop } = _ecdecorator; - - enum HealthType { - HP = 1, - Max = 2, - Current = 3 - } - - // 注册组件 (必须) - @ecclass("Health", ComponentType.Health, { describe: "血量组件" }) - export class Health extends Component { - // 注册组件属性 (可选: 使用kunpo-ec插件则必须注册) - @ecprop({ type: "entity", defaultValue: "", displayName: "实体", tips: "实体" }) - private testentity: string = ""; - - @ecprop({ type: "array", format: "entity", displayName: "实体数组", tips: "实体数组" }) - private testentityarray: string[] = []; - - @ecprop({ type: 'int', defaultValue: 0, displayName: "血量", tips: "当前血量提示" }) - private hp: number = 0; - - @ecprop({ type: 'float', defaultValue: 0, displayName: "最大血量", tips: "最大血量提示" }) - private maxHp: number = 0; - - @ecprop({ type: 'string', defaultValue: "", displayName: "字符串", tips: "字符串提示" }) - private string: string = ""; - - @ecprop({ type: 'boolean', defaultValue: false, displayName: "布尔值", tips: "布尔值提示" }) - private bool: boolean = true; - - @ecprop({ type: "enum", format: Enum(HealthType), defaultValue: HealthType.Current, displayName: "枚举", tips: "枚举提示" }) - private hpeunm: HealthType = HealthType.Current; - - @ecprop({ type: "spriteframe", displayName: "精灵帧" }) - private spriteFrame: SpriteFrame; - - @ecprop({ type: "asset", displayName: "资源" }) - private asset: Asset; - - @ecprop({ type: "prefab", displayName: "预制体" }) - private prefab: Prefab; - - @ecprop({ type: "skeleton", displayName: "骨骼动画" }) - private skeleton: Skeleton; - - @ecprop({ type: "particle", displayName: "粒子" }) - private particle: ParticleAsset; - - @ecprop({ type: "animation", displayName: "动画" }) - private animation: AnimationClip; - - @ecprop({ type: "audio", displayName: "音频" }) - private audio: AudioClip; - - @ecprop({ type: "jsonAsset", displayName: "json资源" }) - private jsonAsset: JsonAsset; - - @ecprop({ - type: "object", format: { - hp1: { - type: "object", - format: { - hp: "int", - max: "int" - } - }, - hp2: { - type: "object", - format: { - hp: "int", - max: "int" - } - }, - }, - }) - private obj: { hp1: { hp: number, max: number }, hp2: { hp: number, max: number } }; - - @ecprop({ - type: "array", format: "int", - }) - private arr: number[]; - - @ecprop({ - type: "array", format: { type: "object", format: { hp: "int", max: "int" } } - }) - private arrobj: { hp: number, max: number }[]; - - @ecprop({ type: "vec2", displayName: "向量2" }) - private vec2: Vec2; - - @ecprop({ type: "vec3", displayName: "向量3" }) - private vec3: Vec3; - - @ecprop({ type: "color", defaultValue: Color.RED, displayName: "颜色" }) - private color: Color; - - @ecprop({ type: "size", displayName: "尺寸" }) - private size: Size; - - protected onAdd(): void { - // 设置组件是否更新,只有需要更新的组件才设置 - this.needUpdate = true; - } - - protected onEnter(): void { - // 可在此获取同实体上的其他组件 - let transform = this.getComponent(ComponentType.Transform); - /** 获取单例组件 */ - let signleton = this.entity.entityManager.getSingleton(ComponentType.XXXX); - } - - protected onRemove(): void { - // 清理组件数据 - } - } - ``` - -3. 创建ec世界,并设置更新 - - ```typescript - /** - * @Author: Gongxh - * @Date: 2025-01-16 - * @Description: 战斗界面 - */ - - import { ECManager } from "kunpocc"; - import { componentUpdateOrderList } from "../../ec/ComponentTypes"; - import { cc, fgui, kunpo } from "../../header"; - const { uiclass, uiprop, uiclick } = kunpo._uidecorator; - - @uiclass("Window", "Game", "GameWindow") - export class GameWindow extends kunpo.Window { - @uiprop container: fgui.GComponent; - public onInit() { - console.log("GameWindow onInit"); - this.adapterType = kunpo.AdapterType.Full; - this.type = kunpo.WindowType.CloseAll; - this.bgAlpha = 0; - } - - protected onShow() { - console.log("GameWindow onShow"); - /** 创建一个ec世界的节点 */ - let node = new cc.Node(); - this.container.node.addChild(node); - - /** - * 创建一个ec世界 - * 参数1: 世界名称 - * 参数2: 世界节点 - * 参数3: 组件更新顺序列表 - * 参数4: 实体池的最大缓存数量,多余的不会被缓存,根据需要调整 - * 参数5: 预创建的实体数量,根据需要调整 - */ - kunpo.log("需要更新的组件", componentUpdateOrderList); - ECManager.createECWorld("world", node, componentUpdateOrderList, 100, 10); - } - - protected onClose() { - /** 退出游戏时 销毁ec世界 */ - ECManager.destroyECWorld("world"); - } - - @uiclick - private onBack(): void { - kunpo.WindowManager.showWindow("HomeWindow"); - } - - @uiclick - private onCreateEntity(): void { - /** 创建一个实体 */ - ECManager.createEntity("world", "entity1"); - } - - protected onUpdate(dt: number): void { - /** 更新ec世界 */ - ECManager.getECWorld("world").update(dt); - } - } - ``` - -#### 重点接口 - -注:详细说明查看声明文件 `kunpocc.d.ts` - -1. 总管理器 `ECManager` - - ```typescript - /**注册所有组件 如果GameEntry因分包导致,组件的代码注册晚于CocosEntry的onInit函数, 则需要在合适的时机手动调用此方法*/ - public static registerComponents(): void - - /** - * 创建EC世界 创建EC世界前必须先注册组件 - * @param {string} worldName 名称 - * @param {Node} node 世界节点 - * @param {number[]} componentUpdateOrderList 组件更新顺序列表 (只传需要更新的组件列表) - * @param {number} [maxCapacityInPool=128] 实体池最大容量,多余的实体不会缓存 - * @param {number} [preloadEntityCount=32] 预加载Entity数量 - */ - public static createECWorld(worldName: string, node: Node, componentUpdateOrderList: number[], maxCapacityInPool = 128, preloadEntityCount = 32): EntityManager - - /** 获取EC世界 */ - public static getECWorld(worldName: string): EntityManager - - /** 获取EC世界节点 */ - public static getECWorldNode(worldName: string): Node - - /** 销毁EC世界 */ - public static destroyECWorld(worldName: string): void - - /** - * 注册配置表中的实体信息 - * 如果在GameEntry中配置了ecConfig,则此方法会自动调用 - * @param config 实体配置信息,格式为 {实体名: {组件名: 组件数据}} - */ - public static registerEntityConfig(config: { [entityName: string]: IEntityConfig }): void - - /** - * 添加实体信息 (如果已经存在, 则数据组合) - * 如果存在编辑器编辑不了的数据 用来给编辑器导出的实体信息 添加扩展数据 - * @param name 实体名 - * @param info 实体信息 - */ - public static addEntityInfo(name: string, info: IEntityConfig): void - - /** 获取实体配置信息 */ - public static getEntityInfo(name: string): Record - - /** - * 创建实体 - * @param worldName 实体管理器名称 - * @param name 实体名字 - * @returns {kunpo.Entity} 实体 - */ - public static createEntity(worldName: string, name: string): Entity - - /** - * 销毁实体 - * @param worldName 世界名称 - * @param entity 实体 - */ - public static destroyEntity(worldName: string, entity: Entity): void - - /** - * 通过实体ID销毁实体 - * @param worldName 世界名称 - * @param entityId 实体ID - */ - public static destroyEntityById(worldName: string, entityId: number): void - ``` - -2. 实体管理器 (创建的world)`EntityManager ` - - ```typescript - /** - * 通过实体ID获取实体 - * @param {EntityId} entityId 实体Id - * @returns {(Entity | null)} 实体 - */ - public getEntity(entityId: EntityId): Entity | null - - /** - * 获取指定标签的实体 - * @param {number} tag 标签 - * @returns {Entity[]} 返回的实体池 - */ - public getEntitiesByTag(tag: number): Entity[] - - /** - * 根据实体ID判断实体是否存在 - * @param {EntityId} entityId 实体Id - * @returns {boolean} - */ - public exists(entityId: EntityId): boolean - - /** 添加单例组件 */ - public addSingleton(component: Component): void - - /** 获取单例组件 */ - public getSingleton(componentType: number): T - - /** 删除单例组件 */ - public removeSingleton(componentType: number): void - - /** 是否存在对应的单例组件 */ - public hasSingleton(componentType: number): boolean - - /** 激活单例组件 */ - public activeSingleton(): void - - - /** 更新 需要外部调用 */ - public update(dt: number): void - ``` - -3. 实体 `Entity` - - ```typescript - /** 实体名称 */ - public name: string; - - /** 实体ID */ - public id: EntityId; - - /** 实体标识 */ - public tags: Set; - - /** 实体状态 */ - public states: Map; - - /** 是否被激活 (添加到实体管理器时激活) */ - public active: boolean = false; - - /** 所属实体管理器 (实体创建后直接赋值) */ - public entityManager: EntityManager; - - /** 所有组件 */ - public readonly components: Map = new Map(); - - /** 添加标签 标签除了表示Entity,还可以通过EntityManager获取指定标签的Entity */ - public addTag(...tag: number[]): void - - /** 删除标签 */ - public removeTag(tag: number): void - - /** 是否包含标签 */ - public hasTag(...tag: number[]): boolean - - /** 获取组件 */ - public getComponent(componentType: number): T - - /** 添加组件 */ - public addComponent(component: Component): void - - /** 删除组件 */ - public removeComponent(componentType: number): void - - /** 删除所有组件 */ - public removeAllComponents(): void - - /** - * 是否包含组件 - * @param {number} componentType 组件类型 - */ - public hasComponent(componentType: number): boolean - - /** 销毁自己 */ - public destroy(): void { - this.entityManager.destroyEntityById(this.id); - } - - /** - * 添加监听 - * @param eventName 监听的消息名 - * @param callback 回调 - * @param entityId 实体ID - * @param once 是否单次监听 - */ - public addEvent(eventName: string, callback: (...args: any[]) => void, once: boolean = false): void - - /** - * 发送消息 - * @param eventName 消息名 - * @param entityId 实体ID - * @param args 发送参数 - */ - public sendListener(eventName: string, ...args: any[]): void - - /** 删除监听 */ - public removeListener(eventName: string, callback?: (...args: any[]) => void): void - - /** - * 添加状态 - * 状态采用计数方式,对状态处理时需要保证addState和removeState成对存在 - * @param {number} state 状态类型 - */ - public addState(state: number): void - - /** - * 删除状态 - * @param {number} state 状态类型 - * @returns {boolean} 如果计数为0或状态不存在,则返回true - */ - public removeState(state: number): boolean - - /** 是否包含指定状态 */ - public hasState(state: number): boolean - - /** 清除状态 */ - public clearState(state: number): void - - /** 清除所有状态 */ - public clearAllStates(): void - ``` - - - -4. 组件 `Component` - - ```typescript - /** 组件名 */ - public name: string; - - /** 组件类型 */ - public type: number; - - /** 是否需要更新 */ - public needUpdate: boolean; - - /** 所属实体 */ - public entity: Entity; - - /** 所属组件管理器 */ - public componentManager: ComponentManager; - - /** - * 获取同实体上的组件 - * @param {number} componentType 组件类型 - */ - public getComponent(componentType: number): T - - /** 删除自己 */ - public destroySelf(): void - - /** - * 生命周期函数 - * 被添加到实体 对应onDestroy - */ - protected onAdd(): void - - /** - * 生命周期函数 - * 组件被销毁 对应onAdd - */ - protected onDestroy(): void - - /** - * 生命周期函数 - * 可在此方法获取实体其他组件 - */ - protected abstract onEnter(): void; - - /** - * 生命周期函数 - * 从实体中删除前执行的函数 在此函数中清理初始化的数据 - */ - protected abstract onRemove(): void; - - /** - * 更新函数 - */ - protected onUpdate(dt: number): void - ``` - - \ No newline at end of file diff --git a/docs/QuadTree.md b/docs/QuadTree.md deleted file mode 100644 index 5a7118c..0000000 --- a/docs/QuadTree.md +++ /dev/null @@ -1,132 +0,0 @@ -## 四叉树 -> 四叉树是一种通过空间划分来进行高效碰撞查询的数据结构。 - -#### 基本概念 - -1. 形状类型 - - ```typescript - import { QuadTree, Box, Circle, Polygon } from 'kunpocc'; - - // 1. 矩形 - const box = new Box(x, y, width, height, tag); - - // 2. 圆形 - const circle = new Circle(x, y, radius, tag); - - // 3. 多边形 - const points = [v2(x1, y1), v2(x2, y2), v2(x3, y3)]; - const polygon = new Polygon(points, tag); - ``` - -2. 配置参数 - - ```typescript - // 四叉树配置 - const QTConfig = { - MAX_SHAPES: 12, // 每个节点最大形状数量 - MAX_LEVELS: 5, // 最大深度 - } - ``` - - -#### 使用示例 - -1. 创建和初始化 - - ```typescript - import { QuadTree, Box, rect } from 'kunpocc'; - - // 创建四叉树(参数:区域范围,层级,绘制组件) - const bounds = rect(0, 0, 800, 600); // x, y, width, height - const quadTree = new QuadTree(bounds); - - // 添加形状 - const player = new Box(100, 100, 50, 50, 1); // 玩家碰撞体,tag=1 - const enemy = new Circle(200, 200, 25, 2); // 敌人碰撞体,tag=2 - quadTree.insert(player); - quadTree.insert(enemy); - ``` - -2. 碰撞检测 - - ```typescript - // 检测指定形状与特定标签的碰撞 - const collisions = quadTree.collide(player, 2); // 检测玩家与 tag=2 的形状碰撞 - if (collisions.length > 0) { - console.log('发生碰撞!'); - for (const target of collisions) { - // 处理碰撞逻辑 - } - } - ``` - -3. 动态更新 - - ```typescript - // 在游戏循环中更新四叉树 - function update() { - // 更新形状位置 - player.position = v2(newX, newY); - enemy.position = v2(newX, newY); - - // 更新四叉树 - quadTree.update(); - - // 检测碰撞 - const collisions = quadTree.collide(player, 2); - } - ``` - -4. 清理 - - ```typescript - // 清理四叉树 - quadTree.clear(); - ``` - - -#### 形状操作 - -1. 位置和缩放 - - ```typescript - // 设置位置 - shape.position = v2(x, y); - - // 设置缩放 - shape.scale = 1.5; - - // 获取包围盒 - const boundingBox = shape.getBoundingBox(); - ``` - -2. 特定形状操作 - - ```typescript - // 矩形重置 - box.resetPoints(x, y, width, height); - - // 圆形半径 - circle.radius = newRadius; - - // 多边形顶点 - polygon.points = newPoints; - ``` - - -#### 性能优化建议 - -1. 合理设置配置参数: - - `MAX_SHAPES`:较小的值会导致更频繁的分裂,较大的值会降低查询效率 - - `MAX_LEVELS`:控制树的最大深度,防止过度分割 - -2. 碰撞检测优化: - - 使用合适的标签系统,只检测需要的碰撞 - - 根据游戏需求选择合适的形状(圆形计算最快) - - 避免使用过于复杂的多边形 - -3. 更新策略: - - 仅在必要时更新四叉树 - - 对于静态物体,可以使用单独的四叉树 - - 动态物体频繁更新时,考虑使用更大的边界范围 \ No newline at end of file diff --git a/image/image-entity-editor.png b/image/image-entity-editor.png deleted file mode 100644 index 9261b124048ebb578b7b54d01169f6a75a81aa1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 143553 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 void; - - /** - * 加载完成回调 - * @internal - */ - private _complete: () => void; - - /** - * 加载失败回调 - * @internal - */ - private _fail: (msg: string, err: Error) => void; - - /** - * 资源配置 - * @internal - */ - private _configs: IAssetConfig[] = []; - /** - * 资源加载项 - * @internal - */ - private _items: { type: typeof Asset, bundle: string, path: string, isFile?: boolean, status: StateType, count: number }[] = []; - /** - * 加载完成数量 - * @internal - */ - private _completeCounts: Map = new Map(); - - constructor(batchName?: string) { - this._name = batchName || ""; - } - - /** - * 开始加载资源 - * @param {IAssetConfig[]} res.configs 资源配置 - * @param {number} res.parallel 并行加载数量 默认 10 - * @param {number} res.retry 失败重试次数 默认 3 - * @param {Function} res.complete 加载完成回调 - * @param {Function} res.progress 加载进度回调 - * @param {Function} res.fail 加载失败回调 - */ - public start(res: { configs: IAssetConfig[], parallel?: number, retry?: number, complete: () => void, fail: (msg: string, err: Error) => void, progress?: (percent: number) => void }): void { - this._configs = res.configs; - this._maxParallel = res.parallel || 10; - this._maxRetry = res.retry || 3; - this._complete = res.complete; - this._progress = res.progress; - this._fail = res.fail; - - this._total = 0; - this._initSuccess = false; - this._items.length = 0; - - let initCount = res.configs.length; - for (const item of res.configs) { - let bundlename = item.bundle || "resources"; - let count = 0; - if (bundlename == "resources") { - count = AssetUtils.getResourceCount(item.path, item.type); - this._total += count; - - this._items.push({ type: item.type, bundle: item.bundle || "resources", path: item.path, isFile: item.isFile || false, status: StateType.Wait, count: count }) - initCount--; - - initCount <= 0 && this.initSuccess(); - } else { - AssetUtils.loadBundle(bundlename).then((bundle: AssetManager.Bundle) => { - count = AssetUtils.getResourceCount(item.path, item.type, bundle); - this._total += count; - - this._items.push({ type: item.type, bundle: item.bundle || "resources", path: item.path, isFile: item.isFile || false, status: StateType.Wait, count: count }) - initCount--; - - initCount <= 0 && this.initSuccess(); - }).catch((err: Error) => { - if (this._retry < this._maxRetry) { - this.retryStart(); - } else { - this._fail(`加载资源包[${bundlename}]失败`, err); - } - }); - } - } - } - - /** 重试 (重新加载失败的资源) */ - public retry(): void { - this._parallel = 0; - this._retry = 0; - if (!this._initSuccess) { - this.retryStart(); - } else { - this.retryLoad(); - } - } - - /** - * 重试开始 - * @internal - */ - private retryStart(): void { - this._retry++; - this.start({ - configs: this._configs, - parallel: this._maxParallel, - retry: this._maxRetry, - complete: this._complete, - fail: this._fail, - progress: this._progress - }); - } - - /** - * 重试加载资源 - * @internal - */ - private retryLoad(): void { - this._retry++; - let count = this.resetErrorItem(); - let maxLoad = Math.min(count, this._maxParallel); - for (let i = 0; i < maxLoad; i++) { - this.loadNext(); - } - } - - /** - * 初始化成功后,开始批量加载资源 - * @internal - */ - private initSuccess(): void { - this._initSuccess = true; - this._parallel = 0; - let maxLoad = Math.min(this._items.length, this._maxParallel); - for (let i = 0; i < maxLoad; i++) { - this.loadNext(); - } - } - - /** - * 加载下一个资源 - * @internal - */ - private loadNext(): void { - // 找到第一个等待中的资源 - let index = this._items.findIndex(item => item.status == StateType.Wait); - if (index > -1) { - this.loadItem(index); - } else if (!this._items.some(item => item.status != StateType.Finish)) { - // 所有资源全部完成了 - this._complete(); - } else if (this._parallel <= 0 && this._retry < this._maxRetry) { - this.retryLoad(); - } - } - - /** - * 重置失败资源状态为等待中 - * @internal - */ - private resetErrorItem(): number { - let count = 0; - for (const item of this._items) { - if (item.status == StateType.Error) { - item.status = StateType.Wait; - count++; - } - } - return count; - } - - /** - * 加载资源 - * @internal - */ - private loadItem(index: number): void { - let item = this._items[index]; - item.status = StateType.Loading; - this._parallel++; - if (item.bundle == "resources") { - if (item.isFile) { - this.loadFile(index, resources); - } else { - this.loadDir(index, resources); - } - } else { - AssetUtils.loadBundle(item.bundle).then((bundle: AssetManager.Bundle) => { - if (item.isFile) { - this.loadFile(index, bundle); - } else { - this.loadDir(index, bundle); - } - }).catch((err: Error) => { - log(`load bundle error, bundle:${item.bundle}, filename:${item.path}`); - item.status = StateType.Error; - }); - } - } - - /** - * 加载资源 - * @internal - */ - private loadDir(index: number, bundle: AssetManager.Bundle): void { - let item = this._items[index]; - bundle.loadDir(item.path, item.type, (finish: number, total: number) => { - if (total > 0 && finish > 0) { - this._completeCounts.set(`${item.bundle}:${item.path}`, finish); - this._progress && this.updateProgress(); - } - }, (error: Error, assets: Array) => { - this._parallel--; - if (error) { - log(`load dir error, bundle:${item.bundle}, dir:${item.path}`); - item.status = StateType.Error; - } else { - item.status = StateType.Finish; - this._completeCounts.set(`${item.bundle}:${item.path}`, assets.length); - AssetPool.add(assets, bundle, this._name); - } - this._progress && this.updateProgress(); - this.loadNext(); - }); - } - - /** - * 加载资源 - * @internal - */ - private loadFile(index: number, bundle: AssetManager.Bundle): void { - let item = this._items[index]; - bundle.load(item.path, item.type, (error: Error, asset: Asset) => { - this._parallel--; - if (error) { - log(`load file error, bundle:${item.bundle}, filename:${item.path}`); - item.status = StateType.Error; - } else { - item.status = StateType.Finish; - this._completeCounts.set(`${item.bundle}:${item.path}`, 1); - AssetPool.add(asset, bundle, this._name); - } - - this._progress && this.updateProgress(); - this.loadNext(); - }); - } - - /** - * 更新进度 - * @internal - */ - private updateProgress(): void { - let value = 0; - for (const count of this._completeCounts.values()) { - value += count; - } - this._progress(MathTool.clampf(value / this._total, 0, 1)); - } -} diff --git a/src/asset/AssetPool.ts b/src/asset/AssetPool.ts deleted file mode 100644 index 3e356db..0000000 --- a/src/asset/AssetPool.ts +++ /dev/null @@ -1,170 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2025-02-11 - * @Description: 资源池 - */ - -import { Asset, AssetManager, resources } from "cc"; -import { log } from "../tool/log"; -import { AssetUtils } from "./AssetUtils"; - -export class AssetPool { - /** - * 资源名对应的资源 - * @internal - */ - private static _assets: { [path: string]: Asset } = {}; - /** - * uuid 对应的资源名 - * @internal - */ - private static _uuidToName: Map = new Map(); - /** - * 资源加载批次对应的资源名 - * @internal - */ - private static _batchAssetNames: Map = new Map(); - - /** 批量添加资源 */ - public static add(asset: Asset[] | Asset, bundle: AssetManager.Bundle = resources, batchName: string = ""): void { - if (Array.isArray(asset)) { - for (const item of asset) { - this.add(item, bundle, batchName); - } - } else { - let uuid = asset.uuid || asset._uuid; - if (this._uuidToName.has(uuid)) { - return; - } - // 增加引用计数 - asset.addRef(); - let info = bundle.getAssetInfo(uuid); - let key = this.getKey(info.path, bundle.name); - // log(`>>>uuid:${uuid}, path:${info.path}`); - this._uuidToName.set(uuid, key); - this._assets[key] = asset; - - if (batchName) { - let names = this._batchAssetNames.get(batchName) || []; - names.push(key); - this._batchAssetNames.set(batchName, names); - } - } - } - - public static has(path: string, bundlename: string = "resources"): boolean { - let key = this.getKey(path, bundlename); - if (!this._assets[key]) { - return false; - } - return true; - } - - public static get(path: string, bundlename: string = "resources"): T { - let key = this.getKey(path, bundlename); - if (!this._assets[key]) { - log(`获取资源失败: 资源 bundle:${bundlename}, path:${path} 未加载`); - } - return this._assets[key] as T; - } - - /** 按 uuid 判断资源是否存在 */ - public static hasUUID(uuid: string): boolean { - if (!this._uuidToName.has(uuid)) { - return false; - } - return true; - } - - /** 按 uuid 获取资源 */ - public static getByUUID(uuid: string): T { - if (!this._uuidToName.has(uuid)) { - log(`获取资源失败: 资源 uuid:${uuid} 未加载`); - } - let key = this._uuidToName.get(uuid); - return this._assets[key] as T; - } - - /** - * 按资源加载批次释放资源 - * @param batchName 资源加载批次名 对应 AssetLoader 实例化时传入的 name - */ - public static releaseBatchAssets(batchName: string): void { - if (!this._batchAssetNames.has(batchName)) { - return; - } - let names = this._batchAssetNames.get(batchName); - for (const name of names) { - this.release(name); - } - this._batchAssetNames.delete(batchName); - } - - /** 按资源路径释放资源 */ - public static releasePath(path: string, bundlename: string = "resources"): void { - let key = this.getKey(path, bundlename); - this.release(key); - } - - /** 按 bundle 和 文件夹释放资源 */ - public static releaseDir(dir: string, bundlename: string = "resources", asset: typeof Asset): Promise { - return new Promise((resolve, reject) => { - if (bundlename == "resources") { - let uuids = AssetUtils.getUUIDs(dir, asset, resources); - for (const uuid of uuids) { - this.releaseUUID(uuid); - } - resolve(true); - } else { - AssetUtils.loadBundle(bundlename).then((bundle: AssetManager.Bundle) => { - let uuids = AssetUtils.getUUIDs(dir, asset, bundle); - for (const uuid of uuids) { - this.releaseUUID(uuid); - } - resolve(true); - }).catch((err: Error) => { - reject(false); - }); - } - }); - } - - /** 按 uuid 释放资源 */ - public static releaseUUID(uuid: string): void { - if (this._uuidToName.has(uuid)) { - let key = this._uuidToName.get(uuid); - this.release(key); - } - } - - /** 释放所有加载的资源 */ - public static releaseAll(): void { - for (const key in this._assets) { - this._assets[key].decRef(); - } - this._assets = {}; - this._uuidToName.clear(); - this._batchAssetNames.clear(); - } - - /** - * 按key释放资源 - * @internal - */ - private static release(key: string): void { - if (this._assets[key]) { - this._uuidToName.delete(this._assets[key].uuid); - - this._assets[key].decRef(); - delete this._assets[key]; - } - } - - /** - * 获取资源 key - * @internal - */ - private static getKey(path: string, bundlename: string = "resources"): string { - return `${bundlename}:${path}`; - } -} diff --git a/src/asset/AssetUtils.ts b/src/asset/AssetUtils.ts deleted file mode 100644 index 040be2e..0000000 --- a/src/asset/AssetUtils.ts +++ /dev/null @@ -1,52 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2025-02-11 - * @Description: 资源工具类 - */ - -import { Asset, AssetManager, assetManager, resources } from "cc"; - - -export class AssetUtils { - /** 获取资源数量 */ - public static getResourceCount(dir: string, type: typeof Asset, bundle: AssetManager.Bundle = resources): number { - dir = assetManager.utils.normalize(dir); - if (dir[dir.length - 1] === "/") { - dir = dir.slice(0, -1); - } - let list = bundle.getDirWithPath(dir, type); - return list.length; - } - - /** 获取资源名称 */ - public static getUUIDs(dir: string, type: typeof Asset, bundle: AssetManager.Bundle = resources): string[] { - let uuids: string[] = []; - let path = assetManager.utils.normalize(dir); - if (path[path.length - 1] === "/") { - path = path.slice(0, -1); - } - let list = bundle.getDirWithPath(path, type); - for (const asset of list) { - uuids.push(asset.uuid); - } - return uuids; - } - - /** 加载 bundle */ - public static loadBundle(bundlename: string): Promise { - return new Promise((resolve, reject) => { - let bundle = assetManager.getBundle(bundlename); - if (bundle) { - resolve(bundle); - } else { - assetManager.loadBundle(bundlename, (err: Error, bundle: AssetManager.Bundle) => { - if (err) { - reject(err); - } else { - resolve(bundle); - } - }); - } - }); - } -} diff --git a/src/behaviortree/Agent.ts b/src/behaviortree/Agent.ts deleted file mode 100644 index 96d0c1e..0000000 --- a/src/behaviortree/Agent.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BehaviorTree } from "./BehaviorTree"; -import { Blackboard } from "./Blackboard"; -import { Ticker } from "./Ticker"; - -/** 代理 */ -export class Agent { - /** 行为树 */ - public tree: BehaviorTree; - /** 黑板 */ - public blackboard: Blackboard; - /** 更新器 */ - public ticker: Ticker; - /** - * constructor - * @param subject // 主体 - * @param tree 行为树 - */ - constructor(subject: any, tree: BehaviorTree) { - this.tree = tree; - this.blackboard = new Blackboard(); - this.ticker = new Ticker(subject, this.blackboard, tree); - } - - /** - * 执行 - */ - public tick(): void { - this.tree.tick(this, this.blackboard, this.ticker); - if (this.blackboard.interrupt) { - this.blackboard.interrupt = false; - - let ticker = this.ticker; - ticker.openNodes.length = 0; - ticker.nodeCount = 0; - - this.blackboard.clear(); - } - } - - /** - * 打断行为树,重新开始执行(如果当前在节点中,下一帧才会清理) - */ - public interruptBTree(): void { - if (!this.blackboard.interruptDefend) { - this.blackboard.interrupt = true; - } - } -} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Action.ts b/src/behaviortree/BTNode/Action.ts deleted file mode 100644 index b87b878..0000000 --- a/src/behaviortree/BTNode/Action.ts +++ /dev/null @@ -1,189 +0,0 @@ -import { Status } from "../header"; -import { Ticker } from "../Ticker"; -import { BaseNode } from "./BaseNode"; - -/** - * 动作节点 - * 没有子节点 - */ -export abstract class Action extends BaseNode { - constructor() { - super(); - } -} - -/** - * 失败节点(无子节点) - * 直接返回FAILURE - */ -export class Failure extends Action { - /** 执行函数 @internal */ - private _func: () => void; - constructor(func: () => void) { - super(); - this._func = func; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - this._func(); - return Status.FAILURE; - } -} - -/** - * 逻辑节点,一直执行 (无子节点) - * 直接返回RUNING - */ -export class Running extends Action { - /** 执行函数 @internal */ - private _func: () => void; - constructor(func: () => void) { - super(); - this._func = func; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - this._func(); - return Status.RUNNING; - } -} - -/** - * 成功节点 无子节点 - * 直接返回SUCCESS - */ -export class Success extends Action { - /** 执行函数 @internal */ - private _func: () => void; - constructor(func: () => void) { - super(); - this._func = func; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - this._func(); - return Status.SUCCESS; - } -} -/** - * 次数等待节点(无子节点) - * 次数内,返回RUNING - * 超次,返回SUCCESS - */ -export class WaitTicks extends Action { - /** 最大次数 @internal */ - private _maxTicks: number; - /** 经过的次数 @internal */ - private _elapsedTicks: number; - constructor(maxTicks: number = 0) { - super(); - this._maxTicks = maxTicks; - this._elapsedTicks = 0; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - this._elapsedTicks = 0; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (++this._elapsedTicks >= this._maxTicks) { - this._elapsedTicks = 0; - return Status.SUCCESS; - } - return Status.RUNNING; - } -} - -/** - * 时间等待节点(无子节点) - * 时间到后返回SUCCESS,否则返回RUNING - */ -export class WaitTime extends Action { - /** 等待时间(毫秒 ms) @internal */ - private _duration: number; - constructor(duration: number = 0) { - super(); - this._duration = duration * 1000; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - let startTime = new Date().getTime(); - ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let currTime = new Date().getTime(); - let startTime = ticker.blackboard.get("startTime", ticker.tree.id, this.id); - if (currTime - startTime >= this._duration) { - return Status.SUCCESS; - } - return Status.RUNNING; - } -} - -/** - * 行为树防止被打断节点 - * 直接返回 SUCCESS - * 和 InterruptDefendCancel 必须成对出现 - */ -export class InterruptDefend extends Action { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - ticker.blackboard.interruptDefend = true; - return Status.SUCCESS; - } -} - -/** - * 行为树被打断取消节点 - * 直接返回 SUCCESS - * 和 InterruptDefend 必须成对出现 - */ -export class InterruptDefendCancel extends Action { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - ticker.blackboard.interruptDefend = false; - return Status.SUCCESS; - } -} \ No newline at end of file diff --git a/src/behaviortree/BTNode/BaseNode.ts b/src/behaviortree/BTNode/BaseNode.ts deleted file mode 100644 index 7244fa9..0000000 --- a/src/behaviortree/BTNode/BaseNode.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { createUUID, Status } from "../header"; -import { Ticker } from "../Ticker"; - -/** - * 基础节点 - * 所有节点全部继承自 BaseNode - */ -export abstract class BaseNode { - /** 唯一标识 */ - public id: string; - /** 子节点 */ - public children: BaseNode[]; - - /** - * 创建 - * @param children 子节点列表 - */ - constructor(children?: BaseNode[]) { - this.id = createUUID(); - this.children = []; - if (!children) { - return; - } - for (let i = 0; i < children.length; i++) { - this.children.push(children[i]); - } - } - - /** - * 执行节点 - * @param ticker 更新器 - * @returns {Status} 状态 - */ - public _execute(ticker: Ticker): Status { - /* ENTER */ - this._enter(ticker); - if (!ticker.blackboard.get("isOpen", ticker.tree.id, this.id)) { - this._open(ticker); - } - let status = this._tick(ticker); - if (status !== Status.RUNNING) { - this._close(ticker); - } - this._exit(ticker); - return status; - } - - /** - * 进入节点 - * @param ticker 更新器 - * @internal - */ - public _enter(ticker: Ticker): void { - ticker.enterNode(this); - this.enter(ticker); - } - - /** - * 打开节点 - * @param ticker 更新器 - * @internal - */ - public _open(ticker: Ticker): void { - ticker.openNode(this); - ticker.blackboard.set("isOpen", true, ticker.tree.id, this.id); - this.open(ticker); - } - - /** - * 更新节点 - * @param ticker 更新器 - * @internal - */ - public _tick(ticker: Ticker): Status { - ticker.tickNode(this); - return this.tick(ticker); - } - - /** - * 关闭节点 - * @param ticker 更新器 - * @internal - */ - public _close(ticker: Ticker): void { - ticker.closeNode(this); - ticker.blackboard.set("isOpen", false, ticker.tree.id, this.id); - this.close(ticker); - } - - /** - * 退出节点 - * @param ticker 更新器 - * @internal - */ - public _exit(ticker: Ticker): void { - ticker.exitNode(this); - this.exit(ticker); - } - - enter(ticker: Ticker): void { - - } - open(ticker: Ticker): void { - - } - close(ticker: Ticker): void { - - } - exit(ticker: Ticker): void { - - } - abstract tick(ticker: Ticker): Status; -} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Composite.ts b/src/behaviortree/BTNode/Composite.ts deleted file mode 100644 index f59c94c..0000000 --- a/src/behaviortree/BTNode/Composite.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { Status } from "../header"; -import { Ticker } from "../Ticker"; -import { BaseNode } from "./BaseNode"; - -/** - * 可以包含多个节点的集合装饰器基类 - * - */ -export abstract class Composite extends BaseNode { - constructor(...children: BaseNode[]) { - super(children); - } -} - -/** - * 记忆选择节点 - * 选择不为 FAILURE 的节点 - * 任意一个Child Node返回不为 FAILURE, 本Node向自己的Parent Node也返回Child Node状态 - */ -export class MemSelector extends Composite { - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - super.open(ticker); - ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let childIndex = ticker.blackboard.get("runningChild", ticker.tree.id, this.id) as number; - - for (let i = childIndex; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - - if (status !== Status.FAILURE) { - if (status === Status.RUNNING) { - ticker.blackboard.set("runningChild", i, ticker.tree.id, this.id); - } - return status; - } - } - - return Status.FAILURE; - } -} - -/** - * 记忆顺序节点 - * 如果上次执行到 RUNING 的节点, 下次进入节点后, 直接从 RUNING 节点开始 - * 遇到 RUNING 或者 FAILURE 停止迭代 - * 任意一个Child Node返回不为 SUCCESS, 本Node向自己的Parent Node也返回Child Node状态 - * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS - */ -export class MemSequence extends Composite { - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - super.open(ticker); - ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let childIndex = ticker.blackboard.get("runningChild", ticker.tree.id, this.id) as number; - for (let i = childIndex; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - if (status !== Status.SUCCESS) { - if (status === Status.RUNNING) { - ticker.blackboard.set("runningChild", i, ticker.tree.id, this.id); - } - return status; - } - } - return Status.SUCCESS; - } -} - -/** - * 随机选择节点 - * 从Child Node中随机选择一个执行 - */ -export class RandomSelector extends Composite { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let childIndex = (Math.random() * this.children.length) | 0; - let child = this.children[childIndex]; - let status = child._execute(ticker); - - return status; - } -} - -/** - * 选择节点,选择不为 FAILURE 的节点 - * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: - * 如遇到一个Child Node执行后返回 SUCCESS 或者 RUNING,那停止迭代,本Node向自己的Parent Node也返回 SUCCESS 或 RUNING - */ -export class Selector extends Composite { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - for (let i = 0; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - if (status !== Status.FAILURE) { - return status; - } - } - return Status.FAILURE; - } -} - -/** - * 顺序节点 - * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: - * 遇到 FAILURE 或 RUNING, 那停止迭代,返回FAILURE 或 RUNING - * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS - */ -export class Sequence extends Composite { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - for (let i = 0; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - if (status !== Status.SUCCESS) { - return status; - } - } - return Status.SUCCESS; - } -} - -/** - * 并行节点 每次进入全部重新执行一遍 - * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: - * 1. 当存在Child Node执行后返回 FAILURE, 本节点返回 FAILURE - * 2. 当存在Child Node执行后返回 RUNING, 本节点返回 RUNING - * 所有节点都返回 SUCCESS, 本节点才返回 SUCCESS - */ -export class Parallel extends Composite { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let result = Status.SUCCESS; - for (let i = 0; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - if (status == Status.FAILURE) { - result = Status.FAILURE; - } else if (result == Status.SUCCESS && status == Status.RUNNING) { - result = Status.RUNNING; - } - } - return result; - } -} - -/** - * 并行节点 每次进入全部重新执行一遍 - * 当执行本类型Node时,它将从begin到end迭代执行自己的Child Node: - * 1. 当存在Child Node执行后返回 FAILURE, 本节点返回 FAILURE - * 2. 任意 Child Node 返回 SUCCESS, 本节点返回 SUCCESS - * 否则返回 RUNNING - */ -export class ParallelAnySuccess extends Composite { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - let result = Status.RUNNING; - for (let i = 0; i < this.children.length; i++) { - let status = this.children[i]._execute(ticker); - if (status == Status.FAILURE) { - result = Status.FAILURE; - } else if (result == Status.RUNNING && status == Status.SUCCESS) { - result = Status.SUCCESS; - } - } - return result; - } -} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Condition.ts b/src/behaviortree/BTNode/Condition.ts deleted file mode 100644 index 9302397..0000000 --- a/src/behaviortree/BTNode/Condition.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Status } from "../header"; -import { Ticker } from "../Ticker"; -import { Action } from "./Action"; - -/** - * 条件节点 - */ -export class Condition extends Action { - /** 执行函数 @internal */ - private _func: (subject: any) => boolean = null; - constructor(func: (subject: any) => boolean) { - super(); - this._func = func; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - return this._func(ticker.subject) ? Status.SUCCESS : Status.FAILURE; - } -} \ No newline at end of file diff --git a/src/behaviortree/BTNode/Decorator.ts b/src/behaviortree/BTNode/Decorator.ts deleted file mode 100644 index 72ebcf9..0000000 --- a/src/behaviortree/BTNode/Decorator.ts +++ /dev/null @@ -1,367 +0,0 @@ -import { Status } from "../header"; -import { Ticker } from "../Ticker"; -import { BaseNode } from "./BaseNode"; - -/** - * 修饰节点基类 - * 只能包含一个子节点 - */ -export abstract class Decorator extends BaseNode { - constructor(child: BaseNode) { - super([child]); - } -} - -/** - * 失败节点 - * 必须且只能包含一个子节点 - * 直接返回 FAILURE - * @extends Decorator - */ -export class Failer extends Decorator { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(Failer)节点必须包含一个子节点"); - } - let child = this.children[0]; - child._execute(ticker); - return Status.FAILURE; - } -} - -/** - * 结果反转节点 - * 必须且只能包含一个子节点 - * 第一个Child Node节点, 返回 FAILURE, 本Node向自己的Parent Node也返回 SUCCESS - * 第一个Child Node节点, 返回 SUCCESS, 本Node向自己的Parent Node也返回 FAILURE - */ -export class Inverter extends Decorator { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(Inverter)节点必须包含一个子节点"); - } - let child = this.children[0]; - let status = child._execute(ticker); - if (status === Status.SUCCESS) { - status = Status.FAILURE; - } else if (status === Status.FAILURE) { - status = Status.SUCCESS; - } - return status; - } -} - -/** - * 次数限制节点 - * 必须且只能包含一个子节点 - * 次数限制内, 根据Child Node的结果, 本Node向自己的Parent Node也返回相同的结果 - * 次数超过后, 直接返回 FAILURE - */ -export class LimiterTicks extends Decorator { - /** 最大次数 @internal */ - private _maxTicks: number; - /** 当前执行过的次数 @internal */ - private _elapsedTicks: number; - - /** - * 创建 - * @param maxTicks 最大次数 - * @param child 子节点 - */ - constructor(maxTicks: number, child: BaseNode) { - super(child); - this._maxTicks = maxTicks; - this._elapsedTicks = 0; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - super.open(ticker); - this._elapsedTicks = 0; - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(LimiterTicks)节点必须包含一个子节点"); - } - let child = this.children[0]; - if (++this._elapsedTicks > this._maxTicks) { - this._elapsedTicks = 0; - return Status.FAILURE; - } - return child._execute(ticker); - } -} - -/** - * 时间限制节点 - * 只能包含一个子节点 - * 规定时间内, 根据Child Node的结果, 本Node向自己的Parent Node也返回相同的结果 - * 超时后, 直接返回 FAILURE - */ -export class LimiterTime extends Decorator { - /** 最大时间 (毫秒 ms) @internal */ - private _maxTime: number; - - /** - * 时间限制节点 - * @param maxTime 最大时间 (微秒ms) - * @param child 子节点 - */ - constructor(maxTime: number, child: BaseNode) { - super(child); - this._maxTime = maxTime * 1000; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - super.open(ticker); - let startTime = new Date().getTime(); - ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(LimiterTime)节点必须包含一个子节点"); - } - - let child = this.children[0]; - let currTime = new Date().getTime(); - let startTime = ticker.blackboard.get("startTime", ticker.tree.id, this.id); - - if (currTime - startTime > this._maxTime) { - return Status.FAILURE; - } - - return child._execute(ticker); - } -} - -/** - * 循环节点 - * 必须且只能包含一个子节点 - * 如果maxLoop < 0, 直接返回成功 - * 否则等待次数超过之后, 返回Child Node的结果(RUNING的次数不计算在内) - */ -export class Repeater extends Decorator { - /** 最大循环次数 @internal */ - private _maxLoop: number; - - /** - * 创建 - * @param child 子节点 - * @param maxLoop 最大循环次数 - */ - constructor(child: BaseNode, maxLoop: number = -1) { - super(child); - this._maxLoop = maxLoop; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - ticker.blackboard.set("i", 0, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(Repeater)节点必须包含一个子节点"); - } - - let child = this.children[0]; - let i = ticker.blackboard.get("i", ticker.tree.id, this.id); - let status = Status.SUCCESS; - - while (this._maxLoop < 0 || i < this._maxLoop) { - status = child._execute(ticker); - - if (status === Status.SUCCESS || status === Status.FAILURE) { - i++; - } else { - break; - } - } - - ticker.blackboard.set("i", i, ticker.tree.id, this.id); - return status; - } -} - -/** - * 循环节点 - * 只能包含一个子节点 - * 如果maxLoop < 0, 直接返回成功 - * 当Child Node返回 FAILURE, 本Node向自己的Parent Node返回 FAILURE - * 循环次数大于等于maxLoop时, 返回Child Node的结果 - */ -export class RepeatUntilFailure extends Decorator { - /** 最大循环次数 @internal */ - private _maxLoop: number; - - constructor(child: BaseNode, maxLoop: number = -1) { - super(child); - this._maxLoop = maxLoop; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - ticker.blackboard.set("i", 0, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(RepeatUntilFailure)节点必须包含一个子节点"); - } - - let child = this.children[0]; - let i = ticker.blackboard.get("i", ticker.tree.id, this.id); - let status = Status.SUCCESS; - - while (this._maxLoop < 0 || i < this._maxLoop) { - status = child._execute(ticker); - - if (status === Status.SUCCESS) { - i++; - } else { - break; - } - } - - ticker.blackboard.set("i", i, ticker.tree.id, this.id); - return status; - } -} - -/** - * 循环节点(只能包含一个子节点) - * 如果maxLoop < 0, 直接返回失败 - * 当Child Node返回 SUCCESS, 本Node向自己的Parent Node返回 SUCCESS - * 循环次数大于等于maxLoop时, 返回Child Node的结果 - */ -export class RepeatUntilSuccess extends Decorator { - /** 最大循环次数 @internal */ - private _maxLoop: number; - - /** - * 创建 - * @param child 子节点 - * @param maxLoop 最大循环次数 - */ - constructor(child: BaseNode, maxLoop: number = -1) { - super(child); - this._maxLoop = maxLoop; - } - - /** - * 打开 - * @param {Ticker} ticker - */ - public open(ticker: Ticker): void { - ticker.blackboard.set("i", 0, ticker.tree.id, this.id); - } - - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(RepeatUntilSuccess)节点必须包含一个子节点"); - } - let child = this.children[0]; - let i = ticker.blackboard.get("i", ticker.tree.id, this.id); - let status = Status.FAILURE; - while (this._maxLoop < 0 || i < this._maxLoop) { - status = child._execute(ticker); - if (status === Status.FAILURE) { - i++; - } else { - break; - } - } - ticker.blackboard.set("i", i, ticker.tree.id, this.id); - return status; - } -} - -/** - * 逻辑节点, 一直执行(只能包含一个子节点) - * 直接返回 RUNING - */ -export class Runner extends Decorator { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(Runner)节点必须包含一个子节点"); - } - let child = this.children[0]; - child._execute(ticker); - return Status.RUNNING; - } -} - -/** - * 成功节点(包含一个子节点) - * 直接返回 SUCCESS - */ -export class Succeeder extends Decorator { - /** - * 执行 - * @param {Ticker} ticker - * @returns {Status} - */ - public tick(ticker: Ticker): Status { - if (this.children.length !== 1) { - throw new Error("(Succeeder)节点必须包含一个子节点"); - } - let child = this.children[0]; - child._execute(ticker); - return Status.SUCCESS; - } -} \ No newline at end of file diff --git a/src/behaviortree/BehaviorTree.ts b/src/behaviortree/BehaviorTree.ts deleted file mode 100644 index ad33f81..0000000 --- a/src/behaviortree/BehaviorTree.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Blackboard } from "./Blackboard"; -import { BaseNode } from "./BTNode/BaseNode"; -import { createUUID } from "./header"; -import { Ticker } from "./Ticker"; - -/** - * 行为树 - * 所有节点全部添加到树中 - */ -export class BehaviorTree { - /** 行为树ID @internal */ - private _id: string; - /** 行为树跟节点 @internal */ - private _root: BaseNode; - /** - * constructor - * @param root 根节点 - */ - constructor(root: BaseNode) { - this._id = createUUID(); - this._root = root; - } - - /** - * 执行 - * @param subject 主体 - * @param blackboard 黑板 - * @param ticker 更新器 - */ - public tick(subject: any, blackboard: Blackboard, ticker?: Ticker): void { - ticker = ticker || new Ticker(subject, blackboard, this); - ticker.openNodes.length = 0; - this._root._execute(ticker); - // 上次打开的节点 - let lastOpenNodes = blackboard.get("openNodes", this._id) as BaseNode[]; - // 当前打开的节点 - let currOpenNodes = ticker.openNodes; - let start = 0; - for (let i = 0; i < Math.min(lastOpenNodes.length, currOpenNodes.length); i++) { - start = i + 1; - if (lastOpenNodes[i] !== currOpenNodes[i]) { - break; - } - } - // 关闭不需要的节点 - for (let i = lastOpenNodes.length - 1; i >= start; i--) { - lastOpenNodes[i]._close(ticker); - } - /* POPULATE BLACKBOARD */ - blackboard.set("openNodes", currOpenNodes, this._id); - blackboard.set("nodeCount", ticker.nodeCount, this._id); - } - - get id(): string { - return this._id; - } - - get root(): BaseNode { - return this._root; - } -} \ No newline at end of file diff --git a/src/behaviortree/Blackboard.ts b/src/behaviortree/Blackboard.ts deleted file mode 100644 index 6fcec06..0000000 --- a/src/behaviortree/Blackboard.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * 行为树数据 - */ -interface ITreeData { - nodeMemory: { [nodeScope: string]: any }; - openNodes: any[]; -} - -/** 平台 */ -export class Blackboard { - /** 行为树打断保护 */ - public interruptDefend: boolean = false; - /** 打断行为树的标记 */ - public interrupt: boolean = false; - /** 基础记忆 @internal */ - private _baseMemory: any; - /** 树记忆 @internal */ - private _treeMemory: { [treeScope: string]: ITreeData }; - - constructor() { - this._baseMemory = {}; - this._treeMemory = {}; - } - - /** - * 清除 - */ - public clear(): void { - this._baseMemory = {}; - this._treeMemory = {}; - } - - /** - * 设置 - * @param key 键 - * @param value 值 - * @param treeScope 树范围 - * @param nodeScope 节点范围 - */ - public set(key: string, value: any, treeScope?: string, nodeScope?: string): void { - let memory = this._getMemory(treeScope, nodeScope); - memory[key] = value; - } - - /** - * 获取 - * @param key 键 - * @param treeScope 树范围 - * @param nodeScope 节点范围 - * @returns 值 - */ - public get(key: string, treeScope?: string, nodeScope?: string): any { - let memory = this._getMemory(treeScope, nodeScope); - return memory[key]; - } - - /** - * 获取树记忆 - * @param treeScope 树范围 - * @returns 树记忆 - * @internal - */ - private _getTreeMemory(treeScope: string): ITreeData { - if (!this._treeMemory[treeScope]) { - this._treeMemory[treeScope] = { - nodeMemory: {}, - openNodes: [], - }; - } - return this._treeMemory[treeScope]; - } - - /** - * 获取节点记忆 - * @param treeMemory 树记忆 - * @param nodeScope 节点范围 - * @returns 节点记忆 - * @internal - */ - private _getNodeMemory(treeMemory: ITreeData, nodeScope: string): { [key: string]: any } { - let memory = treeMemory.nodeMemory; - if (!memory[nodeScope]) { - memory[nodeScope] = {}; - } - return memory[nodeScope]; - } - - /** - * 获取记忆 - * @param treeScope 树范围 - * @param nodeScope 节点范围 - * @returns 记忆 - * @internal - */ - private _getMemory(treeScope?: string, nodeScope?: string): { [key: string]: any } { - let memory = this._baseMemory; - if (treeScope) { - memory = this._getTreeMemory(treeScope); - if (nodeScope) { - memory = this._getNodeMemory(memory, nodeScope); - } - } - return memory; - } -} \ No newline at end of file diff --git a/src/behaviortree/Ticker.ts b/src/behaviortree/Ticker.ts deleted file mode 100644 index 27e3728..0000000 --- a/src/behaviortree/Ticker.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { BehaviorTree } from "./BehaviorTree"; -import { Blackboard } from "./Blackboard"; -import { BaseNode } from "./BTNode/BaseNode"; - -export class Ticker { - tree: BehaviorTree; // 行为树跟节点 - openNodes: BaseNode[]; // 当前打开的节点 - nodeCount: number; // 当前打开的节点数量 - blackboard: Blackboard; // 数据容器 - debug: any; - subject: any; - constructor(subject: any, blackboard: Blackboard, tree: BehaviorTree) { - this.tree = tree; - this.openNodes = []; - this.nodeCount = 0; - this.debug = null; - this.subject = subject; - this.blackboard = blackboard; - } - - /** - * 进入节点 - * @param node 节点 - */ - public enterNode(node: BaseNode): void { - this.nodeCount++; - this.openNodes.push(node); - } - - /** - * 打开节点 - * @param node 节点 - */ - public openNode(node: BaseNode): void { } - - /** - * 更新节点 - * @param node 节点 - */ - public tickNode(node: BaseNode): void { } - - /** - * 关闭节点 - * @param node 节点 - */ - public closeNode(node: BaseNode): void { - this.openNodes.pop(); - } - - /** - * 退出节点 - * @param node 节点 - */ - public exitNode(node: BaseNode): void { } -} \ No newline at end of file diff --git a/src/behaviortree/header.ts b/src/behaviortree/header.ts deleted file mode 100644 index 458bb68..0000000 --- a/src/behaviortree/header.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const enum Status { - FAILURE, - SUCCESS, - RUNNING, -} - -/** - * 创建UUID - * @returns UUID - * @internal - */ -export function createUUID(): string { - let s: string[] = Array(36); - let hexDigits = "0123456789abcdef"; - for (let i = 0; i < 36; i++) { - let start = Math.floor(Math.random() * 0x10); - s[i] = hexDigits.substring(start, start + 1); - } - // bits 12-15 of the time_hi_and_version field to 0010 - s[14] = "4"; - // bits 6-7 of the clock_seq_hi_and_reserved to 01 - let start = (parseInt(s[19], 16) & 0x3) | 0x8; - s[19] = hexDigits.substring(start, start + 1); - s[8] = s[13] = s[18] = s[23] = "-"; - let uuid = s.join(""); - return uuid; -} \ No newline at end of file diff --git a/src/cocos/CocosEntry.ts b/src/cocos/CocosEntry.ts index d9b8020..4e58c53 100644 --- a/src/cocos/CocosEntry.ts +++ b/src/cocos/CocosEntry.ts @@ -5,13 +5,11 @@ */ import { _decorator, Component, director, game, JsonAsset, macro, sys } from "cc"; -import { ECDataHelper } from "../ecmodule/ECDataHelper"; import { GlobalEvent } from "../global/GlobalEvent"; import { GlobalTimer } from "../global/GlobalTimer"; import { enableDebugMode, FrameConfig } from "../global/header"; import { InnerTimer } from "../global/InnerTimer"; import { Platform, PlatformType } from "../global/Platform"; -import { ECManager } from "../kunpocc"; import { ModuleBase } from "../module/ModuleBase"; import { debug, log } from "../tool/log"; import { Time } from "../tool/Time"; @@ -21,7 +19,6 @@ const _global = (globalThis || window || global) as any; const { property } = _decorator; export abstract class CocosEntry extends Component { @property({ displayName: "uiConfig", type: JsonAsset, tooltip: "编辑器导出的UI配置, 可不设置, 之后通过 PropsHelper.setConfig 手动设置" }) uiConfig: JsonAsset = null; - @property({ displayName: "ecConfig", type: JsonAsset, tooltip: "编辑器导出的实体配置, 可不设置, 之后通过 ECManager.registerEntityConfig 手动设置" }) ecConfig: JsonAsset = null; @property({ displayName: "游戏帧率" }) fps: number = 60; /** @@ -49,17 +46,11 @@ export abstract class CocosEntry extends Component { director.addPersistRootNode(this.node); this.node.setSiblingIndex(this.node.children.length - 1); PropsHelper.setConfig(this.uiConfig?.json); - let ecsMaps = _global["getKunpoRegisterECSMaps"]?.(); - if (this.ecConfig && (!ecsMaps || ecsMaps.size <= 0)) { - ECManager.registerEntityConfig(this.ecConfig.json); - } this.initPlatform(); this.initEvent(); this.initTime(); this.initAdapter(); this.initModule(); - // 注册所有组件 - ECDataHelper.registerComponents(); log("kunpo框架初始化完成"); this.onInit(); } diff --git a/src/ecmodule/Component.ts b/src/ecmodule/Component.ts deleted file mode 100644 index 95266c8..0000000 --- a/src/ecmodule/Component.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { ComponentManager } from "./ComponentManager"; -import { Entity } from "./Entity"; -import { ObjectBase } from "./ObjectBase"; - -export abstract class Component extends ObjectBase { - /** 组件名 */ - public name: string; - - /** 组件类型 */ - public type: number; - - /** 是否需要更新 */ - public needUpdate: boolean; - - /** 所属实体 */ - public entity: Entity; - - /** 所属组件管理器 */ - public componentManager: ComponentManager; - - /** 是否需要销毁 @internal */ - public _needDestroy: boolean; - - /** 更新ID @internal */ - public _updateId: number = -1; - - /** 是否更新中 @internal */ - public get _updating(): boolean { - return this._updateId != -1; - } - - /** 生命周期函数 添加到实体 @internal */ - public _add(): void { - this.onAdd(); - } - - /** 生命周期函数 销毁 @internal */ - public _destroy(): void { - this.onDestroy(); - } - - /** 生命周期函数 添加到实体后 在这个函数中可以获取其他组件 @internal */ - public _enter(): void { - // 自动开启更新 - if (this.needUpdate) { - this.componentManager.startUpdateComponent(this); - } - this.onEnter(); - } - - /** 生命周期函数 从实体中移除 @internal */ - public _remove(): void { - this.stopUpdate(); - this.onRemove(); - this.componentManager._destroyComponent(this); - } - - /** 更新 @internal */ - public _update(dt: number): void { - this.onUpdate(dt); - } - - /** 开启更新 */ - public startUpdate(): void { - if (!this.needUpdate) { - this.needUpdate = true; - this.componentManager?.startUpdateComponent(this); - } - } - - /** 停止更新 */ - public stopUpdate(): void { - if (this.needUpdate) { - this.needUpdate = false; - this.componentManager?.stopUpdateComponent(this); - } - } - - /** - * 获取组件 - * @param {number} componentType 组件类型 - * @returns {T} - */ - public getComponent(componentType: number): T { - return this.entity.getComponent(componentType); - } - - /** - * 删除自己 - */ - public destroySelf(): void { - this.entity.removeComponent(this.type); - } - - /** - * 被添加到实体 对应onDestroy - */ - protected onAdd(): void { } - - /** - * 组件被销毁 对应onAdd - */ - protected onDestroy(): void { } - - protected onUpdate(dt: number): void { } - - /** 可在此方法获取实体其他组件 */ - protected abstract onEnter(): void; - - /** 从实体中删除 */ - protected abstract onRemove(): void; -} diff --git a/src/ecmodule/ComponentManager.ts b/src/ecmodule/ComponentManager.ts deleted file mode 100644 index d5ee82f..0000000 --- a/src/ecmodule/ComponentManager.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { Component } from "./Component"; -import { ComponentPool } from "./ComponentPool"; - -/** - * 组件更新信息 - * @internal - */ -export class ComponentUpdate { - /** 组件更新类型 */ - public componentType: number; - - /** 组件更新列表 @internal */ - private readonly _components: Component[] = []; - - /** create constructor @internal */ - public constructor(componentType: number) { - this.componentType = componentType; - } - - /** - * 添加要更新的组件 - * @param component 组件 - * @internal - */ - public addComponent(component: Component): void { - this._components.push(component); - component._updateId = this._components.length - 1; - } - - /** - * 删除要更新的组件 - * @param {Component} component 组件 - * @internal - */ - public removeComponent(component: Component): void { - const components = this._components; - const finalUpdateID = components.length - 1; - const updateID = component._updateId; - - component._updateId = -1; - /** 最后一个和当前要删除的不是同一个,交换位置 */ - if (finalUpdateID != updateID) { - const finalComponent = components[finalUpdateID]; - // #EC_DEBUG_BEGIN - if (finalComponent._updateId != finalUpdateID) { - throw new Error(`组件(${finalComponent.toString()})更新ID(${finalUpdateID})与存储更新ID(${finalComponent._updateId})不一致`); - } - // #EC_DEBUG_END - finalComponent._updateId = updateID; - components[updateID] = finalComponent; - } - components.pop(); - } - - /** 更新 @internal */ - public _update(dt: number): void { - const components = this._components; - const componentCount = components.length; - - if (componentCount > 0) { - for (let i = 0; i < componentCount; ++i) { - const component = components[i]; - - if (component.needUpdate && component._updating) { - component._update(dt); - } - } - } - } -} - -export class ComponentManager { - /** - * 组件池 - * @type {ComponentPool} - * @internal - */ - protected componentPool: ComponentPool; - - /** 更新组件池 @internal */ - protected readonly updatingComponents: ComponentUpdate[] = []; - /** 组件更新顺序 @internal */ - protected readonly componentUpdateOrderList: number[] = []; - - /** 新添加的或者新停止更新的组件池 @internal */ - private readonly _toUpdateComponents: Component[] = []; - /** 新停止更新的组件池 @internal */ - private readonly _toStopComponents: Component[] = []; - - /** 当前更新的组件类型 @internal */ - private _currentUpdateComponentType: number = -1; - - /** - *Creates an instance of ComponentManager. - * @param {ComponentPool} componentPool 组件池 - * @param {number[]} componentUpdateOrderList 组件更新顺序 - * @internal - */ - constructor(componentPool: ComponentPool, componentUpdateOrderList: number[]) { - this.componentPool = componentPool; - this._toUpdateComponents.length = 0; - this._toStopComponents.length = 0; - for (const componentType of componentUpdateOrderList) { - this._addComponentUpdateOrder(componentType); - } - } - - /** - * 销毁组件管理器 - * @internal - */ - public destroy(): void { - this.componentPool.clear(); - this.updatingComponents.length = 0; - this.componentUpdateOrderList.length = 0; - this._toUpdateComponents.length = 0; - this._toStopComponents.length = 0; - } - - /** - * 创建组件 - * @template T - * @param {string} componentName 组件名 - * @returns {T} 创建的组件 - * @internal - */ - public createComponent(componentName: string): T { - const component = this.componentPool.get(componentName) as T; - // component._enable = true; - // component.needDestroy = false; - component.componentManager = this; - return component; - } - - /** - * 开始更新组件 - * @param {Component} component 组件 - */ - public startUpdateComponent(component: Component): void { - if (component._updating) { - return; - } - if (this._currentUpdateComponentType != component.type) { - this._addComponentToUpdateList(component); - return; - } - this._toUpdateComponents.push(component); - } - - /** - * 停止更新组件 - * @param {Component} component 组件 - */ - public stopUpdateComponent(component: Component): void { - if (!component._updating) { - return; - } - - if (this._currentUpdateComponentType != component.type) { - this._removeComponentToUpdateList(component); - return; - } - - this._toStopComponents.push(component); - } - - /** - * 销毁组件(内部使用) - * @param {Component} component - * @internal - */ - public _destroyComponent(component: Component): void { - if (!component._updating) { - component._destroy(); - this.componentPool.recycle(component); - } else { - component._needDestroy = true; - } - } - - /** - * 更新所有组件(内部使用) - * @param {number} dt 时间间隔 - * @internal - */ - public _update(dt: number): void { - this._updateAllComponents(dt); - this._currentUpdateComponentType = -1; - - this._clearStopComponents(); - this._addUpdateComponents(); - } - - /** - * 添加组件更新顺序,先添加的先更新 - * @param {number} componentType 组件类型 - * @returns {ComponentManager} 组件管理器 - * @internal - */ - private _addComponentUpdateOrder(componentType: number): ComponentManager { - this.componentUpdateOrderList.push(componentType); - const updatingComponents = this.updatingComponents; - for (let i = updatingComponents.length; i <= componentType; ++i) { - updatingComponents.push(null); - } - if (updatingComponents[componentType]) { - throw new Error(`组件类型(${componentType}:${this.componentPool.className(componentType)})已经添加到更新列表`); - } - updatingComponents[componentType] = new ComponentUpdate(componentType); - return this; - } - - /** - * 添加组件到组件更新列表 - * @param {Component} component 组件 - * @internal - */ - private _addComponentToUpdateList(component: Component): void { - if (component.type >= this.updatingComponents.length || !this.updatingComponents[component.type]) { - throw new Error(`组件(${component.constructor.name})没有添加到组件更新列表,请使用addComponentUpdateOrder添加更新`); - } - this.updatingComponents[component.type].addComponent(component); - } - - /** - * 组件更新列表中删除组件 - * @param {Component} component 组件 - * @internal - */ - private _removeComponentToUpdateList(component: Component): void { - this.updatingComponents[component.type].removeComponent(component); - } - - /** - * 更新所有组件 - * @param {number} dt 时间间隔 - * @internal - */ - private _updateAllComponents(dt: number): void { - // 按优先级更新所有组件 - const updateList = this.componentUpdateOrderList; - const updatingComponents = this.updatingComponents; - let componentType: number; - - for (let i = 0, l = updateList.length; i < l; ++i) { - componentType = updateList[i]; - this._currentUpdateComponentType = componentType; - updatingComponents[componentType]._update(dt); - } - } - - /** - * 清除停止更新的组件 - * @internal - */ - private _clearStopComponents(): void { - const toStopComponents = this._toStopComponents; - const l = toStopComponents.length; - if (l > 0) { - for (let i = 0; i < l; ++i) { - const component = toStopComponents[i]; - if (!component.needUpdate && component._updating) { - this._removeComponentToUpdateList(component); - if (component._needDestroy) { - this._destroyComponent(component); - } - } - } - toStopComponents.length = 0; - } - } - - /** - * 添加更新组件 - * @internal - */ - private _addUpdateComponents(): void { - const toUpdateComponents = this._toUpdateComponents; - const l = toUpdateComponents.length; - if (l > 0) { - for (let i = 0; i < l; ++i) { - const component = toUpdateComponents[i]; - if (component.needUpdate && !component._updating) { - this._addComponentToUpdateList(component); - } - } - toUpdateComponents.length = 0; - } - } -} diff --git a/src/ecmodule/ComponentPool.ts b/src/ecmodule/ComponentPool.ts deleted file mode 100644 index da4f007..0000000 --- a/src/ecmodule/ComponentPool.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Component } from "./Component"; -import { ObjectBase } from "./ObjectBase"; -import { ObjectFactory } from "./ObjectFactory"; - -export class ComponentPool { - /** 组件对象类型到组件类型转换 @internal */ - private readonly _objectTypeToComponentType: number[] = new Array(128); - /** 组件池 @internal */ - private _pools: Map = new Map(); - /** 组件名称到组件对象类型转换 @internal */ - private _nameToObjectType: Map = new Map(); - /** - * 注册组件 - * @param {number} componentObjectType 组件对象类型 - * @param {number} componentType 组件类型 - * @param {string} name 组件名称 - * @param {new () => Component} ctor 构造函数 - * @internal - */ - public register(componentObjectType: number, componentType: number, name: string, ctor: new () => ObjectBase): void { - if (this._pools.has(componentObjectType)) { - throw new Error(`组件(${name})已注册, 不允许重复注册`); - } - this._pools.set(componentObjectType, new ObjectFactory(componentObjectType, 128, name, ctor)); - this._nameToObjectType.set(name, componentObjectType); - - const objectTypeToComponentType = this._objectTypeToComponentType; - for (let i = objectTypeToComponentType.length; i <= componentObjectType; ++i) { - objectTypeToComponentType.push(i); - } - objectTypeToComponentType[componentObjectType] = componentType; - } - - /** - * 通过组件名称获取组件对象类型 - * @param {string} componentName 组件名称 - * @returns {number} 组件对象类型 - */ - public getObjectTypeByName(componentName: string): number { - return this._nameToObjectType.get(componentName); - } - - /** - * 创建组件 - * @param {number} componentName 组件名 - * @returns {T} 创建的组件 - * @template T - * @internal - */ - public get(componentName: string): T { - let objectType = this.getObjectTypeByName(componentName); - - const factory = this._pools.get(objectType); - if (!factory) { - throw new Error(`组件(${componentName})未注册,使用组件装饰器 ecclass 注册组件`); - } - const component = factory.allocate() as T; - component.name = factory.name; - component.type = this._objectTypeToComponentType[objectType]; - return component; - } - - /** - * 通过组件对象类型获取组件类名 - * @param {number} componentObjectType 组件类型 - * @returns {string} - * @internal - */ - public className(componentObjectType: number): string { - const factory = this._pools.get(componentObjectType); - if (!factory) { - throw new Error( - `组件(${componentObjectType})没有注册,使用ComponentPool.register(componentObjectType, componentType, componentClass)注册组件` - ); - } - return factory.name; - } - - /** - * 回收组件 - * @param {BaseComponent} component 要回收的组件 - * @memberof ComponentPool - * @internal - */ - public recycle(component: Component): void { - const objectFactory = this._pools.get(component.objectType); - objectFactory.recycle(component); - } - - /** - * 清理缓存 - * @internal - */ - public clear(): void { - for (const factory of this._pools.values()) { - factory._clear(); - } - } -} \ No newline at end of file diff --git a/src/ecmodule/ECDataHelper.ts b/src/ecmodule/ECDataHelper.ts deleted file mode 100644 index c0db2a4..0000000 --- a/src/ecmodule/ECDataHelper.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { color, size, v2, v3 } from "cc"; -import { _ecdecorator, ComponentPool, warn } from "../kunpocc"; -import { Component } from "./Component"; - -/** - * @Author: Gongxh - * @Date: 2025-01-24 - * @Description: - */ -export class ECDataHelper { - /** 组件池 @internal */ - public static _componentPool: ComponentPool = new ComponentPool(); - /** 注册所有组件 */ - public static registerComponents(): void { - let index = 0; - let maps = _ecdecorator.getComponentMaps(); - maps.forEach((info: _ecdecorator.ECComponentInfo, ctor: any) => { - this._componentPool.register(index++, info.componentType, info.name, ctor); - }); - } - - public static getComponentPool(): ComponentPool { - return this._componentPool; - } - - /** 解析组件数据 */ - public static parse(component: Component, data: Record): void { - const maps = _ecdecorator.getComponentMaps(); - const ctor = component.constructor; - if (!maps.has(ctor)) { - return; - } - const info = maps.get(ctor); - for (const property in data) { - let propInfo = info.props[property]; - if (!propInfo) { - warn(`组件 ${component.name} 属性 ${property} 未注册`); - continue; - } - let value = data[property]; - (component as any)[property] = this.getPropValue(propInfo, value); - } - } - - private static getPropValue(propInfo: _ecdecorator.ECPropInfo, value: any): any { - switch (propInfo.type) { - case "int": - if (typeof value === "number") { - return value; - } - return propInfo.defaultValue || 0; - case "float": - if (typeof value === "number") { - return value; - } - return propInfo.defaultValue || 0; - case "boolean": - if (typeof value === "boolean") { - return value; - } - return propInfo.defaultValue || false; - case "size": - if (typeof value === "object" && typeof value.width === "number" && typeof value.height === "number") { - return size(value.width, value.height); - } - return propInfo.defaultValue || size(0, 0); - case "vec2": - if (typeof value === "object" && typeof value.x === "number" && typeof value.y === "number") { - return v2(value.x, value.y); - } - return propInfo.defaultValue || v2(0, 0); - case "vec3": - if (typeof value === "object" && typeof value.x === "number" && typeof value.y === "number" && typeof value.z === "number") { - return v3(value.x, value.y, value.z); - } - return propInfo.defaultValue || v3(0, 0, 0); - case "color": - if (typeof value === "object" && typeof value[0] === "number" && typeof value[1] === "number" && typeof value[2] === "number") { - return color(value[0], value[1], value[2], typeof value[3] === "number" ? value[3] : 255); - } - return propInfo.defaultValue || color(255, 255, 255, 255); - case "asset": - case "spriteframe": - case "prefab": - case "jsonAsset": - case "particle": - case "animation": - case "audio": - case "skeleton": - case "entity": - return typeof value === "string" ? value : (propInfo.defaultValue || ""); - case "enum": - return value; - case "array": - if (Array.isArray(value)) { - return value; - } - return propInfo.defaultValue || []; - case "object": - if (typeof value === "object") { - return value; - } - return propInfo.defaultValue || {}; - default: - break; - } - return undefined; - } -} diff --git a/src/ecmodule/ECDecorator.ts b/src/ecmodule/ECDecorator.ts deleted file mode 100644 index e497818..0000000 --- a/src/ecmodule/ECDecorator.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2025-01-14 - * @Description: 实体组件装饰器 - */ - -import { Color, Size, Vec2, Vec3 } from "cc"; -import { ObjectHelper } from "../tool/helper/ObjectHelper"; - - -export namespace _ecdecorator { - /** @internal */ - const ECPropMeta = "__ecpropmeta__" - - type ECPropType = "int" | "float" | "string" | "boolean" | "size" | "vec2" | "vec3" | "color" | "asset" | "spriteframe" | "jsonAsset" | "particle" | "animation" | "audio" | "prefab" | "skeleton" | "enum" | "array" | "object" | "entity"; - - interface ECPropInfoBase { - /** 属性默认值 */ - defaultValue?: any, - /** 编辑器中的显示名称 */ - displayName?: string, - /** 编辑器中的提示 */ - tips?: string - } - - interface ECPropInfoNumber extends ECPropInfoBase { - /** 属性类型 */ - type: "int" | "float"; - /** 默认值:0 */ - defaultValue?: number; - } - - interface ECPropInfoBoolean extends ECPropInfoBase { - /** 属性类型 */ - type: "boolean"; - /** 默认值:false */ - defaultValue?: boolean; - } - - interface ECPropInfoSize extends ECPropInfoBase { - /** 属性类型 */ - type: "size"; - /** 默认值:Size(0,0) */ - defaultValue?: Size; - } - - interface ECPropInfoVec extends ECPropInfoBase { - /** 属性类型 */ - type: "vec2" | "vec3"; - /** 默认值: Vec2(0,0) | Vec3(0,0,0) */ - defaultValue?: Vec2 | Vec3; - } - - interface ECPropInfoString extends ECPropInfoBase { - /** 属性类型 */ - type: "string" | "asset" | "spriteframe" | "jsonAsset" | "particle" | "animation" | "audio" | "prefab" | "skeleton" | "entity"; - /** 默认值: "" */ - defaultValue?: string; - } - - interface ECPropInfoColor extends ECPropInfoBase { - /** 属性类型 */ - type: "color"; - /** 默认值:Color(255, 255, 255, 255) */ - defaultValue?: Color; - } - - interface ECPropInfoArray extends ECPropInfoBase { - /** 属性类型 */ - type: "array"; - /** 类型格式 当类型是复合类型enum、array、object时必须 */ - format: ECPropType | ECPropInfo; - } - - interface ECPropInfoObject extends ECPropInfoBase { - /** 属性类型 */ - type: "object"; - /** 类型格式 当类型是复合类型enum、array、object时必须 */ - format: Record | Record; - } - - interface ECPropInfoEnum extends ECPropInfoBase { - type: "enum"; - /** 枚举值 */ - format: object; - /** 默认值 */ - defaultValue?: string | number; - } - - export type ECPropInfo = ECPropInfoNumber | ECPropInfoBoolean | ECPropInfoSize | ECPropInfoVec | ECPropInfoString | ECPropInfoColor | ECPropInfoArray | ECPropInfoObject | ECPropInfoEnum; - - /** - * 组件注册数据结构 - */ - export interface ECComponentInfo { - /** 组件名 */ - name: string; - /** 组件类型 */ - componentType: number; - /** 组件描述 */ - describe: string; - /** 属性 */ - props: Record; - } - /** 用来存储组件注册信息 */ - const eclassMap: Map = new Map(); - - /** 获取组件注册信息 */ - export function getComponentMaps(): Map { - return eclassMap; - } - - /** - * 实体组件装饰器 - * @param {string} res.describe 组件组描述 - */ - export function ecclass(name: string, componentType: number, res?: { describe?: string }): Function { - /** target 类的构造函数 */ - return function (ctor: any): void { - // console.log(`组件装饰器 组件【${name}】属性:`, JSON.stringify(ctor[ECPropMeta])); - eclassMap.set(ctor, { - name: name, - componentType: componentType, - props: ctor[ECPropMeta], - describe: res?.describe || name - }); - }; - } - - /** 组件属性装饰器 */ - export function ecprop(options: ECPropInfo): any { - return function (target: any, propName: any): void { - ObjectHelper.getObjectProp(target.constructor, ECPropMeta)[propName] = options; - }; - } -} - -let _global = globalThis || window || global; -(_global as any)["getKunpoRegisterECMaps"] = function () { - return _ecdecorator.getComponentMaps() as any; -}; \ No newline at end of file diff --git a/src/ecmodule/ECManager.ts b/src/ecmodule/ECManager.ts deleted file mode 100644 index 6f15e36..0000000 --- a/src/ecmodule/ECManager.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2025-01-14 - * @Description: 实体组件管理对外接口 - */ - -import { Node } from "cc"; -import { ECDataHelper } from "./ECDataHelper"; -import { Entity } from "./Entity"; -import { EntityManager } from "./EntityManager"; - -interface IEntityConfig { - [componentName: string]: Record -} - -interface IWorldConfig { - /** 实体管理器 */ - world: EntityManager; - /** 世界节点 */ - worldNode: Node; -} - -export class ECManager { - /** 实体管理器 @internal */ - private static _worlds: Map = new Map(); - /** 实体配置信息 @internal */ - private static _entityList: { [name: string]: Record } = {}; - - /** 注册所有组件 如果GameEntry因分包导致,组件的代码注册晚于 CocosEntry的 onInit函数, 则需要在合适的时机手动调用此方法 */ - public static registerComponents(): void { - ECDataHelper.registerComponents(); - } - - /** - * 创建EC世界 创建EC世界前必须先注册组件 - * @param {string} worldName 名称 - * @param {Node} node 世界节点 - * @param {number[]} componentUpdateOrderList 组件更新顺序列表 (只传需要更新的组件列表) - * @param {number} [maxCapacityInPool=128] 实体池最大容量,多余的实体不会缓存 - * @param {number} [preloadEntityCount=32] 预加载Entity数量 - */ - public static createECWorld(worldName: string, node: Node, componentUpdateOrderList: number[], maxCapacityInPool = 128, preloadEntityCount = 32): EntityManager { - if (this._worlds.has(worldName)) { - throw new Error(`ECWorld ${worldName} already exists`); - } - const entityManager = new EntityManager(worldName, ECDataHelper.getComponentPool(), componentUpdateOrderList, maxCapacityInPool, preloadEntityCount); - this._worlds.set(worldName, { world: entityManager, worldNode: node }); - return entityManager; - } - - /** 获取EC世界 */ - public static getECWorld(worldName: string): EntityManager { - if (!this._worlds.has(worldName)) { - throw new Error(`ECWorld ${worldName} not found`); - } - const entityManager = this._worlds.get(worldName).world; - if (!entityManager) { - throw new Error(`ECWorld ${worldName} is null`); - } - return entityManager; - } - - /** 获取EC世界节点 */ - public static getECWorldNode(worldName: string): Node { - if (!this._worlds.has(worldName)) { - throw new Error(`ECWorld ${worldName} not found`); - } - const node = this._worlds.get(worldName).worldNode; - if (!node) { - throw new Error(`ECWorld ${worldName} is null`); - } - return node; - } - - /** 销毁EC世界 */ - public static destroyECWorld(worldName: string): void { - let entityManager = this.getECWorld(worldName); - if (entityManager) { - entityManager.destroy(); - this._worlds.delete(worldName); - } - } - - /** - * 注册配置表中的实体信息 - * @param config 实体配置信息,格式为 {实体名: {组件名: 组件数据}} - */ - public static registerEntityConfig(config: { [entityName: string]: IEntityConfig }): void { - if (!config) { - return; - } - // 遍历并注册每个实体的配置 - for (const entityName in config) { - this._entityList[entityName] = config[entityName]; - } - } - - /** - * 添加实体信息 (如果已经存在, 则数据组合) - * 如果存在编辑器编辑不了的数据 用来给编辑器导出的实体信息 添加扩展数据 - * @param name 实体名 - * @param info 实体信息 - */ - public static addEntityInfo(name: string, info: IEntityConfig): void { - if (this._entityList[name]) { - this._entityList[name] = Object.assign(this._entityList[name], info); - } else { - this._entityList[name] = info; - } - } - - /** 获取实体配置信息 */ - public static getEntityInfo(name: string): Record { - if (!this._entityList[name]) { - throw new Error(`Entity ${name} info not found, please register it first`); - } - return this._entityList[name]; - } - - /** - * 创建实体 - * @param worldName 实体管理器名称 - * @param name 实体名字 - * @returns {kunpo.Entity} 实体 - */ - public static createEntity(worldName: string, name: string): Entity { - let info = this.getEntityInfo(name); - let world = this.getECWorld(worldName); - let entity = world.createEntity(name); - info && this._addComponentToEntity(world, entity, info); - - world.addEntity(entity); - return entity; - } - - /** - * 添加组件到实体 - * @param {EntityManager} world 实体管理器 - * @param {Entity} entity 实体 - * @param {Record} componentsData 组件数据 - * @internal - */ - private static _addComponentToEntity(world: EntityManager, entity: Entity, componentsData: Record): void { - for (const componentName in componentsData) { - let component = world.createComponent(componentName); - ECDataHelper.parse(component, componentsData[componentName]); - entity.addComponent(component); - } - } - - /** - * 销毁实体 - * @param worldName 世界名称 - * @param entity 实体 - */ - public static destroyEntity(worldName: string, entity: Entity): void { - if (!entity || !entity.id) { - return; - } - this.destroyEntityById(worldName, entity.id); - } - - /** - * 销毁实体 - * @param worldName 世界名称 - * @param entityId 实体ID - */ - public static destroyEntityById(worldName: string, entityId: number): void { - let world = this.getECWorld(worldName); - world.destroyEntityById(entityId); - } -} diff --git a/src/ecmodule/ECType.ts b/src/ecmodule/ECType.ts deleted file mode 100644 index a5a7520..0000000 --- a/src/ecmodule/ECType.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * @type {&} AND,按位与处理两个长度相同的二进制数,两个相应的二进位都为 1,该位的结果值才为 1,否则为 0 - * @type {|} OR,按位或处理两个长度相同的二进制数,两个相应的二进位中只要有一个为 1,该位的结果值为 1 - * @type {~} 取反,取反是一元运算符,对一个二进制数的每一位执行逻辑反操作。使数字 1 成为 0,0 成为 1 - * @type {^} 异或,按位异或运算,对等长二进制模式按位或二进制数的每一位执行逻辑异按位或操作。操作的结果是如果某位不同则该位为 1,否则该位为 0 - * @type {<<} 左移,把 << 左边的运算数的各二进位全部左移若干位,由 << 右边的数指定移动的位数,高位丢弃,低位补0; 将一个值左移一个位置相当于将其乘以2,移位两个位置相当于乘以4,依此类推。 - * @type {>>} 右移,把 >> 左边的运算数的各二进位全部右移若干位,>> 右边的数指定移动的位数 - * @type {>>>} 无符号右移,与有符号右移位类似,除了左边一律使用0 补位 - */ -import { Stack } from "../tool/DataStruct/Stack"; - -/** 实体索引位数 @internal */ -export const EntityIndexBits = 16; -/** 实体索引掩码 @internal */ -export const EntityIndexMask = (1 << EntityIndexBits) - 1; -/** 最大实体数量 @internal */ -export const MaxEntityCount = 1 << EntityIndexBits; -export type EntityId = number; - -/** - * 2进制转10进制 (不支持小数和负数) - * @param {number} bitNumber 二进制数 - * @return {number} 十进制数 - * @internal - */ -export function bit2Decimal(bitNumber: number): number { - let bitString = String(bitNumber); - let len = bitString.length; - let index = len - 1; - let result: number = 0; - do { - result += Number(bitString[index]) * Math.pow(2, len - index - 1); - index--; - } while (index >= 0); - return result; -} -/** - * 10进制转2进制 (不支持小数和负数) - * @param {number} num 十进制数 - * @return {number} 二进制数 - * @internal - */ -export function decimal2Bit(num: number): number { - let stack = new Stack(); - let dividend: number = Math.floor(num); - let remainder: number; - do { - remainder = dividend % 2; - stack.push(remainder); - dividend = Math.floor(dividend / 2); - } while (dividend > 0); - let result = ""; - while (!stack.isEmpty()) { - result += stack.pop().toString(); - } - return Number(result); -} - -/** - * 通过实体id获取实体index - * @param id 实体id - * @return {number} 实体index - * @internal - */ -export function getEntityIndex(id: EntityId): number { - return id & EntityIndexMask; -} - -/** - * 通过实体id获取实体版本 - * @param id 实体id - * @return {number} 实体版本 - * @internal - */ -export function getEntityVersion(id: EntityId): number { - return id >>> EntityIndexBits; -} - -/** - * 实体描述 - * @param id 实体id - * @return {string} 实体描述 - * @internal - */ -export function entityIdString(id: EntityId): string { - return `${getEntityIndex(id)}:${getEntityVersion(id)}`; -} -// console.log("-------->", EntityIndexBits); 16 -// console.log("-------->", EntityIndexMask); 65535 -// console.log("-------->", MaxEntityCount); 65536 \ No newline at end of file diff --git a/src/ecmodule/Entity.ts b/src/ecmodule/Entity.ts deleted file mode 100644 index 7acbba0..0000000 --- a/src/ecmodule/Entity.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { Component } from "./Component"; -import { EntityId } from "./ECType"; -import { EntityManager } from "./EntityManager"; - -export class Entity { - /** - * 实体名称 - * @type {String} - */ - public name: string; - - /** - * 实体ID - * @type {EntityId} - */ - public id: EntityId; - - /** - * 实体标识 - * @type {Set} - * @memberof Entity - */ - public tags: Set; - - /** - * 实体状态 - * @type {Map} - * @memberof Entity - */ - public states: Map; - /** - * 是否被激活 (添加到实体管理器时激活) - * @type {boolean} - */ - public active: boolean = false; - - /** - * 所属实体管理器 (实体创建后直接赋值) - * @private - * @type {EntityManager} - */ - public entityManager: EntityManager; - - /** - * 所有组件 - * @type {Map} - * @type {number} 组件类型 - * @type {Component} 组件 - */ - public readonly components: Map = new Map(); - - /** - * 实体被添加到EntityManager - * @internal - */ - public _add(): void { - this.active = true; - for (const component of this.components.values()) { - component._enter(); - } - } - - /** - * 实体销毁,不要手动调用 - * @internal - */ - public _destroy(): void { - this.removeAllComponents(); - this.tags && this.tags.clear(); - this.states && this.states.clear(); - this.active = false; - this.entityManager = null; - } - - /** - * 添加标签 - * @param {number[]} ...tags 标签除了表示Entity,还可以通过EntityManager获取指定标签的Entity - */ - public addTag(...tag: number[]): void { - let tags = this.tags; - if (!tags) { - tags = this.tags = new Set(); - } - for (let i = 0; i < tag.length; i++) { - tags.add(tag[i]); - this.active && this.entityManager && this.entityManager._addEntityTag(this.id, tag[i]); - } - } - - /** - * 删除标签 - * @param {number} tag 删除的标签 - */ - public removeTag(tag: number): void { - if (this.tags) { - this.tags.delete(tag); - this.active && this.entityManager && this.entityManager._removeEntityTagById(this.id, tag); - } - } - - /** - * 是否包含标签 - * @param {number} tag 标签 - * @returns {boolean} 是否包含 - */ - public hasTag(...tag: number[]): boolean { - let tags = this.tags; - if (!tags) { - return false; - } - for (let i = 0; i < tag.length; i++) { - if (tags.has(tag[i])) { - return true; - } - } - return false; - } - - /** - * 获取组件 - * @param {number} componentType 组件类型 - * @returns {T} - */ - public getComponent(componentType: number): T { - return this.components.get(componentType) as T; - } - - /** - * 添加组件 - * @param {Component} component 组件 - */ - public addComponent(component: Component): void { - if (this.hasComponent(component.type)) { - throw new Error(`组件{${component.constructor.name}类型:${component.type})已经存在,不允许添加同一类型组件`); - } - this.components.set(component.type, component); - component.entity = this; - component._add(); - - if (this.active) { - component._enter(); - } - } - - /** - * 删除组件 - * @param {number} componentType 组件类型 - */ - public removeComponent(componentType: number): void { - const component = this.components.get(componentType); - - if (component) { - this.components.delete(componentType); - component._remove(); - } - } - - /** - * 删除所有组件 - */ - public removeAllComponents(): void { - for (const component of this.components.values()) { - component._remove(); - } - this.components.clear(); - } - - /** - * 是否包含组件 - * @param {number} componentType 组件类型 - * @returns {boolean} 是否包含组件 - */ - public hasComponent(componentType: number): boolean { - return this.components.has(componentType); - } - - /** - * 销毁自己 - */ - public destroy(): void { - this.entityManager.destroyEntityById(this.id); - } - - /** - * 添加监听 - * @param eventName 监听的消息名 - * @param callback 回调 - * @param entityId 实体ID - * @param once 是否单次监听 - */ - public addEvent(eventName: string, callback: (...args: any[]) => void, once: boolean = false): void { - this.entityManager && this.entityManager._addEvent(eventName, callback, this, once); - } - - /** - * 发送消息 - * @param eventName 消息名 - * @param entityId 实体ID - * @param args 发送参数 - */ - public sendListener(eventName: string, ...args: any[]): void { - this.entityManager && this.entityManager._sendEvent(eventName, this, ...args); - } - - public removeListener(eventName: string, callback?: (...args: any[]) => void): void { - this.entityManager && this.entityManager._removeEvent(eventName, this, callback); - } - - /** - * 添加状态 - * 状态采用计数方式,对状态处理时需要保证addState和removeState成对存在 - * @param {number} state 状态类型 - * @memberof Entity - */ - public addState(state: number): void { - let states = this.states; - if (!states) { - states = this.states = new Map(); - } - states.set(state, (states.get(state) || 0) + 1); - } - - /** - * 删除状态 - * - * @param {number} state 状态类型 - * @returns {boolean} 如果计数为0或状态不存在,则返回true - * @memberof Entity - */ - public removeState(state: number): boolean { - const states = this.states; - if (!states) { - return false; - } - let stateCount = states.get(state); - if (stateCount) { - // 处理状态计数,为0则删除状态 - --stateCount; - if (stateCount == 0) { - states.delete(state); - return true; - } - - states.set(state, stateCount); - return false; - } - return true; - } - - /** - * 是否包含指定状态 - * @param {number} state 状态 - * @returns {boolean} - * @memberof Entity - */ - public hasState(state: number): boolean { - return this.states && this.states.has(state); - } - - /** - * 清除状态 - * @param {number} state 状态 - * @memberof Entity - */ - public clearState(state: number): void { - this.states && this.states.delete(state); - } - - /** - * 清除所有状态 - * @memberof Entity - */ - public clearAllStates(): void { - this.states && this.states.clear(); - } -} diff --git a/src/ecmodule/EntityManager.ts b/src/ecmodule/EntityManager.ts deleted file mode 100644 index 5096784..0000000 --- a/src/ecmodule/EntityManager.ts +++ /dev/null @@ -1,442 +0,0 @@ -import { EventManager } from "../event/EventManager"; -import { warn } from "../tool/log"; -import { Component } from "./Component"; -import { ComponentManager } from "./ComponentManager"; -import { ComponentPool } from "./ComponentPool"; -import { EntityId, entityIdString, EntityIndexBits, getEntityIndex, getEntityVersion, MaxEntityCount } from "./ECType"; -import { Entity } from "./Entity"; - -export class EntityManager { - /** - * 名称 - * @type {string} - */ - public name: string; - - /** - * 单例实体 - * @type {Entity} - * @internal - */ - public readonly insEntity: Entity = new Entity(); - - /** - * 单例实体激活状态 - * @type {boolean} - * @internal - */ - public insActive: boolean = false; - - /** - * 组件管理 - * @type {ComponentManager} - */ - public componentManager: ComponentManager; - - /** - * 普通实体事件容器 - * @type {EventManager} - * @internal - */ - private _eventManager: EventManager; - - /** - * 单例实体消息监听容器 - * @type {EventManager} - * @internal - */ - private _insEventManager: EventManager; - - /** 实体池 @internal */ - private readonly _entityPool: Entity[] = []; - /** tag标记池 @internal */ - private readonly _tagToEntity: Map> = new Map>(); - /** 实体回收池 @internal */ - private _recyclePool: Entity[] = []; - /** 实体回收池最大容量 @internal */ - private _maxCapacityInPool: number; - /** 实体回收版本 @internal */ - private _entityVersion: number[] = []; - /** 回收实体ID @internal */ - private _recycleEntityIds: EntityId[] = []; - /** 世界是否删除 @internal */ - private _isDestroyed: boolean; - /** 是否正在更新 @internal */ - private _updating: boolean; - /** - * 实体池最大容量,回收的多余的实体不会缓存 - * @param {string} name 名称 - * @param {ComponentPool} componentPool 组件池 - * @param {ComponentPool} componentUpdateOrderList 组件更新顺序 - * @param {number} [maxCapacityInPool=128] 实体回收池最大容量 - * @param {number} [preloadEntityCount=32] 预加载Entity数量 - */ - constructor(name: string, componentPool: ComponentPool, componentUpdateOrderList: number[], maxCapacityInPool: number = 128, preloadEntityCount: number = 32) { - this.name = name; - if (preloadEntityCount >= MaxEntityCount) { - throw new Error(`预加载超出实体最大数量:${preloadEntityCount} >= max(${MaxEntityCount})`); - } - // 占位 - this._entityPool.push(null); - this._entityVersion.push(1); - this._maxCapacityInPool = maxCapacityInPool; - // 预创建 - for (let i = 0; i < preloadEntityCount; ++i) { - this._recyclePool.push(new Entity()); - } - // 组件管理器 - this.componentManager = new ComponentManager(componentPool, componentUpdateOrderList); - this.insEntity.entityManager = this; - } - - /** - * 添加实体标签(内部使用) - * @param {EntityId} entityId 实体Id - * @param {number} tag 标签 - * @internal - */ - public _addEntityTag(entityId: EntityId, tag: number): void { - this._validateEntityById(entityId); - let entitiesByTag = this._tagToEntity.get(tag); - if (!entitiesByTag) { - entitiesByTag = new Set(); - this._tagToEntity.set(tag, entitiesByTag); - } - entitiesByTag.add(entityId); - } - - /** - * 删除实体Tag(内部使用) - * @param {Entity} entity 实体 - * @param {number} tag 标签 - * @internal - */ - public _removeEntityTag(entity: Entity, tag: number): void { - this._removeEntityTagById(entity.id, tag); - } - - /** - * 通过实体ID删除实体Tag(内部使用) - * @param {EntityId} entityId 实体Id - * @param {number} tag 标签 - * @internal - */ - public _removeEntityTagById(entityId: EntityId, tag: number): void { - this._validateEntityById(entityId); - const entitiesByTag = this._tagToEntity.get(tag); - if (entitiesByTag) { - entitiesByTag.delete(entityId); - } - } - - /** - * 创建实体 - * @returns {Entity} 实体 - */ - public createEntity(name: string): Entity { - const entity = this._recyclePool.pop() || new Entity(); - entity.id = 0; - entity.name = name; - entity.entityManager = this; - return entity; - } - - /** - * 添加实体 - * @param {Entity} entity 要添加的实体 - */ - public addEntity(entity: Entity): void { - if (this.exists(entity.id)) { - throw new Error(`实体(${entityIdString(entity.id)})已经添加到EntityManager`); - } - // 分配实体Id - if (this._recycleEntityIds.length > 0) { - const newIndex = this._recycleEntityIds.pop(); - this._entityPool[newIndex] = entity; - entity.id = (this._entityVersion[newIndex] << EntityIndexBits) | newIndex; - } else { - this._entityPool.push(entity); - this._entityVersion.push(1); - entity.id = MaxEntityCount | (this._entityPool.length - 1); - } - this._addEntityToTag(entity); - entity._add(); - } - - /** - * 销毁实体 - * @param {Entity} entity 要删除的实体 - */ - public destroyEntity(entity: Entity): void { - this.destroyEntityById(entity.id); - } - - /** - * 销毁指定ID实体 - * @param {EntityId} entityId 实体Id - */ - public destroyEntityById(entityId: EntityId): void { - const entity = this.getEntity(entityId); - if (!entity) { - warn(`实体(${entityIdString(entityId)})已经被销毁`); - return; - } - this._recycleEntity(entity); - this._eventManager && this._eventManager.removeList(entity); - } - - /** - * 销毁所有实体 - * @param {boolean} ignoreSingletonEntity 是否忽略单例实体 - */ - public destroyAllEntities(ignoreSingletonEntity: boolean): void { - const entities = this._entityPool; - for (let i = 1, len = entities.length; i < len; ++i) { - if (entities[i]) { - this._destroyEntity(entities[i]); - } - } - this._recycleEntityIds.length = 0; - this._entityPool.length = 0; - this._entityVersion.length = 0; - this._tagToEntity.clear(); - - // 占位 - this._entityPool.push(null); - this._entityVersion.push(1); - this._eventManager && this._eventManager.destroyAll(); - - // 销毁单例实体组件 - if (!ignoreSingletonEntity) { - this.insEntity._destroy(); - this.insActive = false; - this._insEventManager && this._insEventManager.destroyAll(); - } - } - - /** - * 通过实体ID获取实体 - * @param {EntityId} entityId 实体Id - * @returns {(Entity | null)} 实体 - */ - public getEntity(entityId: EntityId): Entity | null { - const index = getEntityIndex(entityId); - if (index <= 0 || index >= this._entityPool.length) { - return null; - } - if (this._entityVersion[index] == getEntityVersion(entityId)) { - return this._entityPool[index]; - } - return null; - } - - /** - * 获取指定标签的实体 - * @param {number} tag 标签 - * @returns {Entity[]} 返回的实体池 - */ - public getEntitiesByTag(tag: number): Entity[] { - let buffer: Entity[] = []; - const entitiesByTag = this._tagToEntity.get(tag); - if (entitiesByTag) { - for (const entityId of entitiesByTag) { - const entity = this.getEntity(entityId); - entity && buffer.push(entity); - } - } - return buffer; - } - - /** - * 根据实体ID判断实体是否存在 - * @param {EntityId} entityId 实体Id - * @returns {boolean} - */ - public exists(entityId: EntityId): boolean { - const index = getEntityIndex(entityId); - if (index <= 0 || index >= this._entityPool.length) { - return false; - } - const entity = this._entityPool[index]; - return entity && this._entityVersion[index] == getEntityVersion(entityId); - } - - /** - * 创建组件 - * @template T 组件类型 - * @param {string} componentName 组件名 - * @returns {T} 创建的组件 - */ - public createComponent(componentName: string): T { - return this.componentManager.createComponent(componentName); - } - - /** - * 添加单例组件 - * @param component - */ - public addSingleton(component: Component): void { - this.insEntity.addComponent(component); - } - - /** - * 获取单例组件 - */ - public getSingleton(componentType: number): T { - return this.insEntity.getComponent(componentType); - } - - /** - * 删除单例组件 - */ - public removeSingleton(componentType: number): void { - this.insEntity.removeComponent(componentType); - } - - /** - * 是否存在对应的单例组件 - */ - public hasSingleton(componentType: number): boolean { - return this.insEntity.hasComponent(componentType); - } - - /** - * 激活单例组件 - */ - public activeSingleton(): void { - const insEntity = this.insEntity; - if (this.insActive) { - throw new Error("单例实体已经被激活"); - } - this.insActive = true; - insEntity.id = -1; - insEntity._add(); - } - - /** - * 销毁EntityManager - */ - public destroy(): void { - if (this._isDestroyed) { - return; - } - if (this._updating) { - throw new Error("请勿在更新时销毁EntityManager"); - } - this.destroyAllEntities(false); - this.componentManager.destroy(); - this._isDestroyed = true; - } - - /** - * 添加消息监听 (内部使用) - * @param eventName 消息名 - * @param callback 事件回调 - * @param entityId 实体ID - * @param once 是否单次事件 - * @internal - */ - public _addEvent(eventName: string, callback: (...args: any[]) => void, entity: Entity, once: boolean = false): void { - if (entity == this.insEntity) { - this._insEventManager = this._insEventManager ? this._insEventManager : new EventManager(); - this._insEventManager._addEvent(eventName, callback, once, entity); - return; - } - this._eventManager = this._eventManager ? this._eventManager : new EventManager(); - this._eventManager._addEvent(eventName, callback, once, entity); - } - - /** - * 发送消息 (内部使用) - * @param eventName 消息名 - * @param entityId 实体ID - * @param args 发送参数 - * @internal - */ - public _sendEvent(eventName: string, entity: Entity, ...args: any[]): void { - if (entity == this.insEntity) { - this._insEventManager && this._insEventManager.send(eventName, entity, ...args); - return; - } - this._eventManager && this._eventManager.send(eventName, entity, ...args); - } - - /** - * 移除消息监听 (内部使用) - * @param eventName 消息名 - * @param entity 实体 - * @param callback 事件回调 - * @internal - */ - public _removeEvent(eventName: string, entity: Entity, callback?: (...args: any[]) => void): void { - if (entity == this.insEntity) { - this._insEventManager && this._insEventManager.remove(eventName, callback, entity); - return; - } - this._eventManager && this._eventManager.remove(eventName, callback, entity); - } - - /** - * 更新 - * @param {number} dt 时间间隔 - */ - public update(dt: number): void { - this._updating = true; - this.componentManager._update(dt); - this._updating = false; - } - - /** - * 回收Entity - * @param {Entity} entity 要回收的Entity - * @internal - */ - private _recycleEntity(entity: Entity): void { - // 回收实体Id - const entityIndex = getEntityIndex(entity.id); - this._recycleEntityIds.push(entityIndex); - this._entityPool[entityIndex] = null; - ++this._entityVersion[entityIndex]; - - this._destroyEntity(entity); - } - - /** - * 销毁实体 - * @param {Entity} entity - * @internal - */ - private _destroyEntity(entity: Entity): void { - entity._destroy(); - if (this._recyclePool.length < this._maxCapacityInPool) { - this._recyclePool.push(entity); - } - } - - /** - * 实体根据tag添加到tag列表中 - * @param entity - * @internal - */ - private _addEntityToTag(entity: Entity): void { - const tags = entity.tags; - if (!tags || tags.size == 0) { - return; - } - const entityId = entity.id; - for (const tag of tags.values()) { - this._addEntityTag(entityId, tag); - } - } - - /** - * 验证实体ID是否存在 - * @param {EntityId} entityId 实体ID - * @internal - */ - private _validateEntityById(entityId: EntityId): void { - if (!this.exists(entityId)) { - throw new Error(`实体(${entityId})不存在`); - } - } -} diff --git a/src/ecmodule/ObjectBase.ts b/src/ecmodule/ObjectBase.ts deleted file mode 100644 index d285f8c..0000000 --- a/src/ecmodule/ObjectBase.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class ObjectBase { - /** 是否被回收 */ - public recycled: boolean; - - /** 对象类型 */ - public objectType: number; - - /** 回收 @internal */ - public _recycle(): void { - this.recycled = true; - } - - /** 重新利用 @internal */ - public _reuse(): void { - this.recycled = false; - } -} \ No newline at end of file diff --git a/src/ecmodule/ObjectFactory.ts b/src/ecmodule/ObjectFactory.ts deleted file mode 100644 index 7e863c5..0000000 --- a/src/ecmodule/ObjectFactory.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { ObjectBase } from "./ObjectBase"; - -/** @internal */ -export class ObjectFactory { - /** 对象类 */ - private _ctor: new () => ObjectBase; - /** 对象名称 */ - private _name: string; - /** 对象类型 */ - private _objectType: number; - /** 最大容量 */ - private _maxCapacity: number; - /** 对象池 */ - private _stack: ObjectBase[] = []; - - constructor(objectType: number, capacity: number, name: string, objectClass: new () => ObjectBase) { - this._objectType = objectType; - this._maxCapacity = capacity; - this._name = name; - this._ctor = objectClass; - } - - /** - * 获取对象名称 - * @returns {string} 对象名称 - */ - public get name(): string { - return this._name; - } - - /** - * 获取对象 - * @returns {T} 返回的组件 - */ - public allocate(): T { - if (this._stack.length == 0) { - const ret = new this._ctor() as T; - ret.objectType = this._objectType; - return ret; - } - const ret = this._stack.pop() as T; - ret._reuse(); - return ret; - } - - /** - * 回收对象 - * @returns {boolean} - */ - public recycle(ret: ObjectBase): boolean { - if (ret.recycled) { - throw new Error(`对象(${ret.constructor.name})已经被回收了`); - } - if (this._maxCapacity > 0 && this._stack.length < this._maxCapacity) { - ret._recycle(); - this._stack.push(ret); - return true; - } - return false; - } - - public _clear(): void { - this._stack.length = 0; - } -} \ No newline at end of file diff --git a/src/fgui/WindowBase.ts b/src/fgui/WindowBase.ts index 8e70693..fe16e17 100644 --- a/src/fgui/WindowBase.ts +++ b/src/fgui/WindowBase.ts @@ -23,6 +23,8 @@ export abstract class WindowBase extends GComponent implements IWindow { private _header: IWindowHeader = null; /** 窗口是否被遮挡了 @internal */ private _isCover: boolean = false; + /** 吞噬触摸的节点 @internal */ + private _swallowComponent: GComponent = null; /** * 初始化方法 (框架内部使用) * @param swallowTouch 是否吞噬触摸事件 @@ -33,14 +35,16 @@ export abstract class WindowBase extends GComponent implements IWindow { if (swallowTouch) { // 吞噬触摸事件,需要一个全屏的节点, 窗口本身可能留有安全区的边 let bgNode = new GComponent(); + bgNode.name = "swallow"; bgNode.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); bgNode.setPivot(0.5, 0.5, true); - bgNode.setPosition(Screen.ScreenWidth * 0.5, Screen.ScreenHeight * 0.5); + bgNode.setPosition(this.width * 0.5, this.height * 0.5); this.addChild(bgNode); // 调整显示层级 bgNode.parent.setChildIndex(bgNode, 0); bgNode.onClick(this.onEmptyAreaClick, this); bgNode.opaque = swallowTouch; + this._swallowComponent = bgNode; } // 窗口自身也要设置是否吞噬触摸 this.opaque = swallowTouch; @@ -65,6 +69,8 @@ export abstract class WindowBase extends GComponent implements IWindow { default: break; } + this._swallowComponent?.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + this._swallowComponent?.setPosition(this.width * 0.5, + this.height * 0.5); this.onAdapted(); } diff --git a/src/kunpocc.ts b/src/kunpocc.ts index db79336..43c4c66 100644 --- a/src/kunpocc.ts +++ b/src/kunpocc.ts @@ -28,25 +28,6 @@ export { Socket } from "./net/socket/Socket"; /** 读取网络文件 */ export { ReadNetFile } from "./net/nettools/ReadNetFile"; -/** 四叉树 */ -export { Box } from "./quadtree/Box"; -export { Circle } from "./quadtree/Circle"; -export { IShape } from "./quadtree/IShape"; -export { Polygon } from "./quadtree/Polygon"; -export { QTConfig, QuadTree } from "./quadtree/QuadTree"; - -/** 行为树 */ -export { Agent as BTAgent } from "./behaviortree/Agent"; -export { BehaviorTree } from "./behaviortree/BehaviorTree"; -export { Blackboard as BTBlackboard } from "./behaviortree/Blackboard"; -export * as BTAction from "./behaviortree/BTNode/Action"; -export * as BTNode from "./behaviortree/BTNode/BaseNode"; -export * as BTComposite from "./behaviortree/BTNode/Composite"; -export * as BTCondition from "./behaviortree/BTNode/Condition"; -export * as BTDecorator from "./behaviortree/BTNode/Decorator"; -export { Status as BTStatus } from "./behaviortree/header"; -export { Ticker as BTTicker } from "./behaviortree/Ticker"; - /** UI */ export { Window } from "./fgui/Window"; export { WindowHeader } from "./fgui/WindowHeader"; @@ -57,23 +38,10 @@ export { WindowGroup } from "./ui/WindowGroup"; export { WindowHeaderInfo } from "./ui/WindowHeaderInfo"; export { WindowManager } from "./ui/WindowManager"; -/** EC */ -export { Component } from './ecmodule/Component'; -export { ComponentManager } from './ecmodule/ComponentManager'; -export { ComponentPool } from './ecmodule/ComponentPool'; -export { _ecdecorator } from './ecmodule/ECDecorator'; -export { ECManager } from './ecmodule/ECManager'; -export { Entity } from './ecmodule/Entity'; -export { EntityManager } from './ecmodule/EntityManager'; - /** 引擎相关 */ export { CocosEntry } from "./cocos/CocosEntry"; export { CocosUIModule } from "./cocos/CocosUIModule"; -/** 资源相关 */ -export { AssetLoader, IAssetConfig } from "./asset/AssetLoader"; -export { AssetPool } from "./asset/AssetPool"; - /** 条件显示节点 */ export { _conditionDecorator } from "./condition/ConditionDecorator"; export { ConditionManager } from "./condition/ConditionManager"; diff --git a/src/net/http/HttpManager.ts b/src/net/http/HttpManager.ts index ec2d582..20ef654 100644 --- a/src/net/http/HttpManager.ts +++ b/src/net/http/HttpManager.ts @@ -4,7 +4,6 @@ * @Description: 网络请求管理器 */ -import { GlobalEvent } from "../../global/GlobalEvent"; import { HttpRequest } from "./HttpRequest"; import { IHttpEvent } from "./IHttpEvent"; import { IHttpResponse } from "./IHttpResponse"; @@ -82,23 +81,15 @@ export class HttpManager { * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) * @internal */ - private static _send(method: HttpRequestMethod, url: string, data: any, responseType: HttpResponseType, netEvent?: IHttpEvent, headers?: any[], timeout?: number): HttpRequest { + private static _send(method: HttpRequestMethod, url: string, data: any, responseType: HttpResponseType, netEvent: IHttpEvent, headers?: any[], timeout?: number): HttpRequest { let http = new HttpRequest() http.setNetCallback((result: "succeed" | "fail", response: IHttpResponse) => { switch (result) { case "succeed": - if (netEvent) { - netEvent.onComplete(response); - } else { - GlobalEvent.send(HttpManager.HttpEvent, result, response); - } + netEvent?.onComplete(response); break; case "fail": - if (netEvent) { - netEvent.onError(response); - } else { - GlobalEvent.send(HttpManager.HttpEvent, result, response); - } + netEvent?.onError(response); break; } }); diff --git a/src/quadtree/Box.ts b/src/quadtree/Box.ts deleted file mode 100644 index 01b7fad..0000000 --- a/src/quadtree/Box.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2024-12-21 - * @Description: 矩形 - */ -import { v2, Vec2 } from "cc"; -import { ShapeType } from "./IShape"; -import { Polygon } from "./Polygon"; - -// 3|2 -// -- -// 0|1 -// 矩形的四个点 - -export class Box extends Polygon { - - public get shapeType(): ShapeType { - return ShapeType.BOX; - } - - constructor(x: number, y: number, width: number, height: number, tag: number = -1) { - let points: Vec2[] = new Array(4); - points[0] = v2(x, y); - points[1] = v2(x + width, y); - points[2] = v2(x + width, y + height); - points[3] = v2(x, y + height); - super(points, tag); - } - - public resetPoints(x: number, y: number, width: number, height: number): void { - let points: Vec2[] = new Array(4); - points[0] = v2(x, y); - points[1] = v2(x + width, y); - points[2] = v2(x + width, y + height); - points[3] = v2(x, y + height); - this.points = points; - } -} \ No newline at end of file diff --git a/src/quadtree/Circle.ts b/src/quadtree/Circle.ts deleted file mode 100644 index 9a65e1d..0000000 --- a/src/quadtree/Circle.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2024-12-21 - * @Description: 原型 - */ - -import { Rect } from "cc"; -import { ShapeType } from "./IShape"; -import { Shape } from "./Shape"; - -export class Circle extends Shape { - private _radius: number; // 半径 - - public get shapeType(): ShapeType { - return ShapeType.CIRCLE; - } - - constructor(radius: number, tag: number = -1) { - super(tag); - this._radius = radius; - this._boundingBox.x = -this._radius; - this._boundingBox.y = -this._radius; - this._boundingBox.width = this._radius * 2; - this._boundingBox.height = this._radius * 2; - } - - public getBoundingBox(): Rect { - return this._boundingBox; - } - - public get radius(): number { - return this._radius; - } - - public set radius(value: number) { - this._radius = value; - this._boundingBox.x = -this._radius; - this._boundingBox.y = -this._radius; - this._boundingBox.width = this._radius * 2; - this._boundingBox.height = this._radius * 2; - } -} \ No newline at end of file diff --git a/src/quadtree/IShape.ts b/src/quadtree/IShape.ts deleted file mode 100644 index aea3a13..0000000 --- a/src/quadtree/IShape.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2025-05-27 - * @Description: - */ - -import { Rect, Vec2 } from "cc"; - -export enum ShapeType { - CIRCLE = 1, - BOX = 2, - POLYGON = 3, -} - -export interface IShape { - /** 形状类型 */ - get shapeType(): ShapeType; - /** 形状掩码 @internal */ - get mask(): number; - /** 是否有效 @internal */ - - get isValid(): boolean; - get position(): Vec2; - get scale(): number; - get rotation(): number; - /** 获取包围盒 */ - getBoundingBox(): Rect; - setPosition(x: number, y: number): void; - setScale(value: number): void; - setRotation(angle: number): void; - destroy(): void; -} \ No newline at end of file diff --git a/src/quadtree/Polygon.ts b/src/quadtree/Polygon.ts deleted file mode 100644 index d3266d4..0000000 --- a/src/quadtree/Polygon.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2024-12-21 - * @Description: 多边形 - */ - -import { Rect, v2, Vec2 } from "cc"; -import { ShapeType } from "./IShape"; -import { Shape } from "./Shape"; - - -const vec2 = new Vec2(); -/** 点绕原点旋转 radians 弧度后的新点 */ -function rotate(radians: number, x: number, y: number): Vec2 { - let sin = Math.sin(radians); - let cos = Math.cos(radians); - vec2.x = x * cos - y * sin; - vec2.y = y * cos + x * sin; - return vec2; -} - -// /** 点绕点旋转 radians 弧度后的新点 */ -// export function rotateByPoint(radians: number, x: number, y: number, cx: number, cy: number): Vec2 { -// let sin = Math.sin(radians); -// let cos = Math.cos(radians); -// vec2.x = (x - cx) * cos - (y - cy) * sin + cx; -// vec2.y = (y - cy) * cos + (x - cx) * sin + cy; -// return vec2; -// } - -export class Polygon extends Shape { - protected _points: Vec2[] = []; // 多边形 - protected _realPoints: Vec2[]; - - public get shapeType(): ShapeType { - return ShapeType.POLYGON; - } - - constructor(points: Vec2[], tag: number = -1) { - super(tag); - this._points = points; - this._realPoints = new Array(points.length); - for (let i = 0, len = points.length; i < len; i++) { - this._realPoints[i] = v2(points[i].x, points[i].y); - } - this.getBoundingBox(); - } - - public getBoundingBox(): Rect { - if (this._isDirty) { - let minX = Number.MAX_VALUE; - let maxX = Number.MIN_VALUE; - let minY = Number.MAX_VALUE; - let maxY = Number.MIN_VALUE; - for (const point of this._points) { - let a = rotate(Math.PI / 180 * this._rotation, point.x, point.y); - minX = Math.min(minX, a.x); - minY = Math.min(minY, a.y); - maxX = Math.max(maxX, a.x); - maxY = Math.max(maxY, a.y); - } - this._boundingBox.x = minX; - this._boundingBox.y = minY; - this._boundingBox.width = maxX - minX; - this._boundingBox.height = maxY - minY; - this._isDirty = false; - } - return this._boundingBox; - } - - public get points(): Vec2[] { - let points = this._points; - let len = points.length; - for (let i = 0; i < len; i++) { - let m = points[i]; - this._realPoints[i] = m.rotate(Math.PI / 180 * this.rotation); - let a = this._realPoints[i]; - a.x = a.x * this.scale + this.position.x; - a.y = a.y * this.scale + this.position.y; - } - return this._realPoints; - } - - public set points(pts: Vec2[]) { - this._points = pts; - this._realPoints = new Array(pts.length); - for (let i = 0, len = pts.length; i < len; i++) { - this._realPoints[i] = v2(pts[i].x, pts[i].y); - } - this._isDirty = true; - } -} \ No newline at end of file diff --git a/src/quadtree/QuadTree.ts b/src/quadtree/QuadTree.ts deleted file mode 100644 index 777320c..0000000 --- a/src/quadtree/QuadTree.ts +++ /dev/null @@ -1,372 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2024-12-21 - * @Description: 树节点 - */ - -import { Graphics, Intersection2D, rect, Rect } from "cc"; -import { Circle } from "./Circle"; -import { IShape, ShapeType } from "./IShape"; -import { Polygon } from "./Polygon"; - -// 1|0 -// --- -// 2|3 -const enum Quadrant { - ONE = 0, - TWO, - THREE, - FOUR, - MORE, // 多个象限 -} - -const circleCircle = Intersection2D.circleCircle; -const polygonCircle = Intersection2D.polygonCircle; -const polygonPolygon = Intersection2D.polygonPolygon; -/** 两个形状是否碰撞 */ -function isCollide(shape1: IShape, shape2: IShape): boolean { - if (shape1.shapeType === ShapeType.CIRCLE) { - if (shape2.shapeType === ShapeType.CIRCLE) { - return circleCircle(shape1.position, (shape1 as Circle).radius * shape1.scale, shape2.position, (shape2 as Circle).radius * shape2.scale); - } else if (shape2.shapeType === ShapeType.BOX || shape2.shapeType === ShapeType.POLYGON) { - return polygonCircle((shape2 as Polygon).points, shape1.position, (shape1 as Circle).radius * shape1.scale); - } - } else if (shape1.shapeType === ShapeType.BOX || shape1.shapeType === ShapeType.POLYGON) { - if (shape2.shapeType === ShapeType.CIRCLE) { - return polygonCircle((shape1 as Polygon).points, shape2.position, (shape2 as Circle).radius * shape2.scale); - } else if (shape2.shapeType === ShapeType.BOX || shape2.shapeType === ShapeType.POLYGON) { - return polygonPolygon((shape2 as Polygon).points, (shape1 as Polygon).points); - } - } - return false; -} - -export const QTConfig = { - /** 每个节点(象限)所能包含物体的最大数量 */ - MAX_SHAPES: 12, - /** 四叉树的最大深度 */ - MAX_LEVELS: 5, -} - -export class QuadTree { - /** @internal */ - private _graphics: Graphics; - /** @internal */ - private _shapes_map: Map; // 根据类型存储形状对象 - /** @internal */ - private _trees: QuadTree[] = []; // 存储四个子节点 - /** @internal */ - private _level: number; // 树的深度 - /** @internal */ - private _bounds: Rect; // 树的外框 - /** @internal */ - private _ignore_shapes: IShape[] = []; // 不在树中的形状 - /** - * 创建一个四叉树 - * @param rect 该节点对应的象限在屏幕上的范围 - * @param level 该节点的深度,根节点的默认深度为0 - * @param graphics cc中用于绘制树的绘制组件 - */ - constructor(rect: Rect, level: number = 0, graphics: Graphics = undefined) { - this._shapes_map = new Map(); - this._trees = []; - this._level = level || 0; - this._bounds = rect; - this._graphics = graphics; - } - - /** - * 插入形状 - * @param shape 形状数据 - * 如果当前节点存在子节点,则检查物体到底属于哪个子节点,如果能匹配到子节点,则将该物体插入到该子节点中 - * 如果当前节点不存在子节点,将该物体存储在当前节点。随后,检查当前节点的存储数量,如果超过了最大存储数量,则对当前节点进行划分,划分完成后,将当前节点存储的物体重新分配到四个子节点中。 - */ - public insert(shape: IShape): void { - // 如果该节点下存在子节点 - if (this._trees.length > 0) { - let quadrant = this._getQuadrant(shape); - if (quadrant !== Quadrant.MORE) { - this._trees[quadrant].insert(shape); - return; - } - } - if (this._level == 0 && !this._isInner(shape, this._bounds)) { - // 插入跟节点并且形状不在根节点的框内,则把形状放入忽略列表中 - this._ignore_shapes.push(shape); - } else { - // 存储在当前节点下 - this._insert(shape); - // 如果当前节点存储的数量超过了 MAX_OBJECTS,并且深度没超过 MAX_LEVELS,则继续拆分 - if (!this._trees.length && this._size() > QTConfig.MAX_SHAPES && this._level < QTConfig.MAX_LEVELS) { - this._split(); - for (const shapes of this._shapes_map.values()) { - let length = shapes.length - 1; - for (let i = length; i >= 0; i--) { - let quadrant = this._getQuadrant(shapes[i]); - if (quadrant !== Quadrant.MORE) { - this._trees[quadrant].insert(shapes.splice(i, 1)[0]); - } - } - } - } - } - } - - /** @internal */ - private _insert(shape: IShape): void { - if (!this._shapes_map.has(shape.mask)) { - this._shapes_map.set(shape.mask, []); - } - this._shapes_map.get(shape.mask).push(shape); - } - - /** - * 检索功能: - * 给出一个物体对象,该函数负责将该物体可能发生碰撞的所有物体选取出来。该函数先查找物体所属的象限,该象限下的物体都是有可能发生碰撞的,然后再递归地查找子象限... - */ - public collide(shape: IShape, tag: number = -1): IShape[] { - let result: IShape[] = []; - if (this._trees.length > 0) { - let quadrant = this._getQuadrant(shape); - if (quadrant === Quadrant.MORE) { - let len = this._trees.length - 1; - for (let i = len; i >= 0; i--) { - result.push(...this._trees[i].collide(shape, tag)); - } - } else { - result.push(...this._trees[quadrant].collide(shape, tag)); - } - } - - for (const key of this._shapes_map.keys()) { - if (!(tag & key)) { - continue; - } - let shapes = this._shapes_map.get(key); - for (const other_shape of shapes) { - if (other_shape.isValid && shape !== other_shape && isCollide(shape, other_shape)) { - result.push(other_shape); - } - } - } - return result; - } - - /** - * 动态更新 - */ - public update(root?: QuadTree): void { - root = root || this; - let isRoot = (root === this); - isRoot && this.graphicsClear(); - this._updateIgnoreShapes(root); - this._updateShapes(root); - // 递归刷新子象限 - for (const tree of this._trees) { - tree.update(root); - } - this._removeChildTree(); - this.graphicsTreeBound(root); - - if (isRoot && this._graphics) { - this._graphics.stroke(); - } - } - - public clear(): void { - this._level = 0; - this._ignore_shapes.length = 0; - this._shapes_map.clear(); - for (const tree of this._trees) { - tree.clear(); - } - this._trees.length = 0; - } - - /** 当前形状是否包含在象限内 @internal */ - private _isInner(shape: IShape, bounds: Rect): boolean { - let rect = shape.getBoundingBox(); - return ( - rect.xMin * shape.scale + shape.position.x > bounds.xMin && - rect.xMax * shape.scale + shape.position.x < bounds.xMax && - rect.yMin * shape.scale + shape.position.y > bounds.yMin && - rect.yMax * shape.scale + shape.position.y < bounds.yMax - ); - } - - /** - * 获取形状对应的象限序号,以中心为界限切割: - * @param {Shape} shape 形状 - * 右上:象限一 - * 左上:象限二 - * 左下:象限三 - * 右下:象限四 - * @internal - */ - private _getQuadrant(shape: IShape): Quadrant { - let bounds = this._bounds; - let rect = shape.getBoundingBox(); - let center = bounds.center; - - let onTop = rect.yMin * shape.scale + shape.position.y > center.y; - let onBottom = rect.yMax * shape.scale + shape.position.y < center.y; - let onLeft = rect.xMax * shape.scale + shape.position.x < center.x; - let onRight = rect.xMin * shape.scale + shape.position.x > center.x; - if (onTop) { - if (onRight) { - return Quadrant.ONE; - } else if (onLeft) { - return Quadrant.TWO; - } - } else if (onBottom) { - if (onLeft) { - return Quadrant.THREE; - } else if (onRight) { - return Quadrant.FOUR; - } - } - return Quadrant.MORE; // 跨越多个象限 - } - - /** - * 划分函数 - * 如果某一个象限(节点)内存储的物体数量超过了MAX_OBJECTS最大数量 - * 则需要对这个节点进行划分 - * 它的工作就是将一个象限看作一个屏幕,将其划分为四个子象限 - * @internal - */ - private _split(): void { - let bounds = this._bounds; - let x = bounds.x; - let y = bounds.y; - let halfwidth = bounds.width * 0.5; - let halfheight = bounds.height * 0.5; - let nextLevel = this._level + 1; - this._trees.push( - new QuadTree(rect(bounds.center.x, bounds.center.y, halfwidth, halfheight), nextLevel, this._graphics), - new QuadTree(rect(x, bounds.center.y, halfwidth, halfheight), nextLevel, this._graphics), - new QuadTree(rect(x, y, halfwidth, halfheight), nextLevel, this._graphics), - new QuadTree(rect(bounds.center.x, y, halfwidth, halfheight), nextLevel, this._graphics) - ); - } - - /** 删除子树 @internal */ - private _removeChildTree(): void { - if (this._trees.length > 0) { - if (this._totalSize() <= 0) { - this._trees.length = 0; - } - } - } - - /** 更新忽略掉的形状 @internal */ - private _updateIgnoreShapes(root: QuadTree): void { - let shapes = this._ignore_shapes; - let lastIndex = shapes.length - 1; - let index = 0; - while (index < lastIndex) { - let shape = shapes[index]; - if (!shape.isValid) { - if (index !== lastIndex) { - shapes[index] = shapes[lastIndex]; - } - shapes.pop(); - lastIndex--; - continue; - } - if (this._isInner(shape, this._bounds)) { - if (index !== lastIndex) { - [shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]]; - } - root.insert(shapes.pop()); - } else { - index++; - } - } - } - - /** 更新有效的形状 @internal */ - private _updateShapes(root: QuadTree): void { - for (const shapes of this._shapes_map.values()) { - let lastIndex = shapes.length - 1; - let index = 0; - while (index <= lastIndex) { - let shape = shapes[index]; - if (!shape.isValid) { - if (index !== lastIndex) { - shapes[index] = shapes[lastIndex]; - } - shapes.pop(); - lastIndex--; - continue; - } - if (!this._isInner(shape, this._bounds)) { - // 如果矩形不属于该象限,则将该矩形重新插入根节点 - if (index !== lastIndex) { - [shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]]; - } - root.insert(shapes.pop()); - lastIndex--; - } else if (this._trees.length > 0) { - // 如果矩形属于该象限且该象限具有子象限,则将该矩形安插到子象限中 - let quadrant = this._getQuadrant(shape); - if (quadrant !== Quadrant.MORE) { - if (index !== lastIndex) { - [shapes[index], shapes[lastIndex]] = [shapes[lastIndex], shapes[index]]; - } - this._trees[quadrant].insert(shapes.pop()); - lastIndex--; - } else { - index++; - } - } else { - index++; - } - } - } - } - - /** 当前树以及子树中所有的形状数量 @internal */ - private _totalSize(): number { - let size = this._size(); - for (const tree of this._trees) { - size += tree._totalSize(); - } - return size; - } - - /** 当前树中所有的形状数量 @internal */ - private _size(): number { - let size = 0; - for (const shapes of this._shapes_map.values()) { - size += shapes.length; - } - return size + this._ignore_shapes.length; - } - - /** 画出当前树的边界 @internal */ - private graphicsTreeBound(root: QuadTree): void { - if (!this._graphics) { - return; - } - if (this._trees.length > 0) { - this._graphics.moveTo(this._bounds.x, this._bounds.center.y); - this._graphics.lineTo(this._bounds.x + this._bounds.width, this._bounds.center.y); - - this._graphics.moveTo(this._bounds.center.x, this._bounds.y); - this._graphics.lineTo(this._bounds.center.x, this._bounds.y + this._bounds.height); - } - if (this == root) { - this._graphics.moveTo(this._bounds.xMin, this._bounds.yMin); - this._graphics.lineTo(this._bounds.xMax, this._bounds.yMin); - this._graphics.lineTo(this._bounds.xMax, this._bounds.yMax); - this._graphics.lineTo(this._bounds.xMin, this._bounds.yMax); - this._graphics.lineTo(this._bounds.xMin, this._bounds.yMin); - } - } - - /** 清除绘制 @internal */ - private graphicsClear(): void { - this._graphics && this._graphics.clear(); - } -} \ No newline at end of file diff --git a/src/quadtree/Shape.ts b/src/quadtree/Shape.ts deleted file mode 100644 index 94d9864..0000000 --- a/src/quadtree/Shape.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * @Author: Gongxh - * @Date: 2024-12-21 - * @Description: 四叉树的 形状基类 - */ - -import { Rect, Vec2 } from "cc"; -import { IShape, ShapeType } from "./IShape"; - -export abstract class Shape implements IShape { - /** - * 形状的掩码 用来过滤不需要检测的形状 通过&来匹配形状是否需要被检测 -1表示和所有物体碰撞 - */ - private _mask: number = -1; - protected _scale: number; - - /** 脏标记 用来重置包围盒 @internal */ - protected _isDirty: boolean; - - /** 包围盒 @internal */ - protected _boundingBox: Rect; - - /** 位置 @internal */ - protected _position: Vec2; - - /** 旋转角度 @internal */ - protected _rotation: number; - - /** 是否有效 下次更新时删除 @internal */ - private _valid: boolean = true; - - public abstract get shapeType(): ShapeType; - - public get mask(): number { return this._mask; } - public get position(): Vec2 { return this._position; } - public get scale(): number { return this._scale; } - public get rotation(): number { return this._rotation; } - public get isValid(): boolean { return this._valid; } - - constructor(mask: number) { - this._mask = mask; - this._scale = 1.0; - this._rotation = 0; - this._isDirty = true; - this._boundingBox = new Rect(); - this._position = new Vec2(); - } - - public setPosition(x: number, y: number) { - this._position.x = x; - this._position.y = y; - } - - public setRotation(angle: number) { - if (this._rotation !== angle) { - this._rotation = angle; - this._isDirty = true; - } - } - - public setScale(value: number) { - if (this._scale !== value) { - this._scale = value; - this._isDirty = true; - } - } - - /** 包围盒 子类重写 */ - public abstract getBoundingBox(): Rect; - - - public destroy(): void { - this._valid = false; - } -} \ No newline at end of file