【GameMaker:Studio2】残像エフェクトの作り方 (GML)

# はじめに
今回はゲームの見た目を良くする「残像エフェクト」の実装方法を紹介します。
<img width="423" alt="001.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/cb0539cb-e4e9-b2c7-b22d-9f8918bf0b77.png">

残像エフェクトとは、この画像のように過去の座標値に連続して描画することで、軌跡を描くものとなります。
一般的な残像の意味は、「非常に素早い動きを視覚的に表現するもの」ですが、今回は軌跡を含む広い意味を残像と定義します。

# 使用する機能
「配列」という機能を使います。
これは関連性のあるデータを一つにまとめて使うのに便利な機能です。

# プロジェクトの新規作成
プロジェクトの新規作成をします。
GMLを使用するので、「GameMaker Language」 で作成します。
<img width="384" alt="002.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/f8de0dcb-aa5d-10df-c805-fc17f0eb76e7.png">

# プロジェクトの設定
歯車アイコンをクリックして、 `Game Options`  を表示し、 `Game frames per second` を 60に変更します。
<img width="499" alt="003.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/014a228f-4f2e-83ab-f980-f6f4738c7e44.png">

# プレイヤーオブジェクトの作成
オブジェクトを作成し、名前を `objPlayer` とします。
<img width="199" alt="004.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/c7df6da2-caf4-e1bf-3936-3a12590cff01.png">

