Unity

UnityとC#で読みやすいコードを作ろう【C#1.0〜C#8.0】

はじめに

ゲームって、パフォーマンスを要求されたり膨大なロジックだったりで、コードが煩雑になりがちです。
そこを、C#機能を上手く使ってコードを読みやすくしよう、というのがこの記事の主旨です。
自分の専門分野であるUnityとC#で、読みやすいコードを作るノウハウをまとめました。

パワーくん
パワーくん
読みやすいコードってなんだ?
てんぷら
てんぷら
他の人が見た時に理解しやすいコードだよ
読みやすいコードを書けば、みんなに喜ばれるよ
  • Unity2018・C#7.3想定で書かれてます(C#8はまだ使えないので予想で書いてます)
  • パフォーマンスについては触れません(改善手法についてはこちら)
  • C#をより詳しく知りたい方は C# によるプログラミング入門 が超オススメです☆
【2万行のコードに絶望した僕が考える】ゲーム開発でコードを読みやすくする方法10選自分は、ゲーム開発においてコードを読みやすくするのはとても重要だと考えています。 読みにくいと修正が大変→ ゲームのロジッ...

コメント://, /**/, ///

3通りの書き方があります。

// 1行コメント

/*
 複数行コメント
*/

/// <summary>
/// ドキュメンテーションコメント
/// </summary>

// コメントはコードを理解するのに役立つものなら何でもいいから書こう

// すぐに移行するとテンポが悪いのでディレイを入れる
yield return new WaitForSeconds(delay);

/**/ は長いコードをコメントアウトしたり、/*TEST*/ → /*TEST/ のようなテストコード切り替えに便利

/*
 ...
 1000行くらいある長いコード
 ...
*/

var power = /*TEST*/100/*/1/**/;
var power = /*TEST/100/*/1/**/;

/// はVisualStudioでカーソルを当てた時に情報が表示されて便利

/// <summary>
/// 世界創造
/// </summary>
/// <param name="id">ID</param>
/// <returns>世界</returns>
World CreateWorld(int id) => new World(id);

自分は全てのコメントを // で統一して、短く簡潔に書いて、コード全体を通した見やすさを重視してます。
/// は、いかんせん簡単な説明でも3行必要だったり、1行で書くにしても

/// <summary>最強の世界</summary>
void StrongWorld() { }

// 最強の世界
void StrongWorld() { }

と、// の方が見やすかったりで、最近は使用頻度低めです。
外部公開するライブラリとかでは、ちゃんと /// を書いた方が親切かと思います。

てんぷら
てんぷら
気付いたらファイルの半分がコメントだったことがあるよ
パワーくん
パワーくん
丁寧すぎるのも考えものだな

定数:const, readonly

constreadonlyがあります。

// const:コンパイル時定数
const int       ConstId  = 1;
//const Vector3 ConstPos = Vector3.one; // newするものに使えない

// readonly:読み取り専用変数(staticにすれば実行時定数として扱える)
static readonly int     ReadonlyId   = 1;
static readonly Vector3 ReadonlyPos  = Vector3.one; // newするものにも使える
readonly string         ReadonlyName = "sun";       // staticじゃなくても使える

public Elder()
{
    // readonlyならコンストラクタで書き換え可能
    ReadonlyName = "moon";
}

void Explode()
{
    // constならローカル変数にも使える
    const float pi = Mathf.PI;
}

定数は、定数ファイルにまとめておけば後から値を編集しやすい(あの定数どこにあったっけ?がなくなる)

public static class AppConst
{
    public const string         Version      = "3.2.1";
    public static readonly bool IsAndroid    = Application.platform == RuntimePlatform.Android;
    public static readonly bool IsIOS        = Application.platform == RuntimePlatform.IPhonePlayer;
    public static readonly int  ScreenWidth  = Screen.width;
    public static readonly int  ScreenHeight = Screen.height;
    public const int            CanvasWidth  = 1080;
    public const int            CanvasHeight = 1920;
    public const string         PrefabPath   = "Prefabs/{0}";
}

自分はconstを使えるものはconstにし、それ以外はreadonlyにしています。
constは出来ればpublicにしない方がいいのですが(const のバージョニング問題)、Unityで使う分には特に問題になったことがないので、気にせずpublic constにしてます。実行速度も速くなりますし。
ライブラリとか作る時は、constよりもreadonlyにしておいた方が無難だと思います。

てんぷら
てんぷら
昔はreadonlyだけ使ってたんだけど、友達の助言でconst使ってみたらめちゃくちゃ便利で感謝してるよ
パワーくん
パワーくん
持つべきものは友か

多次元配列:[2,2], [2][]

[,](四角い配列)は全ての列数が一緒で、[][](配列の配列)は逆に列数を変えられます。

// 四角い配列
int[,] cells = new int[2, 2] { { 1, 2 }, { 3, 4 } }; // これが
int[,] cells = new int[,] { { 1, 2 }, { 3, 4 } };    // こうなって
int[,] cells = { { 1, 2 }, { 3, 4 } };               // こう書ける(好きな書き方で)
int[,] cells = 
{ 
    { 1, 2 },
    { 3, 4 } 
};

// 配列の配列
int[][] jags = new int[2][] { new int[] { 1 }, new int[] { 2, 3, 4 } }; // これが
int[][] jags = new int[][] { new[] { 1 }, new[] { 2, 3, 4 } };          // こうなって
int[][] jags = { new[] { 1 }, new[] { 2, 3, 4 } };                      // こう書ける(好きな書き方で)
int[][] jags =
{
    new[] { 1 },
    new[] { 2, 3, 4 } // 列数を変えられる
};

void Start()
{
    var cells = new int[2, 2];
    for(var x = 0; x < cells.GetLength(0); x++)
    {
        for(var y = 0; y < cells.GetLength(1); y++)
        {
            cells[x, y] = 0;
        }
    }

    var jags = new int[2][] { new int[1], new int[3] };
    for(var x = 0; x < jags.Length; x++)
    {
        for(var y = 0; y < jags[x].Length; y++)
        {
            jags[x][y] = 0;
        }
    }
}

基本、四角い配列にした方がパフォーマンスがいいのでそっち使った方がいいです。
それに、四角い配列の方が構造がシンプルで、コードを追いやすいです(シンプルな見た目は人を幸せにします)。

パワーくん
パワーくん
列数を変えるって意外と使われそうだが?
アイテムの数がそれぞれ違うとか
てんぷら
てんぷら
そういう場合はクラスにして1次元配列にした方がいいよ
多次元は難しいから、なるべく分かりやすくしよう

ループ処理:for, foreach, while

よく使うループ処理はこの3つ。

for:インデックスが必要な時

for(var i = 0; i < members.Length; i++)
{
    members[i] = new Member();
}

foreach:要素を列挙したい時

foreach(var member in members)
{
    Debug.Log(member.Name);
}

while:条件を満たす間処理し続けたい時

using(var sr = new StreamReader("story.txt"))
{
    string line = "";
    while((line = sr.ReadLine()) != null)
    {
        Debug.Log(line);
    }
}

自分は、基本foreach、インデックスが必要ならforwhileは出来るだけ使わない、という方針です。
forforeachはどっち使っても大丈夫ですが、記述がスッキリするのでforeachを優先してます(見た目重視)。
whileは無限ループが怖いのでなるべく使わないようにしてます。

てんぷら
てんぷら
パフォーマンスはforの方がいい時があるから、大量ループ処理ではforを使った方がいいよ(モーション負荷やばかったなぁ…)
パワーくん
パワーくん
なるほど、そこらへんは使い分けだな

型推論:var

varで変数宣言すれば、右辺から型を推論してくれます。

// これが         
IEnumerable<string> squareNames       = squares.Select(s => s.name);
VanishingAnimation vanishingAnimation = square.GetComponent<VanishingAnimation>();
Transform transform                   = square.transform;
int connectingId                      = square.ConnectingId;

// こう書ける
var squareNames        = squares.Select(s => s.name);
var vanishingAnimation = square.GetComponent<VanishingAnimation>();
var transform          = square.transform;
var connectingId       = square.ConnectingId;

varを使う上で注意するとしたら、こんな場面

// オブジェクトをnull初期化したい
Square fallingSquare = null;            // これは
var fallingSquare    = default(Square); // こう書けるけどなんか無理やり感(他の変数と揃えるため使ってるけど・・・)

// 右辺とは違う型で受け取りたい(これはvarでは無理)
var transform       = image.rectTransform; // Transformで受け取りたい(RectTransformになってしまう)
Transform transform = image.rectTransform; // こういう場合はちゃんと型を明示する

// こんな泥臭いコード(変数が多い上に独自クラスも相まってカオス。。)
void InitBone(Bone bone)
{
    var cell         = bone.SpriteStudioCell;
    var d2d          = bone.D2dDestructible;
    var avatarBaseX  = cell.Rectangle.x;
    var avatarBaseY  = avatarHeight - cell.Rectangle.y - cell.Rectangle.height;
    var alphas       = d2d.AlphaData;
    var alphaWidth   = d2d.AlphaWidth;
    var alphaHeight  = d2d.AlphaHeight;
    // この後もいろいろ続く...
}

自分は使えるとこ全部varにしてます。コードが見渡しやすくなるメリットはデカイです。
それにvarを使うと、自然と変数名に気を遣うようになるのでオススメです(変数名超大事!)。

パワーくん
パワーくん
ちゃんと型を書いた方が分かりやすくないか?
てんぷら
てんぷら
自分も長年そう思ってたよ。でもコードって、全体の流れで読むから変数名から大体分かっちゃうんだよ。
パワーくん
パワーくん
人間の学習能力ってすごいな

