diff --git a/.gitmodules b/.gitmodules index 65de3a54..861c12f9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,27 +4,12 @@ [submodule "thirdparty/admin-backend"] path = thirdparty/admin-backend url = https://github.com/esengine/admin-backend.git -[submodule "extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension"] - path = extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension - url = https://github.com/esengine/cocos-ecs-extension.git -[submodule "extensions/cocos/cocos-ecs/extensions/behaviour-tree"] - path = extensions/cocos/cocos-ecs/extensions/behaviour-tree - url = https://github.com/esengine/behaviour-tree.git -[submodule "extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen"] - path = extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen - url = https://github.com/esengine/cocos-terrain-gen.git -[submodule "extensions/cocos/cocos-ecs/extensions/mvvm-designer"] - path = extensions/cocos/cocos-ecs/extensions/mvvm-designer - url = https://github.com/esengine/mvvm-designer.git [submodule "thirdparty/mvvm-ui-framework"] path = thirdparty/mvvm-ui-framework url = https://github.com/esengine/mvvm-ui-framework.git [submodule "thirdparty/cocos-nexus"] path = thirdparty/cocos-nexus url = https://github.com/esengine/cocos-nexus.git -[submodule "extensions/cocos/cocos-ecs/extensions/utilityai_designer"] - path = extensions/cocos/cocos-ecs/extensions/utilityai_designer - url = https://github.com/esengine/utilityai_designer.git [submodule "thirdparty/ecs-astar"] path = thirdparty/ecs-astar url = https://github.com/esengine/ecs-astar.git diff --git a/extensions/cocos/cocos-ecs/.creator/asset-template/typescript/Custom Script Template Help Documentation.url b/extensions/cocos/cocos-ecs/.creator/asset-template/typescript/Custom Script Template Help Documentation.url deleted file mode 100644 index 7606df06..00000000 --- a/extensions/cocos/cocos-ecs/.creator/asset-template/typescript/Custom Script Template Help Documentation.url +++ /dev/null @@ -1,2 +0,0 @@ -[InternetShortcut] -URL=https://docs.cocos.com/creator/manual/en/scripting/setup.html#custom-script-template \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/.gitignore b/extensions/cocos/cocos-ecs/.gitignore deleted file mode 100644 index a231b3f5..00000000 --- a/extensions/cocos/cocos-ecs/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ - -#/////////////////////////// -# Cocos Creator 3D Project -#/////////////////////////// -library/ -temp/ -local/ -build/ -profiles/ -native -#////////////////////////// -# NPM -#////////////////////////// -node_modules/ - -#////////////////////////// -# VSCode -#////////////////////////// -.vscode/ - -#////////////////////////// -# WebStorm -#////////////////////////// -.idea/ \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/resources.meta b/extensions/cocos/cocos-ecs/assets/resources.meta deleted file mode 100644 index cf9b781c..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources.meta +++ /dev/null @@ -1,14 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "2a691dda-d56d-4a72-9fef-111a999415db", - "files": [], - "subMetas": {}, - "userData": { - "isBundle": true, - "bundleConfigID": "default", - "bundleName": "resources", - "priority": 8 - } -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/effects.meta b/extensions/cocos/cocos-ecs/assets/resources/effects.meta deleted file mode 100644 index 903e8123..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/effects.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "8c25761f-50d6-498b-a95f-d863bf1fbff1", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/materials.meta b/extensions/cocos/cocos-ecs/assets/resources/materials.meta deleted file mode 100644 index 04fcbe6c..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/materials.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "3a66cbbc-6612-4408-838b-875d0bb2e9a3", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json b/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json deleted file mode 100644 index 6cd4b934..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json +++ /dev/null @@ -1,317 +0,0 @@ -{ - "nodes": [ - { - "id": "node_15iffhg4p", - "type": "root", - "name": "根节点", - "description": "行为树的根节点,每棵树只能有一个根节点", - "children": [ - "node_o6tsnrxyg" - ] - }, - { - "id": "node_o6tsnrxyg", - "type": "selector", - "name": "选择器", - "description": "按顺序执行子节点,任一成功则整体成功", - "properties": { - "abortType": "LowerPriority" - }, - "children": [ - "node_tljchzbno", - "node_txhx0hau5", - "node_r9kvcwv8u", - "node_520hedw22" - ] - }, - { - "id": "node_tljchzbno", - "type": "conditional-decorator", - "name": "休息条件装饰器", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "properties": { - "conditionType": "blackboardCompare", - "executeWhenTrue": true, - "abortType": "LowerPriority", - "shouldReevaluate": true, - "variableName": "{{isLowStamina}}", - "operator": "equal", - "compareValue": "true" - }, - "children": [ - "node_ulp8qx68h" - ], - "condition": { - "type": "blackboard-value-comparison", - "properties": {} - } - }, - { - "id": "node_txhx0hau5", - "type": "conditional-decorator", - "name": "存储条件装饰器", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "properties": { - "conditionType": "blackboardCompare", - "executeWhenTrue": true, - "abortType": "LowerPriority", - "shouldReevaluate": true, - "variableName": "{{hasOre}}", - "operator": "equal", - "compareValue": "true" - }, - "children": [ - "node_dhsz8rgl1" - ], - "condition": { - "type": "blackboard-value-comparison", - "properties": {} - } - }, - { - "id": "node_r9kvcwv8u", - "type": "conditional-decorator", - "name": "挖矿条件装饰器", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "properties": { - "conditionType": "blackboardCompare", - "executeWhenTrue": true, - "abortType": "LowerPriority", - "shouldReevaluate": true, - "variableName": "{{isLowStamina}}", - "operator": "equal", - "compareValue": "false" - }, - "children": [ - "node_zguxml6u7" - ], - "condition": { - "type": "blackboard-value-comparison", - "properties": {} - } - }, - { - "id": "node_ulp8qx68h", - "type": "sequence", - "name": "序列器", - "description": "按顺序执行子节点,任一失败则整体失败", - "properties": { - "abortType": "None" - }, - "children": [ - "node_0fgq85ovw", - "node_9v13vpqyr" - ] - }, - { - "id": "node_0fgq85ovw", - "type": "event-action", - "name": "回家休息", - "description": "执行已注册的事件处理函数(推荐)", - "properties": { - "eventName": "go-home-rest", - "parameters": "{}" - } - }, - { - "id": "node_9v13vpqyr", - "type": "event-action", - "name": "恢复体力", - "description": "执行已注册的事件处理函数(推荐)", - "properties": { - "eventName": "recover-stamina", - "parameters": "{}" - } - }, - { - "id": "node_ui4ja9mlj", - "type": "event-action", - "name": "前往仓库存储", - "description": "执行已注册的事件处理函数(推荐)", - "properties": { - "eventName": "store-ore", - "parameters": "{}" - } - }, - { - "id": "node_969njccy2", - "type": "event-action", - "name": "挖掘金矿", - "description": "执行已注册的事件处理函数(推荐)", - "properties": { - "eventName": "mine-gold-ore", - "parameters": "{}" - } - }, - { - "id": "node_520hedw22", - "type": "event-action", - "name": "默认待机", - "description": "执行已注册的事件处理函数(推荐)", - "properties": { - "eventName": "idle-behavior", - "parameters": "{}" - } - }, - { - "id": "node_o5c7hv5wx", - "type": "set-blackboard-value", - "name": "设置黑板变量", - "description": "设置黑板变量的值", - "properties": { - "variableName": "{{hasOre}}", - "value": "false" - } - }, - { - "id": "node_zf0sgkqev", - "type": "set-blackboard-value", - "name": "设置黑板变量", - "description": "设置黑板变量的值", - "properties": { - "variableName": "{{hasOre}}", - "value": "true" - } - }, - { - "id": "node_dhsz8rgl1", - "type": "sequence", - "name": "序列器", - "description": "按顺序执行子节点,任一失败则整体失败", - "properties": { - "abortType": "None" - }, - "children": [ - "node_ui4ja9mlj", - "node_o5c7hv5wx" - ] - }, - { - "id": "node_zguxml6u7", - "type": "sequence", - "name": "序列器", - "description": "按顺序执行子节点,任一失败则整体失败", - "properties": { - "abortType": "None" - }, - "children": [ - "node_969njccy2", - "node_zf0sgkqev" - ] - } - ], - "blackboard": [ - { - "name": "unitType", - "type": "string", - "value": "miner", - "description": "单位类型", - "group": "基础属性" - }, - { - "name": "currentHealth", - "type": "number", - "value": 100, - "description": "当前生命值", - "group": "基础属性" - }, - { - "name": "maxHealth", - "type": "number", - "value": 100, - "description": "最大生命值", - "group": "基础属性" - }, - { - "name": "stamina", - "type": "number", - "value": 100, - "description": "当前体力值 - 挖矿会消耗体力", - "group": "体力系统" - }, - { - "name": "maxStamina", - "type": "number", - "value": 100, - "description": "最大体力值", - "group": "体力系统" - }, - { - "name": "staminaPercentage", - "type": "number", - "value": 1, - "description": "体力百分比", - "group": "体力系统" - }, - { - "name": "isLowStamina", - "type": "boolean", - "value": false, - "description": "是否低体力 - 体力低于20%时为true", - "group": "体力系统" - }, - { - "name": "isResting", - "type": "boolean", - "value": false, - "description": "是否正在休息", - "group": "体力系统" - }, - { - "name": "homePosition", - "type": "vector3", - "value": { - "x": 0, - "y": 0, - "z": 0 - }, - "description": "家的位置 - 矿工休息的地方", - "group": "体力系统" - }, - { - "name": "hasOre", - "type": "boolean", - "value": false, - "description": "是否携带矿石", - "group": "工作状态" - }, - { - "name": "currentCommand", - "type": "string", - "value": "mine", - "description": "当前命令", - "group": "工作状态" - }, - { - "name": "hasTarget", - "type": "boolean", - "value": false, - "description": "是否有目标", - "group": "工作状态" - }, - { - "name": "targetPosition", - "type": "vector3", - "value": { - "x": 0, - "y": 0, - "z": 0 - }, - "description": "目标位置", - "group": "移动属性" - }, - { - "name": "isMoving", - "type": "boolean", - "value": false, - "description": "是否正在移动", - "group": "移动属性" - } - ], - "metadata": { - "name": "behavior-tree", - "created": "2025-06-25T14:06:55.596Z", - "version": "1.0", - "exportType": "clean" - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json.meta b/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json.meta deleted file mode 100644 index 21521c62..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.bt.json.meta +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ver": "2.0.1", - "importer": "json", - "imported": true, - "uuid": "598e1450-8c7a-46c7-9540-398f9809d627", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree b/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree deleted file mode 100644 index 62dc4f05..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree +++ /dev/null @@ -1,1395 +0,0 @@ -{ - "version": "1.0", - "type": "behavior-tree-editor", - "metadata": { - "name": "assets/resources/miner-stamina-ai.btree", - "created": "2025-06-25T08:41:23.672Z", - "modified": "2025-06-25T08:41:23.672Z", - "version": "1.0", - "editorVersion": "1.0.0" - }, - "nodes": [ - { - "id": "node_15iffhg4p", - "type": "root", - "name": "根节点", - "icon": "🌳", - "description": "行为树的根节点,每棵树只能有一个根节点", - "x": 1080, - "y": 50, - "children": [ - "node_o6tsnrxyg" - ], - "properties": {}, - "canHaveChildren": true, - "canHaveParent": false, - "maxChildren": 1, - "minChildren": 1, - "hasError": false - }, - { - "id": "node_o6tsnrxyg", - "type": "selector", - "name": "选择器", - "icon": "?", - "description": "按顺序执行子节点,任一成功则整体成功", - "x": 1090, - "y": 208, - "children": [ - "node_tljchzbno", - "node_txhx0hau5", - "node_r9kvcwv8u", - "node_520hedw22" - ], - "properties": { - "abortType": { - "name": "中止类型", - "type": "select", - "value": "LowerPriority", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "minChildren": 1, - "hasError": false, - "parent": "node_15iffhg4p" - }, - { - "id": "node_tljchzbno", - "type": "conditional-decorator", - "name": "休息条件装饰器", - "icon": "🔀", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "x": 515, - "y": 414, - "children": [ - "node_ulp8qx68h" - ], - "properties": { - "conditionType": { - "name": "条件类型", - "type": "select", - "value": "blackboardCompare", - "description": "装饰器使用的条件类型", - "options": [ - "custom", - "random", - "hasComponent", - "hasTag", - "isActive", - "numericCompare", - "propertyExists" - ], - "required": false - }, - "executeWhenTrue": { - "name": "条件为真时执行", - "type": "boolean", - "value": true, - "description": "条件为真时是否执行子节点", - "required": false - }, - "executeWhenFalse": { - "name": "条件为假时执行", - "type": "boolean", - "value": false, - "description": "条件为假时是否执行子节点", - "required": false - }, - "checkInterval": { - "name": "检查间隔", - "type": "number", - "value": 0, - "description": "条件检查间隔时间(秒),0表示每帧检查", - "required": false - }, - "abortType": { - "name": "中止类型", - "type": "select", - "value": "LowerPriority", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - }, - "shouldReevaluate": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": true, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "variableName": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": "{{isLowStamina}}", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "operator": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": "equal", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareValue": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": "true", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareVariable": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": "", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "maxChildren": 1, - "minChildren": 1, - "hasError": false, - "parent": "node_o6tsnrxyg", - "attachedCondition": { - "type": "blackboard-value-comparison", - "name": "黑板值比较", - "icon": "⚖️" - }, - "conditionExpanded": false - }, - { - "id": "node_txhx0hau5", - "type": "conditional-decorator", - "name": "存储条件装饰器", - "icon": "🔀", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "x": 975, - "y": 414, - "children": [ - "node_dhsz8rgl1" - ], - "properties": { - "conditionType": { - "name": "条件类型", - "type": "select", - "value": "blackboardCompare", - "description": "装饰器使用的条件类型", - "options": [ - "custom", - "random", - "hasComponent", - "hasTag", - "isActive", - "numericCompare", - "propertyExists" - ], - "required": false - }, - "executeWhenTrue": { - "name": "条件为真时执行", - "type": "boolean", - "value": true, - "description": "条件为真时是否执行子节点", - "required": false - }, - "executeWhenFalse": { - "name": "条件为假时执行", - "type": "boolean", - "value": false, - "description": "条件为假时是否执行子节点", - "required": false - }, - "checkInterval": { - "name": "检查间隔", - "type": "number", - "value": 0, - "description": "条件检查间隔时间(秒),0表示每帧检查", - "required": false - }, - "abortType": { - "name": "中止类型", - "type": "select", - "value": "LowerPriority", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - }, - "shouldReevaluate": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": true, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "variableName": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": "{{hasOre}}", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "operator": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": "equal", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareValue": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": "true", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareVariable": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": "", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "maxChildren": 1, - "minChildren": 1, - "hasError": false, - "parent": "node_o6tsnrxyg", - "attachedCondition": { - "type": "blackboard-value-comparison", - "name": "黑板值比较", - "icon": "⚖️" - }, - "conditionExpanded": false - }, - { - "id": "node_r9kvcwv8u", - "type": "conditional-decorator", - "name": "挖矿条件装饰器", - "icon": "🔀", - "description": "基于条件执行子节点(拖拽条件节点到此装饰器来配置条件)", - "x": 1435, - "y": 414, - "children": [ - "node_zguxml6u7" - ], - "properties": { - "conditionType": { - "name": "条件类型", - "type": "select", - "value": "blackboardCompare", - "description": "装饰器使用的条件类型", - "options": [ - "custom", - "random", - "hasComponent", - "hasTag", - "isActive", - "numericCompare", - "propertyExists" - ], - "required": false - }, - "executeWhenTrue": { - "name": "条件为真时执行", - "type": "boolean", - "value": true, - "description": "条件为真时是否执行子节点", - "required": false - }, - "executeWhenFalse": { - "name": "条件为假时执行", - "type": "boolean", - "value": false, - "description": "条件为假时是否执行子节点", - "required": false - }, - "checkInterval": { - "name": "检查间隔", - "type": "number", - "value": 0, - "description": "条件检查间隔时间(秒),0表示每帧检查", - "required": false - }, - "abortType": { - "name": "中止类型", - "type": "select", - "value": "LowerPriority", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - }, - "shouldReevaluate": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": { - "name": "shouldReevaluate", - "type": "string", - "value": true, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "variableName": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": { - "name": "variableName", - "type": "string", - "value": "{{isLowStamina}}", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "operator": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": { - "name": "operator", - "type": "string", - "value": "equal", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareValue": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": { - "name": "compareValue", - "type": "string", - "value": "false", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "compareVariable": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": { - "name": "compareVariable", - "type": "string", - "value": "", - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - }, - "description": "", - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "maxChildren": 1, - "minChildren": 1, - "hasError": false, - "parent": "node_o6tsnrxyg", - "attachedCondition": { - "type": "blackboard-value-comparison", - "name": "黑板值比较", - "icon": "⚖️" - }, - "conditionExpanded": false - }, - { - "id": "node_ulp8qx68h", - "type": "sequence", - "name": "序列器", - "icon": "→", - "description": "按顺序执行子节点,任一失败则整体失败", - "x": 515, - "y": 804, - "children": [ - "node_0fgq85ovw", - "node_9v13vpqyr" - ], - "properties": { - "abortType": { - "name": "中止类型", - "type": "select", - "value": "None", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "minChildren": 1, - "hasError": false, - "parent": "node_tljchzbno" - }, - { - "id": "node_0fgq85ovw", - "type": "event-action", - "name": "回家休息", - "icon": "📢", - "description": "执行已注册的事件处理函数(推荐)", - "x": 400, - "y": 1010, - "children": [], - "properties": { - "eventName": { - "name": "事件名称", - "type": "string", - "value": "go-home-rest", - "description": "要执行的事件名称(如:enemy.attack, player.move)", - "required": true - }, - "parameters": { - "name": "事件参数", - "type": "string", - "value": "{}", - "description": "传递给事件处理函数的参数(JSON格式)", - "required": false - }, - "timeout": { - "name": "超时时间", - "type": "number", - "value": 0, - "description": "事件执行超时时间(秒),0表示无限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_ulp8qx68h" - }, - { - "id": "node_9v13vpqyr", - "type": "event-action", - "name": "恢复体力", - "icon": "📢", - "description": "执行已注册的事件处理函数(推荐)", - "x": 630, - "y": 1010, - "children": [], - "properties": { - "eventName": { - "name": "事件名称", - "type": "string", - "value": "recover-stamina", - "description": "要执行的事件名称(如:enemy.attack, player.move)", - "required": true - }, - "parameters": { - "name": "事件参数", - "type": "string", - "value": "{}", - "description": "传递给事件处理函数的参数(JSON格式)", - "required": false - }, - "timeout": { - "name": "超时时间", - "type": "number", - "value": 0, - "description": "事件执行超时时间(秒),0表示无限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_ulp8qx68h" - }, - { - "id": "node_ui4ja9mlj", - "type": "event-action", - "name": "前往仓库存储", - "icon": "📢", - "description": "执行已注册的事件处理函数(推荐)", - "x": 860, - "y": 1010, - "children": [], - "properties": { - "eventName": { - "name": "事件名称", - "type": "string", - "value": "store-ore", - "description": "要执行的事件名称(如:enemy.attack, player.move)", - "required": true - }, - "parameters": { - "name": "事件参数", - "type": "string", - "value": "{}", - "description": "传递给事件处理函数的参数(JSON格式)", - "required": false - }, - "timeout": { - "name": "超时时间", - "type": "number", - "value": 0, - "description": "事件执行超时时间(秒),0表示无限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_dhsz8rgl1" - }, - { - "id": "node_969njccy2", - "type": "event-action", - "name": "挖掘金矿", - "icon": "📢", - "description": "执行已注册的事件处理函数(推荐)", - "x": 1320, - "y": 1010, - "children": [], - "properties": { - "eventName": { - "name": "事件名称", - "type": "string", - "value": "mine-gold-ore", - "description": "要执行的事件名称(如:enemy.attack, player.move)", - "required": true - }, - "parameters": { - "name": "事件参数", - "type": "string", - "value": "{}", - "description": "传递给事件处理函数的参数(JSON格式)", - "required": false - }, - "timeout": { - "name": "超时时间", - "type": "number", - "value": 0, - "description": "事件执行超时时间(秒),0表示无限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_zguxml6u7" - }, - { - "id": "node_520hedw22", - "type": "event-action", - "name": "默认待机", - "icon": "📢", - "description": "执行已注册的事件处理函数(推荐)", - "x": 1780, - "y": 414, - "children": [], - "properties": { - "eventName": { - "name": "事件名称", - "type": "string", - "value": "idle-behavior", - "description": "要执行的事件名称(如:enemy.attack, player.move)", - "required": true - }, - "parameters": { - "name": "事件参数", - "type": "string", - "value": "{}", - "description": "传递给事件处理函数的参数(JSON格式)", - "required": false - }, - "timeout": { - "name": "超时时间", - "type": "number", - "value": 0, - "description": "事件执行超时时间(秒),0表示无限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_o6tsnrxyg" - }, - { - "id": "node_o5c7hv5wx", - "type": "set-blackboard-value", - "name": "设置黑板变量", - "icon": "📝", - "description": "设置黑板变量的值", - "x": 1090, - "y": 1010, - "children": [], - "properties": { - "variableName": { - "name": "变量名", - "type": "string", - "value": "{{hasOre}}", - "description": "黑板变量名", - "required": true - }, - "value": { - "name": "设置值", - "type": "string", - "value": "false", - "description": "要设置的值(留空则使用源变量)", - "required": false - }, - "sourceVariable": { - "name": "源变量名", - "type": "string", - "value": "", - "description": "从另一个黑板变量复制值", - "required": false - }, - "force": { - "name": "强制设置", - "type": "boolean", - "value": false, - "description": "是否忽略只读限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_dhsz8rgl1" - }, - { - "id": "node_zf0sgkqev", - "type": "set-blackboard-value", - "name": "设置黑板变量", - "icon": "📝", - "description": "设置黑板变量的值", - "x": 1550, - "y": 1010, - "children": [], - "properties": { - "variableName": { - "name": "变量名", - "type": "string", - "value": "{{hasOre}}", - "description": "黑板变量名", - "required": true - }, - "value": { - "name": "设置值", - "type": "string", - "value": "true", - "description": "要设置的值(留空则使用源变量)", - "required": false - }, - "sourceVariable": { - "name": "源变量名", - "type": "string", - "value": "", - "description": "从另一个黑板变量复制值", - "required": false - }, - "force": { - "name": "强制设置", - "type": "boolean", - "value": false, - "description": "是否忽略只读限制", - "required": false - } - }, - "canHaveChildren": false, - "canHaveParent": true, - "maxChildren": 0, - "hasError": false, - "parent": "node_zguxml6u7" - }, - { - "id": "node_dhsz8rgl1", - "type": "sequence", - "name": "序列器", - "icon": "→", - "description": "按顺序执行子节点,任一失败则整体失败", - "x": 975, - "y": 804, - "children": [ - "node_ui4ja9mlj", - "node_o5c7hv5wx" - ], - "properties": { - "abortType": { - "name": "中止类型", - "type": "select", - "value": "None", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "minChildren": 1, - "hasError": false, - "parent": "node_txhx0hau5" - }, - { - "id": "node_zguxml6u7", - "type": "sequence", - "name": "序列器", - "icon": "→", - "description": "按顺序执行子节点,任一失败则整体失败", - "x": 1435, - "y": 804, - "children": [ - "node_969njccy2", - "node_zf0sgkqev" - ], - "properties": { - "abortType": { - "name": "中止类型", - "type": "select", - "value": "None", - "description": "决定节点在何种情况下会被中止", - "options": [ - "None", - "LowerPriority", - "Self", - "Both" - ], - "required": false - } - }, - "canHaveChildren": true, - "canHaveParent": true, - "minChildren": 1, - "hasError": false, - "parent": "node_r9kvcwv8u" - } - ], - "connections": [ - { - "id": "node_15iffhg4p-node_o6tsnrxyg", - "sourceId": "node_15iffhg4p", - "targetId": "node_o6tsnrxyg", - "path": "M 1159.999999999999 144.9999999999999 C 1159.999999999999 176.69639587402332 1170.5931091308585 176.69639587402332 1170.5931091308585 208.39279174804673", - "active": false - }, - { - "id": "node_o6tsnrxyg-node_tljchzbno", - "sourceId": "node_o6tsnrxyg", - "targetId": "node_tljchzbno", - "path": "M 1169.4068908691397 336.32621765136696 C 1169.4068908691397 376.16310119628884 625.0000381469722 376.16310119628884 625.0000381469722 415.99998474121065", - "active": false - }, - { - "id": "node_o6tsnrxyg-node_txhx0hau5", - "sourceId": "node_o6tsnrxyg", - "targetId": "node_txhx0hau5", - "path": "M 1169.4068908691397 336.32621765136696 C 1169.4068908691397 376.16310119628884 1084.999999999999 376.16310119628884 1084.999999999999 415.99998474121065", - "active": false - }, - { - "id": "node_o6tsnrxyg-node_r9kvcwv8u", - "sourceId": "node_o6tsnrxyg", - "targetId": "node_r9kvcwv8u", - "path": "M 1169.4068908691397 336.32621765136696 C 1169.4068908691397 376.16310119628884 1545.0000762939442 376.16310119628884 1545.0000762939442 415.99998474121065", - "active": false - }, - { - "id": "node_o6tsnrxyg-node_520hedw22", - "sourceId": "node_o6tsnrxyg", - "targetId": "node_520hedw22", - "path": "M 1169.4068908691397 336.32621765136696 C 1169.4068908691397 376.16310119628884 1860.000076293944 376.16310119628884 1860.000076293944 415.99998474121065", - "active": false - }, - { - "id": "node_tljchzbno-node_ulp8qx68h", - "sourceId": "node_tljchzbno", - "targetId": "node_ulp8qx68h", - "path": "M 625.0000381469722 642.0781707763667 C 625.0000381469722 722.0781707763667 595.0000381469722 726.0000610351557 595.0000381469722 806.0000610351557", - "active": false - }, - { - "id": "node_txhx0hau5-node_dhsz8rgl1", - "sourceId": "node_txhx0hau5", - "targetId": "node_dhsz8rgl1", - "path": "M 1084.999999999999 642.0781707763667 C 1084.999999999999 722.0781707763667 1054.9999999999993 726.0000610351557 1054.9999999999993 806.0000610351557", - "active": false - }, - { - "id": "node_r9kvcwv8u-node_zguxml6u7", - "sourceId": "node_r9kvcwv8u", - "targetId": "node_zguxml6u7", - "path": "M 1545.0000762939442 642.0781707763667 C 1545.0000762939442 722.0781707763667 1515.0000762939442 726.0000610351557 1515.0000762939442 806.0000610351557", - "active": false - }, - { - "id": "node_ulp8qx68h-node_0fgq85ovw", - "sourceId": "node_ulp8qx68h", - "targetId": "node_0fgq85ovw", - "path": "M 595.0000381469722 932.5937652587884 C 595.0000381469722 972.2969055175774 480.0000381469723 972.2969055175774 480.0000381469723 1012.0000457763664", - "active": false - }, - { - "id": "node_ulp8qx68h-node_9v13vpqyr", - "sourceId": "node_ulp8qx68h", - "targetId": "node_9v13vpqyr", - "path": "M 595.0000381469722 932.5937652587884 C 595.0000381469722 972.2969055175774 709.9999999999994 972.2969055175774 709.9999999999994 1012.0000457763664", - "active": false - }, - { - "id": "node_dhsz8rgl1-node_ui4ja9mlj", - "sourceId": "node_dhsz8rgl1", - "targetId": "node_ui4ja9mlj", - "path": "M 1054.9999999999993 932.5937652587884 C 1054.9999999999993 972.2969055175774 939.9999999999993 972.2969055175774 939.9999999999993 1012.0000457763664", - "active": false - }, - { - "id": "node_dhsz8rgl1-node_o5c7hv5wx", - "sourceId": "node_dhsz8rgl1", - "targetId": "node_o5c7hv5wx", - "path": "M 1054.9999999999993 932.5937652587884 C 1054.9999999999993 972.2969055175774 1169.999999999999 972.2969055175774 1169.999999999999 1012.0000457763664", - "active": false - }, - { - "id": "node_zguxml6u7-node_969njccy2", - "sourceId": "node_zguxml6u7", - "targetId": "node_969njccy2", - "path": "M 1515.0000762939442 932.5937652587884 C 1515.0000762939442 972.2969055175774 1400.0000762939442 972.2969055175774 1400.0000762939442 1012.0000457763664", - "active": false - }, - { - "id": "node_zguxml6u7-node_zf0sgkqev", - "sourceId": "node_zguxml6u7", - "targetId": "node_zf0sgkqev", - "path": "M 1515.0000762939442 932.5937652587884 C 1515.0000762939442 972.2969055175774 1630.0000762939442 972.2969055175774 1630.0000762939442 1012.0000457763664", - "active": false - } - ], - "blackboard": [ - { - "name": "unitType", - "type": "string", - "value": "miner", - "defaultValue": "miner", - "description": "单位类型", - "group": "基础属性", - "readOnly": false - }, - { - "name": "currentHealth", - "type": "number", - "value": 100, - "defaultValue": 100, - "description": "当前生命值", - "group": "基础属性", - "readOnly": false - }, - { - "name": "maxHealth", - "type": "number", - "value": 100, - "defaultValue": 100, - "description": "最大生命值", - "group": "基础属性", - "readOnly": false - }, - { - "name": "stamina", - "type": "number", - "value": 100, - "defaultValue": 100, - "description": "当前体力值 - 挖矿会消耗体力", - "group": "体力系统", - "readOnly": false - }, - { - "name": "maxStamina", - "type": "number", - "value": 100, - "defaultValue": 100, - "description": "最大体力值", - "group": "体力系统", - "readOnly": false - }, - { - "name": "staminaPercentage", - "type": "number", - "value": 1, - "defaultValue": 1, - "description": "体力百分比", - "group": "体力系统", - "readOnly": false - }, - { - "name": "isLowStamina", - "type": "boolean", - "value": false, - "defaultValue": false, - "description": "是否低体力 - 体力低于20%时为true", - "group": "体力系统", - "readOnly": false - }, - { - "name": "isResting", - "type": "boolean", - "value": false, - "defaultValue": false, - "description": "是否正在休息", - "group": "体力系统", - "readOnly": false - }, - { - "name": "homePosition", - "type": "vector3", - "value": { - "x": 0, - "y": 0, - "z": 0 - }, - "defaultValue": { - "x": 0, - "y": 0, - "z": 0 - }, - "description": "家的位置 - 矿工休息的地方", - "group": "体力系统", - "readOnly": false - }, - { - "name": "hasOre", - "type": "boolean", - "value": false, - "defaultValue": false, - "description": "是否携带矿石", - "group": "工作状态", - "readOnly": false - }, - { - "name": "currentCommand", - "type": "string", - "value": "mine", - "defaultValue": "mine", - "description": "当前命令", - "group": "工作状态", - "readOnly": false - }, - { - "name": "hasTarget", - "type": "boolean", - "value": false, - "defaultValue": false, - "description": "是否有目标", - "group": "工作状态", - "readOnly": false - }, - { - "name": "targetPosition", - "type": "vector3", - "value": { - "x": 0, - "y": 0, - "z": 0 - }, - "defaultValue": { - "x": 0, - "y": 0, - "z": 0 - }, - "description": "目标位置", - "group": "移动属性", - "readOnly": false - }, - { - "name": "isMoving", - "type": "boolean", - "value": false, - "defaultValue": false, - "description": "是否正在移动", - "group": "移动属性", - "readOnly": false - } - ], - "editorState": { - "canvasView": { - "panX": 0, - "panY": 0, - "zoomLevel": 1 - }, - "selectedNodeId": "node_o6tsnrxyg", - "uiSettings": { - "showDescriptions": true, - "showNodeProperties": true, - "layoutAlgorithm": "compact" - } - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree.meta b/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree.meta deleted file mode 100644 index 4a8ea423..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/miner-stamina-ai.btree.meta +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ver": "1.0.0", - "importer": "*", - "imported": true, - "uuid": "24c6e7e6-4ff0-4e7b-b470-9468bfa66b5d", - "files": [ - ".btree", - ".json" - ], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/prefabs.meta b/extensions/cocos/cocos-ecs/assets/resources/prefabs.meta deleted file mode 100644 index e61b9316..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/prefabs.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "2bf3ded8-4054-4d8f-a367-c76b21eaf538", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab b/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab deleted file mode 100644 index 0b8e1959..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab +++ /dev/null @@ -1,2034 +0,0 @@ -[ - { - "__type__": "cc.Prefab", - "_name": "Panel_Node", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "data": { - "__id__": 1 - }, - "optimizationPolicy": 0, - "persistent": false - }, - { - "__type__": "cc.Node", - "_name": "Panel_Node", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 8 - }, - { - "__id__": 14 - }, - { - "__id__": 28 - }, - { - "__id__": 34 - }, - { - "__id__": 40 - }, - { - "__id__": 54 - }, - { - "__id__": 60 - } - ], - "_active": true, - "_components": [ - { - "__id__": 74 - } - ], - "_prefab": { - "__id__": 76 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 3 - }, - { - "__id__": 5 - } - ], - "_prefab": { - "__id__": 7 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 4 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "1cy+cyXMxBYZjGq81vWhBp" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": { - "__id__": 6 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "94TNuEThhOOKDKcgjf4wOp" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "4eaXtUO/1Ab7dRSq26dA4i", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Label-001", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 9 - }, - { - "__id__": 11 - } - ], - "_prefab": { - "__id__": 13 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 109.29399999999998, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": { - "__id__": 10 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "d7g65c0StCpZZJIvGVZOZn" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": { - "__id__": 12 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "efv2gH7hBF0ZwtI/PCgV/K" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "adi41Imm5CgYWjmx93PMQU", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Button", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 15 - } - ], - "_active": true, - "_components": [ - { - "__id__": 21 - }, - { - "__id__": 23 - }, - { - "__id__": 25 - } - ], - "_prefab": { - "__id__": 27 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": -80.59100000000001, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 14 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 16 - }, - { - "__id__": 18 - } - ], - "_prefab": { - "__id__": 20 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 15 - }, - "_enabled": true, - "__prefab": { - "__id__": 17 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "35+Cc9ZVJNs4zSEkY1vlCF" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 15 - }, - "_enabled": true, - "__prefab": { - "__id__": 19 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f80m1/aZxGNo9xCrdUs5KM" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "64cUsUOe5Ag4CicSXQ9pB+", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 22 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "93+wBflIBOIafpfvuInc8l" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 24 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "6c2EzA5CBDDp3AxsKMDVUA" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": { - "__id__": 26 - }, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 14 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "a24guBQHhCAYdzINmvn8ad" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "97mosgR5BBja5QPWThErLb", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Label-002", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 29 - }, - { - "__id__": 31 - } - ], - "_prefab": { - "__id__": 33 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 203.13300000000004, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 28 - }, - "_enabled": true, - "__prefab": { - "__id__": 30 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f9cvqbHFFBzKYlmnNv39FA" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 28 - }, - "_enabled": true, - "__prefab": { - "__id__": 32 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "47UAZsKABG5ZkNE/Ug716w" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "c5qflEQndOhaEoLaiJNnP4", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Label-003", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 35 - }, - { - "__id__": 37 - } - ], - "_prefab": { - "__id__": 39 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 280.41200000000003, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 34 - }, - "_enabled": true, - "__prefab": { - "__id__": 36 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "4eoVw/ZABBNY4NqrjYyxF0" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 34 - }, - "_enabled": true, - "__prefab": { - "__id__": 38 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "d1M6w6Sv1Jqqh4mDMtIZuM" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "a0q4iMqHpMY5uN7PuosjNY", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Button-001", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 41 - } - ], - "_active": true, - "_components": [ - { - "__id__": 47 - }, - { - "__id__": 49 - }, - { - "__id__": 51 - } - ], - "_prefab": { - "__id__": 53 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 114.81399999999996, - "y": -80.59100000000001, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 40 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 42 - }, - { - "__id__": 44 - } - ], - "_prefab": { - "__id__": 46 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 41 - }, - "_enabled": true, - "__prefab": { - "__id__": 43 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "93oA/K6lFGSbYsArj7ULjl" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 41 - }, - "_enabled": true, - "__prefab": { - "__id__": 45 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "42DqLa20xJdbMUFCXYLkYb" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "84E2x/epJBSpRpVDjzMDrt", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 40 - }, - "_enabled": true, - "__prefab": { - "__id__": 48 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "4dn4kdXuFF067f+CPBdq0F" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 40 - }, - "_enabled": true, - "__prefab": { - "__id__": 50 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "32FpLpJJ1EoqaJzfUqR+pT" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 40 - }, - "_enabled": true, - "__prefab": { - "__id__": 52 - }, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 40 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "928zECCSVIBpMWurfOeT6J" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "5csmoyQq5KBJaEBEjUVcSx", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Label-004", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 55 - }, - { - "__id__": 57 - } - ], - "_prefab": { - "__id__": 59 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 280.41200000000003, - "y": -239.565, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 54 - }, - "_enabled": true, - "__prefab": { - "__id__": 56 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f3vPduJ9BG/bU5ch4x6Eol" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 54 - }, - "_enabled": true, - "__prefab": { - "__id__": 58 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "8b6PxpfIxJKYRRxJSo1iHl" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "d2BBwGYL1Gba9HBCiXh/hZ", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.Node", - "_name": "Button-002", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 61 - } - ], - "_active": true, - "_components": [ - { - "__id__": 67 - }, - { - "__id__": 69 - }, - { - "__id__": 71 - } - ], - "_prefab": { - "__id__": 73 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 385.28999999999996, - "y": -237.357, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 60 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 62 - }, - { - "__id__": 64 - } - ], - "_prefab": { - "__id__": 66 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 61 - }, - "_enabled": true, - "__prefab": { - "__id__": 63 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f4FPGb/jVFQJZmFa3Ru/eJ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 61 - }, - "_enabled": true, - "__prefab": { - "__id__": 65 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "5cx5pi8UpN57OErgtGAayD" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "b5DN0TMvlBKrFs81/YJAK4", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 60 - }, - "_enabled": true, - "__prefab": { - "__id__": 68 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "5eiIhlcDNC7In/R6fDs7Fb" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 60 - }, - "_enabled": true, - "__prefab": { - "__id__": 70 - }, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "f5IQ1E8W1FQJnlwYBngRnr" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 60 - }, - "_enabled": true, - "__prefab": { - "__id__": 72 - }, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 60 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "a2oYzQcf9PD62fEx/MEHaH" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "54PSby+eBGc7zHijG/Qvwq", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 1 - }, - "_enabled": true, - "__prefab": { - "__id__": 75 - }, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 100 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "" - }, - { - "__type__": "cc.CompPrefabInfo", - "fileId": "041T1AYllNxpkew6Gymsx/" - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 1 - }, - "asset": { - "__id__": 0 - }, - "fileId": "fervqG79FCE6HgTYcdqMgS", - "targetOverrides": null - } -] \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab.meta b/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab.meta deleted file mode 100644 index f670ef40..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/prefabs/Panel_Node.prefab.meta +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ver": "1.1.50", - "importer": "prefab", - "imported": true, - "uuid": "51a6e245-2983-4258-be9f-9e21378f7f9f", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": { - "syncNodeName": "Panel_Node" - } -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/terrains.meta b/extensions/cocos/cocos-ecs/assets/resources/terrains.meta deleted file mode 100644 index d46cdbb5..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/terrains.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "4fd895f7-6b0f-4357-aa3a-7c2e88ffac9a", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/resources/textures.meta b/extensions/cocos/cocos-ecs/assets/resources/textures.meta deleted file mode 100644 index 704e7de5..00000000 --- a/extensions/cocos/cocos-ecs/assets/resources/textures.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "829183be-61a1-4494-bf64-3df359c0e8e7", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scenes.meta b/extensions/cocos/cocos-ecs/assets/scenes.meta deleted file mode 100644 index 38e9792b..00000000 --- a/extensions/cocos/cocos-ecs/assets/scenes.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "240e4a78-e55f-47a8-84de-39220bba1321", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene b/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene deleted file mode 100644 index 513315cf..00000000 --- a/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene +++ /dev/null @@ -1,757 +0,0 @@ -[ - { - "__type__": "cc.SceneAsset", - "_name": "behaviour-example-scene", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "scene": { - "__id__": 1 - } - }, - { - "__type__": "cc.Scene", - "_name": "behaviour-example-scene", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 5 - }, - { - "__id__": 7 - }, - { - "__id__": 8 - }, - { - "__id__": 14 - } - ], - "_active": true, - "_components": [], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "autoReleaseAssets": false, - "_globals": { - "__id__": 16 - }, - "_id": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c" - }, - { - "__type__": "cc.Node", - "_name": "Main Light", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 3 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": -0.06397656665577071, - "y": -0.44608233363525845, - "z": -0.8239028751062036, - "w": -0.3436591377065261 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": -117.894, - "y": -194.909, - "z": 38.562 - }, - "_id": "c0y6F5f+pAvI805TdmxIjx" - }, - { - "__type__": "cc.DirectionalLight", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": null, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 250, - "b": 240, - "a": 255 - }, - "_useColorTemperature": false, - "_colorTemperature": 6550, - "_staticSettings": { - "__id__": 4 - }, - "_visibility": -325058561, - "_illuminanceHDR": 65000, - "_illuminance": 65000, - "_illuminanceLDR": 1.6927083333333335, - "_shadowEnabled": false, - "_shadowPcf": 0, - "_shadowBias": 0.00001, - "_shadowNormalBias": 0, - "_shadowSaturation": 1, - "_shadowDistance": 50, - "_shadowInvisibleOcclusionRange": 200, - "_csmLevel": 4, - "_csmLayerLambda": 0.75, - "_csmOptimizationMode": 2, - "_csmAdvancedOptions": false, - "_csmLayersTransition": false, - "_csmTransitionRange": 0.05, - "_shadowFixedArea": false, - "_shadowNear": 0.1, - "_shadowFar": 10, - "_shadowOrthoSize": 5, - "_id": "597uMYCbhEtJQc0ffJlcgA" - }, - { - "__type__": "cc.StaticLightSettings", - "_baked": false, - "_editorOnly": false, - "_castShadow": false - }, - { - "__type__": "cc.Node", - "_name": "Main Camera", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 6 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": -10, - "y": 10, - "z": 10 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": -0.27781593346944056, - "y": -0.36497167621709875, - "z": -0.11507512748638377, - "w": 0.8811195706053617 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": -35, - "y": -45, - "z": 0 - }, - "_id": "c9DMICJLFO5IeO07EPon7U" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 5 - }, - "_enabled": true, - "__prefab": null, - "_projection": 1, - "_priority": 0, - "_fov": 45, - "_fovAxis": 0, - "_orthoHeight": 10, - "_near": 1, - "_far": 1000, - "_color": { - "__type__": "cc.Color", - "r": 51, - "g": 51, - "b": 51, - "a": 255 - }, - "_depth": 1, - "_stencil": 0, - "_clearFlags": 14, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_aperture": 19, - "_shutter": 7, - "_iso": 0, - "_screenScale": 1, - "_visibility": 1822425087, - "_targetTexture": null, - "_postProcess": null, - "_usePostProcess": false, - "_cameraType": -1, - "_trackingType": 0, - "_id": "7dWQTpwS5LrIHnc1zAPUtf" - }, - { - "__type__": "cc.Node", - "_name": "GameWorld", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "8b9QorrGZIl64tVv0Z0vRQ" - }, - { - "__type__": "cc.Node", - "_name": "Canvas", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 9 - } - ], - "_active": true, - "_components": [ - { - "__id__": 11 - }, - { - "__id__": 12 - }, - { - "__id__": 13 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 640, - "y": 360, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "4edRVPFLtIz5pR5edsryvx" - }, - { - "__type__": "cc.Node", - "_name": "Camera", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 8 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 10 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 1000 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "dfyZdh0bxJop4PyQrmHEP6" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 9 - }, - "_enabled": true, - "__prefab": null, - "_projection": 0, - "_priority": 1073741824, - "_fov": 45, - "_fovAxis": 0, - "_orthoHeight": 360, - "_near": 1, - "_far": 2000, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_depth": 1, - "_stencil": 0, - "_clearFlags": 6, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_aperture": 19, - "_shutter": 7, - "_iso": 0, - "_screenScale": 1, - "_visibility": 41943040, - "_targetTexture": null, - "_postProcess": null, - "_usePostProcess": false, - "_cameraType": -1, - "_trackingType": 0, - "_id": "48lLOhLY5Onqokj70aNP+E" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1280, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "c3qBrLTLNImoltQDlZ6coz" - }, - { - "__type__": "cc.Canvas", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": null, - "_cameraComponent": { - "__id__": 10 - }, - "_alignCanvasWithScreen": true, - "_id": "9d3SdE3ORAOZ6AG/imW6NO" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 8 - }, - "_enabled": true, - "__prefab": null, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "4a8iJypC1J8pMml467hQ6c" - }, - { - "__type__": "cc.Node", - "_name": "RTSDemo", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 15 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "89cmsd2gNNsq155xC7mob8" - }, - { - "__type__": "c33869Km+9Bb7dw/OyRztvE", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 14 - }, - "_enabled": true, - "__prefab": null, - "minerCount": 1, - "goldMineCount": 3, - "_id": "86AIY7iYlMNqJsDC/+LIMU" - }, - { - "__type__": "cc.SceneGlobals", - "ambient": { - "__id__": 17 - }, - "shadows": { - "__id__": 18 - }, - "_skybox": { - "__id__": 19 - }, - "fog": { - "__id__": 20 - }, - "octree": { - "__id__": 21 - }, - "skin": { - "__id__": 22 - }, - "lightProbeInfo": { - "__id__": 23 - }, - "postSettings": { - "__id__": 24 - }, - "bakedWithStationaryMainLight": false, - "bakedWithHighpLightmap": false - }, - { - "__type__": "cc.AmbientInfo", - "_skyColorHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyColor": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyIllumHDR": 20000, - "_skyIllum": 20000, - "_groundAlbedoHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_groundAlbedo": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_skyColorLDR": { - "__type__": "cc.Vec4", - "x": 0.452588, - "y": 0.607642, - "z": 0.755699, - "w": 0 - }, - "_skyIllumLDR": 0.8, - "_groundAlbedoLDR": { - "__type__": "cc.Vec4", - "x": 0.618555, - "y": 0.577848, - "z": 0.544564, - "w": 0 - } - }, - { - "__type__": "cc.ShadowsInfo", - "_enabled": false, - "_type": 0, - "_normal": { - "__type__": "cc.Vec3", - "x": 0, - "y": 1, - "z": 0 - }, - "_distance": 0, - "_planeBias": 1, - "_shadowColor": { - "__type__": "cc.Color", - "r": 76, - "g": 76, - "b": 76, - "a": 255 - }, - "_maxReceived": 4, - "_size": { - "__type__": "cc.Vec2", - "x": 1024, - "y": 1024 - } - }, - { - "__type__": "cc.SkyboxInfo", - "_envLightingType": 0, - "_envmapHDR": { - "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_envmap": { - "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_envmapLDR": { - "__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_diffuseMapHDR": null, - "_diffuseMapLDR": null, - "_enabled": true, - "_useHDR": true, - "_editableMaterial": null, - "_reflectionHDR": null, - "_reflectionLDR": null, - "_rotationAngle": 0 - }, - { - "__type__": "cc.FogInfo", - "_type": 0, - "_fogColor": { - "__type__": "cc.Color", - "r": 200, - "g": 200, - "b": 200, - "a": 255 - }, - "_enabled": false, - "_fogDensity": 0.3, - "_fogStart": 0.5, - "_fogEnd": 300, - "_fogAtten": 5, - "_fogTop": 1.5, - "_fogRange": 1.2, - "_accurate": false - }, - { - "__type__": "cc.OctreeInfo", - "_enabled": false, - "_minPos": { - "__type__": "cc.Vec3", - "x": -1024, - "y": -1024, - "z": -1024 - }, - "_maxPos": { - "__type__": "cc.Vec3", - "x": 1024, - "y": 1024, - "z": 1024 - }, - "_depth": 8 - }, - { - "__type__": "cc.SkinInfo", - "_enabled": true, - "_blurRadius": 0.01, - "_sssIntensity": 3 - }, - { - "__type__": "cc.LightProbeInfo", - "_giScale": 1, - "_giSamples": 1024, - "_bounces": 2, - "_reduceRinging": 0, - "_showProbe": true, - "_showWireframe": true, - "_showConvex": false, - "_data": null, - "_lightProbeSphereVolume": 1 - }, - { - "__type__": "cc.PostSettingsInfo", - "_toneMappingType": 0 - } -] \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene.meta b/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene.meta deleted file mode 100644 index b2556a95..00000000 --- a/extensions/cocos/cocos-ecs/assets/scenes/behaviour-example-scene.scene.meta +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ver": "1.1.50", - "importer": "scene", - "imported": true, - "uuid": "ff354f0b-c2f5-4dea-8ffb-0152d175d11c", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scenes/scene.scene b/extensions/cocos/cocos-ecs/assets/scenes/scene.scene deleted file mode 100644 index 26fe1e2d..00000000 --- a/extensions/cocos/cocos-ecs/assets/scenes/scene.scene +++ /dev/null @@ -1,2676 +0,0 @@ -[ - { - "__type__": "cc.SceneAsset", - "_name": "scene", - "_objFlags": 0, - "__editorExtras__": {}, - "_native": "", - "scene": { - "__id__": 1 - } - }, - { - "__type__": "cc.Scene", - "_name": "scene", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 2 - }, - { - "__id__": 5 - }, - { - "__id__": 7 - }, - { - "__id__": 9 - }, - { - "__id__": 23 - }, - { - "__id__": 63 - } - ], - "_active": true, - "_components": [], - "_prefab": { - "__id__": 65 - }, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "autoReleaseAssets": false, - "_globals": { - "__id__": 66 - }, - "_id": "fcbf2917-6d43-4528-8829-7ee089594879" - }, - { - "__type__": "cc.Node", - "_name": "Main Light", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 3 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": -0.06397656665577071, - "y": -0.44608233363525845, - "z": -0.8239028751062036, - "w": -0.3436591377065261 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": -117.894, - "y": -194.909, - "z": 38.562 - }, - "_id": "c0y6F5f+pAvI805TdmxIjx" - }, - { - "__type__": "cc.DirectionalLight", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 2 - }, - "_enabled": true, - "__prefab": null, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 250, - "b": 240, - "a": 255 - }, - "_useColorTemperature": false, - "_colorTemperature": 6550, - "_staticSettings": { - "__id__": 4 - }, - "_visibility": -325058561, - "_illuminanceHDR": 65000, - "_illuminance": 65000, - "_illuminanceLDR": 1.6927083333333335, - "_shadowEnabled": false, - "_shadowPcf": 0, - "_shadowBias": 0.00001, - "_shadowNormalBias": 0, - "_shadowSaturation": 1, - "_shadowDistance": 50, - "_shadowInvisibleOcclusionRange": 200, - "_csmLevel": 4, - "_csmLayerLambda": 0.75, - "_csmOptimizationMode": 2, - "_csmAdvancedOptions": false, - "_csmLayersTransition": false, - "_csmTransitionRange": 0.05, - "_shadowFixedArea": false, - "_shadowNear": 0.1, - "_shadowFar": 10, - "_shadowOrthoSize": 5, - "_id": "597uMYCbhEtJQc0ffJlcgA" - }, - { - "__type__": "cc.StaticLightSettings", - "_baked": false, - "_editorOnly": false, - "_castShadow": false - }, - { - "__type__": "cc.Node", - "_name": "Main Camera", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 6 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": -10, - "y": 10, - "z": 10 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": -0.27781593346944056, - "y": -0.36497167621709875, - "z": -0.11507512748638377, - "w": 0.8811195706053617 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": -35, - "y": -45, - "z": 0 - }, - "_id": "c9DMICJLFO5IeO07EPon7U" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 5 - }, - "_enabled": true, - "__prefab": null, - "_projection": 1, - "_priority": 0, - "_fov": 45, - "_fovAxis": 0, - "_orthoHeight": 10, - "_near": 1, - "_far": 1000, - "_color": { - "__type__": "cc.Color", - "r": 51, - "g": 51, - "b": 51, - "a": 255 - }, - "_depth": 1, - "_stencil": 0, - "_clearFlags": 14, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_aperture": 19, - "_shutter": 7, - "_iso": 0, - "_screenScale": 1, - "_visibility": 1822425087, - "_targetTexture": null, - "_postProcess": null, - "_usePostProcess": false, - "_cameraType": -1, - "_trackingType": 0, - "_id": "7dWQTpwS5LrIHnc1zAPUtf" - }, - { - "__type__": "cc.Node", - "_name": "ECSManager", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 8 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "3e7rcS/tZHIY0l6bef0fag" - }, - { - "__type__": "b8965bwYyBLbYHNRHv4ESMM", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 7 - }, - "_enabled": true, - "__prefab": null, - "debugMode": true, - "_id": "a5SvVlMsZCrIJB1jr083gX" - }, - { - "__type__": "cc.Node", - "_name": "Canvas", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [ - { - "__id__": 10 - }, - { - "__id__": 12 - } - ], - "_active": true, - "_components": [ - { - "__id__": 20 - }, - { - "__id__": 21 - }, - { - "__id__": 22 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 640, - "y": 360, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "1cmYwsQnBPl7bierzKaJim" - }, - { - "__type__": "cc.Node", - "_name": "Camera", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 9 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 11 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 1000 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "7envQZVKNKVKTLpiFpigaB" - }, - { - "__type__": "cc.Camera", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 10 - }, - "_enabled": true, - "__prefab": null, - "_projection": 0, - "_priority": 1073741824, - "_fov": 45, - "_fovAxis": 0, - "_orthoHeight": 375.06234413965086, - "_near": 1, - "_far": 2000, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_depth": 1, - "_stencil": 0, - "_clearFlags": 6, - "_rect": { - "__type__": "cc.Rect", - "x": 0, - "y": 0, - "width": 1, - "height": 1 - }, - "_aperture": 19, - "_shutter": 7, - "_iso": 0, - "_screenScale": 1, - "_visibility": 41943040, - "_targetTexture": null, - "_postProcess": null, - "_usePostProcess": false, - "_cameraType": -1, - "_trackingType": 0, - "_id": "c7ku0LPhFMxb0OGWOiND/d" - }, - { - "__type__": "cc.Node", - "_objFlags": 0, - "_parent": { - "__id__": 9 - }, - "_prefab": { - "__id__": 13 - }, - "__editorExtras__": {} - }, - { - "__type__": "cc.PrefabInfo", - "root": { - "__id__": 12 - }, - "asset": { - "__uuid__": "51a6e245-2983-4258-be9f-9e21378f7f9f", - "__expectedType__": "cc.Prefab" - }, - "fileId": "fervqG79FCE6HgTYcdqMgS", - "instance": { - "__id__": 14 - }, - "targetOverrides": null, - "nestedPrefabInstanceRoots": null - }, - { - "__type__": "cc.PrefabInstance", - "fileId": "2f+ZS6dslHvZ1gjyBToNys", - "prefabRootNode": null, - "mountedChildren": [], - "mountedComponents": [], - "propertyOverrides": [ - { - "__id__": 15 - }, - { - "__id__": 17 - }, - { - "__id__": 18 - }, - { - "__id__": 19 - } - ], - "removedComponents": [] - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 16 - }, - "propertyPath": [ - "_name" - ], - "value": "Panel_Node" - }, - { - "__type__": "cc.TargetInfo", - "localID": [ - "fervqG79FCE6HgTYcdqMgS" - ] - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 16 - }, - "propertyPath": [ - "_lpos" - ], - "value": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - } - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 16 - }, - "propertyPath": [ - "_lrot" - ], - "value": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - } - }, - { - "__type__": "CCPropertyOverrideInfo", - "targetInfo": { - "__id__": 16 - }, - "propertyPath": [ - "_euler" - ], - "value": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - } - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 9 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 1280, - "height": 720 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "9eENxfIbtHJbmdU+3yyzcW" - }, - { - "__type__": "cc.Canvas", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 9 - }, - "_enabled": true, - "__prefab": null, - "_cameraComponent": { - "__id__": 11 - }, - "_alignCanvasWithScreen": true, - "_id": "d0DTDMMPNAELzW/5Zb52+u" - }, - { - "__type__": "cc.Widget", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 9 - }, - "_enabled": true, - "__prefab": null, - "_alignFlags": 45, - "_target": null, - "_left": 0, - "_right": 0, - "_top": 0, - "_bottom": 0, - "_horizontalCenter": 0, - "_verticalCenter": 0, - "_isAbsLeft": true, - "_isAbsRight": true, - "_isAbsTop": true, - "_isAbsBottom": true, - "_isAbsHorizontalCenter": true, - "_isAbsVerticalCenter": true, - "_originalWidth": 0, - "_originalHeight": 0, - "_alignMode": 2, - "_lockFlags": 0, - "_id": "c3rKvYnj1PPLdmYbEKSLTr" - }, - { - "__type__": "cc.Node", - "_name": "UserInfoView", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 24 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "c0j9jHfY1B675Mz9CsYtU0" - }, - { - "__type__": "0a147ZbIWVPurKGALpIXKoR", - "_name": "", - "_objFlags": 0, - "node": { - "__id__": 23 - }, - "_enabled": true, - "__prefab": null, - "userNameLabel": { - "__id__": 25 - }, - "levelLabel": { - "__id__": 30 - }, - "scoreLabel": { - "__id__": 40 - }, - "coinsLabel": { - "__id__": 43 - }, - "onlineStatusLabel": null, - "displayNameLabel": null, - "totalAssetsLabel": null, - "addScoreButton": { - "__id__": 50 - }, - "addCoinsButton": null, - "levelUpButton": { - "__id__": 37 - }, - "toggleOnlineButton": null, - "resetButton": null, - "_id": "c2pehTFT9BQJnAwfi+n6tP" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 26 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "02ZI1mD4lNe6sGyPLVQm3P" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 62 - }, - { - "__id__": 25 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "e8i8akpF9L15AGufnpTILW" - }, - { - "__type__": "cc.Node", - "_name": "Panel_Node", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": null, - "_children": [ - { - "__id__": 26 - }, - { - "__id__": 28 - }, - { - "__id__": 31 - }, - { - "__id__": 38 - }, - { - "__id__": 41 - }, - { - "__id__": 44 - }, - { - "__id__": 51 - }, - { - "__id__": 54 - } - ], - "_active": true, - "_components": [ - { - "__id__": 61 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "fervqG79FCE6HgTYcdqMgS" - }, - { - "__type__": "cc.Node", - "_name": "Label-001", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 29 - }, - { - "__id__": 30 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 109.29399999999998, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "56sORerhxEvZgvw9Q+60TF" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 28 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "6fb5nAWn9Azalsn2nLEcvL" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 28 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "c1xH6PVUdLDYe3yLOezquI" - }, - { - "__type__": "cc.Node", - "_name": "Button", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [ - { - "__id__": 32 - } - ], - "_active": true, - "_components": [ - { - "__id__": 35 - }, - { - "__id__": 36 - }, - { - "__id__": 37 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": -80.59100000000001, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "d47CctbAVCZJ4WqsaTwpGd" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 31 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 33 - }, - { - "__id__": 34 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "58PpI3+3NFpbdRd5GdjQQF" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 32 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "5d+JlXtnBGdYGFL6GbIimZ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 32 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "2cO7CckqtCHaDo3D/jDAL2" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 31 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "c2jgfqnbNBzrqK0hbbASR/" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 31 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "68YHIuaxFAO5mhYLZKwG0x" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 31 - }, - "_enabled": true, - "__prefab": null, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 31 - }, - "_id": "d9VTa5QMxDq5VHnbaB7Dni" - }, - { - "__type__": "cc.Node", - "_name": "Label-002", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 39 - }, - { - "__id__": 40 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 203.13300000000004, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "b6mg+fIttLMpJp3xNrNTZf" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 38 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "16qJTqnzhDNooZaYJE5m/h" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 38 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "86W9LOLGxGBZUtSXpeQd9M" - }, - { - "__type__": "cc.Node", - "_name": "Label-003", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 42 - }, - { - "__id__": 43 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 280.41200000000003, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "eafDMXmLNFf6Vzl1fR0xWy" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 41 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "26juNCywRKQZvJp28g6+Xv" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 41 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "35X+mpxitNFq/igqZGEjfw" - }, - { - "__type__": "cc.Node", - "_name": "Button-001", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [ - { - "__id__": 45 - } - ], - "_active": true, - "_components": [ - { - "__id__": 48 - }, - { - "__id__": 49 - }, - { - "__id__": 50 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 114.81399999999996, - "y": -80.59100000000001, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "21cfQqfdNJc4AXsKC4RF23" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 44 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 46 - }, - { - "__id__": 47 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "ae7apqU89KPZ1Enel0WTUp" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 45 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "fbNhi55IRI2J7tQvaxJHgJ" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 45 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "14ZCtVbGZBHal1QK3EAe0s" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 44 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d7HQUgeApGCpL/NZglnJzO" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 44 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "e9KkhF8TVBwbhEjVhNHJuM" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 44 - }, - "_enabled": true, - "__prefab": null, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 44 - }, - "_id": "09rcw9tSNKVLT5G6GQX379" - }, - { - "__type__": "cc.Node", - "_name": "Label-004", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 52 - }, - { - "__id__": 53 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 280.41200000000003, - "y": -239.565, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "5doj7uQHlNabsuccWxoDqP" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "91tAaDhvlGWKKHT2V9qmwV" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 51 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_string": "label", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 0, - "_enableWrapText": true, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "bdmZE7nohKIb+n0QmKa1dG" - }, - { - "__type__": "cc.Node", - "_name": "Button-002", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 27 - }, - "_children": [ - { - "__id__": 55 - } - ], - "_active": true, - "_components": [ - { - "__id__": 58 - }, - { - "__id__": 59 - }, - { - "__id__": 60 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 385.28999999999996, - "y": -237.357, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "80nP8NDrxLBKZT0pkkCtl5" - }, - { - "__type__": "cc.Node", - "_name": "Label", - "_objFlags": 512, - "__editorExtras__": {}, - "_parent": { - "__id__": 54 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 56 - }, - { - "__id__": 57 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 33554432, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "13982sanxLvIgB/RKDodfG" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 55 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "499H6swhZNqaP3lJNZfKZo" - }, - { - "__type__": "cc.Label", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 55 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_string": "button", - "_horizontalAlign": 1, - "_verticalAlign": 1, - "_actualFontSize": 20, - "_fontSize": 20, - "_fontFamily": "Arial", - "_lineHeight": 40, - "_overflow": 1, - "_enableWrapText": false, - "_font": null, - "_isSystemFontUsed": true, - "_spacingX": 0, - "_isItalic": false, - "_isBold": false, - "_isUnderline": false, - "_underlineHeight": 2, - "_cacheMode": 0, - "_enableOutline": false, - "_outlineColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_outlineWidth": 2, - "_enableShadow": false, - "_shadowColor": { - "__type__": "cc.Color", - "r": 0, - "g": 0, - "b": 0, - "a": 255 - }, - "_shadowOffset": { - "__type__": "cc.Vec2", - "x": 2, - "y": 2 - }, - "_shadowBlur": 2, - "_id": "c9NR60s8NPFrXF46mOi5qa" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 54 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 40 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "291Y66nspC86lPDwj4w9VD" - }, - { - "__type__": "cc.Sprite", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 54 - }, - "_enabled": true, - "__prefab": null, - "_customMaterial": null, - "_srcBlendFactor": 2, - "_dstBlendFactor": 4, - "_color": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_spriteFrame": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_type": 1, - "_fillType": 0, - "_sizeMode": 0, - "_fillCenter": { - "__type__": "cc.Vec2", - "x": 0, - "y": 0 - }, - "_fillStart": 0, - "_fillRange": 0, - "_isTrimmedMode": true, - "_useGrayscale": false, - "_atlas": null, - "_id": "1f/zgmhD9EUoGX8nmxpZca" - }, - { - "__type__": "cc.Button", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 54 - }, - "_enabled": true, - "__prefab": null, - "clickEvents": [], - "_interactable": true, - "_transition": 2, - "_normalColor": { - "__type__": "cc.Color", - "r": 214, - "g": 214, - "b": 214, - "a": 255 - }, - "_hoverColor": { - "__type__": "cc.Color", - "r": 211, - "g": 211, - "b": 211, - "a": 255 - }, - "_pressedColor": { - "__type__": "cc.Color", - "r": 255, - "g": 255, - "b": 255, - "a": 255 - }, - "_disabledColor": { - "__type__": "cc.Color", - "r": 124, - "g": 124, - "b": 124, - "a": 255 - }, - "_normalSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_hoverSprite": { - "__uuid__": "20835ba4-6145-4fbc-a58a-051ce700aa3e@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_pressedSprite": { - "__uuid__": "544e49d6-3f05-4fa8-9a9e-091f98fc2ce8@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_disabledSprite": { - "__uuid__": "951249e0-9f16-456d-8b85-a6ca954da16b@f9941", - "__expectedType__": "cc.SpriteFrame" - }, - "_duration": 0.1, - "_zoomScale": 1.2, - "_target": { - "__id__": 54 - }, - "_id": "d8xw994+tKfp7rT5BevlNJ" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 27 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 100, - "height": 100 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "92bj1C749JdpTCOuNnleHn" - }, - { - "__type__": "cc.UITransform", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 26 - }, - "_enabled": true, - "__prefab": null, - "_contentSize": { - "__type__": "cc.Size", - "width": 42.255859375, - "height": 50.4 - }, - "_anchorPoint": { - "__type__": "cc.Vec2", - "x": 0.5, - "y": 0.5 - }, - "_id": "d3Wm3kTdVPhqGb4MSMVz70" - }, - { - "__type__": "cc.Node", - "_name": "TestView", - "_objFlags": 0, - "__editorExtras__": {}, - "_parent": { - "__id__": 1 - }, - "_children": [], - "_active": true, - "_components": [ - { - "__id__": 64 - } - ], - "_prefab": null, - "_lpos": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_lrot": { - "__type__": "cc.Quat", - "x": 0, - "y": 0, - "z": 0, - "w": 1 - }, - "_lscale": { - "__type__": "cc.Vec3", - "x": 1, - "y": 1, - "z": 1 - }, - "_mobility": 0, - "_layer": 1073741824, - "_euler": { - "__type__": "cc.Vec3", - "x": 0, - "y": 0, - "z": 0 - }, - "_id": "ffrenA2klFpZuGccvjc2Rt" - }, - { - "__type__": "667e0XE/fdLCIxlV+CFRlIQ", - "_name": "", - "_objFlags": 0, - "__editorExtras__": {}, - "node": { - "__id__": 63 - }, - "_enabled": true, - "__prefab": null, - "testLabel": { - "__id__": 53 - }, - "testButton": { - "__id__": 60 - }, - "_id": "03MT/vNyVE0YyCqO+EUB/o" - }, - { - "__type__": "cc.PrefabInfo", - "root": null, - "asset": null, - "fileId": "fcbf2917-6d43-4528-8829-7ee089594879", - "instance": null, - "targetOverrides": null, - "nestedPrefabInstanceRoots": [ - { - "__id__": 12 - } - ] - }, - { - "__type__": "cc.SceneGlobals", - "ambient": { - "__id__": 67 - }, - "shadows": { - "__id__": 68 - }, - "_skybox": { - "__id__": 69 - }, - "fog": { - "__id__": 70 - }, - "octree": { - "__id__": 71 - }, - "skin": { - "__id__": 72 - }, - "lightProbeInfo": { - "__id__": 73 - }, - "postSettings": { - "__id__": 74 - }, - "bakedWithStationaryMainLight": false, - "bakedWithHighpLightmap": false - }, - { - "__type__": "cc.AmbientInfo", - "_skyColorHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyColor": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.5, - "z": 0.8, - "w": 0.520833125 - }, - "_skyIllumHDR": 20000, - "_skyIllum": 20000, - "_groundAlbedoHDR": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_groundAlbedo": { - "__type__": "cc.Vec4", - "x": 0.2, - "y": 0.2, - "z": 0.2, - "w": 1 - }, - "_skyColorLDR": { - "__type__": "cc.Vec4", - "x": 0.452588, - "y": 0.607642, - "z": 0.755699, - "w": 0 - }, - "_skyIllumLDR": 0.8, - "_groundAlbedoLDR": { - "__type__": "cc.Vec4", - "x": 0.618555, - "y": 0.577848, - "z": 0.544564, - "w": 0 - } - }, - { - "__type__": "cc.ShadowsInfo", - "_enabled": false, - "_type": 0, - "_normal": { - "__type__": "cc.Vec3", - "x": 0, - "y": 1, - "z": 0 - }, - "_distance": 0, - "_planeBias": 1, - "_shadowColor": { - "__type__": "cc.Color", - "r": 76, - "g": 76, - "b": 76, - "a": 255 - }, - "_maxReceived": 4, - "_size": { - "__type__": "cc.Vec2", - "x": 1024, - "y": 1024 - } - }, - { - "__type__": "cc.SkyboxInfo", - "_envLightingType": 0, - "_envmapHDR": { - "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_envmap": { - "__uuid__": "d032ac98-05e1-4090-88bb-eb640dcb5fc1@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_envmapLDR": { - "__uuid__": "6f01cf7f-81bf-4a7e-bd5d-0afc19696480@b47c0", - "__expectedType__": "cc.TextureCube" - }, - "_diffuseMapHDR": null, - "_diffuseMapLDR": null, - "_enabled": true, - "_useHDR": true, - "_editableMaterial": null, - "_reflectionHDR": null, - "_reflectionLDR": null, - "_rotationAngle": 0 - }, - { - "__type__": "cc.FogInfo", - "_type": 0, - "_fogColor": { - "__type__": "cc.Color", - "r": 200, - "g": 200, - "b": 200, - "a": 255 - }, - "_enabled": false, - "_fogDensity": 0.3, - "_fogStart": 0.5, - "_fogEnd": 300, - "_fogAtten": 5, - "_fogTop": 1.5, - "_fogRange": 1.2, - "_accurate": false - }, - { - "__type__": "cc.OctreeInfo", - "_enabled": false, - "_minPos": { - "__type__": "cc.Vec3", - "x": -1024, - "y": -1024, - "z": -1024 - }, - "_maxPos": { - "__type__": "cc.Vec3", - "x": 1024, - "y": 1024, - "z": 1024 - }, - "_depth": 8 - }, - { - "__type__": "cc.SkinInfo", - "_enabled": true, - "_blurRadius": 0.01, - "_sssIntensity": 3 - }, - { - "__type__": "cc.LightProbeInfo", - "_giScale": 1, - "_giSamples": 1024, - "_bounces": 2, - "_reduceRinging": 0, - "_showProbe": true, - "_showWireframe": true, - "_showConvex": false, - "_data": null, - "_lightProbeSphereVolume": 1 - }, - { - "__type__": "cc.PostSettingsInfo", - "_toneMappingType": 0 - } -] \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta b/extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta deleted file mode 100644 index 64969ba5..00000000 --- a/extensions/cocos/cocos-ecs/assets/scenes/scene.scene.meta +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ver": "1.1.50", - "importer": "scene", - "imported": true, - "uuid": "fcbf2917-6d43-4528-8829-7ee089594879", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts.meta b/extensions/cocos/cocos-ecs/assets/scripts.meta deleted file mode 100644 index 3c3b51ab..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "1556cd72-9618-4f9f-b9e7-28152a33bde9", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts b/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts deleted file mode 100644 index 832f77a2..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { _decorator, Component, Node, Vec3, Color } from 'cc'; -import { SimplePrefabFactory } from './components/SimplePrefabFactory'; -import { BehaviorTreeComponent } from './components/BehaviorTreeComponent'; -import { StatusUIManager } from './components/StatusUIManager'; - -const { ccclass, property } = _decorator; - -/** - * 矿工AI演示场景 - */ -@ccclass('SimpleMinerDemo') -export class SimpleMinerDemo extends Component { - - @property - minerCount: number = 1; - - @property - goldMineCount: number = 3; - - private miners: Node[] = []; - private goldMines: Node[] = []; - private warehouse: Node | null = null; - private ground: Node | null = null; - private totalOresCollected: number = 0; - private warehouseUI: any = null; - - start() { - this.createWorld(); - this.createWarehouse(); - this.createGoldMines(); - this.createMiners(); - } - - private createWorld() { - this.ground = SimplePrefabFactory.createGround(new Vec3(20, 0.2, 20)); - this.node.addChild(this.ground); - this.ground.setWorldPosition(new Vec3(0, 0, 0)); - } - - private createWarehouse() { - this.warehouse = SimplePrefabFactory.createBuilding('Warehouse', new Vec3(2, 2, 2), Color.GRAY); - this.node.addChild(this.warehouse); - this.warehouse.setWorldPosition(new Vec3(0, 1, 0)); - this.createWarehouseUI(); - } - - private createGoldMines() { - for (let i = 0; i < this.goldMineCount; i++) { - const angle = (i / this.goldMineCount) * Math.PI * 2; - const radius = 6 + Math.random() * 2; - const position = new Vec3( - Math.cos(angle) * radius, - 0.8, - Math.sin(angle) * radius - ); - - const goldMine = SimplePrefabFactory.createResource(`GoldMine_${i + 1}`, Color.YELLOW); - this.node.addChild(goldMine); - goldMine.setWorldPosition(position); - goldMine.setScale(new Vec3(1.2, 1.2, 1.2)); - this.goldMines.push(goldMine); - } - } - - private createMiners() { - for (let i = 0; i < this.minerCount; i++) { - const angle = (i / this.minerCount) * Math.PI * 2; - const radius = 3; - const position = new Vec3( - Math.cos(angle) * radius, - 1, - Math.sin(angle) * radius - ); - - const miner = SimplePrefabFactory.createUnit(`Miner_${i + 1}`, Color.BLUE); - this.node.addChild(miner); - miner.setWorldPosition(position); - - const behaviorTree = miner.addComponent(BehaviorTreeComponent); - behaviorTree.behaviorTreeFile = 'miner-stamina-ai.bt'; - behaviorTree.debugMode = true; - - this.scheduleOnce(() => { - const blackboard = behaviorTree.getBlackboard(); - if (blackboard) { - blackboard.setValue('homePosition', position.clone()); - } - }, 0.5); - - this.miners.push(miner); - } - } - - public getAllGoldMines(): Node[] { - return this.goldMines.filter(mine => mine && mine.isValid); - } - - public getWarehouse(): Node | null { - return this.warehouse; - } - - public mineGoldOre(miner: Node): boolean { - this.totalOresCollected++; - this.updateWarehouseUI(); - return true; - } - - public getTotalOresCollected(): number { - return this.totalOresCollected; - } - - private createWarehouseUI() { - if (!this.warehouse) return; - - this.warehouseUI = StatusUIManager.createWarehouseUI(this.warehouse); - if (this.warehouseUI) { - this.updateWarehouseUI(); - } - } - - private updateWarehouseUI() { - if (this.warehouseUI && this.warehouseUI.warehouseCountLabel) { - this.warehouseUI.warehouseCountLabel.string = `🏭 总存储: ${this.totalOresCollected}`; - } - } - - onDestroy() { - this.unscheduleAllCallbacks(); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts.meta deleted file mode 100644 index 5738781d..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/RTSDemo.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "c3386f4a-9bef-416f-b770-fcec91cedbc4", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components.meta b/extensions/cocos/cocos-ecs/assets/scripts/components.meta deleted file mode 100644 index fff9e799..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "d07d95ad-f180-4b6e-9d0a-7248e75ec795", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts deleted file mode 100644 index 0ea66a85..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { Node, resources, JsonAsset, Component, _decorator, Vec3, tween, instantiate, Prefab } from 'cc'; -import { BehaviorTree, BehaviorTreeBuilder, Blackboard, TaskStatus, BehaviorTreeJSONConfig, EventRegistry, IBehaviorTreeContext, ActionResult } from '@esengine/ai'; -import { MinerStatusUI } from './MinerStatusUI'; -import { StatusUIManager } from './StatusUIManager'; - -const { ccclass, property } = _decorator; - -@ccclass('BehaviorTreeComponent') -export class BehaviorTreeComponent extends Component { - - @property - behaviorTreeFile: string = ''; - - @property - autoStart: boolean = true; - - @property - debugMode: boolean = false; - - @property - showStatusUI: boolean = true; - - @property(Prefab) - statusUIPrefab: Prefab | null = null; - - private behaviorTree: BehaviorTree | null = null; - private statusUI: MinerStatusUI | null = null; - private blackboard: Blackboard | null = null; - private context: any = null; - private eventRegistry: EventRegistry | null = null; - private isLoaded: boolean = false; - private isRunning: boolean = false; - - private actionStates: Map = new Map(); - - start() { - if (this.autoStart && this.behaviorTreeFile) { - this.initialize(); - } - - if (this.showStatusUI) { - this.createStatusUI(); - } - } - - async initialize() { - if (!this.behaviorTreeFile) { - return; - } - - try { - await this.loadBehaviorTree(); - this.isLoaded = true; - this.isRunning = true; - } catch (error) { - // 静默处理 - } - } - - private async loadBehaviorTree(): Promise { - return new Promise((resolve, reject) => { - let jsonPath = this.behaviorTreeFile; - resources.load(jsonPath, JsonAsset, (err, asset) => { - if (err) { - reject(err); - return; - } - - try { - const treeData = asset.json as BehaviorTreeJSONConfig; - this.buildBehaviorTree(treeData); - resolve(); - } catch (buildError) { - reject(buildError); - } - }); - }); - } - - private buildBehaviorTree(treeData: BehaviorTreeJSONConfig) { - this.eventRegistry = new EventRegistry(); - this.setupEventHandlers(); - - const baseContext = { - node: this.node, - component: this, - eventRegistry: this.eventRegistry - }; - - const result = BehaviorTreeBuilder.fromBehaviorTreeConfig(treeData, baseContext); - this.behaviorTree = result.tree; - this.blackboard = result.blackboard; - this.context = result.context; - - this.initializeBlackboard(); - } - - private setupEventHandlers() { - if (!this.eventRegistry) return; - - this.eventRegistry.registerAction('go-home-rest', (context, params) => { - return this.handleGoHomeRest(context, params); - }); - - this.eventRegistry.registerAction('recover-stamina', (context, params) => { - return this.handleRecoverStamina(context, params); - }); - - this.eventRegistry.registerAction('store-ore', (context, params) => { - return this.handleStoreOre(context, params); - }); - - this.eventRegistry.registerAction('mine-gold-ore', (context, params) => { - return this.handleMineGoldOre(context, params); - }); - - this.eventRegistry.registerAction('idle-behavior', (context, params) => { - return this.handleIdleBehavior(context, params); - }); - } - - private initializeBlackboard() { - if (!this.blackboard) return; - - this.blackboard.setValue('stamina', 100); - this.blackboard.setValue('staminaPercentage', 1.0); - this.blackboard.setValue('isLowStamina', false); - this.blackboard.setValue('hasOre', false); - this.blackboard.setValue('isResting', false); - this.blackboard.setValue('homePosition', this.node.worldPosition); - } - - - private createStatusUI() { - if (!this.statusUIPrefab) { - this.createSimpleStatusUI(); - return; - } - - const uiNode = instantiate(this.statusUIPrefab); - const canvas = this.node.scene?.getChildByName('Canvas'); - if (canvas) { - canvas.addChild(uiNode); - this.statusUI = uiNode.getComponent(MinerStatusUI); - if (this.statusUI) { - this.statusUI.setFollowTarget(this.node); - } - } - } - - private createSimpleStatusUI() { - this.statusUI = StatusUIManager.createStatusUIForMiner(this.node); - } - - private updateStatusUI() { - if (!this.statusUI || !this.blackboard) return; - - const stamina = this.blackboard.getValue('stamina') || 0; - const maxStamina = this.blackboard.getValue('maxStamina') || 100; - const hasOre = this.blackboard.getValue('hasOre') || false; - const isResting = this.blackboard.getValue('isResting') || false; - - // 更新体力 - this.statusUI.updateStamina(stamina, maxStamina); - - // 更新状态文本 - let status = ''; - if (isResting) { - status = '😴休息中'; - } else if (hasOre) { - status = '🚚运输中'; - } else { - status = '⛏️挖矿中'; - } - this.statusUI.updateStatus(status); - - // 获取仓库矿石总数 - const gameManager = this.node.parent?.getComponent('SimpleMinerDemo'); - const warehouseTotal = (gameManager as any)?.getTotalOresCollected() || 0; - - // 更新矿石数量显示 - this.statusUI.updateOreCount(hasOre, warehouseTotal); - - // 更新动作进度 - this.updateActionProgressUI(); - } - - private updateActionProgressUI() { - if (!this.statusUI) return; - - let actionName = ''; - let progress = 0; - - // 检查当前正在执行的动作 - for (const [key, state] of this.actionStates.entries()) { - if (state.isExecuting) { - const elapsed = Date.now() - state.startTime; - progress = Math.min(elapsed / state.duration, 1.0); - - switch (key) { - case 'mine-gold-ore': - actionName = '⛏️ 挖掘中'; - break; - case 'store-ore': - actionName = '📦 存储中'; - break; - case 'recover-stamina': - actionName = '💤 恢复体力'; - break; - default: - actionName = key; - } - break; // 只显示第一个正在执行的动作 - } - } - - // 如果没有正在执行的动作,清空进度显示 - this.statusUI.updateActionProgress(actionName, progress); - } - - // ==================== 行为树事件处理器 ==================== - - /** - * 清理动作状态 - 当动作被中止时调用 - */ - private clearActionState(actionKey: string) { - if (this.actionStates.has(actionKey)) { - this.actionStates.delete(actionKey); - - } - } - - /** - * 回家休息 - 包含体力恢复逻辑 - */ - private handleGoHomeRest(context: any, params: any): ActionResult { - const blackboard = this.blackboard; - if (!blackboard) return 'failure'; - - // 检查是否已经在家了 - const homePos = blackboard.getValue('homePosition') || this.node.worldPosition; - const distance = Vec3.distance(this.node.worldPosition, homePos); - - - - if (distance > 1.0) { - // 还没到家,继续移动 - this.moveToPosition(homePos, 2.0); - return 'running'; - } else { - this.clearActionState('mine-gold-ore'); - this.clearActionState('store-ore'); - blackboard.setValue('isResting', true); - const actionKey = 'go-home-rest'; - const currentTime = Date.now(); - - // 初始化休息状态 - if (!this.actionStates.has(actionKey)) { - this.actionStates.set(actionKey, { - isExecuting: true, - startTime: currentTime, - duration: 2000 // 2秒恢复一次 - }); - return 'running'; - } - - const actionState = this.actionStates.get(actionKey)!; - const elapsed = currentTime - actionState.startTime; - - if (elapsed >= actionState.duration) { - const currentStamina = blackboard.getValue('stamina'); - const newStamina = Math.min(100, currentStamina + 10); - - blackboard.setValue('stamina', newStamina); - blackboard.setValue('staminaPercentage', newStamina / 100); - - if (newStamina >= 80) { - blackboard.setValue('isResting', false); - blackboard.setValue('isLowStamina', false); - this.actionStates.delete(actionKey); - return 'success'; - } - - actionState.startTime = currentTime; - } - - return 'running'; - } - } - - private handleRecoverStamina(context: any, params: any): ActionResult { - return 'success'; - } - - private handleMineGoldOre(context: any, params: any): ActionResult { - const blackboard = this.blackboard; - if (!blackboard) return 'failure'; - - const hasOre = blackboard.getValue('hasOre'); - const isLowStamina = blackboard.getValue('isLowStamina'); - const isResting = blackboard.getValue('isResting'); - - if (hasOre || isLowStamina || isResting) { - return 'failure'; - } - - const gameManager = this.node.parent?.getComponent('SimpleMinerDemo'); - const goldMines = (gameManager as any)?.getAllGoldMines(); - if (!goldMines?.length) return 'failure'; - - let nearestMine = goldMines[0]; - let minDistance = Vec3.distance(this.node.worldPosition, nearestMine.worldPosition); - - for (const mine of goldMines) { - const distance = Vec3.distance(this.node.worldPosition, mine.worldPosition); - if (distance < minDistance) { - minDistance = distance; - nearestMine = mine; - } - } - - if (minDistance > 2.0) { - this.moveToPosition(nearestMine.worldPosition, 2.0); - return 'running'; - } else { - const actionKey = 'mine-gold-ore'; - const currentTime = Date.now(); - - if (!this.actionStates.has(actionKey)) { - this.actionStates.set(actionKey, { - isExecuting: true, - startTime: currentTime, - duration: 3000 - }); - return 'running'; - } - - const actionState = this.actionStates.get(actionKey)!; - const elapsed = currentTime - actionState.startTime; - - if (elapsed >= actionState.duration) { - const currentStamina = blackboard.getValue('stamina'); - const newStamina = Math.max(0, currentStamina - 15); - - blackboard.setValue('stamina', newStamina); - blackboard.setValue('staminaPercentage', newStamina / 100); - blackboard.setValue('hasOre', true); - blackboard.setValue('isLowStamina', newStamina < 20); - - this.actionStates.delete(actionKey); - return 'failure'; - } - - return 'running'; - } - } - - private handleStoreOre(context: any, params: any): ActionResult { - const blackboard = this.blackboard; - if (!blackboard) return 'failure'; - - const hasOre = blackboard.getValue('hasOre'); - if (!hasOre) { - return 'failure'; - } - - const isLowStamina = blackboard.getValue('isLowStamina'); - if (isLowStamina) { - return 'failure'; - } - - this.clearActionState('mine-gold-ore'); - const gameManager = this.node.parent?.getComponent('SimpleMinerDemo'); - const warehouse = (gameManager as any)?.getWarehouse(); - if (!warehouse) return 'failure'; - - const distance = Vec3.distance(this.node.worldPosition, warehouse.worldPosition); - - if (distance > 2.0) { - this.moveToPosition(warehouse.worldPosition, 2.0); - return 'running'; - } else { - const actionKey = 'store-ore'; - const currentTime = Date.now(); - - if (!this.actionStates.has(actionKey)) { - this.actionStates.set(actionKey, { - isExecuting: true, - startTime: currentTime, - duration: 1500 - }); - return 'running'; - } - - const actionState = this.actionStates.get(actionKey)!; - const elapsed = currentTime - actionState.startTime; - - if (elapsed >= actionState.duration) { - blackboard.setValue('hasOre', false); - (gameManager as any).mineGoldOre(this.node); - this.actionStates.delete(actionKey); - return 'success'; - } - - return 'running'; - } - } - - private handleIdleBehavior(context: any, params: any): ActionResult { - return 'success'; - } - - private moveToPosition(targetPos: Vec3, duration: number) { - tween(this.node).stop(); - tween(this.node).to(duration, { worldPosition: targetPos }).start(); - } - - update(deltaTime: number) { - if (this.behaviorTree && this.isRunning) { - this.behaviorTree.tick(deltaTime); - } - - if (this.showStatusUI) { - this.updateStatusUI(); - } - } - - /** - * 获取黑板 - */ - getBlackboard(): Blackboard | null { - return this.blackboard; - } - - /** - * 获取行为树 - */ - getBehaviorTree(): BehaviorTree | null { - return this.behaviorTree; - } - - /** - * 暂停行为树 - */ - pause() { - this.isRunning = false; - if (this.debugMode) { - - } - } - - /** - * 恢复行为树 - */ - resume() { - if (this.isLoaded) { - this.isRunning = true; - if (this.debugMode) { - - } - } - } - - /** - * 停止行为树 - */ - stop() { - this.isRunning = false; - if (this.behaviorTree) { - this.behaviorTree.reset(); - } - if (this.debugMode) { - - } - } - - /** - * 重新加载行为树 - */ - async reload() { - this.stop(); - await this.initialize(); - } - - /** - * 重置行为树状态 - */ - reset() { - if (this.behaviorTree) { - this.behaviorTree.reset(); - } - if (this.debugMode) { - - } - } - - onDestroy() { - this.stop(); - if (this.eventRegistry) { - this.eventRegistry.clear(); - } - - // 清理UI - if (this.statusUI) { - this.statusUI.node.destroy(); - this.statusUI = null; - } - - this.behaviorTree = null; - this.blackboard = null; - this.context = null; - this.eventRegistry = null; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts.meta deleted file mode 100644 index 61ff4919..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeComponent.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "8efd182b-9891-4903-bef2-eb07b5184263", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts deleted file mode 100644 index cd99a001..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts +++ /dev/null @@ -1,299 +0,0 @@ -import { _decorator, Component, resources, JsonAsset, Vec3 } from 'cc'; -import { BehaviorTree, BehaviorTreeBuilder, Blackboard, BehaviorTreeJSONConfig, ExecutionContext, EventRegistry, ActionResult } from '@esengine/ai'; -import { UnitController } from './UnitController'; -import { RTSBehaviorHandler } from './RTSBehaviorHandler'; - -const { ccclass, property } = _decorator; - -/** - * 游戏执行上下文接口 - * 继承框架的ExecutionContext,添加游戏特定的属性 - */ -interface GameExecutionContext extends ExecutionContext { - unitController: UnitController; - gameObject: any; - eventRegistry?: EventRegistry; - // 确保继承索引签名 - [key: string]: unknown; -} - -/** - * 行为树管理器 - 使用@esengine/ai包管理行为树 - */ -@ccclass('BehaviorTreeManager') -export class BehaviorTreeManager extends Component { - - @property - debugMode: boolean = true; - - @property - tickInterval: number = 0.1; // 行为树更新间隔(秒)- 10fps更新频率,平衡性能和响应性 - - private behaviorTree: BehaviorTree | null = null; - private blackboard: Blackboard | null = null; - private context: GameExecutionContext | null = null; - private eventRegistry: EventRegistry | null = null; - private isLoaded: boolean = false; - private isRunning: boolean = false; - private lastTickTime: number = 0; - private unitController: UnitController | null = null; - private currentBehaviorTreeName: string = ''; - private behaviorHandler: RTSBehaviorHandler | null = null; - - /** - * 初始化行为树 - */ - async initializeBehaviorTree(behaviorTreeName: string, unitController: UnitController) { - this.currentBehaviorTreeName = behaviorTreeName; - this.unitController = unitController; - - // 获取RTSBehaviorHandler组件 - this.behaviorHandler = this.getComponent(RTSBehaviorHandler); - if (!this.behaviorHandler) { - console.error(`BehaviorTreeManager: 未找到RTSBehaviorHandler组件 - ${this.node.name}`); - return; - } - - try { - await this.loadBehaviorTree(behaviorTreeName); - this.setupBlackboard(); - this.isLoaded = true; - this.isRunning = true; - - } catch (error) { - console.error(`行为树初始化失败: ${behaviorTreeName}`, error); - } - } - - /** - * 加载行为树文件 - */ - private async loadBehaviorTree(behaviorTreeName: string): Promise { - return new Promise((resolve, reject) => { - const jsonPath = `${behaviorTreeName}.bt`; - - - resources.load(jsonPath, JsonAsset, (err, asset) => { - if (err) { - console.error(`加载行为树文件失败: ${jsonPath}`, err); - reject(err); - return; - } - - try { - const behaviorTreeData = asset.json as BehaviorTreeJSONConfig; - - // 创建执行上下文 - this.blackboard = new Blackboard(); - this.eventRegistry = this.createEventRegistry(); - this.context = { - blackboard: this.blackboard, - unitController: this.unitController!, - gameObject: this.node, - eventRegistry: this.eventRegistry - } as GameExecutionContext; - - // 从JSON数据创建行为树 - const buildResult = BehaviorTreeBuilder.fromBehaviorTreeConfig(behaviorTreeData, this.context); - this.behaviorTree = buildResult.tree; - this.blackboard = buildResult.blackboard; - - resolve(); - } catch (parseError) { - console.error(`创建行为树失败: ${jsonPath}`, parseError); - reject(parseError); - } - }); - }); - } - - /** - * 创建事件注册表 - */ - private createEventRegistry(): EventRegistry { - const registry = new EventRegistry(); - - // 注册体力系统矿工行为事件处理器 - const eventHandlers = { - // 矿工体力系统核心行为 - 'mine-gold-ore': (context: any, params?: any) => this.callBehaviorHandler('onMineGoldOre', params), - 'store-ore': (context: any, params?: any) => this.callBehaviorHandler('onStoreOre', params), - 'go-home-rest': (context: any, params?: any) => this.callBehaviorHandler('onGoHomeRest', params), - 'recover-stamina': (context: any, params?: any) => this.callBehaviorHandler('onRecoverStamina', params), - 'idle-behavior': (context: any, params?: any) => this.callBehaviorHandler('onIdleBehavior', params) - }; - - // 将事件处理器注册到EventRegistry - Object.entries(eventHandlers).forEach(([eventName, handler]) => { - registry.registerAction(eventName, handler); - }); - - return registry; - } - - /** - * 调用行为处理器的方法 - */ - private callBehaviorHandler(methodName: string, params: any = {}): ActionResult { - if (!this.behaviorHandler) { - console.error(`BehaviorTreeManager: RTSBehaviorHandler未初始化 - ${this.node.name}`); - return 'failure'; - } - - try { - // 直接调用RTSBehaviorHandler的方法 - const method = (this.behaviorHandler as any)[methodName]; - if (typeof method === 'function') { - const result = method.call(this.behaviorHandler, params); - return result || 'success'; // 确保有返回值 - } else { - console.error(`BehaviorTreeManager: 方法不存在: ${methodName}`); - return 'failure'; - } - } catch (error) { - console.error(`BehaviorTreeManager: 调用方法失败: ${methodName}`, error); - return 'failure'; - } - } - - /** - * 设置黑板基础信息 - */ - private setupBlackboard() { - if (!this.unitController || !this.blackboard) return; - - // 设置矿工基础信息 - this.blackboard.setValue('unitType', this.unitController.unitType); - this.blackboard.setValue('currentHealth', this.unitController.currentHealth); - this.blackboard.setValue('maxHealth', this.unitController.maxHealth); - this.blackboard.setValue('currentCommand', 'mine'); - this.blackboard.setValue('hasOre', false); - this.blackboard.setValue('hasTarget', false); - this.blackboard.setValue('targetPosition', null); - this.blackboard.setValue('isMoving', false); - - // 设置体力系统信息 - this.blackboard.setValue('stamina', this.unitController.currentStamina); - this.blackboard.setValue('maxStamina', this.unitController.maxStamina); - this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina); - this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2); - this.blackboard.setValue('isResting', false); - this.blackboard.setValue('homePosition', this.unitController.homePosition); - } - - /** - * 更新黑板值 - */ - updateBlackboardValue(key: string, value: any) { - if (this.blackboard) { - this.blackboard.setValue(key, value); - } - } - - /** - * 获取黑板值 - */ - getBlackboardValue(key: string): any { - return this.blackboard?.getValue(key); - } - - /** - * 获取黑板 - */ - getBlackboard(): Blackboard | null { - return this.blackboard; - } - - /** - * 获取行为树 - */ - getBehaviorTree(): BehaviorTree | null { - return this.behaviorTree; - } - - /** - * 更新行为树 - */ - update(deltaTime: number) { - if (!this.isLoaded || !this.isRunning || !this.behaviorTree || !this.blackboard) return; - - // 控制更新频率 - this.lastTickTime += deltaTime; - if (this.lastTickTime < this.tickInterval) return; - - this.lastTickTime = 0; - - // 更新矿工状态信息 - if (this.unitController) { - // 基础属性 - this.blackboard.setValue('currentHealth', this.unitController.currentHealth); - this.blackboard.setValue('currentCommand', this.unitController.currentCommand); - this.blackboard.setValue('hasTarget', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO)); - this.blackboard.setValue('targetPosition', this.unitController.targetPosition); - this.blackboard.setValue('isMoving', this.unitController.targetPosition && !this.unitController.targetPosition.equals(Vec3.ZERO)); - - // 体力系统状态 - this.blackboard.setValue('stamina', this.unitController.currentStamina); - this.blackboard.setValue('maxStamina', this.unitController.maxStamina); - this.blackboard.setValue('staminaPercentage', this.unitController.currentStamina / this.unitController.maxStamina); - this.blackboard.setValue('isLowStamina', this.unitController.currentStamina < this.unitController.maxStamina * 0.2); - this.blackboard.setValue('homePosition', this.unitController.homePosition); - } - - // 执行行为树 - try { - this.behaviorTree.tick(deltaTime); - } catch (error) { - console.error(`行为树执行错误: ${this.node.name}`, error); - } - } - - /** - * 暂停行为树 - */ - pause() { - this.isRunning = false; - } - - /** - * 恢复行为树 - */ - resume() { - if (this.isLoaded) { - this.isRunning = true; - } - } - - /** - * 停止行为树 - */ - stop() { - this.isRunning = false; - } - - /** - * 重新加载行为树 - */ - async reloadBehaviorTree() { - if (this.currentBehaviorTreeName && this.unitController) { - this.stop(); - await this.initializeBehaviorTree(this.currentBehaviorTreeName, this.unitController); - } - } - - /** - * 重置行为树 - */ - reset() { - if (this.behaviorTree) { - this.behaviorTree.reset(); - } - } - - onDestroy() { - this.stop(); - this.behaviorTree = null; - this.blackboard = null; - this.context = null; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts.meta deleted file mode 100644 index 7242fdfc..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/BehaviorTreeManager.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "891c88fc-282d-4791-a961-8d85244bfee7", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts deleted file mode 100644 index 769517ff..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { Component, _decorator, Label, ProgressBar, Node, UITransform, Canvas, find, Camera, Vec3, director, Color, Layers, Graphics } from 'cc'; - -const { ccclass, property } = _decorator; - -/** - * 矿工状态UI组件 - */ -@ccclass('MinerStatusUI') -export class MinerStatusUI extends Component { - - nameLabel: Label | null = null; - statusLabel: Label | null = null; - staminaBar: ProgressBar | null = null; - actionProgressBar: ProgressBar | null = null; - actionLabel: Label | null = null; - oreCountLabel: Label | null = null; - warehouseCountLabel: Label | null = null; - - @property - followTarget: Node | null = null; - - @property - yOffset: number = 100; - - private camera: Camera | null = null; - private canvas: Canvas | null = null; - - start() { - this.node.layer = Layers.Enum.UI_2D; - - this.camera = find('Main Camera')?.getComponent(Camera) || director.getScene()?.getComponentInChildren(Camera); - this.canvas = find('Canvas')?.getComponent(Canvas) || director.getScene()?.getComponentInChildren(Canvas); - - - - if (this.nameLabel && this.followTarget) { - this.nameLabel.string = this.followTarget.name; - } - - this.updateStamina(100, 100); - this.updateStatus('待机中'); - this.updateActionProgress('', 0); - } - - update() { - if (this.followTarget && this.camera && this.canvas) { - this.updateUIPosition(); - } - } - - private updateUIPosition() { - if (!this.followTarget || !this.camera || !this.canvas) return; - - const targetWorldPos = this.followTarget.worldPosition.clone(); - // 根据目标类型设置不同的Y偏移 - if (this.followTarget.name.includes('Warehouse')) { - targetWorldPos.y += 3.0; // 仓库偏移更高 - } else { - targetWorldPos.y += 2.0; // 矿工偏移 - } - - // 将世界坐标直接转换为UI坐标 - const uiPos = new Vec3(); - this.camera.convertToUINode(targetWorldPos, this.canvas.node, uiPos); - this.node.setPosition(uiPos); - } - - setFollowTarget(target: Node) { - this.followTarget = target; - if (this.nameLabel) { - this.nameLabel.string = target.name; - } - } - - updateStamina(current: number, max: number) { - if (this.staminaBar) { - this.staminaBar.progress = current / max; - } - - if (this.staminaBar) { - const percentage = current / max; - const fillNode = this.staminaBar.node.getChildByName('Bar'); - if (fillNode) { - const graphics = fillNode.getComponent(Graphics); - if (graphics) { - let color: Color; - if (percentage > 0.6) { - color = new Color(0, 255, 0, 255); - } else if (percentage > 0.3) { - color = new Color(255, 255, 0, 255); - } else { - color = new Color(255, 0, 0, 255); - } - - graphics.clear(); - graphics.fillColor = color; - graphics.rect(-75, -4, 150 * percentage, 8); - graphics.fill(); - } - } - } - } - - updateStatus(status: string) { - if (this.statusLabel) { - this.statusLabel.string = status; - } - } - - updateActionProgress(actionName: string, progress: number) { - if (this.actionLabel) { - this.actionLabel.string = actionName; - } - - if (this.actionProgressBar) { - this.actionProgressBar.progress = Math.max(0, Math.min(1, progress)); - this.actionProgressBar.node.active = actionName !== '' && progress > 0; - } - } - - setVisible(visible: boolean) { - this.node.active = visible; - } - - updateOreCount(hasOre: boolean, warehouseTotal: number) { - if (this.oreCountLabel) { - this.oreCountLabel.string = hasOre ? '💎1' : '💎0'; - } - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts.meta deleted file mode 100644 index 2e217bf8..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/MinerStatusUI.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "5f877c25-5c26-49c6-bbb5-7ff36323e0a1", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts deleted file mode 100644 index 28555859..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { _decorator, Component, Vec3, Node } from 'cc'; -import { UnitController } from './UnitController'; - -const { ccclass } = _decorator; - -/** - * 矿工体力系统行为处理器 - 处理挖矿、休息、存储的完整循环 - * 展示体力驱动的工作-休息循环系统 - */ -@ccclass('RTSBehaviorHandler') -export class RTSBehaviorHandler extends Component { - - private unitController: UnitController | null = null; - private minerDemo: any = null; // MinerDemo组件引用 - private lastActionTime: number = 0; - private actionCooldown: number = 0.5; // 动作冷却时间,避免频繁切换 - private minerIndex: number = -1; // 矿工索引,用于找到对应的家 - - start() { - this.unitController = this.getComponent(UnitController); - // 获取场景中的MinerDemo组件 - this.minerDemo = this.node.parent?.getComponent('MinerDemo'); - - if (!this.unitController) { - console.error('RTSBehaviorHandler: 未找到UnitController组件'); - } - if (!this.minerDemo) { - console.error('RTSBehaviorHandler: 未找到MinerDemo组件'); - } - - // 从节点名称中提取矿工索引 - const match = this.node.name.match(/Miner_(\d+)/); - if (match) { - this.minerIndex = parseInt(match[1]) - 1; // 转换为0基索引 - } - - this.lastActionTime = Date.now(); - } - - /** - * 检查动作冷却 - */ - private isActionOnCooldown(): boolean { - return (Date.now() - this.lastActionTime) < (this.actionCooldown * 1000); - } - - /** - * 更新动作时间 - */ - private updateActionTime() { - this.lastActionTime = Date.now(); - } - - /** - * 挖掘金矿(永不枯竭) - * @param params 事件参数,包含黑板变量值 - */ - onMineGoldOre(params: any = {}): string { - if (!this.unitController || !this.minerDemo) { - return 'failure'; - } - - // 检查体力是否充足 - if (this.unitController.currentStamina < this.unitController.staminaCostPerMining) { - return 'failure'; - } - - // 检查是否已经携带矿石 - const hasOre = this.unitController.getBlackboardValue('hasOre'); - if (hasOre) { - return 'failure'; - } - - // 动作冷却检查 - if (this.isActionOnCooldown()) { - return 'running'; - } - - // 获取所有金矿 - const goldMines = this.minerDemo.getAllGoldMines(); - if (goldMines.length === 0) { - return 'failure'; - } - - // 寻找最近的金矿 - const currentPos = this.node.worldPosition; - let nearestMine: Node | null = null; - let minDistance = Infinity; - - for (const mine of goldMines) { - if (!mine || !mine.isValid) continue; - - const distance = Vec3.distance(currentPos, mine.worldPosition); - if (distance < minDistance) { - minDistance = distance; - nearestMine = mine; - } - } - - if (!nearestMine) { - return 'failure'; - } - - // 检查是否已经到达金矿位置 - if (minDistance < 2.0) { - // 检查是否正在移动 - const isMoving = this.unitController.getBlackboardValue('isMoving'); - if (isMoving) { - return 'running'; - } - - // 消耗体力 - this.unitController.currentStamina = Math.max(0, this.unitController.currentStamina - this.unitController.staminaCostPerMining); - - // 设置携带矿石状态 - this.unitController.setBlackboardValue('hasOre', true); - - // 通知演示管理器 - this.minerDemo.mineGoldOre(this.node); - - // 清除移动目标 - this.unitController.clearTarget(); - this.unitController.setBlackboardValue('isMoving', false); - - this.updateActionTime(); - return 'success'; - } else { - // 设置移动目标 - this.unitController.setTarget(nearestMine.worldPosition); - return 'running'; - } - } - - /** - * 前往仓库存储矿石 - * @param params 事件参数,包含黑板变量值 - */ - onStoreOre(params: any = {}): string { - if (!this.unitController || !this.minerDemo) { - return 'failure'; - } - - // 检查是否携带矿石 - const hasOre = this.unitController.getBlackboardValue('hasOre'); - if (!hasOre) { - return 'failure'; - } - - // 动作冷却检查 - if (this.isActionOnCooldown()) { - return 'running'; - } - - const warehouse = this.minerDemo.getWarehouse(); - if (!warehouse || !warehouse.isValid) { - return 'failure'; - } - - // 计算到仓库的距离 - const currentPos = this.node.worldPosition; - const warehousePos = warehouse.worldPosition; - const distance = Vec3.distance(currentPos, warehousePos); - - // 检查是否已经到达仓库 - if (distance < 2.5) { - // 检查是否正在移动 - const isMoving = this.unitController.getBlackboardValue('isMoving'); - if (isMoving) { - return 'running'; - } - - // 清除携带矿石状态 - this.unitController.setBlackboardValue('hasOre', false); - - // 清除移动目标 - this.unitController.clearTarget(); - this.unitController.setBlackboardValue('isMoving', false); - - this.updateActionTime(); - return 'success'; - } else { - // 设置移动目标 - this.unitController.setTarget(warehousePos); - return 'running'; - } - } - - /** - * 回家休息 - * @param params 事件参数,包含黑板变量值 - */ - onGoHomeRest(params: any = {}): string { - if (!this.unitController || !this.minerDemo) { - return 'failure'; - } - - // 动作冷却检查 - if (this.isActionOnCooldown()) { - return 'running'; - } - - // 获取矿工的家 - const home = this.minerDemo.getMinerHome(this.minerIndex); - if (!home || !home.isValid) { - return 'failure'; - } - - // 计算到家的距离 - const currentPos = this.node.worldPosition; - const homePos = home.worldPosition; - const distance = Vec3.distance(currentPos, homePos); - - // 检查是否已经到达家 - if (distance < 2.0) { - // 检查是否正在移动 - const isMoving = this.unitController.getBlackboardValue('isMoving'); - if (isMoving) { - return 'running'; - } - - // 设置休息状态 - this.unitController.setBlackboardValue('isResting', true); - - // 清除移动目标 - this.unitController.clearTarget(); - this.unitController.setBlackboardValue('isMoving', false); - - this.updateActionTime(); - return 'success'; - } else { - // 设置移动目标 - this.unitController.setTarget(homePos); - return 'running'; - } - } - - /** - * 恢复体力 - * @param params 事件参数,包含黑板变量值 - */ - onRecoverStamina(params: any = {}): string { - if (!this.unitController) { - return 'failure'; - } - - // 检查是否在家中 - const isResting = this.unitController.getBlackboardValue('isResting'); - if (!isResting) { - return 'failure'; - } - - // 恢复体力 - const oldStamina = this.unitController.currentStamina; - this.unitController.currentStamina = Math.min(this.unitController.maxStamina, - this.unitController.currentStamina + this.unitController.staminaRecoveryRate * 0.1); // 每次恢复2点体力 - - const isFullyRested = this.unitController.currentStamina >= this.unitController.maxStamina; - - if (isFullyRested) { - // 清除休息状态 - this.unitController.setBlackboardValue('isResting', false); - - // 通知演示管理器 - this.minerDemo.completeRestCycle(); - - this.updateActionTime(); - return 'success'; - } else { - // 体力还在恢复中 - return 'running'; - } - } - - /** - * 待机行为 - * @param params 事件参数,包含黑板变量值 - */ - onIdleBehavior(params: any = {}): string { - if (!this.unitController) { - return 'failure'; - } - - // 清除移动目标,确保停止移动 - this.unitController.clearTarget(); - this.unitController.setBlackboardValue('isMoving', false); - - - - return 'success'; - } - - /** - * 获取矿工状态摘要 - */ - getMinerStatus(): string { - if (!this.unitController) return 'Unknown'; - - const hasOre = this.unitController.getBlackboardValue('hasOre'); - const isMoving = this.unitController.getBlackboardValue('isMoving'); - const isResting = this.unitController.getBlackboardValue('isResting'); - const stamina = this.unitController.currentStamina; - const maxStamina = this.unitController.maxStamina; - - let status = ''; - if (isResting) { - status = '😴休息中'; - } else if (hasOre) { - status = isMoving ? '🚚运输中' : '📦携带矿石'; - } else { - status = isMoving ? '🚶移动中' : '⛏️挖矿'; - } - - return `${status} (体力:${stamina.toFixed(0)}/${maxStamina})`; - } - - /** - * 调试信息 - */ - getDebugInfo(): any { - if (!this.unitController) return {}; - - return { - name: this.node.name, - hasOre: this.unitController.getBlackboardValue('hasOre'), - isMoving: this.unitController.getBlackboardValue('isMoving'), - isResting: this.unitController.getBlackboardValue('isResting'), - stamina: this.unitController.currentStamina, - maxStamina: this.unitController.maxStamina, - staminaPercentage: this.unitController.currentStamina / this.unitController.maxStamina, - isLowStamina: this.unitController.currentStamina < this.unitController.maxStamina * 0.2, - status: this.getMinerStatus() - }; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts.meta deleted file mode 100644 index 2ea722f7..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/RTSBehaviorHandler.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "739ff9ee-42d5-4542-bb5b-3e7611c729e2", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts deleted file mode 100644 index a2173cea..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { _decorator, Component, Node, Vec3, MeshRenderer, BoxCollider, RigidBody, Material, Color, primitives, utils } from 'cc'; - -const { ccclass, property } = _decorator; - -/** - * 简单预制体工厂 - */ -@ccclass('SimplePrefabFactory') -export class SimplePrefabFactory extends Component { - - /** - * 创建单位节点 - */ - static createUnit(name: string, color: Color = Color.WHITE): Node { - const unit = new Node(name); - - // 添加网格渲染器 - const meshRenderer = unit.addComponent(MeshRenderer); - - // 创建立方体网格 - const mesh = utils.createMesh(primitives.box({ width: 1, height: 1, length: 1 })); - meshRenderer.mesh = mesh; - - // 创建材质 - const material = new Material(); - material.initialize({ effectName: 'builtin-unlit' }); - material.setProperty('mainColor', color); - meshRenderer.material = material; - - // 添加碰撞器 - const collider = unit.addComponent(BoxCollider); - collider.size = new Vec3(1, 1, 1); - - // 添加刚体 - const rigidBody = unit.addComponent(RigidBody); - rigidBody.type = RigidBody.Type.KINEMATIC; - - return unit; - } - - /** - * 创建建筑节点 - */ - static createBuilding(name: string, size: Vec3 = new Vec3(2, 2, 2), color: Color = Color.GRAY): Node { - const building = new Node(name); - - // 添加网格渲染器 - const meshRenderer = building.addComponent(MeshRenderer); - - // 创建立方体网格 - const mesh = utils.createMesh(primitives.box({ - width: size.x, - height: size.y, - length: size.z - })); - meshRenderer.mesh = mesh; - - // 创建材质 - const material = new Material(); - material.initialize({ effectName: 'builtin-unlit' }); - material.setProperty('mainColor', color); - meshRenderer.material = material; - - // 添加碰撞器 - const collider = building.addComponent(BoxCollider); - collider.size = size; - - return building; - } - - /** - * 创建资源节点 - */ - static createResource(name: string, color: Color = Color.YELLOW): Node { - const resource = new Node(name); - - // 添加网格渲染器 - const meshRenderer = resource.addComponent(MeshRenderer); - - // 创建球体网格 - const mesh = utils.createMesh(primitives.sphere(0.5)); - meshRenderer.mesh = mesh; - - // 创建材质 - const material = new Material(); - material.initialize({ effectName: 'builtin-unlit' }); - material.setProperty('mainColor', color); - meshRenderer.material = material; - - // 添加碰撞器 - const collider = resource.addComponent(BoxCollider); - collider.size = new Vec3(1, 1, 1); - - return resource; - } - - /** - * 创建地面节点 - */ - static createGround(size: Vec3 = new Vec3(50, 0.1, 50), color: Color = new Color(100, 150, 100, 255)): Node { - const ground = new Node('Ground'); - - // 添加网格渲染器 - const meshRenderer = ground.addComponent(MeshRenderer); - - // 创建平面网格 - const mesh = utils.createMesh(primitives.box({ - width: size.x, - height: size.y, - length: size.z - })); - meshRenderer.mesh = mesh; - - // 创建材质 - const material = new Material(); - material.initialize({ effectName: 'builtin-unlit' }); - material.setProperty('mainColor', color); - meshRenderer.material = material; - - // 添加碰撞器 - const collider = ground.addComponent(BoxCollider); - collider.size = size; - - return ground; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts.meta deleted file mode 100644 index 720705f1..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/SimplePrefabFactory.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "ac45cfc7-cf47-4315-bdf0-ba002b45b4b6", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts deleted file mode 100644 index eed2eb28..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts +++ /dev/null @@ -1,282 +0,0 @@ -import { Component, _decorator, Node, Label, ProgressBar, UITransform, Widget, Canvas, find, director, Color, Sprite, Layers, Graphics } from 'cc'; -import { MinerStatusUI } from './MinerStatusUI'; - -const { ccclass, property } = _decorator; - -/** - * 状态UI管理器 - * 负责创建和管理游戏对象的状态显示界面 - */ -@ccclass('StatusUIManager') -export class StatusUIManager extends Component { - - /** - * 为矿工创建状态显示UI - */ - static createStatusUIForMiner(miner: Node): MinerStatusUI | null { - const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas'); - if (!canvas) { - return null; - } - - const uiRoot = new Node(`${miner.name}_StatusUI`); - canvas.addChild(uiRoot); - - const uiTransform = uiRoot.addComponent(UITransform); - uiTransform.setContentSize(200, 100); - - const borderNode = new Node('Border'); - uiRoot.addChild(borderNode); - const borderTransform = borderNode.addComponent(UITransform); - borderTransform.setContentSize(202, 102); - const borderGraphics = borderNode.addComponent(Graphics); - borderGraphics.fillColor = new Color(100, 100, 100, 120); - borderGraphics.rect(-101, -51, 202, 102); - borderGraphics.fill(); - - const borderWidget = borderNode.addComponent(Widget); - borderWidget.isAlignTop = true; - borderWidget.isAlignBottom = true; - borderWidget.isAlignLeft = true; - borderWidget.isAlignRight = true; - borderWidget.top = -1; - borderWidget.bottom = -1; - borderWidget.left = -1; - borderWidget.right = -1; - borderWidget.updateAlignment(); - - const backgroundNode = new Node('Background'); - uiRoot.addChild(backgroundNode); - const backgroundTransform = backgroundNode.addComponent(UITransform); - backgroundTransform.setContentSize(200, 100); - const backgroundGraphics = backgroundNode.addComponent(Graphics); - backgroundGraphics.fillColor = new Color(0, 0, 0, 100); - backgroundGraphics.rect(-100, -50, 200, 100); - backgroundGraphics.fill(); - - const statusUI = uiRoot.addComponent(MinerStatusUI); - statusUI.setFollowTarget(miner); - - const nameNode = new Node('NameLabel'); - uiRoot.addChild(nameNode); - const nameTransform = nameNode.addComponent(UITransform); - nameTransform.setContentSize(200, 25); - const nameLabel = nameNode.addComponent(Label); - nameLabel.string = miner.name; - nameLabel.fontSize = 16; - nameLabel.color = new Color(255, 255, 255, 255); - - const nameWidget = nameNode.addComponent(Widget); - nameWidget.isAlignTop = true; - nameWidget.top = 0; - nameWidget.isAlignHorizontalCenter = true; - nameWidget.updateAlignment(); - - // 创建状态标签 - const statusNode = new Node('StatusLabel'); - uiRoot.addChild(statusNode); - const statusTransform = statusNode.addComponent(UITransform); - statusTransform.setContentSize(200, 20); - const statusLabel = statusNode.addComponent(Label); - statusLabel.string = '待机中'; - statusLabel.fontSize = 14; - statusLabel.color = new Color(200, 200, 200, 255); - - // 设置状态标签位置 - const statusWidget = statusNode.addComponent(Widget); - statusWidget.isAlignTop = true; - statusWidget.top = 25; - statusWidget.isAlignHorizontalCenter = true; - statusWidget.updateAlignment(); - - // 创建体力进度条 - const staminaBarNode = new Node('StaminaBar'); - uiRoot.addChild(staminaBarNode); - const staminaBarTransform = staminaBarNode.addComponent(UITransform); - staminaBarTransform.setContentSize(150, 8); - const staminaBar = staminaBarNode.addComponent(ProgressBar); - staminaBar.progress = 1.0; - - // 创建体力进度条背景 - const staminaBgNode = new Node('Background'); - staminaBarNode.addChild(staminaBgNode); - const staminaBgTransform = staminaBgNode.addComponent(UITransform); - staminaBgTransform.setContentSize(150, 8); - const staminaBgGraphics = staminaBgNode.addComponent(Graphics); - staminaBgGraphics.fillColor = new Color(50, 50, 50, 255); - staminaBgGraphics.rect(-75, -4, 150, 8); - staminaBgGraphics.fill(); - - // 创建体力进度条填充 - const staminaFillNode = new Node('Bar'); - staminaBarNode.addChild(staminaFillNode); - const staminaFillTransform = staminaFillNode.addComponent(UITransform); - staminaFillTransform.setContentSize(150, 8); - const staminaFillGraphics = staminaFillNode.addComponent(Graphics); - staminaFillGraphics.fillColor = new Color(0, 255, 0, 255); - staminaFillGraphics.rect(-75, -4, 150, 8); - staminaFillGraphics.fill(); - - // 设置体力进度条位置 - const staminaWidget = staminaBarNode.addComponent(Widget); - staminaWidget.isAlignTop = true; - staminaWidget.top = 45; - staminaWidget.isAlignHorizontalCenter = true; - staminaWidget.updateAlignment(); - - // 创建动作进度条 - const actionBarNode = new Node('ActionProgressBar'); - uiRoot.addChild(actionBarNode); - const actionBarTransform = actionBarNode.addComponent(UITransform); - actionBarTransform.setContentSize(150, 6); - const actionBar = actionBarNode.addComponent(ProgressBar); - actionBar.progress = 0; - actionBarNode.active = false; // 初始隐藏 - - // 创建动作进度条背景 - const actionBgNode = new Node('Background'); - actionBarNode.addChild(actionBgNode); - const actionBgTransform = actionBgNode.addComponent(UITransform); - actionBgTransform.setContentSize(150, 6); - const actionBgGraphics = actionBgNode.addComponent(Graphics); - actionBgGraphics.fillColor = new Color(50, 50, 50, 255); - actionBgGraphics.rect(-75, -3, 150, 6); - actionBgGraphics.fill(); - - // 创建动作进度条填充 - const actionFillNode = new Node('Bar'); - actionBarNode.addChild(actionFillNode); - const actionFillTransform = actionFillNode.addComponent(UITransform); - actionFillTransform.setContentSize(150, 6); - const actionFillGraphics = actionFillNode.addComponent(Graphics); - actionFillGraphics.fillColor = new Color(255, 255, 0, 255); - actionFillGraphics.rect(-75, -3, 150, 6); - actionFillGraphics.fill(); - - // 设置动作进度条位置 - const actionWidget = actionBarNode.addComponent(Widget); - actionWidget.isAlignTop = true; - actionWidget.top = 55; - actionWidget.isAlignHorizontalCenter = true; - actionWidget.updateAlignment(); - - // 创建动作标签 - const actionLabelNode = new Node('ActionLabel'); - uiRoot.addChild(actionLabelNode); - const actionLabelTransform = actionLabelNode.addComponent(UITransform); - actionLabelTransform.setContentSize(200, 15); - const actionLabel = actionLabelNode.addComponent(Label); - actionLabel.string = ''; - actionLabel.fontSize = 12; - actionLabel.color = new Color(255, 255, 0, 255); - - // 设置动作标签位置 - const actionLabelWidget = actionLabelNode.addComponent(Widget); - actionLabelWidget.isAlignTop = true; - actionLabelWidget.top = 65; - actionLabelWidget.isAlignHorizontalCenter = true; - actionLabelWidget.updateAlignment(); - - // 创建矿石数量标签 - const oreCountNode = new Node('OreCountLabel'); - uiRoot.addChild(oreCountNode); - const oreCountTransform = oreCountNode.addComponent(UITransform); - oreCountTransform.setContentSize(100, 15); - const oreCountLabel = oreCountNode.addComponent(Label); - oreCountLabel.string = '💎0'; - oreCountLabel.fontSize = 12; - oreCountLabel.color = new Color(255, 215, 0, 255); // 金色 - - // 设置矿石数量标签位置(居中显示) - const oreCountWidget = oreCountNode.addComponent(Widget); - oreCountWidget.isAlignTop = true; - oreCountWidget.top = 80; - oreCountWidget.isAlignHorizontalCenter = true; - oreCountWidget.updateAlignment(); - - statusUI.nameLabel = nameLabel; - statusUI.statusLabel = statusLabel; - statusUI.staminaBar = staminaBar; - statusUI.actionProgressBar = actionBar; - statusUI.actionLabel = actionLabel; - statusUI.oreCountLabel = oreCountLabel; - statusUI.warehouseCountLabel = null; - - StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D); - return statusUI; - } - - /** - * 递归设置节点及其子节点的层级 - */ - private static setNodeLayerRecursively(node: Node, layer: number) { - node.layer = layer; - for (const child of node.children) { - StatusUIManager.setNodeLayerRecursively(child, layer); - } - } - - /** - * 从矿工名字中提取索引号 - */ - private static extractMinerIndex(minerName: string): number { - const match = minerName.match(/Miner_(\d+)/); - if (match) { - return parseInt(match[1]) - 1; - } - return 0; - } - - /** - * 为仓库创建存储量显示UI - */ - static createWarehouseUI(warehouse: Node): MinerStatusUI | null { - const canvas = find('Canvas') || director.getScene()?.getChildByName('Canvas'); - if (!canvas) { - return null; - } - - const uiRoot = new Node(`${warehouse.name}_StorageUI`); - canvas.addChild(uiRoot); - - const uiTransform = uiRoot.addComponent(UITransform); - uiTransform.setContentSize(120, 40); - - const backgroundNode = new Node('Background'); - uiRoot.addChild(backgroundNode); - const backgroundTransform = backgroundNode.addComponent(UITransform); - backgroundTransform.setContentSize(120, 40); - const backgroundGraphics = backgroundNode.addComponent(Graphics); - backgroundGraphics.fillColor = new Color(0, 0, 0, 120); - backgroundGraphics.rect(-60, -20, 120, 40); - backgroundGraphics.fill(); - - const storageNode = new Node('StorageLabel'); - uiRoot.addChild(storageNode); - const storageTransform = storageNode.addComponent(UITransform); - storageTransform.setContentSize(120, 30); - const storageLabel = storageNode.addComponent(Label); - storageLabel.string = '🏭 总存储: 0'; - storageLabel.fontSize = 14; - storageLabel.color = new Color(255, 255, 255, 255); - - const storageWidget = storageNode.addComponent(Widget); - storageWidget.isAlignHorizontalCenter = true; - storageWidget.isAlignVerticalCenter = true; - storageWidget.updateAlignment(); - - const statusUI = uiRoot.addComponent(MinerStatusUI); - statusUI.setFollowTarget(warehouse); - - statusUI.nameLabel = null; - statusUI.statusLabel = null; - statusUI.staminaBar = null; - statusUI.actionProgressBar = null; - statusUI.actionLabel = null; - statusUI.oreCountLabel = null; - statusUI.warehouseCountLabel = storageLabel; - - StatusUIManager.setNodeLayerRecursively(uiRoot, Layers.Enum.UI_2D); - return statusUI; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts.meta deleted file mode 100644 index 805a46bd..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/StatusUIManager.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "7478e794-dd80-4661-9421-8e147d33c51e", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts b/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts deleted file mode 100644 index 032233e1..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts +++ /dev/null @@ -1,353 +0,0 @@ -import { _decorator, Component, Node, Vec3, MeshRenderer, Color, tween } from 'cc'; -import { BehaviorTreeManager } from './BehaviorTreeManager'; -import { RTSBehaviorHandler } from './RTSBehaviorHandler'; - -const { ccclass, property } = _decorator; - -/** - * 单位配置接口 - */ -export interface UnitConfig { - unitType: string; - behaviorTreeName: string; - maxHealth: number; - moveSpeed: number; - attackRange: number; - attackDamage: number; - color: string; -} - -/** - * 单位控制器 - */ -@ccclass('UnitController') -export class UnitController extends Component { - - @property - showDebugInfo: boolean = true; - - // 单位属性 - public unitType: string = ''; - public maxHealth: number = 100; - public currentHealth: number = 100; - public moveSpeed: number = 1.5; - public attackRange: number = 2; - public attackDamage: number = 25; - public isSelected: boolean = false; - public currentCommand: string = 'idle'; - public targetPosition: Vec3 = Vec3.ZERO.clone(); - public targetNode: Node | null = null; - public lastAttackTime: number = 0; - public attackCooldown: number = 1.5; - public color: string = 'white'; - - // 体力系统属性 - public maxStamina: number = 100; - public currentStamina: number = 100; - public homePosition: Vec3 = Vec3.ZERO.clone(); - public staminaRecoveryRate: number = 20; // 每秒恢复的体力 - public staminaCostPerMining: number = 15; // 每次挖矿消耗的体力 - - // 移动状态管理 - private isMoving: boolean = false; - private moveStartTime: number = 0; - private lastTargetUpdateTime: number = 0; - - private behaviorTreeManager: BehaviorTreeManager | null = null; - private behaviorHandler: Component | null = null; - private meshRenderer: MeshRenderer | null = null; - - onLoad() { - this.meshRenderer = this.getComponent(MeshRenderer); - - // 创建行为树管理器 - this.behaviorTreeManager = this.addComponent(BehaviorTreeManager); - - // 添加RTS行为处理器 - try { - // 添加RTSBehaviorHandler组件 - this.behaviorHandler = this.addComponent(RTSBehaviorHandler); - } catch (error) { - console.warn('RTSBehaviorHandler组件添加失败', error); - } - } - - /** - * 设置单位配置 - */ - setup(config: UnitConfig) { - this.unitType = config.unitType; - this.maxHealth = config.maxHealth; - this.currentHealth = config.maxHealth; - this.moveSpeed = config.moveSpeed; - this.attackRange = config.attackRange; - this.attackDamage = config.attackDamage; - this.color = config.color; - - // 设置材质颜色 - this.setUnitColor(config.color); - - // 设置节点名称显示单位类型 - this.node.name = `${config.unitType.toUpperCase()}_${this.node.name}`; - - // 初始化行为树 - if (this.behaviorTreeManager) { - this.behaviorTreeManager.initializeBehaviorTree(config.behaviorTreeName, this); - } - } - - /** - * 设置单位颜色 - */ - private setUnitColor(colorName: string) { - if (!this.meshRenderer || !this.meshRenderer.material) return; - - const colorMap: { [key: string]: Color } = { - 'red': Color.RED, - 'green': Color.GREEN, - 'blue': Color.BLUE, - 'yellow': Color.YELLOW, - 'white': Color.WHITE, - 'cyan': Color.CYAN, - 'magenta': Color.MAGENTA - }; - - const color = colorMap[colorName] || Color.WHITE; - this.meshRenderer.material.setProperty('mainColor', color); - } - - /** - * 设置选择状态 - */ - setSelected(selected: boolean) { - this.isSelected = selected; - - // 视觉效果 - if (selected) { - this.showSelectionEffect(); - } else { - this.hideSelectionEffect(); - } - - // 更新行为树黑板 - if (this.behaviorTreeManager) { - this.behaviorTreeManager.updateBlackboardValue('isSelected', selected); - } - } - - /** - * 显示选择效果 - */ - private showSelectionEffect() { - // 添加选择圈效果 - tween(this.node) - .to(0.3, { scale: new Vec3(1.1, 1.1, 1.1) }) - .to(0.3, { scale: Vec3.ONE }) - .union() - .repeatForever() - .start(); - } - - /** - * 隐藏选择效果 - */ - private hideSelectionEffect() { - // 停止所有缩放动画 - tween(this.node).stop(); - this.node.setScale(Vec3.ONE); - } - - /** - * 发布命令 - */ - issueCommand(command: string, target?: Vec3 | Node) { - this.currentCommand = command; - - // 设置目标 - if (target instanceof Vec3) { - this.targetPosition = target.clone(); - this.targetNode = null; - } else if (target instanceof Node) { - this.targetPosition = target.worldPosition.clone(); - this.targetNode = target; - } - - // 更新行为树黑板 - if (this.behaviorTreeManager) { - this.behaviorTreeManager.updateBlackboardValue('currentCommand', command); - this.behaviorTreeManager.updateBlackboardValue('hasTarget', target !== undefined); - this.behaviorTreeManager.updateBlackboardValue('targetPosition', this.targetPosition); - - if (target instanceof Node) { - this.behaviorTreeManager.updateBlackboardValue('targetType', - target.name.includes('Resource') ? 'resource' : - target.name.includes('Building') ? 'building' : 'unit'); - } - } - } - - /** - * 设置黑板变量值 - */ - setBlackboardValue(key: string, value: any) { - if (this.behaviorTreeManager) { - this.behaviorTreeManager.updateBlackboardValue(key, value); - } - } - - /** - * 获取黑板变量值 - */ - getBlackboardValue(key: string): any { - return this.behaviorTreeManager?.getBlackboardValue(key); - } - - /** - * 设置移动目标 - */ - setTarget(position: Vec3) { - this.targetPosition = position.clone(); - this.isMoving = true; - this.moveStartTime = Date.now(); - } - - /** - * 清除移动目标 - */ - clearTarget() { - this.targetPosition = Vec3.ZERO.clone(); - this.isMoving = false; - } - - /** - * 受到伤害 - */ - takeDamage(damage: number) { - this.currentHealth = Math.max(0, this.currentHealth - damage); - - // 更新行为树黑板 - if (this.behaviorTreeManager) { - this.behaviorTreeManager.updateBlackboardValue('currentHealth', this.currentHealth); - this.behaviorTreeManager.updateBlackboardValue('healthPercentage', this.currentHealth / this.maxHealth); - this.behaviorTreeManager.updateBlackboardValue('isLowHealth', this.currentHealth < this.maxHealth * 0.3); - } - - // 视觉效果 - this.showDamageEffect(); - - if (this.currentHealth <= 0) { - this.die(); - } - } - - /** - * 显示受伤效果 - */ - private showDamageEffect() { - if (!this.meshRenderer || !this.meshRenderer.material) return; - - // 闪红效果 - const originalColor = this.meshRenderer.material.getProperty('mainColor') as Color; - this.meshRenderer.material.setProperty('mainColor', Color.RED); - - this.scheduleOnce(() => { - if (this.meshRenderer && this.meshRenderer.material) { - this.meshRenderer.material.setProperty('mainColor', originalColor); - } - }, 0.2); - } - - /** - * 单位死亡 - */ - private die() { - console.log(`单位 ${this.node.name} 死亡`); - - // 播放死亡动画后销毁节点 - tween(this.node) - .to(0.5, { scale: Vec3.ZERO }) - .call(() => { - this.node.destroy(); - }) - .start(); - } - - /** - * 移动到目标位置(只在水平面移动,不改变Y轴) - */ - moveToTarget(targetPos: Vec3, speed?: number, deltaTime?: number): boolean { - const currentPos = this.node.worldPosition; - const distance = Vec3.distance(currentPos, targetPos); - - if (distance < 0.5) { - this.isMoving = false; - return true; - } - - const actualSpeed = speed || this.moveSpeed; - const actualDeltaTime = deltaTime || 0.016; - const direction = new Vec3(); - Vec3.subtract(direction, targetPos, currentPos); - direction.normalize(); - - const moveDistance = actualSpeed * actualDeltaTime; - const newPosition = new Vec3(); - Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance); - - this.node.setWorldPosition(newPosition); - this.isMoving = true; - - return false; - } - - /** - * 攻击目标 - */ - attackTarget(): boolean { - const currentTime = Date.now(); - if (currentTime - this.lastAttackTime < this.attackCooldown * 1000) { - return false; - } - - if (this.targetNode && this.targetNode.isValid) { - const distance = Vec3.distance(this.node.worldPosition, this.targetNode.worldPosition); - if (distance <= this.attackRange) { - this.lastAttackTime = currentTime; - return true; - } - } - - return false; - } - - update(deltaTime: number) { - if (this.behaviorTreeManager) { - this.behaviorTreeManager.update(deltaTime); - } - - if (this.isMoving && !this.targetPosition.equals(Vec3.ZERO)) { - const reached = this.moveToTarget(this.targetPosition, this.moveSpeed, deltaTime); - if (reached) { - this.clearTarget(); - } - } - - // 调试信息显示 - if (this.showDebugInfo) { - this.updateDebugInfo(); - } - } - - /** - * 更新调试信息 - */ - private updateDebugInfo() { - // 可以在这里添加调试信息的显示逻辑 - // 比如在单位上方显示状态文本等 - } - - onDestroy() { - // 停止所有动画 - tween(this.node).stop(); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts.meta deleted file mode 100644 index fc3634a4..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/components/UnitController.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "4ac64480-2d09-4de6-a22c-add022790676", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs.meta deleted file mode 100644 index c667062f..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "a1f43720-46e1-4d07-b56a-c9307e45726c", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts deleted file mode 100644 index bdc394a5..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Core } from '@esengine/ecs-framework'; -import { Component, _decorator } from 'cc'; -import { GameScene } from './scenes/GameScene'; - -const { ccclass, property } = _decorator; - -/** - * ECS管理器 - Cocos Creator组件 - * 将此组件添加到场景中的任意节点上即可启动ECS框架 - * - * 使用说明: - * 1. 在Cocos Creator场景中创建一个空节点 - * 2. 将此ECSManager组件添加到该节点 - * 3. 运行场景即可自动启动ECS框架 - */ -@ccclass('ECSManager') -export class ECSManager extends Component { - - @property({ - tooltip: '是否启用调试模式(建议开发阶段开启)' - }) - public debugMode: boolean = true; - - private isInitialized: boolean = false; - - /** - * 组件启动时初始化ECS - */ - start() { - this.initializeECS(); - } - - /** - * 初始化ECS框架 - */ - private initializeECS(): void { - if (this.isInitialized) return; - - // ECS框架初始化开始 - - try { - // 1. 创建Core实例,启用调试功能 - if (this.debugMode) { - Core.create({ - debug: true, - enableEntitySystems: true, - debugConfig: { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - debugFrameRate: 30, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - } - }); - console.log('✅ ECS调试模式已启用'); - } else { - Core.create({ - debug: false, - enableEntitySystems: true - }); - console.log('ℹ️ ECS调试模式已禁用'); - } - - // 2. 创建游戏场景 - const gameScene = new GameScene(); - - // 3. 设置为当前场景(会自动调用scene.begin()) - Core.setScene(gameScene); - - this.isInitialized = true; - // ECS框架初始化完成 - - } catch (error) { - console.error('ECS框架初始化失败:', error); - } - } - - /** - * 每帧更新ECS框架 - */ - update(deltaTime: number) { - if (this.isInitialized) { - // 更新ECS核心系统 - Core.update(deltaTime); - } - } - - /** - * 组件销毁时清理ECS - */ - onDestroy() { - if (this.isInitialized) { - // ECS框架清理 - this.isInitialized = false; - } - } -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts.meta deleted file mode 100644 index abf66b07..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/ECSManager.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "b89656f0-6320-4b6d-81cd-447bf811230c", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md b/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md deleted file mode 100644 index e259e0f8..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md +++ /dev/null @@ -1,153 +0,0 @@ -# ECS框架启动模板 - -欢迎使用ECS框架!这是一个最基础的启动模板,帮助您快速开始ECS项目开发。 - -## 📁 项目结构 - -``` -ecs/ -├── components/ # 组件目录(请在此添加您的组件) -├── systems/ # 系统目录(请在此添加您的系统) -├── scenes/ # 场景目录 -│ └── GameScene.ts # 主游戏场景 -├── ECSManager.ts # ECS管理器组件 -└── README.md # 本文档 -``` - -## 🚀 快速开始 - -### 1. 启动ECS框架 - -ECS框架已经配置完成!您只需要: - -1. 在Cocos Creator中打开您的场景 -2. 创建一个空节点(例如命名为"ECSManager") -3. 将 `ECSManager` 组件添加到该节点 -4. 运行场景,ECS框架将自动启动 - -### 2. 查看控制台输出 - -如果一切正常,您将在控制台看到: - -``` -🎮 正在初始化ECS框架... -🔧 ECS调试模式已启用,可在Cocos Creator扩展面板中查看调试信息 -🎯 游戏场景已创建 -✅ ECS框架初始化成功! -🚀 游戏场景已启动 -``` - -### 3. 使用调试面板 - -ECS框架已启用调试功能,您可以: - -1. 在Cocos Creator编辑器菜单中选择 "扩展" → "ECS Framework" → "调试面板" -2. 调试面板将显示实时的ECS运行状态: - - 实体数量和状态 - - 系统执行信息 - - 性能监控数据 - - 组件统计信息 - -**注意**:调试功能会消耗一定性能,正式发布时建议关闭调试模式。 - -## 📚 下一步开发 - -### 创建您的第一个组件 - -在 `components/` 目录下创建组件: - -```typescript -// components/PositionComponent.ts -import { Component } from '@esengine/ecs-framework'; -import { Vec3 } from 'cc'; - -export class PositionComponent extends Component { - public position: Vec3 = new Vec3(); - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.position.set(x, y, z); - } -} -``` - -### 创建您的第一个系统 - -在 `systems/` 目录下创建系统: - -```typescript -// systems/MovementSystem.ts -import { EntitySystem, Entity, Matcher } from '@esengine/ecs-framework'; -import { PositionComponent } from '../components/PositionComponent'; - -export class MovementSystem extends EntitySystem { - constructor() { - super(Matcher.empty().all(PositionComponent)); - } - - protected process(entities: Entity[]): void { - for (const entity of entities) { - const position = entity.getComponent(PositionComponent); - if (position) { - // TODO: 在这里编写移动逻辑 - console.log(`实体 ${entity.name} 位置: ${position.position}`); - } - } - } -} -``` - -### 在场景中注册系统 - -在 `scenes/GameScene.ts` 的 `initialize()` 方法中添加: - -```typescript -import { MovementSystem } from '../systems/MovementSystem'; - -public initialize(): void { - super.initialize(); - this.name = "MainGameScene"; - - // 添加系统 - this.addEntityProcessor(new MovementSystem()); - - // 创建测试实体 - const testEntity = this.createEntity("TestEntity"); - testEntity.addComponent(new PositionComponent(0, 0, 0)); -} -``` - -## 🔗 学习资源 - -- [ECS框架完整文档](https://github.com/esengine/ecs-framework) -- [ECS概念详解](https://github.com/esengine/ecs-framework/blob/master/docs/concepts-explained.md) -- [新手教程](https://github.com/esengine/ecs-framework/blob/master/docs/beginner-tutorials.md) -- [组件设计指南](https://github.com/esengine/ecs-framework/blob/master/docs/component-design-guide.md) -- [系统开发指南](https://github.com/esengine/ecs-framework/blob/master/docs/system-guide.md) - -## 💡 开发提示 - -1. **组件只存储数据**:避免在组件中编写复杂逻辑 -2. **系统处理逻辑**:所有业务逻辑应该在系统中实现 -3. **使用Matcher过滤实体**:系统通过Matcher指定需要处理的实体类型 -4. **性能优化**:大量实体时考虑使用位掩码查询和组件索引 - -## ❓ 常见问题 - -### Q: 如何创建实体? -A: 在场景中使用 `this.createEntity("实体名称")` - -### Q: 如何给实体添加组件? -A: 使用 `entity.addComponent(new YourComponent())` - -### Q: 如何获取实体的组件? -A: 使用 `entity.getComponent(YourComponent)` - -### Q: 如何删除实体? -A: 使用 `entity.destroy()` 或 `this.destroyEntity(entity)` - ---- - -🎮 **开始您的ECS开发之旅吧!** - -如有问题,请查阅官方文档或提交Issue。 diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta deleted file mode 100644 index 560800f0..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/README.md.meta +++ /dev/null @@ -1,11 +0,0 @@ -{ - "ver": "1.0.1", - "importer": "text", - "imported": true, - "uuid": "ca94b460-6c6a-4f72-9ec1-ab5fcd2e0e0a", - "files": [ - ".json" - ], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components.meta deleted file mode 100644 index fde4f330..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "3c7bd2b3-6781-482c-be41-21f3dde0e2ab", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts deleted file mode 100644 index b670bb13..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts +++ /dev/null @@ -1,328 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; -import { Vec3 } from 'cc'; - -/** - * AI组件 - 复杂的AI行为和状态管理 - */ -export class AIComponent extends Component { - /** AI状态 */ - public currentState: 'idle' | 'patrol' | 'chase' | 'attack' | 'flee' | 'dead' = 'idle'; - - /** 目标ID(避免循环引用) */ - public targetId: number | null = null; - - /** AI伙伴ID列表(避免循环引用) */ - public allyIds: number[] = []; - - /** 敌人ID列表 */ - public enemyIds: number[] = []; - - /** 复杂的AI配置 */ - public config: { - personality: { - aggression: number; // 攻击性 0-1 - curiosity: number; // 好奇心 0-1 - loyalty: number; // 忠诚度 0-1 - intelligence: number; // 智力 0-1 - }; - capabilities: { - sightRange: number; - hearingRange: number; - movementSpeed: number; - attackDamage: number; - health: number; - }; - behaviorTree: { - rootNode: BehaviorNode; - blackboard: Map; - executionHistory: BehaviorExecution[]; - }; - memory: { - lastSeenEnemyPosition: Vec3 | null; - lastSeenEnemyTime: number; - knownLocations: Array<{ - position: Vec3; - type: 'safe' | 'danger' | 'resource' | 'patrol'; - confidence: number; - lastVisited: number; - }>; - relationships: Map; - }>; - }; - }; - - /** 状态机 */ - public stateMachine: { - states: Map; - transitions: Map boolean; - priority: number; - }>>; - stateHistory: Array<{ - state: string; - enterTime: number; - exitTime: number; - data: any; - }>; - }; - - /** 感知系统 */ - public perception: { - visibleEntities: Array<{ - entityId: number; - position: Vec3; - distance: number; - angle: number; - lastSeen: number; - componentId?: number; // 使用组件ID避免循环引用 - }>; - audibleSounds: Array<{ - source: Vec3; - volume: number; - type: string; - timestamp: number; - }>; - tacticleInfo: Array<{ - entityId: number; - contactPoint: Vec3; - force: number; - timestamp: number; - }>; - }; - - constructor() { - super(); - - // 初始化AI配置 - this.config = { - personality: { - aggression: Math.random(), - curiosity: Math.random(), - loyalty: Math.random(), - intelligence: Math.random() - }, - capabilities: { - sightRange: 100 + Math.random() * 100, - hearingRange: 50 + Math.random() * 50, - movementSpeed: 80 + Math.random() * 40, - attackDamage: 10 + Math.random() * 20, - health: 80 + Math.random() * 40 - }, - behaviorTree: { - rootNode: new BehaviorNode('root'), - blackboard: new Map(), - executionHistory: [] - }, - memory: { - lastSeenEnemyPosition: null, - lastSeenEnemyTime: 0, - knownLocations: [], - relationships: new Map() - } - }; - - // 初始化状态机 - this.stateMachine = { - states: new Map(), - transitions: new Map(), - stateHistory: [] - }; - - // 初始化感知系统 - this.perception = { - visibleEntities: [], - audibleSounds: [], - tacticleInfo: [] - }; - - this.initializeStateMachine(); - this.initializeBehaviorTree(); - } - - /** - * 初始化状态机 - */ - private initializeStateMachine(): void { - // 添加基本状态 - this.stateMachine.states.set('idle', new AIState('idle', this)); - this.stateMachine.states.set('patrol', new AIState('patrol', this)); - this.stateMachine.states.set('chase', new AIState('chase', this)); - this.stateMachine.states.set('attack', new AIState('attack', this)); - this.stateMachine.states.set('flee', new AIState('flee', this)); - - // 设置状态转换 - this.stateMachine.transitions.set('idle', [ - { targetState: 'patrol', condition: () => Math.random() > 0.8, priority: 1 }, - { targetState: 'chase', condition: () => this.perception.visibleEntities.length > 0, priority: 3 } - ]); - - this.stateMachine.transitions.set('patrol', [ - { targetState: 'idle', condition: () => Math.random() > 0.9, priority: 1 }, - { targetState: 'chase', condition: () => this.hasVisibleEnemies(), priority: 3 } - ]); - } - - /** - * 初始化行为树 - */ - private initializeBehaviorTree(): void { - const root = this.config.behaviorTree.rootNode; - - // 构建简单的行为树结构 - const selectorNode = new BehaviorNode('selector'); - const sequenceNode = new BehaviorNode('sequence'); - const conditionNode = new BehaviorNode('condition'); - const actionNode = new BehaviorNode('action'); - - root.addChild(selectorNode); - selectorNode.addChild(sequenceNode); - sequenceNode.addChild(conditionNode); - sequenceNode.addChild(actionNode); - - // 设置黑板数据 - this.config.behaviorTree.blackboard.set('lastPatrolPoint', new Vec3()); - this.config.behaviorTree.blackboard.set('alertLevel', 0); - this.config.behaviorTree.blackboard.set('energy', 100); - } - - /** - * 添加盟友(避免循环引用) - */ - public addAlly(allyEntityId: number): void { - if (!this.allyIds.includes(allyEntityId)) { - this.allyIds.push(allyEntityId); - - // 更新关系记录 - this.config.memory.relationships.set(allyEntityId, { - entityId: allyEntityId, - relationship: 'ally', - trustLevel: 0.8, - lastInteraction: Date.now(), - interactionHistory: [] - }); - } - } - - /** - * 设置目标(避免循环引用) - */ - public setTarget(targetEntityId: number): void { - this.targetId = targetEntityId; - // 不再需要双向引用 - } - - /** - * 更新感知信息 - */ - public updatePerception(deltaTime: number): void { - // 清理过期的感知信息 - const currentTime = Date.now(); - this.perception.visibleEntities = this.perception.visibleEntities.filter( - entity => currentTime - entity.lastSeen < 5000 - ); - - this.perception.audibleSounds = this.perception.audibleSounds.filter( - sound => currentTime - sound.timestamp < 2000 - ); - - // 更新记忆中的位置信息 - this.config.memory.knownLocations.forEach(location => { - location.confidence *= 0.999; // 随时间衰减可信度 - }); - } - - /** - * 检查是否有可见敌人 - */ - private hasVisibleEnemies(): boolean { - return this.perception.visibleEntities.some(entity => - this.config.memory.relationships.get(entity.entityId)?.relationship === 'enemy' - ); - } - - /** - * 重置组件 - */ - public reset(): void { - // 清理ID数组(不再需要处理循环引用) - this.allyIds = []; - this.enemyIds = []; - this.targetId = null; - this.currentState = 'idle'; - - this.config.behaviorTree.blackboard.clear(); - this.config.memory.relationships.clear(); - this.perception.visibleEntities = []; - this.perception.audibleSounds = []; - this.perception.tacticleInfo = []; - } -} - -/** - * 行为树节点 - */ -class BehaviorNode { - public name: string; - public children: BehaviorNode[] = []; - public parent: BehaviorNode | null = null; - public data: Map = new Map(); - - constructor(name: string) { - this.name = name; - } - - public addChild(child: BehaviorNode): void { - this.children.push(child); - child.parent = this; - } -} - -/** - * 行为执行记录 - */ -interface BehaviorExecution { - nodeName: string; - startTime: number; - endTime: number; - result: 'success' | 'failure' | 'running'; - data: any; -} - -/** - * AI状态 - */ -class AIState { - public name: string; - public owner: AIComponent; - public enterTime: number = 0; - public data: Map = new Map(); - - constructor(name: string, owner: AIComponent) { - this.name = name; - this.owner = owner; - } - - public enter(): void { - this.enterTime = Date.now(); - } - - public exit(): void { - // 记录状态历史 - this.owner.stateMachine.stateHistory.push({ - state: this.name, - enterTime: this.enterTime, - exitTime: Date.now(), - data: Object.fromEntries(this.data) - }); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts.meta deleted file mode 100644 index 86f2d7b7..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/AIComponent.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "cc0d3d0d-0c12-4007-8568-11b2cafdfb8f", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts deleted file mode 100644 index 6f0df12c..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -/** - * 生命值组件 - * 管理实体的生命值、最大生命值等 - */ -export class Health extends Component { - /** 当前生命值 */ - public currentHealth: number = 100; - - /** 最大生命值 */ - public maxHealth: number = 100; - - /** 是否死亡 */ - public isDead: boolean = false; - - /** 生命值回复速度 (每秒) */ - public regenRate: number = 0; - - constructor(maxHealth: number = 100) { - super(); - this.maxHealth = maxHealth; - this.currentHealth = maxHealth; - } - - /** - * 受到伤害 - */ - public takeDamage(damage: number): void { - this.currentHealth = Math.max(0, this.currentHealth - damage); - this.isDead = this.currentHealth <= 0; - } - - /** - * 治疗 - */ - public heal(amount: number): void { - if (!this.isDead) { - this.currentHealth = Math.min(this.maxHealth, this.currentHealth + amount); - } - } - - /** - * 复活 - */ - public revive(healthPercent: number = 1.0): void { - this.isDead = false; - this.currentHealth = Math.floor(this.maxHealth * Math.max(0, Math.min(1, healthPercent))); - } - - /** - * 获取生命值百分比 - */ - public getHealthPercent(): number { - return this.maxHealth > 0 ? this.currentHealth / this.maxHealth : 0; - } - - /** - * 重置组件 - */ - public reset(): void { - this.currentHealth = this.maxHealth; - this.isDead = false; - this.regenRate = 0; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts.meta deleted file mode 100644 index 4f880cff..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Health.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "90369635-a6cb-4313-adf1-64117b50f2bc", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts deleted file mode 100644 index afb9e2e3..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; - -/** - * 网络组件 - 模拟复杂的网络连接和数据同步(已移除循环引用) - */ -export class NetworkComponent extends Component { - /** 网络ID */ - public networkId: string = ''; - - /** 连接状态 */ - public connectionState: 'disconnected' | 'connecting' | 'connected' | 'error' = 'disconnected'; - - /** 网络连接信息 */ - public connection: { - sessionId: string; - serverId: string; - roomId: string; - playerId: string; - ping: number; - packetLoss: number; - bandwidth: number; - lastHeartbeat: number; - }; - - /** 同步数据 */ - public syncData: { - dirtyFlags: Set; - lastSyncTime: number; - syncHistory: Array<{ - timestamp: number; - dataSize: number; - properties: string[]; - success: boolean; - }>; - queuedUpdates: Array<{ - property: string; - value: any; - timestamp: number; - priority: number; - }>; - }; - - /** 网络统计 */ - public networkStats: { - totalBytesSent: number; - totalBytesReceived: number; - packetsPerSecond: number; - averageLatency: number; - latencyHistory: number[]; - connectionQuality: 'excellent' | 'good' | 'fair' | 'poor'; - errorCount: number; - reconnectCount: number; - lastErrorTime: number; - errorLog: Array<{ - timestamp: number; - errorType: string; - message: string; - stack?: string; - }>; - }; - - /** 连接的玩家ID列表(避免循环引用) */ - public connectedPlayerIds: Set = new Set(); - - /** 群组成员ID(避免循环引用) */ - public groupMemberIds: string[] = []; - - /** 群组领导者ID(避免循环引用) */ - public groupLeaderId: string | null = null; - - /** 复杂的网络配置 */ - public config: { - autoReconnect: boolean; - maxReconnectAttempts: number; - heartbeatInterval: number; - syncFrequency: number; - compressionEnabled: boolean; - encryptionEnabled: boolean; - priorityLevels: Map; - filters: Array<{ - property: string; - condition: (value: any) => boolean; - action: 'allow' | 'deny' | 'transform'; - transformer?: (value: any) => any; - }>; - bufferSettings: { - maxBufferSize: number; - flushInterval: number; - compressionThreshold: number; - }; - }; - - /** 消息队列 */ - public messageQueue: { - incoming: Array<{ - senderId: string; - messageType: string; - data: any; - timestamp: number; - processed: boolean; - }>; - outgoing: Array<{ - targetId: string; - messageType: string; - data: any; - priority: number; - attempts: number; - maxAttempts: number; - }>; - processing: Map; - }; - - /** 复杂的网络缓存系统 */ - public cacheSystem: { - playerCache: Map; - messageCache: Map; - syncCache: Map; - }; - - constructor(networkId: string = '') { - super(); - - this.networkId = networkId || this.generateNetworkId(); - - this.connection = { - sessionId: '', - serverId: '', - roomId: '', - playerId: '', - ping: 0, - packetLoss: 0, - bandwidth: 0, - lastHeartbeat: 0 - }; - - this.syncData = { - dirtyFlags: new Set(), - lastSyncTime: 0, - syncHistory: [], - queuedUpdates: [] - }; - - this.networkStats = { - totalBytesSent: 0, - totalBytesReceived: 0, - packetsPerSecond: 0, - averageLatency: 0, - latencyHistory: [], - connectionQuality: 'excellent', - errorCount: 0, - reconnectCount: 0, - lastErrorTime: 0, - errorLog: [] - }; - - this.config = { - autoReconnect: true, - maxReconnectAttempts: 5, - heartbeatInterval: 1000, - syncFrequency: 60, - compressionEnabled: true, - encryptionEnabled: false, - priorityLevels: new Map([ - ['critical', 10], - ['high', 7], - ['medium', 5], - ['low', 2] - ]), - filters: [], - bufferSettings: { - maxBufferSize: 1024 * 1024, // 1MB - flushInterval: 100, - compressionThreshold: 1024 - } - }; - - this.messageQueue = { - incoming: [], - outgoing: [], - processing: new Map() - }; - - this.cacheSystem = { - playerCache: new Map(), - messageCache: new Map(), - syncCache: new Map() - }; - } - - /** - * 生成网络ID - */ - private generateNetworkId(): string { - return 'net_' + Math.random().toString(36).substring(2, 15) + - Math.random().toString(36).substring(2, 15); - } - - /** - * 连接到其他网络组件(避免循环引用) - */ - public connectToPlayer(playerNetworkId: string): void { - if (!this.connectedPlayerIds.has(playerNetworkId)) { - this.connectedPlayerIds.add(playerNetworkId); - - // 记录连接事件 - this.logNetworkEvent('player_connected', { - playerId: playerNetworkId, - timestamp: Date.now() - }); - } - } - - /** - * 加入群组(避免循环引用) - */ - public joinGroup(memberIds: string[], leaderId?: string): void { - this.groupMemberIds = [...memberIds]; - this.groupLeaderId = leaderId || null; - - // 更新缓存 - memberIds.forEach(memberId => { - this.cacheSystem.playerCache.set(memberId, { - playerId: memberId, - lastSeen: Date.now(), - cachedData: {}, - cacheExpiry: Date.now() + 300000 // 5分钟缓存 - }); - }); - } - - /** - * 发送消息 - */ - public sendMessage(targetId: string, messageType: string, data: any, priority: number = 5): void { - const message = { - targetId, - messageType, - data: this.processOutgoingData(data), - priority, - attempts: 0, - maxAttempts: 3 - }; - - this.messageQueue.outgoing.push(message); - this.messageQueue.outgoing.sort((a, b) => b.priority - a.priority); - - // 更新统计 - this.networkStats.totalBytesSent += JSON.stringify(data).length; - } - - /** - * 处理传出数据 - */ - private processOutgoingData(data: any): any { - let processedData = data; - - // 应用过滤器 - this.config.filters.forEach(filter => { - if (filter.condition(processedData)) { - if (filter.action === 'transform' && filter.transformer) { - processedData = filter.transformer(processedData); - } - } - }); - - // 压缩数据 - if (this.config.compressionEnabled) { - processedData = this.compressData(processedData); - } - - return processedData; - } - - /** - * 压缩数据(模拟) - */ - private compressData(data: any): any { - // 模拟压缩算法 - const serialized = JSON.stringify(data); - if (serialized.length > this.config.bufferSettings.compressionThreshold) { - // 模拟压缩 - return { - compressed: true, - originalSize: serialized.length, - compressedSize: Math.floor(serialized.length * 0.6), - data: serialized.substring(0, Math.floor(serialized.length * 0.6)) - }; - } - return data; - } - - /** - * 标记属性为脏 - */ - public markDirty(property: string): void { - this.syncData.dirtyFlags.add(property); - } - - /** - * 更新网络统计 - */ - public updateNetworkStats(deltaTime: number): void { - // 更新延迟历史 - if (this.networkStats.latencyHistory.length > 100) { - this.networkStats.latencyHistory.shift(); - } - this.networkStats.latencyHistory.push(this.connection.ping); - - // 计算平均延迟 - this.networkStats.averageLatency = this.networkStats.latencyHistory.reduce((a, b) => a + b, 0) / - this.networkStats.latencyHistory.length; - - // 更新连接质量 - if (this.networkStats.averageLatency < 50) { - this.networkStats.connectionQuality = 'excellent'; - } else if (this.networkStats.averageLatency < 100) { - this.networkStats.connectionQuality = 'good'; - } else if (this.networkStats.averageLatency < 200) { - this.networkStats.connectionQuality = 'fair'; - } else { - this.networkStats.connectionQuality = 'poor'; - } - - // 更新包率 - this.networkStats.packetsPerSecond = this.messageQueue.outgoing.length / deltaTime; - - // 清理过期缓存 - this.cleanupExpiredCache(); - } - - /** - * 清理过期缓存 - */ - private cleanupExpiredCache(): void { - const now = Date.now(); - - // 清理玩家缓存 - for (const [key, value] of this.cacheSystem.playerCache) { - if (value.cacheExpiry < now) { - this.cacheSystem.playerCache.delete(key); - } - } - - // 清理消息缓存 - for (const [key, value] of this.cacheSystem.messageCache) { - if (value.timestamp < now - 600000) { // 10分钟过期 - this.cacheSystem.messageCache.delete(key); - } - } - } - - /** - * 记录网络事件 - */ - private logNetworkEvent(eventType: string, data: any): void { - this.networkStats.errorLog.push({ - timestamp: Date.now(), - errorType: eventType, - message: JSON.stringify(data) - }); - - // 限制日志大小 - if (this.networkStats.errorLog.length > 1000) { - this.networkStats.errorLog = this.networkStats.errorLog.slice(-500); - } - } - - /** - * 重置组件 - */ - public reset(): void { - // 清理ID列表(不再需要处理循环引用) - this.connectedPlayerIds.clear(); - this.groupMemberIds = []; - this.groupLeaderId = null; - this.connectionState = 'disconnected'; - - this.syncData.dirtyFlags.clear(); - this.syncData.syncHistory = []; - this.syncData.queuedUpdates = []; - - this.messageQueue.incoming = []; - this.messageQueue.outgoing = []; - this.messageQueue.processing.clear(); - - this.cacheSystem.playerCache.clear(); - this.cacheSystem.messageCache.clear(); - this.cacheSystem.syncCache.clear(); - - this.networkStats.errorLog = []; - this.networkStats.latencyHistory = []; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts.meta deleted file mode 100644 index 30ab9b64..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NetworkComponent.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "d9263549-7b26-4b4f-9a15-b82e7af5fbd5", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts deleted file mode 100644 index 0ec49c31..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts +++ /dev/null @@ -1,346 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; -import { Node, Vec3, Color, Sprite, Label } from 'cc'; - -/** - * Node组件 - 包含Cocos Creator节点引用(已移除循环引用) - */ -export class NodeComponent extends Component { - /** Cocos Creator节点引用 */ - public node: Node | null = null; - - /** 子节点列表 */ - public children: Node[] = []; - - /** 节点配置信息 */ - public nodeConfig: { - name: string; - layer: number; - tag: string; - userData: Record; - transformData: { - position: Vec3; - rotation: Vec3; - scale: Vec3; - }; - renderData: { - color: Color; - opacity: number; - visible: boolean; - }; - parentId: number | null; // 避免循环引用:使用父节点实体ID - childIds: number[]; // 避免循环引用:使用子节点实体ID列表 - }; - - /** 渲染组件引用 */ - public sprite: Sprite | null = null; - public label: Label | null = null; - - /** 复杂嵌套对象 */ - public complexData: { - statistics: { - frameCount: number; - lastUpdateTime: number; - performance: { - avgRenderTime: number; - maxRenderTime: number; - renderHistory: number[]; - }; - }; - cache: { - textureCache: Map; - materialCache: Map; - shaderCache: Map; - }; - hierarchy: { - parentId: number | null; // 避免循环引用:使用ID - rootId: number | null; // 避免循环引用:使用ID - depth: number; - siblingIndex: number; - }; - animation: { - isPlaying: boolean; - currentFrame: number; - totalFrames: number; - loopCount: number; - animationQueue: Array<{ - name: string; - duration: number; - delay: number; - easing: string; - }>; - }; - interaction: { - isInteractable: boolean; - touchEnabled: boolean; - hitTestResults: Array<{ - position: Vec3; - timestamp: number; - result: boolean; - }>; - boundingBox: { - min: Vec3; - max: Vec3; - center: Vec3; - }; - }; - }; - - /** 复杂的渲染状态 */ - public renderState: { - layerInfo: { - currentLayer: number; - layerStack: number[]; - sortingOrder: number; - cullingMask: number; - }; - materials: Array<{ - materialId: string; - properties: Map; - textures: Map; - shaderParams: Record; - }>; - lightingData: { - ambientColor: Color; - diffuseColor: Color; - specularColor: Color; - lightDirection: Vec3; - shadowData: { - castShadows: boolean; - receiveShadows: boolean; - shadowQuality: 'low' | 'medium' | 'high'; - shadowDistance: number; - }; - }; - }; - - constructor(name: string = "DefaultNode") { - super(); - - this.nodeConfig = { - name: name, - layer: 0, - tag: "default", - userData: {}, - transformData: { - position: new Vec3(), - rotation: new Vec3(), - scale: new Vec3(1, 1, 1) - }, - renderData: { - color: new Color(255, 255, 255, 255), - opacity: 1.0, - visible: true - }, - parentId: null, - childIds: [] - }; - - this.complexData = { - statistics: { - frameCount: 0, - lastUpdateTime: 0, - performance: { - avgRenderTime: 0, - maxRenderTime: 0, - renderHistory: [] - } - }, - cache: { - textureCache: new Map(), - materialCache: new Map(), - shaderCache: new Map() - }, - hierarchy: { - parentId: null, - rootId: null, - depth: 0, - siblingIndex: 0 - }, - animation: { - isPlaying: false, - currentFrame: 0, - totalFrames: 60, - loopCount: 0, - animationQueue: [] - }, - interaction: { - isInteractable: true, - touchEnabled: true, - hitTestResults: [], - boundingBox: { - min: new Vec3(-1, -1, -1), - max: new Vec3(1, 1, 1), - center: new Vec3() - } - } - }; - - this.renderState = { - layerInfo: { - currentLayer: 0, - layerStack: [0], - sortingOrder: 0, - cullingMask: 0xFFFFFFFF - }, - materials: [], - lightingData: { - ambientColor: new Color(128, 128, 128, 255), - diffuseColor: new Color(255, 255, 255, 255), - specularColor: new Color(255, 255, 255, 255), - lightDirection: new Vec3(0, -1, 0), - shadowData: { - castShadows: true, - receiveShadows: true, - shadowQuality: 'medium', - shadowDistance: 100 - } - } - }; - } - - /** - * 设置父节点组件(避免循环引用) - */ - public setParent(parentEntityId: number): void { - this.nodeConfig.parentId = parentEntityId; - this.complexData.hierarchy.parentId = parentEntityId; - // 深度需要通过其他方式计算,避免引用 - } - - /** - * 添加子节点 - */ - public addChild(childEntityId: number): void { - if (!this.nodeConfig.childIds.includes(childEntityId)) { - this.nodeConfig.childIds.push(childEntityId); - } - } - - /** - * 更新性能统计 - */ - public updatePerformance(renderTime: number): void { - this.complexData.statistics.frameCount++; - this.complexData.statistics.lastUpdateTime = Date.now(); - - const perf = this.complexData.statistics.performance; - perf.renderHistory.push(renderTime); - - // 保持历史记录在合理范围内 - if (perf.renderHistory.length > 100) { - perf.renderHistory.shift(); - } - - // 计算平均值和最大值 - perf.avgRenderTime = perf.renderHistory.reduce((a, b) => a + b, 0) / perf.renderHistory.length; - perf.maxRenderTime = Math.max(perf.maxRenderTime, renderTime); - } - - /** - * 更新动画状态 - */ - public updateAnimation(deltaTime: number): void { - if (this.complexData.animation.isPlaying) { - this.complexData.animation.currentFrame++; - - if (this.complexData.animation.currentFrame >= this.complexData.animation.totalFrames) { - this.complexData.animation.currentFrame = 0; - this.complexData.animation.loopCount++; - - // 处理动画队列 - if (this.complexData.animation.animationQueue.length > 0) { - const nextAnim = this.complexData.animation.animationQueue.shift(); - if (nextAnim) { - this.complexData.animation.totalFrames = Math.floor(nextAnim.duration * 60); // 假设60FPS - } - } - } - } - } - - /** - * 添加材质 - */ - public addMaterial(materialId: string, properties: Record): void { - this.renderState.materials.push({ - materialId, - properties: new Map(Object.entries(properties)), - textures: new Map(), - shaderParams: {} - }); - } - - /** - * 更新包围盒 - */ - public updateBoundingBox(): void { - if (this.node) { - const worldPos = this.node.getWorldPosition(); - const scale = this.node.getScale(); - - this.complexData.interaction.boundingBox.center = new Vec3(worldPos.x, worldPos.y, worldPos.z); - this.complexData.interaction.boundingBox.min = new Vec3( - worldPos.x - scale.x * 0.5, - worldPos.y - scale.y * 0.5, - worldPos.z - scale.z * 0.5 - ); - this.complexData.interaction.boundingBox.max = new Vec3( - worldPos.x + scale.x * 0.5, - worldPos.y + scale.y * 0.5, - worldPos.z + scale.z * 0.5 - ); - } - } - - /** - * 执行点击测试 - */ - public hitTest(point: Vec3): boolean { - const bbox = this.complexData.interaction.boundingBox; - const result = point.x >= bbox.min.x && point.x <= bbox.max.x && - point.y >= bbox.min.y && point.y <= bbox.max.y && - point.z >= bbox.min.z && point.z <= bbox.max.z; - - // 记录测试结果 - this.complexData.interaction.hitTestResults.push({ - position: new Vec3(point.x, point.y, point.z), - timestamp: Date.now(), - result - }); - - // 限制历史记录大小 - if (this.complexData.interaction.hitTestResults.length > 50) { - this.complexData.interaction.hitTestResults.shift(); - } - - return result; - } - - /** - * 重置组件 - */ - public reset(): void { - this.node = null; - this.children = []; - this.sprite = null; - this.label = null; - - // 清理ID列表(不再需要处理循环引用) - this.nodeConfig.parentId = null; - this.nodeConfig.childIds = []; - this.complexData.hierarchy.parentId = null; - this.complexData.hierarchy.rootId = null; - this.complexData.hierarchy.depth = 0; - - this.complexData.cache.textureCache.clear(); - this.complexData.cache.materialCache.clear(); - this.complexData.cache.shaderCache.clear(); - - this.complexData.animation.isPlaying = false; - this.complexData.animation.currentFrame = 0; - this.complexData.animation.animationQueue = []; - - this.complexData.interaction.hitTestResults = []; - this.renderState.materials = []; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts.meta deleted file mode 100644 index ecd695dc..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/NodeComponent.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "28e7e8cd-591e-4fde-bb14-d668724a6201", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts deleted file mode 100644 index b478eebb..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; -import { Color } from 'cc'; - -/** - * 渲染组件 - * 存储实体的渲染相关信息 - */ -export class Renderer extends Component { - /** 颜色 */ - public color: Color = new Color(255, 255, 255, 255); - - /** 是否可见 */ - public visible: boolean = true; - - /** 渲染层级 */ - public layer: number = 0; - - /** 精灵名称或纹理路径 */ - public spriteName: string = ''; - - /** 大小 */ - public size: { width: number, height: number } = { width: 32, height: 32 }; - - /** 透明度 (0-1) */ - public alpha: number = 1.0; - - constructor(spriteName: string = '', color?: Color) { - super(); - this.spriteName = spriteName; - if (color) { - this.color = color; - } - } - - /** - * 设置颜色 - */ - public setColor(r: number, g: number, b: number, a: number = 255): void { - this.color.set(r, g, b, a); - } - - /** - * 设置透明度 - */ - public setAlpha(alpha: number): void { - this.alpha = Math.max(0, Math.min(1, alpha)); - this.color.a = Math.floor(this.alpha * 255); - } - - /** - * 设置大小 - */ - public setSize(width: number, height: number): void { - this.size.width = width; - this.size.height = height; - } - - /** - * 显示/隐藏 - */ - public setVisible(visible: boolean): void { - this.visible = visible; - } - - /** - * 重置组件 - */ - public reset(): void { - this.color.set(255, 255, 255, 255); - this.visible = true; - this.layer = 0; - this.spriteName = ''; - this.size = { width: 32, height: 32 }; - this.alpha = 1.0; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts.meta deleted file mode 100644 index 0b07f624..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Renderer.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "bf51f134-6ea5-4a26-8b15-0ed20fe1d605", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts deleted file mode 100644 index 88b802b2..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; -import { Vec3 } from 'cc'; - -/** - * 变换组件 - * 存储实体的位置、旋转和缩放信息 - */ -export class Transform extends Component { - /** 位置 */ - public position: Vec3 = new Vec3(0, 0, 0); - - /** 旋转 (度数) */ - public rotation: Vec3 = new Vec3(0, 0, 0); - - /** 缩放 */ - public scale: Vec3 = new Vec3(1, 1, 1); - - /** 移动速度 */ - public speed: number = 100; - - constructor() { - super(); - } - - /** - * 设置位置 - */ - public setPosition(x: number, y: number, z: number = 0): void { - this.position.set(x, y, z); - } - - /** - * 移动 - */ - public move(deltaX: number, deltaY: number, deltaZ: number = 0): void { - this.position.x += deltaX; - this.position.y += deltaY; - this.position.z += deltaZ; - } - - /** - * 重置组件 - */ - public reset(): void { - this.position.set(0, 0, 0); - this.rotation.set(0, 0, 0); - this.scale.set(1, 1, 1); - this.speed = 100; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts.meta deleted file mode 100644 index 1089261d..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Transform.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "09de6e5b-7bb7-4de8-8038-67be5ae955bc", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts deleted file mode 100644 index adecf0c9..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Component } from '@esengine/ecs-framework'; -import { Vec3 } from 'cc'; - -/** - * 速度组件 - * 存储实体的移动速度和方向 - */ -export class Velocity extends Component { - /** 速度向量 */ - public velocity: Vec3 = new Vec3(0, 0, 0); - - /** 最大速度 */ - public maxSpeed: number = 200; - - /** 摩擦力 (0-1, 1表示无摩擦) */ - public friction: number = 0.98; - - constructor() { - super(); - } - - /** - * 设置速度 - */ - public setVelocity(x: number, y: number, z: number = 0): void { - this.velocity.set(x, y, z); - this.clampToMaxSpeed(); - } - - /** - * 添加速度 - */ - public addVelocity(x: number, y: number, z: number = 0): void { - this.velocity.x += x; - this.velocity.y += y; - this.velocity.z += z; - this.clampToMaxSpeed(); - } - - /** - * 应用摩擦力 - */ - public applyFriction(): void { - this.velocity.multiplyScalar(this.friction); - - // 当速度很小时直接设为0,避免无限减小 - if (this.velocity.length() < 0.1) { - this.velocity.set(0, 0, 0); - } - } - - /** - * 限制到最大速度 - */ - private clampToMaxSpeed(): void { - const currentSpeed = this.velocity.length(); - if (currentSpeed > this.maxSpeed) { - this.velocity.normalize().multiplyScalar(this.maxSpeed); - } - } - - /** - * 获取当前速度大小 - */ - public getSpeed(): number { - return this.velocity.length(); - } - - /** - * 重置组件 - */ - public reset(): void { - this.velocity.set(0, 0, 0); - this.maxSpeed = 200; - this.friction = 0.98; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts.meta deleted file mode 100644 index d8a31d1a..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/Velocity.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "10c40371-267a-4016-a8b5-9f803e68e72b", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts deleted file mode 100644 index f53f9631..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 导出所有组件 -export { Transform } from './Transform'; -export { Health } from './Health'; -export { Velocity } from './Velocity'; -export { Renderer } from './Renderer'; -export { NodeComponent } from './NodeComponent'; -export { AIComponent } from './AIComponent'; -export { NetworkComponent } from './NetworkComponent'; \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts.meta deleted file mode 100644 index ec7bc924..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/components/index.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "dfc46d23-7ad6-4a21-914c-35d948185f93", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes.meta deleted file mode 100644 index f906de6e..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "39da4804-e61e-440e-b73d-544963bd3901", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts deleted file mode 100644 index 9e5079ea..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { Scene } from '@esengine/ecs-framework'; -import { Color, Node } from 'cc'; -import { MovementSystem, HealthSystem, RandomMovementSystem, AISystem, NetworkSystem, NodeRenderSystem } from '../systems'; -import { Transform, Health, Velocity, Renderer, NodeComponent, AIComponent, NetworkComponent } from '../components'; - -/** - * 游戏场景 - * - * 这是您的主游戏场景。在这里可以: - * - 添加游戏系统 - * - 创建初始实体 - * - 设置场景参数 - */ -export class GameScene extends Scene { - - /** - * 场景初始化 - * 在场景创建时调用,用于设置基础配置 - */ - public initialize(): void { - super.initialize(); - - // 设置场景名称 - this.name = "MainGameScene"; - - console.log('🎯 游戏场景已创建'); - - // 添加游戏系统 - this.addEntityProcessor(new MovementSystem()); - this.addEntityProcessor(new HealthSystem()); - this.addEntityProcessor(new RandomMovementSystem()); - // this.addEntityProcessor(new AISystem()); - // this.addEntityProcessor(new NetworkSystem()); - // this.addEntityProcessor(new NodeRenderSystem()); - - // 创建大量复杂的测试实体 - this.createComplexTestEntities(); - } - - /** - * 创建复杂的测试实体(1000+个) - */ - private createComplexTestEntities(): void { - console.log('🚀 开始创建大量复杂测试实体...'); - - // 存储创建的AI和网络组件用于建立循环引用 - const aiComponents: AIComponent[] = []; - const networkComponents: NetworkComponent[] = []; - const nodeComponents: NodeComponent[] = []; - - // 1. 创建玩家实体(具有所有组件类型) - console.log('创建玩家实体...'); - const player = this.createComplexEntity("Player", "player", new Color(0, 255, 0, 255), 0, 0, true, true, true); - if (player) { - const playerAI = player.getComponent(AIComponent); - const playerNetwork = player.getComponent(NetworkComponent); - const playerNode = player.getComponent(NodeComponent); - - if (playerAI) aiComponents.push(playerAI); - if (playerNetwork) networkComponents.push(playerNetwork); - if (playerNode) nodeComponents.push(playerNode); - } - - // 2. 创建AI智能体(500个) - console.log('创建AI智能体...'); - for (let i = 0; i < 500; i++) { - const entityName = `AI_Agent_${i}`; - const x = (Math.random() - 0.5) * 2000; - const y = (Math.random() - 0.5) * 2000; - const color = new Color( - Math.floor(Math.random() * 255), - Math.floor(Math.random() * 255), - Math.floor(Math.random() * 255), - 255 - ); - - const entity = this.createComplexEntity(entityName, "ai_agent", color, x, y, true, true, Math.random() > 0.5); - - if (entity) { - const ai = entity.getComponent(AIComponent); - const network = entity.getComponent(NetworkComponent); - const node = entity.getComponent(NodeComponent); - - if (ai) { - aiComponents.push(ai); - // 设置随机AI个性 - ai.config.personality.aggression = Math.random(); - ai.config.personality.curiosity = Math.random(); - ai.config.personality.loyalty = Math.random(); - ai.config.personality.intelligence = Math.random(); - } - - if (network) { - networkComponents.push(network); - // 随机设置网络状态 - if (Math.random() > 0.2) { - network.connectionState = 'connected'; - } - } - - if (node) { - nodeComponents.push(node); - // 设置随机节点属性 - node.nodeConfig.layer = Math.floor(Math.random() * 10); - node.nodeConfig.tag = `layer_${node.nodeConfig.layer}`; - } - } - } - - // 3. 创建网络节点(300个) - console.log('创建网络节点...'); - for (let i = 0; i < 300; i++) { - const entityName = `Network_Node_${i}`; - const x = (Math.random() - 0.5) * 1500; - const y = (Math.random() - 0.5) * 1500; - const color = new Color(0, 150, 255, 200); - - const entity = this.createComplexEntity(entityName, "network_node", color, x, y, false, true, true); - - if (entity) { - const network = entity.getComponent(NetworkComponent); - const node = entity.getComponent(NodeComponent); - - if (network) { - networkComponents.push(network); - network.connectionState = 'connected'; - network.config.syncFrequency = 30 + Math.random() * 30; // 30-60Hz - } - - if (node) { - nodeComponents.push(node); - // 创建复杂的层次结构 - node.nodeConfig.layer = Math.floor(i / 10); // 每10个一层 - } - } - } - - // 4. 创建简单移动实体(200个) - console.log('创建简单移动实体...'); - for (let i = 0; i < 200; i++) { - const entityName = `Simple_Mover_${i}`; - const x = (Math.random() - 0.5) * 1000; - const y = (Math.random() - 0.5) * 1000; - const color = new Color(255, 255, 255, 150); - - this.createComplexEntity(entityName, "simple_mover", color, x, y, false, false, false); - } - - // 5. 建立循环引用和复杂关系 - console.log('建立实体间的复杂关系...'); - this.establishComplexRelationships(aiComponents, networkComponents, nodeComponents); - - const totalEntities = this.entities.count; - console.log(`✅ 创建完成!总共创建了 ${totalEntities} 个实体`); - console.log(` - AI组件: ${aiComponents.length} 个`); - console.log(` - 网络组件: ${networkComponents.length} 个`); - console.log(` - 节点组件: ${nodeComponents.length} 个`); - } - - /** - * 创建复杂实体的辅助方法 - */ - private createComplexEntity( - name: string, - type: string, - color: Color, - x: number, - y: number, - hasAI: boolean, - hasNetwork: boolean, - hasNode: boolean - ): any { - const entity = this.createEntity(name); - - // 基础组件 - const transform = new Transform(); - transform.setPosition(x, y); - transform.speed = 50 + Math.random() * 100; - entity.addComponent(transform); - - const health = new Health(50 + Math.random() * 100); - health.regenRate = Math.random() * 10; - entity.addComponent(health); - - const velocity = new Velocity(); - velocity.maxSpeed = 80 + Math.random() * 120; - velocity.friction = 0.95 + Math.random() * 0.04; - entity.addComponent(velocity); - - const renderer = new Renderer(type, color.clone()); - renderer.alpha = 0.5 + Math.random() * 0.5; - renderer.layer = Math.floor(Math.random() * 5); - entity.addComponent(renderer); - - // 复杂组件 - if (hasAI) { - const ai = new AIComponent(); - entity.addComponent(ai); - } - - if (hasNetwork) { - const network = new NetworkComponent(`${type}_${name}`); - entity.addComponent(network); - } - - if (hasNode) { - const node = new NodeComponent(name); - entity.addComponent(node); - } - - return entity; - } - - /** - * 建立实体间的复杂关系 - */ - private establishComplexRelationships( - aiComponents: AIComponent[], - networkComponents: NetworkComponent[], - nodeComponents: NodeComponent[] - ): void { - // 建立AI之间的盟友/敌人关系(避免循环引用) - for (let i = 0; i < Math.min(aiComponents.length, 100); i++) { - const ai = aiComponents[i]; - - // 随机添加盟友(使用实体ID) - const allyCount = Math.floor(Math.random() * 5); - for (let j = 0; j < allyCount; j++) { - const randomIndex = Math.floor(Math.random() * aiComponents.length); - const ally = aiComponents[randomIndex]; - if (ally !== ai && !ai.allyIds.includes(ally.entity.id)) { - ai.addAlly(ally.entity.id); - } - } - - // 随机设置目标(使用实体ID) - if (Math.random() > 0.7) { - const randomIndex = Math.floor(Math.random() * aiComponents.length); - const target = aiComponents[randomIndex]; - if (target !== ai) { - ai.setTarget(target.entity.id); - } - } - } - - // 建立网络连接(避免循环引用) - for (let i = 0; i < Math.min(networkComponents.length, 50); i++) { - const network = networkComponents[i]; - - // 连接到其他网络组件(使用网络ID) - const connectionCount = Math.floor(Math.random() * 8); - for (let j = 0; j < connectionCount; j++) { - const randomIndex = Math.floor(Math.random() * networkComponents.length); - const other = networkComponents[randomIndex]; - if (other !== network) { - network.connectToPlayer(other.networkId); - } - } - - // 创建群组(使用网络ID) - if (Math.random() > 0.8) { - const groupSize = Math.floor(Math.random() * 10) + 2; - const groupMemberIds: string[] = [network.networkId]; - - for (let k = 0; k < groupSize - 1; k++) { - const randomIndex = Math.floor(Math.random() * networkComponents.length); - const member = networkComponents[randomIndex]; - if (!groupMemberIds.includes(member.networkId)) { - groupMemberIds.push(member.networkId); - } - } - - network.joinGroup(groupMemberIds, network.networkId); - } - } - - // 建立节点层次结构(避免循环引用) - for (let i = 0; i < Math.min(nodeComponents.length, 30); i++) { - const parent = nodeComponents[i]; - - // 添加一些子节点(使用实体ID) - const childCount = Math.floor(Math.random() * 5); - for (let j = 0; j < childCount; j++) { - const childIndex = Math.floor(Math.random() * nodeComponents.length); - const child = nodeComponents[childIndex]; - - if (child !== parent && !parent.nodeConfig.childIds.includes(child.entity.id)) { - parent.addChild(child.entity.id); - } - } - } - - console.log('🔗 复杂关系建立完成!'); - } - - /** - * 场景开始运行 - * 在场景开始时调用,用于执行启动逻辑 - */ - public onStart(): void { - super.onStart(); - - console.log('🚀 游戏场景已启动'); - - // TODO: 在这里添加场景启动逻辑 - // 例如:创建UI、播放音乐、初始化游戏状态等 - } - - /** - * 场景卸载 - * 在场景结束时调用,用于清理资源 - */ - public unload(): void { - console.log('🛑 游戏场景已结束'); - - // TODO: 在这里添加清理逻辑 - // 例如:清理缓存、释放资源等 - - super.unload(); - } -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts.meta deleted file mode 100644 index 578dcd61..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/scenes/GameScene.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "dbf17c21-6f4a-4f87-8568-7ac5a2ec10cd", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems.meta deleted file mode 100644 index 554b251e..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "f8f0d97b-46f6-49ff-9fe5-b31eee989963", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts deleted file mode 100644 index 885b0fd0..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts +++ /dev/null @@ -1,317 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { AIComponent, Transform, Health } from '../components'; - -/** - * AI系统 - 处理AI行为和状态机 - */ -export class AISystem extends EntitySystem { - - /** 系统处理的实体计数器 */ - private processedEntityCount: number = 0; - - /** 状态转换计数器 */ - private stateTransitionCount: number = 0; - - constructor() { - // 处理具有AI组件的实体 - super(Matcher.empty().all(AIComponent)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - const deltaTime = Time.deltaTime; - const currentTime = Time.totalTime; - - this.processedEntityCount = entities.length; - - for (const entity of entities) { - this.processEntity(entity, deltaTime, currentTime); - } - - // 批量处理AI间的交互 - this.processAIInteractions(entities); - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity, deltaTime: number, currentTime: number): void { - const ai = entity.getComponent(AIComponent); - const transform = entity.getComponent(Transform); - const health = entity.getComponent(Health); - - if (!ai) return; - - // 更新感知系统 - ai.updatePerception(deltaTime); - - // 处理状态机 - this.updateStateMachine(ai, deltaTime); - - // 更新行为树 - this.updateBehaviorTree(ai, deltaTime); - - // 处理AI能力(如果有Transform和Health组件) - if (transform && health) { - this.updateAICapabilities(ai, transform, health, deltaTime); - } - - // 处理记忆衰减 - this.updateMemory(ai, deltaTime); - } - - /** - * 更新状态机 - */ - private updateStateMachine(ai: AIComponent, deltaTime: number): void { - const currentStateName = ai.currentState; - const transitions = ai.stateMachine.transitions.get(currentStateName); - - if (transitions) { - // 按优先级排序转换条件 - const sortedTransitions = transitions.sort((a, b) => b.priority - a.priority); - - for (const transition of sortedTransitions) { - if (transition.condition()) { - // 执行状态转换 - const currentState = ai.stateMachine.states.get(currentStateName); - const newState = ai.stateMachine.states.get(transition.targetState); - - if (currentState && newState) { - currentState.exit(); - ai.currentState = transition.targetState as any; - newState.enter(); - this.stateTransitionCount++; - break; - } - } - } - } - } - - /** - * 更新行为树 - */ - private updateBehaviorTree(ai: AIComponent, deltaTime: number): void { - const behaviorTree = ai.config.behaviorTree; - const blackboard = behaviorTree.blackboard; - - // 更新黑板数据 - blackboard.set('deltaTime', deltaTime); - blackboard.set('currentTime', Date.now()); - - // 模拟行为树执行 - const executionResult = this.executeBehaviorNode(behaviorTree.rootNode, ai); - - // 记录执行历史 - behaviorTree.executionHistory.push({ - nodeName: behaviorTree.rootNode.name, - startTime: Date.now(), - endTime: Date.now() + Math.random() * 10, - result: executionResult, - data: { deltaTime } - }); - - // 保持历史记录在合理范围内 - if (behaviorTree.executionHistory.length > 50) { - behaviorTree.executionHistory.shift(); - } - } - - /** - * 执行行为树节点(模拟) - */ - private executeBehaviorNode(node: any, ai: AIComponent): 'success' | 'failure' | 'running' { - // 简单的行为树执行模拟 - switch (node.name) { - case 'root': - case 'selector': - // 选择器节点:尝试执行子节点直到一个成功 - for (const child of node.children) { - const result = this.executeBehaviorNode(child, ai); - if (result === 'success' || result === 'running') { - return result; - } - } - return 'failure'; - - case 'sequence': - // 序列节点:按顺序执行所有子节点 - for (const child of node.children) { - const result = this.executeBehaviorNode(child, ai); - if (result === 'failure' || result === 'running') { - return result; - } - } - return 'success'; - - case 'condition': - // 条件节点:检查AI状态 - return ai.config.personality.intelligence > 0.5 ? 'success' : 'failure'; - - case 'action': - // 动作节点:执行AI行为 - return Math.random() > 0.3 ? 'success' : 'running'; - - default: - return 'failure'; - } - } - - /** - * 更新AI能力 - */ - private updateAICapabilities(ai: AIComponent, transform: Transform, health: Health, deltaTime: number): void { - const capabilities = ai.config.capabilities; - - // 根据健康状况调整能力 - const healthRatio = health.currentHealth / health.maxHealth; - const effectiveSpeed = capabilities.movementSpeed * healthRatio; - - // 更新移动速度 - transform.speed = effectiveSpeed; - - // 根据个性调整行为 - if (ai.config.personality.aggression > 0.7 && healthRatio > 0.5) { - // 高攻击性且健康状况良好时更主动 - ai.currentState = 'chase'; - } else if (healthRatio < 0.3) { - // 生命值低时逃跑 - ai.currentState = 'flee'; - } - } - - /** - * 更新记忆系统 - */ - private updateMemory(ai: AIComponent, deltaTime: number): void { - const memory = ai.config.memory; - const currentTime = Date.now(); - - // 衰减已知位置的可信度 - memory.knownLocations.forEach(location => { - const timeSinceVisit = currentTime - location.lastVisited; - const decayFactor = Math.exp(-timeSinceVisit / 30000); // 30秒衰减率 - location.confidence *= decayFactor; - }); - - // 移除可信度过低的位置 - memory.knownLocations = memory.knownLocations.filter(location => location.confidence > 0.1); - - // 衰减关系信任度 - memory.relationships.forEach(relation => { - const timeSinceInteraction = currentTime - relation.lastInteraction; - if (timeSinceInteraction > 60000) { // 60秒没有交互 - relation.trustLevel *= 0.99; // 缓慢衰减 - } - }); - } - - /** - * 处理AI间的交互 - */ - private processAIInteractions(entities: Entity[]): void { - const aiEntities = entities.filter(e => e.getComponent(AIComponent)); - - for (let i = 0; i < aiEntities.length; i++) { - for (let j = i + 1; j < aiEntities.length; j++) { - this.processAIPair(aiEntities[i], aiEntities[j]); - } - } - } - - /** - * 处理两个AI实体间的交互 - */ - private processAIPair(entity1: Entity, entity2: Entity): void { - const ai1 = entity1.getComponent(AIComponent); - const ai2 = entity2.getComponent(AIComponent); - const transform1 = entity1.getComponent(Transform); - const transform2 = entity2.getComponent(Transform); - - if (!ai1 || !ai2 || !transform1 || !transform2) return; - - // 计算距离 - const distance = Math.sqrt( - Math.pow(transform1.position.x - transform2.position.x, 2) + - Math.pow(transform1.position.y - transform2.position.y, 2) - ); - - // 视线范围内的交互 - if (distance <= ai1.config.capabilities.sightRange) { - this.handleVisualContact(ai1, ai2, entity1, entity2, distance); - } - - // 听力范围内的交互 - if (distance <= ai1.config.capabilities.hearingRange) { - this.handleAudioContact(ai1, ai2, distance); - } - - // 创建盟友关系(随机) - if (Math.random() < 0.001 && !ai1.allyIds.includes(entity2.id)) { // 0.1%概率每帧 - ai1.addAlly(entity2.id); - } - } - - /** - * 处理视觉接触 - */ - private handleVisualContact(ai1: AIComponent, ai2: AIComponent, entity1: Entity, entity2: Entity, distance: number): void { - const currentTime = Date.now(); - - // 添加到可见实体列表 - const existingEntry = ai1.perception.visibleEntities.find(e => e.entityId === entity2.id); - if (existingEntry) { - existingEntry.distance = distance; - existingEntry.lastSeen = currentTime; - existingEntry.componentId = ai2.id; // 使用组件ID避免循环引用 - } else { - ai1.perception.visibleEntities.push({ - entityId: entity2.id, - position: entity2.getComponent(Transform)!.position.clone(), - distance: distance, - angle: Math.atan2( - entity2.getComponent(Transform)!.position.y - entity1.getComponent(Transform)!.position.y, - entity2.getComponent(Transform)!.position.x - entity1.getComponent(Transform)!.position.x - ), - lastSeen: currentTime, - componentId: ai2.id - }); - } - } - - /** - * 处理音频接触 - */ - private handleAudioContact(ai1: AIComponent, ai2: AIComponent, distance: number): void { - const soundVolume = 1.0 - (distance / ai1.config.capabilities.hearingRange); - - ai1.perception.audibleSounds.push({ - source: ai2.entity.getComponent(Transform)!.position.clone(), - volume: soundVolume, - type: 'movement', - timestamp: Date.now() - }); - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('🤖 AI系统已启动'); - } - - /** - * 获取系统统计信息 - */ - public getSystemStats(): any { - return { - processedEntities: this.processedEntityCount, - stateTransitions: this.stateTransitionCount, - systemName: 'AISystem' - }; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts.meta deleted file mode 100644 index d3d129b6..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/AISystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "449fa887-eece-424d-ae1d-7082454fac3f", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts deleted file mode 100644 index a22acdee..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { Health } from '../components'; - -/** - * 生命值系统 - * 处理生命值回复、死亡检测等逻辑 - */ -export class HealthSystem extends EntitySystem { - - constructor() { - // 处理具有Health组件的实体 - super(Matcher.empty().all(Health)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - const deltaTime = Time.deltaTime; - - for (const entity of entities) { - this.processEntity(entity, deltaTime); - } - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity, deltaTime: number): void { - const health = entity.getComponent(Health); - - if (!health) return; - - // 如果实体已死亡,跳过处理 - if (health.isDead) return; - - // 处理生命值回复 - if (health.regenRate > 0) { - const regenAmount = health.regenRate * deltaTime; - health.heal(regenAmount); - } - - // 检查是否需要标记为死亡 - if (health.currentHealth <= 0 && !health.isDead) { - health.isDead = true; - this.onEntityDied(entity); - } - } - - /** - * 当实体死亡时调用 - */ - private onEntityDied(entity: Entity): void { - console.log(`💀 实体 ${entity.name} 已死亡`); - - // 这里可以添加死亡相关的逻辑 - // 比如播放死亡动画、掉落物品等 - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('❤️ 生命值系统已启动'); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts.meta deleted file mode 100644 index 7ed17b79..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/HealthSystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "074b3e3a-351e-4d95-b502-5a7dab8efc8d", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts deleted file mode 100644 index a20bce4f..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { Transform, Velocity } from '../components'; - -/** - * 移动系统 - * 处理具有Transform和Velocity组件的实体移动 - */ -export class MovementSystem extends EntitySystem { - - constructor() { - // 使用Matcher设置系统处理的组件类型 - super(Matcher.empty().all(Transform, Velocity)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - const deltaTime = Time.deltaTime; - - for (const entity of entities) { - this.processEntity(entity, deltaTime); - } - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity, deltaTime: number): void { - const transform = entity.getComponent(Transform); - const velocity = entity.getComponent(Velocity); - - if (!transform || !velocity) return; - - // 应用摩擦力 - velocity.applyFriction(); - - // 根据速度更新位置 - const deltaX = velocity.velocity.x * deltaTime; - const deltaY = velocity.velocity.y * deltaTime; - const deltaZ = velocity.velocity.z * deltaTime; - - transform.move(deltaX, deltaY, deltaZ); - - // 简单的边界检查 (假设游戏世界是 -500 到 500) - const bounds = 500; - if (transform.position.x > bounds) { - transform.position.x = bounds; - velocity.velocity.x = -Math.abs(velocity.velocity.x) * 0.5; // 反弹并减速 - } else if (transform.position.x < -bounds) { - transform.position.x = -bounds; - velocity.velocity.x = Math.abs(velocity.velocity.x) * 0.5; - } - - if (transform.position.y > bounds) { - transform.position.y = bounds; - velocity.velocity.y = -Math.abs(velocity.velocity.y) * 0.5; - } else if (transform.position.y < -bounds) { - transform.position.y = -bounds; - velocity.velocity.y = Math.abs(velocity.velocity.y) * 0.5; - } - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('🏃 移动系统已启动'); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts.meta deleted file mode 100644 index 0c4b8fe3..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/MovementSystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "5e556e6d-ddd5-415c-b074-3cbdb59ed503", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts deleted file mode 100644 index 0974fb3a..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts +++ /dev/null @@ -1,448 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { NetworkComponent } from '../components'; - -/** - * 网络系统 - 处理网络同步和连接管理 - */ -export class NetworkSystem extends EntitySystem { - - /** 网络统计 */ - private networkStats = { - totalEntities: 0, - connectedEntities: 0, - totalMessagesSent: 0, - totalMessagesReceived: 0, - averagePing: 0, - networkTraffic: 0 - }; - - /** 消息处理队列 */ - private globalMessageQueue: Array<{ - from: string; - to: string; - messageType: string; - data: any; - timestamp: number; - priority: number; - }> = []; - - constructor() { - // 处理具有网络组件的实体 - super(Matcher.empty().all(NetworkComponent)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - const deltaTime = Time.deltaTime; - - this.networkStats.totalEntities = entities.length; - this.networkStats.connectedEntities = entities.filter(e => - e.getComponent(NetworkComponent)?.connectionState === 'connected' - ).length; - - for (const entity of entities) { - this.processEntity(entity, deltaTime); - } - - // 处理全局消息队列 - this.processGlobalMessages(); - - // 更新网络统计 - this.updateGlobalNetworkStats(entities); - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity, deltaTime: number): void { - const network = entity.getComponent(NetworkComponent); - - if (!network) return; - - // 更新网络统计 - network.updateNetworkStats(deltaTime); - - // 处理连接状态 - this.updateConnectionState(network, deltaTime); - - // 处理消息队列 - this.processEntityMessages(network, entity); - - // 处理数据同步 - this.processSynchronization(network, deltaTime); - - // 处理群组通信 - this.processGroupCommunication(network); - } - - /** - * 更新连接状态 - */ - private updateConnectionState(network: NetworkComponent, deltaTime: number): void { - const currentTime = Date.now(); - - switch (network.connectionState) { - case 'disconnected': - // 尝试连接 - if (network.config.autoReconnect && - network.networkStats.reconnectCount < network.config.maxReconnectAttempts) { - network.connectionState = 'connecting'; - network.connection.lastHeartbeat = currentTime; - } - break; - - case 'connecting': - // 模拟连接过程 - if (Math.random() > 0.1) { // 90% 成功率 - network.connectionState = 'connected'; - network.connection.sessionId = this.generateSessionId(); - network.connection.serverId = 'server_001'; - network.connection.lastHeartbeat = currentTime; - } else if (currentTime - network.connection.lastHeartbeat > 5000) { - // 连接超时 - network.connectionState = 'error'; - network.networkStats.errorCount++; - } - break; - - case 'connected': - // 维持连接心跳 - if (currentTime - network.connection.lastHeartbeat > network.config.heartbeatInterval) { - this.sendHeartbeat(network); - network.connection.lastHeartbeat = currentTime; - } - - // 模拟网络质量变化 - network.connection.ping = Math.random() * 100 + 20; // 20-120ms - network.connection.packetLoss = Math.random() * 0.05; // 0-5% - network.connection.bandwidth = 1000 + Math.random() * 500; // 1000-1500 Kbps - break; - - case 'error': - // 错误状态,尝试重连 - if (network.config.autoReconnect && - network.networkStats.reconnectCount < network.config.maxReconnectAttempts) { - network.connectionState = 'disconnected'; - network.networkStats.reconnectCount++; - } - break; - } - } - - /** - * 处理实体消息 - */ - private processEntityMessages(network: NetworkComponent, entity: Entity): void { - // 处理传出消息 - const outgoingMessages = network.messageQueue.outgoing.slice(); - network.messageQueue.outgoing = []; - - for (const message of outgoingMessages) { - if (this.sendMessage(network, message)) { - this.networkStats.totalMessagesSent++; - network.networkStats.totalBytesSent += this.estimateMessageSize(message); - } else { - // 发送失败,重新加入队列 - message.attempts++; - if (message.attempts < message.maxAttempts) { - network.messageQueue.outgoing.push(message); - } - } - } - - // 处理传入消息 - this.processIncomingMessages(network, entity); - } - - /** - * 发送消息 - */ - private sendMessage(network: NetworkComponent, message: any): boolean { - if (network.connectionState !== 'connected') { - return false; - } - - // 模拟网络延迟和丢包 - const shouldDelay = Math.random() < 0.3; // 30% 概率有延迟 - const shouldDrop = Math.random() < network.connection.packetLoss; - - if (shouldDrop) { - network.networkStats.errorCount++; - return false; - } - - // 添加到全局消息队列 - this.globalMessageQueue.push({ - from: network.networkId, - to: message.targetId, - messageType: message.messageType, - data: message.data, - timestamp: Date.now() + (shouldDelay ? Math.random() * 200 : 0), - priority: message.priority - }); - - return true; - } - - /** - * 处理传入消息 - */ - private processIncomingMessages(network: NetworkComponent, entity: Entity): void { - // 从全局队列中获取发给此实体的消息 - const incomingMessages = this.globalMessageQueue.filter(msg => - msg.to === network.networkId && msg.timestamp <= Date.now() - ); - - // 从全局队列中移除这些消息 - this.globalMessageQueue = this.globalMessageQueue.filter(msg => - !(msg.to === network.networkId && msg.timestamp <= Date.now()) - ); - - // 处理消息 - for (const message of incomingMessages) { - network.messageQueue.incoming.push({ - senderId: message.from, - messageType: message.messageType, - data: message.data, - timestamp: message.timestamp, - processed: false - }); - - this.networkStats.totalMessagesReceived++; - network.networkStats.totalBytesReceived += this.estimateMessageSize(message); - - // 立即处理某些类型的消息 - this.handleSpecialMessages(network, message); - } - } - - /** - * 处理特殊消息类型 - */ - private handleSpecialMessages(network: NetworkComponent, message: any): void { - switch (message.messageType) { - case 'player_join_group': - // 处理加入群组消息 - const groupData = message.data; - if (groupData.members && Array.isArray(groupData.members)) { - // 查找对应的网络组件并建立连接 - groupData.members.forEach((memberId: string) => { - // 直接使用成员ID建立连接 - network.connectToPlayer(memberId); - }); - } - break; - - case 'heartbeat': - // 心跳响应 - network.connection.ping = Date.now() - message.data.timestamp; - break; - - case 'sync_request': - // 同步请求 - this.handleSyncRequest(network, message); - break; - } - } - - /** - * 处理数据同步 - */ - private processSynchronization(network: NetworkComponent, deltaTime: number): void { - const currentTime = Date.now(); - const syncInterval = 1000 / network.config.syncFrequency; // 转换为毫秒 - - if (currentTime - network.syncData.lastSyncTime >= syncInterval) { - this.performSynchronization(network); - network.syncData.lastSyncTime = currentTime; - } - - // 处理排队的更新 - this.processQueuedUpdates(network); - } - - /** - * 执行同步 - */ - private performSynchronization(network: NetworkComponent): void { - if (network.syncData.dirtyFlags.size === 0) { - return; // 没有需要同步的数据 - } - - const syncData = { - networkId: network.networkId, - timestamp: Date.now(), - properties: Array.from(network.syncData.dirtyFlags), - checksum: this.calculateChecksum(network) - }; - - // 发送同步数据给连接的玩家 - network.connectedPlayerIds.forEach(playerId => { - network.sendMessage(playerId, 'sync_data', syncData, 7); - }); - - // 记录同步历史 - network.syncData.syncHistory.push({ - timestamp: syncData.timestamp, - dataSize: this.estimateMessageSize(syncData), - properties: syncData.properties, - success: true - }); - - // 清理脏标记 - network.syncData.dirtyFlags.clear(); - } - - /** - * 处理排队的更新 - */ - private processQueuedUpdates(network: NetworkComponent): void { - // 按优先级和时间戳排序 - network.syncData.queuedUpdates.sort((a, b) => { - if (a.priority !== b.priority) { - return b.priority - a.priority; // 高优先级优先 - } - return a.timestamp - b.timestamp; // 时间戳早的优先 - }); - - // 处理前10个更新 - const updatesToProcess = network.syncData.queuedUpdates.splice(0, 10); - for (const update of updatesToProcess) { - network.markDirty(update.property); - } - } - - /** - * 处理群组通信 - */ - private processGroupCommunication(network: NetworkComponent): void { - if (network.groupMemberIds.length === 0) { - return; - } - - // 群组消息广播 - if (Math.random() < 0.01) { // 1% 概率发送群组消息 - const groupMessage = { - type: 'group_update', - data: { - sender: network.networkId, - timestamp: Date.now(), - groupSize: network.groupMemberIds.length, - status: network.connectionState - } - }; - - network.groupMemberIds.forEach(memberId => { - if (memberId !== network.networkId) { - network.sendMessage(memberId, 'group_message', groupMessage, 5); - } - }); - } - } - - /** - * 处理全局消息 - */ - private processGlobalMessages(): void { - // 移除过期消息 - const currentTime = Date.now(); - this.globalMessageQueue = this.globalMessageQueue.filter(msg => - currentTime - msg.timestamp < 30000 // 30秒过期 - ); - - // 按优先级排序 - this.globalMessageQueue.sort((a, b) => b.priority - a.priority); - } - - /** - * 更新全局网络统计 - */ - private updateGlobalNetworkStats(entities: Entity[]): void { - let totalPing = 0; - let connectedCount = 0; - let totalTraffic = 0; - - for (const entity of entities) { - const network = entity.getComponent(NetworkComponent); - if (network && network.connectionState === 'connected') { - totalPing += network.connection.ping; - connectedCount++; - totalTraffic += network.networkStats.totalBytesSent + network.networkStats.totalBytesReceived; - } - } - - this.networkStats.averagePing = connectedCount > 0 ? totalPing / connectedCount : 0; - this.networkStats.networkTraffic = totalTraffic; - } - - /** - * 辅助方法 - */ - private generateSessionId(): string { - return 'session_' + Math.random().toString(36).substring(2, 15); - } - - private estimateMessageSize(message: any): number { - return JSON.stringify(message).length; - } - - private calculateChecksum(network: NetworkComponent): string { - // 简单的校验和计算 - const data = JSON.stringify({ - networkId: network.networkId, - connectionState: network.connectionState - }); - return btoa(data).substring(0, 8); - } - - private sendHeartbeat(network: NetworkComponent): void { - network.sendMessage('server', 'heartbeat', { timestamp: Date.now() }, 10); - } - - private findNetworkComponentById(networkId: string): NetworkComponent | null { - // 这里应该有一个全局的网络组件注册表 - // 为了简化,我们返回null - return null; - } - - private handleSyncRequest(network: NetworkComponent, message: any): void { - // 处理同步请求 - const response = { - requestId: message.data.requestId, - data: this.gatherSyncData(network), - timestamp: Date.now() - }; - - network.sendMessage(message.from, 'sync_response', response, 8); - } - - private gatherSyncData(network: NetworkComponent): any { - return { - networkId: network.networkId, - connectionState: network.connectionState, - ping: network.connection.ping, - groupSize: network.groupMemberIds.length - }; - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('🌐 网络系统已启动'); - } - - /** - * 获取系统统计信息 - */ - public getSystemStats(): any { - return { - ...this.networkStats, - globalMessageQueueSize: this.globalMessageQueue.length, - systemName: 'NetworkSystem' - }; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts.meta deleted file mode 100644 index 5d87f043..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NetworkSystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "09cb67c9-12ef-48e0-949d-c8edf2c7ae22", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts deleted file mode 100644 index cffa755f..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { NodeComponent, Transform, Renderer } from '../components'; -import { Node, Vec3, Color } from 'cc'; - -/** - * 节点渲染系统 - 处理NodeComponent和Cocos Creator节点的同步 - */ -export class NodeRenderSystem extends EntitySystem { - - /** 渲染统计 */ - private renderStats = { - totalNodes: 0, - visibleNodes: 0, - renderCalls: 0, - averageRenderTime: 0, - totalRenderTime: 0, - frameCount: 0 - }; - - /** 节点池 */ - private nodePool: Node[] = []; - - /** 性能监控 */ - private performanceMonitor = { - frameStartTime: 0, - renderTimeHistory: [] as number[], - cullCount: 0, - frustumCullCount: 0 - }; - - constructor() { - // 处理具有NodeComponent的实体 - super(Matcher.empty().all(NodeComponent)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - this.performanceMonitor.frameStartTime = performance.now(); - - this.renderStats.totalNodes = entities.length; - this.renderStats.visibleNodes = 0; - this.renderStats.renderCalls = 0; - - for (const entity of entities) { - this.processEntity(entity); - } - - // 处理节点层次结构 - this.updateNodeHierarchy(entities); - - // 更新性能统计 - this.updatePerformanceStats(); - - // 清理过期的性能缓存 - this.cleanupPerformanceCache(entities); - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity): void { - const nodeComponent = entity.getComponent(NodeComponent); - const transform = entity.getComponent(Transform); - const renderer = entity.getComponent(Renderer); - - if (!nodeComponent) return; - - const renderStartTime = performance.now(); - - // 确保有对应的Cocos Creator节点 - this.ensureNode(nodeComponent, entity); - - // 同步Transform数据 - if (transform && nodeComponent.node) { - this.syncTransform(nodeComponent, transform); - } - - // 同步渲染数据 - if (renderer && nodeComponent.node) { - this.syncRenderer(nodeComponent, renderer); - } - - // 更新节点配置 - this.updateNodeConfig(nodeComponent); - - // 执行视锥体剔除 - const isVisible = this.performCulling(nodeComponent); - if (isVisible) { - this.renderStats.visibleNodes++; - this.performRender(nodeComponent); - } - - // 更新性能统计 - const renderTime = performance.now() - renderStartTime; - nodeComponent.updatePerformance(renderTime); - - this.renderStats.renderCalls++; - this.renderStats.totalRenderTime += renderTime; - } - - /** - * 确保节点存在 - */ - private ensureNode(nodeComponent: NodeComponent, entity: Entity): void { - if (!nodeComponent.node) { - // 从对象池中获取节点或创建新节点 - nodeComponent.node = this.getNodeFromPool() || new Node(nodeComponent.nodeConfig.name); - - // 初始化节点 - this.initializeNode(nodeComponent.node, nodeComponent, entity); - } - } - - /** - * 从对象池获取节点 - */ - private getNodeFromPool(): Node | null { - return this.nodePool.pop() || null; - } - - /** - * 初始化节点 - */ - private initializeNode(node: Node, nodeComponent: NodeComponent, entity: Entity): void { - const config = nodeComponent.nodeConfig; - - // 设置基本属性 - node.name = config.name; - node.layer = config.layer; - node.active = config.renderData.visible; - - // 设置变换 - node.setPosition(config.transformData.position); - node.setRotationFromEuler(config.transformData.rotation); - node.setScale(config.transformData.scale); - - // 设置渲染属性 - const opacity = Math.floor(config.renderData.opacity * 255); - // 这里可以设置更多Cocos Creator特定的属性 - - // 添加用户数据 - config.userData.entityId = entity.id; - config.userData.componentId = nodeComponent.id; - } - - /** - * 同步Transform数据 - */ - private syncTransform(nodeComponent: NodeComponent, transform: Transform): void { - const node = nodeComponent.node!; - const config = nodeComponent.nodeConfig; - - // 更新配置中的变换数据 - config.transformData.position.set(transform.position); - config.transformData.rotation.set(transform.rotation); - config.transformData.scale.set(transform.scale); - - // 同步到Cocos Creator节点 - node.setPosition(transform.position); - node.setRotationFromEuler(transform.rotation); - node.setScale(transform.scale); - - // 更新缓存数据 - nodeComponent.complexData.cache.textureCache.set('lastPosition', transform.position.clone()); - } - - /** - * 同步渲染数据 - */ - private syncRenderer(nodeComponent: NodeComponent, renderer: Renderer): void { - const node = nodeComponent.node!; - const config = nodeComponent.nodeConfig; - - // 更新配置中的渲染数据 - config.renderData.color.set(renderer.color); - config.renderData.opacity = renderer.alpha; - config.renderData.visible = renderer.visible && renderer.alpha > 0; - - // 同步到Cocos Creator节点 - node.active = config.renderData.visible; - - // 更新材质缓存 - nodeComponent.complexData.cache.materialCache.set('currentColor', renderer.color.clone()); - nodeComponent.complexData.cache.materialCache.set('alpha', renderer.alpha); - } - - /** - * 更新节点配置 - */ - private updateNodeConfig(nodeComponent: NodeComponent): void { - const config = nodeComponent.nodeConfig; - const currentTime = Date.now(); - - // 更新统计信息 - nodeComponent.complexData.statistics.frameCount++; - nodeComponent.complexData.statistics.lastUpdateTime = currentTime; - - // 更新用户数据 - config.userData.lastFrameUpdate = currentTime; - config.userData.frameCount = nodeComponent.complexData.statistics.frameCount; - - // 动态调整配置 - if (Math.random() < 0.01) { // 1% 概率调整 - config.renderData.opacity *= (0.95 + Math.random() * 0.1); // 轻微透明度变化 - config.renderData.opacity = Math.max(0.1, Math.min(1.0, config.renderData.opacity)); - } - } - - /** - * 执行视锥体剔除 - */ - private performCulling(nodeComponent: NodeComponent): boolean { - if (!nodeComponent.node) { - return false; - } - - const config = nodeComponent.nodeConfig; - - // 简单的可见性检查 - if (!config.renderData.visible || config.renderData.opacity <= 0) { - this.performanceMonitor.cullCount++; - return false; - } - - // 距离剔除 - const position = config.transformData.position; - const distance = position.length(); - if (distance > 1000) { // 超过1000单位距离的对象被剔除 - this.performanceMonitor.frustumCullCount++; - return false; - } - - // 层级剔除 - if (config.layer < 0) { - this.performanceMonitor.cullCount++; - return false; - } - - return true; - } - - /** - * 执行渲染 - */ - private performRender(nodeComponent: NodeComponent): void { - if (!nodeComponent.node) return; - - const renderStartTime = performance.now(); - - // 模拟复杂的渲染过程 - this.simulateRenderingWork(nodeComponent); - - // 更新子节点 - this.updateChildNodes(nodeComponent); - - // 更新着色器缓存 - this.updateShaderCache(nodeComponent); - - const renderTime = performance.now() - renderStartTime; - - // 更新性能统计 - const perf = nodeComponent.complexData.statistics.performance; - perf.renderHistory.push(renderTime); - - if (perf.renderHistory.length > 100) { - perf.renderHistory.shift(); - } - - perf.avgRenderTime = perf.renderHistory.reduce((a, b) => a + b, 0) / perf.renderHistory.length; - perf.maxRenderTime = Math.max(perf.maxRenderTime, renderTime); - } - - /** - * 模拟渲染工作 - */ - private simulateRenderingWork(nodeComponent: NodeComponent): void { - const complexity = nodeComponent.complexData.cache.materialCache.size + - nodeComponent.complexData.cache.textureCache.size; - - // 模拟基于复杂度的计算工作 - let iterations = Math.min(complexity * 10, 1000); - let result = 0; - for (let i = 0; i < iterations; i++) { - result += Math.sin(i * 0.001) * Math.cos(i * 0.002); - } - - // 存储计算结果到缓存 - nodeComponent.complexData.cache.shaderCache.set('computeResult', result); - } - - /** - * 更新子节点 - */ - private updateChildNodes(nodeComponent: NodeComponent): void { - if (nodeComponent.children.length === 0) return; - - const parentNode = nodeComponent.node!; - - // 同步子节点 - for (let i = 0; i < nodeComponent.children.length; i++) { - const childNode = nodeComponent.children[i]; - if (childNode && childNode.parent !== parentNode) { - parentNode.addChild(childNode); - } - } - - // 更新层次结构数据 - nodeComponent.complexData.hierarchy.siblingIndex = parentNode.getSiblingIndex(); - - // 更新子组件的层次深度(需要通过实体管理器查找) - // 这里省略了复杂的查找逻辑,避免循环引用 - if (nodeComponent.nodeConfig.childIds.length > 0) { - // 实际项目中应该通过实体管理器查找子实体并更新深度 - // 为了示例简化,我们只更新自己的深度 - nodeComponent.complexData.hierarchy.depth = Math.max(0, nodeComponent.complexData.hierarchy.depth); - } - } - - /** - * 更新着色器缓存 - */ - private updateShaderCache(nodeComponent: NodeComponent): void { - const shaderCache = nodeComponent.complexData.cache.shaderCache; - - // 模拟着色器参数更新 - const currentTime = Date.now(); - shaderCache.set('time', currentTime); - shaderCache.set('frameCount', nodeComponent.complexData.statistics.frameCount); - - // 清理过期的着色器缓存 - if (shaderCache.size > 50) { - const keys = Array.from(shaderCache.keys()); - const oldestKey = keys[0]; - shaderCache.delete(oldestKey); - } - } - - /** - * 更新节点层次结构 - */ - private updateNodeHierarchy(entities: Entity[]): void { - // 构建层次结构映射 - const nodeMap = new Map(); - - entities.forEach(entity => { - const nodeComponent = entity.getComponent(NodeComponent); - if (nodeComponent) { - nodeMap.set(entity.id, nodeComponent); - } - }); - - // 更新层次关系(使用ID避免循环引用) - nodeMap.forEach((nodeComponent, entityId) => { - // 更新根节点ID - if (!nodeComponent.complexData.hierarchy.parentId) { - nodeComponent.complexData.hierarchy.rootId = entityId; - } else { - // 查找根节点ID(简化版本,避免深度遍历) - let currentParentId = nodeComponent.complexData.hierarchy.parentId; - let depth = 0; - - // 限制深度以避免无限循环 - while (currentParentId && depth < 10) { - const parentNode = nodeMap.get(currentParentId); - if (parentNode && parentNode.complexData.hierarchy.parentId) { - currentParentId = parentNode.complexData.hierarchy.parentId; - depth++; - } else { - break; - } - } - - nodeComponent.complexData.hierarchy.rootId = currentParentId || entityId; - } - }); - } - - /** - * 更新性能统计 - */ - private updatePerformanceStats(): void { - const frameTime = performance.now() - this.performanceMonitor.frameStartTime; - - this.performanceMonitor.renderTimeHistory.push(frameTime); - if (this.performanceMonitor.renderTimeHistory.length > 60) { - this.performanceMonitor.renderTimeHistory.shift(); - } - - this.renderStats.frameCount++; - if (this.renderStats.renderCalls > 0) { - this.renderStats.averageRenderTime = this.renderStats.totalRenderTime / this.renderStats.renderCalls; - } - } - - /** - * 清理性能缓存 - */ - private cleanupPerformanceCache(entities: Entity[]): void { - entities.forEach(entity => { - const nodeComponent = entity.getComponent(NodeComponent); - if (nodeComponent) { - const caches = nodeComponent.complexData.cache; - - // 清理纹理缓存 - if (caches.textureCache.size > 100) { - const keys = Array.from(caches.textureCache.keys()); - const toDelete = keys.slice(0, 20); // 删除最旧的20个 - toDelete.forEach(key => caches.textureCache.delete(key)); - } - - // 清理材质缓存 - if (caches.materialCache.size > 50) { - const keys = Array.from(caches.materialCache.keys()); - const toDelete = keys.slice(0, 10); // 删除最旧的10个 - toDelete.forEach(key => caches.materialCache.delete(key)); - } - } - }); - } - - /** - * 回收节点到对象池 - */ - public recycleNode(node: Node): void { - if (this.nodePool.length < 100) { // 限制对象池大小 - node.removeFromParent(); - node.destroyAllChildren(); - this.nodePool.push(node); - } else { - node.destroy(); - } - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('🎨 节点渲染系统已启动'); - - // 预热对象池 - for (let i = 0; i < 10; i++) { - this.nodePool.push(new Node(`PooledNode_${i}`)); - } - } - - /** - * 当实体被移除时 - */ - protected onRemoved(entity: Entity): void { - const nodeComponent = entity.getComponent(NodeComponent); - if (nodeComponent && nodeComponent.node) { - this.recycleNode(nodeComponent.node); - nodeComponent.node = null; - } - } - - /** - * 获取系统统计信息 - */ - public getSystemStats(): any { - return { - ...this.renderStats, - cullCount: this.performanceMonitor.cullCount, - frustumCullCount: this.performanceMonitor.frustumCullCount, - nodePoolSize: this.nodePool.length, - averageFrameTime: this.performanceMonitor.renderTimeHistory.length > 0 - ? this.performanceMonitor.renderTimeHistory.reduce((a, b) => a + b, 0) / this.performanceMonitor.renderTimeHistory.length - : 0, - systemName: 'NodeRenderSystem' - }; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts.meta deleted file mode 100644 index 25ad5e55..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/NodeRenderSystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "b4593488-685b-4e52-800b-b2a2990305d6", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts deleted file mode 100644 index ee8ce7ce..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { EntitySystem, Entity, Matcher, Time } from '@esengine/ecs-framework'; -import { Transform, Velocity } from '../components'; - -/** - * 随机移动系统 - * 让实体随机改变移动方向 - */ -export class RandomMovementSystem extends EntitySystem { - - /** 每个实体的下次方向改变时间 */ - private nextDirectionChangeTime: Map = new Map(); - - constructor() { - // 处理具有Transform和Velocity组件的实体 - super(Matcher.empty().all(Transform, Velocity)); - } - - /** - * 处理所有实体 - */ - protected process(entities: Entity[]): void { - const currentTime = Time.totalTime; - - for (const entity of entities) { - this.processEntity(entity, currentTime); - } - } - - /** - * 处理单个实体 - */ - private processEntity(entity: Entity, currentTime: number): void { - const velocity = entity.getComponent(Velocity); - - if (!velocity) return; - - // 检查是否需要改变方向 - const nextChangeTime = this.nextDirectionChangeTime.get(entity.id) || 0; - - if (currentTime >= nextChangeTime) { - // 随机生成新的移动方向 - const angle = Math.random() * Math.PI * 2; // 0-360度 - const speed = 50 + Math.random() * 100; // 50-150的随机速度 - - const newVelocityX = Math.cos(angle) * speed; - const newVelocityY = Math.sin(angle) * speed; - - velocity.setVelocity(newVelocityX, newVelocityY); - - // 设置下次改变方向的时间(1-3秒后) - const nextInterval = 1 + Math.random() * 2; - this.nextDirectionChangeTime.set(entity.id, currentTime + nextInterval); - } - } - - /** - * 当实体被添加到系统时 - */ - protected onAdded(entity: Entity): void { - // 为新实体设置初始方向改变时间 - const initialDelay = Math.random() * 2; // 0-2秒的初始延迟 - this.nextDirectionChangeTime.set(entity.id, Time.totalTime + initialDelay); - } - - /** - * 当实体从系统中移除时 - */ - protected onRemoved(entity: Entity): void { - // 清理实体的时间记录 - this.nextDirectionChangeTime.delete(entity.id); - } - - /** - * 系统初始化时调用 - */ - public initialize(): void { - super.initialize(); - console.log('🎲 随机移动系统已启动'); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts.meta deleted file mode 100644 index 6b376371..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/RandomMovementSystem.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "ed1688a1-b44f-4588-ae6a-080a6af38a94", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts deleted file mode 100644 index d6ba54e3..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// 导出所有系统 -export { MovementSystem } from './MovementSystem'; -export { HealthSystem } from './HealthSystem'; -export { RandomMovementSystem } from './RandomMovementSystem'; -export { AISystem } from './AISystem'; -export { NetworkSystem } from './NetworkSystem'; -export { NodeRenderSystem } from './NodeRenderSystem'; \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts.meta deleted file mode 100644 index 82abf440..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/ecs/systems/index.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "586d2e9b-054b-457b-b44c-dafda0a73b6e", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm-framework.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm-framework.meta deleted file mode 100644 index 27ed14ed..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm-framework.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "0ce4b9fe-436f-4735-8ef3-b90bead65200", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm.meta deleted file mode 100644 index 876d6920..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "ab8a85e4-962f-49ac-843a-b57d534f27c5", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts deleted file mode 100644 index 3195ca9a..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { _decorator, Component, Label, Button } from 'cc'; -import { DataBinding, BindingType, BindingMode } from '@esengine/mvvm-ui-framework'; -import { TestViewModel } from './TestViewModel'; - -const { ccclass, property } = _decorator; - -@ccclass('TestView') -export class TestView extends Component { - - @property(Label) - testLabel: Label = null!; - - @property(Button) - testButton: Button = null!; - - private viewModel: TestViewModel; - private dataBinding: DataBinding; - private bindingId: string = ''; - - onLoad() { - console.log('TestView onLoad'); - - // 初始化数据绑定系统 - this.dataBinding = DataBinding.getInstance(); - - // 创建 ViewModel - this.viewModel = new TestViewModel(); - console.log('创建 ViewModel:', this.viewModel); - - // 手动添加观察者来测试 - this.viewModel.addObserver('testValue', (newValue, oldValue, property) => { - console.log(`属性 ${property} 变化: ${oldValue} -> ${newValue}`); - if (this.testLabel) { - this.testLabel.string = '测试值: ' + newValue; - console.log('手动更新 Label 文本:', this.testLabel.string); - } - }); - - // 设置数据绑定 - this.setupDataBinding(); - - // 设置按钮事件 - this.setupButtonEvent(); - - // 测试初始值 - console.log('初始值:', this.viewModel.testValue); - } - - private setupDataBinding(): void { - if (this.testLabel) { - console.log('设置数据绑定'); - console.log('Label 对象:', this.testLabel); - console.log('Label 初始文本:', this.testLabel.string); - - this.bindingId = this.dataBinding.bind(this.viewModel, this.testLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'testValue', - target: 'string', - format: '测试值: {0}' - }); - console.log('绑定ID:', this.bindingId); - - // 手动测试一下绑定是否工作 - this.testLabel.string = '测试值: ' + this.viewModel.testValue; - console.log('手动设置后的文本:', this.testLabel.string); - } - } - - private setupButtonEvent(): void { - if (this.testButton) { - this.testButton.node.on(Button.EventType.CLICK, () => { - console.log('按钮点击'); - this.viewModel.addValue(); - }); - } - } - - onDestroy() { - if (this.bindingId) { - this.dataBinding.unbind(this.bindingId); - } - if (this.viewModel) { - this.viewModel.destroy(); - } - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts.meta deleted file mode 100644 index 2bd655e4..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestView.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "667e05c4-fdf7-4b08-8c65-57e085465210", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts deleted file mode 100644 index b516052f..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts +++ /dev/null @@ -1,38 +0,0 @@ -import 'reflect-metadata'; -import { ViewModel, observable } from '@esengine/mvvm-ui-framework'; - -/** - * 简单的测试 ViewModel - */ -export class TestViewModel extends ViewModel { - - public get name(): string { - return 'TestViewModel'; - } - - @observable - testValue: number = 0; - - public addValue(): void { - console.log('添加值之前:', this.testValue); - console.log('notifyObservers 方法存在吗?', typeof this.notifyObservers); - - // 检查属性描述符 - const descriptor = Object.getOwnPropertyDescriptor(this, 'testValue') || - Object.getOwnPropertyDescriptor(Object.getPrototypeOf(this), 'testValue'); - console.log('testValue 属性描述符:', descriptor); - - // 检查私有属性 - console.log('_testValue 私有属性:', (this as any)._testValue); - - this.testValue += 1; - console.log('添加值之后:', this.testValue); - console.log('_testValue 私有属性 (之后):', (this as any)._testValue); - - // 手动触发通知测试 - if (this.notifyObservers) { - console.log('手动触发通知'); - this.notifyObservers('testValue', this.testValue, this.testValue - 1); - } - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts.meta deleted file mode 100644 index 1af1a688..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/TestViewModel.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "28999657-2992-4293-839b-101ae666b364", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts deleted file mode 100644 index 7375cb75..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { _decorator, Component, Node, Label, Button } from 'cc'; -import { DataBinding, BindingType, BindingMode } from '@esengine/mvvm-ui-framework'; -import { UserInfoViewModel } from './UserInfoViewModel'; - -const { ccclass, property } = _decorator; - -/** - * 用户信息视图组件 - * 展示如何使用 MVVM 框架进行 Label 数据绑定 - */ -@ccclass('UserInfoView') -export class UserInfoView extends Component { - - @property(Label) - userNameLabel: Label = null!; - - @property(Label) - levelLabel: Label = null!; - - @property(Label) - scoreLabel: Label = null!; - - @property(Label) - coinsLabel: Label = null!; - - @property(Label) - onlineStatusLabel: Label = null!; - - @property(Label) - displayNameLabel: Label = null!; - - @property(Label) - totalAssetsLabel: Label = null!; - - @property(Button) - addScoreButton: Button = null!; - - @property(Button) - addCoinsButton: Button = null!; - - @property(Button) - levelUpButton: Button = null!; - - @property(Button) - toggleOnlineButton: Button = null!; - - @property(Button) - resetButton: Button = null!; - - private viewModel: UserInfoViewModel; - private dataBinding: DataBinding; - private bindingIds: string[] = []; - - onLoad() { - // 初始化数据绑定系统 - this.dataBinding = DataBinding.getInstance(); - - // 创建 ViewModel - this.viewModel = new UserInfoViewModel(); - - // 设置数据绑定 - this.setupDataBindings(); - - // 设置按钮事件 - this.setupButtonEvents(); - } - - /** - * 设置数据绑定 - */ - private setupDataBindings(): void { - // 用户名绑定 - if (this.userNameLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.userNameLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.REPLACE, - source: 'userName', - target: 'string' - }); - this.bindingIds.push(bindingId); - - - this.dataBinding.bind(this.viewModel, this.coinsLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.REPLACE, - source: 'price', - target: 'string', - converter: 'currency', // 货币转换器 - converterParams: ['USD', 2], // 美元,2位小数 - format: '价格: {0}' - }); - - this.dataBinding.registerConverter('currency', { - convert: (value: number) => `${(value * 100).toFixed(1)}%`, - convertBack: (value: string) => parseFloat(value) / 100 - }); - } - - // 等级绑定(使用格式化) - if (this.levelLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.levelLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'level', - target: 'string', - format: '等级: {0}' - }); - this.bindingIds.push(bindingId); - } - - // 分数绑定(使用数字转换器) - if (this.scoreLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.scoreLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'score', - target: 'string', - converter: 'number', - format: '分数: {0}' - }); - this.bindingIds.push(bindingId); - } - - // 金币绑定(使用数字转换器) - if (this.coinsLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.coinsLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'coins', - target: 'string', - converter: 'number', - format: '金币: {0}' - }); - this.bindingIds.push(bindingId); - } - - // 在线状态绑定 - if (this.onlineStatusLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.onlineStatusLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'onlineStatusText', - target: 'string', - format: '状态: {0}' - }); - this.bindingIds.push(bindingId); - } - - // 显示名称绑定(计算属性) - if (this.displayNameLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.displayNameLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.REPLACE, - source: 'displayName', - target: 'string' - }); - this.bindingIds.push(bindingId); - } - - // 总资产绑定(计算属性) - if (this.totalAssetsLabel) { - const bindingId = this.dataBinding.bind(this.viewModel, this.totalAssetsLabel, { - type: BindingType.ONE_WAY, - mode: BindingMode.FORMAT, - source: 'totalAssets', - target: 'string', - converter: 'number', - format: '总资产: {0}' - }); - this.bindingIds.push(bindingId); - } - } - - /** - * 设置按钮事件 - */ - private setupButtonEvents(): void { - // 增加分数按钮 - 直接调用方法 - if (this.addScoreButton) { - this.addScoreButton.node.on(Button.EventType.CLICK, () => { - this.viewModel.executeCommand('addScore'); - }); - } - - // 增加金币按钮 - 使用命令系统,支持 canExecute 检查 - if (this.addCoinsButton) { - this.addCoinsButton.node.on(Button.EventType.CLICK, () => { - // 检查命令是否可以执行 - if (this.viewModel.canExecuteCommand('addCoins')) { - this.viewModel.executeCommand('addCoins'); - } else { - console.log('等级太低,无法获得金币!'); - } - }); - } - - // 升级按钮 - 使用命令系统,支持 canExecute 检查 - if (this.levelUpButton) { - this.levelUpButton.node.on(Button.EventType.CLICK, () => { - // 检查命令是否可以执行 - if (this.viewModel.canExecuteCommand('levelUp')) { - this.viewModel.executeCommand('levelUp'); - } else { - console.log('分数不足,无法升级!'); - } - }); - } - - // 切换在线状态按钮 - 直接调用方法 - if (this.toggleOnlineButton) { - this.toggleOnlineButton.node.on(Button.EventType.CLICK, () => { - this.viewModel.toggleOnlineStatus(); - }); - } - - // 重置按钮 - 直接调用方法 - if (this.resetButton) { - this.resetButton.node.on(Button.EventType.CLICK, () => { - this.viewModel.resetUserData(); - }); - - } - } - - /** - * 手动更新用户信息(演示用) - */ - public updateUserInfo(): void { - // 可以通过代码直接修改 ViewModel 的属性 - // 绑定的 Label 会自动更新 - this.viewModel.userName = '测试用户' + Math.floor(Math.random() * 1000); - this.viewModel.level = Math.floor(Math.random() * 50) + 1; - this.viewModel.score = Math.floor(Math.random() * 10000); - this.viewModel.coins = Math.floor(Math.random() * 1000); - this.viewModel.isOnline = Math.random() > 0.5; - } - - /** - * 获取当前 ViewModel - */ - public getViewModel(): UserInfoViewModel { - return this.viewModel; - } - - onDestroy() { - // 清理绑定 - for (const bindingId of this.bindingIds) { - this.dataBinding.unbind(bindingId); - } - this.bindingIds = []; - - // 销毁 ViewModel - if (this.viewModel) { - this.viewModel.destroy(); - } - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts.meta deleted file mode 100644 index 37338ee8..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoView.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "0a14765b-2165-4fba-b286-00ba485caa11", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts deleted file mode 100644 index c650872e..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { ViewModel, observable, computed, command, viewModel } from '@esengine/mvvm-ui-framework'; - -/** - * 用户信息视图模型 - * 展示如何使用 MVVM 框架进行数据绑定 - */ -@viewModel -export class UserInfoViewModel extends ViewModel { - - public get name(): string { - return 'UserInfoViewModel'; - } - - /** - * 用户名 - 使用 @observable 装饰器自动处理数据绑定 - */ - @observable - userName: string = '未知用户'; - - /** - * 用户等级 - */ - level: number = 1; - - /** - * 用户分数 - */ - @observable - score: number = 0; - - /** - * 用户金币 - */ - @observable - coins: number = 100; - - /** - * 是否在线 - */ - @observable - isOnline: boolean = false; - - /** - * 计算属性:用户显示名称(格式化) - */ - @computed(['userName', 'level']) - get displayName(): string { - return `${this.userName} (Lv.${this.level})`; - } - - /** - * 计算属性:在线状态文本 - */ - @computed(['isOnline']) - get onlineStatusText(): string { - return this.isOnline ? '在线' : '离线'; - } - - /** - * 计算属性:总资产(分数 + 金币) - */ - @computed(['score', 'coins']) - get totalAssets(): number { - return this.score + this.coins; - } - - /** - * 增加分数 - 使用 @command 装饰器,可以通过 executeCommand('addScore') 调用 - */ - @command() - public addScore(amount: number = 10): void { - this.score += amount; - } - - - - /** - * 增加金币 - 带有 canExecute 逻辑的命令 - */ - @command('canAddCoins') - public addCoins(amount: number = 5): void { - this.coins += amount; - } - - /** - * 检查是否可以增加金币(例如:等级必须大于 1) - */ - public canAddCoins(): boolean { - return this.level > 1; - } - - /** - * 升级 - 带有复杂 canExecute 逻辑的命令 - */ - @command('canLevelUp') - public levelUp(): void { - this.level += 1; - this.score += this.level * 100; // 升级奖励 - } - - /** - * 检查是否可以升级(例如:需要足够的分数) - */ - public canLevelUp(): boolean { - return this.score >= this.level * 100; - } - - /** - * 切换在线状态 - */ - @command() - public toggleOnlineStatus(): void { - this.isOnline = !this.isOnline; - } - - /** - * 重置用户数据 - */ - public resetUserData(): void { - this.batchUpdate({ - userName: '新用户', - level: 1, - score: 0, - coins: 100, - isOnline: false - }); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts.meta deleted file mode 100644 index e06399ac..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/UserInfoViewModel.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "0e90a89e-1c64-4c47-ab61-9093f9964678", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game.meta deleted file mode 100644 index 1f42bf6b..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "21b8d75a-82be-4b5a-8ecf-765558907857", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts deleted file mode 100644 index 5e29a619..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { ViewModel, observable, computed, command } from '@esengine/mvvm-ui-framework'; - -/** - * 游戏状态视图模型 - */ -export class GameStateViewModel extends ViewModel { - - public get name(): string { - return 'GameStateViewModel'; - } - - @observable - currentLevel: number = 1; - - @observable - health: number = 100; - - @observable - mana: number = 50; - - @observable - experience: number = 0; - - @computed(['health']) - get healthPercent(): number { - return (this.health / 100) * 100; - } - - @computed(['experience', 'currentLevel']) - get experienceToNextLevel(): number { - return (this.currentLevel * 100) - this.experience; - } - - @command() - public levelUp(): void { - this.currentLevel += 1; - this.health = 100; - this.mana += 10; - } - - @command() - public takeDamage(damage: number): void { - this.health = Math.max(0, this.health - damage); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts.meta deleted file mode 100644 index 4728291f..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/game/GameStateViewModel.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "8fa14e6f-46cd-4cb4-9ac1-0a1919e260a5", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop.meta deleted file mode 100644 index d4c12c0b..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "2c09b280-bf73-4d95-9b1f-d4d915442980", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts deleted file mode 100644 index 618a4fe4..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { ViewModel, observable, computed, command } from '@esengine/mvvm-ui-framework'; - -/** - * 商店视图模型 - */ -export class ShopViewModel extends ViewModel { - - public get name(): string { - return 'ShopViewModel'; - } - - @observable - selectedCategory: string = 'weapons'; - - @observable - playerGold: number = 1000; - - @observable - cartItems: any[] = []; - - @computed(['cartItems']) - get totalPrice(): number { - return this.cartItems.reduce((total, item) => total + item.price, 0); - } - - @computed(['playerGold', 'totalPrice']) - get canPurchase(): boolean { - return this.playerGold >= this.totalPrice; - } - - @command() - public addToCart(item: any): void { - this.cartItems.push(item); - } - - @command('canPurchase') - public purchase(): void { - this.playerGold -= this.totalPrice; - this.cartItems = []; - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts.meta deleted file mode 100644 index 125727dc..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/shop/ShopViewModel.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "148fe394-03cd-45a1-9bc0-5cb24440d8db", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user.meta deleted file mode 100644 index 896aeb00..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "1.2.0", - "importer": "directory", - "imported": true, - "uuid": "62abfd02-b9f5-41d2-9822-2c777af21e27", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts deleted file mode 100644 index 10245924..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ViewModel, observable, computed, command } from '@esengine/mvvm-ui-framework'; - -/** - * 用户配置文件视图模型 - */ -export class UserProfileViewModel extends ViewModel { - - public get name(): string { - return 'UserProfileViewModel'; - } - - @observable - avatar: string = ''; - - @observable - nickname: string = ''; - - @observable - email: string = ''; - - @observable - phone: string = ''; - - @computed(['nickname', 'email']) - get displayInfo(): string { - return `${this.nickname} (${this.email})`; - } - - @command() - public updateProfile(): void { - console.log('更新用户配置文件'); - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts.meta b/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts.meta deleted file mode 100644 index c9449085..00000000 --- a/extensions/cocos/cocos-ecs/assets/scripts/mvvm/user/UserProfileViewModel.ts.meta +++ /dev/null @@ -1,9 +0,0 @@ -{ - "ver": "4.0.24", - "importer": "typescript", - "imported": true, - "uuid": "05648650-5963-4847-8789-7bdc6ea7f43c", - "files": [], - "subMetas": {}, - "userData": {} -} diff --git a/extensions/cocos/cocos-ecs/extensions/behaviour-tree b/extensions/cocos/cocos-ecs/extensions/behaviour-tree deleted file mode 160000 index d4fd74fb..00000000 --- a/extensions/cocos/cocos-ecs/extensions/behaviour-tree +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d4fd74fb9442dbf6beae17a39265e61f113d383d diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension b/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension deleted file mode 160000 index ea76db57..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-ecs-extension +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ea76db57499ee521c5c260942d7d0d8caff42333 diff --git a/extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen b/extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen deleted file mode 160000 index fb7d5bbb..00000000 --- a/extensions/cocos/cocos-ecs/extensions/cocos-terrain-gen +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fb7d5bbb01d8cef8aeaa9f13e361231280858bbc diff --git a/extensions/cocos/cocos-ecs/extensions/mvvm-designer b/extensions/cocos/cocos-ecs/extensions/mvvm-designer deleted file mode 160000 index 8e2527ad..00000000 --- a/extensions/cocos/cocos-ecs/extensions/mvvm-designer +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8e2527ad126961d3bf56f32a4c1419c3e2061f82 diff --git a/extensions/cocos/cocos-ecs/package-lock.json b/extensions/cocos/cocos-ecs/package-lock.json deleted file mode 100644 index deb2cc82..00000000 --- a/extensions/cocos/cocos-ecs/package-lock.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "cocos-ecs", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "cocos-ecs", - "dependencies": { - "@esengine/ai": "^2.0.24", - "@esengine/cocos-nexus": "^1.0.1", - "@esengine/ecs-framework": "^2.1.23", - "@esengine/mvvm-ui-framework": "^1.0.5" - } - }, - "../../../thirdparty/mvvm-ui-framework": { - "name": "@esengine/mvvm-ui-framework", - "version": "1.0.2", - "extraneous": true, - "license": "MIT", - "dependencies": { - "reflect-metadata": "^0.1.13" - }, - "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.3", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-terser": "^0.4.4", - "@types/node": "^20.19.0", - "rimraf": "^5.0.0", - "rollup": "^4.42.0", - "rollup-plugin-dts": "^6.2.1", - "typescript": "^5.8.3" - } - }, - "node_modules/@cocos/creator-types": { - "version": "3.8.6", - "resolved": "https://registry.npmjs.org/@cocos/creator-types/-/creator-types-3.8.6.tgz", - "integrity": "sha512-hyZ4aoqqLxoRtKbBLSJM5RgtK3oGOlTEryHDcyH4znq3h9cFk+MSbQC2aJHvK5/bMlJzsZ641/hD77RGSrvo8Q==", - "peer": true - }, - "node_modules/@esengine/ai": { - "version": "2.0.24", - "resolved": "https://registry.npmjs.org/@esengine/ai/-/ai-2.0.24.tgz", - "integrity": "sha512-Xwy3yTh/UCVtTvtLc8GtHfDWu+opA3hii1MUFoyMvOszNrnWJ92Qs7bAFSmVqCKHR7joBS2SNTP6Htvlo99SbA==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@esengine/cocos-nexus": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@esengine/cocos-nexus/-/cocos-nexus-1.0.1.tgz", - "integrity": "sha512-DWYBBlg3LwuYNVCtP0CTDgLXfix+fo0serfwNRHbKqGxb8yBnzJeGPauVwY7Jupc4nGBRZtC5xDfHW5PIxfQ4Q==", - "dependencies": { - "reflect-metadata": "^0.1.13" - }, - "peerDependencies": { - "@cocos/creator-types": ">=3.3.0" - } - }, - "node_modules/@esengine/ecs-framework": { - "version": "2.1.23", - "resolved": "https://registry.npmjs.org/@esengine/ecs-framework/-/ecs-framework-2.1.23.tgz", - "integrity": "sha512-Em4w+AQ2c/eXN6FqTwySwu869yPjG9kHZp74vEDVBLb7uQOw+g8aMrSALS4tYHTj0PxNofcNplS38xjGsFTekA==", - "license": "MIT", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@esengine/mvvm-ui-framework": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@esengine/mvvm-ui-framework/-/mvvm-ui-framework-1.0.6.tgz", - "integrity": "sha512-YaULGN15k3UORaMY+nkEJv/6Ham3gMtoFBxLZkVF6jNKpqwTDFKiv/Lsd5JyC8KeJas63GFL4Z3c8Ii0oygKHA==", - "license": "MIT", - "dependencies": { - "reflect-metadata": "^0.1.13" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/reflect-metadata": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.14.tgz", - "integrity": "sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==" - } - } -} diff --git a/extensions/cocos/cocos-ecs/package.json b/extensions/cocos/cocos-ecs/package.json deleted file mode 100644 index 5cad23bc..00000000 --- a/extensions/cocos/cocos-ecs/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "cocos-ecs", - "uuid": "e17e4eb7-dfe2-4f87-b77d-13c36da216e3", - "creator": { - "version": "3.8.6", - "registry": { - "remote": {} - }, - "dependencies": { - "localization-editor": "1.0.2" - } - }, - "dependencies": { - "@esengine/ai": "^2.0.24", - "@esengine/cocos-nexus": "^1.0.1", - "@esengine/ecs-framework": "^2.1.23", - "@esengine/mvvm-ui-framework": "^1.0.5" - } -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/builder.json b/extensions/cocos/cocos-ecs/settings/v2/packages/builder.json deleted file mode 100644 index 7526e407..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/builder.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "__version__": "1.3.9" -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/cocos-service.json b/extensions/cocos/cocos-ecs/settings/v2/packages/cocos-service.json deleted file mode 100644 index 3313a8ad..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/cocos-service.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "__version__": "3.0.9", - "game": { - "name": "UNKNOW GAME", - "app_id": "UNKNOW", - "c_id": "0" - }, - "appConfigMaps": [ - { - "app_id": "UNKNOW", - "config_id": "a17c82" - } - ], - "configs": [ - { - "app_id": "UNKNOW", - "config_id": "a17c82", - "config_name": "Default", - "config_remarks": "", - "services": [] - } - ] -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/device.json b/extensions/cocos/cocos-ecs/settings/v2/packages/device.json deleted file mode 100644 index 70e599e6..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/device.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "__version__": "1.0.1" -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/engine.json b/extensions/cocos/cocos-ecs/settings/v2/packages/engine.json deleted file mode 100644 index 1eef437e..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/engine.json +++ /dev/null @@ -1,235 +0,0 @@ -{ - "__version__": "1.0.12", - "modules": { - "configs": { - "defaultConfig": { - "name": "DEFAULT CONFIG", - "cache": { - "base": { - "_value": true - }, - "gfx-webgl": { - "_value": true - }, - "gfx-webgl2": { - "_value": false - }, - "gfx-webgpu": { - "_value": false - }, - "animation": { - "_value": true - }, - "skeletal-animation": { - "_value": true - }, - "3d": { - "_value": true - }, - "meshopt": { - "_value": false - }, - "2d": { - "_value": true - }, - "xr": { - "_value": false - }, - "rich-text": { - "_value": true - }, - "mask": { - "_value": true - }, - "graphics": { - "_value": true - }, - "ui-skew": { - "_value": true - }, - "affine-transform": { - "_value": true - }, - "ui": { - "_value": true - }, - "particle": { - "_value": true - }, - "physics": { - "_value": true, - "_option": "physics-ammo" - }, - "physics-ammo": { - "_value": true, - "_flags": { - "LOAD_BULLET_MANUALLY": false - } - }, - "physics-cannon": { - "_value": false - }, - "physics-physx": { - "_value": false, - "_flags": { - "LOAD_PHYSX_MANUALLY": false - } - }, - "physics-builtin": { - "_value": false - }, - "physics-2d": { - "_value": true, - "_option": "physics-2d-box2d" - }, - "physics-2d-box2d": { - "_value": true - }, - "physics-2d-box2d-wasm": { - "_value": false, - "_flags": { - "LOAD_BOX2D_MANUALLY": false - } - }, - "physics-2d-builtin": { - "_value": false - }, - "physics-2d-box2d-jsb": { - "_value": false - }, - "intersection-2d": { - "_value": true - }, - "primitive": { - "_value": true - }, - "profiler": { - "_value": true - }, - "occlusion-query": { - "_value": false - }, - "geometry-renderer": { - "_value": true - }, - "debug-renderer": { - "_value": false - }, - "particle-2d": { - "_value": true - }, - "audio": { - "_value": true - }, - "video": { - "_value": true - }, - "webview": { - "_value": true - }, - "tween": { - "_value": true - }, - "websocket": { - "_value": true - }, - "websocket-server": { - "_value": false - }, - "terrain": { - "_value": true - }, - "light-probe": { - "_value": true - }, - "tiled-map": { - "_value": true - }, - "vendor-google": { - "_value": false - }, - "spine": { - "_value": true, - "_option": "spine-3.8" - }, - "spine-3.8": { - "_value": true, - "_flags": { - "LOAD_SPINE_MANUALLY": false - } - }, - "spine-4.2": { - "_value": false, - "_flags": { - "LOAD_SPINE_MANUALLY": false - } - }, - "dragon-bones": { - "_value": true - }, - "marionette": { - "_value": true - }, - "procedural-animation": { - "_value": false - }, - "custom-pipeline-post-process": { - "_value": false - }, - "render-pipeline": { - "_value": true, - "_option": "custom-pipeline" - }, - "custom-pipeline": { - "_value": true - }, - "legacy-pipeline": { - "_value": false - } - }, - "flags": { - "LOAD_SPINE_MANUALLY": false, - "LOAD_BULLET_MANUALLY": false - }, - "includeModules": [ - "2d", - "3d", - "affine-transform", - "animation", - "audio", - "base", - "custom-pipeline", - "dragon-bones", - "geometry-renderer", - "gfx-webgl", - "graphics", - "intersection-2d", - "light-probe", - "marionette", - "mask", - "particle", - "particle-2d", - "physics-2d-box2d", - "physics-ammo", - "primitive", - "profiler", - "rich-text", - "skeletal-animation", - "spine-3.8", - "terrain", - "tiled-map", - "tween", - "ui", - "ui-skew", - "video", - "websocket", - "webview" - ], - "noDeprecatedFeatures": { - "value": false, - "version": "" - } - } - } - } -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/information.json b/extensions/cocos/cocos-ecs/settings/v2/packages/information.json deleted file mode 100644 index 94848dec..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/information.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "__version__": "1.0.1", - "information": { - "customSplash": { - "id": "customSplash", - "label": "customSplash", - "enable": false, - "customSplash": { - "complete": false, - "form": "https://creator-api.cocos.com/api/form/show?" - } - }, - "removeSplash": { - "id": "removeSplash", - "label": "removeSplash", - "enable": false, - "removeSplash": { - "complete": false, - "form": "https://creator-api.cocos.com/api/form/show?" - } - } - } -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/program.json b/extensions/cocos/cocos-ecs/settings/v2/packages/program.json deleted file mode 100644 index 916c1b20..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/program.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "__version__": "1.0.4" -} diff --git a/extensions/cocos/cocos-ecs/settings/v2/packages/project.json b/extensions/cocos/cocos-ecs/settings/v2/packages/project.json deleted file mode 100644 index fae06d9c..00000000 --- a/extensions/cocos/cocos-ecs/settings/v2/packages/project.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "__version__": "1.0.6", - "custom_joint_texture_layouts": [] -} diff --git a/extensions/cocos/cocos-ecs/streaming-zones.json b/extensions/cocos/cocos-ecs/streaming-zones.json deleted file mode 100644 index 4c93450f..00000000 --- a/extensions/cocos/cocos-ecs/streaming-zones.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "version": "1.0.0", - "zones": [ - { - "id": "mco83tjfk4ed7a2de4", - "name": "123", - "bounds": { - "x": 0, - "y": 0, - "z": 0, - "width": 100, - "height": 100, - "depth": 100 - }, - "priority": 1, - "preloadDistance": 50, - "unloadDistance": 100, - "visible": true, - "color": [ - 0, - 1, - 0, - 0.5 - ], - "resourcePaths": [], - "sceneNodeUuids": [], - "enabled": true, - "metadata": { - "created": 1751597801547, - "modified": 1751597801548, - "version": "1.0.0" - } - } - ], - "settings": { - "defaultPreloadDistance": 50, - "defaultUnloadDistance": 100, - "defaultPriority": 1, - "autoSave": true, - "gridSize": 10, - "snapToGrid": false - } -} \ No newline at end of file diff --git a/extensions/cocos/cocos-ecs/tsconfig.json b/extensions/cocos/cocos-ecs/tsconfig.json deleted file mode 100644 index 7dc649a9..00000000 --- a/extensions/cocos/cocos-ecs/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - /* Base configuration. Do not edit this field. */ - "extends": "./temp/tsconfig.cocos.json", - - /* Add your custom configuration here. */ - "compilerOptions": { - "strict": false - } -} diff --git a/package-lock.json b/package-lock.json index f4fde694..e052eb44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2105,7 +2105,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -3247,6 +3246,10 @@ "node": ">=12" } }, + "node_modules/@esengine/behavior-tree": { + "resolved": "packages/behavior-tree", + "link": true + }, "node_modules/@esengine/ecs-framework": { "resolved": "packages/core", "link": true @@ -8197,6 +8200,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.2.tgz", + "integrity": "sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.1.tgz", @@ -8530,7 +8542,7 @@ "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/qs": { @@ -8547,7 +8559,7 @@ "version": "18.3.26", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz", "integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -11096,7 +11108,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/dargs": { @@ -12677,6 +12689,16 @@ "dev": true, "license": "ISC" }, + "node_modules/flexlayout-react": { + "version": "0.8.17", + "resolved": "https://registry.npmjs.org/flexlayout-react/-/flexlayout-react-0.8.17.tgz", + "integrity": "sha512-F0utJcrIGBpF4btqRYLFOoITQcyFxUp19X4dvFvEceD/CJkRoefV96iN1lDU63t9ystgltmWw7AscgNbKJMlcA==", + "license": "ISC", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/focus-trap": { "version": "7.6.5", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.6.5.tgz", @@ -13477,6 +13499,15 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-void-elements": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", @@ -13531,6 +13562,37 @@ "node": ">=10.17.0" } }, + "node_modules/i18next": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.6.0.tgz", + "integrity": "sha512-tTn8fLrwBYtnclpL5aPXK/tAYBLWVvoHM1zdfXoRNLcI+RvtMsoZRV98ePlaW3khHYKuNh/Q65W/+NVFUeIwVw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -21336,6 +21398,32 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.1.3.tgz", + "integrity": "sha512-LnHvU73n/GDMWSlK8WJQHEYtKSm5g3lpsrFoxyxp+sgJiI2SZg54U3lnt8zFfo9DOohaPbe5SsjquA0MuuvBqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 25.5.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -24831,7 +24919,7 @@ "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -25399,6 +25487,15 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vue": { "version": "3.5.22", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.22.tgz", @@ -25862,6 +25959,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", @@ -25873,6 +25999,25 @@ "url": "https://github.com/sponsors/wooorm" } }, + "packages/behavior-tree": { + "name": "@esengine/behavior-tree", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^20.19.17", + "jest": "^29.7.0", + "rimraf": "^5.0.0", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "peerDependencies": { + "@esengine/ecs-framework": "^2.2.8" + } + }, "packages/core": { "name": "@esengine/ecs-framework", "version": "2.2.8", @@ -25905,15 +26050,21 @@ "name": "@esengine/editor-app", "version": "1.0.3", "dependencies": { + "@esengine/behavior-tree": "file:../behavior-tree", "@esengine/ecs-framework": "file:../core", "@esengine/editor-core": "file:../editor-core", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", + "flexlayout-react": "^0.8.17", + "i18next": "^25.6.0", "json5": "^2.2.3", "lucide-react": "^0.545.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^16.1.3", + "zustand": "^5.0.8" }, "devDependencies": { "@tauri-apps/cli": "^2.2.0", diff --git a/packages/behavior-tree/jest.config.cjs b/packages/behavior-tree/jest.config.cjs new file mode 100644 index 00000000..a4dcf800 --- /dev/null +++ b/packages/behavior-tree/jest.config.cjs @@ -0,0 +1,15 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/tests'], + testMatch: ['**/*.test.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/index.ts' + ], + coverageDirectory: 'coverage', + verbose: true, + testTimeout: 10000 +}; diff --git a/packages/behavior-tree/package.json b/packages/behavior-tree/package.json new file mode 100644 index 00000000..8b680c61 --- /dev/null +++ b/packages/behavior-tree/package.json @@ -0,0 +1,62 @@ +{ + "name": "@esengine/behavior-tree", + "version": "1.0.0", + "description": "完全ECS化的行为树系统,基于组件和实体的行为树实现", + "main": "bin/index.js", + "types": "bin/index.d.ts", + "exports": { + ".": { + "types": "./bin/index.d.ts", + "import": "./bin/index.js", + "development": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + } + }, + "files": [ + "bin/**/*" + ], + "keywords": [ + "ecs", + "behavior-tree", + "ai", + "game-ai", + "entity-component-system" + ], + "scripts": { + "clean": "rimraf bin dist tsconfig.tsbuildinfo", + "build:ts": "tsc", + "prebuild": "npm run clean", + "build": "npm run build:ts", + "build:watch": "tsc --watch", + "rebuild": "npm run clean && npm run build", + "test": "jest --config jest.config.cjs", + "test:watch": "jest --watch --config jest.config.cjs" + }, + "author": "yhh", + "license": "MIT", + "peerDependencies": { + "@esengine/ecs-framework": "^2.2.8" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "@types/node": "^20.19.17", + "jest": "^29.7.0", + "rimraf": "^5.0.0", + "ts-jest": "^29.4.0", + "typescript": "^5.8.3" + }, + "dependencies": { + "tslib": "^2.8.1" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/behavior-tree" + } +} diff --git a/packages/behavior-tree/src/BehaviorTreeBuilder.ts b/packages/behavior-tree/src/BehaviorTreeBuilder.ts new file mode 100644 index 00000000..53d89ea7 --- /dev/null +++ b/packages/behavior-tree/src/BehaviorTreeBuilder.ts @@ -0,0 +1,547 @@ +import { Entity, IScene } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from './Components/BehaviorTreeNode'; +import { CompositeNodeComponent } from './Components/CompositeNodeComponent'; +import { DecoratorNodeComponent } from './Components/DecoratorNodeComponent'; +import { BlackboardComponent } from './Components/BlackboardComponent'; +import { NodeType, CompositeType, DecoratorType, BlackboardValueType } from './Types/TaskStatus'; + +// 导入动作组件 +import { WaitAction } from './Components/Actions/WaitAction'; +import { LogAction } from './Components/Actions/LogAction'; +import { SetBlackboardValueAction } from './Components/Actions/SetBlackboardValueAction'; +import { ModifyBlackboardValueAction, ModifyOperation } from './Components/Actions/ModifyBlackboardValueAction'; +import { ExecuteAction, CustomActionFunction } from './Components/Actions/ExecuteAction'; + +// 导入条件组件 +import { BlackboardCompareCondition, CompareOperator } from './Components/Conditions/BlackboardCompareCondition'; +import { BlackboardExistsCondition } from './Components/Conditions/BlackboardExistsCondition'; +import { RandomProbabilityCondition } from './Components/Conditions/RandomProbabilityCondition'; +import { ExecuteCondition, CustomConditionFunction } from './Components/Conditions/ExecuteCondition'; + +// 导入装饰器组件 +import { RepeaterNode } from './Components/Decorators/RepeaterNode'; +import { InverterNode } from './Components/Decorators/InverterNode'; +import { UntilSuccessNode } from './Components/Decorators/UntilSuccessNode'; +import { UntilFailNode } from './Components/Decorators/UntilFailNode'; +import { AlwaysSucceedNode } from './Components/Decorators/AlwaysSucceedNode'; +import { AlwaysFailNode } from './Components/Decorators/AlwaysFailNode'; +import { ConditionalNode } from './Components/Decorators/ConditionalNode'; +import { CooldownNode } from './Components/Decorators/CooldownNode'; +import { TimeoutNode } from './Components/Decorators/TimeoutNode'; + +/** + * 行为树构建器 + * + * 提供流式 API 来构建行为树结构 + * + * @example + * ```typescript + * const aiRoot = BehaviorTreeBuilder.create(scene, 'AI') + * .blackboard() + * .defineVariable('health', BlackboardValueType.Number, 100) + * .defineVariable('target', BlackboardValueType.Object, null) + * .endBlackboard() + * .selector('MainSelector') + * .sequence('AttackSequence') + * .condition((entity, blackboard) => { + * return blackboard?.getValue('health') > 50; + * }) + * .action('Attack', (entity) => TaskStatus.Success) + * .end() + * .action('Flee', (entity) => TaskStatus.Success) + * .end() + * .build(); + * ``` + */ +export class BehaviorTreeBuilder { + private scene: IScene; + private currentEntity: Entity; + private entityStack: Entity[] = []; + private blackboardEntity?: Entity; + + private constructor(scene: IScene, rootName: string) { + this.scene = scene; + this.currentEntity = scene.createEntity(rootName); + } + + /** + * 创建行为树构建器 + * + * @param scene 场景实例 + * @param rootName 根节点名称 + * @returns 构建器实例 + */ + static create(scene: IScene, rootName: string = 'BehaviorTreeRoot'): BehaviorTreeBuilder { + return new BehaviorTreeBuilder(scene, rootName); + } + + /** + * 添加黑板组件到根节点 + */ + blackboard(): BehaviorTreeBuilder { + this.blackboardEntity = this.currentEntity; + this.currentEntity.addComponent(new BlackboardComponent()); + return this; + } + + /** + * 定义黑板变量 + */ + defineVariable( + name: string, + type: BlackboardValueType, + initialValue: any, + options?: { readonly?: boolean; description?: string } + ): BehaviorTreeBuilder { + if (!this.blackboardEntity) { + throw new Error('Must call blackboard() first'); + } + + const blackboard = this.blackboardEntity.getComponent(BlackboardComponent); + if (blackboard) { + blackboard.defineVariable(name, type, initialValue, options); + } + + return this; + } + + /** + * 结束黑板定义 + */ + endBlackboard(): BehaviorTreeBuilder { + this.blackboardEntity = undefined; + return this; + } + + /** + * 创建序列节点 + */ + sequence(name: string = 'Sequence'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.Sequence); + } + + /** + * 创建选择器节点 + */ + selector(name: string = 'Selector'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.Selector); + } + + /** + * 创建并行节点 + */ + parallel(name: string = 'Parallel'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.Parallel); + } + + /** + * 创建并行选择器节点 + */ + parallelSelector(name: string = 'ParallelSelector'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.ParallelSelector); + } + + /** + * 创建随机序列节点 + */ + randomSequence(name: string = 'RandomSequence'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.RandomSequence); + } + + /** + * 创建随机选择器节点 + */ + randomSelector(name: string = 'RandomSelector'): BehaviorTreeBuilder { + return this.composite(name, CompositeType.RandomSelector); + } + + /** + * 创建复合节点 + */ + private composite(name: string, type: CompositeType): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Composite; + node.nodeName = name; + + const composite = entity.addComponent(new CompositeNodeComponent()); + composite.compositeType = type; + + this.currentEntity = entity; + return this; + } + + /** + * 创建反转装饰器 + */ + inverter(name: string = 'Inverter'): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + entity.addComponent(new InverterNode()); + + this.currentEntity = entity; + return this; + } + + /** + * 创建重复装饰器 + */ + repeater(name: string = 'Repeater', count: number = -1, endOnFailure: boolean = false): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + const decorator = entity.addComponent(new RepeaterNode()); + decorator.repeatCount = count; + decorator.endOnFailure = endOnFailure; + + this.currentEntity = entity; + return this; + } + + /** + * 创建直到成功装饰器 + */ + untilSuccess(name: string = 'UntilSuccess'): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + entity.addComponent(new UntilSuccessNode()); + + this.currentEntity = entity; + return this; + } + + /** + * 创建直到失败装饰器 + */ + untilFail(name: string = 'UntilFail'): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + entity.addComponent(new UntilFailNode()); + + this.currentEntity = entity; + return this; + } + + /** + * 创建总是成功装饰器 + */ + alwaysSucceed(name: string = 'AlwaysSucceed'): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + entity.addComponent(new AlwaysSucceedNode()); + + this.currentEntity = entity; + return this; + } + + /** + * 创建总是失败装饰器 + */ + alwaysFail(name: string = 'AlwaysFail'): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + entity.addComponent(new AlwaysFailNode()); + + this.currentEntity = entity; + return this; + } + + /** + * 创建条件装饰器 + */ + conditional(name: string, conditionCode: string): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + const decorator = entity.addComponent(new ConditionalNode()); + decorator.conditionCode = conditionCode; + + this.currentEntity = entity; + return this; + } + + /** + * 创建冷却装饰器 + */ + cooldown(name: string = 'Cooldown', cooldownTime: number = 1.0): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + const decorator = entity.addComponent(new CooldownNode()); + decorator.cooldownTime = cooldownTime; + + this.currentEntity = entity; + return this; + } + + /** + * 创建超时装饰器 + */ + timeout(name: string = 'Timeout', timeoutDuration: number = 5.0): BehaviorTreeBuilder { + this.entityStack.push(this.currentEntity); + + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Decorator; + node.nodeName = name; + + const decorator = entity.addComponent(new TimeoutNode()); + decorator.timeoutDuration = timeoutDuration; + + this.currentEntity = entity; + return this; + } + + /** + * 创建等待动作 + */ + wait(waitTime: number, name: string = 'Wait'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Action; + node.nodeName = name; + + const action = entity.addComponent(new WaitAction()); + action.waitTime = waitTime; + + return this; + } + + /** + * 创建日志动作 + */ + log(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log', name: string = 'Log'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Action; + node.nodeName = name; + + const action = entity.addComponent(new LogAction()); + action.message = message; + action.level = level; + + return this; + } + + /** + * 创建设置黑板值动作 + */ + setBlackboardValue(variableName: string, value: any, name: string = 'SetValue'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Action; + node.nodeName = name; + + const action = entity.addComponent(new SetBlackboardValueAction()); + action.variableName = variableName; + action.value = value; + + return this; + } + + /** + * 创建修改黑板值动作 + */ + modifyBlackboardValue( + variableName: string, + operation: ModifyOperation, + operand: any, + name: string = 'ModifyValue' + ): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Action; + node.nodeName = name; + + const action = entity.addComponent(new ModifyBlackboardValueAction()); + action.variableName = variableName; + action.operation = operation; + action.operand = operand; + + return this; + } + + /** + * 创建自定义动作 + */ + action(name: string, func: CustomActionFunction): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Action; + node.nodeName = name; + + const action = entity.addComponent(new ExecuteAction()); + action.setFunction(func); + + return this; + } + + /** + * 创建黑板比较条件 + */ + compareBlackboardValue( + variableName: string, + operator: CompareOperator, + compareValue: any, + name: string = 'Compare' + ): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Condition; + node.nodeName = name; + + const condition = entity.addComponent(new BlackboardCompareCondition()); + condition.variableName = variableName; + condition.operator = operator; + condition.compareValue = compareValue; + + return this; + } + + /** + * 创建黑板变量存在条件 + */ + checkBlackboardExists(variableName: string, checkNotNull: boolean = false, name: string = 'Exists'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Condition; + node.nodeName = name; + + const condition = entity.addComponent(new BlackboardExistsCondition()); + condition.variableName = variableName; + condition.checkNotNull = checkNotNull; + + return this; + } + + /** + * 创建随机概率条件 + */ + randomProbability(probability: number, name: string = 'Random'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Condition; + node.nodeName = name; + + const condition = entity.addComponent(new RandomProbabilityCondition()); + condition.probability = probability; + + return this; + } + + /** + * 创建自定义条件 + */ + condition(func: CustomConditionFunction, name: string = 'Condition'): BehaviorTreeBuilder { + const entity = this.scene.createEntity(name); + this.currentEntity.addChild(entity); + + const node = entity.addComponent(new BehaviorTreeNode()); + node.nodeType = NodeType.Condition; + node.nodeName = name; + + const condition = entity.addComponent(new ExecuteCondition()); + condition.setFunction(func); + + return this; + } + + /** + * 结束当前节点,返回父节点 + */ + end(): BehaviorTreeBuilder { + if (this.entityStack.length === 0) { + throw new Error('No parent node to return to'); + } + + this.currentEntity = this.entityStack.pop()!; + return this; + } + + /** + * 构建并返回根节点实体 + */ + build(): Entity { + // 确保返回到根节点 + while (this.entityStack.length > 0) { + this.currentEntity = this.entityStack.pop()!; + } + + return this.currentEntity; + } +} diff --git a/packages/behavior-tree/src/BehaviorTreePlugin.ts b/packages/behavior-tree/src/BehaviorTreePlugin.ts new file mode 100644 index 00000000..78a9cb4d --- /dev/null +++ b/packages/behavior-tree/src/BehaviorTreePlugin.ts @@ -0,0 +1,97 @@ +import type { Core } from '@esengine/ecs-framework'; +import type { ServiceContainer, IPlugin, IScene } from '@esengine/ecs-framework'; +import { WorldManager } from '@esengine/ecs-framework'; +import { LeafExecutionSystem } from './Systems/LeafExecutionSystem'; +import { DecoratorExecutionSystem } from './Systems/DecoratorExecutionSystem'; +import { CompositeExecutionSystem } from './Systems/CompositeExecutionSystem'; +import { SubTreeExecutionSystem } from './Systems/SubTreeExecutionSystem'; +import { GlobalBlackboardService } from './Services/GlobalBlackboardService'; + +/** + * 行为树插件 + * + * 提供便捷方法向场景添加行为树系统 + * + * @example + * ```typescript + * const core = Core.create(); + * const plugin = new BehaviorTreePlugin(); + * await core.pluginManager.install(plugin); + * + * // 为场景添加行为树系统 + * const scene = new Scene(); + * plugin.setupScene(scene); + * ``` + */ +export class BehaviorTreePlugin implements IPlugin { + readonly name = '@esengine/behavior-tree'; + readonly version = '1.0.0'; + + private worldManager: WorldManager | null = null; + private services: ServiceContainer | null = null; + + /** + * 安装插件 + */ + async install(core: Core, services: ServiceContainer): Promise { + this.services = services; + + // 注册全局黑板服务 + services.registerSingleton(GlobalBlackboardService); + + this.worldManager = services.resolve(WorldManager); + } + + /** + * 卸载插件 + */ + async uninstall(): Promise { + // 注销全局黑板服务 + if (this.services) { + this.services.unregister(GlobalBlackboardService); + } + + this.worldManager = null; + this.services = null; + } + + /** + * 为场景设置行为树系统 + * + * 向场景添加所有必需的行为树系统: + * - LeafExecutionSystem (updateOrder: 100) + * - DecoratorExecutionSystem (updateOrder: 200) + * - CompositeExecutionSystem (updateOrder: 300) + * - SubTreeExecutionSystem (updateOrder: 300) + * + * @param scene 目标场景 + * + * @example + * ```typescript + * const scene = new Scene(); + * behaviorTreePlugin.setupScene(scene); + * ``` + */ + public setupScene(scene: IScene): void { + scene.addSystem(new LeafExecutionSystem()); + scene.addSystem(new DecoratorExecutionSystem()); + scene.addSystem(new CompositeExecutionSystem()); + scene.addSystem(new SubTreeExecutionSystem()); + } + + /** + * 为所有现有场景设置行为树系统 + */ + public setupAllScenes(): void { + if (!this.worldManager) { + throw new Error('Plugin not installed'); + } + + const worlds = this.worldManager.getAllWorlds(); + for (const world of worlds) { + for (const scene of world.getAllScenes()) { + this.setupScene(scene); + } + } + } +} diff --git a/packages/behavior-tree/src/BehaviorTreeStarter.ts b/packages/behavior-tree/src/BehaviorTreeStarter.ts new file mode 100644 index 00000000..8fce7363 --- /dev/null +++ b/packages/behavior-tree/src/BehaviorTreeStarter.ts @@ -0,0 +1,179 @@ +import { Entity } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from './Components/BehaviorTreeNode'; +import { ActiveNode } from './Components/ActiveNode'; +import { TaskStatus } from './Types/TaskStatus'; + +/** + * 行为树启动/停止辅助类 + * + * 提供便捷方法来启动、停止和暂停行为树 + */ +export class BehaviorTreeStarter { + /** + * 启动行为树 + * + * 给根节点添加 ActiveNode 组件,使行为树开始执行 + * + * @param rootEntity 行为树根节点实体 + * + * @example + * ```typescript + * const aiRoot = scene.createEntity('aiRoot'); + * // ... 构建行为树结构 + * BehaviorTreeStarter.start(aiRoot); + * ``` + */ + static start(rootEntity: Entity): void { + if (!rootEntity.hasComponent(BehaviorTreeNode)) { + throw new Error('Entity must have BehaviorTreeNode component'); + } + + if (!rootEntity.hasComponent(ActiveNode)) { + rootEntity.addComponent(new ActiveNode()); + } + } + + /** + * 停止行为树 + * + * 移除所有节点的 ActiveNode 组件,停止执行 + * + * @param rootEntity 行为树根节点实体 + * + * @example + * ```typescript + * BehaviorTreeStarter.stop(aiRoot); + * ``` + */ + static stop(rootEntity: Entity): void { + this.stopRecursive(rootEntity); + } + + /** + * 递归停止所有子节点 + */ + private static stopRecursive(entity: Entity): void { + // 移除活跃标记 + if (entity.hasComponent(ActiveNode)) { + entity.removeComponentByType(ActiveNode); + } + + // 重置节点状态 + const node = entity.getComponent(BehaviorTreeNode); + if (node) { + node.reset(); + } + + // 递归处理子节点 + for (const child of entity.children) { + this.stopRecursive(child); + } + } + + /** + * 暂停行为树 + * + * 移除 ActiveNode 但保留节点状态,可以恢复执行 + * + * @param rootEntity 行为树根节点实体 + * + * @example + * ```typescript + * // 暂停 + * BehaviorTreeStarter.pause(aiRoot); + * + * // 恢复 + * BehaviorTreeStarter.resume(aiRoot); + * ``` + */ + static pause(rootEntity: Entity): void { + this.pauseRecursive(rootEntity); + } + + /** + * 递归暂停所有子节点 + */ + private static pauseRecursive(entity: Entity): void { + // 只移除活跃标记,不重置状态 + if (entity.hasComponent(ActiveNode)) { + entity.removeComponentByType(ActiveNode); + } + + // 递归处理子节点 + for (const child of entity.children) { + this.pauseRecursive(child); + } + } + + /** + * 恢复行为树执行 + * + * 从暂停状态恢复,重新添加 ActiveNode 到之前正在执行的节点 + * + * @param rootEntity 行为树根节点实体 + * + * @example + * ```typescript + * BehaviorTreeStarter.resume(aiRoot); + * ``` + */ + static resume(rootEntity: Entity): void { + this.resumeRecursive(rootEntity); + } + + /** + * 递归恢复所有正在执行的节点 + */ + private static resumeRecursive(entity: Entity): void { + const node = entity.getComponent(BehaviorTreeNode); + if (!node) { + return; + } + + // 如果节点状态是 Running,恢复活跃标记 + if (node.status === TaskStatus.Running) { + if (!entity.hasComponent(ActiveNode)) { + entity.addComponent(new ActiveNode()); + } + } + + // 递归处理子节点 + for (const child of entity.children) { + this.resumeRecursive(child); + } + } + + /** + * 重启行为树 + * + * 停止并重置所有节点,然后重新启动 + * + * @param rootEntity 行为树根节点实体 + * + * @example + * ```typescript + * BehaviorTreeStarter.restart(aiRoot); + * ``` + */ + static restart(rootEntity: Entity): void { + this.stop(rootEntity); + this.start(rootEntity); + } + + /** + * 检查行为树是否正在运行 + * + * @param rootEntity 行为树根节点实体 + * @returns 是否正在运行 + * + * @example + * ```typescript + * if (BehaviorTreeStarter.isRunning(aiRoot)) { + * console.log('AI is active'); + * } + * ``` + */ + static isRunning(rootEntity: Entity): boolean { + return rootEntity.hasComponent(ActiveNode); + } +} diff --git a/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts b/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts new file mode 100644 index 00000000..965d2812 --- /dev/null +++ b/packages/behavior-tree/src/Components/Actions/ExecuteAction.ts @@ -0,0 +1,87 @@ +import { Component, ECSComponent, Entity } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { TaskStatus, NodeType } from '../../Types/TaskStatus'; +import { BlackboardComponent } from '../BlackboardComponent'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 自定义动作函数类型 + */ +export type CustomActionFunction = ( + entity: Entity, + blackboard?: BlackboardComponent, + deltaTime?: number +) => TaskStatus; + +/** + * 执行自定义函数动作组件 + * + * 允许用户提供自定义的动作执行函数 + */ +@BehaviorNode({ + displayName: '自定义动作', + category: '动作', + type: NodeType.Action, + icon: 'Code', + description: '执行自定义代码', + color: '#FFC107' +}) +@ECSComponent('ExecuteAction') +@Serializable({ version: 1 }) +export class ExecuteAction extends Component { + @BehaviorProperty({ + label: '动作代码', + type: 'code', + description: 'JavaScript 代码,返回 TaskStatus', + required: true + }) + @Serialize() + actionCode?: string = 'return TaskStatus.Success;'; + + @Serialize() + parameters: Record = {}; + + /** 编译后的函数(不序列化) */ + @IgnoreSerialization() + private compiledFunction?: CustomActionFunction; + + /** + * 获取或编译执行函数 + */ + getFunction(): CustomActionFunction | undefined { + if (!this.compiledFunction && this.actionCode) { + try { + const func = new Function( + 'entity', + 'blackboard', + 'deltaTime', + 'parameters', + 'TaskStatus', + ` + const { Success, Failure, Running, Invalid } = TaskStatus; + try { + ${this.actionCode} + } catch (error) { + return TaskStatus.Failure; + } + ` + ); + + this.compiledFunction = (entity, blackboard, deltaTime) => { + return func(entity, blackboard, deltaTime, this.parameters, TaskStatus) || TaskStatus.Success; + }; + } catch (error) { + return undefined; + } + } + + return this.compiledFunction; + } + + /** + * 设置自定义函数(运行时使用) + */ + setFunction(func: CustomActionFunction): void { + this.compiledFunction = func; + } +} diff --git a/packages/behavior-tree/src/Components/Actions/LogAction.ts b/packages/behavior-tree/src/Components/Actions/LogAction.ts new file mode 100644 index 00000000..ea6c20ec --- /dev/null +++ b/packages/behavior-tree/src/Components/Actions/LogAction.ts @@ -0,0 +1,49 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 日志动作组件 + * + * 输出日志信息 + */ +@BehaviorNode({ + displayName: '日志', + category: '动作', + type: NodeType.Action, + icon: 'FileText', + description: '输出日志消息', + color: '#673AB7' +}) +@ECSComponent('LogAction') +@Serializable({ version: 1 }) +export class LogAction extends Component { + @BehaviorProperty({ + label: '消息', + type: 'string', + required: true + }) + @Serialize() + message: string = 'Hello'; + + @BehaviorProperty({ + label: '级别', + type: 'select', + options: [ + { label: 'Log', value: 'log' }, + { label: 'Info', value: 'info' }, + { label: 'Warn', value: 'warn' }, + { label: 'Error', value: 'error' } + ] + }) + @Serialize() + level: 'log' | 'info' | 'warn' | 'error' = 'log'; + + @BehaviorProperty({ + label: '包含实体信息', + type: 'boolean' + }) + @Serialize() + includeEntityInfo: boolean = false; +} diff --git a/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts b/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts new file mode 100644 index 00000000..9b86afc8 --- /dev/null +++ b/packages/behavior-tree/src/Components/Actions/ModifyBlackboardValueAction.ts @@ -0,0 +1,76 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 修改操作类型 + */ +export enum ModifyOperation { + /** 加法 */ + Add = 'add', + /** 减法 */ + Subtract = 'subtract', + /** 乘法 */ + Multiply = 'multiply', + /** 除法 */ + Divide = 'divide', + /** 取模 */ + Modulo = 'modulo', + /** 追加(数组/字符串) */ + Append = 'append', + /** 移除(数组) */ + Remove = 'remove' +} + +/** + * 修改黑板变量值动作组件 + * + * 对黑板变量执行数学或逻辑操作 + */ +@BehaviorNode({ + displayName: '修改变量', + category: '动作', + type: NodeType.Action, + icon: 'Calculator', + description: '对黑板变量执行数学或逻辑操作', + color: '#FF9800' +}) +@ECSComponent('ModifyBlackboardValueAction') +@Serializable({ version: 1 }) +export class ModifyBlackboardValueAction extends Component { + @BehaviorProperty({ + label: '变量名', + type: 'variable', + required: true + }) + @Serialize() + variableName: string = ''; + + @BehaviorProperty({ + label: '操作类型', + type: 'select', + options: [ + { label: '加法', value: 'add' }, + { label: '减法', value: 'subtract' }, + { label: '乘法', value: 'multiply' }, + { label: '除法', value: 'divide' }, + { label: '取模', value: 'modulo' }, + { label: '追加', value: 'append' }, + { label: '移除', value: 'remove' } + ] + }) + @Serialize() + operation: ModifyOperation = ModifyOperation.Add; + + @BehaviorProperty({ + label: '操作数', + type: 'string', + description: '可以是固定值或变量引用 {{varName}}' + }) + @Serialize() + operand: any = 0; + + @Serialize() + force: boolean = false; +} diff --git a/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts b/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts new file mode 100644 index 00000000..c45ef7e2 --- /dev/null +++ b/packages/behavior-tree/src/Components/Actions/SetBlackboardValueAction.ts @@ -0,0 +1,43 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 设置黑板变量值动作组件 + * + * 将指定值或另一个黑板变量的值设置到目标变量 + */ +@BehaviorNode({ + displayName: '设置变量', + category: '动作', + type: NodeType.Action, + icon: 'Edit', + description: '设置黑板变量的值', + color: '#3F51B5' +}) +@ECSComponent('SetBlackboardValueAction') +@Serializable({ version: 1 }) +export class SetBlackboardValueAction extends Component { + @BehaviorProperty({ + label: '变量名', + type: 'variable', + required: true + }) + @Serialize() + variableName: string = ''; + + @BehaviorProperty({ + label: '值', + type: 'string', + description: '可以使用 {{varName}} 引用其他变量' + }) + @Serialize() + value: any = ''; + + @Serialize() + sourceVariable?: string; + + @Serialize() + force: boolean = false; +} diff --git a/packages/behavior-tree/src/Components/Actions/WaitAction.ts b/packages/behavior-tree/src/Components/Actions/WaitAction.ts new file mode 100644 index 00000000..a730816d --- /dev/null +++ b/packages/behavior-tree/src/Components/Actions/WaitAction.ts @@ -0,0 +1,43 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 等待动作组件 + * + * 等待指定时间后返回成功 + */ +@BehaviorNode({ + displayName: '等待', + category: '动作', + type: NodeType.Action, + icon: 'Clock', + description: '等待指定时间', + color: '#9E9E9E' +}) +@ECSComponent('WaitAction') +@Serializable({ version: 1 }) +export class WaitAction extends Component { + @BehaviorProperty({ + label: '等待时间', + type: 'number', + min: 0, + step: 0.1, + description: '等待时间(秒)', + required: true + }) + @Serialize() + waitTime: number = 1.0; + + /** 已等待时间(秒) */ + @IgnoreSerialization() + elapsedTime: number = 0; + + /** + * 重置等待状态 + */ + reset(): void { + this.elapsedTime = 0; + } +} diff --git a/packages/behavior-tree/src/Components/ActiveNode.ts b/packages/behavior-tree/src/Components/ActiveNode.ts new file mode 100644 index 00000000..ff16766d --- /dev/null +++ b/packages/behavior-tree/src/Components/ActiveNode.ts @@ -0,0 +1,20 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; + +/** + * 活跃节点标记组件 + * + * 标记当前应该被执行的节点。 + * 只有带有此组件的节点才会被各个执行系统处理。 + * + * 这是一个标记组件(Tag Component),不包含数据,只用于标识。 + * + * 执行流程: + * 1. 初始时只有根节点带有 ActiveNode + * 2. 父节点决定激活哪个子节点时,为子节点添加 ActiveNode + * 3. 节点执行完成后移除 ActiveNode + * 4. 通过这种方式实现按需执行,避免每帧遍历整棵树 + */ +@ECSComponent('ActiveNode') +export class ActiveNode extends Component { + // 标记组件,无需数据字段 +} diff --git a/packages/behavior-tree/src/Components/AssetMetadata.ts b/packages/behavior-tree/src/Components/AssetMetadata.ts new file mode 100644 index 00000000..71b6f5b9 --- /dev/null +++ b/packages/behavior-tree/src/Components/AssetMetadata.ts @@ -0,0 +1,61 @@ +import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; + +/** + * 资产元数据组件 + * + * 附加到从资产实例化的行为树根节点上, + * 用于标记资产ID和版本信息,便于循环引用检测和调试。 + * + * @example + * ```typescript + * const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, scene); + * + * // 添加元数据 + * const metadata = rootEntity.addComponent(new BehaviorTreeAssetMetadata()); + * metadata.assetId = 'patrol'; + * metadata.assetVersion = '1.0.0'; + * ``` + */ +@ECSComponent('BehaviorTreeAssetMetadata') +@Serializable({ version: 1 }) +export class BehaviorTreeAssetMetadata extends Component { + /** + * 资产ID + */ + @Serialize() + assetId: string = ''; + + /** + * 资产版本 + */ + @Serialize() + assetVersion: string = ''; + + /** + * 资产名称 + */ + @Serialize() + assetName: string = ''; + + /** + * 加载时间 + */ + @Serialize() + loadedAt: number = 0; + + /** + * 资产描述 + */ + @Serialize() + description: string = ''; + + /** + * 初始化 + */ + initialize(assetId: string, assetVersion: string, assetName?: string): void { + this.assetId = assetId; + this.assetVersion = assetVersion; + this.assetName = assetName || assetId; + this.loadedAt = Date.now(); + } +} diff --git a/packages/behavior-tree/src/Components/BehaviorTreeNode.ts b/packages/behavior-tree/src/Components/BehaviorTreeNode.ts new file mode 100644 index 00000000..9c35ca28 --- /dev/null +++ b/packages/behavior-tree/src/Components/BehaviorTreeNode.ts @@ -0,0 +1,44 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { TaskStatus, NodeType } from '../Types/TaskStatus'; + +/** + * 行为树节点基础组件 + * + * 所有行为树节点都必须包含此组件 + */ +@ECSComponent('BehaviorTreeNode') +@Serializable({ version: 1 }) +export class BehaviorTreeNode extends Component { + /** 节点类型 */ + @Serialize() + nodeType: NodeType = NodeType.Action; + + /** 节点名称(用于调试) */ + @Serialize() + nodeName: string = 'Node'; + + /** 当前执行状态 */ + @IgnoreSerialization() + status: TaskStatus = TaskStatus.Invalid; + + /** 当前执行的子节点索引(用于复合节点) */ + @IgnoreSerialization() + currentChildIndex: number = 0; + + /** + * 重置节点状态 + */ + reset(): void { + this.status = TaskStatus.Invalid; + this.currentChildIndex = 0; + } + + /** + * 标记节点为失效(递归重置子节点) + * 注意:此方法只重置当前节点,子节点需要在 System 中处理 + */ + invalidate(): void { + this.reset(); + } +} diff --git a/packages/behavior-tree/src/Components/BlackboardComponent.ts b/packages/behavior-tree/src/Components/BlackboardComponent.ts new file mode 100644 index 00000000..02a8f815 --- /dev/null +++ b/packages/behavior-tree/src/Components/BlackboardComponent.ts @@ -0,0 +1,201 @@ +import { Component, ECSComponent, Core } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { BlackboardValueType } from '../Types/TaskStatus'; +import { GlobalBlackboardService } from '../Services/GlobalBlackboardService'; + +/** + * 黑板变量定义 + */ +export interface BlackboardVariable { + name: string; + type: BlackboardValueType; + value: any; + readonly?: boolean; + description?: string; +} + +/** + * 黑板组件 - 用于节点间共享数据 + * + * 支持分层查找: + * 1. 先查找本地变量 + * 2. 如果找不到,自动查找全局 Blackboard + * + * 通常附加到行为树的根节点上 + */ +@ECSComponent('Blackboard') +@Serializable({ version: 1 }) +export class BlackboardComponent extends Component { + /** 存储的本地变量 */ + @Serialize() + private variables: Map = new Map(); + + /** 是否启用全局 Blackboard 查找 */ + private useGlobalBlackboard: boolean = true; + + /** + * 定义一个新变量 + */ + defineVariable( + name: string, + type: BlackboardValueType, + initialValue: any, + options?: { + readonly?: boolean; + description?: string; + } + ): void { + this.variables.set(name, { + name, + type, + value: initialValue, + readonly: options?.readonly ?? false, + description: options?.description + }); + } + + /** + * 获取变量值 + * 先查找本地变量,找不到则查找全局变量 + */ + getValue(name: string): T | undefined { + const variable = this.variables.get(name); + if (variable !== undefined) { + return variable.value as T; + } + + if (this.useGlobalBlackboard) { + return Core.services.resolve(GlobalBlackboardService).getValue(name); + } + + return undefined; + } + + /** + * 获取本地变量值(不查找全局) + */ + getLocalValue(name: string): T | undefined { + const variable = this.variables.get(name); + return variable?.value as T; + } + + /** + * 设置变量值 + * 优先设置本地变量,如果本地不存在且全局存在,则设置全局变量 + */ + setValue(name: string, value: any, force: boolean = false): boolean { + const variable = this.variables.get(name); + + if (variable) { + if (variable.readonly && !force) { + return false; + } + variable.value = value; + return true; + } + + if (this.useGlobalBlackboard) { + return Core.services.resolve(GlobalBlackboardService).setValue(name, value, force); + } + + return false; + } + + /** + * 设置本地变量值(不影响全局) + */ + setLocalValue(name: string, value: any, force: boolean = false): boolean { + const variable = this.variables.get(name); + + if (!variable) { + return false; + } + + if (variable.readonly && !force) { + return false; + } + + variable.value = value; + return true; + } + + /** + * 检查变量是否存在(包括本地和全局) + */ + hasVariable(name: string): boolean { + if (this.variables.has(name)) { + return true; + } + + if (this.useGlobalBlackboard) { + return Core.services.resolve(GlobalBlackboardService).hasVariable(name); + } + + return false; + } + + /** + * 检查本地变量是否存在 + */ + hasLocalVariable(name: string): boolean { + return this.variables.has(name); + } + + /** + * 删除变量 + */ + removeVariable(name: string): boolean { + return this.variables.delete(name); + } + + /** + * 获取所有变量名 + */ + getVariableNames(): string[] { + return Array.from(this.variables.keys()); + } + + /** + * 清空所有本地变量 + */ + clear(): void { + this.variables.clear(); + } + + /** + * 启用/禁用全局 Blackboard 查找 + */ + setUseGlobalBlackboard(enabled: boolean): void { + this.useGlobalBlackboard = enabled; + } + + /** + * 是否启用全局 Blackboard 查找 + */ + isUsingGlobalBlackboard(): boolean { + return this.useGlobalBlackboard; + } + + /** + * 获取所有变量(包括本地和全局) + */ + getAllVariables(): BlackboardVariable[] { + const locals = Array.from(this.variables.values()); + + if (this.useGlobalBlackboard) { + const globals = Core.services.resolve(GlobalBlackboardService).getAllVariables(); + const localNames = new Set(this.variables.keys()); + const filteredGlobals = globals.filter(v => !localNames.has(v.name)); + return [...locals, ...filteredGlobals]; + } + + return locals; + } + + /** + * 获取全局 Blackboard 服务的引用 + */ + static getGlobalBlackboard(): GlobalBlackboardService { + return Core.services.resolve(GlobalBlackboardService); + } +} diff --git a/packages/behavior-tree/src/Components/CompositeNodeComponent.ts b/packages/behavior-tree/src/Components/CompositeNodeComponent.ts new file mode 100644 index 00000000..d122203b --- /dev/null +++ b/packages/behavior-tree/src/Components/CompositeNodeComponent.ts @@ -0,0 +1,66 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { CompositeType } from '../Types/TaskStatus'; + +/** + * 复合节点组件 + * + * 用于标识复合节点类型(Sequence, Selector, Parallel等) + */ +@ECSComponent('CompositeNode') +@Serializable({ version: 1 }) +export class CompositeNodeComponent extends Component { + /** 复合节点类型 */ + @Serialize() + compositeType: CompositeType = CompositeType.Sequence; + + /** 随机化的子节点索引顺序 */ + protected shuffledIndices: number[] = []; + + /** 是否在重启时重新洗牌(子类可选) */ + protected reshuffleOnRestart: boolean = true; + + /** + * 获取下一个子节点索引 + */ + getNextChildIndex(currentIndex: number, totalChildren: number): number { + // 对于随机类型,使用洗牌后的索引 + if (this.compositeType === CompositeType.RandomSequence || + this.compositeType === CompositeType.RandomSelector) { + + // 首次执行或需要重新洗牌 + if (this.shuffledIndices.length === 0 || currentIndex === 0 && this.reshuffleOnRestart) { + this.shuffleIndices(totalChildren); + } + + if (currentIndex < this.shuffledIndices.length) { + return this.shuffledIndices[currentIndex]; + } + return totalChildren; // 结束 + } + + // 普通顺序执行 + return currentIndex; + } + + /** + * 洗牌子节点索引 + */ + private shuffleIndices(count: number): void { + this.shuffledIndices = Array.from({ length: count }, (_, i) => i); + + // Fisher-Yates 洗牌算法 + for (let i = this.shuffledIndices.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [this.shuffledIndices[i], this.shuffledIndices[j]] = + [this.shuffledIndices[j], this.shuffledIndices[i]]; + } + } + + /** + * 重置洗牌状态 + */ + resetShuffle(): void { + this.shuffledIndices = []; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/ParallelNode.ts b/packages/behavior-tree/src/Components/Composites/ParallelNode.ts new file mode 100644 index 00000000..1431ac6d --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/ParallelNode.ts @@ -0,0 +1,51 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 并行节点 + * + * 同时执行所有子节点 + */ +@BehaviorNode({ + displayName: '并行', + category: '组合', + type: NodeType.Composite, + icon: 'Layers', + description: '同时执行所有子节点', + color: '#CDDC39' +}) +@ECSComponent('ParallelNode') +@Serializable({ version: 1 }) +export class ParallelNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '成功策略', + type: 'select', + description: '多少个子节点成功时整体成功', + options: [ + { label: '全部成功', value: 'all' }, + { label: '任意一个成功', value: 'one' } + ] + }) + @Serialize() + successPolicy: 'all' | 'one' = 'all'; + + @BehaviorProperty({ + label: '失败策略', + type: 'select', + description: '多少个子节点失败时整体失败', + options: [ + { label: '任意一个失败', value: 'one' }, + { label: '全部失败', value: 'all' } + ] + }) + @Serialize() + failurePolicy: 'one' | 'all' = 'one'; + + constructor() { + super(); + this.compositeType = CompositeType.Parallel; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts b/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts new file mode 100644 index 00000000..0568b2e5 --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/ParallelSelectorNode.ts @@ -0,0 +1,39 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 并行选择节点 + * + * 并行执行子节点,任一成功则成功 + */ +@BehaviorNode({ + displayName: '并行选择', + category: '组合', + type: NodeType.Composite, + icon: 'Sparkles', + description: '并行执行子节点,任一成功则成功', + color: '#FFC107' +}) +@ECSComponent('ParallelSelectorNode') +@Serializable({ version: 1 }) +export class ParallelSelectorNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '失败策略', + type: 'select', + description: '多少个子节点失败时整体失败', + options: [ + { label: '任意一个失败', value: 'one' }, + { label: '全部失败', value: 'all' } + ] + }) + @Serialize() + failurePolicy: 'one' | 'all' = 'all'; + + constructor() { + super(); + this.compositeType = CompositeType.ParallelSelector; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts b/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts new file mode 100644 index 00000000..c228c03c --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/RandomSelectorNode.ts @@ -0,0 +1,35 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 随机选择节点 + * + * 随机顺序执行子节点选择 + */ +@BehaviorNode({ + displayName: '随机选择', + category: '组合', + type: NodeType.Composite, + icon: 'Dices', + description: '随机顺序执行子节点选择', + color: '#F44336' +}) +@ECSComponent('RandomSelectorNode') +@Serializable({ version: 1 }) +export class RandomSelectorNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '重启时重新洗牌', + type: 'boolean', + description: '每次重启时是否重新随机子节点顺序' + }) + @Serialize() + override reshuffleOnRestart: boolean = true; + + constructor() { + super(); + this.compositeType = CompositeType.RandomSelector; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts b/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts new file mode 100644 index 00000000..7fd661a5 --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/RandomSequenceNode.ts @@ -0,0 +1,35 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 随机序列节点 + * + * 随机顺序执行子节点序列 + */ +@BehaviorNode({ + displayName: '随机序列', + category: '组合', + type: NodeType.Composite, + icon: 'Shuffle', + description: '随机顺序执行子节点序列', + color: '#FF5722' +}) +@ECSComponent('RandomSequenceNode') +@Serializable({ version: 1 }) +export class RandomSequenceNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '重启时重新洗牌', + type: 'boolean', + description: '每次重启时是否重新随机子节点顺序' + }) + @Serialize() + override reshuffleOnRestart: boolean = true; + + constructor() { + super(); + this.compositeType = CompositeType.RandomSequence; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/RootNode.ts b/packages/behavior-tree/src/Components/Composites/RootNode.ts new file mode 100644 index 00000000..c1171faf --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/RootNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, CompositeType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 根节点 + * + * 行为树的根节点,简单地激活第一个子节点 + */ +@BehaviorNode({ + displayName: '根节点', + category: '根节点', + type: NodeType.Composite, + icon: 'TreePine', + description: '行为树的根节点', + color: '#FFD700' +}) +@ECSComponent('RootNode') +@Serializable({ version: 1 }) +export class RootNode extends CompositeNodeComponent { + constructor() { + super(); + this.compositeType = CompositeType.Sequence; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/SelectorNode.ts b/packages/behavior-tree/src/Components/Composites/SelectorNode.ts new file mode 100644 index 00000000..815c5cfe --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/SelectorNode.ts @@ -0,0 +1,41 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 选择节点 + * + * 按顺序执行子节点,任一成功则成功 + */ +@BehaviorNode({ + displayName: '选择', + category: '组合', + type: NodeType.Composite, + icon: 'GitBranch', + description: '按顺序执行子节点,任一成功则成功', + color: '#8BC34A' +}) +@ECSComponent('SelectorNode') +@Serializable({ version: 1 }) +export class SelectorNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '中止类型', + type: 'select', + description: '条件变化时的中止行为', + options: [ + { label: '无', value: 'none' }, + { label: '自身', value: 'self' }, + { label: '低优先级', value: 'lower-priority' }, + { label: '两者', value: 'both' } + ] + }) + @Serialize() + abortType: AbortType = AbortType.None; + + constructor() { + super(); + this.compositeType = CompositeType.Selector; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/SequenceNode.ts b/packages/behavior-tree/src/Components/Composites/SequenceNode.ts new file mode 100644 index 00000000..781d2880 --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/SequenceNode.ts @@ -0,0 +1,41 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType, CompositeType, AbortType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; + +/** + * 序列节点 + * + * 按顺序执行所有子节点,全部成功才成功 + */ +@BehaviorNode({ + displayName: '序列', + category: '组合', + type: NodeType.Composite, + icon: 'List', + description: '按顺序执行子节点,全部成功才成功', + color: '#4CAF50' +}) +@ECSComponent('SequenceNode') +@Serializable({ version: 1 }) +export class SequenceNode extends CompositeNodeComponent { + @BehaviorProperty({ + label: '中止类型', + type: 'select', + description: '条件变化时的中止行为', + options: [ + { label: '无', value: 'none' }, + { label: '自身', value: 'self' }, + { label: '低优先级', value: 'lower-priority' }, + { label: '两者', value: 'both' } + ] + }) + @Serialize() + abortType: AbortType = AbortType.None; + + constructor() { + super(); + this.compositeType = CompositeType.Sequence; + } +} diff --git a/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts b/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts new file mode 100644 index 00000000..ff09e71d --- /dev/null +++ b/packages/behavior-tree/src/Components/Composites/SubTreeNode.ts @@ -0,0 +1,172 @@ +import { ECSComponent, Serializable, Serialize, Entity } from '@esengine/ecs-framework'; +import { CompositeNodeComponent } from '../CompositeNodeComponent'; +import { TaskStatus, NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * SubTree 节点 - 引用其他行为树作为子树 + * + * 允许将其他行为树嵌入到当前树中,实现行为树的复用和模块化。 + * + * 注意:SubTreeNode 是一个特殊的叶子节点,它不会执行编辑器中静态连接的子节点, + * 只会执行从 assetId 动态加载的外部行为树文件。 + * + * @example + * ```typescript + * const subTree = entity.addComponent(SubTreeNode); + * subTree.assetId = 'patrol'; + * subTree.inheritParentBlackboard = true; + * ``` + */ +@BehaviorNode({ + displayName: '子树', + category: '组合', + type: NodeType.Composite, + icon: 'GitBranch', + description: '引用并执行外部行为树文件(不支持静态子节点)', + color: '#FF9800', + requiresChildren: false +}) +@ECSComponent('SubTreeNode') +@Serializable({ version: 1 }) +export class SubTreeNode extends CompositeNodeComponent { + /** + * 引用的子树资产ID + * 逻辑标识符,例如 'patrol' 或 'ai/patrol' + * 实际的文件路径由 AssetLoader 决定 + */ + @BehaviorProperty({ + label: '资产ID', + type: 'asset', + description: '要引用的行为树资产ID' + }) + @Serialize() + assetId: string = ''; + + /** + * 是否将父黑板传递给子树 + * + * - true: 子树可以访问和修改父树的黑板变量 + * - false: 子树使用独立的黑板实例 + */ + @BehaviorProperty({ + label: '继承父黑板', + type: 'boolean', + description: '子树是否可以访问父树的黑板变量' + }) + @Serialize() + inheritParentBlackboard: boolean = true; + + /** + * 子树执行失败时是否传播失败状态 + * + * - true: 子树失败时,SubTree 节点返回 Failure + * - false: 子树失败时,SubTree 节点返回 Success(忽略失败) + */ + @BehaviorProperty({ + label: '传播失败', + type: 'boolean', + description: '子树失败时是否传播失败状态' + }) + @Serialize() + propagateFailure: boolean = true; + + /** + * 是否在行为树启动时预加载子树 + * + * - true: 在根节点开始执行前预加载此子树,确保执行时子树已就绪 + * - false: 运行时异步加载,执行到此节点时才开始加载(可能会有延迟) + */ + @BehaviorProperty({ + label: '预加载', + type: 'boolean', + description: '在行为树启动时预加载子树,避免运行时加载延迟' + }) + @Serialize() + preload: boolean = true; + + /** + * 子树的根实体(运行时) + * 在执行时动态创建,执行结束后销毁 + */ + private subTreeRoot?: Entity; + + /** + * 子树是否已完成 + */ + private subTreeCompleted: boolean = false; + + /** + * 子树的最终状态 + */ + private subTreeResult: TaskStatus = TaskStatus.Invalid; + + /** + * 获取子树根实体 + */ + getSubTreeRoot(): Entity | undefined { + return this.subTreeRoot; + } + + /** + * 设置子树根实体(由执行系统调用) + */ + setSubTreeRoot(root: Entity | undefined): void { + this.subTreeRoot = root; + this.subTreeCompleted = false; + this.subTreeResult = TaskStatus.Invalid; + } + + /** + * 标记子树完成(由执行系统调用) + */ + markSubTreeCompleted(result: TaskStatus): void { + this.subTreeCompleted = true; + this.subTreeResult = result; + } + + /** + * 检查子树是否已完成 + */ + isSubTreeCompleted(): boolean { + return this.subTreeCompleted; + } + + /** + * 获取子树执行结果 + */ + getSubTreeResult(): TaskStatus { + return this.subTreeResult; + } + + /** + * 重置子树状态 + */ + reset(): void { + this.subTreeRoot = undefined; + this.subTreeCompleted = false; + this.subTreeResult = TaskStatus.Invalid; + } + + /** + * 重置完成状态(用于复用预加载的子树) + * 保留子树根引用,只重置完成标记 + */ + resetCompletionState(): void { + this.subTreeCompleted = false; + this.subTreeResult = TaskStatus.Invalid; + } + + /** + * 验证配置 + */ + validate(): string[] { + const errors: string[] = []; + + if (!this.assetId || this.assetId.trim() === '') { + errors.push('SubTree 节点必须指定资产ID'); + } + + return errors; + } +} diff --git a/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts b/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts new file mode 100644 index 00000000..92682f82 --- /dev/null +++ b/packages/behavior-tree/src/Components/Conditions/BlackboardCompareCondition.ts @@ -0,0 +1,83 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 比较运算符 + */ +export enum CompareOperator { + /** 等于 */ + Equal = 'equal', + /** 不等于 */ + NotEqual = 'notEqual', + /** 大于 */ + Greater = 'greater', + /** 大于等于 */ + GreaterOrEqual = 'greaterOrEqual', + /** 小于 */ + Less = 'less', + /** 小于等于 */ + LessOrEqual = 'lessOrEqual', + /** 包含(字符串/数组) */ + Contains = 'contains', + /** 正则匹配 */ + Matches = 'matches' +} + +/** + * 黑板变量比较条件组件 + * + * 比较黑板变量与指定值或另一个变量 + */ +@BehaviorNode({ + displayName: '比较变量', + category: '条件', + type: NodeType.Condition, + icon: 'Scale', + description: '比较黑板变量与指定值', + color: '#2196F3' +}) +@ECSComponent('BlackboardCompareCondition') +@Serializable({ version: 1 }) +export class BlackboardCompareCondition extends Component { + @BehaviorProperty({ + label: '变量名', + type: 'variable', + required: true + }) + @Serialize() + variableName: string = ''; + + @BehaviorProperty({ + label: '运算符', + type: 'select', + options: [ + { label: '等于', value: 'equal' }, + { label: '不等于', value: 'notEqual' }, + { label: '大于', value: 'greater' }, + { label: '大于等于', value: 'greaterOrEqual' }, + { label: '小于', value: 'less' }, + { label: '小于等于', value: 'lessOrEqual' }, + { label: '包含', value: 'contains' }, + { label: '正则匹配', value: 'matches' } + ] + }) + @Serialize() + operator: CompareOperator = CompareOperator.Equal; + + @BehaviorProperty({ + label: '比较值', + type: 'string', + description: '可以是固定值或变量引用 {{varName}}' + }) + @Serialize() + compareValue: any = null; + + @BehaviorProperty({ + label: '反转结果', + type: 'boolean' + }) + @Serialize() + invertResult: boolean = false; +} diff --git a/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts b/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts new file mode 100644 index 00000000..cdc0f2d4 --- /dev/null +++ b/packages/behavior-tree/src/Components/Conditions/BlackboardExistsCondition.ts @@ -0,0 +1,45 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 黑板变量存在性检查条件组件 + * + * 检查黑板变量是否存在 + */ +@BehaviorNode({ + displayName: '检查变量存在', + category: '条件', + type: NodeType.Condition, + icon: 'Search', + description: '检查黑板变量是否存在', + color: '#00BCD4' +}) +@ECSComponent('BlackboardExistsCondition') +@Serializable({ version: 1 }) +export class BlackboardExistsCondition extends Component { + @BehaviorProperty({ + label: '变量名', + type: 'variable', + required: true + }) + @Serialize() + variableName: string = ''; + + @BehaviorProperty({ + label: '检查非空', + type: 'boolean', + description: '检查值不为 null/undefined' + }) + @Serialize() + checkNotNull: boolean = false; + + @BehaviorProperty({ + label: '反转结果', + type: 'boolean', + description: '检查不存在' + }) + @Serialize() + invertResult: boolean = false; +} diff --git a/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts b/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts new file mode 100644 index 00000000..82fc630e --- /dev/null +++ b/packages/behavior-tree/src/Components/Conditions/ExecuteCondition.ts @@ -0,0 +1,92 @@ +import { Component, ECSComponent, Entity } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { BlackboardComponent } from '../BlackboardComponent'; + +/** + * 自定义条件函数类型 + */ +export type CustomConditionFunction = ( + entity: Entity, + blackboard?: BlackboardComponent, + deltaTime?: number +) => boolean; + +/** + * 执行自定义条件组件 + * + * 允许用户提供自定义的条件检查函数 + */ +@BehaviorNode({ + displayName: '自定义条件', + category: '条件', + type: NodeType.Condition, + icon: 'Code', + description: '执行自定义条件代码', + color: '#9C27B0' +}) +@ECSComponent('ExecuteCondition') +@Serializable({ version: 1 }) +export class ExecuteCondition extends Component { + @BehaviorProperty({ + label: '条件代码', + type: 'code', + description: 'JavaScript 代码,返回 boolean', + required: true + }) + @Serialize() + conditionCode?: string; + + @Serialize() + parameters: Record = {}; + + @BehaviorProperty({ + label: '反转结果', + type: 'boolean' + }) + @Serialize() + invertResult: boolean = false; + + /** 编译后的函数(不序列化) */ + @IgnoreSerialization() + private compiledFunction?: CustomConditionFunction; + + /** + * 获取或编译条件函数 + */ + getFunction(): CustomConditionFunction | undefined { + if (!this.compiledFunction && this.conditionCode) { + try { + const func = new Function( + 'entity', + 'blackboard', + 'deltaTime', + 'parameters', + ` + try { + ${this.conditionCode} + } catch (error) { + return false; + } + ` + ); + + this.compiledFunction = (entity, blackboard, deltaTime) => { + return Boolean(func(entity, blackboard, deltaTime, this.parameters)); + }; + } catch (error) { + return undefined; + } + } + + return this.compiledFunction; + } + + /** + * 设置自定义函数(运行时使用) + */ + setFunction(func: CustomConditionFunction): void { + this.compiledFunction = func; + } +} diff --git a/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts b/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts new file mode 100644 index 00000000..a7b8aa50 --- /dev/null +++ b/packages/behavior-tree/src/Components/Conditions/RandomProbabilityCondition.ts @@ -0,0 +1,61 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { NodeType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; + +/** + * 随机概率条件组件 + * + * 根据概率返回成功或失败 + */ +@BehaviorNode({ + displayName: '随机概率', + category: '条件', + type: NodeType.Condition, + icon: 'Dice', + description: '根据概率返回成功或失败', + color: '#E91E63' +}) +@ECSComponent('RandomProbabilityCondition') +@Serializable({ version: 1 }) +export class RandomProbabilityCondition extends Component { + @BehaviorProperty({ + label: '成功概率', + type: 'number', + min: 0, + max: 1, + step: 0.1, + description: '0.0 - 1.0', + required: true + }) + @Serialize() + probability: number = 0.5; + + @BehaviorProperty({ + label: '总是重新随机', + type: 'boolean', + description: 'false则第一次随机后固定结果' + }) + @Serialize() + alwaysRandomize: boolean = true; + + /** 缓存的随机结果(不序列化) */ + private cachedResult?: boolean; + + /** + * 评估随机概率 + */ + evaluate(): boolean { + if (this.alwaysRandomize || this.cachedResult === undefined) { + this.cachedResult = Math.random() < this.probability; + } + return this.cachedResult; + } + + /** + * 重置缓存 + */ + reset(): void { + this.cachedResult = undefined; + } +} diff --git a/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts b/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts new file mode 100644 index 00000000..14e92898 --- /dev/null +++ b/packages/behavior-tree/src/Components/DecoratorNodeComponent.ts @@ -0,0 +1,18 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize } from '@esengine/ecs-framework'; +import { DecoratorType } from '../Types/TaskStatus'; + +/** + * 装饰器节点组件基类 + * + * 只包含通用的装饰器类型标识 + * 具体的属性由各个子类自己定义 + */ +@ECSComponent('DecoratorNode') +@Serializable({ version: 1 }) +export class DecoratorNodeComponent extends Component { + /** 装饰器类型 */ + @Serialize() + decoratorType: DecoratorType = DecoratorType.Inverter; + +} diff --git a/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts b/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts new file mode 100644 index 00000000..e3dde040 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/AlwaysFailNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 总是失败节点 + * + * 无论子节点结果如何都返回失败 + */ +@BehaviorNode({ + displayName: '总是失败', + category: '装饰器', + type: NodeType.Decorator, + icon: 'ThumbsDown', + description: '无论子节点结果如何都返回失败', + color: '#FF5722' +}) +@ECSComponent('AlwaysFailNode') +@Serializable({ version: 1 }) +export class AlwaysFailNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.AlwaysFail; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts b/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts new file mode 100644 index 00000000..b276ce0b --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/AlwaysSucceedNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 总是成功节点 + * + * 无论子节点结果如何都返回成功 + */ +@BehaviorNode({ + displayName: '总是成功', + category: '装饰器', + type: NodeType.Decorator, + icon: 'ThumbsUp', + description: '无论子节点结果如何都返回成功', + color: '#8BC34A' +}) +@ECSComponent('AlwaysSucceedNode') +@Serializable({ version: 1 }) +export class AlwaysSucceedNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.AlwaysSucceed; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts b/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts new file mode 100644 index 00000000..f8434987 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/ConditionalNode.ts @@ -0,0 +1,89 @@ +import { ECSComponent, Entity } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; +import { BlackboardComponent } from '../BlackboardComponent'; + +/** + * 条件装饰器节点 + * + * 基于条件判断是否执行子节点 + */ +@BehaviorNode({ + displayName: '条件装饰器', + category: '装饰器', + type: NodeType.Decorator, + icon: 'Filter', + description: '基于条件判断是否执行子节点', + color: '#3F51B5' +}) +@ECSComponent('ConditionalNode') +@Serializable({ version: 1 }) +export class ConditionalNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.Conditional; + } + + @BehaviorProperty({ + label: '条件代码', + type: 'code', + description: 'JavaScript 代码,返回 boolean', + required: true + }) + @Serialize() + conditionCode?: string; + + @BehaviorProperty({ + label: '重新评估条件', + type: 'boolean', + description: '每次执行时是否重新评估条件' + }) + @Serialize() + shouldReevaluate: boolean = true; + + /** 编译后的条件函数(不序列化) */ + @IgnoreSerialization() + private compiledCondition?: (entity: Entity, blackboard?: BlackboardComponent) => boolean; + + /** + * 评估条件 + */ + evaluateCondition(entity: Entity, blackboard?: BlackboardComponent): boolean { + if (!this.conditionCode) { + return false; + } + + if (!this.compiledCondition) { + try { + const func = new Function( + 'entity', + 'blackboard', + ` + try { + return Boolean(${this.conditionCode}); + } catch (error) { + return false; + } + ` + ); + + this.compiledCondition = (entity, blackboard) => { + return Boolean(func(entity, blackboard)); + }; + } catch (error) { + return false; + } + } + + return this.compiledCondition(entity, blackboard); + } + + /** + * 设置条件函数(运行时使用) + */ + setConditionFunction(func: (entity: Entity, blackboard?: BlackboardComponent) => boolean): void { + this.compiledCondition = func; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts b/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts new file mode 100644 index 00000000..01287be3 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/CooldownNode.ts @@ -0,0 +1,67 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 冷却节点 + * + * 在冷却时间内阻止子节点执行 + */ +@BehaviorNode({ + displayName: '冷却', + category: '装饰器', + type: NodeType.Decorator, + icon: 'Timer', + description: '在冷却时间内阻止子节点执行', + color: '#00BCD4' +}) +@ECSComponent('CooldownNode') +@Serializable({ version: 1 }) +export class CooldownNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.Cooldown; + } + + @BehaviorProperty({ + label: '冷却时间', + type: 'number', + min: 0, + step: 0.1, + description: '冷却时间(秒)', + required: true + }) + @Serialize() + cooldownTime: number = 1.0; + + /** 上次执行时间 */ + @IgnoreSerialization() + lastExecutionTime: number = 0; + + /** + * 检查是否可以执行 + */ + canExecute(currentTime: number): boolean { + // 如果从未执行过,允许执行 + if (this.lastExecutionTime === 0) { + return true; + } + return currentTime - this.lastExecutionTime >= this.cooldownTime; + } + + /** + * 记录执行时间 + */ + recordExecution(currentTime: number): void { + this.lastExecutionTime = currentTime; + } + + /** + * 重置状态 + */ + reset(): void { + this.lastExecutionTime = 0; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/InverterNode.ts b/packages/behavior-tree/src/Components/Decorators/InverterNode.ts new file mode 100644 index 00000000..d13cea6d --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/InverterNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 反转节点 + * + * 反转子节点的执行结果 + */ +@BehaviorNode({ + displayName: '反转', + category: '装饰器', + type: NodeType.Decorator, + icon: 'RotateCcw', + description: '反转子节点的执行结果', + color: '#607D8B' +}) +@ECSComponent('InverterNode') +@Serializable({ version: 1 }) +export class InverterNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.Inverter; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts b/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts new file mode 100644 index 00000000..7e05b598 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/RepeaterNode.ts @@ -0,0 +1,74 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 重复节点 + * + * 重复执行子节点指定次数 + */ +@BehaviorNode({ + displayName: '重复', + category: '装饰器', + type: NodeType.Decorator, + icon: 'Repeat', + description: '重复执行子节点指定次数', + color: '#9E9E9E' +}) +@ECSComponent('RepeaterNode') +@Serializable({ version: 1 }) +export class RepeaterNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.Repeater; + } + + @BehaviorProperty({ + label: '重复次数', + type: 'number', + min: -1, + step: 1, + description: '-1表示无限重复', + required: true + }) + @Serialize() + repeatCount: number = 1; + + @BehaviorProperty({ + label: '失败时停止', + type: 'boolean', + description: '子节点失败时是否停止重复' + }) + @Serialize() + endOnFailure: boolean = false; + + /** 当前已重复次数 */ + @IgnoreSerialization() + currentRepeatCount: number = 0; + + /** + * 增加重复计数 + */ + incrementRepeat(): void { + this.currentRepeatCount++; + } + + /** + * 检查是否应该继续重复 + */ + shouldContinueRepeat(): boolean { + if (this.repeatCount === -1) { + return true; + } + return this.currentRepeatCount < this.repeatCount; + } + + /** + * 重置状态 + */ + reset(): void { + this.currentRepeatCount = 0; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts b/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts new file mode 100644 index 00000000..737d2c07 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/TimeoutNode.ts @@ -0,0 +1,68 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable, Serialize, IgnoreSerialization } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode, BehaviorProperty } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 超时节点 + * + * 子节点执行超时则返回失败 + */ +@BehaviorNode({ + displayName: '超时', + category: '装饰器', + type: NodeType.Decorator, + icon: 'Clock', + description: '子节点执行超时则返回失败', + color: '#FF9800' +}) +@ECSComponent('TimeoutNode') +@Serializable({ version: 1 }) +export class TimeoutNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.Timeout; + } + + @BehaviorProperty({ + label: '超时时间', + type: 'number', + min: 0, + step: 0.1, + description: '超时时间(秒)', + required: true + }) + @Serialize() + timeoutDuration: number = 5.0; + + /** 开始执行时间 */ + @IgnoreSerialization() + startTime: number = 0; + + /** + * 记录开始时间 + */ + recordStartTime(currentTime: number): void { + if (this.startTime === 0) { + this.startTime = currentTime; + } + } + + /** + * 检查是否超时 + */ + isTimeout(currentTime: number): boolean { + if (this.startTime === 0) { + return false; + } + return currentTime - this.startTime >= this.timeoutDuration; + } + + /** + * 重置状态 + */ + reset(): void { + this.startTime = 0; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts b/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts new file mode 100644 index 00000000..133d88b3 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/UntilFailNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 直到失败节点 + * + * 重复执行子节点直到失败 + */ +@BehaviorNode({ + displayName: '直到失败', + category: '装饰器', + type: NodeType.Decorator, + icon: 'XCircle', + description: '重复执行子节点直到失败', + color: '#F44336' +}) +@ECSComponent('UntilFailNode') +@Serializable({ version: 1 }) +export class UntilFailNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.UntilFail; + } +} diff --git a/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts b/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts new file mode 100644 index 00000000..6fcbe988 --- /dev/null +++ b/packages/behavior-tree/src/Components/Decorators/UntilSuccessNode.ts @@ -0,0 +1,27 @@ +import { ECSComponent } from '@esengine/ecs-framework'; +import { Serializable } from '@esengine/ecs-framework'; +import { NodeType, DecoratorType } from '../../Types/TaskStatus'; +import { BehaviorNode } from '../../Decorators/BehaviorNodeDecorator'; +import { DecoratorNodeComponent } from '../DecoratorNodeComponent'; + +/** + * 直到成功节点 + * + * 重复执行子节点直到成功 + */ +@BehaviorNode({ + displayName: '直到成功', + category: '装饰器', + type: NodeType.Decorator, + icon: 'CheckCircle', + description: '重复执行子节点直到成功', + color: '#4CAF50' +}) +@ECSComponent('UntilSuccessNode') +@Serializable({ version: 1 }) +export class UntilSuccessNode extends DecoratorNodeComponent { + constructor() { + super(); + this.decoratorType = DecoratorType.UntilSuccess; + } +} diff --git a/packages/behavior-tree/src/Components/LogOutput.ts b/packages/behavior-tree/src/Components/LogOutput.ts new file mode 100644 index 00000000..7232f0dc --- /dev/null +++ b/packages/behavior-tree/src/Components/LogOutput.ts @@ -0,0 +1,36 @@ +import { Component, ECSComponent } from '@esengine/ecs-framework'; + +/** + * 日志输出组件 + * + * 存储运行时输出的日志信息,用于在UI中显示 + */ +@ECSComponent('LogOutput') +export class LogOutput extends Component { + /** + * 日志消息列表 + */ + messages: Array<{ + timestamp: number; + message: string; + level: 'log' | 'info' | 'warn' | 'error'; + }> = []; + + /** + * 添加日志消息 + */ + addMessage(message: string, level: 'log' | 'info' | 'warn' | 'error' = 'log'): void { + this.messages.push({ + timestamp: Date.now(), + message, + level + }); + } + + /** + * 清空日志 + */ + clear(): void { + this.messages = []; + } +} diff --git a/packages/behavior-tree/src/Components/PropertyBindings.ts b/packages/behavior-tree/src/Components/PropertyBindings.ts new file mode 100644 index 00000000..bb34c170 --- /dev/null +++ b/packages/behavior-tree/src/Components/PropertyBindings.ts @@ -0,0 +1,42 @@ +import { Component } from '@esengine/ecs-framework'; + +/** + * 属性绑定组件 + * 记录节点属性到黑板变量的绑定关系 + */ +export class PropertyBindings extends Component { + /** + * 属性绑定映射 + * key: 属性名称 (如 'message') + * value: 黑板变量名 (如 'test1') + */ + bindings: Map = new Map(); + + /** + * 添加属性绑定 + */ + addBinding(propertyName: string, blackboardKey: string): void { + this.bindings.set(propertyName, blackboardKey); + } + + /** + * 获取属性绑定的黑板变量名 + */ + getBinding(propertyName: string): string | undefined { + return this.bindings.get(propertyName); + } + + /** + * 检查属性是否绑定到黑板变量 + */ + hasBinding(propertyName: string): boolean { + return this.bindings.has(propertyName); + } + + /** + * 清除所有绑定 + */ + clearBindings(): void { + this.bindings.clear(); + } +} diff --git a/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts b/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts new file mode 100644 index 00000000..a10e10d0 --- /dev/null +++ b/packages/behavior-tree/src/Decorators/BehaviorNodeDecorator.ts @@ -0,0 +1,190 @@ +import { NodeTemplate, PropertyDefinition } from '../Serialization/NodeTemplates'; +import { NodeType } from '../Types/TaskStatus'; + +/** + * 行为树节点元数据 + */ +export interface BehaviorNodeMetadata { + displayName: string; + category: string; + type: NodeType; + icon?: string; + description: string; + color?: string; + className?: string; + /** + * 是否需要子节点 + * - true: 节点需要子节点(如 SequenceNode、DecoratorNode) + * - false: 节点不需要子节点(如 ActionNode、SubTreeNode) + * - undefined: 根据节点类型自动判断 + */ + requiresChildren?: boolean; +} + +/** + * 节点类注册表 + */ +class NodeClassRegistry { + private static nodeClasses = new Map(); + + static registerNodeClass(constructor: any, metadata: BehaviorNodeMetadata): void { + const key = `${metadata.category}:${metadata.displayName}`; + this.nodeClasses.set(key, { metadata, constructor }); + } + + static getAllNodeClasses(): Array<{ metadata: BehaviorNodeMetadata; constructor: any }> { + return Array.from(this.nodeClasses.values()); + } + + static getNodeClass(category: string, displayName: string): any { + const key = `${category}:${displayName}`; + return this.nodeClasses.get(key)?.constructor; + } + + static clear(): void { + this.nodeClasses.clear(); + } +} + +/** + * 行为树节点装饰器 + * + * 用于标注一个类是可在编辑器中使用的行为树节点 + * + * @example + * ```typescript + * @BehaviorNode({ + * displayName: '等待', + * category: '动作', + * type: NodeType.Action, + * icon: 'Clock', + * description: '等待指定时间', + * color: '#9E9E9E' + * }) + * class WaitNode extends Component { + * @BehaviorProperty({ + * label: '持续时间', + * type: 'number', + * min: 0, + * step: 0.1, + * description: '等待时间(秒)' + * }) + * duration: number = 1.0; + * } + * ``` + */ +export function BehaviorNode(metadata: BehaviorNodeMetadata) { + return function (constructor: T) { + const metadataWithClassName = { + ...metadata, + className: constructor.name + }; + NodeClassRegistry.registerNodeClass(constructor, metadataWithClassName); + return constructor; + }; +} + +/** + * 行为树属性装饰器 + * + * 用于标注节点的可配置属性,这些属性会在编辑器中显示 + * + * @example + * ```typescript + * @BehaviorNode({ ... }) + * class MyNode { + * @BehaviorProperty({ + * label: '速度', + * type: 'number', + * min: 0, + * max: 100, + * description: '移动速度' + * }) + * speed: number = 10; + * } + * ``` + */ +export function BehaviorProperty(config: Omit) { + return function (target: any, propertyKey: string) { + if (!target.constructor.__nodeProperties) { + target.constructor.__nodeProperties = []; + } + target.constructor.__nodeProperties.push({ + name: propertyKey, + ...config + }); + }; +} + +/** + * @deprecated 使用 BehaviorProperty 代替 + */ +export const NodeProperty = BehaviorProperty; + +/** + * 获取所有注册的节点模板 + */ +export function getRegisteredNodeTemplates(): NodeTemplate[] { + return NodeClassRegistry.getAllNodeClasses().map(({ metadata, constructor }) => { + // 从类的 __nodeProperties 收集属性定义 + const propertyDefs = constructor.__nodeProperties || []; + + const defaultConfig: any = { + nodeType: metadata.type.toLowerCase() + }; + + // 从类的默认值中提取配置,并补充 defaultValue + const instance = new constructor(); + const properties: PropertyDefinition[] = propertyDefs.map((prop: PropertyDefinition) => { + const defaultValue = instance[prop.name]; + if (defaultValue !== undefined) { + defaultConfig[prop.name] = defaultValue; + } + return { + ...prop, + defaultValue: defaultValue !== undefined ? defaultValue : prop.defaultValue + }; + }); + + // 添加子类型字段 + switch (metadata.type) { + case NodeType.Composite: + defaultConfig.compositeType = metadata.displayName; + break; + case NodeType.Decorator: + defaultConfig.decoratorType = metadata.displayName; + break; + case NodeType.Action: + defaultConfig.actionType = metadata.displayName; + break; + case NodeType.Condition: + defaultConfig.conditionType = metadata.displayName; + break; + } + + return { + type: metadata.type, + displayName: metadata.displayName, + category: metadata.category, + icon: metadata.icon, + description: metadata.description, + color: metadata.color, + className: metadata.className, + requiresChildren: metadata.requiresChildren, + defaultConfig, + properties + }; + }); +} + +/** + * 清空所有注册的节点类 + */ +export function clearRegisteredNodes(): void { + NodeClassRegistry.clear(); +} + +export { NodeClassRegistry }; diff --git a/packages/behavior-tree/src/RegisterAllNodes.ts b/packages/behavior-tree/src/RegisterAllNodes.ts new file mode 100644 index 00000000..dc697d36 --- /dev/null +++ b/packages/behavior-tree/src/RegisterAllNodes.ts @@ -0,0 +1,45 @@ +/** + * 注册所有内置节点 + * + * 导入所有节点类以确保装饰器被执行 + */ + +// Actions +import './Components/Actions/ExecuteAction'; +import './Components/Actions/WaitAction'; +import './Components/Actions/LogAction'; +import './Components/Actions/SetBlackboardValueAction'; +import './Components/Actions/ModifyBlackboardValueAction'; + +// Conditions +import './Components/Conditions/BlackboardCompareCondition'; +import './Components/Conditions/BlackboardExistsCondition'; +import './Components/Conditions/RandomProbabilityCondition'; +import './Components/Conditions/ExecuteCondition'; + +// Composites +import './Components/Composites/SequenceNode'; +import './Components/Composites/SelectorNode'; +import './Components/Composites/ParallelNode'; +import './Components/Composites/ParallelSelectorNode'; +import './Components/Composites/RandomSequenceNode'; +import './Components/Composites/RandomSelectorNode'; +import './Components/Composites/SubTreeNode'; + +// Decorators +import './Components/Decorators/InverterNode'; +import './Components/Decorators/RepeaterNode'; +import './Components/Decorators/UntilSuccessNode'; +import './Components/Decorators/UntilFailNode'; +import './Components/Decorators/AlwaysSucceedNode'; +import './Components/Decorators/AlwaysFailNode'; +import './Components/Decorators/ConditionalNode'; +import './Components/Decorators/CooldownNode'; +import './Components/Decorators/TimeoutNode'; + +/** + * 确保所有节点已注册 + */ +export function ensureAllNodesRegistered(): void { + // 这个函数的调用会确保上面的 import 被执行 +} diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts new file mode 100644 index 00000000..8b21a1cf --- /dev/null +++ b/packages/behavior-tree/src/Serialization/BehaviorTreeAsset.ts @@ -0,0 +1,287 @@ +import { NodeType, BlackboardValueType } from '../Types/TaskStatus'; + +/** + * 行为树资产元数据 + */ +export interface AssetMetadata { + name: string; + description?: string; + version: string; + createdAt?: string; + modifiedAt?: string; +} + +/** + * 黑板变量定义 + */ +export interface BlackboardVariableDefinition { + name: string; + type: BlackboardValueType; + defaultValue: any; + readonly?: boolean; + description?: string; +} + +/** + * 行为树节点数据(运行时格式) + */ +export interface BehaviorTreeNodeData { + id: string; + name: string; + nodeType: NodeType; + + // 节点类型特定数据 + data: Record; + + // 子节点ID列表 + children: string[]; +} + +/** + * 属性绑定定义 + */ +export interface PropertyBinding { + nodeId: string; + propertyName: string; + variableName: string; +} + +/** + * 行为树资产(运行时格式) + * + * 这是用于游戏运行时的优化格式,不包含编辑器UI信息 + */ +export interface BehaviorTreeAsset { + /** + * 资产格式版本 + */ + version: string; + + /** + * 元数据 + */ + metadata: AssetMetadata; + + /** + * 根节点ID + */ + rootNodeId: string; + + /** + * 所有节点数据(扁平化存储,通过children建立层级) + */ + nodes: BehaviorTreeNodeData[]; + + /** + * 黑板变量定义 + */ + blackboard: BlackboardVariableDefinition[]; + + /** + * 属性绑定 + */ + propertyBindings?: PropertyBinding[]; +} + +/** + * 资产验证结果 + */ +export interface AssetValidationResult { + valid: boolean; + errors?: string[]; + warnings?: string[]; +} + +/** + * 资产验证器 + */ +export class BehaviorTreeAssetValidator { + /** + * 验证资产数据的完整性和正确性 + */ + static validate(asset: BehaviorTreeAsset): AssetValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + // 检查版本 + if (!asset.version) { + errors.push('Missing version field'); + } + + // 检查元数据 + if (!asset.metadata || !asset.metadata.name) { + errors.push('Missing or invalid metadata'); + } + + // 检查根节点 + if (!asset.rootNodeId) { + errors.push('Missing rootNodeId'); + } + + // 检查节点列表 + if (!asset.nodes || !Array.isArray(asset.nodes)) { + errors.push('Missing or invalid nodes array'); + } else { + const nodeIds = new Set(); + const rootNode = asset.nodes.find(n => n.id === asset.rootNodeId); + + if (!rootNode) { + errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`); + } + + // 检查节点ID唯一性 + for (const node of asset.nodes) { + if (!node.id) { + errors.push('Node missing id field'); + continue; + } + + if (nodeIds.has(node.id)) { + errors.push(`Duplicate node id: ${node.id}`); + } + nodeIds.add(node.id); + + // 检查节点类型 + if (!node.nodeType) { + errors.push(`Node ${node.id} missing nodeType`); + } + + // 检查子节点引用 + if (node.children) { + for (const childId of node.children) { + if (!asset.nodes.find(n => n.id === childId)) { + errors.push(`Node ${node.id} references non-existent child: ${childId}`); + } + } + } + } + + // 检查是否有孤立节点 + const referencedNodes = new Set([asset.rootNodeId]); + const collectReferencedNodes = (nodeId: string) => { + const node = asset.nodes.find(n => n.id === nodeId); + if (node && node.children) { + for (const childId of node.children) { + referencedNodes.add(childId); + collectReferencedNodes(childId); + } + } + }; + collectReferencedNodes(asset.rootNodeId); + + for (const node of asset.nodes) { + if (!referencedNodes.has(node.id)) { + warnings.push(`Orphaned node detected: ${node.id} (${node.name})`); + } + } + } + + // 检查黑板定义 + if (asset.blackboard && Array.isArray(asset.blackboard)) { + const varNames = new Set(); + for (const variable of asset.blackboard) { + if (!variable.name) { + errors.push('Blackboard variable missing name'); + continue; + } + + if (varNames.has(variable.name)) { + errors.push(`Duplicate blackboard variable: ${variable.name}`); + } + varNames.add(variable.name); + + if (!variable.type) { + errors.push(`Blackboard variable ${variable.name} missing type`); + } + } + } + + // 检查属性绑定 + if (asset.propertyBindings && Array.isArray(asset.propertyBindings)) { + const nodeIds = new Set(asset.nodes.map(n => n.id)); + const varNames = new Set(asset.blackboard?.map(v => v.name) || []); + + for (const binding of asset.propertyBindings) { + if (!nodeIds.has(binding.nodeId)) { + errors.push(`Property binding references non-existent node: ${binding.nodeId}`); + } + + if (!varNames.has(binding.variableName)) { + errors.push(`Property binding references non-existent variable: ${binding.variableName}`); + } + + if (!binding.propertyName) { + errors.push('Property binding missing propertyName'); + } + } + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined, + warnings: warnings.length > 0 ? warnings : undefined + }; + } + + /** + * 获取资产统计信息 + */ + static getStats(asset: BehaviorTreeAsset): { + nodeCount: number; + actionCount: number; + conditionCount: number; + compositeCount: number; + decoratorCount: number; + blackboardVariableCount: number; + propertyBindingCount: number; + maxDepth: number; + } { + let actionCount = 0; + let conditionCount = 0; + let compositeCount = 0; + let decoratorCount = 0; + + for (const node of asset.nodes) { + switch (node.nodeType) { + case NodeType.Action: + actionCount++; + break; + case NodeType.Condition: + conditionCount++; + break; + case NodeType.Composite: + compositeCount++; + break; + case NodeType.Decorator: + decoratorCount++; + break; + } + } + + // 计算最大深度 + const getDepth = (nodeId: string, currentDepth: number = 0): number => { + const node = asset.nodes.find(n => n.id === nodeId); + if (!node || !node.children || node.children.length === 0) { + return currentDepth; + } + + let maxChildDepth = currentDepth; + for (const childId of node.children) { + const childDepth = getDepth(childId, currentDepth + 1); + maxChildDepth = Math.max(maxChildDepth, childDepth); + } + return maxChildDepth; + }; + + return { + nodeCount: asset.nodes.length, + actionCount, + conditionCount, + compositeCount, + decoratorCount, + blackboardVariableCount: asset.blackboard?.length || 0, + propertyBindingCount: asset.propertyBindings?.length || 0, + maxDepth: getDepth(asset.rootNodeId) + }; + } +} diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts new file mode 100644 index 00000000..8a6aff14 --- /dev/null +++ b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetLoader.ts @@ -0,0 +1,396 @@ +import { Entity, IScene, createLogger } from '@esengine/ecs-framework'; +import type { BehaviorTreeAsset, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; +import { PropertyBindings } from '../Components/PropertyBindings'; +import { NodeType } from '../Types/TaskStatus'; + +// 导入所有节点组件 +import { RootNode } from '../Components/Composites/RootNode'; +import { SequenceNode } from '../Components/Composites/SequenceNode'; +import { SelectorNode } from '../Components/Composites/SelectorNode'; +import { ParallelNode } from '../Components/Composites/ParallelNode'; +import { ParallelSelectorNode } from '../Components/Composites/ParallelSelectorNode'; +import { RandomSequenceNode } from '../Components/Composites/RandomSequenceNode'; +import { RandomSelectorNode } from '../Components/Composites/RandomSelectorNode'; + +import { InverterNode } from '../Components/Decorators/InverterNode'; +import { RepeaterNode } from '../Components/Decorators/RepeaterNode'; +import { UntilSuccessNode } from '../Components/Decorators/UntilSuccessNode'; +import { UntilFailNode } from '../Components/Decorators/UntilFailNode'; +import { AlwaysSucceedNode } from '../Components/Decorators/AlwaysSucceedNode'; +import { AlwaysFailNode } from '../Components/Decorators/AlwaysFailNode'; +import { ConditionalNode } from '../Components/Decorators/ConditionalNode'; +import { CooldownNode } from '../Components/Decorators/CooldownNode'; +import { TimeoutNode } from '../Components/Decorators/TimeoutNode'; + +import { WaitAction } from '../Components/Actions/WaitAction'; +import { LogAction } from '../Components/Actions/LogAction'; +import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction'; +import { ModifyBlackboardValueAction } from '../Components/Actions/ModifyBlackboardValueAction'; +import { ExecuteAction } from '../Components/Actions/ExecuteAction'; + +import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition'; +import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition'; +import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition'; +import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition'; +import { AbortType } from '../Types/TaskStatus'; + +const logger = createLogger('BehaviorTreeAssetLoader'); + +/** + * 实例化选项 + */ +export interface InstantiateOptions { + /** + * 实体名称前缀 + */ + namePrefix?: string; + + /** + * 是否共享黑板(如果为true,将使用全局黑板服务) + */ + sharedBlackboard?: boolean; + + /** + * 黑板变量覆盖(用于运行时动态设置初始值) + */ + blackboardOverrides?: Record; + + /** + * 是否作为子树实例化 + * 如果为 true,根节点不会添加 RootNode 组件,避免触发预加载逻辑 + */ + asSubTree?: boolean; +} + +/** + * 行为树资产加载器 + * + * 将BehaviorTreeAsset实例化为可运行的Entity树 + */ +export class BehaviorTreeAssetLoader { + /** + * 从资产实例化行为树 + * + * @param asset 行为树资产 + * @param scene 目标场景 + * @param options 实例化选项 + * @returns 根实体 + * + * @example + * ```typescript + * const asset = await loadAssetFromFile('enemy-ai.btree.bin'); + * const aiRoot = BehaviorTreeAssetLoader.instantiate(asset, scene); + * BehaviorTreeStarter.start(aiRoot); + * ``` + */ + static instantiate( + asset: BehaviorTreeAsset, + scene: IScene, + options: InstantiateOptions = {} + ): Entity { + logger.info(`开始实例化行为树: ${asset.metadata.name}`); + + // 创建节点映射 + const nodeMap = new Map(); + for (const node of asset.nodes) { + nodeMap.set(node.id, node); + } + + // 查找根节点 + const rootNodeData = nodeMap.get(asset.rootNodeId); + if (!rootNodeData) { + throw new Error(`未找到根节点: ${asset.rootNodeId}`); + } + + // 创建实体映射 + const entityMap = new Map(); + + // 递归创建实体树 + const rootEntity = this.createEntityTree( + rootNodeData, + nodeMap, + entityMap, + scene, + options.namePrefix, + options.asSubTree + ); + + // 添加黑板 + this.setupBlackboard(rootEntity, asset.blackboard, options.blackboardOverrides); + + // 设置属性绑定 + if (asset.propertyBindings && asset.propertyBindings.length > 0) { + this.setupPropertyBindings(asset.propertyBindings, entityMap); + } + + logger.info(`行为树实例化完成: ${asset.nodes.length} 个节点`); + + return rootEntity; + } + + /** + * 递归创建实体树 + */ + private static createEntityTree( + nodeData: BehaviorTreeNodeData, + nodeMap: Map, + entityMap: Map, + scene: IScene, + namePrefix?: string, + asSubTree?: boolean, + isRootOfSubTree: boolean = true + ): Entity { + const entityName = namePrefix ? `${namePrefix}_${nodeData.name}` : nodeData.name; + const entity = scene.createEntity(entityName); + + // 记录实体 + entityMap.set(nodeData.id, entity); + + // 添加BehaviorTreeNode组件 + const btNode = entity.addComponent(new BehaviorTreeNode()); + btNode.nodeType = nodeData.nodeType; + btNode.nodeName = nodeData.name; + + // 添加节点特定组件(如果是子树的根节点,跳过 RootNode) + this.addNodeComponents(entity, nodeData, asSubTree && isRootOfSubTree); + + // 递归创建子节点 + for (const childId of nodeData.children) { + const childData = nodeMap.get(childId); + if (!childData) { + logger.warn(`子节点未找到: ${childId}`); + continue; + } + + const childEntity = this.createEntityTree( + childData, + nodeMap, + entityMap, + scene, + namePrefix, + asSubTree, + false // 子节点不是根节点 + ); + entity.addChild(childEntity); + } + + return entity; + } + + /** + * 添加节点特定组件 + * @param skipRootNode 是否跳过添加 RootNode 组件(用于子树) + */ + private static addNodeComponents(entity: Entity, nodeData: BehaviorTreeNodeData, skipRootNode: boolean = false): void { + const { nodeType, data, name } = nodeData; + + logger.debug(`addNodeComponents: name=${name}, data.nodeType=${data.nodeType}, skipRootNode=${skipRootNode}`); + + // 根据节点类型和名称添加对应组件 + if (data.nodeType === 'root' || name === '根节点' || name === 'Root') { + if (!skipRootNode) { + logger.debug(`添加 RootNode 组件: ${name}`); + entity.addComponent(new RootNode()); + } else { + // 子树的根节点,使用第一个子节点的类型(通常是 SequenceNode) + logger.debug(`跳过为子树根节点添加 RootNode: ${name}`); + // 添加一个默认的 SequenceNode 作为子树的根 + this.addCompositeComponent(entity, '序列', data); + } + } + // 组合节点 + else if (nodeType === NodeType.Composite) { + this.addCompositeComponent(entity, name, data); + } + // 装饰器节点 + else if (nodeType === NodeType.Decorator) { + this.addDecoratorComponent(entity, name, data); + } + // 动作节点 + else if (nodeType === NodeType.Action) { + this.addActionComponent(entity, name, data); + } + // 条件节点 + else if (nodeType === NodeType.Condition) { + this.addConditionComponent(entity, name, data); + } + } + + /** + * 添加组合节点组件 + */ + private static addCompositeComponent(entity: Entity, name: string, data: Record): void { + const nameLower = name.toLowerCase(); + + if (nameLower.includes('sequence') || nameLower.includes('序列')) { + const node = entity.addComponent(new SequenceNode()); + node.abortType = (data.abortType as AbortType) ?? AbortType.None; + } else if (nameLower.includes('selector') || nameLower.includes('选择')) { + const node = entity.addComponent(new SelectorNode()); + node.abortType = (data.abortType as AbortType) ?? AbortType.None; + } else if (nameLower.includes('parallelselector') || nameLower.includes('并行选择')) { + const node = entity.addComponent(new ParallelSelectorNode()); + node.failurePolicy = data.failurePolicy ?? 'one'; + } else if (nameLower.includes('parallel') || nameLower.includes('并行')) { + const node = entity.addComponent(new ParallelNode()); + node.successPolicy = data.successPolicy ?? 'all'; + node.failurePolicy = data.failurePolicy ?? 'one'; + } else if (nameLower.includes('randomsequence') || nameLower.includes('随机序列')) { + entity.addComponent(new RandomSequenceNode()); + } else if (nameLower.includes('randomselector') || nameLower.includes('随机选择')) { + entity.addComponent(new RandomSelectorNode()); + } else { + logger.warn(`未知的组合节点类型: ${name}`); + } + } + + /** + * 添加装饰器组件 + */ + private static addDecoratorComponent(entity: Entity, name: string, data: Record): void { + const nameLower = name.toLowerCase(); + + if (nameLower.includes('inverter') || nameLower.includes('反转')) { + entity.addComponent(new InverterNode()); + } else if (nameLower.includes('repeater') || nameLower.includes('重复')) { + const node = entity.addComponent(new RepeaterNode()); + node.repeatCount = data.repeatCount ?? -1; + node.endOnFailure = data.endOnFailure ?? false; + } else if (nameLower.includes('untilsuccess') || nameLower.includes('直到成功')) { + entity.addComponent(new UntilSuccessNode()); + } else if (nameLower.includes('untilfail') || nameLower.includes('直到失败')) { + entity.addComponent(new UntilFailNode()); + } else if (nameLower.includes('alwayssucceed') || nameLower.includes('总是成功')) { + entity.addComponent(new AlwaysSucceedNode()); + } else if (nameLower.includes('alwaysfail') || nameLower.includes('总是失败')) { + entity.addComponent(new AlwaysFailNode()); + } else if (nameLower.includes('conditional') || nameLower.includes('条件装饰')) { + const node = entity.addComponent(new ConditionalNode()); + node.conditionCode = data.conditionCode ?? ''; + node.shouldReevaluate = data.shouldReevaluate ?? true; + } else if (nameLower.includes('cooldown') || nameLower.includes('冷却')) { + const node = entity.addComponent(new CooldownNode()); + node.cooldownTime = data.cooldownTime ?? 1.0; + } else if (nameLower.includes('timeout') || nameLower.includes('超时')) { + const node = entity.addComponent(new TimeoutNode()); + node.timeoutDuration = data.timeoutDuration ?? 1.0; + } else { + logger.warn(`未知的装饰器类型: ${name}`); + } + } + + /** + * 添加动作组件 + */ + private static addActionComponent(entity: Entity, name: string, data: Record): void { + const nameLower = name.toLowerCase(); + + if (nameLower.includes('wait') || nameLower.includes('等待')) { + const action = entity.addComponent(new WaitAction()); + action.waitTime = data.waitTime ?? 1.0; + } else if (nameLower.includes('log') || nameLower.includes('日志')) { + const action = entity.addComponent(new LogAction()); + action.message = data.message ?? ''; + action.level = data.level ?? 'log'; + } else if (nameLower.includes('setblackboard') || nameLower.includes('setvalue') || nameLower.includes('设置变量')) { + const action = entity.addComponent(new SetBlackboardValueAction()); + action.variableName = data.variableName ?? ''; + action.value = data.value; + } else if (nameLower.includes('modifyblackboard') || nameLower.includes('modifyvalue') || nameLower.includes('修改变量')) { + const action = entity.addComponent(new ModifyBlackboardValueAction()); + action.variableName = data.variableName ?? ''; + action.operation = data.operation ?? 'add'; + action.operand = data.operand ?? 0; + } else if (nameLower.includes('execute') || nameLower.includes('自定义')) { + const action = entity.addComponent(new ExecuteAction()); + action.actionCode = data.actionCode ?? 'return TaskStatus.Success;'; + } else { + logger.warn(`未知的动作类型: ${name}`); + } + } + + /** + * 添加条件组件 + */ + private static addConditionComponent(entity: Entity, name: string, data: Record): void { + const nameLower = name.toLowerCase(); + + if (nameLower.includes('compare') || nameLower.includes('比较变量')) { + const condition = entity.addComponent(new BlackboardCompareCondition()); + condition.variableName = data.variableName ?? ''; + condition.operator = (data.operator as CompareOperator) ?? CompareOperator.Equal; + condition.compareValue = data.compareValue; + condition.invertResult = data.invertResult ?? false; + } else if (nameLower.includes('exists') || nameLower.includes('变量存在')) { + const condition = entity.addComponent(new BlackboardExistsCondition()); + condition.variableName = data.variableName ?? ''; + condition.checkNotNull = data.checkNotNull ?? false; + condition.invertResult = data.invertResult ?? false; + } else if (nameLower.includes('random') || nameLower.includes('概率')) { + const condition = entity.addComponent(new RandomProbabilityCondition()); + condition.probability = data.probability ?? 0.5; + } else if (nameLower.includes('execute') || nameLower.includes('执行条件')) { + const condition = entity.addComponent(new ExecuteCondition()); + condition.conditionCode = data.conditionCode ?? ''; + condition.invertResult = data.invertResult ?? false; + } else { + logger.warn(`未知的条件类型: ${name}`); + } + } + + /** + * 设置黑板 + */ + private static setupBlackboard( + rootEntity: Entity, + blackboardDef: BlackboardVariableDefinition[], + overrides?: Record + ): void { + const blackboard = rootEntity.addComponent(new BlackboardComponent()); + + for (const variable of blackboardDef) { + const value = overrides && overrides[variable.name] !== undefined + ? overrides[variable.name] + : variable.defaultValue; + + blackboard.defineVariable( + variable.name, + variable.type, + value, + { + readonly: variable.readonly, + description: variable.description + } + ); + } + + logger.info(`已设置黑板: ${blackboardDef.length} 个变量`); + } + + /** + * 设置属性绑定 + */ + private static setupPropertyBindings( + bindings: PropertyBinding[], + entityMap: Map + ): void { + for (const binding of bindings) { + const entity = entityMap.get(binding.nodeId); + if (!entity) { + logger.warn(`属性绑定引用的节点不存在: ${binding.nodeId}`); + continue; + } + + let propertyBindings = entity.getComponent(PropertyBindings); + if (!propertyBindings) { + propertyBindings = entity.addComponent(new PropertyBindings()); + } + + propertyBindings.addBinding(binding.propertyName, binding.variableName); + } + + logger.info(`已设置属性绑定: ${bindings.length} 个绑定`); + } +} diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts new file mode 100644 index 00000000..9f007216 --- /dev/null +++ b/packages/behavior-tree/src/Serialization/BehaviorTreeAssetSerializer.ts @@ -0,0 +1,329 @@ +import { encode, decode } from '@msgpack/msgpack'; +import { createLogger } from '@esengine/ecs-framework'; +import type { BehaviorTreeAsset } from './BehaviorTreeAsset'; +import { BehaviorTreeAssetValidator } from './BehaviorTreeAsset'; +import { EditorFormatConverter, type EditorFormat } from './EditorFormatConverter'; + +const logger = createLogger('BehaviorTreeAssetSerializer'); + +/** + * 序列化格式 + */ +export type SerializationFormat = 'json' | 'binary'; + +/** + * 序列化选项 + */ +export interface SerializationOptions { + /** + * 序列化格式 + */ + format: SerializationFormat; + + /** + * 是否美化JSON输出(仅format='json'时有效) + */ + pretty?: boolean; + + /** + * 是否在序列化前验证资产 + */ + validate?: boolean; +} + +/** + * 反序列化选项 + */ +export interface DeserializationOptions { + /** + * 是否在反序列化后验证资产 + */ + validate?: boolean; + + /** + * 是否严格模式(验证失败抛出异常) + */ + strict?: boolean; +} + +/** + * 行为树资产序列化器 + * + * 支持JSON和二进制(MessagePack)两种格式 + */ +export class BehaviorTreeAssetSerializer { + /** + * 序列化资产 + * + * @param asset 行为树资产 + * @param options 序列化选项 + * @returns 序列化后的数据(字符串或Uint8Array) + * + * @example + * ```typescript + * // JSON格式 + * const jsonData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'json', pretty: true }); + * + * // 二进制格式 + * const binaryData = BehaviorTreeAssetSerializer.serialize(asset, { format: 'binary' }); + * ``` + */ + static serialize( + asset: BehaviorTreeAsset, + options: SerializationOptions = { format: 'json', pretty: true } + ): string | Uint8Array { + // 验证资产(如果需要) + if (options.validate !== false) { + const validation = BehaviorTreeAssetValidator.validate(asset); + if (!validation.valid) { + const errors = validation.errors?.join(', ') || 'Unknown error'; + throw new Error(`资产验证失败: ${errors}`); + } + + if (validation.warnings && validation.warnings.length > 0) { + logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`); + } + } + + // 根据格式序列化 + if (options.format === 'json') { + return this.serializeToJSON(asset, options.pretty); + } else { + return this.serializeToBinary(asset); + } + } + + /** + * 序列化为JSON格式 + */ + private static serializeToJSON(asset: BehaviorTreeAsset, pretty: boolean = true): string { + try { + const json = pretty + ? JSON.stringify(asset, null, 2) + : JSON.stringify(asset); + + logger.info(`已序列化为JSON: ${json.length} 字符`); + return json; + } catch (error) { + throw new Error(`JSON序列化失败: ${error}`); + } + } + + /** + * 序列化为二进制格式(MessagePack) + */ + private static serializeToBinary(asset: BehaviorTreeAsset): Uint8Array { + try { + const binary = encode(asset); + logger.info(`已序列化为二进制: ${binary.length} 字节`); + return binary; + } catch (error) { + throw new Error(`二进制序列化失败: ${error}`); + } + } + + /** + * 反序列化资产 + * + * @param data 序列化的数据(字符串或Uint8Array) + * @param options 反序列化选项 + * @returns 行为树资产 + * + * @example + * ```typescript + * // 从JSON加载 + * const asset = BehaviorTreeAssetSerializer.deserialize(jsonString); + * + * // 从二进制加载 + * const asset = BehaviorTreeAssetSerializer.deserialize(binaryData); + * ``` + */ + static deserialize( + data: string | Uint8Array, + options: DeserializationOptions = { validate: true, strict: true } + ): BehaviorTreeAsset { + let asset: BehaviorTreeAsset; + + try { + if (typeof data === 'string') { + asset = this.deserializeFromJSON(data); + } else { + asset = this.deserializeFromBinary(data); + } + } catch (error) { + throw new Error(`反序列化失败: ${error}`); + } + + // 验证资产(如果需要) + if (options.validate !== false) { + const validation = BehaviorTreeAssetValidator.validate(asset); + + if (!validation.valid) { + const errors = validation.errors?.join(', ') || 'Unknown error'; + if (options.strict) { + throw new Error(`资产验证失败: ${errors}`); + } else { + logger.error(`资产验证失败: ${errors}`); + } + } + + if (validation.warnings && validation.warnings.length > 0) { + logger.warn(`资产验证警告: ${validation.warnings.join(', ')}`); + } + } + + return asset; + } + + /** + * 从JSON反序列化 + */ + private static deserializeFromJSON(json: string): BehaviorTreeAsset { + try { + const data = JSON.parse(json); + + // 检测是否是编辑器格式(EditorFormat) + // 编辑器格式有 nodes/connections/blackboard,但没有 rootNodeId + // 运行时资产格式有 rootNodeId + const isEditorFormat = !data.rootNodeId && data.nodes && data.connections; + + if (isEditorFormat) { + logger.info('检测到编辑器格式,正在转换为运行时资产格式...'); + const editorData = data as EditorFormat; + const asset = EditorFormatConverter.toAsset(editorData); + logger.info(`已从编辑器格式转换: ${asset.nodes.length} 个节点`); + return asset; + } else { + const asset = data as BehaviorTreeAsset; + logger.info(`已从运行时资产格式反序列化: ${asset.nodes.length} 个节点`); + return asset; + } + } catch (error) { + throw new Error(`JSON解析失败: ${error}`); + } + } + + /** + * 从二进制反序列化 + */ + private static deserializeFromBinary(binary: Uint8Array): BehaviorTreeAsset { + try { + const asset = decode(binary) as BehaviorTreeAsset; + logger.info(`已从二进制反序列化: ${asset.nodes.length} 个节点`); + return asset; + } catch (error) { + throw new Error(`二进制解码失败: ${error}`); + } + } + + /** + * 检测数据格式 + * + * @param data 序列化的数据 + * @returns 格式类型 + */ + static detectFormat(data: string | Uint8Array): SerializationFormat { + if (typeof data === 'string') { + return 'json'; + } else { + return 'binary'; + } + } + + /** + * 获取序列化数据的信息(不完全反序列化) + * + * @param data 序列化的数据 + * @returns 资产元信息 + */ + static getInfo(data: string | Uint8Array): { + format: SerializationFormat; + name: string; + version: string; + nodeCount: number; + blackboardVariableCount: number; + size: number; + } | null { + try { + const format = this.detectFormat(data); + let asset: BehaviorTreeAsset; + + if (format === 'json') { + asset = JSON.parse(data as string); + } else { + asset = decode(data as Uint8Array) as BehaviorTreeAsset; + } + + const size = typeof data === 'string' ? data.length : data.length; + + return { + format, + name: asset.metadata.name, + version: asset.version, + nodeCount: asset.nodes.length, + blackboardVariableCount: asset.blackboard.length, + size + }; + } catch (error) { + logger.error(`获取资产信息失败: ${error}`); + return null; + } + } + + /** + * 转换格式 + * + * @param data 源数据 + * @param targetFormat 目标格式 + * @param pretty 是否美化JSON(仅当目标格式为json时有效) + * @returns 转换后的数据 + * + * @example + * ```typescript + * // JSON转二进制 + * const binary = BehaviorTreeAssetSerializer.convert(jsonString, 'binary'); + * + * // 二进制转JSON + * const json = BehaviorTreeAssetSerializer.convert(binaryData, 'json', true); + * ``` + */ + static convert( + data: string | Uint8Array, + targetFormat: SerializationFormat, + pretty: boolean = true + ): string | Uint8Array { + const asset = this.deserialize(data, { validate: false }); + + return this.serialize(asset, { + format: targetFormat, + pretty, + validate: false + }); + } + + /** + * 比较两个资产数据的大小 + * + * @param jsonData JSON格式数据 + * @param binaryData 二进制格式数据 + * @returns 压缩率(百分比) + */ + static compareSize(jsonData: string, binaryData: Uint8Array): { + jsonSize: number; + binarySize: number; + compressionRatio: number; + savedBytes: number; + } { + const jsonSize = jsonData.length; + const binarySize = binaryData.length; + const savedBytes = jsonSize - binarySize; + const compressionRatio = (savedBytes / jsonSize) * 100; + + return { + jsonSize, + binarySize, + compressionRatio, + savedBytes + }; + } +} diff --git a/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts b/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts new file mode 100644 index 00000000..8b0dc4c2 --- /dev/null +++ b/packages/behavior-tree/src/Serialization/BehaviorTreePersistence.ts @@ -0,0 +1,189 @@ +import { Entity, IScene, SceneSerializer, SerializedScene, SerializedEntity } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; + +/** + * 行为树持久化工具 + * + * 使用框架的序列化系统进行二进制/JSON序列化 + */ +export class BehaviorTreePersistence { + /** + * 序列化行为树(JSON格式) + * + * @param rootEntity 行为树根实体 + * @param pretty 是否格式化 + * @returns 序列化数据(JSON字符串或二进制) + * + * @example + * ```typescript + * const data = BehaviorTreePersistence.serialize(aiRoot); + * ``` + */ + static serialize(rootEntity: Entity, pretty: boolean = true): string | Uint8Array { + if (!rootEntity.hasComponent(BehaviorTreeNode)) { + throw new Error('Entity must have BehaviorTreeNode component'); + } + + if (!rootEntity.scene) { + throw new Error('Entity must be attached to a scene'); + } + + // 使用 SceneSerializer,但只序列化这棵行为树 + // 创建一个临时场景包含只这个实体树 + return SceneSerializer.serialize(rootEntity.scene, { + format: 'json', + pretty: pretty, + includeMetadata: true + }); + } + + /** + * 从序列化数据加载行为树 + * + * @param scene 场景实例 + * @param data 序列化数据(JSON字符串或二进制) + * + * @example + * ```typescript + * // 从文件读取 + * const json = await readFile('behavior-tree.json'); + * + * // 恢复行为树到场景 + * BehaviorTreePersistence.deserialize(scene, json); + * ``` + */ + static deserialize(scene: IScene, data: string | Uint8Array): void { + SceneSerializer.deserialize(scene, data, { + strategy: 'merge' + }); + } + + /** + * 序列化为 JSON 字符串 + * + * @param rootEntity 行为树根实体 + * @param pretty 是否格式化 + * @returns JSON 字符串 + */ + static toJSON(rootEntity: Entity, pretty: boolean = true): string { + const data = this.serialize(rootEntity, pretty); + return JSON.stringify(data, null, pretty ? 2 : 0); + } + + /** + * 从 JSON 字符串加载 + * + * @param scene 场景实例 + * @param json JSON 字符串 + */ + static fromJSON(scene: IScene, json: string): void { + this.deserialize(scene, json); + } + + /** + * 保存到文件(需要 Tauri 环境) + * + * @param rootEntity 行为树根实体 + * @param filePath 文件路径 + * + * @example + * ```typescript + * await BehaviorTreePersistence.saveToFile(aiRoot, 'ai-behavior.json'); + * ``` + */ + static async saveToFile(rootEntity: Entity, filePath: string): Promise { + const json = this.toJSON(rootEntity, true); + + // 需要在 Tauri 环境中使用 + // const { writeTextFile } = await import('@tauri-apps/api/fs'); + // await writeTextFile(filePath, json); + + throw new Error('saveToFile requires Tauri environment. Use toJSON() for manual saving.'); + } + + /** + * 从文件加载(需要 Tauri 环境) + * + * @param scene 场景实例 + * @param filePath 文件路径 + * @returns 恢复的根实体 + * + * @example + * ```typescript + * const aiRoot = await BehaviorTreePersistence.loadFromFile(scene, 'ai-behavior.json'); + * ``` + */ + static async loadFromFile(scene: IScene, filePath: string): Promise { + // 需要在 Tauri 环境中使用 + // const { readTextFile } = await import('@tauri-apps/api/fs'); + // const json = await readTextFile(filePath); + // return this.fromJSON(scene, json); + + throw new Error('loadFromFile requires Tauri environment. Use fromJSON() for manual loading.'); + } + + /** + * 验证是否为有效的行为树数据 + * + * @param data 序列化数据(字符串格式) + * @returns 是否有效 + */ + static validate(data: string): boolean { + try { + const parsed = JSON.parse(data) as SerializedScene; + + if (!parsed || typeof parsed !== 'object') { + return false; + } + + // 检查必要字段 + if (!parsed.name || + typeof parsed.version !== 'number' || + !Array.isArray(parsed.entities) || + !Array.isArray(parsed.componentTypeRegistry)) { + return false; + } + + // 检查是否至少有一个实体包含 BehaviorTreeNode 组件 + const hasBehaviorTreeNode = parsed.entities.some((entity: SerializedEntity) => { + return entity.components.some( + (comp: any) => comp.type === 'BehaviorTreeNode' + ); + }); + + return hasBehaviorTreeNode; + } catch { + return false; + } + } + + /** + * 克隆行为树 + * + * @param scene 场景实例 + * @param rootEntity 要克隆的行为树根实体 + * @returns 克隆的新实体 + * + * @example + * ```typescript + * const clonedAI = BehaviorTreePersistence.clone(scene, originalAI); + * ``` + */ + static clone(scene: IScene, rootEntity: Entity): Entity { + const data = this.serialize(rootEntity); + const entityCountBefore = scene.entities.count; + + this.deserialize(scene, data); + + // 找到新添加的根实体(最后添加的实体) + const entities = Array.from(scene.entities.buffer); + for (let i = entities.length - 1; i >= entityCountBefore; i--) { + const entity = entities[i]; + if (entity.hasComponent(BehaviorTreeNode) && !entity.parent) { + return entity; + } + } + + throw new Error('Failed to find cloned root entity'); + } +} diff --git a/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts b/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts new file mode 100644 index 00000000..adae4c6f --- /dev/null +++ b/packages/behavior-tree/src/Serialization/EditorFormatConverter.ts @@ -0,0 +1,369 @@ +import { createLogger } from '@esengine/ecs-framework'; +import type { BehaviorTreeAsset, AssetMetadata, BehaviorTreeNodeData, BlackboardVariableDefinition, PropertyBinding } from './BehaviorTreeAsset'; +import { NodeType, BlackboardValueType } from '../Types/TaskStatus'; + +const logger = createLogger('EditorFormatConverter'); + +/** + * 编辑器节点格式 + */ +export interface EditorNode { + id: string; + template: { + displayName: string; + category: string; + type: NodeType; + [key: string]: any; + }; + data: Record; + position: { x: number; y: number }; + children: string[]; +} + +/** + * 编辑器连接格式 + */ +export interface EditorConnection { + from: string; + to: string; + fromProperty?: string; + toProperty?: string; + connectionType: 'node' | 'property'; +} + +/** + * 编辑器格式 + */ +export interface EditorFormat { + version?: string; + metadata?: { + name: string; + description?: string; + createdAt?: string; + modifiedAt?: string; + }; + nodes: EditorNode[]; + connections: EditorConnection[]; + blackboard: Record; + canvasState?: { + offset: { x: number; y: number }; + scale: number; + }; +} + +/** + * 编辑器格式转换器 + * + * 将编辑器格式转换为运行时资产格式 + */ +export class EditorFormatConverter { + /** + * 转换编辑器格式为资产格式 + * + * @param editorData 编辑器数据 + * @param metadata 可选的元数据覆盖 + * @returns 行为树资产 + */ + static toAsset(editorData: EditorFormat, metadata?: Partial): BehaviorTreeAsset { + logger.info('开始转换编辑器格式到资产格式'); + + // 查找根节点 + const rootNode = this.findRootNode(editorData.nodes); + if (!rootNode) { + throw new Error('未找到根节点'); + } + + // 转换元数据 + const assetMetadata: AssetMetadata = { + name: metadata?.name || editorData.metadata?.name || 'Untitled Behavior Tree', + description: metadata?.description || editorData.metadata?.description, + version: metadata?.version || editorData.version || '1.0.0', + createdAt: metadata?.createdAt || editorData.metadata?.createdAt, + modifiedAt: metadata?.modifiedAt || new Date().toISOString() + }; + + // 转换节点 + const nodes = this.convertNodes(editorData.nodes); + + // 转换黑板 + const blackboard = this.convertBlackboard(editorData.blackboard); + + // 转换属性绑定 + const propertyBindings = this.convertPropertyBindings( + editorData.connections, + editorData.nodes, + blackboard + ); + + const asset: BehaviorTreeAsset = { + version: '1.0.0', + metadata: assetMetadata, + rootNodeId: rootNode.id, + nodes, + blackboard, + propertyBindings: propertyBindings.length > 0 ? propertyBindings : undefined + }; + + logger.info(`转换完成: ${nodes.length}个节点, ${blackboard.length}个黑板变量, ${propertyBindings.length}个属性绑定`); + + return asset; + } + + /** + * 查找根节点 + */ + private static findRootNode(nodes: EditorNode[]): EditorNode | null { + return nodes.find(node => + node.template.category === '根节点' || + node.data.nodeType === 'root' + ) || null; + } + + /** + * 转换节点列表 + */ + private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] { + return editorNodes.map(node => this.convertNode(node)); + } + + /** + * 转换单个节点 + */ + private static convertNode(editorNode: EditorNode): BehaviorTreeNodeData { + // 复制data,去除编辑器特有的字段 + const data = { ...editorNode.data }; + + // 移除可能存在的UI相关字段 + delete data.nodeType; // 这个信息已经在nodeType字段中 + + return { + id: editorNode.id, + name: editorNode.template.displayName || editorNode.data.name || 'Node', + nodeType: editorNode.template.type, + data, + children: editorNode.children || [] + }; + } + + /** + * 转换黑板变量 + */ + private static convertBlackboard(blackboard: Record): BlackboardVariableDefinition[] { + const variables: BlackboardVariableDefinition[] = []; + + for (const [name, value] of Object.entries(blackboard)) { + // 推断类型 + const type = this.inferBlackboardType(value); + + variables.push({ + name, + type, + defaultValue: value + }); + } + + return variables; + } + + /** + * 推断黑板变量类型 + */ + private static inferBlackboardType(value: any): BlackboardValueType { + if (typeof value === 'number') { + return BlackboardValueType.Number; + } else if (typeof value === 'string') { + return BlackboardValueType.String; + } else if (typeof value === 'boolean') { + return BlackboardValueType.Boolean; + } else { + return BlackboardValueType.Object; + } + } + + /** + * 转换属性绑定 + */ + private static convertPropertyBindings( + connections: EditorConnection[], + nodes: EditorNode[], + blackboard: BlackboardVariableDefinition[] + ): PropertyBinding[] { + const bindings: PropertyBinding[] = []; + const blackboardVarNames = new Set(blackboard.map(v => v.name)); + + // 只处理属性类型的连接 + const propertyConnections = connections.filter(conn => conn.connectionType === 'property'); + + for (const conn of propertyConnections) { + const fromNode = nodes.find(n => n.id === conn.from); + const toNode = nodes.find(n => n.id === conn.to); + + if (!fromNode || !toNode || !conn.toProperty) { + logger.warn(`跳过无效的属性连接: from=${conn.from}, to=${conn.to}`); + continue; + } + + let variableName: string | undefined; + + // 检查 from 节点是否是黑板变量节点 + if (fromNode.data.nodeType === 'blackboard-variable') { + variableName = fromNode.data.variableName; + } else if (conn.fromProperty) { + variableName = conn.fromProperty; + } + + if (!variableName) { + logger.warn(`无法确定变量名: from节点=${fromNode.template.displayName}`); + continue; + } + + if (!blackboardVarNames.has(variableName)) { + logger.warn(`属性绑定引用了不存在的黑板变量: ${variableName}`); + continue; + } + + bindings.push({ + nodeId: toNode.id, + propertyName: conn.toProperty, + variableName + }); + } + + return bindings; + } + + /** + * 从资产格式转换回编辑器格式(用于加载) + * + * @param asset 行为树资产 + * @returns 编辑器格式数据 + */ + static fromAsset(asset: BehaviorTreeAsset): EditorFormat { + logger.info('开始转换资产格式到编辑器格式'); + + // 转换节点 + const nodes = this.convertNodesFromAsset(asset.nodes); + + // 转换黑板 + const blackboard: Record = {}; + for (const variable of asset.blackboard) { + blackboard[variable.name] = variable.defaultValue; + } + + // 转换属性绑定为连接 + const connections = this.convertPropertyBindingsToConnections( + asset.propertyBindings || [], + asset.nodes + ); + + // 添加节点连接(基于children关系) + const nodeConnections = this.buildNodeConnections(asset.nodes); + connections.push(...nodeConnections); + + const editorData: EditorFormat = { + version: asset.metadata.version, + metadata: { + name: asset.metadata.name, + description: asset.metadata.description, + createdAt: asset.metadata.createdAt, + modifiedAt: asset.metadata.modifiedAt + }, + nodes, + connections, + blackboard, + canvasState: { + offset: { x: 0, y: 0 }, + scale: 1 + } + }; + + logger.info(`转换完成: ${nodes.length}个节点, ${connections.length}个连接`); + + return editorData; + } + + /** + * 从资产格式转换节点 + */ + private static convertNodesFromAsset(assetNodes: BehaviorTreeNodeData[]): EditorNode[] { + return assetNodes.map((node, index) => { + // 简单的自动布局:按索引计算位置 + const position = { + x: 100 + (index % 5) * 250, + y: 100 + Math.floor(index / 5) * 150 + }; + + return { + id: node.id, + template: { + displayName: node.name, + category: this.inferCategory(node.nodeType), + type: node.nodeType + }, + data: { ...node.data }, + position, + children: node.children + }; + }); + } + + /** + * 推断节点分类 + */ + private static inferCategory(nodeType: NodeType): string { + switch (nodeType) { + case NodeType.Action: + return '动作'; + case NodeType.Condition: + return '条件'; + case NodeType.Composite: + return '组合'; + case NodeType.Decorator: + return '装饰器'; + default: + return '其他'; + } + } + + /** + * 将属性绑定转换为连接 + */ + private static convertPropertyBindingsToConnections( + bindings: PropertyBinding[], + nodes: BehaviorTreeNodeData[] + ): EditorConnection[] { + const connections: EditorConnection[] = []; + + for (const binding of bindings) { + // 需要找到代表这个黑板变量的节点(如果有的话) + // 这里简化处理,在实际使用中可能需要更复杂的逻辑 + connections.push({ + from: 'blackboard', // 占位符,实际使用时需要更复杂的处理 + to: binding.nodeId, + toProperty: binding.propertyName, + connectionType: 'property' + }); + } + + return connections; + } + + /** + * 根据children关系构建节点连接 + */ + private static buildNodeConnections(nodes: BehaviorTreeNodeData[]): EditorConnection[] { + const connections: EditorConnection[] = []; + + for (const node of nodes) { + for (const childId of node.children) { + connections.push({ + from: node.id, + to: childId, + connectionType: 'node' + }); + } + } + + return connections; + } +} diff --git a/packages/behavior-tree/src/Serialization/NodeTemplates.ts b/packages/behavior-tree/src/Serialization/NodeTemplates.ts new file mode 100644 index 00000000..a8b05b35 --- /dev/null +++ b/packages/behavior-tree/src/Serialization/NodeTemplates.ts @@ -0,0 +1,81 @@ +import { NodeType } from '../Types/TaskStatus'; +import { getRegisteredNodeTemplates } from '../Decorators/BehaviorNodeDecorator'; + +/** + * 节点数据JSON格式(用于编辑器) + */ +export interface NodeDataJSON { + nodeType: string; + compositeType?: string; + decoratorType?: string; + [key: string]: any; +} + +/** + * 属性定义(用于编辑器) + */ +export interface PropertyDefinition { + name: string; + type: 'string' | 'number' | 'boolean' | 'select' | 'blackboard' | 'code' | 'variable' | 'asset'; + label: string; + description?: string; + defaultValue?: any; + options?: Array<{ label: string; value: any }>; + min?: number; + max?: number; + step?: number; + required?: boolean; +} + +/** + * 节点模板(用于编辑器) + */ +export interface NodeTemplate { + type: NodeType; + displayName: string; + category: string; + icon?: string; + description: string; + color?: string; + className?: string; + requiresChildren?: boolean; + defaultConfig: Partial; + properties: PropertyDefinition[]; +} + +/** + * 编辑器节点模板库 + * + * 使用装饰器系统管理所有节点 + */ +export class NodeTemplates { + /** + * 获取所有节点模板(通过装饰器注册) + */ + static getAllTemplates(): NodeTemplate[] { + return getRegisteredNodeTemplates(); + } + + /** + * 根据类型和子类型获取模板 + */ + static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined { + return this.getAllTemplates().find(t => { + if (t.type !== type) return false; + const config: any = t.defaultConfig; + + switch (type) { + case NodeType.Composite: + return config.compositeType === subType; + case NodeType.Decorator: + return config.decoratorType === subType; + case NodeType.Action: + return config.actionType === subType; + case NodeType.Condition: + return config.conditionType === subType; + default: + return false; + } + }); + } +} diff --git a/packages/behavior-tree/src/Services/AssetLoadingManager.ts b/packages/behavior-tree/src/Services/AssetLoadingManager.ts new file mode 100644 index 00000000..924c6434 --- /dev/null +++ b/packages/behavior-tree/src/Services/AssetLoadingManager.ts @@ -0,0 +1,382 @@ +import { Entity, IService, createLogger } from '@esengine/ecs-framework'; +import { + LoadingState, + LoadingTask, + LoadingTaskHandle, + LoadingOptions, + LoadingProgress, + TimeoutError, + CircularDependencyError, + EntityDestroyedError +} from './AssetLoadingTypes'; + +const logger = createLogger('AssetLoadingManager'); + +/** + * 资产加载管理器 + * + * 统一管理行为树资产的异步加载,提供: + * - 超时检测和自动重试 + * - 循环引用检测 + * - 实体生命周期安全 + * - 加载状态追踪 + * + * @example + * ```typescript + * const manager = new AssetLoadingManager(); + * + * const handle = manager.startLoading( + * 'patrol', + * parentEntity, + * () => assetLoader.loadBehaviorTree('patrol'), + * { timeoutMs: 5000, maxRetries: 3 } + * ); + * + * // 在系统的 process() 中轮询检查 + * const state = handle.getState(); + * if (state === LoadingState.Loaded) { + * const entity = await handle.promise; + * // 使用加载的实体 + * } + * ``` + */ +export class AssetLoadingManager implements IService { + /** 正在进行的加载任务 */ + private tasks: Map = new Map(); + + /** 加载栈(用于循环检测) */ + private loadingStack: Set = new Set(); + + /** 默认配置 */ + private defaultOptions: Required> = { + timeoutMs: 5000, + maxRetries: 3, + retryDelayBase: 100, + maxRetryDelay: 2000 + }; + + /** + * 开始加载资产 + * + * @param assetId 资产ID + * @param parentEntity 父实体(用于生命周期检查) + * @param loader 加载函数 + * @param options 加载选项 + * @returns 加载任务句柄 + */ + startLoading( + assetId: string, + parentEntity: Entity, + loader: () => Promise, + options: LoadingOptions = {} + ): LoadingTaskHandle { + // 合并选项 + const finalOptions = { + ...this.defaultOptions, + ...options + }; + + // 循环引用检测 + if (options.parentAssetId) { + if (this.detectCircularDependency(assetId, options.parentAssetId)) { + const error = new CircularDependencyError( + `检测到循环引用: ${options.parentAssetId} → ${assetId}\n` + + `加载栈: ${Array.from(this.loadingStack).join(' → ')}` + ); + logger.error(error.message); + throw error; + } + } + + // 检查是否已有任务 + const existingTask = this.tasks.get(assetId); + if (existingTask) { + logger.debug(`资产 ${assetId} 已在加载中,返回现有任务`); + return this.createHandle(existingTask); + } + + // 创建新任务 + const task: LoadingTask = { + assetId, + promise: null as any, // 稍后设置 + startTime: Date.now(), + lastRetryTime: 0, + retryCount: 0, + maxRetries: finalOptions.maxRetries, + timeoutMs: finalOptions.timeoutMs, + state: LoadingState.Pending, + parentEntityId: parentEntity.id, + parentEntity: parentEntity, + parentAssetId: options.parentAssetId + }; + + // 添加到加载栈(循环检测) + this.loadingStack.add(assetId); + + // 创建带超时和重试的Promise + task.promise = this.loadWithTimeoutAndRetry(task, loader, finalOptions); + task.state = LoadingState.Loading; + + this.tasks.set(assetId, task); + + logger.info(`开始加载资产: ${assetId}`, { + timeoutMs: finalOptions.timeoutMs, + maxRetries: finalOptions.maxRetries, + parentAssetId: options.parentAssetId + }); + + return this.createHandle(task); + } + + /** + * 带超时和重试的加载 + */ + private async loadWithTimeoutAndRetry( + task: LoadingTask, + loader: () => Promise, + options: Required> + ): Promise { + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= task.maxRetries; attempt++) { + // 检查父实体是否还存在 + if (task.parentEntity.isDestroyed) { + const error = new EntityDestroyedError( + `父实体已销毁,取消加载: ${task.assetId}` + ); + task.state = LoadingState.Cancelled; + this.cleanup(task.assetId); + logger.warn(error.message); + throw error; + } + + try { + task.retryCount = attempt; + task.lastRetryTime = Date.now(); + + logger.debug(`加载尝试 ${attempt + 1}/${task.maxRetries + 1}: ${task.assetId}`); + + // 使用超时包装 + const result = await this.withTimeout( + loader(), + task.timeoutMs, + `加载资产 ${task.assetId} 超时(${task.timeoutMs}ms)` + ); + + // 加载成功 + task.state = LoadingState.Loaded; + task.result = result; + this.cleanup(task.assetId); + + logger.info(`资产加载成功: ${task.assetId}`, { + attempts: attempt + 1, + elapsedMs: Date.now() - task.startTime + }); + + return result; + + } catch (error) { + lastError = error as Error; + + // 记录错误类型 + if (error instanceof TimeoutError) { + task.state = LoadingState.Timeout; + logger.warn(`资产加载超时: ${task.assetId} (尝试 ${attempt + 1})`); + } else if (error instanceof EntityDestroyedError) { + // 实体已销毁,不需要重试 + throw error; + } else { + logger.warn(`资产加载失败: ${task.assetId} (尝试 ${attempt + 1})`, error); + } + + // 最后一次尝试失败 + if (attempt === task.maxRetries) { + task.state = LoadingState.Failed; + task.error = lastError; + this.cleanup(task.assetId); + + logger.error(`资产加载最终失败: ${task.assetId}`, { + attempts: attempt + 1, + error: lastError.message + }); + + throw lastError; + } + + // 计算重试延迟(指数退避) + const delayMs = Math.min( + Math.pow(2, attempt) * options.retryDelayBase, + options.maxRetryDelay + ); + + logger.debug(`等待 ${delayMs}ms 后重试...`); + await this.delay(delayMs); + } + } + + throw lastError!; + } + + /** + * Promise 超时包装 + */ + private withTimeout( + promise: Promise, + timeoutMs: number, + message: string + ): Promise { + let timeoutId: NodeJS.Timeout | number; + + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new TimeoutError(message)); + }, timeoutMs); + }); + + return Promise.race([ + promise.then(result => { + clearTimeout(timeoutId as any); + return result; + }), + timeoutPromise + ]).catch(error => { + clearTimeout(timeoutId as any); + throw error; + }); + } + + /** + * 循环依赖检测 + */ + private detectCircularDependency(assetId: string, parentAssetId: string): boolean { + // 如果父资产正在加载中,说明有循环 + if (this.loadingStack.has(parentAssetId)) { + return true; + } + + // TODO: 更复杂的循环检测(检查完整的依赖链) + // 当前只检测直接循环(A→B→A) + // 未来可以检测间接循环(A→B→C→A) + + return false; + } + + /** + * 获取任务状态 + */ + getTaskState(assetId: string): LoadingState { + return this.tasks.get(assetId)?.state ?? LoadingState.Idle; + } + + /** + * 获取任务 + */ + getTask(assetId: string): LoadingTask | undefined { + return this.tasks.get(assetId); + } + + /** + * 取消加载 + */ + cancelLoading(assetId: string): void { + const task = this.tasks.get(assetId); + if (task) { + task.state = LoadingState.Cancelled; + this.cleanup(assetId); + logger.info(`取消加载: ${assetId}`); + } + } + + /** + * 清理任务 + */ + private cleanup(assetId: string): void { + const task = this.tasks.get(assetId); + if (task) { + // 清除实体引用,帮助GC + (task as any).parentEntity = null; + } + this.tasks.delete(assetId); + this.loadingStack.delete(assetId); + } + + /** + * 延迟 + */ + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /** + * 创建任务句柄 + */ + private createHandle(task: LoadingTask): LoadingTaskHandle { + return { + assetId: task.assetId, + + getState: () => task.state, + + getError: () => task.error, + + getProgress: (): LoadingProgress => { + const now = Date.now(); + const elapsed = now - task.startTime; + const remaining = Math.max(0, task.timeoutMs - elapsed); + + return { + state: task.state, + elapsedMs: elapsed, + remainingTimeoutMs: remaining, + retryCount: task.retryCount, + maxRetries: task.maxRetries + }; + }, + + cancel: () => this.cancelLoading(task.assetId), + + promise: task.promise + }; + } + + /** + * 获取所有正在加载的资产 + */ + getLoadingAssets(): string[] { + return Array.from(this.tasks.keys()); + } + + /** + * 获取加载统计信息 + */ + getStats(): { + totalTasks: number; + loadingTasks: number; + failedTasks: number; + timeoutTasks: number; + } { + const tasks = Array.from(this.tasks.values()); + + return { + totalTasks: tasks.length, + loadingTasks: tasks.filter(t => t.state === LoadingState.Loading).length, + failedTasks: tasks.filter(t => t.state === LoadingState.Failed).length, + timeoutTasks: tasks.filter(t => t.state === LoadingState.Timeout).length + }; + } + + /** + * 清空所有任务 + */ + clear(): void { + logger.info('清空所有加载任务', this.getStats()); + this.tasks.clear(); + this.loadingStack.clear(); + } + + /** + * 释放资源 + */ + dispose(): void { + this.clear(); + } +} diff --git a/packages/behavior-tree/src/Services/AssetLoadingTypes.ts b/packages/behavior-tree/src/Services/AssetLoadingTypes.ts new file mode 100644 index 00000000..36c87ef2 --- /dev/null +++ b/packages/behavior-tree/src/Services/AssetLoadingTypes.ts @@ -0,0 +1,158 @@ +import { Entity } from '@esengine/ecs-framework'; + +/** + * 资产加载状态 + */ +export enum LoadingState { + /** 未开始 */ + Idle = 'idle', + /** 即将开始 */ + Pending = 'pending', + /** 加载中 */ + Loading = 'loading', + /** 加载成功 */ + Loaded = 'loaded', + /** 加载失败 */ + Failed = 'failed', + /** 加载超时 */ + Timeout = 'timeout', + /** 已取消 */ + Cancelled = 'cancelled' +} + +/** + * 加载任务 + */ +export interface LoadingTask { + /** 资产ID */ + assetId: string; + + /** 加载Promise */ + promise: Promise; + + /** 开始时间 */ + startTime: number; + + /** 上次重试时间 */ + lastRetryTime: number; + + /** 当前重试次数 */ + retryCount: number; + + /** 最大重试次数 */ + maxRetries: number; + + /** 超时时间(毫秒) */ + timeoutMs: number; + + /** 当前状态 */ + state: LoadingState; + + /** 错误信息 */ + error?: Error; + + /** 父实体ID */ + parentEntityId: number; + + /** 父实体引用(需要在使用前检查isDestroyed) */ + parentEntity: Entity; + + /** 父资产ID(用于循环检测) */ + parentAssetId?: string; + + /** 加载结果(缓存) */ + result?: Entity; +} + +/** + * 加载任务句柄 + */ +export interface LoadingTaskHandle { + /** 资产ID */ + assetId: string; + + /** 获取当前状态 */ + getState(): LoadingState; + + /** 获取错误信息 */ + getError(): Error | undefined; + + /** 获取加载进度信息 */ + getProgress(): LoadingProgress; + + /** 取消加载 */ + cancel(): void; + + /** 加载Promise */ + promise: Promise; +} + +/** + * 加载进度信息 + */ +export interface LoadingProgress { + /** 当前状态 */ + state: LoadingState; + + /** 已耗时(毫秒) */ + elapsedMs: number; + + /** 剩余超时时间(毫秒) */ + remainingTimeoutMs: number; + + /** 当前重试次数 */ + retryCount: number; + + /** 最大重试次数 */ + maxRetries: number; +} + +/** + * 加载选项 + */ +export interface LoadingOptions { + /** 超时时间(毫秒),默认5000 */ + timeoutMs?: number; + + /** 最大重试次数,默认3 */ + maxRetries?: number; + + /** 父资产ID(用于循环检测) */ + parentAssetId?: string; + + /** 重试延迟基数(毫秒),默认100 */ + retryDelayBase?: number; + + /** 最大重试延迟(毫秒),默认2000 */ + maxRetryDelay?: number; +} + +/** + * 超时错误 + */ +export class TimeoutError extends Error { + constructor(message: string) { + super(message); + this.name = 'TimeoutError'; + } +} + +/** + * 循环依赖错误 + */ +export class CircularDependencyError extends Error { + constructor(message: string) { + super(message); + this.name = 'CircularDependencyError'; + } +} + +/** + * 实体已销毁错误 + */ +export class EntityDestroyedError extends Error { + constructor(message: string) { + super(message); + this.name = 'EntityDestroyedError'; + } +} diff --git a/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts b/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts new file mode 100644 index 00000000..45c64f9e --- /dev/null +++ b/packages/behavior-tree/src/Services/FileSystemAssetLoader.ts @@ -0,0 +1,227 @@ +import type { IService } from '@esengine/ecs-framework'; +import { IAssetLoader } from './IAssetLoader'; +import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset'; +import { BehaviorTreeAssetSerializer, DeserializationOptions } from '../Serialization/BehaviorTreeAssetSerializer'; +import { createLogger } from '@esengine/ecs-framework'; + +const logger = createLogger('FileSystemAssetLoader'); + +/** + * 文件系统资产加载器配置 + */ +export interface FileSystemAssetLoaderConfig { + /** 资产基础路径 */ + basePath: string; + + /** 资产格式 */ + format: 'json' | 'binary'; + + /** 文件扩展名(可选,默认根据格式自动设置) */ + extension?: string; + + /** 是否启用缓存 */ + enableCache?: boolean; + + /** 自定义文件读取函数(可选) */ + readFile?: (path: string) => Promise; +} + +/** + * 文件系统资产加载器 + * + * 从文件系统加载行为树资产,支持 JSON 和 Binary 格式。 + * 提供资产缓存和预加载功能。 + * + * @example + * ```typescript + * // 创建加载器 + * const loader = new FileSystemAssetLoader({ + * basePath: 'assets/behavior-trees', + * format: 'json', + * enableCache: true + * }); + * + * // 加载资产 + * const asset = await loader.loadBehaviorTree('patrol'); + * ``` + */ +export class FileSystemAssetLoader implements IAssetLoader, IService { + private config: Required; + private cache: Map = new Map(); + + constructor(config: FileSystemAssetLoaderConfig) { + this.config = { + basePath: config.basePath, + format: config.format, + extension: config.extension || (config.format === 'json' ? '.btree.json' : '.btree.bin'), + enableCache: config.enableCache ?? true, + readFile: config.readFile || this.defaultReadFile.bind(this) + }; + + // 规范化路径 + this.config.basePath = this.config.basePath.replace(/\\/g, '/').replace(/\/$/, ''); + } + + /** + * 加载行为树资产 + */ + async loadBehaviorTree(assetId: string): Promise { + // 检查缓存 + if (this.config.enableCache && this.cache.has(assetId)) { + logger.debug(`从缓存加载资产: ${assetId}`); + return this.cache.get(assetId)!; + } + + logger.info(`加载行为树资产: ${assetId}`); + + try { + // 构建文件路径 + const filePath = this.resolveAssetPath(assetId); + + // 读取文件 + const data = await this.config.readFile(filePath); + + // 反序列化(自动根据 data 类型判断格式) + const options: DeserializationOptions = { + validate: true, + strict: true + }; + + const asset = BehaviorTreeAssetSerializer.deserialize(data, options); + + // 缓存资产 + if (this.config.enableCache) { + this.cache.set(assetId, asset); + } + + logger.info(`成功加载资产: ${assetId}`); + return asset; + } catch (error) { + logger.error(`加载资产失败: ${assetId}`, error); + throw new Error(`Failed to load behavior tree asset '${assetId}': ${error}`); + } + } + + /** + * 检查资产是否存在 + */ + async exists(assetId: string): Promise { + // 如果在缓存中,直接返回 true + if (this.config.enableCache && this.cache.has(assetId)) { + return true; + } + + try { + const filePath = this.resolveAssetPath(assetId); + // 尝试读取文件(如果文件不存在会抛出异常) + await this.config.readFile(filePath); + return true; + } catch { + return false; + } + } + + /** + * 预加载资产 + */ + async preload(assetIds: string[]): Promise { + logger.info(`预加载 ${assetIds.length} 个资产...`); + + const promises = assetIds.map(id => this.loadBehaviorTree(id).catch(error => { + logger.warn(`预加载资产失败: ${id}`, error); + })); + + await Promise.all(promises); + + logger.info(`预加载完成`); + } + + /** + * 卸载资产 + */ + unload(assetId: string): void { + if (this.cache.has(assetId)) { + this.cache.delete(assetId); + logger.debug(`卸载资产: ${assetId}`); + } + } + + /** + * 清空缓存 + */ + clearCache(): void { + this.cache.clear(); + logger.info('缓存已清空'); + } + + /** + * 获取缓存的资产数量 + */ + getCacheSize(): number { + return this.cache.size; + } + + /** + * 释放资源 + */ + dispose(): void { + this.clearCache(); + } + + /** + * 解析资产路径 + */ + private resolveAssetPath(assetId: string): string { + // 移除开头的斜杠 + const normalizedId = assetId.replace(/^\/+/, ''); + + // 构建完整路径 + return `${this.config.basePath}/${normalizedId}${this.config.extension}`; + } + + /** + * 默认文件读取实现 + * + * 注意:此实现依赖运行环境 + * - 浏览器:需要通过 fetch 或 XMLHttpRequest + * - Node.js:需要使用 fs + * - 游戏引擎:需要使用引擎的文件 API + * + * 用户应该提供自己的 readFile 实现 + */ + private async defaultReadFile(path: string): Promise { + // 检测运行环境 + if (typeof window !== 'undefined' && typeof fetch !== 'undefined') { + // 浏览器环境 + const response = await fetch(path); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + if (this.config.format === 'binary') { + const buffer = await response.arrayBuffer(); + return new Uint8Array(buffer); + } else { + return await response.text(); + } + } else if (typeof require !== 'undefined') { + // Node.js 环境 + try { + const fs = require('fs').promises; + if (this.config.format === 'binary') { + const buffer = await fs.readFile(path); + return new Uint8Array(buffer); + } else { + return await fs.readFile(path, 'utf-8'); + } + } catch (error) { + throw new Error(`Failed to read file '${path}': ${error}`); + } + } else { + throw new Error( + 'No default file reading implementation available. ' + + 'Please provide a custom readFile function in the config.' + ); + } + } +} diff --git a/packages/behavior-tree/src/Services/GlobalBlackboardService.ts b/packages/behavior-tree/src/Services/GlobalBlackboardService.ts new file mode 100644 index 00000000..ee724ebe --- /dev/null +++ b/packages/behavior-tree/src/Services/GlobalBlackboardService.ts @@ -0,0 +1,175 @@ +import { IService } from '@esengine/ecs-framework'; +import { BlackboardValueType } from '../Types/TaskStatus'; +import { BlackboardVariable } from '../Components/BlackboardComponent'; + +/** + * 全局黑板配置 + */ +export interface GlobalBlackboardConfig { + version: string; + variables: BlackboardVariable[]; +} + +/** + * 全局黑板服务 + * + * 提供所有行为树共享的全局变量存储 + * + * 使用方式: + * ```typescript + * // 注册服务(在 BehaviorTreePlugin.install 中自动完成) + * core.services.registerSingleton(GlobalBlackboardService); + * + * // 获取服务 + * const blackboard = core.services.resolve(GlobalBlackboardService); + * ``` + */ +export class GlobalBlackboardService implements IService { + private variables: Map = new Map(); + + dispose(): void { + this.variables.clear(); + } + + /** + * 定义全局变量 + */ + defineVariable( + name: string, + type: BlackboardValueType, + initialValue: any, + options?: { + readonly?: boolean; + description?: string; + } + ): void { + this.variables.set(name, { + name, + type, + value: initialValue, + readonly: options?.readonly ?? false, + description: options?.description + }); + } + + /** + * 获取全局变量值 + */ + getValue(name: string): T | undefined { + const variable = this.variables.get(name); + return variable?.value as T; + } + + /** + * 设置全局变量值 + */ + setValue(name: string, value: any, force: boolean = false): boolean { + const variable = this.variables.get(name); + + if (!variable) { + return false; + } + + if (variable.readonly && !force) { + return false; + } + + variable.value = value; + return true; + } + + /** + * 检查全局变量是否存在 + */ + hasVariable(name: string): boolean { + return this.variables.has(name); + } + + /** + * 删除全局变量 + */ + removeVariable(name: string): boolean { + return this.variables.delete(name); + } + + /** + * 获取所有变量名 + */ + getVariableNames(): string[] { + return Array.from(this.variables.keys()); + } + + /** + * 获取所有变量 + */ + getAllVariables(): BlackboardVariable[] { + return Array.from(this.variables.values()); + } + + /** + * 清空所有全局变量 + */ + clear(): void { + this.variables.clear(); + } + + /** + * 批量设置变量 + */ + setVariables(values: Record): void { + for (const [name, value] of Object.entries(values)) { + const variable = this.variables.get(name); + if (variable && !variable.readonly) { + variable.value = value; + } + } + } + + /** + * 批量获取变量 + */ + getVariables(names: string[]): Record { + const result: Record = {}; + for (const name of names) { + const value = this.getValue(name); + if (value !== undefined) { + result[name] = value; + } + } + return result; + } + + /** + * 导出配置 + */ + exportConfig(): GlobalBlackboardConfig { + return { + version: '1.0', + variables: Array.from(this.variables.values()) + }; + } + + /** + * 导入配置 + */ + importConfig(config: GlobalBlackboardConfig): void { + this.variables.clear(); + for (const variable of config.variables) { + this.variables.set(variable.name, variable); + } + } + + /** + * 序列化为 JSON + */ + toJSON(): string { + return JSON.stringify(this.exportConfig(), null, 2); + } + + /** + * 从 JSON 反序列化 + */ + static fromJSON(json: string): GlobalBlackboardConfig { + return JSON.parse(json); + } +} diff --git a/packages/behavior-tree/src/Services/IAssetLoader.ts b/packages/behavior-tree/src/Services/IAssetLoader.ts new file mode 100644 index 00000000..c1f5aa5e --- /dev/null +++ b/packages/behavior-tree/src/Services/IAssetLoader.ts @@ -0,0 +1,68 @@ +import { BehaviorTreeAsset } from '../Serialization/BehaviorTreeAsset'; + +/** + * 资产加载器接口 + * + * 提供可扩展的资产加载机制,允许用户自定义资产加载逻辑。 + * 支持从文件系统、网络、数据库、自定义打包格式等加载资产。 + * + * @example + * ```typescript + * // 使用默认的文件系统加载器 + * const loader = new FileSystemAssetLoader({ + * basePath: 'assets/behavior-trees', + * format: 'json' + * }); + * core.services.registerInstance(FileSystemAssetLoader, loader); + * + * // 或实现自定义加载器 + * class NetworkAssetLoader implements IAssetLoader { + * async loadBehaviorTree(assetId: string): Promise { + * const response = await fetch(`/api/assets/${assetId}`); + * return response.json(); + * } + * + * async exists(assetId: string): Promise { + * const response = await fetch(`/api/assets/${assetId}/exists`); + * return response.json(); + * } + * } + * core.services.registerInstance(FileSystemAssetLoader, new NetworkAssetLoader()); + * ``` + */ +export interface IAssetLoader { + /** + * 加载行为树资产 + * + * @param assetId 资产逻辑ID,例如 'patrol' 或 'ai/patrol' + * @returns 行为树资产对象 + * @throws 如果资产不存在或加载失败 + */ + loadBehaviorTree(assetId: string): Promise; + + /** + * 检查资产是否存在 + * + * @param assetId 资产逻辑ID + * @returns 资产是否存在 + */ + exists(assetId: string): Promise; + + /** + * 预加载资产(可选) + * + * 用于提前加载资产到缓存,减少运行时延迟 + * + * @param assetIds 要预加载的资产ID列表 + */ + preload?(assetIds: string[]): Promise; + + /** + * 卸载资产(可选) + * + * 释放资产占用的内存 + * + * @param assetId 资产ID + */ + unload?(assetId: string): void; +} diff --git a/packages/behavior-tree/src/Services/WorkspaceService.ts b/packages/behavior-tree/src/Services/WorkspaceService.ts new file mode 100644 index 00000000..36bee450 --- /dev/null +++ b/packages/behavior-tree/src/Services/WorkspaceService.ts @@ -0,0 +1,355 @@ +import { IService } from '@esengine/ecs-framework'; + +/** + * 资产类型 + */ +export enum AssetType { + BehaviorTree = 'behavior-tree', + Blackboard = 'blackboard', + Unknown = 'unknown' +} + +/** + * 资产注册信息 + */ +export interface AssetRegistry { + /** 资产唯一ID */ + id: string; + + /** 资产名称 */ + name: string; + + /** 资产相对路径(相对于工作区根目录) */ + path: string; + + /** 资产类型 */ + type: AssetType; + + /** 依赖的其他资产ID列表 */ + dependencies: string[]; + + /** 最后修改时间 */ + lastModified?: number; + + /** 资产元数据 */ + metadata?: Record; +} + +/** + * 工作区配置 + */ +export interface WorkspaceConfig { + /** 工作区名称 */ + name: string; + + /** 工作区版本 */ + version: string; + + /** 工作区根目录(绝对路径) */ + rootPath: string; + + /** 资产目录配置 */ + assetPaths: { + /** 行为树目录 */ + behaviorTrees: string; + + /** 黑板目录 */ + blackboards: string; + }; + + /** 资产注册表 */ + assets: AssetRegistry[]; +} + +/** + * 工作区服务 + * + * 管理项目的工作区配置和资产注册表,提供: + * - 工作区配置的加载和保存 + * - 资产注册和查询 + * - 依赖关系追踪 + * - 循环依赖检测 + */ +export class WorkspaceService implements IService { + private config: WorkspaceConfig | null = null; + private assetMap: Map = new Map(); + private assetPathMap: Map = new Map(); + + /** + * 初始化工作区 + */ + initialize(config: WorkspaceConfig): void { + this.config = config; + this.rebuildAssetMaps(); + } + + /** + * 重建资产映射表 + */ + private rebuildAssetMaps(): void { + this.assetMap.clear(); + this.assetPathMap.clear(); + + if (!this.config) return; + + for (const asset of this.config.assets) { + this.assetMap.set(asset.id, asset); + this.assetPathMap.set(asset.path, asset); + } + } + + /** + * 获取工作区配置 + */ + getConfig(): WorkspaceConfig | null { + return this.config; + } + + /** + * 更新工作区配置 + */ + updateConfig(config: WorkspaceConfig): void { + this.config = config; + this.rebuildAssetMaps(); + } + + /** + * 注册资产 + */ + registerAsset(asset: AssetRegistry): void { + if (!this.config) { + throw new Error('工作区未初始化'); + } + + // 检查是否已存在 + const existing = this.config.assets.find(a => a.id === asset.id); + if (existing) { + // 更新现有资产 + Object.assign(existing, asset); + } else { + // 添加新资产 + this.config.assets.push(asset); + } + + this.rebuildAssetMaps(); + } + + /** + * 取消注册资产 + */ + unregisterAsset(assetId: string): void { + if (!this.config) return; + + const index = this.config.assets.findIndex(a => a.id === assetId); + if (index !== -1) { + this.config.assets.splice(index, 1); + this.rebuildAssetMaps(); + } + } + + /** + * 通过ID获取资产 + */ + getAssetById(assetId: string): AssetRegistry | undefined { + return this.assetMap.get(assetId); + } + + /** + * 通过路径获取资产 + */ + getAssetByPath(path: string): AssetRegistry | undefined { + return this.assetPathMap.get(path); + } + + /** + * 获取所有资产 + */ + getAllAssets(): AssetRegistry[] { + return this.config?.assets || []; + } + + /** + * 按类型获取资产 + */ + getAssetsByType(type: AssetType): AssetRegistry[] { + return this.getAllAssets().filter(a => a.type === type); + } + + /** + * 获取行为树资产列表 + */ + getBehaviorTreeAssets(): AssetRegistry[] { + return this.getAssetsByType(AssetType.BehaviorTree); + } + + /** + * 获取黑板资产列表 + */ + getBlackboardAssets(): AssetRegistry[] { + return this.getAssetsByType(AssetType.Blackboard); + } + + /** + * 获取资产的所有依赖(递归) + */ + getAssetDependencies(assetId: string, visited = new Set()): AssetRegistry[] { + if (visited.has(assetId)) { + return []; + } + + visited.add(assetId); + + const asset = this.getAssetById(assetId); + if (!asset) { + return []; + } + + const dependencies: AssetRegistry[] = []; + + for (const depId of asset.dependencies) { + const depAsset = this.getAssetById(depId); + if (depAsset) { + dependencies.push(depAsset); + // 递归获取依赖的依赖 + dependencies.push(...this.getAssetDependencies(depId, visited)); + } + } + + return dependencies; + } + + /** + * 检测循环依赖 + * + * @param assetId 要检查的资产ID + * @returns 如果存在循环依赖,返回循环路径;否则返回 null + */ + detectCircularDependency(assetId: string): string[] | null { + const visited = new Set(); + const path: string[] = []; + + const dfs = (currentId: string): boolean => { + if (path.includes(currentId)) { + // 找到循环 + path.push(currentId); + return true; + } + + if (visited.has(currentId)) { + return false; + } + + visited.add(currentId); + path.push(currentId); + + const asset = this.getAssetById(currentId); + if (asset) { + for (const depId of asset.dependencies) { + if (dfs(depId)) { + return true; + } + } + } + + path.pop(); + return false; + }; + + return dfs(assetId) ? path : null; + } + + /** + * 检查是否可以添加依赖(不会造成循环依赖) + * + * @param assetId 资产ID + * @param dependencyId 要添加的依赖ID + * @returns 是否可以安全添加 + */ + canAddDependency(assetId: string, dependencyId: string): boolean { + const asset = this.getAssetById(assetId); + if (!asset) return false; + + // 临时添加依赖 + const originalDeps = [...asset.dependencies]; + asset.dependencies.push(dependencyId); + + // 检测循环依赖 + const hasCircular = this.detectCircularDependency(assetId) !== null; + + // 恢复原始依赖 + asset.dependencies = originalDeps; + + return !hasCircular; + } + + /** + * 添加资产依赖 + */ + addAssetDependency(assetId: string, dependencyId: string): boolean { + if (!this.canAddDependency(assetId, dependencyId)) { + return false; + } + + const asset = this.getAssetById(assetId); + if (!asset) return false; + + if (!asset.dependencies.includes(dependencyId)) { + asset.dependencies.push(dependencyId); + } + + return true; + } + + /** + * 移除资产依赖 + */ + removeAssetDependency(assetId: string, dependencyId: string): void { + const asset = this.getAssetById(assetId); + if (!asset) return; + + const index = asset.dependencies.indexOf(dependencyId); + if (index !== -1) { + asset.dependencies.splice(index, 1); + } + } + + /** + * 解析资产路径(支持相对路径和绝对路径) + */ + resolveAssetPath(path: string): string { + if (!this.config) return path; + + // 如果是绝对路径,直接返回 + if (path.startsWith('/') || path.match(/^[A-Za-z]:/)) { + return path; + } + + // 相对路径,拼接工作区根目录 + return `${this.config.rootPath}/${path}`.replace(/\\/g, '/'); + } + + /** + * 获取资产的相对路径 + */ + getRelativePath(absolutePath: string): string { + if (!this.config) return absolutePath; + + const rootPath = this.config.rootPath.replace(/\\/g, '/'); + const absPath = absolutePath.replace(/\\/g, '/'); + + if (absPath.startsWith(rootPath)) { + return absPath.substring(rootPath.length + 1); + } + + return absolutePath; + } + + /** + * 清理资源 + */ + dispose(): void { + this.config = null; + this.assetMap.clear(); + this.assetPathMap.clear(); + } +} diff --git a/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts b/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts new file mode 100644 index 00000000..6640cf7f --- /dev/null +++ b/packages/behavior-tree/src/Systems/CompositeExecutionSystem.ts @@ -0,0 +1,704 @@ +import { EntitySystem, Matcher, Entity } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { CompositeNodeComponent } from '../Components/CompositeNodeComponent'; +import { ActiveNode } from '../Components/ActiveNode'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; +import { TaskStatus, NodeType, CompositeType, AbortType } from '../Types/TaskStatus'; +import { SequenceNode } from '../Components/Composites/SequenceNode'; +import { SelectorNode } from '../Components/Composites/SelectorNode'; +import { RootNode } from '../Components/Composites/RootNode'; +import { SubTreeNode } from '../Components/Composites/SubTreeNode'; +import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition'; +import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition'; +import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition'; +import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition'; + +/** + * 复合节点执行系统 + * + * 负责处理所有活跃的复合节点 + * 读取子节点状态,根据复合规则决定自己的状态和激活哪些子节点 + * + * updateOrder: 300 (在叶子节点和装饰器之后执行) + */ +export class CompositeExecutionSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(BehaviorTreeNode, ActiveNode).exclude(RootNode, SubTreeNode)); + this.updateOrder = 300; + } + + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const node = entity.getComponent(BehaviorTreeNode)!; + + // 只处理复合节点 + if (node.nodeType !== NodeType.Composite) { + continue; + } + + // 使用 getComponentByType 支持继承查找 + const composite = entity.getComponentByType(CompositeNodeComponent); + + if (!composite) { + this.logger.warn(`复合节点 ${entity.name} 没有找到复合节点组件`); + const components = entity.components.map(c => c.constructor.name).join(', '); + this.logger.warn(` 组件列表: ${components}`); + continue; + } + + this.executeComposite(entity, node, composite); + } + } + + /** + * 执行复合节点逻辑 + */ + private executeComposite(entity: Entity, node: BehaviorTreeNode, composite: CompositeNodeComponent): void { + const children = entity.children; + + if (children.length === 0) { + node.status = TaskStatus.Success; + this.completeNode(entity); + return; + } + + // 根据复合节点类型处理 + switch (composite.compositeType) { + case CompositeType.Sequence: + this.handleSequence(entity, node, children); + break; + + case CompositeType.Selector: + this.handleSelector(entity, node, children); + break; + + case CompositeType.Parallel: + this.handleParallel(entity, node, children); + break; + + case CompositeType.ParallelSelector: + this.handleParallelSelector(entity, node, children); + break; + + case CompositeType.RandomSequence: + this.handleRandomSequence(entity, node, composite, children); + break; + + case CompositeType.RandomSelector: + this.handleRandomSelector(entity, node, composite, children); + break; + + default: + node.status = TaskStatus.Failure; + this.completeNode(entity); + break; + } + } + + /** + * 序列节点:所有子节点都成功才成功 + */ + private handleSequence(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void { + // 检查是否需要中止 + const sequenceNode = entity.getComponentByType(SequenceNode); + if (sequenceNode && sequenceNode.abortType !== AbortType.None) { + if (this.shouldAbort(entity, node, children, sequenceNode.abortType)) { + this.abortExecution(entity, node, children); + return; + } + } + + // 检查当前子节点 + if (node.currentChildIndex >= children.length) { + // 所有子节点都成功 + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + this.completeNode(entity); + return; + } + + const currentChild = children[node.currentChildIndex]; + const childNode = currentChild.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 如果子节点还没开始执行,激活它 + if (childNode.status === TaskStatus.Invalid) { + if (!currentChild.hasComponent(ActiveNode)) { + currentChild.addComponent(new ActiveNode()); + } + node.status = TaskStatus.Running; + return; + } + + // 检查子节点状态 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else if (childNode.status === TaskStatus.Failure) { + // 任一失败则失败 + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + this.completeNode(entity); + } else if (childNode.status === TaskStatus.Success) { + // 成功则移动到下一个子节点 + // 重置已完成的子节点状态,以便下次行为树重新执行时从头开始 + childNode.reset(); + node.currentChildIndex++; + // 继续保持活跃,下一帧处理下一个子节点 + node.status = TaskStatus.Running; + } + } + + /** + * 选择器节点:任一子节点成功就成功 + */ + private handleSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void { + // 检查是否需要中止 + const selectorNode = entity.getComponentByType(SelectorNode); + if (selectorNode && selectorNode.abortType !== AbortType.None) { + if (this.shouldAbort(entity, node, children, selectorNode.abortType)) { + this.abortExecution(entity, node, children); + return; + } + } + + // 检查当前子节点 + if (node.currentChildIndex >= children.length) { + // 所有子节点都失败 + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + this.completeNode(entity); + return; + } + + const currentChild = children[node.currentChildIndex]; + const childNode = currentChild.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 如果子节点还没开始执行,激活它 + if (childNode.status === TaskStatus.Invalid) { + if (!currentChild.hasComponent(ActiveNode)) { + currentChild.addComponent(new ActiveNode()); + } + node.status = TaskStatus.Running; + return; + } + + // 检查子节点状态 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else if (childNode.status === TaskStatus.Success) { + // 任一成功则成功 + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + this.completeNode(entity); + } else if (childNode.status === TaskStatus.Failure) { + // 失败则移动到下一个子节点 + // 重置已完成的子节点状态,以便下次行为树重新执行时从头开始 + childNode.reset(); + node.currentChildIndex++; + // 继续保持活跃,下一帧处理下一个子节点 + node.status = TaskStatus.Running; + } + } + + /** + * 并行节点:所有子节点都执行,全部成功才成功 + */ + private handleParallel(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void { + let hasRunning = false; + let hasFailed = false; + + // 激活所有子节点 + for (const child of children) { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + } + + const childNode = child.getComponent(BehaviorTreeNode); + if (!childNode) continue; + + if (childNode.status === TaskStatus.Running) { + hasRunning = true; + } else if (childNode.status === TaskStatus.Failure) { + hasFailed = true; + } + } + + if (hasRunning) { + node.status = TaskStatus.Running; + } else if (hasFailed) { + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + this.completeNode(entity); + } else { + // 所有子节点都成功 + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + this.completeNode(entity); + } + } + + /** + * 并行选择器:任一成功则成功 + */ + private handleParallelSelector(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void { + let hasRunning = false; + let hasSucceeded = false; + + // 激活所有子节点 + for (const child of children) { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + } + + const childNode = child.getComponent(BehaviorTreeNode); + if (!childNode) continue; + + if (childNode.status === TaskStatus.Running) { + hasRunning = true; + } else if (childNode.status === TaskStatus.Success) { + hasSucceeded = true; + } + } + + if (hasSucceeded) { + // 任一成功则成功 + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + // 停止所有子节点 + for (const child of children) { + child.removeComponentByType(ActiveNode); + } + this.completeNode(entity); + } else if (hasRunning) { + node.status = TaskStatus.Running; + } else { + // 所有子节点都失败 + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + this.completeNode(entity); + } + } + + /** + * 随机序列 + */ + private handleRandomSequence( + entity: Entity, + node: BehaviorTreeNode, + composite: CompositeNodeComponent, + children: readonly Entity[] + ): void { + // 获取洗牌后的子节点索引 + const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length); + + if (childIndex >= children.length) { + // 所有子节点都成功 + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + composite.resetShuffle(); + this.completeNode(entity); + return; + } + + const currentChild = children[childIndex]; + const childNode = currentChild.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 如果子节点还没开始执行,激活它 + if (childNode.status === TaskStatus.Invalid) { + if (!currentChild.hasComponent(ActiveNode)) { + currentChild.addComponent(new ActiveNode()); + } + node.status = TaskStatus.Running; + return; + } + + // 检查子节点状态 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else if (childNode.status === TaskStatus.Failure) { + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + composite.resetShuffle(); + this.completeNode(entity); + } else if (childNode.status === TaskStatus.Success) { + // 成功则移动到下一个子节点 + // 重置已完成的子节点状态,以便下次行为树重新执行时从头开始 + childNode.reset(); + node.currentChildIndex++; + node.status = TaskStatus.Running; + } + } + + /** + * 随机选择器 + */ + private handleRandomSelector( + entity: Entity, + node: BehaviorTreeNode, + composite: CompositeNodeComponent, + children: readonly Entity[] + ): void { + // 获取洗牌后的子节点索引 + const childIndex = composite.getNextChildIndex(node.currentChildIndex, children.length); + + if (childIndex >= children.length) { + // 所有子节点都失败 + node.status = TaskStatus.Failure; + node.currentChildIndex = 0; // 只重置索引,保持状态为Failure + composite.resetShuffle(); + this.completeNode(entity); + return; + } + + const currentChild = children[childIndex]; + const childNode = currentChild.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 如果子节点还没开始执行,激活它 + if (childNode.status === TaskStatus.Invalid) { + if (!currentChild.hasComponent(ActiveNode)) { + currentChild.addComponent(new ActiveNode()); + } + node.status = TaskStatus.Running; + return; + } + + // 检查子节点状态 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else if (childNode.status === TaskStatus.Success) { + node.status = TaskStatus.Success; + node.currentChildIndex = 0; // 只重置索引,保持状态为Success + composite.resetShuffle(); + this.completeNode(entity); + } else if (childNode.status === TaskStatus.Failure) { + // 失败则移动到下一个子节点 + // 重置已完成的子节点状态,以便下次行为树重新执行时从头开始 + childNode.reset(); + node.currentChildIndex++; + node.status = TaskStatus.Running; + } + } + + /** + * 检查是否应该中止当前执行 + */ + private shouldAbort( + entity: Entity, + node: BehaviorTreeNode, + children: readonly Entity[], + abortType: AbortType + ): boolean { + const currentIndex = node.currentChildIndex; + + // 如果还没开始执行任何子节点,不需要中止 + if (currentIndex === 0) { + return false; + } + + // Self: 检查当前执行路径中的条件节点是否失败 + if (abortType === AbortType.Self || abortType === AbortType.Both) { + // 检查当前正在执行的分支之前的条件节点 + for (let i = 0; i < currentIndex; i++) { + const child = children[i]; + const childNode = child.getComponent(BehaviorTreeNode); + if (childNode && childNode.nodeType === NodeType.Condition) { + // 如果条件节点现在失败了,应该中止 + if (childNode.status === TaskStatus.Failure) { + return true; + } + } + } + } + + // LowerPriority: 检查高优先级分支的条件是否满足 + if (abortType === AbortType.LowerPriority || abortType === AbortType.Both) { + // 检查当前索引之前的所有分支(优先级更高) + for (let i = 0; i < currentIndex; i++) { + const child = children[i]; + const childNode = child.getComponent(BehaviorTreeNode); + if (!childNode) continue; + + // 如果是条件节点且现在成功了 + if (childNode.nodeType === NodeType.Condition) { + if (this.evaluateCondition(child, childNode)) { + return true; + } + } + // 如果是复合节点,检查其第一个子节点(通常是条件) + else if (childNode.nodeType === NodeType.Composite && child.children.length > 0) { + const firstGrandChild = child.children[0]; + const firstGrandChildNode = firstGrandChild.getComponent(BehaviorTreeNode); + if (firstGrandChildNode && firstGrandChildNode.nodeType === NodeType.Condition) { + if (this.evaluateCondition(firstGrandChild, firstGrandChildNode)) { + return true; + } + } + } + } + } + + return false; + } + + /** + * 评估条件节点 + */ + private evaluateCondition(entity: Entity, node: BehaviorTreeNode): boolean { + if (node.nodeType !== NodeType.Condition) { + return false; + } + + let result = false; + + if (entity.hasComponent(BlackboardCompareCondition)) { + result = this.evaluateBlackboardCompare(entity); + } else if (entity.hasComponent(BlackboardExistsCondition)) { + result = this.evaluateBlackboardExists(entity); + } else if (entity.hasComponent(RandomProbabilityCondition)) { + result = this.evaluateRandomProbability(entity); + } else if (entity.hasComponent(ExecuteCondition)) { + result = this.evaluateCustomCondition(entity); + } + + return result; + } + + /** + * 评估黑板比较条件 + */ + private evaluateBlackboardCompare(entity: Entity): boolean { + const condition = entity.getComponent(BlackboardCompareCondition)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard || !blackboard.hasVariable(condition.variableName)) { + return false; + } + + const value = blackboard.getValue(condition.variableName); + let compareValue = condition.compareValue; + + if (typeof compareValue === 'string') { + compareValue = this.resolveVariableReferences(compareValue, blackboard); + } + + let result = false; + switch (condition.operator) { + case CompareOperator.Equal: + result = value === compareValue; + break; + case CompareOperator.NotEqual: + result = value !== compareValue; + break; + case CompareOperator.Greater: + result = value > compareValue; + break; + case CompareOperator.GreaterOrEqual: + result = value >= compareValue; + break; + case CompareOperator.Less: + result = value < compareValue; + break; + case CompareOperator.LessOrEqual: + result = value <= compareValue; + break; + case CompareOperator.Contains: + if (typeof value === 'string') { + result = value.includes(compareValue); + } else if (Array.isArray(value)) { + result = value.includes(compareValue); + } + break; + case CompareOperator.Matches: + if (typeof value === 'string' && typeof compareValue === 'string') { + const regex = new RegExp(compareValue); + result = regex.test(value); + } + break; + } + + return condition.invertResult ? !result : result; + } + + /** + * 评估黑板变量存在性 + */ + private evaluateBlackboardExists(entity: Entity): boolean { + const condition = entity.getComponent(BlackboardExistsCondition)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard) { + return false; + } + + let result = blackboard.hasVariable(condition.variableName); + + if (result && condition.checkNotNull) { + const value = blackboard.getValue(condition.variableName); + result = value !== null && value !== undefined; + } + + return condition.invertResult ? !result : result; + } + + /** + * 评估随机概率 + */ + private evaluateRandomProbability(entity: Entity): boolean { + const condition = entity.getComponent(RandomProbabilityCondition)!; + return condition.evaluate(); + } + + /** + * 评估自定义条件 + */ + private evaluateCustomCondition(entity: Entity): boolean { + const condition = entity.getComponent(ExecuteCondition)!; + const func = condition.getFunction(); + + if (!func) { + return false; + } + + const blackboard = this.findBlackboard(entity); + const result = func(entity, blackboard, 0); + + return condition.invertResult ? !result : result; + } + + /** + * 解析字符串中的变量引用 + */ + private resolveVariableReferences(value: string, blackboard: BlackboardComponent): any { + const pureMatch = value.match(/^{{\s*(\w+)\s*}}$/); + if (pureMatch) { + const varName = pureMatch[1]; + if (blackboard.hasVariable(varName)) { + return blackboard.getValue(varName); + } + return value; + } + + return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => { + if (blackboard.hasVariable(varName)) { + const val = blackboard.getValue(varName); + return val !== undefined ? String(val) : match; + } + return match; + }); + } + + /** + * 查找黑板组件 + */ + private findBlackboard(entity: Entity): BlackboardComponent | undefined { + let current: Entity | null = entity; + + while (current) { + const blackboard = current.getComponent(BlackboardComponent); + if (blackboard) { + return blackboard; + } + current = current.parent; + } + + return undefined; + } + + /** + * 中止当前执行 + */ + private abortExecution(entity: Entity, node: BehaviorTreeNode, children: readonly Entity[]): void { + // 停止当前正在执行的子节点 + const currentIndex = node.currentChildIndex; + if (currentIndex < children.length) { + const currentChild = children[currentIndex]; + this.deactivateNode(currentChild); + } + + // 重置节点状态,从头开始 + node.currentChildIndex = 0; + node.status = TaskStatus.Running; + + // 不需要 completeNode,因为我们要继续执行(从头开始) + } + + /** + * 递归停用节点及其所有子节点 + */ + private deactivateNode(entity: Entity): void { + // 移除活跃标记 + entity.removeComponentByType(ActiveNode); + + // 重置节点状态 + const node = entity.getComponent(BehaviorTreeNode); + if (node) { + node.reset(); + } + + // 递归停用所有子节点 + for (const child of entity.children) { + this.deactivateNode(child); + } + } + + /** + * 递归重置所有子节点的状态 + */ + private resetAllChildren(entity: Entity): void { + for (const child of entity.children) { + const childNode = child.getComponent(BehaviorTreeNode); + if (childNode) { + childNode.reset(); + } + // 递归重置孙子节点 + this.resetAllChildren(child); + } + } + + /** + * 完成节点执行 + */ + private completeNode(entity: Entity): void { + entity.removeComponentByType(ActiveNode); + + // 如果是复合节点完成,重置所有子节点状态 + const node = entity.getComponent(BehaviorTreeNode); + if (node && node.nodeType === NodeType.Composite) { + this.resetAllChildren(entity); + } + + // 通知父节点 + if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) { + if (!entity.parent.hasComponent(ActiveNode)) { + entity.parent.addComponent(new ActiveNode()); + } + } + } + + protected override getLoggerName(): string { + return 'CompositeExecutionSystem'; + } +} diff --git a/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts b/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts new file mode 100644 index 00000000..c520eff4 --- /dev/null +++ b/packages/behavior-tree/src/Systems/DecoratorExecutionSystem.ts @@ -0,0 +1,515 @@ +import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { DecoratorNodeComponent } from '../Components/DecoratorNodeComponent'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; +import { ActiveNode } from '../Components/ActiveNode'; +import { PropertyBindings } from '../Components/PropertyBindings'; +import { LogOutput } from '../Components/LogOutput'; +import { TaskStatus, NodeType, DecoratorType } from '../Types/TaskStatus'; +import { RepeaterNode } from '../Components/Decorators/RepeaterNode'; +import { ConditionalNode } from '../Components/Decorators/ConditionalNode'; +import { CooldownNode } from '../Components/Decorators/CooldownNode'; +import { TimeoutNode } from '../Components/Decorators/TimeoutNode'; + +/** + * 装饰器节点执行系统 + * + * 负责处理所有活跃的装饰器节点 + * 读取子节点状态,根据装饰器规则决定自己的状态 + * + * updateOrder: 200 (在叶子节点之后执行) + */ +export class DecoratorExecutionSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(BehaviorTreeNode, ActiveNode)); + this.updateOrder = 200; + } + + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const node = entity.getComponent(BehaviorTreeNode)!; + + // 只处理装饰器节点 + if (node.nodeType !== NodeType.Decorator) { + continue; + } + + // 使用 getComponentByType 支持继承查找 + const decorator = entity.getComponentByType(DecoratorNodeComponent); + if (!decorator) { + continue; + } + + this.executeDecorator(entity, node, decorator); + } + } + + /** + * 执行装饰器逻辑 + */ + private executeDecorator(entity: Entity, node: BehaviorTreeNode, decorator: DecoratorNodeComponent): void { + const children = entity.children; + + if (children.length === 0) { + this.logger.warn('装饰器节点没有子节点'); + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + const child = children[0]; // 装饰器只有一个子节点 + const childNode = child.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 根据装饰器类型处理 + switch (decorator.decoratorType) { + case DecoratorType.Inverter: + this.handleInverter(entity, node, child, childNode); + break; + + case DecoratorType.Repeater: + this.handleRepeater(entity, node, decorator, child, childNode); + break; + + case DecoratorType.UntilSuccess: + this.handleUntilSuccess(entity, node, child, childNode); + break; + + case DecoratorType.UntilFail: + this.handleUntilFail(entity, node, child, childNode); + break; + + case DecoratorType.AlwaysSucceed: + this.handleAlwaysSucceed(entity, node, child, childNode); + break; + + case DecoratorType.AlwaysFail: + this.handleAlwaysFail(entity, node, child, childNode); + break; + + case DecoratorType.Conditional: + this.handleConditional(entity, node, decorator, child, childNode); + break; + + case DecoratorType.Cooldown: + this.handleCooldown(entity, node, decorator, child, childNode); + break; + + case DecoratorType.Timeout: + this.handleTimeout(entity, node, decorator, child, childNode); + break; + + default: + node.status = TaskStatus.Failure; + this.completeNode(entity); + break; + } + } + + /** + * 反转装饰器 + */ + private handleInverter(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void { + if (!child.hasComponent(ActiveNode)) { + // 子节点未激活,激活它 + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + } else { + // 子节点正在执行 + node.status = TaskStatus.Running; + } + + // 如果子节点完成了 + if (childNode.status === TaskStatus.Success || childNode.status === TaskStatus.Failure) { + // 反转结果 + node.status = childNode.status === TaskStatus.Success ? TaskStatus.Failure : TaskStatus.Success; + this.completeNode(entity); + } + } + + /** + * 重复装饰器 + */ + private handleRepeater( + entity: Entity, + node: BehaviorTreeNode, + decorator: DecoratorNodeComponent, + child: Entity, + childNode: BehaviorTreeNode + ): void { + const repeater = decorator as RepeaterNode; + + // 从 PropertyBindings 读取绑定的黑板变量值 + const repeatCount = this.resolvePropertyValue(entity, 'repeatCount', repeater.repeatCount); + const endOnFailure = this.resolvePropertyValue(entity, 'endOnFailure', repeater.endOnFailure); + + // 如果子节点未激活,激活它 + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + // 子节点正在执行 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + return; + } + + // 子节点完成 + if (childNode.status === TaskStatus.Failure && endOnFailure) { + node.status = TaskStatus.Failure; + repeater.reset(); + this.completeNode(entity); + return; + } + + // 增加重复计数 + repeater.incrementRepeat(); + + // 检查是否继续重复(使用解析后的值) + const shouldContinue = (repeatCount === -1) || (repeater.currentRepeatCount < repeatCount); + if (shouldContinue) { + // 重置子节点并继续 + childNode.invalidate(); + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + } else { + // 完成 + node.status = TaskStatus.Success; + repeater.reset(); + this.completeNode(entity); + } + } + + /** + * 直到成功装饰器 + */ + private handleUntilSuccess(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Success) { + node.status = TaskStatus.Success; + this.completeNode(entity); + } else { + // 失败则重试 + childNode.invalidate(); + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + } + } + + /** + * 直到失败装饰器 + */ + private handleUntilFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Failure) { + node.status = TaskStatus.Success; + this.completeNode(entity); + } else { + // 成功则重试 + childNode.invalidate(); + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + } + } + + /** + * 总是成功装饰器 + */ + private handleAlwaysSucceed(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else { + node.status = TaskStatus.Success; + this.completeNode(entity); + } + } + + /** + * 总是失败装饰器 + */ + private handleAlwaysFail(entity: Entity, node: BehaviorTreeNode, child: Entity, childNode: BehaviorTreeNode): void { + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else { + node.status = TaskStatus.Failure; + this.completeNode(entity); + } + } + + /** + * 条件装饰器 + */ + private handleConditional( + entity: Entity, + node: BehaviorTreeNode, + decorator: DecoratorNodeComponent, + child: Entity, + childNode: BehaviorTreeNode + ): void { + const conditional = decorator as ConditionalNode; + + // 评估条件 + const conditionMet = conditional.evaluateCondition(entity, this.findBlackboard(entity)); + + if (!conditionMet) { + // 条件不满足,直接失败 + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 条件满足,执行子节点 + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + node.status = childNode.status; + + if (childNode.status !== TaskStatus.Running) { + this.completeNode(entity); + } + } + + /** + * 冷却装饰器 + */ + private handleCooldown( + entity: Entity, + node: BehaviorTreeNode, + decorator: DecoratorNodeComponent, + child: Entity, + childNode: BehaviorTreeNode + ): void { + const cooldown = decorator as CooldownNode; + + // 从 PropertyBindings 读取绑定的黑板变量值 + const cooldownTime = this.resolvePropertyValue(entity, 'cooldownTime', cooldown.cooldownTime); + + // 检查冷却(使用解析后的值) + // 如果从未执行过(lastExecutionTime === 0),允许执行 + const timeSinceLastExecution = Time.totalTime - cooldown.lastExecutionTime; + const canExecute = (cooldown.lastExecutionTime === 0) || (timeSinceLastExecution >= cooldownTime); + + // 添加调试日志 + this.outputLog( + entity, + `[冷却检查] Time.totalTime=${Time.totalTime.toFixed(3)}, lastExecution=${cooldown.lastExecutionTime.toFixed(3)}, ` + + `cooldownTime=${cooldownTime}, timeSince=${timeSinceLastExecution.toFixed(3)}, canExecute=${canExecute}, childStatus=${childNode.status}`, + 'info' + ); + + if (!canExecute) { + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 先检查子节点状态,再决定是否激活 + if (childNode.status !== TaskStatus.Invalid && childNode.status !== TaskStatus.Running) { + // 子节点已经完成(Success 或 Failure) + node.status = childNode.status; + cooldown.recordExecution(Time.totalTime); + this.outputLog( + entity, + `[冷却记录] 记录执行时间: ${Time.totalTime.toFixed(3)}, 下次可执行时间: ${(Time.totalTime + cooldownTime).toFixed(3)}`, + 'info' + ); + this.completeNode(entity); + return; + } + + // 子节点还没开始或正在执行 + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + node.status = TaskStatus.Running; + } + + /** + * 超时装饰器 + */ + private handleTimeout( + entity: Entity, + node: BehaviorTreeNode, + decorator: DecoratorNodeComponent, + child: Entity, + childNode: BehaviorTreeNode + ): void { + const timeout = decorator as TimeoutNode; + + // 从 PropertyBindings 读取绑定的黑板变量值 + const timeoutDuration = this.resolvePropertyValue(entity, 'timeoutDuration', timeout.timeoutDuration); + + timeout.recordStartTime(Time.totalTime); + + // 检查超时(使用解析后的值) + const isTimeout = timeout.startTime > 0 && (Time.totalTime - timeout.startTime >= timeoutDuration); + if (isTimeout) { + node.status = TaskStatus.Failure; + timeout.reset(); + // 移除子节点的活跃标记 + child.removeComponentByType(ActiveNode); + this.completeNode(entity); + return; + } + + if (!child.hasComponent(ActiveNode)) { + child.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + node.status = childNode.status; + + if (childNode.status !== TaskStatus.Running) { + timeout.reset(); + this.completeNode(entity); + } + } + + /** + * 完成节点执行 + */ + private completeNode(entity: Entity): void { + entity.removeComponentByType(ActiveNode); + + // 通知父节点 + if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) { + if (!entity.parent.hasComponent(ActiveNode)) { + entity.parent.addComponent(new ActiveNode()); + } + } + } + + /** + * 查找黑板组件(向上遍历父节点) + */ + private findBlackboard(entity: Entity): BlackboardComponent | undefined { + let current: Entity | null = entity; + + while (current) { + const blackboard = current.getComponent(BlackboardComponent); + if (blackboard) { + return blackboard; + } + current = current.parent; + } + + return undefined; + } + + /** + * 解析属性值 + * 如果属性绑定到黑板变量,从黑板读取最新值 + */ + private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any { + const bindings = entity.getComponent(PropertyBindings); + if (!bindings || !bindings.hasBinding(propertyName)) { + return defaultValue; + } + + const blackboardKey = bindings.getBinding(propertyName)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard || !blackboard.hasVariable(blackboardKey)) { + return defaultValue; + } + + return blackboard.getValue(blackboardKey); + } + + /** + * 查找根实体 + */ + private findRootEntity(entity: Entity): Entity | null { + let current: Entity | null = entity; + while (current) { + if (!current.parent) { + return current; + } + current = current.parent; + } + return null; + } + + /** + * 统一的日志输出方法 + * 同时输出到控制台和LogOutput组件,确保用户在UI中能看到 + */ + private outputLog( + entity: Entity, + message: string, + level: 'log' | 'info' | 'warn' | 'error' = 'info' + ): void { + switch (level) { + case 'info': + this.logger.info(message); + break; + case 'warn': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + default: + this.logger.info(message); + break; + } + + const rootEntity = this.findRootEntity(entity); + if (rootEntity) { + const logOutput = rootEntity.getComponent(LogOutput); + if (logOutput) { + logOutput.addMessage(message, level); + } + } + } + + protected override getLoggerName(): string { + return 'DecoratorExecutionSystem'; + } +} diff --git a/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts b/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts new file mode 100644 index 00000000..edd204b7 --- /dev/null +++ b/packages/behavior-tree/src/Systems/LeafExecutionSystem.ts @@ -0,0 +1,565 @@ +import { EntitySystem, Matcher, Entity, Time } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; +import { ActiveNode } from '../Components/ActiveNode'; +import { PropertyBindings } from '../Components/PropertyBindings'; +import { LogOutput } from '../Components/LogOutput'; +import { TaskStatus, NodeType } from '../Types/TaskStatus'; + +// 导入具体的动作组件 +import { WaitAction } from '../Components/Actions/WaitAction'; +import { LogAction } from '../Components/Actions/LogAction'; +import { SetBlackboardValueAction } from '../Components/Actions/SetBlackboardValueAction'; +import { ModifyBlackboardValueAction, ModifyOperation } from '../Components/Actions/ModifyBlackboardValueAction'; +import { ExecuteAction } from '../Components/Actions/ExecuteAction'; + +// 导入具体的条件组件 +import { BlackboardCompareCondition, CompareOperator } from '../Components/Conditions/BlackboardCompareCondition'; +import { BlackboardExistsCondition } from '../Components/Conditions/BlackboardExistsCondition'; +import { RandomProbabilityCondition } from '../Components/Conditions/RandomProbabilityCondition'; +import { ExecuteCondition } from '../Components/Conditions/ExecuteCondition'; + +/** + * 叶子节点执行系统 + * + * 负责执行所有活跃的叶子节点(Action 和 Condition) + * 只处理带有 ActiveNode 标记的节点 + * + * updateOrder: 100 (最先执行) + */ +export class LeafExecutionSystem extends EntitySystem { + constructor() { + // 只处理活跃的叶子节点 + super(Matcher.empty().all(BehaviorTreeNode, ActiveNode)); + this.updateOrder = 100; + } + + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const node = entity.getComponent(BehaviorTreeNode)!; + + // 只处理叶子节点 + if (node.nodeType === NodeType.Action) { + this.executeAction(entity, node); + } else if (node.nodeType === NodeType.Condition) { + this.executeCondition(entity, node); + } + } + } + + /** + * 执行动作节点 + */ + private executeAction(entity: Entity, node: BehaviorTreeNode): void { + let status = TaskStatus.Failure; + + const { displayName, nodeIdShort } = this.getNodeInfo(entity); + + // 检测实体有哪些动作组件并执行 + if (entity.hasComponent(WaitAction)) { + status = this.executeWaitAction(entity); + } else if (entity.hasComponent(LogAction)) { + status = this.executeLogAction(entity); + } else if (entity.hasComponent(SetBlackboardValueAction)) { + status = this.executeSetBlackboardValue(entity); + } else if (entity.hasComponent(ModifyBlackboardValueAction)) { + status = this.executeModifyBlackboardValue(entity); + } else if (entity.hasComponent(ExecuteAction)) { + status = this.executeCustomAction(entity); + } else { + this.outputLog(entity, `动作节点没有找到任何已知的动作组件`, 'warn'); + } + + node.status = status; + + // 输出节点执行后的状态 + const statusText = status === TaskStatus.Success ? 'Success' : + status === TaskStatus.Failure ? 'Failure' : + status === TaskStatus.Running ? 'Running' : 'Unknown'; + + if (status !== TaskStatus.Running) { + this.outputLog(entity, `[${displayName}#${nodeIdShort}] 执行完成 -> ${statusText}`, + status === TaskStatus.Success ? 'info' : 'warn'); + } + + // 如果不是 Running 状态,节点执行完成 + if (status !== TaskStatus.Running) { + this.deactivateNode(entity); + this.notifyParent(entity); + } + } + + /** + * 执行等待动作 + */ + private executeWaitAction(entity: Entity): TaskStatus { + const waitAction = entity.getComponent(WaitAction)!; + const node = entity.getComponent(BehaviorTreeNode); + + const { displayName, nodeIdShort } = this.getNodeInfo(entity); + + // 从 PropertyBindings 读取绑定的黑板变量值 + const waitTime = this.resolvePropertyValue(entity, 'waitTime', waitAction.waitTime); + + waitAction.elapsedTime += Time.deltaTime; + + // 输出调试信息(显示在UI中) + this.outputLog( + entity, + `[${displayName}#${nodeIdShort}] deltaTime=${Time.deltaTime.toFixed(3)}s, ` + + `elapsed=${waitAction.elapsedTime.toFixed(3)}s/${waitTime.toFixed(3)}s`, + 'info' + ); + + if (waitAction.elapsedTime >= waitTime) { + waitAction.reset(); + this.outputLog(entity, `[${displayName}#${nodeIdShort}] 等待完成,返回成功`, 'info'); + return TaskStatus.Success; + } + + return TaskStatus.Running; + } + + /** + * 执行日志动作 + */ + private executeLogAction(entity: Entity): TaskStatus { + const logAction = entity.getComponent(LogAction)!; + const node = entity.getComponent(BehaviorTreeNode); + + // 从 PropertyBindings 读取绑定的黑板变量值 + let message = this.resolvePropertyValue(entity, 'message', logAction.message); + + const { displayName, nodeIdShort } = this.getNodeInfo(entity); + + // 在消息前添加节点ID信息 + if (node) { + message = `[${displayName}#${nodeIdShort}] ${message}`; + } + + if (logAction.includeEntityInfo) { + message = `[Entity: ${entity.name}] ${message}`; + } + + // 输出到浏览器控制台 + switch (logAction.level) { + case 'info': + console.info(message); + break; + case 'warn': + console.warn(message); + break; + case 'error': + console.error(message); + break; + default: + console.log(message); + break; + } + + // 同时记录到LogOutput组件,以便在UI中显示 + const rootEntity = this.findRootEntity(entity); + if (rootEntity) { + const logOutput = rootEntity.getComponent(LogOutput); + if (logOutput) { + logOutput.addMessage(message, logAction.level); + } + } + + return TaskStatus.Success; + } + + /** + * 查找根实体 + */ + private findRootEntity(entity: Entity): Entity | null { + let current: Entity | null = entity; + while (current) { + if (!current.parent) { + return current; + } + current = current.parent; + } + return null; + } + + /** + * 执行设置黑板变量值 + */ + private executeSetBlackboardValue(entity: Entity): TaskStatus { + const action = entity.getComponent(SetBlackboardValueAction)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard) { + this.outputLog(entity, '未找到黑板组件', 'warn'); + return TaskStatus.Failure; + } + + let valueToSet: any; + + // 如果指定了源变量,从中读取值 + if (action.sourceVariable) { + if (!blackboard.hasVariable(action.sourceVariable)) { + this.outputLog(entity, `源变量不存在: ${action.sourceVariable}`, 'warn'); + return TaskStatus.Failure; + } + valueToSet = blackboard.getValue(action.sourceVariable); + } else { + // 从 PropertyBindings 读取绑定的值 + valueToSet = this.resolvePropertyValue(entity, 'value', action.value); + } + + const success = blackboard.setValue(action.variableName, valueToSet, action.force); + return success ? TaskStatus.Success : TaskStatus.Failure; + } + + /** + * 执行修改黑板变量值 + */ + private executeModifyBlackboardValue(entity: Entity): TaskStatus { + const action = entity.getComponent(ModifyBlackboardValueAction)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard) { + this.outputLog(entity, '未找到黑板组件', 'warn'); + return TaskStatus.Failure; + } + + if (!blackboard.hasVariable(action.variableName)) { + this.outputLog(entity, `变量不存在: ${action.variableName}`, 'warn'); + return TaskStatus.Failure; + } + + let currentValue = blackboard.getValue(action.variableName); + + // 从 PropertyBindings 读取绑定的值 + let operand = this.resolvePropertyValue(entity, 'operand', action.operand); + + // 执行操作 + let newValue: any; + switch (action.operation) { + case ModifyOperation.Add: + newValue = Number(currentValue) + Number(operand); + break; + case ModifyOperation.Subtract: + newValue = Number(currentValue) - Number(operand); + break; + case ModifyOperation.Multiply: + newValue = Number(currentValue) * Number(operand); + break; + case ModifyOperation.Divide: + if (Number(operand) === 0) { + this.outputLog(entity, '除数不能为0', 'warn'); + return TaskStatus.Failure; + } + newValue = Number(currentValue) / Number(operand); + break; + case ModifyOperation.Modulo: + newValue = Number(currentValue) % Number(operand); + break; + case ModifyOperation.Append: + if (Array.isArray(currentValue)) { + newValue = [...currentValue, operand]; + } else if (typeof currentValue === 'string') { + newValue = currentValue + operand; + } else { + this.outputLog(entity, `变量 ${action.variableName} 不支持 append 操作`, 'warn'); + return TaskStatus.Failure; + } + break; + case ModifyOperation.Remove: + if (Array.isArray(currentValue)) { + newValue = currentValue.filter(item => item !== operand); + } else { + this.outputLog(entity, `变量 ${action.variableName} 不是数组,不支持 remove 操作`, 'warn'); + return TaskStatus.Failure; + } + break; + default: + return TaskStatus.Failure; + } + + const success = blackboard.setValue(action.variableName, newValue, action.force); + return success ? TaskStatus.Success : TaskStatus.Failure; + } + + /** + * 执行自定义动作 + */ + private executeCustomAction(entity: Entity): TaskStatus { + const action = entity.getComponent(ExecuteAction)!; + const func = action.getFunction(); + + if (!func) { + return TaskStatus.Failure; + } + + const blackboard = this.findBlackboard(entity); + return func(entity, blackboard, Time.deltaTime); + } + + /** + * 执行条件节点 + */ + private executeCondition(entity: Entity, node: BehaviorTreeNode): void { + let result = false; + + const { displayName, nodeIdShort } = this.getNodeInfo(entity); + + // 检测实体有哪些条件组件并评估 + if (entity.hasComponent(BlackboardCompareCondition)) { + result = this.evaluateBlackboardCompare(entity); + } else if (entity.hasComponent(BlackboardExistsCondition)) { + result = this.evaluateBlackboardExists(entity); + } else if (entity.hasComponent(RandomProbabilityCondition)) { + result = this.evaluateRandomProbability(entity); + } else if (entity.hasComponent(ExecuteCondition)) { + result = this.evaluateCustomCondition(entity); + } else { + this.outputLog(entity, '条件节点没有找到任何已知的条件组件', 'warn'); + } + + node.status = result ? TaskStatus.Success : TaskStatus.Failure; + + // 输出条件评估结果 + const statusText = result ? 'Success (true)' : 'Failure (false)'; + this.outputLog(entity, `[${displayName}#${nodeIdShort}] 条件评估 -> ${statusText}`, + result ? 'info' : 'warn'); + + // 条件节点总是立即完成 + this.deactivateNode(entity); + this.notifyParent(entity); + } + + /** + * 评估黑板比较条件 + */ + private evaluateBlackboardCompare(entity: Entity): boolean { + const condition = entity.getComponent(BlackboardCompareCondition)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard || !blackboard.hasVariable(condition.variableName)) { + return false; + } + + const value = blackboard.getValue(condition.variableName); + + // 从 PropertyBindings 读取绑定的值 + let compareValue = this.resolvePropertyValue(entity, 'compareValue', condition.compareValue); + + let result = false; + switch (condition.operator) { + case CompareOperator.Equal: + result = value === compareValue; + break; + case CompareOperator.NotEqual: + result = value !== compareValue; + break; + case CompareOperator.Greater: + result = value > compareValue; + break; + case CompareOperator.GreaterOrEqual: + result = value >= compareValue; + break; + case CompareOperator.Less: + result = value < compareValue; + break; + case CompareOperator.LessOrEqual: + result = value <= compareValue; + break; + case CompareOperator.Contains: + if (typeof value === 'string') { + result = value.includes(compareValue); + } else if (Array.isArray(value)) { + result = value.includes(compareValue); + } + break; + case CompareOperator.Matches: + if (typeof value === 'string' && typeof compareValue === 'string') { + const regex = new RegExp(compareValue); + result = regex.test(value); + } + break; + } + + return condition.invertResult ? !result : result; + } + + /** + * 评估黑板变量存在性 + */ + private evaluateBlackboardExists(entity: Entity): boolean { + const condition = entity.getComponent(BlackboardExistsCondition)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard) { + return false; + } + + let result = blackboard.hasVariable(condition.variableName); + + if (result && condition.checkNotNull) { + const value = blackboard.getValue(condition.variableName); + result = value !== null && value !== undefined; + } + + return condition.invertResult ? !result : result; + } + + /** + * 评估随机概率 + */ + private evaluateRandomProbability(entity: Entity): boolean { + const condition = entity.getComponent(RandomProbabilityCondition)!; + + // 从 PropertyBindings 读取绑定的黑板变量值 + const probability = this.resolvePropertyValue(entity, 'probability', condition.probability); + + // 使用解析后的概率值进行评估 + if (condition.alwaysRandomize || condition['cachedResult'] === undefined) { + condition['cachedResult'] = Math.random() < probability; + } + return condition['cachedResult']; + } + + /** + * 评估自定义条件 + */ + private evaluateCustomCondition(entity: Entity): boolean { + const condition = entity.getComponent(ExecuteCondition)!; + const func = condition.getFunction(); + + if (!func) { + return false; + } + + const blackboard = this.findBlackboard(entity); + const result = func(entity, blackboard, Time.deltaTime); + + return condition.invertResult ? !result : result; + } + + /** + * 解析属性值 + * 如果属性绑定到黑板变量,从黑板读取最新值 + */ + private resolvePropertyValue(entity: Entity, propertyName: string, defaultValue: any): any { + // 检查实体是否有 PropertyBindings 组件 + const bindings = entity.getComponent(PropertyBindings); + if (!bindings || !bindings.hasBinding(propertyName)) { + // 没有绑定,返回默认值 + return defaultValue; + } + + // 有绑定,从黑板读取值 + const blackboardKey = bindings.getBinding(propertyName)!; + const blackboard = this.findBlackboard(entity); + + if (!blackboard) { + this.outputLog(entity, `[属性绑定] 未找到黑板组件,实体: ${entity.name}`, 'warn'); + return defaultValue; + } + + if (!blackboard.hasVariable(blackboardKey)) { + this.outputLog(entity, `[属性绑定] 黑板变量不存在: ${blackboardKey}`, 'warn'); + return defaultValue; + } + + const value = blackboard.getValue(blackboardKey); + return value; + } + + + /** + * 移除节点的活跃标记 + */ + private deactivateNode(entity: Entity): void { + entity.removeComponentByType(ActiveNode); + } + + /** + * 通知父节点子节点已完成 + */ + private notifyParent(entity: Entity): void { + if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) { + // 为父节点添加活跃标记,让它在下一帧被处理 + if (!entity.parent.hasComponent(ActiveNode)) { + entity.parent.addComponent(new ActiveNode()); + } + } + } + + /** + * 查找黑板组件(向上遍历父节点) + */ + private findBlackboard(entity: Entity): BlackboardComponent | undefined { + let current: Entity | null = entity; + + while (current) { + const blackboard = current.getComponent(BlackboardComponent); + if (blackboard) { + return blackboard; + } + current = current.parent; + } + + return undefined; + } + + /** + * 从Entity提取节点显示名称和ID + */ + private getNodeInfo(entity: Entity): { displayName: string; nodeIdShort: string } { + let displayName = 'Node'; + let nodeIdShort = ''; + + if (entity.name && entity.name.includes('#')) { + const parts = entity.name.split('#'); + displayName = parts[0]; + nodeIdShort = parts[1]; + } else { + nodeIdShort = entity.id.toString().substring(0, 8); + } + + return { displayName, nodeIdShort }; + } + + /** + * 统一的日志输出方法 + * 同时输出到控制台和LogOutput组件,确保用户在UI中能看到 + */ + private outputLog( + entity: Entity, + message: string, + level: 'log' | 'info' | 'warn' | 'error' = 'info' + ): void { + // 输出到浏览器控制台(方便开发调试) + switch (level) { + case 'info': + this.logger.info(message); + break; + case 'warn': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + default: + this.logger.info(message); + break; + } + + // 输出到LogOutput组件(显示在UI中) + const rootEntity = this.findRootEntity(entity); + if (rootEntity) { + const logOutput = rootEntity.getComponent(LogOutput); + if (logOutput) { + logOutput.addMessage(message, level); + } + } + } + + protected override getLoggerName(): string { + return 'LeafExecutionSystem'; + } +} diff --git a/packages/behavior-tree/src/Systems/RootExecutionSystem.ts b/packages/behavior-tree/src/Systems/RootExecutionSystem.ts new file mode 100644 index 00000000..1624160e --- /dev/null +++ b/packages/behavior-tree/src/Systems/RootExecutionSystem.ts @@ -0,0 +1,388 @@ +import { EntitySystem, Matcher, Entity, Core } from '@esengine/ecs-framework'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { RootNode } from '../Components/Composites/RootNode'; +import { ActiveNode } from '../Components/ActiveNode'; +import { TaskStatus, NodeType } from '../Types/TaskStatus'; +import { SubTreeNode } from '../Components/Composites/SubTreeNode'; +import { LogOutput } from '../Components/LogOutput'; +import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader'; +import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader'; +import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; + +/** + * 预加载状态 + */ +enum PreloadState { + /** 未开始预加载 */ + NotStarted, + /** 正在预加载 */ + Loading, + /** 预加载完成 */ + Completed, + /** 预加载失败 */ + Failed +} + +/** + * 根节点执行系统 + * + * 专门处理根节点的执行逻辑 + * 根节点的职责: + * 1. 扫描并预加载所有标记为 preload=true 的子树 + * 2. 激活第一个子节点,并根据子节点的状态来设置自己的状态 + * + * updateOrder: 350 (在所有其他执行系统之后) + */ +export class RootExecutionSystem extends EntitySystem { + /** 跟踪每个根节点的预加载状态 */ + private preloadStates: Map = new Map(); + + /** 跟踪预加载任务 */ + private preloadTasks: Map> = new Map(); + + /** AssetLoader 实例 */ + private assetLoader?: FileSystemAssetLoader; + + constructor() { + super(Matcher.empty().all(BehaviorTreeNode, ActiveNode)); + this.updateOrder = 350; + } + + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const node = entity.getComponent(BehaviorTreeNode)!; + + // 只处理根节点 + if (node.nodeType !== NodeType.Composite) { + continue; + } + + // 检查是否是根节点 + if (!entity.hasComponent(RootNode)) { + continue; + } + + this.executeRoot(entity, node); + } + } + + /** + * 执行根节点逻辑 + */ + private executeRoot(entity: Entity, node: BehaviorTreeNode): void { + // 检查预加载状态 + const preloadState = this.preloadStates.get(entity.id) || PreloadState.NotStarted; + + if (preloadState === PreloadState.NotStarted) { + // 开始预加载 + this.startPreload(entity, node); + return; + } else if (preloadState === PreloadState.Loading) { + // 正在预加载,等待 + node.status = TaskStatus.Running; + return; + } else if (preloadState === PreloadState.Failed) { + // 预加载失败,标记为失败 + node.status = TaskStatus.Failure; + entity.removeComponentByType(ActiveNode); + return; + } + + // 预加载完成,执行正常逻辑 + const children = entity.children; + + // 如果没有子节点,标记为成功 + if (children.length === 0) { + node.status = TaskStatus.Success; + return; + } + + // 获取第一个子节点 + const firstChild = children[0]; + const childNode = firstChild.getComponent(BehaviorTreeNode); + + if (!childNode) { + node.status = TaskStatus.Failure; + return; + } + + // 激活第一个子节点(如果还没激活) + if (!firstChild.hasComponent(ActiveNode)) { + firstChild.addComponent(new ActiveNode()); + node.status = TaskStatus.Running; + return; + } + + // 根据第一个子节点的状态来设置根节点的状态 + if (childNode.status === TaskStatus.Running) { + node.status = TaskStatus.Running; + } else if (childNode.status === TaskStatus.Success) { + node.status = TaskStatus.Success; + // 移除根节点的 ActiveNode,结束整个行为树 + entity.removeComponentByType(ActiveNode); + } else if (childNode.status === TaskStatus.Failure) { + node.status = TaskStatus.Failure; + // 移除根节点的 ActiveNode,结束整个行为树 + entity.removeComponentByType(ActiveNode); + } + } + + /** + * 开始预加载子树 + */ + private startPreload(rootEntity: Entity, node: BehaviorTreeNode): void { + // 扫描所有需要预加载的子树节点 + const subTreeNodesToPreload = this.scanSubTreeNodes(rootEntity); + + if (subTreeNodesToPreload.length === 0) { + // 没有需要预加载的子树,直接标记为完成 + this.preloadStates.set(rootEntity.id, PreloadState.Completed); + this.outputLog(rootEntity, '没有需要预加载的子树', 'info'); + return; + } + + // 标记为正在加载 + this.preloadStates.set(rootEntity.id, PreloadState.Loading); + node.status = TaskStatus.Running; + + this.outputLog( + rootEntity, + `开始预加载 ${subTreeNodesToPreload.length} 个子树...`, + 'info' + ); + + // 并行加载所有子树 + const loadTask = this.preloadAllSubTrees(rootEntity, subTreeNodesToPreload); + this.preloadTasks.set(rootEntity.id, loadTask); + + // 异步处理加载结果 + loadTask.then(() => { + this.preloadStates.set(rootEntity.id, PreloadState.Completed); + this.outputLog(rootEntity, '所有子树预加载完成', 'info'); + }).catch(error => { + this.preloadStates.set(rootEntity.id, PreloadState.Failed); + this.outputLog(rootEntity, `子树预加载失败: ${error.message}`, 'error'); + }); + } + + /** + * 扫描所有需要预加载的子树节点 + */ + private scanSubTreeNodes(entity: Entity): Array<{ entity: Entity; subTree: SubTreeNode }> { + const result: Array<{ entity: Entity; subTree: SubTreeNode }> = []; + + // 检查当前实体 + const subTree = entity.getComponent(SubTreeNode); + if (subTree && subTree.preload) { + result.push({ entity, subTree }); + } + + // 递归扫描子节点 + for (const child of entity.children) { + result.push(...this.scanSubTreeNodes(child)); + } + + return result; + } + + /** + * 预加载所有子树 + */ + private async preloadAllSubTrees( + rootEntity: Entity, + subTreeNodes: Array<{ entity: Entity; subTree: SubTreeNode }> + ): Promise { + // 确保 AssetLoader 已初始化 + if (!this.assetLoader) { + try { + this.assetLoader = Core.services.resolve(FileSystemAssetLoader); + } catch (error) { + throw new Error('AssetLoader 未配置,无法预加载子树'); + } + } + + // 并行加载所有子树 + await Promise.all( + subTreeNodes.map(({ entity, subTree }) => + this.preloadSingleSubTree(rootEntity, entity, subTree) + ) + ); + } + + /** + * 预加载单个子树 + */ + private async preloadSingleSubTree( + rootEntity: Entity, + subTreeEntity: Entity, + subTree: SubTreeNode + ): Promise { + try { + this.outputLog(rootEntity, `预加载子树: ${subTree.assetId}`, 'info'); + + // 加载资产 + const asset = await this.assetLoader!.loadBehaviorTree(subTree.assetId); + + // 实例化为 Entity 树(作为子树,跳过 RootNode) + const subTreeRoot = BehaviorTreeAssetLoader.instantiate(asset, this.scene!, { + asSubTree: true + }); + + // 设置子树根实体 + subTree.setSubTreeRoot(subTreeRoot); + + // 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点 + subTreeEntity.addChild(subTreeRoot); + + // 添加资产元数据 + const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata()); + metadata.initialize(subTree.assetId, '1.0.0'); + + // 处理黑板继承 + if (subTree.inheritParentBlackboard) { + this.setupBlackboardInheritance(subTreeEntity, subTreeRoot); + } + + // 输出子树内部结构(用于调试) + this.outputLog(rootEntity, `=== 预加载子树 ${subTree.assetId} 的内部结构 ===`, 'info'); + this.logSubTreeStructure(rootEntity, subTreeRoot, 0); + this.outputLog(rootEntity, `=== 预加载子树结构结束 ===`, 'info'); + + this.outputLog(rootEntity, `✓ 子树 ${subTree.assetId} 预加载完成`, 'info'); + } catch (error: any) { + this.outputLog( + rootEntity, + `✗ 子树 ${subTree.assetId} 预加载失败: ${error.message}`, + 'error' + ); + throw error; + } + } + + /** + * 设置黑板继承 + */ + private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void { + const parentBlackboard = this.findBlackboard(parentEntity); + if (!parentBlackboard) { + return; + } + + // 找到子树的黑板 + const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent); + if (subTreeBlackboard) { + subTreeBlackboard.setUseGlobalBlackboard(true); + } + } + + /** + * 查找黑板组件 + */ + private findBlackboard(entity: Entity): BlackboardComponent | undefined { + let current: Entity | null = entity; + + while (current) { + const blackboard = current.getComponent(BlackboardComponent); + if (blackboard) { + return blackboard; + } + current = current.parent; + } + + return undefined; + } + + /** + * 查找根实体 + */ + private findRootEntity(entity: Entity): Entity | null { + let current: Entity | null = entity; + while (current) { + if (!current.parent) { + return current; + } + current = current.parent; + } + return null; + } + + /** + * 统一的日志输出方法 + */ + private outputLog( + entity: Entity, + message: string, + level: 'log' | 'info' | 'warn' | 'error' = 'info' + ): void { + // 输出到控制台 + switch (level) { + case 'info': + this.logger.info(message); + break; + case 'warn': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + default: + this.logger.info(message); + break; + } + + // 输出到LogOutput组件 + const rootEntity = this.findRootEntity(entity); + if (rootEntity) { + const logOutput = rootEntity.getComponent(LogOutput); + if (logOutput) { + logOutput.addMessage(message, level); + } + } + } + + /** + * 递归打印子树结构(用于调试) + */ + private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void { + const indent = ' '.repeat(depth); + const btNode = entity.getComponent(BehaviorTreeNode); + + // 获取节点的具体类型组件 + const allComponents = entity.components.map(c => c.constructor.name); + const nodeTypeComponent = allComponents.find(name => + name !== 'BehaviorTreeNode' && name !== 'ActiveNode' && + name !== 'BlackboardComponent' && name !== 'LogOutput' && + name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata' + ) || 'Unknown'; + + // 构建节点显示名称 + let nodeName = entity.name; + if (nodeTypeComponent !== 'Unknown') { + nodeName = `${nodeName} [${nodeTypeComponent}]`; + } + + this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info'); + + // 递归打印子节点 + if (entity.children.length > 0) { + this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info'); + entity.children.forEach((child: Entity) => { + this.logSubTreeStructure(parentEntity, child, depth + 1); + }); + } + } + + /** + * 清理资源 + */ + protected override onDestroy(): void { + this.preloadStates.clear(); + this.preloadTasks.clear(); + super.onDestroy(); + } + + protected override getLoggerName(): string { + return 'RootExecutionSystem'; + } +} diff --git a/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts b/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts new file mode 100644 index 00000000..c7db83dc --- /dev/null +++ b/packages/behavior-tree/src/Systems/SubTreeExecutionSystem.ts @@ -0,0 +1,667 @@ +import { EntitySystem, Matcher, Entity, Core, createLogger } from '@esengine/ecs-framework'; +import { SubTreeNode } from '../Components/Composites/SubTreeNode'; +import { ActiveNode } from '../Components/ActiveNode'; +import { BehaviorTreeNode } from '../Components/BehaviorTreeNode'; +import { TaskStatus } from '../Types/TaskStatus'; +import { IAssetLoader } from '../Services/IAssetLoader'; +import { FileSystemAssetLoader } from '../Services/FileSystemAssetLoader'; +import { BehaviorTreeAssetLoader } from '../Serialization/BehaviorTreeAssetLoader'; +import { BlackboardComponent } from '../Components/BlackboardComponent'; +import { LogOutput } from '../Components/LogOutput'; +import { AssetLoadingManager } from '../Services/AssetLoadingManager'; +import { + LoadingState, + LoadingTaskHandle, + CircularDependencyError, + EntityDestroyedError +} from '../Services/AssetLoadingTypes'; +import { BehaviorTreeAssetMetadata } from '../Components/AssetMetadata'; + +/** + * SubTree 执行系统 + * + * 处理 SubTree 节点的执行,包括: + * - 子树资产加载 + * - 子树实例化 + * - 黑板继承 + * - 子树执行和状态管理 + * + * updateOrder: 300 (与 CompositeExecutionSystem 同级) + */ +export class SubTreeExecutionSystem extends EntitySystem { + private assetLoader?: IAssetLoader; + private assetLoaderInitialized = false; + private hasLoggedMissingAssetLoader = false; + private loadingManager: AssetLoadingManager; + private loadingTasks: Map = new Map(); + + constructor(loadingManager?: AssetLoadingManager) { + super(Matcher.empty().all(SubTreeNode, ActiveNode, BehaviorTreeNode)); + this.updateOrder = 300; + this.loadingManager = loadingManager || new AssetLoadingManager(); + } + + protected override onInitialize(): void { + // 延迟初始化 AssetLoader,不在这里尝试获取 + // 只在第一次真正需要处理 SubTree 节点时才获取 + } + + protected override process(entities: readonly Entity[]): void { + for (const entity of entities) { + const subTree = entity.getComponent(SubTreeNode)!; + const node = entity.getComponent(BehaviorTreeNode)!; + + this.executeSubTree(entity, subTree, node); + } + } + + /** + * 执行子树节点 + */ + private executeSubTree( + entity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode + ): void { + // 验证配置 + const errors = subTree.validate(); + if (errors.length > 0) { + this.logger.error(`SubTree 节点配置错误: ${errors.join(', ')}`); + node.status = TaskStatus.Failure; + this.completeNode(entity); + return; + } + + // 检查是否已有子树(可能是预加载的) + const existingSubTreeRoot = subTree.getSubTreeRoot(); + if (existingSubTreeRoot) { + const subTreeNode = existingSubTreeRoot.getComponent(BehaviorTreeNode); + + if (subTreeNode) { + const statusName = TaskStatus[subTreeNode.status]; + const hasActive = existingSubTreeRoot.hasComponent(ActiveNode); + this.outputLog( + entity, + `检查预加载子树 ${subTree.assetId}: status=${statusName}, hasActive=${hasActive}`, + 'info' + ); + + // 如果子树还没开始执行(状态是 Invalid),需要激活它 + if (subTreeNode.status === TaskStatus.Invalid) { + this.outputLog(entity, `使用预加载的子树: ${subTree.assetId}`, 'info'); + + // 检查子节点 + this.outputLog(entity, `激活前:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info'); + if (existingSubTreeRoot.children.length > 0) { + const firstChild = existingSubTreeRoot.children[0]; + this.outputLog(entity, ` 第一个子节点: ${firstChild.name}`, 'info'); + } + + // 激活根节点 + if (!existingSubTreeRoot.hasComponent(ActiveNode)) { + existingSubTreeRoot.addComponent(new ActiveNode()); + this.outputLog(entity, `为子树根节点添加 ActiveNode: ${existingSubTreeRoot.name}`, 'info'); + } + + const subTreeRootNode = existingSubTreeRoot.getComponent(BehaviorTreeNode); + if (subTreeRootNode) { + this.outputLog(entity, `设置子树根节点状态: ${existingSubTreeRoot.name} -> Running`, 'info'); + subTreeRootNode.status = TaskStatus.Running; + } + + // 再次检查(验证激活后子节点没有丢失) + this.outputLog(entity, `激活后:子树根节点 ${existingSubTreeRoot.name} 有 ${existingSubTreeRoot.children.length} 个子节点`, 'info'); + + this.outputLog(entity, `激活预加载的子树: ${subTree.assetId}`, 'info'); + node.status = TaskStatus.Running; + return; + } + } + + // 子树已激活或已完成,更新状态 + this.updateSubTree(entity, subTree, node); + return; + } + + // 子树未预加载,开始运行时加载 + this.outputLog(entity, `子树未预加载,开始运行时加载: ${subTree.assetId}`, 'info'); + this.loadAndInstantiateSubTree(entity, subTree, node); + } + + /** + * 延迟初始化 AssetLoader + */ + private ensureAssetLoaderInitialized(): boolean { + if (!this.assetLoaderInitialized) { + try { + this.assetLoader = Core.services.resolve(FileSystemAssetLoader); + this.assetLoaderInitialized = true; + this.logger.debug('AssetLoader 已初始化'); + } catch (error) { + this.assetLoaderInitialized = true; + this.assetLoader = undefined; + + // 只在第一次失败时记录警告,避免重复日志 + if (!this.hasLoggedMissingAssetLoader) { + this.logger.warn( + 'AssetLoader 未配置。SubTree 节点需要 AssetLoader 来加载子树资产。\n' + + '如果您在编辑器中,请确保已打开项目并配置了项目路径。\n' + + '如果您在运行时环境,请确保已正确注册 FileSystemAssetLoader 服务。' + ); + this.hasLoggedMissingAssetLoader = true; + } + + return false; + } + } + + return this.assetLoader !== undefined; + } + + /** + * 加载并实例化子树(使用加载管理器) + */ + private loadAndInstantiateSubTree( + parentEntity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode + ): void { + // 延迟初始化 AssetLoader + if (!this.ensureAssetLoaderInitialized()) { + this.logger.debug('AssetLoader 不可用,SubTree 节点执行失败'); + node.status = TaskStatus.Failure; + this.completeNode(parentEntity); + return; + } + + const assetId = subTree.assetId; + + // 检查是否有正在进行的加载任务 + let taskHandle = this.loadingTasks.get(parentEntity.id); + + if (taskHandle) { + // 轮询检查状态 + const state = taskHandle.getState(); + + switch (state) { + case LoadingState.Loading: + case LoadingState.Pending: + // 仍在加载中 + node.status = TaskStatus.Running; + + // 输出进度信息 + const progress = taskHandle.getProgress(); + if (progress.elapsedMs > 1000) { + this.logger.debug( + `子树加载中: ${assetId} (已耗时: ${Math.round(progress.elapsedMs / 1000)}s, ` + + `重试: ${progress.retryCount}/${progress.maxRetries})` + ); + } + return; + + case LoadingState.Loaded: + // 加载完成 + this.onLoadingComplete(parentEntity, subTree, node, taskHandle); + return; + + case LoadingState.Failed: + case LoadingState.Timeout: + // 加载失败 + const error = taskHandle.getError(); + this.outputLog( + parentEntity, + `子树加载失败: ${assetId} - ${error?.message || '未知错误'}`, + 'error' + ); + node.status = TaskStatus.Failure; + this.loadingTasks.delete(parentEntity.id); + this.completeNode(parentEntity); + return; + + case LoadingState.Cancelled: + // 已取消(实体被销毁) + this.loadingTasks.delete(parentEntity.id); + return; + } + } + + // 开始新的加载任务 + this.startNewLoading(parentEntity, subTree, node); + } + + /** + * 开始新的加载任务 + */ + private startNewLoading( + parentEntity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode + ): void { + const assetId = subTree.assetId; + + // 获取父树的资产ID(用于循环检测) + const parentAssetId = this.getParentTreeAssetId(parentEntity); + + try { + // 使用加载管理器 + const taskHandle = this.loadingManager.startLoading( + assetId, + parentEntity, + () => this.loadAsset(assetId), + { + timeoutMs: 5000, + maxRetries: 2, + parentAssetId: parentAssetId + } + ); + + this.loadingTasks.set(parentEntity.id, taskHandle); + node.status = TaskStatus.Running; + + this.outputLog( + parentEntity, + `开始加载子树: ${assetId} (父树: ${parentAssetId || 'none'})`, + 'info' + ); + + } catch (error) { + if (error instanceof CircularDependencyError) { + this.outputLog(parentEntity, `检测到循环引用: ${error.message}`, 'error'); + } else { + this.outputLog(parentEntity, `启动加载失败: ${assetId}`, 'error'); + } + + node.status = TaskStatus.Failure; + this.completeNode(parentEntity); + } + } + + /** + * 加载完成时的处理 + */ + private onLoadingComplete( + parentEntity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode, + taskHandle: LoadingTaskHandle + ): void { + // 获取加载结果 + taskHandle.promise.then(subTreeRoot => { + // 再次检查实体是否存在 + if (parentEntity.isDestroyed) { + this.logger.warn(`父实体已销毁,丢弃加载结果: ${taskHandle.assetId}`); + subTreeRoot.destroy(); + return; + } + + // 设置子树 + subTree.setSubTreeRoot(subTreeRoot); + + // 将子树根实体设置为 SubTreeNode 的子节点,这样子树中的节点可以通过 parent 链找到主树的根节点 + parentEntity.addChild(subTreeRoot); + + // 添加资产元数据(用于循环检测) + const metadata = subTreeRoot.addComponent(new BehaviorTreeAssetMetadata()); + metadata.initialize(taskHandle.assetId, '1.0.0'); + + // 处理黑板继承 + if (subTree.inheritParentBlackboard) { + this.setupBlackboardInheritance(parentEntity, subTreeRoot); + } + + this.outputLog(parentEntity, `子树 ${taskHandle.assetId} 加载成功并激活`, 'info'); + + // 打印子树结构(用于调试) + this.outputLog(parentEntity, `=== 子树 ${taskHandle.assetId} 内部结构 ===`, 'info'); + this.logSubTreeStructure(parentEntity, subTreeRoot, 0); + this.outputLog(parentEntity, `=== 子树结构结束 ===`, 'info'); + + // 激活子树执行 + this.startSubTreeExecution(subTreeRoot, parentEntity); + + // 清理任务 + this.loadingTasks.delete(parentEntity.id); + + }).catch(error => { + // 这里不应该到达,因为错误应该在状态机中处理了 + if (!(error instanceof EntityDestroyedError)) { + this.logger.error('意外错误:', error); + } + }); + } + + /** + * 加载资产 + */ + private async loadAsset(assetId: string): Promise { + if (!this.scene) { + throw new Error('Scene 不存在'); + } + + // 加载资产 + const asset = await this.assetLoader!.loadBehaviorTree(assetId); + + // 实例化为 Entity 树(作为子树,跳过 RootNode) + const rootEntity = BehaviorTreeAssetLoader.instantiate(asset, this.scene, { + asSubTree: true + }); + + return rootEntity; + } + + /** + * 设置黑板继承 + */ + private setupBlackboardInheritance(parentEntity: Entity, subTreeRoot: Entity): void { + const parentBlackboard = this.findBlackboard(parentEntity); + if (!parentBlackboard) { + return; + } + + // 找到子树的黑板 + const subTreeBlackboard = subTreeRoot.getComponent(BlackboardComponent); + if (subTreeBlackboard) { + // 启用全局黑板查找(这样子树可以访问父树的变量) + subTreeBlackboard.setUseGlobalBlackboard(true); + } + } + + /** + * 查找黑板 + */ + private findBlackboard(entity: Entity): BlackboardComponent | undefined { + let current: Entity | null = entity; + + while (current) { + const blackboard = current.getComponent(BlackboardComponent); + if (blackboard) { + return blackboard; + } + + current = current.parent; + } + + return undefined; + } + + /** + * 开始子树执行 + */ + private startSubTreeExecution(subTreeRoot: Entity, parentEntity?: Entity): void { + // 调试:检查子树根节点的子节点 + if (parentEntity) { + this.outputLog(parentEntity, `子树根节点 ${subTreeRoot.name} 有 ${subTreeRoot.children.length} 个子节点`, 'info'); + } + + // 激活根节点 + if (!subTreeRoot.hasComponent(ActiveNode)) { + subTreeRoot.addComponent(new ActiveNode()); + if (parentEntity) { + this.outputLog(parentEntity, `为子树根节点添加 ActiveNode: ${subTreeRoot.name}`, 'info'); + } + } + + const node = subTreeRoot.getComponent(BehaviorTreeNode); + if (node) { + if (parentEntity) { + this.outputLog(parentEntity, `设置子树根节点状态: ${subTreeRoot.name} -> Running`, 'info'); + } + node.status = TaskStatus.Running; + } + } + + /** + * 更新子树状态 + */ + private updateSubTree( + parentEntity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode + ): void { + const subTreeRoot = subTree.getSubTreeRoot(); + if (!subTreeRoot) { + return; + } + + // 检查子树是否完成 + const subTreeNode = subTreeRoot.getComponent(BehaviorTreeNode); + if (!subTreeNode) { + return; + } + + // 输出子树当前状态(调试) + const statusName = TaskStatus[subTreeNode.status]; + this.outputLog( + parentEntity, + `子树 ${subTree.assetId} 当前状态: ${statusName}`, + 'info' + ); + + if (subTreeNode.status !== TaskStatus.Running) { + // 子树完成 + this.onSubTreeCompleted(parentEntity, subTree, node, subTreeNode.status); + } else { + // 子树仍在运行 + node.status = TaskStatus.Running; + } + } + + /** + * 子树完成时的处理 + */ + private onSubTreeCompleted( + parentEntity: Entity, + subTree: SubTreeNode, + node: BehaviorTreeNode, + subTreeStatus: TaskStatus + ): void { + this.outputLog(parentEntity, `子树完成,状态: ${TaskStatus[subTreeStatus]}`, 'info'); + + // 检查完成前 SubTreeNode 的子节点 + this.outputLog(parentEntity, `完成前:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info'); + + // 标记子树完成 + subTree.markSubTreeCompleted(subTreeStatus); + + // 决定父节点状态 + if (subTreeStatus === TaskStatus.Success) { + node.status = TaskStatus.Success; + } else if (subTreeStatus === TaskStatus.Failure) { + if (subTree.propagateFailure) { + node.status = TaskStatus.Failure; + } else { + // 忽略失败,返回成功 + node.status = TaskStatus.Success; + } + } else { + node.status = subTreeStatus; + } + + // 清理子树 + this.cleanupSubTree(subTree); + + // 检查清理后 SubTreeNode 的子节点 + this.outputLog(parentEntity, `清理后:SubTreeNode ${parentEntity.name} 有 ${parentEntity.children.length} 个子节点`, 'info'); + + // 完成父节点 + this.completeNode(parentEntity); + } + + /** + * 清理子树 + */ + private cleanupSubTree(subTree: SubTreeNode): void { + const subTreeRoot = subTree.getSubTreeRoot(); + if (!subTreeRoot) { + return; + } + + // 如果是预加载的子树,不销毁,只重置状态以便复用 + if (subTree.preload) { + this.logger.debug(`重置预加载子树以便复用: ${subTree.assetId}`); + + // 递归重置整个子树的所有节点 + this.resetSubTreeRecursively(subTreeRoot); + + // 重置 SubTreeNode 的完成状态,但保留 subTreeRoot 引用 + subTree.resetCompletionState(); + } else { + // 运行时加载的子树,销毁并清理 + this.logger.debug(`销毁运行时加载的子树: ${subTree.assetId}`); + subTreeRoot.destroy(); + subTree.setSubTreeRoot(undefined); + subTree.reset(); + } + } + + /** + * 递归重置子树的所有节点 + */ + private resetSubTreeRecursively(entity: Entity): void { + // 移除 ActiveNode + if (entity.hasComponent(ActiveNode)) { + entity.removeComponentByType(ActiveNode); + } + + // 重置节点状态 + const node = entity.getComponent(BehaviorTreeNode); + if (node) { + node.status = TaskStatus.Invalid; + } + + // 递归处理子节点 + for (const child of entity.children) { + this.resetSubTreeRecursively(child); + } + } + + /** + * 完成节点执行 + */ + private completeNode(entity: Entity): void { + entity.removeComponentByType(ActiveNode); + + // 通知父节点 + if (entity.parent && entity.parent.hasComponent(BehaviorTreeNode)) { + if (!entity.parent.hasComponent(ActiveNode)) { + entity.parent.addComponent(new ActiveNode()); + } + } + } + + /** + * 获取父树的资产ID(用于循环检测) + */ + private getParentTreeAssetId(entity: Entity): string | undefined { + let current: Entity | null = entity; + + while (current) { + // 查找带有资产元数据的组件 + const metadata = current.getComponent(BehaviorTreeAssetMetadata); + if (metadata && metadata.assetId) { + return metadata.assetId; + } + current = current.parent; + } + + return undefined; + } + + /** + * 系统销毁时清理 + */ + protected override onDestroy(): void { + // 取消所有正在加载的任务 + for (const taskHandle of this.loadingTasks.values()) { + taskHandle.cancel(); + } + this.loadingTasks.clear(); + + super.onDestroy(); + } + + /** + * 查找根实体 + */ + private findRootEntity(entity: Entity): Entity | null { + let current: Entity | null = entity; + while (current) { + if (!current.parent) { + return current; + } + current = current.parent; + } + return null; + } + + /** + * 统一的日志输出方法 + * 同时输出到控制台和LogOutput组件,确保用户在UI中能看到 + */ + private outputLog( + entity: Entity, + message: string, + level: 'log' | 'info' | 'warn' | 'error' = 'info' + ): void { + // 输出到浏览器控制台(方便开发调试) + switch (level) { + case 'info': + this.logger.info(message); + break; + case 'warn': + this.logger.warn(message); + break; + case 'error': + this.logger.error(message); + break; + default: + this.logger.info(message); + break; + } + + // 输出到LogOutput组件(显示在UI中) + const rootEntity = this.findRootEntity(entity); + if (rootEntity) { + const logOutput = rootEntity.getComponent(LogOutput); + if (logOutput) { + logOutput.addMessage(message, level); + } + } + } + + /** + * 递归打印子树结构(用于调试) + */ + private logSubTreeStructure(parentEntity: Entity, entity: Entity, depth: number): void { + const indent = ' '.repeat(depth); + const btNode = entity.getComponent(BehaviorTreeNode); + + // 获取节点的具体类型组件 + const allComponents = entity.components.map(c => c.constructor.name); + const nodeTypeComponent = allComponents.find(name => + name !== 'BehaviorTreeNode' && name !== 'ActiveNode' && + name !== 'BlackboardComponent' && name !== 'LogOutput' && + name !== 'PropertyBindings' && name !== 'BehaviorTreeAssetMetadata' + ) || 'Unknown'; + + // 构建节点显示名称 + let nodeName = entity.name; + if (nodeTypeComponent !== 'Unknown') { + nodeName = `${nodeName} [${nodeTypeComponent}]`; + } + + this.outputLog(parentEntity, `${indent}└─ ${nodeName}`, 'info'); + + // 递归打印子节点 + if (entity.children.length > 0) { + this.outputLog(parentEntity, `${indent} 子节点数: ${entity.children.length}`, 'info'); + entity.children.forEach((child: Entity) => { + this.logSubTreeStructure(parentEntity, child, depth + 1); + }); + } + } + + protected override getLoggerName(): string { + return 'SubTreeExecutionSystem'; + } +} diff --git a/packages/behavior-tree/src/Types/TaskStatus.ts b/packages/behavior-tree/src/Types/TaskStatus.ts new file mode 100644 index 00000000..c0fd620f --- /dev/null +++ b/packages/behavior-tree/src/Types/TaskStatus.ts @@ -0,0 +1,98 @@ +/** + * 任务执行状态 + */ +export enum TaskStatus { + /** 无效状态 - 节点未初始化或已被重置 */ + Invalid = 0, + /** 成功 - 节点执行成功完成 */ + Success = 1, + /** 失败 - 节点执行失败 */ + Failure = 2, + /** 运行中 - 节点正在执行,需要在后续帧继续 */ + Running = 3 +} + +/** + * 节点类型 + */ +export enum NodeType { + /** 复合节点 - 有多个子节点 */ + Composite = 'composite', + /** 装饰器节点 - 有一个子节点 */ + Decorator = 'decorator', + /** 动作节点 - 叶子节点 */ + Action = 'action', + /** 条件节点 - 叶子节点 */ + Condition = 'condition' +} + +/** + * 复合节点类型 + */ +export enum CompositeType { + /** 序列 - 按顺序执行,全部成功才成功 */ + Sequence = 'sequence', + /** 选择 - 按顺序执行,任一成功则成功 */ + Selector = 'selector', + /** 并行 - 同时执行所有子节点 */ + Parallel = 'parallel', + /** 并行选择 - 并行执行,任一成功则成功 */ + ParallelSelector = 'parallel-selector', + /** 随机序列 - 随机顺序执行序列 */ + RandomSequence = 'random-sequence', + /** 随机选择 - 随机顺序执行选择 */ + RandomSelector = 'random-selector' +} + +/** + * 装饰器节点类型 + */ +export enum DecoratorType { + /** 反转 - 反转子节点结果 */ + Inverter = 'inverter', + /** 重复 - 重复执行子节点 */ + Repeater = 'repeater', + /** 直到成功 - 重复直到成功 */ + UntilSuccess = 'until-success', + /** 直到失败 - 重复直到失败 */ + UntilFail = 'until-fail', + /** 总是成功 - 无论子节点结果都返回成功 */ + AlwaysSucceed = 'always-succeed', + /** 总是失败 - 无论子节点结果都返回失败 */ + AlwaysFail = 'always-fail', + /** 条件装饰器 - 基于条件执行子节点 */ + Conditional = 'conditional', + /** 冷却 - 冷却时间内阻止执行 */ + Cooldown = 'cooldown', + /** 超时 - 超时则返回失败 */ + Timeout = 'timeout' +} + +/** + * 中止类型 + * + * 用于动态优先级和条件重新评估 + */ +export enum AbortType { + /** 无 - 不中止任何节点 */ + None = 'none', + /** 自身 - 条件变化时可以中止自身的执行 */ + Self = 'self', + /** 低优先级 - 条件满足时可以中止低优先级的兄弟节点 */ + LowerPriority = 'lower-priority', + /** 两者 - 可以中止自身和低优先级节点 */ + Both = 'both' +} + +/** + * 黑板变量类型 + */ +export enum BlackboardValueType { + String = 'string', + Number = 'number', + Boolean = 'boolean', + Vector2 = 'vector2', + Vector3 = 'vector3', + Object = 'object', + Array = 'array' +} diff --git a/packages/behavior-tree/src/index.ts b/packages/behavior-tree/src/index.ts new file mode 100644 index 00000000..2908a5b4 --- /dev/null +++ b/packages/behavior-tree/src/index.ts @@ -0,0 +1,92 @@ +/** + * @esengine/behavior-tree + * + * 完全ECS化的行为树系统 + * + * @packageDocumentation + */ + +// 注册所有内置节点 +import './RegisterAllNodes'; + +// 类型定义 +export * from './Types/TaskStatus'; + +// 基础组件 +export * from './Components/BehaviorTreeNode'; +export * from './Components/BlackboardComponent'; +export * from './Components/CompositeNodeComponent'; +export * from './Components/DecoratorNodeComponent'; +export * from './Components/ActiveNode'; +export * from './Components/PropertyBindings'; +export * from './Components/LogOutput'; +export * from './Components/AssetMetadata'; + +// 动作组件 +export * from './Components/Actions/WaitAction'; +export * from './Components/Actions/LogAction'; +export * from './Components/Actions/SetBlackboardValueAction'; +export * from './Components/Actions/ModifyBlackboardValueAction'; +export * from './Components/Actions/ExecuteAction'; + +// 条件组件 +export * from './Components/Conditions/BlackboardCompareCondition'; +export * from './Components/Conditions/BlackboardExistsCondition'; +export * from './Components/Conditions/RandomProbabilityCondition'; +export * from './Components/Conditions/ExecuteCondition'; + +// 组合节点 +export * from './Components/Composites/RootNode'; +export * from './Components/Composites/SequenceNode'; +export * from './Components/Composites/SelectorNode'; +export * from './Components/Composites/ParallelNode'; +export * from './Components/Composites/ParallelSelectorNode'; +export * from './Components/Composites/RandomSequenceNode'; +export * from './Components/Composites/RandomSelectorNode'; +export * from './Components/Composites/SubTreeNode'; + +// 装饰器节点 +export * from './Components/Decorators/InverterNode'; +export * from './Components/Decorators/RepeaterNode'; +export * from './Components/Decorators/UntilSuccessNode'; +export * from './Components/Decorators/UntilFailNode'; +export * from './Components/Decorators/AlwaysSucceedNode'; +export * from './Components/Decorators/AlwaysFailNode'; +export * from './Components/Decorators/ConditionalNode'; +export * from './Components/Decorators/CooldownNode'; +export * from './Components/Decorators/TimeoutNode'; + +// 系统 +export * from './Systems/RootExecutionSystem'; +export * from './Systems/LeafExecutionSystem'; +export * from './Systems/DecoratorExecutionSystem'; +export * from './Systems/CompositeExecutionSystem'; +export * from './Systems/SubTreeExecutionSystem'; + +// 服务 +export * from './Services/GlobalBlackboardService'; +export * from './Services/WorkspaceService'; +export * from './Services/IAssetLoader'; +export * from './Services/FileSystemAssetLoader'; +export * from './Services/AssetLoadingManager'; +export * from './Services/AssetLoadingTypes'; + +// 插件 +export * from './BehaviorTreePlugin'; + +// 辅助工具 +export * from './BehaviorTreeStarter'; +export * from './BehaviorTreeBuilder'; + +// 序列化(编辑器支持) +export * from './Serialization/BehaviorTreePersistence'; +export * from './Serialization/NodeTemplates'; + +// 资产系统(运行时) +export * from './Serialization/BehaviorTreeAsset'; +export * from './Serialization/BehaviorTreeAssetSerializer'; +export * from './Serialization/BehaviorTreeAssetLoader'; +export * from './Serialization/EditorFormatConverter'; + +// 装饰器(扩展支持) +export * from './Decorators/BehaviorNodeDecorator'; diff --git a/packages/behavior-tree/tests/AssetLoadingManager.test.ts b/packages/behavior-tree/tests/AssetLoadingManager.test.ts new file mode 100644 index 00000000..5208128a --- /dev/null +++ b/packages/behavior-tree/tests/AssetLoadingManager.test.ts @@ -0,0 +1,311 @@ +import { World, Scene, Entity } from '@esengine/ecs-framework'; +import { AssetLoadingManager } from '../src/Services/AssetLoadingManager'; +import { + LoadingState, + TimeoutError, + CircularDependencyError, + EntityDestroyedError +} from '../src/Services/AssetLoadingTypes'; + +describe('AssetLoadingManager', () => { + let manager: AssetLoadingManager; + let world: World; + let scene: Scene; + let parentEntity: Entity; + + beforeEach(() => { + manager = new AssetLoadingManager(); + world = new World(); + scene = world.createScene('test'); + parentEntity = scene.createEntity('parent'); + }); + + afterEach(() => { + manager.dispose(); + parentEntity.destroy(); + }); + + describe('基本加载功能', () => { + test('成功加载资产', async () => { + const mockEntity = scene.createEntity('loaded'); + + const loader = jest.fn().mockResolvedValue(mockEntity); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader + ); + + expect(handle.getState()).toBe(LoadingState.Loading); + + const result = await handle.promise; + + expect(result).toBe(mockEntity); + expect(handle.getState()).toBe(LoadingState.Loaded); + expect(loader).toHaveBeenCalledTimes(1); + }); + + test('加载失败', async () => { + const mockError = new Error('Load failed'); + const loader = jest.fn().mockRejectedValue(mockError); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader, + { maxRetries: 0 } + ); + + await expect(handle.promise).rejects.toThrow('Load failed'); + expect(handle.getState()).toBe(LoadingState.Failed); + expect(handle.getError()).toBe(mockError); + }); + }); + + describe('超时机制', () => { + test('加载超时', async () => { + const loader = jest.fn().mockImplementation(() => + new Promise(resolve => setTimeout(resolve, 10000)) + ); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader, + { timeoutMs: 100, maxRetries: 0 } + ); + + await expect(handle.promise).rejects.toThrow(TimeoutError); + expect(handle.getState()).toBe(LoadingState.Timeout); + }); + + test('超时前完成', async () => { + const mockEntity = scene.createEntity('loaded'); + + const loader = jest.fn().mockImplementation(() => + new Promise(resolve => setTimeout(() => resolve(mockEntity), 50)) + ); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader, + { timeoutMs: 200 } + ); + + const result = await handle.promise; + + expect(result).toBe(mockEntity); + expect(handle.getState()).toBe(LoadingState.Loaded); + }); + }); + + describe('重试机制', () => { + test('失败后自动重试', async () => { + const mockEntity = scene.createEntity('loaded'); + let attemptCount = 0; + + const loader = jest.fn().mockImplementation(() => { + attemptCount++; + if (attemptCount < 3) { + return Promise.reject(new Error('Temporary error')); + } + return Promise.resolve(mockEntity); + }); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader, + { maxRetries: 3 } + ); + + const result = await handle.promise; + + expect(result).toBe(mockEntity); + expect(loader).toHaveBeenCalledTimes(3); + expect(handle.getState()).toBe(LoadingState.Loaded); + }); + + test('重试次数用尽后失败', async () => { + const loader = jest.fn().mockRejectedValue(new Error('Persistent error')); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader, + { maxRetries: 2 } + ); + + await expect(handle.promise).rejects.toThrow('Persistent error'); + expect(loader).toHaveBeenCalledTimes(3); // 初始 + 2次重试 + expect(handle.getState()).toBe(LoadingState.Failed); + }); + }); + + describe('循环引用检测', () => { + test('检测直接循环引用', () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + // 先加载 assetA + const handleA = manager.startLoading( + 'assetA', + parentEntity, + loader, + { parentAssetId: undefined } + ); + + expect(handleA.getState()).toBe(LoadingState.Loading); + + // 尝试在 assetA 的上下文中加载 assetB + // assetB 又尝试加载 assetA(循环) + expect(() => { + manager.startLoading( + 'assetB', + parentEntity, + loader, + { parentAssetId: 'assetB' } // assetB 的父是 assetB(自我循环) + ); + }).toThrow(CircularDependencyError); + }); + + test('不误报非循环引用', () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + // assetA 加载 assetB(正常) + const handleA = manager.startLoading( + 'assetA', + parentEntity, + loader + ); + + // assetB 加载 assetC(正常,不是循环) + expect(() => { + manager.startLoading( + 'assetC', + parentEntity, + loader, + { parentAssetId: 'assetB' } + ); + }).not.toThrow(); + }); + }); + + describe('实体生命周期安全', () => { + test('实体销毁后取消加载', async () => { + const loader = jest.fn().mockImplementation(() => + new Promise(resolve => setTimeout(resolve, 100)) + ); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader + ); + + // 销毁实体 + parentEntity.destroy(); + + // 等待一小段时间让检测生效 + await new Promise(resolve => setTimeout(resolve, 50)); + + await expect(handle.promise).rejects.toThrow(EntityDestroyedError); + expect(handle.getState()).toBe(LoadingState.Cancelled); + }); + }); + + describe('状态查询', () => { + test('获取加载进度', async () => { + const mockEntity = scene.createEntity('loaded'); + + const loader = jest.fn().mockImplementation(() => + new Promise(resolve => setTimeout(() => resolve(mockEntity), 100)) + ); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader + ); + + const progress = handle.getProgress(); + + expect(progress.state).toBe(LoadingState.Loading); + expect(progress.elapsedMs).toBeGreaterThanOrEqual(0); + expect(progress.retryCount).toBe(0); + expect(progress.maxRetries).toBe(3); + + await handle.promise; + }); + + test('获取统计信息', () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + manager.startLoading('asset1', parentEntity, loader); + manager.startLoading('asset2', parentEntity, loader); + + const stats = manager.getStats(); + + expect(stats.totalTasks).toBe(2); + expect(stats.loadingTasks).toBe(2); + }); + + test('获取正在加载的资产列表', () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + manager.startLoading('asset1', parentEntity, loader); + manager.startLoading('asset2', parentEntity, loader); + + const loadingAssets = manager.getLoadingAssets(); + + expect(loadingAssets).toContain('asset1'); + expect(loadingAssets).toContain('asset2'); + expect(loadingAssets.length).toBe(2); + }); + }); + + describe('任务管理', () => { + test('取消加载任务', () => { + const loader = jest.fn().mockImplementation(() => + new Promise(resolve => setTimeout(resolve, 1000)) + ); + + const handle = manager.startLoading( + 'test-asset', + parentEntity, + loader + ); + + expect(handle.getState()).toBe(LoadingState.Loading); + + handle.cancel(); + + expect(handle.getState()).toBe(LoadingState.Cancelled); + }); + + test('清空所有任务', async () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + manager.startLoading('asset1', parentEntity, loader); + manager.startLoading('asset2', parentEntity, loader); + + expect(manager.getLoadingAssets().length).toBe(2); + + manager.clear(); + + expect(manager.getLoadingAssets().length).toBe(0); + }); + + test('复用已存在的加载任务', () => { + const loader = jest.fn().mockResolvedValue(scene.createEntity('loaded')); + + const handle1 = manager.startLoading('test-asset', parentEntity, loader); + const handle2 = manager.startLoading('test-asset', parentEntity, loader); + + // 应该返回同一个任务 + expect(handle1.assetId).toBe(handle2.assetId); + expect(loader).toHaveBeenCalledTimes(1); // 只加载一次 + }); + }); +}); diff --git a/packages/behavior-tree/tsconfig.json b/packages/behavior-tree/tsconfig.json new file mode 100644 index 00000000..ff4273da --- /dev/null +++ b/packages/behavior-tree/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./bin" + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "bin", + "**/*.test.ts", + "**/*.spec.ts" + ], + "references": [ + { + "path": "../core" + } + ] +} diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 6d8736d6..28b0c64b 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -649,22 +649,49 @@ export class Entity { /** * 获取所有指定类型的组件 - * + * * @param type - 组件类型 * @returns 组件实例数组 */ public getComponents(type: ComponentType): T[] { const result: T[] = []; - + for (const component of this.components) { if (component instanceof type) { result.push(component as T); } } - + return result; } + /** + * 获取指定基类的组件(支持继承查找) + * + * 与 getComponent() 不同,此方法使用 instanceof 检查,支持子类查找。 + * 性能比位掩码查询稍慢,但支持继承层次结构。 + * + * @param baseType - 组件基类类型 + * @returns 第一个匹配的组件实例,如果不存在则返回 null + * + * @example + * ```typescript + * // 查找 CompositeNodeComponent 或其子类 + * const composite = entity.getComponentByType(CompositeNodeComponent); + * if (composite) { + * // composite 可能是 SequenceNode, SelectorNode 等 + * } + * ``` + */ + public getComponentByType(baseType: ComponentType): T | null { + for (const component of this.components) { + if (component instanceof baseType) { + return component as T; + } + } + return null; + } + /** * 添加子实体 * diff --git a/packages/core/src/ECS/World.ts b/packages/core/src/ECS/World.ts index bed1a83c..6e018f29 100644 --- a/packages/core/src/ECS/World.ts +++ b/packages/core/src/ECS/World.ts @@ -102,8 +102,6 @@ export class World { this.name = this._config.name!; this._createdAt = Date.now(); - - logger.info(`创建World: ${this.name}`); } // ===== Scene管理 ===== @@ -132,11 +130,10 @@ export class World { } this._scenes.set(sceneId, scene); - + // 初始化Scene scene.initialize(); - - logger.info(`在World '${this.name}' 中创建Scene: ${sceneId}`); + return scene; } diff --git a/packages/core/src/ECS/WorldManager.ts b/packages/core/src/ECS/WorldManager.ts index 5296e8a9..a617fd3c 100644 --- a/packages/core/src/ECS/WorldManager.ts +++ b/packages/core/src/ECS/WorldManager.ts @@ -117,7 +117,6 @@ export class WorldManager implements IService { const world = new World(worldConfig); this._worlds.set(worldId, world); - logger.info(`创建World: ${worldId}`, { config: worldConfig }); return world; } diff --git a/packages/editor-app/package.json b/packages/editor-app/package.json index bf337ee2..257ef911 100644 --- a/packages/editor-app/package.json +++ b/packages/editor-app/package.json @@ -9,20 +9,27 @@ "build": "tsc && vite build", "preview": "vite preview", "tauri": "tauri", - "tauri:dev": "tauri dev", + "kill-dev": "node scripts/kill-dev-server.js", + "tauri:dev": "npm run kill-dev && tauri dev", "tauri:build": "tauri build", "version": "node scripts/sync-version.js && git add src-tauri/tauri.conf.json" }, "dependencies": { + "@esengine/behavior-tree": "file:../behavior-tree", "@esengine/ecs-framework": "file:../core", "@esengine/editor-core": "file:../editor-core", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-fs": "^2.4.2", "@tauri-apps/plugin-shell": "^2.0.0", + "flexlayout-react": "^0.8.17", + "i18next": "^25.6.0", "json5": "^2.2.3", "lucide-react": "^0.545.0", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "react-i18next": "^16.1.3", + "zustand": "^5.0.8" }, "devDependencies": { "@tauri-apps/cli": "^2.2.0", diff --git a/packages/editor-app/scripts/kill-dev-server.js b/packages/editor-app/scripts/kill-dev-server.js new file mode 100644 index 00000000..99ca2755 --- /dev/null +++ b/packages/editor-app/scripts/kill-dev-server.js @@ -0,0 +1,47 @@ +/** + * 清理开发服务器进程 + * 用于 Windows 平台自动清理残留的 Vite 进程 + */ + +import { execSync } from 'child_process'; + +const PORT = 5173; + +try { + console.log(`正在查找占用端口 ${PORT} 的进程...`); + + // Windows 命令 + const result = execSync(`netstat -ano | findstr :${PORT}`, { encoding: 'utf8' }); + + // 解析 PID + const lines = result.split('\n'); + const pids = new Set(); + + for (const line of lines) { + if (line.includes('LISTENING')) { + const parts = line.trim().split(/\s+/); + const pid = parts[parts.length - 1]; + if (pid && pid !== '0') { + pids.add(pid); + } + } + } + + if (pids.size === 0) { + console.log(`✓ 端口 ${PORT} 未被占用`); + } else { + console.log(`发现 ${pids.size} 个进程占用端口 ${PORT}`); + for (const pid of pids) { + try { + // Windows 需要使用 /F /PID 而不是 //F //PID + execSync(`taskkill /F /PID ${pid}`, { encoding: 'utf8', stdio: 'ignore' }); + console.log(`✓ 已终止进程 PID: ${pid}`); + } catch (e) { + console.log(`✗ 无法终止进程 PID: ${pid}`); + } + } + } +} catch (error) { + // 如果 netstat 没有找到结果,会抛出错误,这是正常的 + console.log(`✓ 端口 ${PORT} 未被占用`); +} diff --git a/packages/editor-app/src-tauri/Cargo.lock b/packages/editor-app/src-tauri/Cargo.lock index 3f57ae6c..8fc810c5 100644 --- a/packages/editor-app/src-tauri/Cargo.lock +++ b/packages/editor-app/src-tauri/Cargo.lock @@ -754,6 +754,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "tauri-plugin-fs", "tauri-plugin-shell", "tauri-plugin-updater", "tokio", diff --git a/packages/editor-app/src-tauri/Cargo.toml b/packages/editor-app/src-tauri/Cargo.toml index 06bc113a..9edff1f8 100644 --- a/packages/editor-app/src-tauri/Cargo.toml +++ b/packages/editor-app/src-tauri/Cargo.toml @@ -16,6 +16,7 @@ tauri-build = { version = "2.0", features = [] } tauri = { version = "2.0", features = ["protocol-asset"] } tauri-plugin-shell = "2.0" tauri-plugin-dialog = "2.0" +tauri-plugin-fs = "2.0" tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/packages/editor-app/src-tauri/src/commands.rs b/packages/editor-app/src-tauri/src/commands.rs index 6e4abf72..7d0ed619 100644 --- a/packages/editor-app/src-tauri/src/commands.rs +++ b/packages/editor-app/src-tauri/src/commands.rs @@ -74,3 +74,55 @@ pub async fn get_profiler_status( let server_lock = state.server.lock().await; Ok(server_lock.is_some()) } + +#[tauri::command] +pub async fn read_behavior_tree_file(file_path: String) -> Result { + use std::fs; + + // 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制 + fs::read_to_string(&file_path) + .map_err(|e| format!("Failed to read file {}: {}", file_path, e)) +} + +#[tauri::command] +pub async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> { + use std::fs; + + // 使用 Rust 标准库直接写入文件 + fs::write(&file_path, content) + .map_err(|e| format!("Failed to write file {}: {}", file_path, e)) +} + +#[tauri::command] +pub async fn read_global_blackboard(project_path: String) -> Result { + use std::fs; + use std::path::Path; + + let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json"); + + if !config_path.exists() { + return Ok(String::from(r#"{"version":"1.0","variables":[]}"#)); + } + + fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read global blackboard: {}", e)) +} + +#[tauri::command] +pub async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let ecs_dir = Path::new(&project_path).join(".ecs"); + let config_path = ecs_dir.join("global-blackboard.json"); + + // 创建 .ecs 目录(如果不存在) + if !ecs_dir.exists() { + fs::create_dir_all(&ecs_dir) + .map_err(|e| format!("Failed to create .ecs directory: {}", e))?; + } + + fs::write(&config_path, content) + .map_err(|e| format!("Failed to write global blackboard: {}", e)) +} + diff --git a/packages/editor-app/src-tauri/src/main.rs b/packages/editor-app/src-tauri/src/main.rs index 34e7e7e1..60870e29 100644 --- a/packages/editor-app/src-tauri/src/main.rs +++ b/packages/editor-app/src-tauri/src/main.rs @@ -97,6 +97,68 @@ async fn open_scene_dialog(app: AppHandle) -> Result, String> { Ok(file.map(|path| path.to_string())) } +#[tauri::command] +async fn open_behavior_tree_dialog(app: AppHandle) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let file = app.dialog() + .file() + .set_title("Select Behavior Tree") + .add_filter("Behavior Tree Files", &["btree"]) + .blocking_pick_file(); + + Ok(file.map(|path| path.to_string())) +} + +#[tauri::command] +fn scan_behavior_trees(project_path: String) -> Result, String> { + use std::path::Path; + use std::fs; + + let behaviors_path = Path::new(&project_path).join(".ecs").join("behaviors"); + + if !behaviors_path.exists() { + fs::create_dir_all(&behaviors_path) + .map_err(|e| format!("Failed to create behaviors directory: {}", e))?; + return Ok(Vec::new()); + } + + let mut btree_files = Vec::new(); + scan_directory_recursive(&behaviors_path, &behaviors_path, &mut btree_files)?; + + Ok(btree_files) +} + +fn scan_directory_recursive( + base_path: &std::path::Path, + current_path: &std::path::Path, + results: &mut Vec +) -> Result<(), String> { + use std::fs; + + let entries = fs::read_dir(current_path) + .map_err(|e| format!("Failed to read directory: {}", e))?; + + for entry in entries { + let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?; + let path = entry.path(); + + if path.is_dir() { + scan_directory_recursive(base_path, &path, results)?; + } else if path.extension().and_then(|s| s.to_str()) == Some("btree") { + if let Ok(relative) = path.strip_prefix(base_path) { + let relative_str = relative.to_string_lossy() + .replace('\\', "/") + .trim_end_matches(".btree") + .to_string(); + results.push(relative_str); + } + } + } + + Ok(()) +} + #[tauri::command] fn scan_directory(path: String, pattern: String) -> Result, String> { use glob::glob; @@ -147,6 +209,8 @@ struct DirectoryEntry { name: String, path: String, is_dir: bool, + size: Option, + modified: Option, } #[tauri::command] @@ -172,10 +236,36 @@ fn list_directory(path: String) -> Result, String> { Ok(entry) => { let entry_path = entry.path(); if let Some(name) = entry_path.file_name() { + let is_dir = entry_path.is_dir(); + + // 获取文件元数据 + let (size, modified) = match fs::metadata(&entry_path) { + Ok(metadata) => { + let size = if is_dir { + None + } else { + Some(metadata.len()) + }; + + let modified = metadata.modified() + .ok() + .and_then(|time| { + time.duration_since(std::time::UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()) + }); + + (size, modified) + } + Err(_) => (None, None), + }; + entries.push(DirectoryEntry { name: name.to_string_lossy().to_string(), path: entry_path.to_string_lossy().to_string(), - is_dir: entry_path.is_dir(), + is_dir, + size, + modified, }); } } @@ -283,6 +373,133 @@ async fn get_profiler_status( Ok(server_lock.is_some()) } +#[tauri::command] +async fn read_behavior_tree_file(file_path: String) -> Result { + use std::fs; + + // 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制 + fs::read_to_string(&file_path) + .map_err(|e| format!("Failed to read file {}: {}", file_path, e)) +} + +#[tauri::command] +async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> { + use std::fs; + + // 使用 Rust 标准库直接写入文件 + fs::write(&file_path, content) + .map_err(|e| format!("Failed to write file {}: {}", file_path, e)) +} + +#[tauri::command] +async fn write_binary_file(file_path: String, content: Vec) -> Result<(), String> { + use std::fs; + + // 写入二进制文件 + fs::write(&file_path, content) + .map_err(|e| format!("Failed to write binary file {}: {}", file_path, e)) +} + +#[tauri::command] +async fn read_global_blackboard(project_path: String) -> Result { + use std::fs; + use std::path::Path; + + let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json"); + + if !config_path.exists() { + return Ok(String::from(r#"{"version":"1.0","variables":[]}"#)); + } + + fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read global blackboard: {}", e)) +} + +#[tauri::command] +async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> { + use std::fs; + use std::path::Path; + + let ecs_dir = Path::new(&project_path).join(".ecs"); + let config_path = ecs_dir.join("global-blackboard.json"); + + // 创建 .ecs 目录(如果不存在) + if !ecs_dir.exists() { + fs::create_dir_all(&ecs_dir) + .map_err(|e| format!("Failed to create .ecs directory: {}", e))?; + } + + fs::write(&config_path, content) + .map_err(|e| format!("Failed to write global blackboard: {}", e)) +} + +#[tauri::command] +fn open_file_with_default_app(file_path: String) -> Result<(), String> { + use std::process::Command; + + #[cfg(target_os = "windows")] + { + Command::new("cmd") + .args(["/C", "start", "", &file_path]) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + #[cfg(target_os = "macos")] + { + Command::new("open") + .arg(&file_path) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + #[cfg(target_os = "linux")] + { + Command::new("xdg-open") + .arg(&file_path) + .spawn() + .map_err(|e| format!("Failed to open file: {}", e))?; + } + + Ok(()) +} + +#[tauri::command] +fn show_in_folder(file_path: String) -> Result<(), String> { + use std::process::Command; + + #[cfg(target_os = "windows")] + { + Command::new("explorer") + .args(["/select,", &file_path]) + .spawn() + .map_err(|e| format!("Failed to show in folder: {}", e))?; + } + + #[cfg(target_os = "macos")] + { + Command::new("open") + .args(["-R", &file_path]) + .spawn() + .map_err(|e| format!("Failed to show in folder: {}", e))?; + } + + #[cfg(target_os = "linux")] + { + use std::path::Path; + let path = Path::new(&file_path); + let parent = path.parent() + .ok_or_else(|| "Failed to get parent directory".to_string())?; + + Command::new("xdg-open") + .arg(parent) + .spawn() + .map_err(|e| format!("Failed to show in folder: {}", e))?; + } + + Ok(()) +} + fn main() { let project_paths: Arc>> = Arc::new(Mutex::new(HashMap::new())); let project_paths_clone = Arc::clone(&project_paths); @@ -294,6 +511,7 @@ fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .register_uri_scheme_protocol("project", move |_app, request| { let project_paths = Arc::clone(&project_paths_clone); @@ -357,14 +575,23 @@ fn main() { open_project_dialog, save_scene_dialog, open_scene_dialog, + open_behavior_tree_dialog, scan_directory, + scan_behavior_trees, read_file_content, list_directory, set_project_base_path, toggle_devtools, start_profiler_server, stop_profiler_server, - get_profiler_status + get_profiler_status, + read_behavior_tree_file, + write_behavior_tree_file, + write_binary_file, + read_global_blackboard, + write_global_blackboard, + open_file_with_default_app, + show_in_folder ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/packages/editor-app/src-tauri/tauri.conf.json b/packages/editor-app/src-tauri/tauri.conf.json index a2d9dedf..e26388b8 100644 --- a/packages/editor-app/src-tauri/tauri.conf.json +++ b/packages/editor-app/src-tauri/tauri.conf.json @@ -46,7 +46,8 @@ "decorations": true, "transparent": false, "center": true, - "skipTaskbar": false + "skipTaskbar": false, + "dragDropEnabled": false } ], "security": { @@ -72,8 +73,22 @@ "updater:default", "updater:allow-check", "updater:allow-download", - "updater:allow-install" - ] + "updater:allow-install", + "fs:default", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-read-dir", + "fs:allow-exists" + ], + "scope": { + "allow": [ + "$HOME/**", + "$APPDATA/**", + "$DESKTOP/**", + "$DOCUMENT/**", + "$DOWNLOAD/**" + ] + } } ] } @@ -82,6 +97,9 @@ "shell": { "open": true }, + "fs": { + "requireLiteralLeadingDot": false + }, "updater": { "active": true, "endpoints": [ diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 75375235..3fd60e28 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -1,9 +1,12 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { Core, Scene } from '@esengine/ecs-framework'; +import * as ECSFramework from '@esengine/ecs-framework'; import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, PropertyMetadataService, LogService, SettingsRegistry, SceneManagerService } from '@esengine/editor-core'; +import { GlobalBlackboardService } from '@esengine/behavior-tree'; import { SceneInspectorPlugin } from './plugins/SceneInspectorPlugin'; import { ProfilerPlugin } from './plugins/ProfilerPlugin'; import { EditorAppearancePlugin } from './plugins/EditorAppearancePlugin'; +import { BehaviorTreePlugin } from './plugins/BehaviorTreePlugin'; import { StartupPage } from './components/StartupPage'; import { SceneHierarchy } from './components/SceneHierarchy'; import { EntityInspector } from './components/EntityInspector'; @@ -16,9 +19,11 @@ import { SettingsWindow } from './components/SettingsWindow'; import { AboutDialog } from './components/AboutDialog'; import { ErrorDialog } from './components/ErrorDialog'; import { ConfirmDialog } from './components/ConfirmDialog'; +import { BehaviorTreeWindow } from './components/BehaviorTreeWindow'; +import { ToastProvider } from './components/Toast'; import { Viewport } from './components/Viewport'; import { MenuBar } from './components/MenuBar'; -import { DockContainer, DockablePanel } from './components/DockContainer'; +import { FlexLayoutDockContainer, FlexDockPanel } from './components/FlexLayoutDockContainer'; import { TauriAPI } from './api/tauri'; import { TauriFileAPI } from './adapters/TauriFileAPI'; import { SettingsService } from './services/SettingsService'; @@ -35,6 +40,9 @@ localeService.registerTranslations('en', en); localeService.registerTranslations('zh', zh); Core.services.registerInstance(LocaleService, localeService); +// 注册全局黑板服务 +Core.services.registerSingleton(GlobalBlackboardService); + function App() { const initRef = useRef(false); const [initialized, setInitialized] = useState(false); @@ -51,12 +59,14 @@ function App() { const [sceneManager, setSceneManager] = useState(null); const { t, locale, changeLocale } = useLocale(); const [status, setStatus] = useState(t('header.status.initializing')); - const [panels, setPanels] = useState([]); + const [panels, setPanels] = useState([]); const [showPluginManager, setShowPluginManager] = useState(false); const [showProfiler, setShowProfiler] = useState(false); const [showPortManager, setShowPortManager] = useState(false); const [showSettings, setShowSettings] = useState(false); const [showAbout, setShowAbout] = useState(false); + const [showBehaviorTreeEditor, setShowBehaviorTreeEditor] = useState(false); + const [behaviorTreeFilePath, setBehaviorTreeFilePath] = useState(null); const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); const [isRemoteConnected, setIsRemoteConnected] = useState(false); const [isProfilerMode, setIsProfilerMode] = useState(false); @@ -137,7 +147,7 @@ function App() { initRef.current = true; try { - (window as any).__ECS_FRAMEWORK__ = await import('@esengine/ecs-framework'); + (window as any).__ECS_FRAMEWORK__ = ECSFramework; const editorScene = new Scene(); Core.setScene(editorScene); @@ -180,12 +190,15 @@ function App() { await pluginMgr.installEditor(new SceneInspectorPlugin()); await pluginMgr.installEditor(new ProfilerPlugin()); await pluginMgr.installEditor(new EditorAppearancePlugin()); + await pluginMgr.installEditor(new BehaviorTreePlugin()); messageHub.subscribe('ui:openWindow', (data: any) => { if (data.windowId === 'profiler') { setShowProfiler(true); } else if (data.windowId === 'pluginManager') { setShowPluginManager(true); + } else if (data.windowId === 'behavior-tree-editor') { + setShowBehaviorTreeEditor(true); } }); @@ -420,6 +433,11 @@ function App() { } }, [sceneManager, locale]); + const handleOpenBehaviorTree = useCallback((btreePath: string) => { + setBehaviorTreeFilePath(btreePath); + setShowBehaviorTreeEditor(true); + }, []); + const handleSaveScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); @@ -498,28 +516,25 @@ function App() { useEffect(() => { if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) { - let corePanels: DockablePanel[]; + let corePanels: FlexDockPanel[]; if (isProfilerMode) { corePanels = [ { id: 'scene-hierarchy', title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - position: 'left', content: , closable: false }, { id: 'inspector', title: locale === 'zh' ? '检视器' : 'Inspector', - position: 'right', content: , closable: false }, { id: 'console', title: locale === 'zh' ? '控制台' : 'Console', - position: 'bottom', content: , closable: false } @@ -529,35 +544,24 @@ function App() { { id: 'scene-hierarchy', title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - position: 'left', content: , closable: false }, { id: 'inspector', title: locale === 'zh' ? '检视器' : 'Inspector', - position: 'right', content: , closable: false }, - { - id: 'viewport', - title: locale === 'zh' ? '视口' : 'Viewport', - position: 'center', - content: , - closable: false - }, { id: 'assets', title: locale === 'zh' ? '资产' : 'Assets', - position: 'bottom', - content: , + content: , closable: false }, { id: 'console', title: locale === 'zh' ? '控制台' : 'Console', - position: 'bottom', content: , closable: false } @@ -568,7 +572,7 @@ function App() { .filter(p => p.enabled) .map(p => p.name); - const pluginPanels: DockablePanel[] = uiRegistry.getAllPanels() + const pluginPanels: FlexDockPanel[] = uiRegistry.getAllPanels() .filter(panelDesc => { if (!panelDesc.component) { return false; @@ -587,7 +591,6 @@ function App() { return { id: panelDesc.id, title: (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title, - position: panelDesc.position as any, content: , closable: panelDesc.closable ?? true }; @@ -596,15 +599,8 @@ function App() { console.log('[App] Loading plugin panels:', pluginPanels); setPanels([...corePanels, ...pluginPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, handleOpenBehaviorTree]); - const handlePanelMove = (panelId: string, newPosition: any) => { - setPanels(prevPanels => - prevPanels.map(panel => - panel.id === panelId ? { ...panel, position: newPosition } : panel - ) - ); - }; if (!initialized) { return ( @@ -689,7 +685,7 @@ function App() {
- +
@@ -721,6 +717,18 @@ function App() { setShowAbout(false)} locale={locale} /> )} + {showBehaviorTreeEditor && ( + { + setShowBehaviorTreeEditor(false); + setBehaviorTreeFilePath(null); + }} + filePath={behaviorTreeFilePath} + projectPath={currentProjectPath} + /> + )} + {errorDialog && ( + + + ); +} + +export default AppWithToast; diff --git a/packages/editor-app/src/api/tauri.ts b/packages/editor-app/src/api/tauri.ts index 0fd0b54c..34d47dae 100644 --- a/packages/editor-app/src/api/tauri.ts +++ b/packages/editor-app/src/api/tauri.ts @@ -113,12 +113,47 @@ export class TauriAPI { static async pathExists(path: string): Promise { return await invoke('path_exists', { path }); } + + /** + * 使用系统默认程序打开文件 + * @param path 文件路径 + */ + static async openFileWithSystemApp(path: string): Promise { + await invoke('open_file_with_default_app', { filePath: path }); + } + + /** + * 在文件管理器中显示文件 + * @param path 文件路径 + */ + static async showInFolder(path: string): Promise { + await invoke('show_in_folder', { filePath: path }); + } + + /** + * 打开行为树文件选择对话框 + * @returns 用户选择的文件路径,取消则返回 null + */ + static async openBehaviorTreeDialog(): Promise { + return await invoke('open_behavior_tree_dialog'); + } + + /** + * 扫描项目中的所有行为树文件 + * @param projectPath 项目路径 + * @returns 行为树资产ID列表(相对于 .ecs/behaviors 的路径,不含扩展名) + */ + static async scanBehaviorTrees(projectPath: string): Promise { + return await invoke('scan_behavior_trees', { projectPath }); + } } export interface DirectoryEntry { name: string; path: string; is_dir: boolean; + size?: number; + modified?: number; } /** diff --git a/packages/editor-app/src/components/AssetBrowser.tsx b/packages/editor-app/src/components/AssetBrowser.tsx index 8387a067..48c8add7 100644 --- a/packages/editor-app/src/components/AssetBrowser.tsx +++ b/packages/editor-app/src/components/AssetBrowser.tsx @@ -1,9 +1,11 @@ import { useState, useEffect } from 'react'; +import { Folder, File, FileCode, FileJson, FileImage, FileText, FolderOpen, Copy, Trash2, Edit3 } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { FileTree } from './FileTree'; import { ResizablePanel } from './ResizablePanel'; +import { ContextMenu, ContextMenuItem } from './ContextMenu'; import '../styles/AssetBrowser.css'; interface AssetItem { @@ -17,39 +19,38 @@ interface AssetBrowserProps { projectPath: string | null; locale: string; onOpenScene?: (scenePath: string) => void; + onOpenBehaviorTree?: (btreePath: string) => void; } -type ViewMode = 'tree-split' | 'tree-only'; - -export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) { - const [viewMode, setViewMode] = useState('tree-split'); +export function AssetBrowser({ projectPath, locale, onOpenScene, onOpenBehaviorTree }: AssetBrowserProps) { + const [currentPath, setCurrentPath] = useState(null); const [selectedPath, setSelectedPath] = useState(null); const [assets, setAssets] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [loading, setLoading] = useState(false); + const [contextMenu, setContextMenu] = useState<{ + position: { x: number; y: number }; + asset: AssetItem; + } | null>(null); const translations = { en: { - title: 'Assets', + title: 'Content Browser', noProject: 'No project loaded', loading: 'Loading...', empty: 'No assets found', search: 'Search...', - viewTreeSplit: 'Tree + List', - viewTreeOnly: 'Tree Only', name: 'Name', type: 'Type', file: 'File', folder: 'Folder' }, zh: { - title: '资产', + title: '内容浏览器', noProject: '没有加载项目', loading: '加载中...', empty: '没有找到资产', search: '搜索...', - viewTreeSplit: '树形+列表', - viewTreeOnly: '纯树形', name: '名称', type: '类型', file: '文件', @@ -61,14 +62,14 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP useEffect(() => { if (projectPath) { - if (viewMode === 'tree-split') { - loadAssets(projectPath); - } + setCurrentPath(projectPath); + loadAssets(projectPath); } else { setAssets([]); + setCurrentPath(null); setSelectedPath(null); } - }, [projectPath, viewMode]); + }, [projectPath]); // Listen for asset reveal requests useEffect(() => { @@ -79,19 +80,17 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP const filePath = data.path; if (filePath) { setSelectedPath(filePath); - - if (viewMode === 'tree-split') { - const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); - const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null; - if (dirPath) { - loadAssets(dirPath); - } + const lastSlashIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\')); + const dirPath = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : null; + if (dirPath) { + setCurrentPath(dirPath); + loadAssets(dirPath); } } }); return () => unsubscribe(); - }, [viewMode]); + }, []); const loadAssets = async (path: string) => { setLoading(true); @@ -110,7 +109,10 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP }; }); - setAssets(assetItems); + setAssets(assetItems.sort((a, b) => { + if (a.type === b.type) return a.name.localeCompare(b.name); + return a.type === 'folder' ? -1 : 1; + })); } catch (error) { console.error('Failed to load assets:', error); setAssets([]); @@ -119,69 +121,154 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP } }; - const handleTreeSelect = (path: string) => { - setSelectedPath(path); - if (viewMode === 'tree-split') { - loadAssets(path); - } + const handleFolderSelect = (path: string) => { + setCurrentPath(path); + loadAssets(path); }; const handleAssetClick = (asset: AssetItem) => { setSelectedPath(asset.path); }; - const handleAssetDoubleClick = (asset: AssetItem) => { - if (asset.type === 'file' && asset.extension === 'ecs') { - if (onOpenScene) { + const handleAssetDoubleClick = async (asset: AssetItem) => { + if (asset.type === 'folder') { + setCurrentPath(asset.path); + loadAssets(asset.path); + } else if (asset.type === 'file') { + if (asset.extension === 'ecs' && onOpenScene) { onOpenScene(asset.path); + } else if (asset.extension === 'btree' && onOpenBehaviorTree) { + onOpenBehaviorTree(asset.path); + } else { + // 其他文件使用系统默认程序打开 + try { + await TauriAPI.openFileWithSystemApp(asset.path); + } catch (error) { + console.error('Failed to open file:', error); + } } } }; + const handleContextMenu = (e: React.MouseEvent, asset: AssetItem) => { + e.preventDefault(); + setContextMenu({ + position: { x: e.clientX, y: e.clientY }, + asset + }); + }; + + const getContextMenuItems = (asset: AssetItem): ContextMenuItem[] => { + const items: ContextMenuItem[] = []; + + // 打开 + if (asset.type === 'file') { + items.push({ + label: locale === 'zh' ? '打开' : 'Open', + icon: , + onClick: () => handleAssetDoubleClick(asset) + }); + } + + // 在文件管理器中显示 + items.push({ + label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer', + icon: , + onClick: async () => { + try { + await TauriAPI.showInFolder(asset.path); + } catch (error) { + console.error('Failed to show in folder:', error); + } + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 复制路径 + items.push({ + label: locale === 'zh' ? '复制路径' : 'Copy Path', + icon: , + onClick: () => { + navigator.clipboard.writeText(asset.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 重命名 + items.push({ + label: locale === 'zh' ? '重命名' : 'Rename', + icon: , + onClick: () => { + // TODO: 实现重命名功能 + console.log('Rename:', asset.path); + }, + disabled: true + }); + + // 删除 + items.push({ + label: locale === 'zh' ? '删除' : 'Delete', + icon: , + onClick: () => { + // TODO: 实现删除功能 + console.log('Delete:', asset.path); + }, + disabled: true + }); + + return items; + }; + + const getBreadcrumbs = () => { + if (!currentPath || !projectPath) return []; + + const relative = currentPath.replace(projectPath, ''); + const parts = relative.split(/[/\\]/).filter(p => p); + + const crumbs = [{ name: 'Content', path: projectPath }]; + let accPath = projectPath; + + for (const part of parts) { + accPath = `${accPath}${accPath.endsWith('\\') || accPath.endsWith('/') ? '' : '/'}${part}`; + crumbs.push({ name: part, path: accPath }); + } + + return crumbs; + }; + const filteredAssets = searchQuery ? assets.filter(asset => - asset.type === 'file' && asset.name.toLowerCase().includes(searchQuery.toLowerCase()) + asset.name.toLowerCase().includes(searchQuery.toLowerCase()) ) - : assets.filter(asset => asset.type === 'file'); + : assets; - const getFileIcon = (extension?: string) => { - switch (extension?.toLowerCase()) { + const getFileIcon = (asset: AssetItem) => { + if (asset.type === 'folder') { + return ; + } + + const ext = asset.extension?.toLowerCase(); + switch (ext) { + case 'ecs': + return ; + case 'btree': + return ; case 'ts': case 'tsx': case 'js': case 'jsx': - return ( - - - - - - ); + return ; case 'json': - return ( - - - - - ); + return ; case 'png': case 'jpg': case 'jpeg': case 'gif': - return ( - - - - - - ); + return ; default: - return ( - - - - - ); + return ; } }; @@ -198,114 +285,96 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP ); } - const renderListView = () => ( -
-
- setSearchQuery(e.target.value)} - /> -
- {loading ? ( -
-

{t.loading}

-
- ) : filteredAssets.length === 0 ? ( -
-

{t.empty}

-
- ) : ( -
- {filteredAssets.map((asset, index) => ( -
handleAssetClick(asset)} - onDoubleClick={() => handleAssetDoubleClick(asset)} - > - {getFileIcon(asset.extension)} -
- {asset.name} -
-
- {asset.extension || t.file} -
-
- ))} -
- )} -
- ); + const breadcrumbs = getBreadcrumbs(); return (
-
-

{t.title}

-
- - -
-
+

{t.title}

- {viewMode === 'tree-only' ? ( -
-
- setSearchQuery(e.target.value)} + +
- -
- ) : ( - - +
+ {breadcrumbs.map((crumb, index) => ( + + { + setCurrentPath(crumb.path); + loadAssets(crumb.path); + }} + > + {crumb.name} + + {index < breadcrumbs.length - 1 && / } + + ))} +
+
+ setSearchQuery(e.target.value)} />
- } - rightOrBottom={renderListView()} - /> - )} + {loading ? ( +
+

{t.loading}

+
+ ) : filteredAssets.length === 0 ? ( +
+

{t.empty}

+
+ ) : ( +
+ {filteredAssets.map((asset, index) => ( +
handleAssetClick(asset)} + onDoubleClick={() => handleAssetDoubleClick(asset)} + onContextMenu={(e) => handleContextMenu(e, asset)} + > + {getFileIcon(asset)} +
+ {asset.name} +
+
+ {asset.type === 'folder' ? t.folder : (asset.extension || t.file)} +
+
+ ))} +
+ )} +
+ } + />
+ {contextMenu && ( + setContextMenu(null)} + /> + )}
); } diff --git a/packages/editor-app/src/components/AssetPicker.tsx b/packages/editor-app/src/components/AssetPicker.tsx new file mode 100644 index 00000000..8351367b --- /dev/null +++ b/packages/editor-app/src/components/AssetPicker.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from 'react'; +import { RefreshCw, Folder } from 'lucide-react'; +import { TauriAPI } from '../api/tauri'; + +interface AssetPickerProps { + value: string; + onChange: (value: string) => void; + projectPath: string | null; + filter?: 'btree' | 'ecs'; + label?: string; +} + +/** + * 资产选择器组件 + * 用于选择项目中的资产文件 + */ +export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) { + const [assets, setAssets] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (projectPath) { + loadAssets(); + } + }, [projectPath]); + + const loadAssets = async () => { + if (!projectPath) return; + + setLoading(true); + try { + if (filter === 'btree') { + const btrees = await TauriAPI.scanBehaviorTrees(projectPath); + setAssets(btrees); + } + } catch (error) { + console.error('Failed to load assets:', error); + setAssets([]); + } finally { + setLoading(false); + } + }; + + const handleBrowse = async () => { + try { + if (filter === 'btree') { + const path = await TauriAPI.openBehaviorTreeDialog(); + if (path && projectPath) { + const behaviorsPath = `${projectPath}\\.ecs\\behaviors\\`.replace(/\\/g, '\\\\'); + const relativePath = path.replace(behaviorsPath, '') + .replace(/\\/g, '/') + .replace('.btree', ''); + onChange(relativePath); + await loadAssets(); + } + } + } catch (error) { + console.error('Failed to browse asset:', error); + } + }; + + return ( +
+ {label && ( + + )} +
+ + + +
+ {!projectPath && ( +
+ 未加载项目 +
+ )} + {value && assets.length > 0 && !assets.includes(value) && ( +
+ 警告: 资产 "{value}" 不存在于项目中 +
+ )} +
+ ); +} diff --git a/packages/editor-app/src/components/AssetPickerDialog.tsx b/packages/editor-app/src/components/AssetPickerDialog.tsx new file mode 100644 index 00000000..a714877a --- /dev/null +++ b/packages/editor-app/src/components/AssetPickerDialog.tsx @@ -0,0 +1,339 @@ +import { useState, useEffect } from 'react'; +import { X, Folder, File, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react'; +import { TauriAPI, DirectoryEntry } from '../api/tauri'; +import '../styles/AssetPickerDialog.css'; + +interface AssetPickerDialogProps { + projectPath: string; + fileExtension: string; + onSelect: (assetId: string) => void; + onClose: () => void; + locale: string; + /** 资产基础路径(相对于项目根目录),用于计算 assetId */ + assetBasePath?: string; +} + +interface AssetItem { + name: string; + path: string; + isDir: boolean; + extension?: string; + size?: number; + modified?: number; +} + +type ViewMode = 'list' | 'grid'; + +export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) { + // 计算实际的资产目录路径 + const actualAssetPath = assetBasePath + ? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/') + : projectPath; + + const [currentPath, setCurrentPath] = useState(actualAssetPath); + const [assets, setAssets] = useState([]); + const [selectedPath, setSelectedPath] = useState(null); + const [loading, setLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const [viewMode, setViewMode] = useState('list'); + + const translations = { + en: { + title: 'Select Asset', + loading: 'Loading...', + empty: 'No assets found', + select: 'Select', + cancel: 'Cancel', + search: 'Search...', + back: 'Back', + listView: 'List View', + gridView: 'Grid View' + }, + zh: { + title: '选择资产', + loading: '加载中...', + empty: '没有找到资产', + select: '选择', + cancel: '取消', + search: '搜索...', + back: '返回上级', + listView: '列表视图', + gridView: '网格视图' + } + }; + + const t = translations[locale as keyof typeof translations] || translations.en; + + useEffect(() => { + loadAssets(currentPath); + }, [currentPath]); + + const loadAssets = async (path: string) => { + setLoading(true); + try { + const entries = await TauriAPI.listDirectory(path); + const assetItems: AssetItem[] = entries + .map((entry: DirectoryEntry) => { + const extension = entry.is_dir ? undefined : + (entry.name.includes('.') ? entry.name.split('.').pop() : undefined); + + return { + name: entry.name, + path: entry.path, + isDir: entry.is_dir, + extension, + size: entry.size, + modified: entry.modified + }; + }) + .filter(item => item.isDir || item.extension === fileExtension) + .sort((a, b) => { + if (a.isDir === b.isDir) return a.name.localeCompare(b.name); + return a.isDir ? -1 : 1; + }); + + setAssets(assetItems); + } catch (error) { + console.error('Failed to load assets:', error); + setAssets([]); + } finally { + setLoading(false); + } + }; + + // 过滤搜索结果 + const filteredAssets = assets.filter(item => + item.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + // 格式化文件大小 + const formatFileSize = (bytes?: number): string => { + if (!bytes) return ''; + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + }; + + // 格式化修改时间 + const formatDate = (timestamp?: number): string => { + if (!timestamp) return ''; + const date = new Date(timestamp * 1000); + return date.toLocaleDateString(locale === 'zh' ? 'zh-CN' : 'en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + // 返回上级目录 + const handleGoBack = () => { + const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/'); + const minPath = actualAssetPath.replace(/[/\\]$/, ''); + if (parentPath && parentPath !== minPath) { + setCurrentPath(parentPath); + } else if (currentPath !== actualAssetPath) { + setCurrentPath(actualAssetPath); + } + }; + + // 只能返回到资产基础目录,不能再往上 + const canGoBack = currentPath !== actualAssetPath; + + const handleItemClick = (item: AssetItem) => { + if (item.isDir) { + setCurrentPath(item.path); + } else { + setSelectedPath(item.path); + } + }; + + const handleItemDoubleClick = (item: AssetItem) => { + if (!item.isDir) { + const assetId = calculateAssetId(item.path); + onSelect(assetId); + } + }; + + const handleSelect = () => { + if (selectedPath) { + const assetId = calculateAssetId(selectedPath); + onSelect(assetId); + } + }; + + /** + * 计算资产ID + * 将绝对路径转换为相对于资产基础目录的assetId(不含扩展名) + */ + const calculateAssetId = (absolutePath: string): string => { + const normalized = absolutePath.replace(/\\/g, '/'); + const baseNormalized = actualAssetPath.replace(/\\/g, '/'); + + // 获取相对于资产基础目录的路径 + let relativePath = normalized; + if (normalized.startsWith(baseNormalized)) { + relativePath = normalized.substring(baseNormalized.length); + } + + // 移除开头的斜杠 + relativePath = relativePath.replace(/^\/+/, ''); + + // 移除文件扩展名 + const assetId = relativePath.replace(new RegExp(`\\.${fileExtension}$`), ''); + + return assetId; + }; + + const getBreadcrumbs = () => { + const basePathNormalized = actualAssetPath.replace(/\\/g, '/'); + const currentPathNormalized = currentPath.replace(/\\/g, '/'); + + const relative = currentPathNormalized.replace(basePathNormalized, ''); + const parts = relative.split('/').filter(p => p); + + // 根路径名称(显示"行为树"或"Assets") + const rootName = assetBasePath + ? assetBasePath.split('/').pop() || 'Assets' + : 'Content'; + + const crumbs = [{ name: rootName, path: actualAssetPath }]; + let accPath = actualAssetPath; + + for (const part of parts) { + accPath = `${accPath}/${part}`; + crumbs.push({ name: part, path: accPath }); + } + + return crumbs; + }; + + const breadcrumbs = getBreadcrumbs(); + + return ( +
+
e.stopPropagation()}> +
+

{t.title}

+ +
+ +
+ + +
+ {breadcrumbs.map((crumb, index) => ( + + setCurrentPath(crumb.path)} + > + {crumb.name} + + {index < breadcrumbs.length - 1 && / } + + ))} +
+ +
+ + +
+
+ +
+ + setSearchQuery(e.target.value)} + className="search-input" + /> + {searchQuery && ( + + )} +
+ +
+ {loading ? ( +
{t.loading}
+ ) : filteredAssets.length === 0 ? ( +
{t.empty}
+ ) : ( +
+ {filteredAssets.map((item, index) => ( +
handleItemClick(item)} + onDoubleClick={() => handleItemDoubleClick(item)} + > +
+ {item.isDir ? ( + + ) : ( + + )} +
+
+ {item.name} + {viewMode === 'list' && !item.isDir && ( +
+ {item.size && {formatFileSize(item.size)}} + {item.modified && {formatDate(item.modified)}} +
+ )} +
+
+ ))} +
+ )} +
+ +
+
+ {filteredAssets.length} {locale === 'zh' ? '项' : 'items'} +
+
+ + +
+
+
+
+ ); +} diff --git a/packages/editor-app/src/components/BehaviorTreeBlackboard.tsx b/packages/editor-app/src/components/BehaviorTreeBlackboard.tsx new file mode 100644 index 00000000..c589b240 --- /dev/null +++ b/packages/editor-app/src/components/BehaviorTreeBlackboard.tsx @@ -0,0 +1,831 @@ +import { useState } from 'react'; +import { Clipboard, Edit2, Trash2, ChevronDown, ChevronRight, Globe, Save, Folder, FileCode } from 'lucide-react'; +import { save } from '@tauri-apps/plugin-dialog'; +import { invoke } from '@tauri-apps/api/core'; +import { Core } from '@esengine/ecs-framework'; +import type { BlackboardValueType } from '@esengine/behavior-tree'; +import { GlobalBlackboardService } from '@esengine/behavior-tree'; +import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator'; +import { createLogger } from '@esengine/ecs-framework'; + +const logger = createLogger('BehaviorTreeBlackboard'); + +type SimpleBlackboardType = 'number' | 'string' | 'boolean' | 'object'; + +interface BlackboardVariable { + key: string; + value: any; + type: SimpleBlackboardType; +} + +interface BehaviorTreeBlackboardProps { + variables: Record; + initialVariables?: Record; + globalVariables?: Record; + onVariableChange: (key: string, value: any) => void; + onVariableAdd: (key: string, value: any, type: SimpleBlackboardType) => void; + onVariableDelete: (key: string) => void; + onVariableRename?: (oldKey: string, newKey: string) => void; + onGlobalVariableChange?: (key: string, value: any) => void; + onGlobalVariableAdd?: (key: string, value: any, type: BlackboardValueType) => void; + onGlobalVariableDelete?: (key: string) => void; + projectPath?: string; + hasUnsavedGlobalChanges?: boolean; + onSaveGlobal?: () => void; +} + +/** + * 行为树黑板变量面板 + * + * 用于管理和调试行为树运行时的黑板变量 + */ +export const BehaviorTreeBlackboard: React.FC = ({ + variables, + initialVariables, + globalVariables, + onVariableChange, + onVariableAdd, + onVariableDelete, + onVariableRename, + onGlobalVariableChange, + onGlobalVariableAdd, + onGlobalVariableDelete, + projectPath, + hasUnsavedGlobalChanges, + onSaveGlobal +}) => { + const [viewMode, setViewMode] = useState<'local' | 'global'>('local'); + + const isModified = (key: string): boolean => { + if (!initialVariables) return false; + return JSON.stringify(variables[key]) !== JSON.stringify(initialVariables[key]); + }; + + const handleExportTypeScript = async () => { + try { + const globalBlackboard = Core.services.resolve(GlobalBlackboardService); + const config = globalBlackboard.exportConfig(); + + const tsCode = GlobalBlackboardTypeGenerator.generate(config); + + const outputPath = await save({ + filters: [{ + name: 'TypeScript', + extensions: ['ts'] + }], + defaultPath: 'GlobalBlackboard.ts' + }); + + if (outputPath) { + await invoke('write_file_content', { + path: outputPath, + content: tsCode + }); + logger.info('TypeScript 类型定义已导出', outputPath); + } + } catch (error) { + logger.error('导出 TypeScript 失败', error); + } + }; + + const [isAdding, setIsAdding] = useState(false); + const [newKey, setNewKey] = useState(''); + const [newValue, setNewValue] = useState(''); + const [newType, setNewType] = useState('string'); + const [editingKey, setEditingKey] = useState(null); + const [editingNewKey, setEditingNewKey] = useState(''); + const [editValue, setEditValue] = useState(''); + const [editType, setEditType] = useState('string'); + const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); + + const handleAddVariable = () => { + if (!newKey.trim()) return; + + let parsedValue: any = newValue; + if (newType === 'number') { + parsedValue = parseFloat(newValue) || 0; + } else if (newType === 'boolean') { + parsedValue = newValue === 'true'; + } else if (newType === 'object') { + try { + parsedValue = JSON.parse(newValue); + } catch { + parsedValue = {}; + } + } + + if (viewMode === 'global' && onGlobalVariableAdd) { + const globalType = newType as BlackboardValueType; + onGlobalVariableAdd(newKey, parsedValue, globalType); + } else { + onVariableAdd(newKey, parsedValue, newType); + } + + setNewKey(''); + setNewValue(''); + setIsAdding(false); + }; + + const handleStartEdit = (key: string, value: any) => { + setEditingKey(key); + setEditingNewKey(key); + const currentType = getVariableType(value); + setEditType(currentType); + setEditValue(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)); + }; + + const handleSaveEdit = (key: string) => { + const newKey = editingNewKey.trim(); + if (!newKey) return; + + let parsedValue: any = editValue; + if (editType === 'number') { + parsedValue = parseFloat(editValue) || 0; + } else if (editType === 'boolean') { + parsedValue = editValue === 'true' || editValue === '1'; + } else if (editType === 'object') { + try { + parsedValue = JSON.parse(editValue); + } catch { + return; + } + } + + if (viewMode === 'global' && onGlobalVariableChange) { + if (newKey !== key && onGlobalVariableDelete) { + onGlobalVariableDelete(key); + } + onGlobalVariableChange(newKey, parsedValue); + } else { + if (newKey !== key && onVariableRename) { + onVariableRename(key, newKey); + } + onVariableChange(newKey, parsedValue); + } + + setEditingKey(null); + }; + + const toggleGroup = (groupName: string) => { + setCollapsedGroups(prev => { + const newSet = new Set(prev); + if (newSet.has(groupName)) { + newSet.delete(groupName); + } else { + newSet.add(groupName); + } + return newSet; + }); + }; + + const getVariableType = (value: any): BlackboardVariable['type'] => { + if (typeof value === 'number') return 'number'; + if (typeof value === 'boolean') return 'boolean'; + if (typeof value === 'object') return 'object'; + return 'string'; + }; + + const currentVariables = viewMode === 'global' ? (globalVariables || {}) : variables; + const variableEntries = Object.entries(currentVariables); + + const currentOnDelete = viewMode === 'global' ? onGlobalVariableDelete : onVariableDelete; + + const groupedVariables: Record> = variableEntries.reduce((groups, [key, value]) => { + const parts = key.split('.'); + const groupName = (parts.length > 1 && parts[0]) ? parts[0] : 'default'; + const varName = parts.length > 1 ? parts.slice(1).join('.') : key; + + if (!groups[groupName]) { + groups[groupName] = []; + } + const group = groups[groupName]; + if (group) { + group.push({ fullKey: key, varName, value }); + } + return groups; + }, {} as Record>); + + const groupNames = Object.keys(groupedVariables).sort((a, b) => { + if (a === 'default') return 1; + if (b === 'default') return -1; + return a.localeCompare(b); + }); + + return ( +
+ + + {/* 标题栏 */} +
+
+
+ + Blackboard +
+
+ + +
+
+ + {/* 工具栏 */} +
+
+ {viewMode === 'global' && projectPath ? ( + <> + + .ecs/global-blackboard.json + + ) : ( + + {viewMode === 'local' ? '当前行为树的本地变量' : '所有行为树共享的全局变量'} + + )} +
+
+ {viewMode === 'global' && onSaveGlobal && ( + <> + + + + )} + +
+
+
+ + {/* 变量列表 */} +
+ {variableEntries.length === 0 && !isAdding && ( +
+ No variables yet. Click "Add" to create one. +
+ )} + + {groupNames.map(groupName => { + const isCollapsed = collapsedGroups.has(groupName); + const groupVars = groupedVariables[groupName]; + + if (!groupVars) return null; + + return ( +
+ {groupName !== 'default' && ( +
toggleGroup(groupName)} + style={{ + display: 'flex', + alignItems: 'center', + gap: '4px', + padding: '4px 6px', + backgroundColor: '#252525', + borderRadius: '3px', + cursor: 'pointer', + marginBottom: '4px', + userSelect: 'none' + }} + > + {isCollapsed ? : } + + {groupName} ({groupVars.length}) + +
+ )} + + {!isCollapsed && groupVars.map(({ fullKey: key, varName, value }) => { + const type = getVariableType(value); + const isEditing = editingKey === key; + + const handleDragStart = (e: React.DragEvent) => { + const variableData = { + variableName: key, + variableValue: value, + variableType: type + }; + e.dataTransfer.setData('application/blackboard-variable', JSON.stringify(variableData)); + e.dataTransfer.effectAllowed = 'copy'; + }; + + const typeColor = + type === 'number' ? '#4ec9b0' : + type === 'boolean' ? '#569cd6' : + type === 'object' ? '#ce9178' : '#d4d4d4'; + + const displayValue = type === 'object' ? + JSON.stringify(value) : + String(value); + + const truncatedValue = displayValue.length > 30 ? + displayValue.substring(0, 30) + '...' : + displayValue; + + return ( +
+ {isEditing ? ( +
+
+ Name +
+ setEditingNewKey(e.target.value)} + style={{ + width: '100%', + padding: '4px', + marginBottom: '4px', + backgroundColor: '#1e1e1e', + border: '1px solid #3c3c3c', + borderRadius: '2px', + color: '#9cdcfe', + fontSize: '11px', + fontFamily: 'monospace' + }} + placeholder="Variable name (e.g., player.health)" + /> +
+ Type +
+ +
+ Value +
+