デスクトップマスコットを作ってみる。【マスコットを投げる編】

# 変更履歴

- 2021/3/28
    - 内容をjavaのコードに変更
    - タイトルを変更。


#初めに
前回ではアプリケーションの終了処理を実装しました。

これでマスコットを表示して、終了する機能は実現できました。
マウスでの操作も可能ですが、少し物足りないと感じます。

マウスで操作できるんだから、投げたりできれば、なお面白いのでは？

ということで、マスコットを投げられるように改造していきます。








# 投げられるマスコットクラスを作成

マウス操作ができるマスコットを作成しているため、これを継承して
投げられるマスコットクラスを作成していきます。

```java:JDThrowableMascot.java

public class JDThrowableMascot extends JDMascot {

    // 中身はまだ

}

```

投げる機能を入れるということは**投げる力**を計算する必要があります。
複雑なことはせず、なんとなく投げるアニメーションを再生するのに必要な程度の計算をします。

ここでは**投げる力 = 時間当たりのマウス移動量**とします。
**時間当たり**の部分は30msで何ピクセル(ドット？)移動したかとします。

ではまず、計算を開始する起点の処理を作ります。
ここはクリックイベント時を想定して作成します。

```java:JDThrowableMascot.java
/** 投げる力の監視用 */
private Timer timer;
/** 現在位置を記憶する */
private JDPoint currentPoint = new JDPoint(0, 0);
private JDPoint clickScreenPoint = new JDPoint(0, 0);

public JDThrowableMascot() {
    // 投げる力の監視
    timer = new Timer(30, e -> { /* 投げる力を計算するメソッドを呼ぶ */ });
}

private void eventThrowPoint(MouseEvent e) {
    // 投げる力の監視監視
    timer.start();

    // ドラッグしているときの現在位置をクリックイベント時に初期化
    currentPoint.setX(e.getXOnScreen());
    currentPoint.setY(e.getYOnScreen());

    // 最初にクリックした位置を保存
    clickScreenPoint.setX(e.getXOnScreen());
    clickScreenPoint.setY(e.getYOnScreen());
}

```

これで、クリックした際に投げる力を監視するTimer起動と計算時に必要なクリック位置を保存しました。

次はドラッグ時に現在位置を更新する処理を作成します。
とはいっても、やることは単純です。


```java:JDThrowableMascot.java

private void eventDirection(MouseEvent e) {
    // ドラッグ時の移動位置を記憶
    currentPoint.setX(e.getXOnScreen());
    currentPoint.setY(e.getYOnScreen());
}

```


次に投げる力を計算する処理を作成していきます。
これはTimerの30msのインターバルごとに呼ばれます。

```java:JDThrowableMascot.java
/** 移動したピクセル量だと大きすぎるので調整用 */
private static final int DIV = 5;

/** パワーカウントX */
private int powerX;
private JDEnumDirection directionX = JDEnumDirection.CENTER;

/** パワーカウントY */
private int powerY;
private JDEnumDirection directionY = JDEnumDirection.CENTER;

private void eventPowerCount() {

    // 移動量の計算
    var subPointX = currentPoint.getX() - clickScreenPoint.getX();
    var subPointY = currentPoint.getY() - clickScreenPoint.getY();

    // 移動した量なので、絶対値にする
    powerX = Math.abs(subPointX / DIV);
    powerY = Math.abs(subPointY / DIV);

    // 右に移動している場合
    if (0 < subPointX) {
        directionX = JDEnumDirection.RIGHT;
    }
    // 左に移動している場合
    else if (subPointX < 0) {
        directionX = JDEnumDirection.LEFT;
    }

    // 下に移動している場合
    if (0 < subPointY) {
        directionY = JDEnumDirection.DOWN;
    }
    // 上に移動している場合
    else if (subPointY < 0) {
        directionY = JDEnumDirection.UP;
    }

    // 計算し終わったら、現在位置で最初のクリック位置を更新
    clickScreenPoint.setX(currentPoint.getX());
    clickScreenPoint.setY(currentPoint.getY());
}
```

フィールドに定義しているEnumは方向を持つEnumです。

```java:JDEnumDirection.java
public enum JDEnumDirection {
  RIGHT, LEFT, CENTER, UP, DOWN;
}
```

では、必要なイベントに作成したメソッドを設定していきましょう。

今回はコンストラクタに直接記載しますが、専用メソッドを作成して、
それを呼ぶようにしてもかまいません。


```java:JDThrowableMascot.java

public JDThrowableMascot() {
    // パワーの監視
    timer = new Timer(30, (ActionEvent e) -> { eventPowerCount(); });

    // マウスクリック時
    JDIMousePressed eventThrowPoint = e -> { eventThrowPoint(e); };
    this.addMouseListener(eventThrowPoint);

    // ドラッグ中
    JDIMouseDragged eventPowerCount = e -> { eventDirection(e); };
    this.addMouseMotionListener(eventPowerCount);
}
```

これで、投げる力を計算して保存することはできました。
まだ保存しただけなので、ここからマスコットを動かすための処理を作る必要あります。

この処理は別クラスにしましょう。







# マスコットの投げる処理を行うクラスを作成

このクラスは外からもらったX軸とY軸に対しての移動量をもとにマスコットを移動させるクラスになります。
実際の物理法則にのっとるわけではなく、見た目それっぽく動くように値を調整して作っていきます。

```java:JDThrow.java

public final class JDThrow {

    // 中身はまだ

}
```

中身は以下の様に作っていきます。

