e.blog

主にUnity/UE周りのことについてまとめていきます

TransformクラスのTransform**** / InverseTransform****系メソッドのメモ

以前、Qiitaで書いていた記事の移植です。
Inverse系も追加したのでこちらに移動しました。

概要

TransformPointTransformVectorTransformDirectionがそれぞれなにをやってくれているのか、ピンと来てなかったので、どういう処理をしたら同じ結果が得られるのか書いてみたのでメモ。

まぁやってることはシンプルに、モデル行列掛けているだけだけど、その後の処理で平行移動を無効化したり正規化したり、といったことをしている。方向だけを取りたい、みたいなときに使う。
なにやってるか分からないと気持ち悪かったので書いてみた、完全にメモですw

Transform****系メソッド

// ----------------------------------------------------------------------------
// TransformPoint
// 意味: 通常のモデル行列を掛けたポイントを得る
Vector3 point = Vector3.one;
Vector3 newPoint1 = transform.TransformPoint(point);

Vector4 v = new Vector4(point.x, point.y, point.z, 1f);
Vector3 newPoint2 = transform.localToWorldMatrix * v;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newPoint2 = transform.localToWorldMatrix.MultiplyPoint(point);

// ----------------------------------------------------------------------------
// TransformVector
// 意味: 回転を適用したのちに、平行移動を無効化したベクトルを得る
Vector3 vector = new Vector3(0, 1.5f, 0);
Vector3 newVector1 = transform.TransformVector(vector);

Vector4 v = new Vector4(vector.x, vector.y, vector.z, 1f);
Vector3 newVector2 = (transform.localToWorldMatrix * v) - transform.position;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newVector2 = transform.localToWorldMatrix.MultiplyVector(vector);

// ----------------------------------------------------------------------------
// TransformDirection
// 意味: 回転を適用したのちに、平行移動を無効化し正規化したベクトルを得る
Vector3 direction = Vector3.up;
Vector3 newDirection1 = transform.TransformDirection(direction);

Vector4 v = new Vector4(direction.x, direction.y, direction.z, 1f);
Vector3 newDirection2 = ((transform.localToWorldMatrix * v) - transform.position).normalized;

// ↓Vector4作らないで済むのでこちらのほうが便利
// Vector3 newDirection2 = transform.localToWorldMatrix.MultiplyVector(direction);

InverseTransform****系メソッド

// ----------------------------------------------------------------------------
// InverseTransformPoint
// 意味: ワールド座標からローカル座標へ変換する

Vector3 newPoint1 = transform.InverseTransformPoint(_v);

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 p = new Vector4(_v.x, _v.y, _v.z, 1f);
Vector3 newPoint2 = m * p;


// ----------------------------------------------------------------------------
// InverseTransformVector
// 意味: 指定したベクトルの方向のみ、ワールド座標からローカル座標へ変換する

Vector3 newdir1 = transform.InverseTransformVector(_v);

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 c0 = m.GetColumn(0);
Vector4 c1 = m.GetColumn(1);
Vector4 c2 = m.GetColumn(2);
Vector4 c3 = new Vector4(0, 0, 0, 1f);
Matrix4x4 newM = new Matrix4x4(c0, c1, c2, c3);
newM = newM.inverse.transpose;

Vector4 p = new Vector4(_v.x, _v.y, _v.z, 1f);
Vector3 newdir2 = newM * p;

// ↓内容的には以下でまかなえる
// Vector3 newdir2 = transform.worldToLocalMatrix.MultiplyVector(_v);


// ----------------------------------------------------------------------------
// InverseTransformDirection
//
// ※ InverseTransformVectorの正規化した単位ベクトルを得るのかと思いきや、InverseTransformVectorと同じだったので割愛

解説

Inverse系のVector処理はやや複雑です。抜粋するとコードは以下。

Matrix4x4 m = transform.worldToLocalMatrix;
Vector4 c0 = m.GetColumn(0);
Vector4 c1 = m.GetColumn(1);
Vector4 c2 = m.GetColumn(2);
Vector4 c3 = new Vector4(0, 0, 0, 1f);
Matrix4x4 newM = new Matrix4x4(c0, c1, c2, c3);
newM = newM.inverse.transpose;

取り出した、ワールドからローカルへの変換行列をさらに分解して新しい行列を生成しています。
ここでやっていることは方向のみのベクトルを扱う関係で平行移動部分を除去し、移動なしの行列に変換しています。

そして最後に、逆転置行列にするためにnewM = newM.inverse.transpose;としています。

なぜ逆転置行列なのか。
それには理由があります。詳細については以下の記事がとても詳しいのでそちらを御覧ください。

raytracing.hatenablog.com

言葉で説明している箇所を引用させていただくと、

頂点について、モデル座標系からワールド座標系に移すときには何らかの変換行列を使って変換する。
しかし、頂点に対して作用する行列と同じものを法線に対して作用させてもうまくいかない。 まず、平行移動成分を除去しなければならない。法線は向きのみの量なので平行移動は関係ないからである。
回転移動成分についてはそのまま使うことが出来る。
拡大縮小などのスケーリング成分については、たとえば各頂点をX軸方向に3倍にする場合、法線はX軸方向に1/3倍にする必要がある。(最終的に正規化して長さは1にする)

これらの各要素を考慮しつつ法線を適切に変換するための行列を求めるには、頂点の変換行列の逆転置行列を求めればよい、ということが知られている。

ということ。
実際にベクトルを書いてみると分かりますが、スケールを掛けると方向が異なった方向を向いてしまいます。
しかし今回の求めるべきベクトルは方向を適切に変換することにあります。

なので上記のように「逆転置行列」を使う必要がある、というわけです。

TransformのLookAtをUnity APIを使わずに書く

概要

仕事でLookAtを、Unity APIを使わずに書くことがあったのでメモ。

実行した結果。ちゃんとCubeを注視し続けます。
f:id:edo_m18:20180418015908g:plain

サンプルコード

実装したサンプルコードは以下。

このスクリプトをカメラに貼り付けて、ターゲットを指定すると常にそれを視界に捉えた状態になります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LookAtSample : MonoBehaviour
{
    [SerializeField]
    private Transform _target;
    
    private void Update()
    {
        LookAt(_target.position);
    }

    private void LookAt(Vector3 target)
    {
        Vector3 z = (target - transform.position).normalized;
        Vector3 x = Vector3.Cross(Vector3.up, z).normalized;
        Vector3 y = Vector3.Cross(z, x).normalized;

        Matrix4x4 m = Matrix4x4.identity;
        m[0, 0] = x.x; m[0, 1] = y.x; m[0, 2] = z.x;
        m[1, 0] = x.y; m[1, 1] = y.y; m[1, 2] = z.y;
        m[2, 0] = x.z; m[2, 1] = y.z; m[2, 2] = z.z;

        Quaternion rot = GetRotation(m);
        transform.rotation = rot;
    }

    private Quaternion GetRotation(Matrix4x4 m)
    {
        float[] elem = new float[4];
        elem[0] = m.m00 - m.m11 - m.m22 + 1.0f;
        elem[1] = -m.m00 + m.m11 - m.m22 + 1.0f;
        elem[2] = -m.m00 - m.m11 + m.m22 + 1.0f;
        elem[3] = m.m00 + m.m11 + m.m22 + 1.0f;

        int biggestIdx = 0;
        for (int i = 0; i < elem.Length; i++)
        {
            if (elem[i] > elem[biggestIdx])
            {
                biggestIdx = i;
            }
        }

        if (elem[biggestIdx] < 0)
        {
            Debug.Log("Wrong matrix.");
            return new Quaternion();
        }

        float[] q = new float[4];
        float v = Mathf.Sqrt(elem[biggestIdx]) * 0.5f;
        q[biggestIdx] = v;
        float mult = 0.25f / v;

        switch (biggestIdx)
        {
            case 0:
                q[1] = (m.m10 + m.m01) * mult;
                q[2] = (m.m02 + m.m20) * mult;
                q[3] = (m.m21 - m.m12) * mult;
                break;
            case 1:
                q[0] = (m.m10 + m.m01) * mult;
                q[2] = (m.m21 + m.m12) * mult;
                q[3] = (m.m02 - m.m20) * mult;
                break;
            case 2:
                q[0] = (m.m02 + m.m20) * mult;
                q[1] = (m.m21 + m.m12) * mult;
                q[3] = (m.m10 - m.m01) * mult;
                break;
            case 3:
                q[0] = (m.m21 - m.m12) * mult;
                q[1] = (m.m02 - m.m20) * mult;
                q[2] = (m.m10 - m.m01) * mult;
                break;
        }

        return new Quaternion(q[0], q[1], q[2], q[3]);
    }
}

LookAtの行列を作る

