Feature/render pipeline (#232)

* refactor(engine): 重构2D渲染管线坐标系统

* feat(engine): 完善2D渲染管线和编辑器视口功能

* feat(editor): 实现Viewport变换工具系统

* feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示

* feat(editor): 实现Run on Device移动预览功能

* feat(editor): 添加组件属性控制和依赖关系系统

* feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器

* feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm

* feat(ci): 迁移项目到pnpm并修复CI构建问题

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 迁移CI工作流到pnpm并添加WASM构建支持

* chore: 移除 network 相关包

* chore: 移除 network 相关包
This commit is contained in:
YHH
2025-11-23 14:49:37 +08:00
committed by GitHub
parent b15cbab313
commit a3f7cc38b1
247 changed files with 33561 additions and 52047 deletions

View File

@@ -55,6 +55,7 @@
"@rollup/plugin-commonjs": "^28.0.3",
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-terser": "^0.4.4",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.14",
"@types/node": "^20.19.17",
"@eslint/js": "^9.37.0",

View File

@@ -255,7 +255,7 @@ export class SoAStorage<T extends Component> {
for (const key of uint8ClampedFields) decoratedFields.set(key, 'uint8clamped');
// 只遍历实例自身的属性(不包括原型链),跳过 id
const instanceKeys = Object.keys(instance).filter(key => key !== 'id');
const instanceKeys = Object.keys(instance).filter((key) => key !== 'id');
for (const key of instanceKeys) {
const value = (instance as Record<string, unknown>)[key];
@@ -264,31 +264,31 @@ export class SoAStorage<T extends Component> {
// 跳过函数(通常不会出现在 Object.keys 中,但以防万一)
if (type === 'function') continue;
// 检查装饰器类型
const decoratorType = decoratedFields.get(key);
const effectiveType = decoratorType ? 'number' : type;
this.fieldTypes.set(key, effectiveType);
// 检查装饰器类型
const decoratorType = decoratedFields.get(key);
const effectiveType = decoratorType ? 'number' : type;
this.fieldTypes.set(key, effectiveType);
if (decoratorType) {
// 有装饰器标记的数字字段
const ArrayConstructor = SoATypeRegistry.getConstructor(decoratorType as TypedArrayTypeName);
this.fields.set(key, new ArrayConstructor(this._capacity));
} else if (type === 'number') {
// 无装饰器的数字字段,默认使用 Float32Array
this.fields.set(key, new Float32Array(this._capacity));
} else if (type === 'boolean') {
// 布尔值使用 Uint8Array 存储为 0/1
this.fields.set(key, new Uint8Array(this._capacity));
} else if (type === 'string') {
// 字符串专门处理
this.stringFields.set(key, new Array(this._capacity));
} else if (type === 'object' && value !== null) {
// 处理集合类型
if (this.serializeMapFields.has(key) || this.serializeSetFields.has(key) || this.serializeArrayFields.has(key)) {
// 序列化存储
this.serializedFields.set(key, new Array(this._capacity));
}
// 其他对象类型会在updateComponentAtIndex中作为复杂对象处理
if (decoratorType) {
// 有装饰器标记的数字字段
const ArrayConstructor = SoATypeRegistry.getConstructor(decoratorType as TypedArrayTypeName);
this.fields.set(key, new ArrayConstructor(this._capacity));
} else if (type === 'number') {
// 无装饰器的数字字段,默认使用 Float32Array
this.fields.set(key, new Float32Array(this._capacity));
} else if (type === 'boolean') {
// 布尔值使用 Uint8Array 存储为 0/1
this.fields.set(key, new Uint8Array(this._capacity));
} else if (type === 'string') {
// 字符串专门处理
this.stringFields.set(key, new Array(this._capacity));
} else if (type === 'object' && value !== null) {
// 处理集合类型
if (this.serializeMapFields.has(key) || this.serializeSetFields.has(key) || this.serializeArrayFields.has(key)) {
// 序列化存储
this.serializedFields.set(key, new Array(this._capacity));
}
// 其他对象类型会在updateComponentAtIndex中作为复杂对象处理
}
}
}
@@ -515,7 +515,7 @@ export class SoAStorage<T extends Component> {
enumerable: true,
configurable: true,
// entityId 是只读的
writable: propStr !== 'entityId',
writable: propStr !== 'entityId'
};
}
return undefined;

View File

@@ -0,0 +1,94 @@
import 'reflect-metadata';
export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'enum' | 'asset' | 'animationClips';
/**
* Action button configuration for property fields
* 属性字段的操作按钮配置
*/
export interface PropertyAction {
/** Action identifier | 操作标识符 */
id: string;
/** Button label | 按钮标签 */
label: string;
/** Button tooltip | 按钮提示 */
tooltip?: string;
/** Icon name from Lucide | Lucide图标名称 */
icon?: string;
}
/**
* 控制关系声明
* Control relationship declaration
*/
export interface PropertyControl {
/** 被控制的组件名称 | Target component name */
component: string;
/** 被控制的属性名称 | Target property name */
property: string;
}
export interface PropertyOptions {
/** 属性类型 */
type: PropertyType;
/** 显示标签 */
label?: string;
/** 最小值 (number/integer) */
min?: number;
/** 最大值 (number/integer) */
max?: number;
/** 步进值 (number/integer) */
step?: number;
/** 枚举选项 (enum) */
options?: Array<{ label: string; value: any }>;
/** 是否只读 */
readOnly?: boolean;
/** 资源文件扩展名 (asset) */
fileExtension?: string;
/** Action buttons for this property | 属性的操作按钮 */
actions?: PropertyAction[];
/** 此属性控制的其他组件属性 | Properties this field controls */
controls?: PropertyControl[];
}
export const PROPERTY_METADATA = Symbol('property:metadata');
/**
* 属性装饰器 - 声明组件属性的编辑器元数据
*
* @example
* ```typescript
* @ECSComponent('Transform')
* export class TransformComponent extends Component {
* @Property({ type: 'vector3', label: 'Position' })
* public position: Vector3 = { x: 0, y: 0, z: 0 };
*
* @Property({ type: 'number', label: 'Speed', min: 0, max: 100 })
* public speed: number = 10;
* }
* ```
*/
export function Property(options: PropertyOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
const constructor = target.constructor;
const existingMetadata = Reflect.getMetadata(PROPERTY_METADATA, constructor) || {};
existingMetadata[propertyKey as string] = options;
Reflect.defineMetadata(PROPERTY_METADATA, existingMetadata, constructor);
};
}
/**
* 获取组件类的所有属性元数据
*/
export function getPropertyMetadata(target: Function): Record<string, PropertyOptions> | undefined {
return Reflect.getMetadata(PROPERTY_METADATA, target);
}
/**
* 检查组件类是否有属性元数据
*/
export function hasPropertyMetadata(target: Function): boolean {
return Reflect.hasMetadata(PROPERTY_METADATA, target);
}

View File

@@ -7,16 +7,30 @@ import { ComponentType } from '../../Types';
*/
export const COMPONENT_TYPE_NAME = Symbol('ComponentTypeName');
/**
* 存储组件依赖的Symbol键
*/
export const COMPONENT_DEPENDENCIES = Symbol('ComponentDependencies');
/**
* 存储系统类型名称的Symbol键
*/
export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
/**
* 组件装饰器配置选项
*/
export interface ComponentOptions {
/** 依赖的其他组件名称列表 */
requires?: string[];
}
/**
* 组件类型装饰器
* 用于为组件类指定固定的类型名称,避免在代码混淆后失效
*
* @param typeName 组件类型名称
* @param options 组件配置选项
* @example
* ```typescript
* @ECSComponent('Position')
@@ -24,9 +38,15 @@ export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
* x: number = 0;
* y: number = 0;
* }
*
* // 带依赖声明
* @ECSComponent('SpriteAnimator', { requires: ['Sprite'] })
* class SpriteAnimatorComponent extends Component {
* // ...
* }
* ```
*/
export function ECSComponent(typeName: string) {
export function ECSComponent(typeName: string, options?: ComponentOptions) {
return function <T extends new (...args: any[]) => Component>(target: T): T {
if (!typeName || typeof typeName !== 'string') {
throw new Error('ECSComponent装饰器必须提供有效的类型名称');
@@ -35,10 +55,22 @@ export function ECSComponent(typeName: string) {
// 在构造函数上存储类型名称
(target as any)[COMPONENT_TYPE_NAME] = typeName;
// 存储依赖关系
if (options?.requires) {
(target as any)[COMPONENT_DEPENDENCIES] = options.requires;
}
return target;
};
}
/**
* 获取组件的依赖列表
*/
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
return (componentType as any)[COMPONENT_DEPENDENCIES];
}
/**
* System元数据配置
*/

View File

@@ -6,11 +6,13 @@ export {
getComponentInstanceTypeName,
getSystemInstanceTypeName,
getSystemMetadata,
getComponentDependencies,
COMPONENT_TYPE_NAME,
COMPONENT_DEPENDENCIES,
SYSTEM_TYPE_NAME
} from './TypeDecorators';
export type { SystemMetadata } from './TypeDecorators';
export type { SystemMetadata, ComponentOptions } from './TypeDecorators';
export {
EntityRef,
@@ -20,3 +22,12 @@ export {
} from './EntityRefDecorator';
export type { EntityRefMetadata } from './EntityRefDecorator';
export {
Property,
getPropertyMetadata,
hasPropertyMetadata,
PROPERTY_METADATA
} from './PropertyDecorator';
export type { PropertyOptions, PropertyType, PropertyControl } from './PropertyDecorator';

View File

@@ -495,8 +495,6 @@ export interface ISceneDebugData {
sceneEntityCount: number;
/** 场景系统数 */
sceneSystemCount: number;
/** 场景内存使用量 */
sceneMemory: number;
/** 场景启动时间 */
sceneUptime: number;
}

View File

@@ -19,7 +19,6 @@ export class SceneDataCollector {
sceneRunTime: 0,
sceneEntityCount: 0,
sceneSystemCount: 0,
sceneMemory: 0,
sceneUptime: 0
};
}
@@ -36,7 +35,6 @@ export class SceneDataCollector {
sceneRunTime: runTime,
sceneEntityCount: entityList?.buffer?.length || 0,
sceneSystemCount: entityProcessors?.processors?.length || 0,
sceneMemory: 0, // TODO: 计算实际场景内存
sceneUptime: runTime
};
}

View File

@@ -1,12 +1,16 @@
import { Component } from '../../../src/ECS/Component';
import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem';
import {
ECSComponent,
ECSSystem,
getComponentTypeName,
import {
ECSComponent,
ECSSystem,
getComponentTypeName,
getSystemTypeName,
getComponentInstanceTypeName,
getSystemInstanceTypeName
getSystemInstanceTypeName,
getComponentDependencies,
Property,
getPropertyMetadata,
hasPropertyMetadata
} from '../../../src/ECS/Decorators';
describe('TypeDecorators', () => {
@@ -121,4 +125,106 @@ describe('TypeDecorators', () => {
}).toThrow('ECSSystem装饰器必须提供有效的类型名称');
});
});
describe('组件依赖', () => {
test('应该存储和获取组件依赖关系', () => {
@ECSComponent('BaseComponent')
class BaseComponent extends Component {}
@ECSComponent('DependentComponent', { requires: ['BaseComponent'] })
class DependentComponent extends Component {}
const dependencies = getComponentDependencies(DependentComponent);
expect(dependencies).toEqual(['BaseComponent']);
});
test('没有依赖的组件应该返回undefined', () => {
@ECSComponent('IndependentComponent')
class IndependentComponent extends Component {}
const dependencies = getComponentDependencies(IndependentComponent);
expect(dependencies).toBeUndefined();
});
test('应该支持多个依赖', () => {
@ECSComponent('MultiDependentComponent', { requires: ['ComponentA', 'ComponentB', 'ComponentC'] })
class MultiDependentComponent extends Component {}
const dependencies = getComponentDependencies(MultiDependentComponent);
expect(dependencies).toEqual(['ComponentA', 'ComponentB', 'ComponentC']);
});
});
describe('@Property 装饰器', () => {
test('应该为属性设置元数据', () => {
@ECSComponent('PropertyTestComponent')
class PropertyTestComponent extends Component {
@Property({ type: 'number', label: 'Speed' })
public speed: number = 10;
}
const metadata = getPropertyMetadata(PropertyTestComponent);
expect(metadata).toBeDefined();
expect(metadata!['speed']).toEqual({ type: 'number', label: 'Speed' });
});
test('应该支持多个属性装饰器', () => {
@ECSComponent('MultiPropertyComponent')
class MultiPropertyComponent extends Component {
@Property({ type: 'number', label: 'X Position' })
public x: number = 0;
@Property({ type: 'number', label: 'Y Position' })
public y: number = 0;
@Property({ type: 'string', label: 'Name' })
public name: string = '';
}
const metadata = getPropertyMetadata(MultiPropertyComponent);
expect(metadata).toBeDefined();
expect(metadata!['x']).toEqual({ type: 'number', label: 'X Position' });
expect(metadata!['y']).toEqual({ type: 'number', label: 'Y Position' });
expect(metadata!['name']).toEqual({ type: 'string', label: 'Name' });
});
test('hasPropertyMetadata 应该正确检测属性元数据', () => {
@ECSComponent('HasMetadataComponent')
class HasMetadataComponent extends Component {
@Property({ type: 'boolean' })
public active: boolean = true;
}
@ECSComponent('NoMetadataComponent')
class NoMetadataComponent extends Component {
public value: number = 0;
}
expect(hasPropertyMetadata(HasMetadataComponent)).toBe(true);
expect(hasPropertyMetadata(NoMetadataComponent)).toBe(false);
});
test('应该支持完整的属性选项', () => {
@ECSComponent('FullOptionsComponent')
class FullOptionsComponent extends Component {
@Property({
type: 'number',
label: 'Health',
min: 0,
max: 100,
step: 1
})
public health: number = 100;
}
const metadata = getPropertyMetadata(FullOptionsComponent);
expect(metadata!['health']).toEqual({
type: 'number',
label: 'Health',
min: 0,
max: 100,
step: 1
});
});
});
});