Unityで2Dシューティングゲームを作るときに考えたこと

大学が一週間休みになったのでその期間で2DSTGを作りました。その時、詰まったところや便利だったものをまとめます。

自機移動

チュートリアルで学んだ通りにVectorによって力を与えて動かしました。…が、動かしてみると、違和感。入力が少し遅れて反映されているように感じます。数分の間、違和感と格闘して気が付きました。Vectorによる加速は物理演算を用いているため、慣性がかかっていることが原因でした。つまり、初速が遅く、徐々に加速していくような挙動を取るのです。3Dゲームを作るのが得意なツールであるUnityでは簡単に物理演算が実装できることが大きな強みですが、市販の"2D"ゲームにおいて、移動に慣性の概念を持つゲームが少なく、それが違和感を生んでいました。

f:id:NajaNaja:20201211203404g:plain
Vector加算の移動とposition加算の移動

Unityにおける移動の実装をいくつか調べましたが、最終的にUnityで位置情報を扱うtransformコンポーネントのpositionの値を直接書き換えることにしました。Unity公式では「非現実的な挙動になる」ために推奨されていませんが…しかし時として非現実的な挙動の方が気持ちいいのがゲームなのでこれでいいんじゃないでしょうか。もっといい方法あれば教えてください。

また、この処理だと、フレーム毎に移動距離が決まっているので実行環境によって移動速度に差ができてしまいます。この問題を解決するにはFixedUpdate関数内に処理を書くか、Time.deltaTimeと定数の積を移動距離とする必要があります。ただし、FixedUpdate関数は液晶のリフレッシュレートによっては誤差が発生し、時折コマ落ちが発生することがあるようです。これは処理の工夫やUnityの物理演算速度をフレームレートよりも高くすることで対処できます。

今回制作するゲームにおいて厳密な物理演算処理は必要ないと考えているのでTime.deltaTimeを使う方法で実装しました。

コルーチン

弾の発射周期をUpdateで管理すると環境によってFPSの違いがあるため弾の発射周期も環境に依存してしまいます。

ここでもTime.deltaTimeを用いて実装する方法を考えていましたが、毎フレーム処理をしなければならない移動と違い、弾の発射は0.2秒に一度などの(フレームレートに比べて)低頻度でしか行いません。60FPSの動作環境だとUpdate関数は約0.017秒毎に呼ばれるため、Update関数に処理を記述すると大部分の処理は時間経過をカウントするだけの処理になってしまいます。そもそもUpdateは重い関数なので必要がないなら呼ばない方がいい関数です。

そこで指定秒数毎に処理を実行できるコルーチンを用いた非同期処理で記述しました。

using System.Collections;
using UnityEngine;

public class weapon1 : MonoBehaviour
{
    [SerializeField] GameObject bullet;
    public bool fire = false;
    void Start()
    {
        StartCoroutine("shot");
    }
    IEnumerator shot()
    {
        //無限ループで入力待ち
        while (true)
        {
            while (fire == true)
            {
                //弾のインスタンスを生成
                Instantiate(bullet, this.gameObject.transform.position, transform.localRotation);
                //発射間隔の時間だけ待機する
                yield return new WaitForSeconds(0.5f);
            }
            yield return null;
        }
    }
}

 

dotween

 友人がすごく便利だといっていたので前々から興味があったdotweenというアセットを導入。下記のような短いコードで敵機のループ移動が書けます。

using UnityEngine;
using DG.Tweening;

public class enemymove : MonoBehaviour
{
    Vector2 move;
    void Start()
    {
        move = new Vector2(0, -5);
        transform.DOLocalMove(move, 4f).SetRelative().SetEase(Ease.InOutFlash, 2).SetLoops(-1).SetLink(gameObject);
    }
}

 
メインはtransform.DOLocalMove(move, 4f)の部分でtransformを4秒かけてmoveのように動かす命令になっています。そこにいろいろなオプションを付けることで理想の動きを実現する感じです。

今回の場合は「相対座標で(SetRelative())」、「移動はinとoutを滑らかに行って戻っての二回行う(SetEase(Ease.InOutFlash,2))」、「無限回行う(SetLoops(-1))」、「ゲームオブジェクトを削除したときは自動でtweenを破棄する(SetLink(gameObject))」といった感じです。

これはtransformによる移動だけでなく、colorやscaleの変更にも使えるため、UIに使うと簡単に見栄えが良くなっていいと思います。処理も軽いらしいのでUnityで動きのあるものを作るときは使い得かもしれません。

感想

チュートリアルや小さい機能を作っているだけでは気にならないような問題が多く勉強になりました。

一週間でどこまでいけるんだろう?という興味と勢いだけで突貫で作ったため設計や画像には目も当てられないようなところも多いです。しかし、課題が明らかになると何を勉強すればいいのかわかっていいですね。

今後はUniRxとか触ってみたいかなぁ…とか思っています。