変数名を作る時は codic がオススメです☆

動的型:dynamic

dynamicで変数宣言すれば、動的な型を定義出来ます(コンパイル時に動的コードが生成される)。

var     v = new ResponseHeader();
object  o = new ResponseHeader();
dynamic d = new ResponseHeader();

// dynamicなら定義されてないメンバでもコンパイルエラーにならない(実行時に調べられる)
int userId = v.UserId; // ×
int userId = o.UserId; // ×
int userId = d.UserId; // ○

v.Parse(); // ×
o.Parse(); // ×
d.Parse(); // ○

// 共通レスポンス(今後メンバを追加していきたい)
class ResponseHeader { }

いろんなC#機能を使ってきたけど、いまだにdynamicは使い所が難しい(というか使えてない)。
JsonMVVMで試したことがあるけど、最終的には使わずじまいでした(パフォーマンス優先)。
基本、静的な型にしてエラーを検知した方が安全なので、これだ!っていう時の切り札に。

パワーくん
パワーくん
ダイナミックって俺みたいだな
[意味]動的なさま。力強く生き生きと躍動するさま。「ダイナミックな演技」
てんぷら
てんぷら
ああ、なぜかdynamicに惹かれた理由が分かった気がするよ…

コンパイル結果(IL)を見るなら SharpLab がオススメです☆
【C# によるプログラミング入門】dynamic で何ができるか

null許容値型(Nullable型):int?

値型にnullを追加出来ます。

// intにnullを追加
int? id = null;

// Valueで値を取得(もしくはキャスト)。ただし id = null の場合は例外が発生。
int value = id.Value;
int value = (int)id;

// なのでHasValueで判定する(値がある場合は不要だけど、判定しといた方が無難)
if(id.HasValue) value = id.Value;

// もしくはGetValueOrDefaultなら規定値を返してくれる(どっちの0か区別がつかないけど・・・)
value = id.GetValueOrDefault();

// 一応そのまま判定も出来るけど、複雑なのでちゃんとValueを取得した方がいいと思う
int? a = null;
int? b = 0;
int  c = 0;
int? d = null;
if(a == b) { } // false
if(a == c) { } // false
if(b == c) { } // true
if(a == d) { } // true   ← !?
            
if(a <= b) { } // false
if(a <= c) { } // false
if(b <= c) { } // true
if(a <= d) { } // false  ← !?

null条件演算子で値型の値を取得するのに便利

int? squareY = map.GetSquare(0, 0, 0)?.Y;
if(squareY.HasValue)
{
    int y = squareY.Value;
}

C#8から参照型でもnullを許容するか選べるようになり、nullをなくしていこう、という流れになりつつあります。
その流れには自分も賛成で、あえて値型にnullを追加する必要はないかと思いますが、null条件演算子を使ってく中で自然と使われると思います。

てんぷら
てんぷら
それは10億ドルにも相当する私の誤りだ
null参照を発明した人も今では後悔してるみたいだよ
パワーくん
パワーくん
何もないを表すって大…ぬるぽ
てんぷら
てんぷら
あ!パワーくん止まった!?

【C#8.0】null許容参照型:string a = "", string? a = null

C#8から参照型でもnullを許容するか選べる機能が追加されました(オプションで機能をONにする)。

ザックリしたイメージ(ちゃんとした使い方は参考リンクをご確認ください)

// #nullable でファイルや行単位のnull許容を設定出来る(コンパイラーオプションでプロジェクト全体に設定する方法もある)
#nullable enable               // 有効
//#nullable disable            // 無効
//#nullable restore            // プロジェクト設定に戻す
//#nullable enable annotations // annotations:null許容機能状態かだけ変える
//#nullable enable warnings    // warnings:実際に警告を出す

string  name = null;  // ×:警告が出る(コンパイルエラーになるわけではない)
string  name = "";    // ○:値を割り当てたのでOK
string? name = null;  // ○:null許容にするには後ろに ? を付ける(null許容値型と同じ)
string  name = null!; // ○:null免除演算子(後ろに ! )を使って一時的に警告を無視することも出来る

正直、ものすごい変更点なので、こればっかりは実際使ってみないと分かりません・・・。
なので、ゲームプログラマー視点で思ったことだけ。

  1. SerializeFieldScriptableObjectのような初期値nullだけどエディタで値が入る前提のものはどうする?
  2. #nullable enable
    
    public class Scene : MonoBehaviour
    {
        [SerializeField] string?    message;
        [SerializeField] WorldData? worldData;
        [SerializeField] Transform? ui;
    }
    
    public class WorldData : ScriptableObject
    {
        public string?    WorldName;
        public MapData[]? Maps;
    }
    

    UnityだとSerializeFieldは頻繁に使うので、毎回null許容にする必要があるのと、実際にはエディタで値が入ってからnullになることがない「nullだけどほぼnullじゃない」的な扱いをどうするか?
    あとMonoBehaviourだとコンストラクタが使えないので、クラス変数がnull許容になりやすい気も。

  3. メモリ解放のためのnull初期化
  4. void Dispose()
    {
        world = null;
        map   = null;
        for(var i = 0; i < players.Length; i++) players[i] = null;
        for(var i = 0; i < enemies.Length; i++) enemies[i] = null;
    }
    

    メモリ解放のために、使い終わったらnull初期化してることが多いかと思います。
    特にゲームだとメモリに相当気を遣うので、メモリ優先だと機能を使いたくないって方針になりそうな気も。
    個人的には要所を絞ってメモリ管理すればいいと思ってますが、チーム開発だとルールって大事だからなぁ。

焦点としては「個人開発」と「チーム開発」で分かれるかもです。
個人開発では、null許容参照型をガンガン使っていこうかと思ってます(試してみたい)。
チーム開発では、機能が使えるようになっても当面は様子見になるんじゃないかなぁ。

てんぷら
てんぷら
他の言語でnull非許容を使ったことがあるんだけど、慣れなくて !(警告無視)を使いまくっちゃったよ
パワーくん
パワーくん
当たり前を変えるって難しいんだな

キーワードを変数名に使う:@object = default(object)

変数名の前に@を付けることで、キーワードも変数名に使えます。

キーワードそのものを表しそうな変数の場合に有効

// MVVMのバインドデータを初期化する
void InitializeDynamicObject(M4uBindingObject bo, M4uBindingMember bm)
{
    var memberNames   = bm.MemberPath.Replace('.', '/').Split('/');
    var componentName = memberNames[0];
    var transform     = this.transform;
    var @object       = default(object); // バインドオブジェクトなのでそのまま変数名にobjectを使いたい
}

// G2U4SでC#コードを作る
foreach(var @class in UsingClasses)
{
  //Usings += $"{NewLine}using {usingClass};"; // こう書くより
    Usings += $"{NewLine}using {@class};";     // こう書いた方が直感的
}

普段はちゃんと意味のある名前を付けた方がいいけど、状況次第では使えるかもです。

てんぷら
てんぷら
G2U4SではC#コードを作る関係上、どうしてもキーワードと名前がバッディングしてそんな時役立ったんだ
パワーくん
パワーくん
普段は使えない力を解放したわけか…

デフォルト引数:Method(int a = 0, int b = 0)

メソッド引数にデフォルト値を設定でき、呼び出す時に省略出来ます。

// これが
void Move(float duration, bool isRelative = false) { }
//void Move(bool isRelative = false, float duration) { } // これはダメ(後ろの引数のみOK)

// こう呼べる
Move(1);

// デフォルト値にはconstやdefaultキーワードも使える
const float MoveDuration = 1;
void Move(float duration = MoveDuration, Vector3 pos = default) { }

コード量を減らせる

// これが
void Move()               => Move(1);
void Move(float duration) => Move(duration, null);
void Move(float duration, Action onComplete) { }

// こう書ける
void Move(float duration = 1, Action onComplete = null) { }

onCompleteのようなコールバックは、デフォルト値nullにしておくと便利です(大抵必要な時だけ指定するので)。

てんぷら
てんぷら
const値をデフォルト値にしてあげると分かりやすくなるよね
パワーくん
パワーくん
定数はこんなところでも活かされるのか(ふむふむ)

名前付き引数:Method(b: 1);

メソッドを呼び出す時に、デフォルト値が指定されてるものは名前付きで呼べます。

// これが
void Move(float duration = 1, Vector3 position = default, bool isRelative = false) { }

// こう呼べる
Move(position: Vector3.one);         // 必要なのだけ指定したり
Move(2, isRelative: true);           // 後ろだけ名前付きにしたり
Move(isRelative: true, duration: 2); // 順番を変えられたり
Move(duration: 2, Vector3.one);      // C#7.2からは前だけ名前付きも出来る
//Move(position: Vector3.one, 2);    // ただしこれはダメ(名前なしの順番は守らないといけない)

コードを見やすく出来る

// 位置だけ指定したい・・・(時間はデフォルトでいい)
Move(1, Vector3.one);

// こう書ける
Move(position: Vector3.one);

// あとこの場合は名前いらないけど
var duration = 1;
var position = Vector3.one;
Move(duration, position, true);

// あえて書いた方が見やすい時もある(特にフラグ)
Move(duration, position, isRelative: true);

名前付き引数は、名前が変わった時に呼び出し元と定義の両方を修正しないといけなくて面倒だったりするけど、エラーが出るからすぐに気づくし、それよりもコードが見やすくなる恩恵はデカイので重宝してます。

てんぷら
てんぷら
昔は「引数名って変わりやすいじゃん!使うのやめよう」って思ってたけど、実際使ってみたら全然便利だったよ。。
パワーくん
パワーくん
使ってみないと分からないものだな

