デスクトップマスコットを作ってみる。【アニメーション管理編】

# 変更履歴
- 2021/3/28
    - 内容をjavaのコードに変更
    - 設計の説明に図を追加
    - タイトルの変更
    - リンクの追加

#初めに
前回までの内容でマウスで投げることができるようになりました。
今回はマスコット自身のアニメーションを行う機能を作成していこうと思います。

設計内容として言葉で説明すると複雑になると思いますので、
図を使って説明をしようと思います。














# アニメーションに関わるクラスの実装方針

アニメーション切り替えについての設計をざっと図にした内容が以下になります。
![20210328_001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/9d76b9a1-6b55-f296-0f4d-44c3d702ae5c.png)


アニメーションに関する処理のざっくりとした流れは以下のようなイメージです。

1. アニメーションを切り替えるイベントが発生する。
1. アニメーションコントローラに遷移対象のアニメーション情報(Enum)を渡す。
1. アニメーションタイムラインマネージャからEnumをキーとしてアニメーションタイムラインを取得する。
1. アニメーションコントローラがアニメーションタイムラインから取得したアニメーションに切り替える。
1. 切り替え後、一定間隔でアニメーションタイムラインクラスの処理が実行される。
1. 実行される処理の中で、アニメーションフレームマネージャからアニメーションフレームの配列を取得する。
1. フレームカウント番目のフレームに表示を切り替える。

アニメーションの切り替えイベントが発生するまでは、アニメーションタイムライン内で７番をくりかえすような感じです。

この方針で作成していきます。
本記事ではアニメーションタイムとタイムラインマネージャを作成していきます。








# アニメーションタイムライン
アニメーションタイムラインクラスはマスコットの表示している画像を差し替える機能を有します。

```java:JDTimeline.java

public final class JDTimeline {
    /** アニメーションが最終フレームの場合に遷移する対象 */
    private JDIEnumAnimation transitionOnEndFrameTarget;
    /** 実行中か */
    private boolean isRunning;
    /** フレームの位置 */
    private int frameCount;
    /** アニメーションの長さ */
    private int frameLength;
    /** ループをするか */
    private boolean isLoop;
    /** 最終フレームで遷移するか */
    private boolean isTransitionOnEndFrame;
    /** フレーム間の停止時間 */
    private int waitTime = 30;
    /** アニメーションフレームのキー情報 */
    private JDIEnumAnimation animationFrameKey;
    /** アニメーション再生クラス */
    private Timer timeline;
    /** 最終フレームか */
    private boolean isEndFrame;

    // 以下getter、setter

```

アニメーションに必要な情報をフィールドに定義していますが、
アニメーションを再生する処理も必要なので、作成していきます。

```java:JDTimeline.java

private void doAnimation() {
    var isTransitioned = false;

    // 最終フレームか判断
    isEndFrame = frameCount >= frameLength;

    // 繰り返す場合
    if (isLoop) {
        // 現在フレームがフレーム長以上の場合は０にもどす
        if (isEndFrame) {
            frameCount = 0;
        }
        // 最終フレームで遷移する設定の場合
        if (isEndFrame && isTransitionOnEndFrame) {
            isTransitioned = transitionAfterFinished();
        }
    } else if (isEndFrame) {
        // 繰り返しのアニメーションではなく、最終フレームの場合で
        // 最終フレームで遷移する設定の場合
        if (isTransitionOnEndFrame) {
            transitionAfterFinished();
        }
        // 最終フレームに遷移したら動かさない
        return;
    }

    // すでに遷移済みの場合は以降を処理しない
    if (isTransitioned) {
        return;
    }

    // フレームマネージャから遷移対象のフレームを取得する
    var f = JDFrameManager.getAnimationFrame(animationFrameKey);
    var image = f.get(frameCount);

    // マスコットの画像を差し替える
    // JDMascotManagerを使って取得しているが、staticで参照を引き継いでいるだけ。
    var mascot = JDMascotManager.getMascot();
    mascot.setMascot(image);

    frameCount++;
}

private boolean transitionAfterFinished() {
    if (transitionOnEndFrameTarget != null) {
        return JDAnimationController.transitionOnEndFrame(transitionOnEndFrameTarget);
    }
    return false;
}

```
**JDAnimationController**クラスの説明は次の記事で説明するため、割愛します。

上記の処理を一定時間ごとに動かす処理を**crateAnimation**メソッドとして作成します。
ついでに、外からアニメーションの開始・終了ができるようにしておきます。