LookAtは結局のところ、ターゲットに向く「回転」を作ることと同義です。

上のコードでやっていることはシンプルです。
「向かせたい方向のベクトル」と「空方向のベクトル」から、外積を使ってX軸を割り出します。
(空方向と垂直なベクトルはX軸ですよね)

そして向かせたい方向ベクトルがZ軸となるわけなので、結果として、割り出したX軸と向かせたいベクトルとの外積を取ることで最終的な「上方向」のベクトルを得ることができます。
(「上」が空方向とは限らない)

そうやって求めた3軸の各要素を元に行列を作ります。
なぜこれが「LookAt」に相当するのか。

(x, y, z)(1, 1, 1)のときを考えてみましょう。
該当のコードは以下です。

private void LookAt(Vector3 target)
{
    Vector3 z = (target - transform.position).normalized;
    Vector3 x = Vector3.Cross(Vector3.up, z).normalized;
    Vector3 y = Vector3.Cross(z, x).normalized;

    Matrix4x4 m = Matrix4x4.identity;
    m[0, 0] = x.x; m[0, 1] = y.x; m[0, 2] = z.x;
    m[1, 0] = x.y; m[1, 1] = y.y; m[1, 2] = z.y;
    m[2, 0] = x.z; m[2, 1] = y.z; m[2, 2] = z.z;

    Quaternion rot = GetRotation(m);
    transform.rotation = rot;
}

そして今作った行列をそのベクトル(1, 1, 1)に適用してみます。
Unityの行列は列オーダーなので、ベクトルを右から掛けることで計算します。
つまり、

 \begin{vmatrix}
x.x &y.x &z.x \\
x.y &y.y &z.y \\
x.z &y.z &z.z \\
\end{vmatrix}\times
\begin{vmatrix}
1 \\
1 \\
1 \\
\end{vmatrix}=
\begin{vmatrix}
x.x &y.x & z.x
\end{vmatrix}

となって、回転後の値になっているのが分かるかと思います。

回転行列からクォータニオンを計算する

さて、無事に回転行列が作れましたが、Unityでは回転をクォータニオンで扱っています。
そのため、求めた回転行列はそのまま使えません。なので、クォータニオンに変換する必要があります。

回転行列からクォータニオンへの変換は、マルペケさんのこちらの記事(その58 やっぱり欲しい回転行列⇔クォータニオン相互変換)を参考にさせていただきました。

コード部分は以下です。

private Quaternion GetRotation(Matrix4x4 m)
{
    float[] elem = new float[4];
    elem[0] = m.m00 - m.m11 - m.m22 + 1.0f;
    elem[1] = -m.m00 + m.m11 - m.m22 + 1.0f;
    elem[2] = -m.m00 - m.m11 + m.m22 + 1.0f;
    elem[3] = m.m00 + m.m11 + m.m22 + 1.0f;

    int biggestIdx = 0;
    for (int i = 0; i < elem.Length; i++)
    {
        if (elem[i] > elem[biggestIdx])
        {
            biggestIdx = i;
        }
    }

    if (elem[biggestIdx] < 0)
    {
        Debug.Log("Wrong matrix.");
        return new Quaternion();
    }

    float[] q = new float[4];
    float v = Mathf.Sqrt(elem[biggestIdx]) * 0.5f;
    q[biggestIdx] = v;
    float mult = 0.25f / v;

    switch (biggestIdx)
    {
        case 0:
            q[1] = (m.m10 + m.m01) * mult;
            q[2] = (m.m02 + m.m20) * mult;
            q[3] = (m.m21 - m.m12) * mult;
            break;
        case 1:
            q[0] = (m.m10 + m.m01) * mult;
            q[2] = (m.m21 + m.m12) * mult;
            q[3] = (m.m02 - m.m20) * mult;
            break;
        case 2:
            q[0] = (m.m02 + m.m20) * mult;
            q[1] = (m.m21 + m.m12) * mult;
            q[3] = (m.m10 - m.m01) * mult;
            break;
        case 3:
            q[0] = (m.m21 - m.m12) * mult;
            q[1] = (m.m02 - m.m20) * mult;
            q[2] = (m.m10 - m.m01) * mult;
            break;
    }

    return new Quaternion(q[0], q[1], q[2], q[3]);
}

詳細についてはマルペケさんの記事をご覧ください。

ざっくり概要だけを書くと、回転行列の各要素はクォータニオンから回転行列に変換する際に「どの要素を使うか」で求めることができます。
マルペケさんで書かれている行列を引用させていただくと、以下のようになります。


\begin{vmatrix}
1 - 2y^2 - 2z^2 &2xy + 2wz &2xz - 2wy &0 \\
2xy - 2wz &1 - 2x^2 - 2z^2 &2yz + 2wx &0 \\
2xz + 2wy &2yz - 2wx &1 - 2x^2 - 2y^2 &0 \\
0 &0 &0 &1
\end{vmatrix}

ここで使われているx, y, z, wクォータニオンの各成分です。

そして、この値を組み合わせることによって目的のx, y, z, wを求める、というのが目標です。
例えば、以下の回転行列の要素を組み合わせることで値を取り出します。


m_{11} + m_{22} + m_{33} = 3 - 4(x^2 + y^2 + z^2)  \\
= 3 - 4(n_x^2 + n_y^2 + n_z^2) \sin^2 \theta ' \\
= 3 - 4(1 - \cos^2 \theta ' \\
= 4w^2 - 1

となります。
ここで、 \theta ' = \frac{\theta}{2}なので、wの式に直すと以下のようになります。


w = \frac{\sqrt{m_{11} + m_{22} + m_{33} + 1}}{2}

他の値も同様にして計算していきます。
これ以外にも色々考慮しないとならないことがありますが、詳細についてはマルペケさんの記事をご覧ください。

こうして求めたクォータニオンを、該当オブジェクトのrotationに設定することで、冒頭の画像のようにLookAtが完成します。

フリーハンドで描いた図形をポリゴン分割する

概要

フリーハンドで平面に描いた形に図形を形成するのをやりたかったので、頂点群からポリゴンを形成する処理について書きたいと思います。

こんな感じで、適当に打った点からポリゴンを形成します↓

点群からポリゴンを形成する

まず必要になるのが、点群からポリゴンを形成する処理です。

実装についてはこちらの記事(Javaゲーム制作記 任意多角形の三角形分割)を参考にさせていただきました。

ちなみに、記事中に、さらに元となった記事へのリンクが書いてありましたがリンク切れしてました。
おそらく元となった記事はこちらだと思います↓

sonson.jp

任意の点群からポリゴンを形成する

点群からポリゴンを形成するには、当然ですが点群が囲む任意多角形をポリゴンとして三角形に分割していく作業が必要になります。

大まかに流れを書くと以下のようになります。

  1. 点群を得る
  2. 点群の中で、任意の点(※1)から一番遠い点を見つける
  3. (2)で見つかった点とその両隣の点で三角形を作る
  4. このとき、(2)の点と両隣の点から成る線が作る角度が180度を超えていないことを確認する(※2)
  5. (3)で形成した三角形の中に、点郡の他の点が含まれていないことを確認する
  6. (5)で内包している点がなかったら、それを分割された三角形として採用し、(2)で見つかった点を点群リストから除外する
  7. もし(5)の工程で内包する点が見つかった場合は三角形が構成できないので、ひとつ隣の点を採用点に変更し、もう一度(5)の処理を行う。その際、確認した三角形の向きを保持しておく(※3)
  8. (5)の処理を行い、見つかった三角形と、(7)で保持していた三角形の向きをチェックし、異なった方向だった場合はまた隣の点に移動して(5)の処理を繰り返す
  9. 以後、点群が残り3点(三角形が構成できる最後の点群)になるまで繰り返す

※1 ... 任意の点なのでどこでも構いません。原点などが採用しやすいでしょう。
※2 ... もし超えている場合、見つけた点よりも遠い点が存在するため判定がおかしい。
※3 ... 向きをチェックする理由は、多角形の点の構成によっては外側の三角形が見つかる可能性があるため(図解で後述します)

以上の手順を繰り返すことで、点群を複数の三角形に分割し、冒頭の画像のように任意の多角形をポリゴンに分解することができるようになります。

・・・と、文章だとなんのこっちゃ、だと思うのでまずは図解してみます。

f:id:edo_m18:20180324231129p:plain f:id:edo_m18:20180324231123p:plain f:id:edo_m18:20180324231218p:plain f:id:edo_m18:20180324231222p:plain f:id:edo_m18:20180324231321p:plain f:id:edo_m18:20180324231324p:plain f:id:edo_m18:20180324231327p:plain f:id:edo_m18:20180324231330p:plain f:id:edo_m18:20180324231337p:plain f:id:edo_m18:20180324231340p:plain f:id:edo_m18:20180324231346p:plain f:id:edo_m18:20180324231349p:plain
作った三角形の中に別の点が含まれてしまっている f:id:edo_m18:20180324231353p:plain
三角形の向きは採用点から次の点を辺1、前の点を辺2として、その外積を使って求める f:id:edo_m18:20180324231356p:plain f:id:edo_m18:20180324231402p:plain f:id:edo_m18:20180324231405p:plain f:id:edo_m18:20180324231408p:plain

以上のような感じで、順々に三角形に分解していき、最後の3頂点になるまでそれを繰り返す、という方法です。
仕組み自体はとてもシンプルですね。

コードで見てみる

今回実装したコードも合わせて載せておきます。
(図解したことをコードにしているだけなので、詳細は割愛します)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<Vector3> _leftVertices = new List<Vector3>();
    private List<Vector3> _triangles = new List<Vector3>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _leftVertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _leftVertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _leftVertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear vertices and triangles.
    /// </summary>
    private void ClearMesh()
    {
        _leftVertices.Clear();
        _triangles.Clear();
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        ClearMesh();

        _leftVertices.AddRange(vertices);

        while (_leftVertices.Count > 3)
        {
            DetecteTriangle();
        }

        _triangles.AddRange(_leftVertices);

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _triangles.ToArray();

        int[] indices = new int[_triangles.Count];
        for (int i = 0; i < indices.Length; i ++)
        {
            indices[i] = i;
        }

        mesh.triangles = indices;
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle direction.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(a);
        _triangles.Add(b);
        _triangles.Add(c);

        _leftVertices.RemoveAt(_curIndex);
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            // skip if index in detected three points.
            if (i == _curIndex || i == _nextIndex || i == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_leftVertices[i]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint);
        Vector3 edge2 = (PreviousPoiont - CurrentPoint);

        return Vector3.Cross(edge1, edge2).normalized;
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = (_curIndex + 1) % _leftVertices.Count;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = _curIndex - 1 >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }

    /// <summary>
    /// Find far point from origin.
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;
        for (int i = 0; i < _leftVertices.Count; i++)
        {
            float dist = Vector3.Distance(Vector3.zero, _leftVertices[i]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = i;
            }
        }

        _curIndex = farIndex;
        _nextIndex = (_curIndex + 1) % _leftVertices.Count;
        _prevIndex = (_curIndex - 1) >= 0 ? _curIndex - 1 : _leftVertices.Count - 1;
    }
}

