第1章: C#とUnityの基礎
C#(シーシャープ)は、Microsoftが開発した強力なプログラミング言語です。Unityでゲーム開発を行う際、C#は主要な言語として使用されます。C#はオブジェクト指向プログラミングを支援し、型安全性が高く、効率的なコードを書くことができます。
Unityでは、C#スクリプトを使ってゲームオブジェクトの動作を制御します。以下は、基本的なC#スクリプトの例です:
using UnityEngine;
public class HelloWorld : MonoBehaviour
{
void Start()
{
Debug.Log("こんにちは、Unity世界!");
}
void Update()
{
// 毎フレーム実行される処理
}
}
このスクリプトでは、MonoBehaviour
を継承したクラスを定義しています。Start()
メソッドはゲームオブジェクトが初期化されたときに一度だけ呼ばれ、Update()
メソッドは毎フレーム呼び出されます。
第2章: 変数と型
C#では、変数を使ってデータを保存し操作します。Unityでよく使用される基本的な型には、以下のようなものがあります:
- int: 整数
- float: 小数点数
- bool: 真偽値
- string: 文字列
- Vector3: 3次元ベクトル
以下は、これらの型を使用した例です:
public class VariableExample : MonoBehaviour
{
public int playerScore = 0;
public float playerSpeed = 5.0f;
public bool isGameOver = false;
public string playerName = "勇者";
public Vector3 playerPosition = new Vector3(0, 0, 0);
void Start()
{
Debug.Log($"プレイヤー名: {playerName}");
Debug.Log($"スコア: {playerScore}");
Debug.Log($"速度: {playerSpeed}");
Debug.Log($"ゲームオーバー?: {isGameOver}");
Debug.Log($"位置: {playerPosition}");
}
}
この例では、異なる型の変数を宣言し、それらの値をコンソールに出力しています。
第3章: 制御構造
プログラムの流れを制御するために、C#はいくつかの制御構造を提供しています。主な制御構造には、if文、for文、while文などがあります。
以下は、これらの制御構造を使用した例です:
public class ControlStructuresExample : MonoBehaviour
{
public int playerHealth = 100;
public int[] scores = { 10, 20, 30, 40, 50 };
void Start()
{
// if文の例
if (playerHealth > 50)
{
Debug.Log("プレイヤーの体力は良好です。");
}
else
{
Debug.Log("プレイヤーの体力が低下しています。");
}
// for文の例
int totalScore = 0;
for (int i = 0; i < scores.Length; i++)
{
totalScore += scores[i];
}
Debug.Log($"合計スコア: {totalScore}");
// while文の例
int countdown = 5;
while (countdown > 0)
{
Debug.Log($"カウントダウン: {countdown}");
countdown--;
}
Debug.Log("発射!");
}
}
この例では、if文を使ってプレイヤーの体力状態を判断し、for文を使って配列の要素を合計し、while文を使ってカウントダウンを行っています。
第4章: 関数とメソッド
関数(またはメソッド)は、特定のタスクを実行するコードのまとまりです。C#では、関数を使って複雑な処理を小さな部分に分割し、コードの再利用性を高めることができます。
以下は、関数を使用した例です:
public class FunctionExample : MonoBehaviour
{
void Start()
{
int result = AddNumbers(5, 3);
Debug.Log($"5 + 3 = {result}");
string greeting = GetGreeting("太郎");
Debug.Log(greeting);
PrintMultiplicationTable(5);
}
int AddNumbers(int a, int b)
{
return a + b;
}
string GetGreeting(string name)
{
return $"こんにちは、{name}さん!";
}
void PrintMultiplicationTable(int number)
{
for (int i = 1; i <= 10; i++)
{
Debug.Log($"{number} x {i} = {number * i}");
}
}
}
この例では、数値を加算する関数、挨拶文を生成する関数、掛け算表を出力する関数を定義しています。これらの関数はStart()
メソッド内で呼び出されています。
第5章: クラスとオブジェクト
C#はオブジェクト指向プログラミング言語であり、クラスとオブジェクトの概念が重要です。クラスは、データ(フィールド)と振る舞い(メソッド)をカプセル化したものです。オブジェクトは、クラスのインスタンスです。
以下は、簡単なPlayer
クラスを定義し、それを使用する例です:
public class Player
{
public string name;
public int health;
public int score;
public Player(string name, int health)
{
this.name = name;
this.health = health;
this.score = 0;
}
public void TakeDamage(int damage)
{
health -= damage;
if (health < 0) health = 0;
}
public void AddScore(int points)
{
score += points;
}
public string GetStatus()
{
return $"プレイヤー: {name}, 体力: {health}, スコア: {score}";
}
}
public class ClassExample : MonoBehaviour
{
void Start()
{
Player player1 = new Player("勇者A", 100);
Player player2 = new Player("勇者B", 120);
player1.TakeDamage(30);
player1.AddScore(50);
player2.TakeDamage(50);
player2.AddScore(100);
Debug.Log(player1.GetStatus());
Debug.Log(player2.GetStatus());
}
}
この例では、Player
クラスを定義し、名前、体力、スコアのプロパティと、ダメージを受ける、スコアを加算する、ステータスを取得するメソッドを持たせています。ClassExample
クラスでは、Player
クラスのインスタンスを作成し、それらのメソッドを呼び出しています。
第6章: 継承とポリモーフィズム
継承は、既存のクラスを基に新しいクラスを作成する機能です。ポリモーフィズムは、同じインターフェースを持つ異なるクラスのオブジェクトを同じように扱える機能です。これらの概念は、コードの再利用性と柔軟性を高めます。
以下は、継承とポリモーフィズムを使用した例です:
public abstract class Character
{
public string name;
public int health;
public Character(string name, int health)
{
this.name = name;
this.health = health;
}
public abstract void Attack();
public virtual void TakeDamage(int damage)
{
health -= damage;
if (health < 0) health = 0;
Debug.Log($"{name}は{damage}ダメージを受けた。残り体力: {health}");
}
}
public class Warrior : Character
{
public Warrior(string name) : base(name, 150) { }
public override void Attack()
{
Debug.Log($"{name}は剣で攻撃した!");
}
}
public class Mage : Character
{
public Mage(string name) : base(name, 100) { }
public override void Attack()
{
Debug.Log($"{name}は魔法で攻撃した!");
}
public override void TakeDamage(int damage)
{
base.TakeDamage(damage);
Debug.Log($"{name}は魔法のバリアで一部のダメージを軽減した。");
}
}
public class InheritanceExample : MonoBehaviour
{
void Start()
{
Character[] characters = new Character[]
{
new Warrior("戦士A"),
new Mage("魔法使いB")
};
foreach (var character in characters)
{
character.Attack();
character.TakeDamage(50);
}
}
}
この例では、Character
という抽象基底クラスを定義し、Warrior
とMage
クラスがそれを継承しています。各クラスはAttack()
メソッドを独自に実装し、Mage
クラスはTakeDamage()
メソッドをオーバーライドしています。InheritanceExample
クラスでは、異なる型のキャラクターを同じ配列に格納し、同じように扱っています。
第7章: インターフェースと抽象クラス
インターフェースと抽象クラスは、C#でコードの構造を設計する際に重要な役割を果たします。インターフェースはメソッドの署名のみを定義し、抽象クラスは一部の実装を持つことができます。
以下は、インターフェースと抽象クラスを使用した例です:
public interface IDestructible
{
void TakeDamage(int damage);
bool IsDestroyed();
}
public abstract class GameObject : IDestructible
{
protected string name;
protected int durability;
public GameObject(string name, int durability)
{
this.name = name;
this.durability = durability;
}
public abstract void Interact();
public virtual void TakeDamage(int damage)
{
durability -= damage;
if (durability < 0) durability = 0;
Debug.Log($"{name}は{damage}ダメージを受けた。残り耐久度: {durability}");
}
public bool IsDestroyed()
{
return durability <= 0;
}
}
public class Crate : GameObject
{
public Crate(string name) : base(name, 50) { }
public override void Interact()
{
Debug.Log($"{name}を開けた。アイテムを入手した!");
}
}
public class Door : GameObject
{
private bool isOpen = false;
public Door(string name) : base(name, 100) { }
public override void Interact()
{
isOpen = !isOpen;
Debug.Log($"{name}を{(isOpen ? "開けた" : "閉めた")}。");
}
}
public class InterfaceAbstractExample : MonoBehaviour
{
void Start()
{
GameObject[] objects = new GameObject[]
{
new Crate("木箱"),
new Door("鉄の扉")
};
foreach (var obj in objects)
{
obj.Interact();
obj.TakeDamage(30);
Debug.Log($"破壊されたか: {obj.IsDestroyed()}");
}
}
}
この例では、IDestructible
インターフェースを定義し、GameObject
抽象クラスがそれを実装しています。Crate
とDoor
クラスはGameObject
を継承し、Interact()
メソッドを独自に実装しています。InterfaceAbstractExample
クラスでは、これらのオブジェクトを同じように扱い、それぞれの特性を活かした動作を行っています。
第8章: コレクションとジェネリクス
C#では、データを効率的に管理するためのさまざまなコレクションタイプが提供されています。また、ジェネリクスを使用することで、型安全性を保ちながら再利用可能なコードを書くことができます。
以下は、コレクションとジェネリクスを使用した例です:
using System.Collections.Generic;
public class Item
{
public string Name { get; private set; }
public int Value { get; private set; }
public Item(string name, int value)
{
Name = name;
Value = value;
}
}
public class Inventory<T> where T : Item
{
private List<T> items = new List<T>();
public void AddItem(T item)
{
items.Add(item);
Debug.Log($"{item.Name}をインベントリに追加しました。");
}
public void RemoveItem(T item)
{
if (items.Remove(item))
{
Debug.Log($"{item.Name}をインベントリから削除しました。");
}
else
{
Debug.Log($"{item.Name}はインベントリにありません。");
}
}
public void ListItems()
{
Debug.Log("インベントリ内のアイテム:");
foreach (var item in items)
{
Debug.Log($"- {item.Name} (価値: {item.Value})");
}
}
}
public class CollectionGenericExample : MonoBehaviour
{
void Start()
{
Inventory<Item> playerInventory = new Inventory<Item>();
Item sword = new Item("鋼の剣", 100);
Item potion = new Item("回復薬", 50);
Item shield = new Item("木の盾", 75);
playerInventory.AddItem(sword);
playerInventory.AddItem(potion);
playerInventory.AddItem(shield);
playerInventory.ListItems();
playerInventory.RemoveItem(potion);
playerInventory.ListItems();
// Dictionaryの使用例
Dictionary<string, int> enemyLevels = new Dictionary<string, int>
{
{"スライム", 1},
{"ゴブリン", 3},
{"オーク", 5}
};
foreach (var enemy in enemyLevels)
{
Debug.Log($"{enemy.Key}のレベル: {enemy.Value}");
}
// 敵のレベルを更新
if (enemyLevels.ContainsKey("ゴブリン"))
{
enemyLevels["ゴブリン"] = 4;
Debug.Log($"ゴブリンの新しいレベル: {enemyLevels["ゴブリン"]}");
}
}
}
この例では、ジェネリックなInventory<T>
クラスを作成し、Item
クラスとその派生クラスのみを扱えるようにしています。CollectionGenericExample
クラスでは、このインベントリを使用してアイテムの追加、削除、一覧表示を行っています。
また、Dictionary<TKey, TValue>
を使用して敵のレベルを管理する例も示しています。これにより、キーと値のペアを効率的に保存し、アクセスすることができます。
第9章: デリゲートとイベント
デリゲートとイベントは、C#でコールバックやイベント駆動プログラミングを実現するための重要な機能です。デリゲートは、メソッドを参照するための型安全なオブジェクトであり、イベントはデリゲートを使用して実装されます。
以下は、デリゲートとイベントを使用した例です:
public class Player
{
public string Name { get; private set; }
public int Health { get; private set; }
public delegate void HealthChangedHandler(int newHealth);
public event HealthChangedHandler OnHealthChanged;
public Player(string name, int health)
{
Name = name;
Health = health;
}
public void TakeDamage(int damage)
{
Health -= damage;
if (Health < 0) Health = 0;
OnHealthChanged?.Invoke(Health);
}
public void Heal(int amount)
{
Health += amount;
OnHealthChanged?.Invoke(Health);
}
}
public class DelegateEventExample : MonoBehaviour
{
private Player player;
void Start()
{
player = new Player("勇者", 100);
player.OnHealthChanged += HandleHealthChanged;
Debug.Log($"{player.Name}の冒険が始まります!");
player.TakeDamage(30);
player.Heal(20);
player.TakeDamage(50);
}
void HandleHealthChanged(int newHealth)
{
Debug.Log($"{player.Name}の体力が変化しました。新しい体力: {newHealth}");
if (newHealth == 0)
{
Debug.Log($"{player.Name}は倒れました...");
}
}
}
この例では、Player
クラスにOnHealthChanged
イベントを定義しています。このイベントは、プレイヤーの体力が変化するたびに発火されます。DelegateEventExample
クラスでは、このイベントにハンドラーを登録し、体力の変化に応じて適切なメッセージを表示しています。
第10章: LINQ (Language Integrated Query)
LINQは、C#に組み込まれたクエリ言語で、データの検索、フィルタリング、変換を簡潔に行うことができます。LINQを使用することで、コレクションやデータベースに対する操作を効率的に記述できます。
以下は、LINQを使用した例です:
using System.Linq;
using System.Collections.Generic;
public class Item
{
public string Name { get; set; }
public int Value { get; set; }
public string Type { get; set; }
public Item(string name, int value, string type)
{
Name = name;
Value = value;
Type = type;
}
}
public class LinqExample : MonoBehaviour
{
void Start()
{
List<Item> items = new List<Item>
{
new Item("鋼の剣", 100, "武器"),
new Item("鉄の鎧", 150, "防具"),
new Item("回復薬", 50, "消耗品"),
new Item("魔法の杖", 200, "武器"),
new Item("皮の盾", 80, "防具"),
new Item("解毒薬", 30, "消耗品")
};
// 価値が100以上のアイテムを取得
var valuableItems = items.Where(item => item.Value >= 100);
Debug.Log("価値の高いアイテム:");
foreach (var item in valuableItems)
{
Debug.Log($"- {item.Name} (価値: {item.Value})");
}
// タイプごとにアイテムをグループ化
var groupedItems = items.GroupBy(item => item.Type);
foreach (var group in groupedItems)
{
Debug.Log($"{group.Key}:");
foreach (var item in group)
{
Debug.Log($"- {item.Name}");
}
}
// アイテムの平均価値を計算
double averageValue = items.Average(item => item.Value);
Debug.Log($"アイテムの平均価値: {averageValue:F2}");
// 最も価値の高いアイテムを取得
var mostValuableItem = items.OrderByDescending(item => item.Value).First();
Debug.Log($"最も価値の高いアイテム: {mostValuableItem.Name} (価値: {mostValuableItem.Value})");
// アイテム名に「薬」が含まれるアイテムを取得
var potions = items.Where(item => item.Name.Contains("薬"));
Debug.Log("薬のリスト:");
foreach (var potion in potions)
{
Debug.Log($"- {potion.Name}");
}
}
}
この例では、Item
クラスのリストに対してさまざまなLINQクエリを実行しています。価値の高いアイテムのフィルタリング、タイプごとのグループ化、平均価値の計算、最も価値の高いアイテムの取得、特定の名前を含むアイテムの検索など、LINQの多様な機能を示しています。
第11章: 非同期プログラミング
非同期プログラミングは、長時間実行される操作をバックグラウンドで実行し、アプリケーションの応答性を維持するために重要です。C#では、async
とawait
キーワードを使用して非同期プログラミングを簡単に実装できます。
以下は、非同期プログラミングの例です:
using System.Threading.Tasks;
using UnityEngine.Networking;
public class AsyncExample : MonoBehaviour
{
void Start()
{
Debug.Log("非同期処理を開始します。");
LoadDataAsync();
Debug.Log("他の処理を続行します。");
}
async void LoadDataAsync()
{
string result = await FetchDataFromServerAsync("https://api.example.com/data");
Debug.Log($"サーバーからのデータ: {result}");
int processedResult = await ProcessDataAsync(result);
Debug.Log($"処理結果: {processedResult}");
}
async Task<string> FetchDataFromServerAsync(string url)
{
using (UnityWebRequest webRequest = UnityWebRequest.Get(url))
{
Debug.Log("サーバーからデータを取得中...");
await webRequest.SendWebRequest();
if (webRequest.result == UnityWebRequest.Result.Success)
{
return webRequest.downloadHandler.text;
}
else
{
Debug.LogError($"エラー: {webRequest.error}");
return null;
}
}
}
async Task<int> ProcessDataAsync(string data)
{
Debug.Log("データを処理中...");
await Task.Delay(2000); // 重い処理をシミュレート
// ここでデータを処理する(この例では単純に文字数を返す)
return data?.Length ?? 0;
}
}
この例では、LoadDataAsync
メソッドが非同期で実行されます。このメソッドは、まずサーバーからデータを取得し(FetchDataFromServerAsync
)、次にそのデータを処理します(ProcessDataAsync
)。await
キーワードを使用することで、各非同期操作の完了を待ちながら、アプリケーションのメインスレッドをブロックすることなく実行できます。
第12章: コルーチン
コルーチンは、Unityで非同期処理を実装するためのもう一つの方法です。コルーチンを使用すると、時間をかけて実行される処理を簡単に記述できます。
以下は、コルーチンを使用した例です:
using System.Collections;
using UnityEngine.Networking;
public class CoroutineExample : MonoBehaviour
{
void Start()
{
Debug.Log("ゲームを開始します。");
StartCoroutine(GameLoop());
}
IEnumerator GameLoop()
{
yield return StartCoroutine(LoadLevel());
yield return StartCoroutine(PlayLevel());
yield return StartCoroutine(EndLevel());
}
IEnumerator LoadLevel()
{
Debug.Log("レベルをロード中...");
yield return new WaitForSeconds(2f); // ロード時間をシミュレート
Debug.Log("レベルのロードが完了しました。");
}
IEnumerator PlayLevel()
{
Debug.Log("レベルをプレイ中...");
yield return new WaitForSeconds(5f); // プレイ時間をシミュレート
Debug.Log("レベルクリア!");
}
IEnumerator EndLevel()
{
Debug.Log("結果を計算中...");
yield return StartCoroutine(CalculateScore());
Debug.Log("次のレベルに進みます。");
}
IEnumerator CalculateScore()
{
yield return new WaitForSeconds(1f);
int score = Random.Range(100, 1000);
Debug.Log($"スコア: {score}");
}
}
この例では、ゲームのループをコルーチンを使って実装しています。GameLoop
コルーチンは、レベルのロード、プレイ、終了の各段階を順番に実行します。各段階も別のコルーチンとして実装されており、yield return
を使用して処理の一時停止と再開を制御しています。
第13章: スクリプタブルオブジェクト
スクリプタブルオブジェクトは、Unityでデータを保存し、管理するための強力な方法です。これらは、プロジェクト内でアセットとして作成され、インスペクタで編集できるため、デザイナーやアーティストとの協業に適しています。
以下は、スクリプタブルオブジェクトを使用した例です:
using UnityEngine;
[CreateAssetMenu(fileName = "New Item Data", menuName = "Inventory/Item Data")]
public class ItemData : ScriptableObject
{
public string itemName;
public Sprite icon;
public int value;
[TextArea(3, 10)]
public string description;
}
public class Item : MonoBehaviour
{
public ItemData data;
void Start()
{
DisplayItemInfo();
}
void DisplayItemInfo()
{
Debug.Log($"アイテム名: {data.itemName}");
Debug.Log($"価値: {data.value}");
Debug.Log($"説明: {data.description}");
}
}
public class InventoryManager : MonoBehaviour
{
public ItemData[] availableItems;
void Start()
{
foreach (var itemData in availableItems)
{
Debug.Log($"インベントリにある項目: {itemData.itemName}");
}
}
}
この例では、ItemData
というスクリプタブルオブジェクトを定義しています。これは、アイテムの名前、アイコン、価値、説明を保持します。Item
クラスは、このデータを使用してゲーム内のアイテムを表現します。InventoryManager
クラスは、利用可能なアイテムのリストを管理します。
スクリプタブルオブジェクトを使用することで、データとロジックを分離し、再利用可能なデータ構造を作成できます。
第14章: オブジェクトプーリング
オブジェクトプーリングは、頻繁に生成と破棄が行われるオブジェクト(例:弾丸、パーティクル)のパフォーマンスを向上させるための技術です。オブジェクトを再利用することで、メモリの割り当てと解放の回数を減らすことができます。
以下は、オブジェクトプーリングの例です。
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool : MonoBehaviour
{
[System.Serializable]
public class Pool
{
public string tag;
public GameObject prefab;
public int size;
}
public List<Pool> pools;
public Dictionary<string, Queue<GameObject>> poolDictionary;
void Start()
{
poolDictionary = new Dictionary<string, Queue<GameObject>>();
foreach (Pool pool in pools)
{
Queue<GameObject> objectPool = new Queue<GameObject>();
for (int i = 0; i < pool.size; i++)
{
GameObject obj = Instantiate(pool.prefab);
obj.SetActive(false);
objectPool.Enqueue(obj);
}
poolDictionary.Add(pool.tag, objectPool);
}
}
public GameObject SpawnFromPool(string tag, Vector3 position, Quaternion rotation)
{
if (!poolDictionary.ContainsKey(tag))
{
Debug.LogWarning($"プール内に{tag}というタグのオブジェクトが存在しません。");
return null;
}
GameObject objectToSpawn = poolDictionary[tag].Dequeue();
objectToSpawn.SetActive(true);
objectToSpawn.transform.position = position;
objectToSpawn.transform.rotation = rotation;
poolDictionary[tag].Enqueue(objectToSpawn);
return objectToSpawn;
}
}
public class BulletSpawner : MonoBehaviour
{
public ObjectPool objectPool;
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
SpawnBullet();
}
}
void SpawnBullet()
{
GameObject bullet = objectPool.SpawnFromPool("Bullet", transform.position, Quaternion.identity);
if (bullet != null)
{
// 弾丸の動きなどの追加ロジックをここに記述
Debug.Log("弾丸を発射しました。");
}
}
}
この例では、ObjectPool
クラスがオブジェクトプールを管理します。各プールは、タグ、プレハブ、サイズを持ちます。Start
メソッドで、指定されたサイズのプールを初期化します。
SpawnFromPool
メソッドは、指定されたタグのオブジェクトをプールから取得し、指定された位置と回転で配置します。使用後、オブジェクトはプールに戻されます。
BulletSpawner
クラスは、スペースキーが押されたときに弾丸を生成する例を示しています。これにより、弾丸オブジェクトを効率的に再利用できます。
第15章: シングルトンパターン
シングルトンパターンは、クラスのインスタンスが1つだけ存在することを保証するデザインパターンです。ゲーム管理、オーディオ管理、データ管理などのグローバルな機能に適しています。
以下は、シングルトンパターンを使用した例です:
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
public static GameManager Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<GameManager>();
if (_instance == null)
{
GameObject go = new GameObject("GameManager");
_instance = go.AddComponent<GameManager>();
}
}
return _instance;
}
}
public int score = 0;
public int highScore = 0;
void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(this.gameObject);
}
else
{
_instance = this;
DontDestroyOnLoad(this.gameObject);
}
}
public void AddScore(int points)
{
score += points;
if (score > highScore)
{
highScore = score;
}
Debug.Log($"スコア: {score}, ハイスコア: {highScore}");
}
public void ResetScore()
{
score = 0;
}
}
public class Player : MonoBehaviour
{
void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Coin"))
{
GameManager.Instance.AddScore(10);
Destroy(other.gameObject);
}
}
}
この例では、GameManager
クラスをシングルトンとして実装しています。Instance
プロパティを通じて、どこからでもGameManager
の唯一のインスタンスにアクセスできます。
Awake
メソッドでは、重複するインスタンスが作成されないようにチェックし、シーン遷移時にも破棄されないようにしています。
Player
クラスは、コインを集めたときにGameManager
のスコア機能を利用する例を示しています。
第16章: オブザーバーパターン
オブザーバーパターンは、オブジェクト間の1対多の依存関係を定義し、あるオブジェクトの状態が変化したときに、それに依存するすべてのオブジェクトに自動的に通知されるようにするデザインパターンです。
以下は、オブザーバーパターンを使用した例です:
using System.Collections.Generic;
using UnityEngine;
public interface IObserver
{
void OnNotify(string message);
}
public class Subject : MonoBehaviour
{
private List<IObserver> observers = new List<IObserver>();
public void AddObserver(IObserver observer)
{
observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
observers.Remove(observer);
}
protected void NotifyObservers(string message)
{
foreach (var observer in observers)
{
observer.OnNotify(message);
}
}
}
public class Player : Subject
{
public int health = 100;
public void TakeDamage(int damage)
{
health -= damage;
if (health <= 0)
{
health = 0;
NotifyObservers("PlayerDied");
}
else
{
NotifyObservers($"PlayerHealthChanged:{health}");
}
}
}
public class UIManager : MonoBehaviour, IObserver
{
public void OnNotify(string message)
{
if (message.StartsWith("PlayerHealthChanged:"))
{
int health = int.Parse(message.Split(':')[1]);
UpdateHealthUI(health);
}
else if (message == "PlayerDied")
{
ShowGameOverScreen();
}
}
private void UpdateHealthUI(int health)
{
Debug.Log($"UIを更新: プレイヤーの体力 = {health}");
}
private void ShowGameOverScreen()
{
Debug.Log("ゲームオーバー画面を表示");
}
}
public class AchievementManager : MonoBehaviour, IObserver
{
public void OnNotify(string message)
{
if (message == "PlayerDied")
{
UnlockAchievement("初めての死");
}
}
private void UnlockAchievement(string achievementName)
{
Debug.Log($"実績解除: {achievementName}");
}
}
public class GameController : MonoBehaviour
{
public Player player;
public UIManager uiManager;
public AchievementManager achievementManager;
void Start()
{
player.AddObserver(uiManager);
player.AddObserver(achievementManager);
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
player.TakeDamage(20);
}
}
}
この例では、Player
クラスがSubject
(観察される側)として実装され、UIManager
とAchievementManager
がIObserver
(観察者)として実装されています。プレイヤーの体力が変化したり、プレイヤーが死亡したりすると、登録されたすべてのオブザーバーに通知が送られます。
これにより、プレイヤーの状態変化に応じて、UIの更新や実績の解除などの処理を柔軟に行うことができます。
第17章: ステートパターン
ステートパターンは、オブジェクトの内部状態が変化したときにその振る舞いを変更できるようにするデザインパターンです。これは、複雑な条件分岐を避け、コードをより整理された形で管理するのに役立ちます。
以下は、ステートパターンを使用した例です:
public abstract class EnemyState
{
protected Enemy enemy;
public EnemyState(Enemy enemy)
{
this.enemy = enemy;
}
public abstract void EnterState();
public abstract void UpdateState();
public abstract void ExitState();
}
public class IdleState : EnemyState
{
public IdleState(Enemy enemy) : base(enemy) { }
public override void EnterState()
{
Debug.Log("敵がアイドル状態に入りました。");
}
public override void UpdateState()
{
// プレイヤーが近くにいるか確認
if (enemy.IsPlayerNearby())
{
enemy.ChangeState(new ChaseState(enemy));
}
}
public override void ExitState()
{
Debug.Log("敵がアイドル状態を終了しました。");
}
}
public class ChaseState : EnemyState
{
public ChaseState(Enemy enemy) : base(enemy) { }
public override void EnterState()
{
Debug.Log("敵が追跡状態に入りました。");
}
public override void UpdateState()
{
enemy.ChasePlayer();
if (enemy.IsPlayerInAttackRange())
{
enemy.ChangeState(new AttackState(enemy));
}
else if (!enemy.IsPlayerNearby())
{
enemy.ChangeState(new IdleState(enemy));
}
}
public override void ExitState()
{
Debug.Log("敵が追跡状態を終了しました。");
}
}
public class AttackState : EnemyState
{
public AttackState(Enemy enemy) : base(enemy) { }
public override void EnterState()
{
Debug.Log("敵が攻撃状態に入りました。");
}
public override void UpdateState()
{
enemy.AttackPlayer();
if (!enemy.IsPlayerInAttackRange())
{
enemy.ChangeState(new ChaseState(enemy));
}
}
public override void ExitState()
{
Debug.Log("敵が攻撃状態を終了しました。");
}
}
public class Enemy : MonoBehaviour
{
private EnemyState currentState;
void Start()
{
ChangeState(new IdleState(this));
}
void Update()
{
currentState.UpdateState();
}
public void ChangeState(EnemyState newState)
{
if (currentState != null)
{
currentState.ExitState();
}
currentState = newState;
currentState.EnterState();
}
public bool IsPlayerNearby()
{
// プレイヤーが近くにいるかどうかのロジック
return Random.value > 0.7f;
}
public bool IsPlayerInAttackRange()
{
// プレイヤーが攻撃範囲内にいるかどうかのロジック
return Random.value > 0.8f;
}
public void ChasePlayer()
{
Debug.Log("敵がプレイヤーを追跡しています。");
}
public void AttackPlayer()
{
Debug.Log("敵がプレイヤーを攻撃しています。");
}
}
この例では、敵キャラクターの行動をステートパターンを使って実装しています。EnemyState
は抽象基底クラスで、具体的な状態(IdleState
、ChaseState
、AttackState
)がこれを継承しています。
Enemy
クラスは現在の状態を保持し、状態に応じて適切な行動を取ります。状態の変更はChangeState
メソッドで行われ、各状態はEnterState
、UpdateState
、ExitState
メソッドを持っています。
これにより、敵の行動をより柔軟かつ拡張性の高い方法で管理できます。新しい状態を追加する場合も、既存のコードを大きく変更することなく実装できます。
第18章: コマンドパターン
コマンドパターンは、要求をオブジェクトとしてカプセル化し、異なる要求やキューイング、ログ記録、取り消し可能な操作などをパラメータ化できるようにするデザインパターンです。
以下は、コマンドパターンを使用した例です:
using System.Collections.Generic;
using UnityEngine;
public interface ICommand
{
void Execute();
void Undo();
}
public class MoveCommand : ICommand
{
private Transform objectToMove;
private Vector3 direction;
private float distance;
public MoveCommand(Transform objectToMove, Vector3 direction, float distance)
{
this.objectToMove = objectToMove;
this.direction = direction;
this.distance = distance;
}
public void Execute()
{
objectToMove.Translate(direction * distance);
}
public void Undo()
{
objectToMove.Translate(-direction * distance);
}
}
public class RotateCommand : ICommand
{
private Transform objectToRotate;
private Vector3 axis;
private float angle;
public RotateCommand(Transform objectToRotate, Vector3 axis, float angle)
{
this.objectToRotate = objectToRotate;
this.axis = axis;
this.angle = angle;
}
public void Execute()
{
objectToRotate.Rotate(axis, angle);
}
public void Undo()
{
objectToRotate.Rotate(axis, -angle);
}
}
public class CommandInvoker : MonoBehaviour
{
private Stack<ICommand> commandHistory = new Stack<ICommand>();
public void ExecuteCommand(ICommand command)
{
command.Execute();
commandHistory.Push(command);
}
public void UndoLastCommand()
{
if (commandHistory.Count > 0)
{
ICommand lastCommand = commandHistory.Pop();
lastCommand.Undo();
}
else
{
Debug.Log("取り消す操作がありません。");
}
}
}
public class PlayerController : MonoBehaviour
{
public CommandInvoker commandInvoker;
public float moveDistance = 1f;
public float rotateAngle = 45f;
void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
ICommand moveForward = new MoveCommand(transform, Vector3.forward, moveDistance);
commandInvoker.ExecuteCommand(moveForward);
}
else if (Input.GetKeyDown(KeyCode.S))
{
ICommand moveBackward = new MoveCommand(transform, Vector3.back, moveDistance);
commandInvoker.ExecuteCommand(moveBackward);
}
else if (Input.GetKeyDown(KeyCode.A))
{
ICommand rotateLeft = new RotateCommand(transform, Vector3.up, -rotateAngle);
commandInvoker.ExecuteCommand(rotateLeft);
}
else if (Input.GetKeyDown(KeyCode.D))
{
ICommand rotateRight = new RotateCommand(transform, Vector3.up, rotateAngle);
commandInvoker.ExecuteCommand(rotateRight);
}
else if (Input.GetKeyDown(KeyCode.Z))
{
commandInvoker.UndoLastCommand();
}
}
}
この例では、MoveCommand
とRotateCommand
がICommand
インターフェースを実装しています。各コマンドはExecute
メソッドとUndo
メソッドを持っており、それぞれ操作の実行と取り消しを行います。
CommandInvoker
クラスはコマンドの実行と取り消しを管理します。実行されたコマンドはスタックに保存され、後で取り消すことができます。
PlayerController
クラスは、プレイヤーの入力に応じて適切なコマンドを生成し、CommandInvoker
を通じて実行します。
このパターンを使用することで、以下のような利点があります:
- 操作の追加や変更が容易になります。新しいコマンドを追加するだけで、新しい機能を実装できます。
- 操作の取り消し(Undo)や再実行(Redo)が簡単に実装できます。
- マクロコマンド(複数のコマンドをまとめたもの)の実装が容易になります。
- コマンドをシリアライズして保存し、後で再生することができます(リプレイ機能など)。
第19章: ファクトリーパターン
ファクトリーパターンは、オブジェクトの作成ロジックを隠蔽し、クライアントコードから分離するためのデザインパターンです。これにより、オブジェクトの生成を柔軟に行うことができます。
以下は、ファクトリーパターンを使用した例です:
public abstract class Enemy : MonoBehaviour
{
public abstract void Attack();
}
public class Slime : Enemy
{
public override void Attack()
{
Debug.Log("スライムが体当たりをしました!");
}
}
public class Goblin : Enemy
{
public override void Attack()
{
Debug.Log("ゴブリンが剣で攻撃しました!");
}
}
public class Dragon : Enemy
{
public override void Attack()
{
Debug.Log("ドラゴンが炎を吐きました!");
}
}
public enum EnemyType
{
Slime,
Goblin,
Dragon
}
public class EnemyFactory : MonoBehaviour
{
public GameObject slimePrefab;
public GameObject goblinPrefab;
public GameObject dragonPrefab;
public Enemy CreateEnemy(EnemyType type, Vector3 position)
{
GameObject enemyObject = null;
switch (type)
{
case EnemyType.Slime:
enemyObject = Instantiate(slimePrefab, position, Quaternion.identity);
break;
case EnemyType.Goblin:
enemyObject = Instantiate(goblinPrefab, position, Quaternion.identity);
break;
case EnemyType.Dragon:
enemyObject = Instantiate(dragonPrefab, position, Quaternion.identity);
break;
default:
Debug.LogError("Unknown enemy type");
return null;
}
Enemy enemy = enemyObject.GetComponent<Enemy>();
if (enemy == null)
{
Debug.LogError("Enemy component not found on prefab");
Destroy(enemyObject);
return null;
}
return enemy;
}
}
public class EnemySpawner : MonoBehaviour
{
public EnemyFactory enemyFactory;
void Start()
{
SpawnRandomEnemies(5);
}
void SpawnRandomEnemies(int count)
{
for (int i = 0; i < count; i++)
{
EnemyType randomType = (EnemyType)Random.Range(0, System.Enum.GetValues(typeof(EnemyType)).Length);
Vector3 randomPosition = new Vector3(Random.Range(-10f, 10f), 0, Random.Range(-10f, 10f));
Enemy enemy = enemyFactory.CreateEnemy(randomType, randomPosition);
if (enemy != null)
{
enemy.Attack();
}
}
}
}
この例では、EnemyFactory
クラスが敵キャラクターの生成を担当しています。CreateEnemy
メソッドは、指定されたEnemyType
に基づいて適切な敵を生成します。
EnemySpawner
クラスはEnemyFactory
を使用して、ランダムな敵を生成し、配置します。
このパターンを使用することで、以下のような利点があります:
- オブジェクトの生成ロジックを一箇所にまとめることができ、コードの保守性が向上します。
- 新しい種類の敵を追加する際、ファクトリークラスの修正だけで済みます。
- オブジェクトの生成に関する複雑なロジックをクライアントコードから隠蔽できます。
- テストが容易になります。モックファクトリーを使用して、特定の敵タイプのみをテストすることができます。
第20章: コンポーネントパターン
コンポーネントパターンは、ゲームオブジェクトの機能を小さな、再利用可能なコンポーネントに分割するデザインパターンです。Unityのコンポーネントシステムはこのパターンに基づいています。
以下は、コンポーネントパターンを活用した例です:
public class Health : MonoBehaviour
{
public int maxHealth = 100;
private int currentHealth;
void Start()
{
currentHealth = maxHealth;
}
public void TakeDamage(int damage)
{
currentHealth -= damage;
if (currentHealth <= 0)
{
Die();
}
}
private void Die()
{
Debug.Log($"{gameObject.name}が倒れました。");
Destroy(gameObject);
}
}
public class Movement : MonoBehaviour
{
public float speed = 5f;
void Update()
{
float horizontal = Input.GetAxis("Horizontal");
float vertical = Input.GetAxis("Vertical");
Vector3 movement = new Vector3(horizontal, 0f, vertical).normalized * speed * Time.deltaTime;
transform.Translate(movement);
}
}
public class Weapon : MonoBehaviour
{
public int damage = 10;
public float attackRange = 2f;
public void Attack()
{
Collider[] hitColliders = Physics.OverlapSphere(transform.position, attackRange);
foreach (var hitCollider in hitColliders)
{
Health health = hitCollider.GetComponent<Health>();
if (health != null)
{
health.TakeDamage(damage);
Debug.Log($"{hitCollider.name}に{damage}ダメージを与えました。");
}
}
}
}
public class Player : MonoBehaviour
{
private Health health;
private Movement movement;
private Weapon weapon;
void Start()
{
health = GetComponent<Health>();
movement = GetComponent<Movement>();
weapon = GetComponent<Weapon>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
weapon.Attack();
}
}
}
public class Enemy : MonoBehaviour
{
private Health health;
void Start()
{
health = GetComponent<Health>();
}
void OnCollisionEnter(Collision collision)
{
if (collision.gameObject.CompareTag("Player"))
{
Health playerHealth = collision.gameObject.GetComponent<Health>();
if (playerHealth != null)
{
playerHealth.TakeDamage(10);
Debug.Log("敵がプレイヤーに接触してダメージを与えました。");
}
}
}
}
この例では、ゲームオブジェクトの機能を複数のコンポーネントに分割しています:
-
Health
: オブジェクトの体力を管理します。 -
Movement
: オブジェクトの移動を制御します。 -
Weapon
: 攻撃機能を提供します。 -
Player
: プレイヤー特有の動作を定義します。 -
Enemy
: 敵特有の動作を定義します。
各コンポーネントは特定の機能に特化しており、必要に応じて異なるゲームオブジェクトに追加できます。例えば、Health
コンポーネントはプレイヤーにも敵にも使用できます。
このパターンを使用することで、以下のような利点があります:
- コードの再利用性が高まります。同じコンポーネントを異なるオブジェクトで使用できます。
- 機能の追加や削除が容易になります。新しいコンポーネントを追加したり、既存のコンポーネントを削除したりするだけで、オブジェクトの機能を変更できます。
- コードの保守性が向上します。各コンポーネントは独立しているため、一つの機能を修正する際に他の機能に影響を与えにくくなります。
- 柔軟性が高まります。異なるコンポーネントの組み合わせで、多様なゲームオブジェクトを作成できます。
以上で、UnityでのC#プログラミングに関する20章の解説を終わります。これらの概念と技術を理解し、実践することで、より効率的で保守性の高いゲーム開発が可能になります。常に新しい知識を吸収し、実際のプロジェクトで適用していくことが重要です。がんばってください!