【Unity】インスペクターから関数を実行する

忙しいので一口メモ📝

ContextMenu属性を使用すると
インスペクター上から任意のタイミングで関数が実行が可能。
docs.unity3d.com

ソースコード

/// <summary>
/// ContextMenuサンプル
/// </summary>
[ContextMenu("FunctionExecution")]
private void ExampleFunction()
{
     Debug.Log("ContextMenu!");
}

使い方

インスペクターから対象のコンポーネント上で右クリックを押すと
第一引数で渡した文字列のメニュー項目が増えている。

クリックで実行可能。

参考サイト

Unity - Scripting API: ContextMenu

【C#】recordのススメ

ダメージ計算があるゲームなどにおいて、
パラメータの値のみを格納しているデータクラス
使用したい場面があったりします。

public class Parameter
{
    public string name { get; init; }
    public int hp { get; init; }
    public int attack { get; init; }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="name">名前</param>
    /// <param name="hp">体力</param>
    /// <param name="attack">攻撃力</param>
    public Parameter(string name, int hp, int attack)
    {
        this.name = name;
        this.hp = hp;
        this.attack = attack;
    }

    /// <summary>
    /// 比較
    /// </summary>
    public override bool Equals(object other)
    {
        if (other == null || this.GetType() != other.GetType())
        {
            return false;
        }

        Parameter otherPara = (Parameter)other;
        return this.name == otherPara.name &&
               this.hp == otherPara.hp &&
               this.attack == otherPara.attack;
    }
}

こんな感じの単純にプロパティのみを持ったクラスを使いたい場合は、
recordという選択肢があります。 learn.microsoft.com

下準備

Unityでrecordを使用するにあたり、Initアクセサがあると便利です。
Scriptを新規作成し、以下のソースコードを記述します。

using System.ComponentModel;

namespace System.Runtime.CompilerServices
{
    [EditorBrowsable(EditorBrowsableState.Never)]
    internal class IsExternalInit { }
}


stackoverflow.com

使い方

冒頭で記述したclassを、recordを使用して最小で記述するとこうなります。

public record Parameter(string name, int hp, int attack);

なんと1行です。
record名の横に記述した引数名がそのままgetプロパティとなります。
record名の横に引数を記述し、{}の中に関数を書くことも可能です。

ただ、一行で書くのであればローカル変数として使うか、
タプル型を使用した方が良いでしょう。

丁寧に書くとこんな感じです。
個人的にはこっちの方がスキ。

public record Parameter
{
    public string name { get; init; }
    public int hp { get; init; }
    public int attack { get; init; }

    public ParameterRecord(string name, int hp, int attack)
    {
        this.name = name;
        this.hp = hp;
        this.attack = attack;
    }
}

Equal(比較演算)

class動作の違いはEqual(比較演算)です。
冒頭で上げたプロパティを持つ、classrecordを作成します。
プロパティの内容はどちらも同じです。

それぞれ2つずつインスタンスを作成します。
プロパティの内容は同じにし、Equalで調べるとどうなるでしょうか?

using UnityEngine;

/// <summary> 例 </summary>
public class Example : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 定数
    //-----------------------------------------------------------------------

    // 固定値
    private const string Name = "Human";
    private const int Hp = 10;
    private const int Attack = 5;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        DataClassEqualTest();

        DataRecordEqualTest();
    }

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// クラス比較
    /// </summary>
    private void DataClassEqualTest()
    {
        var para1 = new ParameterClass(Name, Hp, Attack);
        var para2 = new ParameterClass(Name, Hp, Attack);

        Debug.Log($"{nameof(ParameterClass)} IsEqual : {para1.Equals(para2)}");
    }

    /// <summary>
    /// レコード比較
    /// </summary>
    private void DataRecordEqualTest()
    {
        var para1= new ParameterRecord(Name,Hp,Attack);
        var para2= new ParameterRecord(Name,Hp,Attack);

        Debug.Log($"{nameof(ParameterRecord)} IsEqual : {para1.Equals(para2)}");
    }

}




結果は以下のようになります。