参照渡し:Method(ref int a), Method(out int b), Method(in int c)

メソッド引数は基本値渡しですが、refoutinを付けることで参照渡しに出来ます。

// ref:メソッド内で値を変えられる(値の初期化が必要)
var executedId = 0;
Execute(ref executedId);
//  executedId = 1

void Execute(ref int executedId)
{
    executedId = 1;
}

// out:必ずメソッド内で値が変わる(値の初期化は不要。複数の戻り値に便利)
int bonus;
var point = CalculatePoint(out bonus);
var point = CalculatePoint(out var bonus); // C#7からはこう書ける
//  bonus = 5
//  point = 10
     
int CalculatePoint(out int bonus)
{
    bonus = 5;
    return 10;
}     

// in:参照渡しだけど読み取り専用(参照渡しで大きめの値のコピーをなくす)
var bigdata = new Bigdata();
StartEvent(bigdata); // in は不要

void StartEvent(in Bigdata bigdata)
{
    //bigdata = new Bigdata(); // 変更は出来ない
}

struct Bigdata { } // 巨大な構造体

値の初期化、キーワード指定、メソッド内での書き換え、の比較

int a = 0;
int b;
int c = 0;
Method(ref a);
Method(out b);
Method(c);

void Method(ref int a)
{

}

void Method(out int b)
{
    b = 0;
}

void Method(in int c)
{
    //c = 0;
}

ref:変数をメソッド内で変えたい

// yだけはメソッド内で判定して決めたい
int x = 0, y = 0, z = 0;
SearchPosition(x, ref y, z);
//  x = 0, y = 1, z = 0

void SearchPosition(int x, ref int y, int z) => y = 1;

out:C#6まで→複数の戻り値、C#7以降→TryParse系メソッド(C#7以降の複数の戻り値はタプルの方がスマート)

// C#6までは複数の戻り値はoutで作ってた
List<Square> vanishedSquares;
List<int>    vanishedConnectingCounts;
SearchVanishedSquares(out vanishedSquares, out vanishedConnectingCounts);

// C#7以降はこう書けるようになった(1行で書ける)
SearchVanishedSquares(out var vanishedSquares, out var vanishedConnectingCounts);

// ただしC#7以降の複数の戻り値はタプルの方がスマート
var (vanishedSquares, _) = SearchVanishedSquares();

// C#7以降でのoutはTryParse系メソッドに便利(直感的なコードになる)
if(int.TryParse("1", out var value)) { }

refoutはたまに使います。inは下手に使うと火傷しそうなので、いい感じの使い方を模索中。
あとrefはC#7から他の場所でもいろいろ使えるようになったようです(参考リンク参照)。

パワーくん
パワーくん
refはC#7から他の場所でもいろいろ使えるようになった
ってなんか適当な説明だな(ピキッ)
てんぷら
てんぷら
ごめんごめん。参照渡しってパフォーマンス改善に有効なんだけど、状況によって変わってくるから、ここでは説明を省かせてもらったよ(怖ぇ…)

タプル:(int a, int b) Method

