* 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 失败
229 lines
7.5 KiB
TypeScript
229 lines
7.5 KiB
TypeScript
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;
|
||
}
|
||
}
|