Cooooding!!

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

VisualStudioでクラス名や変数名の命名規則をチェックする【C#】

概要

クラス名や変数名などの命名規則を決めていてもうっかり間違えることがよくあるので、Visual Studio命名規則をチェックする機能を使ってみました。命名規則にあっていない部分に線が引かれるようになります。

設定例(今筆者が使っている設定)
VisualStudioで命名規則を設定した例

表示例
VisualStudioで命名規則を設定した結果

設定方法

設定画面の開き方

  1. 画面上部のメニューから「ツール」→「オプション」を選択
  2. 「テキスト エディター」 → 「C#」 → 「コードスタイル」 → 「名前指定」 を選択

新しい命名規則を追加する

開いた設定画面下の[+]ボタンを押すと命名規則が1行追加されます。

命名規則の追加ボタン

仕様

仕様というのはパスカルケースやキャメルケースなどのルールを何に対して適用するかを指定する設定のことです。例えば「クラス」や「privateなフィールド」「publicなメソッド」などがあります。

仕様はウィンドウの下にある「仕様の管理」から種類を増やしたり内容を変更できます。
仕様の管理画面

「〇〇〇と△△△と□□□は全てパスカルケース」という命名規則を作りたい場合はそれらをまとめた仕様を作ると綺麗にまとまります。
名前空間、クラス名、構造体名、enum名、型引数名をまとめた仕様

シンボルの種類の中にいくつかわかりにくいものがありますが「parameter」はメソッドの引数、「type parameter」はGenericの型引数、「local」はローカル変数になります。
各シンボルの説明。メソッドの引数、Genericの型引数、ローカル変数

必要なスタイル

スタイルというのは「パスカルケース」や「キャメルケース」などのことです。最初からある「キャメルケース」というスタイルは先頭が小文字になるLower Camel Caseを指します。

「m_」などのプレフィックスを付けたい場合は以下のように設定します。
m_で始まるパスカルケースを設定した例

スネークケースならこうなります。
スネークケースを設定した例

重要度

重要度は命名規則違反をどう表示するかというルールです。
重要度の種類

エラーの場合
重要度をエラーにした場合の表示
命名規則違反の個所に赤線が引かれます。「考えられる修正内容を表示する」から名前を自動修正することもできます。「エラー」という名前ですがコンパイルエラーにはなりません。

警告の場合
重要度を警告にした場合の表示
命名規則違反の個所に緑の線が引かれます。

提案事項の場合
重要度を提案事項にした場合の表示
わかりにくいですが、命名規則違反の個所の先頭に破線が引かれます。

リファクタリングのみの場合
重要度をリファクタリングのみにした場合の表示
命名規則違反の個所に線は引かれません。カーソルを合わせると電球マークが表示され修正が提案されます。

残念なところ

「キャメルケース」はスネークケースが違反にならない

「キャメルケース」というスタイルにはアンダースコアを含まないというルールがないためスネークケースが違反になりません。
キャメルケースなのにスネークケースが違反にならない

同じように「パスカルケース」でスクリーミングスネークケース(全て大文字のスネークケース)が違反にならなかったり、意図通りのチェックをしてくれないことがあります。

プロジェクト単位やソリューション単位の設定ではない

この命名規則の設定はソリューションやプロジェクトのプロパティではなくVisualStudioのオプションにあるため、全てのプロジェクトで同じ設定が適用されてしまいます。UnityのAssetやOSSなど他人が書いたコードを開くと違反が指摘されてしまうことが多々あります。

感想

いくつか残念なところはあるものの個人的にはこれでも十分だと思いました。意図せず命名規則違反をすることが減りましたし、今まで気づいていなかった命名規則違反に気付くことができました。重要度をどれにするかはまだ決まっていませんがしばらくは「エラー」にしてみようと思っています。

環境

4つの頂点座標からRectを求める【C#】

概要

与えられた複数の頂点(Vector2)が矩形(Rect)になるかどうかを判定して処理を分岐させたかったので、与えられた頂点からRectを求める関数を実装してみました。

4頂点から矩形を求める

実装

Rectを求める関数の実装:

using System.Collections.Generic;
using UnityEngine;

