commit 68090ca38d0e2b054c3e12e2542fc062bb704adb Author: 宫欣海 Date: Thu Feb 20 11:27:28 2025 +0800 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..296d188 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# 文件 +.DS_Store +package-lock.json + +# 文件夹 +.vscode/ +node_modules/ +dist/ +build/ \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..061267b --- /dev/null +++ b/.npmignore @@ -0,0 +1,5 @@ +# 文件夹 +node_modules/ +libs/ +build/ +src/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..626580d --- /dev/null +++ b/README.md @@ -0,0 +1,1502 @@ +# KunpoLib + +基于 Cocos Creator 3.0+ 的游戏开发工具库,提供了一系列实用的功能模块,帮助开发者快速构建高质量的游戏项目。 + +项目持续优化中,敬请期待~~ + +## 特性 + +- **UI 系统** + - 基于 FairyGUI 的窗口管理系统 + - 灵活的 UI 装饰器 (装饰器需配合 creator插件 kunpo-fgui使用) + - 窗口分组管理 + - 窗口顶部资源栏(Header)管理 +- **全局工具** + - 全局事件系统 + - 全局计时器 + - 平台适配工具 + - 调试模式支持 +- **网络模块** + - HTTP 请求管理 + - 任务队列 + - 完整的请求响应接口 +- **数据结构** + - 四叉树(支持碰撞检测) + - 矩形碰撞 + - 圆形碰撞 + - 多边形碰撞 +- **行为树系统** + - 完整的行为树实现 + - 支持多种节点类型 + - Action(动作节点) + - Composite(组合节点) + - Condition(条件节点) + - Decorator(装饰节点) + - 黑板数据共享 + - Ticker 系统 +- **EC 框架** + - 实体组件系统 + - 组件池优化 + - 实体管理器 +- **资源管理** + * 资源加载 + * 资源池 +- 条件显示节点系统 (用来处理游戏中的提示红点信息) + +## 安装kunpocc + +```bash +npm install kunpocc +``` + +## 项目基础配置 + +1. 新建项目,创建首场景文件 + +2. 编写入口脚本 + + ```typescript + import { _decorator } from "cc"; + /** 引入kunpocc入口 */ + import { CocosEntry, log } from "kunpocc"; + + const { ccclass, property, menu } = _decorator; + @ccclass("GameEntry") + @menu("kunpo/GameEntry") + export class GameEntry extends CocosEntry { + @property(cc.Node) + private root: cc.Node = null; + onInit(): void { + log("GameEntry onInit"); + } + } + ``` + +3. 创建入口节点`GameEntry`,创建UI模块节点 `UI` 、UI容器节点`window`, 并关联对应的脚本 + + ![image-20250213102309047](https://gitee.com/gongxinhai/public-image/raw/master/image-20250213102309047.png) + +4. 配置完毕 + + + +## 使用方法 + +### 一、UI 系统使用 + +> 注:UI制作需要使用 FairyGUI,[FairyGUI官方文档](https://www.fairygui.com/docs/editor) + +> ![image-20250213105921142](https://gitee.com/gongxinhai/public-image/raw/master/image-20250213105921142.png) + +#### *UI 装饰器使用* + +> 注:只有使用了装饰器的内容才能在 `kunpo-fgui` 插件中识别,`kunpo-fgui`插件操作界面如下图 + +> ![image-20250213110353385](https://gitee.com/gongxinhai/public-image/raw/master/image-20250213110353385.png) + + + +1. 窗口装饰器 + + ```typescript + import { Window, _uidecorator } from 'kunpocc'; + const { uiclass, uiprop, uiclick } = _uidecorator; + + /** + * 窗口装饰器 + * @param 参数1: 窗口容器节点名字 + * @param 参数2: FairyGUI中的UI包名 + * @param 参数3: FairyGUI中的组件名 必须和 class 类同名 这里是 MyWindow + */ + @uiclass("Window", "UI包名", "MyWindow") + export class MyWindow extends Window { + // ... 窗口实现 + } + ``` + +2. 窗口 Header 装饰器 + + ```typescript + import { WindowHeader, _uidecorator } from 'kunpocc'; + const { uiheader } = _uidecorator; + + /** + * 窗口顶部资源栏装饰器 + * @param 参数1: FairyGUI中的UI包名 + * @param 参数2: FairyGUI中的组件名 必须和 class 类同名 这里是 MyWindowHeader + */ + @uiheader("UI包名", "WindowHeader") + export class MyWindowHeader extends WindowHeader { + // ... Header 实现 + } + ``` + +3. UI组件装饰器 + + ```typescript + import { _uidecorator } from 'kunpocc'; + const { uicom, uiprop, uiclick } = _uidecorator; + + /** + * UI组件类装饰器 + * @param 参数1: FairyGUI中的UI包名 + * @param 参数2: FairyGUI中的组件名 必须和 class 类同名 这里是 MyComponent + */ + @uicom("Home", "MyComponent") + export class MyComponent { + // ... 组件实现 + } + ``` + +4. UI属性装饰器 + + ```typescript + import { Window, _uidecorator } from 'kunpocc'; + const { uiclass, uiprop, uiclick } = _uidecorator; + + @uiclass("Window", "Home", "MyWindow") + export class MyWindow extends Window { + // FairyGUI 组件属性装饰器 + @uiprop private btnConfirm: GButton; // 按钮组件 + @uiprop private txtTitle: GTextField; // 文本组件 + @uiprop private listItems: GList; // 列表组件 + } + ``` + +5. 点击事件装饰器 + + ```typescript + import { Window, _uidecorator } from 'kunpocc'; + const { uiclass, uiprop, uiclick } = _uidecorator; + + @uiclass("Window", "Home", "MyWindow") + export class MyWindow extends Window { + // 点击事件装饰器 + @uiclick + private onTouchEvent(event: cc.Event): void { + console.log('确认按钮被点击'); + } + } + ``` + + +#### *创建窗口* + +1. 创建窗口类 + + ```typescript + /** + * 窗口名必须和FairyGUI中的组件同名 + */ + import { Window, _uidecorator } from 'kunpocc'; + const { uiclass, uiprop, uiclick } = _uidecorator; + + @uiclass("Window", "UI包名", "MyWindow") + export class MyWindow extends Window { + protected onInit(): void { + // 初始化窗口 + } + + protected onShow(userdata?: any): void { + // 窗口显示时的逻辑 + } + + protected onClose(): void { + // 窗口关闭时的逻辑 + } + } + ``` + +2. 窗口管理 + + * 重要:显示窗口前必须确保窗口资源已加载 !!! + + ```typescript + // 显示窗口 + WindowManager.showWindow("MyWindow", { /* 用户数据 */ }); + + // 关闭窗口 + WindowManager.closeWindow("MyWindow"); + + // 获取窗口实例 + const window = WindowManager.getWindow("MyWindow"); + + // 获取当前最顶层窗口 + const topWindow = WindowManager.getTopWindow(); + + // 检查窗口是否存在 + const exists = WindowManager.hasWindow("MyWindow"); + ``` + +3. 窗口生命周期 +- `onInit`: 窗口初始化时调用 +- `onShow`: 窗口显示时调用 +- `onClose`: 窗口关闭时调用 +- `onHide`: 窗口隐藏时调用 +- `onShowFromHide`: 窗口从隐藏状态恢复时调用 +- `onCover`: 窗口被覆盖时调用 +- `onRecover`: 窗口恢复时调用 +- `onEmptyAreaClick`: 点击窗口空白区域时调用 + + + +### 二、 全局事件系统 + +```typescript +import { GlobalEvent } from 'kunpocc'; + +// 添加事件监听 +GlobalEvent.add('eventName', (arg1, arg2) => { + console.log('事件触发:', arg1, arg2); +}, this); + +// 添加一次性事件监听 +GlobalEvent.addOnce('oneTimeEvent', (data) => { + console.log('一次性事件触发:', data); +}, this); + +// 发送事件 +GlobalEvent.send('eventName', 'arg1', 'arg2'); + +// 发送事件到指定目标 +GlobalEvent.sendToTarget('eventName', target, 'arg1', 'arg2'); + +// 移除事件监听 +GlobalEvent.remove('eventName', callback, this); + +// 移除指定目标的所有事件监听 +GlobalEvent.removeByTarget(this); + +// 移除指定事件名和目标的事件监听 +GlobalEvent.removeByNameAndTarget('eventName', this); +``` + + + +### 三、全局计时器 + +```typescript +import { GlobalTimer } from 'kunpocc'; + +// 启动一次性定时器(2秒后执行) +const timerId1 = GlobalTimer.startTimer(() => { + console.log('2秒后执行一次'); +}, 2); + +// 启动循环定时器(每3秒执行一次,执行5次) +const timerId2 = GlobalTimer.startTimer(() => { + console.log('每3秒执行一次,总共执行5次'); +}, 3, 5); + +// 启动无限循环定时器(每1秒执行一次) +const timerId3 = GlobalTimer.startTimer(() => { + console.log('每1秒执行一次,无限循环'); +}, 1, -1); + +// 停止定时器 +GlobalTimer.stopTimer(timerId1); +GlobalTimer.stopTimer(timerId2); +GlobalTimer.stopTimer(timerId3); +``` + +注意事项: +- 定时器的时间间隔单位为秒 +- loop 参数说明: + - 0:执行一次 + - 正整数 n:执行 n 次 + - -1:无限循环 + +### 四、 网络模块 + +#### *HTTP 请求* + +```typescript +import { HttpManager, IHttpEvent, HttpResponseType } from 'kunpocc'; + +// 1. 使用回调方式处理响应 +const event: IHttpEvent = { + name: "login", + onComplete: (response) => { + console.log('请求成功:', response.data); + }, + onError: (response) => { + console.log('请求失败:', response.error); + } +}; + +// POST 请求 +HttpManager.post( + "https://api.example.com/login", + { username: "test", password: "123456" }, + "json", // 响应类型:'json' | 'text' | 'arraybuffer' + event, + ["Content-Type", "application/json"], // 请求头 + 5 // 超时时间(秒) +); + +// GET 请求 +HttpManager.get( + "https://api.example.com/users", + { id: 1 }, + "json", + event +); + +// 2. 使用全局事件方式处理响应 +GlobalEvent.add(HttpManager.HttpEvent, (result, response) => { + if (result === "succeed") { + console.log('请求成功:', response.data); + } else { + console.log('请求失败:', response.error); + } +}, this); + +// 发送请求(不传入 event 参数) +HttpManager.post("https://api.example.com/data", { /* data */ }); +``` + +#### *请求方法* +- `post(url, data, responseType?, event?, headers?, timeout?)` +- `get(url, data, responseType?, event?, headers?, timeout?)` +- `put(url, data, responseType?, event?, headers?, timeout?)` +- `head(url, data, responseType?, event?, headers?, timeout?)` + +#### *参数说明* +- `url`: 请求地址 +- `data`: 请求数据 +- `responseType`: 响应类型(可选,默认 'json') + - `'json'`: JSON 格式 + - `'text'`: 文本格式 + - `'arraybuffer'`: 二进制数据 +- `event`: 请求事件回调(可选) +- `headers`: 请求头(可选) +- `timeout`: 超时时间,单位秒(可选,0表示不超时) + +#### *响应处理* +1. 回调方式(通过 IHttpEvent): +```typescript +const event: IHttpEvent = { + name: "自定义名称", + data?: "自定义数据", // 可选 + onComplete: (response) => { + // 成功回调 + }, + onError: (response) => { + // 失败回调 + } +}; +``` + +2. 全局事件方式: +```typescript +GlobalEvent.add(HttpManager.HttpEvent, (result, response) => { + // result: "succeed" | "fail" + // response: IHttpResponse +}, this); +``` + +### 五、行为树系统 + +> 行为树是一个强大的 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. 性能优化: + - 使用黑板共享数据,避免重复计算 + - 合理使用记忆节点,减少重复执行 + - 控制行为树的深度,避免过于复杂 + + + +### 六、平台相关 + +> Platform 类提供了游戏运行平台的相关信息和判断方法。 + +#### *平台类型* + +```typescript +import { Platform, PlatformType } from 'kunpocc'; + +// 平台类型枚举 +enum PlatformType { + Android = 1, // 安卓 + IOS, // iOS + HarmonyOS, // 鸿蒙 + WX, // 微信小游戏 + Alipay, // 支付宝小游戏 + Bytedance, // 字节小游戏 + HuaweiQuick, // 华为快游戏 + Browser // 浏览器 +} + +// 获取当前平台类型 +const currentPlatform = Platform.platform; +``` + +#### *平台判断* + +```typescript +import { Platform } from 'kunpocc'; + +// 原生平台判断 +if (Platform.isNative) { + console.log('当前是原生平台'); +} + +// 移动平台判断 +if (Platform.isMobile) { + console.log('当前是移动平台'); +} + +// 原生移动平台判断 +if (Platform.isNativeMobile) { + console.log('当前是原生移动平台'); +} + +// 具体平台判断 +if (Platform.isAndroid) { + console.log('当前是安卓平台'); +} + +if (Platform.isIOS) { + console.log('当前是iOS平台'); +} + +if (Platform.isHarmonyOS) { + console.log('当前是鸿蒙系统'); +} + +// 小游戏平台判断 +if (Platform.isWX) { + console.log('当前是微信小游戏'); +} + +if (Platform.isAlipay) { + console.log('当前是支付宝小游戏'); +} + +if (Platform.isBytedance) { + console.log('当前是字节小游戏'); +} + +if (Platform.isHuaweiQuick) { + console.log('当前是华为快游戏'); +} + +// 浏览器判断 +if (Platform.isBrowser) { + console.log('当前是浏览器环境'); +} +``` + +#### *使用示例* + +```typescript +import { Platform, PlatformType } from 'kunpocc'; + +// 根据平台类型执行不同逻辑 +switch (Platform.platform) { + case PlatformType.Android: + // 安卓平台特定逻辑 + break; + case PlatformType.IOS: + // iOS平台特定逻辑 + break; + case PlatformType.WX: + // 微信小游戏特定逻辑 + break; + default: + // 其他平台逻辑 + break; +} + +// 针对不同平台进行适配 +if (Platform.isNativeMobile) { + // 原生移动平台的处理 + if (Platform.isAndroid) { + // 安卓特有功能 + } else if (Platform.isIOS) { + // iOS特有功能 + } +} else if (Platform.isWX || Platform.isAlipay || Platform.isBytedance) { + // 小游戏平台的处理 +} else { + // 浏览器平台的处理 +} +``` + +### 七、四叉树碰撞检测 + +> 四叉树是一种用于高效进行空间划分和碰撞检测的数据结构。 + +#### *基本概念* + +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. 更新策略: + - 仅在必要时更新四叉树 + - 对于静态物体,可以使用单独的四叉树 + - 动态物体频繁更新时,考虑使用更大的边界范围 + +### 八、工具类 + +#### *数学工具 (MathTool)* + +```typescript +import { MathTool } from 'kunpocc'; + +// 1. 数值限制 +// 将数值限制在指定范围内 +const value = MathTool.clampf(75, 0, 100); // 返回75,因为在0-100范围内 +const value2 = MathTool.clampf(150, 0, 100); // 返回100,因为超出上限 +const value3 = MathTool.clampf(-50, 0, 100); // 返回0,因为低于下限 + +// 2. 随机数生成 +// 生成指定范围内的整数(包含边界值) +const randomInt = MathTool.rand(1, 10); // 返回1到10之间的整数 + +// 生成指定范围内的浮点数(包含最小值,不包含最大值) +const randomFloat = MathTool.randRange(0, 1); // 返回0到1之间的浮点数 + +// 3. 角度与弧度转换 +// 角度转弧度 +const radian = MathTool.rad(90); // 90度转换为弧度:约1.57 + +// 弧度转角度 +const degree = MathTool.deg(Math.PI); // π弧度转换为角度:180 + +// 4. 平滑过渡 +// 用于实现数值的平滑变化,常用于相机跟随、UI动画等 +const smoothValue = MathTool.smooth( + 0, // 起始值 + 100, // 目标值 + 0.16, // 已经过时间(秒) + 0.3 // 响应时间(秒) +); // 返回一个平滑过渡的中间值 +``` + +使用说明: + +1. `clampf(value: number, min: number, max: number): number` + - 将数值限制在指定范围内 + - 如果小于最小值,返回最小值 + - 如果大于最大值,返回最大值 + - 否则返回原值 + +2. `rand(min: number, max: number): number` + - 生成指定范围内的随机整数 + - 包含最小值和最大值 + - 常用于随机选择、随机掉落等场景 + +3. `randRange(min: number, max: number): number` + - 生成指定范围内的随机浮点数 + - 包含最小值,不包含最大值 + - 常用于需要精确浮点随机数的场景 + +4. `rad(angle: number): number` + - 将角度转换为弧度 + - 计算公式:angle * Math.PI / 180 + +5. `deg(radian: number): number` + - 将弧度转换为角度 + - 计算公式:radian * 180 / Math.PI + +6. `smooth(current: number, target: number, elapsedTime: number, responseTime: number): number` + - 计算平滑过渡的值 + - current: 当前值 + - target: 目标值 + - elapsedTime: 已经过时间(秒) + - responseTime: 响应时间(秒) + - 常用于实现平滑的相机移动、UI动画等 + +#### *MD5 加密* + +```typescript +import { md5 } from 'kunpocc'; + +// 字符串 MD5 加密 +const hash = md5('Hello, World!'); +console.log(hash); // 输出32位MD5哈希值 + +// 注意: +// 1. 输入必须是字符串类型 +// 2. 不能传入 undefined 或 null +try { + md5(null); // 将抛出错误 +} catch (error) { + console.error('MD5输入不能为null或undefined'); +} +``` + +### 九、实体组件系统(EC) + +* 不使用creator官方的挂载脚本的方式,有以下几个原因 + > 1. node挂脚本的方式效率低 + > 1. 支持多世界,方便管理 + > 3. 通过装饰器注册属性给creator插件 kunpo-ec 使用 + > 4. 组件分数据组件和逻辑组件,只更新逻辑组件 + +* 实体组件系统是一种用于游戏开发的架构模式,它将游戏对象(实体)的数据(组件)和行为分离。 + +#### *creator插件`kunpo-ec`* + +> `kunpo-cc`可以方便创建、配置、导出实体,操作界面如下图: + +![image-20250213111622050](https://gitee.com/gongxinhai/public-image/raw/master/image-20250213111622050.png) + + + +#### *重点接口说明* + +注:详细说明查看声明文件 `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 + ``` + + + +#### *使用* + +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. 编写组件脚本 + + * 在组件的 onAdd 方法中,设置组件是否更新,只有需要更新的组件才需要设置 + + ```typescript + protected onAdd(): void { + this.needUpdate = true; + } + ``` + + + + 组件完整示例内容如下: + + ```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); + } + } + ``` + + + +### 十、资源加载工具 + +#### *资源加载器* + +> 注意:资源就算加载多次和一次效果一样 + +```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 async releaseDir(dir: string, bundlename: string = "resources", asset: typeof Asset): Promise + +/** 按 uuid 释放资源 */ +public static releaseUUID(uuid: string): void + +/** 释放所有加载的资源 */ +public static releaseAll(): void +``` + +### 十、条件显示节点系统 + + + +## 类型支持 + +该库完全使用 TypeScript 编写,提供完整的类型定义文件。 + +## 许可证 + +ISC License + +## 作者 + +gongxh + +## 联系作者 + +* 邮箱: gong.xinhai@163.com + +## 仓库 + +[github demo地址](https://github.com/Gongxh0901/KunpoDemo) + +[gitee demo地址](https://gitee.com/gongxinhai/kunpo-demo) diff --git a/package.json b/package.json new file mode 100644 index 0000000..c02d949 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "kunpocc", + "version": "1.0.18", + "type": "module", + "description": "基于creator3.0+的kunpocc库", + "main": "./dist/kunpocc.cjs", + "module": "./dist/kunpocc.mjs", + "types": "./dist/kunpocc.d.ts", + "exports": { + ".": { + "require": "./dist/kunpocc.cjs", + "import": "./dist/kunpocc.mjs", + "types": "./dist/kunpocc.d.ts", + "default": "./dist/kunpocc.cjs" + }, + "./min": { + "require": "./dist/kunpocc.min.cjs", + "import": "./dist/kunpocc.min.mjs" + } + }, + "scripts": { + "clean": "rm -rf dist", + "build": "npm run clean && rollup -c rollup.config.mjs", + "copy": "cp -r dist/* /Users/gongxh/work/kunpo-lib/KunpoDemo/node_modules/kunpocc/dist/", + "build:all": "npm run build && npm run copy" + }, + "files": [ + "dist/kunpocc.cjs", + "dist/kunpocc.mjs", + "dist/kunpocc.min.cjs", + "dist/kunpocc.min.mjs", + "dist/kunpocc.d.ts" + ], + "author": "gongxh", + "license": "ISC", + "repository": { + "type": "gitlab", + "url": "https://git.lanfeitech.com/gongxinhai/kunpolibrary/-/tree/main/KunpoLib" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "dependencies": { + "fairygui-cc": "^1.2.2" + }, + "devDependencies": { + "@cocos/creator-types": "^3.8.0", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-typescript": "^12.1.2", + "@types/lodash": "^4.17.13", + "@types/node": "^22.10.2", + "rollup": "^4.28.1", + "rollup-plugin-dts": "^6.1.1", + "ts-node": "^10.9.2", + "tslib": "^2.6.2" + } +} diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..c6554ab --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,62 @@ +import terser from '@rollup/plugin-terser'; +import typescript from '@rollup/plugin-typescript'; +import dts from 'rollup-plugin-dts'; + +export default [ + { + // 生成未压缩的 JS 文件 + input: 'src/kunpocc.ts', + external: ['cc', 'fairygui-cc'], + output: [ + { + file: 'dist/kunpocc.mjs', + format: 'esm', + name: 'kunpocc' + }, + { + file: 'dist/kunpocc.cjs', + format: 'cjs', + name: 'kunpocc' + } + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + importHelpers: false + }) + ] + }, + { + // 生成压缩的 JS 文件 + input: 'src/kunpocc.ts', + external: ['cc', 'fairygui-cc'], + output: [ + { + file: 'dist/kunpocc.min.mjs', + format: 'esm', + name: 'kunpocc' + }, + { + file: 'dist/kunpocc.min.cjs', + format: 'cjs', + name: 'kunpocc' + } + ], + plugins: [ + typescript({ + tsconfig: './tsconfig.json', + importHelpers: false + }), + terser() + ] + }, + { + // 生成声明文件的配置 + input: 'src/kunpocc.ts', + output: { + file: 'dist/kunpocc.d.ts', + format: 'es' + }, + plugins: [dts()] + } +]; \ No newline at end of file diff --git a/src/asset/AssetLoader.ts b/src/asset/AssetLoader.ts new file mode 100644 index 0000000..446c5b7 --- /dev/null +++ b/src/asset/AssetLoader.ts @@ -0,0 +1,251 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-11 + * @Description: 资源加载器 + */ + +import { Asset, AssetManager, resources } from "cc"; +import { log } from "../tool/log"; +import { MathTool } from "../tool/Math"; +import { AssetPool } from "./AssetPool"; +import { AssetUtils } from "./AssetUtils"; + +export interface IAssetConfig { + /** 资源类型 */ + type: typeof Asset; + /** 资源路径 */ + path: string; + /** 是否是单个文件 默认是文件夹 */ + isFile?: boolean; + /** 资源包名 默认 resources */ + bundle?: string; +} + +/** 资源加载的状态类型 */ +enum StateType { + Error, + Wait, + Loading, + Finish, +} + +export class AssetLoader { + /** 资源加载器名称 */ + private _name: string = ""; + /** 资源总数 */ + private _total: number = 0; + /** 最大并行加载数量 */ + private _maxParallel: number = 10; + /** 当前并行加载数量 */ + private _parallel: number = 0; + /** 失败重试次数 */ + private _maxRetry: number = 3; + /** 失败重试次数 */ + private _retry: number = 0; + + /** 获取资源数量是否成功 */ + private _initSuccess: boolean = false; + + private _progress: (percent: number) => void; + + private _complete: () => void; + private _fail: (msg: string, err: Error) => void; + + private _configs: IAssetConfig[] = []; + private _items: { type: typeof Asset, bundle: string, path: string, isFile?: boolean, status: StateType, count: number }[] = []; + /** load完成数量 */ + private _completeCounts: Map = new Map(); + constructor(name?: string) { + this._name = name || "AssetLoader"; + } + + /** + * 开始加载资源 + * @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(); + } + } + + /** 重试开始 */ + 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 + }); + } + + /** 重试加载资源 */ + 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(); + } + } + + /** 初始化成功后,开始批量加载资源 */ + 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(); + } + } + + /** 加载下一个资源 */ + 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(); + } + } + + /** 重置失败资源状态为等待中 */ + private resetErrorItem(): number { + let count = 0; + for (const item of this._items) { + if (item.status == StateType.Error) { + item.status = StateType.Wait; + count++; + } + } + return count; + } + + private async loadItem(index: number): Promise { + let item = this._items[index]; + item.status = StateType.Loading; + this._parallel++; + + let bundle = null; + if (item.bundle == "resources") { + bundle = resources; + } else { + bundle = await AssetUtils.loadBundle(item.bundle); + } + if (item.isFile) { + this.loadFile(index, bundle); + } else { + this.loadDir(index, bundle); + } + } + + 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._progress && this.updateProgress(); + this.loadNext(); + }); + } + + 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._progress && this.updateProgress(); + this.loadNext(); + }); + } + + /** 更新进度 */ + 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 new file mode 100644 index 0000000..2395bc4 --- /dev/null +++ b/src/asset/AssetPool.ts @@ -0,0 +1,118 @@ +/** + * @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 { + private static _assets: { [path: string]: Asset } = {}; + private static _uuidToName: Map = new Map(); + + /** 批量添加资源 */ + public static add(asset: Asset[] | Asset, bundle: AssetManager.Bundle = resources): void { + if (Array.isArray(asset)) { + for (const item of asset) { + this.add(item, bundle); + } + } else { + let 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; + } + } + + 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; + } + + /** 按资源路径释放资源 */ + public static releasePath(path: string, bundlename: string = "resources"): void { + let key = this.getKey(path, bundlename); + this.release(key); + } + + /** 按 bundle 和 文件夹释放资源 */ + public static async releaseDir(dir: string, bundlename: string = "resources", asset: typeof Asset): Promise { + let bundle = null; + if (bundlename == "resources") { + bundle = resources; + } else { + bundle = await AssetUtils.loadBundle(bundlename); + } + let uuids = AssetUtils.getUUIDs(dir, asset, bundle); + for (const uuid of uuids) { + this.releaseUUID(uuid); + } + } + + /** 按 uuid 释放资源 */ + public static releaseUUID(uuid: string): void { + if (this._uuidToName.has(uuid)) { + let key = this._uuidToName.get(uuid); + this.release(key); + } + } + + 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]; + } + } + + /** 释放所有加载的资源 */ + public static releaseAll(): void { + for (const key in this._assets) { + this._assets[key].decRef(); + } + this._assets = {}; + this._uuidToName.clear(); + } + + private static getKey(path: string, bundlename: string = "resources"): string { + return `${bundlename}:${path}`; + } +} diff --git a/src/asset/AssetUtils.ts b/src/asset/AssetUtils.ts new file mode 100644 index 0000000..4eb705a --- /dev/null +++ b/src/asset/AssetUtils.ts @@ -0,0 +1,52 @@ +/** + * @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 async 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 new file mode 100644 index 0000000..1eb1db7 --- /dev/null +++ b/src/behaviortree/Agent.ts @@ -0,0 +1,42 @@ +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 new file mode 100644 index 0000000..d5d2275 --- /dev/null +++ b/src/behaviortree/BTNode/Action.ts @@ -0,0 +1,143 @@ +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 { + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + public tick(ticker: Ticker): Status { + this._func(); + return Status.FAILURE; + } +} + +/** + * 逻辑节点,一直执行 (无子节点) + * 直接返回RUNING + */ +export class Running extends Action { + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + public tick(ticker: Ticker): Status { + this._func(); + return Status.RUNNING; + } +} + +/** + * 成功节点 无子节点 + * 直接返回SUCCESS + */ +export class Success extends Action { + private _func: () => void; + constructor(func: () => void) { + super(); + this._func = func; + } + + public tick(ticker: Ticker): Status { + this._func(); + return Status.SUCCESS; + } +} +/** + * 次数等待节点(无子节点) + * 次数内,返回RUNING + * 超次,返回SUCCESS + */ +export class WaitTicks extends Action { + /** 最大次数 */ + private _maxTicks: number; + /** 经过的次数 */ + private _elapsedTicks: number; + constructor(maxTicks: number = 0) { + super(); + this._maxTicks = maxTicks; + this._elapsedTicks = 0; + } + + public open(ticker: Ticker): void { + this._elapsedTicks = 0; + } + + 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) */ + private _duration: number; + constructor(duration: number = 0) { + super(); + this._duration = duration * 1000; + } + + public open(ticker: Ticker): void { + let startTime = new Date().getTime(); + ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); + } + + 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 { + public tick(ticker: Ticker): Status { + ticker.blackboard.interruptDefend = true; + return Status.SUCCESS; + } +} + +/** + * 行为树被打断取消节点 + * 直接返回 SUCCESS + * 和 InterruptDefend 必须成对出现 + */ +export class InterruptDefendCancel extends Action { + 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 new file mode 100644 index 0000000..20b8ee8 --- /dev/null +++ b/src/behaviortree/BTNode/BaseNode.ts @@ -0,0 +1,104 @@ +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]); + } + } + + /** 执行节点 */ + 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 更新器 + */ + public _enter(ticker: Ticker): void { + ticker.enterNode(this); + this.enter(ticker); + } + + /** + * 打开节点 + * @param ticker 更新器 + */ + public _open(ticker: Ticker): void { + ticker.openNode(this); + ticker.blackboard.set("isOpen", true, ticker.tree.id, this.id); + this.open(ticker); + } + + /** + * 更新节点 + * @param ticker 更新器 + */ + public _tick(ticker: Ticker): Status { + ticker.tickNode(this); + return this.tick(ticker); + } + + /** + * 关闭节点 + * @param ticker 更新器 + */ + public _close(ticker: Ticker): void { + ticker.closeNode(this); + ticker.blackboard.set("isOpen", false, ticker.tree.id, this.id); + this.close(ticker); + } + + /** + * 退出节点 + * @param ticker 更新器 + */ + 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 new file mode 100644 index 0000000..2ed53bf --- /dev/null +++ b/src/behaviortree/BTNode/Composite.ts @@ -0,0 +1,163 @@ +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 { + open(ticker: Ticker): void { + super.open(ticker); + ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); + } + + 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 { + open(ticker: Ticker): void { + super.open(ticker); + ticker.blackboard.set("runningChild", 0, ticker.tree.id, this.id); + } + + 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 { + 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 { + 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 { + 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 { + 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 { + 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 new file mode 100644 index 0000000..932c629 --- /dev/null +++ b/src/behaviortree/BTNode/Condition.ts @@ -0,0 +1,18 @@ +import { Status } from "../header"; +import { Ticker } from "../Ticker"; +import { Action } from "./Action"; + +/** + * 条件节点 + */ +export class Condition extends Action { + private _func: (subject: any) => boolean = null; + constructor(func: (subject: any) => boolean) { + super(); + this._func = func; + } + + 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 new file mode 100644 index 0000000..9eed0cb --- /dev/null +++ b/src/behaviortree/BTNode/Decorator.ts @@ -0,0 +1,288 @@ +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 { + 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 { + 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 { + /** 最大次数 */ + private _maxTicks: number; + /** 当前执行过的次数 */ + private _elapsedTicks: number; + + /** + * 创建 + * @param maxTicks 最大次数 + * @param child 子节点 + */ + constructor(maxTicks: number, child: BaseNode) { + super(child); + this._maxTicks = maxTicks; + this._elapsedTicks = 0; + } + + open(ticker: Ticker): void { + super.open(ticker); + this._elapsedTicks = 0; + } + + 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) */ + private _maxTime: number; + + /** + * 时间限制节点 + * @param maxTime 最大时间 (微秒ms) + * @param child 子节点 + */ + constructor(maxTime: number, child: BaseNode) { + super(child); + this._maxTime = maxTime * 1000; + } + + open(ticker: Ticker): void { + super.open(ticker); + let startTime = new Date().getTime(); + ticker.blackboard.set("startTime", startTime, ticker.tree.id, this.id); + } + + 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 { + maxLoop: number; + + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this.maxLoop = maxLoop; + } + + open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + 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 { + maxLoop: number; + + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this.maxLoop = maxLoop; + } + + open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + 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 { + private _maxLoop: number; + constructor(child: BaseNode, maxLoop: number = -1) { + super(child); + this._maxLoop = maxLoop; + } + + open(ticker: Ticker): void { + ticker.blackboard.set("i", 0, ticker.tree.id, this.id); + } + + 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 { + 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 { + 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 new file mode 100644 index 0000000..b09faa9 --- /dev/null +++ b/src/behaviortree/BehaviorTree.ts @@ -0,0 +1,51 @@ +import { Blackboard } from "./Blackboard"; +import { BaseNode } from "./BTNode/BaseNode"; +import { createUUID } from "./header"; +import { Ticker } from "./Ticker"; + +/** + * 行为树 + * 所有节点全部添加到树中 + */ +export class BehaviorTree { + /** 行为树ID */ + private _id: string; + /** 行为树跟节点 */ + private _root: BaseNode; + constructor(root: BaseNode) { + this._id = createUUID(); + this._root = root; + } + + 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 new file mode 100644 index 0000000..2a099cd --- /dev/null +++ b/src/behaviortree/Blackboard.ts @@ -0,0 +1,64 @@ +/** + * 行为树数据 + */ +interface ITreeData { + nodeMemory: { [nodeScope: string]: any }; + openNodes: any[]; +} + +/** 平台 */ +export class Blackboard { + public interruptDefend: boolean = false; // 行为树打断保护 + public interrupt: boolean = false; // 打断行为树的标记 + private _baseMemory: any; + private _treeMemory: { [treeScope: string]: ITreeData }; + + constructor() { + this._baseMemory = {}; + this._treeMemory = {}; + } + + clear(): void { + this._baseMemory = {}; + this._treeMemory = {}; + } + + set(key: string, value: any, treeScope?: string, nodeScope?: string): void { + let memory = this._getMemory(treeScope, nodeScope); + memory[key] = value; + } + + get(key: string, treeScope?: string, nodeScope?: string): any { + let memory = this._getMemory(treeScope, nodeScope); + return memory[key]; + } + + private _getTreeMemory(treeScope: string): ITreeData { + if (!this._treeMemory[treeScope]) { + this._treeMemory[treeScope] = { + nodeMemory: {}, + openNodes: [], + }; + } + return this._treeMemory[treeScope]; + } + + private _getNodeMemory(treeMemory: ITreeData, nodeScope: string): { [key: string]: any } { + let memory = treeMemory.nodeMemory; + if (!memory[nodeScope]) { + memory[nodeScope] = {}; + } + return memory[nodeScope]; + } + + 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 new file mode 100644 index 0000000..e2ddfc1 --- /dev/null +++ b/src/behaviortree/Ticker.ts @@ -0,0 +1,40 @@ +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; + } + + /** 进入节点 */ + enterNode(node: BaseNode): void { + this.nodeCount++; + this.openNodes.push(node); + } + + /** 打开节点 */ + openNode(node: BaseNode): void { } + + /** 更新节点 */ + tickNode(node: BaseNode): void { } + + /** 关闭节点 */ + closeNode(node: BaseNode): void { + this.openNodes.pop(); + } + + /** 退出节点 */ + exitNode(node: BaseNode): void { } +} \ No newline at end of file diff --git a/src/behaviortree/header.ts b/src/behaviortree/header.ts new file mode 100644 index 0000000..4dfdafc --- /dev/null +++ b/src/behaviortree/header.ts @@ -0,0 +1,22 @@ +export const enum Status { + FAILURE, + SUCCESS, + RUNNING, +} + +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/CocosAdapter.ts b/src/cocos/CocosAdapter.ts new file mode 100644 index 0000000..22ca52b --- /dev/null +++ b/src/cocos/CocosAdapter.ts @@ -0,0 +1,51 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: + */ + +import { screen as ccScreen, view } from "cc"; +import { Adapter } from "../global/Adapter"; +import { size } from "../global/header"; +import { info } from "../tool/log"; + +export class CocosAdapter extends Adapter { + /** + * 获取屏幕像素尺寸 + * @returns {size} + */ + protected getScreenSize(): size { + let windowSize = ccScreen.windowSize; + let width = Math.ceil(windowSize.width / view.getScaleX()); + let height = Math.ceil(windowSize.height / view.getScaleY()); + return { width, height }; + } + + /** + * 获取设计尺寸 + * @returns {size} + */ + protected getDesignSize(): size { + let designSize = view.getDesignResolutionSize(); + return { width: designSize.width, height: designSize.height }; + } + + /** + * 设置尺寸发生变化的监听 + * @param callback + */ + protected registerResizeCallback(callback: (...args: any) => void): void { + ccScreen.on("window-resize", (...args: any) => { + info("window-resize"); + callback(...args); + }, this); + ccScreen.on("orientation-change", (...args: any) => { + info("orientation-change"); + callback(...args); + }, this); + ccScreen.on("fullscreen-change", (...args: any) => { + info("fullscreen-change"); + callback(...args); + }, this); + } +} diff --git a/src/cocos/CocosEntry.ts b/src/cocos/CocosEntry.ts new file mode 100644 index 0000000..cd7c987 --- /dev/null +++ b/src/cocos/CocosEntry.ts @@ -0,0 +1,134 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description:cocos游戏入口 定义了游戏启动时的基本配置和初始化流程。 + */ + +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 { info } from "../tool/log"; +import { PropsHelper } from "../ui/PropsHelper"; +import { CocosAdapter } from "./CocosAdapter"; +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; + + /** + * 虚函数,子类需要实现 + * kunpo库初始化完成后调用 + */ + public abstract onInit(): void; + + public getConfig(): FrameConfig { + return {}; + } + + protected start(): void { + info("开始初始化kunpo框架"); + + const config = this.getConfig(); + enableDebugMode(config.debug); + + // 设置游戏真帧率 + game.frameRate = this.fps; + director.addPersistRootNode(this.node); + this.node.setSiblingIndex(this.node.children.length - 1); + PropsHelper.setConfig(this.uiConfig?.json); + ECManager.registerEntityConfig(this.ecConfig?.json); + this.initPlatform(); + this.initEvent(); + this.initTime(); + this.initAdapter(); + this.initModule(); + // 注册所有组件 + ECDataHelper.registerComponents(); + info("kunpo框架初始化完成"); + this.onInit(); + } + + private initPlatform(): void { + // 处理平台判断 + Platform.isNative = sys.isNative; + Platform.isMobile = sys.isMobile; + Platform.isNativeMobile = sys.isNative && sys.isMobile; + + switch (sys.os) { + case sys.OS.ANDROID: + Platform.isAndroid = true; + info("系统类型 Android"); + break; + case sys.OS.IOS: + Platform.isIOS = true; + info("系统类型 IOS"); + break; + case sys.OS.OPENHARMONY: + Platform.isHarmonyOS = true; + info("系统类型 HarmonyOS"); + break; + default: + break; + } + + switch (sys.platform) { + case sys.Platform.WECHAT_GAME: + Platform.isWX = true; + Platform.platform = PlatformType.WX; + break; + case sys.Platform.ALIPAY_MINI_GAME: + Platform.isAlipay = true; + Platform.platform = PlatformType.Alipay; + break; + case sys.Platform.BYTEDANCE_MINI_GAME: + Platform.isBytedance = true; + Platform.platform = PlatformType.Bytedance; + break + case sys.Platform.HUAWEI_QUICK_GAME: + Platform.isHuaweiQuick = true; + Platform.platform = PlatformType.HuaweiQuick; + break; + default: + // 其他都设置为浏览器 + Platform.isBrowser = true; + Platform.platform = PlatformType.Browser; + break; + } + info(`platform: ${PlatformType[Platform.platform]}`); + } + + private initEvent(): void { + GlobalEvent._initGlobalEvent(); + } + + private initTime(): void { + InnerTimer.initTimer(); + GlobalTimer.initTimer(); + this.schedule(this.tick.bind(this), 0, macro.REPEAT_FOREVER); + } + + private initAdapter(): void { + new CocosAdapter().init(); + } + + private initModule(): void { + info(`初始化模块`); + // 递归查找自身或所有子节点中指定类型的组件。 + for (const module of this.getComponentsInChildren(ModuleBase)) { + info(`module:${module.moduleName}`); + module.init(); + } + } + + private tick(dt: number): void { + InnerTimer.update(dt); + GlobalTimer.update(dt); + } +} \ No newline at end of file diff --git a/src/cocos/CocosUIModule.ts b/src/cocos/CocosUIModule.ts new file mode 100644 index 0000000..fdf49ee --- /dev/null +++ b/src/cocos/CocosUIModule.ts @@ -0,0 +1,45 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: cocos UI模块 + */ +import { _decorator } from "cc"; + +import { GRoot } from "fairygui-cc"; +import { ModuleBase } from "../module/ModuleBase"; +import { info } from "../tool/log"; +import { WindowManager } from "../ui/WindowManager"; +import { WindowResPool } from "../ui/WindowResPool"; +import { CocosWindowContainer } from "./CocosWindowContainer"; + +const { ccclass, menu, property } = _decorator; + +@ccclass("CocosUIModule") +@menu("kunpo/UI/UIModule") +export class CocosUIModule extends ModuleBase { + /** 模块名称 */ + public moduleName: string = "UI模块"; + /** 模块初始化 (内部使用) */ + public init(): void { + /** 初始化窗口管理系统 */ + WindowManager._init(new WindowResPool()); + GRoot.create(); + info("初始化 WindowContainers"); + for (const child of this.node.children) { + const containerComponent = child.getComponent(CocosWindowContainer); + containerComponent?.init(); + } + // fgui.UIObjectFactory.setLoaderExtension(GLoader); + // this._uiInitializer = new UIInitializer(this.node, this.getPackageLoader()); + // this._uiInitializer.init(this.reAdaptWhenScreenResize, this.fullIfWideScreen); + this.node.destroyAllChildren(); + /** 注册窗口信息 */ + WindowManager.registerUI() + this.onInit(); + } + + /** 模块初始化完成后调用的函数 */ + protected onInit(): void { + info("UIModule init complete"); + } +} \ No newline at end of file diff --git a/src/cocos/CocosWindowContainer.ts b/src/cocos/CocosWindowContainer.ts new file mode 100644 index 0000000..4f6bce1 --- /dev/null +++ b/src/cocos/CocosWindowContainer.ts @@ -0,0 +1,35 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: + */ + +import { Component, _decorator } from "cc"; +import { GComponent, GRoot } from "fairygui-cc"; +import { Screen } from "../global/Screen"; +import { info } from "../tool/log"; +import { WindowGroup } from "../ui/WindowGroup"; +import { WindowManager } from "../ui/WindowManager"; +const { ccclass, property, menu } = _decorator; +@ccclass("CocosWindowContainer") +@menu("kunpo/UI/UIContainer") +export class CocosWindowContainer extends Component { + @property({ displayName: "忽略顶部窗口查询", tooltip: "当通过窗口管理器获取顶部窗口时,是否忽略查询" }) ignoreQuery: boolean = false; + @property({ displayName: "吞噬触摸事件", tooltip: "窗口组是否会吞噬触摸事件,防止层级下的窗口接收触摸事件" }) swallowTouch: boolean = false; + @property({ displayName: "底部遮罩透明度", tooltip: "底部半透明遮罩的默认透明度", min: 0, max: 1, step: 0.01 }) bgAlpha: number = 0.75; + /** + * 初始化窗口容器 + */ + public init(): void { + let name = this.node.name; + info(`\tUIContainer name:${name} 忽略顶部窗口查询:${this.ignoreQuery} 吞噬触摸事件:${this.swallowTouch}`); + const root = new GComponent(); + root.name = name; + root.node.name = name; + root.visible = false; + root.opaque = this.swallowTouch; + root.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + GRoot.inst.addChild(root); + WindowManager._addWindowGroup(new WindowGroup(name, root, this.ignoreQuery, this.swallowTouch, this.bgAlpha)); + } +} \ No newline at end of file diff --git a/src/condition/ConditionDecorator.ts b/src/condition/ConditionDecorator.ts new file mode 100644 index 0000000..ac35073 --- /dev/null +++ b/src/condition/ConditionDecorator.ts @@ -0,0 +1,25 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-17 + * @Description: 条件装饰器 + */ +export namespace _conditionDecorator { + /** 用来存储条件注册信息 */ + const cdClassMap: Map = new Map(); + + /** 获取组件注册信息 */ + export function getConditionMaps(): Map { + return cdClassMap; + } + + /** + * 条件装饰器 + * @param {number} conditionType 条件类型 + */ + export function conditionClass(conditionType: number): Function { + /** target 类的构造函数 */ + return function (ctor: any): void { + cdClassMap.set(conditionType, ctor); + }; + } +} \ No newline at end of file diff --git a/src/condition/ConditionManager.ts b/src/condition/ConditionManager.ts new file mode 100644 index 0000000..c10d329 --- /dev/null +++ b/src/condition/ConditionManager.ts @@ -0,0 +1,210 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: + */ +import { warn } from "../tool/log"; +import { _conditionDecorator } from "./ConditionDecorator"; +import { ConditionMode } from "./ConditionMode"; +import { ConditionBase } from "./node/ConditionBase"; +import { ConditionNode } from "./node/ConditionNode"; +export class ConditionManager { + /** 注册的 条件类型对应条件的信息 */ + private static readonly _typeToCondition: Map = new Map(); + + /** 条件类型 对应 条件节点 */ + private static readonly _typeToNotifyNodes: Map> = new Map>(); + /** 条件节点 对应 条件类型 */ + private static readonly _nodeToConditionTypes: Map> = new Map>(); + + /** 需要更新的条件 */ + private static readonly _needUpdateConditions: Set = new Set(); + /** 需要更新的节点 */ + private static readonly _needUpdateNodes: Set = new Set(); + + /** 是否正在更新 */ + private static _updating: boolean = false; + + /** 初始化所有条件,并全部更新一次 */ + public static initCondition(): void { + const conditionMaps = _conditionDecorator.getConditionMaps(); + conditionMaps.forEach((ctor, conditionType) => { + if (!this._typeToCondition.has(conditionType)) { + const condition = new ctor(); + condition.type = conditionType; + condition._init(); + this._addCondition(condition); + } else { + warn(`条件(${conditionType})已经被注册, 跳过`); + } + }); + this._refreshAllConditions(); + } + + /** + * 添加条件 + * @param {IConditionBase} condition 条件 + */ + private static _addCondition(condition: ConditionBase): void { + if (this._updating) { + throw new Error("请不要在ConditionManager更新过程中添加要更新的条件"); + } + this._typeToNotifyNodes.set(condition.type, new Set()); + this._typeToCondition.set(condition.type, condition); + this._needUpdateConditions.add(condition); + } + + + private static _refreshAllConditions(): void { + let allCondition = this._typeToCondition; + for (const condition of allCondition.values()) { + condition._updateCondition(); + } + } + + /** + * 添加到更新列表中 + * @param conditionType 条件类型 + */ + public static _addUpdateCondition(conditionType: number): void { + if (this._updating) { + throw new Error("请不要在ConditionManager更新过程中添加要更新的条件"); + } + // 添加待更新的条件; + const condition = this._typeToCondition.get(conditionType); + if (condition) { + this._needUpdateConditions.add(condition); + } + } + + /** + * 添加条件节点 + * @param notifyNode 条件节点 + * @param conditionType 条件类型 + */ + public static _addConditionNode(conditionNode: ConditionNode, conditionType: number): void { + const condition = this._typeToCondition.get(conditionType); + if (!condition) { + warn(`不存在条件类型(${conditionType}),请通过装饰器()注册条件类型`); + return; + } + // 添加通知类型对应节点 + let nodes = this._typeToNotifyNodes.get(condition.type); + if (!nodes.has(conditionNode)) { + nodes.add(conditionNode); + } + // 添加节点对应通知类型 + let conditionTypes = this._nodeToConditionTypes.get(conditionNode); + if (!conditionTypes) { + conditionTypes = new Set(); + this._nodeToConditionTypes.set(conditionNode, conditionTypes); + } + if (!conditionTypes.has(condition.type)) { + conditionTypes.add(condition.type); + } + } + + /** + * 移除条件节点 + * @param conditionNode 条件节点 + * @param conditionType 条件类型 + */ + public static _removeConditionNode(conditionNode: ConditionNode): void { + let types = this._nodeToConditionTypes.get(conditionNode); + for (const conditionType of types.values()) { + let nodes = this._typeToNotifyNodes.get(conditionType); + nodes.delete(conditionNode); + } + this._nodeToConditionTypes.delete(conditionNode); + } + + /** + * 立即更新条件节点(内部使用) + * @param conditionNode 条件节点 + */ + public static _nowUpdateConditionNode(conditionNode: ConditionNode): void { + this._tryUpdateConditionNode(conditionNode); + } + + /** 更新函数(内部使用)*/ + public static _update(): void { + this._updating = true; + // 更新条件 + let needUpdateConditions = this._needUpdateConditions; + if (needUpdateConditions.size > 0) { + for (const condition of needUpdateConditions.values()) { + this._tryUpdateCondition(condition); + } + needUpdateConditions.clear(); + } + // 更新条件节点 + let needUpdateConditionNodes = this._needUpdateNodes; + if (needUpdateConditionNodes.size > 0) { + for (const conditionNode of needUpdateConditionNodes.values()) { + this._tryUpdateConditionNode(conditionNode); + } + needUpdateConditionNodes.clear(); + } + this._updating = false; + } + + /** + * 更新条件节点,如果状态改变,收集需要更新的通知节点(内部使用) + * @param {ConditionNode} conditionNode 条件节点 + */ + private static _tryUpdateCondition(condition: ConditionBase): void { + // 更新条件 + if (!condition._updateCondition()) { + return; + } + // 条件改变,收集需要更新的通知节点 + if (this._typeToNotifyNodes.has(condition.type)) { + let nodes = this._typeToNotifyNodes.get(condition.type); + let needUpdateConditionNodes = this._needUpdateNodes; + for (const conditionNode of nodes) { + if (!needUpdateConditionNodes.has(conditionNode)) { + needUpdateConditionNodes.add(conditionNode); + } + } + } + } + + /** + * 更新条件节点(内部使用) + * @param {ConditionNode} conditionNode 条件节点 + */ + private static _tryUpdateConditionNode(conditionNode: ConditionNode): void { + if (!this._nodeToConditionTypes.has(conditionNode)) { + return; + } + // 获取节点对应的所有通知条件 + const conditionTypes = this._nodeToConditionTypes.get(conditionNode); + const conditions = this._typeToCondition; + let canNotify = false; + let modeType = conditionNode._modeType; + switch (modeType) { + case ConditionMode.Any: + for (const conditionType of conditionTypes.values()) { + // 有一个满足就退出 + if (conditions.get(conditionType).canNotify()) { + canNotify = true; + break; + } + } + break; + case ConditionMode.All: + canNotify = true; + for (const conditionType of conditionTypes.values()) { + // 有任意一个不满足就退出 + if (!conditions.get(conditionType).canNotify()) { + canNotify = false; + break; + } + } + break; + default: + break; + } + conditionNode.notify(canNotify); + } +} \ No newline at end of file diff --git a/src/condition/ConditionMode.ts b/src/condition/ConditionMode.ts new file mode 100644 index 0000000..6e5d4bd --- /dev/null +++ b/src/condition/ConditionMode.ts @@ -0,0 +1,12 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 条件模式 + */ + +export enum ConditionMode { + /** 满足任意条件显示 */ + Any, + /** 满足所有条件显示 */ + All, +} diff --git a/src/condition/ConditionModule.ts b/src/condition/ConditionModule.ts new file mode 100644 index 0000000..58ef2b1 --- /dev/null +++ b/src/condition/ConditionModule.ts @@ -0,0 +1,44 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 条件显示模块 + */ +import { _decorator } from "cc"; +import { InnerTimer } from "../global/InnerTimer"; +import { ModuleBase } from "../module/ModuleBase"; +import { info } from "../tool/log"; +import { ConditionManager } from "./ConditionManager"; + +const { ccclass, menu, property } = _decorator; + +@ccclass("ConditionModule") +@menu("kunpo/condition/ConditionModule") +export class ConditionModule extends ModuleBase { + @property({ + displayName: "更新间隔(秒)", + min: 0.1, + step: 0.1, + }) + updateDeltaTime: number = 0.3; + + /** 模块名称 */ + public moduleName: string = "条件显示模块"; + + private _timer: number = 0; + public init(): void { + this.onInit(); + + this._timer = InnerTimer.startTimer(() => { + ConditionManager._update(); + }, this.updateDeltaTime, -1); + } + + /** 模块初始化完成后调用的函数 */ + protected onInit(): void { + info("ConditionModule init complete"); + } + + public onDestroy(): void { + InnerTimer.stopTimer(this._timer); + } +} \ No newline at end of file diff --git a/src/condition/node/ConditionAllNode.ts b/src/condition/node/ConditionAllNode.ts new file mode 100644 index 0000000..4662d14 --- /dev/null +++ b/src/condition/node/ConditionAllNode.ts @@ -0,0 +1,18 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 满足所有条件显示 + */ +import { GObject } from "fairygui-cc"; +import { ConditionMode } from "../ConditionMode"; +import { ConditionFGUINode } from "./ConditionFGUINode"; +export class ConditionAllNode extends ConditionFGUINode { + /** + * 构建红点节点 + * @param {GObject} node 关联节点 + * @param {...number[]} conditionTypes 条件类型 + */ + public constructor(node: GObject, ...conditionTypes: number[]) { + super(node, ConditionMode.All, ...conditionTypes); + } +} \ No newline at end of file diff --git a/src/condition/node/ConditionAnyNode.ts b/src/condition/node/ConditionAnyNode.ts new file mode 100644 index 0000000..df4821d --- /dev/null +++ b/src/condition/node/ConditionAnyNode.ts @@ -0,0 +1,18 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 满足任意条件显示 + */ +import { GObject } from "fairygui-cc"; +import { ConditionMode } from "../ConditionMode"; +import { ConditionFGUINode } from "./ConditionFGUINode"; +export class ConditionAnyNode extends ConditionFGUINode { + /** + * 构建红点节点 + * @param {GObject} node 关联节点 + * @param {...number[]} conditionTypes 条件类型 + */ + public constructor(node: GObject, ...conditionTypes: number[]) { + super(node, ConditionMode.Any, ...conditionTypes); + } +} \ No newline at end of file diff --git a/src/condition/node/ConditionBase.ts b/src/condition/node/ConditionBase.ts new file mode 100644 index 0000000..a796a96 --- /dev/null +++ b/src/condition/node/ConditionBase.ts @@ -0,0 +1,54 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 条件基类 + */ + +import { ConditionManager } from "../ConditionManager"; + +export abstract class ConditionBase { + /** 初始化 */ + public _init(): void { + this.onInit(); + } + + /** 条件类型 */ + public type: number; + + private _canNotify: boolean; + /** + * 是否可以通知 + * @returns {boolean} + */ + public canNotify(): boolean { + return this._canNotify; + } + + public tryUpdate(): void { + ConditionManager._addUpdateCondition(this.type); + } + + /** + * 更新条件 + * @returns {boolean} 是否发生变化 + */ + public _updateCondition(): boolean { + let canNotify = this.evaluate(); + if (canNotify == this._canNotify) { + return; + } + this._canNotify = canNotify; + return true; + } + + /** + * 初始化 + */ + protected abstract onInit(): void; + + /** + * 返回条件结果 子类实现 + * @returns {boolean} + */ + protected abstract evaluate(): boolean; +} diff --git a/src/condition/node/ConditionFGUINode.ts b/src/condition/node/ConditionFGUINode.ts new file mode 100644 index 0000000..f0aafde --- /dev/null +++ b/src/condition/node/ConditionFGUINode.ts @@ -0,0 +1,52 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-17 + * @Description: + */ + +import { GObject } from "fairygui-cc"; +import { ConditionManager } from "../ConditionManager"; +import { ConditionMode } from "../ConditionMode"; +import { ConditionNode } from "./ConditionNode"; +export class ConditionFGUINode extends ConditionNode { + /** + * 红点节点 + * @protected + * @type {GObject | Node} fgui节点 或 node节点 + * @memberof NotityFGUINode + */ + protected node: GObject; + + private _oldRemoveFromParent: () => void; + + /** + * 构建红点节点 + * @param {GObject} node 关联节点 + * @param {...number[]} conditionTypes 条件类型 + */ + public constructor(node: GObject, modeType: ConditionMode, ...conditionTypes: number[]) { + super(modeType, ...conditionTypes); + this.node = node; + const oldRemoveFromParent = (this._oldRemoveFromParent = node.removeFromParent); + node.removeFromParent = (): void => { + super.destroy(); + oldRemoveFromParent.call(node); + this.node.removeFromParent = this._oldRemoveFromParent; + this.node = null; + }; + // 立即更新一次 + ConditionManager._nowUpdateConditionNode(this); + } + + public destroy(): void { + super.destroy(); + if (this.node) { + this.node.removeFromParent = this._oldRemoveFromParent; + this.node = null; + } + } + + public notify(visible: boolean): void { + this.node.visible = visible; + } +} \ No newline at end of file diff --git a/src/condition/node/ConditionNode.ts b/src/condition/node/ConditionNode.ts new file mode 100644 index 0000000..bb93db9 --- /dev/null +++ b/src/condition/node/ConditionNode.ts @@ -0,0 +1,36 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 条件节点 + */ + +import { ConditionManager } from "../ConditionManager"; +import { ConditionMode } from "../ConditionMode"; + +export abstract class ConditionNode { + /** 条件类型 */ + public _modeType: ConditionMode; + + /** + * 构建红点节点 + * @param {GObject} node 关联节点 + * @param {...number[]} conditionTypes 条件类型 + */ + public constructor(modeType: ConditionMode, ...conditionTypes: number[]) { + this._modeType = modeType; + for (const conditionType of conditionTypes) { + ConditionManager._addConditionNode(this, conditionType); + } + } + + /** 移除节点 */ + public destroy(): void { + ConditionManager._removeConditionNode(this); + } + + /** + * 通知节点更新 + * @param {boolean} visible 节点显示状态 + */ + public abstract notify(visible: boolean): void; +} diff --git a/src/ecmodule/Component.ts b/src/ecmodule/Component.ts new file mode 100644 index 0000000..4614450 --- /dev/null +++ b/src/ecmodule/Component.ts @@ -0,0 +1,112 @@ +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; + + /** 是否需要销毁 */ + public _needDestroy: boolean; + + /** 更新ID */ + public _updateId: number = -1; + + /** 是否更新中 */ + public get _updating(): boolean { + return this._updateId != -1; + } + + /** 生命周期函数 添加到实体 */ + public _add(): void { + this.onAdd(); + } + + /** 生命周期函数 销毁 */ + public _destroy(): void { + this.onDestroy(); + } + + /** 生命周期函数 添加到实体后 在这个函数中可以获取其他组件 */ + public _enter(): void { + // 自动开启更新 + if (this.needUpdate) { + this.componentManager.startUpdateComponent(this); + } + this.onEnter(); + } + + /** 生命周期函数 从实体中移除 */ + public _remove(): void { + this.stopUpdate(); + this.onRemove(); + this.componentManager._destroyComponent(this); + } + + /** 更新 */ + 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 new file mode 100644 index 0000000..05ce96e --- /dev/null +++ b/src/ecmodule/ComponentManager.ts @@ -0,0 +1,254 @@ +import { Component } from "./Component"; +import { ComponentPool } from "./ComponentPool"; + +/** + * 组件更新信息 + * + * @export + * @class ComponentUpdate + */ +export class ComponentUpdate { + /** 组件更新类型 */ + public componentType: number; + + /** 组件更新列表 */ + private readonly _components: Component[] = []; + + /** create constructor */ + public constructor(componentType: number) { + this.componentType = componentType; + } + + /** + * 添加要更新的组件 + * @param component 组件 + */ + public addComponent(component: Component): void { + this._components.push(component); + component._updateId = this._components.length - 1; + } + + /** + * 删除要更新的组件 + * @param {Component} component 组件 + */ + 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(); + } + + /** 更新 */ + 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} + */ + protected componentPool: ComponentPool; + + /** 更新组件池 */ + protected readonly updatingComponents: ComponentUpdate[] = []; + protected readonly componentUpdateOrderList: number[] = []; + + /** 新添加的或者新停止更新的组件池 */ + private readonly _toUpdateComponents: Component[] = []; + private readonly _toStopComponents: Component[] = []; + + /** 当前更新的组件类型 */ + private _currentUpdateComponentType: number = -1; + + /** + *Creates an instance of ComponentManager. + * @param {ComponentPool} componentPool 组件池 + * @param {number[]} componentUpdateOrderList 组件更新顺序 + */ + constructor(componentPool: ComponentPool, componentUpdateOrderList: number[]) { + this.componentPool = componentPool; + this._toUpdateComponents.length = 0; + this._toStopComponents.length = 0; + for (const componentType of componentUpdateOrderList) { + this._addComponentUpdateOrder(componentType); + } + } + + 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} 创建的组件 + */ + 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 + */ + public _destroyComponent(component: Component): void { + if (!component._updating) { + component._destroy(); + this.componentPool.recycle(component); + } else { + component._needDestroy = true; + } + } + + /** 更新所有组件(内部使用) */ + public _update(dt: number): void { + this._updateAllComponents(dt); + this._currentUpdateComponentType = -1; + + this._clearStopComponents(); + this._addUpdateComponents(); + } + + /** + * 添加组件更新顺序,先添加的先更新 + * @param {number} componentType 组件类型 + */ + 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; + } + + /** 添加组件到组件更新列表 */ + 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); + } + + /** 组件更新列表中删除组件 */ + private _removeComponentToUpdateList(component: Component): void { + this.updatingComponents[component.type].removeComponent(component); + } + + /** 更新所有组件 */ + 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); + } + } + + 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; + } + } + + 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 new file mode 100644 index 0000000..7a62191 --- /dev/null +++ b/src/ecmodule/ComponentPool.ts @@ -0,0 +1,84 @@ +import { Component } from "./Component"; +import { ObjectBase } from "./ObjectBase"; +import { ObjectFactory } from "./ObjectFactory"; + +export class ComponentPool { + /** 组件对象类型到组件类型转换 */ + private readonly _objectTypeToComponentType: number[] = new Array(128); + private _pools: Map = new Map(); + private _nameToObjectType: Map = new Map(); + /** + * 注册组件 + * @param {number} componentObjectType 组件对象类型 + * @param {number} componentType 组件类型 + * @param {string} name 组件名称 + * @param {new () => Component} ctor 构造函数 + */ + 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; + } + + public getObjectTypeByName(componentName: string): number { + return this._nameToObjectType.get(componentName); + } + + /** + * 创建组件 + * @param {number} componentName 组件名 + * @returns {T} 创建的组件 + */ + 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} + */ + 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 + */ + public recycle(component: Component): void { + const objectFactory = this._pools.get(component.objectType); + objectFactory.recycle(component); + } + + /** 清理缓存 */ + 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 new file mode 100644 index 0000000..c831c17 --- /dev/null +++ b/src/ecmodule/ECDataHelper.ts @@ -0,0 +1,109 @@ +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 { + /** 组件池 */ + 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 new file mode 100644 index 0000000..8dc8fc7 --- /dev/null +++ b/src/ecmodule/ECDecorator.ts @@ -0,0 +1,140 @@ +/** + * @Author: Gongxh + * @Date: 2025-01-14 + * @Description: 实体组件装饰器 + */ + +import { Color, Size, Vec2, Vec3 } from "cc"; +import { ObjectHelper } from "../tool/helper/ObjectHelper"; + + +export namespace _ecdecorator { + 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 new file mode 100644 index 0000000..056c35d --- /dev/null +++ b/src/ecmodule/ECManager.ts @@ -0,0 +1,162 @@ +/** + * @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 { + /** 实体管理器 */ + private static _worlds: Map = new Map(); + /** 实体配置信息 */ + 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 { + // 遍历并注册每个实体的配置 + 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; + } + + 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 new file mode 100644 index 0000000..e06a26b --- /dev/null +++ b/src/ecmodule/ECType.ts @@ -0,0 +1,79 @@ +/** + * @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"; + +export const EntityIndexBits = 16; +export const EntityIndexMask = (1 << EntityIndexBits) - 1; +export const MaxEntityCount = 1 << EntityIndexBits; +export type EntityId = number; + +/** + * 2进制转10进制 (不支持小数和负数) + * @param {number} bitNumber 二进制数 + * @return {number} 十进制数 + */ +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} 二进制数 + */ +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 + */ +export function getEntityIndex(id: EntityId): number { + return id & EntityIndexMask; +} + +/** + * 通过实体id获取实体版本 + * @param id + */ +export function getEntityVersion(id: EntityId): number { + return id >>> EntityIndexBits; +} + +/** + * 实体描述 + * @param id 实体id + */ +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 new file mode 100644 index 0000000..bb39639 --- /dev/null +++ b/src/ecmodule/Entity.ts @@ -0,0 +1,277 @@ +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 + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _add(): void { + this.active = true; + for (const component of this.components.values()) { + component._enter(); + } + } + + /** + * 实体销毁,不要手动调用 + * @memberof Entity + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + 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 new file mode 100644 index 0000000..8797307 --- /dev/null +++ b/src/ecmodule/EntityManager.ts @@ -0,0 +1,420 @@ +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} + */ + public readonly insEntity: Entity = new Entity(); + + /** + * 单例实体激活状态 + * @type {boolean} + */ + public insActive: boolean = false; + + /** + * 组件管理 + * @type {ComponentManager} + */ + public componentManager: ComponentManager; + + /** + * 普通实体事件容器 + * @type {EventManager} + */ + private _eventManager: EventManager; + + /** + * 单例实体消息监听容器 + * @type {EventManager} + */ + private _insEventManager: EventManager; + + /** 实体池 */ + private readonly _entityPool: Entity[] = []; + /** tag标记池 */ + private readonly _tagToEntity: Map> = new Map>(); + /** 实体回收池 */ + private _recyclePool: Entity[] = []; + /** 实体回收池最大容量 */ + private _maxCapacityInPool: number; + /** 实体回收版本 */ + private _entityVersion: number[] = []; + /** 回收实体ID */ + private _recycleEntityIds: EntityId[] = []; + /** 世界是否删除 */ + private _isDestroyed: boolean; + /** 是否正在更新 */ + private _updating: boolean; + /** + * 实体池最大容量,回收的多余的实体不会缓存 + * @param {string} name 名称 + * @param {ComponentPool} componentPool 组件池 + * @param {ComponentPool} componentUpdateOrderList 组件更新顺序 + * @param {number} [maxCapacityInPool=128] 实体回收池最大容量 + * @param {number} [preloadEntityCount=32] 预加载Entity数量 + */ + // eslint-disable-next-line prettier/prettier + 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 标签 + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + 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 标签 + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + public _removeEntityTag(entity: Entity, tag: number): void { + this._removeEntityTagById(entity.id, tag); + } + + /** + * 通过实体ID删除实体Tag(内部使用) + * @param {EntityId} entityId 实体Id + * @param {number} tag 标签 + */ + // eslint-disable-next-line @typescript-eslint/naming-convention + 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 是否单次事件 + */ + 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 发送参数 + */ + 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); + } + + 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); + } + + /** 更新 */ + public update(dt: number): void { + this._updating = true; + this.componentManager._update(dt); + this._updating = false; + } + + /** + * 回收Entity + * @param {Entity} entity 要回收的Entity + */ + 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 + */ + // eslint-disable-next-line @typescript-eslint/member-ordering + private _destroyEntity(entity: Entity): void { + entity._destroy(); + if (this._recyclePool.length < this._maxCapacityInPool) { + this._recyclePool.push(entity); + } + } + + /** + * 实体根据tag添加到tag列表中 + * @param entity + */ + 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); + } + } + + 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 new file mode 100644 index 0000000..22c1579 --- /dev/null +++ b/src/ecmodule/ObjectBase.ts @@ -0,0 +1,17 @@ +export class ObjectBase { + /** 是否被回收 */ + public recycled: boolean; + + /** 对象类型 */ + public objectType: number; + + /** 回收 */ + public _recycle(): void { + this.recycled = true; + } + + /** 重新利用 */ + public _reuse(): void { + this.recycled = false; + } +} \ No newline at end of file diff --git a/src/ecmodule/ObjectFactory.ts b/src/ecmodule/ObjectFactory.ts new file mode 100644 index 0000000..c490ba4 --- /dev/null +++ b/src/ecmodule/ObjectFactory.ts @@ -0,0 +1,64 @@ +import { ObjectBase } from "./ObjectBase"; + +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/event/Event.ts b/src/event/Event.ts new file mode 100644 index 0000000..f0466af --- /dev/null +++ b/src/event/Event.ts @@ -0,0 +1,20 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: + */ + +export class Event { + public id: number; + public name: string; + public target: any; + public once: boolean = false; + public callback: (...arg: any[]) => void; + public _destroy: boolean = false; + public _reset(): void { + this._destroy = false; + } + public _recycle(): void { + this._destroy = true; + } +} \ No newline at end of file diff --git a/src/event/EventFactory.ts b/src/event/EventFactory.ts new file mode 100644 index 0000000..55b93f3 --- /dev/null +++ b/src/event/EventFactory.ts @@ -0,0 +1,43 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: + */ + +import { Event } from "./Event"; + +export class EventFactory { + private _id: number = 0; + private _stack: Event[] = []; + private _maxCapacity: number = 64; + private _msgClass: new () => Event; + + get id(): number { + return this._id++; + } + + constructor(capacity: number, objectClass: new () => Event) { + this._maxCapacity = capacity; + this._msgClass = objectClass; + } + + public allocate(): T { + if (this._stack.length == 0) { + const ret = new this._msgClass() as T; + ret.id = this.id; + return ret; + } + const ret = this._stack.pop() as T; + ret._reset(); + return ret; + } + + public recycle(ret: Event): boolean { + if (this._maxCapacity > 0 && this._stack.length < this._maxCapacity) { + ret._recycle(); + this._stack.push(ret); + return true; + } + return false; + } +} \ No newline at end of file diff --git a/src/event/EventManager.ts b/src/event/EventManager.ts new file mode 100644 index 0000000..f2580a0 --- /dev/null +++ b/src/event/EventManager.ts @@ -0,0 +1,213 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: + */ + +import { Event } from "./Event"; +import { EventFactory } from "./EventFactory"; + +export class EventManager { + private _idToEvent: Map = new Map(); + private _nameToIds: Map> = new Map>(); + private _targetToIds: Map> = new Map>(); + private _factroy: EventFactory = new EventFactory(64, Event); + /** + * 添加事件监听器。 + * @param name - 事件名称。 + * @param callback - 回调函数,当事件触发时执行。 + * @param target - 可选参数,指定事件监听的目标对象。 + * 该方法将事件和回调函数注册到事件管理器中,以便在事件触发时执行相应的回调函数。 + */ + public addEvent(name: string, callback: (...args: any[]) => void, target?: any): void { + this._addEvent(name, callback, false, target); + } + + /** + * 添加一个只触发一次的事件监听器。 + * @param name - 事件名称。 + * @param callback - 事件触发时要执行的回调函数。 + * @param target - 可选参数,指定事件监听器的目标对象。 + */ + public addEventOnce(name: string, callback: (...args: any[]) => void, target?: any): void { + this._addEvent(name, callback, true, target); + } + + /** + * 发送事件给所有注册的监听器。 + * @param name - 事件名称。 + * @param target - 可选参数,指定目标对象,只有目标对象匹配时才会触发监听器。 (制定目标对象 效率更高) + * @param args - 传递给监听器回调函数的参数。 + */ + public send(name: string, target?: any, ...args: any[]): void { + let nameToIds = this._nameToIds; + if (!nameToIds.has(name)) { + return; + } + let ids = nameToIds.get(name); + let listenerMap = this._idToEvent; + + let needRemoveIds: number[] = []; + let triggerList: Event[] = []; + for (const id of ids.values()) { + if (!listenerMap.has(id)) { + throw new Error(`消息ID:【${id}】不存在`); + } + let listener = listenerMap.get(id); + if (!listener._destroy && (!target || target == listener.target)) { + triggerList.push(listener); + if (listener.once) { + listener._destroy = true; + needRemoveIds.push(listener.id); + } + } + } + for (const listener of triggerList) { + listener.callback(...args); + } + if (needRemoveIds.length > 0) { + for (const id of needRemoveIds) { + this._remove(id); + } + } + } + + /** + * 移除指定名称的事件监听器。 + * @param name - 事件名称。 + * @param callback - 要移除的回调函数。 + * @param target - 回调函数绑定的目标对象。 + * 该方法会遍历与指定名称关联的所有监听器ID,检查每个监听器的回调函数和目标对象, + * 如果匹配则将其ID添加到待移除列表中,最后统一移除这些监听器。 + */ + public remove(name: string, callback: () => void, target: any): void { + let nameToIds = this._nameToIds; + if (!nameToIds.has(name)) { + return; + } + let ids = nameToIds.get(name); + if (ids.size == 0) { + return; + } + + let needRemoveIds: number[] = []; + for (const id of ids.values()) { + let listener = this._idToEvent.get(id); + let needRemove = true; + if (callback && listener.callback != callback) { + needRemove = false; + } + if (target && listener.target != target) { + needRemove = false; + } + needRemove && needRemoveIds.push(id); + } + if (needRemoveIds.length > 0) { + for (const id of needRemoveIds) { + this._remove(id); + } + } + } + + public removeByNameAndTarget(name: string, target: any): void { + this.remove(name, null, target); + } + + public removeByNameAndCallback(name: string, callback: () => void): void { + this.remove(name, callback, null); + } + + /** + * 移除与指定目标关联的所有监听器。 + * 如果目标不存在或关联的监听器ID集合为空,则不执行任何操作。 + * 对于每个监听器ID,从_idToEvent映射中删除监听器,并将其回收到工厂中。 + * 同时,更新_nameToIds映射,确保名称到ID集合的映射保持最新。 + * @param target - 要移除监听器的目标对象。 + */ + public removeList(target: any): void { + let targetToIds = this._targetToIds; + if (!targetToIds.has(target)) { + return; + } + let ids = targetToIds.get(target); + if (ids.size == 0) { + return; + } + for (const id of ids.values()) { + let listener = this._idToEvent.get(id); + let name = listener.name; + + this._idToEvent.delete(id); + this._factroy.recycle(listener); + + let nameToIds = this._nameToIds; + if (nameToIds.has(name)) { + nameToIds.get(name).delete(id); + } + } + ids.clear(); + } + + public destroyAll(): void { + let listeners = this._idToEvent; + for (const listener of listeners.values()) { + this._factroy.recycle(listener); + } + this._idToEvent.clear(); + this._nameToIds.clear(); + this._targetToIds.clear(); + } + + public _addEvent(name: string, callback: (...arg: any[]) => void, once: boolean, target: any): void { + let listener = this._factroy.allocate(); + listener.name = name; + listener.target = target; + listener.once = once; + listener.callback = callback; + this._idToEvent.set(listener.id, listener); + + let nameToIds = this._nameToIds; + let ids: Set; + if (nameToIds.has(name)) { + ids = nameToIds.get(name); + } else { + ids = new Set(); + nameToIds.set(name, ids); + } + ids.add(listener.id); + if (target) { + let targetToIds = this._targetToIds; + if (!targetToIds.has(target)) { + let ids = new Set(); + ids.add(listener.id); + targetToIds.set(target, ids); + } else { + let ids = targetToIds.get(target); + ids.add(listener.id); + } + } + } + + private _remove(id: number): void { + if (!this._idToEvent.has(id)) { + return; + } + let ids = this._idToEvent.get(id); + let name = ids.name; + let target = ids.target; + + this._idToEvent.delete(id); + this._factroy.recycle(ids); + + let nameToIds = this._nameToIds; + if (nameToIds.has(name)) { + nameToIds.get(name).delete(id); + } + if (target) { + let targetToIds = this._targetToIds; + if (targetToIds.has(target)) { + targetToIds.get(target).delete(id); + } + } + } +} \ No newline at end of file diff --git a/src/fgui/Window.ts b/src/fgui/Window.ts new file mode 100644 index 0000000..83798e7 --- /dev/null +++ b/src/fgui/Window.ts @@ -0,0 +1,70 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-14 + * @Description: + */ +import { WindowBase } from "./WindowBase"; + +export abstract class Window extends WindowBase { + protected onAdapted(): void { + + } + /** + * 初始化窗口时调用的方法。 + * 子类必须实现的方法,用来设置窗口的属性。 + */ + protected abstract onInit(): void + /** + * 窗口关闭时的处理逻辑。 + * 子类可以重写此方法以实现自定义的关闭行为。 + */ + protected onClose(): void { + + } + + /** + * 窗口显示时的回调函数。 + * @param userdata 可选参数,传递给窗口显示时的用户数据。 + */ + protected onShow(userdata?: any): void { + + } + /** + * 隐藏窗口时的处理逻辑。 + * 重写此方法以实现自定义的隐藏行为。 + */ + protected onHide(): void { + + } + + /** + * 当窗口从隐藏状态变为显示状态时调用。 + * 这个方法可以被子类重写以实现特定的显示逻辑。 + */ + protected onShowFromHide(): void { + + } + + /** + * 当窗口被覆盖时触发的事件处理函数。 + * 子类可以重写此方法以添加自定义行为。 + */ + protected onCover(): void { + + } + /** + * 恢复窗口状态时的处理逻辑。 + * 此方法在窗口从隐藏或最小化状态恢复时被调用。 + */ + protected onRecover(): void { + + } + + /** + * 空白区域点击事件处理函数。 + * 当用户点击窗口的空白区域时触发此方法。 + */ + protected onEmptyAreaClick(): void { + + } +} \ No newline at end of file diff --git a/src/fgui/WindowBase.ts b/src/fgui/WindowBase.ts new file mode 100644 index 0000000..1aac463 --- /dev/null +++ b/src/fgui/WindowBase.ts @@ -0,0 +1,155 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-14 + * @Description: 窗口基类 + */ + +import { GComponent } from "fairygui-cc"; +import { Screen } from "../global/Screen"; +import { AdapterType, WindowType } from "../ui/header"; +import { IWindow } from "../ui/IWindow"; +import { IWindowHeader } from "../ui/IWindowHeader"; +import { WindowHeaderInfo } from "../ui/WindowHeaderInfo"; +import { WindowHeader } from "./WindowHeader"; + +export abstract class WindowBase extends GComponent implements IWindow { + /** 窗口类型 */ + public type: WindowType = WindowType.Normal; + /** 窗口适配类型 */ + public adapterType: AdapterType = AdapterType.Full; + /** 底部遮罩的透明度 */ + public bgAlpha: number; + /** header (内部使用) */ + private _header: IWindowHeader = null; + /** 窗口是否被遮挡了 */ + private _isCover: boolean = false; + /** + * 初始化方法 (框架内部使用) + * @param swallowTouch 是否吞噬触摸事件 + */ + public _init(swallowTouch: boolean, bgAlpha: number): void { + if (swallowTouch) { + // 吞噬触摸事件,需要一个全屏的节点, 窗口本身可能留有安全区的边 + let bgNode = new GComponent(); + bgNode.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + bgNode.setPivot(0.5, 0.5, true); + bgNode.setPosition(Screen.ScreenWidth * 0.5, Screen.ScreenHeight * 0.5); + this.addChild(bgNode); + // 调整显示层级 + bgNode.parent.setChildIndex(bgNode, 0); + bgNode.onClick(this.onEmptyAreaClick, this); + bgNode.opaque = swallowTouch; + } + // 窗口自身也要设置是否吞噬触摸 + this.opaque = swallowTouch; + this.bgAlpha = bgAlpha; + this.onInit(); + } + + public _adapted(): void { + this.setPosition(Screen.ScreenWidth * 0.5, Screen.ScreenHeight * 0.5); + this.setPivot(0.5, 0.5, true); + switch (this.adapterType) { + case AdapterType.Full: + this.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + break; + case AdapterType.Bang: + this.setSize(Screen.SafeWidth, Screen.SafeHeight, true); + break; + default: + break; + } + this.onAdapted(); + } + + /** + * 窗口关闭 (框架内部使用) + */ + public _close(): void { + this.onClose(); + this.dispose(); + } + /** + * 显示窗口 (框架内部使用) + * @param userdata 用户自定义数据 + */ + public _show(userdata?: any): void { + this.visible = true; + this.onShow(userdata); + } + /** + * 隐藏窗口 (框架内部使用) + */ + public _hide(): void { + this.visible = false; + this.onHide(); + } + /** + * 从隐藏状态恢复显示 + */ + public _showFromHide(): void { + this.visible = true; + this.onShowFromHide(); + } + + /** + * 遮挡窗口 被同组或者不同组的其他窗口覆盖 (框架内部使用) + */ + public _cover(): void { + this._isCover = true; + this.onCover(); + } + /** + * 遮挡恢复窗口 被同组或者不同组的其他窗口覆盖恢复 (框架内部使用) + */ + public _recover(): void { + this._isCover = false; + this.onRecover(); + } + + public _setDepth(depth: number): void { + this.parent.setChildIndex(this, depth); + } + + public isShowing(): boolean { + return this.visible; + } + + public isCover(): boolean { + return this._isCover; + } + + public screenResize(): void { + this._adapted(); + } + + /** + * 获取窗口顶部资源栏数据 默认返回空数组 + * @returns {WindowHeaderInfo[]} + */ + public getHeaderInfo(): WindowHeaderInfo { + return null; + } + + public getHeader(): T | null { + return this._header as T; + } + + public setHeader(header: T): void { + this._header = header; + } + + protected abstract onAdapted(): void; + + protected abstract onInit(): void; + protected abstract onClose(): void; + + protected abstract onShow(userdata?: any): void; + protected abstract onShowFromHide(): void; + protected abstract onHide(): void; + + protected abstract onCover(): void; + protected abstract onRecover(): void; + + protected abstract onEmptyAreaClick(): void; +} \ No newline at end of file diff --git a/src/fgui/WindowHeader.ts b/src/fgui/WindowHeader.ts new file mode 100644 index 0000000..9f43669 --- /dev/null +++ b/src/fgui/WindowHeader.ts @@ -0,0 +1,97 @@ +/** + * @Author: Gongxh + * @Date: 2025-01-11 + * @Description: 窗口顶边栏 + * 窗口顶边资源栏 同组中只会有一个显示 + */ + +import { GComponent } from "fairygui-cc"; +import { Screen } from "../global/Screen"; +import { AdapterType } from "../kunpocc"; +import { IWindow } from "../ui/IWindow"; +import { IWindowHeader } from "../ui/IWindowHeader"; + + +export abstract class WindowHeader extends GComponent implements IWindowHeader { + /** 窗口适配类型 */ + public adapterType: AdapterType = AdapterType.Full; + /** 引用计数 */ + public _refCount: number = 0; + + protected abstract onInit(): void; + protected abstract onShow(window: IWindow, userdata?: any): void; + protected abstract onClose(): void; + + protected onHide(): void { + + } + protected onAdapted(): void { + + } + + /** + * 初始化 (内部方法) + */ + public _init(): void { + this.onInit(); + } + + /** + * 窗口适配 (内部方法) + */ + public _adapted(): void { + this.setPosition(Screen.ScreenWidth * 0.5, Screen.ScreenHeight * 0.5); + this.setPivot(0.5, 0.5, true); + switch (this.adapterType) { + case AdapterType.Full: + this.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + break; + case AdapterType.Bang: + this.setSize(Screen.SafeWidth, Screen.SafeHeight, true); + break; + default: + break; + } + this.onAdapted(); + } + + /** + * 显示 (内部方法) + * @param {IWindow} window 所属窗口 + */ + public _show(window: IWindow): void { + this.visible = true; + this.onShow(window, window.getHeaderInfo()?.userdata); + } + + /** + * 隐藏 (内部方法) + */ + public _hide(): void { + this.visible = false; + this.onHide(); + } + + /** + * 关闭 (内部方法) + */ + public _close(): void { + this.onClose(); + this.dispose(); + } + + /** 增加引用计数 (内部方法) */ + public _addRef(): void { + this._refCount++; + } + + /** 减少引用计数 (内部方法) */ + public _decRef(): number { + return --this._refCount; + } + + /** 屏幕大小改变时被调用 (内部方法) */ + public _screenResize(): void { + this._adapted(); + } +} \ No newline at end of file diff --git a/src/global/Adapter.ts b/src/global/Adapter.ts new file mode 100644 index 0000000..f4180b7 --- /dev/null +++ b/src/global/Adapter.ts @@ -0,0 +1,85 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 适配用的类 + */ + +import { ResolutionPolicy, view } from "cc"; +import { info } from "../tool/log"; +import { WindowManager } from "../ui/WindowManager"; +import { Screen } from "./Screen"; +import { size } from "./header"; + +export abstract class Adapter { + public init() { + // 设计尺寸 不会变化 + let designSize = this.getDesignSize(); + Screen.DesignHeight = designSize.height; + Screen.DesignWidth = designSize.width; + view.setDesignResolutionSize(Screen.DesignWidth, Screen.DesignWidth, ResolutionPolicy.SHOW_ALL); + + this.resize(); + this.registerResizeCallback((...args: any) => { + info("屏幕发生变化", ...args); + this.resize(); + }); + } + + protected resize(): void { + Screen.SafeAreaHeight = 60; + // 屏幕像素尺寸 + const winSize = this.getScreenSize(); + const isDesignLandscape = Screen.DesignWidth > Screen.DesignHeight; + const isLandscape = winSize.width > winSize.height; + if (isDesignLandscape == isLandscape) { + Screen.ScreenWidth = winSize.width; + Screen.ScreenHeight = winSize.height; + } else { + Screen.ScreenWidth = winSize.height; + Screen.ScreenHeight = winSize.width; + } + if (isDesignLandscape) { + // 横屏 + /** 安全区的宽度 */ + Screen.SafeWidth = Screen.ScreenWidth - Screen.SafeAreaHeight * 2; + /** 安全区的高度 */ + Screen.SafeHeight = Screen.ScreenHeight; + } else { + // 竖屏 + /** 安全区的宽度 */ + Screen.SafeWidth = Screen.ScreenWidth; + /** 安全区的高度 */ + Screen.SafeHeight = Screen.ScreenHeight - Screen.SafeAreaHeight * 2; + } + WindowManager._screenResize(); + this.printScreen(); + } + + private printScreen() { + info(`设计分辨率: ${Screen.DesignWidth}x${Screen.DesignHeight}`); + info(`屏幕分辨率: ${Screen.ScreenWidth}x${Screen.ScreenHeight}`); + info(`安全区域高度: ${Screen.SafeAreaHeight}`); + info(`安全区宽高: ${Screen.SafeWidth}x${Screen.SafeHeight}`); + } + + /** + * 获取屏幕尺寸 + * @abstract 子类实现 + * @returns {size} + */ + protected abstract getScreenSize(): size; + + /** + * 获取设计尺寸 + * @abstract 子类实现 + * @returns {size} + */ + protected abstract getDesignSize(): size; + + /** + * 设置尺寸发生变化的监听 + * @abstract 子类实现 + * @param callback + */ + protected abstract registerResizeCallback(callback: () => void): void; +} \ No newline at end of file diff --git a/src/global/GlobalEvent.ts b/src/global/GlobalEvent.ts new file mode 100644 index 0000000..4121624 --- /dev/null +++ b/src/global/GlobalEvent.ts @@ -0,0 +1,44 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-22 + * @Description: 全局事件 + */ + +import { EventManager } from "../event/EventManager"; + +export class GlobalEvent { + private static _globalEvent: EventManager = null; + public static add(eventName: string, callback: (...args: any[]) => void, target: any): void { + this._globalEvent.addEvent(eventName, callback, target); + } + + public static addOnce(eventName: string, callback: (...args: any[]) => void, target: any): void { + this._globalEvent.addEventOnce(eventName, callback, target); + } + + public static send(eventName: string, ...args: any[]): void { + this._globalEvent.send(eventName, null, ...args); + } + + public static sendToTarget(eventName: string, target: any, ...args: any[]) { + this._globalEvent.send(eventName, target, ...args); + } + + public static remove(eventName: string, callback: (...args: any[]) => void, target?: any): void { + this._globalEvent.remove(eventName, callback, target); + } + + public static removeByNameAndTarget(eventName: string, target: any) { + this._globalEvent.removeByNameAndTarget(eventName, target); + } + + public static removeByTarget(target: any): void { + this._globalEvent.removeList(target); + } + + public static _initGlobalEvent(): void { + if (!this._globalEvent) { + this._globalEvent = new EventManager(); + } + } +} \ No newline at end of file diff --git a/src/global/GlobalTimer.ts b/src/global/GlobalTimer.ts new file mode 100644 index 0000000..4d7b6db --- /dev/null +++ b/src/global/GlobalTimer.ts @@ -0,0 +1,76 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: + */ + +import { Timer } from "../tool/timer/Timer"; + +export class GlobalTimer { + private static _timer: Timer = null; + /** + * 初始化全局定时器,设置定时器间隔为16毫秒。 + * 此方法用于启动一个定时器实例,以便在整个应用程序中跟踪时间相关的操作。 + */ + public static initTimer(): void { + this._timer = new Timer(16); + } + + /** + * 获取全局定时器实例。如果定时器尚未初始化,则进行初始化。 + * @returns {Timer} 全局定时器实例 + */ + public static get Timer(): Timer { + if (this._timer) { + return this._timer; + } + this.initTimer(); + return this._timer; + } + + /** + * 启动一个定时器,执行指定的回调函数。 + * @param callback - 要定时执行的回调函数。 + * @param interval - 定时器的时间间隔(秒)。 + * @param loop - [loop=0] 重复次数:0:回调一次,1~n:回调n次,-1:无限重复 + * @returns 返回定时器的ID。 + */ + public static startTimer(callback: () => void, interval: number, loop: number = 0): number { + return this.Timer.start(callback, interval, loop); + } + + /** + * 停止指定ID的计时器。 + * @param timerId - 要停止的计时器的唯一标识符。 + */ + public static stopTimer(timerId: number): void { + this.Timer.stop(timerId); + } + + /** + * 暂停指定ID的计时器。 + * @param timerId - 要暂停的计时器的唯一标识符。 + */ + public static pauseTimer(timerId: number): void { + this.Timer.pause(timerId); + } + + /** + * 恢复指定ID的计时器。 + * @param timerId - 要恢复的计时器的唯一标识符。 + */ + public static resumeTimer(timerId: number): void { + this.Timer.resume(timerId); + } + + /** + * 清除所有定时器。 + */ + public static clearAllTimer(): void { + this.Timer.clear(); + } + + public static update(dt: number): void { + this._timer?.update(dt); + } +} \ No newline at end of file diff --git a/src/global/IModule.ts b/src/global/IModule.ts new file mode 100644 index 0000000..45766da --- /dev/null +++ b/src/global/IModule.ts @@ -0,0 +1,13 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 模块接口 + */ + +export interface IModule { + /** 模块名称 */ + moduleName: string; + + /** 模块初始化 */ + init(): void; +} \ No newline at end of file diff --git a/src/global/InnerTimer.ts b/src/global/InnerTimer.ts new file mode 100644 index 0000000..240494a --- /dev/null +++ b/src/global/InnerTimer.ts @@ -0,0 +1,40 @@ +/** + * @Author: Gongxh + * @Date: 2025-02-14 + * @Description: 内部使用的全局定时器 + */ +import { Timer } from "../tool/timer/Timer"; + +export class InnerTimer { + private static _timer: Timer = null; + /** + * 初始化全局定时器,设置定时器间隔为16毫秒。 + * 此方法用于启动一个定时器实例,以便在整个应用程序中跟踪时间相关的操作。 + */ + public static initTimer(): void { + this._timer = new Timer(16); + } + + /** + * 启动一个定时器,执行指定的回调函数。 + * @param callback - 要定时执行的回调函数。 + * @param interval - 定时器的时间间隔(秒)。 + * @param loop - [loop=0] 重复次数:0:回调一次,1~n:回调n次,-1:无限重复 + * @returns 返回定时器的ID。 + */ + public static startTimer(callback: () => void, interval: number, loop: number = 0): number { + return this._timer.start(callback, interval, loop); + } + + /** + * 停止指定ID的计时器。 + * @param timerId - 要停止的计时器的唯一标识符。 + */ + public static stopTimer(timerId: number): void { + this._timer.stop(timerId); + } + + public static update(dt: number): void { + this._timer?.update(dt); + } +} \ No newline at end of file diff --git a/src/global/Platform.ts b/src/global/Platform.ts new file mode 100644 index 0000000..0db460d --- /dev/null +++ b/src/global/Platform.ts @@ -0,0 +1,101 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 平台相关 + */ + +export enum PlatformType { + Android = 1, + IOS, + HarmonyOS, + /** 微信小游戏 */ + WX, + /** 支付宝小游戏 */ + Alipay, + /** 字节小游戏 */ + Bytedance, + /** 华为快游戏 */ + HuaweiQuick, + /** 其他都为Browser */ + Browser, +} + +export class Platform { + /** + * 是否为原生平台 + * @type {boolean} + */ + public static isNative: boolean = false; + + /** + * 是否为移动平台 + * @type {boolean} + */ + public static isMobile: boolean = false; + + /** + * 是否为原生移动平台 + * @type {boolean} + */ + public static isNativeMobile: boolean = false; + + /** + * 是否为安卓平台 + * @type {boolean} + */ + public static isAndroid: boolean = false; + + /** + * 是否为IOS平台 + * @type {boolean} + */ + public static isIOS: boolean = false; + + /** + * 是否为HarmonyOS平台 + * @type {boolean} + */ + public static isHarmonyOS: boolean = false; + + /** + * 是否为微信小游戏 + * @type {boolean} + */ + public static isWX: boolean = false; + + /** + * 是否为支付宝小游戏 + * @type {boolean} + */ + public static isAlipay: boolean = false; + + /** + * 是否为字节小游戏 + * @type {boolean} + */ + public static isBytedance: boolean = false; + + /** + * 是否是华为快游戏 + * @type {boolean} + */ + public static isHuaweiQuick: boolean = false; + + /** + * 是否为浏览器 + * @type {boolean} + */ + public static isBrowser: boolean = false; + + /** + * 平台名 + * @type {string} + */ + public static platform: PlatformType; + + /** + * 设备ID + * @type {string} + */ + public static deviceId: string; +} \ No newline at end of file diff --git a/src/global/Screen.ts b/src/global/Screen.ts new file mode 100644 index 0000000..5589c20 --- /dev/null +++ b/src/global/Screen.ts @@ -0,0 +1,21 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: 屏幕尺寸信息接口 + */ +export class Screen { + /** 屏幕宽度 */ + public static ScreenWidth: number; + /** 屏幕高度 */ + public static ScreenHeight: number; + /** 设计分辨率宽 */ + public static DesignWidth: number; + /** 设计分辨率高 */ + public static DesignHeight: number; + /** 安全区外一侧的高度 或 宽度 */ + public static SafeAreaHeight: number; + /** 安全区的宽度 */ + public static SafeWidth: number; + /** 安全区的高度 */ + public static SafeHeight: number; +} \ No newline at end of file diff --git a/src/global/header.ts b/src/global/header.ts new file mode 100644 index 0000000..f51ea13 --- /dev/null +++ b/src/global/header.ts @@ -0,0 +1,31 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: 一些数据结构 + */ + +import { warn } from "../tool/log"; + +export interface size { + width: number; + height: number; +} + +export interface FrameConfig { + /** 开启debug 默认: false */ + debug?: boolean; +} + +export let KUNPO_DEBUG: boolean = false; +/** + * 启用或禁用调试模式。 + * @param enable - 如果为 true,则启用调试模式;如果为 false,则禁用调试模式。不设置默认不开启 + */ +export function enableDebugMode(enable: boolean): void { + if (enable == true) { + KUNPO_DEBUG = true; + warn("调试模式已开启"); + } else { + KUNPO_DEBUG = false; + } +} \ No newline at end of file diff --git a/src/kunpocc.ts b/src/kunpocc.ts new file mode 100644 index 0000000..d392bdc --- /dev/null +++ b/src/kunpocc.ts @@ -0,0 +1,74 @@ +/** 一些全局工具 */ +export { GlobalEvent } from "./global/GlobalEvent"; +export { GlobalTimer } from "./global/GlobalTimer"; +export { enableDebugMode, FrameConfig, KUNPO_DEBUG } from "./global/header"; +export { Platform, PlatformType } from "./global/Platform"; +export { Screen } from "./global/Screen"; + +/** tool */ +export * from "./tool/log"; +export { MathTool } from "./tool/Math"; +export { md5 } from "./tool/MD5"; + +/** 消息监听 */ +export { EventManager } from "./event/EventManager"; + +/** 网络 */ +export * from "./net/http/HttpManager"; +export { HttpTask } from "./net/http/HttpTask"; +export { IHttpEvent } from "./net/http/IHttpEvent"; +export { IHttpRequest } from "./net/http/IHttpRequest"; +export { IHttpResponse } from "./net/http/IHttpResponse"; + +/** 四叉树 */ +export { Box } from "./quadtree/Box"; +export { Circle } from "./quadtree/Circle"; +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"; +export * from "./ui/header"; +export { _uidecorator } from "./ui/UIDecorator"; +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"; +export { ConditionModule } from "./condition/ConditionModule"; +export { ConditionAllNode } from "./condition/node/ConditionAllNode"; +export { ConditionAnyNode } from "./condition/node/ConditionAnyNode"; +export { ConditionBase } from "./condition/node/ConditionBase"; + diff --git a/src/module/ModuleBase.ts b/src/module/ModuleBase.ts new file mode 100644 index 0000000..6752cbb --- /dev/null +++ b/src/module/ModuleBase.ts @@ -0,0 +1,20 @@ + +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 模块基类 + */ + +import { Component } from "cc"; +import { IModule } from "../global/IModule"; + +export abstract class ModuleBase extends Component implements IModule { + /** 模块名称 */ + public moduleName: string; + + /** 模块初始化 (内部使用) */ + public init(): void { } + + /** 模块初始化完成后调用的函数 */ + protected abstract onInit(): void; +} diff --git a/src/net/http/HttpManager.ts b/src/net/http/HttpManager.ts new file mode 100644 index 0000000..9de4379 --- /dev/null +++ b/src/net/http/HttpManager.ts @@ -0,0 +1,110 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络请求管理器 + */ + +import { GlobalEvent } from "../../global/GlobalEvent"; +import { HttpRequest } from "./HttpRequest"; +import { IHttpEvent } from "./IHttpEvent"; +import { IHttpResponse } from "./IHttpResponse"; + +/** http请求方法 */ +export type HttpRequestMethod = "GET" | "POST" | "HEAD" | "PUT" +/** http响应类型 */ +export type HttpResponseType = "text" | "json" | "arraybuffer"; +/** http响应数据类型 */ +export type HttpResponseDataType = string | ArrayBuffer | object; + +export class HttpManager { + public static HttpEvent: string = "event::http"; + + /** + * 发送post请求 + * @param {string} url 请求地址 + * @param {any} data 请求数据 + * @param {HttpResponseType} responseType 响应类型 + * @param {IHttpEvent} netEvent 网络事件 + * @param {any[]} headers 请求头 [key1, value1, key2, value2, ...] 形式 + * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) + */ + public static post(url: string, data: any, responseType: HttpResponseType = "json", netEvent: IHttpEvent, headers?: any[], timeout: number = 0): HttpRequest { + return this._send("POST", url, data, responseType, netEvent, headers, timeout); + } + + /** + * 发送get请求 + * @param {string} url 请求地址 + * @param {any} data 请求数据 + * @param {HttpResponseType} responseType 响应类型 + * @param {IHttpEvent} netEvent 网络事件 + * @param {any[]} headers 请求头 [key1, value1, key2, value2, ...] 形式 + * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) + */ + public static get(url: string, data: any, responseType: HttpResponseType = "json", netEvent: IHttpEvent, headers?: any[], timeout: number = 0): HttpRequest { + return this._send("GET", url, data, responseType, netEvent, headers, timeout); + } + + /** + * 发送put请求 + * @param {string} url 请求地址 + * @param {any} data 请求数据 + * @param {HttpResponseType} responseType 响应类型 + * @param {IHttpEvent} netEvent 网络事件 + * @param {any[]} headers 请求头 [key1, value1, key2, value2, ...] 形式 + * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) + */ + public static put(url: string, data: any, responseType: HttpResponseType = "json", netEvent: IHttpEvent, headers?: any[], timeout: number = 0): HttpRequest { + return this._send("PUT", url, data, responseType, netEvent, headers, timeout); + } + + /** + * 发送head请求 + * @param {string} url 请求地址 + * @param {any} data 请求数据 + * @param {HttpResponseType} responseType 响应类型 + * @param {IHttpEvent} netEvent 网络事件 + * @param {any[]} headers 请求头 [key1, value1, key2, value2, ...] 形式 + * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) + */ + public static head(url: string, data: any, responseType: HttpResponseType = "json", netEvent: IHttpEvent, headers?: any[], timeout: number = 0): HttpRequest { + return this._send("HEAD", url, data, responseType, netEvent, headers, timeout); + } + + /** + * 发送http请求 + * @param {HttpRequestMethod} method 请求方式 + * @param {string} url 请求地址 + * @param {any} data 请求数据 + * @param {HttpResponseType} responseType 响应类型 + * @param {IHttpEvent} netEvent 网络事件 + * @param {any[]} headers 请求头 [key1, value1, key2, value2, ...] 形式 + * @param {number} timeout (单位s) 请求超时时间 默认0 (0表示不超时) + */ + 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); + } + break; + case "fail": + if (netEvent) { + netEvent.onError(response); + } else { + GlobalEvent.send(HttpManager.HttpEvent, result, response); + } + break; + } + }); + http.method = method; + http.timeout = timeout; + http.responseType = responseType; + http.send(url, data, headers); + return http; + } +} \ No newline at end of file diff --git a/src/net/http/HttpRequest.ts b/src/net/http/HttpRequest.ts new file mode 100644 index 0000000..a2fabea --- /dev/null +++ b/src/net/http/HttpRequest.ts @@ -0,0 +1,151 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络请求 + */ +import { Platform } from "../../global/Platform"; +import { HttpRequestMethod, HttpResponseDataType, HttpResponseType } from "./HttpManager"; +import { IHttpRequest } from "./IHttpRequest"; +import { IHttpResponse } from "./IHttpResponse"; + +export class HttpRequest implements IHttpRequest, IHttpResponse { + /** 请求方法 */ + public method: HttpRequestMethod; + /** xhr实例 */ + private _xhr: XMLHttpRequest; + /** 请求超时时间 (s) */ + public timeout: number; + /** 响应类型 */ + public responseType: HttpResponseType; + /** 信息 */ + public message: string; + /** 响应数据 */ + public data: HttpResponseDataType; + + /** 网络事件回调 */ + private _callback: (result: "succeed" | "fail", response: IHttpResponse) => void; + + /** + * http相应状态码 + * @readonly + * @type {number} + */ + public get statusCode(): number { + return this._xhr.status; + } + + /** 相应头 */ + public get headers(): any { + return this._xhr.getAllResponseHeaders(); + } + + constructor() { + this._xhr = new XMLHttpRequest(); + } + + public setNetCallback(callback: (result: "succeed" | "fail", response: IHttpResponse) => void): void { + this._callback = callback; + } + + public send(url: string, data: any, headers: any[]): void { + let xhr = this._xhr; + /** 设置请求超时时间 */ + xhr.timeout = this.timeout * 1000; + /** 设置响应类型 */ + xhr.responseType = this.responseType; + xhr.onabort = this._onHttpAbort.bind(this); + xhr.onerror = this._onHttpError.bind(this); + xhr.onload = this._onHttpLoad.bind(this); + xhr.ontimeout = this._onHttpTimeout.bind(this); + xhr.open(this.method, encodeURI(url)); + if (headers) { + for (let i = 0; i < headers.length; i += 2) { + xhr.setRequestHeader(headers[i], headers[i + 1]); + } + } else if (!Platform.isMobile && Platform.isBrowser) { + if (!data || typeof data == "string") { + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + } else { + xhr.setRequestHeader("Content-Type", "application/json"); + } + } + xhr.send(data); + } + + /** + * 终止Http请求 + * @param {boolean} [silent=false] 如果为true则不会回调错误信息 + */ + public abort(silent: boolean = false): void { + if (silent) { + this._clear(); + } + this._xhr.abort(); + } + + /** + * 请求中断 + * + */ + private _onHttpAbort(): void { + this.message = "request aborted by user"; + this.onError(); + } + + private _onHttpError(): void { + this.message = "request error"; + this.onError(); + } + + private _onHttpLoad(): void { + const xhr = this._xhr; + const status = xhr.status !== undefined ? xhr.status : 200; + if (status === 200 || status === 204 || status === 0) { + this.onComplete(); + } else { + this.message = 'status:' + xhr.status + 'statusText:' + xhr.statusText + "responseURL:" + xhr.responseURL; + this.onError(); + } + } + + private _onHttpTimeout(): void { + this.message = "request timeout"; + this.onError(); + } + + /** + * 请求发生错误 + */ + private onError(): void { + this._callback?.("fail", this); + this._clear(); + } + + /** + * 请求完成 + */ + private onComplete(): void { + try { + if (this.responseType == "json") { + this.data = this._xhr.response; + } else if (this.responseType == "arraybuffer") { + this.data = this._xhr.response; + } else if (this.responseType == "text") { + this.data = this._xhr.responseText; + } + this._callback?.("succeed", this); + this._clear(); + } catch (e) { + console.warn(`http响应数据解析错误,HttpResponseType(${this.responseType})\n url: ${this._xhr.responseURL}\n error: ` + e); + this.onError(); + } + } + + private _clear(): void { + this._xhr.onabort = null; + this._xhr.onerror = null; + this._xhr.onload = null; + this._xhr.ontimeout = null; + this._callback = null; + } +} \ No newline at end of file diff --git a/src/net/http/HttpTask.ts b/src/net/http/HttpTask.ts new file mode 100644 index 0000000..ca94f30 --- /dev/null +++ b/src/net/http/HttpTask.ts @@ -0,0 +1,21 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络任务 + */ + +import { IHttpEvent } from "./IHttpEvent"; +import { IHttpResponse } from "./IHttpResponse"; + +export abstract class HttpTask implements IHttpEvent { + /** 名称 */ + public name: string; + /** 自定义参数 */ + public data?: any; + /** 请求完成 */ + public abstract onComplete(response: IHttpResponse): void; + /** 请求错误 */ + public abstract onError(response: IHttpResponse): void; + /** 请求开始 */ + public abstract start(): void; +} \ No newline at end of file diff --git a/src/net/http/IHttpEvent.ts b/src/net/http/IHttpEvent.ts new file mode 100644 index 0000000..7127946 --- /dev/null +++ b/src/net/http/IHttpEvent.ts @@ -0,0 +1,18 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络事件 + */ + +import { IHttpResponse } from "./IHttpResponse"; + +export interface IHttpEvent { + /** 名称 */ + name: string; + /** 自定义参数 */ + data?: any; + /** 网络请求成功 */ + onComplete(response: IHttpResponse): void; + /** 网络请求失败 */ + onError(response: IHttpResponse): void; +} \ No newline at end of file diff --git a/src/net/http/IHttpRequest.ts b/src/net/http/IHttpRequest.ts new file mode 100644 index 0000000..6b4223d --- /dev/null +++ b/src/net/http/IHttpRequest.ts @@ -0,0 +1,15 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络请求接口 + */ + +import { HttpRequestMethod, HttpResponseType } from "./HttpManager"; +export interface IHttpRequest { + /** 请求方法 */ + readonly method: HttpRequestMethod; + /** 请求超时时间 (s) */ + readonly timeout: number; + /** 响应类型 */ + readonly responseType: HttpResponseType; +} \ No newline at end of file diff --git a/src/net/http/IHttpResponse.ts b/src/net/http/IHttpResponse.ts new file mode 100644 index 0000000..f63532e --- /dev/null +++ b/src/net/http/IHttpResponse.ts @@ -0,0 +1,20 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-28 + * @Description: 网络响应接口 + */ + +import { HttpResponseDataType } from "./HttpManager"; +export interface IHttpResponse { + /** 信息 */ + readonly message: string; + + /** 响应数据 */ + readonly data: HttpResponseDataType; + + /** http状态码 */ + readonly statusCode: number; + + /** 相应头 */ + readonly headers: any; +} \ No newline at end of file diff --git a/src/quadtree/Box.ts b/src/quadtree/Box.ts new file mode 100644 index 0000000..09d7b57 --- /dev/null +++ b/src/quadtree/Box.ts @@ -0,0 +1,32 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: 矩形 + */ +import { v2, Vec2 } from "cc"; +import { Polygon } from "./Polygon"; + +// 3|2 +// -- +// 0|1 +// 矩形的四个点 + +export class Box extends Polygon { + 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 new file mode 100644 index 0000000..205408d --- /dev/null +++ b/src/quadtree/Circle.ts @@ -0,0 +1,28 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: 原型 + */ + +import { Graphics, Rect } from "cc"; +import { Shape } from "./Shape"; + +export class Circle extends Shape { + public radius: number; // 半径 + 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 drawShape(draw: Graphics): void { + draw && draw.circle(this.position.x, this.position.y, this.radius * this.scale); + } +} \ No newline at end of file diff --git a/src/quadtree/Polygon.ts b/src/quadtree/Polygon.ts new file mode 100644 index 0000000..bbfaf3a --- /dev/null +++ b/src/quadtree/Polygon.ts @@ -0,0 +1,96 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: 多边形 + */ + +import { Graphics, Rect, v2, Vec2 } from "cc"; +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[]; + 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); + } + } + + public drawShape(draw: Graphics): void { + if (draw) { + let points = this.points; + draw.moveTo(points[0].x, points[0].y); + for (let i = 1; i < points.length; i++) { + draw.lineTo(points[i].x, points[i].y); + } + draw.lineTo(points[0].x, points[0].y); + } + } +} \ No newline at end of file diff --git a/src/quadtree/QuadTree.ts b/src/quadtree/QuadTree.ts new file mode 100644 index 0000000..d5adf77 --- /dev/null +++ b/src/quadtree/QuadTree.ts @@ -0,0 +1,341 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: 树节点 + */ + +import { Color, Graphics, Intersection2D, rect, Rect } from "cc"; +import { Box } from "./Box"; +import { Circle } from "./Circle"; +import { Polygon } from "./Polygon"; +import { Shape } from "./Shape"; + +// 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: Shape, shape2: Shape): boolean { + if (shape1 instanceof Circle) { + if (shape2 instanceof Circle) { + return circleCircle(shape1.position, shape1.radius * shape1.scale, shape2.position, shape2.radius * shape2.scale); + } else if (shape2 instanceof Box || shape2 instanceof Polygon) { + return polygonCircle(shape2.points, shape1.position, shape1.radius * shape1.scale); + } + } else if (shape1 instanceof Box || shape1 instanceof Polygon) { + if (shape2 instanceof Circle) { + return polygonCircle(shape1.points, shape2.position, shape2.radius * shape2.scale); + } else if (shape2 instanceof Box || shape2 instanceof Polygon) { + return polygonPolygon(shape2.points, shape1.points); + } + } + return false; +} + +export const QTConfig = { + /** 每个节点(象限)所能包含物体的最大数量 */ + MAX_SHAPES: 12, + /** 四叉树的最大深度 */ + MAX_LEVELS: 5, +} + +export class QuadTree { + private _draw: Graphics; + private _shapes_map: Map; // 根据类型存储形状对象 + private _trees: QuadTree[] = []; // 存储四个子节点 + private _level: number; // 树的深度 + private _bounds: Rect; // 树的外框 + private _ignore_shapes: Shape[] = []; // 不在树中的形状 + /** + * 创建一个四叉树 + * @param rect 该节点对应的象限在屏幕上的范围 + * @param level 该节点的深度,根节点的默认深度为0 + * @param draw cc中用于绘制树的绘制组件 + */ + constructor(rect: Rect, level: number = 0, draw: Graphics = undefined) { + this._shapes_map = new Map(); + this._trees = []; + this._level = level || 0; + this._bounds = rect; + this._draw = draw; + } + + /** + * 插入形状 + * @param shape 形状数据 + * 如果当前节点存在子节点,则检查物体到底属于哪个子节点,如果能匹配到子节点,则将该物体插入到该子节点中 + * 如果当前节点不存在子节点,将该物体存储在当前节点。随后,检查当前节点的存储数量,如果超过了最大存储数量,则对当前节点进行划分,划分完成后,将当前节点存储的物体重新分配到四个子节点中。 + */ + public insert(shape: Shape): 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]); + } + } + } + } + } + } + + private _insert(shape: Shape): void { + if (!this._shapes_map.has(shape.tag)) { + this._shapes_map.set(shape.tag, []); + } + this._shapes_map.get(shape.tag).push(shape); + } + + /** + * 检索功能: + * 给出一个物体对象,该函数负责将该物体可能发生碰撞的所有物体选取出来。该函数先查找物体所属的象限,该象限下的物体都是有可能发生碰撞的,然后再递归地查找子象限... + */ + public collide(shape: Shape, tag: number = -1): Shape[] { + let result: any[] = []; + 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 = result.concat(this._trees[i].collide(shape, tag)); + } + } else { + result = result.concat(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.invalid && 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._strokeClear() + this._updateIgnoreShapes(root); + this._updateShapes(root); + // 递归刷新子象限 + for (const tree of this._trees) { + tree.update(root); + } + this._removeChildTree(); + this._drawTreeBound(root); + + if (isRoot && this._draw) { + this._draw.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; + } + + /** 当前形状是否包含在象限内 */ + private _isInner(shape: Shape, 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 形状 + * 右上:象限一 + * 左上:象限二 + * 左下:象限三 + * 右下:象限四 + */ + private _getQuadrant(shape: Shape): 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最大数量 + * 则需要对这个节点进行划分 + * 它的工作就是将一个象限看作一个屏幕,将其划分为四个子象限 + */ + 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._draw), + new QuadTree(rect(x, bounds.center.y, halfwidth, halfheight), nextLevel, this._draw), + new QuadTree(rect(x, y, halfwidth, halfheight), nextLevel, this._draw), + new QuadTree(rect(bounds.center.x, y, halfwidth, halfheight), nextLevel, this._draw) + ); + } + + /** 删除子树 */ + private _removeChildTree(): void { + if (this._trees.length > 0) { + if (this._totalSize() <= 0) { + this._trees.length = 0; + } + } + } + + /** 更新忽略掉的形状 */ + private _updateIgnoreShapes(root: QuadTree): void { + let len = this._ignore_shapes.length; + if (len <= 0) { + return; + } + for (let i = len - 1; i >= 0; i--) { + let shape = this._ignore_shapes[i]; + if (shape.invalid) { + this._ignore_shapes.splice(i, 1); + continue; + } + if (!this._isInner(shape, this._bounds)) { + continue; + } + root.insert(this._ignore_shapes.splice(i, 1)[0]); + } + } + + /** 更新有效的形状 */ + private _updateShapes(root: QuadTree): void { + for (const shapes of this._shapes_map.values()) { + let len = shapes.length; + for (let i = len - 1; i >= 0; i--) { + let shape = shapes[i]; + if (shape.invalid) { + shapes.splice(i, 1); + continue; + } + if (!this._isInner(shape, this._bounds)) { + // 如果矩形不属于该象限,则将该矩形重新插入根节点 + root.insert(shapes.splice(i, 1)[0]); + } else if (this._trees.length > 0) { + // 如果矩形属于该象限且该象限具有子象限,则将该矩形安插到子象限中 + let quadrant = this._getQuadrant(shape); + if (quadrant !== Quadrant.MORE) { + this._trees[quadrant].insert(shapes.splice(i, 1)[0]); + } + } + shape.drawShape(this._draw); + } + } + } + + /** 当前树以及子树中所有的形状数量 */ + private _totalSize(): number { + let size = this._size(); + for (const tree of this._trees) { + size += tree._totalSize(); + } + return size; + } + + private _size(): number { + let size = 0; + for (const shapes of this._shapes_map.values()) { + size += shapes.length; + } + return size + this._ignore_shapes.length; + } + + /** 画出当前树的边界 */ + private _drawTreeBound(root: QuadTree): void { + if (!this._draw) { + return; + } + this._draw.lineWidth = 4; + this._draw.strokeColor = Color.BLUE; + if (this._trees.length > 0) { + this._draw.moveTo(this._bounds.x, this._bounds.center.y); + this._draw.lineTo(this._bounds.x + this._bounds.width, this._bounds.center.y); + + this._draw.moveTo(this._bounds.center.x, this._bounds.y); + this._draw.lineTo(this._bounds.center.x, this._bounds.y + this._bounds.height); + } + if (this == root) { + this._draw.moveTo(this._bounds.xMin, this._bounds.yMin); + this._draw.lineTo(this._bounds.xMax, this._bounds.yMin); + this._draw.lineTo(this._bounds.xMax, this._bounds.yMax); + this._draw.lineTo(this._bounds.xMin, this._bounds.yMax); + this._draw.lineTo(this._bounds.xMin, this._bounds.yMin); + } + } + + private _strokeClear(): void { + this._draw && this._draw.clear(); + } +} \ No newline at end of file diff --git a/src/quadtree/Shape.ts b/src/quadtree/Shape.ts new file mode 100644 index 0000000..15ddb20 --- /dev/null +++ b/src/quadtree/Shape.ts @@ -0,0 +1,71 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-21 + * @Description: 四叉树的 形状基类 + */ + +import { Graphics, Rect, Vec2 } from "cc"; + +export abstract class Shape { + /** + * 形状的标记 用来过滤不需要检测的形状 + * 通过 & 来匹配形状是否需要被检测 + * -1 表示和所有物体碰撞 + */ + public tag: number = -1; + + /** 被标记为无效 下次更新时删除 */ + public invalid: boolean = false; + + /** 缩放 */ + public scale: number; // 缩放 + + /** 脏标记 用来重置包围盒 */ + protected isDirty: boolean; + + /** 包围盒 */ + protected boundingBox: Rect; + + /** 位置 */ + protected _position: Vec2; + + /** 旋转角度 */ + protected _rotation: number; + + constructor(tag: number) { + this.tag = tag; + this.scale = 1.0; + this._rotation = 0; + this.isDirty = true; + this.boundingBox = new Rect(); + this._position = new Vec2(); + } + + set position(pos: Vec2) { + this._position.x = pos.x; + this._position.y = pos.y; + } + + get position(): Vec2 { + return this._position; + } + + set rotation(angle: number) { + if (this._rotation !== angle) { + this._rotation = angle; + this.isDirty = true; + } + } + + get rotation(): number { + return this._rotation; + } + + /** 包围盒 子类重写 */ + public abstract getBoundingBox(): Rect; + + public drawShape(draw: Graphics): void { + + } +} + diff --git a/src/tool/DataStruct/BinaryHeap.ts b/src/tool/DataStruct/BinaryHeap.ts new file mode 100644 index 0000000..c90a075 --- /dev/null +++ b/src/tool/DataStruct/BinaryHeap.ts @@ -0,0 +1,195 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 二叉堆(默认最小堆) 支持最大堆和最小堆 + */ + +export abstract class HeapNode { + public index: number; + public abstract lessThan(other: HeapNode): boolean; +} + + +export class BinaryHeap { + private _nodes: Array; + private _size: number; + private _capacity: number; + + constructor(capacity: number) { + this._size = 0; + this._capacity = capacity <= 0 ? 4 : capacity; + this._nodes = new Array(this._capacity); + } + + /** + * 清空 + */ + public clear(): void { + this._size = 0; + } + + /** + * 获取节点 + * @param index 节点索引 + */ + public get(index: number): T { + return this._nodes[index]; + } + + /** + * 获取顶部节点 + */ + public top(): T { + return this._nodes[0]; + } + + /** + * 是否包含节点 + * @param node 节点 + */ + public contains(node: T): boolean { + return node.index >= 0 && node.index < this._size; + } + + /** + * Push节点 + * @param node 节点 + */ + public push(node: T): void { + const size = ++this._size; + + if (size > this._capacity) { + this._capacity = this._nodes.length *= 2; + } + + this._sortUp(node, size - 1); + } + + /** + * Pop节点 + * @returns + */ + public pop(): T { + if (this._size == 0) { + return null; + } + + const nodes = this._nodes; + const node = nodes[0]; + + node.index = -1; + nodes[0] = null; + + const size = --this._size; + + if (size > 0) { + const finalNode = nodes[size]; + + nodes[size] = null; + this._sortDown(finalNode, 0); + } + + return node; + } + + /** + * 移除节点 + * @param node 要移除的节点 + */ + public remove(node: T): void { + if (!this.contains(node)) { + return; + } + + const size = --this._size; + const nodes = this._nodes; + const newNode = (nodes[node.index] = nodes[size]); + + newNode.index = node.index; + nodes[size] = null; + this.update(newNode); + node.index = -1; + } + + /** + * 更新节点 + * @param node 要更新的节点 + */ + public update(node: T): boolean { + if (!this.contains(node)) { + return false; + } + + const index = node.index; + const nodes = this._nodes; + + if (index > 0 && nodes[index].lessThan(nodes[this._parent(index)])) { + this._sortUp(nodes[index], index); + } else { + this._sortDown(nodes[index], index); + } + + return true; + } + + private _parent(index: number): number { + return (index - 1) >> 1; + } + + public get count(): number { + return this._size; + } + + public get empty(): boolean { + return this._size == 0; + } + + private _sortUp(node: T, index: number): void { + let parentIndex = this._parent(index); + const nodes = this._nodes; + + while (index > 0 && node.lessThan(nodes[parentIndex])) { + nodes[parentIndex].index = index; + nodes[index] = nodes[parentIndex]; + index = parentIndex; + parentIndex = this._parent(parentIndex); + } + + node.index = index; + nodes[index] = node; + } + + private _sortDown(node: T, index: number): void { + let childIndex = (index << 1) + 1; + const nodes = this._nodes; + const size = this._size; + + while (childIndex < size) { + let newParent = node; + + // left + if (nodes[childIndex].lessThan(newParent)) { + newParent = nodes[childIndex]; + } + + // right + if (childIndex + 1 < size && nodes[childIndex + 1].lessThan(newParent)) { + ++childIndex; + newParent = nodes[childIndex]; + } + + if (node == newParent) { + break; + } + + // swap down + newParent.index = index; + nodes[index] = newParent; + index = childIndex; + childIndex = (childIndex << 1) + 1; + } + + node.index = index; + nodes[index] = node; + } +} \ No newline at end of file diff --git a/src/tool/DataStruct/LinkedList.ts b/src/tool/DataStruct/LinkedList.ts new file mode 100644 index 0000000..c0fc75f --- /dev/null +++ b/src/tool/DataStruct/LinkedList.ts @@ -0,0 +1,301 @@ + +function defaultEquals(a: T, b: T): boolean { + return a === b; +} + +/** 单链表结结构节点 */ +export class LinkedNode { + public element: T; + public next: LinkedNode; // 下一项元素的指针 + constructor(element: T) { + this.element = element; + this.next = undefined; + } +} + +/** 双向链表结结构节点 */ +export class DoublyNode extends LinkedNode { + public prev: DoublyNode; // 上一项元素的指针 + public next: DoublyNode; // 下一元素的指针(重新定义下一个元素的类型) + constructor(element: T) { + super(element); + this.prev = undefined; + } +} + +/** 单向链表 */ +export class LinkedList { + protected _equalsFn: (a: T, b: T) => boolean; + protected _count: number; + protected _head: LinkedNode; + /** + * create + * @param equalsFn 比较是否相等(支持自定义) + */ + constructor(equalsFn?: (a: T, b: T) => boolean) { + this._equalsFn = equalsFn || defaultEquals; + this._count = 0; + this._head = undefined; + } + + /** 向链表尾部添加元素 */ + public push(element: T): void { + const node = new LinkedNode(element); + let current: LinkedNode; + if (this._head === undefined) { + this._head = node; + } else { + current = this._head; + while (current.next !== undefined) { + current = current.next; + } + current.next = node; + } + this._count++; + } + + + /** + * 在链表的指定位置插入一个元素。 + * @param element 要插入的元素。 + * @param index 插入位置的索引,从0开始计数。 + * @returns 如果插入成功返回true,否则返回false。 + */ + public insert(element: T, index: number): boolean { + if (index >= 0 && index <= this._count) { + const node = new LinkedNode(element); + if (index === 0) { + const current = this._head; + node.next = current; + this._head = node; + } else { + const previous = this.getElementAt(index - 1); + const current = previous.next; + node.next = current; + previous.next = node; + } + this._count++; + return true; + } + return false; + } + + /** + * 获取链表中指定位置的元素,如果不存在返回 underfined + * @param index + */ + public getElementAt(index: number): LinkedNode { + if (index >= 0 && index <= this._count) { + let node = this._head; + for (let i = 0; i < index && node !== undefined; i++) { + node = node.next; + } + return node; + } + return undefined; + } + + /** + * 从链表中移除一个元素 + * @param element + */ + public remove(element: T): T { + return this.removeAt(this.indexOf(element)); + } + + /** + * 从链表的特定位置移除一个元素 + * @param index + */ + public removeAt(index: number): T { + if (index >= 0 && index < this._count) { + let current = this._head; + if (index === 0) { + this._head = current.next; + } else { + const previous = this.getElementAt(index - 1); + current = previous.next; + previous.next = current.next; + } + this._count--; + current.next = undefined; + return current.element; + } + return undefined; + } + + /** + * 返回元素在链表中的索引,如果没有则返回-1 + * @param element + */ + public indexOf(element: T): number { + let current = this._head; + for (let i = 0; i < this._count && current !== undefined; i++) { + if (this._equalsFn(element, current.element)) { + return i; + } + current = current.next; + } + return -1; + } + + public clear(): void { + this._head = undefined; + this._count = 0; + } + + public getHead(): LinkedNode { + return this._head; + } + + public isEmpty(): boolean { + return this.size() === 0; + } + + public size(): number { + return this._count; + } + + public toString(): string { + if (this._head === undefined) { + return ""; + } + let objString = `${this._head.element}`; + let current = this._head.next; + for (let i = 0; i < this.size() && current !== undefined; i++) { + objString = `${objString},${current.element}`; + current = current.next; + } + return objString; + } +} + +/** 双向链表 */ +export class DoublyLinkedList extends LinkedList { + protected _head: DoublyNode; // 重新定义 head 类型 + protected _tail: DoublyNode; + /** + * create + * @param equalsFn 比较是否相等(支持自定义) + */ + constructor(equalsFn?: (a: T, b: T) => boolean) { + super(equalsFn); + this._tail = undefined; + } + + /** + * 向链表尾部添加元素 + * @param element + */ + public push(element: T): void { + this.insert(element, this._count); + } + + /** + * 向链表指定位置添加元素 + * @param element + * @param index + */ + public insert(element: T, index: number): boolean { + if (index >= 0 && index <= this._count) { + const node = new DoublyNode(element); + let current = this._head; + if (index === 0) { + if (this._head === undefined) { + this._head = node; + this._tail = node; + } else { + node.next = current; + current.prev = node; + this._head = node; + } + } else if (index === this._count) { + current = this._tail; + current.next = node; + node.prev = current; + this._tail = node; + } else { + const previous = this.getElementAt(index - 1); + current = previous.next; + node.next = current; + previous.next = node; + current.prev = node; + node.prev = previous; + } + this._count++; + return true; + } + return false; + } + + /** + * 从链表的特定位置移除一个元素 + * @param index + */ + public removeAt(index: number): T { + if (index >= 0 && index < this._count) { + let current = this._head; + if (index === 0) { + this._head = current.next; + if (this._count === 1) { + this._tail = undefined; + } else { + this._head.prev = undefined; + } + } else if (index === this._count - 1) { + current = this._tail; + this._tail = current.prev; + this._tail.next = undefined; + } else { + current = this.getElementAt(index); + const previous = current.prev; + previous.next = current.next; + current.next.prev = previous; + } + this._count--; + current.next = undefined; + current.prev = undefined; + return current.element; + } + return null; + } + + /** + * 获取链表中指定位置的元素,如果不存在返回 null + * @param index + */ + public getElementAt(index: number): DoublyNode { + if (index >= 0 && index <= this._count) { + if (index > this._count * 0.5) { + // 从后向前找 + let node = this._tail; + for (let i = this._count - 1; i > index && node !== undefined; i--) { + node = node.prev; + } + return node; + } else { + // 从前向后找 + let node = this._head; + for (let i = 0; i < index && node !== undefined; i++) { + node = node.next; + } + return node; + } + } + return undefined; + } + + public getHead(): DoublyNode { + return this._head; + } + + public getTail(): DoublyNode { + return this._tail; + } + + public clear(): void { + this._head = undefined; + this._tail = undefined; + this._count = 0; + } +} \ No newline at end of file diff --git a/src/tool/DataStruct/Stack.ts b/src/tool/DataStruct/Stack.ts new file mode 100644 index 0000000..b30fae6 --- /dev/null +++ b/src/tool/DataStruct/Stack.ts @@ -0,0 +1,42 @@ +import { DoublyLinkedList } from "./LinkedList"; + +export class Stack { + private _items: DoublyLinkedList; + constructor(equalsFn?: (a: T, b: T) => boolean) { + this._items = new DoublyLinkedList(equalsFn); + } + + public push(element: T): void { + this._items.push(element); + } + + public pop(): T { + if (this.isEmpty()) { + return undefined; + } + return this._items.removeAt(this.size() - 1); + } + + public peek(): T { + if (this.isEmpty()) { + return undefined; + } + return this._items.getTail().element; + } + + public size(): number { + return this._items.size(); + } + + public isEmpty(): boolean { + return this._items.isEmpty(); + } + + public clear(): void { + this._items.clear(); + } + + public toString(): string { + return this._items.toString(); + } +} diff --git a/src/tool/MD5.ts b/src/tool/MD5.ts new file mode 100644 index 0000000..9dca24b --- /dev/null +++ b/src/tool/MD5.ts @@ -0,0 +1,250 @@ +// const base64map = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +class Crypt { + // Bit-wise rotation left + public static rotl(n: number, b: number): number { + return (n << b) | (n >>> (32 - b)); + } + + // Bit-wise rotation right + public static rotr(n: number, b: number): number { + return (n << (32 - b)) | (n >>> b); + } + + // Swap big-endian to little-endian and vice versa + public static endianNumber(n: number): any { + return (Crypt.rotl(n, 8) & 0x00ff00ff) | (Crypt.rotl(n, 24) & 0xff00ff00); + } + + // Swap big-endian to little-endian and vice versa + public static endianArray(n: number[]): any { + for (let i = 0, l = n.length; i < l; i++) { + n[i] = Crypt.endianNumber(n[i]); + } + return n; + } + + // Generate an array of any length of random bytes + public static randomBytes(n: number): number[] { + const bytes = []; + + for (; n > 0; n--) { + bytes.push(Math.floor(Math.random() * 256)); + } + return bytes; + } + + // Convert a byte array to big-endian 32-bit words + public static bytesToWords(bytes: number[]): number[] { + const words: any[] = []; + + for (let i = 0, b = 0, l = bytes.length; i < l; i++, b += 8) { + words[b >>> 5] |= bytes[i] << (24 - (b % 32)); + } + return words; + } + + // Convert big-endian 32-bit words to a byte array + public static wordsToBytes(words: number[]): number[] { + const bytes = []; + + for (let b = 0, l = words.length * 32; b < l; b += 8) { + bytes.push((words[b >>> 5] >>> (24 - (b % 32))) & 0xff); + } + return bytes; + } + + // Convert a byte array to a hex string + public static bytesToHex(bytes: number[]): string { + const hex = []; + + for (let i = 0, l = bytes.length; i < l; i++) { + hex.push((bytes[i] >>> 4).toString(16)); + hex.push((bytes[i] & 0xf).toString(16)); + } + return hex.join(""); + } + + // Convert a hex string to a byte array + public static hexToBytes(hex: string): number[] { + const bytes = []; + + for (let c = 0, l = hex.length; c < l; c += 2) { + bytes.push(parseInt(hex.substr(c, 2), 16)); + } + return bytes; + } +} + +// Convert a string to a byte array +function stringToBytes(str: string): number[] { + str = unescape(encodeURIComponent(str)); + const bytes = []; + + for (let i = 0, l = str.length; i < l; i++) { + bytes.push(str.charCodeAt(i) & 0xff); + } + return bytes; +} + +function isFastBuffer(obj: any): boolean { + return !!obj.constructor && typeof obj.constructor.isBuffer === "function" && obj.constructor.isBuffer(obj); +} + +// For Node v0.10 support. Remove this eventually. +function isSlowBuffer(obj: any): boolean { + return typeof obj.readFloatLE === "function" && typeof obj.slice === "function" && isBuffer(obj.slice(0, 0)); +} + +function isBuffer(obj: any): boolean { + return obj && (isFastBuffer(obj) || isSlowBuffer(obj) || !!obj._isBuffer); +} + +// The core +const md5Lib = function (message: string): number[] { + const bytes = stringToBytes(message); + const m = Crypt.bytesToWords(bytes), + l = bytes.length * 8; + let ml = m.length; + let a = 1732584193, + b = -271733879, + c = -1732584194, + d = 271733878; + + // Swap endian + for (let i = 0; i < ml; i++) { + m[i] = (((m[i] << 8) | (m[i] >>> 24)) & 0x00ff00ff) | (((m[i] << 24) | (m[i] >>> 8)) & 0xff00ff00); + } + + // Padding + m[l >>> 5] |= 0x80 << l % 32; + m[(((l + 64) >>> 9) << 4) + 14] = l; + + // Method shortcuts + const FF = md5Lib._ff, + GG = md5Lib._gg, + HH = md5Lib._hh, + II = md5Lib._ii; + + ml = m.length; + for (let i = 0; i < ml; i += 16) { + const aa = a, + bb = b, + cc = c, + dd = d; + + a = FF(a, b, c, d, m[i + 0], 7, -680876936); + d = FF(d, a, b, c, m[i + 1], 12, -389564586); + c = FF(c, d, a, b, m[i + 2], 17, 606105819); + b = FF(b, c, d, a, m[i + 3], 22, -1044525330); + a = FF(a, b, c, d, m[i + 4], 7, -176418897); + d = FF(d, a, b, c, m[i + 5], 12, 1200080426); + c = FF(c, d, a, b, m[i + 6], 17, -1473231341); + b = FF(b, c, d, a, m[i + 7], 22, -45705983); + a = FF(a, b, c, d, m[i + 8], 7, 1770035416); + d = FF(d, a, b, c, m[i + 9], 12, -1958414417); + c = FF(c, d, a, b, m[i + 10], 17, -42063); + b = FF(b, c, d, a, m[i + 11], 22, -1990404162); + a = FF(a, b, c, d, m[i + 12], 7, 1804603682); + d = FF(d, a, b, c, m[i + 13], 12, -40341101); + c = FF(c, d, a, b, m[i + 14], 17, -1502002290); + b = FF(b, c, d, a, m[i + 15], 22, 1236535329); + + a = GG(a, b, c, d, m[i + 1], 5, -165796510); + d = GG(d, a, b, c, m[i + 6], 9, -1069501632); + c = GG(c, d, a, b, m[i + 11], 14, 643717713); + b = GG(b, c, d, a, m[i + 0], 20, -373897302); + a = GG(a, b, c, d, m[i + 5], 5, -701558691); + d = GG(d, a, b, c, m[i + 10], 9, 38016083); + c = GG(c, d, a, b, m[i + 15], 14, -660478335); + b = GG(b, c, d, a, m[i + 4], 20, -405537848); + a = GG(a, b, c, d, m[i + 9], 5, 568446438); + d = GG(d, a, b, c, m[i + 14], 9, -1019803690); + c = GG(c, d, a, b, m[i + 3], 14, -187363961); + b = GG(b, c, d, a, m[i + 8], 20, 1163531501); + a = GG(a, b, c, d, m[i + 13], 5, -1444681467); + d = GG(d, a, b, c, m[i + 2], 9, -51403784); + c = GG(c, d, a, b, m[i + 7], 14, 1735328473); + b = GG(b, c, d, a, m[i + 12], 20, -1926607734); + + a = HH(a, b, c, d, m[i + 5], 4, -378558); + d = HH(d, a, b, c, m[i + 8], 11, -2022574463); + c = HH(c, d, a, b, m[i + 11], 16, 1839030562); + b = HH(b, c, d, a, m[i + 14], 23, -35309556); + a = HH(a, b, c, d, m[i + 1], 4, -1530992060); + d = HH(d, a, b, c, m[i + 4], 11, 1272893353); + c = HH(c, d, a, b, m[i + 7], 16, -155497632); + b = HH(b, c, d, a, m[i + 10], 23, -1094730640); + a = HH(a, b, c, d, m[i + 13], 4, 681279174); + d = HH(d, a, b, c, m[i + 0], 11, -358537222); + c = HH(c, d, a, b, m[i + 3], 16, -722521979); + b = HH(b, c, d, a, m[i + 6], 23, 76029189); + a = HH(a, b, c, d, m[i + 9], 4, -640364487); + d = HH(d, a, b, c, m[i + 12], 11, -421815835); + c = HH(c, d, a, b, m[i + 15], 16, 530742520); + b = HH(b, c, d, a, m[i + 2], 23, -995338651); + + a = II(a, b, c, d, m[i + 0], 6, -198630844); + d = II(d, a, b, c, m[i + 7], 10, 1126891415); + c = II(c, d, a, b, m[i + 14], 15, -1416354905); + b = II(b, c, d, a, m[i + 5], 21, -57434055); + a = II(a, b, c, d, m[i + 12], 6, 1700485571); + d = II(d, a, b, c, m[i + 3], 10, -1894986606); + c = II(c, d, a, b, m[i + 10], 15, -1051523); + b = II(b, c, d, a, m[i + 1], 21, -2054922799); + a = II(a, b, c, d, m[i + 8], 6, 1873313359); + d = II(d, a, b, c, m[i + 15], 10, -30611744); + c = II(c, d, a, b, m[i + 6], 15, -1560198380); + b = II(b, c, d, a, m[i + 13], 21, 1309151649); + a = II(a, b, c, d, m[i + 4], 6, -145523070); + d = II(d, a, b, c, m[i + 11], 10, -1120210379); + c = II(c, d, a, b, m[i + 2], 15, 718787259); + b = II(b, c, d, a, m[i + 9], 21, -343485551); + + a = (a + aa) >>> 0; + b = (b + bb) >>> 0; + c = (c + cc) >>> 0; + d = (d + dd) >>> 0; + } + + return Crypt.endianArray([a, b, c, d]); +}; + +// Auxiliary functions +// eslint-disable-next-line max-params +md5Lib._ff = function (a: any, b: any, c: any, d: any, x: any, s: any, t: any): any { + const n = a + ((b & c) | (~b & d)) + (x >>> 0) + t; + + return ((n << s) | (n >>> (32 - s))) + b; +}; +// eslint-disable-next-line max-params +md5Lib._gg = function (a: any, b: any, c: any, d: any, x: any, s: any, t: any): any { + const n = a + ((b & d) | (c & ~d)) + (x >>> 0) + t; + + return ((n << s) | (n >>> (32 - s))) + b; +}; +// eslint-disable-next-line max-params +md5Lib._hh = function (a: any, b: any, c: any, d: any, x: any, s: any, t: any): any { + const n = a + (b ^ c ^ d) + (x >>> 0) + t; + + return ((n << s) | (n >>> (32 - s))) + b; +}; +// eslint-disable-next-line max-params +md5Lib._ii = function (a: any, b: any, c: any, d: any, x: any, s: any, t: any): any { + const n = a + (c ^ (b | ~d)) + (x >>> 0) + t; + + return ((n << s) | (n >>> (32 - s))) + b; +}; + +/** + * 对字符串执行md5处理 + * + * @export + * @param {string} message 要处理的字符串 + * @returns {string} md5 + */ +export function md5(message: string): string { + if (message === undefined || message === null) { + throw new Error("Illegal argument " + message); + } + return Crypt.bytesToHex(Crypt.wordsToBytes(md5Lib(message))); +} \ No newline at end of file diff --git a/src/tool/Math.ts b/src/tool/Math.ts new file mode 100644 index 0000000..6ce8712 --- /dev/null +++ b/src/tool/Math.ts @@ -0,0 +1,36 @@ +const MathMin = Math.min; +const MathMax = Math.max; +const MathFloor = Math.floor; +const MathRandom = Math.random; + +export class MathTool { + public static clampf(value: number, min: number, max: number): number { + return MathMin(MathMax(value, min), max); + } + + /** 随机 min 到 max之间的整数 (包含 min 和 max) */ + public static rand(min: number, max: number): number { + return MathFloor(MathRandom() * (max - min + 1) + min); + } + + /** 随机 min 到 max之间的浮点数 (包含 min 和 max) */ + public static randRange(min: number, max: number): number { + return MathRandom() * (max - min) + min; + } + + public static rad(angle: number): number { + return (angle * Math.PI) / 180; + } + + public static deg(radian: number): number { + return (radian * 180) / Math.PI; + } + + public static smooth(num1: number, num2: number, elapsedTime: number, responseTime: number): number { + let out: number = num1; + if (elapsedTime > 0) { + out = out + (num2 - num1) * (elapsedTime / (elapsedTime + responseTime)); + } + return out; + } +} \ No newline at end of file diff --git a/src/tool/header.ts b/src/tool/header.ts new file mode 100644 index 0000000..3fd4cab --- /dev/null +++ b/src/tool/header.ts @@ -0,0 +1,7 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-14 + * @Description: tools 导出 + */ + + diff --git a/src/tool/helper/ObjectHelper.ts b/src/tool/helper/ObjectHelper.ts new file mode 100644 index 0000000..7b2dd9e --- /dev/null +++ b/src/tool/helper/ObjectHelper.ts @@ -0,0 +1,13 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-12 + * @Description: 对象帮助类 + */ +export class ObjectHelper { + public static getObjectProp(obj: Record, key: string): any { + if (obj.hasOwnProperty(key)) { + return obj[key]; + } + return (obj[key] = Object.assign({}, obj[key])); + } +} \ No newline at end of file diff --git a/src/tool/log.ts b/src/tool/log.ts new file mode 100644 index 0000000..3709a18 --- /dev/null +++ b/src/tool/log.ts @@ -0,0 +1,44 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-05 + * @Description: log相关的api + */ + +import { KUNPO_DEBUG } from "../global/header"; + +function log(...args: any[]) { + console.log("kunpo:", ...args); +} + +/** + * 开启debug模式后 输出调试信息 + * @param args + */ +function debug(...args: any[]): void { + KUNPO_DEBUG && console.log("kunpo:", ...args); +} + +/** + * 信息性消息 某些浏览器中会带有小图标,但颜色通常与 log 相同 + * @param args + */ +function info(...args: any[]): void { + console.info("kunpo:", ...args); +} + +/** + * 警告信息 黄色背景,通常带有警告图标 + * @param args + */ +function warn(...args: any[]): void { + console.warn("kunpo:", ...args); +} + +/** + * 错误消息 红色背景,通常带有错误图标 + * @param args + */ +function error(...args: any[]): void { + console.error("kunpo:", ...args); +} +export { debug, error, info, log, warn }; diff --git a/src/tool/timer/Timer.ts b/src/tool/timer/Timer.ts new file mode 100644 index 0000000..9f32e0c --- /dev/null +++ b/src/tool/timer/Timer.ts @@ -0,0 +1,199 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 定时器管理类 + */ + +import { BinaryHeap } from "../DataStruct/BinaryHeap"; +import { TimerNode } from "./TimerNode"; +import { TimerNodePool } from "./TimerNodePool"; + +export class Timer { + private _timerNodeOrder: number = 0; + + /** 经过的时间 */ + private _elapsedTime: number = 0; + + private _pool: TimerNodePool; + private _heap: BinaryHeap; + + /** 暂停的计时器 */ + private _pausedTimers: Map; + + /** + * 定时器数量 + * @readonly + * @type {number} + */ + public get timerCount(): number { + return this._heap.count; + } + + /** + * 定时器管理类 + * + * @param {number} initTimerCapacity 初始定时器容量 + * @memberof Timer + */ + public constructor(initTimerCapacity: number) { + this._heap = new BinaryHeap(initTimerCapacity); + this._pool = new TimerNodePool(initTimerCapacity); + this._pausedTimers = new Map(); + } + + /** + * 启动一个计时器 + * @param { Function } callback 回调方法 + * @param {number} interval 回调间隔 (秒) + * @param {number} [loop=0] 重复次数:0:回调一次,1~n:回调n次,-1:无限重复 + * @returns {number} 返回计时器id + */ + public start(callback: () => void, interval: number, loop: number = 0): number { + const timerNode = this._getTimerNode(callback, interval, loop); + this._heap.push(timerNode); + return timerNode.id; + } + + /** + * 删除指定计时器 + * + * @param {number} timerId 定时器ID + * @memberof Timer + */ + public stop(timerId: number): void { + const timerNode = this._pool.get(timerId); + + if (timerNode) { + if (timerNode.pause) { + this._pausedTimers.delete(timerId); + } + + this._heap.remove(timerNode); + this._pool.recycle(timerId); + } + } + + /** + * 暂停定时器 + * + * @param {number} timerId 定时器ID + * @memberof Timer + */ + public pause(timerId: number): void { + const timerNode = this._pool.get(timerId); + + if (timerNode) { + timerNode.pauseRemainTime = timerNode.expireTime - this._elapsedTime; + this._heap.remove(timerNode); + this._pausedTimers.set(timerId, timerNode); + } + } + + /** + * 继续定时器 + * + * @param {number} timerId 定时器ID + * @memberof Timer + */ + public resume(timerId: number): void { + const timerNode = this._pausedTimers.get(timerId); + + if (timerNode) { + timerNode.pause = false; + timerNode.expireTime = this._elapsedTime + timerNode.pauseRemainTime; + this._pausedTimers.delete(timerId); + this._heap.push(timerNode); + } + } + + // /** + // * 根据回调更新定时器 + // * + // * @param {number} timerId 定时器ID + // * @param {number} interval 回调间隔 + // * @param {number} loop 重复次数 + // * @param {boolean} [resetTime=false] 是否更新下次回调时间(从当前时间开始计时) + // * @returns {boolean} 如果TimerID存在则返回true + // * @memberof Timer + // */ + // public updateTimer(timerId: number, interval: number, loop: number, resetTime: boolean = false): boolean { + // const timerNode = this._pool.get(timerId); + // if (!timerNode) { + // return false; + // } + // timerNode.interval = interval; + // timerNode.loop = loop; + // if (resetTime) { + // timerNode.expireTime = this._elapsedTime + interval; + // } + // return this._heap.update(timerNode); + // } + + /** + * 更新时钟 + * + * @param {number} deltaTime 更新间隔 + * @memberof Timer + */ + public update(deltaTime: number): void { + const elapsedTime = (this._elapsedTime += deltaTime); + + const heap = this._heap; + let timerNode = heap.top(); + + while (timerNode && timerNode.expireTime <= elapsedTime) { + const callback = timerNode.callback; + if (timerNode.loop == 0) { + heap.pop(); + this._recycle(timerNode); + } else if (timerNode.loop > 0) { + // 处理多次回调定时器 + if (--timerNode.loop == 0) { + heap.pop(); + this._recycle(timerNode); + } else { + // 更新下一次回调 + timerNode.expireTime = timerNode.expireTime + timerNode.interval; + heap.update(timerNode); + } + } else { + // 无限次数回调 + // 更新下一次回调 + timerNode.expireTime = timerNode.expireTime + timerNode.interval; + heap.update(timerNode); + } + + callback(); + timerNode = heap.top(); + } + } + + /** + * 清空所有定时器 + * + * @memberof Timer + */ + public clear(): void { + this._heap.clear(); + this._pool.clear(); + this._pausedTimers.clear(); + this._timerNodeOrder = 0; + } + + private _getTimerNode(callback: () => void, interval: number, loop: number): TimerNode { + const timerNode = this._pool.allocate(); + + timerNode.orderIndex = ++this._timerNodeOrder; + timerNode.callback = callback; + timerNode.interval = interval; + timerNode.expireTime = this._elapsedTime + interval; + timerNode.loop = loop; + timerNode.pause = false; + + return timerNode; + } + + private _recycle(timerNode: TimerNode): void { + this._pool.recycle(timerNode.id); + } +} \ No newline at end of file diff --git a/src/tool/timer/TimerNode.ts b/src/tool/timer/TimerNode.ts new file mode 100644 index 0000000..841acfa --- /dev/null +++ b/src/tool/timer/TimerNode.ts @@ -0,0 +1,55 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 计时器节点 + */ +import { HeapNode } from "../DataStruct/BinaryHeap"; + +export class TimerNode extends HeapNode { + /** 定时器ID */ + public id: number; + + /** 定时器添加索引,同一时间回调根据OrderIndex排序 */ + public orderIndex: number; + + /** 定时间隔 */ + public interval: number; + + /** 回调时间点 */ + public expireTime: number; + + /** 重复次数 */ + public loop: number = 0; + + /** 定时回调 */ + public callback: () => void; + + /** 暂停时剩余时间 */ + public pauseRemainTime: number; + + /** 是否暂停 */ + public pause: boolean; + + /** * 是否被回收 */ + public recycled: boolean; + + constructor(id: number) { + super(); + this.id = id; + } + + /** + * 是否比其他定时节点小 + * @param {HeapNode} other 其他定时节点 + * @returns {boolean} + */ + public lessThan(other: HeapNode): boolean { + const otherTimerNode = other as TimerNode; + + if (Math.abs(this.expireTime - otherTimerNode.expireTime) <= 1e-5) { + return this.orderIndex < otherTimerNode.orderIndex; + } + + return this.expireTime < otherTimerNode.expireTime; + } +} diff --git a/src/tool/timer/TimerNodePool.ts b/src/tool/timer/TimerNodePool.ts new file mode 100644 index 0000000..ad0ccf7 --- /dev/null +++ b/src/tool/timer/TimerNodePool.ts @@ -0,0 +1,123 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 计时器节点回收池 + */ +import { TimerNode } from "./TimerNode"; + +const TimerIdBit = 19; +const TimerCount = 1 << (32 - TimerIdBit); +const TimerVersionMask = (1 << TimerIdBit) - 1; +const TimerMaxVersion = TimerVersionMask; + +export class TimerNodePool { + private _pool: Array = new Array(); + private _freeIndices: Array = new Array(); + + /** + * 定时器池 + * @param {number} capacity 初始容量 + */ + public constructor(capacity: number) { + for (let i = 0; i < capacity; ++i) { + const timerNode = new TimerNode(i << TimerIdBit); + + timerNode.recycled = true; + this._pool.push(timerNode); + this._freeIndices.push(i); + } + } + + /** + * 分配定时器节点 + * @returns {TimerNode} 定时器节点 + */ + public allocate(): TimerNode { + let timerNode: TimerNode; + const pools = this._pool; + + if (this._freeIndices.length == 0) { + if (pools.length == TimerCount) { + throw new Error("超出时钟个数: " + TimerCount); + } + timerNode = new TimerNode(pools.length << TimerIdBit); + pools.push(timerNode); + } else { + const index = this._freeIndices.pop(); + + timerNode = pools[index]; + timerNode.recycled = false; + if ((timerNode.id & TimerVersionMask) == TimerMaxVersion) { + throw new Error("时钟版本号过高: " + TimerMaxVersion); + } + ++timerNode.id; + } + + return timerNode; + } + + /** + * 回收定时器节点 + * @param {number} timerId 定时器ID + */ + public recycle(timerId: number): void { + const index = timerId >>> TimerIdBit; + + if (index < 0 || index >= this._pool.length) { + throw new Error("定时器不存在"); + } + + const timerNode = this._pool[index]; + + if (timerNode.recycled) { + throw new Error("定时器已经被回收"); + } + + timerNode.recycled = true; + timerNode.callback = null; + this._freeIndices.push(index); + } + + /** + * 根据TimerID获取定时器节点 + * @param {number} timerId 定时器ID + * @returns {TimerNode} + */ + public get(timerId: number): TimerNode | undefined { + const index = timerId >>> TimerIdBit; + const version = timerId & TimerVersionMask; + + if (index < 0 || index >= this._pool.length) { + return null; + } + + const timerNode = this._pool[index]; + if (timerNode.recycled) { + return null; + } + + const timerNodeVersion = timerNode.id & TimerVersionMask; + + if (timerNodeVersion != version) { + return null; + } + + return timerNode; + } + + /** + * 清空正在使用的Timer + */ + public clear(): void { + const pools = this._pool; + const timerNodeCount = pools.length; + const freeIndices = this._freeIndices; + + freeIndices.length = 0; + for (let i = 0; i < timerNodeCount; ++i) { + pools[i].recycled = true; + pools[i].callback = null; + freeIndices.push(i); + } + } +} diff --git a/src/ui/ComponentExtendHelper.ts b/src/ui/ComponentExtendHelper.ts new file mode 100644 index 0000000..901a26c --- /dev/null +++ b/src/ui/ComponentExtendHelper.ts @@ -0,0 +1,33 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-26 + * @Description: 自定义组件扩展帮助类 + */ +import { UIObjectFactory } from "fairygui-cc"; +import { debug } from "../tool/log"; +import { PropsHelper } from "./PropsHelper"; +import { _uidecorator } from "./UIDecorator"; + +export class ComponentExtendHelper { + public static register(): void { + for (const { ctor, res } of _uidecorator.getComponentMaps().values()) { + debug(`自定义组件注册 组件名:${res.name} 包名:${res.pkg}`); + this.registerComponent(ctor, res.pkg, res.name); + } + } + + /** + * 注册自定义组件信息 + * @param info + */ + private static registerComponent(ctor: any, pkg: string, name: string): void { + // 自定义组件扩展 + const onConstruct = function (this: any): void { + PropsHelper.serializeProps(this, pkg); + this.onInit && this.onInit(); + }; + ctor.prototype.onConstruct = onConstruct; + // 自定义组件扩展 + UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor); + } +} \ No newline at end of file diff --git a/src/ui/IWindow.ts b/src/ui/IWindow.ts new file mode 100644 index 0000000..6e76793 --- /dev/null +++ b/src/ui/IWindow.ts @@ -0,0 +1,79 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: + */ + +import { AdapterType, WindowType } from "./header"; +import { IWindowHeader } from "./IWindowHeader"; +import { WindowHeaderInfo } from "./WindowHeaderInfo"; + +export interface IWindow { + /** 窗口类型 */ + type: WindowType; + /** 窗口适配类型 */ + adapterType: AdapterType; + /** 底部遮罩的透明度 */ + bgAlpha: number; + /** + * 窗口适配 (框架内部使用) + */ + _adapted(): void; + + /** + * 初始化方法 (框架内部使用) + * @param swallowTouch 是否吞噬触摸事件 + */ + _init(swallowTouch: boolean, bgAlpha: number): void; + /** + * 窗口关闭 (框架内部使用) + */ + _close(): void; + /** + * 显示窗口 (框架内部使用) + * @param userdata 用户自定义数据 + */ + _show(userdata?: any): void; + /** + * 从隐藏状态恢复显示 + */ + _showFromHide(): void; + /** + * 隐藏窗口 (框架内部使用) + */ + _hide(): void; + /** + * 窗口被遮挡 被同组或者不同组的其他窗口覆盖 (框架内部使用) + */ + _cover(): void; + /** + * 恢复窗口遮挡 被同组或者不同组的其他窗口覆盖恢复 (框架内部使用) + */ + _recover(): void; + + /** + * 调整窗口的显示层级 + * @param depth + */ + _setDepth(depth: number): void; + + /** + * 窗口是否显示 + */ + isShowing(): boolean; + + /** + * 窗口是否被遮挡了 + */ + isCover(): boolean; + + /** + * 窗口尺寸发生改变时被调用 + */ + screenResize(): void; + + /** 获取资源栏数据 */ + getHeaderInfo(): WindowHeaderInfo; + + setHeader(header: IWindowHeader): void; +} \ No newline at end of file diff --git a/src/ui/IWindowHeader.ts b/src/ui/IWindowHeader.ts new file mode 100644 index 0000000..657060e --- /dev/null +++ b/src/ui/IWindowHeader.ts @@ -0,0 +1,45 @@ +import { AdapterType } from "./header"; +import { IWindow } from "./IWindow"; + +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: 窗口顶边资源栏 + */ +export interface IWindowHeader { + /** 资源栏名称 */ + name: string; + /** 窗口适配类型 */ + adapterType: AdapterType; + /** 引用计数 */ + _refCount: number; + /** + * 初始化 (内部方法) + */ + _init(): void; + /** + * 窗口适配 (内部方法) + */ + _adapted(): void; + /** + * 显示 (内部方法) + * @param {IWindow} window 所属窗口 + */ + _show(window: IWindow): void; + /** + * 隐藏 (内部方法) + */ + _hide(): void; + /** + * 关闭 (内部方法) + */ + _close(): void; + + /** 增加引用计数 (内部方法) */ + _addRef(): void; + /** 减少引用计数 (内部方法) */ + _decRef(): number; + + /** 屏幕大小改变时被调用 (内部方法) */ + _screenResize(): void; +} diff --git a/src/ui/PropsHelper.ts b/src/ui/PropsHelper.ts new file mode 100644 index 0000000..efffc6e --- /dev/null +++ b/src/ui/PropsHelper.ts @@ -0,0 +1,89 @@ +/** + * @Author: Gongxh + * @Date: 2025-01-09 + * @Description: 属性辅助类 + */ + +import { GComponent } from "fairygui-cc"; +import { warn } from "../tool/log"; + +interface IPropsConfig { + [packageName: string]: { [componentName: string]: IPropsInfo }; +} + +interface IPropsInfo { + props: (string | number)[]; + callbacks: (string | number)[]; +} + +export class PropsHelper { + private static _config: IPropsConfig = {}; + public static setConfig(config: IPropsConfig): void { + this._config = config; + } + + /** 序列化属性 */ + public static serializeProps(component: GComponent, packageName: string): void { + if (!this._config) { + return; + } + const config = this._config[packageName]; + if (!config) { + return; + } + const componentName = component.name; + const propsInfo = config[componentName]; + if (!propsInfo) { + return; + } + // 设置属性 + const props = propsInfo.props; + this.serializationPropsNode(component, props); + + // 设置回调 + const callbacks = propsInfo.callbacks; + this.serializationCallbacksNode(component, callbacks); + } + + /** 给界面中定义的属性赋值 */ + private static serializationPropsNode(component: GComponent, props: (string | number)[]) { + const propsCount = props.length; + // [name1, len, ...props1, name2, len, ...props2, ...] + let index = 0; + while (index < propsCount) { + const propName = props[index++] as string; + const endIndex = index + (props[index] as number); + let uinode = component; + while (++index <= endIndex) { + uinode = uinode.getChildAt(props[index] as number); + if (!uinode) { + warn(`无法对UI类(${component.name})属性(${propName})赋值,请检查节点配置是否正确`); + break; + } + } + (component as any)[propName] = (uinode == component ? null : uinode); + } + } + + private static serializationCallbacksNode(component: GComponent, callbacks: (string | number)[]) { + const propsCount = callbacks.length; + // [name1, len, ...props1, name2, len, ...props2, ...] + let index = 0; + while (index < propsCount) { + const propName = callbacks[index++] as string; + const endIndex = index + (callbacks[index] as number); + let uinode = component; + while (++index <= endIndex) { + uinode = uinode.getChildAt(callbacks[index] as number); + if (!uinode) { + warn(`无法对UI类(${component.name})的(${propName})设置回调,请检查节点配置是否正确`); + break; + } + } + if (uinode != component) { + uinode.onClick((component as any)[propName], component); + } + } + } +} + diff --git a/src/ui/UIDecorator.ts b/src/ui/UIDecorator.ts new file mode 100644 index 0000000..80f032a --- /dev/null +++ b/src/ui/UIDecorator.ts @@ -0,0 +1,180 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-11 + * @Description: UI 装饰器 + */ + +import { ObjectHelper } from "../tool/helper/ObjectHelper"; +export namespace _uidecorator { + const UIPropMeta = "__uipropmeta__" + const UICBMeta = "__uicbmeta__" + + interface IUIInfoBase { + /** 构造函数 */ + ctor: any; + /** 属性 */ + props: Record; + /** 方法 */ + callbacks: Record; + } + + /** + * 窗口属性注册数据结构 + */ + interface UIWindowInfo extends IUIInfoBase { + /** 配置信息 */ + res: { + /** 窗口组名称 */ + group: string; + /** fgui包名 */ + pkg: string; + /** 窗口名 */ + name: string; + }; + } + /** 用来存储窗口注册信息 */ + const uiclassMap: Map = new Map(); + + /** 获取窗口注册信息 */ + export function getWindowMaps(): Map { + return uiclassMap; + } + + /** + * 窗口装饰器 + * @param {string} groupName 窗口组名称 + * @param {string} pkgName fgui包名 + * @param {string} name 窗口名 (与fgui中的组件名一一对应) + */ + export function uiclass(groupName: string, pkgName: string, name: string): Function { + /** target 类的构造函数 */ + return function (ctor: any): void { + // debug(`uiclass >${JSON.stringify(res)}<`); + // debug(`uiclass prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`); + uiclassMap.set(ctor, { + ctor: ctor, + props: ctor[UIPropMeta] || null, + callbacks: ctor[UICBMeta] || null, + res: { + group: groupName, + pkg: pkgName, + name: name, + }, + }); + }; + } + + + /** + * 组件属性注册数据结构 + */ + interface IUIComInfo extends IUIInfoBase { + /** 配置信息 */ + res: { + /** fgui包名 */ + pkg: string; + /** 组件名 */ + name: string; + }; + } + /** 用来存储组件注册信息 */ + let uicomponentMap: Map = new Map(); + + /** 获取组件注册信息 */ + export function getComponentMaps(): Map { + return uicomponentMap; + } + + /** + * UI组件装饰器 + * @param {string} pkg 包名 + * @param {string} name 组件名 + */ + export function uicom(pkg: string, name: string): Function { + return function (ctor: any): void { + // log(`pkg:【${pkg}】 uicom prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`); + uicomponentMap.set(ctor, { + ctor: ctor, + props: ctor[UIPropMeta] || null, + callbacks: ctor[UICBMeta] || null, + res: { + pkg: pkg, + name: name, + } + }); + }; + } + + /** + * header属性注册数据结构 + */ + interface IUIHeaderInfo extends IUIInfoBase { + /** 配置信息 */ + res: { + /** fgui包名 */ + pkg: string; + /** 组件名 */ + name: string; + }; + } + /** 用来存储组件注册信息 */ + let uiheaderMap: Map = new Map(); + + /** 获取header注册信息 */ + export function getHeaderMaps(): Map { + return uiheaderMap; + } + + /** + * UI header装饰器 + * @param {string} pkg 包名 + * @param {string} name 组件名 + */ + export function uiheader(pkg: string, name: string): Function { + return function (ctor: any): void { + // log(`pkg:【${pkg}】 uiheader prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`); + uiheaderMap.set(ctor, { + ctor: ctor, + props: ctor[UIPropMeta] || null, + callbacks: ctor[UICBMeta] || null, + res: { + pkg: pkg, + name: name, + } + }); + }; + } + + /** + * UI属性装饰器 + * @param {Object} target 实例成员的类的原型 + * @param {string} name 属性名 + * + * example: @uiprop node: GObject + */ + export function uiprop(target: Object, name: string): any { + // debug("属性装饰器:", target.constructor, name); + ObjectHelper.getObjectProp(target.constructor, UIPropMeta)[name] = 1; + } + + /** + * 方法装饰器 (给点击事件用) + * @param {Object} target 实例成员的类的原型 + * @param {string} name 方法名 + */ + export function uiclick(target: Object, name: string, descriptor: PropertyDescriptor): void { + // debug("方法装饰器:", target.constructor, name, descriptor); + ObjectHelper.getObjectProp(target.constructor, UICBMeta)[name] = descriptor.value; + } +} + +let _global = globalThis || window || global; +(_global as any)["getKunpoRegisterWindowMaps"] = function () { + return _uidecorator.getWindowMaps() as any; +}; +(_global as any)["getKunpoRegisterComponentMaps"] = function () { + return _uidecorator.getComponentMaps() as any; +}; +(_global as any)["getKunpoRegisterHeaderMaps"] = function () { + return _uidecorator.getHeaderMaps() as any; +}; \ No newline at end of file diff --git a/src/ui/WindowGroup.ts b/src/ui/WindowGroup.ts new file mode 100644 index 0000000..e0e0c29 --- /dev/null +++ b/src/ui/WindowGroup.ts @@ -0,0 +1,393 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: 窗口组 (在同一个窗口容器的上的窗口) + */ + +import { Color, warn } from "cc"; +import { GComponent, GGraph, UIPackage } from "fairygui-cc"; +import { WindowBase } from "../fgui/WindowBase"; +import { WindowHeader } from "../fgui/WindowHeader"; +import { Screen } from "../global/Screen"; +import { WindowType } from "./header"; +import { IWindow } from "./IWindow"; +import { PropsHelper } from "./PropsHelper"; +import { WindowManager } from "./WindowManager"; +import { WindowInfo } from "./WindowResPool"; + +export class WindowGroup { + /** 窗口组的名字 */ + private _name: string = ""; + /** 窗口组的根节点 */ + private _root: GComponent; + /** 忽略顶部窗口查询 */ + private _ignoreQuery: boolean = false; + /** 吞噬触摸事件 */ + private _swallowTouch: boolean = false; + /** 窗口容器中的窗口名列表 */ + private _windowNames: string[] = []; + /** 窗口顶部资源栏 */ + private _headers: Map = new Map(); + /** 半透明遮罩的透明度 */ + private _bgAlpha: number = 0; + /** 半透明节点 */ + private _alphaGraph: GGraph; + /** 半透明遮罩的颜色 */ + private _color: Color = new Color(0, 0, 0, 255); + + /** + * 获取窗口组的名称。 + * @returns {string} 窗口组的名称。 + */ + public get name(): string { + return this._name; + } + + /** + * 获取当前窗口组中窗口的数量。 + * @returns 窗口数量 + */ + public get size(): number { + return this._windowNames.length; + } + + /** + * 获取是否忽略查询的状态。 + * @returns {boolean} 如果忽略查询,则返回 true,否则返回 false。 + */ + public get isIgnore(): boolean { + return this._ignoreQuery; + } + + /** + * 实例化 + * @param name 组名 + * @param root 窗口组的根节点 一个fgui的组件 + * @param ignoreQuery 是否忽略顶部窗口查询 + * @param swallowTouch 是否吞掉触摸事件 + */ + constructor(name: string, root: GComponent, ignoreQuery: boolean, swallowTouch: boolean, bgAlpha: number) { + this._name = name; + this._root = root; + this._ignoreQuery = ignoreQuery; + this._swallowTouch = swallowTouch; + this._bgAlpha = bgAlpha; + + const alphaGraph = new GGraph(); + alphaGraph.touchable = false; + alphaGraph.name = "bgAlpha"; + alphaGraph.setPosition(root.width * 0.5, root.height * 0.5); + alphaGraph.setSize(root.width, root.height, true); + alphaGraph.setPivot(0.5, 0.5, true); + root.addChild(alphaGraph); + this._alphaGraph = alphaGraph; + } + + /** + * 根据窗口名创建窗口 并添加到显示节点 + * @param windowName 窗口名 + */ + private _createWindow(pkg: string, name: string): WindowBase { + let window = UIPackage.createObject(pkg, name) as WindowBase; + window.name = name; + PropsHelper.serializeProps(window, pkg); + window._init(this._swallowTouch, this._bgAlpha); + window._adapted(); + this._createHeader(window); + // 添加到显示节点 + this._addWindow(window); + return window; + } + + private _addWindow(window: WindowBase): void { + this._root.addChild(window); + WindowManager._addWindow(window.name, window); + } + + public showWindow(info: WindowInfo, userdata?: any): void { + let name = info.name; + let window = WindowManager.getWindow(name); + if (window) { + window._show(userdata); + } else { + window = this._createWindow(info.pkg, name); + this._processWindowCloseStatus(window); + this._windowNames.push(name); + window._show(userdata); + } + this._moveWindowToTop(name); + // 处理header的显示 + this._processHeaderStatus(); + // 显示窗口组 + this._root.visible = true; + } + + /** + * 移除指定名称的窗口。 + * @param name 窗口的名称。 + */ + public _removeWindow(name: string): void { + let index = this._windowNames.lastIndexOf(name); + let lastIndex = this.size - 1; + if (index < 0) { + warn(`窗口组${this._name}中未找到窗口${name} 删除失败`); + return; + } + let window = WindowManager.getWindow(name); + let header = window.getHeader(); + header && this._removeHeader(header); + + this._windowNames.splice(index, 1); + + // 关闭窗口 并从窗口map中移除 + WindowManager._removeWindow(name); + + // 处理窗口显示和隐藏状态 + this._processWindowHideStatus(this.size - 1, true); + if (this.size == 0) { + // 窗口组中不存在窗口时 隐藏窗口组节点 + this._root.visible = false; + } else if (lastIndex == index && index > 0) { + // 删除的窗口是最后一个 并且前边还有窗口 调整半透明节点的显示层级 + let topName = this.getTopWindowName(); + let window = WindowManager.getWindow(topName); + // 调整半透明遮罩 + this._adjustAlphaGraph(window); + // 调整窗口的显示层级 + window._setDepth(this._root.numChildren - 1); + } + this._processHeaderStatus(); + } + + /** + * 将指定名称的窗口移动到窗口组的最顶层。 + * @param name 窗口的名称。 + */ + public _moveWindowToTop(name: string): boolean { + let isMoved = false; + if (this.size == 0) { + warn(`WindowGroup.moveWindowToTop: window group 【${this._name}】 is empty`); + return; + } + if (this._windowNames[this.size - 1] == name) { + // 已经在最顶层了 + } else { + const index = this._windowNames.indexOf(name); + if (index == -1) { + warn(`WindowGroup.moveWindowToTop: window 【${name}】 not found in window group 【${this._name}】`); + return; + } + if (index < this._windowNames.length - 1) { + this._windowNames.splice(index, 1); + // 放到数组的末尾 + this._windowNames.push(name); + isMoved = true; + } + } + + let window = WindowManager.getWindow(name); + // 先调整半透明遮罩 + this._adjustAlphaGraph(window); + // 再调整窗口的显示层级 + window._setDepth(this._root.numChildren - 1); + // 处理窗口显示和隐藏状态 + this._processWindowHideStatus(this.size - 1, isMoved); + } + + /** + * 处理index下层窗口的隐藏状态的私有方法。递归调用 + * @param index - 窗口索引 + * @param isRecursion - 是否递归调用 + */ + private _processWindowHideStatus(index: number, isRecursion: boolean = true): void { + if (index < 0) { + return; + } + let windowName = this._windowNames[index]; + let curWindow = WindowManager.getWindow(windowName); + // 如果当前是当前组中的最后一个窗口并且当前窗口是隐藏状态 则恢复隐藏 + if (index == this.size - 1 && !curWindow.isShowing()) { + curWindow._showFromHide(); + } + if (index == 0) { + return; + } + let windowType = curWindow.type; + if (windowType == WindowType.HideAll) { + for (let i = index - 1; i >= 0; --i) { + let name = this._windowNames[i]; + const window = WindowManager.getWindow(name); + window.isShowing() && window._hide(); + } + return; + } else if (windowType == WindowType.HideOne) { + // 隐藏前一个 + let prevWindowName = this._windowNames[index - 1]; + let prevWindow = WindowManager.getWindow(prevWindowName); + prevWindow.isShowing() && prevWindow._hide(); + } else { + // 如果前一个窗口被隐藏了 需要恢复显示 + let prevWindowName = this._windowNames[index - 1]; + let prevWindow = WindowManager.getWindow(prevWindowName); + !prevWindow.isShowing() && prevWindow._showFromHide(); + } + isRecursion && this._processWindowHideStatus(index - 1, isRecursion); + } + + /** + * 新创建窗口时,根据新创建的窗口类型 + * 处理上一个窗口或者所有窗口的关闭 + */ + private _processWindowCloseStatus(window: IWindow): void { + // 新创建窗口 如果需要关闭窗口或者关闭所有窗口 处理窗口的关闭 + if (window.type == WindowType.CloseOne) { + let size = this.size; + while (size > 0) { + let name = this._windowNames.pop(); + let window = WindowManager.getWindow(name); + let header = window.getHeader(); + header && this._removeHeader(header); + WindowManager._removeWindow(name); + break; + } + } else if (window.type == WindowType.CloseAll) { + let size = this.size; + for (let i = size; i > 0;) { + let name = this._windowNames[--i] + let window = WindowManager.getWindow(name); + let header = window.getHeader(); + header && this._removeHeader(header); + WindowManager._removeWindow(name); + } + this._windowNames.length = 0; + } + } + + /** 处理header的显示状态 并调整层级 */ + private _processHeaderStatus(): void { + // 找到第一个要显示的header + let firstHeader: WindowHeader = null; + let firstWindow: IWindow = null; + let index = this.size - 1; + for (let i = this.size - 1; i >= 0; --i) { + let name = this._windowNames[i]; + let window = WindowManager.getWindow(name);; + if (window.isShowing() && window.getHeader()) { + firstWindow = window; + firstHeader = window.getHeader(); + index = i; + break; + } + } + this._headers.forEach((header, name) => { + this._root.setChildIndex(header, 0); + if (!firstHeader && header.visible) { + header._hide(); + } else if (firstHeader) { + if (firstHeader.name == name && !header.visible) { + header._show(firstWindow); + } else if (firstHeader.name != name && header.visible) { + header._hide(); + } + } + }); + if (firstHeader) { + if (index == this.size - 1) { + this._root.setChildIndex(firstHeader, this._root.numChildren - 1); + } else { + this._root.setChildIndex(firstHeader, this._root.numChildren - this.size + index - 1); + } + } + } + + /** + * 调整指定窗口的透明度图形。并根据窗口的背景透明度绘制半透明遮罩。 + * @param window - 需要调整透明度的窗口对象。 + */ + private _adjustAlphaGraph(window: IWindow): void { + this._root.setChildIndex(this._alphaGraph, this._root.numChildren - 1); + + // 半透明遮罩绘制 + this._color.a = window.bgAlpha * 255; + this._alphaGraph.clearGraphics(); + this._alphaGraph.drawRect(0, this._color, this._color); + } + + public hasWindow(name: string): boolean { + return this._windowNames.indexOf(name) >= 0; + } + + /** + * 获取窗口组中顶部窗口的名称。 + * @returns {string} 顶部窗口的名称。 + */ + public getTopWindowName(): string { + if (this.size > 0) { + return this._windowNames[this.size - 1]; + } + warn(`WindowGroup.getTopWindowName: window group 【${this._name}】 is empty`); + } + + + /** 根据窗口 创建顶部资源栏 (内部方法) */ + private _createHeader(window: IWindow): void { + // 只有创建界面的时候, 才会尝试创建顶部资源栏 + let headerInfo = window.getHeaderInfo(); + if (!headerInfo) { + return; + } + let name = headerInfo.name; + let header = this._getHeader(name); + if (header) { + window.setHeader(header); + header._addRef(); + } else { + // 创建header节点 + let { pkg } = WindowManager._getResPool().getHeader(name); + let newHeader = UIPackage.createObject(pkg, name) as WindowHeader; + newHeader.name = name; + newHeader.opaque = false; + window.setHeader(newHeader); + newHeader.visible = false; + PropsHelper.serializeProps(newHeader, pkg); + newHeader._init(); + newHeader._adapted(); + this._root.addChild(newHeader); + // 添加到显示节点 + newHeader._addRef(); + this._headers.set(newHeader.name, newHeader); + } + } + + /** + * 顶部资源栏窗口 从管理器中移除 (内部方法) + * @param header 资源栏 + */ + public _removeHeader(header: WindowHeader): void { + if (this._headers.has(header.name)) { + let refCount = header._decRef(); + if (refCount <= 0) { + this._headers.delete(header.name); + header._close(); + } + } + } + + /** + * 获取顶部资源栏 (内部方法) + * @param name 资源栏的名称 + */ + public _getHeader(name: string): T | null { + return this._headers.get(name) as T; + } + + /** 屏幕大小改变时被调用 (内部方法) */ + public _screenResize(): void { + this._headers.forEach((header) => { + header._screenResize(); + }); + this._alphaGraph.setPosition(Screen.ScreenWidth * 0.5, Screen.ScreenHeight * 0.5); + this._alphaGraph.setSize(Screen.ScreenWidth, Screen.ScreenHeight, true); + this._alphaGraph.setPivot(0.5, 0.5, true); + } +} diff --git a/src/ui/WindowHeaderInfo.ts b/src/ui/WindowHeaderInfo.ts new file mode 100644 index 0000000..52d13a7 --- /dev/null +++ b/src/ui/WindowHeaderInfo.ts @@ -0,0 +1,25 @@ +/** + * @Author: Gongxh + * @Date: 2025-01-10 + * @Description: 窗口顶部资源栏信息 + */ + +export class WindowHeaderInfo { + /** header名字 */ + name: string; + /** 自定义数据 用于Header窗口 onShow方法的自定义参数 */ + userdata: any; + + /** + * 创建 WindowHeaderInfo + * @param {string} name header窗口名 + * @param {*} [userdata] 自定义数据 + * @returns {WindowHeaderInfo} + */ + static create(name: string, userdata?: any): WindowHeaderInfo { + const info = new WindowHeaderInfo(); + info.name = name; + info.userdata = userdata; + return info; + } +} \ No newline at end of file diff --git a/src/ui/WindowManager.ts b/src/ui/WindowManager.ts new file mode 100644 index 0000000..7ca9d89 --- /dev/null +++ b/src/ui/WindowManager.ts @@ -0,0 +1,216 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-07 + * @Description: 窗口管理类 + */ + +import { debug, warn } from "../tool/log"; +import { ComponentExtendHelper } from "./ComponentExtendHelper"; +import { IWindow } from "./IWindow"; +import { _uidecorator } from "./UIDecorator"; +import { WindowGroup } from "./WindowGroup"; +import { WindowResPool } from "./WindowResPool"; + +export class WindowManager { + /** 窗口组 */ + private static _groups: Map = new Map(); + /** 不忽略查询的窗口组名 */ + private static _queryGroupNames: string[] = []; + /** 所有窗口全部放到这个map中 */ + private static _windows: Map = new Map(); + /** 初始化时传入实例 */ + private static _resPool: WindowResPool; + + /** + * 打开一个窗口 + * @param windowName 窗口名 + * @param userdata 用户数据 + */ + public static showWindow(windowName: string, userdata?: any): void { + //TODO::如果没有资源 加载资源 + this.showWindowIm(windowName, userdata); + } + + /** + * 显示指定名称的窗口,并传递可选的用户数据。 + * @param windowName - 窗口的名称。 + * @param userdata - 可选参数,用于传递给窗口的用户数据。 + */ + public static showWindowIm(windowName: string, userdata?: any): void { + const info = this._resPool.get(windowName); + const windowGroup = this.getWindowGroup(info.group); + windowGroup.showWindow(info, userdata); + } + + /** + * 关闭窗口 + * @param windowName 窗口名 + */ + public static closeWindow(windowName: string): void { + if (!this._windows.has(windowName)) { + warn(`窗口不存在 ${windowName} 不需要关闭`); + return; + } + // 先在窗口组中移除 + let info = this._resPool.get(windowName); + const windowGroup = this.getWindowGroup(info.group); + windowGroup._removeWindow(windowName); + // 窗口组中没有窗口了 + if (windowGroup.size == 0) { + let index = this._queryGroupNames.indexOf(windowGroup.name); + if (index > 0 && windowGroup.name == this.getTopGroupName()) { + do { + const groupName = this._queryGroupNames[--index]; + let group = this.getWindowGroup(groupName); + if (group.size > 0) { + this.getWindow(group.getTopWindowName())._recover(); + break; + } + } while (index >= 0); + } + } + } + + /** + * 获取当前最顶层的窗口实例。 + * @template T - 窗口实例的类型,必须继承自 IWindow 接口。 + * @returns {T | null} - 返回最顶层的窗口实例,如果没有找到则返回 null。 + * @description 该方法会遍历所有窗口组,找到最顶层的窗口并返回其实例。 + */ + public static getTopWindow(): T | null { + let len = this._queryGroupNames.length; + for (let i = len; i > 0;) { + let group = this.getWindowGroup(this._queryGroupNames[--i]); + if (group.size > 0) { + return this.getWindow(group.getTopWindowName()); + } + } + return null; + } + + /** + * 根据窗口名称获取窗口实例。 + * @template T 窗口类型,必须继承自IWindow接口。 + * @param name 窗口的名称。 + * @returns 如果找到窗口,则返回对应类型的窗口实例;否则返回null。 + */ + public static getWindow(name: string): T | null { + return this._windows.get(name) as T; + } + + /** + * 检查是否存在指定名称的窗口。 + * @param name 窗口的名称。 + * @returns 如果存在指定名称的窗口,则返回 true,否则返回 false。 + */ + public static hasWindow(name: string): boolean { + return this._windows.has(name); + } + + /** + * 根据给定的组名获取窗口组。如果组不存在,则抛出错误。 + * @param groupName 窗口组的名称。 + * @returns 返回找到的窗口组。 + */ + public static getWindowGroup(groupName: string): WindowGroup { + if (this._groups.has(groupName)) { + return this._groups.get(groupName); + } + throw new Error(`WindowManager.getWindowGroup: window group 【${groupName}】 not found`); + } + + + /** + * 获取当前顶层窗口组的名称。 + * 返回第一个包含至少一个窗口的窗口组名称。(该方法只检查不忽略查询的窗口组) + * 如果没有找到任何包含窗口的组,则返回空字符串。 + */ + public static getTopGroupName(): string { + let len = this._queryGroupNames.length; + for (let i = len - 1; i >= 0; i--) { + let name = this._queryGroupNames[i]; + let group = this._groups.get(name); + if (group.size > 0) { + return name; + } + } + return ""; + } + + /** + * 初始化窗口管理器,设置资源池。 (框架内部使用) + * @param resPool - 窗口资源池实例。 + */ + public static _init(resPool: WindowResPool): void { + this._resPool = resPool; + } + + /** + * 向窗口管理器添加一个新窗口。 (框架内部使用) + * @param name 窗口的唯一标识符。 + * @param window 要添加的窗口对象,需实现 IWindow 接口。 + */ + public static _addWindow(name: string, window: IWindow): void { + this._windows.set(name, window); + } + + /** + * 移除指定名称的窗口。 (框架内部使用) + * @param name 窗口的名称。 + */ + public static _removeWindow(name: string): void { + if (this.hasWindow(name)) { + this._windows.get(name)._close(); + this._windows.delete(name); + } + } + + /** + * 注册所有UI窗口类到资源池中。 (框架内部使用) + * 该方法遍历所有通过_uidecorator.getWindowMaps()获取的窗口映射, + * 并将每个窗口的资源名称、构造函数、分组和包信息添加到资源池中。 + */ + public static registerUI(): void { + // 窗口注册 + for (const { ctor, res } of _uidecorator.getWindowMaps().values()) { + debug(`窗口注册 窗口名:${res.name} 包名:${res.pkg} 组名:${res.group}`); + this._resPool.add(ctor, res.group, res.pkg, res.name); + } + // 窗口header注册 + for (const { ctor, res } of _uidecorator.getHeaderMaps().values()) { + debug(`header注册 header名:${res.name} 包名:${res.pkg}`); + this._resPool.addHeader(ctor, res.pkg, res.name); + } + // 组件注册 + ComponentExtendHelper.register(); + } + + /** + * 向窗口管理器添加一个窗口组 如果窗口组名称已存在,则抛出错误. (内部方法) + * @param group 要添加的窗口组 + */ + public static _addWindowGroup(group: WindowGroup): void { + if (this._groups.has(group.name)) { + throw new Error(`WindowManager._addWindowGroup: window group 【${group.name}】 already exists`); + } + this._groups.set(group.name, group); + // 不忽略查询 加到列表中 + !group.isIgnore && this._queryGroupNames.push(group.name); + } + + /** + * 屏幕大小改变时 调用所有窗口的screenResize方法 (内部方法) + */ + public static _screenResize(): void { + this._windows.forEach((window: IWindow) => { + window.screenResize(); + }); + this._groups.forEach((group: WindowGroup) => { + group._screenResize(); + }); + } + + public static _getResPool(): WindowResPool { + return this._resPool; + } +} diff --git a/src/ui/WindowResPool.ts b/src/ui/WindowResPool.ts new file mode 100644 index 0000000..180bb5b --- /dev/null +++ b/src/ui/WindowResPool.ts @@ -0,0 +1,85 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-13 + * @Description: + */ + +import { UIObjectFactory } from "fairygui-cc"; + +export interface WindowInfo { + /** 类的构造函数 */ + ctor: any; + /** 窗口组名 */ + group: string; + /** fgui包名 */ + pkg: string; + /** 窗口名 */ + name: string; +} + +export interface HeaderInfo { + ctor: any; + pkg: string; +} + +export class WindowResPool { + /** 窗口信息池 */ + protected _windowInfos: Map = new Map(); + /** 窗口header信息池 */ + protected _headerInfos: Map = new Map(); + + /** 可扩展 窗口资源引用计数 */ + + /** + * 注册窗口信息 + * @param info + */ + public add(ctor: any, group: string, pkg: string, name: string): void { + if (this.has(name)) { + throw new Error(`窗口【${name}】信息已注册 请勿重复注册`); + } + this._windowInfos.set(name, { + ctor: ctor, + group: group, + pkg: pkg, + name: name + }); + // 窗口组件扩展 + UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor); + } + + public has(name: string): boolean { + return this._windowInfos.has(name); + } + + public get(name: string): WindowInfo { + if (!this.has(name)) { + throw new Error(`窗口【${name}】未注册,请使用 _uidecorator.uiclass 注册窗口`); + } + return this._windowInfos.get(name); + } + + /** + * 注册窗口header信息 + * @param info + */ + public addHeader(ctor: any, pkg: string, name: string): void { + this._headerInfos.set(name, { + ctor: ctor, + pkg: pkg + }); + // 窗口header扩展 + UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor); + } + + public hasHeader(name: string): boolean { + return this._headerInfos.has(name); + } + + public getHeader(name: string): HeaderInfo { + if (!this.hasHeader(name)) { + throw new Error(`窗口header【${name}】未注册,请使用 _uidecorator.uiheader 注册窗口header`); + } + return this._headerInfos.get(name); + } +} \ No newline at end of file diff --git a/src/ui/header.ts b/src/ui/header.ts new file mode 100644 index 0000000..fcac045 --- /dev/null +++ b/src/ui/header.ts @@ -0,0 +1,30 @@ +/** + * @Author: Gongxh + * @Date: 2024-12-08 + * @Description: 窗口的一些类型配置 + */ + +/** 窗口显示时,对其他窗口的隐藏处理类型 */ +export enum WindowType { + /** 不做任何处理 */ + Normal = 0, + /** 关闭所有 */ + CloseAll = 1 << 0, + /** 关闭上一个 */ + CloseOne = 1 << 1, + /** 隐藏所有 */ + HideAll = 1 << 2, + /** 隐藏上一个 */ + HideOne = 1 << 3, +} + +/** 窗口适配类型,默认全屏 */ +export enum AdapterType { + /** 全屏适配 */ + Full = 0, + /** 空出刘海 */ + Bang = 1, + /** 固定的 不适配 */ + Fixed = 2, +} + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f21f222 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "es6", // + "module": "es6", // + "experimentalDecorators": true, // 启用ES装饰器。 + "strict": true, + "strictNullChecks": false, + "moduleResolution": "Node", + "skipLibCheck": true, + "esModuleInterop": true, + "types": [ + "@cocos/creator-types/engine", + "@cocos/creator-types/editor", + ] + }, + "include": [ + "./src/**/*" + // "libs" + ], + // 排除 + "exclude": [ + "node_modules", + "dist", + "build" + ] +} \ No newline at end of file