【GameMaker Studio2】Tweenオブジェクトの実装

# 概要
この記事では、Tweenオブジェクトの実装方法について書きます。

## Tweenオブジェクトのメリット
Tweenオブジェクトを使えると何が便利になるのかというと、GMLを数行書くだけでオブジェクトのアニメーションが実装できるようになることです。

例えば、Tweenを使った以下のコードは、X座標の100から800に向けて減速しながら移動するアニメーションです。

```js
// Tweenオブジェクトを生成.
var tween = instance_create_depth(0, 0, 0, obj_Tween);

// 自身をTweenにアタッチしてTweenアニメーションを実行する
tween.tween(
  self       // 自身のインスタンス
  ,["x:100"] // 開始X座標
  ,[800]     // 終端X座標
  ,0         // 開始遅延フレーム
  ,60        // 移動フレーム数
  ,expoOut   // イージング関数の種類
  ,noone     // 移動完了後に実行されるコールバック関数
);
```

これを実行すると、以下のような動きとなります。
![tween.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/80e7d9ce-51c2-5aac-3605-ce999f5cbcbb.gif)

v2.3 で実装された Sequence / Animation Curve を使うことでだいたい同じようなことはできますが、GMLで手早く実装したいときに便利なのではないかと思います。あと GML で書くと細かい制御や調整もやりやすいです。（プログラマー的には）

# Tweenオブジェクトの実装
ということで、Tweenオブジェクトの実装です。
オブジェクト名は "obj_Tween" として作成し、Createイベントに以下のコードを記述します。

```js:Createイベント
target       = noone; // アニメーションを行う対象
start_vals   = [];    // 開始パラメータ
end_vals     = [];    // 終端のパラメータ
delay        = 0;     // 開始遅延フレーム数
max_frame    = 0;     // アニメーションフレーム数
easing       = noone; // イージング関数
cb_completed = noone; // アニメ終了時に実行されるコールバック関数

// Tween開始関数を定義.
tween = function(_target, _start, _end, _delay, _frame, _ease, _cb) {
  target       = _target;
  start_vals   = _start;
  end_vals     = _end;
  delay        = _delay;
  max_frame    = _frame;
  alarm[0]     = _frame + delay; // 遅延分を含める
  easing       = _ease;
  cb_completed = _cb;
};
```

次に Stepイベントを作成します。

```js:Stepイベント
if(instance_exists(target) == false) {
  // 対象が消滅していたら即座に終了する
  instance_destroy();
  return;
}


// 経過時間を求める (Alarmは最大値から減少するので減算で逆にする)
var past = max_frame - alarm[0];
// 経過時間の割合を求める (rateは 0.0〜1.0 の範囲)
var rate = floor(past) / max_frame;
if(alarm[0] > max_frame) {
  rate = 0; // 開始遅延中.
}

for(var i = 0; i < array_length(start_vals); i++) {
  // 開始パラメータは ":" 区切りで、"変数名:開始値" となる
  // "string_split" で文字列を分割する
  var tmp = string_split(start_vals[i], ":");
  // 変数名を格納
  var name = tmp[0];
  // 開始パラメータ
  var a    = real(tmp[1]);
  // 終端パラメータ
  var b    = real(end_vals[i]);

  // イージング関数で補完する	
  var v = a + (b - a) * easing(rate);
  // 対象オブジェクトの変数に値を代入する
  variable_instance_set(target, name, v);
}
```

ここで標準のAPIにないものを使用しているので、2つスクリプトを追加します。
スクリプトを作成し、スクリプト名を "string_split" として、以下のように記述します。

```js:string_split
/// @description 指定の文字で文字列を分割する
/// @param str. 元の文字列
/// @param sep. 分割文字.
/// @return 分割後の文字列 (array 1d).
function string_split(str, sep) {
  var sep_len = string_length(sep);
  var ret = [];
  var idx = 0;
  while(true) {
    var pos = string_pos(sep, str);
    if(pos == 0) {
      // 見つからなかったので最後の文字を入れて終了.
      ret[idx] = str;
      break;
    }
	
    // 区切り文字の手前まで切り出す.
    ret[idx] = string_copy(str, 1, pos-1);
    idx++;

    // 区切り文字より後ろの文字を切り出す.	
    str = string_copy(str, pos+sep_len, 65535);
  }

  return ret;
}
```

この関数(スクリプト)は渡された文字列を特定の文字で分割するものです。
例えば、"a,b,c" という文字が渡されて、区切り文字を "," とすると、`["a", "b", "c"]` というように配列に分割して返します。

あと、イージング関数を定義するスクリプトを追加します。スクリプト名は "scr_Ease" にして、以下のコードを記述します。