複数の戻り値に便利な、名前のない型です(C#7から)。

// タプルは普通の型と同じとこに大体書ける(一部制限はある)
public class Scene : MonoBehaviour
{
    // 定数
    static readonly (int x, int y) ReadonlyPos = (1, 1);

    // フィールド
    (int x, int y) pos;

    // プロパティ
    public (int x, int y) Pos => pos;

    void Start()
    {
        // 値の書き換え
        pos.x = 1;
        pos.y = 1;
        pos   = (2, 2);
        pos   = (x: 3, y: 3); // 名前も付けられる(見やすく出来る)

        // 戻り値は変数で受け取ったり
        var newPos = Calculate(pos);
        var x      = newPos.x;
        var y      = newPos.y;

        // ダイレクトにこう書ける
        (var x, var y) = Calculate(pos);
        var (x, y)     = Calculate(pos);
        var (x, _)     = Calculate(pos); // 不要な値は _ で破棄出来る

        // 中身の全比較は == や != が便利
        var srcPos = (x: 1, y:       1);
        var dstPos = (x: 1, y: (byte)1);

        // これが
        if(srcPos.x == dstPos.x && srcPos.y == dstPos.y) { }
        if(srcPos.x != dstPos.x || srcPos.y != dstPos.y) { }

        // こう書ける(暗黙的型変換による比較も可能)
        if(srcPos == dstPos) { } // true
        if(srcPos != dstPos) { } // false
    }

    // メソッド
    (int x, int y) Calculate((int x, int y) pos)
    {
        var x = pos.x + 1;
        var y = pos.y + 1;
        return (x, y);
    }
}

複数の戻り値はoutよりタプルがオススメ

// これが
SearchVanishedSquares(out var vanishedSquares, out var _);
if(vanishedSquares.Count > 0) { }

// こう書ける(戻り値が自然な位置に来る)
var (vanishedSquares, _) = SearchVanishedSquares();
if(vanishedSquares.Count > 0) { }

void SearchVanishedSquares(out List<Square> vanishedSquares, out List<int> vanishedConnectingCounts)
{
    vanishedSquares          = null;
    vanishedConnectingCounts = null;
}

(List<Square> vanishedSquares, List<int> vanishedConnectingCounts) SearchVanishedSquares()
{
    return (null, null);
}

タプルは使い始めたばかりで、まだまだ応用が効きそう。

てんぷら
てんぷら
以前、タプルにするかクラスにするか迷ったことがあるんだけど、今思えば戻り値みたいなすぐに使いたい場合がタプルにはいいんだろうね
パワーくん
パワーくん
まぁサンプル見て、長く使うにはなんか分かりづらくなりそうだもんなぁ

拡張メソッド:Method(this Transform t, int a)

既存の型に後からメソッドを追加出来ます(静的メソッドをインスタンスメソッドのように呼べます)。

// 名前空間の指定が必要(内部的には静的メソッドだから)
using Unisharp.Extensions;

namespace Unisharp
{
    public class Scene : MonoBehaviour
    {
        IPeco   peco;
        Dir     dir;
        Vector3 pos;

        void Start()
        {
            // これが(静的メソッドを)
            UnisharpExtensions.SetPositionX(transform, 1);

            // こう書ける(インスタンスメソッドのように呼べる)
            transform.SetPositionX(1);
            peco.Fly();
            dir.IsUp();
            pos.SetX(1);
        }
    }

    public interface IPeco { }
    public enum Dir { Up, Right, Down, Left }
}

namespace Unisharp.Extensions
{
    // 拡張メソッドはstaticクラスにしか定義出来ない
    public static class UnisharpExtensions
    {
        // 参照型の拡張メソッド
        public static void SetPositionX(this Transform t, float x)
        {
            t.position = new Vector3(x, t.position.y, t.position.z);
        }

        // interfaceの拡張メソッド(LINQで使われてる)
        public static void Fly(this IPeco peco) { }

        // enumの拡張メソッド
        public static bool IsUp(this Dir dir) => (dir == Dir.Up);

        // 構造体の拡張メソッド(C#7.2から構造体も参照渡しで作れるようになった)
        public static void SetX(ref this Vector3 v, float x) => v.x = x;
    }
}

Unityのクラスを拡張するのに便利(特に構造体のプロパティ)

// こうしたいけど構造体のプロパティなので直接書き換えられない
//transform.position.x = 1;

// 変数で受け取る必要がある(2行必要)
var pos = transform.position;
transform.position = new Vector3(1, pos.y, pos.z);

// こう書ける(1行で書ける)
transform.SetPositionX(1);

// {型名}Etensionsと名付けておくと分かりやすい
public static class TransformExtensions
{
    public static void SetPositionX(this Transform t, float x) => t.position = new Vector3(x, t.position.y, t.position.z);
    public static void SetPositionY(this Transform t, float y) => t.position = new Vector3(t.position.x, y, t.position.z);
    public static void SetPositionZ(this Transform t, float z) => t.position = new Vector3(t.position.x, t.position.y, z);
}

DOTweenは拡張メソッドで定義されてて使いやすい

transform.DOMove(Vector3.one, 1).SetDelay(1).SetEase(Ease.Linear).SetLoops(-1, LoopType.Yoyo);

拡張メソッドは便利な機能だけど、実態としてはpublic staticな静的メソッドなので濫用は控えた方がいいです。
基本はUnityのクラスで頻繁に使用する静的メソッドを拡張メソッド化する、って感じがいいかと思います。

てんぷら
てんぷら
拡張メソッドを使い始めたばかりの頃は作りまくってたけど、定義がバラバラになって分かりづらくなったよ…
パワーくん
パワーくん
力の代償だな
Unityで超便利なTweenアセット「DOTween」が好き ゲーム開発ではいろいろなアニメーションが必要になる。 キャラクターモーション バトルエフェクト UIアニメーション...

デリゲートとラムダ式:delegate void Method, Action, Func<bool>

デリゲートはメソッドへの参照を表す型です。ラムダ式を使えばデリゲートが使いやすくなります。

// デリゲート定義
delegate void AnimationComplete(GameObject go);

// デリゲート変数
AnimationComplete onAnimationComplete;

void Start()
{
    // デリゲート登録
    onAnimationComplete = UpdateCharacter;

    // もしくはこんな感じで2つ以上登録したり、削除したりも出来る
    //onAnimationComplete += UpdateCharacter;
    //onAnimationComplete += UpdateMonster;
    //onAnimationComplete -= UpdateCharacter;
    //onAnimationComplete -= UpdateMonster;

    // アニメーション再生
    PlayAnimation();
}

// デリゲートメソッド(アニメーション完了後にこのメソッドが呼ばれる)
void UpdateCharacter(GameObject go) => Debug.Log("完了しまうま");

void PlayAnimation()
{
    // アニメーション完了後にデリゲートを呼ぶ
    onAnimationComplete?.Invoke(gameObject); // "完了しまうま"
}

//---------------------------------------------------------------
// ただし、今ではラムダ式を使う方が一般的(デリゲートをちゃんと定義するのは稀)
//---------------------------------------------------------------

// Actionを使えばデリゲート定義がなくなる
Action<GameObject> onAnimationComplete;
//Action           onAnimationComplete; // 引数なし
//Action<int>      onAnimationComplete; // 引数1個
//Action<int, int> onAnimationComplete; // 引数2個

void Start()
{
    // あとこう書くより
    onAnimationComplete = UpdateCharacter;
    PlayAnimation();

    // 引数に渡した方がスマート(デリゲート変数がなくなる)
    PlayAnimation(UpdateCharacter);

    // さらにラムダ式を使えばダイレクトにこう書ける(デリゲートメソッドがなくなる)
    PlayAnimation(go => Debug.Log("完了しまうま"));

    // 2行以上必要な場合は { } で括る
    PlayAnimation(go =>
    {
        Debug.Log("完了しまうま");
        go.SetActive(false);
    });
}

void PlayAnimation(Action<GameObject> onComplete = null)
{
    onComplete?.Invoke(gameObject);
}

// Funcは戻り値あり(最後の引数が戻り値の型)
Func<Animation, bool>  isPlayingAnimation;
//Func<bool>           isPlayingAnimation; // 引数なし・戻り値1個
//Func<int, bool>      isPlayingAnimation; // 引数1個・戻り値1個
//Func<int, int, bool> isPlayingAnimation; // 引数2個・戻り値1個
        
void Start()
{
    isPlayingAnimation = animation => animation.isPlaying;

    isPlayingAnimation = animation =>
    {
        Debug.Log(animation.name);
        return animation.isPlaying;
    };

    var nowAnimation = GetComponent<Animation>();
    if(isPlayingAnimation(nowAnimation)) { }
}

Actionはこの形で使うことが多い(コールバックラムダ式)

void Start()
{
    var nowAnimation = GetComponent<Animation>();
    PlayAnimation(nowAnimation, () => 
    {
        // アニメーション完了後の処理
    });
}

IEnumerator PlayAnimation(Animation animation, Action onComplete = null)
{
    animation.Play();
    yield return new WaitWhile(() => animation.isPlaying); // 地味にFuncが使われてる(Funcはこの使い方)
    onComplete?.Invoke();
}

FuncはLINQで使う

var ids  = new int[] { 12, 1, 7, 3, 5 };
var text = ids.Where(id => id <= 5).Select(id => $"ID:{id}").First();
// Where(Func<int, bool> predicate)   条件を書く
// Select(Func<int, string> selector) 変換結果を書く

パワーくん
パワーくん
いまいちラムダ式のメリットが分からないな
てんぷら
てんぷら
自分もそうだった。慣れないうちは「〜した時」って場合に、Actionを使うといいよ。あとは無理して使わなくても、他の人のコード見てればだんだん分かってくると思う。

ローカル関数:void Start(){ void execute(){} }

C#7から関数内で関数を書ける機能が追加されました(今まではラムダ式で同様のことをしてました)。

void Start()
{
    // ローカル関数(書き方は普通のメソッドと一緒)
    void execute()
    {
        Debug.Log("完了しまうま");
    }
    execute();

    // ラムダ式(今まではこっちで書いてた)
    Action execute = () =>
    {
        Debug.Log("完了しまうま");
    };
    execute();

    // ローカル関数とラムダ式の違い

    // 定義前に呼び出せる
    nailpunch();
    void nailpunch() { }

    Action nailpunch = () => { };
    nailpunch();

    // 再帰呼び出しが簡潔に書ける
    void nailpunch(int count)
    {
        if(count < 5) nailpunch(count + 1);
    }

    Action<int> nailpunch = null;
    nailpunch = count =>
    {
        if(count < 5) nailpunch(count + 1);
    };

    // イテレーターを使える
    IEnumerator nailpunch(Animation animation)
    {
        yield return new WaitWhile(() => animation.isPlaying);
        Debug.Log("いただきます");
    }

    //Func<Animation, IEnumerator> nailpunch = animation =>
    //{
    //    yield return new WaitWhile(() => animation.isPlaying);
    //    Debug.Log("いただきます");
    //};
            
    // ジェネリックやデフォルト引数を使える
    void nailpunch<T>(T count = default) where T : struct { }
    //Action<T> nailpunch = (count = default) => { };

    // C#8からはstaticを付けてクロージャを避けられるようになった
    var twin = 2;
    int nailpunch(int count)            => count;        // ○
    int twinNailpunch(int count)        => count * twin; // ○
    static int nailpunch(int count)     => count;        // ○
  //static int twinNailpunch(int count) => count * twin; // ×
}

関数内で関数を作りたい場合は、ローカル関数の方が使いやすそうです(クロージャの最適化もあるよう)。
ただ自分はなかなかこういう書き方をしないですが・・・(見づらくなるのでちゃんとメソッドを定義する)。

てんぷら
てんぷら
関数内で関数は再帰呼び出しで使ったことがあるくらいかなぁ。でも思い返してみるとコストに見合ってたかは微妙かも。
パワーくん
パワーくん
そういうことしたい人も中にはいるんじゃね?

LINQ:users.Select(user => user.Id).Where(id => id == 1).First()

LINQを使うことでループ処理が簡潔に書けます。

// System.LinqをインポートするとLINQメソッドが使えるようになる
using System.Linq;

// 5以下の最初のidを "ID:{id}" というテキストにしたい
var ids = new int[] { 12, 1, 7, 3, 5 };

// これが
var text = "";
foreach(var id in ids)
{
    if(id <= 5)
    {
        text = $"ID:{id}"; // "ID:1"
        break;
    }
}

// こう書ける
var text = ids.Where(id => id <= 5).Select(id => $"ID:{id}").First(); // "ID:1"
// Where :5以下のidに絞って( 1, 3, 5 )
// Select:idをテキストに変換し( "ID:1", "ID:3", "ID:5" )
// First :最初のものを取得( "ID:1" )

よく使う使い方

class Monster
{
    public int  Id      { get; set; }
    public bool IsBoss  { get; set; }
    public bool IsAlive { get; set; }
    public bool IsDead  => !IsAlive;
}

Monster[] monsters;

// 絞ったり変えたり(基本操作)
// Where :条件を満たす要素を返す
// Select:新しい要素に変える
var aliveMonsterIds = monsters.Where(m => m.IsAlive).Select(m => m.Id);
foreach(var aliveMonsterId in aliveMonsterIds) { }
            
// リスト内に1つしかないものを探す
// FirstOrDefault:条件を満たす最初の要素か既定値を返す(Firstだとない場合に例外が吐かれるので大体FirstOrDefaultを使う)
var boss = monsters.FirstOrDefault(m => m.IsBoss);
if(boss != null) { }
            
// 簡易な全体チェック
// Any:条件をいずれか満たすか
// All:条件を全て満たすか
if(monsters.Any(m => m.IsAlive)) { } // まだ誰か生きてるか
if(monsters.All(m => m.IsDead))  { } // 全員倒したか

// {id1}, {id2}, {id3}... みたいなデバッグ出力(ザッと中身を確認したい時)
// Aggregate:要素を集計する (1, 2) → (2, 3) → (3, 4)...
var debugText = monsters.Select(m => m.Id.ToString()).Aggregate((id1, id2) => $"{id1}, {id2}");
Debug.Log(debugText); // "1, 2. 3, 4, 5"

// カンマ区切り文字列をint配列にしたい
// ToArray:配列に変える
var monsterIds = "1, 2, 3, 4, 5".Split(',').Select(id => int.Parse(id)).ToArray();

// 操作するからリストで持ちたい
// ToList:リストに変える
var animations = GetComponentsInChildren<Animation>().ToList();

// 図鑑で使うからソートしたい
// OrderBy          :昇順
// OrderByDescending:降順
var ascMonsters  = monsters.OrderBy(m => m.Id);           // Id:1, 2. 3, 4, 5
var descMonsters = monsters.OrderByDescending(m => m.Id); // Id:5, 4. 3, 2, 1

自分はユーザ環境ではLINQは使わず、ユーザに影響しない環境(ツール開発など)では多用してます。
誤ってメモリを食いやすいからです(よくあるのは無駄にToArray()ToList()してしまうとか)。
正しく使えれば強力なのですが、実際、以前作ったソシャゲでボトルネックになったこともあって、ルールを制限して使用しています。

パワーくん
パワーくん
「ユーザ環境では使わない」という制約と
「ユーザ環境で使った場合は命を絶つ」という誓約か
てんぷら
てんぷら
え・・・?

クラスのカタチ:using, namespace, class, Property, Method

よく使うクラスのカタチをまとめました。

using UnityEngine;                 // usingディレクティブ
using System;
using static UnityEngine.Debug;    // using staticディレクティブ
using Random = UnityEngine.Random; // usingエイリアス

namespace Unisharp // 名前空間
{
    public class Scene : MonoBehaviour // クラス
    {
        const int SkillCount              = 100;         // 定数:const
        static readonly Vector2 PowerSize = Vector2.one; // 定数:static readonly

        [SerializeField] Powerkun powerkun; // 変数
        SceneType type;
        Skill[] skills = new Skill[SkillCount];
        
        public bool IsStarted    { get; private set; } // プロパティ
        public SceneType Type    { get => type; set => type = value; }
        public Powerkun Powerkun => powerkun;
        
        void Start() // メソッド
        {
            for(var i = 0; i < skills.Length; i++)
            {
                skills[i] = new Skill();
            }

            var powerSkill = GetSkill(Random.Range(0, skills.Length)); // UnityEngine.Random
            powerkun.Init(PowerSize, powerSkill);

            IsStarted = true;
            Log("StartingOver"); // Debug.Log
        }

        public Skill GetSkill(int idx) => skills[idx];
    }
}

名前空間:クラス等を名前空間ごとに分けて管理出来る(フォルダ分けに似た仕組み)

// 名前空間を分けることでクラス名の重複を避けられる
namespace Unisharp
{
    public class Scene : MonoBehaviour
    {
        void Start()
        {
            var battlePlayer = new Battle.Player(); // Unisharp.Battle.Player がフルネームだけど親名は省略出来る
            var storyPlayer  = new Story.Player();
        }
    }
}

namespace Unisharp.Battle
{
    public class Player { } // バトル担当が作ったプレイヤークラス
}

namespace Unisharp.Story
{
    public class Player { } // ストーリー担当が作ったプレイヤークラス
}

// 名前空間は便利だけど、分けすぎると探すのが大変(フォルダ分けしすぎて逆に管理しづらいのと一緒)
namespace Unisharp.Common    { } // なんかいろいろ入ってそう・・・
namespace Unisharp.Manager   { }
namespace Unisharp.Character { } // Player/Enemy関連も混ざってたり。。
namespace Unisharp.Player    { }
namespace Unisharp.Enemy     { }
namespace Unisharp.UI        { } // UI全部?

// プログラマ3人で「アプリ全般」「バトル」「ストーリー」で分担しよう(このくらいシンプルで十分)
namespace Unisharp        { }
namespace Unisharp.Battle { }
namespace Unisharp.Story  { }

// 【重要】 Unisharp という一番上の名前は頻繁に使われるので超大事(開発コードや愛着のある名前にするのがオススメ)
namespace Maze     { } // 迷路を題材にしたゲームだから Maze
namespace G2U4S    { } // アセット名をそのまま名前にすれば利用者も分かりやすい
namespace Unisharp { } // 左手から「Unity」、右手から「C#」 ・・・ Unisharp!

usingディレクティブ:名前空間の指定を省略出来る

using UnityEngine;
using Unisharp.Battle;

namespace Unisharp
{
    public class Scene : MonoBehaviour
    {
        void Start()
        {
            var player = new Player(); // Unisharp.Battle.Player → Player
            Debug.Log("StartingOver"); // UnityEngine.Debug      → Debug
        }
    }
}

using staticディレクティブ:型名の指定を省略して静的メンバを呼べる

using UnityEngine;
using static UnityEngine.Debug;
using static Unisharp.AppConst;
using static Unisharp.Dir;

namespace Unisharp
{
    public class Scene : MonoBehaviour
    {
        Dir dir;

        void Start()
        {
            Log("StartingOver"); // Debug.Log         → Log
            if(Version == 1) { } // AppConst.Version  → Version
            if(IsEditor)     { } // AppConst.IsEditor → IsEditor
            if(dir == Up)    { } // Dir.Up            → Up
        }
    }
}

namespace Unisharp 
{
    public static class AppConst
    {
        public const int            Version  = 1;
        public static readonly bool IsEditor = Application.isEditor;
    }
    
    public enum Dir { Up, Right, Down, Left }
}

usingエイリアス:名前空間または型の別名を付けれれる

// usingした時に同名クラスがあった場合・・・
using UnityEngine; // UnityEngine.Random
using System;      // System.Random

//Random.Range(0, 3);           // ×:こう書きたいけど2つあって分からない
UnityEngine.Random.Range(0, 3); // ○:こう書く必要がある

// エイリアスを付ければ解決
using UnityEngine;
using System;
using Random = UnityEngine.Random;

Random.Range(0, 3); // Random = UnityEngine.Random となってるのでOK

プロパティ:外部公開する変数をメソッドを通してアクセスするようにした方法(C#ではそれを機能化した)

public class Scene : MonoBehaviour 
{
    int id;

    // 変数は直接 public にせず Get{変数名} Set{変数名} というメソッドを通してアクセスするのがセオリー
    public int GetId()
    { 
        return id;
    }
    public void SetId(int id)
    {
        this.id = id;
    }

    // プロパティはそれをこう書けるようにした機能
    public int Id
    {
        get // GetId()
        {
            return id;
        }
        set // SetId(int value)
        {
            id = value;
        }
    }

    // 自動プロパティを使えば変数の定義がいらなくなる(コンパイラが __id みたいな変数を勝手に作ってくれる)
    public int Id
    {
        get; // get { return __id;  }
        set; // set { __id = value; }
    }

    // あとは概ねいろんな書き方があるってだけ(後からいろいろ追加されてった)
    public int Id { get { return id;  } } // getだけ可能
    public int Id { set { id = value; } } // setだけ可能
    public int Id { get; set; }           // 長いので1行で書いちゃう
    public int Id
    {
        get         { return id;  }
        private set { id = value; }       // getとsetでアクセサレベルを変えられる
    }
    public int Id { get; } = 1;           // 【C#6.0】自動プロパティに初期値が指定出来るようになった
    public int Id { get { return id; } }  // 【C#6.0】getのみのプロパティが
    public int Id => id;                  // こう書けるようになった( => の簡易文法 )
    public int Id                         // 【C#7.0】両方とも => が使えるようになった
    {
        get => id;
        set => id = value;
    }

    // 短い書き方をまとめると
    public int Id { get; set; }                     // 変数いらない
    public int Id => id;                            // 変数の取得だけしたい
    public int Id { get => id; set => id = value; } // 変数をラップしたい
    public int Id                                   // 設定だけ処理を書きたい
    {
        get => id;
        set
        {
            if(value <= 0) return;
            id = value;
        }
    }
}

=>:メソッドやプロパティなどの簡易文法(関数が1つの式で表せる場合に使える)

public class Scene
{
    int[] ids = { 1, 2, 3 };

    // インデクサー
    public int this[int i] => ids[i];                                // 【C#6.0】
    public int this[int i] { get => ids[i]; set => ids[i] = value; } // 【C#7.0】

    // プロパティ
    public int[] Ids => ids;                             // 【C#6.0】
    public int[] Ids { get => ids; set => ids = value; } // 【C#7.0】

    // コンストラクタ
    public Scene(int[] ids) => this.ids = ids; // 【C#7.0】

    // メソッド
    public int  GetId(int i)    => ids[i];         // 【C#6.0】
    public void Log(string mes) => Debug.Log(mes); // 【C#6.0】
}

Unityだとクラスをコンポーネントにするか(MonoBehaviourにするか)、普通のクラスにするか迷うと思います。
正直、一概には言えないんですけど、自分はこのようにしてます。

  • コンポーネント:数が少なくてUnityエディタで値を確認したいもの(Powerkunみたいな個体とか)
  • 普通のクラス:数が多くてStartUpdateが不要なもの(Skillみたいなデータとか)

コンポーネントは便利ですが、その分メモリを食います。なので、そこらへんはバランスかなぁって思います。
個人的には、よっぽど大規模なゲームでない限りコンポーネント指向でいいかと思います。

てんぷら
てんぷら
コンポーネント作るの楽しくて肝心のゲーム制作が全然進まなかったことがあるよ
パワーくん
パワーくん
本末転倒だろ
Unityでのコンポーネント指向のあれこれここ最近コンポーネント指向にハマってたことがあって、あれこれいろいろ試してた。 そこらへんをまとめておく。 己の肉体と技術に...

staticクラス:static class

staticメンバのみ持てるクラスです。

// 定数
public static class Const
{
    public const string         Version = "3.2.1";
    public static readonly bool IsIOS   = Application.platform == RuntimePlatform.IPhonePlayer;
}

// ユーティル
public static class Util
{
    [System.Diagnostics.Conditional("DEBUG")]
    public static void Log(string mes) => Debug.Log(mes);
}

// 拡張メソッド
public static class Extensions
{
    public static void SetPositionX(this Transform t, float x) => t.position = new Vector3(x, t.position.y, t.position.z);
    public static void SetPositionY(this Transform t, float y) => t.position = new Vector3(t.position.x, y, t.position.z);
    public static void SetPositionZ(this Transform t, float z) => t.position = new Vector3(t.position.x, t.position.y, z);
}
    
// ネイティブプラグイン
public static class Plugin
{
#if UNITY_IOS
    [DllImport("__Internal")] static extern int getUsedMemory();
#endif
    public static long GetUsedMemory()
    {
#if UNITY_IOS
        return getUsedMemory();
#else
        return GC.GetTotalMemory(false) + Profiler.usedHeapSizeLong;
#endif
    }
}

staticで定義が必要なものは用途ごとに分けることで、コードが分かりやすくなります。

てんぷら
てんぷら
Utilクラスのようなpublic staticメソッドが初心者の頃は多かったけど、今ではほとんどなくなって、成長を実感したよ
パワーくん
パワーくん
staticおじさんにはなれなかったか…
てんぷら
てんぷら
なんでちょっと残念そうなの?

partialクラス:partial class

クラスを複数ファイルに分割出来ます(コンパイル時に1つのクラスに結合されます)。

MapEnum.cs

// Auto-generated files
public enum Dir { Up, Right, Down, Left }

public static partial class Subenum
{
    public static bool IsUp(this Dir value)    => (value == Dir.Up);
    public static bool IsRight(this Dir value) => (value == Dir.Right);
    public static bool IsDown(this Dir value)  => (value == Dir.Down);
    public static bool IsLeft(this Dir value)  => (value == Dir.Left);
}

CharacterEnum.cs

// Auto-generated files
public enum Motion { Idle, Move, Attack }

public static partial class Subenum
{
    public static bool IsIdle(this Motion value)   => (value == Motion.Idle);
    public static bool IsMove(this Motion value)   => (value == Motion.Move);
    public static bool IsAttack(this Motion value) => (value == Motion.Attack);
}

G2U4SのSubenumpartialクラスを使用しています。
Subenumはひとことでいうと、enumの拡張メソッドをツールで自動生成した機能です。
自動生成だとクラスが増えるのでメモリ節約のためと、拡張メソッドはクラス名がそれほど重要ではないので、この形にしました。

ただしpartialクラスは安易に使うとコードが追いづらくなるので注意です(基本使わない方がいい)。
同名クラスが散らばってると、目的のものを探すのに苦労します。。

てんぷら
てんぷら
昔、Windowsフォームアプリケーションでダブルクリックした時に勝手にイベントが作られてたけど、partialを使ってたんだね〜
パワーくん
パワーくん
なるほど、自動生成コードと実際に触るコードを分ければ、やりたいことに集中出来るわけだな

【MicrosoftDocs】partial 型
大規模なプロジェクトや、Windows フォーム デザイナーで自動生成されるコードを処理する場合に役立ちます。

〝 G2U4S 〟G2Uで読み込んだゲームデータを1クリックでScriptableObjectに変換日本語 / English G2U4Sは、SpreadsheetやExcelで作成したゲームデータを1クリックでScr...

ジェネリック:<T> where T : class

型だけ違って処理内容が同じクラスやメソッドを定義出来ます。

// ジェネリッククラス
class UnisharpObject<T>
{
    // どんな型が来ても処理内容を同じに出来る
    T value;

    public UnisharpObject(T value) => this.value = value;
}

// 型制約
class UnisharpObject<T> where T : Component
{
    T value;

    public UnisharpObject(T value) => this.value = value;

    // 制約を設けることでComponentのメンバが使える
    public GameObject GameObject => value.gameObject;
}

class UnisharpObject<T> where T : struct       { } // 値型
class UnisharpObject<T> where T : class        { } // 参照型
class UnisharpObject<T> where T : IList        { } // 指定インターフェイス(継承先も)
class UnisharpObject<T> where T : Component    { } // 指定クラス(継承先も)
class UnisharpObject<T> where T : new()        { } // 引数なしコンストラクタを要求(実行速度が遅いので注意)
class UnisharpObject<T> where T : unmanaged    { } // 【C#7.3】アンマネージ型(unsafeコードで使用)
class UnisharpObject<T> where T : Enum         { } // 【C#7.3】System.Enum型
class UnisharpObject<T> where T : Delegate     { } // 【C#7.3】System.Delegate型
class UnisharpObject<T> where T : notnull      { } // 【C#8.0】null非許容型(C#8からnull非許容の機能が追加された)
class UnisharpObject<T> where T : class, IList { } // 2つ以上も指定出来る

// ジェネリックインターフェイス
interface IUnisharpList<T> where T : IList<T> { }

// ジェネリックメソッド
T LoadResource<T>(string path) where T : Object => Resources.Load<T>(path);

// こんな感じで使う
var uo = new UnisharpObject<GameObject>(gameObject);
var go = LoadResource<GameObject>("Unisharprefab");

ジェネリッククラス:MVVMでいろいろな型をバインド

M4uProperty<int>    userId   = new M4uProperty<int>();
M4uProperty<string> userName = new M4uProperty<string>();

public int    UserId   { get { return userId.Value;   } set { userId.Value   = value; } }
public string UserName { get { return userName.Value; } set { userName.Value = value; } }

class M4uProperty<T>
{
    public T Value { get; set; }
}

ジェネリックメソッド:型だけ違うPrefabを読み込む(リソースマネージャ)

T LoadPrefab<T>(Transform parent = null) where T : Component
{
    var path = $"Prefabs/{typeof(T).Name}";
    return Instantiate(Resources.Load<T>(path), parent);
}

ジェネリックはやりすぎると抽象化しすぎて分かりづらくなるので注意です(ご使用は慎重に)。
同じ構造で型だけ違うキャラクターをジェネリッククラスで作る、とかやり始めるとヤバイです(過去の自分)。
そういう場合はコンポーネント指向の検討を。

パワーくん
パワーくん
俺とお前じゃ全然違うからジェネリックとか無理じゃね?
てんぷら
てんぷら
ぐっ…(なんもいえねぇ)
Unityでのコンポーネント指向のあれこれここ最近コンポーネント指向にハマってたことがあって、あれこれいろいろ試してた。 そこらへんをまとめておく。 己の肉体と技術に...

コレクション:List<int>, Dictionary<int, string>

よく使うジェネリックコレクションです。適切に使えばコードの意図が分かりやすくなります。

// List:インデックスアクセスしたい時(最初のオブジェクトを取得したいなぁ)
var squares     = new List<Square>();
var firstSquare = squares[0];

// Dictionary:キーアクセスしたい時(キャッシュしておいてメモリ節約したいなぁ)
var cacheTextures = new Dictionary<string, Texture>();
var loadTexture   = default(Texture);
if(cacheTextures.ContainsKey(fileName))
{
    loadTexture = cacheTextures[fileName];
}
else
{
    loadTexture = Resources.Load<Texture>(fileName);
}

// Stack:先頭に追加→先頭から取り出したい時(アンドゥ:直近のスタンプを1つ消したいなぁ)
var stampIds = new Stack<int>();
stampIds.Push(1);
stampIds.Push(2);
stampIds.Push(3);
foreach(var stampId in stampIds)
{
    // 3
    // 2
    // 1
}
// 3
var removedStampId = stampIds.Pop(); // 取得と同時に削除される

// Queue:後ろに追加→先頭から取り出したい時(コマンドを予約しといて順次実行させたいなぁ)
var commandIds = new Queue<int>();
commandIds.Enqueue(1);
commandIds.Enqueue(2);
commandIds.Enqueue(3);
foreach(var commandId in commandIds)
{
    // 1
    // 2
    // 3
}
// 1
var executedCommandId = commandIds.Dequeue(); // 取得と同時に削除される

// SortedList/SortedDictionary:キーでソートしておいて高速検索したい時(ボトルネックを解消したいなぁ)
// SortedList                 :小メモリ・インデックスアクセス可能
// SortedDictionary           :追加・削除速い
var items = new SortedList<int, string>();
items.Add(2, "微笑みの爆弾");
items.Add(1, "希望の底");
items.Add(3, "アクアウィタエ");
var selectedItem = items[2]; // 微笑みの爆弾

// HashSet/SortedSet:値が重複せず、数学の集合操作のようなことをやりたい時(重複なし保存をしたいなぁ)
// SortedSet        :ソートされる
var createdDataTypes = new HashSet<string>();
createdDataTypes.Add("int");
createdDataTypes.Add("GameData");
createdDataTypes.Add("int");
foreach(var type in createdDataTypes)
{
    // int
    // GameData
} 

// LinkedList:追加・削除を高速化したい時(自分は使ったことないが、参考リンクが分かりやすい)
var cards          = new LinkedList<string>();
var babyDragon     = cards.AddFirst("BabyDragon");
var alligatorSword = cards.AddBefore(babyDragon, "AlligatorSword");
var polymerization = cards.AddLast("Polymerization");
foreach(var card in cards)
{
    // AlligatorSword
    // BabyDragon
    // Polymerization
}

いろいろありますが、あとはメモリや速度にどれだけ気を使いたいかによるかもです。
自分はなるべくデータ構造をシンプルにしたいので、基本ListDictionaryしか使わないです。

てんぷら
てんぷら
StackQueueを使えた時の型にハマった感が好きなんだけど、結局改修していくうちにListが使いたくなるんだよね〜
パワーくん
パワーくん
使い慣れたものの方が応用利くしな

nameof演算子:string a = nameof(a)

型、変数、メンバの名前を取得出来ます。

name = nameof(Transform);          // "Transform"
name = nameof(transform);          // "transform"
name = nameof(transform.position); // "position"
name = nameof(transform.Find);     // "Find"

こうしておけばコンポーネント名が変わった時に気付ける

// コンポーネントメニュー
[AddComponentMenu("M4u2/" + nameof(M4uBinding))]
public class M4uBinding : MonoBehaviour { }

// コンポーネント名になってるPrefabを読み込む
var square = Resources.Load<Square>(nameof(Square));

G2U4Sではテンプレートファイルと変数名を一緒にしてエラーを検知出来るようにした

// これが
ScriptableObjectTemplate = File.ReadAllText($"{templatesDir}/ScriptableObjectTemplate.txt");
      
// こう書ける
ScriptableObjectTemplate = File.ReadAllText($"{templatesDir}/{nameof(ScriptableObjectTemplate)}.txt");

ScriptableObjectTemplate.txt

// Auto-generated files
{0}

namespace {1}
{
    /// <summary>
    /// {2}
    /// </summary>
    public class {3} : ScriptableObject
    {
{4}
    }
}

nameofを使用すると名前が変わった時にエラーが出るのでミスを防げます。
名前の一貫性を助けるのにも便利です。

てんぷら
てんぷら
まぁやりすぎると修正めんどくさくなるけどね
パワーくん
パワーくん
てんぷらは適当なのかキッチリしてるのか分からんな
てんぷら
てんぷら
うん、よく言われる笑
〝 G2U4S 〟G2Uで読み込んだゲームデータを1クリックでScriptableObjectに変換日本語 / English G2U4Sは、SpreadsheetやExcelで作成したゲームデータを1クリックでScr...

null条件演算子:a?.b

nullでなければ〜、のようなnullチェックを?で繋げて書けます。

// これが
if(map != null) cell = map.Cell;

// こう書ける
cell = map?.Cell;

// nullでなければメソッドを呼ぶ、ということも出来る
map?.Cell.SetPosition(Vector3.zero);

// ?を繋げて書くことも出来る(値型の場合はnull許容値型になることに注意)
Renderer renderer = map?.Cell?.Renderer;
Vector3? position = map?.Cell?.Position;

// デリゲートはこう書ける
if(onMapTap != null) onMapTap();
onMapTap?.Invoke();

// インデクサーにも使える(map != null、map[0, 0] != null。見づらいけど・・・)
var renderer = map?[0, 0]?.Renderer;

// ただしUnityのオブジェクトはnullチェックが拡張されてるので注意(破棄された場合の挙動が怪しい)
//var renderer = destructibleObject?.GetComponent<Renderer>();

簡易なnullチェックや、デリゲートに使うと便利です(デリゲートは大抵nullチェックが入るので)。

パワーくん
パワーくん
こういう機能が入るくらいだから皆よっぽどnullに苦しめられてきたんだな
てんぷら
てんぷら
まじでそう。何度nullチェック忘れてゲームがフリーズしたことか。。

【C#8.0】null合体代入演算子:a ??= b

左辺がnullだったら右辺を入れる、という演算子です。

// これが
if(instance == null) instance = this;

// こう書ける
instance ??= this;

プロパティアクセス時にGetComponentしたい

// いつでも必要ではないので使う時に初期化したい
SpecialSkill specialSkill;

// これが
public SpecialSkill SpecialSkill
{
    get
    {
        if(specialSkill == null) specialSkill = GetComponent<SpecialSkill>();
        return specialSkill;
    }
}

// こう書ける
public SpecialSkill SpecialSkill => specialSkill ??= GetComponent<SpecialSkill>();

nullなら初期化する、という用途に便利です。

てんぷら
てんぷら
地味に便利だよ、これ
パワーくん
パワーくん
のちのちのコード量に効いてくるってやつか

文字列整形:@, string.Format, $, $@, StringBuilder

特殊な文字列整形の方法です。

@(逐語的文字列):\ をそのまま使える(通常は \\ のエスケープシーケンスにする必要がある)

path =  "C:\\Program Files\\Unity\\Unity.exe";
path = @"C:\Program Files\Unity\Unity.exe";
//      "C:\Program Files\Unity\Unity.exe"
            
// ただし @ の場合 " は "" にする
json =  "{ \"name\": \"Neterro\" }";
json = @"{ ""name"": ""Neterro"" }";
//      "{ "name": "Neterro" }"

// @ の場合、改行がそのまま使える
text = "心が正しく形を成せば想いとなり" + System.Environment.NewLine +
       " 想いこそが実を結ぶのだ";
text =
@"心が正しく形を成せば想いとなり
 想いこそが実を結ぶのだ";

string.Format:文字列をフォーマット出来る

hand = 1;
path = "Assets/ScriptableObjects/hand_" + hand + ".asset";
path = string.Format("Assets/ScriptableObjects/hand_{0}.asset",    hand);
path = string.Format("Assets/ScriptableObjects/hand_{0:00}.asset", hand); // 0埋めとかも出来る(書式指定)
//     "Assets/ScriptableObjects/hand_1.asset"
//     "Assets/ScriptableObjects/hand_01.asset"

$(文字列挿入):ダイレクトに挿入出来る(後でstring.Formatに置き換えられる)

path = string.Format("Assets/ScriptableObjects/hand_{0}.asset", hand);
path = $"Assets/ScriptableObjects/hand_{hand}.asset";
path = $"Assets/ScriptableObjects/hand_{hand:00}.asset"; // string.Formatと同じ書式指定を使える
//      "Assets/ScriptableObjects/hand_1.asset"
//      "Assets/ScriptableObjects/hand_01.asset"

// ただし $ の場合 { } は {{ }} にする
json =  "{ \"name\": \"Neterro\" }";
json = $"{{ \"name\": \"Neterro\" }}";
//      "{ "name": "Neterro" }"

$@:同時に使える( \ を使いつつ挿入出来る)

unity = "Unity_2018.4";
path  = $@"C:\Program Files\{unity}\Unity.exe";
//        "C:\Program Files\Unity_2018.4\Unity.exe"

StringBuilder:文字列生成コストを抑えられる(stringだと結合の度に新しい文字列が生成される)

// 通信の度に巨大な文字列を生成するような場合
id   = 1;
log  = "PvNUd3QSLC6F9kBytZNnUz84x3eYJqakWw7Wrehs4Ai985AEWKRcEAUpdeRxXMEMn4RitDHZV5zb5Xw4DSHHzmJMwP6kh3";
json = $"{{ \"id\": {id}, \"log\": \"{log}\" }}";
//      "{ "id": 1, "log": "PvNUd3QSLC6F9kBytZNnUz84x3eYJqakWw7Wrehs4Ai985AEWKRcEAUpdeRxXMEMn4RitDHZV5zb5Xw4DSHHzmJMwP6kh3" }"

// StringBuilderにすればメモリ節約になる
var sb = new StringBuilder(); // 非同期でないならクラス変数にした方が尚良い(毎回 new せずに済む)
sb.Append("{ \"id\": ");
sb.Append(id);
sb.Append(", \"log\": \"");
sb.Append(log);
sb.Append("\" }");
json = sb.ToString();
// "{ "id": 1, "log": "PvNUd3QSLC6F9kBytZNnUz84x3eYJqakWw7Wrehs4Ai985AEWKRcEAUpdeRxXMEMn4RitDHZV5zb5Xw4DSHHzmJMwP6kh3" }"

個人的な使い分けですが

  • @:ほとんど使ってないです(/Applications/Unity/Unity.app のようなパス指定にしますし・・・)
  • string.Format:あらかじめ定数としてフォーマット文字列を定義したい場合に使います
  • $:よく使います
  • StringBuilder:よっぽどパフォーマンス改善したい時だけ使います(見づらいですし・・・)
てんぷら
てんぷら
メモリが少ない環境でガンガン文字列連結してたらメモリが枯渇したことがあるよ。StringBuilderに救われたよ。
パワーくん
パワーくん
文字列って意外とメモリ食うんだな

属性:[SerializeField]

publicprivateのようなクラスやメンバへの追加情報です。

属性の使用

// 属性はクラスやフィールドなど、対象にしたいところの前に [属性名(パラメータ)] で指定する

// Serializable属性:クラスをシリアル化する(Inspectorで表示出来るようになる)
[Serializable]
public class Parameter { }

// RequireComponent属性:コンポーネント追加時、引数で指定したコンポーネントも追加する(必須コンポーネントの指定)
[RequireComponent(typeof(RectTransform))] // コンストラクタ引数でコンポーネントを指定
public class Canvas : MonoBehaviour { }

// CreateAssetMenu属性:Assets/Create に ScriptableObject 作成メニューが出来る
[CreateAssetMenu(fileName = "GameData")] // 名前付きパラメータでファイル名を指定
public class GameData : ScriptableObject { }
    
public class Scene : MonoBehaviour
{
    // SerializeField属性:フィールドをシリアル化する(Inspectorで表示出来るようになる)
    [SerializeField]           int id;
    [SerializeField][ReadOnly] int id; // 属性は2つ以上も指定可能
    [SerializeField, ReadOnly] int id; // カンマで繋げても書ける(上と意味は一緒)

    // DllImport属性:ネイティブプラグインのメソッドを使えるようにする
    #if UNITY_IOS
    [DllImport("__Internal")] static extern int getUsedMemory();
    #endif

    // Conditional属性:DEBUGシンボルがない時はメソッド呼び出しを無視出来る
    [Conditional("DEBUG")]
    void Log(string mes) => Debug.Log(mes);
}

属性の作成

// 属性はAttributeクラスを継承して作る。{属性名}Attribute というクラス名にするのが一般的
// AttributeTargets:属性対象
// AllowMultiple   :同じ対象に複数回指定可能か
// Inherited       :継承するか(抽象クラスの時に使う)
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public class SaveFieldAttribute : Attribute { }

[SaveField]            int id; // 使う時は Attribute 語尾は省略出来る
[SaveField, SaveField] int id; // AllowMultiple = true ならこれが可能

// ただし属性を自作することは稀。Unityの場合はほとんど拡張可能な定義済み属性を使う

// Unityエディタで閲覧だけ可能にする属性(PropertyAttributeを継承することでInspector表示をカスタマイズ出来る)
public class ReadOnlyAttribute : PropertyAttribute { }

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.PropertyField(position, property, label);
        EditorGUI.EndDisabledGroup();
    }
}
#endif