public static class RectUtility
{
    /// <summary>
    /// 頂点が矩形を表すならtrueを返し、その情報をresultに格納する
    /// </summary>
    public static bool ToRect(
        out Rect result,
        IList<Vector2> vertices,
        float allowedError = 0      // 許容できる誤差
    )
    {
        result = default;
        if (vertices == null || vertices.Count != 4)
        {
            return false;
        }
        // 0番目の頂点から見て左 or 右にある頂点を特定
        var h = GetHorizontalVertexIndex(vertices, 0, allowedError);
        if (h <= 0) { return false; }
        // 0番目の頂点から見て上 or 下にある頂点を特定
        var v = GetVerticalVertexIndex(vertices, 0, allowedError);
        if (v <= 0) { return false; }
        // 0番目の頂点から見て斜めの位置にある頂点を特定
        var d = GetDiagonalVertexIndex(vertices, 0, allowedError);
        if (d <= 0) { return false; }

        // 位置関係に問題がないかを確認
        if (!(
            Equals(vertices[h].x, vertices[d].x, allowedError) &&
            Equals(vertices[v].y, vertices[d].y, allowedError)
        ))
        {
            return false;
        }
        // 位置やサイズを設定
        result = new Rect(
            Mathf.Min(vertices[h].x, vertices[0].x),
            Mathf.Min(vertices[v].y, vertices[0].y),
            Mathf.Abs(vertices[h].x - vertices[0].x),
            Mathf.Abs(vertices[v].y - vertices[0].y)
        );
        return true;
    }

    /// <summary>
    /// selfIndex番目の頂点から斜めの位置にある頂点のIndexを取得する
    /// </summary>
    private static int GetDiagonalVertexIndex(
        IList<Vector2> vertices,
        int selfIndex,
        float allowedError
    )
    {
        var self = vertices[selfIndex];
        for (int i = 0; i < vertices.Count; ++i)
        {
            if (i == selfIndex) { continue; }

            if (
                !Equals(vertices[i].x, self.x, allowedError) &&
                !Equals(vertices[i].y, self.y, allowedError)
            )
            {
                return i;
            }
        }
        return -1;
    }

    /// <summary>
    /// selfIndex番目の頂点から上 or 下の位置にある頂点のIndexを取得する
    /// </summary>
    private static int GetVerticalVertexIndex(
        IList<Vector2> vertices,
        int selfIndex,
        float allowedError
    )
    {
        var self = vertices[selfIndex];
        for (int i = 0; i < vertices.Count; ++i)
        {
            if (i == selfIndex) { continue; }

            if (Equals(vertices[i].x, self.x, allowedError))
            {
                return i;
            }
        }
        return -1;
    }

    /// <summary>
    /// selfIndex番目の頂点から左 or 右の位置にある頂点のIndexを取得する
    /// </summary>
    private static int GetHorizontalVertexIndex(
        IList<Vector2> vertices,
        int selfIndex,
        float allowedError
    )
    {
        var self = vertices[selfIndex];
        for (int i = 0; i < vertices.Count; ++i)
        {
            if (i == selfIndex) { continue; }

            if (Equals(vertices[i].y, self.y, allowedError))
            {
                return i;
            }
        }
        return -1;
    }

    /// <summary>
    /// 誤差を許容するfloatの比較
    /// </summary>
    private static bool Equals(float a, float b, float allowedError)
    {
        return Mathf.Abs(a - b) <= allowedError;
    }
}

使用例:

var list = new Vector2[]
{
    new Vector2(1, -1),
    new Vector2(1, 0),
    new Vector2(3, -1),
    new Vector2(3, 0)
};
if (RectUtility.ToRect(out Rect rect, list))
{
    Debug.Log(rect);
}

ToRectの第三引数を指定すれば誤差を許容することもできます。

出力結果:

(x:1.00, y:-1.00, width:2.00, height:1.00)

※ 回転した状態の矩形には対応していません

環境

  • Unity 2018.4.2f1
  • VisualStudio 2019

Tilemapの情報をスクリプトから取得する【Unity】

概要

Tilemapの情報をスクリプトで取得して処理したかったので取得方法を調べてみました。

取得方法

タイルがどこに存在するかを取得する

タイルが存在する範囲はTilemap.cellBoundsで知ることができます。この範囲のセルに対してTilemap.HasTileを使うことでどのセルにタイルが存在するかを取得できます。

実装例:

public static void OutputPosition(Tilemap tilemap)
{
    var builder = new StringBuilder();
    var bound = tilemap.cellBounds;
    for (int y = bound.max.y - 1; y >= bound.min.y; --y)
    {
        for (int x = bound.min.x; x < bound.max.x; ++x)
        {
            builder.Append(tilemap.HasTile(new Vector3Int(x, y, 0)) ? "■" : "□");
        }
        builder.Append("\n");
    }
    Debug.Log(builder.ToString());
}

出力例:
f:id:nyama41:20190622221507p:plain

cellBoundsの外側にタイルを置くと自動的にこの範囲は拡張されますが、そのタイルを消しても自動的に縮小されることはありません。縮小したい場合はTilemap.CompressBoundsを使って縮小できます。

タイルにどのSpriteが設定されているかを取得する

タイルにどのSpriteが設定されているか知りたいときはTilemap.GetTile<Tile>を使ってTile型の値を取得しTile.spriteをチェックします。

実装例:

public static void OutputSpriteType(Tilemap tilemap)
{
    // 使われているSpriteをリストアップ
    var bound = tilemap.cellBounds;
    var spriteList = new List<Sprite>();
    for (int y = bound.max.y - 1; y >= bound.min.y; --y)
    {
        for (int x = bound.min.x; x < bound.max.x; ++x)
        {
            var tile = tilemap.GetTile<Tile>(new Vector3Int(x, y, 0));
            if (tile != null && !spriteList.Contains(tile.sprite))
            {
                spriteList.Add(tile.sprite);
            }
        }
    }
    // どの場所でそのSpriteが使われているかを出力
    var builder = new StringBuilder();
    for (int y = bound.max.y - 1; y >= bound.min.y; --y)
    {
        for (int x = bound.min.x; x < bound.max.x; ++x)
        {
            var tile = tilemap.GetTile<Tile>(new Vector3Int(x, y, 0));
            if (tile == null)
            {
                builder.Append("_");
            }
            else
            {
                var index = spriteList.IndexOf(tile.sprite);
                builder.Append(index);
            }
        }
        builder.Append("\n");
    }
    Debug.Log(builder.ToString());
}

出力例:
f:id:nyama41:20190622221524p:plain

特定のタイルがある範囲を取得する

特定のタイルがある範囲を取得したい場合はGetTileは使わずにTilemap.GetCellCenterLocalTilemap.originなどを使って範囲を取得します。
実装:

public static Rect OutputTilePosition(Tilemap tilemap, int x, int y)
{
    var rect = new Rect();
    rect.center = tilemap.GetCellCenterLocal(new Vector3Int(x, y, 0)) + tilemap.origin;
    rect.size = tilemap.cellSize;
    return rect;
}

環境

  • Unity 2018.4.2f1
  • VisualStudio 2019

2Dの地形に3DのColliderを付けてみる【Unity】

概要

少し前の記事で地形は2Dで3Dモデルに2DのColliderを付ける検証をしました。今回は逆に地形の方に3DのColliderを付ける検証をしてみました。3DモデルはRigidbodyのZ方向の移動とXY方向の回転を禁止(Freeze)してRigidbody2Dのような動きしかしないようにしています。また、こちらの問題が発生するためDefault Solver Iterationsを100に変更して検証しています。

今回はTilemapだけでなくSpriteShapeも使っています。

検証

Tilemap + シンプルな3Dオブジェクト

TilemapにはこのようにColliderを付けました。
Tilemapに3DのColliderを付けた
Colliderのz軸方向の厚みが0でも問題なく動くことが多いですが後述する理由により厚みを付けています。

Cylinderなどのオブジェクトは以下のようにFreezeにチェックを入れてRigidbody2Dのような動きをするようにしています。
Rigidbody2Dのような動きをするようにFreeze

これらのオブジェクトを地形に落下させてみるとこのようになります。

特に問題なく動作しています。

Tilemap + 複雑なモデル

次に複雑なモデルを使ってみます。
Rigidbody2Dのような動きをするようにFreeze


これも問題なさそうに見えますが…

3Dモデルに2D Colliderを付けたときと同じ問題

3Dモデルに2DのColliderを付けたときと同じでPerspectiveの問題が発生します。
Sceneビューでは問題なさそうに見えるけど、Gameビューでははみだしている
これもOrthographicにすればはみ出すことはなくなりますが3D感が薄れます。