[2018.05.02追記]

上記コードではポリゴン用の頂点を複製してポリゴン数(x3)と同じだけの頂点を生成していましたが、インデックスを割り当てるように修正したのでそちらのコードも載せておきます。

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

/// <summary>
/// Draw mesh by clicked points.
/// </summary>
public class DrawMesh : MonoBehaviour
{
    private List<int> _triangles = new List<int>();
    private List<Vector3> _vertices = new List<Vector3>();
    private Dictionary<int, bool> _verticesBuffer = new Dictionary<int, bool>();

    private Vector3 _prevDirection = Vector3.zero;

    private bool _isIncluding = false;
    private int _curIndex;
    private int _nextIndex;
    private int _prevIndex;

    private Vector3 CurrentPoint
    {
        get { return _vertices[_curIndex]; }
    }
    private Vector3 PreviousPoiont
    {
        get { return _vertices[_prevIndex]; }
    }
    private Vector3 NextPoint
    {
        get { return _vertices[_nextIndex]; }
    }

    /// <summary>
    /// Clear buffers.
    /// </summary>
    private void Clear()
    {
        _vertices.Clear();
        _verticesBuffer.Clear();
        _triangles.Clear();
    }

    private void Initialize(List<Vector3> vertices)
    {
        Clear();

        // 設定された頂点を保持しておく
        _vertices.AddRange(vertices);

        // 全頂点のインデックスを保持、使用済みフラグをfalseで初期化
        for (int i = 0; i < vertices.Count; i++)
        {
            _verticesBuffer.Add(i, false);
        }
    }

    /// <summary>
    /// Create mesh by vertices.
    /// </summary>
    public GameObject CreateMesh(List<Vector3> vertices)
    {
        Initialize(vertices);

        while (true)
        {
            KeyValuePair<int, bool>[] left = _verticesBuffer.Where(buf => !buf.Value).ToArray();
            if (left.Length <= 3)
            {
                break;
            }
            DetecteTriangle();
        }

        int[] keys = _verticesBuffer.Keys.ToArray();
        foreach (int key in keys)
        {
            if (!_verticesBuffer[key])
            {
                _verticesBuffer[key] = true;
                _triangles.Add(key);
            }
        }

        Debug.Log("Done chekcing.");

        Mesh mesh = new Mesh();
        mesh.vertices = _vertices.ToArray();

        mesh.triangles = _triangles.ToArray();
        mesh.RecalculateNormals();

        GameObject go = new GameObject("MeshObject", typeof(MeshFilter), typeof(MeshRenderer));

        MeshFilter filter = go.GetComponent<MeshFilter>();
        filter.mesh = mesh;

        return go;
    }

    /// <summary>
    /// Detect triangle from far point.
    /// </summary>
    private void DetecteTriangle()
    {
        if (!_isIncluding)
        {
            FindFarPoint();
        }

        Vector3 a = CurrentPoint;
        Vector3 b = NextPoint;
        Vector3 c = PreviousPoiont;

        Vector3 edge1 = b - a;
        Vector3 edge2 = c - a;

        float angle = Vector3.Angle(edge1, edge2);
        if (angle >= 180)
        {
            Debug.LogError("Something was wrong.");
            return;
        }

        if (IsIncludePoint())
        {
            Debug.Log("Point is including.");

            // try to find other point.
            _isIncluding = true;

            // Store current triangle dicretion.
            _prevDirection = GetCurrentDirection();

            MoveToNext();

            return;
        }

        _isIncluding = false;

        _triangles.Add(_curIndex);
        _triangles.Add(_nextIndex);
        _triangles.Add(_prevIndex);

        bool isDtected = true;
        _verticesBuffer[_curIndex] = isDtected; 
    }

    /// <summary>
    /// Check to include point in the triangle.
    /// </summary>
    /// <returns></returns>
    private bool IsIncludePoint()
    {
        foreach (var key in _verticesBuffer.Keys)
        {
            int index = key;

            if (_verticesBuffer[key])
            {
                continue;
            }

            // skip if index in detected three points.
            if (index == _curIndex || index == _nextIndex || index == _prevIndex)
            {
                continue;
            }

            if (CheckInPoint(_vertices[index]))
            {
                return true;
            }
        }

        return false;
    }

    /// <summary>
    /// Get current triangle direction.
    /// </summary>
    /// <returns>Triagnel direction normal.</returns>
    private Vector3 GetCurrentDirection()
    {
        Vector3 edge1 = (NextPoint - CurrentPoint).normalized;
        Vector3 edge2 = (PreviousPoiont - CurrentPoint).normalized;

        return Vector3.Cross(edge1, edge2);
    }