```java:JDTimeline.java

public void crateAnimation() {
    // アニメーションを再生するタイマーを生成
    timeline = new Timer(waitTime, e -> { doAnimation(); });
    // 開始までの遅延は０
    timeline.setInitialDelay(0);
}

public void start() {
    timeline.start();
}

public void stop() {
    timeline.stop();
}

```











# アニメーションタイムラインマネージャ

アニメーションタイムラインマネージャクラスを作成します。

```java:JDTimelineManager.java
public final class JDTimelineManager {
    // 中身はまだ
}
```

タイムラインの管理をする際に事前に必要な情報を持っておく必要があるため、
その情報を読み込む処理を作成します。

引数ではEnumの配列をもらい、そこに設定されているファイルパスから
アニメーションタイムラインの設定を読み込んでいきます。

設定ファイルはJsonで定義します。

```java:JDTimelineManager.java
/** アニメーション管理用 */
private static HashMap<JDIEnumAnimation, JDTimeline> manager = new HashMap<>();
/** jackson用 */
private static ObjectMapper mapper = new ObjectMapper();

public static void load(JDIEnumAnimation[] keys) {

    for (var animationKey : keys) {
        var timeline = new JDTimeline();

        String json;
        JDAnimationInfo info;

        try {
            // Jsonをクラスに変換
            json = new String(Files.readAllBytes(animationKey.getInfoName()));
            info = mapper.readValue(json, JDAnimationInfo.class);
        } catch(Throwable e) {
            // 何かしらの処理
        }

        // アニメーションがループするか
        timeline.setLoop(info.isLoop());
        // アニメーションのフレーム間遅延時間
        timeline.setWaitTime(info.getWaitTime());
        // 最終フレームに到達してからアニメーションを切り替えるか
        timeline.setTransitionOnEndFrame(info.isTransitionOnEndFrame());
        // アニメーションの長さ
        timeline.setFrameLength(info.getFrameLength());
        // アニメーションのキー情報(Enum)
        timeline.setAnimationFrameKey(animationKey);
        // 渡した情報をもとにアニメーションを作成する
        timeline.crateAnimation();

        // Mapに保存
        manager.put(animationKey, timeline);
    }
}
```

読み込んだタイムラインを取得する処理を作成します。
これは引数でEnumを受け取って、Mapから返却するだけでかまいません。

と思ったんですが、nullチェックして内容が設定されていない場合は
例外をスローするような形にしました。

```java:JDTimelineManager.java
public static JDTimeline getAnimationTimeline(JDIEnumAnimation key) {
    if (manager.get(key) == null) {
        // 例外をスローするなど
    }

    var buffer = manager.get(key);
    return buffer.get(key);
}
```







# アニメーション情報クラス

このクラスはJsonで定義されたデータを受け取るようのクラスです。
アニメーションタイムラインマネージャ内で使用されます。

```java:JDAnimationInfo.java
public final class JDAnimationInfo {
    /** アニメーションをくりかえすか */
    private boolean isLoop = false;
    /** 最終フレームで次のアニメーションに移るか */
    private boolean isTransitionOnEndFrame = false;
    /** フレーム間の停止時間 */
    private int waitTime = 30;
    /** アニメーションの長さ */
    private int frameLength = 0;

    // 以降getter、setterを定義

}
```











# アニメーション情報Enum

アニメーション情報Enumは、アニメーションに必要な情報を定義しているJsonファイルが
どこに格納しているかを定義するEnumになります。

このEnumはマスコットを実際に作成する際に、作成するものであるためインターフェースとして定義します。

```java:JDIEnumAnimation.java
public interface JDIEnumAnimation {
    /**
     * アニメーションフレームの格納場所を取得します。
     *
     * @return アニメーションフレームのファイルパス
     */
    String getFramePath();

    /**
     * 次のアニメーションへ遷移可能かを判定します。
     *
     * @param paramJDIAnimationEnum 遷移先
     * @return 遷移可能か
     */
    boolean transitional(JDIEnumAnimation status);

    /**
     * <p>アニメーション設定のファイルパスを取得します。</p>
     * <p>defaultはanimation_info.jsonです。</p>
     *
     * @return アニメーション設定ファイルのパス
     */
    default String getInfoName() {
        return "animation_info.json";
    }
}
```





# テストコード

今回はテストするためのクラスが足りないので、テストはしません。


#終わりに
今回はアニメーションを再生するために必要なクラスを作成しました。
次はアニメーションコントローラを作成する予定です。

# リンク
- [デスクトップマスコットを作ってみる。【目次】](https://qiita.com/urado/items/4a810c457cb862e370e8)