20201010のUnityに関する記事は10件です。

【Unity】オブジェクトをなめらかに移動させる【コルーチン】

オブジェクトをなめらかに動かしたい。
でも、Update()の中に書くのは邪魔だし、他のスクリプトから移動を開始させたいときはコルーチンを使う。

やってること

動かしたいオブジェクトにアタッチしているスクリプトにコルーチンの処理を書く。
コルーチン内には1フレームずつの繰り返し処理を書き、Lerpで目的地までの移動をなめらかにする。
コルーチンを別スクリプトから起動する。

【環境】

・Mac OSX El Capitan
・Unity versiton:2018.3.0

【スクリプト】

Move.cs
using System.Collections;
using UnityEngine;

public class Move : MonoBehaviour
{
    public float speed;
    public Coroutine myCor;


    void Update()
    {
        //このスクリプト内でコルーチンをスタートさせる場合

        //if (Input.GetKeyDown(KeyCode.Space)) {
        //    myCor = StartCoroutine(MoveTo(goal));
        //}else if(Input.GetKeyDown(KeyCode.Backspace)){
        //   StopCoroutine(myCor);
        //}
    }

    //MoveToをスタートさせるメソッド
    //外部からコルーチンを呼び出すときはこのメソッドを使う
    public void StartCor(Vector3 goal)
    {
        if (myCor != null)
        {
            StopCoroutine(myCor);//StartCoroutine()する前に停止させて、重複して実行されないようにする。
        }
        myCor = StartCoroutine(MoveTo(goal));
    }

    //goalの位置までスムーズに移動する
    public IEnumerator MoveTo(Vector3 goal)
    {
        while (Vector3.Distance(transform.position, goal) > 0.05f)
        {
            Vector3 nextPos = Vector3.Lerp(transform.position, goal, Time.deltaTime * speed);
            transform.position = nextPos;
            yield return null;//ここまでが1フレームの間に処理される
        }
        transform.position = goal;
        print("終了");
        yield break;//処理が終わったら破棄する
    }

}

他のスクリプトから直接Move.csのMoveToをStopCoroutine()しようとするとエラーが吐かれます。
詳しくはこっち
https://qiita.com/maqiita/items/0f9fa1fd2fbd6ded2fee

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのGLを使って数なし数直線を作ってみた

はじめに

Unityで画面上に線を引くためには様々な手段がありますが、その中でもGLと言うOpenGLのイミディエイトモードと同様のコマンドを実行することができるグラフィックスライブラリがあるらしいです。

今回はそれを使って数直線(数なし)←は?を作ってみたのでご紹介したいと思います。
※2D上でしか検証していないので3Dでの動作は保証できません

コードはGithubにて公開しています。https://github.com/HarumaroJP/GLMarkLineDrawer

run.gif

使い方

Githubに公開している二つのスクリプトをUnityに入れて、GLMarkLineDrawer.csをGameObjectにくっつけるだけです。

スクリーンショット 2020-10-10 17.02.02.png

設定欄はこんな感じです。

名前 用途
Paths Transform型で線の通る座標を指定します
Color 数直線の色を指定します
Width 線の幅を指定します
Edge Mark Length 外側のメモリの長さを指定します。
Inside Mark Length 内側のメモリの長さを指定します。
Interval Count メモリの分割数を指定します。

最初はVector3型で指定するようにしようかなと思ったのですが、かなり面倒になるので、Transform型を使うことにしました。

GLについて

まず前提としてGLクラスでは、このような書き方で描画します。

        //頂点マトリックスが漏れないようにするためのおまじない
        GL.PushMatrix();
        {
            //描画開始
            GL.Begin(GL.QUADS);
            {
                GL.Color(Color.white);

                GL.Vertex3(x,y,z);
            }
            GL.End();
        }
        GL.PopMatrix();

GL.PushMatrix() , GL.PopMatrix()は、行列マトリックスが範囲外に漏れないようにするためのおまじないのようなものです。複雑な描画をする際には必要になります。今回は一応書いていますが、簡単な描画の場合は必要ないかも..?

GL.Begin() , GL.End()の間で、描画する頂点を指定することができます。また、引数でメッシュの形成方法を指定する方法ができます。「GL Primitives」で検索するといろいろな方法が出てくるので、調べてみてください。今回は、四角形を形成するように設定しています。

GL.Colorは色を設定するためのメソッドです。このメソッドで設定して実行された後に面が描画された場合、設定した色が反映されるようになります。

GL.Vertex3(x,y,z) or GL.Vertex(Vector3)で頂点を設定することができます。

またブロック文で囲っている箇所がありますが、これは特に必要というわけではありません。可読性が上がるので、付けた方が良いというだけです。

Drawerの解説

一旦コードを貼ります。

GLMarkLineDrawer.cs
using System;
using UnityEngine;

[ExecuteInEditMode]
public class GLMarkLineDrawer : MonoBehaviour {

    [SerializeField]
    private GLMarkLine.LineSettings settings =
        new GLMarkLine.LineSettings(new Transform[0], Color.white, 100f, 2f, 1f, 10);

    private GLMarkLine line = new GLMarkLine(); //ラインのインスタンスを生成


    private void OnRenderObject() {
        line.Draw(settings); //描画
    }
}

こちらの方はあまり説明は必要ないと思いますが、

一応補足をしておくと、MonobehaviourのOnRenderObject()はカメラがシーンをレンダリングした後に呼び出されるメソッドで、今回はこのメソッドを通して描画しています。

また設定のパラメータは構造体で管理しています。あまり構造体で自前のコンストラクタは使いたくなかったのですが、入れたらすぐ使えるようにしたかったので、今回は自前のコンストラクタで初期化しています。

本体の解説

次が本体のコードです。

GLMarkLine.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Rendering;

public class GLMarkLine {