```js:scr_Ease
/// @description back in.
/// @param t (0〜1)
/// @return back in value.
function backIn(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return t * t * (2.70158 * t - 1.70158);
}

/// @description back out.
/// @param t (0〜1)
/// @return back out value.
function backOut(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return 1 - (t - 1) * (t-1) * (-2.70158 * (t-1) - 1.70158);
}

/// @description bounce in.
/// @param t (0〜1)
/// @return bounce in value.
function bounceIn(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	var B1 = 1 / 2.75;
	var B2 = 2 / 2.75;
	var B3 = 1.5 / 2.75;
	var B4 = 2.5 / 2.75;
	var B5 = 2.25 / 2.75;
	var B6 = 2.625 / 2.75;

	t = 1 - t;
	if (t < B1) return 1 - 7.5625 * t * t;
	if (t < B2) return 1 - (7.5625 * (t - B3) * (t - B3) + .75);
	if (t < B4) return 1 - (7.5625 * (t - B5) * (t - B5) + .9375);
    
	return 1 - (7.5625 * (t - B6) * (t - B6) + .984375);
}

/// @description bounce out.
/// @param t (0〜1)
/// @return bounce out value.
function bounceOut(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	var B1 = 1 / 2.75;
	var B2 = 2 / 2.75;
	var B3 = 1.5 / 2.75;
	var B4 = 2.5 / 2.75;
	var B5 = 2.25 / 2.75;
	var B6 = 2.625 / 2.75;
	if (t < B1) return 7.5625 * t * t;
	if (t < B2) return 7.5625 * (t - B3) * (t - B3) + .75;
	if (t < B4) return 7.5625 * (t - B5) * (t - B5) + .9375;
    
	return 7.5625 * (t - B6) * (t - B6) + .984375;
}

/// @description cube in.
/// @param t (0〜1)
/// @return cube in value.
function cubeIn(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return t * t * t;
}

/// @description cube out.
/// @param t (0〜1)
/// @return cube in value.
function cubeOut(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return 1 + (t - 1) * (t - 1) * (t - 1)
}

/// @description elastic in.
/// @param t (0〜1)
/// @return elastic in value.
function elasticIn(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	var ELASTIC_AMPLITUDE = 1
	var ELASTIC_PERIOD = 0.4
	t -= 1
	return -(ELASTIC_AMPLITUDE * power(2, 10 * t) * sin( (t - (ELASTIC_PERIOD / (2 * pi) * arcsin(1 / ELASTIC_AMPLITUDE))) * (2 * pi) / ELASTIC_PERIOD));
}

/// @description elastic out.
/// @param t (0〜1)
/// @return elatic out value.
function elasticOut(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	var ELASTIC_AMPLITUDE = 1;
	var ELASTIC_PERIOD = 0.4;
	return (ELASTIC_AMPLITUDE * power(2, -10 * t) * sin((t - (ELASTIC_PERIOD / (2 * pi) * arcsin(1 / ELASTIC_AMPLITUDE))) * (2 * pi) / ELASTIC_PERIOD) + 1);
}

/// @description expo in.
/// @param t (0〜1)
/// @return expo in value.
function expoIn(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return power(2, 10 * (t - 1));
}

/// @description expo out.
/// @param t (0〜1)
/// @return expo out value.
function expoOut(t) {
	if(t < 0) return 0;
	if(t > 1) return 1;

	return -power(2, -10*t) + 1;
}
```

ここに定義した関数はイージングの計算を行うものです。イージング関数は他にも用意されていますが、個人的によく使うものだけをまとめました。

* cube: 3次関数
* back: 開始または終端を少し通り過ぎる
* bounce: 開始または終端でバウンドする
* elastic: バネのような激しい振動をする
* expo: 指数関数

なお、イージング関数については以下のページにサンプルがあります。

