342 lines
9.1 KiB
TypeScript
342 lines
9.1 KiB
TypeScript
|
|
/**
|
|||
|
|
* Animation3DComponent - 3D animation playback component.
|
|||
|
|
* Animation3DComponent - 3D 动画播放组件。
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
import { Component, ECSComponent, Property, Serializable, Serialize } from '@esengine/ecs-framework';
|
|||
|
|
import type { IGLTFAnimationClip } from '@esengine/asset-system';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Animation play state.
|
|||
|
|
* 动画播放状态。
|
|||
|
|
*/
|
|||
|
|
export enum AnimationPlayState {
|
|||
|
|
/** Stopped - not playing. | 停止 - 未播放。 */
|
|||
|
|
Stopped = 'stopped',
|
|||
|
|
/** Playing forward. | 正向播放。 */
|
|||
|
|
Playing = 'playing',
|
|||
|
|
/** Paused. | 暂停。 */
|
|||
|
|
Paused = 'paused'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Animation wrap mode.
|
|||
|
|
* 动画循环模式。
|
|||
|
|
*/
|
|||
|
|
export enum AnimationWrapMode {
|
|||
|
|
/** Play once and stop. | 播放一次后停止。 */
|
|||
|
|
Once = 'once',
|
|||
|
|
/** Loop continuously. | 连续循环。 */
|
|||
|
|
Loop = 'loop',
|
|||
|
|
/** Play forward then backward (ping-pong). | 往返播放。 */
|
|||
|
|
PingPong = 'pingpong',
|
|||
|
|
/** Clamp to last frame. | 停在最后一帧。 */
|
|||
|
|
ClampForever = 'clampForever'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 3D Animation component for playing skeletal/node animations.
|
|||
|
|
* 用于播放骨骼/节点动画的 3D 动画组件。
|
|||
|
|
*
|
|||
|
|
* Requires MeshComponent for animation data source.
|
|||
|
|
* 需要 MeshComponent 作为动画数据来源。
|
|||
|
|
*/
|
|||
|
|
@ECSComponent('Animation3D', { requires: ['Mesh'] })
|
|||
|
|
@Serializable({ version: 1, typeId: 'Animation3D' })
|
|||
|
|
export class Animation3DComponent extends Component {
|
|||
|
|
/**
|
|||
|
|
* 默认动画片段名称
|
|||
|
|
* Default animation clip name
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'string', label: 'Default Clip' })
|
|||
|
|
public defaultClip: string = '';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 播放速度(1.0 = 正常速度)
|
|||
|
|
* Playback speed (1.0 = normal speed)
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'number', label: 'Speed', min: 0, max: 10 })
|
|||
|
|
public speed: number = 1.0;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 循环模式
|
|||
|
|
* Wrap mode
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({
|
|||
|
|
type: 'enum',
|
|||
|
|
label: 'Wrap Mode',
|
|||
|
|
options: ['once', 'loop', 'pingpong', 'clampForever']
|
|||
|
|
})
|
|||
|
|
public wrapMode: AnimationWrapMode = AnimationWrapMode.Loop;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否启动时自动播放
|
|||
|
|
* Whether to auto-play on start
|
|||
|
|
*/
|
|||
|
|
@Serialize()
|
|||
|
|
@Property({ type: 'boolean', label: 'Play On Awake' })
|
|||
|
|
public playOnAwake: boolean = true;
|
|||
|
|
|
|||
|
|
// ===== Runtime State | 运行时状态 =====
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 当前播放状态
|
|||
|
|
* Current play state
|
|||
|
|
*/
|
|||
|
|
private _playState: AnimationPlayState = AnimationPlayState.Stopped;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 当前播放的动画片段
|
|||
|
|
* Currently playing animation clip
|
|||
|
|
*/
|
|||
|
|
private _currentClip: IGLTFAnimationClip | null = null;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 当前播放时间(秒)
|
|||
|
|
* Current playback time (seconds)
|
|||
|
|
*/
|
|||
|
|
private _currentTime: number = 0;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 播放方向(1 = 正向,-1 = 反向)
|
|||
|
|
* Playback direction (1 = forward, -1 = backward)
|
|||
|
|
*/
|
|||
|
|
private _direction: number = 1;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 可用的动画片段列表
|
|||
|
|
* Available animation clips
|
|||
|
|
*/
|
|||
|
|
private _clips: IGLTFAnimationClip[] = [];
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 当前片段名称到索引的映射
|
|||
|
|
* Map of clip name to index
|
|||
|
|
*/
|
|||
|
|
private _clipNameToIndex: Map<string, number> = new Map();
|
|||
|
|
|
|||
|
|
// ===== Public Getters | 公共获取器 =====
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前播放状态
|
|||
|
|
* Get current play state
|
|||
|
|
*/
|
|||
|
|
public get playState(): AnimationPlayState {
|
|||
|
|
return this._playState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前播放的动画片段
|
|||
|
|
* Get currently playing clip
|
|||
|
|
*/
|
|||
|
|
public get currentClip(): IGLTFAnimationClip | null {
|
|||
|
|
return this._currentClip;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前播放时间
|
|||
|
|
* Get current playback time
|
|||
|
|
*/
|
|||
|
|
public get currentTime(): number {
|
|||
|
|
return this._currentTime;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取当前片段的持续时间
|
|||
|
|
* Get duration of current clip
|
|||
|
|
*/
|
|||
|
|
public get duration(): number {
|
|||
|
|
return this._currentClip?.duration ?? 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取归一化时间(0-1)
|
|||
|
|
* Get normalized time (0-1)
|
|||
|
|
*/
|
|||
|
|
public get normalizedTime(): number {
|
|||
|
|
if (!this._currentClip || this._currentClip.duration <= 0) return 0;
|
|||
|
|
return this._currentTime / this._currentClip.duration;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否正在播放
|
|||
|
|
* Whether playing
|
|||
|
|
*/
|
|||
|
|
public get isPlaying(): boolean {
|
|||
|
|
return this._playState === AnimationPlayState.Playing;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有可用的动画片段
|
|||
|
|
* Get all available clips
|
|||
|
|
*/
|
|||
|
|
public get clips(): readonly IGLTFAnimationClip[] {
|
|||
|
|
return this._clips;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 获取所有动画片段名称
|
|||
|
|
* Get all clip names
|
|||
|
|
*/
|
|||
|
|
public get clipNames(): string[] {
|
|||
|
|
return this._clips.map(c => c.name);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ===== Public Methods | 公共方法 =====
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置动画片段列表(由 Animation3DSystem 调用)
|
|||
|
|
* Set animation clips (called by Animation3DSystem)
|
|||
|
|
*/
|
|||
|
|
public setClips(clips: IGLTFAnimationClip[]): void {
|
|||
|
|
this._clips = clips;
|
|||
|
|
this._clipNameToIndex.clear();
|
|||
|
|
clips.forEach((clip, index) => {
|
|||
|
|
this._clipNameToIndex.set(clip.name, index);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 播放动画
|
|||
|
|
* Play animation
|
|||
|
|
*
|
|||
|
|
* @param clipName - 动画片段名称,不指定则播放当前/默认片段
|
|||
|
|
*/
|
|||
|
|
public play(clipName?: string): void {
|
|||
|
|
const name = clipName ?? this.defaultClip ?? (this._clips[0]?.name ?? '');
|
|||
|
|
|
|||
|
|
if (!name) {
|
|||
|
|
console.warn('[Animation3DComponent] No clip to play');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const index = this._clipNameToIndex.get(name);
|
|||
|
|
if (index === undefined) {
|
|||
|
|
console.warn(`[Animation3DComponent] Clip not found: ${name}`);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
this._currentClip = this._clips[index];
|
|||
|
|
this._currentTime = 0;
|
|||
|
|
this._direction = 1;
|
|||
|
|
this._playState = AnimationPlayState.Playing;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 停止动画
|
|||
|
|
* Stop animation
|
|||
|
|
*/
|
|||
|
|
public stop(): void {
|
|||
|
|
this._playState = AnimationPlayState.Stopped;
|
|||
|
|
this._currentTime = 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 暂停动画
|
|||
|
|
* Pause animation
|
|||
|
|
*/
|
|||
|
|
public pause(): void {
|
|||
|
|
if (this._playState === AnimationPlayState.Playing) {
|
|||
|
|
this._playState = AnimationPlayState.Paused;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 恢复播放
|
|||
|
|
* Resume playback
|
|||
|
|
*/
|
|||
|
|
public resume(): void {
|
|||
|
|
if (this._playState === AnimationPlayState.Paused) {
|
|||
|
|
this._playState = AnimationPlayState.Playing;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置播放时间
|
|||
|
|
* Set playback time
|
|||
|
|
*/
|
|||
|
|
public setTime(time: number): void {
|
|||
|
|
this._currentTime = Math.max(0, Math.min(time, this.duration));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 设置归一化时间
|
|||
|
|
* Set normalized time
|
|||
|
|
*/
|
|||
|
|
public setNormalizedTime(t: number): void {
|
|||
|
|
this._currentTime = Math.max(0, Math.min(t, 1)) * this.duration;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新播放时间(由 Animation3DSystem 调用)
|
|||
|
|
* Update playback time (called by Animation3DSystem)
|
|||
|
|
*
|
|||
|
|
* @param deltaTime - 时间增量(秒)
|
|||
|
|
*/
|
|||
|
|
public updateTime(deltaTime: number): void {
|
|||
|
|
if (this._playState !== AnimationPlayState.Playing || !this._currentClip) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const scaledDelta = deltaTime * this.speed * this._direction;
|
|||
|
|
this._currentTime += scaledDelta;
|
|||
|
|
|
|||
|
|
const duration = this._currentClip.duration;
|
|||
|
|
if (duration <= 0) return;
|
|||
|
|
|
|||
|
|
// Handle wrap mode
|
|||
|
|
// 处理循环模式
|
|||
|
|
switch (this.wrapMode) {
|
|||
|
|
case AnimationWrapMode.Once:
|
|||
|
|
if (this._currentTime >= duration || this._currentTime < 0) {
|
|||
|
|
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
|
|||
|
|
this._playState = AnimationPlayState.Stopped;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationWrapMode.Loop:
|
|||
|
|
while (this._currentTime >= duration) {
|
|||
|
|
this._currentTime -= duration;
|
|||
|
|
}
|
|||
|
|
while (this._currentTime < 0) {
|
|||
|
|
this._currentTime += duration;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationWrapMode.PingPong:
|
|||
|
|
if (this._currentTime >= duration) {
|
|||
|
|
this._currentTime = duration - (this._currentTime - duration);
|
|||
|
|
this._direction = -1;
|
|||
|
|
} else if (this._currentTime < 0) {
|
|||
|
|
this._currentTime = -this._currentTime;
|
|||
|
|
this._direction = 1;
|
|||
|
|
}
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
case AnimationWrapMode.ClampForever:
|
|||
|
|
this._currentTime = Math.max(0, Math.min(this._currentTime, duration));
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 重置组件
|
|||
|
|
* Reset component
|
|||
|
|
*/
|
|||
|
|
reset(): void {
|
|||
|
|
this.defaultClip = '';
|
|||
|
|
this.speed = 1.0;
|
|||
|
|
this.wrapMode = AnimationWrapMode.Loop;
|
|||
|
|
this.playOnAwake = true;
|
|||
|
|
this._playState = AnimationPlayState.Stopped;
|
|||
|
|
this._currentClip = null;
|
|||
|
|
this._currentTime = 0;
|
|||
|
|
this._direction = 1;
|
|||
|
|
this._clips = [];
|
|||
|
|
this._clipNameToIndex.clear();
|
|||
|
|
}
|
|||
|
|
}
|