Unityで学ぶプログラミングの基本ルール:SOLID原則
はじめに:SOLID原則とは?
ゲームやアプリを作るとき、プログラムがわかりやすくて、直しやすくて、作りやすいととても便利です。そんなときに役立つのが「SOLID原則(ソリッドげんそく)」と呼ばれる5つのルールです。
この資料では、Unityというゲームエンジンを使って、SOLID原則をやさしく学べるように、実際のコード例といっしょに紹介します。
SOLID原則のメリット:
- プログラムが整理されて、あとで直すのが簡単になります。
- 新しい機能を追加するとき、前のコードにあまり触らなくてすみます。
- 同じクラスやスクリプトを、ほかのゲームでも使いやすくなります。
- テスト(うまく動くか確認すること)がしやすくなります。
- チームで作業するとき、役割がはっきりするので、みんなで作りやすくなります。
- 不具合(バグ)が起きたとき、どこを直せばいいか分かりやすくなります。
SOLIDの意味:
- S: 単一責任の原則(Single Responsibility Principle)
- O: 開いていて閉じている原則(Open/Closed Principle)
- L: リスコフの置きかえ原則(Liskov Substitution Principle)
- I: インターフェース分けの原則(Interface Segregation Principle)
- D: 依存の向きを逆にする原則(Dependency Inversion Principle)
原則1:単一責任の原則(SRP)
1つのクラスには、1つの仕事だけをさせよう!
クラスにたくさんの役割を持たせると、あとで変更が大変になります。この原則では、「ひとつのクラス=ひとつの役割」と考えます。
// スコアを管理するクラス
public class ScoreManager : MonoBehaviour
{
private int score = 0;
public void AddScore(int points)
{
score += points;
Debug.Log($"Score: {score}");
}
}
// ゲームの流れを管理するクラス
public class GameManager : MonoBehaviour
{
public ScoreManager scoreManager;
void Update()
{
if (Input.GetKeyDown(KeyCode.S))
scoreManager.AddScore(10);
}
}

スコアの処理はScoreManager、ゲームの流れはGameManagerが担当。
スコアの仕組みを変えたくなっても、ScoreManagerだけ直せばOKです。
原則2:開いていて閉じている原則(OCP):開放/閉鎖の原則
新しい機能は追加OK。でも、古いコードはなるべく変えない!
この原則では、「拡張はOK、変更はNG」がキーワードです。あとから新しい機能を増やすのに、前のコードを触らずにすむと安心です。
public interface IAttack
{
void Execute();
}
public class MeleeAttack : MonoBehaviour, IAttack
{
public void Execute() => Debug.Log("近づいて攻撃!");
}
public class RangedAttack : MonoBehaviour, IAttack
{
public void Execute() => Debug.Log("遠くから攻撃!");
}
public class Player : MonoBehaviour
{
public MonoBehaviour attackBehaviour; // MeleeAttack または RangedAttack
private IAttack attack;
void Awake() => attack = attackBehaviour as IAttack;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
attack.Execute();
}
}

新しい攻撃方法(例:魔法攻撃)を追加しても、PlayerクラスはそのままでOK!
原則3:リスコフの置きかえ原則(LSP):リスコフの置換原則
親クラスの代わりに子クラスを使っても、ちゃんと動くようにしよう!
いろんな敵を同じように扱えたら便利ですね。親クラスの「Enemy」と同じルールを守っていれば、どんな子クラスでも使えます。
public abstract class Enemy : MonoBehaviour
{
public abstract void Attack();
}
public class Goblin : Enemy
{
public override void Attack() => Debug.Log("ゴブリンの攻撃!");
}
public class Troll : Enemy
{
public override void Attack() => Debug.Log("トロールの攻撃!");
}
public class EnemyManager : MonoBehaviour
{
public List<Enemy> enemies;
void Start()
{
foreach (var enemy in enemies)
enemy.Attack();
}
}

GoblinもTrollも、Enemyとして扱えるから、コードを分けずに一括で使えます。
原則4:インターフェース分けの原則(ISP):インターフェース分離の原則
必要な機能だけ、必要な人に!
「全部入り」のインターフェースより、「必要な分だけ」を分けて使った方が使いやすいです。
public interface IMovable
{
void Move(Vector3 direction);
}
public interface IDamageable
{
void TakeDamage(int amount);
}
public class Player : MonoBehaviour, IMovable
{
public float speed = 5f;
public void Move(Vector3 dir) => transform.Translate(dir * speed * Time.deltaTime);
void Update()
{
var dir = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
Move(dir);
}
}
public class DestructibleBox : MonoBehaviour, IDamageable
{
public int hp = 3;
public void TakeDamage(int amount)
{
hp -= amount;
if (hp <= 0) Destroy(gameObject);
}
}