    //設定パラメータ用のstruct
    [Serializable]
    public struct LineSettings {
        public Transform[] paths;
        public Color color;
        public float width;
        public float edgeMarkLength;
        public float insideMarkLength;
        public int intervalCount;


        public LineSettings(Transform[] paths,
                            Color color,
                            float width,
                            float edgeMarkLength,
                            float insideMarkLength,
                            int intervalCount) {
            this.paths = paths;
            this.color = color;
            this.width = width;
            this.edgeMarkLength = edgeMarkLength;
            this.insideMarkLength = insideMarkLength;
            this.intervalCount = intervalCount;
        }
    }


    public LineSettings settings;
    private LineSettings _settings;
    private IReadOnlyList<Vector3> _paths = new List<Vector3>();
    private List<Vector3> currentPaths = new List<Vector3>();
    private Material lineMaterial;
    private float relativeWidth;
    public bool enabled = true;

    private static readonly int SrcBlend = Shader.PropertyToID("_SrcBlend");
    private static readonly int DstBlend = Shader.PropertyToID("_DstBlend");
    private static readonly int Cull = Shader.PropertyToID("_Cull");
    private static readonly int ZWrite = Shader.PropertyToID("_ZWrite");

    //描画するために使うマテリアルの初期化
    private void InitMaterial() {
        if (!lineMaterial) {
            lineMaterial = new Material(Shader.Find("Hidden/Internal-Colored"));
            lineMaterial.hideFlags = HideFlags.HideAndDontSave;
            lineMaterial.SetInt(SrcBlend, (int) BlendMode.SrcAlpha);
            lineMaterial.SetInt(DstBlend, (int) BlendMode.OneMinusSrcAlpha);
            lineMaterial.SetInt(Cull, (int) CullMode.Off);
            lineMaterial.SetInt(ZWrite, 0);
        }
    }


    public void Draw(LineSettings currentSetting) {
        settings = currentSetting;
        //座標が入っていなかったら描画しない
        if (!enabled || settings.paths.Length <= 0 || settings.paths.Any(t => t == null)) return;
        IReadOnlyList<Vector3> vecPaths = settings.paths.Select(t => t.position).ToList();


        InitMaterial();
        lineMaterial.SetPass(0);
        GL.PushMatrix();
        {
            GL.Begin(GL.QUADS);
            {
                GL.Color(settings.color);

                //設定が変更されていなかったら計算しない
                if (_settings.Equals(settings) && _paths.SequenceEqual(vecPaths)) {
                    DotVertexes(currentPaths.ToArray());
                }
                else {
                    currentPaths.Clear();
                    _settings = settings;
                    _paths = vecPaths;

                    Vector3 v0, v1, o;
                    //解像度に対する幅を求める
                    relativeWidth = 1.0f / Screen.width * settings.width * 0.5f;

                    for (int index = 0; index < _paths.Count - 1; index++) {
                        v0 = _paths[index];
                        v1 = _paths[index + 1];
                        //2点の単位ベクトルを求める
                        o = (new Vector3(v1.y, v0.x, 0.0f) - new Vector3(v0.y, v1.x, 0.0f)).normalized;

                        DrawLine2D(v0, v1, o);
                        DrawMark2D(v0, v1, o);
                    }
                }
            }
            GL.End();
        }
        GL.PopMatrix();
    }

    //2点に線を引く関数
    void DrawLine2D(Vector3 v0, Vector3 v1, Vector3 o) {
        Vector3 n = o * relativeWidth;
        Vector3[] vertex = new[] {
            new Vector3(v0.x - n.x, v0.y - n.y, 0.0f),
            new Vector3(v0.x + n.x, v0.y + n.y, 0.0f),
            new Vector3(v1.x + n.x, v1.y + n.y, 0.0f),
            new Vector3(v1.x - n.x, v1.y - n.y, 0.0f),
        };

        DotVertexes(vertex);

        foreach (Vector3 v in vertex) {
            currentPaths.Add(v);
        }
    }

    //2点にメモリをつける関数
    void DrawMark2D(Vector3 v0, Vector3 v1, Vector3 o) {
        Vector3 markLength, _v0, _v1, _o;
        Vector3 _unitVec = (v1 - v0) / settings.intervalCount;
        List<Vector3> _pos = new List<Vector3>();

        for (int i = 0; i < settings.intervalCount + 1; i++) {
            _pos.Add(v0 + _unitVec * i);
        }

        for (int i = 0; i < _pos.Count; i++) {
            Vector3 vec = _pos[i];
            float length = (i == 0 || i == _pos.Count - 1 ? settings.edgeMarkLength : settings.insideMarkLength);

            markLength = o * length;
            _v0 = new Vector3(vec.x - markLength.x, vec.y - markLength.y, 0.0f);
            _v1 = new Vector3(vec.x + markLength.x, vec.y + markLength.y, 0.0f);
            _o = (new Vector3(_v1.y, _v0.x, 0.0f) - new Vector3(_v0.y, _v1.x, 0.0f)).normalized;

            DrawLine2D(_v0, _v1, _o);
        }
    }

    //与えられた座標配列に頂点を打つ関数
    void DotVertexes(Vector3[] pos) {
        foreach (Vector3 v in pos) {
            GL.Vertex3(v.x, v.y, v.z);
        }
    }
}

コードが長くなっているので、細かい説明は割愛します。

1、数直線に幅を持たせたい

Unity側で用意されているGLクラスには、ラインの幅を設定するための方法が用意されていません。
なので、自分でメッシュの頂点座標を計算して描画する必要があります。(OpenGLの方だとあるらしい)

スクリーンショット 2020-10-10 20.23.25.png

実装方法としては、ある直線があったとしてその直線を囲む四角形の頂点座標を算出し、描画すれば作ることができます。(要するに赤い点の座標を求めたい)

ここからは少し、数学の話になります。
下のテキストを見てください。