    /// <summary>
    /// Check including point.
    /// </summary>
    /// <param name="target">Target point.</param>
    /// <returns>return true if point is including.</returns>
    private bool CheckInPoint(Vector3 target)
    {
        // Triangle points.
        Vector3[] tp =
        {
            CurrentPoint,
            NextPoint,
            PreviousPoiont,
        };

        Vector3 prevNormal = default(Vector3);
        for (int i = 0; i < tp.Length; i++)
        {
            Vector3 edge1 = (target - tp[i]);
            Vector3 edge2 = (target - tp[(i + 1) % tp.Length]);

            Vector3 normal = Vector3.Cross(edge1, edge2).normalized;

            if (prevNormal == default(Vector3))
            {
                prevNormal = normal;
                continue;
            }

            // If not same direction, the point out of a triangle.
            if (Vector3.Dot(prevNormal, normal) <= 0.99f)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Poition reference move to next.
    /// </summary>
    private void MoveToNext()
    {
        _curIndex = FindNextIndex(_curIndex);
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 原点から最も遠い点を探す
    /// </summary>
    private void FindFarPoint()
    {
        int farIndex = -1;
        float maxDist = float.MinValue;

        foreach (var key in _verticesBuffer.Keys)
        {
            if (_verticesBuffer[key])
            {
                continue;
            }

            float dist = Vector3.Distance(Vector3.zero, _vertices[key]);
            if (dist > maxDist)
            {
                maxDist = dist;
                farIndex = key;
            }
        }

        _curIndex = farIndex;
        _nextIndex = FindNextIndex(_curIndex);
        _prevIndex = FindPrevIndex(_curIndex);
    }

    /// <summary>
    /// 指定インデックスから調べて次の有効頂点インデックスを探す
    /// </summary>
    private int FindNextIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i + 1) % _vertices.Count;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }

    /// <summary>
    /// 指定インデックスから調べて前の有効頂点インデックスを探す
    /// </summary>
    private int FindPrevIndex(int start)
    {
        int i = start;
        while (true)
        {
            i = (i - 1) >= 0 ? i - 1 : _vertices.Count - 1;
            if (!_verticesBuffer[i])
            {
                return i;
            }
        }
    }
}

クリック位置を点群として採用する

さて、任意の多角形(点群)から三角形(ポリゴン)に分割する処理ができました。
あとはドラッグした位置の点を取り、それを点群としてリスト化することで目的のことが達成できます。

今回はシンプルに以下のように点群を得る処理を書きました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PointDrawer : MonoBehaviour
{
    [SerializeField]
    private DrawMesh _drawMesh;

    [SerializeField]
    private Material _dotMat;

    [SerializeField]
    private float _dotSize = 0.05f;

    [SerializeField]
    private Material _material;

    [SerializeField]
    private float _threshold = 0.1f;

    private float _sqrThreshold = 0;

    private List<Vector3> _samplingVertices = new List<Vector3>();

    private List<GameObject> _dotList = new List<GameObject>();
    private List<Vector3> _vertices = new List<Vector3>();
    private List<GameObject> _meshList = new List<GameObject>();

    /// <summary>
    /// Get average point.
    /// </summary>
    private Vector3 AveragePoint
    {
        get
        {
            Vector3 avg = Vector3.zero;
            for (int i = 0; i < _samplingVertices.Count; i++)
            {
                avg += _samplingVertices[i];
            }
            avg /= _samplingVertices.Count;

            return avg;
        }
    }

    private void Awake()
    {
        _sqrThreshold = _threshold * _threshold;
    }

    private void Update()
    {
        if (Input.GetMouseButton(0))
        {
            TryRaycast();
        }

        if (Input.GetMouseButtonUp(0))
        {
            GameObject go = _drawMesh.CreateMesh(_vertices);
            go.GetComponent<MeshRenderer>().material = _material;
            go.transform.position += go.transform.forward * -0.001f;
            _meshList.Add(go);
        }

        if (Input.GetKeyDown(KeyCode.Q))
        {
            Clear();
        }
    }

    /// <summary>
    /// Try raycast to the plane.
    /// </summary>
    private void TryRaycast()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, float.MaxValue))
        {
            if (_vertices.Count == 0)
            {
                AddVertex(hit.point);
                return;
            }

            if (_samplingVertices.Count == 0)
            {
                _samplingVertices.Add(hit.point);
                return;
            }

            float dist = (AveragePoint - hit.point).sqrMagnitude;
            if (dist >= _sqrThreshold)
            {
                AddVertex(hit.point);
            }
            else
            {
                _samplingVertices.Add(hit.point);
            }
        }
    }

    private void AddVertex(Vector3 point)
    {
        CreateDot(point);
        _vertices.Add(point);
        _samplingVertices.Clear();
    }

    /// <summary>
    /// Create dot for clicked poisition.
    /// </summary>
    /// <returns>Dot GameObject.</returns>
    private GameObject CreateDot(Vector3 position)
    {
        Debug.Log("Create dot.");

        GameObject dot = GameObject.CreatePrimitive(PrimitiveType.Sphere);
        dot.transform.localScale = Vector3.one * _dotSize;
        dot.transform.position = position;
        dot.GetComponent<MeshRenderer>().material = _dotMat;
        Destroy(dot.GetComponent<Collider>());

        _dotList.Add(dot);

        return dot;
    }

    public void Clear()
    {
        for (int i = 0; i < _dotList.Count; i++)
        {
            Destroy(_dotList[i]);
        }
        _dotList.Clear();

        for (int i = 0; i < _meshList.Count; i++)
        {
            Destroy(_meshList[i]);
        }
        _meshList.Clear();
    }
}

実装は大したことしてないですが、ドラッグ中(マウスダウン中)にRaycastを行って点の位置を特定、さらにそれを即座に採用せず、閾値以上動いたらそれを点として採用する、という感じです。

閾値以上移動したかどうかは、現在のマウス位置を毎フレーム取り、それの平均位置から現在のマウス位置との距離で判定しています。

このあたりは、要は点群が得られればいいだけなので如何用にでも実装できるかと思います。

まとめ

点群が用意できれば、それをポリゴンに分解できるので、あとはそれに対してやりたいことを実装すれば完了です。
今回はマスクとして使いたかったので、作ったメッシュにマスク用シェーダ(マテリアル)を割り当てて実際は利用しました。

ちなみに、(たまたまかもしれませんが)多少立体的になってもちゃんとポリゴンが形成されたのでもしかしたら3Dでも応用できるかもしれません。

AndroidのSpeechRecognizerをネイティブプラグイン化してUnityで使う

概要

UnityでAndroidのネイティブな音声認識機能を利用したかったのでプラグインから作成してみました。
今回は作成方法などのまとめです。

なお、ネイティブプラグイン自体の作成方法については以前書いたのでそちらを参照ください。

edom18.hateblo.jp

今回実装したものを実際に動かした動画です↓



音声認識する部分の処理を書く

さて、さっそくプラグイン部分のコードを。
プラグイン自体の作成方法は前回の記事を見ていただくとして、今回はプラグイン部分のみを抜き出しています。

プラグイン用のプロジェクトを作成したら、以下のように音声認識エンジンを起動するクラスを実装します。

package com.edo.speechplugin.recoginizer;

import android.content.Context;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.content.Intent;

import static com.unity3d.player.UnityPlayer.UnitySendMessage;

public class NativeSpeechRecognizer
{
    static public void StartRecognizer(Context context, final String callbackTarget, final String callbackMethod)
    {
        Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, context.getPackageName());

        SpeechRecognizer recognizer = SpeechRecognizer.createSpeechRecognizer(context);
        recognizer.setRecognitionListener(new RecognitionListener()
        {
            @Override
            public void onReadyForSpeech(Bundle params)
            {
                // On Ready for speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onReadyForSpeech");
            }

            @Override
            public void onBeginningOfSpeech()
            {
                // On begining of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "OnBeginningOfSpheech");
            }

            @Override
            public void onRmsChanged(float rmsdB)
            {
                // On Rms changed.
                UnitySendMessage(callbackTarget, callbackMethod, "onRmsChanged");
            }

            @Override
            public void onBufferReceived(byte[] buffer)
            {
                // On buffer received.
                UnitySendMessage(callbackTarget, callbackMethod, "onBufferReceived");
            }

            @Override
            public void onEndOfSpeech()
            {
                // On end of speech.
                UnitySendMessage(callbackTarget, callbackMethod, "onEndOfSpeech");
            }

            @Override
            public void onError(int error)
            {
                // On error.
                UnitySendMessage(callbackTarget, callbackMethod, "onError");
            }

            @Override
            public void onResults(Bundle results)
            {
                // On results.
                UnitySendMessage(callbackTarget, callbackMethod, "onResults");
            }

            @Override
            public void onPartialResults(Bundle partialResults)
            {
                // On partial results.
                UnitySendMessage(callbackTarget, callbackMethod, "onPartialResults");
            }

            @Override
            public void onEvent(int eventType, Bundle params)
            {
                // On event.
                UnitySendMessage(callbackTarget, callbackMethod, "onEvent");
            }
        });

        recognizer.startListening(intent);
    }
}

上記はリスナーの登録の雛形です。
Unityの機能であるUnitySendMessageを使っていますが詳細は後述します。

SpeechRecognizer#setRecognitionListenerでリスナを登録し、SpeechRecognizer#startListening音声認識エンジンを起動します。

認識した文字列を受け取る

認識した文字列を受け取る箇所については認識した際のコールバックで文字列を取り出します。

@Override
public void onResults(Bundle results)
{
    ArrayList<String> list = results.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
    String str = "";
    for (String s : list)
    {
        if (str.length() > 0)
        {
            str += "\n";
        }
        str += s;
    }

    UnitySendMessage(callbackTarget, callbackMethod, "onResults\n" + str);
}

基本的にプラグインとの値の受け渡しは文字列で行うのが通常のようです。
あとはUnity側で文字列を受け取り、適切に分解して利用することで無事、Unity上で音声認識を利用することができるようになります。

Unityの機能を利用できるようにclasses.jarをimportする

音声認識したあと、それをコールバックするためUnity側にメッセージを送信する必要があります。
その際、Unity側の実装を呼び出す必要があるため、それを利用するためにclasses.jarをimportしておく必要があります。

