目次
- はじめに
- コメント://, /**/, ///
- 定数:const, readonly
- 多次元配列:[2,2], [2][]
- ループ処理:for, foreach, while
- 型推論:var
- 動的型:dynamic
- null許容値型(Nullable型):int?
- 【C#8.0】null許容参照型:string a = "", string? a = null
- キーワードを変数名に使う:@object = default(object)
- デフォルト引数:Method(int a = 0, int b = 0)
- 名前付き引数:Method(b: 1);
- 参照渡し:Method(ref int a), Method(out int b), Method(in int c)
- タプル:(int a, int b) Method
- 拡張メソッド:Method(this Transform t, int a)
- デリゲートとラムダ式:delegate void Method, Action, Func<bool>
- ローカル関数:void Start(){ void execute(){} }
- LINQ:users.Select(user => user.Id).Where(id => id == 1).First()
- クラスのカタチ:using, namespace, class, Property, Method
- staticクラス:static class
- partialクラス:partial class
- ジェネリック:<T> where T : class
- コレクション:List<int>, Dictionary<int, string>
- nameof演算子:string a = nameof(a)
- null条件演算子:a?.b
- 【C#8.0】null合体代入演算子:a ??= b
- 文字列整形:@, string.Format, $, $@, StringBuilder
- 属性:[SerializeField]
- 呼び出し元情報:Method([CallerMemberName] string a = "")
- プリプロセス:#define, #if, #region, #pragma
はじめに
ゲームって、パフォーマンスを要求されたり膨大なロジックだったりで、コードが煩雑になりがちです。
そこを、C#機能を上手く使ってコードを読みやすくしよう、というのがこの記事の主旨です。
自分の専門分野であるUnityとC#で、読みやすいコードを作るノウハウをまとめました。
読みやすいコードを書けば、みんなに喜ばれるよ
- Unity2018・C#7.3想定で書かれてます(C#8はまだ使えないので予想で書いてます)
- パフォーマンスについては触れません(改善手法についてはこちら)
- C#をより詳しく知りたい方は C# によるプログラミング入門 が超オススメです☆
コメント://, /**/, ///
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
const
とreadonly
があります。
// 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;
}
}
}
基本、四角い配列にした方がパフォーマンスがいいのでそっち使った方がいいです。
それに、四角い配列の方が構造がシンプルで、コードを追いやすいです(シンプルな見た目は人を幸せにします)。
アイテムの数がそれぞれ違うとか
多次元は難しいから、なるべく分かりやすくしよう
ループ処理: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
、インデックスが必要ならfor
、while
は出来るだけ使わない、という方針です。
for
とforeach
はどっち使っても大丈夫ですが、記述がスッキリするので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
は使い所が難しい(というか使えてない)。
Json
やMVVM
で試したことがあるけど、最終的には使わずじまいでした(パフォーマンス優先)。
基本、静的な型にしてエラーを検知した方が安全なので、これだ!っていう時の切り札に。
[意味]動的なさま。力強く生き生きと躍動するさま。「ダイナミックな演技」
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条件演算子を使ってく中で自然と使われると思います。
【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免除演算子(後ろに ! )を使って一時的に警告を無視することも出来る
正直、ものすごい変更点なので、こればっかりは実際使ってみないと分かりません・・・。
なので、ゲームプログラマー視点で思ったことだけ。
SerializeField
やScriptableObject
のような初期値null
だけどエディタで値が入る前提のものはどうする?- メモリ解放のための
null
初期化
#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
許容になりやすい気も。
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};"; // こう書いた方が直感的
}
普段はちゃんと意味のある名前を付けた方がいいけど、状況次第では使えるかもです。
デフォルト引数: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)
メソッド引数は基本値渡しですが、ref
、out
、in
を付けることで参照渡しに出来ます。
// 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)) { }
ref
とout
はたまに使います。in
は下手に使うと火傷しそうなので、いい感じの使い方を模索中。
あとref
はC#7から他の場所でもいろいろ使えるようになったようです(参考リンク参照)。
refはC#7から他の場所でもいろいろ使えるようになった
ってなんか適当な説明だな(ピキッ)
【Squares】Game.cs / Map.cs
【M4u2】M4uConvertBool.cs
【C# によるプログラミング入門】参照渡し
【C# によるプログラミング入門】注意: in 引数を使ってもコピーが発生する場合
タプル
タプル:(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のクラスで頻繁に使用する静的メソッドを拡張メソッド化する、って感じがいいかと思います。
デリゲートとラムダ式: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()
してしまうとか)。
正しく使えれば強力なのですが、実際、以前作ったソシャゲでボトルネックになったこともあって、ルールを制限して使用しています。
「ユーザ環境で使った場合は命を絶つ」という誓約か
【M4u2】M4uHierarchyIcon.cs / M4uBindingEditor.cs
【Unity】LINQのパフォーマンス検証 – KAYAC engineers' blog
LINQ 拡張メソッド一覧 | JOHOBASE
クラスのカタチ: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
みたいな個体とか) - 普通のクラス:数が多くて
Start
やUpdate
が不要なもの(Skill
みたいなデータとか)
コンポーネントは便利ですが、その分メモリを食います。なので、そこらへんはバランスかなぁって思います。
個人的には、よっぽど大規模なゲームでない限りコンポーネント指向でいいかと思います。
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
メソッドが初心者の頃は多かったけど、今ではほとんどなくなって、成長を実感したよ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のSubenum
はpartial
クラスを使用しています。
Subenum
はひとことでいうと、enum
の拡張メソッドをツールで自動生成した機能です。
自動生成だとクラスが増えるのでメモリ節約のためと、拡張メソッドはクラス名がそれほど重要ではないので、この形にしました。
ただしpartial
クラスは安易に使うとコードが追いづらくなるので注意です(基本使わない方がいい)。
同名クラスが散らばってると、目的のものを探すのに苦労します。。
partial
を使ってたんだね〜【MicrosoftDocs】partial 型
大規模なプロジェクトや、Windows フォーム デザイナーで自動生成されるコードを処理する場合に役立ちます。
ジェネリック:<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);
}
ジェネリックはやりすぎると抽象化しすぎて分かりづらくなるので注意です(ご使用は慎重に)。
同じ構造で型だけ違うキャラクターをジェネリッククラスで作る、とかやり始めるとヤバイです(過去の自分)。
そういう場合はコンポーネント指向の検討を。
コレクション: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
}
いろいろありますが、あとはメモリや速度にどれだけ気を使いたいかによるかもです。
自分はなるべくデータ構造をシンプルにしたいので、基本List
かDictionary
しか使わないです。
Stack
やQueue
を使えた時の型にハマった感が好きなんだけど、結局改修していくうちに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
を使用すると名前が変わった時にエラーが出るのでミスを防げます。
名前の一貫性を助けるのにも便利です。
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]
public
やprivate
のようなクラスやメンバへの追加情報です。
属性の使用
// 属性はクラスやフィールドなど、対象にしたいところの前に [属性名(パラメータ)] で指定する
// 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)。
呼び出し元情報: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に原因がある!
}
プリプロセス:#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
だとコードが汚くなりやすいのか?もちろん不要コードをなくすって大事なことだけど、ほんのちょっとでいいから読みやすさも大切にして欲しいな
〜オセロを作りながらゲームのプログラムを学ぼう〜