結果だけでなく過程も見てください

日々の奮闘を綴る日記です。

Unity Tips

Unityを使う上で自分が使用した手法等をまとめていきます。
適宜追加記事です。



フェードアウト/フェードインの実装

Unityエディタでの操作

画像を使わず、Panelを使ったフェードアウト/フェードインの方法をご紹介します。

f:id:taiyakisun:20211015121716p:plain
①Hierarchyツリーで、右クリック→UI→Panelでパネルを追加してください。
②Panelが画面全体に満たない場合は、Scaleを調整して画面全体を覆うように調整してください
③フェードする色を選びます。ここでは白色としています。
④Panelは普段は非表示にするため、ここのチェックは外します。

スクリプトの作成

続いて、フェード処理を行うためのC#スクリプトを作成して、Panelにスクリプトを追加します
ここではクラス名をFadeControllerとしています。
アルファ値(透明度)は0.0f~1.0fの間で指定するので注意してください。0.0fが完全透明、1.0fが完全不透明です。

public class FadeController : MonoBehaviour
{
    private float _fadeSpeed = 0.02f;       // 透明度が変わるスピードを管理
    private float _r, _g, _b, _a;           // パネルの色、不透明度を管理

    private bool _bFadeOut = false;         // フェードアウト処理の開始、完了を管理するフラグ
    private bool _bFadeIn = false;          // フェードイン処理の開始、完了を管理するフラグ

    Image _fadeImage;                       // 透明度を変更するパネルのイメージ(UnityEditorで設定)


    // Start is called before the first frame update
    void Start()
    {
        _fadeImage = GetComponent<Image>();
        _r = _fadeImage.color.r;
        _g = _fadeImage.color.g;
        _b = _fadeImage.color.b;
        _a = _fadeImage.color.a;
    }

    // Update is called once per frame
    void Update()
    {
        if (_bFadeIn)
        {
            _a -= _fadeSpeed;          // 不透明度を徐々に下げる
            _fadeImage.color = new Color(_r, _g, _b, _a);
            if (_a <= 0.0f)
            {
                // フェードイン完了
                _bFadeIn = false;
                _fadeImage.enabled = false;      // パネルもついでに消しておく
            }
        }

        if (_bFadeOut)
        {
            _a += _fadeSpeed;         // 不透明度を徐々に上げる
            _fadeImage.color = new Color(_r, _g, _b, _a);
            if (_a >= 1.0f)
            {
                // フェードインアウト完了
                _bFadeOut = false;
            }
        }
    }

    public void StartFadeIn(float fAlphaSpeed = 0.02f)
    {
        if (_bFadeIn)
        {
            // すでにフェードイン中なら何もしない
            return;
        }

        _fadeImage.enabled = true;
        _fadeSpeed = fAlphaSpeed;
        _bFadeIn = true;
        _a = 1.0f;
    }

    public void StartFadeOut(float fAlphaSpeed = 0.02f)
    {
        if (_bFadeOut)
        {
            // すでにフェードアウト中なら何もしない
            return;
        }

        _fadeImage.enabled = true;
        _fadeSpeed = fAlphaSpeed;
        _bFadeOut = true;
        _a = 0.0f;
    }
}

フェード処理の呼び出し

例えばゲームシーンのクラスから呼び出したいとします。
まず、_fadeWhiteという変数をpublicで定義し、Unityエディタから↑で作成したPanelを割り当ててください。

次にプレイヤーが死亡したときにPlayerDied()という関数が呼ばれるとします。
PlayerDied()の中でStartCoroutineをして、PlayerDiedRoutine()を呼び、PlayerDiedRoutine()の中でフェード処理を書きます。

public class GameScene : MonoBehaviour
{
    //フェード用Panel(白)(UnityEditorで設定する)
    public GameObject _fadeWhite;

    :

    private void PlayerDied()
    {
        StartCoroutine(PlayerDiedRoutine());
    }

    private IEnumerator PlayerDiedRoutine()
    {
        //ここにプレイヤー死亡時のコードを書く

        //フェード処理開始
        _fadeWhite.GetComponent<FadeController>().StartFadeOut(0.016f);  // 60FPSなので1.0fまで約1秒
        yield return new WaitForSeconds(1.0f);  //フェード完了まで一秒ウェイト

        //その他死亡処理をここに書く
    }
    :
}



当たり判定/衝突判定

全当たり判定を取得する

Collider2D[] objAllBoxCollider = obj.GetComponents<Collider2D>();
foreach (var col in objAllBoxCollider)
{
    col.enabled = false;  // 当たり判定を無効にする
}