Colliderのz座標に注意

2DのColliderを使ったときとは違ってz座標にも注意する必要があります。例えば、地形のColliderの厚みを0にすると以下のように一部のColliderが地形と当たらなくなることがあります。
地形のColliderの厚みを0にすると、剣のColliderがz=0ではないため地形と当たらない
これは地形のColliderに厚みを付ければいいだけなので付け忘れないように注意していれば大丈夫です。

SpriteShape

SpriteShapeでも同じように検証してみます。ColliderはBox Colliderを複数組み合わせました。
地形に沿ってBox Colliderを複数付ける


Sprite Shapeでも特に問題なさそうです。

Perspectiveの問題が目立ちにくい分 Sprite Shapeの方がいいかもしれません。
Perspectiveの問題が目立ちにくい

感想

2Dの地形に3DのColliderを付けても大きな問題はなさそうです。

3Dモデルは人型に限定しなければ様々なバリエーションがありアニメが付いていることも多いと考えると、3Dモデルに2DのColliderを付けるよりも2Dの地形に3DのColliderを付けた方が想定外のトラブルでハマる可能性が低くなりそうな気がしました。

環境

  • Unity 2018.4.0f1

FreezeをOnにしたRigidbodyを積み上げると勝手に動くことがある問題を解決する【Unity】

概要

Rigidbodyの動きを制限して2Dゲームのような動きをさせようと検証していたらRigidbodyが不自然な動きをすることがあったので調査してみました。

問題の再現方法

  1. 新規3Dプロジェクトを作る
  2. Terrainを作る
    1. カメラとの位置関係を調整するために座標を(-250,0,-250)にする
    2. 白いままだとわかりにくいので適当なTextureで色を付ける
  3. 落下させるprefabを作る
    1. Cubeを新規作成
    2. Rigidbodyを付ける
    3. ConstraintsをいくつかOnにする
      1. Freeze PositionのZをOn
      2. Freeze RotationのX, YをOn
    4. prefabとして保存
  4. 作ったprefabを、落下したら縦に積み重なるようにいくつか配置
    • 位置や個数によって再現するかどうかが変わる
    • 今回は5つ置いて、それぞれ高さを1, 4, 7, 10, 14にした

Rigidbodyを付けてFreezeをいくつかOnにしたものを、落下したら積みあがるように配置

プレビューするとCubeが勝手に動くのが見えます。

Freezeを全てOffにするとこの問題は発生しなくなります。


対策

1. Default Solver Iterationsを大きくする

Default Solver Iterationsは計算回数を増やすことで精度を高くするもののようです。最大値は255です。この値を大きくするほど不自然な動きをすることが無くなっていきました。

f:id:nyama41:20190616153740p:plain

この値を変更して解決するのでこの問題はバグではなく計算誤差によるものなのかもしれません。

ちなみに、マニュアルを読む限りこの値を大きくすると処理コストが増えそうな気がするのですが、Profilerで見た限り明らかな処理の増加は見られませんでした。わからなかっただけで微妙に増えているのかもしれません。
→ Rigidbodyの数を300まで増やしてRigidbody同士が接触しやすい状態にして計測したらDefault Solver Iterationsが大きい方が明らかに処理が増えていました。(2019/06/17 21:00修正)

2. Sleep Thresholdを大きくする

Sleep Thresholdを増やしても問題が発生しなくなりました。Rigidbodyは運動エネルギーが小さくなったときに動きを止めSleep状態になります。この値を増やすことで勝手に動き出す前にSleep状態に入ったのだと思います。

f:id:nyama41:20190616153757p:plain

ただ、この値を増やしすぎると不自然な状態で動きが止まってしまうこともありました。処理負荷の面では良いですが調整に苦労するかもしれません。

試したけどダメだったこと

ちなみに、以下の変更も試してみましたが問題は解決しませんでした。

  • PhysicsMaterialを付けてFrictionの値を大きくしてもダメ
  • Enable Adaptive ForceをOnにしてもダメ
  • RigidbodyのIterpolateを変更してもダメ
  • RigidbodyのCollision Detectionを変更してもダメ
  • Unity 2019.1.2f1にしてもダメ
  • 2Dでプロジェクトを作ってもダメ

環境

  • Unity 2018.4.0f1