スクリーンショット 2020-10-10 21.13.31.png

ここでは、上の図のような座標があったとして、まず頂点との差分(緑の線の部分)を求めます。
左のページでは、二つの頂点座標から垂直なベクトルを求めています。しかし、これだけだと二つの頂点の距離によって緑の線の長さも変わってしまいます

そこで長さを1に固定した単位ベクトルを求めることで、それに適当な幅をかけると狙い通りの長さにすることができます。

スクリーンショット 2020-10-10 21.40.18.png
そして最後にその差分を、元の頂点座標に足し合わせることで4点が求まります。

この部分の実装はこのようになっています。

    //垂直なベクトルを求め、正規化する
    o = (new Vector3(v1.y, v0.x, 0.0f) - new Vector3(v0.y, v1.x, 0.0f)).normalized;

    void DrawLine2D(Vector3 v0, Vector3 v1, Vector3 o) {
        //単位ベクトルに設定された幅をかける
        Vector3 n = o * relativeWidth;

        //オフセットを足して、頂点座標を求める
        Vector3[] vertex = new[] {
            new Vector3(v0.x - n.x, v0.y - n.y, 0.0f),
            new Vector3(v0.x + n.x, v0.y + n.y, 0.0f),
            new Vector3(v1.x + n.x, v1.y + n.y, 0.0f),
            new Vector3(v1.x - n.x, v1.y - n.y, 0.0f),
        };

        //描画する(自作関数)
        DotVertexes(vertex);
    }

2、数直線にメモリを付けたい

お気づきの方もいるかもしれませんが、1番の方法を利用すれば簡単に作ることができます。

スクリーンショット 2020-10-10 21.58.59.png

先ほどの4点をそれぞれv0,v1とし、新たにまた4点を求めるだけです。

まとめ

これを実装するのに約半日潰してしまった...
多分LineRendererよりは軽くなっている気がする(気がするだけ)。
正直線を引くだけならLineRendererで良い気がします。たくさん線を引きたいのであれば、GLの方が高速かも?

たくさん機能を追加すれば、もっと利便性は上がると思います。でも僕はやりません()

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity ML-Agents で好奇心を持つエージェントを使ってみた

好奇心をもったエージェントは、学習することで高速でぐるぐるまわることができるようになりました。
コードはこちら

好奇心とは?

好奇心(curiosity)とはどういう定義だという話ですが、ここでは未知の状態に訪れた報酬のことを言います。
定義してしまうと、好奇心も一つのパラメーターですね。

そして、結果的に好奇心ありとなしでは、圧倒的に学習スピードが異なります。
残念ながら今回のテストでは好奇心のないエージェントはほぼ学習することがありませんでした。

やったこと

  • エージェントがぐるぐる半時計周りに1周するごとに報酬を与える
  • エージェントが偶然一周するのはなかなか難しい(言い換えると偶然周ることもある)

結果

オレンジ:好奇心あり、ブルー:好奇心なし
bandicam 2020-10-10 18-53-04-767.jpg

学習曲線

好奇心ありだと途中からの学びが全然違いますね。
学習なしでも周ることがあるので、ものすごい時間をかければ学習できる可能性はあります。
bandicam 2020-10-10 18-53-31-448.jpg

好奇心による平均累積報酬グラフ

今回はぐるぐる回るだけの単純な環境なため、あるポイントから好奇心による報酬が減っていることがわかります。
bandicam 2020-10-10 18-54-03-917.jpg

その他

最初は進める方向を3軸に方向に設定していたのですが、学習が全然進まずうまくいきませんでした。
結局XZの平面に固定することでうまく学習するようになりました。
3軸から2軸に変更したのは、私自身が操作してぐるっと回そうとしたときに、難しすぎると感じたからです。
エージェントが機械的に操作する場合も同じで、難しすぎるといかに好奇心があっても学習が難しいらしく、
ある程度操作できる状態にすることで解決できました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityからC#のファイルをVscodeで開いても補完機能が効いていなかったので効くようにした

C#の補完機能が効いていない

Vscodeの拡張で、下記を入れていたが補完が効いていなかった。

  • C#
  • Debugger for Unity

そのため、以下の記事を参考にさせていただきながら補完を効くように作業しました。
ほぼ記事内容をなぞっただけですが、備忘させてください...
超絶謝謝参考記事: UnityでVSCodeを使うための最低限必要な設定

homebrewのインストール

以下URLからコードコピってきて、terminalに貼り付けて、homebrewのインストール
https://brew.sh/index_ja

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

インストールできたか確認。

brew -v
Homebrew 2.5.5
Homebrew/homebrew-core (git revision 62667b; last commit 2020-10-10)

入った。

Monoのインストール

brew install mono

ログを見てもSUCCESS的なの出てなくて、完了したか分からなかったのでversion確認してみる

mono --version

Mono JIT compiler version 6.12.0.93 (2020-02/620cf538206 Tue Aug 25 14:04:52 EDT 2020)
Copyright (C) 2002-2014 Novell, Inc, Xamarin Inc and Contributors. www.mono-project.com
    TLS:
    SIGSEGV:       altstack
    Notification:  kqueue
    Architecture:  amd64
    Disabled:      none
    Misc:          softdebug
    Interpreter:   yes
    LLVM:          yes(610)
    Suspend:       hybrid
    GC:            sgen (concurrent by default)

入ってる。

.NET CLI toolsのインストール

https://dotnet.microsoft.com/download/

上記からダウンロードしてインストール。

Vscodeの拡張機能をインストール

以下三つ

  • C#
  • Debugger for Unity
  • Mono Debug

Mono Debugだけインストールしていなかったので、新たにインストール
mono-install.png

Unity上でVscodeを標準エディタに設定する

メニューバーから、
Preferences > External Tools > External Script Editor
の箇所を、Visual Studio Codeにする。
Visual Studio Codeがプルダウンに出いていない場合、Browseから検索。