属性の操作

// 属性はリフレクションで操作可能(属性を自作することが稀なので操作することも少ないけど)
    
// SaveField属性:属性を付けたフィールドのみセーブデータの対象とする属性
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
public class SaveFieldAttribute : Attribute { }

public class SaveData
{
    [SaveField] int    id   = 1;
    [SaveField] string name = "怪獣";
    string comment = "海がみたい 人を愛したい"; // コメントはセーブデータに含めない
}

public class Scene : MonoBehaviour
{        
    SaveData saveData = new SaveData();

    void Save()
    {
        var fields = saveData.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
        foreach(var field in fields)
        {
            // 属性が付いてたらセーブする
            if(Attribute.IsDefined(field, typeof(SaveFieldAttribute)))
            {
                var fieldName  = field.Name;
                var fieldValue = field.GetValue(saveData);
                Debug.Log($"{fieldName} = {fieldValue}");
                // id   = 1
                // name = 怪獣
            }

            // 属性を取得する場合はこう書く
            var saveField = Attribute.GetCustomAttribute(field, typeof(SaveFieldAttribute)) as SaveFieldAttribute;
        }
    }
}

SerializeField属性を使えば、初期化コードがなくなってコード量を減らせる(Unityエディタで値を設定する)

// これが
Story         story;
Animation     animation;
AudioListener sound;

