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:
52
packages/components/package.json
Normal file
52
packages/components/package.json
Normal file
@@ -0,0 +1,52 @@
|
||||
{
|
||||
"name": "@esengine/ecs-components",
|
||||
"version": "1.0.0",
|
||||
"description": "Standard component library for ECS Framework",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./bin/index.d.ts",
|
||||
"import": "./bin/index.js",
|
||||
"development": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"bin/**/*"
|
||||
],
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"components",
|
||||
"game-engine",
|
||||
"typescript"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf bin dist tsconfig.tsbuildinfo",
|
||||
"build:ts": "tsc",
|
||||
"prebuild": "npm run clean",
|
||||
"build": "npm run build:ts",
|
||||
"build:watch": "tsc --watch",
|
||||
"rebuild": "npm run clean && npm run build"
|
||||
},
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": "^2.2.8",
|
||||
"@esengine/asset-system": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/components"
|
||||
}
|
||||
}
|
||||
39
packages/components/src/AudioSourceComponent.ts
Normal file
39
packages/components/src/AudioSourceComponent.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 音频源组件 - 管理音频播放
|
||||
*/
|
||||
@ECSComponent('AudioSource')
|
||||
@Serializable({ version: 1, typeId: 'AudioSource' })
|
||||
export class AudioSourceComponent extends Component {
|
||||
/** 音频资源路径 */
|
||||
@Serialize() public clip: string = '';
|
||||
|
||||
/** 音量 (0-1) */
|
||||
@Serialize() public volume: number = 1;
|
||||
|
||||
/** 音调 */
|
||||
@Serialize() public pitch: number = 1;
|
||||
|
||||
/** 是否循环 */
|
||||
@Serialize() public loop: boolean = false;
|
||||
|
||||
/** 是否启动时播放 */
|
||||
@Serialize() public playOnAwake: boolean = false;
|
||||
|
||||
/** 是否静音 */
|
||||
@Serialize() public mute: boolean = false;
|
||||
|
||||
/** 空间混合 (0=2D, 1=3D) */
|
||||
@Serialize() public spatialBlend: number = 0;
|
||||
|
||||
/** 最小距离(3D音效) */
|
||||
@Serialize() public minDistance: number = 1;
|
||||
|
||||
/** 最大距离(3D音效) */
|
||||
@Serialize() public maxDistance: number = 500;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
33
packages/components/src/BoxColliderComponent.ts
Normal file
33
packages/components/src/BoxColliderComponent.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 盒型碰撞器组件
|
||||
*/
|
||||
@ECSComponent('BoxCollider')
|
||||
@Serializable({ version: 1, typeId: 'BoxCollider' })
|
||||
export class BoxColliderComponent extends Component {
|
||||
/** 是否为触发器 */
|
||||
@Serialize() public isTrigger: boolean = false;
|
||||
|
||||
/** 中心点X偏移 */
|
||||
@Serialize() public centerX: number = 0;
|
||||
|
||||
/** 中心点Y偏移 */
|
||||
@Serialize() public centerY: number = 0;
|
||||
|
||||
/** 中心点Z偏移 */
|
||||
@Serialize() public centerZ: number = 0;
|
||||
|
||||
/** 宽度 */
|
||||
@Serialize() public width: number = 1;
|
||||
|
||||
/** 高度 */
|
||||
@Serialize() public height: number = 1;
|
||||
|
||||
/** 深度 */
|
||||
@Serialize() public depth: number = 1;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
82
packages/components/src/CameraComponent.ts
Normal file
82
packages/components/src/CameraComponent.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 相机投影类型
|
||||
*/
|
||||
export enum CameraProjection {
|
||||
Perspective = 'perspective',
|
||||
Orthographic = 'orthographic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 相机组件 - 管理视图和投影
|
||||
*/
|
||||
@ECSComponent('Camera')
|
||||
@Serializable({ version: 1, typeId: 'Camera' })
|
||||
export class CameraComponent extends Component {
|
||||
/** 投影类型 */
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Projection',
|
||||
options: [
|
||||
{ label: 'Orthographic', value: CameraProjection.Orthographic },
|
||||
{ label: 'Perspective', value: CameraProjection.Perspective }
|
||||
]
|
||||
})
|
||||
public projection: CameraProjection = CameraProjection.Orthographic;
|
||||
|
||||
/** 视野角度(透视模式) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Field of View', min: 1, max: 179 })
|
||||
public fieldOfView: number = 60;
|
||||
|
||||
/** 正交尺寸(正交模式) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Orthographic Size', min: 0.1, step: 0.1 })
|
||||
public orthographicSize: number = 5;
|
||||
|
||||
/** 近裁剪面 */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Near Clip', min: 0.01, step: 0.1 })
|
||||
public nearClipPlane: number = 0.1;
|
||||
|
||||
/** 远裁剪面 */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Far Clip', min: 1, step: 10 })
|
||||
public farClipPlane: number = 1000;
|
||||
|
||||
/** 视口X */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Viewport X', min: 0, max: 1, step: 0.01 })
|
||||
public viewportX: number = 0;
|
||||
|
||||
/** 视口Y */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Viewport Y', min: 0, max: 1, step: 0.01 })
|
||||
public viewportY: number = 0;
|
||||
|
||||
/** 视口宽度 */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Viewport Width', min: 0, max: 1, step: 0.01 })
|
||||
public viewportWidth: number = 1;
|
||||
|
||||
/** 视口高度 */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Viewport Height', min: 0, max: 1, step: 0.01 })
|
||||
public viewportHeight: number = 1;
|
||||
|
||||
/** 渲染优先级 */
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Depth' })
|
||||
public depth: number = 0;
|
||||
|
||||
/** 背景颜色 */
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Background Color' })
|
||||
public backgroundColor: string = '#000000';
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
24
packages/components/src/CircleColliderComponent.ts
Normal file
24
packages/components/src/CircleColliderComponent.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 圆形碰撞器组件
|
||||
*/
|
||||
@ECSComponent('CircleCollider')
|
||||
@Serializable({ version: 1, typeId: 'CircleCollider' })
|
||||
export class CircleColliderComponent extends Component {
|
||||
/** 是否为触发器 */
|
||||
@Serialize() public isTrigger: boolean = false;
|
||||
|
||||
/** 中心点X偏移 */
|
||||
@Serialize() public centerX: number = 0;
|
||||
|
||||
/** 中心点Y偏移 */
|
||||
@Serialize() public centerY: number = 0;
|
||||
|
||||
/** 半径 */
|
||||
@Serialize() public radius: number = 0.5;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
57
packages/components/src/RigidBodyComponent.ts
Normal file
57
packages/components/src/RigidBodyComponent.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 刚体类型
|
||||
*/
|
||||
export enum BodyType {
|
||||
Static = 'static',
|
||||
Dynamic = 'dynamic',
|
||||
Kinematic = 'kinematic'
|
||||
}
|
||||
|
||||
/**
|
||||
* 刚体组件 - 管理物理模拟
|
||||
*/
|
||||
@ECSComponent('RigidBody')
|
||||
@Serializable({ version: 1, typeId: 'RigidBody' })
|
||||
export class RigidBodyComponent extends Component {
|
||||
/** 刚体类型 */
|
||||
@Serialize() public bodyType: BodyType = BodyType.Dynamic;
|
||||
|
||||
/** 质量 */
|
||||
@Serialize() public mass: number = 1;
|
||||
|
||||
/** 线性阻尼 */
|
||||
@Serialize() public linearDamping: number = 0;
|
||||
|
||||
/** 角阻尼 */
|
||||
@Serialize() public angularDamping: number = 0.05;
|
||||
|
||||
/** 重力缩放 */
|
||||
@Serialize() public gravityScale: number = 1;
|
||||
|
||||
/** 是否使用连续碰撞检测 */
|
||||
@Serialize() public continuousDetection: boolean = false;
|
||||
|
||||
/** 是否冻结X轴旋转 */
|
||||
@Serialize() public freezeRotationX: boolean = false;
|
||||
|
||||
/** 是否冻结Y轴旋转 */
|
||||
@Serialize() public freezeRotationY: boolean = false;
|
||||
|
||||
/** 是否冻结Z轴旋转 */
|
||||
@Serialize() public freezeRotationZ: boolean = false;
|
||||
|
||||
/** X轴速度 */
|
||||
@Serialize() public velocityX: number = 0;
|
||||
|
||||
/** Y轴速度 */
|
||||
@Serialize() public velocityY: number = 0;
|
||||
|
||||
/** Z轴速度 */
|
||||
@Serialize() public velocityZ: number = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
369
packages/components/src/SpriteAnimatorComponent.ts
Normal file
369
packages/components/src/SpriteAnimatorComponent.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 动画帧数据
|
||||
* Animation frame data
|
||||
*/
|
||||
export interface AnimationFrame {
|
||||
/** 纹理路径 | Texture path */
|
||||
texture: string;
|
||||
/** 帧持续时间(秒) | Frame duration in seconds */
|
||||
duration: number;
|
||||
/** UV坐标 [u0, v0, u1, v1] | UV coordinates */
|
||||
uv?: [number, number, number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
* 动画剪辑数据
|
||||
* Animation clip data
|
||||
*/
|
||||
export interface AnimationClip {
|
||||
/** 动画名称 | Animation name */
|
||||
name: string;
|
||||
/** 动画帧列表 | Animation frames */
|
||||
frames: AnimationFrame[];
|
||||
/** 是否循环 | Whether to loop */
|
||||
loop: boolean;
|
||||
/** 播放速度倍数 | Playback speed multiplier */
|
||||
speed: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 精灵动画组件 - 管理精灵帧动画
|
||||
* Sprite animator component - manages sprite frame animation
|
||||
*/
|
||||
@ECSComponent('SpriteAnimator', { requires: ['Sprite'] })
|
||||
@Serializable({ version: 1, typeId: 'SpriteAnimator' })
|
||||
export class SpriteAnimatorComponent extends Component {
|
||||
/**
|
||||
* 动画剪辑列表
|
||||
* Animation clips
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'animationClips',
|
||||
label: 'Animation Clips',
|
||||
controls: [{ component: 'Sprite', property: 'texture' }]
|
||||
})
|
||||
public clips: AnimationClip[] = [];
|
||||
|
||||
/**
|
||||
* 当前播放的动画名称
|
||||
* Currently playing animation name
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'string', label: 'Default Animation' })
|
||||
public defaultAnimation: string = '';
|
||||
|
||||
/**
|
||||
* 是否自动播放
|
||||
* Auto play on start
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Auto Play' })
|
||||
public autoPlay: boolean = true;
|
||||
|
||||
/**
|
||||
* 全局播放速度
|
||||
* Global playback speed
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Speed', min: 0, max: 10, step: 0.1 })
|
||||
public speed: number = 1;
|
||||
|
||||
// Runtime state (not serialized)
|
||||
private _currentClip: AnimationClip | null = null;
|
||||
private _currentFrameIndex: number = 0;
|
||||
private _frameTimer: number = 0;
|
||||
private _isPlaying: boolean = false;
|
||||
private _isPaused: boolean = false;
|
||||
|
||||
// Callbacks
|
||||
private _onAnimationComplete?: (clipName: string) => void;
|
||||
private _onFrameChange?: (frameIndex: number, frame: AnimationFrame) => void;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加动画剪辑
|
||||
* Add animation clip
|
||||
*/
|
||||
addClip(clip: AnimationClip): void {
|
||||
// Remove existing clip with same name
|
||||
this.clips = this.clips.filter((c) => c.name !== clip.name);
|
||||
this.clips.push(clip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从精灵图集创建动画剪辑
|
||||
* Create animation clip from sprite atlas
|
||||
*
|
||||
* @param name - 动画名称 | Animation name
|
||||
* @param texture - 纹理路径 | Texture path
|
||||
* @param frameCount - 帧数 | Number of frames
|
||||
* @param frameWidth - 每帧宽度 | Frame width
|
||||
* @param frameHeight - 每帧高度 | Frame height
|
||||
* @param atlasWidth - 图集宽度 | Atlas width
|
||||
* @param atlasHeight - 图集高度 | Atlas height
|
||||
* @param fps - 帧率 | Frames per second
|
||||
* @param loop - 是否循环 | Whether to loop
|
||||
*/
|
||||
createClipFromAtlas(
|
||||
name: string,
|
||||
texture: string,
|
||||
frameCount: number,
|
||||
frameWidth: number,
|
||||
frameHeight: number,
|
||||
atlasWidth: number,
|
||||
atlasHeight: number,
|
||||
fps: number = 12,
|
||||
loop: boolean = true
|
||||
): AnimationClip {
|
||||
const frames: AnimationFrame[] = [];
|
||||
const duration = 1 / fps;
|
||||
const cols = Math.floor(atlasWidth / frameWidth);
|
||||
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
const col = i % cols;
|
||||
const row = Math.floor(i / cols);
|
||||
const x = col * frameWidth;
|
||||
const y = row * frameHeight;
|
||||
|
||||
frames.push({
|
||||
texture,
|
||||
duration,
|
||||
uv: [
|
||||
x / atlasWidth,
|
||||
y / atlasHeight,
|
||||
(x + frameWidth) / atlasWidth,
|
||||
(y + frameHeight) / atlasHeight
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
const clip: AnimationClip = {
|
||||
name,
|
||||
frames,
|
||||
loop,
|
||||
speed: 1
|
||||
};
|
||||
|
||||
this.addClip(clip);
|
||||
return clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从帧序列创建动画剪辑
|
||||
* Create animation clip from frame sequence
|
||||
*
|
||||
* @param name - 动画名称 | Animation name
|
||||
* @param textures - 纹理路径数组 | Array of texture paths
|
||||
* @param fps - 帧率 | Frames per second
|
||||
* @param loop - 是否循环 | Whether to loop
|
||||
*/
|
||||
createClipFromSequence(
|
||||
name: string,
|
||||
textures: string[],
|
||||
fps: number = 12,
|
||||
loop: boolean = true
|
||||
): AnimationClip {
|
||||
const duration = 1 / fps;
|
||||
const frames: AnimationFrame[] = textures.map((texture) => ({
|
||||
texture,
|
||||
duration
|
||||
}));
|
||||
|
||||
const clip: AnimationClip = {
|
||||
name,
|
||||
frames,
|
||||
loop,
|
||||
speed: 1
|
||||
};
|
||||
|
||||
this.addClip(clip);
|
||||
return clip;
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放动画
|
||||
* Play animation
|
||||
*/
|
||||
play(clipName?: string): void {
|
||||
const name = clipName || this.defaultAnimation;
|
||||
if (!name) return;
|
||||
|
||||
const clip = this.clips.find((c) => c.name === name);
|
||||
if (!clip || clip.frames.length === 0) {
|
||||
console.warn(`Animation clip not found: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentClip = clip;
|
||||
this._currentFrameIndex = 0;
|
||||
this._frameTimer = 0;
|
||||
this._isPlaying = true;
|
||||
this._isPaused = false;
|
||||
|
||||
this._notifyFrameChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止动画
|
||||
* Stop animation
|
||||
*/
|
||||
stop(): void {
|
||||
this._isPlaying = false;
|
||||
this._isPaused = false;
|
||||
this._currentFrameIndex = 0;
|
||||
this._frameTimer = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停动画
|
||||
* Pause animation
|
||||
*/
|
||||
pause(): void {
|
||||
if (this._isPlaying) {
|
||||
this._isPaused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复动画
|
||||
* Resume animation
|
||||
*/
|
||||
resume(): void {
|
||||
if (this._isPlaying && this._isPaused) {
|
||||
this._isPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新动画(由系统调用)
|
||||
* Update animation (called by system)
|
||||
*/
|
||||
update(deltaTime: number): void {
|
||||
if (!this._isPlaying || this._isPaused || !this._currentClip) return;
|
||||
|
||||
const clip = this._currentClip;
|
||||
const frame = clip.frames[this._currentFrameIndex];
|
||||
if (!frame) return;
|
||||
|
||||
this._frameTimer += deltaTime * this.speed * clip.speed;
|
||||
|
||||
if (this._frameTimer >= frame.duration) {
|
||||
this._frameTimer -= frame.duration;
|
||||
this._currentFrameIndex++;
|
||||
|
||||
if (this._currentFrameIndex >= clip.frames.length) {
|
||||
if (clip.loop) {
|
||||
this._currentFrameIndex = 0;
|
||||
} else {
|
||||
this._currentFrameIndex = clip.frames.length - 1;
|
||||
this._isPlaying = false;
|
||||
this._onAnimationComplete?.(clip.name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._notifyFrameChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧
|
||||
* Get current frame
|
||||
*/
|
||||
getCurrentFrame(): AnimationFrame | null {
|
||||
if (!this._currentClip) return null;
|
||||
return this._currentClip.frames[this._currentFrameIndex] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧索引
|
||||
* Get current frame index
|
||||
*/
|
||||
getCurrentFrameIndex(): number {
|
||||
return this._currentFrameIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前帧
|
||||
* Set current frame
|
||||
*/
|
||||
setFrame(index: number): void {
|
||||
if (!this._currentClip) return;
|
||||
this._currentFrameIndex = Math.max(0, Math.min(index, this._currentClip.frames.length - 1));
|
||||
this._frameTimer = 0;
|
||||
this._notifyFrameChange();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否正在播放
|
||||
* Whether animation is playing
|
||||
*/
|
||||
isPlaying(): boolean {
|
||||
return this._isPlaying && !this._isPaused;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前动画名称
|
||||
* Get current animation name
|
||||
*/
|
||||
getCurrentClipName(): string | null {
|
||||
return this._currentClip?.name || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置动画完成回调
|
||||
* Set animation complete callback
|
||||
*/
|
||||
onAnimationComplete(callback: (clipName: string) => void): void {
|
||||
this._onAnimationComplete = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置帧变化回调
|
||||
* Set frame change callback
|
||||
*/
|
||||
onFrameChange(callback: (frameIndex: number, frame: AnimationFrame) => void): void {
|
||||
this._onFrameChange = callback;
|
||||
}
|
||||
|
||||
private _notifyFrameChange(): void {
|
||||
const frame = this.getCurrentFrame();
|
||||
if (frame) {
|
||||
this._onFrameChange?.(this._currentFrameIndex, frame);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动画剪辑
|
||||
* Get animation clip by name
|
||||
*/
|
||||
getClip(name: string): AnimationClip | undefined {
|
||||
return this.clips.find((c) => c.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除动画剪辑
|
||||
* Remove animation clip
|
||||
*/
|
||||
removeClip(name: string): void {
|
||||
this.clips = this.clips.filter((c) => c.name !== name);
|
||||
if (this._currentClip?.name === name) {
|
||||
this.stop();
|
||||
this._currentClip = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有动画名称
|
||||
* Get all animation names
|
||||
*/
|
||||
getClipNames(): string[] {
|
||||
return this.clips.map((c) => c.name);
|
||||
}
|
||||
}
|
||||
243
packages/components/src/SpriteComponent.ts
Normal file
243
packages/components/src/SpriteComponent.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
import type { AssetReference } from '@esengine/asset-system';
|
||||
|
||||
/**
|
||||
* 精灵组件 - 管理2D图像渲染
|
||||
* Sprite component - manages 2D image rendering
|
||||
*/
|
||||
@ECSComponent('Sprite')
|
||||
@Serializable({ version: 2, typeId: 'Sprite' })
|
||||
export class SpriteComponent extends Component {
|
||||
/** 纹理路径或资源ID | Texture path or asset ID */
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', fileExtension: '.png' })
|
||||
public texture: string = '';
|
||||
|
||||
/**
|
||||
* 资产GUID(新的资产系统)
|
||||
* Asset GUID for new asset system
|
||||
*/
|
||||
@Serialize()
|
||||
public assetGuid?: string;
|
||||
|
||||
/**
|
||||
* 纹理ID(运行时使用)
|
||||
* Texture ID for runtime rendering
|
||||
*/
|
||||
public textureId: number = 0;
|
||||
|
||||
/**
|
||||
* 资产引用(运行时,不序列化)
|
||||
* Asset reference (runtime only, not serialized)
|
||||
*/
|
||||
private _assetReference?: AssetReference<HTMLImageElement>;
|
||||
|
||||
/**
|
||||
* 精灵宽度(像素)
|
||||
* Sprite width in pixels
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'number',
|
||||
label: 'Width',
|
||||
min: 0,
|
||||
actions: [{
|
||||
id: 'nativeSize',
|
||||
label: 'Native',
|
||||
tooltip: 'Set to texture native size',
|
||||
icon: 'Maximize2'
|
||||
}]
|
||||
})
|
||||
public width: number = 64;
|
||||
|
||||
/**
|
||||
* 精灵高度(像素)
|
||||
* Sprite height in pixels
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'number',
|
||||
label: 'Height',
|
||||
min: 0,
|
||||
actions: [{
|
||||
id: 'nativeSize',
|
||||
label: 'Native',
|
||||
tooltip: 'Set to texture native size',
|
||||
icon: 'Maximize2'
|
||||
}]
|
||||
})
|
||||
public height: number = 64;
|
||||
|
||||
/**
|
||||
* UV坐标 [u0, v0, u1, v1]
|
||||
* UV coordinates [u0, v0, u1, v1]
|
||||
* 默认为完整纹理 [0, 0, 1, 1]
|
||||
* Default is full texture [0, 0, 1, 1]
|
||||
*/
|
||||
@Serialize()
|
||||
public uv: [number, number, number, number] = [0, 0, 1, 1];
|
||||
|
||||
/** 颜色(十六进制)| Color (hex string) */
|
||||
@Serialize()
|
||||
@Property({ type: 'color', label: 'Color' })
|
||||
public color: string = '#ffffff';
|
||||
|
||||
/** 透明度 (0-1) | Alpha (0-1) */
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Alpha', min: 0, max: 1, step: 0.01 })
|
||||
public alpha: number = 1;
|
||||
|
||||
/**
|
||||
* 原点X (0-1, 0.5为中心)
|
||||
* Origin point X (0-1, where 0.5 is center)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Origin X', min: 0, max: 1, step: 0.01 })
|
||||
public originX: number = 0.5;
|
||||
|
||||
/**
|
||||
* 原点Y (0-1, 0.5为中心)
|
||||
* Origin point Y (0-1, where 0.5 is center)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Origin Y', min: 0, max: 1, step: 0.01 })
|
||||
public originY: number = 0.5;
|
||||
|
||||
/**
|
||||
* 精灵是否可见
|
||||
* Whether sprite is visible
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Visible' })
|
||||
public visible: boolean = true;
|
||||
|
||||
/** 是否水平翻转 | Flip sprite horizontally */
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Flip X' })
|
||||
public flipX: boolean = false;
|
||||
|
||||
/** 是否垂直翻转 | Flip sprite vertically */
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Flip Y' })
|
||||
public flipY: boolean = false;
|
||||
|
||||
/**
|
||||
* 渲染层级/顺序(越高越在上面)
|
||||
* Render layer/order (higher = rendered on top)
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Sorting Order' })
|
||||
public sortingOrder: number = 0;
|
||||
|
||||
/** 锚点X (0-1) - 别名为originX | Anchor X (0-1) - alias for originX */
|
||||
get anchorX(): number {
|
||||
return this.originX;
|
||||
}
|
||||
set anchorX(value: number) {
|
||||
this.originX = value;
|
||||
}
|
||||
|
||||
/** 锚点Y (0-1) - 别名为originY | Anchor Y (0-1) - alias for originY */
|
||||
get anchorY(): number {
|
||||
return this.originY;
|
||||
}
|
||||
set anchorY(value: number) {
|
||||
this.originY = value;
|
||||
}
|
||||
|
||||
constructor(texture: string = '') {
|
||||
super();
|
||||
this.texture = texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从精灵图集区域设置UV
|
||||
* Set UV from a sprite atlas region
|
||||
*
|
||||
* @param x - 区域X(像素)| Region X in pixels
|
||||
* @param y - 区域Y(像素)| Region Y in pixels
|
||||
* @param w - 区域宽度(像素)| Region width in pixels
|
||||
* @param h - 区域高度(像素)| Region height in pixels
|
||||
* @param atlasWidth - 图集总宽度 | Atlas total width
|
||||
* @param atlasHeight - 图集总高度 | Atlas total height
|
||||
*/
|
||||
setAtlasRegion(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
atlasWidth: number,
|
||||
atlasHeight: number
|
||||
): void {
|
||||
this.uv = [
|
||||
x / atlasWidth,
|
||||
y / atlasHeight,
|
||||
(x + w) / atlasWidth,
|
||||
(y + h) / atlasHeight
|
||||
];
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置资产引用
|
||||
* Set asset reference
|
||||
*/
|
||||
setAssetReference(reference: AssetReference<HTMLImageElement>): void {
|
||||
// 释放旧引用 / Release old reference
|
||||
if (this._assetReference) {
|
||||
this._assetReference.release();
|
||||
}
|
||||
this._assetReference = reference;
|
||||
if (reference) {
|
||||
this.assetGuid = reference.guid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产引用
|
||||
* Get asset reference
|
||||
*/
|
||||
getAssetReference(): AssetReference<HTMLImageElement> | undefined {
|
||||
return this._assetReference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载纹理
|
||||
* Load texture asynchronously
|
||||
*/
|
||||
async loadTextureAsync(): Promise<void> {
|
||||
if (this._assetReference) {
|
||||
try {
|
||||
const textureAsset = await this._assetReference.loadAsync();
|
||||
// 如果纹理资产有 textureId 属性,使用它
|
||||
// If texture asset has textureId property, use it
|
||||
if (textureAsset && typeof textureAsset === 'object' && 'textureId' in textureAsset) {
|
||||
this.textureId = (textureAsset as any).textureId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load texture:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的纹理源
|
||||
* Get effective texture source
|
||||
*/
|
||||
getTextureSource(): string {
|
||||
return this.assetGuid || this.texture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时调用
|
||||
* Called when component is destroyed
|
||||
*/
|
||||
onDestroy(): void {
|
||||
// 释放资产引用 / Release asset reference
|
||||
if (this._assetReference) {
|
||||
this._assetReference.release();
|
||||
this._assetReference = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
46
packages/components/src/TextComponent.ts
Normal file
46
packages/components/src/TextComponent.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 文本对齐方式
|
||||
*/
|
||||
export enum TextAlignment {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Right = 'right'
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本组件 - 管理文本渲染
|
||||
*/
|
||||
@ECSComponent('Text')
|
||||
@Serializable({ version: 1, typeId: 'Text' })
|
||||
export class TextComponent extends Component {
|
||||
/** 文本内容 */
|
||||
@Serialize() public text: string = '';
|
||||
|
||||
/** 字体 */
|
||||
@Serialize() public font: string = 'Arial';
|
||||
|
||||
/** 字体大小 */
|
||||
@Serialize() public fontSize: number = 16;
|
||||
|
||||
/** 颜色 */
|
||||
@Serialize() public color: string = '#ffffff';
|
||||
|
||||
/** 对齐方式 */
|
||||
@Serialize() public alignment: TextAlignment = TextAlignment.Left;
|
||||
|
||||
/** 行高 */
|
||||
@Serialize() public lineHeight: number = 1.2;
|
||||
|
||||
/** 是否加粗 */
|
||||
@Serialize() public bold: boolean = false;
|
||||
|
||||
/** 是否斜体 */
|
||||
@Serialize() public italic: boolean = false;
|
||||
|
||||
constructor(text: string = '') {
|
||||
super();
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
61
packages/components/src/TransformComponent.ts
Normal file
61
packages/components/src/TransformComponent.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 3D向量
|
||||
*/
|
||||
export interface Vector3 {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变换组件 - 管理实体的位置、旋转和缩放
|
||||
*/
|
||||
@ECSComponent('Transform')
|
||||
@Serializable({ version: 1, typeId: 'Transform' })
|
||||
export class TransformComponent extends Component {
|
||||
/** 位置 */
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Position' })
|
||||
public position: Vector3 = { x: 0, y: 0, z: 0 };
|
||||
|
||||
/** 旋转(欧拉角,度) */
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Rotation' })
|
||||
public rotation: Vector3 = { x: 0, y: 0, z: 0 };
|
||||
|
||||
/** 缩放 */
|
||||
@Serialize()
|
||||
@Property({ type: 'vector3', label: 'Scale' })
|
||||
public scale: Vector3 = { x: 1, y: 1, z: 1 };
|
||||
|
||||
constructor(x: number = 0, y: number = 0, z: number = 0) {
|
||||
super();
|
||||
this.position = { x, y, z };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置位置
|
||||
*/
|
||||
public setPosition(x: number, y: number, z: number = 0): this {
|
||||
this.position = { x, y, z };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置旋转
|
||||
*/
|
||||
public setRotation(x: number, y: number, z: number): this {
|
||||
this.rotation = { x, y, z };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置缩放
|
||||
*/
|
||||
public setScale(x: number, y: number, z: number = 1): this {
|
||||
this.scale = { x, y, z };
|
||||
return this;
|
||||
}
|
||||
}
|
||||
19
packages/components/src/index.ts
Normal file
19
packages/components/src/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// 变换
|
||||
export { TransformComponent, Vector3 } from './TransformComponent';
|
||||
|
||||
// 渲染
|
||||
export { SpriteComponent } from './SpriteComponent';
|
||||
export { SpriteAnimatorComponent, AnimationFrame, AnimationClip } from './SpriteAnimatorComponent';
|
||||
export { TextComponent, TextAlignment } from './TextComponent';
|
||||
export { CameraComponent, CameraProjection } from './CameraComponent';
|
||||
|
||||
// 系统
|
||||
export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem';
|
||||
|
||||
// 物理
|
||||
export { RigidBodyComponent, BodyType } from './RigidBodyComponent';
|
||||
export { BoxColliderComponent } from './BoxColliderComponent';
|
||||
export { CircleColliderComponent } from './CircleColliderComponent';
|
||||
|
||||
// 音频
|
||||
export { AudioSourceComponent } from './AudioSourceComponent';
|
||||
86
packages/components/src/systems/SpriteAnimatorSystem.ts
Normal file
86
packages/components/src/systems/SpriteAnimatorSystem.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { EntitySystem, Matcher, ECSSystem, Time, Entity } from '@esengine/ecs-framework';
|
||||
import { SpriteAnimatorComponent } from '../SpriteAnimatorComponent';
|
||||
import { SpriteComponent } from '../SpriteComponent';
|
||||
|
||||
/**
|
||||
* 精灵动画系统 - 更新所有精灵动画
|
||||
* Sprite animator system - updates all sprite animations
|
||||
*/
|
||||
@ECSSystem('SpriteAnimator', { updateOrder: 50 })
|
||||
export class SpriteAnimatorSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(SpriteAnimatorComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统初始化时调用
|
||||
* Called when system is initialized
|
||||
*/
|
||||
protected override onInitialize(): void {
|
||||
// System initialized
|
||||
}
|
||||
|
||||
/**
|
||||
* 每帧开始时调用
|
||||
* Called at the beginning of each frame
|
||||
*/
|
||||
protected override onBegin(): void {
|
||||
// Frame begin
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理匹配的实体
|
||||
* Process matched entities
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent) as SpriteAnimatorComponent | null;
|
||||
if (!animator) continue;
|
||||
|
||||
// Only call update if playing
|
||||
if (animator.isPlaying()) {
|
||||
animator.update(deltaTime);
|
||||
}
|
||||
|
||||
// Sync current frame to sprite component (always, even if not playing)
|
||||
const sprite = entity.getComponent(SpriteComponent) as SpriteComponent | null;
|
||||
if (sprite) {
|
||||
const frame = animator.getCurrentFrame();
|
||||
if (frame) {
|
||||
sprite.texture = frame.texture;
|
||||
|
||||
// Update UV if specified
|
||||
if (frame.uv) {
|
||||
sprite.uv = frame.uv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体添加到系统时调用
|
||||
* Called when entity is added to system
|
||||
*/
|
||||
protected override onAdded(entity: Entity): void {
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent) as SpriteAnimatorComponent | null;
|
||||
if (animator && animator.autoPlay && animator.defaultAnimation) {
|
||||
animator.play();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体从系统移除时调用
|
||||
* Called when entity is removed from system
|
||||
*/
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent) as SpriteAnimatorComponent | null;
|
||||
if (animator) {
|
||||
animator.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
51
packages/components/tsconfig.json
Normal file
51
packages/components/tsconfig.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": false,
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./bin",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"removeComments": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noImplicitThis": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": false,
|
||||
"noUncheckedIndexedAccess": false,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"importHelpers": true,
|
||||
"downlevelIteration": true,
|
||||
"isolatedModules": false,
|
||||
"allowJs": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"bin",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user