カスタムE で (((🍮)))

Akashic Advent Calendar 2019 七日目の記事です。

Akashic Engine では画面に画像や文字などを表示するために、様々なクラスが用意されています。それらはどれも `g.E` の派生クラスとして実装されています。この記事では、ちょっと変わった `g.E` の派生クラスとして、 `append()` された要素をなんでもプリンのようにプルプルさせる E (以降カスタムE)を実装してみます。

## ゴールの確認

最初に完成形を確認しましょう。

![akashic-pudding-demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/28cd6d9c-d81d-459e-9ea4-513d2dfbbeaa.gif)

_プリンの画像は [こちら](http://putiya.com/html/food/okashi01_03.html) のものを使用しています。_

プリンがクリックされるとプルプルします。プリンの画像は `g.Sprite` で表示されていて、動きはカスタムEによるものです。

## 二つの課題

プリンを実現するためには、「震える運動」と「画像の変形」の２つの機能が必要そうです。１つ１つ検討します。

### 震える運動

震える運動を考えます。プリンは変形すると元の形に戻ろうとします。これはバネの動きに似ています。

次のような、一方が固定され、もう一方に質点が繋がれたバネを考えてみます。質点を移動させると、振動して元の位置に戻ります。

```
+∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂∂●
|-------- len ---------|
```

- + : バネを固定した位置。
- ● : 質点
- len: バネの自然長（伸び縮みしていない、本来の長さ）。

そのほかにもバネの運動に必要なパラメータがあります。

- k: バネ係数。バネの伸び縮みに対する復元力を求めるための量。
- m: 質点の重さ。
- d: 質点の速度を減衰させる係数。

これらを用いて、バネの先端に取り付けられた質点の運動をコードの形で表現してみます。

```typescript
/**
 * バネの先端に取り付けられた質点の位置と速度を求める。
 * 
 * @param dt 経過時間。
 * @param x 質点の位置。
 * @param v 質点の速度。
 */
function massSpringSolver(dt: number, x: number, v: number): { x: number, v: number } {
    const k = 64; // バネ係数。
    const len = 10; // バネの自然長。
    const m = 1; // バネの先端に繋がれた質点の質量。
    const d = 0.95; // 徐々に減速させるための値。

    const dx = x - len; // バネの伸び縮みを求める。
    const f = -dx * k; // 伸び縮みにバネ係数をかけて、質点に加わる力を求める。
    const a = f / m; // 質点に加わっている力を質点の質量で割って、加速度を求める。

    v += a * dt; // 質点を加速させる。
    v *= d; // 質点が徐々に静止するよう、速度を減衰させる。
    x += v * dt; // 速度に応じて質点の位置を更新する。

    return { x, v }; // 新しい質点の位置と速度を返す。
}
```

振動しながら徐々に元の位置に戻る、という処理が実現できそうです。

### 画像の変形

次に、質点の運動を用いて画像を変形することを考えます。

Akashic Engine のレンダリング機能を司る `g.Renderer` は画像の平行移動・回転・拡大縮小に行列を用います。行列は２行３列で、コード上は１次元配列で表されます。

```typescript
const matrix = [
    a, b,
    c, d,
    e, f
];
```

この６つの数値はどんな働きがあるのでしょうか。詳しい説明は省略しますが、 (a, b) はX軸を、(c, d)はY軸を変形させる働きがあります。

次の行列は平行移動・回転・拡大縮小しません。

```typescript
const matrix = [
    1, 0,
    0, 1,
    0, 0
];
```

次の行列はY軸方向に２倍に拡大します。

```typescript
const matrix = [
    1, 0,
    0, 2,
    0, 0
];
```

バネの力で運動する質点の位置を行列にうまく組み込めば、プルプルとした変形が実現できそうです。

## 完成

振動の方法と、それを変形に用いる方法がわかりました。これらを組み込んだカスタムE `Puddinizer` のコードは次のようになります。

```typescript
interface PuddinizerParameterObject extends g.EParameterObject {
	/** バネ定数 */
	k: number;

	/** 質量 */
	m: number;

	/** 減衰係数 */
	d: number;
}

class Puddinizer extends g.E {
	private k: number;
	private m: number;
	private d: number;
	private px: number;
	private py: number;
	private vx: number;
	private vy: number;

	constructor(param: PuddinizerParameterObject) {
		super(param);

		this.k = param.k;
		this.m = param.m;
		this.d = param.d;
		this.px = 0;
		this.py = 0;
		this.vx = 0;
		this.vy = 0;

		// 毎フレーム自身を更新する。
		this.update.add(() => this.onUpdate());
	}

	/**
	 * プリンを突っつく。
	 *
	 * @param px 質点の位置(X成分)
	 * @param py　質点の位置(Y成分)
	 * @param vx 質点の速度(X成分)
	 * @param vy 質点の速度(Y成分)
	 */
	poke(px: number, py: number, vx: number, vy: number): void {
		this.px = px;
		this.py = py;
		this.vx = vx;
		this.vy = vy;
	}

	renderSelf(renderer: g.Renderer, camera?: g.Camera): boolean {
		// ここで設定した行列が子の E に作用する。
		renderer.transform([
			1, 0,
			// Y軸を表す成分に質点の位置を加味する。
			0 + this.px, 1 + this.py,
			0, 0
		]);
		return true;
	}

	private onUpdate(): void {
		const dt = 1 / g.game.fps;

		const ax = (-this.px * this.k) / this.m;
		const ay = (-this.py * this.k) / this.m;

		this.vx *= this.d;
		this.vy *= this.d;
		this.vx += ax * dt;
		this.vy += ay * dt;
		this.px += this.vx * dt;
		this.py += this.vy * dt;

		this.modified();
	}
}
```

振動の計算は `onUpdate()` で、それを用いた行列の設定は `renderSelf()`で行われています。

ここまでの検討とは次の点で違いがあるので注意してください。

- 平面上の振動を計算するため、質点の位置を px, py の２変数で表している。
- 位置 (0, 0) に固定された自然長 0 のバネに質点が繋がれている、として振動を計算している。

プルプルさせるカスタムEの実装は以上です。`Puddinizer` のデモは[GitHubから入手](https://github.com/blackspotbear/akashic-sample/tree/master/pudding)できます。

## 最後に

この記事では子要素に動きをつけるカスタムEを実装しました。質量やバネ定数を変更すると、色々な質感が表現できます。ゲームなら、スライムのダメージ表現といった使い方もできそうです。