void Start()
{
    story     = GetComponent<Story>();
    animation = GetComponent<Animation>();
    sound     = GetComponent<AudioListener>();
}

// こう書ける(Unityエディタで値を設定する)
[SerializeField] Story         story;
[SerializeField] Animation     animation;
[SerializeField] AudioListener sound;

Unityはコンポーネント指向なので、SerializeField属性を使うのがオススメです。
Unityエディタで値の確認も出来るようになるので、デバッグもしやすくなります。
ただし名前を変えたりすると値の参照がなくなってしまうことがあるので注意が必要です(特にAssetBundle)。

パワーくん
パワーくん
特別な能力を付け足すって感じだな
てんぷら
てんぷら
それそれ。一朝一夕では身につかないんだ。
◆ Squares ◇ Unityでボクセルパズルを作ろう! 〜塗り絵アプリのその先へ〜 WebGL / Android / GitHub マップタップ:スクエアの移動 落下地点長押し:スクエアの高速落下 画面ス...

呼び出し元情報:Method([CallerMemberName] string a = "")

メソッド引数にCallerInfo属性を付けると、呼び出し元情報をコンパイラから自動取得出来ます。

void Start()
{
    var chara   = new Character(); // コンストラクタ
    var charaId = chara.Id;        // プロパティ
    chara.Execute();               // メソッド
}