Unity-settings.png

Vscodeを再度確認

一応再起動して、VscodeでC#のファイルを開く。

C#-script.png

int型の変数に、float型の変数を代入しようとしてみると、
電球が出た!!!
やりました。ありがとうございます捗ります。

気になる箇所二点

1. VscodeでC#のファイルを開いた時に、右下にerrorが出る。

net-error.png

The .NET Core SDK cannot be located. .NET Core debugging will not be enabled. Make sure the .NET Core SDK is installed and is on the path.

.NETをインストールしたから出ないと思ったの出てきた。
気になる。
下記記事を参考に出ないようにしました。
VSCodeで「The .NET Core SDK cannot be located.」メッセージを抑止

2. 変数,関数の宣言をしたコードの上にreferencesが出てくる

Screen Shot 2020-10-10 at 18.53.56.png

邪魔だなと思ったので下記記事を参考にさせて頂き、非表示に
Visual Studio CodeのReference informationを非表示にする方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Coroutine Continue Failureのエラーメッセージ

他のスクリプトのコルーチンに対してStopCoroutine()しようとすると、エラーが吐き出されました。
でも実行はされるので、問題はないけどエラーメッセージが邪魔...。
他のスクリプトから直接StopCoroutine()するのではなく、StopCoroutine()させるメソッドを作って間接的に実行させました。

【環境】

・Mac OSX El Capitan
・Unity versiton:2018.3.0

【実行状況】

BallオブジェクトがStartオブジェクトとGoalオブジェクトの間を行き来するプログラムです。
BallオブジェクトにはMove.cs、StartとGoalオブジェクトにはKickBall.csをアタッチしています。

Move.csのIEnumerator MoveTo( Vector3 goal)をKickBall.csから呼び出していました。

Move.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{
    public float speed;

    //goalの位置までスムーズに移動する
    public IEnumerator MoveTo( Vector3 goal) {
        while (Vector3.Distance(transform.position, goal) > 0.05f) 
        {
            Vector3 nextPos = Vector3.Lerp(transform.position, goal, Time.deltaTime * speed);
            transform.position = nextPos;
            yield return null;//ここまでが1フレームの間に処理される
        }
        transform.position = goal;
        print("終了");
        yield break;//処理が終わったら破棄する
    }

}
KickBall.cs
using UnityEngine;

public class KickBall : MonoBehaviour
{
    public Transform target;
    private Vector3 targetPos;
    private Coroutine myCor;
    // Start is called before the first frame update
    void Start()
    {
        targetPos = target.position;
    }

    private void OnTriggerEnter(Collider other)
    {

        //Move.csのコルーチンを止めようとするとエラーメッセージが出た
        if (other.GetComponent<Move>().myCor != null)
        {
            StopCoroutine(other.GetComponent<Move>().myCor);
        }
        other.GetComponent<Move>().myCor = StartCoroutine(other.GetComponent<Move>().MoveTo(targetPos));

    }
}

スクリーンショット 2020-10-10 17.02.42.png

【解決方法】

下記フォーラムによると、
https://answers.unity.com/questions/989547/coroutine-continue-failure-when-using-stopcoroutin.html

Make sure to call StopCoroutine() on the same object (MonoBehavior) that you started the coroutine.
コルーチンをスタートさせたオブジェクト(MonoBehavior)と同じオブジェクト内でStopCoroutine()を呼び出していることを確かめてください。

とあります。
同じMonoBehaviorでStartCoroutine()してるんですが、StopCoroutine()を外から呼び出すことでエラーメッセージを吐いてしまうのかも、と思い、下記のように変更してメッセージが出ないようにしました。

Move.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Move : MonoBehaviour
{
    public float speed;
    private Coroutine myCor;

    //MoveToをスタートさせるメソッド
    //外部からコルーチンを呼び出すときはこのメソッドを使う
    public void StartCor(Vector3 goal)
    {
        if (myCor != null)
        {
            StopCoroutine(myCor);//StartCoroutine()する前に停止させて、重複して実行されないようにする。
        }
        myCor = StartCoroutine(MoveTo(goal));
    }

    //goalの位置までスムーズに移動する
    public IEnumerator MoveTo( Vector3 goal) {
        while (Vector3.Distance(transform.position, goal) > 0.05f) 
        {
            Vector3 nextPos = Vector3.Lerp(transform.position, goal, Time.deltaTime * speed);
            transform.position = nextPos;
            yield return null;//ここまでが1フレームの間に処理される
        }
        transform.position = goal;
        print("終了");
        yield break;//処理が終わったら破棄する
    }

}
KickBall.cs
using UnityEngine;

public class KickBall : MonoBehaviour
{
    public Transform target;
    private Vector3 targetPos;
    private Coroutine myCor;
    // Start is called before the first frame update
    void Start()
    {
        targetPos = target.position;
    }

