refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
39
packages/rendering/sprite/module.json
Normal file
39
packages/rendering/sprite/module.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "sprite",
|
||||
"name": "@esengine/sprite",
|
||||
"globalKey": "sprite",
|
||||
"displayName": "Sprite 2D",
|
||||
"description": "2D sprite rendering | 2D 精灵渲染",
|
||||
"version": "1.0.0",
|
||||
"category": "Rendering",
|
||||
"icon": "Image",
|
||||
"tags": [
|
||||
"2d",
|
||||
"sprite",
|
||||
"rendering"
|
||||
],
|
||||
"isCore": false,
|
||||
"defaultEnabled": true,
|
||||
"isEngineModule": true,
|
||||
"canContainContent": true,
|
||||
"platforms": [
|
||||
"web",
|
||||
"desktop"
|
||||
],
|
||||
"dependencies": [
|
||||
"core",
|
||||
"math"
|
||||
],
|
||||
"exports": {
|
||||
"components": [
|
||||
"SpriteComponent"
|
||||
],
|
||||
"systems": [
|
||||
"SpriteRenderSystem"
|
||||
]
|
||||
},
|
||||
"editorPackage": "@esengine/sprite-editor",
|
||||
"requiresWasm": true,
|
||||
"outputPath": "dist/index.js",
|
||||
"pluginExport": "SpritePlugin"
|
||||
}
|
||||
49
packages/rendering/sprite/package.json
Normal file
49
packages/rendering/sprite/package.json
Normal file
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "@esengine/sprite",
|
||||
"version": "1.0.0",
|
||||
"description": "ECS-based 2D sprite rendering and animation system",
|
||||
"esengine": {
|
||||
"plugin": true,
|
||||
"pluginExport": "SpritePlugin",
|
||||
"category": "rendering",
|
||||
"isEnginePlugin": true
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rimraf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/asset-system": "workspace:*",
|
||||
"@esengine/engine-core": "workspace:*",
|
||||
"@esengine/material-system": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"rimraf": "^5.0.5",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"sprite",
|
||||
"animation",
|
||||
"2d",
|
||||
"webgl"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT"
|
||||
}
|
||||
175
packages/rendering/sprite/src/ShinyEffectComponent.ts
Normal file
175
packages/rendering/sprite/src/ShinyEffectComponent.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Shiny effect component for sprite elements.
|
||||
* 精灵元素的闪光效果组件。
|
||||
*
|
||||
* This component configures a sweeping highlight animation that moves across
|
||||
* the sprite's texture.
|
||||
* 此组件配置一个扫过精灵纹理的高光动画。
|
||||
*/
|
||||
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import type { IShinyEffect } from '@esengine/material-system';
|
||||
import {
|
||||
SHINY_EFFECT_DEFAULTS,
|
||||
resetShinyEffect,
|
||||
startShinyEffect,
|
||||
stopShinyEffect,
|
||||
getShinyRotationRadians
|
||||
} from '@esengine/material-system';
|
||||
|
||||
/**
|
||||
* Shiny effect component.
|
||||
* 闪光效果组件。
|
||||
*
|
||||
* Adds a sweeping highlight animation to sprites.
|
||||
* 为精灵添加扫光动画效果。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add shiny effect to an entity with SpriteComponent
|
||||
* const shiny = entity.addComponent(ShinyEffectComponent);
|
||||
* shiny.play = true;
|
||||
* shiny.loop = true;
|
||||
* shiny.duration = 2.0;
|
||||
* shiny.loopDelay = 2.0;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('ShinyEffect', { requires: ['Sprite'] })
|
||||
@Serializable({ version: 1, typeId: 'ShinyEffect' })
|
||||
export class ShinyEffectComponent extends Component implements IShinyEffect {
|
||||
// ============= Effect Parameters =============
|
||||
// ============= 效果参数 =============
|
||||
|
||||
/**
|
||||
* Width of the shiny band (0.0 - 1.0).
|
||||
* 闪光带宽度 (0.0 - 1.0)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
|
||||
public width: number = 0.25;
|
||||
|
||||
/**
|
||||
* Rotation angle in degrees.
|
||||
* 旋转角度(度)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
|
||||
public rotation: number = 129;
|
||||
|
||||
/**
|
||||
* Edge softness (0.0 - 1.0).
|
||||
* 边缘柔和度 (0.0 - 1.0)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
|
||||
public softness: number = 1.0;
|
||||
|
||||
/**
|
||||
* Brightness multiplier.
|
||||
* 亮度倍增器。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
|
||||
public brightness: number = 1.0;
|
||||
|
||||
/**
|
||||
* Gloss intensity.
|
||||
* 光泽度。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
|
||||
public gloss: number = 1.0;
|
||||
|
||||
// ============= Animation Settings =============
|
||||
// ============= 动画设置 =============
|
||||
|
||||
/**
|
||||
* Whether the animation is playing.
|
||||
* 动画是否正在播放。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Play' })
|
||||
public play: boolean = true;
|
||||
|
||||
/**
|
||||
* Whether to loop the animation.
|
||||
* 是否循环动画。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Loop' })
|
||||
public loop: boolean = true;
|
||||
|
||||
/**
|
||||
* Animation duration in seconds.
|
||||
* 动画持续时间(秒)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
|
||||
public duration: number = 2.0;
|
||||
|
||||
/**
|
||||
* Delay between loops in seconds.
|
||||
* 循环之间的延迟(秒)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
|
||||
public loopDelay: number = 2.0;
|
||||
|
||||
/**
|
||||
* Initial delay before first play in seconds.
|
||||
* 首次播放前的初始延迟(秒)。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
|
||||
public initialDelay: number = 0;
|
||||
|
||||
// ============= Runtime State (not serialized) =============
|
||||
// ============= 运行时状态(不序列化)=============
|
||||
|
||||
/** Current animation progress (0.0 - 1.0). | 当前动画进度。 */
|
||||
public progress: number = 0;
|
||||
|
||||
/** Current elapsed time in the animation cycle. | 当前周期已用时间。 */
|
||||
public elapsedTime: number = 0;
|
||||
|
||||
/** Whether currently in delay phase. | 是否处于延迟阶段。 */
|
||||
public inDelay: boolean = false;
|
||||
|
||||
/** Remaining delay time. | 剩余延迟时间。 */
|
||||
public delayRemaining: number = 0;
|
||||
|
||||
/** Whether the initial delay has been processed. | 初始延迟是否已处理。 */
|
||||
public initialDelayProcessed: boolean = false;
|
||||
|
||||
/**
|
||||
* Reset the animation to the beginning.
|
||||
* 重置动画到开始状态。
|
||||
*/
|
||||
reset(): void {
|
||||
resetShinyEffect(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start playing the animation.
|
||||
* 开始播放动画。
|
||||
*/
|
||||
start(): void {
|
||||
startShinyEffect(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the animation.
|
||||
* 停止动画。
|
||||
*/
|
||||
stop(): void {
|
||||
stopShinyEffect(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rotation in radians for shader use.
|
||||
* 获取弧度制的旋转角度供着色器使用。
|
||||
*/
|
||||
getRotationRadians(): number {
|
||||
return getShinyRotationRadians(this);
|
||||
}
|
||||
}
|
||||
372
packages/rendering/sprite/src/SpriteAnimatorComponent.ts
Normal file
372
packages/rendering/sprite/src/SpriteAnimatorComponent.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* 动画帧数据
|
||||
* Animation frame data
|
||||
*/
|
||||
export interface AnimationFrame {
|
||||
/**
|
||||
* 纹理资产 GUID
|
||||
* Texture asset GUID
|
||||
*/
|
||||
textureGuid: 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: 'textureGuid' }]
|
||||
})
|
||||
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 textureGuid - 纹理资产 GUID | Texture asset GUID
|
||||
* @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,
|
||||
textureGuid: 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({
|
||||
textureGuid,
|
||||
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 textureGuids - 纹理资产 GUID 数组 | Array of texture asset GUIDs
|
||||
* @param fps - 帧率 | Frames per second
|
||||
* @param loop - 是否循环 | Whether to loop
|
||||
*/
|
||||
createClipFromSequence(
|
||||
name: string,
|
||||
textureGuids: string[],
|
||||
fps: number = 12,
|
||||
loop: boolean = true
|
||||
): AnimationClip {
|
||||
const duration = 1 / fps;
|
||||
const frames: AnimationFrame[] = textureGuids.map((textureGuid) => ({
|
||||
textureGuid,
|
||||
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);
|
||||
}
|
||||
}
|
||||
488
packages/rendering/sprite/src/SpriteComponent.ts
Normal file
488
packages/rendering/sprite/src/SpriteComponent.ts
Normal file
@@ -0,0 +1,488 @@
|
||||
import type { AssetReference } from '@esengine/asset-system';
|
||||
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
||||
import { SortingLayers, type ISortable } from '@esengine/engine-core';
|
||||
import type {
|
||||
IMaterialOverridable,
|
||||
MaterialPropertyOverride,
|
||||
MaterialOverrides
|
||||
} from '@esengine/material-system';
|
||||
|
||||
/**
|
||||
* 精灵组件 - 管理2D图像渲染
|
||||
* Sprite component - manages 2D image rendering
|
||||
*
|
||||
* 需要 TransformComponent 才能被 EngineRenderSystem 处理
|
||||
* Requires TransformComponent to be processed by EngineRenderSystem
|
||||
*/
|
||||
@ECSComponent('Sprite', { requires: ['Transform'] })
|
||||
@Serializable({ version: 5, typeId: 'Sprite' })
|
||||
export class SpriteComponent extends Component implements ISortable, IMaterialOverridable {
|
||||
/**
|
||||
* 纹理资产 GUID
|
||||
* Texture asset GUID
|
||||
*
|
||||
* Stores the unique identifier of the texture asset.
|
||||
* The actual file path is resolved at runtime via AssetDatabase.
|
||||
* 存储纹理资产的唯一标识符。
|
||||
* 实际文件路径在运行时通过 AssetDatabase 解析。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public textureGuid: 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;
|
||||
|
||||
/**
|
||||
* 排序层
|
||||
* Sorting layer
|
||||
*
|
||||
* 决定渲染的大类顺序,如 Background, Default, UI, Overlay 等。
|
||||
* Determines the major render order category.
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({
|
||||
type: 'enum',
|
||||
label: 'Sorting Layer',
|
||||
options: ['Background', 'Default', 'Foreground', 'WorldOverlay', 'UI', 'ScreenOverlay', 'Modal']
|
||||
})
|
||||
public sortingLayer: string = SortingLayers.Default;
|
||||
|
||||
/**
|
||||
* 层内顺序(越高越在上面)
|
||||
* Order within layer (higher = rendered on top)
|
||||
*
|
||||
* 同一排序层内的细分顺序。
|
||||
* Fine-grained order within the same sorting layer.
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'integer', label: 'Order in Layer' })
|
||||
public orderInLayer: number = 0;
|
||||
|
||||
/**
|
||||
* 材质资产 GUID(共享材质)
|
||||
* Material asset GUID (shared material)
|
||||
*
|
||||
* Multiple sprites can reference the same material file.
|
||||
* 多个精灵可以引用同一个材质文件。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Material', extensions: ['.mat'] })
|
||||
public materialGuid: string = '';
|
||||
|
||||
/**
|
||||
* 材质属性覆盖(实例级别)
|
||||
* Material property overrides (instance level)
|
||||
*
|
||||
* Override specific uniform parameters without creating a new material.
|
||||
* 覆盖特定的 uniform 参数,无需创建新材质。
|
||||
*/
|
||||
@Serialize()
|
||||
public materialOverrides: MaterialOverrides = {};
|
||||
|
||||
/**
|
||||
* 是否使用独立材质实例
|
||||
* Whether to use an independent material instance
|
||||
*
|
||||
* When true, a copy of the shared material is created for this sprite.
|
||||
* Changes to this material won't affect other sprites using the same source.
|
||||
* 当为 true 时,会为此精灵创建共享材质的副本。
|
||||
* 对此材质的更改不会影响使用相同源的其他精灵。
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'boolean', label: 'Use Instance Material' })
|
||||
public useInstanceMaterial: boolean = false;
|
||||
|
||||
/**
|
||||
* 运行时材质ID(缓存)
|
||||
* Runtime material ID (cached)
|
||||
*
|
||||
* Cached material ID for rendering. Updated when material path changes.
|
||||
* 用于渲染的缓存材质ID。当材质路径更改时更新。
|
||||
*/
|
||||
private _materialId: number = 0;
|
||||
|
||||
/**
|
||||
* 独立材质实例(如果 useInstanceMaterial 为 true)
|
||||
* Independent material instance (if useInstanceMaterial is true)
|
||||
*/
|
||||
private _instanceMaterial: unknown = null;
|
||||
|
||||
/** 锚点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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param textureGuidOrPath - Texture GUID or path (for backward compatibility)
|
||||
*/
|
||||
constructor(textureGuidOrPath: string = '') {
|
||||
super();
|
||||
// Support both GUID and path for backward compatibility
|
||||
this.textureGuid = textureGuidOrPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从精灵图集区域设置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.textureGuid = reference.guid;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产引用
|
||||
* Get asset reference
|
||||
*/
|
||||
getAssetReference(): AssetReference<HTMLImageElement> | undefined {
|
||||
return this._assetReference;
|
||||
}
|
||||
|
||||
/**
|
||||
* 异步加载纹理
|
||||
* Load texture asynchronously
|
||||
*/
|
||||
async loadTextureAsync(): Promise<void> {
|
||||
if (this._assetReference) {
|
||||
try {
|
||||
const result = await this._assetReference.loadAsync();
|
||||
// 检查返回值是否包含 textureId 属性(ITextureAsset 类型)
|
||||
// Check if result has textureId property (ITextureAsset type)
|
||||
if (result && typeof result === 'object' && 'textureId' in result) {
|
||||
this.textureId = (result as { textureId: number }).textureId;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load texture:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取纹理 GUID
|
||||
* Get texture GUID
|
||||
*/
|
||||
getTextureSource(): string {
|
||||
return this.textureGuid;
|
||||
}
|
||||
|
||||
// ============= Material Override Methods =============
|
||||
// ============= 材质覆盖方法 =============
|
||||
|
||||
/**
|
||||
* 获取材质ID
|
||||
* Get material ID
|
||||
*
|
||||
* # Returns | 返回
|
||||
* The cached material ID for rendering.
|
||||
* 用于渲染的缓存材质ID。
|
||||
*/
|
||||
getMaterialId(): number {
|
||||
return this._materialId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置材质ID
|
||||
* Set material ID
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `id` - Material ID from MaterialManager. | 来自 MaterialManager 的材质ID。
|
||||
*/
|
||||
setMaterialId(id: number): void {
|
||||
this._materialId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浮点覆盖值
|
||||
* Set float override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `value` - Float value. | 浮点值。
|
||||
*/
|
||||
setOverrideFloat(name: string, value: number): this {
|
||||
this.materialOverrides[name] = { type: 'float', value };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 vec2 覆盖值
|
||||
* Set vec2 override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `x` - X component. | X 分量。
|
||||
* * `y` - Y component. | Y 分量。
|
||||
*/
|
||||
setOverrideVec2(name: string, x: number, y: number): this {
|
||||
this.materialOverrides[name] = { type: 'vec2', value: [x, y] };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 vec3 覆盖值
|
||||
* Set vec3 override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `x` - X component. | X 分量。
|
||||
* * `y` - Y component. | Y 分量。
|
||||
* * `z` - Z component. | Z 分量。
|
||||
*/
|
||||
setOverrideVec3(name: string, x: number, y: number, z: number): this {
|
||||
this.materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 vec4 覆盖值
|
||||
* Set vec4 override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `x` - X component. | X 分量。
|
||||
* * `y` - Y component. | Y 分量。
|
||||
* * `z` - Z component. | Z 分量。
|
||||
* * `w` - W component. | W 分量。
|
||||
*/
|
||||
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
|
||||
this.materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置颜色覆盖值
|
||||
* Set color override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `r` - Red component (0-1). | 红色分量 (0-1)。
|
||||
* * `g` - Green component (0-1). | 绿色分量 (0-1)。
|
||||
* * `b` - Blue component (0-1). | 蓝色分量 (0-1)。
|
||||
* * `a` - Alpha component (0-1). | 透明度分量 (0-1)。
|
||||
*/
|
||||
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
|
||||
this.materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置整数覆盖值
|
||||
* Set integer override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
* * `value` - Integer value. | 整数值。
|
||||
*/
|
||||
setOverrideInt(name: string, value: number): this {
|
||||
this.materialOverrides[name] = { type: 'int', value: Math.floor(value) };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取覆盖值
|
||||
* Get override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name. | Uniform 名称。
|
||||
*
|
||||
* # Returns | 返回
|
||||
* Override value or undefined if not set.
|
||||
* 覆盖值,如果未设置则返回 undefined。
|
||||
*/
|
||||
getOverride(name: string): MaterialPropertyOverride | undefined {
|
||||
return this.materialOverrides[name];
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除覆盖值
|
||||
* Remove override value
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `name` - Uniform name to remove. | 要移除的 Uniform 名称。
|
||||
*/
|
||||
removeOverride(name: string): this {
|
||||
delete this.materialOverrides[name];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有覆盖值
|
||||
* Clear all override values
|
||||
*/
|
||||
clearOverrides(): this {
|
||||
this.materialOverrides = {};
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有覆盖值
|
||||
* Check if there are any overrides
|
||||
*
|
||||
* # Returns | 返回
|
||||
* True if there are any material overrides.
|
||||
* 如果有任何材质覆盖则返回 true。
|
||||
*/
|
||||
hasOverrides(): boolean {
|
||||
return Object.keys(this.materialOverrides).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时调用
|
||||
* Called when component is destroyed
|
||||
*/
|
||||
onDestroy(): void {
|
||||
// 释放资产引用 / Release asset reference
|
||||
if (this._assetReference) {
|
||||
this._assetReference.release();
|
||||
this._assetReference = undefined;
|
||||
}
|
||||
// 清理材质覆盖 / Clear material overrides
|
||||
this.materialOverrides = {};
|
||||
this._instanceMaterial = null;
|
||||
}
|
||||
}
|
||||
56
packages/rendering/sprite/src/SpriteRuntimeModule.ts
Normal file
56
packages/rendering/sprite/src/SpriteRuntimeModule.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
|
||||
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
|
||||
import { SpriteComponent } from './SpriteComponent';
|
||||
import { SpriteAnimatorComponent } from './SpriteAnimatorComponent';
|
||||
import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem';
|
||||
import { SpriteAnimatorSystemToken } from './tokens';
|
||||
|
||||
export type { SystemContext, ModuleManifest, IRuntimeModule, IRuntimePlugin };
|
||||
|
||||
// 重新导出 tokens | Re-export tokens
|
||||
export { SpriteAnimatorSystemToken } from './tokens';
|
||||
|
||||
class SpriteRuntimeModule implements IRuntimeModule {
|
||||
registerComponents(registry: IComponentRegistry): void {
|
||||
registry.register(SpriteComponent);
|
||||
registry.register(SpriteAnimatorComponent);
|
||||
}
|
||||
|
||||
createSystems(scene: IScene, context: SystemContext): void {
|
||||
const animatorSystem = new SpriteAnimatorSystem();
|
||||
|
||||
if (context.isEditor) {
|
||||
animatorSystem.enabled = false;
|
||||
}
|
||||
|
||||
scene.addSystem(animatorSystem);
|
||||
|
||||
// 注册服务到服务注册表 | Register service to service registry
|
||||
context.services.register(SpriteAnimatorSystemToken, animatorSystem);
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: 'sprite',
|
||||
name: '@esengine/sprite',
|
||||
displayName: 'Sprite 2D',
|
||||
version: '1.0.0',
|
||||
description: 'Sprite and SpriteAnimator components for 2D rendering',
|
||||
category: 'Rendering',
|
||||
icon: 'Image',
|
||||
isCore: false,
|
||||
defaultEnabled: true,
|
||||
isEngineModule: true,
|
||||
canContainContent: true,
|
||||
dependencies: ['core', 'math'],
|
||||
exports: { components: ['SpriteComponent', 'SpriteAnimatorComponent'] },
|
||||
editorPackage: '@esengine/sprite-editor',
|
||||
requiresWasm: true
|
||||
};
|
||||
|
||||
export const SpritePlugin: IRuntimePlugin = {
|
||||
manifest,
|
||||
runtimeModule: new SpriteRuntimeModule()
|
||||
};
|
||||
|
||||
export { SpriteRuntimeModule };
|
||||
13
packages/rendering/sprite/src/index.ts
Normal file
13
packages/rendering/sprite/src/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { SpriteComponent } from './SpriteComponent';
|
||||
// Re-export material types from material-system for convenience
|
||||
// 从 material-system 重新导出材质类型以方便使用
|
||||
export type { MaterialPropertyOverride, MaterialOverrides } from '@esengine/material-system';
|
||||
export { SpriteAnimatorComponent } from './SpriteAnimatorComponent';
|
||||
export type { AnimationFrame, AnimationClip } from './SpriteAnimatorComponent';
|
||||
export { ShinyEffectComponent } from './ShinyEffectComponent';
|
||||
export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem';
|
||||
export { ShinyEffectSystem } from './systems/ShinyEffectSystem';
|
||||
export { SpriteRuntimeModule, SpritePlugin } from './SpriteRuntimeModule';
|
||||
|
||||
// Service tokens | 服务令牌
|
||||
export { SpriteAnimatorSystemToken } from './tokens';
|
||||
46
packages/rendering/sprite/src/systems/ShinyEffectSystem.ts
Normal file
46
packages/rendering/sprite/src/systems/ShinyEffectSystem.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Shiny effect animation system.
|
||||
* 闪光效果动画系统。
|
||||
*
|
||||
* Updates ShinyEffectComponent animations and applies material overrides
|
||||
* to the associated SpriteComponent.
|
||||
* 更新 ShinyEffectComponent 动画并将材质覆盖应用到关联的 SpriteComponent。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, ECSSystem, Time, Entity } from '@esengine/ecs-framework';
|
||||
import { ShinyEffectAnimator } from '@esengine/material-system';
|
||||
import { ShinyEffectComponent } from '../ShinyEffectComponent';
|
||||
import { SpriteComponent } from '../SpriteComponent';
|
||||
|
||||
/**
|
||||
* System that animates shiny effects on sprites.
|
||||
* 为精灵动画闪光效果的系统。
|
||||
*/
|
||||
@ECSSystem('ShinyEffect', { updateOrder: 100 })
|
||||
export class ShinyEffectSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.empty().all(ShinyEffectComponent));
|
||||
}
|
||||
|
||||
/**
|
||||
* Process all entities with ShinyEffectComponent.
|
||||
* 处理所有带有 ShinyEffectComponent 的实体。
|
||||
*/
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
const deltaTime = Time.deltaTime;
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
|
||||
const shiny = entity.getComponent(ShinyEffectComponent);
|
||||
if (!shiny || !shiny.play) continue;
|
||||
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
if (!sprite) continue;
|
||||
|
||||
// Use shared animator logic
|
||||
// 使用共享的动画器逻辑
|
||||
ShinyEffectAnimator.processEffect(shiny, sprite, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.textureGuid = frame.textureGuid;
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
17
packages/rendering/sprite/src/tokens.ts
Normal file
17
packages/rendering/sprite/src/tokens.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Sprite 模块服务令牌
|
||||
* Sprite module service tokens
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/ecs-framework';
|
||||
import type { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem';
|
||||
|
||||
// ============================================================================
|
||||
// Sprite 模块导出的令牌 | Tokens exported by Sprite module
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Sprite 动画系统令牌
|
||||
* Sprite animator system token
|
||||
*/
|
||||
export const SpriteAnimatorSystemToken = createServiceToken<SpriteAnimatorSystem>('spriteAnimatorSystem');
|
||||
12
packages/rendering/sprite/tsconfig.build.json
Normal file
12
packages/rendering/sprite/tsconfig.build.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
||||
}
|
||||
23
packages/rendering/sprite/tsconfig.json
Normal file
23
packages/rendering/sprite/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../framework/core"
|
||||
},
|
||||
{
|
||||
"path": "../../engine/asset-system"
|
||||
}
|
||||
]
|
||||
}
|
||||
7
packages/rendering/sprite/tsup.config.ts
Normal file
7
packages/rendering/sprite/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
Reference in New Issue
Block a user