# 精灵动画实体

`SpriteAnimatorEntity` 是由 实体`Entity` 和 `SpriteAnimatorModule` 模块组合而成的新的实体。

 [SpriteAnimatorEntity](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html) =  [Entity](https://vjmap.com/map3d/api/classes/Entity.html) + [SpriteAnimatorModule](https://vjmap.com/map3d/api/classes/SpriteAnimatorModule.html) 

 ## 常用方法

 `SpriteAnimatorEntity` 是由 实体`Entity` 派生而来。具体 [Entity](https://vjmap.com/map3d/api/classes/Entity.html) 的所有属性和方法。同时也具有[SpriteAnimatorModule](https://vjmap.com/map3d/api/classes/SpriteAnimatorModule.html)  的相关方法。通过 [spriteAnimator](https://vjmap.com/map3d/api/classes/SpriteAnimatorModule.html#spriteAnimator) 对象 可访问相关方法

|方法|描述|
|:---|:---|
|[spriteAnimator](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#spriteAnimator)|获取精灵动画对象|
|[pauseAnimation](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#pauseAnimation)|暂停动画|
|[playAnimation](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#playAnimation)|运行动画|
|[setFrameName](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#setFrameName)|设置动画帧名称（切换动画）。⚠️ 仅对 JSON Hash 格式有效；资源加载完成前调用是空操作|

## 支持格式

动画格式支持`动画图像集`(将所有动画图像放至一张图片上) 和 精灵图(Sprite Sheet)格式(它描述了一个包含多帧图像的动画序列，每帧图像在一个较大的图像中的位置和大小。这种格式广泛用于2D游戏开发和其他图形应用中)。

> **各格式的必填项**：
> - **动画图像集**（只给 `textureImageURL`）：必须传 `numberOfFrames`，且所有帧须**横向排成一行**、图片宽度能被帧数整除，否则帧列表为空、不播放。
> - **JSON Array**（`frames` 是数组，如 TexturePacker 的 "JSON (Array)"）：按数组顺序整体播放，无需 `animationNames`，`frameName`/`setFrameName` 无作用。
> - **JSON Hash**（`frames` 是以帧名为 key 的对象）：必须传 `animationNames`（按"帧名包含该字符串"分组成多段动画），并用 `frameName` 指定初始播放哪一段；缺 `animationNames` 时加载阶段抛错、实体始终不显示。

精灵图(Sprite Sheet)格式创建工具 [texturepacker](https://www.codeandweb.com/texturepacker)

## 使用示例

### 创建SpriteAnimatorEntity

```js
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true,  } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let spriteAnimatorEnt = new vjmap3d.SpriteAnimatorEntity({
    startFrame: 0,
    autoPlay: true,
    loop: true,
    numberOfFrames: 16,
    textureImageURL: env.assetsPath + "sprites/alien.png",
    asSprite: true
  })
spriteAnimatorEnt.position.set(-2, 0, 2);
spriteAnimatorEnt.addTo(app);
```

### 加载精灵图(Sprite Sheet)格式动画

```js
let spriteAnimatorEnt = new vjmap3d.SpriteAnimatorEntity({
    startFrame: 0,
    fps: 40,
    autoPlay: true,
    loop: true,
    textureImageURL:  env.assetsPath + "sprites/flame.png",
    textureDataURL:  env.assetsPath + "sprites/flame.json",
    asSprite:  true
 })
spriteAnimatorEnt.position.set(2, 0, 2);
spriteAnimatorEnt.addTo(app);
```

### 动画控制

[pauseAnimation](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#pauseAnimation) 暂停动画

[playAnimation](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#playAnimation)  运行动画

[setFrameName](https://vjmap.com/map3d/api/classes/SpriteAnimatorEntity.html#setFrameName) 设置动画帧名称——**只对 JSON Hash 格式**（构造时传了 `animationNames`）有效；对上面的图像集/JSON Array 实体调用无效果。在资源加载完成（模块 `isLoaded`）之前调用会被直接忽略，应在加载完成后（如用户点击时）再调用。

```js
// JSON Hash 格式：帧名里含 'idle' / 'celebration' 的帧各成一段动画
const animNames = ['idle', 'celebration'];
let boyEnt = new vjmap3d.SpriteAnimatorEntity({
    frameName: 'idle',          // 初始播放的动画段
    animationNames: animNames,  // JSON Hash 必填
    fps: 24,
    autoPlay: true,
    loop: true,
    textureImageURL: env.assetsPath + "sprites/boy_hash.png",
    textureDataURL: env.assetsPath + "sprites/boy_hash.json",
    asSprite: true
});
boyEnt.addTo(app);
// 须在加载完成后调用（构造后立即调用会被忽略），例如放在按钮回调里切换动画段
const switchAnim = () => boyEnt.setFrameName('celebration');
```

### 事件交互
```js
spriteAnimatorEnt.add(vjmap3d.EventModule, {
    hoverSelect: true,
    clickCallback: (ent, isClick) => {
        if (!isClick) return
        app.logInfo("您点击我了")
    }
});
```

### 示例

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true,  } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let spriteAnimatorEnt = new vjmap3d.SpriteAnimatorEntity({
    startFrame: 0,
    autoPlay: true,
    loop: true,
    numberOfFrames: 16,
    textureImageURL: env.assetsPath + "sprites/alien.png",
    asSprite: true
  })
spriteAnimatorEnt.position.set(-2, 0, 2);
spriteAnimatorEnt.addTo(app);
spriteAnimatorEnt.add(vjmap3d.EventModule, {
    hoverSelect: true,
    clickCallback: (ent, isClick) => {
        if (!isClick) return
        app.logInfo("您点击我了")
    }
});
// fire
let entFire = new vjmap3d.SpriteAnimatorEntity({
    startFrame: 0,
    fps: 40,
    autoPlay: true,
    loop: true,
    textureImageURL:  env.assetsPath + "sprites/flame.png",
    textureDataURL:  env.assetsPath + "sprites/flame.json",
    asSprite:  true
 })
entFire.position.set(0, 0, -5);
entFire.addTo(app);
//
const animNames = ['idle', 'celebration']
let entBoy = new vjmap3d.SpriteAnimatorEntity({
    frameName: 'idle',
    fps: 24,
    animationNames: animNames,
    autoPlay: true,
    loop: true,
    textureImageURL: env.assetsPath + "sprites/boy_hash.png",
    textureDataURL: env.assetsPath + "sprites/boy_hash.json",
    asSprite: true,
})
entBoy.position.set(2, 0, 2);
entBoy.scale.set(3, 3, 3)
entBoy.addTo(app)
//
let aniIndex = 0;
const ui = await app.getConfigPane({ title: "设置", style: { width: "250px", overflow: "hidden"}});
let cfg = [
    {
        type: 'button',
        label: '切换动画',
        value: ()=> {
            let name = animNames[++aniIndex % 2];
            entBoy.setFrameName(name)
        }
    },
    {
        type: 'button',
        label: '停止动画',
        value: ()=> {
            entBoy.pauseAnimation()
        }
    },
    {
        type: 'button',
        label: '开始动画',
        value: ()=> {
            entBoy.playAnimation()
        }
    },
]
cfg.forEach(c => ui.appendChild(c));
</pre>
</mapFrame>
