Cooooding!!

Unity(C#)を使ったゲーム開発関連Tipsなど

FieldInfoが自動実装されたフィールドのものか判定する【C#】

概要

プロパティの実装を省略する自動実装プロパティ(Auto-Implemented Properties)を使うと自動的に見えないフィールドが追加されます。このフィールドはBackingFieldと呼ばれます。BackingFieldは普通にアクセスすることはできませんが、Reflectionを使って全てのFieldInfoを取得するとその中にBackingFieldのFieldInfoが含まれます。BackingFieldのFieldInfoを除外するために区別する方法を調べてみました。

public class Hoge
{
    // 自動実装プロパティを使わない場合
    private int m_A;
    public int a { get { return m_A; } set { m_A = value; } }

    // 自動実装プロパティを使う場合("<b>k__BackingField"というフィールドが追加される)
    public int b { get; set; }
}

判別方法

自動実装されたフィールドかどうかは以下のようにAttributeをチェックすることで判定できます。

public static bool IsBackingField(FieldInfo field)
{
    return field.IsDefined(typeof(CompilerGeneratedAttribute), false);
}

ただし、自動実装プロパティ以外にもフィールドが自動実装されるパターンがあるため、自動実装プロパティによって追加されたフィールドであるかどうかの判定には使えません。

自動実装プロパティと同じように、実装を省略したeventを定義すると自動的にフィールドが追加されます。

public class Hoge
{
    // 自動でフィールドが追加されないevent
    public event System.Action action0{ add { } remove { } }
    // 自動でフィールドが追加されるevent("action1"というフィールドが追加される)
    public event System.Action action1;
}

これらはフィールドの名前で区別することができます。

/// <summary>
/// 自動実装プロパティによって追加されたフィールドかどうかを判定する
/// </summary>
public static bool IsPropertyBackingField(FieldInfo field)
{
    return IsBackingField(field) && field.Name[0] == '<';
}

/// <summary>
/// 自動が省略されたeventによって追加されたフィールドかどうかを判定する
/// </summary>
public static bool IsEventBackingField(FieldInfo field)
{
    return IsBackingField(field) && field.Name[0] != '<';
}

サンプル

private class Hoge
{
    // 自動でフィールドが追加されないevent
    public event System.Action action0{ add { } remove { } }
    // 自動でフィールドが追加されるevent
    public event System.Action action1;
    // 自動実装プロパティを使わない場合
    private int m_A;
    public int a { get { return m_A; } set { m_A = value; } }
    // 自動実装プロパティを使う場合
    public int b { get; set; }
}

public void IsBackingField()
{
    var flags = BindingFlags.Public |
            BindingFlags.NonPublic |
            BindingFlags.Instance;
    foreach(var field in typeof(Hoge).GetFields(flags))
    {
        UnityEngine.Debug.LogFormat(
            "{0}: {1}, {2}, {3}",
            field.Name,
            IsBackingField(field),
            IsPropertyBackingField(field),
            IsEventBackingField(field)
        );
    }
}

実行結果
(フィールド名: 自動実装されたものか, プロパティによるものか, イベントによるものか)

action1: True, False, True
m_A: False, False, False
<b>k__BackingField: True, True, False

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019

TypeがStructかどうかを判定する【C#】

概要

System.Typeがstructかどうかを判定しようと思ったら、System.Type.IsClassはあるのに何故かIsStructがなかったので判定方法を調べてみました。

判定方法

判定はIsValueTypeやIsPrimitiveなどを組み合わせて実装します。

public static bool IsStruct(System.Type type)
{
    return type.IsValueType &&  // 値型に限定(classを除外)
        !type.IsPrimitive &&    // intやfloatなどを除外
        !type.IsEnum;           // enumを除外(enumはValueTypeだけどPrimitiveではない)
}

サンプルコード

UnityEngine.Debug.Log(IsStruct(typeof(int)));
UnityEngine.Debug.Log(IsStruct(typeof(string)));
UnityEngine.Debug.Log(IsStruct(typeof(UnityEngine.Vector2)));
UnityEngine.Debug.Log(IsStruct(typeof(UnityEngine.Rect)));
UnityEngine.Debug.Log(IsStruct(typeof(UnityEngine.MonoBehaviour)));
UnityEngine.Debug.Log(IsStruct(typeof(UnityEngine.TextureFormat)));

実行結果

False
False
True
True
False
False

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019

Interfaceに抽象ではないメソッドを追加する【C#】

概要

C# Job Systemの勉強をしていたらインターフェースに抽象ではないメソッドを追加する面白いテクニックを見かけたので紹介します。

追加する方法

追加する方法は簡単で拡張メソッドを使うだけです。例えば以下のように実装します。

public interface IEnemySetting
{
    string name { get; }

    int level { get; }
}

public static class IEnemyExtensions
{
    public static void Print(this IEnemySetting enemy)
    {
        UnityEngine.Debug.LogFormat("{0} Lv{1}", enemy.name, enemy.level);
    }
}

これはインターフェースなのでstructに継承させることもできます。普通は継承できないabstract classを継承するようなことが可能になります。

public struct HogeSetting : IEnemySetting
{
    public string name => "Hoge";

    public int level => 12;
}

public class NewBehaviourScript : UnityEngine.MonoBehaviour
{
    void Start()
    {
        var hoge = new HogeSetting();
        hoge.Print();       // インターフェースの拡張メソッドが呼べている
    }
}

C#8からはインターフェースのデフォルト実装が入るらしいのでもっと簡単にもっと便利なことができそうですが、それまでの代替機能としてときどき使えそうな気がします。

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019

クリックした先のXY平面上の座標を取得する【Unity】

概要

クリックした先のXY平面上にエフェクトや敵を生成するテスト用のシーンを作るために、その座標を計算する関数を実装しました。Colliderを置いてPhysics.Raycastで座標を取得する方法もあるのですが、わざわざColliderを置くのも手間なのでColliderを使わない方法で実装してみました。

座標取得方法

public bool GetPosition(out Vector3 result)
{
    // カメラはメインカメラを使う
    var camera = Camera.main;
    // クリック位置を取得
    var touchPosition = Input.mousePosition;
    // XY平面を作る
    var plane = new Plane(Vector3.forward, 0);
    // カメラからのRayを作成
    var ray = camera.ScreenPointToRay(touchPosition);
    // rayと平面の交点を求める(交差しない可能性もある)
    if (plane.Raycast(ray, out float enter))
    {
        result = ray.GetPoint(enter);
        return true;
    }
    else
    {
        // rayと平面が交差しなかったので座標が取得できなかった
        result = Vector3.zero;
        return false;
    }
}

この関数はカメラの向きがXY平面に対して垂直になっていなくてもXY平面上の座標を返します。

XY平面上の座標ではなくXZ平面上の座標を取りたい場合にはplaneの値を変えればOKです。

// XZ平面を作る
var plane = new Plane(Vector3.up, 0);

環境

  • Unity 2019.1.9f1
  • VisualStudio 2019

Unity2019に移行して発生したトラブルとその対処【Unity】

概要

これまでUnity2018.4で作っていたプロジェクトをそろそろ2019.1に乗り換えようと思い移行してみたところいくつかトラブルが発生しました。そのトラブル内容と対処内容をメモしておきます。

トラブル

1. prefabがおかしくなった

prefabに付けた自作のコンポーネントのSerializeFieldに保存していた値の一部が何故か初期値に戻っていました。Nestして配置した先やProjectViewで選択してInspectorを見ると初期値に戻っていますが、ダブルクリックして編集画面を開くと何故か正しい値が保存されている奇妙な状態になっていました。

これはUnityを再起動しても直りませんでしたが、Libraryフォルダを削除してから再起動したら直りました。

2. UnityEngine.Vector3.magnitudeの計算結果が微妙に変わった

これは計算誤差を許容するように実装していなかった自分が悪いのですが、UnityEngine.Vector3.magnitude(もしくはその中で使われている関数)の計算結果が微妙に変わったようで処理に影響が出たところがありました。

サンプルコード

var vec = new UnityEngine.Vector3(1, 2, 3).normalized * -5.0f;
Debug.LogFormat("{0} == {1}: {2}", 5, vec.magnitude.ToString("r"), 5 == vec.magnitude);

Unity2018.4.2f1の出力結果

5 == 5: True

Unity2019.1.9f1の出力結果

5 == 4.99999952: False

これは計算誤差を許容するように修正して解決しました。

3. NUnit.Framework.Assert.AreEqualの比較結果が一部変わった

これも自分の実装が悪かったのですが、NUnit.Framework.Assert.AreEqualの比較処理が一部変更になったようでTest Runnerでエラーが出るようになりました。

Unity2018.4では通るがUnity2019.1では通らないサンプルコード

Assert.AreEqual(
    new Vector3(float.NaN, float.NaN, float.NaN),
    new Vector3(float.NaN, float.NaN, float.NaN)
);

NaN同士を==で比較した場合はfalseになるのが正しいのでこちらの結果の方が正しいように思えます。これはIsNaNという関数を作って判定することで解決しました。

public static bool IsNaN(UnityEngine.Vector3 value)
{
    return float.IsNaN(value.x) &&
        float.IsNaN(value.y) &&
        float.IsNaN(value.z);
}

ちなみに、float.NaN同士をAreEqualで比較した場合は通ってしまうのでこの変更は意図したものではないのかもしれません。

Assert.AreEqual(float.NaN, float.NaN); // 何故かこれは通る

環境

  • Unity 2018.4.2f1
  • Unity 2019.1.9f1

Planeのdistanceについて【Unity】

概要

UnityEngine.Planeのdistanceがわかりにくく理解に少し苦労したのでわかったことをまとめました。

法線と距離を使った平面の表現

平面は法線と原点との距離を使って表すことができます。
Planeと法線とdistanceの関係図

UnityのPlaneも法線と距離を指定して初期化することができます。

var plane = new Plane(Vector3.up, 1);

ただ、ここで注意しなければならないのはdistanceが「原点からの距離」ではなく「原点までの距離」であるということです。

例えば、Plane(Vector3.up, 1)ならば以下の図のようになるのは間違いです。
間違い。これはPlane(Vector3.up, 1)ではない

正解はこちらです。
こちらが正解。これがPlane(Vector3.up, 1)

公式ドキュメントには「原点から平面への距離を返します(Distance from the origin to the plane)」と書かれていますが、おそらくこれが間違っています。GetDistanceToPointという平面と点の距離を計算するメソッドがあるのでこれを使って確認してみます。

ソースコード

// y軸方向の法線を持ち、距離1の平面を作る
var plane = new Plane(Vector3.up, 1);
// (x, y, z) = (0, 1, 0)の点と平面の距離を計算
var distance = plane.GetDistanceToPoint(new Vector3(0, 1, 0));
// 距離を出力
Debug.LogFormat("distance = {0}", distance);

出力結果

distance = 2

(0, 1, 0)と平面の距離が2になるのでPlane(Vector3.up, 1)は(0, 1, 0)を通っていないことがわかります。

環境

  • Unity 2018.4.2f1

Gizmosで平面を描画する【Unity】

概要

計算結果の平面(UnityEngine.Plane)が正しいかどうかを確認するためにGizmosで描画しようと思ったら簡単に描画できる関数がなかったので実装してみました。Planeは長さを持たず無限に広がるため、描画時に座標とサイズを指定して一部分を描画することになります。

実装

片面の平面を描画

Planeは向き(法線方向)を持つため向きが確認できるように片面だけ描画します。

/// <summary>
/// 片面の平面を描画する
/// </summary>
/// <param name="plane">平面</param>
/// <param name="point">この点に最も近い平面上の座標を中心とする</param>
/// <param name="size">描画するサイズ</param>
public static void Draw(
    Plane plane, Vector3 point, Vector2 size
)
{
    var mesh = MakeMesh(plane, point, size);
    Gizmos.DrawMesh(mesh);
}

/// <summary>
/// 平面を描画するためのMeshを作る
/// </summary>
public static Mesh MakeMesh(Plane plane, Vector3 point, Vector2 size)
{
    // Unityは左手系なので、時計回りの頂点で法線は(0, 0, -1)方向
    var vertices = GetVertices(plane, point, size);
    var mesh = new Mesh();
    mesh.SetVertices(new List<Vector3>(vertices));
    mesh.SetTriangles(new List<int>(){ 0, 1, 2, 0, 2, 3 }, 0);
    mesh.RecalculateBounds();
    mesh.RecalculateNormals();
    return mesh;
}

/// <summary>
/// 平面を描画するための頂点位置計算
/// </summary>
public static Vector3[] GetVertices(
    Plane plane, Vector3 point, Vector2 size
)
{
    // pointが平面上にないなら最も近い平面上の座標を中心とする
    var center = plane.ClosestPointOnPlane(point);

    var rotation = Quaternion.LookRotation(-plane.normal);
    var width = 0.5f * size.x;
    var height = 0.5f * size.y;
    var vertices = new Vector3[] {
        new Vector3(-width, -height, 0),
        new Vector3(-width,  height, 0),
        new Vector3( width,  height, 0),
        new Vector3( width, -height, 0),
    };
    for (int i = 0; i < vertices.Length; ++i)
    {
        vertices[i] = rotation * vertices[i] + center;
    }
    return vertices;
}

片面の平面を描画した結果

両面の平面を描画

Planeは向きを持つものの両面表示したいこともあります。これは細長いCubeを使うと簡単に実装できます。

public static void DrawBothFace(
    Plane plane, Vector3 point, Vector2 size
)
{
    // pointが平面上にないなら最も近い平面上の座標を中心とする
    var center = plane.ClosestPointOnPlane(point);

    var backup = UnityEngine.Gizmos.matrix;
    Gizmos.matrix = Matrix4x4.TRS(
        center,                                 // 中心
        Quaternion.LookRotation(plane.normal),  // 回転
        new Vector3(size.x, size.y, 0.0001f)    // サイズ(厚みをほぼ0する)
    );
    Gizmos.DrawCube(Vector3.zero, Vector3.one);
    Gizmos.matrix = backup;
}

両面の平面を描画した結果

枠だけを描画

簡単なのでついでに枠の描画も実装してみます。

public static void DrawWire(
    Plane plane, Vector3 point, Vector2 size
)
{
    var vertices = GetVertices(plane, point, size);
    for (int i = 0, length = vertices.Length; i < length; ++i)
    {
        var p0 = vertices[i];
        var p1 = vertices[(i + 1) % length];
        Gizmos.DrawLine(p0, p1);
    }
}

平面の枠だけ描画した結果

環境

  • Unity 2018.4.2f1
  • VisualStudio 2019 (ver16.1.5)