四角形のタイルを地面に敷き詰めた場合、左右にプレイヤー移動するとたまにひっかかってしまう

プレイヤーの当たり判定をBoxColliderでisTriggerをつけていない当たり判定にしてしまうと、微妙にめり込んだ際に引っかかってしまう模様。地面との衝突判定はBoxColliderではなく、CircleColliderで行うことで解決できる。

2つのオブジェクトがあるとき、互いに動かし合うような衝突判定はしたくないが、当たった場合はダメージを受けるような処理を作りたい

例をいうと、プレイヤー(RigidbodyとColliderで重力に従う)と、弾(RigidbodyとColliderで重力に従う)があった場合、弾のIsTriggerをONにしてしまうと重力によって地面をすり抜けてしまうが、OFFにするとプレイヤーと弾がお互いに押し合ってしまう。お互いすり抜けさせつつ、当たったらダメージを受けるようにするにはどうしたらよいか?

答えはダメージを受けたらプレイヤーのレイヤーを変更して対応します。

PlayerのレイヤーをPlayer、弾のレイヤーをAmmoとする。
ここで新たにレイヤーPlayerDamagedを作成しておく。
次にEdit→Project Settings→Physics 2DにあるマトリクスでPlayerDamagedとAmmoのチェックを外す。
これによりレイヤーPlayerDamangedとAmmoの衝突判定は行われなくなる。

プレイヤーはダメージを受けたときレイヤーを変更する。

void OnTriggerEnter2D(Collider2D collision)
{
    :
    if(ダメージを受けた)
    {
        this.gameObject.layer = LayerMask.NameToLayer("PlayerDamaged");
    }
}

プレイヤーの無敵時間が終了するとき、レイヤーを元に戻す。

if (_bDamaged)
{
    if (無敵時間が終了した)
    {
        this.gameObject.layer = LayerMask.NameToLayer("Player");
    }
}

これにより、無敵時間は衝突判定が行われなくなる。無敵時間が終わればレイヤーが元に戻るので衝突判定が復活する。



カメラ

スプライトがプレビュー画面には表示されるのに、ゲーム画面には表示されない

スプライトのZ座標がカメラよりも手前に来てしまっている可能性がある。
スプライトのZ座標がカメラの目の前に来ているようにすることで解決できる。



アニメーション

アニメーションクリップの編集ができない

ヒエラルキーでオブジェクトを選択した状態でないと、アニメーションクリップは選択できません。

ステートに遷移はするが、アニメーションの進捗が進まずアニメーションの最初しか再生されない

Any Stageからの遷移の場合の話ですが、遷移の矢印のプロパティの中にSetting→Can Transition To Selfというパラメーターがあり、これをOFFにすることで正常にアニメーションさせることができます。
ONになっていると自分自身に遷移し続けるため最初のアニメーションしか表示されない動きになります。



スプライト(SpriteRenderer)の座標やサイズを取得する方法

var sr = GetComponent<SpriteRenderer>();
Debug.Log("center=" + sr.bounds.center);  // 中心ワールド座標。Pivotに関係なく中心座標が返る。
Debug.Log("min=" + sr.bounds.min);  // 左下ワールド座標。Pivotに関係なく中心座標が返る。
Debug.Log("max=" + sr.bounds.max);  // 右上ワールド座標。Pivotに関係なく中心座標が返る。
Debug.Log("size=" + sr.bounds.size);  // サイズ
Debug.Log("extents=" + sr.bounds.extents);  // 半分のサイズ



Time.timeScale=0を使わずにキャラクターの動きを止める

2Dアクションゲームなどで、一部オブジェクトの動きだけを止めたい場合等。
Time.timeScale=0だと意図しないものまで止まってしまうため、自分はこうしています…。
ただしこのやり方が本当に正しいかどうかは謎です。みんなどうやっているのか知りたいです。

gameSceneManager.IsPauseGameSceneTimer()がポーズ中かどうかを返すメソッドとなります。ここは皆さまご自分のコードに置き換えてください。

public static bool timerPauseProc(ref Vector2 velocityBackup, ref float gravityBackup, GameSceneManager gameSceneManager, Rigidbody2D rbody)
{
    if (gameSceneManager.IsPauseGameSceneTimer())
    {
        if (rbody.velocity != Vector2.zero)
        {
            // タイマーが停止された

            // いまから停止するので速度と重力を記憶しておく
            velocityBackup = rbody.velocity;
            gravityBackup = rbody.gravityScale;

            rbody.velocity = Vector2.zero;
            rbody.gravityScale = 0;       // これもやらないと微妙に動く
        }

        return true;
    }
    else
    {
        if (velocityBackup != Vector2.zero)
        {
            // タイマーが再開された                

            // 速度と重力を再開させる
            rbody.velocity = velocityBackup;
            rbody.gravityScale = gravityBackup;       // 重力の影響を元に戻す

            // バックアップは消す
            velocityBackup = Vector2.zero;
            gravityBackup = 0f;
        }

        return false;
    }
}

