Feature/ecs behavior tree (#188)
* feat(behavior-tree): 完全 ECS 化的行为树系统 * feat(editor-app): 添加行为树可视化编辑器 * chore: 移除 Cocos Creator 扩展目录 * feat(editor-app): 行为树编辑器功能增强 * fix(editor-app): 修复 TypeScript 类型错误 * feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器 * feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序 * feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能 * feat(behavior-tree,editor-app): 添加属性绑定系统 * feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能 * feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能 * feat(behavior-tree,editor-app): 添加运行时资产导出系统 * feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器 * feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理 * fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告 * fix(editor-app): 修复局部黑板类型定义文件扩展名错误
This commit is contained in:
355
packages/behavior-tree/src/Services/WorkspaceService.ts
Normal file
355
packages/behavior-tree/src/Services/WorkspaceService.ts
Normal file
@@ -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<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作区配置
|
||||
*/
|
||||
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<string, AssetRegistry> = new Map();
|
||||
private assetPathMap: Map<string, AssetRegistry> = 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<string>()): 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<string>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user