class Character
{
    int id;

    public int Id => GetId();

    public Character() => Init();

    // コンストラクタから呼び出し
    void Init(
        [System.Runtime.CompilerServices.CallerFilePath]   string callerFilePath   = "", 
        [System.Runtime.CompilerServices.CallerLineNumber] int    callerLineNumber = 0,
        [System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = "")
    {
        // callerFilePath   = "/Applications/workspace/Unisharp/Assets/Unisharp/Scene.cs"
        // callerLineNumber = 14
        // callerMemberName = "ctor"
    }
        
    // プロパティから呼び出し
    int GetId(
        [System.Runtime.CompilerServices.CallerFilePath]   string callerFilePath   = "", 
        [System.Runtime.CompilerServices.CallerLineNumber] int    callerLineNumber = 0,
        [System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = "")
    {
        // callerFilePath   = "/Applications/workspace/Unisharp/Assets/Unisharp/Scene.cs"
        // callerLineNumber = 12
        // callerMemberName = "Id"
        return id;
    }

    // メソッドから呼び出し
    public void Execute(
        [System.Runtime.CompilerServices.CallerFilePath]   string callerFilePath   = "", 
        [System.Runtime.CompilerServices.CallerLineNumber] int    callerLineNumber = 0,
        [System.Runtime.CompilerServices.CallerMemberName] string callerMemberName = "")
    {
        // callerFilePath   = "/Applications/workspace/Unisharp/Assets/Unisharp/Scene.cs"
        // callerLineNumber = 5
        // callerMemberName = "Start"
    }
}

MVVMでメンバー名を自動取得

public int Progress
{
    get { return progress; }
    set
    {
        // これが
        // SetMember(ref progress, value, nameof(Progress));

        // こう書ける
        SetMember(ref progress, value);
    }
}

void SetMember<T>(ref T member, T value, [CallerMemberName] string memberName = null) { }

ログ出力してバグを追いたい

// どのメソッドから呼ばれた場合にバグが発生する?(発生頻度が低くてブレイクも使いづらい)
void A() => PlayAnimation();
void B() => PlayAnimation();
void C() => PlayAnimation();
        
void PlayAnimation([CallerMemberName] string callerMethod = "")
{
    Debug.Log(callerMethod);
    // "B" と出たらBに原因がある!
}

てんぷら
てんぷら
そんなに使わないけど、バグ修正で助かったので紹介した
パワーくん
パワーくん
頭の片隅にでも置いとくといいかもな
M4u 〜 あとがき M4u 目次 MVVM 4 uGUI – uGUIにMVVM(Model-View-ViewModel)パターンを導入 ...

プリプロセス:#define, #if, #region, #pragma

#で始まるコンパイラへの特別命令です。

#define:シンボルを定義出来る
#if:シンボルを使って条件付きコンパイルが出来る

// DEBUG というシンボルを定義(ファイルの先頭で定義が必要)
// シンボルは「そのファイル内だけ」有効(Unityプロジェクト全体で使いたい場合は Player Settings > Scripting Define Symbols)
#define DEBUG

using UnityEngine;

public class Scene : MonoBehaviour
{
    // #if DEBUG 〜 #endif で括ると DEBUG がある時だけコンパイルされる = デバッグ時以外はコードを削除出来る
#if DEBUG
    void Log(string mes) => Debug.Log(mes);
#endif

    // #elif, #else, &&, ||, ! も使える(UNITY_* はUnityで標準定義されてるシンボル)
#if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR
    void LogMobile(string mes) => Debug.Log(mes);
#elif UNITY_WEBGL && !UNITY_EDITOR
    void LogWeb(string mes) => Debug.Log(mes);
#else
    void Log(string mes) => Debug.Log(mes);
#endif
}

#region:VisualStudioで折り畳める

public static class AppUtil
{
    // #region コメント 〜 #endregion で括ると、括った範囲が折り畳めるようになる
    #region アプリ全体で使う関数
    [System.Diagnostics.Conditional("DEBUG")]
    public static void Log(string mes) => Debug.Log(mes);
    #endregion

    // こんな感じで用途分けに使える(ただしコードが短くなるわけではないのでやりすぎ厳禁)
    #region ネイティブプラグイン
    #if UNITY_IOS
    [DllImport("__Internal")] static extern int getUsedMemory();
    #endif
    public static long GetUsedMemory()
    {
        #if UNITY_IOS
        return getUsedMemory();
        #else
        return GC.GetTotalMemory(false) + Profiler.usedHeapSizeLong;
        #endif
    }
    #endregion
}

#pragma warning:警告の有効・無効を設定出来る

// SerializeFieldで値が割り当てられてないよ警告を消す
#pragma warning disable 649   // 無効
//#pragma warning restore 649 // 有効

#ifは不要なコードを削除出来るので、ゲームにおいては必須機能です(不正防止に繋がる)。
ただ個人的にはコードが汚くなりやすいので、あまり好きな機能ではありません・・・。
#ifが必要なのはこんな場面(ポイントは絶対に消さないといけないコードか)。

デバッグコード(コードが残ってると解析された時に不正に繋がる)

#if DEBUG
void DebugSkill()
{
    // デバッグ処理がたくさん書かれてる(悪意ある第三者に改竄されるとチートの危険が)
}
#endif

エディタ拡張やネイティブプラグイン(物理的にコンパイルエラーが出る)

// Unityエディタで閲覧だけ可能にする属性(#if UNITY_EDITOR で括らないとビルド出来ない)
public class ReadOnlyAttribute : PropertyAttribute { }

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(ReadOnlyAttribute))]
public class ReadOnlyDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginDisabledGroup(true);
        EditorGUI.PropertyField(position, property, label);
        EditorGUI.EndDisabledGroup();
    }
}
#endif