上記関数をFixedUpdate()の内部で呼び出します。

Vector2 _velocityBackup = Vector2.zero;     // ポーズ時速度記憶用
float _gravityBackup = 0f;  // ポーズ時重力記憶用

private void FixedUpdate()
{
        Rigidbody2D rbody = GetComponent<Rigidbody2D>();

        // タイマー停止中は速度を停止させる
        if (CommonManager.timerPauseProc(ref _velocityBackup, ref _gravityScaleBackup, _gameSceneManager.GetComponent<GameSceneManager>(), _rbody))
        {
            return;     // ポーズ中なので何もせず帰る
        }

        // TODO:ここにポーズ中でないときのコードを記載する
}



コルーチン

コルーチンの中で独自(自作)したタイマーでウェイトを行う

まず、以下のようにスレッドを占有してウェイトするタイマーがあったとします。
このクラスでは、Time.timeではなく独自のデルタ時間_deltaTimeを使っているものとします。
独自のタイマーをPauseしたときは、この_deltaTimeは常に0が返ってくる仕組みのため、Time.timeScale=0にすることなく、タイマーを一時停止させられるというものです。

class MyTimer
{
 :
    public void Wait(float waitTime)
    {
        float total = 0f;

        while(true)
        {
            total += _deltaTime;    // フレームの差分の分時間を加算する
            if (total >= waitTime) return;   // 待ち時間停止したので処理終了

            Thread.Sleep(16);       // 60FPSで1フレーム分ウェイト
        }
    }
 :
}

続いてCoroutineから呼び出す、上記タイマーでウェイト処理を行うメソッドを作成します。

using System.Threading;
using System.Threading.Tasks;
 :
public void threadProc()
{
    // TODO:ここにInstantiateとか重い処理とかいろいろ描きます。

    // 別スレッドで独自タイマーが完了するまでここで待機する
    Task task = Task.Run(() => { _gameSceneTimer.Wait(0.15f); });
    while (!task.IsCompleted)
    {
        yield return null;    //1フレーム分待機
    }    
}

最後に、上記メソッドをコルーチンで呼び出します。

StartCoroutine(threadProc());

最初自分はCoroutineを使わず、完全な子スレッド内で処理を実施しようと思ったのですが、
別スレッド内ではUnityのInstantiateなどの機能が使えないため、やはりコルーチンを使う必要がありました。
また、コルーチンでWaitForSecondなどTime.timeに依存するタイマーを使いたくなかったという背景もあります。

コルーチンで匿名関数を使う

少しウェイトしたあとで特定の処理を行いたい場合等、わざわざ関数を作るのが面倒なときは匿名関数を使うと便利です。

using System.Collections;

public static class CommonFunc
{
    public static IEnumerator WaitAction(float waitTime, Action action)
    {
        if (waitTime > 0)
        {
            yield return new WaitForSeconds(waitTime);
        }

        action();
    }
}

呼び出すときはこのようにします。

// 3秒後に状態を遷移させる
StartCoroutine(CommonFunc.WaitAction(3.0f, () => { _state = EnemyState.NextState; }));

テキスト(UI Text)

ぼやけの防止する

Fix Blurry UI text? - Unity Answers
ここにまさに答えが書いてある。以下引用。

The best way I've found is to:
1) Increase font size massively (size 150 or something).
2) Set both horizontal and vertical overflow to 'overflow' in the inspector for the text box.
3) Scale the textbox down using the scaler tool.
The text should now be sharper.

Tilemap

Tilemapを使うまでの手順

ヒエラルキーで右クリック→2D Object→Tilemap→RectangularでGrid(親)-Tilemap(子)という2つのオブジェクトが生成される
・メニューバーの[Window]→2D→Tile Paletteを選択してTile Paletteを表示する
・Tile Paletteの左上のプルダウンから、新規のパレットを作成する(Create New Paletteを選択)
 パレット名は任意。GridはRectangleにして作る。Cell SizeはAutomaticにする。
 保存すると作成場所を聞かれるので、自分はAssets/Tilemapフォルダを選択する。(Tilemapフォルダはなければ作成しておく)