```java:JDThrow.java

/** 重力加速度 */
private static final int G = 2;
/** X軸方向の力 */
private int powerX;
/** Y軸方向の力 */
private int powerY;
/** アニメーションタイムライン */
private Timer timeline;
// マスコット 方法は指定しないがコンストラクタやsetterでもらう
private JDThrowableMascot stage;
// 画面下に到達したか
private boolean isEndY;

public JDThrow() {
    timeline = new Timer(17, e -> { eventThrow(); });
}

private void eventThrow() {
    // 画面の領域を取得
    var screen = JDScreenValidator.SCREEN;
    // 移動領域 
    var maxX = (int) screen.getMaxX();
    var maxY = (int) screen.getMaxY();

    // 移動後の位置
    var movedX = stage.getX() + powerX;
    var movedY = stage.getY() + powerY;

    // X軸のアニメーション継続判定
    if (JDScreenValidator.isInScreenX(movedX, stage.getWidth())) {
        stage.setX(movedX);
    }  else {
        if (JDScreenValidator.isOverScreenRight(movedX, stage.getWidth())) {
            stage.setX(maxX - stage.getWidth() - 1);
        } else if (JDScreenValidator.isOverScreenLeft(movedX)) {
            // 表示領域の計算
            stage.setX((int) screen.getMinX() + 1);
        }
        powerX = 0;
    }

    if (JDScreenValidator.isInScreenY(movedY, stage.getHeight())) {
        stage.setY(movedY);
    } else {
        if (JDScreenValidator.isOverScreenTop(movedY)) {
            // 表示領域の計算
            stage.setY((int) screen.getMinY() + 1);
        } else if (JDScreenValidator.isOverScreenBottom(movedY, stage.getHeight())) {
            stage.setY(maxY - stage.getHeight() - 2);
            isEndY = true;
        }
    }

    // 重力による力の減衰
    decayPowerY();
    // アニメーションの終了判定
    stopAnimation();
}

private void decayPowerY() {
    powerY += sumG;
}

private void stopAnimation() {
    // 移動終了していた場合
    if (powerX == 0.0 && isEndY) {
        stop();
    }
}

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

public void stop() {
    isEndY = false;
    timeline.stop();
}

public void setPowerX(int powerX) {
    this.powerX = powerX;
}

public void setPowerY(int powerY) {
    this.powerY = powerY;
}
```

ずらっと書きましたが、やっていることとしては画面範囲内であったら、
外からもらった投げる力分、Ｘ軸、Ｙ軸を動かして、範囲外に移動しようとしていた場合は
その位置をキープするようにしています。

Y軸に関しては上に投げた際に、重力で下に落ちるようにするため、投げる力の減衰処理を入れています。
ほかの要素で減衰処理などを追加するとより良いかもしれません。

アニメーションクラス内で使用しているバリデータクラスは以下のようなクラスです。

```java:JDScreenValidator.java

/** 画面サイズ */
public static final Rectangle SCREEN = GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds();

public static boolean isInScreenX(int x, int width) {
    // 現在のウィンドウ位置を計算
    var windowX = x + width;
    // 領域内のみ移動可能とする
    return SCREEN.getMinX() < x && windowX < SCREEN.getMaxX();
}

public static boolean isOverScreenRight(int x, int width) {
    // 現在のウィンドウ位置を計算
    var windowX = x + width;
    var maxScreenWidth = SCREEN.getMaxX();

    return maxScreenWidth <= windowX;
}

public static boolean isOverScreenLeft(int x) {
    return x <= SCREEN.getMinX();
}

public static boolean isInScreenY(int y, int height) {
    // 現在のウィンドウ位置を計算
    var windowY = y + height;
    // 領域内のみ移動可能とする
    return SCREEN.getMinY() < y && windowY < SCREEN.getMaxY();
}

public static boolean isOverScreenBottom(int y, int height) {
    // 現在のウィンドウ位置を計算
    var windowY = y + height;
    var maxScreenHeight = SCREEN.getMaxY();

    return maxScreenHeight <= windowY;
}

public static boolean isOverScreenTop(int y) {
    return y <= SCREEN.getMinY();
}

```

ここまで来たら、あと一息。
投げられるマスコットに作成したアニメーションを開始する処理を追加します。

```java:JDThrowableMascot.java

public JDThrowableMascot() {

    // ほかの処理は省略

    // マウスクリック終了時
    JDIMouseReleased eventThrowStart = e -> { eventThrowStart(e); };
    this.addMouseListener(eventThrowStart);

    // ほかの処理は省略
}


private void eventThrowStart(MouseEvent e) {

    // 左方向ならパワー値を反転する
    if (directionX == JDEnumDirection.LEFT) {
        powerX *= (-1);
    }

    // 上方向ならパワー値を反転する
    if (directionY == JDEnumDirection.UP) {
        powerY *= (-1);
    }

    // パワーの設定
    timeline.setPowerX(powerX);
    timeline.setPowerY(powerY);

    timeline.start();

    // 設定は初期化
    powerX = 0;
    powerY = 0;
    directionX = JDEnumDirection.CENTER;
    directionY = JDEnumDirection.CENTER;

    timer.stop();
}

```

# テストコードの作成

では、テストコードを作成してテストしてみましょう。

```java:JDThrowableMascotTest.java

public class JDThrowableMascotTest {
    public static void main(String[] args) throws Throwable {
        JDThrowableMascot win = new JDThrowableMascot();
        BufferedImage image = ImageIO.read(new File("desktop.png"));

        win.setMascot(image);
        win.open();
    }
}

```


実際の映像をご覧ください。
Macのままですが、gifはそのうち差し替えます。
![タイトルなし.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/a480bdef-32e9-b9b3-1ade-57bd30e0203e.gif)



うむ。



#終わりに

今回は、マスコットの投げる処理を作成しました。
次回は、マスコットの動きを行うアニメーションを作成していこうと思います。

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