プレイヤーは「動く」、箱は「ダメージを受ける」。それぞれ必要なことだけ持っています。
以下のコードは、プレイヤーの移動と、シーン内のすべての「ダメージを受けられるオブジェクト」にダメージを与える仕組みをまとめたものです。各セクションごとに解説します。
using System.Linq;
using UnityEngine;
namespace InterfaceSegregationPrinciple
{
public class Player : MonoBehaviour, IMovable
{
public float speed = 5f;
public void Move(Vector3 dir) => transform.Translate(dir * speed * Time.deltaTime);
void Update()
{
var dir = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
Move(dir);
if (Input.GetKeyDown(KeyCode.Space))
{
GetDamagableObject();
}
}
private void GetDamagableObject()
{
var damageableObjects = FindObjectsByType<MonoBehaviour>(
FindObjectsInactive.Include, // 非アクティブな GameObject 上のコンポーネントも含めて検索
FindObjectsSortMode.None // ソートせず、シーン内で見つかった順序のまま返す
)
.OfType<IDamageable>(); // IDamageable を実装しているものだけに絞り込む
foreach (var damageable in damageableObjects)
{
damageable.TakeDamage(1); // ダメージ量を 1 に設定して呼び出し
}
}
}
}
各部分のポイント
- public class Player : MonoBehaviour, IMovable
- MonoBehaviour を継承することで Unity コンポーネントとして動作。
- IMovable インターフェースを実装し、移動機能を分離・明示。
- 移動処理 (Move/Update)
- Input.GetAxis(“Horizontal")/(“Vertical") でキー入力を取得。
- Move(dir) で transform.Translate を呼び、フレームレートに依存しない移動を実現。
- ダメージ発動タイミング
- Input.GetKeyDown(KeyCode.Space) でスペースキー押下を検出。
- 押された瞬間だけ一度だけ処理が走る。
- GetDamagableObject の検索ロジック
- FindObjectsByType<MonoBehaviour>(IncludeInactive, None) でシーン内の全ての MonoBehaviour を配列取得。
- .OfType<IDamageable>() でインターフェースを実装したコンポーネントだけを抽出。
- foreach で順に TakeDamage(1) を呼び出し、HP 減少やオブジェクト破壊などの処理を行う。
設計上の注意
- 性能:シーン全体を都度スキャンするため、オブジェクト数が多い場合は重くなりやすい。
- 最適化案:レイヤーやタグで事前に絞り込む、あるいは生成時にリスト登録するキャッシュ方式などを併用するとよいでしょう。
原則5:依存の向きを逆にする原則(DIP):依存性逆転の原則
大事な役割(管理クラス)は、細かい仕組み(実装クラス)に直接つながらないようにする!
音を鳴らす仕組みを「ルール(インターフェース)」にしておけば、中身を自由に変えられます。
テストや将来の拡張もラクになります。
public interface IAudioService
{
void PlaySound(string clipName);
}
public class AudioService : MonoBehaviour, IAudioService
{
[SerializeField] private AudioClip[] clips;
[SerializeField] private AudioSource source;
public void PlaySound(string clipName)
{
AudioClip clip = clips.FirstOrDefault(c => c.name == clipName);
source.PlayOneShot(clip);
}
}
public class SoundManager : MonoBehaviour
{
[SerializeField] private MonoBehaviour audioServiceObject;
private IAudioService audioService;
void Awake() => audioService = audioServiceObject as IAudioService;
public void OnButtonClick()
{
audioService?.PlaySound("Click");
}
}
public class MockAudioService : IAudioService
{
public void PlaySound(string clipName)
{
Debug.Log($"[Mock] 再生: {clipName}");
}
}

SoundManagerは「音を鳴らす」だけを考え、どう鳴らすかはAudioServiceにまかせています。
こうすることで、別の音再生方法にもすぐに差し替えできます。
おわりに:SOLID原則を使って、よいプログラムを作ろう!
この5つの原則は、どれも「読みやすく、直しやすく、安全なコード」を作るための大事な考え方です。
最初はむずかしく感じるかもしれません。でも、少しずつ使ってみることで自然と身につきます。
Unityでのゲーム作りを楽しみながら、SOLID原則も覚えていきましょう!
ディスカッション
コメント一覧
まだ、コメントがありません