20201010のC#に関する記事は8件です。

【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から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で続きを読む

インターフェース

インターフェースのどこが有効なのかよくわからないけど、
この投稿はわかりやすい

https://qiita.com/yutorisan/items/d28386f168f2f3ab166d

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

[wpf] ボタンのIsDefault・IsCancelを設定する

はじめに

仕事でOKボタンを押したらシャットダウンする自作windowを作る機会があったのですが、
windowが表示されたときにEnterキー押したら、OKボタンのクリックイベントが実行されるようにしてね」と言われました。
image.png

言われるまではクリックでok等のボタンイベントが正常か確認していましたが、キーイベントでの挙動は見ていなかったのでこの機会に少しまとめてみました。

IsDefaultIsCancelという便利なプロパティがwpfのボタンには存在するようなので、それを使ってみます。

作ったサンプル

image.png

テキストボックスを1つと、ボタンを5つ並べました。

default(okCancel)と書かれたボタンにIsDefault=Trueを割り当て、escape(ync)と書かれたボタンにIsCancel=Trueを割り当てています。

各ボタンにはクリックイベントを割り当てていて、show NodDefWin以外はメッセージボックスを表示させています。

xaml

<Grid Background="#b33e5c">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <Grid Grid.ColumnSpan="3">
            <TextBlock x:Name="text" Text="result" />
        </Grid>

        <!--Defaultのボタン-->
        <Grid Grid.Column="0">
            <Button Content="default(okCancel)" Width="100" Height="30" Click="Button_Default" IsDefault="True" />
        </Grid>

        <Grid Grid.Column="1">
            <StackPanel>
                <!--Escape時に選択されるボタン-->
                <Button Content="escape(ync)" Width="100" Height="30" IsCancel="True" Click="Button_Escape" />
                <Button Content="onlyOk" Width="100" Height="30" Click="Button_OnlyOK" />
                <Button Content="YsNo" Width="100" Height="30" Click="Button_YesNo" />
            </StackPanel>
        </Grid>

        <Grid Grid.Column="2">
            <Button Content="show NoDefWin" Width="100" Height="30" Click="Button_Custom" />
        </Grid>
    </Grid>

xaml.cs

private void Button_Default(object sender, RoutedEventArgs e)
        {
            var r = MessageBox.Show("Enter default ok", "caption", MessageBoxButton.OKCancel);
            this.text.Text = r.ToString();
        }

        private void Button_Escape(object sender, RoutedEventArgs e)
        {
            var r = MessageBox.Show("Enter", "caption", MessageBoxButton.YesNoCancel);
            this.text.Text = r.ToString();
        }

        private void Button_OnlyOK(object sender, RoutedEventArgs e)
        {
            var r = MessageBox.Show("Enter", "caption", MessageBoxButton.OK);
            this.text.Text = r.ToString();
        }

        private void Button_YesNo(object sender, RoutedEventArgs e)
        {
            var r = MessageBox.Show("Enter", "caption", MessageBoxButton.YesNo);
            this.text.Text = r.ToString();
        }

        /// 自作のwindowクラスを表示する
        private void Button_Custom(object sender, RoutedEventArgs e)
        {
            var s = new SubWindow();
            s.ShowDialog();
        }

動作

Enterキー

default(okCancel) と書かれたボタンの IsDefault=True にしているので、アプリ起動時にEnterを押すとこのボタンのClickイベントが実行されて、MessageBoxButton.OKCancelのメッセージボックスが表示されます。

image.png

MessageBoxButton.OKCancelのメッセージボックスは OKボタンがIsDefault=TrueキャンセルボタンがIsCancel=Trueになっています。
ですので、Enterを押すとテキストブロックにOKが表示され、Escapeを押すとCancelが表示されます。

image.png

テキストブロックの文字がCancelになっていますね。

escapeキー

escapeキーを押すと IsCancel=trueにしているescape(ync)を表示しているボタンのclickイベントが実行されます。

image.png

MessageBoxButton.YesNoCancel で表示しているメッセージボックスも デフォルトでYesEscapeでCancelが選択されるようです。

OKのみ / YesNo

MessageBoxButton.OKのメッセージボックスは デフォルト・Escape共にtrueのようで、Enter・escapeどちらでもokが選択されます。

MessageBoxButton.YesNoのメッセージボックスは デフォルトでyesが選択されるようですが、Escapeでは反応がありません。

IsCancelというプロパティ名なので、キャンセルボタンがない場合は基本的にはtrueにしないほうがいいようです。

image.png

escapeを押してもcloseしてくれないメッセージボックス。

show NoDefWin(自作window)

show NodefWinボタンで表示する自作windowはボタンを2つ並べているwindowです。

<!--一部のみ-->
<Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>

        <TextBlock Text="okでシャットダウンします" />

        <Grid Grid.Column="0">
            <Button Content="ok" Width="100" Height="30" Click="Button_Close"/>

        </Grid>

        <Grid Grid.Column="1">
            <Button Content="cancel" Width="100" Height="30" Click="Button_Close"/>
        </Grid>

Button_Closeイベントはthis.Close()のみです。

image.png

IsDefaultもIsCancelもtrueにしていないので、Enter・Escapeキーを押しても反応がありません。まぁ、当然ですね。

ということで自作windowでOK・キャンセルボタンを設置するときはIsDefaultIsCancelプロパティを使ってあげようというお話でした。

まとめ

windowsの仕様に倣うのであれば、okタイプのボタンならDefault=Trueにして、キャンセルのボタンならIsCancel=Trueにしたほうがいいと改めて感じました。

・・・評価仕様でこういう確認見たことないな。

参考サイト

https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.controls.button.isdefault?view=netcore-3.1

https://threeshark3.com/isdefault/

  • このエントリーをはてなブックマークに追加
  • 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で続きを読む