import先はモジュールのlibsフォルダ内です。

なお、該当のファイルは以下の場所にあります。(環境によってパスは読み替えてください)

D:\Program Files\Unity2017.3.1p4\Editor\Data\PlaybackEngines\AndroidPlayer\Variations\mono\Release\Classes\classes.jar

Gradle設定にclasses.jarを含めないよう追記する

classes.jarは、UnitySendMessageを使うのに必要ですが、aarに含まれてしまうとUnityでのビルド時にエラーが出てしまうため、aarに含めないよう設定ファイルに記述する必要があります。

以下を、モジュール用のbuild.gradleに追記します。

android.libraryVariants.all{ variant->
  variant.outputs.each{output->
    output.packageLibrary.exclude('libs/classes.jar')
  }
}

全体としては以下のようになります。

plugins {
    id 'com.android.library'
}

android {
    namespace 'com.edo.speechrecognizer'
    compileSdk 32

    defaultConfig {
        minSdk 23
        targetSdk 32

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        consumerProguardFiles "consumer-rules.pro"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

}

android.libraryVariants.all{ variant->
    variant.outputs.each{output->
        output.packageLibrary.exclude('libs/classes.jar')
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.5.1'
    implementation 'com.google.android.material:material:1.7.0'
    implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

設定ファイルはこれです。

ちなみに設定しないと、

com.android.build.api.transform.TrasnformException: com.android.idle.common.process.ProcessException: java.util.concurrent.ExecutionException: com.android.dex.DexException: Multiple dex files define Lbitter/jnibridge/JNIBridge$a;

みたいなエラーが出ます。

[2023.03.04 追記]

どうやら上の記述だとビルド時に含まれてしまうようになったっぽいです。
ので、依存関係の解決を以下のように変更します。( compileOnly に変更する)

- implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
+ compileOnly fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])

AndroidManifestに録音権限を追記する

当然ですが、音声認識を利用するためにはマイクからの音を利用する必要があるため、AndroidManifestに下記の権限を追記する必要があります。

<uses-permission android:name="android.permission.RECORD_AUDIO" />

Unity側から呼び出す処理を実装する

プラグインが作成できたらそれを利用するためのC#側の実装を行います。
以下のようにしてプラグイン側の処理を呼び出します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class AndroidSpeechRecognizer : MonoBehaviour
{
    [SerializeField]
    private Text _text;

    private void Update()
    {
        if (Input.touchCount > 0)
        {
            Touch touch = Input.touches[0];
            if (touch.phase == TouchPhase.Began)
            {
                StartRecognizer();
            }
        }
    }

    private void StartRecognizer()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeRecognizer = new AndroidJavaClass("com.edo.speechplugin.recoginizer.NativeSpeechRecognizer");
        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeRecognizer.CallStatic(
                "StartRecognizer",
                context,
                gameObject.name,
                "CallbackMethod"
            );
        }));
#endif
    }

    private void CallbackMethod(string message)
    {
        string[] messages = message.Split('\n');
        if (messages[0] == "onResults")
        {
            string msg = "";
            for (int i = 1; i < messages.Length; i++)
            {
                msg += messages[i] + "\n";
            }

            _text.text = msg;
            Debug.Log(msg);
        }
        else
        {
            Debug.Log(message);
        }
    }
}

ネイティブプラグイン側で音声認識の結果をUnitySendMessageによってコールバックするようになっているので、それを受け取るコールバック用メソッドを実装しています。

プラグイン側では文字列でメソッド名を指定してコールバックを実行するので、プラグイン側にメソッド名を適切に渡す必要があります。

あとは認識した文字列を元に、行いたい処理を実装すれば音声認識を利用したアプリを制作することができます。

トラブルシューティング

もし、プラグインは実行できるのにすぐにエラーで停止してしまう場合は、マイクの権限が付与されていない可能性があるのでアプリの設定を見直してみてください。

参考記事

今回のプラグイン作成には以下の記事を参考にさせていただきました。

indie-du.com

fantom1x.blog130.fc2.com

fantom1x.blog130.fc2.com

Unity向けにAndroidのネイティブプラグインを作成する

概要

Unity向けに、Androidのネイティブ機能を呼び出す部分が作りたくて色々調べたのでまとめておきます。

今回は特にこちらの記事を参考にさせていただきました。

indie-du.com

Android Studioで新規プロジェクトを作成する

今回作成するのはaar(Android Archive)なので、新規で作成するプロジェクトは空の、ごくシンプルな状態で作成して問題ありません。(プロジェクト作成後に、モジュール追加してそっちでコードを書くので、通常のapk作成フローとは異なります)

f:id:edo_m18:20180320163214p:plain

次に、Phone and Tabletを選択し、Minimum SDKのバージョンを設定します。
ここは、後々、Unityの設定でも最低バージョンを指定する際に同じバージョンにしないとエラーが出るので、それなりに小さいのを選んでおいたほうがいいでしょう。

f:id:edo_m18:20180320163359p:plain

そしてAdd No Activityを選択してプロジェクトを作成します。

f:id:edo_m18:20180320163520p:plain

ライブラリ用モジュールを作成する

プロジェクトが作成できたら、ライブラリ用のモジュールを新規追加します。

f:id:edo_m18:20180320163800p:plain

Android Libraryを選択して追加します。

f:id:edo_m18:20180320163843p:plain

プラグイン名となるモジュール名を入力します。

f:id:edo_m18:20180320164033p:plain

すると、プロジェクトビューに、先ほど入力した名前でモジュールが追加されます。

f:id:edo_m18:20180320164210p:plain

Unityからアクセスするクラスを実装する

今回はネイティブの機能(ダイアログ)を簡単に呼び出すだけなので、なにも継承しないシンプルなJavaクラスをモジュールに追加します。
追加するには、Androidビュー内のモジュール名上で右クリックしてNew > Java Classを選択します。

f:id:edo_m18:20180320164823p:plain

そして、NativeDialogというクラス名でファイルを作成し、参考にさせていただいた記事同様の記述を行います。

引用させてもらうと、以下のコードになります。ネイティブのダイアログを表示する、というものです。
パッケージ名は自身の環境に合わせてください

package jp.co.test.dialog_plugin;

import android.app.AlertDialog;
import android.content.Context;

public class NativeDialog {

    static public void showMessage(Context context, String title, String message) {

        new AlertDialog.Builder(context)
                .setTitle(title)
                .setMessage(message)
                .setPositiveButton("YES, YES, YES!", null)
                .setNegativeButton("...No?", null)
                .show();
    }
}

Gradleを使ってビルドする

ファイルが追加されたら、Gradleを使ってビルドします。

Android Studioの右側にある「Gradle」タブを開き、:plugin-name(名称は自身で設定したもの)の下にある「Tasks > build > assemble」をダブルクリックして実行します。

f:id:edo_m18:20180320170330p:plain

基本はこれでビルドされるはずですが、自分の環境ではちょっとエラーで躓いてしまったので、同様のエラーが出た場合は、後述の対策を参照してみてください。

エラー内容

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

ビルドが成功すると、プロジェクトフォルダ内(※)の「myplugin/build/outputs/aar」に、ビルドされたaarファイルが作成されています。

Android Studioのプロジェクトビューはいくつかのモードがあり、デフォルトはAndroidになっているので、これをProjectに変更するとフォルダ構造が見れるようになるので、そちらのモードにすると表示されるようになります。

f:id:edo_m18:20180320185855p:plain

上記dialog-pluginというモジュール名で作成した時のキャプチャです。
****-debug.aar****-release.aarが作成されます。今回はdebugのほうをimportしました。

これを、Unityのプラグインフォルダにコピーして利用します。

f:id:edo_m18:20180320170912p:plain

Unityから、ネイティブプラグインの機能を呼び出す

プラグイン部分が作成できたので、あとはこれをC#から読み込み、利用するコードを書きます。
以下のように、AndroidJavaClassAndroidJavaObjectを利用して構築します。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AndroidDialogTest : MonoBehaviour
{
    private void Update()
    {
        if (Input.touchCount > 0)
        {
            ShowDialog();
        }
    }

    private void ShowDialog()
    {
#if UNITY_ANDROID
        AndroidJavaClass nativeDialog = new AndroidJavaClass("plugintest.edo.com.dialog_plugin.NativeDialog");

        AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
        AndroidJavaObject context = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");

        context.Call("runOnUiThread", new AndroidJavaRunnable(() =>
        {
            nativeDialog.CallStatic(
                "showMessage",
                context,
                "テスト",
                "ほげ"
            );
        }));
#endif
    }
}

Android Studioのビルドでハマった

Android開発をしている人にとっては多分なんてことはないことなんだと思いますが、Android Studioを使ったことがほぼないので、細かなエラーに悩まされました。

