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

# 初めに
前回までの内容では、アニメーションの再生を行うタイムラインを管理するクラスの作成を行いました。
今回はアニメーションの遷移を行う用のクラスを作成していきます。

設計は、[前回の記事](https://qiita.com/urado/items/f951f5f05d74be4dc9b4)の**実装方針**の箇所に記載している通りに進めていきます。

ここができると、やっとマスコットらしきものが完成します。

# アニメーションコントローラ
アニメーションコントローラはマスコットから特定のイベントでアニメーションを切り替えたい場合に
指定のアニメーションへの切り替えを行うクラスです。

現在のアニメーションの状態から遷移可能かを判定し、遷移可能であれば現在のアニメーションを停止して
遷移先のアニメーションへと切り替えを行います。

アニメーションの現在の状態を保持するために、staticで状態を持つようにします。
操作するメソッドは同期処理にしたほうがいいかもです。

```java:JDAnimationController.java

public final class JDAnimationController {

  /** アニメーションステータス */
  public static JDIEnumAnimation STATUS;
  
  // 中身はこれから
}

```

指定されたアニメーションへ遷移する処理になります。

これは単純にSTATUSのアニメーションを管理マネージャから取得して
変更前のアニメーションを停止してから値の初期化を行い
変更後のアニメーションを開始するだけです。


```java

/**
 * アニメーションを遷移を行います。
 *
 * @param target 遷移対象のアニメーション
 */
public static synchronized void animationTransition(JDIEnumAnimation target) {
    var timeline = JDTimelineManager.getAnimationTimeline(STATUS);
    transition(target);
}

/**
 * アニメーションの遷移を行います。
 *
 * @param target 遷移対象のアニメーション
 * @return 遷移ができたか
 */
private static boolean transition(JDIEnumAnimation target) {
    // 一番最初もしくは遷移可能なアニメーションであるか
    if (STATUS == null || target.transitional(STATUS)) {
        var before = JDTimelineManager.getAnimationTimeline(STATUS);
        var after = JDTimelineManager.getAnimationTimeline(target);

        before.stop();
        before.setFrameCount(0);
        before.setTransitionOnEndFrameTarget(null);
        before.setRunning(false);

        after.start();
        after.setRunning(true);

        STATUS = target;
        return true;
    }
    return false;
}

```

アニメーションコントローラの内容は一旦このぐらいで、問題ないかと思います。
（本当はいろいろ必要なことがある気がするけど、後でリファクタするとして）

**transition** メソッドがbooleanを返却する用にしているのは、
後々別の処理で変更ができたかどうかを返せるようにするためです。

（使うと思っていますが、使わなかったらその時に直します）

# アニメーションフレームマネージャー

アニメーションをする際に必要な画像はアニメーション情報Enumで定義します。

この情報を使用して、事前にアニメーションの画像をメモリにロードしておき、
アニメーションの切り替えが発生した際に、この管理クラスから画像を取得するようにします。

```java:JDFrameManager.java

    /** アニメーションフレームのバッファリング */
    private static final HashMap<JDIEnumAnimation, ArrayList<BufferedImage>> frameBuff = new HashMap<>();

    // 中身はこれから

```

JDFrameManagerクラスでは、アニメーションに使用する画像をEnumに定義している数だけ、保持するようにします。
管理クラス内で保持用のHashMapを定義して起き、ロード処理でこの中に情報を格納していきます。

読み込むファイルはPNGに限定しています。

```java

/**
 * アニメーションのロードを行います。
 *
 * @param frameList アニメーションフレームのリスト
 */
public static void load(JDIEnumAnimation[] frameList) {

    if (frameList == null) {
        return;
    }

    for (JDIEnumAnimation key : frameList) {
        frameBuff.put(key, getAnimationFrameBuffer(key));
    }
} 

/**
 * アニメーションフレームバッファーを取得します。
 *
 * @param path アニメーション情報
 * @return アニメーションフレームバッファー
 */
private static ArrayList<BufferedImage>> getAnimationFrameBuffer(JDIEnumAnimation path) {
    var buffer = new ArrayList<BufferedImage>>();

    // ディレクトリのオブジェクトのみ出力
    var item = getFrameFileList(path.getFramePath());

    // ディレクトリが存在した場合
    if (!item.isEmpty()) {
        // アニメーションリストを追加
        buffer = getFrameList(item);
    }

    return buffer;
}

public static File getFrameFileList(String path)  {
    // Enumに指定されているファイル名からファイルオブジェクトを取得
    var filePath = Paths.get("").toAbsolutePath().toString();
    filePath = Paths.get(filePath, path).toString();
    return new File(filePath);
}

/**
 * フレームのリストを取得します。
 *
 * @param file フレームのファイル情報
 * @return イメージファイルリスト
 */
private static ArrayList<BufferedImage> getFrameList(File file) {
    // 返却用オブジェクト
    var list = new ArrayList<BufferedImage>();
    // ストリームに変換
    try (var stream = Arrays.stream(file.listFiles())) {
        // ファイルのみのイメージ配列を作成する
        stream.filter((File f) -> f.isFile() && f.getName().indexOf(".png") > -1).sorted()
        .forEach((File obj) -> {
            try {
                list.add(ImageIO.read(obj));
            } catch (IOException e) {
                // エラー処理
            }
        });
    }

    if (list.size() < 1) {
        // ファイルの中身がない場合は例外
        // エラー処理
    }

    return list;
}

```



後は、ロードしたフレームを取り出すためのメソッドを実装していきます。


```java

/**
 * アニメーションフレームを取得します。
 *
 * @param key アニメーション情報
 * @return アニメーションフレーム
 */
public static ArrayList<BufferedImage> getAnimationFrame(JDIEnumAnimation key) {
    if (frameBuff.get(key) == null) {
        // エラー処理
    }
    var buffer = frameBuff.get(key);
    return buffer.get(key);
}

```

# マスコットアプリケーションの起動

アニメーションを行う用のクラスが大体できたので、実際に動かす用にマスコット起動用クラスを作成します。
（ここは、あってもなくてもいいと思いますが、共通的に使えるように作成しておきます）

抽象クラスにして、継承先のクラスで個別に実装したマスコットクラスをもらうようにして
マスコットの起動を行います。

また、継承先からアニメーション情報Enumの情報を取得するようにして、必要なロード処理を
行うようにします。

```java

/**
 * マスコットアニメーションを起動するためのクラスです。
 *
 */
public abstract class JDApplication {

    /**
     * マスコットアプリケーションを起動します。
     *
     * @param clazz
     */
    public static void launch(Class<? extends JDApplication> clazz) {
        try {
            var constructor =  clazz.getConstructor();
            var app = constructor.newInstance();
            app.execute();
        } catch (Throwable e) {
            // ログにエラーを出力する処理を入れること
            System.exit(0);
        }
    }

    /**
     * アプリケーションの開始
     */
    private void execute() {
        // システムトレイの追加
        JDSystemTray.addSystemTray();
        // 例外ハンドラを設定
        Thread.setDefaultUncaughtExceptionHandler((Thread t, Throwable e) -> { loggingException(t, e); });
        // マスコットクラスの取得
        var mascot = getMascot();

        // アニメーションの読み込み
        JDTimelineManager.load(getAnimationInfo());
        // アニメーションフレームの読み込み
        JDFrameManager.load(getAnimationInfo());

        // マスコットの起動
        mascot.open();

        JDLogger.info("アプリケーションを開始しました。");
    }

    /**
     * 例外出力処理.
     */
    private void loggingException(Thread t, Throwable e) {
        // ログに出力する処理をいれる
        System.exit(0);
    }

    /**
     * 独自実装したアニメーション情報を取得するメソッド
     */
    public abstract JDIEnumAnimation[] getAnimationInfo();

    /**
     * 独自実装したマスコットを返却するメソッド
     */
    public abstract JDMascot getMascot();
}

```


# テスト
記事を分割してしまったので、ちょっとわかりづらくなっていますが
いままで作成したクラスでマスコットを動かすことができるようになっているはず・・・。

それを確認する用のテストをしてみます。
（個々の単体テストをするテストコードはまた別途作成しようと思います）

まずはアニメーションに必要な情報を持っているEnumを作成します。
定義するのは**各アニメーションに必要な画像ファイルの場所**と**そのアニメーションに遷移可能な状態**です。

```java:JDEnumAnimationTest.java

public enum JDEnumAnimationTest implements JDIEnumAnimation {
    OPENING {

		@Override
		public String getFramePath() {
			return "animation/opening";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return OPENING == status;
		}

    },
    IDLE {

		@Override
		public String getFramePath() {
			return "animation/idle";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return true;
		}

    },
    DRAG {

		@Override
		public String getFramePath() {
			return "animation/drag";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return IDLE == status || WALK == status;
		}

    },
    THROW {

		@Override
		public String getFramePath() {
			return "animation/throw";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return DRAG == status;
		}

    },
    WALK {

		@Override
		public String getFramePath() {
			return "animation/walk";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return IDLE == status;
		}

    },
    DOWNUP {

		@Override
		public String getFramePath() {
			return "animation/downup";
		}

		@Override
		public boolean transitional(JDIEnumAnimation status) {
			return THROW == status;
		}

    };
}

```

テストで起動するための処理（mainメソッド）と先ほど作成したマスコット起動用の
抽象クラスの実装、マスコットの各イベントにおいてのアニメーション切り替え処理を
まとめて作ります。

```java:JDApplicationTest.java

public class JDApplicationTest {

	public static void main(String[] args) {
		JDApplication.launch(TestTApp.class);
	}
}

class TestTMascot extends JDThrowableMascot {

	public TestTMascot() {

	}

	@Override
	public void eventIsMouseDragged(MouseEvent paramMouseEvent) {
        // マウスドラッグイベントで使用するアニメーションに切り替え
		JDAnimationController.animationTransition(JDEnumAnimationTest.DRAG);
	}

	@Override
	public void eventIsMouseReleased(MouseEvent paramMouseEvent) {
        // マウスボタンの離したイベントで使用するアニメーションに切り替え
		JDAnimationController.animationTransition(JDEnumAnimationTest.THROW);
	}

	@Override
	public void eventIsGround() {
		super.eventIsGround();

        // 地面に接地した際のイベントで使用するアニメーションに切り替え
		JDAnimationController.animationTransition(JDEnumAnimationTest.DOWNUP);
		JDAnimationController.animationTransition(JDEnumAnimationTest.IDLE);
	}
}

class TestTApp extends JDApplication {

	public TestTApp() {

	}

	@Override
	public JDIEnumAnimation[] getAnimationInfo() {
		return JDEnumAnimationTest.values();
	}

	@Override
	public JDMascot getMascot() {
		return new TestTMascot();
	}
}

```

あ、アニメーションに必要な画像たちは事前に用意して、Enumで定義した箇所に配置しておきます。

単純なので割愛していますが、停止時のアニメーションや歩くアニメーションは
切り替えるイベントがユーザの操作で発生しないため、個別の処理として
ランダムに切り替わる処理をrunnerクラスで作成して入れてます。

実際に動かしている内容が以下になります。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/06afe46a-03e1-6ef3-6655-0cb0a8cc2931.gif)


# 今後の課題

ざっと作っただけなので、まだ改善すべき点が今回動かしてみてわかりました。

* runnerとして作った歩く、停止のアニメーションの切り替えがランダムすぎて切り替わらない時間が長いときがある。
* 投げる際のアニメーションの向きは、その前に動いた方向に依存しているため投げた方向と同じ向きにならない。
* 停止時のアニメーションが一つしか設定できないので、いくつか設定できるようにしたい。
* アニメーションに使用している画像ファイルを起動時に一気にロードするので、画像ファイルが多いと起動が遅い。
  または、起動時にメモリーが足らなくなる可能性がある。
  実際の画像ファイルの容量より、読み込んだ時のほうがメモリ使用量が多くなっているので、
    代替案を検討したほうがいいかも

処理のリファクタも含めて、今後の課題としてきます。


# 終わりに

これでおおむね完成となります。

マスコットアプリケーション自体は機能の微修正やら処理内容を変えたりなどまだまだ作成中ですが、
細かいところなので記事にしていくのは一旦ここまでとしようと思います。

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