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:
YHH
2025-10-27 09:29:11 +08:00
committed by GitHub
parent 0cd99209c4
commit 009f8af4e1
234 changed files with 21824 additions and 15295 deletions

View File

@@ -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<string, LoadingTask> = new Map();
/** 加载栈(用于循环检测) */
private loadingStack: Set<string> = new Set();
/** 默认配置 */
private defaultOptions: Required<Omit<LoadingOptions, 'parentAssetId'>> = {
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<Entity>,
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<Entity>,
options: Required<Omit<LoadingOptions, 'parentAssetId'>>
): Promise<Entity> {
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<T>(
promise: Promise<T>,
timeoutMs: number,
message: string
): Promise<T> {
let timeoutId: NodeJS.Timeout | number;
const timeoutPromise = new Promise<T>((_, 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<void> {
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();
}
}

View File

@@ -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<Entity>;
/** 开始时间 */
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<Entity>;
}
/**
* 加载进度信息
*/
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';
}
}

View File

@@ -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<string | Uint8Array>;
}
/**
* 文件系统资产加载器
*
* 从文件系统加载行为树资产,支持 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<FileSystemAssetLoaderConfig>;
private cache: Map<string, BehaviorTreeAsset> = 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<BehaviorTreeAsset> {
// 检查缓存
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<boolean> {
// 如果在缓存中,直接返回 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<void> {
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<string | Uint8Array> {
// 检测运行环境
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.'
);
}
}
}

View File

@@ -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<string, BlackboardVariable> = 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<T = any>(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<string, any>): 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<string, any> {
const result: Record<string, any> = {};
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);
}
}

View File

@@ -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<BehaviorTreeAsset> {
* const response = await fetch(`/api/assets/${assetId}`);
* return response.json();
* }
*
* async exists(assetId: string): Promise<boolean> {
* 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<BehaviorTreeAsset>;
/**
* 检查资产是否存在
*
* @param assetId 资产逻辑ID
* @returns 资产是否存在
*/
exists(assetId: string): Promise<boolean>;
/**
* 预加载资产(可选)
*
* 用于提前加载资产到缓存,减少运行时延迟
*
* @param assetIds 要预加载的资产ID列表
*/
preload?(assetIds: string[]): Promise<void>;
/**
* 卸载资产(可选)
*
* 释放资产占用的内存
*
* @param assetId 资产ID
*/
unload?(assetId: string): void;
}

View 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();
}
}