    private void OnTriggerEnter(Collider other)
    {
        //StartCor()を使ってMove.csのMoveToを開始
        other.GetComponent<Move>().StartCor(targetPos);
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】FungusをLuaで使用する方法

Fungusはビジュアルスクリプティングで会話パートを実装できるアセットです。

ビジュアルスクリプティングなのでUnityEditor上で会話を打ち込む必要があります。

簡単な会話パートならUnityEditor上で済むと思いますが、本格的にアドベンチャーゲームを作る、もしくは会話だけUnityを知らない人に作ってもらおうとすると大変です。

会話パートをLuaに記述することで解決しようと思います。

ステップ0 Luaとは?

C#とは別の昔からゲームに組み込まれてきた汎用スクリプト言語です。

ステップ1 Fungusをアセットストアからインポートする。

https://assetstore.unity.com/packages/templates/systems/fungus-34184

Asset StoreでFungusを検索しダウンロード&インポートします。

1.png

ステップ2 Flowchart、LuaEnvironment、LuaScriptをシーンに配置する。

メニューの
Tools > Fungus > Create
から

  • Flowchart
  • LuaEnvironment
  • LuaScript

をシーンに配置します。

2.png

ステップ3 Luaファイルを配置する。

任意のフォルダにLuaファイルを配置します。

Luaファイルの文字コードはUTF-8にしてください。
UTF-8でないと日本語が文字化けしてしまいます。

ステップ4 Luaファイルを編集する。

Fungusで会話を表示したい場合は

say("会話")

を記述してください。
改行は\r\nで表現できます。

ステップ5

Lua Scriptの

  • Lua Enviromentに先ほどHierarchyに配置したLuaEnvironmentをドラッグ&ドロップしてください。
  • Lua Fileに任意の場所に保存したLuaファイルをドラッグ&ドロップしてください。
  • Lua Scriptに直接Luaコマンドを打ち込めます。Lua Fileがある場合は、Lua Fileが実行された後に、Lua Scriptのコマンドが実行されます。

4.png

ステップ6 Luaの実行タイミングをスクリプトで操作する。

初期設定ではシーン開始時にLuaが実行されます。
実行タイミングを変更したい場合は、
LuaScript > Execute Handler > On Event
をStartからNothingにに変更してください。

3.png

LuaScriptを他のスクリプトでGetComponentして、OnExecuteメソッドを実行してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Deformアセットを元に頂点を変形させる頂点シェーダーを作る

概要

DeformとはKeenan Woodall氏が公開しているUnityの無料アセットで、マルチスレッディングでメッシュを高速に変形させることができるすごいアセットです。GitHubにソースコードが公開されています。

日本語の紹介記事があります。
<参考>Unity:高速動作するメッシュ変形「Deform 1.0」が無料で使える!

今回はシェーダーの勉強として、このDeformの変形を頂点シェーダーに移植していきます。

Deformのソースコード

今回はDeformの40種類以上ある変形の中から、範囲内のメッシュを曲げるLimitedBendを選んで頂点シェーダーに移植します。
DeformのBendのソースコードの一部抜粋して見てみます。

BendDefomer.cs
namespace Deform
{
    [Deformer (Name = "Bend", Description = "Bends a mesh", Type = typeof (BendDeformer))]
    [HelpURL("https://github.com/keenanwoodall/Deform/wiki/BendDeformer")]
    public class BendDeformer : Deformer, IFactor
    {
        //...中略...
        [BurstCompile (CompileSynchronously = COMPILE_SYNCHRONOUSLY)]
        public struct LimitedBendJob : IJobParallelFor
        {
            public float angle;
            public float top;
            public float bottom;
            public float4x4 meshToAxis;
            public float4x4 axisToMesh;
            public NativeArray<float3> vertices;

            public void Execute (int index)
            {
                var point = mul (meshToAxis, float4 (vertices[index], 1f));

                var unbentPoint = point;

                var angleRadians = radians (angle);
                var scale = 1f / (angleRadians * (1f / (top - bottom)));
                var rotation = (clamp (point.y, bottom, top) - bottom) / (top - bottom) * angleRadians;

                var c = cos ((float)PI - rotation);
                var s = sin ((float)PI - rotation);
                point.xy = float2 
                (
                    (scale * c) + scale - (point.x * c),
                    (scale * s) - (point.x * s)
                );

                if (unbentPoint.y > top)
                {
                    point.y += -c * (unbentPoint.y - top);
                    point.x += s * (unbentPoint.y - top);
                }
                else if (unbentPoint.y < bottom)
                {
                    point.y += -c * (unbentPoint.y - bottom);
                    point.x += s * (unbentPoint.y - bottom);
                }

                point.y += bottom;

                vertices[index] = mul (axisToMesh, point).xyz;
            }
        }
    }
}

DeformはJob SystemやBurstコンパイラを使用して高速化を図っているようです。
<参考>【Unity】C# Job Systemを使ってみる(Burst Compilerも)

IJobParallelForを使用していることからも分かるように、Deformのほとんどの変形はポリゴンの各頂点に対して他の頂点とは独立に作用しており、またDeformで使用している数学関数ライブラリUnity.Mathematicsは関数の名前がCg/HLSLと同じなので簡単に頂点シェーダーに移植できます。

シェーダー作成

それでは頂点シェーダーを書いていきましょう。変形用の関数をDeform.cgincとして別ファイルにしておき、DeformTest.shaderからインクルードします。

Deform.cginc
#ifndef DEFORM_INCLUDED
#define DEFORM_INCLUDED

inline void swapMinMax(inout float x, inout float y){
    float tmpMin = min(x, y);
    float tmpMax = max(x, y);
    x = tmpMin;
    y = tmpMax;
}


inline float4 deformLimitedBend(float4 vertex, float4x4 meshToAxis, float4x4 axisToMesh, float angle, float top, float bottom){
    if (top == bottom)
        return vertex;
    float4 vertex_ = mul(meshToAxis, vertex);
    swapMinMax(bottom, top);

    float4 unbentVertex = vertex_;

    float angleRadians = radians(angle);
    float scale = (top - bottom) / angleRadians;
    float rotation = (clamp(vertex_.y, bottom, top) - bottom) / scale;

    float s, c;
    sincos((float)UNITY_PI - rotation, s, c);
    vertex_.xy = float2
    (
        (scale - vertex_.x) * c + scale,
        (scale - vertex_.x) * s
    );

    vertex_.xy += (unbentVertex.y > top) ? float2(s * (unbentVertex.y - top), -c * (unbentVertex.y - top)) :
    ((unbentVertex.y < bottom) ? float2(s * (unbentVertex.y - bottom), -c * (unbentVertex.y - bottom)) : float2(0.0,![変形なしキューブ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270414/6a487bd6-0c44-8384-5ec6-b89582cf074e.png)
 0.0));

    vertex_.y += bottom;

    return mul(axisToMesh, vertex_);
}

変数名を変えたりsinとcosをsincosにまとめたりしていますが基本的に元のC#プログラムと同じことをしています。

DeformTest.shader
Shader "Custom/DeformTest"
{
     Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma vertex vert
        #pragma surface surf Standard noshadow


        #include "UnityCG.cginc"
        #include "Deform.cginc"

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        float4x4 unitMatrix(){
            float4x4 m;
            m._11_21_31_41 = float4(1, 0, 0, 0);
            m._12_22_32_42 = float4(0, 1, 0, 0);
            m._13_23_33_43 = float4(0, 0, 1, 0);
            m._14_24_34_44 = float4(0, 0, 0, 1);
            return m;
        }

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void vert(inout appdata_full v)
        {
            float4x4 meshToAxis = unitMatrix();
            float4x4 axisToMesh = unitMatrix();

            #define deform(pos) deformLimitedBend(pos, unitMatrix(), unitMatrix(), 60, 1, 0)

            float4 vertex = mul(meshToAxis, v.vertex);
            float3 normal = mul(meshToAxis, float4(v.normal.xyz, 0));
            float3 tangent = mul(meshToAxis, float4(v.tangent.xyz, 0));
            float3 binormal = normalize(cross(normal, tangent));
            float4 deformedVertex = deform(vertex);

            float delta = 0.05;
            float3 deformedVertexDeltaTangent = deform(vertex + float4(tangent, 0) * delta).xyz;
            float3 deformedVertexDeltaBinormal = deform(vertex + float4(binormal, 0) * delta).xyz;

            float3 deformedTangent = deformedVertexDeltaTangent - deformedVertex;
            float3 deformedBinormal = deformedVertexDeltaBinormal - deformedVertex;
            float3 deformedNormal = normalize(cross(deformedTangent, deformedBinormal));

            v.vertex = mul(axisToMesh, deformedVertex);
            v.normal = mul(axisToMesh, float4(deformedNormal.xyz, 0)).xyz;
        }

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

頂点シェーダーでは頂点を移動して変形を行うほかに、頂点から接線および従法線方向に少し移動した座標に対しても同様に変形を行うことで法線が滑らかになるように計算しています。
<参考>【Unity】頂点シェーダーで頂点を変形した後に法線を再計算する

作成したシェーダーをオブジェクトに適用した結果がこちらです。なおキューブのメッシュはデフォルトのものではなくポリゴンの多いものを使っています。

結果

シェーダー適用前
変形なしキューブ.PNG

シェーダー適用後
変形ありキューブ.PNG

うまくいきました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity Test Runner】テスト駆動開発でプレイヤーHPクラスを実装する

はじめに

テスト駆動開発(TDD)に興味があったので、いろいろ調べて挑戦してみました。
今回はその忘備録として、テスト駆動開発でプレイヤーのHPクラスを実装した話を書こうと思います。


環境
Unity : 2019.4.10f1
Visual Studio : 2019 ver16.7.2

Test Framework : 1.1.16
最新は1.1.18なんですね。アップデートしてない・・・。

コード全文(Gist)
https://gist.github.com/poyoppo/904ec3acca61b0ded405563010696a7c
コード内の英語コメントが初心者すぎるけど気持ちで読んでください:bow:丸投げ

テスト駆動開発とは

めちゃくちゃざっくりいうと、先にテストコードを書いてからプログラムを実装する開発方法です。
テスト駆動で開発するとプログラムの振る舞いがテストによって担保される、バグが減る、リファクタリングしやすくなる、などいろいろ良いことづくしなようですね。
初学者のわたしがアレコレ説明するよりも分かりやすい記事がたくさんあるのでご覧ください(丸投げ

参考になる記事
なぜテストコードを書く必要があるのか?
僕たちがテスト駆動開発をする理由

テスト駆動開発する!!

それでは早速テストを書いていきたいと思います。

Unity Test Runnerの導入についてはこちらの記事が分かりやすいです。
Unityでテストを書くのが当然になる時代に今から備えよう

わたしのフォルダ構成もこの記事に準拠しています:thumbsup:
0000_フォルダ構成.jpg

テストコードに「満たすべき仕様」を書いていく

今回はゼルダのようなHPを実装することにしたので、次のような仕様が求められそうです。

  • HPはハートで表される
  • ハートのかけら4つでハートが1つ増える(最大HPアップ)
  • 最小ダメージはハート4分の1つ
  • その他、基本的なHPの仕様(回復・ダメージ・HPは最大HPを超えない・HPは-1以下にならない・etc...)

「こんなことを調べる必要があるな」というものを、とりあえず書いていきます。

テストコード
[Test]
public void A_コンストラクタテスト()
{
}

[Test]
public void A_コンストラクタの引数に0以下を渡すと例外が発生する()
{
}

[Test]
public void D_IsFullプロパティテスト()
{
}

[Test]
public void G_回復メソッドテスト()
{
}

[Test]
public void G_HPは最大HPを超えない()
{
}

[Test]
public void G_回復メソッドの引数に負の値を指定すると例外が発生する()
{
}

// 以下略

わたしの場合、テストコード内では分かりやすさ優先で日本語で書いていきます。
英語力のある方は英語でまったく問題なしです。
(メソッド名などに日本語を使えるのは、[Test]属性が付いているから?とか、テストコード内だから?とか思っていましたが、ふつうのクラスでも日本語で書けるんですね。そんなことしないけど・・・)

テストコード内のメソッドの順番と、Test Runnnerウィンドウのメソッドの順番(50音・ABC順になる?)が異なり分かりづらいので、大文字の英字_テスト名()にしています。
また、コンストラクタのテストはA_コンストラクタテスト()、プロパティのテストはD_プロパティテスト()・・・のようにテストしたい項目によって先頭の英字を変えました。
こうすることによってTest Runnerウィンドウ上の表示が何もしないよりは分かりやすくなります。
0001_TestRunnnerウィンドウ.jpg
ちなみにテストをカテゴリ分けすることもできるようですが、詳しく調べてません。
【Unity】Unity Test Runner のテストをカテゴリ分けする方法

テストコードを書く

実際にテストコードを書いていきます。
テスト対象であるPlayerHpの実装より先にテストコードを書きます。

例えば、コンストラクタのテストをするのであれば、しっかりインスタンスが生成されていること、初期状態は最大HPと現在HPが等しいことなどが分かれば良さそうです。
また、インスタンス生成時に0以下の値を渡すと例外が発生するようにしたいです。

以上の仕様をテストするコードが以下です。
※今回はPlayerHpクラスの生成時に、内部で「初期ハート数*4」という計算をすることをこの時点で決めてました。
(最初はハート4分の1つ=0.25fという扱いにしてたのですが、不便すぎたので・・・)

テストコード
[Test]
public void A_コンストラクタテスト()
{
    // 初期HPはハート3つ分
    var playerHp = new PlayerHp(3);

    // playerHp が存在する
    Assert.That(playerHp, Is.Not.Null);

    // 最大HP, 現在のHPの初期値は12(3 * 4 = 12)である
    Assert.That(playerHp.MaxHp, Is.EqualTo(12));
    Assert.That(playerHp.CurrentHp, Is.EqualTo(12));
}

[Test]
public void A_コンストラクタの引数に0以下を渡すと例外が発生する()
{
    TypeInitializationException ex = Assert.Throws<TypeInitializationException>(() => new PlayerHp(0));
    Assert.AreEqual(ex.TypeName, typeof(PlayerHp).FullName);
    Assert.That(ex.InnerException, Is.TypeOf<ArgumentOutOfRangeException>());
}

// 以下略

テストにはNunit.FrameworkのAssertクラスを使います。
基本的には次のAssert.Thatメソッドを用いて書きます。

Assert.That(テストしたい値, 期待する値);

Assert.AreEqual()という書き方もあるようですが、現在はAssert.Thatで書くのが推奨されているようです。
NUnitのAssert.ThatメソッドにIsとかHasとかを入れて柔軟なテストコードを書こう

PlayerHpクラスに最小限の実装を書く

このままだとコンパイルエラーになるので、PlayerHpクラスにプロパティやコンストラクタを定義しておきます。あとからテストする回復メソッドやダメージメソッドも定義だけしておきます。
中身の実装はしません。

PlayerHp.cs
public class PlayerHp
{
    private int maxHp;
    private int currentHp;

    public int MaxHp { get { return maxHp; } } // 最大HP
    public int CurrentHp { get { return currentHp; } } // 現在のHP

    // コンストラクタ
    public PlayerHp(int initialHeartCount)
    {
    }

    // メソッドも同様に書いていく
    public void HealHp(int healPoint)
    {
    }

    public void DamageHp(int damagePoint)
    {
    }
}

テスト実行→失敗

プロパティなどを定義するとコンパイルが通るので、Unityに戻ってテストを実行してみます。

・・・。

0002_テスト失敗.jpg
当たり前ですが、テストは失敗します。

なぜコードそのものを書く前にテストを書くのか、不思議に思う方もいらっしゃるでしょう。その理由は、コードを書いてからテストを書くようにすると、開発者はしばしばテストが通るようにテストを書いてしまうことがあるからです。まず失敗するテストを書けば、テストが失敗するのには妥当な理由(たとえば、必要な機能が正しく実装されていない)があることを確信でき、誤検知を排除することができます。

Unity Test Runner でテスト駆動開発を試す(Unity公式ブログより)

「実装してないので失敗する」ということが分かるのが重要なようです。

テスト対象スクリプトを実装する

ここまできて初めてPlayerHpの実装をしていきます。

PlayerHp.cs
// 最大HP計算用の定数
private const int PIEACE_AMOUNT = 4;

// コンストラクタ
public PlayerHp(int initialHeartCount)
{
    if (initialHeartCount <= 0)
    {
        throw new TypeInitializationException(typeof(PlayerHp).FullName,
            new ArgumentOutOfRangeException());
    }

    int initHp = initialHeartCount * PIEACE_AMOUNT;

    maxHp = initHp;
    currentHp = initHp;
}

// 以下略

改めてテスト実行

今度はテストが通るはずです。
この繰り返しで他のメソッドも実装していきます。

Ej7ii-JVoAAULQq.png

ほかのテストもすべて通るようになればPlayerHpクラスは完成です。

まとめ

やってみる前はとっつきにくいイメージでしたが、挑戦してみると面白く、テストがすべて通ったときの気持ちよさもやみつきになりそうです・・・。
ただ慣れるまでは「テストコード自体が間違っている」ということも起きかねないので、色んなクラスのテストを書いてみて経験を積んでいきたいと思います。

一度やってみたくらいではテスト駆動開発による恩恵をすべて体感することはできませんでしたが、テスト駆動を用いていなかった今までと比べて、「どう書けばいいのか分からない」が無くなる(減る)というのが自分にとっての一番のメリットに感じました。

次の挑戦

各テスト前後に実行される属性である[SetUp][TearDown]などには触れなかったので、今後使ってみようと思います。
テスト毎にPlayerHpをインスタンス化しているところも[SetUp]を用いて書き直せそうですね:thinking:

さいごに

もっと良い実装方法やテスト方法があったら教えていただけると嬉しいです。
また、誤字脱字・コードの間違いなどがあればそちらもご指摘いただけると助かります:relaxed:
最後まで読んでくださりありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityCollabのユーザーの追加方法

ゲーム開発に向けてチーム共有のためデータ共有の方法を調べているとUnityCollabなるものを見つけたので、わからないながらも使ってみました。
自分用の備忘録でもあります。

UnityCollabにプロジェクトを登録する

①Unityの右上のほうにCollabというボタンがあるので押してユーザーを登録し、プロジェクト名を登録する。

ーとかだったような気がする。
ー覚えていない。
ーググりながらやってた。
ー次はメモりながらやろう。(戒め)

「組織」を作成する

①Unity開発者ダッシュボードで「組織」を作成する
②UnityHub右上のアカウントをクリックすると「開発者ダッシュボードに移動」というのがあるので移動する。
③組織を適当に作成する。
組織名はあとからでも変更できるし、クレジットカードなどを登録する必要はないので気軽に適当に作成する。

1.PNG

メンバーを追加する

①組織画面からだと画面右下に「メンバーグループ」のとなりに「すべてを見る」ボタンがあるのでクリックする。

2.PNG

②「メンバーを追加」ボタンをクリックすると追加画面に移るので追加したいユーザーのメールアドレスを入力し、役割を適当に設定し招待する。
※メンバーの役割はあとででも変更可能

3.png

ただ、このままでは追加したユーザーは変更箇所をプッシュしても共有できないようなのでUnityダッシュボードでも追加する。

③UnityID画面が全画面表示の場合、左側にメニューが表示されるので「プロジェクト」をクリックする。

5.png

④すると、今まで作ったプロジェクトの一覧が表示されると思うのでユーザーを追加するプロジェクトをクリックする。

6.png

⑤画面左のSetting→Usersをクリックすると現在のプロジェクトに登録されている共有ユーザーの一覧が表示されるのでドロップボックスから組織で登録したユーザーを選択し、追加する。
これで該当プロジェクトでユーザー共有ができていると思う。

7.PNG

所感

お金はかからないし、共有サイズは1GBのみだけどゲームデータのみだけだし特に問題はないかなと今のところ思っています。
Libraryフォルダは参照していないのでキャッシュは含めていないようです。DropBoxなどローカルフォルダとクラウド同期型のAssetフォルダ参照のみということでしょうかね?
無料でも3人まではチーム共有できるので悪くはないのかなと思います。VisualStudioでのチームエクスプローラーやSourceTreeよりかは導入としては楽なような気がするので今後はこれで進められたら楽そうですかね。

あと、記事を書いている途中で思ったんですけどメンバー追加の①②では組織には所属するけどプロジェクトには追加していないっていうことなんですかね?③④⑤でプロジェクトに追加するみたいな感じかな?
他のプロジェクトを見る限りユーザーを追加していなければそこのプロジェクトの所属ユーザーは自分のみだし、組織には追加されていても共有はされないっぽいので確かにプロジェクト単位での追加方法になるのかなと思いました。
終わり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityCollabの登録/共有ユーザーの追加方法

ゲーム開発に向けてチーム共有のためデータ共有の方法を調べているとUnityCollabなるものを見つけたので、わからないながらも使ってみました。
自分用の備忘録でもあります。

UnityCollabにプロジェクトを登録する

①Unityの右上のほうにCollabというボタンがあるので押してユーザーを登録し、プロジェクト名を登録する。

ーとかだったような気がする。
ー覚えていない。
ーググりながらやってた。
ー次はメモりながらやろう。(戒め)

「組織」を作成する

①Unity開発者ダッシュボードで「組織」を作成する
②UnityHub右上のアカウントをクリックすると「開発者ダッシュボードに移動」というのがあるので移動する。
③組織を適当に作成する。
組織名はあとからでも変更できるし、クレジットカードなどを登録する必要はないので気軽に適当に作成する。

1.PNG

メンバーを追加する

①組織画面からだと画面右下に「メンバーグループ」のとなりに「すべてを見る」ボタンがあるのでクリックする。

2.PNG

②「メンバーを追加」ボタンをクリックすると追加画面に移るので追加したいユーザーのメールアドレスを入力し、役割を適当に設定し招待する。
※メンバーの役割はあとででも変更可能

3.png

ただ、このままでは追加したユーザーは変更箇所をプッシュしても共有できないようなのでUnityダッシュボードでも追加する。

③UnityID画面が全画面表示の場合、左側にメニューが表示されるので「プロジェクト」をクリックする。

5.png

④すると、今まで作ったプロジェクトの一覧が表示されると思うのでユーザーを追加するプロジェクトをクリックする。

6.png

⑤画面左のSetting→Usersをクリックすると現在のプロジェクトに登録されている共有ユーザーの一覧が表示されるのでドロップボックスから組織で登録したユーザーを選択し、追加する。
これで該当プロジェクトでユーザー共有ができていると思う。

7.PNG

所感

お金はかからないし、共有サイズは1GBのみだけどゲームデータのみだけだし特に問題はないかなと今のところ思っています。
Libraryフォルダは参照していないのでキャッシュは含めていないようです。DropBoxなどローカルフォルダとクラウド同期型のAssetフォルダ参照のみということでしょうかね?
無料でも3人まではチーム共有できるので悪くはないのかなと思います。VisualStudioでのチームエクスプローラーやSourceTreeよりかは導入としては楽なような気がするので今後はこれで進められたら楽そうですかね。

あと、記事を書いている途中で思ったんですけどメンバー追加の①②では組織には所属するけどプロジェクトには追加していないっていうことなんですかね?③④⑤でプロジェクトに追加するみたいな感じかな?
他のプロジェクトを見る限りユーザーを追加していなければそこのプロジェクトの所属ユーザーは自分のみだし、組織には追加されていても共有はされないっぽいので確かにプロジェクト単位での追加方法になるのかなと思いました。
終わり。

【参考リンク】
【Unity】Unity Collaborateでリアルタイム通信コンテンツの制作フローを効率化
初心者向け!Unity Collaborateを使ってみる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む