こちらの記事を参考に、ごく簡単なプラグイン作成を試してみたところ、

indie-du.com

以下のようなエラーが。

$ Error:A problem occurred configuring project ':hoge-plugin'.
> Could not resolve all dependencies for configuration ':hoge-plugin:_debugPublishCopy'.
   > Could not find any version that matches com.android.support:appcompat-v7:27.+.
     Versions that do not match:
         26.0.0-alpha1
         25.3.1
         25.3.0
         25.2.0
         25.1.1
         + 31 more
     Required by:
         project :dialog-plugin

どうも調べていくと、Android Support Libraryを最新にしろ、ということのよう。
ただ、調べてもSDK Managerでチェック入れてインストール、みたいなのしか出てこないのに、利用しているAndroid Studioだとそもそもそのチェック項目がない。なんでやねん。

と思っていたら、こちらにやり方が書いてありました。

qiita.com

要は、build.gradleに依存関係を書け、ということのよう。

以下のように追記しました。

allprojects {
  repositories {
    jcenter()
    maven { url 'https://maven.google.com' } // 変更点
  }
}

こちらも参考にさせていただきました。

animane.hatenablog.com

gradle

ちなみに余談ですが、最初、gradle自体が分かっていなかったんですが、ビルド自動化ツールでした。

GradleはApache AntやApache Mavenのコンセプトに基づくオープンソースビルド自動化システムであり、プロジェクト設定の宣言にはApache Mavenが利用するXML形式ではなくGroovyベースのドメイン固有言語 (DSL) を採用している[2]。Gradleはタスクの起動順序の決定に有向非巡回グラフ(英: Directed Acyclic Graph、DAG)を利用する。

[出典: wikipedia]

Android Studioではこれを取り入れていて、専用のタブやビューが存在します。
build.gradleはテキストファイルで、自動ビルドツール用の設定ファイルです。
(なので、プロジェクト内のどこかに存在しているので、それを編集します)

まとめ

以上で、ネイティブのダイアログを表示するだけのプラグインが作成できました。
最後に、全体の流れをまとめとして書いておきます。

  1. Android Studioで空プロジェクトを作る
  2. 作成したプロジェクトにモジュールを新規追加する
  3. モジュールに、ネイティブ機能を実行する処理を追加・実装する
  4. Gradleを使ってモジュールをビルドする
  5. 生成されたaarファイルをUnityにimportする
  6. Unityから、aar内の実装を呼び出す処理を書く

大まかにはこんな流れで実装していきます。
ビルドエラー周りが少々厄介ですが、それをクリアしてしまえば、比較的ネイティブの機能を実行するのは容易になるかなと思います。

Unreal Engine C++ 逆引きメモ

目次

概要

まだまだUEに慣れていないので、色々なことを忘れる前に逆引きでメモしておきます。
(なので、随時更新予定)

また、UEでC++を書くにあたって理解しておかなければならない点として、標準のC++とは「異なる」という点です。
どういうことかと言うと、UEではガベージコレクション、通称GCと呼ばれる仕組みを導入しています。

しかし、当然ながらC++にはガベージコレクションはありません。
そこでUEでは、独自のインスンタンス生成の仕組みや、マクロを用いたプリプロセッサ経由で様々な、GCのための準備をしてくれます。

そのため、標準のC++とUEで使うC++、言ってみればUE C++とで作法が違う、という点を覚えておく必要があります。

生成・取得・削除

C++クラスの生成

通常のC++では、newを用いてインスタンスを生成します。
しかし、UE C++ではインスタンスの生成方法に違いがあります。理由は前述のように、GC対象として管理するためです。
最近ではスマートポインタを使ったりしますが、それと似た感じですね。

UMyClass MyClass = NewObject<UMyClass>();

// オーナーを指定する場合は引数に入れる
// UMyClass MyClass = NewObject<UMyClass>(Owner);

当然のことながら、UE管理下に置かないような純粋なC++で書く処理についてはこの作法は適用されません。

コンストラクタ内でNewObjectは使えない

どうやら、コンストラクタ内では上記のNewObjectは使えないようです。(使うとクラッシュする)

ではどうするかというと、FObjectInitializer::CreateDefaultSubobjectを利用します。
FObjectInitializerは、コンストラクタ引数に指定しておくと、UEシステムが適切に渡してくれるようになっています。(詳細は後述)

以下のようにコンストラクタを定義することで利用できるようになります。

UAnyClass::UAnyClass(const class FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
    // オブジェクト生成
    UAthorClass* instance = ObjectInitializer.CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

しかし実はこのCreateDefaultSubobjectは、UObject基底クラスでラッパーが実装されているようで、オブジェクトを生成する目的だけであれば、CreateDefaultSubobjectを直に呼ぶことで同様のことを達成することができます。

UAnyClass::UAnyClass()
{
    // オブジェクト生成
    UAthorClass* instance = CreateDefaultSubobject<UAthorClass>(this, TEXT("hoge"));
}

なおこのコンストラクタに引数を指定した場合としない場合の挙動の差ですが、UEシステムが自動的に生成する****.generated.h内にてマクロが生成され、コンストラクタの定義に応じて書き換わるよういなっているようです。

詳細はこちら([UE4] ObjectInitializerでコンポーネント生成を制御する | 株式会社ヒストリア)の記事をご覧ください。

PlayerController / PlayerPawnを取得する

ゲーム開始時に生成されたプレイヤーコントローラ / プレイヤーポーンを取得するには、Kismet/GameplayStatics.hを読み込む必要があります。

PlayerController

#include "Kismet/GameplayStatics.h"

APlayerController* PlayerController = UGameplayStatics::GetPlayerController(this, 0);
if (PlayerController)
{
    // do anything.
}

PlayerPawn

#include "Kismet/GameplayStatics.h"

// UGameplayStatics::GetPlayerPawnを介して取得し、適切にキャストする
AAnyCharacter* MyCharacter = Cast<AAnyCharacter>(UGameplayStatics::GetPlayerPawn(this, 0));
if (MyCharacter)
{
    // do anything.
}

GameModeとの関連

なお、このPlayerControllerPlayerPawnの関係は、PawnをコントロールするのがPlayerControllerの役割です。
これらの設定はGameModeに設定するようになっており、またGameModeはプロジェクト設定にて設定され、これが実行時に起動するポイントとなるようです。

詳細はこちら↓

msyasuda.hatenablog.com

ワールドに存在するアクターをすべて取得する

#include "Kismet/GameplayStatics.h"

// find all AnyActors
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(GetWorld(), AAnyActor::StaticClass(), FoundActors);

for (auto Actor : FoundActors)
{
    AAnyActor* AnyActor = Cast<AAnyActor>(Actor);
    if (AnyActor )
    {
        // do anything.
    }
}

アクターオブジェクトを生成する

オブジェクトの生成には、UWorldクラスのメソッドを利用します。

// FActorSpawnParametersを使うのに必要
#include "Runtime/Engine/Classes/Engine/World.h"

// AActor::GetWorldから、UWorldを得る
UWorld* const World = GetWorld();

// Nullチェック
if (!World)
{
    return;
}

FVector Location(0.0f, 0.0f, 0.0f);
FRotator Rotator(0.0f, 0.0f, 0.0f);
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.Instigator = Instigator;

AAnyActor* AnyActor = World->SpawnActor<AAnyActor>(ActorBP, Location, Rotation, SpawnParams);

削除中かを知る

まだ理解が浅いですが、Unityと同様、Destroyを実行しても即座にメモリから消えるわけではなく、ゲームとして破綻しないよう様々な終了処理が存在するはずです。
そのため、削除中、という状態が存在します。
それをチェックするには以下のようにします。

AActor* anyActor = ...;
anyActor->IsPendingKill();

移動

アクターを移動させる

AActorクラスにはGetActorLocationSetActorLocationがあるのでこれを利用する。

FVector location = actor->GetActorLocation();
location.X += 10.0f;
actor->SetActorLocation(location);

ちなみに、SetActorLocationの定義を見ると以下のようになっています。

/** 
 * Move the actor instantly to the specified location. 
 * 
 * @param NewLocation  The new location to teleport the Actor to.
 * @param bSweep       Whether we sweep to the destination location, triggering overlaps along the way and stopping short of the target if blocked by something.
 *                     Only the root component is swept and checked for blocking collision, child components move without sweeping. If collision is off, this has no effect.
 * @param Teleport     How we teleport the physics state (if physics collision is enabled for this object).
 *                     If equal to ETeleportType::TeleportPhysics, physics velocity for this object is unchanged (so ragdoll parts are not affected by change in location).
 *                     If equal to ETeleportType::None, physics velocity is updated based on the change in position (affecting ragdoll parts).
 *                     If CCD is on and not teleporting, this will affect objects along the entire swept volume.
 * @param OutSweepHitResult The hit result from the move if swept.
 * @return Whether the location was successfully set if not swept, or whether movement occurred if swept.
 */
bool SetActorLocation(const FVector& NewLocation, bool bSweep=false, FHitResult* OutSweepHitResult=nullptr, ETeleportType Teleport = ETeleportType::None);

引数を色々変更することによって、移動後の物理干渉などに対する設定や演算結果を受け取ることができるようです。

カメラの向いている方向に移動させる

まず、Worldからカメラマネージャを取得し、そこからカメラの前方を取得、それを元にアクターを移動させる、という手順で行います。

#include "Engine.h" // GEngineを使うのでインクルードしておく

void AMyActor::BeginPlay()
{
    if (GEngine != nullptr)
    {
        // CameraManagerをワールドから取得する
        CameraManager = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager;
    }
}

void AMyActor::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    FRotator cameraRot = CameraManager->GetCameraRotation();
    FVector dir = cameraRot.Vector();
    dir.Normalize();

    FVector location = GetActorLocation();
    location += dir * 10.0f * DeltaTime;
    
    SetActorLocation(location);
}