逆に、それ以外はこんな代替案もあるよーというのを上げておきます。

Conditional属性を使う

// これが
#if DEBUG
Debug.Log("パイルバンカー");
#endif

// こう書ける
Log("パイルバンカー");
    
// Conditional属性を使えば、シンボルがない時にメソッド呼び出しを無視できる
// 【重要】ただしLogメソッド自体は残ることに注意(メソッド内に固有処理を書かないこと)
[System.Diagnostics.Conditional("DEBUG")]
void Log(string mes) => Debug.Log(mes);

フラグに変える

// これが
#if TUTORIAL
Debug.Log("無敵バリヤー");
#endif

// こう書ける
if(IsTutorial) Debug.Log("無敵バリヤー");

// フラグとして持てば見慣れたif文で使える
// 【重要】ただしフラグは解析されて書き換えられやすいので注意(見られても問題ないものをフラグにする)
#if TUTORIAL
const bool IsTutorial = true;
#else
const bool IsTutorial = false;
#endif

パワーくん
パワーくん
#if だとコードが汚くなりやすいのか?
てんぷら
てんぷら
ネストが見づらかったり長いスコープが作られたり…
もちろん不要コードをなくすって大事なことだけど、ほんのちょっとでいいから読みやすさも大切にして欲しいな
【エビでもわかる】オセロプログラミング
〜オセロを作りながらゲームのプログラムを学ぼう〜
「Unityで初めてゲームを作ってみたい!」

そんな人のためにこの本を作りました。
オセロを一から作りながら実践形式でプログラムを学べる本です。
すでに完成したプロジェクトを説明するのではなく、実際に作りながら説明していきます。
一緒に手を動かしながら、プログラムを覚えていきましょう🌟