View on GitHub

adventure-cube

メタプログラミング

実務で覚えておくと便利な、「プログラムで抽象を扱う処理」についてです。

メタプログラミングとは?

メタプログラミングとは、プログラムをプログラムを使って作りだす、という行為を指します。
プログラムコードを抽象化する、というやり方ともいえます。

具体的に、みんな使っているもので、List<T>とかが該当します。
この<T>っていう部分が抽象化されている部分で、「好きな型を入れろよ」という意味を持っています。

同じコードを書きたくない

メタプログラミングですが、なぜやるかというと、
型や一部の処理が違うだけの処理を複数回書きたくないからです。

これはListみたいな簡単な処理から、キャラクターの管理クラスなどの複雑なクラスに対しても使用することができます。

whereって何?

型制約といいます。
入る型を抽象化したいけど、限度はあるよね…stringとかintとかじゃなくて、もっとゲーム中のオブジェクトだけで考えたいです…みたいなクラスに対して、型を制限してメタプログラミングする実装です。

具体的には、

CacheTemplate.cs
public class CacheTemplate<T> where T : UnityEngine.Object
{
}

Bullet.cs
static public T Build<T>(string name, MasterCube master, MonoBlock owner, Transform parent) where T : Bullet
{
}

等の処理が該当します。

where T : 型名

をクラス名か、関数名の後ろにつけて使用します。

ゲーム中の実装から確認するメタプログラミングの便利な使い方

さきほど例に挙げた2つの処理が該当します。それぞれ解説しましょう。

まず、クラス名にをつけているこのクラスです。

CacheTemplate.cs
public class CacheTemplate<T> where T : UnityEngine.Object
{
    Dictionary<string, T> Cache = new Dictionary<string, T>(); //キャッシュ辞書

    /// <summary>
    /// Prefabを取得する
    /// NOTE: キャッシュが無かったら探してきて読み込む
    /// </summary>
    /// <param name="path">アセットやPrefabのパス</param>
    /// <returns>アセットやPrefab</returns>
    public T GetCache(string path)
    {
        if (!Cache.ContainsKey(path))
        {
            Cache.Add(path, Resources.Load<T>(path));
        }
        return Cache[path];
    }
}

このクラスは、Resourcesにあるさまざまな型のPrefabをキャッシュするためのクラスです。
Prefabには、MonoBlockや、Materialが入る予定があります。
それをイチイチ書いていると面倒ですし、型が違うだけなのでまとめられそうですよね。

また、将来的にやる予定でやってないものとして、AssetBundle対応というのがUnityにはあります。
このとき、基本的にはResources.Loadは使わなくなっていきます。

参考)
https://qiita.com/k7a/items/df6dd8ea66cbc5a1e21d

つまり、AssetBundle対応をやるとき、Rersources.Loadしている場所はすべて影響を受けます。
なので、今後の対応のためにも、読み込み部分はまとまっていると楽ですよね。

次に、Bulletのビルダーです。
ビルダーとは何ぞや?という人は、次にやるデザインパターン編を見てください。

Bullet.cs
static public T Build<T>(string name, MasterCube master, MonoBlock owner, Transform parent) where T : Bullet
{
    var prefab = ResourceCache.GetCache(ResourceType.Bullet, name);
    GameObject obj = null;
    if (parent == null)
    {
        obj = GameObject.Instantiate(prefab);
    }
    else
    {
        obj = GameObject.Instantiate(prefab, parent);
    }

    var blt = obj.GetComponent<T>();
    blt.MasterCube = master;
    blt.OwnerCube = owner;
    blt.Setup();
    return blt;
}

このコード、実はジェネリックで型を受け取る意味はそこまでないです。
よって制約をつけている意味も実はそこまでない(すべてBulletで直打ちしても通る処理)んですが、型制約をつけることで、どのオブジェクトを作ったのかというのがコード上で確認しやすくなります。