本記事は CA Tech Lounge Advent Calendar 2025 4日目の記事となります。
はじめに
Unityにおける設計の参考となる指針として、とりすーぷ氏(@toRisouP)の設計レベルの定義が存在しますが、具体的にどのようなコードがどの設計レベルに該当するのでしょうか?
本記事シリーズでは、簡単な将棋ゲームを具体例にリファクタリングを繰り返し、より保守性や安全性、パフォーマンスの高いコードを目指します。
本記事では筆者が設計レベルを独自解釈して進めていきます。
そのため参考記事の設計レベルと必ずしも一致しない場合があります。
1つの設計の手法としてお楽しみください。
ロードマップ
Part1 : 体裁を整える ←この記事
Part2 : データをプログラムで保持する
Part3 : オブジェクトに分割する
Part4 : 依存の方向性を整える
以下作業中(変わる可能性あり)
Part5 : ロジックと演出を分離する
Part6 : インターフェースを導入する
Part7 : ModelをUnityから切り離す
Part8 : 安全性を考える
Part9 : パフォーマンスを考える
Part10 : 拡張性があるか検証する(オンライン対応する)
対象読者
- はじめてチーム開発をする方
- ゲームプロジェクトの設計やリファクタリングの具体例をみたい方
筆者の環境
- Ubuntu 22.04.5 LTS
- Unity6.0(Unity6000.0.60f1)
使用するゲーム
本記事用に作成した将棋ゲームを使用します。
リポジトリ
最初の段階(レベル0)のソースコード
この時点で基本的なルールは実装され(打ち歩詰めや千日手などを除く)、将棋として遊べるようにはなっています。
しかし、チームで開発したり、機能を拡張していくとなった際に問題が発生しやすい状態になっています。
このプロジェクトのコードのどこが問題になりやすいのか、ポイントを見ながらリファクタリングをしていきます。
Part1である今回は、設計の前段階として取り組めることについて語ります。
Part1 : 体裁を整える
設計の修正の前段階としてすぐに取り入れることのできることがいくつかあります。
これらを習慣化することでコードを読みやすくすることができるようになります。
1-1 コメントを書く
コメントは素早く仕様を把握するための重要な要素です。
- 難しいコードだったとしてもコメントさえあればそれを読み解く時間を減らせます
- 他の人に説明するようにコメントを丁寧に書くことで自分の頭を整理することもできます
コメントは2種類のものを書くようにします。
1-1-1 ドキュメントコメントを書く
C#ではクラスや関数, 変数の定義に対してその仕様を説明する、ドキュメントコメントを書くことができます。
/// <summary>
/// タイトルに戻るボタン
/// </summary>
[SerializeField] private Button _backToTitleButton;
ドキュメントコメントはカーソルを合わせるなどの動作をした際に表示されます。
VisualStuio等を使用している場合、クラスや関数の前で///を入力するとドキュメントコメントのベースが生成されます。
ドキュメントコメントには引数の説明やコード例等、詳しく書くことができますが、最低限どんなものなのか説明を書いておくとカーソルを合わせた際にコメントを読むことができるようになります。
/// <summary>
/// ここに概要を書く(ここだけでも書くようにする!)
/// </summary>
/// <param name="position">ここに引数の説明を書く</param>
/// <param name="clickedPiece">ここに引数の説明を書く</param>
/// <returns>ここに戻り値の説明を書く</returns>
public bool TryGetClickedPosition(out Vector3 position, out Piece clickedPiece)
{
実際の作業内容
1-1-2 処理の説明コメントを書く
処理についても一定の間隔で説明するコメントを書くことをおすすめします。
// pPrefabをVector2.zeroに生成する
var p = Instantiate(pPrefab, Vector2.zero, Quaternion.identity);
// pを_pieceListに追加する
_pieceList.Add(p);
好みにもよると思いますが、上記のようにpPrefabやpとそのままコメントを書いてしまうと実際何をしているのかイマイチ掴みにくいです。
pPrefabやpが何を抽象化したものなのか伝わるように、下記のようにそのコードをゲームの絵面にした時にどんな部分を表しているのか考えてコメントするように心がけると良いと思います。
// 将棋の駒を原点に生成し、駒リストに追加する
var p = Instantiate(pPrefab, Vector2.zero);
_pieceList.Add(p);
-
if等の条件を説明する
条件は複雑になったとき、すぐに論理を読み解くことは難しいです。
また、想定通りの論理になっているかどうかもコメントがない状態だと判断できません。
このような意味から特にコメントが重要な箇所だと言えるかと思います。
// 移動の前後どちらかで相手陣地に入っていて、
// まだ成っていなくて、
// 今ターンに盤上に出た駒でなく、
// 成れる種類の駒であれば 成る
if(isInPromotionZone &&
!piece._isPromoted &&
piece._isMainStagePiece &&
isPromotablePiece)
実際の作業内容
1-2 ルールを統一する
コーディング規則や命名規則等のルールを定めることでいくつかのメリットがあります。
C#のコーディング規則
C#の命名規則
メリット1 : チームで共通認識を持てる
命名規則を統一することで型の種類やアクセス範囲等の性質を把握しやすくなり、コードを読む際の手間を減らすことができます。
各々が好きな命名規則でプログラムを書くとコードの統一感がなくなり、
どんな型なのか、アクセス範囲はどうなっているか等を確認する手間が発生し、コードを読む速度が落ちたり、
勘違いによる不具合を生み出しやすくなってしまいます。
メリット2 : 影響範囲がすぐにわかる
以下は与えられた数を2乗した結果を返す関数です。
この関数では_が先頭についた変数名がなく、ローカル変数のみが使用されていることがわかります。
そのため、メンバー変数等の他の要素によって結果の変わらない関数であることが命名規則から読み取れます。
public int Square(int value)
{
return value * value;
}
一方、以下の駒の移動シーケンス中のフレームの処理を行う関数HandlePieceMoving()では_isPieceMovingや_currentSelectedPiece等のメンバー変数が使用されています。
これはこの関数の外から書き換えられる可能性のある変数であり、呼び出しのたびに異なる挙動をする可能性があるということが読み取れます。
/// <summary>
/// 駒を移動中の処理
/// </summary>
private void HandlePieceMoving()
{
// 移動中なら何もしない
if (_isPieceMoving) return;
// 駒を移動中フラグを立てる
_isPieceMoving = true;
// コルーチンで駒を移動
StartCoroutine(MovePieceToDestination(_currentSelectedPiece, _currentSelectedPieceDestination));
}
+α : 命名を見直す
命名そのものについても見直すことが重要です
例えば
- UnityやC#標準ライブラリで使用されている一般的な命名を使用する
-
Find〇〇,bool TryGet〇〇(out 〇〇),bool _is〇〇;
-
- その役割に最も適した単語は何か考える
- 生成する - Instantiate, Create, Generate, Spawn
- 取得する - Get, Find, Search, Take
このあたりの話はリーダブルコードという書籍にまとめられています。
+β : その他のルールを設定する
gitのブランチ戦略や、GitHubの運用ルール、フォルダ構成等、他にも設定することのできるルールはありますが、今回は触れません
実際の作業
命名規則を以下のように統一しました。
- クラス名, 関数名, プロパティ名, 列挙型名 は
UpperCamelCase -
publicなメンバー変数はUpperCamelCase -
constやstatic readonlyな変数はUPPER_SNAKE_CASE - 列挙型メンバーは
UPPER_SNAKE_CASE -
privateなメンバー変数は_lowerCamelCase - ローカル変数は
lowerCamelCase - boolは接頭辞
isをつける - 配列は単語を複数形にする
- 何かを取得する関数は、取得の成否を戻り値の
bool, 実際のオブジェクトはoutキーワードをつけた引数で返すようにし、関数名をbool TryGetXXX(out outXXX)とする
他にも以下のようなルールを定めました
- 列挙型はメンバーに値を割り当てておき、その値を極力変更しない
- これによってメンバー追加時に設定していた値がずれることを防止する
- publicなメンバー変数は定義せず、プロパティを公開する
- アクセス範囲を明示する
現在のリファクタリングの範囲ではありませんでしたが、他にも以下のようなルールを今後使用します。
- インターフェースは接頭辞
Iをつける - ジェネリックの型パラメータ名は接頭辞
Tをつける - 名前空間は
UpperCamelCase
変更内容
1-3 処理を関数にまとめる
関数に同じ処理をまとめることも重要です。
IsCellOccupiedという関数では指定したロジック座標に駒が存在するかどうかを判定しています。
8行の短いコードでやっていることは単純ですが、現時点で12箇所に使用されている関数です。
まとめることによって実質的に
- コード量を1/12に削減でき、
- バグがあった際に一箇所の修正で済むようになっています。
実際にコードを書く際の関数にまとめるタイミングとしては、1つの処理を行うためのコードが長くなってきたり、複数ヶ所に同じ処理が登場した段階で関数として切り離すことを推奨します。
また、関数にまとめることで、処理のまとまりに関する理解を整理することができ、今後のリファクタリングの際に、どこでクラスを分割するかを考える際の材料にもなります。
/// <summary>
/// 指定したロジック座標に駒が存在するかどうかを判定する
/// </summary>
/// <param name="logicPos">ロジック座標</param>
/// <param name="occupyingPiece">駒が存在する場合、その駒の参照</param>
/// <returns>駒が存在する場合はtrue、存在しない場合はfalse</returns>
private bool IsCellOccupied(Vector2Int logicPos, out Piece occupyingPiece)
{
// 範囲外チェック
if (logicPos.x < 0 || logicPos.x >= BOARD_SIZE || logicPos.y < 0 || logicPos.y >= BOARD_SIZE)
{
occupyingPiece = null;
return false;
}
// ロジック座標からワールド座標を取得し、その座標に駒が存在するかを判定
Vector3 cellWorldPos = _cellWorldPositions[logicPos.x, logicPos.y];
_clickRaycaster.TryGetPiece(cellWorldPos, out occupyingPiece);
return occupyingPiece != null;
}
Part1まとめ
Part1では、体裁を整えるということでいくつかのすぐに取り組める事柄を取り上げました。
- コメントを書く
- ルールを統一する
- 処理を関数にまとめる
これらを意識することで、設計がなくてもある程度読みやすさが担保されます。
また、原則に則った設計がされていたとしても上記の項目が整っていないとプログラムを理解する際にかかる労力は大きくかかってしまいます。
Part2ではデータをプログラムで保持するように修正していきます。
Part2はこちら
Part2 : データをプログラムで保持する