コンポーネント

Blueprintに、カスタムコンポーネントを表示させる

UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))
class PROJECTNAME_API UHogeMovementComponent : public UPawnMovementComponent
{
    // ... 略 ...
}

こんな感じで、UCLASS(Blueprintable, meta = (BlueprintSpawnableComponent))を指定すると、BPのAddComponentのリストに表示されるようになります。

コンポーネントのセットアップ

コンポーネントを適切にセットアップしないと、TickComponentが呼ばれないなど問題が出るため、適切にセットアップする必要があります。

void USampleComponent::AttachSampleComponent(AActor* Actor)
{
    if (!Actor)
    {
        return;
    }

    // Componentを新規生成
    UOtherComponent* Comp = NewObject<UOtherComponent>(Actor);

    // アクタにアタッチする
    Actor->AddInstanceComponent(Comp);

    // UActorComponent::RegisterComponentで、イベントループに登録する
    Comp->RegisterComponent();
}

追加されているコンポーネントを取得する

TArray<AnyComponent*> components;
GetComponents<AnyComponent>(components);

// コンポーネントの数を確認
UE_LOG(LogTemp, Log, TEXT("Count: %d"), components.Num());

Ownerを取得する

ComponentはActorのコンポーネントとして振る舞うため、オーナーを取得して操作することが増えるかと思います。
オーナーの取得は以下のようにします。

AActor *owner = GetOwner();

ComponentからInputを使う

InputまわりはUInputComponentが司ります。
UActorComponentではUInputComponentを保持していないので、オーナーなどから取得して適切にセットアップする必要があります。

PawnクラスのサブクラスなどではSetupPlayerInputComponentのタイミングでUInputComponentが渡ってくるので、そこでセットアップの機会があるようです)

https://docs.unrealengine.com/latest/INT/Programming/Tutorials/PlayerCamera/3/docs.unrealengine.com

AActor *actor = GetOwner();
actor->InputComponent->BindAction("Fire", IE_Pressed, this, &UAnyComponent::FireHandler);

void UAnyComponent::FireHandler()
{
    UE_LOG(LogTemp, Log, TEXT("Fire!!!");
}

※ Input系については、UE4のお作法に則って適切にセットアップする必要があります。

セットアップについては、以下の引越しガイドの「入力イベント」あたりに載っています。

docs.unrealengine.com

Static Meshを生成する

USceneComponent *root = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
UStaticMeshComponent *mesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("mesh"));
mesh->SetupAttachment(root);

static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("[reference]"));
UStaticMesh *asset = MeshAsset.Object;

mesh->SetStaticMesh(asset);

ちなみに、[reference]の部分は、UEエディタのアセットのコンテキストメニュー内のCopy Referenceから得ることができます。

f:id:edo_m18:20180208102926p:plain

参考:
forums.unrealengine.com

Colliderを設定する

コライダの設定にはUSphereComponentなどを利用します。
詳細パネルではSphere Collisionとか表示されるやつです。

AMyPawn::AMyPawn()
{
    // ... 中略 ...

    // Sphereコライダを生成
    USphereComponent *sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
    sphere->InitSphereRadius(100.0f);
    sphere->SetSimulatePhysics(false);
    sphere->SetCollisionProfileName(TEXT("BlockAll"));

    // Sphereの見た目を生成
    UStaticMeshComponent *sphereVisual = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("SphereVisual"));
    sphereVisual->AttachTo(sphere);

    static ConstructorHelpers::FObjectFinder<UStaticMesh> meshAsset(TEXT("/Game/StarterContent/Shapes/Shape_Sphere.Shape_Sphere"));
    if (meshAsset.Succeeded())
    {
        sphereVisual->SetStaticMesh(meshAsset.Object);
        sphereVisual->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
        sphereVisual->SetWorldScale3D(FVector(0.8f));
    }
}

セットアップ時に行っているのは3点。

  • sphere->InitSphereRadius(100.0f);
  • sphere->SetSimulatePhysics(false);
  • sphere->SetCollisionProfileName(TEXT("BlockAll"));

です。それぞれ上から、

  • 球体の半径の設定
  • 物理挙動させるか(falseの場合は、Unityで言うIsKinematic = trueの状態)
  • コリジョンの仕方

となります。
特に最後の「コリジョンの仕方」は、衝突するのか、イベントのみ(overlap)なのか、というところ設定するので、意図した通りに設定しておかないと「衝突しないじゃん」となるので注意です。(というか、ここを勘違いしていてずっと衝突しなくてちょっとハマった)

コリジョンを処理する

コリジョンを設定したあとは、コリジョン発生時になにかしらの処理をしたい場合があります。
その場合に使えるのが、ColliderのもつGetOverlappingActorsメソッドです。

コライダの範囲にあるAActorオブジェクトを取得してくれます。

※ 以下のコードは、公式のチュートリアルの中で使われているコードの抜粋です。

void ABatteryCollectorCharacter::CollectPickups()
{
    // Get all overlapping Actors and store them in an array
    TArray<AActor*> CollectedActors;
    CollectionSphere->GetOverlappingActors(CollectedActors);

    // For each Actor we collected
    for (int32 iCollected = 0; iCollected < CollectedActors.Num(); ++iCollected)
    {
        // Cast the actor to APickup
        APickup* const TestPickup = Cast<APickup>(CollectedActors[iCollected]);

        // If the cast is successful and the pickup is valid and active
        if (TestPickup && !TestPickup->IsPendingKill() && TestPickup->IsActive())
        {
            // call the pickup's WasCollected function
            TestPickup->WasCollected();

            // Deactivate the pickup 
            TestPickup->SetActive(false);
        }
    }
}

Blueprintで利用する

C++で書いたクラス(コンポーネント)も、ブループリントから利用するように作成することが出来ます。
そのためのマクロがUPROPERTYUFUNCTIONです。
これらを適切に設定することで、ブループリントから設定できたり、あるいはGetter / Setterとして機能したり、あるいはブループリントで実装を促す、なんてこともできるようになります。

以下に、キーワードと意味を、よく見るものを抜粋して記載しておきます。
※ 英語ドキュメントの翻訳(意訳)なので、詳細についてはドキュメントをご覧ください。

Keyword 意味
BlueprintImplementableEvent ブループリント(ノード)でオーバーライドするように促す。そのため、Body(実装)部分は書いてはならない。UEにより、(ブループリントで)オーバーライドされた本体を実行するProcessEventを呼び出すためのコードが自動生成される
BlueprintNativeEvent BlueprintImplementableEvent同様、ブループリントでオーバーライドするようデザインされたものであるが、違いとしてはC++による実装を行う点。実装本体は[FunctionName]_Implementationを実装する必要がある。元の[FunctionName]内には、自動生成コードとして、[FunctionName]_Implementationを呼び出すコードが追加される(※1)
BlueprintPure このキーワードをつけられた関数は、副作用を起こさないものとしてマーク付けられ、さらにBlueprintCallableを意味します。Getメソッドの実装に適したものです。さらに、non-pureになるようにconst functionでfalseとマークすることもできる(※2)
BlueprintCallable Blueprintから呼び出しできるようにマークする
Category カテゴリを定義する。設定すると、Blueprint上でカテゴライズされて表示される

※1 [FunctionName]\_Implementationを実装しないとコンパイルエラーになる。定義すると、BPのEvent Graphで配置して利用できるようになる。以下、サンプル↓

UFUNCTION(BlueprintNativeEvent)
void HogeHoge();

virtual void HogeHoge_Implementation();