`classOのEqualは同一のインスタンスかどうかを調べます。
別々のインスタンスなので結果はfalseとなります。

一方、recordのEqualは変数の値で比較を行います。
同じ値を使用しているためtrueとなります。

with(一部分コピー)

また、元々あるrecord一部分のプロパティをコピーして
新規recordインスタンスが作成できる
機能 withを使用できます。

// var【コピー先】 = 【コピー元】 with { プロパティ名 = 値, ... }

var original= new ParameterRecord(Name,Hp,Attack);
var copy = original with { name = "copyHuman" };

参考サイト

レコード - C# リファレンス | Microsoft Learn
init キーワード - C# リファレンス | Microsoft Learn
c# 9.0 - Predefined type 'System.Runtime.CompilerServices.IsExternalInit' is not defined or imported - Stack Overflow

【Unity】段階的にObjectPoolに置き換えていく

Unityのよくある話として、Instantiate()Destroy()が重いという話は聞いた事があると思います。
シューティングゲームを作ると仮定して考えてみましょう。

まずは作る

まずは何もリソース管理を意識しないで作ってみます。

Bulletクラス

Bulletクラスを作成します。
Update()で移動し、一定秒数進んだらDestroy()を行うクラスです。

using System;
using UnityEngine;

/// <summary> 弾 </summary>
public class Bullet : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const float TotalTime = 1.0f;

    /// <summary>
    /// 時間
    /// </summary>
    private float _lifetimer = 0.0f;

    /// <summary>
    /// 移動スピード
    /// </summary>
    private float _speed = 5.0f;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        _lifetimer = TotalTime;
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        transform.position += (transform.right * (_speed * Time.deltaTime));

        _lifetimer -= Time.deltaTime;
        if (_lifetimer <= 0.0f)
        {
            Destroy(this.gameObject);
        }
    }
}

Gunクラス

弾を打つためのGunクラス。
Spaceを押すと弾を生成するクラスです。

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{

    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet= null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        if (_sourceBullet== null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Instantiate(_sourceBullet, transform.position, Quaternion.identity);
        }
    }
}

実行結果


弾の数が増えれば増えるほどInstantiateDestroyが走ります。
わざわざ生成と破壊処理を繰り返すのではなく、
アクティブ状態を切り替えた方が効率的です。

アクティブ状態を切り替えるようにする

それではアクティブ状態を切り替えられるように
クラスを改良していきます。

Bulletクラス

まずはBulletクラスから。
値の初期化をStart()で行っていましたが、
Init()という公開関数を用意して個別で初期化できるようにします。

また、_lifeTimerが0以下になった時にDestroy()を行うのではなく、
自身の役割の終了を伝えるデリゲート
Action<Bullet> _timerFinishCallBackを用意しておきます。

using System;
using UnityEngine;

/// <summary> 弾 </summary>
public class Bullet : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const float TotalTime = 1.0f;

    /// <summary>
    /// 時間
    /// </summary>
    private float _lifetimer = 0.0f;

    /// <summary>
    /// 移動スピード
    /// </summary>
    private float _speed = 5.0f;

    /// <summary>
    /// 時間が0になった時に呼ぶデリゲート
    /// </summary>
    private Action<Bullet> _timerFinishCallBack = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        transform.position += (transform.right * (_speed * Time.deltaTime));

        _lifetimer -= Time.deltaTime;
        if (_lifetimer <= 0.0f)
        {
            _timerFinishCallBack.Invoke(this);
        }
    }

    /// <summary>
    /// 破壊時処理
    /// </summary>
    private void OnDestroy()
    {
        ClearTimerFinishCallBack();
    }

    //-----------------------------------------------------------------------
    // 公開関数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化
    /// </summary>
    public void Init(Vector3 pos)
    {
        transform.position = pos;
        transform.rotation = Quaternion.identity;
        _lifetimer = TotalTime;
    }

    /// <summary>
    /// デリゲート登録
    /// </summary>
    /// <param name="func">時間が0になった時に呼ぶデリゲート</param>
    public void RegisterTimerFinishCallBack(Action<Bullet> onAction)
    {
        _timerFinishCallBack = onAction;
    }

    /// <summary>
    /// デリゲート解除
    /// </summary>
    public void ClearTimerFinishCallBack()
    {
        _timerFinishCallBack = null;
    }
}

Gunクラス

Gunクラス側ではQueue<Bullet> _bulletsを用意。
弾のアクティブ/非アクティブ処理はGun側で制御します。

弾を打つ時に_bulletsの数を確認し、存在していれば取り出して使用します。

もし数が足りない場合は新しく生成し、
RegisterTimerFinishCallBack_bulletsに格納し直すアクション
である
OnFinishActionを登録しておきます。

using System.Collections.Generic;
using UnityEngine;

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{

    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet = null;

    /// <summary>
    /// 弾を格納するキュー
    /// </summary>
    private Queue<Bullet> _bullets = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        _bullets = new();

        if (_sourceBullet == null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }
    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if(_bullets.Count > 0)
            {
                // 既にある物から取り出して初期化
                var bullet = _bullets.Dequeue();
                bullet.Init(transform.position);
                bullet.gameObject.SetActive(true);
            }
            else
            {
                // 新たに生成
                var bullet = Instantiate(_sourceBullet);

                // デリゲード登録->初期化
                bullet.RegisterTimerFinishCallBack(OnFinishAction) 
                bullet.Init(transform.position);
            }
        }
    }

    /// <summary>
    /// プールに格納する処理
    /// </summary>
    /// <param name="bullet">弾</param>
    private void OnFinishAction(Bullet bullet)
    {
        _bullets.Enqueue(bullet);
         bullet.gameObject.SetActive(false);
    }
}

実行結果


自前で処理が完了しました。
時間が0以下になった時に非アクティブになっています。

ObjectPoolをつかう

自前で作成したものをObjectPoolに置き換えていきます。
docs.unity3d.com

ObjectPoolのコンストラクタ引数は以下の通りです。
Unity - Scripting API: Pool.ObjectPool_1.ObjectPool<T0>

引数名 説明 主な処理内容・備考
Func <T> createFunc プールが空の時に呼ばれるデリゲート Instantiate()->初期化処理 等
Action<T> actionOnGet プールからの取り出し時に
呼ばれるデリゲート
SetActive(true)->初期化処理 等
Action<T> actionOnRelease プール格納時に呼ばれるデリゲート SetActive(false) 等
Action<T> actionOnDestroy プールに格納しようとしたが
maxSizeを超えていた時
Destroy() 等
bool collectionCheck 同一インスタンスを使用した際に
例外を投げるか
例外処理はEditorのみ
int defaultCapacity デフォルトの容量 -
int maxSize 最大サイズ 最大サイズを超えると
actionOnDestroyが呼ばれる

※処理内容はあくまで個人的な仕様用途で

取得はGet()。使わなくなったらRelease()を呼びます。
Unity - Scripting API: Pool.ObjectPool_1.Release
Unity - Scripting API: Pool.ObjectPool_1.Get

Gunクラス

_bulletsObjectPoolに置き換えて
Update()の弾生成時に行っていたことを
Start()内、_bulletsの初期化処理に置き換えていきます。

using UnityEngine;
using UnityEngine.Pool;

/// <summary> 銃 </summary>
public class Gun : MonoBehaviour
{
    //-----------------------------------------------------------------------
    // 変数
    //-----------------------------------------------------------------------

    private const int Capacity = 3;

    private const int MaxSize = 5;

    /// <summary>
    /// 弾
    /// </summary>
    [SerializeField]
    private Bullet _sourceBullet = null;

    /// <summary>
    /// オブジェクトプール
    /// </summary>
    private ObjectPool<Bullet> _bullets = null;

    //-----------------------------------------------------------------------
    // Unityメゾット
    //-----------------------------------------------------------------------

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Start()
    {
        if (_sourceBullet == null)
        {
            Debug.Log($"{nameof(Bullet)} null!");
        }

        // プールの生成
        _bullets = new ObjectPool<Bullet>(
            // インスタンス生成時
            () =>
            {
                // 生成
                var obj = Instantiate(_sourceBullet);
                var bullet = obj.GetComponent<Bullet>();

                // Poolに格納するデリゲートの登録->初期化
                bullet.RegisterTimerFinishCallBack(OnFinishAction);
                bullet.Init(transform.position);
                bullet.gameObject.SetActive(true);
                return bullet;
            },

            // プール取り出し時
            (b) =>
            {
                b.Init(transform.position);
                b.gameObject.SetActive(true);
            },

            // プール格納時
            (b) => b.gameObject.SetActive(false),

            // プールに戻せなかった時
            (b) => Destroy(b.gameObject),

            // 同一インスタンス時例外を投げるか?
            true,

            // スタックのデフォルト容量
            Capacity,

            // 最大サイズ
            MaxSize);

    }

    /// <summary>
    /// 更新処理
    /// </summary>
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            // 取得はGet
            _bullets.Get();
        }
    }

    /// <summary>
    /// プールに格納する処理
    /// </summary>
    /// <param name="bullet">弾</param>
    private void OnFinishAction(Bullet bullet)
    {
        // 格納はRelese
        _bullets.Release(bullet);
    }
}

実行結果


非アクティブ化の処理に加えて、
最大数を超えたときにDestroy()してくれるようになりました。
これで効率よく弾が発射できるようになりました。

参考サイト

Unity - Scripting API: ObjectPool<T0>

【C#】Listの内容をランダムにシャッフルする拡張メゾットを作る

リストの中身を手軽にシャッフルする拡張メゾットを作ります。
カードゲームなど、中身をランダムにさせたいときに使うかも。

ソースコード

ソースコードこちら

使い方

通常通りリストを作成し、Shuffle()を呼び出すだけで完了します。

ゆる解説

シャッフルのアルゴリズムにはFisher–Yates shuffleを使用しています。
en.wikipedia.org

配列の末尾から中身を入れ替えていくアルゴリズムです。
図にすると以下のような感じ。

また、最近のUnityのバージョンではタプル型が使用可能です。
この拡張メゾット内では値を入れ替える箇所で使っています。

値の入れ替えでローカル変数を用意しなくてよくなりました。
タプル様様ですね…!

int x = 0;
int y = 1;

// 今までは入れ替える処理を行うときは
// 格納する変数が必要だった
int tmp = x;
x = y;
y = tmp;
int x = 0 ;
int y = 1;

// タプル型を使用すれば一行で終わる
(x, y) = (y, x);

参考サイト

Fisher–Yates shuffle - Wikipedia
タプル型 - C# リファレンス | Microsoft Learn

【C#】自分だけのC#コーディングルールを作った

タイトルの通り、自分用のC#コーディングルールを作成しました。

github.com

背景

自分の頭の中での思考だったり、今までに作ってきたソースコードを見直したりして
一貫性がありませんでした。

自分が気に入った、色んな文献のコーディングルールが頭の中に整理されていない始末。
何かしらの指標が欲しいと思い作成しました。

環境・参考元


Visual Studio Codeで作成しました。Visual Studioと違ってサクサク。
azure.microsoft.com

また、Markdown All in One という拡張機能を使って作成しています。
marketplace.visualstudio.com

それから、かなりの割合で参考にしたコーディングルールが2つ存在します。
github.com
google.github.io

良かった点

ソースコードに一貫性ができる

当たり前ですが、自分の中での明確なルールができるため
書く時に悩む時間が減り、読む時間が減りました。

私の場合、一貫性を持たせるために他のソースコードを見に行く時間を奪われており、
その時間が明確に減ったと思っています。

共有ができる

GitHubで公開しているため、今後他の人と作るときにリンクをぶん投げるだけで
このルールに従って作成が可能になります。

もし誰かが休んだとしてもルールに乗っ取って作っていれば
読む時間を減らし、修正に充てられます。

日本語である

日本語で作られているということは、日本語で作られているということです。
私英語苦手なので…。

悪かった点

ソースコードが書けない

スタイルガイドを自分が満足するまで書ききるのに約1週間
時間にして約10時間程度かけて作成しました。
この時間でどれだけ他の事に費やせたのかと考えると…

表現の言語化が難しい

これまでなんとなく作っていたので、日本語にして書き起こすことが非常に難しいです。 曖昧な表現はできるだけ避け、不明な部分はきちんと調べました。

終わりに

スタイルガイドを書くのは非常に疲れました。 書ききると綺麗に作成されており、非常に自己肯定感が上がり満足しています。

また、Markdownについては
このはてなブログで日々ブログを書いていた知識が使えて良かったです。

これで終わりではなく、このルールを元にして作られていくのを忘れないように
ゲームを作っていきたいです。

参考サイト

編集

Visual Studio Code – コード エディター | Microsoft Azure
Markdown All in One - Visual Studio Marketplace

コーディングルール

C# at Google Style Guide | styleguide
GitHub - DotNetAnalyzers/StyleCopAnalyzers: An implementation of StyleCop rules using the .NET Compiler Platform
Nasan_UnityStyleGuide/CSharpStyleGuide.md at main · nasan730/Nasan_UnityStyleGuide · GitHub

【Unity】TextMeshProで日本語フォントを扱う

二番煎じかもしれませんが、TextMeshProを使用する際にいつも調べているので
自分のブログに書き記しておきます。

フォントをダウンロードする

フォントのダウンロードは権利表記がしっかりしているものを選びます。
特に決まってない場合はGoogleFontがおすすめです。
fonts.google.com

商用利用が可能であり、種類も豊富です。
よくある質問  |  Google Fonts  |  Google for Developers

フォントをインポートする

ダウンロードしたものを解凍し、
そのままUnityのProjectウィンドウにドラッグ&ドロップします。

上記タブからWindow → TextMeshPro → FontAssetCreator を選択します。

フォントからフォントアセットに変換する

FontAssetCreator にはフォントアセットに変換するための項目が存在します。

各種項目設定をしたのちにGenerateFontAtlasを押すと
フォントアセットが作成されます。

その後Saveを押してフォントアセットとして保存され、
UI等で使用できるようになります。

各種設定項目で重要度の高いものは以下の通りです。

項目 説明
Source Font File 元となるフォントデータ
Sampling Point Size アトラスに入る一文字のサイズ
Padding アトラスに入る文字間のサイズ
Atlas Resolution 生成されるアトラスの解像度。後述。
Character Set アトラスに入る文字の指定。後述。

アセットサイズについて

アトラスの解像度(Atlas Resolution)が高ければ高いほど
アセットのデータサイズが大きくなります。

・256の場合 - 144KB

・8192の場合 - 131,088KB

個人的に開発しているものであれば問題ありませんが、
Githubを使用して開発を行う場合は100MB(==102400KB)を超えるファイルがあげられないため、
注意が必要です。

GitHub100 MB を超えるファイルをブロックします。 docs.github.com

Character Setについて

テキストアセットに必要な文字を指定できます。

項目 説明
ASCII ASCII文字のみ
ASCII - Wikipedia
Numbers + Symbols 四則演算+数値+()等の記号
Custom Characters 実際に使用する文字を直接指定

英語文字で使用するならASCII
ゲーム内の通貨などを扱う文字だけであればNumbers + Symbolsを選択し、
アセットサイズをできるだけ小さくした方が良いです。

日本語を使用したい場合、Custom Characters を選択して
実際に使用したい文字を直接打ち込みます。

全ての文字列を扱いたい場合は以下のGistを使ってコピー&ペーストすれば問題ないです。
日本語文字コード範囲指定(ascii・ひらがな・カタカナ・第一水準および第二水準(JIS-X0208-1997)に含まれる漢字) · GitHub

ただ、上記のGistすべてを使用してゲームを作成することはまれだと思います。
予め使う文字を絞ってアセットサイズを小さくしていくのがおすすめです。

【Unity】できるだけ楽してテンプレートスクリプトが作成できる機能を用意する

Unityで新たにソースコードを作成する際、プロジェクトタブでC#スクリプト作成して、名前つけて、 クラスの役割に権利表記やXML タグを使ってコメント付けて、UTF-8で保存して、場合によっては継承元を変えて…。

赤文字の作業が非常に面倒なので、あらかじめいくつかテンプレートを作っておいて
普段のC#スクリプトと遜色ない形で作成できる機能を作りました。

ソースコード

ソースコードこちら。

使い方

はじめに、テンプレート化させたいScriptをテキストファイル形式で用意しておきます。
今回の場合は普通のMonoBehaviourクラスにいくつかコメントを追加してUTF-8で保存したものです。

作成するときに名前空間やクラス名のところに予約語を入れておきます。
これらは実際にファイルが作成されたときに適切なものに変換してくれるものです。

予約語 説明
#ROOTNAMESPACEBEGIN#
#ROOTNAMESPACEEND#
プロジェクトのEditor情報で設定した名前空間が割り当てられます。
設定していない場合何も表記されません。
#SCRIPTNAME# 作成するときにつけたファイル名がそのまま割り当てられます。
#NOTRIM# 明示的に空白行を開けること意味します。

その後配置したい階層で右クリック→TemplateScriptと選択し、作成します。 いくつか自分で使いたいテンプレートを作成することが可能です。