Naming Conventions
- PascalCase for public fields, properties, methods, and class names.
- camelCase for private fields, local variables, and parameters.
- Prefix interfaces with
I(e.g.,IUpdateListener). - Use descriptive names — avoid abbreviations unless universally understood (
fps,id).
MonoBehaviour Lifecycle
Awake()for self-initialization: singleton setup, internal state, component caching.Start()only when initialization depends on other objects being ready.OnEnable()/OnDisable()for registering / unregistering listeners — prevents stale references.- Prefer
FixedUpdate()for physics and time-critical logic. - Avoid per-frame
Update()when a custom update cycle (via a central manager) is available.
Singleton Pattern
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
Rules:
- Guard against duplicates in
Awake()withDestroy(this)orDestroy(gameObject). - Call
DontDestroyOnLoad(gameObject)for persistent managers. - Never use singletons for data that should be per-scene.
Serialization & Inspector
- Use
[SerializeField]for private fields that need Inspector exposure — never make them public just for the Inspector. - Use
[System.Serializable]for nested data classes shown in the Inspector. - Avoid exposing runtime-only fields to the Inspector.
- Use
[Header("Section")]and[Tooltip("...")]to organize complex inspectors.
Performance (Hot Paths)
- Zero allocations in
Update,FixedUpdate, and listener callbacks. - Cache component references in
Awake()— never callGetComponent<T>()per frame. - Pre-allocate
List<T>with expected capacity. - Prefer
foreachover LINQ in hot paths — LINQ allocates enumerators. - Use object pooling for frequently instantiated/destroyed objects.
- Prefer
CompareTag("Enemy")overgameObject.tag == "Enemy"(avoids string alloc).
Interfaces & Custom Update Cycles
public interface IUpdateListener
{
void OnTick(float deltaTime);
}
// Register in OnEnable, unregister in OnDisable
private void OnEnable() => UpdateManager.Instance.Register(this);
private void OnDisable() => UpdateManager.Instance.Unregister(this);
Benefits:
- Decoupled communication between systems.
- Controlled execution order via the manager.
- Easy to pause/resume groups of listeners.
ScriptableObject for Data
- Use
ScriptableObjectfor shared configuration, game balance data, and event channels. - Create assets via
[CreateAssetMenu(fileName = "New Config", menuName = "Game/Config")]. - Prefer ScriptableObject events over direct references for loose coupling.
Assembly Definitions
- Use
.asmdeffiles to split code into assemblies for faster compile times. - Separate Editor code into its own assembly with
Editorplatform only. - Keep test assemblies separate with
[TestAssembly]references.
Code Style
- One primary responsibility per script/class.
- Keep methods under ~30 lines — extract helpers for complex logic.
- Use enums for fixed value sets (e.g.,
UpdateCycle,DamageType). - Use
#regionsparingly — prefer small classes over large regions. - Always add
<summary>XML doc comments on public types and methods.
Common Pitfalls
- Coroutine leak: Always
StopCoroutineorStopAllCoroutinesinOnDisable. - Null after Destroy: Unity overloads
==onUnityEngine.Object— useif (obj != null)or the null-conditional pattern, but never rely on C#is null. - Awake order: Don't depend on
Awake()order between scripts — useStart()or explicit initialization order via[DefaultExecutionOrder]. - String-based APIs: Avoid
SendMessage,Invoke("MethodName")— use direct calls or events.