f:id:edo_m18:20180225214321p:plain

※2 PureとNon-Pureについて書かれている記事があったので、詳細はこちらをご覧ください→ [UE4] Pure関数とNonPure関数|株式会社ヒストリア

ドキュメント:UFUNCTION - Unreal Engine Wiki

Blueprintからプロパティに値を設定できるようにする

コンポーネントを作成し配置しても、適切にマクロを設定しておかないとBPからプロパティに対して値を設定することができません。

以下のように設定することでそれが可能となります。

UPROPERTY(BlueprintReadWrite, meta = (ExposeOnSpawn)

ドキュメント: UPROPERTY - Epic Wiki


その他

enumを定義する

l参考にしていたVideo Tutorialは若干古いバージョンだったため、その中で説明されていたenumだとエラーが出てコンパイルができませんでした。
ということで、いちおうメモ。

UENUM(BlueprintType)
enum class EHogeEnum : uint8
{
    EFuga,
    EFoo,
    EBar,
};

と、uint8を継承した形で宣言しないとなりません。

タイマーを利用する

タイマーの利用には、FTimerManagerクラスを利用します。
以下の例では、自身を再帰的にタイマーで呼び出します。

// header
FTimeHandle Timer;
void AAnyActor::TimerHandler();

// implementation
void AAnyActor::TimerHandler()
{
    // &AActor::GetWorldTimerManager
    float Delay = FMath::FRandRange(1.0f, 2.0f);
    GetWorldTimerManager().SetTimer(Timer, this, &AAnyActor::TimerHandler, Delay, false);
}

ドキュメントはこちら

docs.unrealengine.com


トラブルシューティング

TickComponentが呼ばれなくなった!

C++を書いていて、突然、少し前まで正常に動いていたTickComponentが動かなくなるケースがありました。

色々調べてみても、必要なフラグの扱いやらメソッドの定義やらは正常に行っている・・でも動かない。

最終的に解決したのは、「該当のComponentを一度消し、追加し直す」ことで解消しました。
そのあたりが書かれていた記事がこちら↓

community.gamedev.tv

まさか入れ直しだけで解決するとは・・。
おそらくですが、(社内のエンジンに詳しい人と話していて聞いたのは)Hot Reloadの機能がUE4には備わっていて、それの関連付けなどがおかしくなってしまったのでは、とのこと。
多分、その関連付け周りの処理が、コンポーネントの追加・削除のタイミングで行われているのでしょう。

なので、追加し直しで直ったのではないかなと。

ちなみに、TickComponentを呼ぶ必要があるかどうか、みたいなフラグ周りについては以下の記事が色々まとめてくれているので参考にしてみてください。

usagi.hatenablog.jp

case内の初期化

これはUEというよりC++の問題ですが、switch文内で初期化を伴う処理を書いている場合、case文を{}で囲まないとコンパイルエラーとなるようです。


その他Tips

色々な値をログ出力

Log Fomatting

  • LogMessage
UE_LOG(LogTemp, Log, TEXT("Hoge"));
  • Log an FString
FString anyString = ...;
UE_LOG(LogTemp, Log, TEXT("Log: %s"), *anyString);
  • Log an Bool
bool anyBool = ...;
UE_LOG(LogTemp, Log, TEXT(Bool value: %s"), anyBool ? TEXT("True") : TEXT("False")); 
  • Log an Int
int anyInt = ...;
UE_LOG(LogTemp, Log, TEXT("Int value: %d"), anyInt);
  • Log a Float
float anyFloat = ...;
UE_LOG(LogTemp, Log, TEXT("Float value: %f"), anyFloat);
  • Log an FVector
FVector anyVector = ...;
UE_LOG(LogTemp, Log, TEXT("FVector value: %s"), *anyVector.ToString());
  • Log an FName
FName anyName = ...;
UE_LOG(LogTemp, Log, TEXT("FName value: %s"), *anyName.ToString());

ドキュメントはこちら → Logs, Printing Messages To Yourself During Runtime

Tickメソッド外で時間を扱う

Tickメソッド内では、引数にDeltaTimeが渡ってくるのでそれを利用すればいいですが、それ以外の場合はワールドから取得する必要があります。

void AnyMethod()
{
    float time = GetWorld()->GetTimeSeconds();
    float deltaTime = GetWorld()->GetDeltaSeconds();

    UE_LOG(LogTemp, Log, TEXT("Time: %f, DeltaTime: %f"), time, deltaTime);
}

番外編

ちょっと昔に書いた記事ですが、Cocos2D-xでアプリを作る際にまとめた、C++関連の記事です。
UEとは関係ない部分もありますが、C++的なところは同じなので紹介。

qiita.com

UnityのネイティブプラグインをC/C++で作成する準備

概要

ネイティブで書かれたプラグインを、使うことはあっても自分で書いたことがなかったのでHello Worldしてみたメモです。

ちなみに、できるだけ動作を把握する意味も込めてコマンドラインで作ることを前提としています。

開発環境を整える

最初、Git Bashを使っていたのでそこにgccコマンドが入っていてそれを利用していたのですが、ごく簡単な関数を書いてインポートしたところエラーが。
(と思ったら、会社のPCでの環境だった。いつgcc入れたっけな・・。デフォルトでは入ってませんでした)

$ Failed to load 'Assets/Plugins/************.dll', expected 64 bit architecture 

64bitを期待してるけど、32bit向けに作られたものですよ、ということ。
ならばと、64bit向けにコンパイルすればいいのね、はいはい、と思いつつ、-m64オプションを付けて実行するも・・・

$ sorry, unimplemented: 64-bit mode not compiled in

MINGW64って書いてあるのに、なんでさ。

その後、色々調べてみたら、MINGW64自体を使うこと自体は間違っていない模様。
64bit版で書き出せるコマンドがあるらしく探してみるものの、どうもGit Bashはビルドが違うのか、該当のコマンドが見当たりませんでした。

なので、別途新しくMINGW64をインストール。
このへん(Windowsの無料で使える 64bit/32bit C/C++コンパイラ)を参考にしました。

x86_64向けにコンパイルするには、以下の位置にあるコマンドを利用します。

path/to/location/mingw-w64/x86_64-7.2.0-win32-seh-rt_v5-rev1/mingw64/bin/x86_64-w64-mingw32-gcc

コンパイルする

これに、-m64オプションを付けてコンパイルしたところ、無事に64bit向けにコンパイルすることができました。

$ x86_64-w64-mingw32-gcc -m64 -c anyplugin.c

そして、生成されたオブジェクトファイル(.o)を、DLLに変換します。

$  x86_64-w64-mingw32-gcc -shared -o anyplugin.dll anyplugin.o

これで無事にDLLファイルが作成されます。

※ Pathを通して↑のコマンドを使ってます。

コンパイルされたファイルのアーキテクチャを確認する

ちなみに、すでにコンパイルされているdllやオブジェクトファイルがどのアーキテクチャ向けにビルドされているかを確認するには、Visual Studioに同梱されているdumpbin.exeを利用すると分かるようです。

該当のexeは以下の場所らへんにあります。(インストール環境による)

C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\bin

このあたりの詳細は以下を参照ください。

rms-099.hatenablog.jp

d.hatena.ne.jp

そして、チェックしたいファイルを引数に以下のように実行します。

$ dumpbin /headers anyplugin.o

すると、ダーっと色々な情報が表示されますが、先頭のほうにある、

// ... 略 ...

FILE HEADER VALUES
            8664 machine (x64)

// ... 略 ...

を見てみると、x64と書かれているので、64bit向けにコンパイルできていることが確認できます。

Cで書いたDLLを読み込んでUnityで実行する

さて、これでDLLを作成する準備が整いました。
最後に、ごく簡単なサンプルを書いて終わりにしたいと思います。

Cで処理を書く

今回はサンプルなので、Cで簡単な処理を書いてみます。

// plugin.c
int Add(int a, int b)
{
    return a + b;
}

さて、これをUnityのC#側で利用できるようにします。
前述のようにコンパイルを行い、DLLファイルを作成します。

そしてそれを、Assets/Pluginsフォルダにコピーし、以下のようにC#側で呼び出します。

// ... 略 ...

using System.Runtime.InteropServices; // <- DllImportを使うために追加

public class AnyClass : MonoBehaviour
{
    private void Start()
    {
        int test = Add(1, 2);
        Debug.Log(test); // -> 3
    }


    // プラグインのファイル名を指定する
    [DllImport("plugin")
    private static extern int Add(int a, int b);
}

橋渡しができたら、あとは実際の処理を書いていくことでネイティブ側のコードを実行することができるようになります。

参考

なお、共有ライブラリのコンパイル周りについては、過去にQiita記事に書いているので、よかったら見てみてください。

qiita.com