スプライトを作成して、名前を `sprPlayer` とし、以下の画像をインポートします。
![player_strip2.png](https://qiita-image-store.s3.amazonaws.com/0/43772/4e6399eb-82dc-90e9-c6ea-f0bb6c8be713.png)

ファイル名は `player_strip2.png` として保存します。
インポートしたら、`Frames per second` を 3 に変更します。
あとスプライトの原点を中央にしておきたいので、 `Middle Centre` にします。
<img width="447" alt="005.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/a519e1e3-4564-afe5-e8da-a7576ff5981f.png">

# プレイヤーの移動処理の作成
`objPlayer` を開いて、 `Add Event > Step > Step` を選び、Stepイベントを作成します。
<img width="355" alt="006.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/bab75b9b-b3d9-0857-6413-b713f806053f.png">

Stepイベントのコードは以下のものを入力します。

```js:objPlayer>Stepイベント
// 移動処理
var dx = mouse_x - x;
var dy = mouse_y - y;
x += dx * 0.1;
y += dy * 0.1;
```

マウスの位置と現在値との差を取り、10分の1をどんどん足し込んで、じわじわ動かす移動処理となります。

# プレイヤーをルームに配置する
room0 を開いて、 `objPlayer` を配置します。
<img width="635" alt="007.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/bf28d32e-4fb2-3096-bafb-2da75e9fd6bb.png">

ルーム内に配置されていればどこに置いても問題ありません。
では、実行してプレイヤーがマウスの位置に付いてくることを確認します。

# 残像の実装
準備ができたので、ここから残像を実装します。
`objPlayer` を開いて、 `Add Event > Create` でCreateイベントを作成します。
<img width="243" alt="008.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/99fa367e-1f67-37ba-76cb-b88278001dce.png">

```js:objPlayer>Createイベント
// 残像の数
trail_size = 10;

// 残像の位置を初期設定する (配列作成)
for(var i = 0; i < trail_size; i++) {
	xprev_array[i] = x;
	yprev_array[i] = y;
}
```

残像の位置を保持するため、10個の座標を保持するようにします。
`xprev_array` が 残像のX座標の配列、 `yprev_array` が残像のY座標の配列となります。
<img width="182" alt="009.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/edfa04f9-5b79-c32f-c4ea-c2f529b2195d.png">

イメージとしては、このような座標のハコが作られたわけです。
ここに残像の座標を入れていきます。

続けて、Stepイベントを修正します。

```js:objPlayer>Stepイベント
// 移動処理
var dx = mouse_x - x;
var dy = mouse_y - y;
x += dx * 0.1;
y += dy * 0.1;

// ※ここから追加
// 残像の更新
// 配列の先頭に前回フレームの座標を代入
xprev_array[0] = xprevious;;
yprev_array[0] = yprevious;

// 配列を後ろから更新する
for(var i = trail_size-1; i > 0; i--) {
	xprev_array[i] = xprev_array[i-1];
	yprev_array[i] = yprev_array[i-1];
}
```

残像の更新処理を追加しました。
コードだけではわかりにくいので図で説明をします。
<img width="751" alt="010.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/2365c973-710a-d6ea-e4de-e08d68aa1e4e.png">

配列の最後尾から1つ前のものを移動させるという、バケツリレーのようなことをして、
本来のプレイヤーの座標よりも少し遅れて追いかける動きとなります。

配列の先頭に設定した1フレーム前の座標が、配列の末尾に移動するには9フレーム必要となります。
これにより、配列の値は座標が1フレームずつズレたものになります。
<img width="296" alt="011.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/241cc181-5808-ebc7-f9a3-865ce9a58b73.png">

ではこれを使って残像の描画処理を追加します。
`Add Event > Draw > Draw` で描画イベントを追加します。
<img width="310" alt="012.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/d6e09c87-f6ef-efb0-b49c-6a6b25dac8f6.png">

```js:objPlayer>Drawイベント
// 残像の描画
// 末尾から描画する
for(var i = trail_size-1; i >= 0; i--) {
	var px = xprev_array[i];
	var py = yprev_array[i];
	
	// 末尾にあるほど透過値を小さくする
	var alpha = 1.0 - 1.0 * (i + 1) / (trail_size + 1);
	draw_sprite_ext(sprPlayer, 0, px, py, 1, 1, 0, c_white, alpha);
}

// 自身を最後に描画する
draw_self();
```

Stepイベントと同様、配列の末尾から操作を行っています。これは末尾にある残像ほど背面に描画したいので、先に描画をしているわけです。
あと、透過値の計算が少しややこしいですが、考え方は以下の通りです。
元となる式は以下のものです。

```
1.0 * i / trail_size
```

`i` の増加率に合わせて透過値を減少する、という式です。
ただこの場合、i = 0 の時に透過値が 0.0 になって完全に透明になってしまうので、 `(i + 1)` としました。
`(i + 1)` とすると、分子の最大値が分母の値と同じ(=1.0 となり不透過)になるので、 `(trail + 1)` としました。
そして、透過値を `i` の増加率とは反比例にしたいので、1.0 から引いています。
これにより、 `i` が増加するに従って、透過値が小さくなります。

実行して、プレイヤーの残像が表示されるのを確認します。
<img width="560" alt="013.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/075f84eb-6869-85f3-d444-06458b34fd4a.png">

# 移動速度が遅くても残像がわかりやすく見えるようにする
この実装の問題点は、移動速度が遅い場合に残像が見た目であまりわからないことです。
また10回描画しているため、残像が目立ちすぎているという欠点もありますし、描画の処理負荷も高くなります。

そこで、回数を減らしても残像がよく見えるようにします。

まずは、`objPlayer > Createイベント` を開いて、以下のように修正します。

```js:objPlayer>Createイベント
// 残像の数
trail_size = 5; // ※残像の数を減らす

// 残像の位置を初期設定する (配列作成)
for(var i = 0; i < trail_size; i++) {
	xprev_array[i] = x;
	yprev_array[i] = y;
}
```

残像の数を5個に減らしました。
次に、`objPlayer > Stepイベント` を開いて、以下のように修正します。

```js:objPlayer>Stepイベント
// 移動処理
var dx = mouse_x - x;
var dy = mouse_y - y;
x += dx * 0.1;
y += dy * 0.1;

// 残像の更新
var decay = 0.5; // 追いかける速度

xprev_array[0] += (x - xprev_array[0]) * decay;
yprev_array[0] += (y - yprev_array[0]) * decay;

for(var i = trail_size-1; i > 0; i--) {
	var xprev = xprev_array[i];
	var yprev = yprev_array[i];
	var xnext = xprev_array[i-1];
	var ynext = yprev_array[i-1];
	xprev_array[i] += (xnext - xprev) * decay;
	yprev_array[i] += (ynext - yprev) * decay;
}
```

以前の記述は、1つ前の値をそのまま代入するだけでしたが、この修正では、1つ前の座標との差の2分の1を足しこむようにしています。 これにより、じわじわ追いかけるような動きとなります。

では、実行して、移動速度が遅くても残像が視認しやすくなっていることを確認します。

# 最後に
アクションゲームなどで、プレイヤーキャラクターに常に残像をつける場合は、3〜5個あたりに抑えると、プレイヤーの操作の邪魔にならず、いい感じの見た目になります。

ただ、移動速度によって追いかける速度 `decay` の値を小さくしたり大きくしたりして良い感じになるよう、調整が必要となると思います。

# おまけ
残像とは少し異なりますが、一定のフレーム数の間隔でエフェクトを連続して発生させると、同じような軌跡っぽいエフェクトになります。

例えば、`objPlayer > Stepイベント` を開いて、以下のコードに修正します。

```js:objPlayer>Stepイベント
// 移動処理
var dx = mouse_x - x;
var dy = mouse_y - y;
x += dx * 0.1;
y += dy * 0.1;

// 残像の更新
var decay = 0.5; // 追いかける速度
for(var i = trail_size-1; i > 0; i--) {
	var xprev = xprev_array[i];
	var yprev = yprev_array[i];
	var xnext = xprev_array[i-1];
	var ynext = yprev_array[i-1];
	xprev_array[i] += (xnext - xprev) * decay;
	yprev_array[i] += (ynext - yprev) * decay;
}

xprev_array[0] += (x - xprev_array[0]) * decay;
yprev_array[0] += (y - yprev_array[0]) * decay;

// ※これを追加
// エフェクトを毎フレーム生成する
effect_create_below(ef_explosion, x, y, 0, c_white);
```

最後の部分が追加したコードとなります。
実行すると、以下のような見た目となり、派手な感じになりました。
<img width="460" alt="014.png" src="https://qiita-image-store.s3.amazonaws.com/0/43772/ca20688b-34c0-2964-2cdf-fb7ccc15c058.png">

毎フレーム出力すると描画負荷も大きくなってしまうので、数フレームごとに生成する、という修正をしてみるのも良いです。

以上、残像エフェクトの作り方でした。