Compare commits
8 Commits
issue-204-
...
issue-213-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc7a7750e | ||
|
|
50a01d9dd3 | ||
|
|
793aad0a5e | ||
|
|
9c1bf8dbed | ||
|
|
620f3eecc7 | ||
|
|
4355538d8d | ||
|
|
3ad5dc9ca3 | ||
|
|
57c7e7be3f |
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"root": true,
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module",
|
||||
"project": "./tsconfig.json"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"semi": ["error", "always"],
|
||||
"quotes": ["error", "single", { "avoidEscape": true }],
|
||||
"indent": ["error", 4, { "SwitchCase": 1 }],
|
||||
"no-trailing-spaces": "error",
|
||||
"eol-last": ["error", "always"],
|
||||
"comma-dangle": ["error", "none"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"arrow-parens": ["error", "always"],
|
||||
"no-multiple-empty-lines": ["error", { "max": 2, "maxEOF": 1 }],
|
||||
"no-console": "off",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-return": "warn",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-non-null-assertion": "off"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"node_modules/",
|
||||
"dist/",
|
||||
"bin/",
|
||||
"build/",
|
||||
"coverage/",
|
||||
"thirdparty/",
|
||||
"examples/lawn-mower-demo/",
|
||||
"extensions/",
|
||||
"*.min.js",
|
||||
"*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -10,38 +10,43 @@ export default [
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module'
|
||||
sourceType: 'module',
|
||||
project: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': 'warn',
|
||||
'quotes': 'warn',
|
||||
'indent': 'off',
|
||||
'semi': ['warn', 'always'],
|
||||
'quotes': ['warn', 'single', { avoidEscape: true }],
|
||||
'indent': ['warn', 4, {
|
||||
SwitchCase: 1,
|
||||
ignoredNodes: [
|
||||
'PropertyDefinition[decorators.length > 0]',
|
||||
'TSTypeParameterInstantiation'
|
||||
]
|
||||
}],
|
||||
'no-trailing-spaces': 'warn',
|
||||
'eol-last': 'warn',
|
||||
'comma-dangle': 'warn',
|
||||
'object-curly-spacing': 'warn',
|
||||
'array-bracket-spacing': 'warn',
|
||||
'arrow-parens': 'warn',
|
||||
'prefer-const': 'warn',
|
||||
'no-multiple-empty-lines': 'warn',
|
||||
'eol-last': ['warn', 'always'],
|
||||
'comma-dangle': ['warn', 'never'],
|
||||
'object-curly-spacing': ['warn', 'always'],
|
||||
'array-bracket-spacing': ['warn', 'never'],
|
||||
'arrow-parens': ['warn', 'always'],
|
||||
'no-multiple-empty-lines': ['warn', { max: 2, maxEOF: 1 }],
|
||||
'no-console': 'off',
|
||||
'no-empty': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-useless-catch': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/no-unsafe-function-type': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
'@typescript-eslint/no-unsafe-return': 'off',
|
||||
'@typescript-eslint/no-unsafe-argument': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'warn',
|
||||
'@typescript-eslint/no-this-alias': 'warn',
|
||||
'no-case-declarations': 'warn',
|
||||
'no-prototype-builtins': 'warn',
|
||||
'no-empty': 'warn',
|
||||
'no-useless-catch': 'warn'
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -216,7 +216,7 @@ export class BehaviorTreeRuntimeComponent extends Component {
|
||||
*/
|
||||
unobserveBlackboard(nodeId: string): void {
|
||||
for (const observers of this.blackboardObservers.values()) {
|
||||
const index = observers.findIndex(o => o.nodeId === nodeId);
|
||||
const index = observers.findIndex((o) => o.nodeId === nodeId);
|
||||
if (index !== -1) {
|
||||
observers.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -49,11 +49,11 @@ export class NodeMetadataRegistry {
|
||||
}
|
||||
|
||||
static getByCategory(category: string): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.category === category);
|
||||
return this.getAllMetadata().filter((m) => m.category === category);
|
||||
}
|
||||
|
||||
static getByNodeType(nodeType: NodeType): NodeMetadata[] {
|
||||
return this.getAllMetadata().filter(m => m.nodeType === nodeType);
|
||||
return this.getAllMetadata().filter((m) => m.nodeType === nodeType);
|
||||
}
|
||||
|
||||
static getImplementationType(executorClass: Function): string | undefined {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class BehaviorTreeAssetValidator {
|
||||
errors.push('Missing or invalid nodes array');
|
||||
} else {
|
||||
const nodeIds = new Set<string>();
|
||||
const rootNode = asset.nodes.find(n => n.id === asset.rootNodeId);
|
||||
const rootNode = asset.nodes.find((n) => n.id === asset.rootNodeId);
|
||||
|
||||
if (!rootNode) {
|
||||
errors.push(`Root node '${asset.rootNodeId}' not found in nodes array`);
|
||||
@@ -157,7 +157,7 @@ export class BehaviorTreeAssetValidator {
|
||||
// 检查子节点引用
|
||||
if (node.children) {
|
||||
for (const childId of node.children) {
|
||||
if (!asset.nodes.find(n => n.id === childId)) {
|
||||
if (!asset.nodes.find((n) => n.id === childId)) {
|
||||
errors.push(`Node ${node.id} references non-existent child: ${childId}`);
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ export class BehaviorTreeAssetValidator {
|
||||
// 检查是否有孤立节点
|
||||
const referencedNodes = new Set<string>([asset.rootNodeId]);
|
||||
const collectReferencedNodes = (nodeId: string) => {
|
||||
const node = asset.nodes.find(n => n.id === nodeId);
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (node && node.children) {
|
||||
for (const childId of node.children) {
|
||||
referencedNodes.add(childId);
|
||||
@@ -206,8 +206,8 @@ export class BehaviorTreeAssetValidator {
|
||||
|
||||
// 检查属性绑定
|
||||
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) || []);
|
||||
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)) {
|
||||
@@ -276,7 +276,7 @@ export class BehaviorTreeAssetValidator {
|
||||
|
||||
// 计算最大深度
|
||||
const getDepth = (nodeId: string, currentDepth: number = 0): number => {
|
||||
const node = asset.nodes.find(n => n.id === nodeId);
|
||||
const node = asset.nodes.find((n) => n.id === nodeId);
|
||||
if (!node || !node.children || node.children.length === 0) {
|
||||
return currentDepth;
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export class EditorFormatConverter {
|
||||
* 查找根节点
|
||||
*/
|
||||
private static findRootNode(nodes: EditorNode[]): EditorNode | null {
|
||||
return nodes.find(node =>
|
||||
return nodes.find((node) =>
|
||||
node.template.category === '根节点' ||
|
||||
node.data.nodeType === 'root'
|
||||
) || null;
|
||||
@@ -144,7 +144,7 @@ export class EditorFormatConverter {
|
||||
* 转换节点列表
|
||||
*/
|
||||
private static convertNodes(editorNodes: EditorNode[]): BehaviorTreeNodeData[] {
|
||||
return editorNodes.map(node => this.convertNode(node));
|
||||
return editorNodes.map((node) => this.convertNode(node));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -211,13 +211,13 @@ export class EditorFormatConverter {
|
||||
blackboard: BlackboardVariableDefinition[]
|
||||
): PropertyBinding[] {
|
||||
const bindings: PropertyBinding[] = [];
|
||||
const blackboardVarNames = new Set(blackboard.map(v => v.name));
|
||||
const blackboardVarNames = new Set(blackboard.map((v) => v.name));
|
||||
|
||||
const propertyConnections = connections.filter(conn => conn.connectionType === 'property');
|
||||
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);
|
||||
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}`);
|
||||
|
||||
@@ -154,14 +154,14 @@ export class NodeTemplates {
|
||||
*/
|
||||
static getAllTemplates(): NodeTemplate[] {
|
||||
const allMetadata = NodeMetadataRegistry.getAllMetadata();
|
||||
return allMetadata.map(metadata => this.convertMetadataToTemplate(metadata));
|
||||
return allMetadata.map((metadata) => this.convertMetadataToTemplate(metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型和子类型获取模板
|
||||
*/
|
||||
static getTemplate(type: NodeType, subType: string): NodeTemplate | undefined {
|
||||
return this.getAllTemplates().find(t => {
|
||||
return this.getAllTemplates().find((t) => {
|
||||
if (t.type !== type) return false;
|
||||
const config: any = t.defaultConfig;
|
||||
|
||||
@@ -266,7 +266,7 @@ export class NodeTemplates {
|
||||
}
|
||||
|
||||
if (field.options) {
|
||||
property.options = field.options.map(opt => ({
|
||||
property.options = field.options.map((opt) => ({
|
||||
label: opt,
|
||||
value: opt
|
||||
}));
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
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); // 只加载一次
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import eslint from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default [
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'semi': ['error', 'always'],
|
||||
'quotes': ['error', 'single', { avoidEscape: true }],
|
||||
'indent': ['error', 4, { SwitchCase: 1 }],
|
||||
'no-trailing-spaces': 'error',
|
||||
'eol-last': ['error', 'always'],
|
||||
'comma-dangle': ['error', 'never'],
|
||||
'object-curly-spacing': ['error', 'always'],
|
||||
'array-bracket-spacing': ['error', 'never'],
|
||||
'arrow-parens': ['error', 'always'],
|
||||
'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }],
|
||||
'no-console': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-call': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-non-null-assertion': 'off'
|
||||
}
|
||||
},
|
||||
{
|
||||
ignores: [
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
'bin/**',
|
||||
'coverage/**',
|
||||
'**/*.min.js',
|
||||
'**/*.d.ts'
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -329,8 +329,8 @@ export class Core {
|
||||
*/
|
||||
public static setScene<T extends IScene>(scene: T): T {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
throw new Error("Core实例未创建");
|
||||
Core._logger.warn('Core实例未创建,请先调用Core.create()');
|
||||
throw new Error('Core实例未创建');
|
||||
}
|
||||
|
||||
return this._instance._sceneManager.setScene(scene);
|
||||
@@ -387,7 +387,7 @@ export class Core {
|
||||
*/
|
||||
public static loadScene<T extends IScene>(scene: T): void {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
Core._logger.warn('Core实例未创建,请先调用Core.create()');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@ export class Core {
|
||||
*/
|
||||
public static update(deltaTime: number): void {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
Core._logger.warn('Core实例未创建,请先调用Core.create()');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -472,7 +472,7 @@ export class Core {
|
||||
*/
|
||||
public static enableDebug(config: IECSDebugConfig): void {
|
||||
if (!this._instance) {
|
||||
Core._logger.warn("Core实例未创建,请先调用Core.create()");
|
||||
Core._logger.warn('Core实例未创建,请先调用Core.create()');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ export function Updatable(priority: number = 0): ClassDecorator {
|
||||
if (!prototype || typeof prototype.update !== 'function') {
|
||||
throw new Error(
|
||||
`@Updatable() decorator requires class ${target.name} to implement IUpdatable interface with update() method. ` +
|
||||
`Please add 'implements IUpdatable' and define update(deltaTime?: number): void method.`
|
||||
'Please add \'implements IUpdatable\' and define update(deltaTime?: number): void method.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ export function createInstance<T>(
|
||||
if (typeof serviceType === 'string' || typeof serviceType === 'symbol') {
|
||||
// 字符串或Symbol类型的服务标识
|
||||
throw new Error(
|
||||
`String and Symbol service identifiers are not yet supported in constructor injection. ` +
|
||||
'String and Symbol service identifiers are not yet supported in constructor injection. ' +
|
||||
`Please use class types for ${constructor.name} parameter ${i}`
|
||||
);
|
||||
} else {
|
||||
@@ -338,7 +338,7 @@ export function registerInjectable<T extends IService>(
|
||||
if (!isInjectable(serviceType)) {
|
||||
throw new Error(
|
||||
`${serviceType.name} is not marked as @Injectable(). ` +
|
||||
`Please add @Injectable() decorator to the class.`
|
||||
'Please add @Injectable() decorator to the class.'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -240,7 +240,7 @@ export class ServiceContainer {
|
||||
|
||||
// 检测循环依赖
|
||||
if (this._resolving.has(type as ServiceType<IService>)) {
|
||||
const chain = Array.from(this._resolving).map(t => t.name).join(' -> ');
|
||||
const chain = Array.from(this._resolving).map((t) => t.name).join(' -> ');
|
||||
throw new Error(`Circular dependency detected: ${chain} -> ${type.name}`);
|
||||
}
|
||||
|
||||
@@ -337,7 +337,7 @@ export class ServiceContainer {
|
||||
// 如果有单例实例,调用 dispose
|
||||
if (registration.instance) {
|
||||
// 从可更新列表中移除
|
||||
const index = this._updatableServices.findIndex(item => item.instance === registration.instance);
|
||||
const index = this._updatableServices.findIndex((item) => item.instance === registration.instance);
|
||||
if (index !== -1) {
|
||||
this._updatableServices.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export abstract class Component implements IComponent {
|
||||
*
|
||||
* 用于为每个组件分配唯一的ID。
|
||||
*/
|
||||
public static _idGenerator: number = 0;
|
||||
private static idGenerator: number = 0;
|
||||
|
||||
/**
|
||||
* 组件唯一标识符
|
||||
@@ -58,7 +58,7 @@ export abstract class Component implements IComponent {
|
||||
* 自动分配唯一ID给组件。
|
||||
*/
|
||||
constructor() {
|
||||
this.id = Component._idGenerator++;
|
||||
this.id = Component.idGenerator++;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Entity } from '../Entity';
|
||||
import { ComponentType, ComponentRegistry } from './ComponentStorage';
|
||||
import { BitMask64Data, BitMask64Utils } from "../Utils";
|
||||
import { BitMaskHashMap } from "../Utils/BitMaskHashMap";
|
||||
import { BitMask64Data, BitMask64Utils } from '../Utils';
|
||||
import { BitMaskHashMap } from '../Utils/BitMaskHashMap';
|
||||
|
||||
/**
|
||||
* 原型标识符
|
||||
@@ -247,7 +247,7 @@ export class ArchetypeSystem {
|
||||
*/
|
||||
private updateAllArchetypeArrays(): void {
|
||||
this._allArchetypes = [];
|
||||
for (let archetype of this._archetypes.values()) {
|
||||
for (const archetype of this._archetypes.values()) {
|
||||
this._allArchetypes.push(archetype);
|
||||
}
|
||||
}
|
||||
@@ -258,7 +258,7 @@ export class ArchetypeSystem {
|
||||
private getEntityComponentTypes(entity: Entity): ComponentType[] {
|
||||
let componentTypes = this._entityComponentTypesCache.get(entity);
|
||||
if (!componentTypes) {
|
||||
componentTypes = entity.components.map(component => component.constructor as ComponentType);
|
||||
componentTypes = entity.components.map((component) => component.constructor as ComponentType);
|
||||
this._entityComponentTypesCache.set(entity, componentTypes);
|
||||
}
|
||||
return componentTypes;
|
||||
@@ -269,7 +269,7 @@ export class ArchetypeSystem {
|
||||
* 使用ComponentRegistry确保与Entity.componentMask使用相同的bitIndex
|
||||
*/
|
||||
private generateArchetypeId(componentTypes: ComponentType[]): ArchetypeId {
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const type of componentTypes) {
|
||||
if (!ComponentRegistry.isRegistered(type)) {
|
||||
ComponentRegistry.register(type);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ComponentRegistry, ComponentType } from './ComponentStorage/ComponentRe
|
||||
export { ComponentRegistry, ComponentType };
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 高性能组件存储器
|
||||
*/
|
||||
@@ -342,7 +341,7 @@ export class ComponentStorageManager {
|
||||
* @returns 组件位掩码
|
||||
*/
|
||||
public getComponentMask(entityId: number): BitMask64Data {
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
|
||||
for (const [componentType, storage] of this.storages.entries()) {
|
||||
if (storage.hasComponent(entityId)) {
|
||||
|
||||
@@ -181,7 +181,7 @@ export class ComponentRegistry {
|
||||
return this.maskCache.get(cacheKey)!;
|
||||
}
|
||||
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const name of componentNames) {
|
||||
const componentId = this.getComponentId(name);
|
||||
if (componentId !== undefined) {
|
||||
|
||||
@@ -89,10 +89,13 @@ export class EventBus implements IEventBus {
|
||||
const eventConfig: EventListenerConfig = {
|
||||
once: config.once || false,
|
||||
priority: config.priority || EventPriority.NORMAL,
|
||||
async: config.async || false,
|
||||
context: config.context
|
||||
async: config.async || false
|
||||
};
|
||||
|
||||
if (config.thisArg) {
|
||||
eventConfig.thisArg = config.thisArg as object;
|
||||
}
|
||||
|
||||
if (this.isDebugMode) {
|
||||
EventBus._logger.info(`添加监听器: ${eventType}`, eventConfig);
|
||||
}
|
||||
@@ -127,7 +130,7 @@ export class EventBus implements IEventBus {
|
||||
handler: (data: T) => Promise<void>,
|
||||
config: IEventListenerConfig = {}
|
||||
): string {
|
||||
return this.on(eventType, handler as any, { ...config, async: true });
|
||||
return this.on(eventType, handler, { ...config, async: true });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -466,4 +469,3 @@ export class GlobalEventBus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ import { createLogger } from '../../Utils/Logger';
|
||||
/**
|
||||
* 事件处理器函数类型
|
||||
*/
|
||||
export type EventHandler<T = any> = (event: T) => void;
|
||||
export type EventHandler<T> = (event: T) => void;
|
||||
|
||||
/**
|
||||
* 异步事件处理器函数类型
|
||||
*/
|
||||
export type AsyncEventHandler<T = any> = (event: T) => Promise<void>;
|
||||
export type AsyncEventHandler<T> = (event: T) => Promise<void>;
|
||||
|
||||
/**
|
||||
* 事件监听器配置
|
||||
@@ -20,15 +20,20 @@ export interface EventListenerConfig {
|
||||
priority?: number;
|
||||
/** 是否异步执行 */
|
||||
async?: boolean;
|
||||
/** 执行上下文 */
|
||||
context?: any;
|
||||
/** 事件处理函数的 this 绑定对象 */
|
||||
thisArg?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部事件监听器
|
||||
*
|
||||
* 注意:handler 使用 any 是必要的类型擦除
|
||||
* 原因:需要在同一数组中存储处理不同事件类型(T)的监听器
|
||||
* 类型安全保证:公共 API (on<T>/emit<T>) 在编译时保证类型匹配
|
||||
*/
|
||||
interface InternalEventListener<T = any> {
|
||||
handler: EventHandler<T> | AsyncEventHandler<T>;
|
||||
interface InternalEventListener {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handler: EventHandler<any> | AsyncEventHandler<any>;
|
||||
config: EventListenerConfig;
|
||||
id: string;
|
||||
}
|
||||
@@ -71,8 +76,9 @@ export class TypeSafeEventSystem {
|
||||
private static readonly _logger = createLogger('EventSystem');
|
||||
private listeners = new Map<string, InternalEventListener[]>();
|
||||
private stats = new Map<string, EventStats>();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private batchQueue = new Map<string, any[]>();
|
||||
private batchTimers = new Map<string, number>();
|
||||
private batchTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||
private batchConfigs = new Map<string, EventBatchConfig>();
|
||||
private nextListenerId = 0;
|
||||
private isEnabled = true;
|
||||
@@ -81,13 +87,13 @@ export class TypeSafeEventSystem {
|
||||
/**
|
||||
* 添加事件监听器
|
||||
* @param eventType 事件类型
|
||||
* @param handler 事件处理器
|
||||
* @param handler 事件处理器(同步或异步,根据 config.async 决定)
|
||||
* @param config 监听器配置
|
||||
* @returns 监听器ID(用于移除)
|
||||
*/
|
||||
public on<T>(
|
||||
eventType: string,
|
||||
handler: EventHandler<T>,
|
||||
handler: EventHandler<T> | AsyncEventHandler<T>,
|
||||
config: EventListenerConfig = {}
|
||||
): string {
|
||||
return this.addListener(eventType, handler, config);
|
||||
@@ -100,11 +106,7 @@ export class TypeSafeEventSystem {
|
||||
* @param config 监听器配置
|
||||
* @returns 监听器ID
|
||||
*/
|
||||
public once<T>(
|
||||
eventType: string,
|
||||
handler: EventHandler<T>,
|
||||
config: EventListenerConfig = {}
|
||||
): string {
|
||||
public once<T>(eventType: string, handler: EventHandler<T>, config: EventListenerConfig = {}): string {
|
||||
return this.addListener(eventType, handler, { ...config, once: true });
|
||||
}
|
||||
|
||||
@@ -115,11 +117,7 @@ export class TypeSafeEventSystem {
|
||||
* @param config 监听器配置
|
||||
* @returns 监听器ID
|
||||
*/
|
||||
public onAsync<T>(
|
||||
eventType: string,
|
||||
handler: AsyncEventHandler<T>,
|
||||
config: EventListenerConfig = {}
|
||||
): string {
|
||||
public onAsync<T>(eventType: string, handler: AsyncEventHandler<T>, config: EventListenerConfig = {}): string {
|
||||
return this.addListener(eventType, handler, { ...config, async: true });
|
||||
}
|
||||
|
||||
@@ -133,7 +131,7 @@ export class TypeSafeEventSystem {
|
||||
const listeners = this.listeners.get(eventType);
|
||||
if (!listeners) return false;
|
||||
|
||||
const index = listeners.findIndex(l => l.id === listenerId);
|
||||
const index = listeners.findIndex((l) => l.id === listenerId);
|
||||
if (index === -1) return false;
|
||||
|
||||
listeners.splice(index, 1);
|
||||
@@ -197,8 +195,8 @@ export class TypeSafeEventSystem {
|
||||
if (listener.config.async) continue; // 跳过异步监听器
|
||||
|
||||
try {
|
||||
if (listener.config.context) {
|
||||
(listener.handler as EventHandler<T>).call(listener.config.context, event);
|
||||
if (listener.config.thisArg) {
|
||||
(listener.handler as EventHandler<T>).call(listener.config.thisArg, event);
|
||||
} else {
|
||||
(listener.handler as EventHandler<T>)(event);
|
||||
}
|
||||
@@ -344,7 +342,7 @@ export class TypeSafeEventSystem {
|
||||
}
|
||||
|
||||
const listenerId = `listener_${this.nextListenerId++}`;
|
||||
const listener: InternalEventListener<T> = {
|
||||
const listener: InternalEventListener = {
|
||||
handler,
|
||||
config: {
|
||||
priority: 0,
|
||||
@@ -379,14 +377,14 @@ export class TypeSafeEventSystem {
|
||||
const sortedListeners = this.sortListenersByPriority(listeners);
|
||||
|
||||
// 分离同步和异步监听器
|
||||
const syncListeners = sortedListeners.filter(l => !l.config.async);
|
||||
const asyncListeners = sortedListeners.filter(l => l.config.async);
|
||||
const syncListeners = sortedListeners.filter((l) => !l.config.async);
|
||||
const asyncListeners = sortedListeners.filter((l) => l.config.async);
|
||||
|
||||
// 执行同步监听器
|
||||
for (const listener of syncListeners) {
|
||||
try {
|
||||
if (listener.config.context) {
|
||||
(listener.handler as EventHandler<T>).call(listener.config.context, event);
|
||||
if (listener.config.thisArg) {
|
||||
(listener.handler as EventHandler<T>).call(listener.config.thisArg, event);
|
||||
} else {
|
||||
(listener.handler as EventHandler<T>)(event);
|
||||
}
|
||||
@@ -402,8 +400,8 @@ export class TypeSafeEventSystem {
|
||||
// 执行异步监听器
|
||||
const asyncPromises = asyncListeners.map(async (listener) => {
|
||||
try {
|
||||
if (listener.config.context) {
|
||||
await (listener.handler as AsyncEventHandler<T>).call(listener.config.context, event);
|
||||
if (listener.config.thisArg) {
|
||||
await (listener.handler as AsyncEventHandler<T>).call(listener.config.thisArg, event);
|
||||
} else {
|
||||
await (listener.handler as AsyncEventHandler<T>)(event);
|
||||
}
|
||||
@@ -431,7 +429,7 @@ export class TypeSafeEventSystem {
|
||||
* @param listeners 监听器数组
|
||||
* @returns 排序后的监听器数组
|
||||
*/
|
||||
private sortListenersByPriority<T>(listeners: InternalEventListener<T>[]): InternalEventListener<T>[] {
|
||||
private sortListenersByPriority(listeners: InternalEventListener[]): InternalEventListener[] {
|
||||
return listeners.slice().sort((a, b) => (b.config.priority || 0) - (a.config.priority || 0));
|
||||
}
|
||||
|
||||
@@ -447,7 +445,7 @@ export class TypeSafeEventSystem {
|
||||
if (!listeners) return;
|
||||
|
||||
for (const id of listenerIds) {
|
||||
const index = listeners.findIndex(l => l.id === id);
|
||||
const index = listeners.findIndex((l) => l.id === id);
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1);
|
||||
}
|
||||
@@ -488,7 +486,7 @@ export class TypeSafeEventSystem {
|
||||
this.flushBatch(eventType);
|
||||
}, config.delay);
|
||||
|
||||
this.batchTimers.set(eventType, timer as any);
|
||||
this.batchTimers.set(eventType, timer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,5 +575,3 @@ export class TypeSafeEventSystem {
|
||||
* 全局事件系统实例
|
||||
*/
|
||||
export const GlobalEventSystem = new TypeSafeEventSystem();
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class EntityBuilder {
|
||||
this.scene = scene;
|
||||
this.storageManager = storageManager;
|
||||
const id = scene.identifierPool.checkOut();
|
||||
this.entity = new Entity("", id);
|
||||
this.entity = new Entity('', id);
|
||||
this.entity.scene = this.scene as any;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
export { QuerySystem } from '../QuerySystem';
|
||||
export { ECSFluentAPI, createECSAPI } from '../FluentAPI';
|
||||
|
||||
@@ -43,21 +43,21 @@ interface QueryCacheEntry {
|
||||
* ```
|
||||
*/
|
||||
export class QuerySystem {
|
||||
private _logger = createLogger('QuerySystem');
|
||||
private entities: Entity[] = [];
|
||||
private entityIndex: EntityIndex;
|
||||
private readonly _logger = createLogger('QuerySystem');
|
||||
private _entities: Entity[] = [];
|
||||
private _entityIndex: EntityIndex;
|
||||
|
||||
private _version = 0;
|
||||
|
||||
private queryCache = new Map<string, QueryCacheEntry>();
|
||||
private cacheMaxSize = 1000;
|
||||
private cacheTimeout = 5000;
|
||||
private _queryCache = new Map<string, QueryCacheEntry>();
|
||||
private _cacheMaxSize = 1000;
|
||||
private _cacheTimeout = 5000;
|
||||
|
||||
private componentMaskCache = new Map<string, BitMask64Data>();
|
||||
private _componentMaskCache = new Map<string, BitMask64Data>();
|
||||
|
||||
private archetypeSystem: ArchetypeSystem;
|
||||
private _archetypeSystem: ArchetypeSystem;
|
||||
|
||||
private queryStats = {
|
||||
private _queryStats = {
|
||||
totalQueries: 0,
|
||||
cacheHits: 0,
|
||||
indexHits: 0,
|
||||
@@ -67,12 +67,12 @@ export class QuerySystem {
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.entityIndex = {
|
||||
this._entityIndex = {
|
||||
byTag: new Map(),
|
||||
byName: new Map()
|
||||
};
|
||||
|
||||
this.archetypeSystem = new ArchetypeSystem();
|
||||
this._archetypeSystem = new ArchetypeSystem();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +84,7 @@ export class QuerySystem {
|
||||
* @param entities 新的实体列表
|
||||
*/
|
||||
public setEntities(entities: Entity[]): void {
|
||||
this.entities = entities;
|
||||
this._entities = entities;
|
||||
this.clearQueryCache();
|
||||
this.clearReactiveQueries();
|
||||
this.rebuildIndexes();
|
||||
@@ -100,11 +100,11 @@ export class QuerySystem {
|
||||
* @param deferCacheClear 是否延迟缓存清理(用于批量操作)
|
||||
*/
|
||||
public addEntity(entity: Entity, deferCacheClear: boolean = false): void {
|
||||
if (!this.entities.includes(entity)) {
|
||||
this.entities.push(entity);
|
||||
if (!this._entities.includes(entity)) {
|
||||
this._entities.push(entity);
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
this._archetypeSystem.addEntity(entity);
|
||||
|
||||
// 通知响应式查询
|
||||
this.notifyReactiveQueriesEntityAdded(entity);
|
||||
@@ -131,16 +131,16 @@ export class QuerySystem {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
// 使用Set来快速检查重复
|
||||
const existingIds = new Set(this.entities.map(e => e.id));
|
||||
const existingIds = new Set(this._entities.map((e) => e.id));
|
||||
let addedCount = 0;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!existingIds.has(entity.id)) {
|
||||
this.entities.push(entity);
|
||||
this._entities.push(entity);
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
// 更新索引管理器
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
this._archetypeSystem.addEntity(entity);
|
||||
|
||||
existingIds.add(entity.id);
|
||||
addedCount++;
|
||||
@@ -166,7 +166,7 @@ export class QuerySystem {
|
||||
|
||||
// 避免调用栈溢出,分批添加
|
||||
for (const entity of entities) {
|
||||
this.entities.push(entity);
|
||||
this._entities.push(entity);
|
||||
}
|
||||
|
||||
// 批量更新索引
|
||||
@@ -174,7 +174,7 @@ export class QuerySystem {
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
// 更新索引管理器
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
this._archetypeSystem.addEntity(entity);
|
||||
}
|
||||
|
||||
// 清理缓存
|
||||
@@ -189,17 +189,17 @@ export class QuerySystem {
|
||||
* @param entity 要移除的实体
|
||||
*/
|
||||
public removeEntity(entity: Entity): void {
|
||||
const index = this.entities.indexOf(entity);
|
||||
const index = this._entities.indexOf(entity);
|
||||
if (index !== -1) {
|
||||
const componentTypes: ComponentType[] = [];
|
||||
for (const component of entity.components) {
|
||||
componentTypes.push(component.constructor as ComponentType);
|
||||
}
|
||||
|
||||
this.entities.splice(index, 1);
|
||||
this._entities.splice(index, 1);
|
||||
this.removeEntityFromIndexes(entity);
|
||||
|
||||
this.archetypeSystem.removeEntity(entity);
|
||||
this._archetypeSystem.removeEntity(entity);
|
||||
|
||||
if (componentTypes.length > 0) {
|
||||
this.notifyReactiveQueriesEntityRemoved(entity, componentTypes);
|
||||
@@ -222,7 +222,7 @@ export class QuerySystem {
|
||||
*/
|
||||
public updateEntity(entity: Entity): void {
|
||||
// 检查实体是否在查询系统中
|
||||
if (!this.entities.includes(entity)) {
|
||||
if (!this._entities.includes(entity)) {
|
||||
// 如果实体不在系统中,直接添加
|
||||
this.addEntity(entity);
|
||||
return;
|
||||
@@ -232,7 +232,7 @@ export class QuerySystem {
|
||||
this.removeEntityFromIndexes(entity);
|
||||
|
||||
// 更新ArchetypeSystem中的实体状态
|
||||
this.archetypeSystem.updateEntity(entity);
|
||||
this._archetypeSystem.updateEntity(entity);
|
||||
// 重新添加实体到索引(基于新的组件状态)
|
||||
this.addEntityToIndexes(entity);
|
||||
|
||||
@@ -253,28 +253,27 @@ export class QuerySystem {
|
||||
// 标签索引
|
||||
const tag = entity.tag;
|
||||
if (tag !== undefined) {
|
||||
const tagSet = this.entityIndex.byTag.get(tag) || this.createAndSetTagIndex(tag);
|
||||
const tagSet = this._entityIndex.byTag.get(tag) || this.createAndSetTagIndex(tag);
|
||||
tagSet.add(entity);
|
||||
}
|
||||
|
||||
// 名称索引
|
||||
const name = entity.name;
|
||||
if (name) {
|
||||
const nameSet = this.entityIndex.byName.get(name) || this.createAndSetNameIndex(name);
|
||||
const nameSet = this._entityIndex.byName.get(name) || this.createAndSetNameIndex(name);
|
||||
nameSet.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private createAndSetTagIndex(tag: number): Set<Entity> {
|
||||
const set = new Set<Entity>();
|
||||
this.entityIndex.byTag.set(tag, set);
|
||||
this._entityIndex.byTag.set(tag, set);
|
||||
return set;
|
||||
}
|
||||
|
||||
private createAndSetNameIndex(name: string): Set<Entity> {
|
||||
const set = new Set<Entity>();
|
||||
this.entityIndex.byName.set(name, set);
|
||||
this._entityIndex.byName.set(name, set);
|
||||
return set;
|
||||
}
|
||||
|
||||
@@ -284,22 +283,22 @@ export class QuerySystem {
|
||||
private removeEntityFromIndexes(entity: Entity): void {
|
||||
// 从标签索引移除
|
||||
if (entity.tag !== undefined) {
|
||||
const tagSet = this.entityIndex.byTag.get(entity.tag);
|
||||
const tagSet = this._entityIndex.byTag.get(entity.tag);
|
||||
if (tagSet) {
|
||||
tagSet.delete(entity);
|
||||
if (tagSet.size === 0) {
|
||||
this.entityIndex.byTag.delete(entity.tag);
|
||||
this._entityIndex.byTag.delete(entity.tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从名称索引移除
|
||||
if (entity.name) {
|
||||
const nameSet = this.entityIndex.byName.get(entity.name);
|
||||
const nameSet = this._entityIndex.byName.get(entity.name);
|
||||
if (nameSet) {
|
||||
nameSet.delete(entity);
|
||||
if (nameSet.size === 0) {
|
||||
this.entityIndex.byName.delete(entity.name);
|
||||
this._entityIndex.byName.delete(entity.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,15 +311,15 @@ export class QuerySystem {
|
||||
* 通常在大量实体变更后调用以确保索引一致性。
|
||||
*/
|
||||
private rebuildIndexes(): void {
|
||||
this.entityIndex.byTag.clear();
|
||||
this.entityIndex.byName.clear();
|
||||
this._entityIndex.byTag.clear();
|
||||
this._entityIndex.byName.clear();
|
||||
|
||||
// 清理ArchetypeSystem和ComponentIndexManager
|
||||
this.archetypeSystem.clear();
|
||||
this._archetypeSystem.clear();
|
||||
|
||||
for (const entity of this.entities) {
|
||||
for (const entity of this._entities) {
|
||||
this.addEntityToIndexes(entity);
|
||||
this.archetypeSystem.addEntity(entity);
|
||||
this._archetypeSystem.addEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -342,7 +341,7 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryAll(...componentTypes: ComponentType[]): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes);
|
||||
@@ -351,7 +350,7 @@ export class QuerySystem {
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
@@ -379,7 +378,7 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryAny(...componentTypes: ComponentType[]): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes);
|
||||
@@ -388,7 +387,7 @@ export class QuerySystem {
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
@@ -416,7 +415,7 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryNone(...componentTypes: ComponentType[]): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
// 使用内部响应式查询作为智能缓存
|
||||
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes);
|
||||
@@ -425,7 +424,7 @@ export class QuerySystem {
|
||||
const entities = reactiveQuery.getEntities();
|
||||
|
||||
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
|
||||
return {
|
||||
entities,
|
||||
@@ -452,14 +451,14 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryByTag(tag: number): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
const cacheKey = `tag:${tag}`;
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
@@ -469,8 +468,8 @@ export class QuerySystem {
|
||||
}
|
||||
|
||||
// 使用索引查询
|
||||
this.queryStats.indexHits++;
|
||||
const entities = Array.from(this.entityIndex.byTag.get(tag) || []);
|
||||
this._queryStats.indexHits++;
|
||||
const entities = Array.from(this._entityIndex.byTag.get(tag) || []);
|
||||
|
||||
// 缓存结果
|
||||
this.addToCache(cacheKey, entities);
|
||||
@@ -500,14 +499,14 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryByName(name: string): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
const cacheKey = `name:${name}`;
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
@@ -517,8 +516,8 @@ export class QuerySystem {
|
||||
}
|
||||
|
||||
// 使用索引查询
|
||||
this.queryStats.indexHits++;
|
||||
const entities = Array.from(this.entityIndex.byName.get(name) || []);
|
||||
this._queryStats.indexHits++;
|
||||
const entities = Array.from(this._entityIndex.byName.get(name) || []);
|
||||
|
||||
// 缓存结果
|
||||
this.addToCache(cacheKey, entities);
|
||||
@@ -548,14 +547,14 @@ export class QuerySystem {
|
||||
*/
|
||||
public queryByComponent<T extends Component>(componentType: ComponentType<T>): QueryResult {
|
||||
const startTime = performance.now();
|
||||
this.queryStats.totalQueries++;
|
||||
this._queryStats.totalQueries++;
|
||||
|
||||
const cacheKey = this.generateCacheKey('component', [componentType]);
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.getFromCache(cacheKey);
|
||||
if (cached) {
|
||||
this.queryStats.cacheHits++;
|
||||
this._queryStats.cacheHits++;
|
||||
return {
|
||||
entities: cached,
|
||||
count: cached.length,
|
||||
@@ -564,8 +563,8 @@ export class QuerySystem {
|
||||
};
|
||||
}
|
||||
|
||||
this.queryStats.indexHits++;
|
||||
const entities = this.archetypeSystem.getEntitiesByComponent(componentType);
|
||||
this._queryStats.indexHits++;
|
||||
const entities = this._archetypeSystem.getEntitiesByComponent(componentType);
|
||||
|
||||
// 缓存结果
|
||||
this.addToCache(cacheKey, entities);
|
||||
@@ -582,12 +581,12 @@ export class QuerySystem {
|
||||
* 从缓存获取查询结果
|
||||
*/
|
||||
private getFromCache(cacheKey: string): readonly Entity[] | null {
|
||||
const entry = this.queryCache.get(cacheKey);
|
||||
const entry = this._queryCache.get(cacheKey);
|
||||
if (!entry) return null;
|
||||
|
||||
// 检查缓存是否过期或版本过期
|
||||
if (Date.now() - entry.timestamp > this.cacheTimeout || entry.version !== this._version) {
|
||||
this.queryCache.delete(cacheKey);
|
||||
if (Date.now() - entry.timestamp > this._cacheTimeout || entry.version !== this._version) {
|
||||
this._queryCache.delete(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -600,11 +599,11 @@ export class QuerySystem {
|
||||
*/
|
||||
private addToCache(cacheKey: string, entities: Entity[]): void {
|
||||
// 如果缓存已满,清理最少使用的条目
|
||||
if (this.queryCache.size >= this.cacheMaxSize) {
|
||||
if (this._queryCache.size >= this._cacheMaxSize) {
|
||||
this.cleanupCache();
|
||||
}
|
||||
|
||||
this.queryCache.set(cacheKey, {
|
||||
this._queryCache.set(cacheKey, {
|
||||
entities: entities, // 直接使用引用,通过版本号控制失效
|
||||
timestamp: Date.now(),
|
||||
hitCount: 0,
|
||||
@@ -618,22 +617,24 @@ export class QuerySystem {
|
||||
private cleanupCache(): void {
|
||||
// 移除过期的缓存条目
|
||||
const now = Date.now();
|
||||
for (const [key, entry] of this.queryCache.entries()) {
|
||||
if (now - entry.timestamp > this.cacheTimeout) {
|
||||
this.queryCache.delete(key);
|
||||
for (const [key, entry] of this._queryCache.entries()) {
|
||||
if (now - entry.timestamp > this._cacheTimeout) {
|
||||
this._queryCache.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还是太满,移除最少使用的条目
|
||||
if (this.queryCache.size >= this.cacheMaxSize) {
|
||||
if (this._queryCache.size >= this._cacheMaxSize) {
|
||||
let minHitCount = Infinity;
|
||||
let oldestKey = '';
|
||||
let oldestTimestamp = Infinity;
|
||||
|
||||
// 单次遍历找到最少使用或最旧的条目
|
||||
for (const [key, entry] of this.queryCache.entries()) {
|
||||
if (entry.hitCount < minHitCount ||
|
||||
(entry.hitCount === minHitCount && entry.timestamp < oldestTimestamp)) {
|
||||
for (const [key, entry] of this._queryCache.entries()) {
|
||||
if (
|
||||
entry.hitCount < minHitCount ||
|
||||
(entry.hitCount === minHitCount && entry.timestamp < oldestTimestamp)
|
||||
) {
|
||||
minHitCount = entry.hitCount;
|
||||
oldestKey = key;
|
||||
oldestTimestamp = entry.timestamp;
|
||||
@@ -641,7 +642,7 @@ export class QuerySystem {
|
||||
}
|
||||
|
||||
if (oldestKey) {
|
||||
this.queryCache.delete(oldestKey);
|
||||
this._queryCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,8 +651,8 @@ export class QuerySystem {
|
||||
* 清除所有查询缓存
|
||||
*/
|
||||
private clearQueryCache(): void {
|
||||
this.queryCache.clear();
|
||||
this.componentMaskCache.clear();
|
||||
this._queryCache.clear();
|
||||
this._componentMaskCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -679,10 +680,13 @@ export class QuerySystem {
|
||||
}
|
||||
|
||||
// 多组件查询:使用排序后的类型名称创建键
|
||||
const sortKey = componentTypes.map(t => {
|
||||
const sortKey = componentTypes
|
||||
.map((t) => {
|
||||
const name = getComponentTypeName(t);
|
||||
return name;
|
||||
}).sort().join(',');
|
||||
})
|
||||
.sort()
|
||||
.join(',');
|
||||
|
||||
const fullKey = `${prefix}:${sortKey}`;
|
||||
|
||||
@@ -724,10 +728,7 @@ export class QuerySystem {
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
public createReactiveQuery(
|
||||
componentTypes: ComponentType[],
|
||||
config?: ReactiveQueryConfig
|
||||
): ReactiveQuery {
|
||||
public createReactiveQuery(componentTypes: ComponentType[], config?: ReactiveQueryConfig): ReactiveQuery {
|
||||
if (!componentTypes || componentTypes.length === 0) {
|
||||
throw new Error('组件类型列表不能为空');
|
||||
}
|
||||
@@ -741,10 +742,7 @@ export class QuerySystem {
|
||||
|
||||
const query = new ReactiveQuery(condition, config);
|
||||
|
||||
const initialEntities = this.executeTraditionalQuery(
|
||||
QueryConditionType.ALL,
|
||||
componentTypes
|
||||
);
|
||||
const initialEntities = this.executeTraditionalQuery(QueryConditionType.ALL, componentTypes);
|
||||
query.initializeWith(initialEntities);
|
||||
|
||||
const cacheKey = this.generateCacheKey('all', componentTypes);
|
||||
@@ -810,18 +808,21 @@ export class QuerySystem {
|
||||
*/
|
||||
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
|
||||
// 生成缓存键
|
||||
const cacheKey = componentTypes.map(t => {
|
||||
const cacheKey = componentTypes
|
||||
.map((t) => {
|
||||
return getComponentTypeName(t);
|
||||
}).sort().join(',');
|
||||
})
|
||||
.sort()
|
||||
.join(',');
|
||||
|
||||
// 检查缓存
|
||||
const cached = this.componentMaskCache.get(cacheKey);
|
||||
const cached = this._componentMaskCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
// 使用ComponentRegistry而不是ComponentTypeManager,确保bitIndex一致
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const type of componentTypes) {
|
||||
// 确保组件已注册
|
||||
if (!ComponentRegistry.isRegistered(type)) {
|
||||
@@ -832,7 +833,7 @@ export class QuerySystem {
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
this.componentMaskCache.set(cacheKey, mask);
|
||||
this._componentMaskCache.set(cacheKey, mask);
|
||||
return mask;
|
||||
}
|
||||
|
||||
@@ -847,7 +848,7 @@ export class QuerySystem {
|
||||
* 获取所有实体
|
||||
*/
|
||||
public getAllEntities(): readonly Entity[] {
|
||||
return this.entities;
|
||||
return this._entities;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -883,28 +884,32 @@ export class QuerySystem {
|
||||
};
|
||||
} {
|
||||
return {
|
||||
entityCount: this.entities.length,
|
||||
entityCount: this._entities.length,
|
||||
indexStats: {
|
||||
componentIndexSize: this.archetypeSystem.getAllArchetypes().length,
|
||||
tagIndexSize: this.entityIndex.byTag.size,
|
||||
nameIndexSize: this.entityIndex.byName.size
|
||||
componentIndexSize: this._archetypeSystem.getAllArchetypes().length,
|
||||
tagIndexSize: this._entityIndex.byTag.size,
|
||||
nameIndexSize: this._entityIndex.byName.size
|
||||
},
|
||||
queryStats: {
|
||||
...this.queryStats,
|
||||
cacheHitRate: this.queryStats.totalQueries > 0 ?
|
||||
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%'
|
||||
...this._queryStats,
|
||||
cacheHitRate:
|
||||
this._queryStats.totalQueries > 0
|
||||
? ((this._queryStats.cacheHits / this._queryStats.totalQueries) * 100).toFixed(2) + '%'
|
||||
: '0%'
|
||||
},
|
||||
optimizationStats: {
|
||||
archetypeSystem: this.archetypeSystem.getAllArchetypes().map(a => ({
|
||||
archetypeSystem: this._archetypeSystem.getAllArchetypes().map((a) => ({
|
||||
id: a.id,
|
||||
componentTypes: a.componentTypes.map(t => getComponentTypeName(t)),
|
||||
componentTypes: a.componentTypes.map((t) => getComponentTypeName(t)),
|
||||
entityCount: a.entities.size
|
||||
}))
|
||||
},
|
||||
cacheStats: {
|
||||
size: this._reactiveQueries.size,
|
||||
hitRate: this.queryStats.totalQueries > 0 ?
|
||||
(this.queryStats.cacheHits / this.queryStats.totalQueries * 100).toFixed(2) + '%' : '0%'
|
||||
hitRate:
|
||||
this._queryStats.totalQueries > 0
|
||||
? ((this._queryStats.cacheHits / this._queryStats.totalQueries) * 100).toFixed(2) + '%'
|
||||
: '0%'
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -915,7 +920,7 @@ export class QuerySystem {
|
||||
* @param entity 要查询的实体
|
||||
*/
|
||||
public getEntityArchetype(entity: Entity): Archetype | undefined {
|
||||
return this.archetypeSystem.getEntityArchetype(entity);
|
||||
return this._archetypeSystem.getEntityArchetype(entity);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -941,10 +946,7 @@ export class QuerySystem {
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 响应式查询实例
|
||||
*/
|
||||
private getOrCreateReactiveQuery(
|
||||
queryType: QueryConditionType,
|
||||
componentTypes: ComponentType[]
|
||||
): ReactiveQuery {
|
||||
private getOrCreateReactiveQuery(queryType: QueryConditionType, componentTypes: ComponentType[]): ReactiveQuery {
|
||||
// 生成缓存键(与传统缓存键格式一致)
|
||||
const cacheKey = this.generateCacheKey(queryType, componentTypes);
|
||||
|
||||
@@ -996,13 +998,10 @@ export class QuerySystem {
|
||||
* @param componentTypes 组件类型列表
|
||||
* @returns 匹配的实体列表
|
||||
*/
|
||||
private executeTraditionalQuery(
|
||||
queryType: QueryConditionType,
|
||||
componentTypes: ComponentType[]
|
||||
): Entity[] {
|
||||
private executeTraditionalQuery(queryType: QueryConditionType, componentTypes: ComponentType[]): Entity[] {
|
||||
switch (queryType) {
|
||||
case QueryConditionType.ALL: {
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
|
||||
const archetypeResult = this._archetypeSystem.queryArchetypes(componentTypes, 'AND');
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
@@ -1012,7 +1011,7 @@ export class QuerySystem {
|
||||
return entities;
|
||||
}
|
||||
case QueryConditionType.ANY: {
|
||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
|
||||
const archetypeResult = this._archetypeSystem.queryArchetypes(componentTypes, 'OR');
|
||||
const entities: Entity[] = [];
|
||||
for (const archetype of archetypeResult.archetypes) {
|
||||
for (const entity of archetype.entities) {
|
||||
@@ -1023,9 +1022,7 @@ export class QuerySystem {
|
||||
}
|
||||
case QueryConditionType.NONE: {
|
||||
const mask = this.createComponentMask(componentTypes);
|
||||
return this.entities.filter(entity =>
|
||||
BitMask64Utils.hasNone(entity.componentMask, mask)
|
||||
);
|
||||
return this._entities.filter((entity) => BitMask64Utils.hasNone(entity.componentMask, mask));
|
||||
}
|
||||
default:
|
||||
return [];
|
||||
@@ -1241,7 +1238,7 @@ export class QueryBuilder {
|
||||
* 创建组件掩码
|
||||
*/
|
||||
private createComponentMask(componentTypes: ComponentType[]): BitMask64Data {
|
||||
let mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const mask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const type of componentTypes) {
|
||||
try {
|
||||
const bitMask = ComponentRegistry.getBitMask(type);
|
||||
|
||||
@@ -136,7 +136,7 @@ export class ReactiveQuery {
|
||||
private generateQueryId(): string {
|
||||
const typeStr = this._condition.type;
|
||||
const componentsStr = this._condition.componentTypes
|
||||
.map(t => t.name)
|
||||
.map((t) => t.name)
|
||||
.sort()
|
||||
.join(',');
|
||||
return `${typeStr}:${componentsStr}`;
|
||||
|
||||
@@ -578,7 +578,7 @@ export class SoAStorage<T extends Component> {
|
||||
}
|
||||
|
||||
if (obj instanceof Array) {
|
||||
return obj.map(item => this.deepClone(item));
|
||||
return obj.map((item) => this.deepClone(item));
|
||||
}
|
||||
|
||||
if (obj instanceof Map) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type {Component} from '../Component';
|
||||
import type {EntitySystem} from '../Systems';
|
||||
import {ComponentType} from "../../Types";
|
||||
import type { Component } from '../Component';
|
||||
import type { EntitySystem } from '../Systems';
|
||||
import { ComponentType } from '../../Types';
|
||||
|
||||
/**
|
||||
* 存储组件类型名称的Symbol键
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Component } from './Component';
|
||||
import { ComponentRegistry, ComponentType } from './Core/ComponentStorage';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { BitMask64Utils, BitMask64Data } from './Utils/BigIntCompatibility';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
import { getComponentInstanceTypeName, getComponentTypeName } from './Decorators';
|
||||
import type { IScene } from './IScene';
|
||||
|
||||
/**
|
||||
* 组件活跃状态变化接口
|
||||
*/
|
||||
interface IActiveChangeable {
|
||||
onActiveChanged(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体比较器
|
||||
*
|
||||
@@ -21,14 +27,11 @@ export class EntityComparer {
|
||||
*/
|
||||
public compare(self: Entity, other: Entity): number {
|
||||
let compare = self.updateOrder - other.updateOrder;
|
||||
if (compare == 0)
|
||||
compare = self.id - other.id;
|
||||
if (compare == 0) compare = self.id - other.id;
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 游戏实体类
|
||||
*
|
||||
@@ -66,25 +69,6 @@ export class Entity {
|
||||
*/
|
||||
public static entityComparer: EntityComparer = new EntityComparer();
|
||||
|
||||
/**
|
||||
* 全局事件总线实例
|
||||
* 用于发射组件相关事件
|
||||
*/
|
||||
public static eventBus: EventBus | null = null;
|
||||
|
||||
/**
|
||||
* 通知Scene中的QuerySystem实体组件发生变动
|
||||
*
|
||||
* @param entity 发生组件变动的实体
|
||||
*/
|
||||
private static notifyQuerySystems(entity: Entity): void {
|
||||
// 只通知Scene中的QuerySystem
|
||||
if (entity.scene && entity.scene.querySystem) {
|
||||
entity.scene.querySystem.updateEntity(entity);
|
||||
entity.scene.clearSystemEntityCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体名称
|
||||
*/
|
||||
@@ -103,7 +87,7 @@ export class Entity {
|
||||
/**
|
||||
* 销毁状态标志
|
||||
*/
|
||||
public _isDestroyed: boolean = false;
|
||||
private _isDestroyed: boolean = false;
|
||||
|
||||
/**
|
||||
* 父实体引用
|
||||
@@ -164,6 +148,18 @@ export class Entity {
|
||||
return this._isDestroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置销毁状态(内部使用)
|
||||
*
|
||||
* 此方法供Scene和批量操作使用,以提高性能。
|
||||
* 不应在普通业务逻辑中调用,应使用destroy()方法。
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public setDestroyedState(destroyed: boolean): void {
|
||||
this._isDestroyed = destroyed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件数组(懒加载)
|
||||
* @returns 只读的组件数组
|
||||
@@ -193,10 +189,7 @@ export class Entity {
|
||||
if (BitMask64Utils.getBit(mask, bitIndex)) {
|
||||
const componentType = ComponentRegistry.getTypeByBitIndex(bitIndex);
|
||||
if (componentType) {
|
||||
const component = this.scene.componentStorageManager.getComponent(
|
||||
this.id,
|
||||
componentType
|
||||
);
|
||||
const component = this.scene.componentStorageManager.getComponent(this.id, componentType);
|
||||
|
||||
if (component) {
|
||||
components.push(component);
|
||||
@@ -348,8 +341,9 @@ export class Entity {
|
||||
*/
|
||||
public createComponent<T extends Component>(
|
||||
componentType: ComponentType<T>,
|
||||
...args: any[]
|
||||
...args: ConstructorParameters<ComponentType<T>>
|
||||
): T {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
const component = new componentType(...args);
|
||||
return this.addComponent(component);
|
||||
}
|
||||
@@ -377,6 +371,16 @@ export class Entity {
|
||||
return component;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知Scene中的QuerySystem实体组件发生变动
|
||||
*/
|
||||
private notifyQuerySystems(): void {
|
||||
if (this.scene && this.scene.querySystem) {
|
||||
this.scene.querySystem.updateEntity(this);
|
||||
this.scene.clearSystemEntityCaches();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加组件到实体
|
||||
*
|
||||
@@ -394,11 +398,13 @@ export class Entity {
|
||||
const componentType = component.constructor as ComponentType<T>;
|
||||
|
||||
if (!this.scene) {
|
||||
throw new Error(`Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()`);
|
||||
throw new Error(
|
||||
'Entity must be added to Scene before adding components. Use scene.createEntity() instead of new Entity()'
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.scene.componentStorageManager) {
|
||||
throw new Error(`Scene does not have componentStorageManager`);
|
||||
throw new Error('Scene does not have componentStorageManager');
|
||||
}
|
||||
|
||||
if (this.hasComponent(componentType)) {
|
||||
@@ -415,8 +421,8 @@ export class Entity {
|
||||
}
|
||||
component.onAddedToEntity();
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentAdded({
|
||||
if (this.scene && this.scene.eventSystem) {
|
||||
this.scene.eventSystem.emitSync('component:added', {
|
||||
timestamp: Date.now(),
|
||||
source: 'Entity',
|
||||
entityId: this.id,
|
||||
@@ -427,9 +433,7 @@ export class Entity {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 通知所有相关的QuerySystem组件已变动
|
||||
Entity.notifyQuerySystems(this);
|
||||
this.notifyQuerySystems();
|
||||
|
||||
return component;
|
||||
}
|
||||
@@ -464,9 +468,6 @@ export class Entity {
|
||||
return component as T | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 检查实体是否拥有指定类型的组件
|
||||
*
|
||||
@@ -508,10 +509,11 @@ export class Entity {
|
||||
*/
|
||||
public getOrCreateComponent<T extends Component>(
|
||||
type: ComponentType<T>,
|
||||
...args: any[]
|
||||
...args: ConstructorParameters<ComponentType<T>>
|
||||
): T {
|
||||
let component = this.getComponent(type);
|
||||
if (!component) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
component = this.createComponent(type, ...args);
|
||||
}
|
||||
return component;
|
||||
@@ -552,8 +554,8 @@ export class Entity {
|
||||
|
||||
component.entityId = null;
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.emitComponentRemoved({
|
||||
if (this.scene && this.scene.eventSystem) {
|
||||
this.scene.eventSystem.emitSync('component:removed', {
|
||||
timestamp: Date.now(),
|
||||
source: 'Entity',
|
||||
entityId: this.id,
|
||||
@@ -564,8 +566,7 @@ export class Entity {
|
||||
});
|
||||
}
|
||||
|
||||
// 通知所有相关的QuerySystem组件已变动
|
||||
Entity.notifyQuerySystems(this);
|
||||
this.notifyQuerySystems();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,8 +606,7 @@ export class Entity {
|
||||
component.onRemovedFromEntity();
|
||||
}
|
||||
|
||||
// 通知所有相关的QuerySystem组件已全部移除
|
||||
Entity.notifyQuerySystems(this);
|
||||
this.notifyQuerySystems();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -645,8 +645,6 @@ export class Entity {
|
||||
return removedComponents;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有指定类型的组件
|
||||
*
|
||||
@@ -700,7 +698,7 @@ export class Entity {
|
||||
*/
|
||||
public addChild(child: Entity): Entity {
|
||||
if (child === this) {
|
||||
throw new Error("Entity cannot be its own child");
|
||||
throw new Error('Entity cannot be its own child');
|
||||
}
|
||||
|
||||
if (child._parent === this) {
|
||||
@@ -808,11 +806,10 @@ export class Entity {
|
||||
* @returns 层次结构的根实体
|
||||
*/
|
||||
public getRoot(): Entity {
|
||||
let root: Entity = this;
|
||||
while (root._parent) {
|
||||
root = root._parent;
|
||||
if (!this._parent) {
|
||||
return this;
|
||||
}
|
||||
return root;
|
||||
return this._parent.getRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -878,7 +875,7 @@ export class Entity {
|
||||
private onActiveChanged(): void {
|
||||
for (const component of this.components) {
|
||||
if ('onActiveChanged' in component && typeof component.onActiveChanged === 'function') {
|
||||
(component as any).onActiveChanged();
|
||||
(component as IActiveChangeable).onActiveChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -891,7 +888,6 @@ export class Entity {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 销毁实体
|
||||
*
|
||||
@@ -949,7 +945,7 @@ export class Entity {
|
||||
collectChildren(this);
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
entity._isDestroyed = true;
|
||||
entity.setDestroyedState(true);
|
||||
}
|
||||
|
||||
for (const entity of toDestroy) {
|
||||
@@ -1016,11 +1012,11 @@ export class Entity {
|
||||
activeInHierarchy: this.activeInHierarchy,
|
||||
destroyed: this._isDestroyed,
|
||||
componentCount: this.components.length,
|
||||
componentTypes: this.components.map(c => getComponentInstanceTypeName(c)),
|
||||
componentTypes: this.components.map((c) => getComponentInstanceTypeName(c)),
|
||||
componentMask: BitMask64Utils.toString(this._componentMask, 2), // 二进制表示
|
||||
parentId: this._parent?.id || null,
|
||||
childCount: this._children.length,
|
||||
childIds: this._children.map(c => c.id),
|
||||
childIds: this._children.map((c) => c.id),
|
||||
depth: this.getDepth(),
|
||||
cacheBuilt: this._componentCache !== null
|
||||
};
|
||||
|
||||
@@ -2,19 +2,26 @@ import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStorage';
|
||||
import { ComponentStorageManager, ComponentRegistry, ComponentType } from './Core/ComponentStorage';
|
||||
import { QuerySystem } from './Core/QuerySystem';
|
||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||
import { EventBus } from './Core/EventBus';
|
||||
import { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
import { IScene, ISceneConfig } from './IScene';
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators";
|
||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from './Decorators';
|
||||
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||
import { SceneSerializer, SceneSerializationOptions, SceneDeserializationOptions } from './Serialization/SceneSerializer';
|
||||
import { IncrementalSerializer, IncrementalSnapshot, IncrementalSerializationOptions } from './Serialization/IncrementalSerializer';
|
||||
import {
|
||||
SceneSerializer,
|
||||
SceneSerializationOptions,
|
||||
SceneDeserializationOptions
|
||||
} from './Serialization/SceneSerializer';
|
||||
import {
|
||||
IncrementalSerializer,
|
||||
IncrementalSnapshot,
|
||||
IncrementalSerializationOptions
|
||||
} from './Serialization/IncrementalSerializer';
|
||||
import { ComponentPoolManager } from './Core/ComponentPool';
|
||||
import { PerformanceMonitor } from '../Utils/PerformanceMonitor';
|
||||
import { ServiceContainer, type ServiceType } from '../Core/ServiceContainer';
|
||||
import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer';
|
||||
import { createInstance, isInjectable, injectProperties } from '../Core/DI';
|
||||
import { createLogger } from '../Utils/Logger';
|
||||
|
||||
@@ -30,13 +37,14 @@ export class Scene implements IScene {
|
||||
*
|
||||
* 用于标识和调试的友好名称。
|
||||
*/
|
||||
public name: string = "";
|
||||
public name: string = '';
|
||||
|
||||
/**
|
||||
* 场景自定义数据
|
||||
*
|
||||
* 用于存储场景级别的配置和状态数据。
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
public readonly sceneData: Map<string, any> = new Map();
|
||||
|
||||
/**
|
||||
@@ -46,7 +54,6 @@ export class Scene implements IScene {
|
||||
*/
|
||||
public readonly entities: EntityList;
|
||||
|
||||
|
||||
/**
|
||||
* 实体ID池
|
||||
*
|
||||
@@ -119,6 +126,20 @@ export class Scene implements IScene {
|
||||
*/
|
||||
private _systemsOrderDirty: boolean = true;
|
||||
|
||||
/**
|
||||
* 系统错误计数器
|
||||
*
|
||||
* 跟踪每个系统的错误次数,用于自动禁用频繁出错的系统
|
||||
*/
|
||||
private _systemErrorCount: Map<EntitySystem, number> = new Map();
|
||||
|
||||
/**
|
||||
* 最大允许错误次数
|
||||
*
|
||||
* 系统错误次数超过此阈值后将被自动禁用
|
||||
*/
|
||||
private _maxErrorCount: number = 10;
|
||||
|
||||
/**
|
||||
* 获取场景中所有已注册的EntitySystem
|
||||
*
|
||||
@@ -131,24 +152,35 @@ export class Scene implements IScene {
|
||||
return this._cachedSystems;
|
||||
}
|
||||
|
||||
// 重新构建系统列表
|
||||
const services = this._services.getAll();
|
||||
const systems: EntitySystem[] = [];
|
||||
|
||||
for (const service of services) {
|
||||
if (service instanceof EntitySystem) {
|
||||
systems.push(service);
|
||||
}
|
||||
}
|
||||
|
||||
// 按updateOrder排序
|
||||
systems.sort((a, b) => a.updateOrder - b.updateOrder);
|
||||
|
||||
// 缓存结果
|
||||
this._cachedSystems = systems;
|
||||
this._cachedSystems = this._rebuildSystemsCache();
|
||||
this._systemsOrderDirty = false;
|
||||
|
||||
return systems;
|
||||
return this._cachedSystems;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新构建系统缓存
|
||||
*
|
||||
* 从服务容器中提取所有EntitySystem并排序
|
||||
*/
|
||||
private _rebuildSystemsCache(): EntitySystem[] {
|
||||
const allServices = this._services.getAll();
|
||||
const systems = this._filterEntitySystems(allServices);
|
||||
return this._sortSystemsByUpdateOrder(systems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务列表中过滤出EntitySystem实例
|
||||
*/
|
||||
private _filterEntitySystems(services: IService[]): EntitySystem[] {
|
||||
return services.filter((service): service is EntitySystem => service instanceof EntitySystem);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按updateOrder排序系统
|
||||
*/
|
||||
private _sortSystemsByUpdateOrder(systems: EntitySystem[]): EntitySystem[] {
|
||||
return systems.sort((a, b) => a.updateOrder - b.updateOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,16 +244,6 @@ export class Scene implements IScene {
|
||||
if (config?.name) {
|
||||
this.name = config.name;
|
||||
}
|
||||
|
||||
if (!Entity.eventBus) {
|
||||
Entity.eventBus = new EventBus(false);
|
||||
}
|
||||
|
||||
if (Entity.eventBus) {
|
||||
Entity.eventBus.onComponentAdded((data: unknown) => {
|
||||
this.eventSystem.emitSync('component:added', data);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,8 +253,7 @@ export class Scene implements IScene {
|
||||
*/
|
||||
private get performanceMonitor(): PerformanceMonitor {
|
||||
if (!this._performanceMonitor) {
|
||||
this._performanceMonitor = this._services.tryResolve(PerformanceMonitor)
|
||||
?? new PerformanceMonitor();
|
||||
this._performanceMonitor = this._services.tryResolve(PerformanceMonitor) ?? new PerformanceMonitor();
|
||||
}
|
||||
return this._performanceMonitor;
|
||||
}
|
||||
@@ -242,24 +263,21 @@ export class Scene implements IScene {
|
||||
*
|
||||
* 在场景创建时调用,子类可以重写此方法来设置初始实体和组件。
|
||||
*/
|
||||
public initialize(): void {
|
||||
}
|
||||
public initialize(): void {}
|
||||
|
||||
/**
|
||||
* 场景开始运行时的回调
|
||||
*
|
||||
* 在场景开始运行时调用,可以在此方法中执行场景启动逻辑。
|
||||
*/
|
||||
public onStart(): void {
|
||||
}
|
||||
public onStart(): void {}
|
||||
|
||||
/**
|
||||
* 场景卸载时的回调
|
||||
*
|
||||
* 在场景被销毁时调用,可以在此方法中执行清理工作。
|
||||
*/
|
||||
public unload(): void {
|
||||
}
|
||||
public unload(): void {}
|
||||
|
||||
/**
|
||||
* 开始场景,启动实体处理器等
|
||||
@@ -309,36 +327,61 @@ export class Scene implements IScene {
|
||||
|
||||
this.entities.updateLists();
|
||||
|
||||
// 更新所有EntitySystem
|
||||
const systems = this.systems;
|
||||
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.update();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.update():`, error);
|
||||
this._handleSystemError(system, 'update', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LateUpdate
|
||||
for (const system of systems) {
|
||||
if (system.enabled) {
|
||||
try {
|
||||
system.lateUpdate();
|
||||
} catch (error) {
|
||||
this.logger.error(`Error in system ${system.constructor.name}.lateUpdate():`, error);
|
||||
this._handleSystemError(system, 'lateUpdate', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统执行错误
|
||||
*
|
||||
* 记录错误信息并跟踪错误次数。当系统错误次数超过阈值时自动禁用该系统。
|
||||
*
|
||||
* @param system 出错的系统
|
||||
* @param phase 错误发生的阶段(update 或 lateUpdate)
|
||||
* @param error 错误对象
|
||||
*/
|
||||
private _handleSystemError(system: EntitySystem, phase: 'update' | 'lateUpdate', error: unknown): void {
|
||||
const errorCount = (this._systemErrorCount.get(system) || 0) + 1;
|
||||
this._systemErrorCount.set(system, errorCount);
|
||||
|
||||
this.logger.error(
|
||||
`Error in system ${system.constructor.name}.${phase}() [${errorCount}/${this._maxErrorCount}]:`,
|
||||
error
|
||||
);
|
||||
|
||||
if (errorCount >= this._maxErrorCount) {
|
||||
system.enabled = false;
|
||||
this.logger.error(
|
||||
`System ${system.constructor.name} has been disabled due to excessive errors (${errorCount} errors)`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将实体添加到此场景,并返回它
|
||||
* @param name 实体名称
|
||||
*/
|
||||
public createEntity(name: string) {
|
||||
let entity = new Entity(name, this.identifierPool.checkOut());
|
||||
const entity = new Entity(name, this.identifierPool.checkOut());
|
||||
|
||||
this.eventSystem.emitSync('entity:created', { entityName: name, entity, scene: this });
|
||||
|
||||
@@ -384,7 +427,7 @@ export class Scene implements IScene {
|
||||
* @param namePrefix 实体名称前缀
|
||||
* @returns 创建的实体列表
|
||||
*/
|
||||
public createEntities(count: number, namePrefix: string = "Entity"): Entity[] {
|
||||
public createEntities(count: number, namePrefix: string = 'Entity'): Entity[] {
|
||||
const entities: Entity[] = [];
|
||||
|
||||
// 批量创建实体对象,不立即添加到系统
|
||||
@@ -408,7 +451,6 @@ export class Scene implements IScene {
|
||||
return entities;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 批量销毁实体
|
||||
*/
|
||||
@@ -416,7 +458,7 @@ export class Scene implements IScene {
|
||||
if (entities.length === 0) return;
|
||||
|
||||
for (const entity of entities) {
|
||||
entity._isDestroyed = true;
|
||||
entity.setDestroyedState(true);
|
||||
}
|
||||
|
||||
for (const entity of entities) {
|
||||
@@ -473,7 +515,9 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 根据名称查找实体(别名方法)
|
||||
*
|
||||
* @param name 实体名称
|
||||
* @deprecated 请使用 findEntity() 代替此方法
|
||||
*/
|
||||
public getEntityByName(name: string): Entity | null {
|
||||
return this.findEntity(name);
|
||||
@@ -481,7 +525,9 @@ export class Scene implements IScene {
|
||||
|
||||
/**
|
||||
* 根据标签查找实体(别名方法)
|
||||
*
|
||||
* @param tag 实体标签
|
||||
* @deprecated 请使用 findEntitiesByTag() 代替此方法
|
||||
*/
|
||||
public getEntitiesByTag(tag: number): Entity[] {
|
||||
return this.findEntitiesByTag(tag);
|
||||
@@ -502,7 +548,7 @@ export class Scene implements IScene {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
public queryAll(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
public queryAll(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryAll(...componentTypes);
|
||||
}
|
||||
|
||||
@@ -512,7 +558,7 @@ export class Scene implements IScene {
|
||||
* @param componentTypes - 组件类型数组
|
||||
* @returns 查询结果
|
||||
*/
|
||||
public queryAny(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
public queryAny(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryAny(...componentTypes);
|
||||
}
|
||||
|
||||
@@ -522,7 +568,7 @@ export class Scene implements IScene {
|
||||
* @param componentTypes - 组件类型数组
|
||||
* @returns 查询结果
|
||||
*/
|
||||
public queryNone(...componentTypes: any[]): { entities: readonly Entity[] } {
|
||||
public queryNone(...componentTypes: ComponentType[]): { entities: readonly Entity[] } {
|
||||
return this.querySystem.queryNone(...componentTypes);
|
||||
}
|
||||
|
||||
@@ -577,11 +623,9 @@ export class Scene implements IScene {
|
||||
* scene.addEntityProcessor(system);
|
||||
* ```
|
||||
*/
|
||||
public addEntityProcessor<T extends EntitySystem>(
|
||||
systemTypeOrInstance: ServiceType<T> | T
|
||||
): T {
|
||||
public addEntityProcessor<T extends EntitySystem>(systemTypeOrInstance: ServiceType<T> | T): T {
|
||||
let system: T;
|
||||
let constructor: any;
|
||||
let constructor: ServiceType<T>;
|
||||
|
||||
if (typeof systemTypeOrInstance === 'function') {
|
||||
constructor = systemTypeOrInstance;
|
||||
@@ -595,11 +639,11 @@ export class Scene implements IScene {
|
||||
if (isInjectable(constructor)) {
|
||||
system = createInstance(constructor, this._services) as T;
|
||||
} else {
|
||||
system = new (constructor as any)() as T;
|
||||
system = new constructor() as T;
|
||||
}
|
||||
} else {
|
||||
system = systemTypeOrInstance;
|
||||
constructor = system.constructor;
|
||||
constructor = system.constructor as ServiceType<T>;
|
||||
|
||||
if (this._services.isRegistered(constructor)) {
|
||||
const existingSystem = this._services.resolve(constructor);
|
||||
@@ -609,7 +653,7 @@ export class Scene implements IScene {
|
||||
} else {
|
||||
this.logger.warn(
|
||||
`Attempting to register a different instance of ${constructor.name}, ` +
|
||||
`but type is already registered. Returning existing instance.`
|
||||
'but type is already registered. Returning existing instance.'
|
||||
);
|
||||
return existingSystem as T;
|
||||
}
|
||||
@@ -701,7 +745,7 @@ export class Scene implements IScene {
|
||||
* @param processor 要删除的处理器
|
||||
*/
|
||||
public removeEntityProcessor(processor: EntitySystem): void {
|
||||
const constructor = processor.constructor as any;
|
||||
const constructor = processor.constructor as ServiceType<EntitySystem>;
|
||||
|
||||
// 从ServiceContainer移除
|
||||
this._services.unregister(constructor);
|
||||
@@ -740,7 +784,7 @@ export class Scene implements IScene {
|
||||
* ```
|
||||
*/
|
||||
public getEntityProcessor<T extends EntitySystem>(type: new (...args: unknown[]) => T): T | null {
|
||||
return this._services.tryResolve(type as any) as T | null;
|
||||
return this._services.tryResolve(type as ServiceType<T>) as T | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -749,7 +793,7 @@ export class Scene implements IScene {
|
||||
public getStats(): {
|
||||
entityCount: number;
|
||||
processorCount: number;
|
||||
componentStorageStats: Map<string, any>;
|
||||
componentStorageStats: Map<string, { totalSlots: number; usedSlots: number; freeSlots: number; fragmentation: number }>;
|
||||
} {
|
||||
return {
|
||||
entityCount: this.entities.count,
|
||||
@@ -758,7 +802,6 @@ export class Scene implements IScene {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取场景的调试信息
|
||||
*/
|
||||
@@ -778,7 +821,7 @@ export class Scene implements IScene {
|
||||
updateOrder: number;
|
||||
entityCount: number;
|
||||
}>;
|
||||
componentStats: Map<string, any>;
|
||||
componentStats: Map<string, { totalSlots: number; usedSlots: number; freeSlots: number; fragmentation: number }>;
|
||||
} {
|
||||
const systems = this.systems;
|
||||
return {
|
||||
@@ -786,16 +829,16 @@ export class Scene implements IScene {
|
||||
entityCount: this.entities.count,
|
||||
processorCount: systems.length,
|
||||
isRunning: this._didSceneBegin,
|
||||
entities: this.entities.buffer.map(entity => ({
|
||||
entities: this.entities.buffer.map((entity) => ({
|
||||
name: entity.name,
|
||||
id: entity.id,
|
||||
componentCount: entity.components.length,
|
||||
componentTypes: entity.components.map(c => getComponentInstanceTypeName(c))
|
||||
componentTypes: entity.components.map((c) => getComponentInstanceTypeName(c))
|
||||
})),
|
||||
processors: systems.map(processor => ({
|
||||
processors: systems.map((processor) => ({
|
||||
name: getSystemInstanceTypeName(processor),
|
||||
updateOrder: processor.updateOrder,
|
||||
entityCount: (processor as any)._entities?.length || 0
|
||||
entityCount: processor.entities.length
|
||||
})),
|
||||
componentStats: this.componentStorageManager.getAllStats()
|
||||
};
|
||||
@@ -855,7 +898,7 @@ export class Scene implements IScene {
|
||||
// ==================== 增量序列化 API ====================
|
||||
|
||||
/** 增量序列化的基础快照 */
|
||||
private _incrementalBaseSnapshot?: any;
|
||||
private _incrementalBaseSnapshot?: unknown;
|
||||
|
||||
/**
|
||||
* 创建增量序列化的基础快照
|
||||
@@ -910,11 +953,7 @@ export class Scene implements IScene {
|
||||
throw new Error('必须先调用 createIncrementalSnapshot() 创建基础快照');
|
||||
}
|
||||
|
||||
return IncrementalSerializer.computeIncremental(
|
||||
this,
|
||||
this._incrementalBaseSnapshot,
|
||||
options
|
||||
);
|
||||
return IncrementalSerializer.computeIncremental(this, this._incrementalBaseSnapshot as Parameters<typeof IncrementalSerializer.computeIncremental>[1], options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -939,16 +978,15 @@ export class Scene implements IScene {
|
||||
*/
|
||||
public applyIncremental(
|
||||
incremental: IncrementalSnapshot | string | Uint8Array,
|
||||
componentRegistry?: Map<string, any>
|
||||
componentRegistry?: Map<string, ComponentType>
|
||||
): void {
|
||||
const isSerializedData = typeof incremental === 'string' ||
|
||||
incremental instanceof Uint8Array;
|
||||
const isSerializedData = typeof incremental === 'string' || incremental instanceof Uint8Array;
|
||||
|
||||
const snapshot = isSerializedData
|
||||
? IncrementalSerializer.deserializeIncremental(incremental as string | Uint8Array)
|
||||
: incremental as IncrementalSnapshot;
|
||||
: (incremental as IncrementalSnapshot);
|
||||
|
||||
const registry = componentRegistry || ComponentRegistry.getAllComponentNames() as Map<string, any>;
|
||||
const registry = componentRegistry || (ComponentRegistry.getAllComponentNames() as Map<string, ComponentType>);
|
||||
|
||||
IncrementalSerializer.applyIncremental(this, snapshot, registry);
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ export class ComponentSerializer {
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.serializeValue(item));
|
||||
return value.map((item) => this.serializeValue(item));
|
||||
}
|
||||
|
||||
// Map (如果没有使用@SerializeMap装饰器)
|
||||
@@ -276,7 +276,7 @@ export class ComponentSerializer {
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.deserializeValue(item));
|
||||
return value.map((item) => this.deserializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
@@ -340,10 +340,10 @@ export class ComponentSerializer {
|
||||
return {
|
||||
type: metadata.options.typeId || getComponentTypeName(componentType),
|
||||
version: metadata.options.version,
|
||||
fields: Array.from(metadata.fields.keys()).map(k =>
|
||||
fields: Array.from(metadata.fields.keys()).map((k) =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
ignoredFields: Array.from(metadata.ignoredFields).map(k =>
|
||||
ignoredFields: Array.from(metadata.ignoredFields).map((k) =>
|
||||
typeof k === 'symbol' ? k.toString() : k
|
||||
),
|
||||
isSerializable: true
|
||||
|
||||
@@ -696,22 +696,22 @@ export class IncrementalSerializer {
|
||||
componentChanges: incremental.componentChanges.length,
|
||||
sceneDataChanges: incremental.sceneDataChanges.length,
|
||||
addedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityAdded
|
||||
(c) => c.operation === ChangeOperation.EntityAdded
|
||||
).length,
|
||||
removedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityRemoved
|
||||
(c) => c.operation === ChangeOperation.EntityRemoved
|
||||
).length,
|
||||
updatedEntities: incremental.entityChanges.filter(
|
||||
c => c.operation === ChangeOperation.EntityUpdated
|
||||
(c) => c.operation === ChangeOperation.EntityUpdated
|
||||
).length,
|
||||
addedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentAdded
|
||||
(c) => c.operation === ChangeOperation.ComponentAdded
|
||||
).length,
|
||||
removedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentRemoved
|
||||
(c) => c.operation === ChangeOperation.ComponentRemoved
|
||||
).length,
|
||||
updatedComponents: incremental.componentChanges.filter(
|
||||
c => c.operation === ChangeOperation.ComponentUpdated
|
||||
(c) => c.operation === ChangeOperation.ComponentUpdated
|
||||
).length
|
||||
};
|
||||
}
|
||||
|
||||
@@ -363,7 +363,7 @@ export class SceneSerializer {
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.serializeValue(item));
|
||||
return value.map((item) => this.serializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
@@ -409,7 +409,7 @@ export class SceneSerializer {
|
||||
|
||||
// 数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.map(item => this.deserializeValue(item));
|
||||
return value.map((item) => this.deserializeValue(item));
|
||||
}
|
||||
|
||||
// 普通对象
|
||||
@@ -437,8 +437,8 @@ export class SceneSerializer {
|
||||
const componentTypeSet = new Set(options.components);
|
||||
|
||||
// 只返回拥有指定组件的实体
|
||||
return entities.filter(entity => {
|
||||
return Array.from(entity.components).some(component =>
|
||||
return entities.filter((entity) => {
|
||||
return Array.from(entity.components).some((component) =>
|
||||
componentTypeSet.has(component.constructor as ComponentType)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -127,7 +127,7 @@ export class VersionMigrationManager {
|
||||
return component;
|
||||
}
|
||||
|
||||
let migratedData = { ...component };
|
||||
const migratedData = { ...component };
|
||||
let version = currentVersion;
|
||||
|
||||
// 执行迁移链
|
||||
@@ -193,12 +193,12 @@ export class VersionMigrationManager {
|
||||
private static migrateSceneComponents(scene: SerializedScene): SerializedScene {
|
||||
const migratedScene = { ...scene };
|
||||
|
||||
migratedScene.entities = scene.entities.map(entity => ({
|
||||
migratedScene.entities = scene.entities.map((entity) => ({
|
||||
...entity,
|
||||
components: entity.components.map(component => {
|
||||
components: entity.components.map((component) => {
|
||||
// 查找组件的目标版本
|
||||
const typeInfo = scene.componentTypeRegistry.find(
|
||||
t => t.typeName === component.type
|
||||
(t) => t.typeName === component.type
|
||||
);
|
||||
|
||||
if (typeInfo && typeInfo.version !== component.version) {
|
||||
@@ -220,10 +220,10 @@ export class VersionMigrationManager {
|
||||
entities: any[],
|
||||
typeRegistry: Array<{ typeName: string; version: number }>
|
||||
): any[] {
|
||||
return entities.map(entity => ({
|
||||
return entities.map((entity) => ({
|
||||
...entity,
|
||||
components: entity.components.map((component: SerializedComponent) => {
|
||||
const typeInfo = typeRegistry.find(t => t.typeName === component.type);
|
||||
const typeInfo = typeRegistry.find((t) => t.typeName === component.type);
|
||||
|
||||
if (typeInfo && typeInfo.version !== component.version) {
|
||||
return this.migrateComponent(component, typeInfo.version);
|
||||
|
||||
166
packages/core/src/ECS/Systems/EntityCache.ts
Normal file
166
packages/core/src/ECS/Systems/EntityCache.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { Entity } from '../Entity';
|
||||
|
||||
/**
|
||||
* 实体缓存管理器
|
||||
*
|
||||
* 负责管理 EntitySystem 中的实体缓存,提供帧缓存和持久缓存两级缓存机制。
|
||||
* 使用面向对象设计,将数据和行为封装在类中。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cache = new EntityCache();
|
||||
* cache.setPersistent(entities);
|
||||
* const cached = cache.getPersistent();
|
||||
* cache.invalidate();
|
||||
* ```
|
||||
*/
|
||||
export class EntityCache {
|
||||
/**
|
||||
* 帧缓存
|
||||
*
|
||||
* 在update周期内使用,每帧结束后清理
|
||||
*/
|
||||
private _frameCache: readonly Entity[] | null = null;
|
||||
|
||||
/**
|
||||
* 持久缓存
|
||||
*
|
||||
* 跨帧使用,直到被显式失效
|
||||
*/
|
||||
private _persistentCache: readonly Entity[] | null = null;
|
||||
|
||||
/**
|
||||
* 被跟踪的实体集合
|
||||
*
|
||||
* 用于跟踪哪些实体正在被此系统处理
|
||||
*/
|
||||
private _trackedEntities: Set<Entity> = new Set();
|
||||
|
||||
/**
|
||||
* 获取帧缓存
|
||||
*/
|
||||
public getFrame(): readonly Entity[] | null {
|
||||
return this._frameCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置帧缓存
|
||||
*
|
||||
* @param entities 要缓存的实体列表
|
||||
*/
|
||||
public setFrame(entities: readonly Entity[]): void {
|
||||
this._frameCache = entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取持久缓存
|
||||
*/
|
||||
public getPersistent(): readonly Entity[] | null {
|
||||
return this._persistentCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置持久缓存
|
||||
*
|
||||
* @param entities 要缓存的实体列表
|
||||
*/
|
||||
public setPersistent(entities: readonly Entity[]): void {
|
||||
this._persistentCache = entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取被跟踪的实体集合
|
||||
*/
|
||||
public getTracked(): ReadonlySet<Entity> {
|
||||
return this._trackedEntities;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加被跟踪的实体
|
||||
*
|
||||
* @param entity 要跟踪的实体
|
||||
*/
|
||||
public addTracked(entity: Entity): void {
|
||||
this._trackedEntities.add(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除被跟踪的实体
|
||||
*
|
||||
* @param entity 要移除的实体
|
||||
*/
|
||||
public removeTracked(entity: Entity): void {
|
||||
this._trackedEntities.delete(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查实体是否被跟踪
|
||||
*
|
||||
* @param entity 要检查的实体
|
||||
*/
|
||||
public isTracked(entity: Entity): boolean {
|
||||
return this._trackedEntities.has(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使持久缓存失效
|
||||
*
|
||||
* 当实体变化时调用,强制下次查询时重新计算
|
||||
*/
|
||||
public invalidate(): void {
|
||||
this._persistentCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除帧缓存
|
||||
*
|
||||
* 在每帧结束时调用
|
||||
*/
|
||||
public clearFrame(): void {
|
||||
this._frameCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有缓存
|
||||
*
|
||||
* 在系统重置或销毁时调用
|
||||
*/
|
||||
public clearAll(): void {
|
||||
this._frameCache = null;
|
||||
this._persistentCache = null;
|
||||
this._trackedEntities.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有有效的持久缓存
|
||||
*/
|
||||
public hasPersistent(): boolean {
|
||||
return this._persistentCache !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有有效的帧缓存
|
||||
*/
|
||||
public hasFrame(): boolean {
|
||||
return this._frameCache !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存统计信息
|
||||
*/
|
||||
public getStats(): {
|
||||
hasFrame: boolean;
|
||||
hasPersistent: boolean;
|
||||
trackedCount: number;
|
||||
frameEntityCount: number;
|
||||
persistentEntityCount: number;
|
||||
} {
|
||||
return {
|
||||
hasFrame: this._frameCache !== null,
|
||||
hasPersistent: this._persistentCache !== null,
|
||||
trackedCount: this._trackedEntities.size,
|
||||
frameEntityCount: this._frameCache?.length ?? 0,
|
||||
persistentEntityCount: this._persistentCache?.length ?? 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,18 @@ import { createLogger } from '../../Utils/Logger';
|
||||
import type { EventListenerConfig, TypeSafeEventSystem, EventHandler } from '../Core/EventSystem';
|
||||
import type { ComponentConstructor, ComponentInstance } from '../../Types/TypeHelpers';
|
||||
import type { IService } from '../../Core/ServiceContainer';
|
||||
import { EntityCache } from './EntityCache';
|
||||
|
||||
/**
|
||||
* 事件监听器记录
|
||||
* 只存储引用信息,用于系统销毁时自动清理
|
||||
*/
|
||||
interface EventListenerRecord {
|
||||
eventSystem: TypeSafeEventSystem;
|
||||
eventType: string;
|
||||
handler: EventHandler;
|
||||
listenerRef: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 实体系统的基类
|
||||
*
|
||||
@@ -64,9 +64,7 @@ interface EventListenerRecord {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class EntitySystem<
|
||||
_TComponents extends readonly ComponentConstructor[] = []
|
||||
> implements ISystemBase, IService {
|
||||
export abstract class EntitySystem implements ISystemBase, IService {
|
||||
private _updateOrder: number;
|
||||
private _enabled: boolean;
|
||||
private _performanceMonitor: PerformanceMonitor | null;
|
||||
@@ -77,7 +75,6 @@ export abstract class EntitySystem<
|
||||
private _scene: Scene | null;
|
||||
protected logger: ReturnType<typeof createLogger>;
|
||||
|
||||
|
||||
/**
|
||||
* 实体ID映射缓存
|
||||
*/
|
||||
@@ -87,30 +84,24 @@ export abstract class EntitySystem<
|
||||
/**
|
||||
* 统一的实体缓存管理器
|
||||
*/
|
||||
private _entityCache: {
|
||||
frame: readonly Entity[] | null;
|
||||
persistent: readonly Entity[] | null;
|
||||
tracked: Set<Entity>;
|
||||
invalidate(): void;
|
||||
clearFrame(): void;
|
||||
clearAll(): void;
|
||||
};
|
||||
private _entityCache: EntityCache;
|
||||
|
||||
/**
|
||||
* 获取系统处理的实体列表
|
||||
*/
|
||||
public get entities(): readonly Entity[] {
|
||||
// 如果在update周期内,优先使用帧缓存
|
||||
if (this._entityCache.frame !== null) {
|
||||
return this._entityCache.frame;
|
||||
const frameCache = this._entityCache.getFrame();
|
||||
if (frameCache !== null) {
|
||||
return frameCache;
|
||||
}
|
||||
|
||||
// 否则使用持久缓存
|
||||
if (this._entityCache.persistent === null) {
|
||||
this._entityCache.persistent = this.queryEntities();
|
||||
if (!this._entityCache.hasPersistent()) {
|
||||
this._entityCache.setPersistent(this.queryEntities());
|
||||
}
|
||||
|
||||
return this._entityCache.persistent;
|
||||
return this._entityCache.getPersistent()!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,23 +152,7 @@ export abstract class EntitySystem<
|
||||
// 初始化logger
|
||||
this.logger = createLogger(this.getLoggerName());
|
||||
|
||||
|
||||
this._entityCache = {
|
||||
frame: null,
|
||||
persistent: null,
|
||||
tracked: new Set<Entity>(),
|
||||
invalidate() {
|
||||
this.persistent = null;
|
||||
},
|
||||
clearFrame() {
|
||||
this.frame = null;
|
||||
},
|
||||
clearAll() {
|
||||
this.frame = null;
|
||||
this.persistent = null;
|
||||
this.tracked.clear();
|
||||
}
|
||||
};
|
||||
this._entityCache = new EntityCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +178,9 @@ export abstract class EntitySystem<
|
||||
*/
|
||||
private getPerformanceMonitor(): PerformanceMonitor {
|
||||
if (!this._performanceMonitor) {
|
||||
throw new Error(`${this._systemName}: PerformanceMonitor未注入,请确保在Core.create()之后再添加System到Scene`);
|
||||
throw new Error(
|
||||
`${this._systemName}: PerformanceMonitor未注入,请确保在Core.create()之后再添加System到Scene`
|
||||
);
|
||||
}
|
||||
return this._performanceMonitor;
|
||||
}
|
||||
@@ -316,16 +293,28 @@ export abstract class EntitySystem<
|
||||
|
||||
/**
|
||||
* 检查是否为单一条件查询
|
||||
*
|
||||
* 使用位运算优化多条件检测。将每种查询条件映射到不同的位:
|
||||
* - all: 第0位 (1)
|
||||
* - any: 第1位 (2)
|
||||
* - none: 第2位 (4)
|
||||
* - tag: 第3位 (8)
|
||||
* - name: 第4位 (16)
|
||||
* - component: 第5位 (32)
|
||||
*/
|
||||
private isSingleCondition(condition: QueryCondition): boolean {
|
||||
// 使用位OR运算合并所有条件标记
|
||||
const flags =
|
||||
((condition.all.length > 0) ? 1 : 0) |
|
||||
((condition.any.length > 0) ? 2 : 0) |
|
||||
((condition.none.length > 0) ? 4 : 0) |
|
||||
((condition.tag !== undefined) ? 8 : 0) |
|
||||
((condition.name !== undefined) ? 16 : 0) |
|
||||
((condition.component !== undefined) ? 32 : 0);
|
||||
(condition.all.length > 0 ? 1 : 0) |
|
||||
(condition.any.length > 0 ? 2 : 0) |
|
||||
(condition.none.length > 0 ? 4 : 0) |
|
||||
(condition.tag !== undefined ? 8 : 0) |
|
||||
(condition.name !== undefined ? 16 : 0) |
|
||||
(condition.component !== undefined ? 32 : 0);
|
||||
|
||||
// 位运算技巧:如果只有一个位被设置,则 flags & (flags - 1) == 0
|
||||
// 例如:flags=4 (100), flags-1=3 (011), 4&3=0
|
||||
// 但如果 flags=6 (110), flags-1=5 (101), 6&5=4≠0
|
||||
return flags !== 0 && (flags & (flags - 1)) === 0;
|
||||
}
|
||||
|
||||
@@ -472,8 +461,7 @@ export abstract class EntitySystem<
|
||||
*/
|
||||
private getEntityIdMap(allEntities: readonly Entity[]): Map<number, Entity> {
|
||||
const currentVersion = this.scene?.querySystem?.version ?? 0;
|
||||
if (this._entityIdMap !== null &&
|
||||
this._entityIdMapVersion === currentVersion) {
|
||||
if (this._entityIdMap !== null && this._entityIdMapVersion === currentVersion) {
|
||||
return this._entityIdMap;
|
||||
}
|
||||
|
||||
@@ -511,7 +499,7 @@ export abstract class EntitySystem<
|
||||
const entityMap = this.getEntityIdMap(allEntities);
|
||||
|
||||
const size = idSet.size;
|
||||
const result = new Array(size);
|
||||
const result = new Array<Entity>(size);
|
||||
let index = 0;
|
||||
|
||||
for (const id of idSet) {
|
||||
@@ -554,10 +542,11 @@ export abstract class EntitySystem<
|
||||
this.onBegin();
|
||||
// 查询实体并存储到帧缓存中
|
||||
// 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate
|
||||
this._entityCache.frame = this.queryEntities();
|
||||
entityCount = this._entityCache.frame.length;
|
||||
const queriedEntities = this.queryEntities();
|
||||
this._entityCache.setFrame(queriedEntities);
|
||||
entityCount = queriedEntities.length;
|
||||
|
||||
this.process(this._entityCache.frame);
|
||||
this.process(queriedEntities);
|
||||
} finally {
|
||||
monitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
}
|
||||
@@ -577,7 +566,7 @@ export abstract class EntitySystem<
|
||||
|
||||
try {
|
||||
// 使用缓存的实体列表,避免重复查询
|
||||
const entities = this._entityCache.frame || [];
|
||||
const entities = this._entityCache.getFrame() || [];
|
||||
entityCount = entities.length;
|
||||
this.lateProcess(entities);
|
||||
this.onEnd();
|
||||
@@ -687,17 +676,17 @@ export abstract class EntitySystem<
|
||||
|
||||
// 检查新增的实体
|
||||
for (const entity of currentEntities) {
|
||||
if (!this._entityCache.tracked.has(entity)) {
|
||||
this._entityCache.tracked.add(entity);
|
||||
if (!this._entityCache.isTracked(entity)) {
|
||||
this._entityCache.addTracked(entity);
|
||||
this.onAdded(entity);
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查移除的实体
|
||||
for (const entity of this._entityCache.tracked) {
|
||||
for (const entity of this._entityCache.getTracked()) {
|
||||
if (!currentSet.has(entity)) {
|
||||
this._entityCache.tracked.delete(entity);
|
||||
this._entityCache.removeTracked(entity);
|
||||
this.onRemoved(entity);
|
||||
hasChanged = true;
|
||||
}
|
||||
@@ -768,15 +757,16 @@ export abstract class EntitySystem<
|
||||
* @param eventType 事件类型
|
||||
* @param handler 事件处理函数
|
||||
* @param config 监听器配置
|
||||
* @returns 监听器引用ID,可用于手动移除监听器
|
||||
*/
|
||||
protected addEventListener<T = any>(
|
||||
protected addEventListener<T>(
|
||||
eventType: string,
|
||||
handler: EventHandler<T>,
|
||||
config?: EventListenerConfig
|
||||
): void {
|
||||
): string | null {
|
||||
if (!this.scene?.eventSystem) {
|
||||
this.logger.warn(`${this.systemName}: 无法添加事件监听器,scene.eventSystem 不可用`);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const listenerRef = this.scene.eventSystem.on(eventType, handler, config);
|
||||
@@ -786,24 +776,22 @@ export abstract class EntitySystem<
|
||||
this._eventListeners.push({
|
||||
eventSystem: this.scene.eventSystem,
|
||||
eventType,
|
||||
handler,
|
||||
listenerRef
|
||||
});
|
||||
}
|
||||
|
||||
return listenerRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除特定的事件监听器
|
||||
*
|
||||
* @param eventType 事件类型
|
||||
* @param handler 事件处理函数
|
||||
* @param listenerRef 监听器引用ID(由 addEventListener 返回)
|
||||
*/
|
||||
protected removeEventListener<T = any>(
|
||||
eventType: string,
|
||||
handler: EventHandler<T>
|
||||
): void {
|
||||
protected removeEventListener(eventType: string, listenerRef: string): void {
|
||||
const listenerIndex = this._eventListeners.findIndex(
|
||||
listener => listener.eventType === eventType && listener.handler === handler
|
||||
(listener) => listener.eventType === eventType && listener.listenerRef === listenerRef
|
||||
);
|
||||
|
||||
if (listenerIndex >= 0) {
|
||||
@@ -887,15 +875,10 @@ export abstract class EntitySystem<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected requireComponent<T extends ComponentConstructor>(
|
||||
entity: Entity,
|
||||
componentType: T
|
||||
): ComponentInstance<T> {
|
||||
const component = entity.getComponent(componentType as any);
|
||||
protected requireComponent<T extends ComponentConstructor>(entity: Entity, componentType: T): ComponentInstance<T> {
|
||||
const component = entity.getComponent(componentType);
|
||||
if (!component) {
|
||||
throw new Error(
|
||||
`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`
|
||||
);
|
||||
throw new Error(`Component ${componentType.name} not found on entity ${entity.name} in ${this.systemName}`);
|
||||
}
|
||||
return component as ComponentInstance<T>;
|
||||
}
|
||||
@@ -927,9 +910,9 @@ export abstract class EntitySystem<
|
||||
entity: Entity,
|
||||
...components: T
|
||||
): { [K in keyof T]: ComponentInstance<T[K]> } {
|
||||
return components.map((type) =>
|
||||
this.requireComponent(entity, type)
|
||||
) as any;
|
||||
return components.map((type) => this.requireComponent(entity, type)) as {
|
||||
[K in keyof T]: ComponentInstance<T[K]>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -950,10 +933,7 @@ export abstract class EntitySystem<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected forEach(
|
||||
entities: readonly Entity[],
|
||||
processor: (entity: Entity, index: number) => void
|
||||
): void {
|
||||
protected forEach(entities: readonly Entity[], processor: (entity: Entity, index: number) => void): void {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
processor(entities[i]!, i);
|
||||
}
|
||||
@@ -1000,10 +980,7 @@ export abstract class EntitySystem<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected mapEntities<R>(
|
||||
entities: readonly Entity[],
|
||||
mapper: (entity: Entity, index: number) => R
|
||||
): R[] {
|
||||
protected mapEntities<R>(entities: readonly Entity[], mapper: (entity: Entity, index: number) => R): R[] {
|
||||
return Array.from(entities).map(mapper);
|
||||
}
|
||||
|
||||
@@ -1052,10 +1029,7 @@ export abstract class EntitySystem<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected someEntity(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): boolean {
|
||||
protected someEntity(entities: readonly Entity[], predicate: (entity: Entity, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
if (predicate(entities[i]!, i)) {
|
||||
return true;
|
||||
@@ -1081,10 +1055,7 @@ export abstract class EntitySystem<
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
protected everyEntity(
|
||||
entities: readonly Entity[],
|
||||
predicate: (entity: Entity, index: number) => boolean
|
||||
): boolean {
|
||||
protected everyEntity(entities: readonly Entity[], predicate: (entity: Entity, index: number) => boolean): boolean {
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
if (!predicate(entities[i]!, i)) {
|
||||
return false;
|
||||
|
||||
@@ -414,7 +414,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
${sharedProcessFunctionBody}
|
||||
};
|
||||
userProcessFunction(sharedFloatArray, startIndex, endIndex, deltaTime, systemConfig);
|
||||
` : ``}
|
||||
` : ''}
|
||||
}
|
||||
`;
|
||||
}
|
||||
@@ -494,7 +494,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
// 3. Worker执行阶段
|
||||
const promises = batches.map(batch =>
|
||||
const promises = batches.map((batch) =>
|
||||
this.workerPool!.execute({
|
||||
entities: batch,
|
||||
deltaTime,
|
||||
@@ -525,7 +525,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
*/
|
||||
private processSynchronously(entities: readonly Entity[]): void {
|
||||
// 1. 数据提取阶段
|
||||
const entityData = entities.map(entity => this.extractEntityData(entity));
|
||||
const entityData = entities.map((entity) => this.extractEntityData(entity));
|
||||
|
||||
// 2. 主线程处理阶段
|
||||
const deltaTime = Time.deltaTime;
|
||||
@@ -534,7 +534,7 @@ export abstract class WorkerEntitySystem<TEntityData = any> extends EntitySystem
|
||||
// 3. 结果应用阶段
|
||||
// 处理Promise返回值
|
||||
if (results && typeof (results as any).then === 'function') {
|
||||
(results as Promise<TEntityData[]>).then(finalResults => {
|
||||
(results as Promise<TEntityData[]>).then((finalResults) => {
|
||||
entities.forEach((entity, index) => {
|
||||
this.applyResult(entity, finalResults[index]!);
|
||||
});
|
||||
|
||||
@@ -69,7 +69,7 @@ export class BitMask64Utils {
|
||||
return maskSegments.some((seg, index) => {
|
||||
const bitsSeg = bitsSegments[index];
|
||||
return bitsSeg && ((seg[SegmentPart.LOW] & bitsSeg[SegmentPart.LOW]) !== 0 || (seg[SegmentPart.HIGH] & bitsSeg[SegmentPart.HIGH]) !== 0);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,7 +145,7 @@ export class BitMask64Utils {
|
||||
return baseIsZero;
|
||||
}
|
||||
// 额外检查扩展区域是否都为0
|
||||
return mask.segments.every(seg => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0);
|
||||
return mask.segments.every((seg) => seg[SegmentPart.LOW] === 0 && seg[SegmentPart.HIGH] === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,7 +155,7 @@ export class BitMask64Utils {
|
||||
* @returns 如果两个掩码完全相等则返回true
|
||||
*/
|
||||
public static equals(a: BitMask64Data, b: BitMask64Data): boolean {
|
||||
let baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH];
|
||||
const baseEquals = a.base[SegmentPart.LOW] === b.base[SegmentPart.LOW] && a.base[SegmentPart.HIGH] === b.base[SegmentPart.HIGH];
|
||||
// base不相等,或ab都没有扩展区域位,直接返回base比较结果
|
||||
if(!baseEquals || (!a.segments && !b.segments)) return baseEquals;
|
||||
// 不能假设a,b的segments都存在或长度相同.
|
||||
@@ -355,7 +355,7 @@ export class BitMask64Utils {
|
||||
if(!source.segments || source.segments.length == 0) return;
|
||||
// 没有拓展段,则直接复制数组
|
||||
if(!target.segments){
|
||||
target.segments = source.segments.map(seg => [...seg]);
|
||||
target.segments = source.segments.map((seg) => [...seg]);
|
||||
return;
|
||||
}
|
||||
// source有扩展段,target扩展段不足,则补充长度
|
||||
@@ -382,7 +382,7 @@ export class BitMask64Utils {
|
||||
public static clone(mask: BitMask64Data): BitMask64Data {
|
||||
return {
|
||||
base: mask.base.slice() as BitMask64Segment,
|
||||
...(mask.segments && { segments: mask.segments.map(seg => [...seg] as BitMask64Segment) })
|
||||
...(mask.segments && { segments: mask.segments.map((seg) => [...seg] as BitMask64Segment) })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -414,8 +414,8 @@ export class BitMask64Utils {
|
||||
for (let i = -1; i < totalLength; i++) {
|
||||
let segResult = '';
|
||||
const bitMaskData = i == -1 ? mask.base : mask.segments![i]!;
|
||||
let hi = bitMaskData[SegmentPart.HIGH];
|
||||
let lo = bitMaskData[SegmentPart.LOW];
|
||||
const hi = bitMaskData[SegmentPart.HIGH];
|
||||
const lo = bitMaskData[SegmentPart.LOW];
|
||||
if(radix == 2){
|
||||
const hiBits = hi.toString(2).padStart(32, '0');
|
||||
const loBits = lo.toString(2).padStart(32, '0');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BitMask64Data } from "./BigIntCompatibility";
|
||||
import { BitMask64Data } from './BigIntCompatibility';
|
||||
|
||||
// FlatHashMapFast.ts
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ComponentSparseSet {
|
||||
this.removeEntity(entity);
|
||||
}
|
||||
|
||||
let componentMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const componentMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const entityComponents = new Set<ComponentType>();
|
||||
|
||||
// 分析实体组件并构建位掩码
|
||||
@@ -169,7 +169,7 @@ export class ComponentSparseSet {
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
let targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const componentType of componentTypes) {
|
||||
if (!ComponentRegistry.isRegistered(componentType)) {
|
||||
return new Set<Entity>(); // 未注册的组件类型,结果为空
|
||||
@@ -209,7 +209,7 @@ export class ComponentSparseSet {
|
||||
}
|
||||
|
||||
// 构建目标位掩码
|
||||
let targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
const targetMask = BitMask64Utils.clone(BitMask64Utils.ZERO);
|
||||
for (const componentType of componentTypes) {
|
||||
if (ComponentRegistry.isRegistered(componentType)) {
|
||||
const bitMask = ComponentRegistry.getBitMask(componentType);
|
||||
|
||||
@@ -117,8 +117,8 @@ export class IdentifierPool {
|
||||
if (this._nextAvailableIndex > IdentifierPool.MAX_INDEX) {
|
||||
throw new Error(
|
||||
`实体索引已达到框架设计限制 (${IdentifierPool.MAX_INDEX})。` +
|
||||
`这意味着您已经分配了超过65535个不同的实体索引。` +
|
||||
`这是16位索引设计的限制,考虑优化实体回收策略或升级到64位ID设计。`
|
||||
'这意味着您已经分配了超过65535个不同的实体索引。' +
|
||||
'这是16位索引设计的限制,考虑优化实体回收策略或升级到64位ID设计。'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export class IdentifierPool {
|
||||
|
||||
// 检查是否已经在待回收队列中
|
||||
const alreadyPending = this._pendingRecycle.some(
|
||||
item => item.index === index && item.generation === generation
|
||||
(item) => item.index === index && item.generation === generation
|
||||
);
|
||||
|
||||
if (alreadyPending) {
|
||||
|
||||
@@ -267,15 +267,15 @@ export class Matcher {
|
||||
const parts: string[] = [];
|
||||
|
||||
if (this.condition.all.length > 0) {
|
||||
parts.push(`all(${this.condition.all.map(t => getComponentTypeName(t)).join(', ')})`);
|
||||
parts.push(`all(${this.condition.all.map((t) => getComponentTypeName(t)).join(', ')})`);
|
||||
}
|
||||
|
||||
if (this.condition.any.length > 0) {
|
||||
parts.push(`any(${this.condition.any.map(t => getComponentTypeName(t)).join(', ')})`);
|
||||
parts.push(`any(${this.condition.any.map((t) => getComponentTypeName(t)).join(', ')})`);
|
||||
}
|
||||
|
||||
if (this.condition.none.length > 0) {
|
||||
parts.push(`none(${this.condition.none.map(t => getComponentTypeName(t)).join(', ')})`);
|
||||
parts.push(`none(${this.condition.none.map((t) => getComponentTypeName(t)).join(', ')})`);
|
||||
}
|
||||
|
||||
if (this.condition.tag !== undefined) {
|
||||
|
||||
@@ -408,7 +408,7 @@ export class World {
|
||||
globalSystemCount: this._globalSystems.length,
|
||||
createdAt: this._createdAt,
|
||||
config: { ...this._config },
|
||||
scenes: Array.from(this._scenes.keys()).map(sceneId => ({
|
||||
scenes: Array.from(this._scenes.keys()).map((sceneId) => ({
|
||||
id: sceneId,
|
||||
isActive: this._activeScenes.has(sceneId),
|
||||
name: this._scenes.get(sceneId)?.name || sceneId
|
||||
|
||||
@@ -444,7 +444,7 @@ export class WorldManager implements IService {
|
||||
|
||||
// 检查是否所有Scene都是空的
|
||||
const allScenes = world.getAllScenes();
|
||||
const hasEntities = allScenes.some(scene =>
|
||||
const hasEntities = allScenes.some((scene) =>
|
||||
scene.entities && scene.entities.count > 0
|
||||
);
|
||||
|
||||
|
||||
@@ -208,8 +208,8 @@ export class DebugPlugin implements IPlugin, IService {
|
||||
return {
|
||||
name: scene.name,
|
||||
entityCount: entities.length,
|
||||
systems: systems.map(sys => this.getSystemInfo(sys)),
|
||||
entities: entities.map(entity => this.getEntityInfo(entity))
|
||||
systems: systems.map((sys) => this.getSystemInfo(sys)),
|
||||
entities: entities.map((entity) => this.getEntityInfo(entity))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,7 +246,7 @@ export class DebugPlugin implements IPlugin, IService {
|
||||
enabled: entity.enabled,
|
||||
tag: entity.tag,
|
||||
componentCount: components.length,
|
||||
components: components.map(comp => this.getComponentInfo(comp))
|
||||
components: components.map((comp) => this.getComponentInfo(comp))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ export class DebugPlugin implements IPlugin, IService {
|
||||
|
||||
if (filter.hasComponent) {
|
||||
const hasComp = entity.components.some(
|
||||
c => c.constructor.name === filter.hasComponent
|
||||
(c) => c.constructor.name === filter.hasComponent
|
||||
);
|
||||
if (!hasComp) {
|
||||
continue;
|
||||
|
||||
@@ -142,8 +142,8 @@ export interface IEventListenerConfig {
|
||||
priority?: number;
|
||||
/** 是否异步执行 */
|
||||
async?: boolean;
|
||||
/** 执行上下文 */
|
||||
context?: unknown;
|
||||
/** 事件处理函数的 this 绑定对象 */
|
||||
thisArg?: object;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -47,8 +47,8 @@ export class ComponentDataCollector {
|
||||
});
|
||||
|
||||
// 获取池利用率信息
|
||||
let poolUtilizations = new Map<string, number>();
|
||||
let poolSizes = new Map<string, number>();
|
||||
const poolUtilizations = new Map<string, number>();
|
||||
const poolSizes = new Map<string, number>();
|
||||
|
||||
try {
|
||||
const poolManager = ComponentPoolManager.getInstance();
|
||||
|
||||
@@ -135,7 +135,7 @@ export class DebugManager implements IService, IUpdatable {
|
||||
* 格式化日志消息
|
||||
*/
|
||||
private formatLogMessage(args: unknown[]): string {
|
||||
return args.map(arg => {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === 'string') return arg;
|
||||
if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
|
||||
if (arg === null) return 'null';
|
||||
@@ -173,7 +173,7 @@ export class DebugManager implements IService, IUpdatable {
|
||||
seen.add(value);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.map(item => stringify(item, depth + 1));
|
||||
const result = value.map((item) => stringify(item, depth + 1));
|
||||
seen.delete(value);
|
||||
return result;
|
||||
}
|
||||
@@ -436,9 +436,6 @@ export class DebugManager implements IService, IUpdatable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 处理内存快照请求
|
||||
*/
|
||||
@@ -575,7 +572,6 @@ export class DebugManager implements IService, IUpdatable {
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 收集组件内存统计(仅用于内存快照)
|
||||
*/
|
||||
|
||||
@@ -52,7 +52,6 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取原始实体列表
|
||||
* @param scene 场景实例
|
||||
@@ -92,7 +91,6 @@ export class EntityDataCollector {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取实体详细信息
|
||||
* @param entityId 实体ID
|
||||
@@ -108,9 +106,9 @@ export class EntityDataCollector {
|
||||
const entity = entityList.buffer.find((e: any) => e.id === entityId);
|
||||
if (!entity) return null;
|
||||
|
||||
const baseDebugInfo = entity.getDebugInfo ?
|
||||
entity.getDebugInfo() :
|
||||
this.buildFallbackEntityInfo(entity, scene);
|
||||
const baseDebugInfo = entity.getDebugInfo
|
||||
? entity.getDebugInfo()
|
||||
: this.buildFallbackEntityInfo(entity, scene);
|
||||
|
||||
const componentDetails = this.extractComponentDetails(entity.components);
|
||||
|
||||
@@ -163,7 +161,6 @@ export class EntityDataCollector {
|
||||
return { name: sceneName, type: sceneType };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 收集实体数据(包含内存信息)
|
||||
* @param scene 场景实例
|
||||
@@ -208,7 +205,6 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private collectArchetypeData(scene: any): {
|
||||
distribution: Array<{ signature: string; count: number; memory: number }>;
|
||||
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
|
||||
@@ -224,7 +220,9 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
private getArchetypeDistributionFast(entityContainer: any): Array<{ signature: string; count: number; memory: number }> {
|
||||
private getArchetypeDistributionFast(
|
||||
entityContainer: any
|
||||
): Array<{ signature: string; count: number; memory: number }> {
|
||||
const distribution = new Map<string, { count: number; componentTypes: string[] }>();
|
||||
|
||||
if (entityContainer && entityContainer.entities) {
|
||||
@@ -251,7 +249,9 @@ export class EntityDataCollector {
|
||||
.slice(0, 20);
|
||||
}
|
||||
|
||||
private getTopEntitiesByComponentsFast(entityContainer: any): Array<{ id: string; name: string; componentCount: number; memory: number }> {
|
||||
private getTopEntitiesByComponentsFast(
|
||||
entityContainer: any
|
||||
): Array<{ id: string; name: string; componentCount: number; memory: number }> {
|
||||
if (!entityContainer || !entityContainer.entities) {
|
||||
return [];
|
||||
}
|
||||
@@ -266,7 +266,6 @@ export class EntityDataCollector {
|
||||
.sort((a: any, b: any) => b.componentCount - a.componentCount);
|
||||
}
|
||||
|
||||
|
||||
private collectArchetypeDataWithMemory(scene: any): {
|
||||
distribution: Array<{ signature: string; count: number; memory: number }>;
|
||||
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
|
||||
@@ -282,7 +281,6 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private extractArchetypeStatistics(archetypeSystem: any): {
|
||||
distribution: Array<{ signature: string; count: number; memory: number }>;
|
||||
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
|
||||
@@ -319,7 +317,6 @@ export class EntityDataCollector {
|
||||
return { distribution, topEntities };
|
||||
}
|
||||
|
||||
|
||||
private extractArchetypeStatisticsWithMemory(archetypeSystem: any): {
|
||||
distribution: Array<{ signature: string; count: number; memory: number }>;
|
||||
topEntities: Array<{ id: string; name: string; componentCount: number; memory: number }>;
|
||||
@@ -368,9 +365,9 @@ export class EntityDataCollector {
|
||||
return { distribution, topEntities };
|
||||
}
|
||||
|
||||
|
||||
|
||||
private getArchetypeDistributionWithMemory(entityContainer: any): Array<{ signature: string; count: number; memory: number }> {
|
||||
private getArchetypeDistributionWithMemory(
|
||||
entityContainer: any
|
||||
): Array<{ signature: string; count: number; memory: number }> {
|
||||
const distribution = new Map<string, { count: number; memory: number; componentTypes: string[] }>();
|
||||
|
||||
if (entityContainer && entityContainer.entities) {
|
||||
@@ -403,8 +400,9 @@ export class EntityDataCollector {
|
||||
.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
|
||||
private getTopEntitiesByComponentsWithMemory(entityContainer: any): Array<{ id: string; name: string; componentCount: number; memory: number }> {
|
||||
private getTopEntitiesByComponentsWithMemory(
|
||||
entityContainer: any
|
||||
): Array<{ id: string; name: string; componentCount: number; memory: number }> {
|
||||
if (!entityContainer || !entityContainer.entities) {
|
||||
return [];
|
||||
}
|
||||
@@ -419,7 +417,6 @@ export class EntityDataCollector {
|
||||
.sort((a: any, b: any) => b.componentCount - a.componentCount);
|
||||
}
|
||||
|
||||
|
||||
private getEmptyEntityDebugData(): IEntityDebugData {
|
||||
return {
|
||||
totalEntities: 0,
|
||||
@@ -433,20 +430,20 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
private calculateFallbackEntityStats(entityList: any): any {
|
||||
const allEntities = entityList.buffer || [];
|
||||
const activeEntities = allEntities.filter((entity: any) =>
|
||||
entity.enabled && !entity._isDestroyed
|
||||
);
|
||||
const activeEntities = allEntities.filter((entity: any) => entity.enabled && !entity.isDestroyed);
|
||||
|
||||
return {
|
||||
totalEntities: allEntities.length,
|
||||
activeEntities: activeEntities.length,
|
||||
pendingAdd: 0,
|
||||
pendingRemove: 0,
|
||||
averageComponentsPerEntity: activeEntities.length > 0 ?
|
||||
allEntities.reduce((sum: number, e: any) => sum + (e.components?.length || 0), 0) / activeEntities.length : 0
|
||||
averageComponentsPerEntity:
|
||||
activeEntities.length > 0
|
||||
? allEntities.reduce((sum: number, e: any) => sum + (e.components?.length || 0), 0) /
|
||||
activeEntities.length
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
@@ -496,11 +493,14 @@ export class EntityDataCollector {
|
||||
|
||||
for (let i = 0; i < maxKeys; i++) {
|
||||
const key = keys[i];
|
||||
if (!key || excludeKeys.includes(key) ||
|
||||
if (
|
||||
!key ||
|
||||
excludeKeys.includes(key) ||
|
||||
key === 'constructor' ||
|
||||
key === '__proto__' ||
|
||||
key.startsWith('_cc_') ||
|
||||
key.startsWith('__')) {
|
||||
key.startsWith('__')
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -534,7 +534,6 @@ export class EntityDataCollector {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private buildEntityHierarchyTree(entityList: { buffer?: Entity[] }): Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -553,7 +552,6 @@ export class EntityDataCollector {
|
||||
|
||||
const rootEntities: any[] = [];
|
||||
|
||||
|
||||
entityList.buffer.forEach((entity: Entity) => {
|
||||
if (!entity.parent) {
|
||||
const hierarchyNode = this.buildEntityHierarchyNode(entity);
|
||||
@@ -626,12 +624,13 @@ export class EntityDataCollector {
|
||||
const batch = entities.slice(i, i + batchSize);
|
||||
|
||||
batch.forEach((entity: Entity) => {
|
||||
const baseDebugInfo = entity.getDebugInfo ?
|
||||
entity.getDebugInfo() :
|
||||
this.buildFallbackEntityInfo(entity, scene);
|
||||
const baseDebugInfo = entity.getDebugInfo
|
||||
? entity.getDebugInfo()
|
||||
: this.buildFallbackEntityInfo(entity, scene);
|
||||
|
||||
const componentCacheStats = (entity as any).getComponentCacheStats ?
|
||||
(entity as any).getComponentCacheStats() : null;
|
||||
const componentCacheStats = (entity as any).getComponentCacheStats
|
||||
? (entity as any).getComponentCacheStats()
|
||||
: null;
|
||||
|
||||
const componentDetails = this.extractComponentDetails(entity.components);
|
||||
|
||||
@@ -639,13 +638,14 @@ export class EntityDataCollector {
|
||||
...baseDebugInfo,
|
||||
parentName: entity.parent?.name || null,
|
||||
components: componentDetails,
|
||||
componentTypes: baseDebugInfo.componentTypes ||
|
||||
componentDetails.map((comp) => comp.typeName),
|
||||
cachePerformance: componentCacheStats ? {
|
||||
componentTypes: baseDebugInfo.componentTypes || componentDetails.map((comp) => comp.typeName),
|
||||
cachePerformance: componentCacheStats
|
||||
? {
|
||||
hitRate: componentCacheStats.cacheStats.hitRate,
|
||||
size: componentCacheStats.cacheStats.size,
|
||||
maxSize: componentCacheStats.cacheStats.maxSize
|
||||
} : null
|
||||
}
|
||||
: null
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -694,7 +694,7 @@ export class EntityDataCollector {
|
||||
|
||||
try {
|
||||
const propertyKeys = Object.keys(component);
|
||||
propertyKeys.forEach(propertyKey => {
|
||||
propertyKeys.forEach((propertyKey) => {
|
||||
if (!propertyKey.startsWith('_') && propertyKey !== 'entity' && propertyKey !== 'constructor') {
|
||||
const propertyValue = (component as any)[propertyKey];
|
||||
if (propertyValue !== undefined && propertyValue !== null) {
|
||||
@@ -726,7 +726,11 @@ export class EntityDataCollector {
|
||||
* @param componentIndex 组件索引
|
||||
* @param scene 场景实例
|
||||
*/
|
||||
public getComponentProperties(entityId: number, componentIndex: number, scene?: IScene | null): Record<string, any> {
|
||||
public getComponentProperties(
|
||||
entityId: number,
|
||||
componentIndex: number,
|
||||
scene?: IScene | null
|
||||
): Record<string, any> {
|
||||
try {
|
||||
if (!scene) return {};
|
||||
|
||||
@@ -740,7 +744,7 @@ export class EntityDataCollector {
|
||||
const properties: Record<string, any> = {};
|
||||
|
||||
const propertyKeys = Object.keys(component);
|
||||
propertyKeys.forEach(propertyKey => {
|
||||
propertyKeys.forEach((propertyKey) => {
|
||||
if (!propertyKey.startsWith('_') && propertyKey !== 'entity') {
|
||||
const propertyValue = (component as any)[propertyKey];
|
||||
if (propertyValue !== undefined && propertyValue !== null) {
|
||||
@@ -786,7 +790,7 @@ export class EntityDataCollector {
|
||||
if (obj.length === 0) return [];
|
||||
|
||||
if (obj.length > 10) {
|
||||
const sample = obj.slice(0, 3).map(item => this.formatPropertyValue(item, 1));
|
||||
const sample = obj.slice(0, 3).map((item) => this.formatPropertyValue(item, 1));
|
||||
return {
|
||||
_isLazyArray: true,
|
||||
_arrayLength: obj.length,
|
||||
@@ -795,7 +799,7 @@ export class EntityDataCollector {
|
||||
};
|
||||
}
|
||||
|
||||
return obj.map(item => this.formatPropertyValue(item, 1));
|
||||
return obj.map((item) => this.formatPropertyValue(item, 1));
|
||||
}
|
||||
|
||||
const keys = Object.keys(obj);
|
||||
@@ -922,7 +926,12 @@ export class EntityDataCollector {
|
||||
* @param propertyPath 属性路径
|
||||
* @param scene 场景实例
|
||||
*/
|
||||
public expandLazyObject(entityId: number, componentIndex: number, propertyPath: string, scene?: IScene | null): any {
|
||||
public expandLazyObject(
|
||||
entityId: number,
|
||||
componentIndex: number,
|
||||
propertyPath: string,
|
||||
scene?: IScene | null
|
||||
): any {
|
||||
try {
|
||||
if (!scene) return null;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export class PerformanceDataCollector {
|
||||
}
|
||||
|
||||
// 计算ECS执行时间统计
|
||||
const history = this.frameTimeHistory.filter(t => t >= 0);
|
||||
const history = this.frameTimeHistory.filter((t) => t >= 0);
|
||||
const averageECSTime = history.length > 0 ? history.reduce((a, b) => a + b, 0) / history.length : ecsExecutionTimeMs;
|
||||
const minECSTime = history.length > 0 ? Math.min(...history) : ecsExecutionTimeMs;
|
||||
const maxECSTime = history.length > 0 ? Math.max(...history) : ecsExecutionTimeMs;
|
||||
@@ -100,7 +100,7 @@ export class PerformanceDataCollector {
|
||||
}
|
||||
|
||||
// 计算各系统占ECS总时间的百分比
|
||||
systemBreakdown.forEach(system => {
|
||||
systemBreakdown.forEach((system) => {
|
||||
system.percentage = totalTime > 0 ? (system.executionTime / totalTime * 100) : 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ export class WebSocketManager {
|
||||
this.reconnectAttempts++;
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect().catch(_error => {
|
||||
this.connect().catch((_error) => {
|
||||
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@ export class Emitter<T, TContext = unknown> {
|
||||
* @param handler 事件函数
|
||||
*/
|
||||
public removeObserver(eventType: T, handler: Function) {
|
||||
let messageData = this._messageTable.get(eventType);
|
||||
const messageData = this._messageTable.get(eventType);
|
||||
if (messageData) {
|
||||
let index = messageData.findIndex(data => data.func == handler);
|
||||
const index = messageData.findIndex((data) => data.func == handler);
|
||||
if (index != -1)
|
||||
messageData.splice(index, 1);
|
||||
}
|
||||
@@ -61,9 +61,9 @@ export class Emitter<T, TContext = unknown> {
|
||||
* @param data 事件数据
|
||||
*/
|
||||
public emit<TData = unknown>(eventType: T, ...data: TData[]) {
|
||||
let list = this._messageTable.get(eventType);
|
||||
const list = this._messageTable.get(eventType);
|
||||
if (list) {
|
||||
for (let observer of list) {
|
||||
for (const observer of list) {
|
||||
observer.func.call(observer.context, ...data);
|
||||
}
|
||||
}
|
||||
@@ -75,8 +75,8 @@ export class Emitter<T, TContext = unknown> {
|
||||
* @param handler 事件函数
|
||||
*/
|
||||
public hasObserver(eventType: T, handler: Function): boolean {
|
||||
let list = this._messageTable.get(eventType);
|
||||
return list ? list.some(observer => observer.func === handler) : false;
|
||||
const list = this._messageTable.get(eventType);
|
||||
return list ? list.some((observer) => observer.func === handler) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Colors, LogLevel } from "./Constants";
|
||||
import { ILogger, LoggerColorConfig, LoggerConfig } from "./Types";
|
||||
import { Colors, LogLevel } from './Constants';
|
||||
import { ILogger, LoggerColorConfig, LoggerConfig } from './Types';
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ConsoleLogger } from "./ConsoleLogger";
|
||||
import { LogLevel } from "./Constants";
|
||||
import { ILogger, LoggerColorConfig } from "./Types";
|
||||
import { ConsoleLogger } from './ConsoleLogger';
|
||||
import { LogLevel } from './Constants';
|
||||
import { ILogger, LoggerColorConfig } from './Types';
|
||||
|
||||
/**
|
||||
* 日志管理器
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LogLevel } from "./Constants";
|
||||
import type { LogLevel } from './Constants';
|
||||
|
||||
/**
|
||||
* 日志接口
|
||||
|
||||
@@ -276,12 +276,12 @@ export class PerformanceMonitor implements IService {
|
||||
*/
|
||||
public getPerformanceReport(): string {
|
||||
if (!this._isEnabled) {
|
||||
return "Performance monitoring is disabled.";
|
||||
return 'Performance monitoring is disabled.';
|
||||
}
|
||||
|
||||
const lines: string[] = [];
|
||||
lines.push("=== ECS Performance Report ===");
|
||||
lines.push("");
|
||||
lines.push('=== ECS Performance Report ===');
|
||||
lines.push('');
|
||||
|
||||
// 按平均执行时间排序
|
||||
const sortedSystems = Array.from(this._systemStats.entries())
|
||||
@@ -300,7 +300,7 @@ export class PerformanceMonitor implements IService {
|
||||
lines.push(` Per Entity: ${data.averageTimePerEntity.toFixed(4)}ms`);
|
||||
}
|
||||
|
||||
lines.push("");
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
// 总体统计
|
||||
|
||||
@@ -30,7 +30,7 @@ export class TimerManager implements IService, IUpdatable {
|
||||
* @param onTime
|
||||
*/
|
||||
public schedule<TContext = unknown>(timeInSeconds: number, repeats: boolean, context: TContext, onTime: (timer: ITimer<TContext>)=>void): Timer<TContext> {
|
||||
let timer = new Timer<TContext>();
|
||||
const timer = new Timer<TContext>();
|
||||
timer.initialize(timeInSeconds, repeats, context, onTime);
|
||||
this._timers.push(timer as Timer<unknown>);
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ describe('Component - 组件基类测试', () => {
|
||||
let scene: Scene;
|
||||
|
||||
beforeEach(() => {
|
||||
Component._idGenerator = 0;
|
||||
component = new TestComponent();
|
||||
scene = new Scene();
|
||||
entity = scene.createEntity('TestEntity');
|
||||
@@ -51,12 +50,11 @@ describe('Component - 组件基类测试', () => {
|
||||
});
|
||||
|
||||
test('组件ID应该递增分配', () => {
|
||||
const startId = Component._idGenerator;
|
||||
const component1 = new TestComponent();
|
||||
const component2 = new TestComponent();
|
||||
|
||||
expect(component2.id).toBe(component1.id + 1);
|
||||
expect(component1.id).toBeGreaterThanOrEqual(startId);
|
||||
expect(component1.id).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,17 +50,19 @@ class ConcreteEntitySystem extends EntitySystem {
|
||||
const handler = (event: any) => {
|
||||
this.eventHandlerCallCount++;
|
||||
};
|
||||
this.addEventListener('manual_event', handler);
|
||||
this.removeEventListener('manual_event', handler);
|
||||
const listenerRef = this.addEventListener('manual_event', handler);
|
||||
if (listenerRef) {
|
||||
this.removeEventListener('manual_event', listenerRef);
|
||||
}
|
||||
}
|
||||
|
||||
// 公开测试方法
|
||||
public testAddEventListener(eventType: string, handler: (event: any) => void): void {
|
||||
this.addEventListener(eventType, handler);
|
||||
public testAddEventListener(eventType: string, handler: (event: any) => void): string | null {
|
||||
return this.addEventListener(eventType, handler);
|
||||
}
|
||||
|
||||
public testRemoveEventListener(eventType: string, handler: (event: any) => void): void {
|
||||
this.removeEventListener(eventType, handler);
|
||||
public testRemoveEventListener(eventType: string, listenerRef: string): void {
|
||||
this.removeEventListener(eventType, listenerRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,14 +120,16 @@ describe('EntitySystem', () => {
|
||||
const handler = jest.fn();
|
||||
|
||||
// 添加监听器
|
||||
system.testAddEventListener('manual_remove_event', handler);
|
||||
const listenerRef = system.testAddEventListener('manual_remove_event', handler);
|
||||
|
||||
// 发射事件验证监听器工作
|
||||
scene.eventSystem.emitSync('manual_remove_event', {});
|
||||
expect(handler).toHaveBeenCalledTimes(1);
|
||||
|
||||
// 移除监听器
|
||||
system.testRemoveEventListener('manual_remove_event', handler);
|
||||
if (listenerRef) {
|
||||
system.testRemoveEventListener('manual_remove_event', listenerRef);
|
||||
}
|
||||
|
||||
// 再次发射事件
|
||||
scene.eventSystem.emitSync('manual_remove_event', {});
|
||||
@@ -205,11 +209,11 @@ describe('EntitySystem', () => {
|
||||
});
|
||||
|
||||
it('当移除不存在的监听器时,应该安全处理', () => {
|
||||
const nonExistentHandler = () => {};
|
||||
const nonExistentListenerRef = 'non_existent_listener_ref';
|
||||
|
||||
// 应该不会抛出错误
|
||||
expect(() => {
|
||||
system.testRemoveEventListener('non_existent_event', nonExistentHandler);
|
||||
system.testRemoveEventListener('non_existent_event', nonExistentListenerRef);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ import { ConfirmDialog } from './components/ConfirmDialog';
|
||||
import { BehaviorTreeWindow } from './components/BehaviorTreeWindow';
|
||||
import { PluginGeneratorWindow } from './components/PluginGeneratorWindow';
|
||||
import { ToastProvider } from './components/Toast';
|
||||
import { Viewport } from './components/Viewport';
|
||||
import { MenuBar } from './components/MenuBar';
|
||||
import { FlexLayoutDockContainer, FlexDockPanel } from './components/FlexLayoutDockContainer';
|
||||
import { TauriAPI } from './api/tauri';
|
||||
@@ -99,11 +98,11 @@ function App() {
|
||||
useEffect(() => {
|
||||
if (messageHub) {
|
||||
const unsubscribeEnabled = messageHub.subscribe('plugin:enabled', () => {
|
||||
setPluginUpdateTrigger(prev => prev + 1);
|
||||
setPluginUpdateTrigger((prev) => prev + 1);
|
||||
});
|
||||
|
||||
const unsubscribeDisabled = messageHub.subscribe('plugin:disabled', () => {
|
||||
setPluginUpdateTrigger(prev => prev + 1);
|
||||
setPluginUpdateTrigger((prev) => prev + 1);
|
||||
});
|
||||
|
||||
return () => {
|
||||
@@ -262,7 +261,7 @@ function App() {
|
||||
|
||||
if (sceneFiles.length > 0) {
|
||||
const defaultScenePath = projectService.getDefaultScenePath();
|
||||
const sceneToLoad = sceneFiles.find(f => f === defaultScenePath) || sceneFiles[0];
|
||||
const sceneToLoad = sceneFiles.find((f) => f === defaultScenePath) || sceneFiles[0];
|
||||
|
||||
await sceneManagerService.openScene(sceneToLoad);
|
||||
} else {
|
||||
@@ -480,7 +479,7 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportScene = async () => {
|
||||
const _handleExportScene = async () => {
|
||||
if (!sceneManager) {
|
||||
console.error('SceneManagerService not available');
|
||||
return;
|
||||
@@ -517,7 +516,7 @@ function App() {
|
||||
// 通知所有已加载的插件更新语言
|
||||
if (pluginManager) {
|
||||
const allPlugins = pluginManager.getAllEditorPlugins();
|
||||
allPlugins.forEach(plugin => {
|
||||
allPlugins.forEach((plugin) => {
|
||||
if (plugin.setLocale) {
|
||||
plugin.setLocale(newLocale);
|
||||
}
|
||||
@@ -601,24 +600,24 @@ function App() {
|
||||
}
|
||||
|
||||
const enabledPlugins = pluginManager.getAllPluginMetadata()
|
||||
.filter(p => p.enabled)
|
||||
.map(p => p.name);
|
||||
.filter((p) => p.enabled)
|
||||
.map((p) => p.name);
|
||||
|
||||
const pluginPanels: FlexDockPanel[] = uiRegistry.getAllPanels()
|
||||
.filter(panelDesc => {
|
||||
.filter((panelDesc) => {
|
||||
if (!panelDesc.component) {
|
||||
return false;
|
||||
}
|
||||
return enabledPlugins.some(pluginName => {
|
||||
return enabledPlugins.some((pluginName) => {
|
||||
const plugin = pluginManager.getEditorPlugin(pluginName);
|
||||
if (plugin && plugin.registerPanels) {
|
||||
const pluginPanels = plugin.registerPanels();
|
||||
return pluginPanels.some(p => p.id === panelDesc.id);
|
||||
return pluginPanels.some((p) => p.id === panelDesc.id);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
})
|
||||
.map(panelDesc => {
|
||||
.map((panelDesc) => {
|
||||
const Component = panelDesc.component;
|
||||
return {
|
||||
id: panelDesc.id,
|
||||
@@ -724,7 +723,7 @@ function App() {
|
||||
<div className="editor-footer">
|
||||
<span>{t('footer.plugins')}: {pluginManager?.getAllEditorPlugins().length ?? 0}</span>
|
||||
<span>{t('footer.entities')}: {entityStore?.getAllEntities().length ?? 0}</span>
|
||||
<span>{t('footer.core')}: {initialized ? t('footer.active') : t('footer.inactive')}</span>
|
||||
<span>{t('footer.core')}: {t('footer.active')}</span>
|
||||
</div>
|
||||
|
||||
{showPluginManager && pluginManager && (
|
||||
@@ -735,7 +734,7 @@ function App() {
|
||||
onOpen={() => {
|
||||
// 同步所有插件的语言状态
|
||||
const allPlugins = pluginManager.getAllEditorPlugins();
|
||||
allPlugins.forEach(plugin => {
|
||||
allPlugins.forEach((plugin) => {
|
||||
if (plugin.setLocale) {
|
||||
plugin.setLocale(locale);
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ export function AssetBrowser({ projectPath, locale, onOpenScene, onOpenBehaviorT
|
||||
if (!currentPath || !projectPath) return [];
|
||||
|
||||
const relative = currentPath.replace(projectPath, '');
|
||||
const parts = relative.split(/[/\\]/).filter(p => p);
|
||||
const parts = relative.split(/[/\\]/).filter((p) => p);
|
||||
|
||||
const crumbs = [{ name: 'Content', path: projectPath }];
|
||||
let accPath = projectPath;
|
||||
@@ -239,7 +239,7 @@ export function AssetBrowser({ projectPath, locale, onOpenScene, onOpenBehaviorT
|
||||
};
|
||||
|
||||
const filteredAssets = searchQuery
|
||||
? assets.filter(asset =>
|
||||
? assets.filter((asset) =>
|
||||
asset.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
: assets;
|
||||
|
||||
@@ -83,7 +83,7 @@ export function AssetPicker({ value, onChange, projectPath, filter = 'btree', la
|
||||
}}
|
||||
>
|
||||
<option value="">{loading ? '加载中...' : '选择资产...'}</option>
|
||||
{assets.map(asset => (
|
||||
{assets.map((asset) => (
|
||||
<option key={asset} value={asset}>
|
||||
{asset}
|
||||
</option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Folder, File, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
|
||||
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import '../styles/AssetPickerDialog.css';
|
||||
|
||||
@@ -86,7 +86,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
modified: entry.modified
|
||||
};
|
||||
})
|
||||
.filter(item => item.isDir || item.extension === fileExtension)
|
||||
.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;
|
||||
@@ -102,7 +102,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
};
|
||||
|
||||
// 过滤搜索结果
|
||||
const filteredAssets = assets.filter(item =>
|
||||
const filteredAssets = assets.filter((item) =>
|
||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
@@ -189,7 +189,7 @@ export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClos
|
||||
const currentPathNormalized = currentPath.replace(/\\/g, '/');
|
||||
|
||||
const relative = currentPathNormalized.replace(basePathNormalized, '');
|
||||
const parts = relative.split('/').filter(p => p);
|
||||
const parts = relative.split('/').filter((p) => p);
|
||||
|
||||
// 根路径名称(显示"行为树"或"Assets")
|
||||
const rootName = assetBasePath
|
||||
|
||||
@@ -167,7 +167,7 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
|
||||
};
|
||||
|
||||
const toggleGroup = (groupName: string) => {
|
||||
setCollapsedGroups(prev => {
|
||||
setCollapsedGroups((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(groupName)) {
|
||||
newSet.delete(groupName);
|
||||
@@ -416,7 +416,7 @@ export const BehaviorTreeBlackboard: React.FC<BehaviorTreeBlackboardProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{groupNames.map(groupName => {
|
||||
{groupNames.map((groupName) => {
|
||||
const isCollapsed = collapsedGroups.has(groupName);
|
||||
const groupVars = groupedVariables[groupName];
|
||||
|
||||
|
||||
@@ -166,7 +166,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
boxSelectStart,
|
||||
boxSelectEnd,
|
||||
dragDelta,
|
||||
forceUpdateCounter,
|
||||
forceUpdateCounter: _forceUpdateCounter,
|
||||
setNodes,
|
||||
setConnections,
|
||||
setSelectedNodeIds,
|
||||
@@ -286,11 +286,11 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
|
||||
// 运行状态
|
||||
const [executionMode, setExecutionMode] = useState<ExecutionMode>('idle');
|
||||
const [executionHistory, setExecutionHistory] = useState<string[]>([]);
|
||||
const [_executionHistory, setExecutionHistory] = useState<string[]>([]);
|
||||
const [executionLogs, setExecutionLogs] = useState<ExecutionLog[]>([]);
|
||||
const [executionSpeed, setExecutionSpeed] = useState<number>(1.0);
|
||||
const [tickCount, setTickCount] = useState(0);
|
||||
const executionTimerRef = useRef<number | null>(null);
|
||||
const _executionTimerRef = useRef<number | null>(null);
|
||||
const executionModeRef = useRef<ExecutionMode>('idle');
|
||||
const executorRef = useRef<BehaviorTreeExecutor | null>(null);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
@@ -384,20 +384,20 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
// 重新运行时清空未提交节点列表
|
||||
setUncommittedNodeIds(new Set());
|
||||
// 记录当前所有节点ID
|
||||
activeNodeIdsRef.current = new Set(nodes.map(n => n.id));
|
||||
activeNodeIdsRef.current = new Set(nodes.map((n) => n.id));
|
||||
} else if (executionMode === 'running' || executionMode === 'paused') {
|
||||
// 检测新增的节点
|
||||
const currentNodeIds = new Set(nodes.map(n => n.id));
|
||||
const currentNodeIds = new Set(nodes.map((n) => n.id));
|
||||
const newNodeIds = new Set<string>();
|
||||
|
||||
currentNodeIds.forEach(id => {
|
||||
currentNodeIds.forEach((id) => {
|
||||
if (!activeNodeIdsRef.current.has(id)) {
|
||||
newNodeIds.add(id);
|
||||
}
|
||||
});
|
||||
|
||||
if (newNodeIds.size > 0) {
|
||||
setUncommittedNodeIds(prev => new Set([...prev, ...newNodeIds]));
|
||||
setUncommittedNodeIds((prev) => new Set([...prev, ...newNodeIds]));
|
||||
}
|
||||
}
|
||||
}, [nodes, executionMode]);
|
||||
@@ -545,7 +545,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
};
|
||||
|
||||
const handleReplaceNode = (newTemplate: NodeTemplate) => {
|
||||
const nodeToReplace = nodes.find(n => n.id === quickCreateMenu.replaceNodeId);
|
||||
const nodeToReplace = nodes.find((n) => n.id === quickCreateMenu.replaceNodeId);
|
||||
if (!nodeToReplace) return;
|
||||
|
||||
// 如果行为树正在执行,先停止
|
||||
@@ -557,7 +557,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
const newData = { ...newTemplate.defaultConfig };
|
||||
|
||||
// 获取新模板的属性名列表
|
||||
const newPropertyNames = new Set(newTemplate.properties.map(p => p.name));
|
||||
const newPropertyNames = new Set(newTemplate.properties.map((p) => p.name));
|
||||
|
||||
// 遍历旧节点的 data,保留新模板中也存在的属性
|
||||
for (const [key, value] of Object.entries(nodeToReplace.data)) {
|
||||
@@ -583,10 +583,10 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
};
|
||||
|
||||
// 替换节点
|
||||
setNodes(nodes.map(n => n.id === newNode.id ? newNode : n));
|
||||
setNodes(nodes.map((n) => n.id === newNode.id ? newNode : n));
|
||||
|
||||
// 删除所有指向该节点的属性连接,让用户重新连接
|
||||
const updatedConnections = connections.filter(conn =>
|
||||
const updatedConnections = connections.filter((conn) =>
|
||||
!(conn.connectionType === 'property' && conn.to === newNode.id)
|
||||
);
|
||||
setConnections(updatedConnections);
|
||||
@@ -1223,7 +1223,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
};
|
||||
|
||||
// 计算属性引脚的Y坐标偏移(从节点中心算起)
|
||||
const getPropertyPinYOffset = (node: BehaviorTreeNode, propertyIndex: number): number => {
|
||||
const _getPropertyPinYOffset = (node: BehaviorTreeNode, propertyIndex: number): number => {
|
||||
// 从节点顶部开始的距离:
|
||||
const paddingTop = 12;
|
||||
const titleArea = 18 + 6; // icon高度 + marginBottom
|
||||
@@ -1268,7 +1268,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
const statusMap: Record<string, NodeExecutionStatus> = {};
|
||||
|
||||
// 直接操作DOM来更新节点样式,避免重渲染
|
||||
statuses.forEach(s => {
|
||||
statuses.forEach((s) => {
|
||||
statusMap[s.nodeId] = s.status;
|
||||
|
||||
// 检查状态是否真的变化了
|
||||
@@ -1468,7 +1468,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
if (executionModeRef.current === 'running') {
|
||||
executionModeRef.current = 'paused';
|
||||
setExecutionMode('paused');
|
||||
setExecutionHistory(prev => [...prev, '执行已暂停']);
|
||||
setExecutionHistory((prev) => [...prev, '执行已暂停']);
|
||||
|
||||
if (executorRef.current) {
|
||||
executorRef.current.pause();
|
||||
@@ -1481,7 +1481,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
} else if (executionModeRef.current === 'paused') {
|
||||
executionModeRef.current = 'running';
|
||||
setExecutionMode('running');
|
||||
setExecutionHistory(prev => [...prev, '执行已恢复']);
|
||||
setExecutionHistory((prev) => [...prev, '执行已恢复']);
|
||||
lastTickTimeRef.current = 0;
|
||||
|
||||
if (executorRef.current) {
|
||||
@@ -1500,7 +1500,7 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
lastTickTimeRef.current = 0;
|
||||
|
||||
// 清除所有状态定时器
|
||||
statusTimersRef.current.forEach(timer => clearTimeout(timer));
|
||||
statusTimersRef.current.forEach((timer) => clearTimeout(timer));
|
||||
statusTimersRef.current.clear();
|
||||
|
||||
// 清除DOM缓存
|
||||
@@ -1508,12 +1508,12 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
cache.lastNodeStatus.clear();
|
||||
|
||||
// 使用缓存来移除节点状态类
|
||||
cache.nodes.forEach(node => {
|
||||
cache.nodes.forEach((node) => {
|
||||
node.classList.remove('running', 'success', 'failure', 'executed');
|
||||
});
|
||||
|
||||
// 使用缓存来重置连线样式
|
||||
cache.connections.forEach((path, connKey) => {
|
||||
cache.connections.forEach((path, _connKey) => {
|
||||
const connectionType = path.getAttribute('data-connection-type');
|
||||
if (connectionType === 'property') {
|
||||
path.setAttribute('stroke', '#9c27b0');
|
||||
@@ -1973,8 +1973,8 @@ export const BehaviorTreeEditor: React.FC<BehaviorTreeEditorProps> = ({
|
||||
{/* 空节点警告图标 */}
|
||||
{!isRoot && !isUncommitted && node.template.type === 'composite' &&
|
||||
(node.template.requiresChildren === undefined || node.template.requiresChildren === true) &&
|
||||
!nodes.some(n =>
|
||||
connections.some(c => c.from === node.id && c.to === n.id)
|
||||
!nodes.some((n) =>
|
||||
connections.some((c) => c.from === node.id && c.to === n.id)
|
||||
) && (
|
||||
<div
|
||||
className="bt-node-empty-warning-container"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Play, Pause, Square, RotateCcw, Trash2, Copy } from 'lucide-react';
|
||||
import { Trash2, Copy } from 'lucide-react';
|
||||
|
||||
interface ExecutionLog {
|
||||
timestamp: number;
|
||||
@@ -59,14 +59,14 @@ export const BehaviorTreeExecutionPanel: React.FC<BehaviorTreeExecutionPanelProp
|
||||
};
|
||||
|
||||
const handleCopyLogs = () => {
|
||||
const logsText = logs.map(log =>
|
||||
const logsText = logs.map((log) =>
|
||||
`${formatTime(log.timestamp)} ${getLevelIcon(log.level)} ${log.message}`
|
||||
).join('\n');
|
||||
|
||||
navigator.clipboard.writeText(logsText).then(() => {
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
}).catch(err => {
|
||||
}).catch((err) => {
|
||||
console.error('复制失败:', err);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -99,15 +99,15 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
|
||||
// 按类别分组(排除根节点类别)
|
||||
const categories = useMemo(() =>
|
||||
['all', ...new Set(allTemplates
|
||||
.filter(t => t.category !== '根节点')
|
||||
.map(t => t.category))]
|
||||
.filter((t) => t.category !== '根节点')
|
||||
.map((t) => t.category))]
|
||||
, [allTemplates]);
|
||||
|
||||
const filteredTemplates = useMemo(() =>
|
||||
(selectedCategory === 'all'
|
||||
? allTemplates
|
||||
: allTemplates.filter(t => t.category === selectedCategory))
|
||||
.filter(t => t.category !== '根节点')
|
||||
: allTemplates.filter((t) => t.category === selectedCategory))
|
||||
.filter((t) => t.category !== '根节点')
|
||||
, [allTemplates, selectedCategory]);
|
||||
|
||||
const handleNodeClick = (template: NodeTemplate) => {
|
||||
@@ -158,7 +158,7 @@ export const BehaviorTreeNodePalette: React.FC<BehaviorTreeNodePaletteProps> = (
|
||||
flexWrap: 'wrap',
|
||||
gap: '5px'
|
||||
}}>
|
||||
{categories.map(category => (
|
||||
{categories.map((category) => (
|
||||
<button
|
||||
key={category}
|
||||
onClick={() => setSelectedCategory(category)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TreePine, X, Settings, Clipboard, Save, FolderOpen, Maximize2, Minimize2, Download, FilePlus } from 'lucide-react';
|
||||
import { save, open, ask, message } from '@tauri-apps/plugin-dialog';
|
||||
import { open, ask, message } from '@tauri-apps/plugin-dialog';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { BehaviorTreeEditor } from './BehaviorTreeEditor';
|
||||
@@ -124,7 +124,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
const globalBlackboard = Core.services.resolve(GlobalBlackboardService);
|
||||
const allVars = globalBlackboard.getAllVariables();
|
||||
const varsObject: Record<string, any> = {};
|
||||
allVars.forEach(v => {
|
||||
allVars.forEach((v) => {
|
||||
varsObject[v.name] = v.value;
|
||||
});
|
||||
setGlobalVariables(varsObject);
|
||||
@@ -276,7 +276,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
|
||||
const allVars = Core.services.resolve(GlobalBlackboardService).getAllVariables();
|
||||
const varsObject: Record<string, any> = {};
|
||||
allVars.forEach(v => {
|
||||
allVars.forEach((v) => {
|
||||
varsObject[v.name] = v.value;
|
||||
});
|
||||
setGlobalVariables(varsObject);
|
||||
@@ -309,7 +309,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
globalBlackboard.setValue(key, value, true);
|
||||
const allVars = globalBlackboard.getAllVariables();
|
||||
const varsObject: Record<string, any> = {};
|
||||
allVars.forEach(v => {
|
||||
allVars.forEach((v) => {
|
||||
varsObject[v.name] = v.value;
|
||||
});
|
||||
setGlobalVariables(varsObject);
|
||||
@@ -321,7 +321,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
globalBlackboard.defineVariable(key, type, value);
|
||||
const allVars = globalBlackboard.getAllVariables();
|
||||
const varsObject: Record<string, any> = {};
|
||||
allVars.forEach(v => {
|
||||
allVars.forEach((v) => {
|
||||
varsObject[v.name] = v.value;
|
||||
});
|
||||
setGlobalVariables(varsObject);
|
||||
@@ -333,7 +333,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
globalBlackboard.removeVariable(key);
|
||||
const allVars = globalBlackboard.getAllVariables();
|
||||
const varsObject: Record<string, any> = {};
|
||||
allVars.forEach(v => {
|
||||
allVars.forEach((v) => {
|
||||
varsObject[v.name] = v.value;
|
||||
});
|
||||
setGlobalVariables(varsObject);
|
||||
@@ -352,7 +352,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
let saveFilePath = currentFilePath;
|
||||
const saveFilePath = currentFilePath;
|
||||
|
||||
// 如果没有当前文件路径,打开自定义保存对话框
|
||||
if (!saveFilePath) {
|
||||
@@ -788,7 +788,7 @@ export const BehaviorTreeWindow: React.FC<BehaviorTreeWindowProps> = ({
|
||||
<button
|
||||
onClick={() => setIsFullscreen(!isFullscreen)}
|
||||
className="behavior-tree-toolbar-btn"
|
||||
title={isFullscreen ? "退出全屏" : "全屏"}
|
||||
title={isFullscreen ? '退出全屏' : '全屏'}
|
||||
>
|
||||
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
|
||||
</button>
|
||||
|
||||
@@ -162,7 +162,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
setLogs(logService.getLogs().slice(-MAX_LOGS));
|
||||
|
||||
const unsubscribe = logService.subscribe((entry) => {
|
||||
setLogs(prev => {
|
||||
setLogs((prev) => {
|
||||
const newLogs = [...prev, entry];
|
||||
if (newLogs.length > MAX_LOGS) {
|
||||
return newLogs.slice(-MAX_LOGS);
|
||||
@@ -316,7 +316,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
}
|
||||
|
||||
// 清理不再需要的缓存(日志被删除)
|
||||
const logIds = new Set(logs.map(log => log.id));
|
||||
const logIds = new Set(logs.map((log) => log.id));
|
||||
for (const cachedId of cache.keys()) {
|
||||
if (!logIds.has(cachedId)) {
|
||||
cache.delete(cachedId);
|
||||
@@ -327,7 +327,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
}, [logs, extractJSON]);
|
||||
|
||||
const filteredLogs = useMemo(() => {
|
||||
return logs.filter(log => {
|
||||
return logs.filter((log) => {
|
||||
if (!levelFilter.has(log.level)) return false;
|
||||
if (showRemoteOnly && log.source !== 'remote') return false;
|
||||
if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) {
|
||||
@@ -357,14 +357,14 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
};
|
||||
|
||||
const levelCounts = useMemo(() => ({
|
||||
[LogLevel.Debug]: logs.filter(l => l.level === LogLevel.Debug).length,
|
||||
[LogLevel.Info]: logs.filter(l => l.level === LogLevel.Info).length,
|
||||
[LogLevel.Warn]: logs.filter(l => l.level === LogLevel.Warn).length,
|
||||
[LogLevel.Error]: logs.filter(l => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||
[LogLevel.Debug]: logs.filter((l) => l.level === LogLevel.Debug).length,
|
||||
[LogLevel.Info]: logs.filter((l) => l.level === LogLevel.Info).length,
|
||||
[LogLevel.Warn]: logs.filter((l) => l.level === LogLevel.Warn).length,
|
||||
[LogLevel.Error]: logs.filter((l) => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length
|
||||
}), [logs]);
|
||||
|
||||
const remoteLogCount = useMemo(() =>
|
||||
logs.filter(l => l.source === 'remote').length
|
||||
logs.filter((l) => l.source === 'remote').length
|
||||
, [logs]);
|
||||
|
||||
return (
|
||||
@@ -442,7 +442,7 @@ export function ConsolePanel({ logService }: ConsolePanelProps) {
|
||||
<p>No logs to display</p>
|
||||
</div>
|
||||
) : (
|
||||
filteredLogs.map(log => (
|
||||
filteredLogs.map((log) => (
|
||||
<LogEntryItem
|
||||
key={log.id}
|
||||
log={log}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Entity, Core } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub, ComponentRegistry } from '@esengine/editor-core';
|
||||
import { Entity } from '@esengine/ecs-framework';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import { PropertyInspector } from './PropertyInspector';
|
||||
import { FileSearch, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
|
||||
import '../styles/EntityInspector.css';
|
||||
@@ -38,7 +38,7 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
||||
};
|
||||
|
||||
const handleComponentChange = () => {
|
||||
setComponentVersion(prev => prev + 1);
|
||||
setComponentVersion((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
|
||||
@@ -67,7 +67,7 @@ export function EntityInspector({ entityStore: _entityStore, messageHub }: Entit
|
||||
};
|
||||
|
||||
const toggleComponentExpanded = (index: number) => {
|
||||
setExpandedComponents(prev => {
|
||||
setExpandedComponents((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(index)) {
|
||||
newSet.delete(index);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, FileJson, Binary, Info, File, FolderTree, FolderOpen, Code } from 'lucide-react';
|
||||
import { X, File, FolderTree, FolderOpen } from 'lucide-react';
|
||||
import { open } from '@tauri-apps/plugin-dialog';
|
||||
import '../styles/ExportRuntimeDialog.css';
|
||||
|
||||
@@ -66,7 +66,7 @@ export const ExportRuntimeDialog: React.FC<ExportRuntimeDialogProps> = ({
|
||||
setSelectAll(true);
|
||||
|
||||
const newFormats = new Map<string, 'json' | 'binary'>();
|
||||
availableFiles.forEach(file => {
|
||||
availableFiles.forEach((file) => {
|
||||
newFormats.set(file, 'binary');
|
||||
});
|
||||
setFileFormats(newFormats);
|
||||
|
||||
@@ -37,7 +37,7 @@ export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps
|
||||
const children = entriesToNodes(entries);
|
||||
|
||||
// 创建根节点
|
||||
const rootName = path.split(/[/\\]/).filter(p => p).pop() || 'Project';
|
||||
const rootName = path.split(/[/\\]/).filter((p) => p).pop() || 'Project';
|
||||
const rootNode: TreeNode = {
|
||||
name: rootName,
|
||||
path: path,
|
||||
@@ -59,8 +59,8 @@ export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps
|
||||
const entriesToNodes = (entries: DirectoryEntry[]): TreeNode[] => {
|
||||
// 只显示文件夹,过滤掉文件
|
||||
return entries
|
||||
.filter(entry => entry.is_dir)
|
||||
.map(entry => ({
|
||||
.filter((entry) => entry.is_dir)
|
||||
.map((entry) => ({
|
||||
name: entry.name,
|
||||
path: entry.path,
|
||||
type: 'folder' as const,
|
||||
@@ -141,7 +141,7 @@ export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps
|
||||
</div>
|
||||
{node.expanded && node.children && (
|
||||
<div className="tree-children">
|
||||
{node.children.map(child => renderNode(child, level + 1))}
|
||||
{node.children.map((child) => renderNode(child, level + 1))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -158,7 +158,7 @@ export function FileTree({ rootPath, onSelectFile, selectedPath }: FileTreeProps
|
||||
|
||||
return (
|
||||
<div className="file-tree">
|
||||
{tree.map(node => renderNode(node))}
|
||||
{tree.map((node) => renderNode(node))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useRef, useCallback, ReactNode, useMemo } from 'react';
|
||||
import { useCallback, ReactNode, useMemo } from 'react';
|
||||
import { Layout, Model, TabNode, IJsonModel, Actions, IJsonTabSetNode, IJsonRowNode } from 'flexlayout-react';
|
||||
import 'flexlayout-react/style/light.css';
|
||||
import '../styles/FlexLayoutDock.css';
|
||||
@@ -17,16 +17,16 @@ interface FlexLayoutDockContainerProps {
|
||||
|
||||
export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDockContainerProps) {
|
||||
const createDefaultLayout = useCallback((): IJsonModel => {
|
||||
const leftPanels = panels.filter(p => p.id.includes('hierarchy'));
|
||||
const rightPanels = panels.filter(p => p.id.includes('inspector'));
|
||||
const bottomPanels = panels.filter(p => p.id.includes('console') || p.id.includes('asset'))
|
||||
const leftPanels = panels.filter((p) => p.id.includes('hierarchy'));
|
||||
const rightPanels = panels.filter((p) => p.id.includes('inspector'));
|
||||
const bottomPanels = panels.filter((p) => p.id.includes('console') || p.id.includes('asset'))
|
||||
.sort((a, b) => {
|
||||
// 控制台排在前面
|
||||
if (a.id.includes('console')) return -1;
|
||||
if (b.id.includes('console')) return 1;
|
||||
return 0;
|
||||
});
|
||||
const centerPanels = panels.filter(p =>
|
||||
const centerPanels = panels.filter((p) =>
|
||||
!leftPanels.includes(p) && !rightPanels.includes(p) && !bottomPanels.includes(p)
|
||||
);
|
||||
|
||||
@@ -36,26 +36,26 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
centerColumnChildren.push({
|
||||
type: 'tabset',
|
||||
weight: 70,
|
||||
children: centerPanels.map(p => ({
|
||||
children: centerPanels.map((p) => ({
|
||||
type: 'tab',
|
||||
name: p.title,
|
||||
id: p.id,
|
||||
component: p.id,
|
||||
enableClose: p.closable !== false,
|
||||
})),
|
||||
enableClose: p.closable !== false
|
||||
}))
|
||||
});
|
||||
}
|
||||
if (bottomPanels.length > 0) {
|
||||
centerColumnChildren.push({
|
||||
type: 'tabset',
|
||||
weight: 30,
|
||||
children: bottomPanels.map(p => ({
|
||||
children: bottomPanels.map((p) => ({
|
||||
type: 'tab',
|
||||
name: p.title,
|
||||
id: p.id,
|
||||
component: p.id,
|
||||
enableClose: p.closable !== false,
|
||||
})),
|
||||
enableClose: p.closable !== false
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,13 +65,13 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
mainRowChildren.push({
|
||||
type: 'tabset',
|
||||
weight: 20,
|
||||
children: leftPanels.map(p => ({
|
||||
children: leftPanels.map((p) => ({
|
||||
type: 'tab',
|
||||
name: p.title,
|
||||
id: p.id,
|
||||
component: p.id,
|
||||
enableClose: p.closable !== false,
|
||||
})),
|
||||
enableClose: p.closable !== false
|
||||
}))
|
||||
});
|
||||
}
|
||||
if (centerColumnChildren.length > 0) {
|
||||
@@ -94,7 +94,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
mainRowChildren.push({
|
||||
type: 'row',
|
||||
weight: 60,
|
||||
children: centerColumnChildren,
|
||||
children: centerColumnChildren
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -102,13 +102,13 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
mainRowChildren.push({
|
||||
type: 'tabset',
|
||||
weight: 20,
|
||||
children: rightPanels.map(p => ({
|
||||
children: rightPanels.map((p) => ({
|
||||
type: 'tab',
|
||||
name: p.title,
|
||||
id: p.id,
|
||||
component: p.id,
|
||||
enableClose: p.closable !== false,
|
||||
})),
|
||||
enableClose: p.closable !== false
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
@@ -120,14 +120,14 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
tabSetEnableDrop: true,
|
||||
tabSetEnableDrag: true,
|
||||
tabSetEnableDivide: true,
|
||||
borderEnableDrop: true,
|
||||
borderEnableDrop: true
|
||||
},
|
||||
borders: [],
|
||||
layout: {
|
||||
type: 'row',
|
||||
weight: 100,
|
||||
children: mainRowChildren,
|
||||
},
|
||||
children: mainRowChildren
|
||||
}
|
||||
};
|
||||
}, [panels]);
|
||||
|
||||
@@ -135,7 +135,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock
|
||||
|
||||
const factory = useCallback((node: TabNode) => {
|
||||
const component = node.getComponent();
|
||||
const panel = panels.find(p => p.id === component);
|
||||
const panel = panels.find((p) => p.id === component);
|
||||
return panel?.content || <div>Panel not found</div>;
|
||||
}, [panels]);
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export function MenuBar({
|
||||
onCloseProject,
|
||||
onExit,
|
||||
onOpenPluginManager,
|
||||
onOpenProfiler,
|
||||
onOpenProfiler: _onOpenProfiler,
|
||||
onOpenPortManager,
|
||||
onOpenSettings,
|
||||
onToggleDevtools,
|
||||
@@ -64,17 +64,17 @@ export function MenuBar({
|
||||
const items = uiRegistry.getChildMenus('window');
|
||||
// 过滤掉被禁用插件的菜单项
|
||||
const enabledPlugins = pluginManager.getAllPluginMetadata()
|
||||
.filter(p => p.enabled)
|
||||
.map(p => p.name);
|
||||
.filter((p) => p.enabled)
|
||||
.map((p) => p.name);
|
||||
|
||||
// 只显示启用插件的菜单项
|
||||
const filteredItems = items.filter(item => {
|
||||
const filteredItems = items.filter((item) => {
|
||||
// 检查菜单项是否属于某个插件
|
||||
return enabledPlugins.some(pluginName => {
|
||||
return enabledPlugins.some((pluginName) => {
|
||||
const plugin = pluginManager.getEditorPlugin(pluginName);
|
||||
if (plugin && plugin.registerMenuItems) {
|
||||
const pluginMenus = plugin.registerMenuItems();
|
||||
return pluginMenus.some(m => m.id === item.id);
|
||||
return pluginMenus.some((m) => m.id === item.id);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
@@ -218,7 +218,7 @@ export function MenuBar({
|
||||
{ label: t('selectAll'), shortcut: 'Ctrl+A', disabled: true }
|
||||
],
|
||||
window: [
|
||||
...pluginMenuItems.map(item => ({
|
||||
...pluginMenuItems.map((item) => ({
|
||||
label: item.label || '',
|
||||
icon: item.icon,
|
||||
disabled: item.disabled,
|
||||
@@ -270,7 +270,7 @@ export function MenuBar({
|
||||
|
||||
return (
|
||||
<div className="menu-bar" ref={menuRef}>
|
||||
{Object.keys(menus).map(menuKey => (
|
||||
{Object.keys(menus).map((menuKey) => (
|
||||
<div key={menuKey} className="menu-item">
|
||||
<button
|
||||
className={`menu-button ${openMenu === menuKey ? 'active' : ''}`}
|
||||
|
||||
@@ -97,7 +97,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
const response = await fetch('/@plugin-generator', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
pluginName,
|
||||
@@ -149,7 +149,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<input
|
||||
type="text"
|
||||
value={pluginName}
|
||||
onChange={e => setPluginName(e.target.value)}
|
||||
onChange={(e) => setPluginName(e.target.value)}
|
||||
placeholder={t('pluginNamePlaceholder')}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
@@ -160,7 +160,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<input
|
||||
type="text"
|
||||
value={pluginVersion}
|
||||
onChange={e => setPluginVersion(e.target.value)}
|
||||
onChange={(e) => setPluginVersion(e.target.value)}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
</div>
|
||||
@@ -171,7 +171,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<input
|
||||
type="text"
|
||||
value={outputPath}
|
||||
onChange={e => setOutputPath(e.target.value)}
|
||||
onChange={(e) => setOutputPath(e.target.value)}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<button
|
||||
@@ -190,7 +190,7 @@ export function PluginGeneratorWindow({ onClose, projectPath, locale, onSuccess
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeExample}
|
||||
onChange={e => setIncludeExample(e.target.checked)}
|
||||
onChange={(e) => setIncludeExample(e.target.checked)}
|
||||
disabled={isGenerating}
|
||||
/>
|
||||
<span>{t('includeExample')}</span>
|
||||
|
||||
@@ -144,7 +144,7 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
setExpandedCategories(newExpanded);
|
||||
};
|
||||
|
||||
const filteredPlugins = plugins.filter(plugin => {
|
||||
const filteredPlugins = plugins.filter((plugin) => {
|
||||
if (!filter) return true;
|
||||
const searchLower = filter.toLowerCase();
|
||||
return (
|
||||
@@ -162,8 +162,8 @@ export function PluginManagerWindow({ pluginManager, onClose, onRefresh, onOpen,
|
||||
return acc;
|
||||
}, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>);
|
||||
|
||||
const enabledCount = plugins.filter(p => p.enabled).length;
|
||||
const disabledCount = plugins.filter(p => !p.enabled).length;
|
||||
const enabledCount = plugins.filter((p) => p.enabled).length;
|
||||
const disabledCount = plugins.filter((p) => !p.enabled).length;
|
||||
|
||||
const renderPluginCard = (plugin: IEditorPluginMetadata) => {
|
||||
const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null;
|
||||
|
||||
@@ -65,7 +65,7 @@ export function PluginPanel({ pluginManager }: PluginPanelProps) {
|
||||
setExpandedCategories(newExpanded);
|
||||
};
|
||||
|
||||
const filteredPlugins = plugins.filter(plugin => {
|
||||
const filteredPlugins = plugins.filter((plugin) => {
|
||||
if (!filter) return true;
|
||||
const searchLower = filter.toLowerCase();
|
||||
return (
|
||||
@@ -83,8 +83,8 @@ export function PluginPanel({ pluginManager }: PluginPanelProps) {
|
||||
return acc;
|
||||
}, {} as Record<EditorPluginCategory, IEditorPluginMetadata[]>);
|
||||
|
||||
const enabledCount = plugins.filter(p => p.enabled).length;
|
||||
const disabledCount = plugins.filter(p => !p.enabled).length;
|
||||
const enabledCount = plugins.filter((p) => p.enabled).length;
|
||||
const disabledCount = plugins.filter((p) => !p.enabled).length;
|
||||
|
||||
const renderPluginCard = (plugin: IEditorPluginMetadata) => {
|
||||
const IconComponent = plugin.icon ? (LucideIcons as any)[plugin.icon] : null;
|
||||
|
||||
@@ -72,7 +72,7 @@ export function PortManager({ onClose }: PortManagerProps) {
|
||||
const profilerService = (window as any).__PROFILER_SERVICE__ as ProfilerService | undefined;
|
||||
if (profilerService) {
|
||||
await profilerService.manualStartServer();
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
await checkServerStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -57,7 +57,7 @@ export function ProfilerPanel() {
|
||||
}
|
||||
|
||||
// Calculate percentages
|
||||
systemsData.forEach(system => {
|
||||
systemsData.forEach((system) => {
|
||||
system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0;
|
||||
});
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
}
|
||||
}
|
||||
|
||||
sceneSystems.forEach(system => {
|
||||
sceneSystems.forEach((system) => {
|
||||
system.percentage = coreUpdate.executionTime > 0
|
||||
? (system.executionTime / coreUpdate.executionTime) * 100
|
||||
: 0;
|
||||
@@ -201,7 +201,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
handleRemoteDebugData({
|
||||
performance: {
|
||||
frameTime: data.totalFrameTime,
|
||||
systemPerformance: data.systems.map(sys => ({
|
||||
systemPerformance: data.systems.map((sys) => ({
|
||||
systemName: sys.name,
|
||||
lastExecutionTime: sys.executionTime,
|
||||
averageTime: sys.averageTime,
|
||||
@@ -276,7 +276,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
|
||||
const toggleExpand = (systemName: string) => {
|
||||
const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
|
||||
return nodes.map(node => {
|
||||
return nodes.map((node) => {
|
||||
if (node.name === systemName) {
|
||||
return { ...node, isExpanded: !node.isExpanded };
|
||||
}
|
||||
@@ -310,7 +310,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
if (viewMode === 'tree') {
|
||||
displaySystems = displaySystems.filter(sys =>
|
||||
displaySystems = displaySystems.filter((sys) =>
|
||||
sys.name.toLowerCase().includes(query)
|
||||
);
|
||||
} else {
|
||||
@@ -323,7 +323,7 @@ export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
|
||||
}
|
||||
};
|
||||
flatten(systems);
|
||||
displaySystems = flatList.filter(sys =>
|
||||
displaySystems = flatList.filter((sys) =>
|
||||
sys.name.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function PropertyInspector({ component, onChange }: PropertyInspectorProp
|
||||
const componentAsAny = component as any;
|
||||
componentAsAny[propertyName] = value;
|
||||
|
||||
setValues(prev => ({
|
||||
setValues((prev) => ({
|
||||
...prev,
|
||||
[propertyName]: value
|
||||
}));
|
||||
@@ -497,7 +497,7 @@ function EnumField({ label, value, options, readOnly, onChange }: EnumFieldProps
|
||||
value={value ?? ''}
|
||||
disabled={readOnly}
|
||||
onChange={(e) => {
|
||||
const selectedOption = options.find(opt => String(opt.value) === e.target.value);
|
||||
const selectedOption = options.find((opt) => String(opt.value) === e.target.value);
|
||||
if (selectedOption) {
|
||||
onChange(selectedOption.value);
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
|
||||
if (connected && data.entities && data.entities.length > 0) {
|
||||
// 只在实体列表发生实质性变化时才更新
|
||||
setRemoteEntities(prev => {
|
||||
setRemoteEntities((prev) => {
|
||||
if (prev.length !== data.entities!.length) {
|
||||
return data.entities!;
|
||||
}
|
||||
@@ -231,7 +231,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
if (!searchQuery.trim()) return entityList;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return entityList.filter(entity => {
|
||||
return entityList.filter((entity) => {
|
||||
const name = entity.name;
|
||||
const id = entity.id.toString();
|
||||
|
||||
@@ -242,7 +242,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
|
||||
// Search by component types
|
||||
if (Array.isArray(entity.componentTypes)) {
|
||||
return entity.componentTypes.some(type =>
|
||||
return entity.componentTypes.some((type) =>
|
||||
type.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
@@ -255,7 +255,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
if (!searchQuery.trim()) return entityList;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return entityList.filter(entity => {
|
||||
return entityList.filter((entity) => {
|
||||
const id = entity.id.toString();
|
||||
return id.includes(query);
|
||||
});
|
||||
@@ -330,7 +330,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
</div>
|
||||
) : isRemoteConnected ? (
|
||||
<ul className="entity-list">
|
||||
{(displayEntities as RemoteEntity[]).map(entity => (
|
||||
{(displayEntities as RemoteEntity[]).map((entity) => (
|
||||
<li
|
||||
key={entity.id}
|
||||
className={`entity-item remote-entity ${selectedId === entity.id ? 'selected' : ''} ${!entity.enabled ? 'disabled' : ''}`}
|
||||
@@ -352,7 +352,7 @@ export function SceneHierarchy({ entityStore, messageHub }: SceneHierarchyProps)
|
||||
</ul>
|
||||
) : (
|
||||
<ul className="entity-list">
|
||||
{entities.map(entity => (
|
||||
{entities.map((entity) => (
|
||||
<li
|
||||
key={entity.id}
|
||||
className={`entity-item ${selectedId === entity.id ? 'selected' : ''}`}
|
||||
|
||||
@@ -156,7 +156,7 @@ export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProp
|
||||
className={`settings-select ${error ? 'settings-input-error' : ''}`}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const option = setting.options?.find(opt => String(opt.value) === e.target.value);
|
||||
const option = setting.options?.find((opt) => String(opt.value) === e.target.value);
|
||||
if (option) {
|
||||
handleValueChange(setting.key, option.value, setting);
|
||||
}
|
||||
@@ -221,7 +221,7 @@ export function SettingsWindow({ onClose, settingsRegistry }: SettingsWindowProp
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCategory = categories.find(c => c.id === selectedCategoryId);
|
||||
const selectedCategory = categories.find((c) => c.id === selectedCategoryId);
|
||||
|
||||
return (
|
||||
<div className="settings-overlay">
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||
const id = `toast-${Date.now()}-${Math.random()}`;
|
||||
const toast: Toast = { id, message, type, duration };
|
||||
|
||||
setToasts(prev => [...prev, toast]);
|
||||
setToasts((prev) => [...prev, toast]);
|
||||
|
||||
if (duration > 0) {
|
||||
setTimeout(() => {
|
||||
@@ -47,7 +47,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||
}, []);
|
||||
|
||||
const hideToast = useCallback((id: string) => {
|
||||
setToasts(prev => prev.filter(t => t.id !== id));
|
||||
setToasts((prev) => prev.filter((t) => t.id !== id));
|
||||
}, []);
|
||||
|
||||
const getIcon = (type: ToastType) => {
|
||||
@@ -67,7 +67,7 @@ export const ToastProvider: React.FC<ToastProviderProps> = ({ children }) => {
|
||||
<ToastContext.Provider value={{ showToast, hideToast }}>
|
||||
{children}
|
||||
<div className="toast-container">
|
||||
{toasts.map(toast => (
|
||||
{toasts.map((toast) => (
|
||||
<div key={toast.id} className={`toast toast-${toast.type}`}>
|
||||
<div className="toast-icon">
|
||||
{getIcon(toast.type)}
|
||||
|
||||
@@ -89,12 +89,12 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
||||
const deltaY = e.clientY - lastMousePosRef.current.y;
|
||||
|
||||
if (is3D) {
|
||||
setCameraRotation(prev => ({
|
||||
setCameraRotation((prev) => ({
|
||||
yaw: prev.yaw - deltaX * 0.005,
|
||||
pitch: Math.max(-Math.PI / 2 + 0.1, Math.min(Math.PI / 2 - 0.1, prev.pitch + deltaY * 0.005))
|
||||
}));
|
||||
} else {
|
||||
setCamera2DOffset(prev => ({
|
||||
setCamera2DOffset((prev) => ({
|
||||
x: prev.x - deltaX * 0.05,
|
||||
y: prev.y - deltaY * 0.05
|
||||
}));
|
||||
@@ -113,9 +113,9 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
if (is3D) {
|
||||
setCameraDistance(prev => Math.max(5, Math.min(50, prev + e.deltaY * 0.01)));
|
||||
setCameraDistance((prev) => Math.max(5, Math.min(50, prev + e.deltaY * 0.01)));
|
||||
} else {
|
||||
setCamera2DZoom(prev => Math.max(5, Math.min(100, prev + e.deltaY * 0.01)));
|
||||
setCamera2DZoom((prev) => Math.max(5, Math.min(100, prev + e.deltaY * 0.01)));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -237,7 +237,7 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
||||
};
|
||||
|
||||
const startRenderLoop = () => {
|
||||
let startTime = performance.now();
|
||||
const startTime = performance.now();
|
||||
|
||||
const render = (currentTime: number) => {
|
||||
const elapsed = (currentTime - startTime) / 1000;
|
||||
@@ -252,7 +252,7 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
||||
animationFrameRef.current = requestAnimationFrame(render);
|
||||
};
|
||||
|
||||
const renderFrame = (gl: WebGLRenderingContext, time: number) => {
|
||||
const renderFrame = (gl: WebGLRenderingContext, _time: number) => {
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
|
||||
let currentDrawCalls = 0;
|
||||
@@ -349,7 +349,7 @@ export function Viewport({ locale = 'en' }: ViewportProps) {
|
||||
const viewHeight = zoom * 2;
|
||||
const maxViewSize = Math.max(viewWidth, viewHeight);
|
||||
|
||||
let baseGridStep = 1;
|
||||
let baseGridStep;
|
||||
if (maxViewSize > 200) {
|
||||
baseGridStep = 100;
|
||||
} else if (maxViewSize > 100) {
|
||||
|
||||
@@ -183,7 +183,7 @@ export const ${opts.constantsName} = {} as const;`;
|
||||
if (Object.keys(grouped).length === 1 && grouped[''] !== undefined) {
|
||||
// 无命名空间,扁平结构
|
||||
const entries = variables
|
||||
.map(v => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.join(',\n');
|
||||
|
||||
return `/**
|
||||
@@ -200,13 +200,13 @@ ${entries}
|
||||
if (namespace === '') {
|
||||
// 根级别变量
|
||||
return vars
|
||||
.map(v => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.map((v) => ` ${this.transformName(v.name, opts.constantCase)}: ${quote}${v.name}${quote}`)
|
||||
.join(',\n');
|
||||
} else {
|
||||
// 命名空间变量
|
||||
const nsName = this.toPascalCase(namespace);
|
||||
const entries = vars
|
||||
.map(v => {
|
||||
.map((v) => {
|
||||
const shortName = v.name.substring(namespace.length + 1);
|
||||
return ` ${this.transformName(shortName, opts.constantCase)}: ${quote}${v.name}${quote}`;
|
||||
})
|
||||
@@ -238,7 +238,7 @@ export interface ${opts.interfaceName} {}`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
.map(v => {
|
||||
.map((v) => {
|
||||
const tsType = this.mapBlackboardTypeToTS(v.type);
|
||||
const comment = v.description ? ` /** ${v.description} */\n` : '';
|
||||
return `${comment} ${v.name}: ${tsType};`;
|
||||
@@ -334,7 +334,7 @@ export const ${opts.defaultsName}: ${opts.interfaceName} = {};`;
|
||||
}
|
||||
|
||||
const properties = variables
|
||||
.map(v => {
|
||||
.map((v) => {
|
||||
const value = this.formatValue(v.value, v.type, opts);
|
||||
return ` ${v.name}: ${value}`;
|
||||
})
|
||||
@@ -407,7 +407,7 @@ ${properties}
|
||||
private static toPascalCase(str: string): string {
|
||||
return str
|
||||
.split(/[._-]/)
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
|
||||
@@ -495,7 +495,7 @@ ${properties}
|
||||
const parts = str.split(/[._-]/);
|
||||
if (parts.length === 0) return str;
|
||||
return (parts[0] || '').toLowerCase() + parts.slice(1)
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { BlackboardValueType } from '@esengine/behavior-tree';
|
||||
|
||||
/**
|
||||
* 局部黑板变量信息
|
||||
*/
|
||||
@@ -253,7 +251,7 @@ export function is${this.toPascalCase(treeName)}Variable(
|
||||
if (value.length === 0) {
|
||||
return '[]';
|
||||
}
|
||||
const items = value.map(v => this.formatValue(v, quoteStyle)).join(', ');
|
||||
const items = value.map((v) => this.formatValue(v, quoteStyle)).join(', ');
|
||||
return `[${items}]`;
|
||||
}
|
||||
// Vector2/Vector3
|
||||
@@ -286,7 +284,7 @@ export function is${this.toPascalCase(treeName)}Variable(
|
||||
private static toPascalCase(str: string): string {
|
||||
return str
|
||||
.split(/[._-]/)
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||
.join('');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ export class PluginLoader {
|
||||
}
|
||||
|
||||
const entries = await TauriAPI.listDirectory(pluginsPath);
|
||||
const pluginDirs = entries.filter(entry => entry.is_dir && !entry.name.startsWith('.'));
|
||||
console.log('[PluginLoader] Found plugin directories:', pluginDirs.map(d => d.name));
|
||||
const pluginDirs = entries.filter((entry) => entry.is_dir && !entry.name.startsWith('.'));
|
||||
console.log('[PluginLoader] Found plugin directories:', pluginDirs.map((d) => d.name));
|
||||
|
||||
for (const entry of pluginDirs) {
|
||||
const pluginPath = `${pluginsPath}/${entry.name}`;
|
||||
@@ -101,14 +101,14 @@ export class PluginLoader {
|
||||
console.log(`[PluginLoader] Loading plugin from: ${moduleUrl}`);
|
||||
|
||||
const module = await import(/* @vite-ignore */ moduleUrl);
|
||||
console.log(`[PluginLoader] Module loaded successfully`);
|
||||
console.log('[PluginLoader] Module loaded successfully');
|
||||
|
||||
let pluginInstance: IEditorPlugin | null = null;
|
||||
try {
|
||||
pluginInstance = this.findPluginInstance(module);
|
||||
} catch (findError) {
|
||||
console.error(`[PluginLoader] Error finding plugin instance:`, findError);
|
||||
console.error(`[PluginLoader] Module object:`, module);
|
||||
console.error('[PluginLoader] Error finding plugin instance:', findError);
|
||||
console.error('[PluginLoader] Module object:', module);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -139,14 +139,14 @@ export class PluginLoader {
|
||||
messageHub.publish('locale:changed', { locale: localeService.getCurrentLocale() });
|
||||
console.log(`[PluginLoader] Published locale:changed event for plugin ${packageJson.name}`);
|
||||
} catch (error) {
|
||||
console.warn(`[PluginLoader] Failed to publish locale:changed event:`, error);
|
||||
console.warn('[PluginLoader] Failed to publish locale:changed event:', error);
|
||||
}
|
||||
|
||||
console.log(`[PluginLoader] Successfully loaded plugin: ${packageJson.name}`);
|
||||
} catch (error) {
|
||||
console.error(`[PluginLoader] Failed to load plugin from ${pluginPath}:`, error);
|
||||
if (error instanceof Error) {
|
||||
console.error(`[PluginLoader] Error stack:`, error.stack);
|
||||
console.error('[PluginLoader] Error stack:', error.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +424,7 @@ export class ProfilerService {
|
||||
}
|
||||
|
||||
private notifyListeners(data: ProfilerData): void {
|
||||
this.listeners.forEach(listener => {
|
||||
this.listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -71,14 +71,14 @@ export class SettingsService {
|
||||
|
||||
public addRecentProject(projectPath: string): void {
|
||||
const recentProjects = this.getRecentProjects();
|
||||
const filtered = recentProjects.filter(p => p !== projectPath);
|
||||
const filtered = recentProjects.filter((p) => p !== projectPath);
|
||||
const updated = [projectPath, ...filtered].slice(0, 10);
|
||||
this.set('recentProjects', updated);
|
||||
}
|
||||
|
||||
public removeRecentProject(projectPath: string): void {
|
||||
const recentProjects = this.getRecentProjects();
|
||||
const filtered = recentProjects.filter(p => p !== projectPath);
|
||||
const filtered = recentProjects.filter((p) => p !== projectPath);
|
||||
this.set('recentProjects', filtered);
|
||||
}
|
||||
|
||||
|
||||
@@ -207,24 +207,24 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
updateNodePosition: (nodeId: string, position: { x: number; y: number }) => set((state: BehaviorTreeState) => ({
|
||||
nodes: state.nodes.map((n: BehaviorTreeNode) =>
|
||||
n.id === nodeId ? { ...n, position } : n
|
||||
),
|
||||
)
|
||||
})),
|
||||
|
||||
updateNodesPosition: (updates: Map<string, { x: number; y: number }>) => set((state: BehaviorTreeState) => ({
|
||||
nodes: state.nodes.map((node: BehaviorTreeNode) => {
|
||||
const newPos = updates.get(node.id);
|
||||
return newPos ? { ...node, position: newPos } : node;
|
||||
}),
|
||||
})
|
||||
})),
|
||||
|
||||
setConnections: (connections: Connection[]) => set({ connections }),
|
||||
|
||||
addConnection: (connection: Connection) => set((state: BehaviorTreeState) => ({
|
||||
connections: [...state.connections, connection],
|
||||
connections: [...state.connections, connection]
|
||||
})),
|
||||
|
||||
removeConnections: (filter: (conn: Connection) => boolean) => set((state: BehaviorTreeState) => ({
|
||||
connections: state.connections.filter(filter),
|
||||
connections: state.connections.filter(filter)
|
||||
})),
|
||||
|
||||
setSelectedNodeIds: (nodeIds: string[]) => set({ selectedNodeIds: nodeIds }),
|
||||
@@ -232,14 +232,14 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
toggleNodeSelection: (nodeId: string) => set((state: BehaviorTreeState) => ({
|
||||
selectedNodeIds: state.selectedNodeIds.includes(nodeId)
|
||||
? state.selectedNodeIds.filter((id: string) => id !== nodeId)
|
||||
: [...state.selectedNodeIds, nodeId],
|
||||
: [...state.selectedNodeIds, nodeId]
|
||||
})),
|
||||
|
||||
clearSelection: () => set({ selectedNodeIds: [] }),
|
||||
|
||||
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) => set({
|
||||
draggingNodeId: nodeId,
|
||||
dragStartPositions: startPositions,
|
||||
dragStartPositions: startPositions
|
||||
}),
|
||||
|
||||
stopDragging: () => set({ draggingNodeId: null }),
|
||||
@@ -267,7 +267,7 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
clearConnecting: () => set({
|
||||
connectingFrom: null,
|
||||
connectingFromProperty: null,
|
||||
connectingToPos: null,
|
||||
connectingToPos: null
|
||||
}),
|
||||
|
||||
// 框选 Actions
|
||||
@@ -280,7 +280,7 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
clearBoxSelect: () => set({
|
||||
isBoxSelecting: false,
|
||||
boxSelectStart: null,
|
||||
boxSelectEnd: null,
|
||||
boxSelectEnd: null
|
||||
}),
|
||||
|
||||
// 拖动偏移 Actions
|
||||
@@ -306,9 +306,9 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
// 自动排序子节点(按X坐标从左到右)
|
||||
sortChildrenByPosition: () => set((state: BehaviorTreeState) => {
|
||||
const nodeMap = new Map<string, BehaviorTreeNode>();
|
||||
state.nodes.forEach(node => nodeMap.set(node.id, node));
|
||||
state.nodes.forEach((node) => nodeMap.set(node.id, node));
|
||||
|
||||
const sortedNodes = state.nodes.map(node => {
|
||||
const sortedNodes = state.nodes.map((node) => {
|
||||
if (node.children.length <= 1) {
|
||||
return node;
|
||||
}
|
||||
@@ -366,7 +366,7 @@ export const useBehaviorTreeStore = create<BehaviorTreeState>((set, get) => ({
|
||||
const className = node.template?.className;
|
||||
if (className) {
|
||||
const allTemplates = NodeTemplates.getAllTemplates();
|
||||
const latestTemplate = allTemplates.find(t => t.className === className);
|
||||
const latestTemplate = allTemplates.find((t) => t.className === className);
|
||||
|
||||
if (latestTemplate) {
|
||||
return {
|
||||
|
||||
@@ -118,7 +118,7 @@ export class BehaviorTreeExecutor {
|
||||
blackboard: Record<string, any>,
|
||||
connections: Array<{ from: string; to: string; fromProperty?: string; toProperty?: string; connectionType: 'node' | 'property' }>
|
||||
): BehaviorTreeData {
|
||||
const rootNode = nodes.find(n => n.id === rootNodeId);
|
||||
const rootNode = nodes.find((n) => n.id === rootNodeId);
|
||||
if (!rootNode) {
|
||||
throw new Error('未找到根节点');
|
||||
}
|
||||
@@ -163,7 +163,7 @@ export class BehaviorTreeExecutor {
|
||||
for (const conn of connections) {
|
||||
if (conn.connectionType === 'property' && conn.toProperty) {
|
||||
const targetNodeData = treeData.nodes.get(conn.to);
|
||||
const sourceNode = nodes.find(n => n.id === conn.from);
|
||||
const sourceNode = nodes.find((n) => n.id === conn.from);
|
||||
|
||||
if (targetNodeData && sourceNode) {
|
||||
// 检查源节点是否是黑板变量节点
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
"build:npm": "npm run build && node build-rollup.cjs",
|
||||
"test": "jest --config jest.config.cjs",
|
||||
"test:watch": "jest --watch --config jest.config.cjs",
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs"
|
||||
"test:coverage": "jest --coverage --config jest.config.cjs",
|
||||
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
||||
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
@@ -50,6 +52,9 @@
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.19.17",
|
||||
"@eslint/js": "^9.37.0",
|
||||
"eslint": "^9.37.0",
|
||||
"typescript-eslint": "^8.46.1",
|
||||
"jest": "^29.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"rollup": "^4.42.0",
|
||||
|
||||
@@ -208,7 +208,7 @@ export class EditorPluginManager extends PluginManager {
|
||||
* 按类别获取插件
|
||||
*/
|
||||
public getPluginsByCategory(category: EditorPluginCategory): IEditorPlugin[] {
|
||||
return this.getAllEditorPlugins().filter(plugin => plugin.category === category);
|
||||
return this.getAllEditorPlugins().filter((plugin) => plugin.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ export class ComponentRegistry implements IService {
|
||||
}
|
||||
|
||||
public getComponentsByCategory(category: string): ComponentTypeInfo[] {
|
||||
return this.getAllComponents().filter(c => c.category === category);
|
||||
return this.getAllComponents().filter((c) => c.category === category);
|
||||
}
|
||||
|
||||
public createInstance(name: string, ...args: any[]): Component | null {
|
||||
|
||||
@@ -61,7 +61,7 @@ export class EntityStoreService implements IService {
|
||||
|
||||
public getRootEntities(): Entity[] {
|
||||
return Array.from(this.rootEntities)
|
||||
.map(id => this.entities.get(id))
|
||||
.map((id) => this.entities.get(id))
|
||||
.filter((e): e is Entity => e !== undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ export class LocaleService implements IService {
|
||||
this.currentLocale = locale;
|
||||
this.saveLocale(locale);
|
||||
|
||||
this.changeListeners.forEach(listener => listener(locale));
|
||||
this.changeListeners.forEach((listener) => listener(locale));
|
||||
|
||||
logger.info(`Locale changed to: ${locale}`);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class LogService implements IService {
|
||||
* 格式化消息
|
||||
*/
|
||||
private formatMessage(args: unknown[]): string {
|
||||
return args.map(arg => {
|
||||
return args.map((arg) => {
|
||||
if (typeof arg === 'string') return arg;
|
||||
if (arg instanceof Error) return arg.message;
|
||||
try {
|
||||
|
||||
@@ -68,7 +68,7 @@ export class SettingsRegistry implements IService {
|
||||
this.categories.set(categoryId, category);
|
||||
}
|
||||
|
||||
const existingIndex = category.sections.findIndex(s => s.id === section.id);
|
||||
const existingIndex = category.sections.findIndex((s) => s.id === section.id);
|
||||
if (existingIndex >= 0) {
|
||||
category.sections[existingIndex] = section;
|
||||
console.warn(`[SettingsRegistry] Section ${section.id} in category ${categoryId} already exists, overwriting`);
|
||||
@@ -89,7 +89,7 @@ export class SettingsRegistry implements IService {
|
||||
this.categories.set(categoryId, category);
|
||||
}
|
||||
|
||||
let section = category.sections.find(s => s.id === sectionId);
|
||||
let section = category.sections.find((s) => s.id === sectionId);
|
||||
if (!section) {
|
||||
section = {
|
||||
id: sectionId,
|
||||
@@ -99,7 +99,7 @@ export class SettingsRegistry implements IService {
|
||||
category.sections.push(section);
|
||||
}
|
||||
|
||||
const existingIndex = section.settings.findIndex(s => s.key === setting.key);
|
||||
const existingIndex = section.settings.findIndex((s) => s.key === setting.key);
|
||||
if (existingIndex >= 0) {
|
||||
section.settings[existingIndex] = setting;
|
||||
console.warn(`[SettingsRegistry] Setting ${setting.key} in section ${sectionId} already exists, overwriting`);
|
||||
@@ -115,7 +115,7 @@ export class SettingsRegistry implements IService {
|
||||
public unregisterSection(categoryId: string, sectionId: string): void {
|
||||
const category = this.categories.get(categoryId);
|
||||
if (category) {
|
||||
category.sections = category.sections.filter(s => s.id !== sectionId);
|
||||
category.sections = category.sections.filter((s) => s.id !== sectionId);
|
||||
if (category.sections.length === 0) {
|
||||
this.categories.delete(categoryId);
|
||||
}
|
||||
@@ -134,10 +134,10 @@ export class SettingsRegistry implements IService {
|
||||
const category = this.categories.get(categoryId);
|
||||
if (!category) return undefined;
|
||||
|
||||
const section = category.sections.find(s => s.id === sectionId);
|
||||
const section = category.sections.find((s) => s.id === sectionId);
|
||||
if (!section) return undefined;
|
||||
|
||||
return section.settings.find(s => s.key === key);
|
||||
return section.settings.find((s) => s.key === key);
|
||||
}
|
||||
|
||||
public getAllSettings(): Map<string, SettingDescriptor> {
|
||||
@@ -174,7 +174,7 @@ export class SettingsRegistry implements IService {
|
||||
|
||||
case 'select':
|
||||
if (!setting.options) return false;
|
||||
return setting.options.some(opt => opt.value === value);
|
||||
return setting.options.some((opt) => opt.value === value);
|
||||
|
||||
case 'range':
|
||||
if (typeof value !== 'number') return false;
|
||||
|
||||
@@ -70,7 +70,7 @@ export class UIRegistry implements IService {
|
||||
*/
|
||||
public getChildMenus(parentId: string): MenuItem[] {
|
||||
return this.getAllMenus()
|
||||
.filter(item => item.parentId === parentId)
|
||||
.filter((item) => item.parentId === parentId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export class UIRegistry implements IService {
|
||||
*/
|
||||
public getToolbarItemsByGroup(groupId: string): ToolbarItem[] {
|
||||
return this.getAllToolbarItems()
|
||||
.filter(item => item.groupId === groupId)
|
||||
.filter((item) => item.groupId === groupId)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ export class UIRegistry implements IService {
|
||||
*/
|
||||
public getPanelsByPosition(position: string): PanelDescriptor[] {
|
||||
return this.getAllPanels()
|
||||
.filter(panel => panel.position === position)
|
||||
.filter((panel) => panel.position === position)
|
||||
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -397,7 +397,7 @@ export class Easing {
|
||||
const totalDuration = segments.reduce((sum, seg) => sum + seg.duration, 0);
|
||||
|
||||
// 归一化持续时间
|
||||
const normalizedSegments = segments.map(seg => ({
|
||||
const normalizedSegments = segments.map((seg) => ({
|
||||
...seg,
|
||||
duration: seg.duration / totalDuration
|
||||
}));
|
||||
|
||||
@@ -260,8 +260,8 @@ export class Interpolation {
|
||||
if (points.length === 1) return points[0].clone();
|
||||
if (points.length === 2) return Vector2.lerp(points[0], points[1], t);
|
||||
|
||||
const xPoints = points.map(p => p.x);
|
||||
const yPoints = points.map(p => p.y);
|
||||
const xPoints = points.map((p) => p.x);
|
||||
const yPoints = points.map((p) => p.y);
|
||||
|
||||
return new Vector2(
|
||||
Interpolation.spline(xPoints, t),
|
||||
|
||||
@@ -73,7 +73,7 @@ export class CollisionDetector {
|
||||
// 找到最小距离确定法线方向
|
||||
const minDist = Math.min(distLeft, distRight, distTop, distBottom);
|
||||
let normal: Vector2;
|
||||
let penetration = minDist;
|
||||
const penetration = minDist;
|
||||
|
||||
if (minDist === distLeft) {
|
||||
normal = new Vector2(-1, 0);
|
||||
@@ -252,7 +252,7 @@ export class CollisionDetector {
|
||||
const t2 = (-b + sqrt) / (2 * a);
|
||||
|
||||
// 选择最近的正距离
|
||||
let t = t1 >= 0 ? t1 : t2;
|
||||
const t = t1 >= 0 ? t1 : t2;
|
||||
|
||||
if (t < 0 || t > maxDistance) {
|
||||
return { collided: false };
|
||||
|
||||
@@ -468,7 +468,7 @@ export class MathUtils {
|
||||
* @returns 噪声值(0到1)
|
||||
*/
|
||||
static noise(x: number, y: number = 0, seed: number = 0): number {
|
||||
let n = Math.sin(x * 12.9898 + y * 78.233 + seed * 37.719) * 43758.5453;
|
||||
const n = Math.sin(x * 12.9898 + y * 78.233 + seed * 37.719) * 43758.5453;
|
||||
return n - Math.floor(n);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user