Files
esengine/packages/particle/src/modules/TextureSheetAnimationModule.ts
YHH c3b7250f85 refactor(plugin): 重构插件系统架构,统一类型导入路径 (#296)
* refactor(plugin): 重构插件系统架构,统一类型导入路径

## 主要更改

### 新增 @esengine/plugin-types 包
- 提供打破循环依赖的最小类型定义
- 包含 ServiceToken, createServiceToken, PluginServiceRegistry, IEditorModuleBase

### engine-core 类型统一
- IPlugin<T> 泛型参数改为 T = unknown
- 所有运行时类型(IRuntimeModule, ModuleManifest, SystemContext)在此定义
- 新增 PluginServiceRegistry.ts 导出服务令牌相关类型

### editor-core 类型优化
- 重命名 IPluginLoader.ts 为 EditorModule.ts
- 新增 IEditorPlugin = IPlugin<IEditorModuleLoader> 类型别名
- 移除弃用别名 IPluginLoader, IRuntimeModuleLoader

### 编辑器插件更新
- 所有 9 个编辑器插件使用 IEditorPlugin 类型
- 统一从 editor-core 导入编辑器类型

### 服务令牌规范
- 各模块在 tokens.ts 中定义自己的服务接口和令牌
- 遵循"谁定义接口,谁导出 Token"原则

## 导入规范

| 场景 | 导入来源 |
|------|---------|
| 运行时模块 | @esengine/engine-core |
| 编辑器插件 | @esengine/editor-core |
| 服务令牌 | @esengine/engine-core |

* refactor(plugin): 完善服务令牌规范,统一运行时模块

## 更改内容

### 运行时模块优化
- particle: 使用 PluginServiceRegistry 获取依赖服务
- physics-rapier2d: 通过服务令牌注册/获取物理查询接口
- tilemap: 使用服务令牌获取物理系统依赖
- sprite: 导出服务令牌
- ui: 导出服务令牌
- behavior-tree: 使用服务令牌系统

### 资产系统增强
- IAssetManager 接口扩展
- 加载器使用服务令牌获取依赖

### 运行时核心
- GameRuntime 使用 PluginServiceRegistry
- 导出服务令牌相关类型

### 编辑器服务
- EngineService 适配新的服务令牌系统
- AssetRegistryService 优化

* fix: 修复 editor-app 和 behavior-tree-editor 中的类型引用

- editor-app/PluginLoader.ts: 使用 IPlugin 替代 IPluginLoader
- behavior-tree-editor: 使用 IEditorPlugin 替代 IPluginLoader

* fix(ui): 添加缺失的 ecs-engine-bindgen 依赖

UIRuntimeModule 使用 EngineBridgeToken,需要声明对 ecs-engine-bindgen 的依赖

* fix(type): 解决 ServiceToken 跨包类型兼容性问题

- 在 engine-core 中直接定义 ServiceToken 和 PluginServiceRegistry
  而不是从 plugin-types 重新导出,确保 tsup 生成的类型声明
  以 engine-core 作为类型来源
- 移除 RuntimeResolver.ts 中的硬编码模块 ID 检查,
  改用 module.json 中的 name 配置
- 修复 pnpm-lock.yaml 中的依赖记录

* refactor(arch): 改进架构设计,移除硬编码

- 统一类型导出:editor-core 从 engine-core 导入 IEditorModuleBase
- RuntimeResolver: 将硬编码路径改为配置常量和搜索路径列表
- 添加跨平台安装路径支持(Windows/macOS/Linux)
- 使用 ENGINE_WASM_CONFIG 配置引擎 WASM 文件信息
- IBundlePackOptions 添加 preloadBundles 配置项

* fix(particle): 添加缺失的 ecs-engine-bindgen 依赖

ParticleRuntimeModule 导入了 @esengine/ecs-engine-bindgen 的 tokens,
但 package.json 中未声明该依赖,导致 CI 构建失败。

* fix(physics-rapier2d): 移除不存在的 PhysicsSystemContext 导出

PhysicsRuntimeModule 中不存在该类型,导致 type-check 失败
2025-12-08 21:10:57 +08:00

229 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Particle } from '../Particle';
import type { IParticleModule } from './IParticleModule';
/**
* 动画播放模式
* Animation playback mode
*/
export enum AnimationPlayMode {
/** 单次播放(生命周期内完成一次循环)| Single loop over lifetime */
LifetimeLoop = 'lifetimeLoop',
/** 固定帧率播放 | Fixed frame rate */
FixedFPS = 'fixedFps',
/** 随机选择帧 | Random frame selection */
Random = 'random',
/** 使用速度控制帧 | Speed-based frame */
SpeedBased = 'speedBased',
}
/**
* 动画循环模式
* Animation loop mode
*/
export enum AnimationLoopMode {
/** 不循环(停在最后一帧)| No loop (stop at last frame) */
Once = 'once',
/** 循环播放 | Loop continuously */
Loop = 'loop',
/** 往返循环 | Ping-pong loop */
PingPong = 'pingPong',
}
/**
* 纹理图集动画模块
* Texture sheet animation module
*
* Animates particles through sprite sheet frames.
* 通过精灵图帧动画化粒子。
*/
export class TextureSheetAnimationModule implements IParticleModule {
readonly name = 'TextureSheetAnimation';
enabled = false;
// 图集配置 | Sheet configuration
/** 水平帧数 | Number of columns */
tilesX: number = 1;
/** 垂直帧数 | Number of rows */
tilesY: number = 1;
/** 总帧数0=自动计算为 tilesX * tilesY| Total frames (0 = auto-calculate) */
totalFrames: number = 0;
/** 起始帧 | Start frame index */
startFrame: number = 0;
// 播放配置 | Playback configuration
/** 播放模式 | Playback mode */
playMode: AnimationPlayMode = AnimationPlayMode.LifetimeLoop;
/** 循环模式 | Loop mode */
loopMode: AnimationLoopMode = AnimationLoopMode.Loop;
/** 固定帧率FPS用于 FixedFPS 模式)| Fixed frame rate (for FixedFPS mode) */
frameRate: number = 30;
/** 播放速度乘数 | Playback speed multiplier */
speedMultiplier: number = 1;
/** 循环次数0=无限)| Number of loops (0 = infinite) */
cycleCount: number = 0;
// 内部状态 | Internal state
private _cachedTotalFrames: number = 0;
/**
* 获取实际总帧数
* Get actual total frames
*/
get actualTotalFrames(): number {
return this.totalFrames > 0 ? this.totalFrames : this.tilesX * this.tilesY;
}
/**
* 更新粒子
* Update particle
*
* @param p - 粒子 | Particle
* @param dt - 增量时间 | Delta time
* @param normalizedAge - 归一化年龄 (0-1) | Normalized age
*/
update(p: Particle, dt: number, normalizedAge: number): void {
const frameCount = this.actualTotalFrames;
if (frameCount <= 1) return;
let frameIndex: number;
switch (this.playMode) {
case AnimationPlayMode.LifetimeLoop:
frameIndex = this._getLifetimeFrame(normalizedAge, frameCount);
break;
case AnimationPlayMode.FixedFPS:
frameIndex = this._getFixedFPSFrame(p, dt, frameCount);
break;
case AnimationPlayMode.Random:
frameIndex = this._getRandomFrame(p, frameCount);
break;
case AnimationPlayMode.SpeedBased:
frameIndex = this._getSpeedBasedFrame(p, frameCount);
break;
default:
frameIndex = this.startFrame;
}
// 设置粒子的 UV 坐标 | Set particle UV coordinates
this._setParticleUV(p, frameIndex);
}
/**
* 生命周期帧计算
* Calculate frame based on lifetime
*/
private _getLifetimeFrame(normalizedAge: number, frameCount: number): number {
const progress = normalizedAge * this.speedMultiplier;
return this._applyLoopMode(progress, frameCount);
}
/**
* 固定帧率计算
* Calculate frame based on fixed FPS
*/
private _getFixedFPSFrame(p: Particle, dt: number, frameCount: number): number {
// 使用粒子的 age 来计算当前帧 | Use particle age to calculate current frame
const animTime = p.age * this.frameRate * this.speedMultiplier;
const progress = animTime / frameCount;
return this._applyLoopMode(progress, frameCount);
}
/**
* 随机帧选择(每个粒子使用固定随机帧)
* Random frame selection (each particle uses fixed random frame)
*/
private _getRandomFrame(p: Particle, frameCount: number): number {
// 使用粒子的起始位置作为随机种子(确保一致性)
// Use particle's start position as random seed (for consistency)
const seed = Math.abs(p.startR * 1000 + p.startG * 100 + p.startB * 10) % 1;
return Math.floor(seed * frameCount);
}
/**
* 基于速度的帧计算
* Calculate frame based on particle speed
*/
private _getSpeedBasedFrame(p: Particle, frameCount: number): number {
const speed = Math.sqrt(p.vx * p.vx + p.vy * p.vy);
// 归一化速度(假设最大速度为 500| Normalize speed (assume max 500)
const normalizedSpeed = Math.min(speed / 500, 1);
const frameIndex = Math.floor(normalizedSpeed * (frameCount - 1));
return this.startFrame + frameIndex;
}
/**
* 应用循环模式
* Apply loop mode to progress
*/
private _applyLoopMode(progress: number, frameCount: number): number {
let loopProgress = progress;
switch (this.loopMode) {
case AnimationLoopMode.Once:
// 停在最后一帧 | Stop at last frame
loopProgress = Math.min(progress, 0.9999);
break;
case AnimationLoopMode.Loop:
// 简单循环 | Simple loop
loopProgress = progress % 1;
break;
case AnimationLoopMode.PingPong:
// 往返循环 | Ping-pong
const cycle = Math.floor(progress);
const remainder = progress - cycle;
loopProgress = (cycle % 2 === 0) ? remainder : (1 - remainder);
break;
}
// 检查循环次数限制 | Check cycle count limit
if (this.cycleCount > 0) {
const currentCycle = Math.floor(progress);
if (currentCycle >= this.cycleCount) {
loopProgress = 0.9999; // 停在最后一帧 | Stop at last frame
}
}
// 计算帧索引 | Calculate frame index
const frameIndex = Math.floor(loopProgress * frameCount);
return this.startFrame + Math.min(frameIndex, frameCount - 1);
}
/**
* 设置粒子 UV 坐标
* Set particle UV coordinates
*/
private _setParticleUV(p: Particle, frameIndex: number): void {
// 计算 UV 坐标 | Calculate UV coordinates
const col = frameIndex % this.tilesX;
const row = Math.floor(frameIndex / this.tilesX);
const uWidth = 1 / this.tilesX;
const vHeight = 1 / this.tilesY;
// UV 坐标(左上角为原点)| UV coordinates (top-left origin)
const u0 = col * uWidth;
const v0 = row * vHeight;
const u1 = u0 + uWidth;
const v1 = v0 + vHeight;
// 存储动画帧信息到粒子 | Store animation frame info to particle
// 渲染数据提供者会使用这些值计算实际的 UV | Render data provider will use these to calculate actual UVs
p._animFrame = frameIndex;
p._animTilesX = this.tilesX;
p._animTilesY = this.tilesY;
}
}