3Dモデルに2DのColliderを付けてみる【Unity】

概要

地形は2Dでキャラクターは3Dモデルを使う2Dゲームを考えているのですが、2DのColliderと3DのColliderは衝突しないので3Dモデルに2DのColliderを付ける検証をしてみました。

検証

1.シンプルな形状

まずはCubeやCylinderなどのシンプルな形状のモデルにColliderを付けてみます。Cubeなら↓こんな感じです。RigidbodyやColliderが2Dのものになっています。

f:id:nyama41:20190610225201p:plain

地形はTilemapを使いました。

これは問題なく使えてそうです。

2.複雑なモデル(複数のCollider)

次にAsset Storeで配布されている複雑なモデルを使ってみます。配布されているモデルには3D用のColliderが付いているのでそれらは削除して2D用のColliderに付け替えます。Colliderは形状に応じて複数使っています。

f:id:nyama41:20190610231258p:plain

2DのColliderを付けるときにGameObjectがX軸、Y軸方向に回転しているとColliderも3次元的に回転してしまうので注意が必要です。

この例でも問題なく使えているように見えます。実はダメなモデルもあったのですがそれについては後述します。

3.動くモデル

アニメに合わせてColliderを動かしてみます。この例ではクジラのしっぽのBoneに体とは別のColliderを付けています。

アニメに合わせて動くようにBoneにColliderを付ける

アニメに合わせてColliderを動かすことも問題なさそうです。

4.うまくいかない例

でも、実は3Dモデルの中でいくつか当たり判定がうまく設定できないものがありました。

ちゃんとColliderが設定されているように見えるのに羽が地面を突き抜けている

これはカメラがPerspectiveになっているため、z軸方向に厚みがあるモデルだと画面の位置によってモデルの見え方が異なることが原因です。

3Dで見るとこうなっている

Sceneビューで見ると全て同じ姿勢に見えるが、Gameビューではそれぞれ微妙に異なって見える

カメラのProjectionをOrthographicにするとこの問題は発生しなくなりますが、それでは見栄えが悪くなり3Dモデルを使っている意味が薄れてしまいます。Cubeで見るとその見栄えの差がわかりやすいかと思います。

Perspective
Orthographic。3D感がかなり乏しい

Orthographicにするのではなくz軸方向に厚みのある部分には当たり判定を付けないとか、そういうモデルを使わないという回避策の方がいいかもしれません。

感想

いくつか問題はありますが3Dモデルに2DのColliderを付けてゲームをつくることもできないことはなさそうです。ただ、今回は簡単な検証をしただけなので他にも問題が出てくる可能性はあります。Orthographicで妥協できるなら比較的安全そうですが、Perspectiveは苦労するかもしれません。

(2019/06/21 21:39追記)
この記事とは逆に 2Dの地形に3DのColliderを付ける検証もしてみました。

環境

  • Unity 2018.4.0f1

ProCamera2Dのカメラが別のシーンに移動してしまう問題を解決する【Unity】

概要

ProCamera2Dのカメラを配置したシーンをAdditiveでロードすると何故かカメラが遷移前のシーンに移動してしまうことがありました。これはProCamera2Dの振動機能(ProCamera2DShake)などを使ったときにカメラの親GameObject (ProCamera2DShakeContainer)が自動で作られる処理の問題のようだったので対策を調べてみました。

f:id:nyama41:20190608160958p:plain

対処方法1: カメラを別GameObjectの子にする

簡単な対処はカメラをルートに置かないことです。ProCamera2DShakeContainerがカメラの親GameObject(CameraContainer)の子になるので同じシーンになります。

f:id:nyama41:20190608161730p:plain

対処方法2: ソースコードを修正する

カメラをルートに置きたい場合はソースコードの修正が必要になりそうです。ProCamera2DShake.csの82行目で"ProCamera2DShakeContainer"という名前のGameObjectをnewしているところを変更します。

変更後のコード

var obj = new GameObject("ProCamera2DShakeContainer");
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(obj, ProCamera2D.gameObject.scene);
_shakeParent = ProCamera2D.transform.parent = obj.transform;

開発者の方も問題を認識してはいるようなのでいずれこういう修正をしなくても済むようになるかもしれません。

環境

  • Unity 2018.4.0f1
  • ProCamera2D 2.6.10
  • VisualStudio 2019