* [イージング関数チートシート](https://easings.net/ja)

イージング関数の仕組みはシンプルで、渡された一次変数 0.0〜1.0 に対して曲線となる値を返すものです。

イージング関数でUIの動きを作るときのコツは note に記事を書きましたので、そのあたりを知りたい人はこちらのページが参考になるかもしれません

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">UIを良い感じに動かす「イージング関数」についての記事をまとめました。Pyxelでの実装例ものせてあります <a href="https://t.co/J1EM7ejldh">https://t.co/J1EM7ejldh</a></p>&mdash; しゅん (@2dgames_jp) <a href="https://twitter.com/2dgames_jp/status/1100218749733359617?ref_src=twsrc%5Etfw">February 26, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

次に、Alarm[0]イベントです。

```js:Alarm[0]
// インスタンスを破棄
instance_destroy();
```

ここでは、自身のインスタンスを破棄しているだけです

最後に、Destroyイベントです。

```js:Destroyイベント
if(instance_exists(target)) {
  // 対象が存在している
  if(cb_completed != noone) {
    // 終了コールバックが指定されていたら実行
    cb_completed(target);
  }
}
```

アニメーション対象のオブジェクトが存在しており、終了コールバックが指定されていれば、その関数を実行して終わります。

----

これでTweenオブジェクトを実装できます。ちょっとしたアニメーションを作るときに GML で制御できるようになります。

最後に、最初のサンプルで使用しなかった、「遅延フレーム」と「終了コールバック」を使ったサンプルです。

```js
for(var i = 0; i < 4; i++) {
  var py = i * sprite_height; // スプライトの高さの間隔で配置
  var delay = i * 8; // それぞれを8F遅延する
  var _inst = instance_create_depth(0, py, 0, obj_Test);
  var tween = instance_create_depth(0, 0, 0, obj_Tween);
  // Tween実行
  tween.tween(_inst, ["x:100"], [800], delay, 60, expoOut, function(target) {
    // 実行完了時にインスタンスを削除する
    instance_destroy(target);
  });
}
```

4つのオブジェクトを生成し、それぞれ "8F" の遅延でアニメーションを再生するサンプルです。

これを実行すると以下のように再生されます。

![delay.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/5431fe29-72e3-60a3-6425-aa144ec15a14.gif)

これは各項目をずらしてアニメーションすることで、柔らかい動きに魅せるテクニックです。

![picture_pc_625a1e4056ddaefedec45bde30805d09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/d782b061-735d-8924-d691-db81446b4580.png)

一緒に入場するアニメーションと比較するとよくわかると思います。

![picture_pc_a9be471f4488564d4982260c00323e23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ade9be1c-5df9-ee47-0c05-950ef10c508e.gif)

なんだか硬い動きに見えます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">UIを良い動きにするには、<br>①直線ではなく曲線を使う<br>②各部品をずらして動かす<br>の２つが基本となりそう。これをやるだけで柔らかく見えて、さわり心地の良いUIになるはず <a href="https://t.co/zafDQkgCVH">pic.twitter.com/zafDQkgCVH</a></p>&mdash; しゅん (@2dgames_jp) <a href="https://twitter.com/2dgames_jp/status/1100157411954839552?ref_src=twsrc%5Etfw">February 25, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ということで、Tweenの実装方法の紹介でした

# プロジェクトファイル

今回作成したプロジェクトファイルは以下からダウンロードできます。

* [TestTween.yyz](http://syun777.sakura.ne.jp/tmp/gms2/TestTween.yyz)


----

# おまけ
以下のようなスクリプトを用意して、Tweenオブジェクトの生成を自動でやってくれるようにしてもいいかもしれません。

```js:tween_start
/// @description Tweenアニメーションを開始する
/// @param target 対象のインスタンス
/// @param start  開始値
/// @param end    終端値
/// @param delay  開始遅延フレーム
/// @param frame  移動フレーム数
/// @param ease   イージング関数の種類
/// @param cb     移動完了後に実行されるコールバック関数
/// @return tween 生成したTweenオブジェクト
function tween_start(_target, _start, _end, _delay, _frame, _ease, _cb) {
	// Tweenオブジェクトを生成.
	var tween = instance_create_depth(0, 0, 0, obj_Tween);
	
	// targetをTweenにアタッチしてTweenアニメーションを実行する
	tween.tween(
	  _target     // 対象のインスタンス
	  ,_start     // 開始値
	  ,_end       // 終端値
	  ,_delay     // 開始遅延フレーム
	  ,_frame     // 移動フレーム数
	  ,_ease      // イージング関数の種類
	  ,_cb        // 移動完了後に実行されるコールバック関数
	);
	
	return tween;
}
```

----
# おまけ２

やや複雑な記述となりますが、Tween完了後、さらにTweenを呼び出すことも可能です。

```js
for(var i = 0; i < 4; i++) {
  var py = i * sprite_height; // スプライトの高さの間隔で配置
  var delay = i * 8; // それぞれを8F遅延する
  var _inst = instance_create_depth(0, py, 0, obj_Test);
  // Tween実行
  tween_start(_inst, ["x:-600", "image_alpha:0"], [580, 1], delay, 60, expoOut, function(target) {
    tween_start(target, ["x:" + string(target.x), "image_alpha:1"], [1000, 0], 0, 60, expoIn, function(target) {
      // 実行完了時にインスタンスを削除する
      instance_destroy(target);
	});
  });
}
```
![tween2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/79843027-9b27-79a0-3b1c-7375a0fff70e.gif)

Outで入場してInで退場すると、入退場をスムーズな動きに見せることができます。

![picture_pc_cbbfb2fd02001b1ad364152db28152df.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/71c43fd6-71d2-8259-447a-b957e8201fbf.png)


## プロジェクトファイル

おまけの内容を含めたプロジェクトファイルは以下からダウンロードできます。

* [TestTween2.yyz](http://syun777.sakura.ne.jp/tmp/gms2/TestTween2.yyz)