← スキル一覧に戻る

csharp-unity-patterns
by henboffman
⭐ 1🍴 0📅 2026年1月21日
SKILL.md
name: csharp-unity-patterns description: C# coding patterns, best practices, and architectural guidelines for Unity game development. Use when writing C# scripts in Unity, implementing design patterns for games, optimizing Unity code, or establishing code architecture for Unity projects. Covers MonoBehaviour lifecycle, ScriptableObjects, events, coroutines, and Unity-specific C# patterns.
C# Unity Patterns
Best practices and patterns for writing clean, performant C# code in Unity.
MonoBehaviour Lifecycle
Execution Order
Awake() → Called once when script instance loads
OnEnable() → Called when object becomes active
Start() → Called once before first Update
FixedUpdate() → Physics updates (fixed timestep)
Update() → Called every frame
LateUpdate() → Called after all Updates
OnDisable() → Called when object becomes inactive
OnDestroy() → Called when object is destroyed
When to Use Each
public class LifecycleExample : MonoBehaviour
{
// Awake: Initialize self, get own components
// Called even if script is disabled
void Awake()
{
rb = GetComponent<Rigidbody2D>();
spriteRenderer = GetComponent<SpriteRenderer>();
}
// OnEnable: Subscribe to events, reset state
void OnEnable()
{
GameEvents.OnGamePaused += HandlePause;
}
// Start: Initialize references to other objects
// Called only if script is enabled
void Start()
{
player = FindObjectOfType<Player>();
uiManager = UIManager.Instance;
}
// Update: Input, non-physics game logic
void Update()
{
HandleInput();
UpdateAnimations();
}
// FixedUpdate: Physics, movement
void FixedUpdate()
{
rb.MovePosition(rb.position + velocity * Time.fixedDeltaTime);
}
// LateUpdate: Camera follow, after-movement adjustments
void LateUpdate()
{
UpdateCamera();
}
// OnDisable: Unsubscribe from events
void OnDisable()
{
GameEvents.OnGamePaused -= HandlePause;
}
// OnDestroy: Cleanup, save state if needed
void OnDestroy()
{
SaveProgress();
}
}
Singleton Pattern
Basic Singleton
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
Lazy Singleton (Creates if Missing)
public class AudioManager : MonoBehaviour
{
private static AudioManager instance;
public static AudioManager Instance
{
get
{
if (instance == null)
{
instance = FindObjectOfType<AudioManager>();
if (instance == null)
{
var go = new GameObject("AudioManager");
instance = go.AddComponent<AudioManager>();
DontDestroyOnLoad(go);
}
}
return instance;
}
}
}
Generic Singleton Base Class
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T instance;
private static readonly object lockObj = new object();
public static T Instance
{
get
{
lock (lockObj)
{
if (instance == null)
{
instance = FindObjectOfType<T>();
if (instance == null)
{
var go = new GameObject(typeof(T).Name);
instance = go.AddComponent<T>();
}
}
return instance;
}
}
}
protected virtual void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this as T;
DontDestroyOnLoad(gameObject);
}
}
// Usage:
public class GameManager : Singleton<GameManager>
{
protected override void Awake()
{
base.Awake();
// Additional initialization
}
}
Event Systems
C# Events
public class Player : MonoBehaviour
{
// Simple event
public event Action OnDeath;
// Event with data
public event Action<int> OnHealthChanged;
public event Action<int, int> OnDamageTaken; // damage, currentHealth
private int health;
public void TakeDamage(int damage)
{
health -= damage;
OnHealthChanged?.Invoke(health);
OnDamageTaken?.Invoke(damage, health);
if (health <= 0)
OnDeath?.Invoke();
}
}
// Subscriber
public class HealthUI : MonoBehaviour
{
[SerializeField] private Player player;
void OnEnable()
{
player.OnHealthChanged += UpdateHealthBar;
}
void OnDisable()
{
player.OnHealthChanged -= UpdateHealthBar;
}
private void UpdateHealthBar(int newHealth)
{
// Update UI
}
}
Static Event Bus
public static class GameEvents
{
public static event Action OnGamePaused;
public static event Action OnGameResumed;
public static event Action<int> OnScoreChanged;
public static event Action<Season> OnSeasonChanged;
public static void TriggerPause() => OnGamePaused?.Invoke();
public static void TriggerResume() => OnGameResumed?.Invoke();
public static void TriggerScoreChange(int score) => OnScoreChanged?.Invoke(score);
public static void TriggerSeasonChange(Season season) => OnSeasonChanged?.Invoke(season);
}
ScriptableObject Event System
[CreateAssetMenu(fileName = "GameEvent", menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
private readonly List<GameEventListener> listeners = new();
public void Raise()
{
for (int i = listeners.Count - 1; i >= 0; i--)
listeners[i].OnEventRaised();
}
public void RegisterListener(GameEventListener listener) => listeners.Add(listener);
public void UnregisterListener(GameEventListener listener) => listeners.Remove(listener);
}
public class GameEventListener : MonoBehaviour
{
[SerializeField] private GameEvent gameEvent;
[SerializeField] private UnityEvent response;
void OnEnable() => gameEvent.RegisterListener(this);
void OnDisable() => gameEvent.UnregisterListener(this);
public void OnEventRaised() => response.Invoke();
}
ScriptableObject Patterns
Data Container
[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class ItemData : ScriptableObject
{
[Header("Basic Info")]
public string itemName;
[TextArea(2, 5)] public string description;
public Sprite icon;
[Header("Properties")]
public ItemType itemType;
public int maxStack = 99;
public int buyPrice;
public int sellPrice;
[Header("Usage")]
public bool isConsumable;
public int healAmount;
public int energyRestore;
}
Runtime Data (Variables)
[CreateAssetMenu(fileName = "IntVariable", menuName = "Variables/Int")]
public class IntVariable : ScriptableObject
{
[SerializeField] private int initialValue;
[NonSerialized] public int RuntimeValue;
public void OnEnable()
{
RuntimeValue = initialValue;
}
public void Add(int amount) => RuntimeValue += amount;
public void Set(int value) => RuntimeValue = value;
}
// Usage in Inspector: Reference the same IntVariable asset
// across multiple components to share state
Configuration Assets
[CreateAssetMenu(fileName = "GameConfig", menuName = "Config/Game Config")]
public class GameConfig : ScriptableObject
{
[Header("Time Settings")]
public float secondsPerGameMinute = 0.5f;
public int dayStartHour = 6;
public int dayEndHour = 22;
[Header("Player Settings")]
public float walkSpeed = 3f;
public float runSpeed = 6f;
public int maxStamina = 100;
[Header("Economy")]
public float sellPriceMultiplier = 0.5f;
}
Object Pooling
Simple Pool
public class ObjectPool : MonoBehaviour
{
[SerializeField] private GameObject prefab;
[SerializeField] private int initialSize = 10;
private Queue<GameObject> pool = new Queue<GameObject>();
void Start()
{
for (int i = 0; i < initialSize; i++)
{
CreateNewObject();
}
}
private GameObject CreateNewObject()
{
var obj = Instantiate(prefab, transform);
obj.SetActive(false);
pool.Enqueue(obj);
return obj;
}
public GameObject Get()
{
if (pool.Count == 0)
CreateNewObject();
var obj = pool.Dequeue();
obj.SetActive(true);
return obj;
}
public void Return(GameObject obj)
{
obj.SetActive(false);
pool.Enqueue(obj);
}
}
Generic Pool with Interface
public interface IPoolable
{
void OnSpawn();
void OnDespawn();
}
public class GenericPool<T> where T : MonoBehaviour, IPoolable
{
private readonly T prefab;
private readonly Transform parent;
private readonly Queue<T> pool = new Queue<T>();
public GenericPool(T prefab, Transform parent, int initialSize)
{
this.prefab = prefab;
this.parent = parent;
for (int i = 0; i < initialSize; i++)
pool.Enqueue(CreateNew());
}
private T CreateNew()
{
var obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
return obj;
}
public T Get(Vector3 position, Quaternion rotation)
{
var obj = pool.Count > 0 ? pool.Dequeue() : CreateNew();
obj.transform.SetPositionAndRotation(position, rotation);
obj.gameObject.SetActive(true);
obj.OnSpawn();
return obj;
}
public void Return(T obj)
{
obj.OnDespawn();
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
}
}
State Machine
Simple State Machine
public interface IState
{
void Enter();
void Update();
void Exit();
}
public class StateMachine
{
private IState currentState;
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState.Enter();
}
public void Update()
{
currentState?.Update();
}
}
// Example states
public class IdleState : IState
{
private readonly Player player;
public IdleState(Player player) => this.player = player;
public void Enter() => player.Animator.Play("Idle");
public void Update() { /* Check for input */ }
public void Exit() { }
}
Enum-Based State Machine
public class EnemyAI : MonoBehaviour
{
public enum State { Idle, Patrol, Chase, Attack }
private State currentState = State.Idle;
void Update()
{
switch (currentState)
{
case State.Idle:
UpdateIdle();
break;
case State.Patrol:
UpdatePatrol();
break;
case State.Chase:
UpdateChase();
break;
case State.Attack:
UpdateAttack();
break;
}
}
public void SetState(State newState)
{
if (currentState == newState) return;
ExitState(currentState);
currentState = newState;
EnterState(newState);
}
private void EnterState(State state)
{
switch (state)
{
case State.Chase:
// Start chase animation
break;
}
}
private void ExitState(State state)
{
// Cleanup for state
}
}
Coroutines
Common Patterns
public class CoroutineExamples : MonoBehaviour
{
// Delayed action
public void DoAfterDelay(float delay, Action action)
{
StartCoroutine(DelayedAction(delay, action));
}
private IEnumerator DelayedAction(float delay, Action action)
{
yield return new WaitForSeconds(delay);
action?.Invoke();
}
// Fade effect
public IEnumerator FadeOut(SpriteRenderer sr, float duration)
{
Color color = sr.color;
float startAlpha = color.a;
for (float t = 0; t < duration; t += Time.deltaTime)
{
color.a = Mathf.Lerp(startAlpha, 0, t / duration);
sr.color = color;
yield return null;
}
color.a = 0;
sr.color = color;
}
// Wait for condition
public IEnumerator WaitForCondition(Func<bool> condition, Action onComplete)
{
yield return new WaitUntil(condition);
onComplete?.Invoke();
}
// Repeat with interval
public IEnumerator RepeatAction(float interval, Action action)
{
while (true)
{
action?.Invoke();
yield return new WaitForSeconds(interval);
}
}
// Cancellable coroutine
private Coroutine runningRoutine;
public void StartMyRoutine()
{
if (runningRoutine != null)
StopCoroutine(runningRoutine);
runningRoutine = StartCoroutine(MyRoutine());
}
public void StopMyRoutine()
{
if (runningRoutine != null)
{
StopCoroutine(runningRoutine);
runningRoutine = null;
}
}
}
Component Access Patterns
Cached Component References
public class Player : MonoBehaviour
{
// Serialize for Inspector assignment (preferred)
[SerializeField] private Rigidbody2D rb;
[SerializeField] private Animator animator;
[SerializeField] private SpriteRenderer spriteRenderer;
// Or cache in Awake
void Awake()
{
rb ??= GetComponent<Rigidbody2D>();
animator ??= GetComponent<Animator>();
spriteRenderer ??= GetComponent<SpriteRenderer>();
}
}
RequireComponent Attribute
[RequireComponent(typeof(Rigidbody2D))]
[RequireComponent(typeof(Collider2D))]
public class PhysicsEntity : MonoBehaviour
{
private Rigidbody2D rb;
void Awake()
{
rb = GetComponent<Rigidbody2D>(); // Guaranteed to exist
}
}
TryGetComponent (Unity 2019.2+)
void OnTriggerEnter2D(Collider2D other)
{
// More efficient than GetComponent + null check
if (other.TryGetComponent<IDamageable>(out var damageable))
{
damageable.TakeDamage(10);
}
}
Performance Tips
Avoid in Update()
// BAD - allocates every frame
void Update()
{
var enemies = FindObjectsOfType<Enemy>();
string name = "Player" + score.ToString();
}
// GOOD - cache references
private Enemy[] enemies;
private StringBuilder nameBuilder = new StringBuilder();
void Start()
{
enemies = FindObjectsOfType<Enemy>();
}
Use NonAlloc Methods
// BAD - allocates new array
var hits = Physics2D.RaycastAll(origin, direction);
// GOOD - reuse buffer
private RaycastHit2D[] hitBuffer = new RaycastHit2D[10];
void CheckHits()
{
int hitCount = Physics2D.RaycastNonAlloc(origin, direction, hitBuffer);
for (int i = 0; i < hitCount; i++)
{
// Process hitBuffer[i]
}
}
CompareTag Instead of == for Tags
// BAD - allocates string
if (other.tag == "Player")
// GOOD - no allocation
if (other.CompareTag("Player"))
Reference Files
- references/attributes.md - Unity C# attributes reference
- references/serialization.md - Unity serialization rules and patterns
スコア
総合スコア
40/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です