・UnityのAssetのイメージから、追加したいタイルの画像を、タイルパレットにドラッグアンドドロップする。保存場所を聞かれるので、自分はAssets/TilePaletteフォルダを選択している。(TilePaletteフォルダがない場合は作成する)
・「Grid」を選択して、Cell Sizeを調整する。デフォルト1になっている。自分は64x64のタイルを作りたいからここを0.64にしている。ただしこれはPixel per Unitが影響すると思うので自分の環境に合わせて調整する。自分の場合はPixel per Unitが100なので、100で1→64なら0.64という具合。
・タイルマップ上部のボタンから筆アイコンを選び、タイルマップからは配置したいタイルを選択する、その状態でSceneビューに左クリックしていくとタイルを配置できる。ちなみにShiftを押しながら左クリックすると削除できる。
・Gridの下のTilemapを選びながら、筆ボタンを選び、Sceneビューに対して配置していく
・Gridの下のTilemapはOrder in Layerを設定する。自分は3くらいにしている。
・衝突判定を追加する。Gridの下のTimemapを選び、Add Component→Tilemap→Tile Map Collider 2Dを選ぶ
・衝突判定のため、必要に応じてGridの下のTilemapにはBlockやGroundレイヤーを設定する。

タイルマップを編集したい場合は画面の中上部にある「Edit」を押す。で、画面上部のボタンから消しゴムを選んでタイルを選択するとそのタイルを削除したりできます。

TilemapがSceneに描画(ブラシ使用)できない

Tilemapでタイルを選択しているのに、Sceneウィンドウではそのタイルの画像が出てこず、左クリックしても無反応。↓が対処方法だった。引用。

I had the same issue, and it was a rather easy fix if nothing above has fixed it for you.

In your palette, you may have selected "Edit" at some point, this will be indicated by an * - after the word "Edit"
just click "Edit" again, press b to select brush tool, select the tile you want to paint and go at it.

Editに更新マーク(*)がついているとき、Editを一度クリックして(*)がなくなった後、タイルを選ぶと、シーンウィンドウに配置できるようになった。はっきりいってわかりづらいよ!!!

入力

InputSystemを使って、ゲーム内でキーのリバインドを実施する

PCでプレイする場合など、ユーザーがどのようなゲームパッドを利用するかわからないため、ゲーム内のコンフィグシーン等でキーのリバインドをできるようにしなければなりません。

自分も理解できていない部分があるので、コードを中心に部分的に簡単な手順だけ・・・。
ここでは上下のキーのリバインド方法をご紹介します。

Action MapとActions

これが正しいやり方なのかサッパリわかっていませんが、まずInput Action1つ作成し、ゲーム全体で使用するAction Mapを1つだけ作ります。これ以外は作業用のActionMap、Empty以外なにも作りません。
WASDで上下左右を分けているのは、WASDを一緒にしてしまうと右を押した後に追加で上を押したときに正しく検知しない問題があったからです。上下と左右でわけることでそれぞれのキーを個別に入手できます(ただしナナメ入力時に大きさを少し減らすなどの処理は自分で行わなければならなくなります)。EmptyはActionsが1つもないAction Mapsでとある作業のときにEmptyに切り替えて、すぐにGameInputに戻すという動作で使用します。
f:id:taiyakisun:20220119004813p:plain

続いて、任意のシーン(どのシーンよりも先に初期化されるシーンが望ましい)に、GameObjectを追加し、InspectorにPlayer Inputと、Input System Managerというスクリプトを追加します。スクリプトは以下に示します。Player Inputに↑で作成したInput Actionを指定し、それぞれの入力を受け付ける関数を指定します。
f:id:taiyakisun:20220119005345p:plain

以下のスクリプトでコールバックされる関数を作成します。
OnMoveVerticalが、上下を押下したときにコールされる関数です。
if文で意味もなく分けちゃってますが、一緒にしてしまってよいです(デバッグのため分けていてそのままにしちゃった)

    Vector2 _verticalVector = Vector2.zero;
    public Vector2 VerticalVector { get { return _verticalVector; } }
    public void OnMoveVertical(InputAction.CallbackContext context)
    {
        if (context.phase == InputActionPhase.Started ||
            context.phase == InputActionPhase.Performed)
        {
            _verticalVector = context.ReadValue<Vector2>();
        }
        else if (context.phase == InputActionPhase.Canceled)
        {
            _verticalVector = context.ReadValue<Vector2>();
        }
    }

まず上下のキーリバインドですが、Vector2で値を受け取る設定にしています。で、どのBinding(末端のUp:D-Pad/Up [GamePad]のような項目)を変更するのか、ここではゲームパッドの項目を取得するため、以下のような関数を作成して、ゲームパッドの設定の場所(インデックス)を取得できるようにします。

