紙吹雪を飛ばしてみよう

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

ゲーム開発中、プレイ内容に応じて演出を加えたくなることがあります。この記事では、そういった演出に利用できそうな紙吹雪エンジン [kamihubuki-js](https://github.com/blackspotbear/kamihubuki-js) を Akashic Engine 上で実行する手順を解説します。

## kamihubuki-js とは

[kamihubuki-js](https://github.com/blackspotbear/kamihubuki-js) は2Dの紙吹雪専用物理エンジンです。エンジン自体は描画機能を持っておらず、利用者が実装する必要があります。

![confetti-anim.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/e60bc871-07f2-1a1a-e0b3-6da5bda648b4.gif)

_動画は [kamihubuki-js](https://github.com/blackspotbear/kamihubuki-js/blob/master/README.ja.md) から転載。_

## kamihubuki-js の導入

kamihubuki-js は npmjs に publish されていません。GitHubからインストールします。

```sh
npm install blackspotbear/kamihubuki-js
```

## Akashic Engine からの利用

[kamihubuki-js](https://github.com/blackspotbear/kamihubuki-js) 付属のサンプルの１つは Akashic Engine への組み込み例となっています。これを参考にします。１つ１つみていきましょう。

### 紙吹雪の生成

[main.ts#L12-L45](https://github.com/blackspotbear/kamihubuki-js/blob/09cc02aada94acae7927d070715ad44cf982b224/sample.ae/src/main.ts#L12-L45)

```typescript:main.ts#L12-L45
function createConfetti(x: number, y: number): kh.Confetti {
	const vx = 0;
	const vy = 200;
	const angle = g.game.random.get(0, 100) / 100 * Math.PI;

	const fins: kh.Fin[] = [
		{
			angle: g.game.random.get(-100, 100) / 100 * (Math.PI / 2),
			size: 30,
			armAngle: 0,
			armLength: 1
		},
		{
			angle: g.game.random.get(-100, 100) / 100 * (Math.PI / 2),
			size: 30,
			armAngle: g.game.random.get(50, 100) / 100 * Math.PI,
			armLength: 1
		}
	];

	const co = new kh.Confetti({
		x: x, y: y,
		vx: vx, vy: vy,
		angle: angle,
		fins: fins,
		av: 0,
		M: 0.5,
		K: 0.02,
		I: 3,
		RD: 0.99
	});

	return co;
}
```

`createConfetti()` で kamihubuki-js の `Confetti` インスタンスを生成しています。 kamihubuki-js では１つの紙吹雪につき１つの `Confetti` インスタンスを用意します。

コンストラクタには初期位置(x、y)、初速度(vx, vy)、空気抵抗(K)などの値が渡されています。 しかしこれだけでは、紙吹雪が回転する様子を扱うことができません。 そこで kamihubuki-js では、質点にフィンを取り付けます。フィンが受ける風の力で質点が回転します。 `fins` がフィンのパラメータです。

`fins` パラメータによって、質点から任意の数の腕を伸ばし、その先にフィンを取り付けることができます。

<img width="499" alt="physics-model.ja.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/fb8f0992-efd4-f231-b7b7-13b1974aa19b.png">

_図は[kamihubuki-js](https://github.com/blackspotbear/kamihubuki-js/blob/master/how-it-works.ja.md)から転載。_

フィンの取り付け方によって、紙吹雪はさまざまな運動をするようになります。

### 紙吹雪の描画

[main.ts#L47-L97](https://github.com/blackspotbear/kamihubuki-js/blob/09cc02aada94acae7927d070715ad44cf982b224/sample.ae/src/main.ts#L47-L97)

```typescript:main.ts#L47-L97
function createConfettiEntity(scene: g.Scene, co: kh.Confetti, noCollision: boolean): g.E {
	const w = 20;
	const h = 10;
	const w2 = w / 2;
	const h2 = h / 2;

	const rect = new g.FilledRect({
		scene: scene,
		width: w,
		height: h,
		x: co.x - w2,
		y: co.y - h2,
		cssColor: colors[colorIndex]
	});

	colorIndex = (colorIndex + 1) % colors.length;

	rect.update.add(() => {
		co.update(1 / g.game.fps, windX, windY, 0, 200);


		// collision detection and resolution.
		if (noCollision) {
			if (co.x < 0 || co.x > g.game.width || co.y > g.game.height) {
				rect.destroy();
			}
		} else {
			const k = 0.5;
			if (co.x < 0) {
				co.x = 0;
				co.vx = Math.abs(co.vx) * k;
			} else if (co.x > g.game.width) {
				co.x = g.game.width;
				co.vx = -Math.abs(co.vx) * k;
			}
			if (co.y < -g.game.height) {
				co.y = -g.game.height;
				co.vy = 0;
			} else if (co.y > g.game.height) {
				co.y = g.game.height;
				co.vy = -Math.abs(co.vy) * k;
			}
		}

		rect.x = co.x - w2;
		rect.y = co.y - h2;
		rect.angle = -co.angle / Math.PI * 180;
		rect.modified();
	});

	return rect;
}
```

`createConfettiEntity()` は紙吹雪を描画するインスタンスを生成する関数です。サンプルでは、紙吹雪の描画に `g.FilledRect` を利用しています。色の変更や画面端との衝突処理を省いて、大切なところだけ抜き出します。

```typescript
	rect.update.add(() => {
		co.update(1 / g.game.fps, windX, windY, 0, 200);

		// 省略

		rect.x = co.x - w2;
		rect.y = co.y - h2;
		rect.angle = -co.angle / Math.PI * 180;
		rect.modified();
	});
```

`rect` の `update` イベントで `co.update()` を実行するようにしています。これによって、毎フレーム紙吹雪の状態を更新しています。次に紙吹雪の位置と角度を `rect` に反映しています。 kamihubuki-js は角度をラジアンで扱っていますので、Akashicの角度の単位である度に変換していることに注意していください。

Akashic Engine 製コンテンツに紙吹雪を導入する手順は以上となります。

## 応用: 光沢のある紙吹雪

サンプルに少し手を加えて、光沢のある紙吹雪にしてみましょう。紙吹雪がある角度では白く光るようにします。

白く光る処理は、ここでは簡単に `rect` にもう１つ光沢のための `g.FilledRect` インスタンスを重ねることします。

```typescript
	// const rect = new g.FilledRect... の後ろに以下を追加する。
	// 光沢を表現する g.FilledRect インスタンス。
	const specular = new g.FilledRect({
		scene: scene,
		// rect と同じサイズだとわずかに rect の輪郭が滲み出るので、少し大きくする。
		width: rect.width + 2,
		height: rect.height + 2,
		x: -1,
		y: -1,
		opacity: 0,
		cssColor: "white",
		// 加算合成を指定することで、光っている感じを出す。
		compositeOperation: g.CompositeOperation.Lighter
	});
	rect.append(specular);
```

次に `specular` の透明度を変更する処理を追加します。

```typescript
	// co.update( ... ) の後ろに以下を追加する。
	let t = Math.abs((co.angle / Math.PI) % 2);
	if (t > 1) t = 2 - t;
	specular.opacity = Math.pow(t, 5);
	specular.modified();
```

`co.angle` から、角度に比例して 0 ~ 1 の間で変化する値 `t` を求めています。これを `specular.opacity` に与えると、角度に応じて `specular` が姿を現して光沢となります。 `Math.pow(t, 5)` としているのは `t` の変化に緩急をつけるためです。

次のグラフの黒い線、青い線、赤い線は順に `Math.pow` の第２引数が 1, 2, 5 の時の `specular.opacity` の変化を表しています。第２引数が大きな値であるほど、鋭く光沢が現れることがわかります。

![pow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/d232bc0b-6e95-56bc-4af2-662f2af9d3ed.png)

角度に応じて白く光るようになりました（わかりやすくするため、背景色を黒にしています）。

![confetti-specular.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540569/b0e70678-ed33-4c15-c5c3-11a17e82fde5.gif)

## 最後に

kamihubuki-js 付属のサンプルでは `g.FilledRect` で紙吹雪を描画していますが、桜の花びらや紅葉、雪のような画像を用意して物理量を調整すれば、四季の表現にも使えそうです。

このような演出には [akashic-animation](https://github.com/akashic-games/akashic-animation) を用いることも１つの方法です。 akashic-animation は [SpriteStudio](https://www.webtech.co.jp/spritestudio/) で作成したアニメーションの再生を行うことことができます。また、ただアニメーションを再生するだけでなく、当たり判定やプログラムからアニメーションを上書きするといった機能もあります。