int getGamePadCompositeBindingIndex(InputActionReference inputAction, string actionName, string bindingName)
    {
        var tmpBindingSyntax = inputAction.action.ChangeCompositeBinding(actionName); 

        while (tmpBindingSyntax.bindingIndex != -1)
        {
            tmpBindingSyntax = tmpBindingSyntax.NextPartBinding(bindingName);
            if (tmpBindingSyntax.binding.groups.Equals("Gamepad")) break;    // Gamepadの設定が見つかった
        }

        return tmpBindingSyntax.bindingIndex;
    }

上記関数は以下のようにして使います。_arrowAxisVerticalはInspectorから設定されたInputActionReference型の変数です。要は「AxisVertical」アクションの部分になります。

_arrowAxisVertical.Disable();  // キーのリバインドをするときはそのActionを無効化しておく必要がある。

// アクション_arrowAxisVerticalの下の"WS"の下の"Up"から、"GamePad"に該当するインデックスを取得する
int nBindingIndex = getGamePadCompositeBindingIndex(_arrowAxisVertical, "WS", "Up");
if (nBindingIndex == -1) return;    // GamePadの設定が見つからなかったので、ここで打ち切り(バグもしくはGamePadが1つもない?)

続いて、キーリバインドを開始するためのオブジェクトを作成します。
_playerInputはGameObjectにAddComponentした、PlayerInputオブジェクトです。

  // キーリバインド中の誤操作を防ぐため、一時的に何もしないAction Mapに切り替えておく
  _playerInput.SwitchCurrentActionMap("Empty");

   // キーリバインドのための操作オブジェクト(こいつにStart()するとキーバインドが開始される)
    InputActionRebindingExtensions.RebindingOperation _rebindingOperation;

 _rebindingOperation = _arrowAxisVerticalAction.action.PerformInteractiveRebinding()
                                                    .WithTargetBinding(nBindingIndex)
                                                    .WithControlsExcluding("Mouse")     // マウスを対象外
                                                    .WithControlsExcluding("Keyboard")  // キーボードを対象外
                                                    .WithBindingGroup("GamePad")        // ゲームパッドを対象とする
                                                    .WithCancelingThrough("<Keyboard>/escape")  // キャンセルはescape
                                                    .OnMatchWaitForAnother(0.1f)        // ゲームパッドでのキー入力を受け付けるまでのディレイ時間 
                                                    .OnComplete(operation => RebindComplete())      // 完了関数を登録
                                                    .Start();

いろいろと関数を重ねてコールしていますが、それぞれキーリバインドの動作を指定するようなものです。完了したらRebindComplete()という関数をコールするようにします。Start()が呼ばれるとキーリバインドのモードになり、ゲームパッドの任意のキーを押すことでキーリバインドが完了し、関数RebindComplete()がコールバックされます。

RebindComplete()は主に後始末を行います。

public void RebindComplete()
{
    _arrowAxisVerticalAction.action.Enable();
    _rebindingOperation.Dispose();

    // ボタンの誤作動を防ぐために切り替えていたアクションマップを戻す
    _playerInput.SwitchCurrentActionMap("GameInput");
}

これで、例えばアーケードのジョイスティックなどに上のキーを割り当てたいときは、上記Start()を呼んだあと、ジョイスティックの上を入力することにより、以降はジョイスティックの上を押すとUpのキーを押したことになります。Downの処理は省略していますが、やり方はUpと同じです。"Up"の部分を"Down"に変えるだけで動作するでしょう。

スクリプトで値を設定しているはずなのに、なぜかnullが設定されてしまう場合

ほとんどありえないケースとは思いますが、同じScriptを2つ以上設定してしまっている場合に発生します。
2つ以上設定されている場合、どちらかのスクリプトにしか値が設定されないように見えます。

検索ワード

日本語で検索しても情報が出てこないことが多いので英語で検索する際の表現をまとめる。
経験上英語で検索する方が、答えにたどり着くのが圧倒的に早い。

表現 表現を使う場面 英語
ひっかかる BoxColliderでひっかかって動けないときなどに使う snag/get caught
動けない ↑のひっかかると似ているが動けなくなるときに使う get stuck
ちゃんと動かない おおざっぱに、なんか思った通りに処理されない…というときに使う does not work
ぼやける・ぼける スプライトやテキストがぼやける場合に使う Blurry
延々と・連続して 例外が出続ける場合など continuously
区別する・見分ける 複数の当たり判定のうちどれに当たったのかを区別する等 Distinguish
重なる 当たり判定(Collider)が重なっているか等 overlap
プライバシーポリシー お問い合わせ