Unity Framework Guide¶
Framework: Unity 2022 LTS+ with C# Type: Game Engine / Interactive Applications Use Cases: Games, VR/AR, Simulations, Interactive Media
Overview¶
Unity is a cross-platform game engine for creating 2D/3D games, VR/AR experiences, simulations, and interactive applications. It uses C# as its primary scripting language.
When to Use Unity¶
- ✅ 2D and 3D game development
- ✅ VR/AR applications (Meta Quest, HoloLens, etc.)
- ✅ Interactive simulations and training
- ✅ Cross-platform deployment (PC, Mobile, Console, Web)
- ✅ Rapid prototyping with visual editor
When NOT to Use Unity¶
- ❌ Web applications (use Blazor, ASP.NET)
- ❌ Enterprise business software
- ❌ Simple mobile apps without 3D/game elements
- ❌ High-performance AAA graphics (consider Unreal)
Project Structure¶
MyGame/
├── Assets/
│ ├── _Project/ # Project-specific assets
│ │ ├── Art/
│ │ │ ├── Materials/
│ │ │ ├── Models/
│ │ │ ├── Sprites/
│ │ │ └── Textures/
│ │ ├── Audio/
│ │ │ ├── Music/
│ │ │ └── SFX/
│ │ ├── Prefabs/
│ │ │ ├── Characters/
│ │ │ ├── Environment/
│ │ │ └── UI/
│ │ ├── Scenes/
│ │ │ ├── MainMenu.unity
│ │ │ ├── GameLevel.unity
│ │ │ └── Loading.unity
│ │ ├── Scripts/
│ │ │ ├── Core/ # Core systems
│ │ │ │ ├── GameManager.cs
│ │ │ │ ├── SceneLoader.cs
│ │ │ │ └── ServiceLocator.cs
│ │ │ ├── Player/
│ │ │ │ ├── PlayerController.cs
│ │ │ │ ├── PlayerInput.cs
│ │ │ │ └── PlayerHealth.cs
│ │ │ ├── Enemies/
│ │ │ ├── UI/
│ │ │ │ ├── HUDController.cs
│ │ │ │ └── MenuController.cs
│ │ │ ├── Systems/
│ │ │ │ ├── AudioManager.cs
│ │ │ │ ├── SaveSystem.cs
│ │ │ │ └── ObjectPool.cs
│ │ │ └── Data/
│ │ │ ├── GameSettings.cs
│ │ │ └── PlayerData.cs
│ │ ├── ScriptableObjects/
│ │ │ ├── Items/
│ │ │ ├── Enemies/
│ │ │ └── Settings/
│ │ └── Settings/
│ │ ├── InputActions.inputactions
│ │ └── GameSettings.asset
│ ├── Plugins/ # Third-party plugins
│ └── Resources/ # Runtime-loaded assets
├── Packages/
│ └── manifest.json
├── ProjectSettings/
│ ├── ProjectSettings.asset
│ ├── InputManager.asset
│ └── TagManager.asset
├── Tests/
│ ├── EditMode/
│ └── PlayMode/
└── .gitignore
Dependencies (Package Manager)¶
manifest.json¶
{
"dependencies": {
"com.unity.2d.sprite": "1.0.0",
"com.unity.2d.tilemap": "1.0.0",
"com.unity.cinemachine": "2.9.7",
"com.unity.inputsystem": "1.7.0",
"com.unity.textmeshpro": "3.0.6",
"com.unity.addressables": "1.21.19",
"com.unity.localization": "1.4.5",
"com.unity.ai.navigation": "1.1.5",
"com.unity.test-framework": "1.3.9",
"com.unity.ide.rider": "3.0.27"
}
}
Core Components¶
MonoBehaviour Lifecycle¶
using UnityEngine;
namespace MyGame.Core
{
/// <summary>
/// Demonstrates the Unity MonoBehaviour lifecycle.
/// Methods are called in the order shown.
/// </summary>
public class LifecycleExample : MonoBehaviour
{
// === INITIALIZATION ===
// Called when script instance is loaded (even if disabled)
private void Awake()
{
// Initialize references, don't access other objects
Debug.Log("Awake");
}
// Called before first Update (only if enabled)
private void Start()
{
// Safe to access other objects initialized in Awake
Debug.Log("Start");
}
// Called when object becomes enabled
private void OnEnable()
{
// Subscribe to events
Debug.Log("OnEnable");
}
// === UPDATE LOOP ===
// Called every frame
private void Update()
{
// Game logic, input handling
// Time.deltaTime for frame-independent movement
}
// Called every fixed timestep (physics)
private void FixedUpdate()
{
// Physics calculations, rigidbody movement
// Time.fixedDeltaTime is constant
}
// Called after all Update calls
private void LateUpdate()
{
// Camera follow, post-processing
}
// === CLEANUP ===
// Called when object becomes disabled
private void OnDisable()
{
// Unsubscribe from events
Debug.Log("OnDisable");
}
// Called when object is destroyed
private void OnDestroy()
{
// Final cleanup
Debug.Log("OnDestroy");
}
}
}
Game Manager (Singleton Pattern)¶
using UnityEngine;
using UnityEngine.SceneManagement;
namespace MyGame.Core
{
public enum GameState
{
MainMenu,
Playing,
Paused,
GameOver
}
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("Settings")]
[SerializeField] private GameSettings settings;
public GameState CurrentState { get; private set; } = GameState.MainMenu;
public int Score { get; private set; }
public int HighScore { get; private set; }
public event System.Action<GameState> OnGameStateChanged;
public event System.Action<int> OnScoreChanged;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
LoadHighScore();
}
public void SetState(GameState newState)
{
if (CurrentState == newState) return;
CurrentState = newState;
OnGameStateChanged?.Invoke(newState);
switch (newState)
{
case GameState.Playing:
Time.timeScale = 1f;
break;
case GameState.Paused:
Time.timeScale = 0f;
break;
case GameState.GameOver:
Time.timeScale = 0f;
SaveHighScore();
break;
}
}
public void AddScore(int points)
{
Score += points;
OnScoreChanged?.Invoke(Score);
if (Score > HighScore)
{
HighScore = Score;
}
}
public void ResetGame()
{
Score = 0;
OnScoreChanged?.Invoke(Score);
}
public void LoadScene(string sceneName)
{
SceneManager.LoadSceneAsync(sceneName);
}
public void QuitGame()
{
SaveHighScore();
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
private void LoadHighScore()
{
HighScore = PlayerPrefs.GetInt("HighScore", 0);
}
private void SaveHighScore()
{
PlayerPrefs.SetInt("HighScore", HighScore);
PlayerPrefs.Save();
}
}
}
Service Locator Pattern¶
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MyGame.Core
{
/// <summary>
/// Service Locator for dependency management.
/// Alternative to Singleton pattern.
/// </summary>
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> Services = new();
public static void Register<T>(T service) where T : class
{
var type = typeof(T);
if (Services.ContainsKey(type))
{
Debug.LogWarning($"Service {type.Name} already registered. Replacing.");
}
Services[type] = service;
}
public static T Get<T>() where T : class
{
var type = typeof(T);
if (Services.TryGetValue(type, out var service))
{
return service as T;
}
Debug.LogError($"Service {type.Name} not found!");
return null;
}
public static bool TryGet<T>(out T service) where T : class
{
var type = typeof(T);
if (Services.TryGetValue(type, out var obj))
{
service = obj as T;
return true;
}
service = null;
return false;
}
public static void Unregister<T>() where T : class
{
Services.Remove(typeof(T));
}
public static void Clear()
{
Services.Clear();
}
}
// Usage: Register services in bootstrapper
public class GameBootstrapper : MonoBehaviour
{
[SerializeField] private AudioManager audioManager;
[SerializeField] private SaveSystem saveSystem;
private void Awake()
{
ServiceLocator.Register<IAudioManager>(audioManager);
ServiceLocator.Register<ISaveSystem>(saveSystem);
}
private void OnDestroy()
{
ServiceLocator.Clear();
}
}
}
Player Controller¶
Input System Setup¶
using UnityEngine;
using UnityEngine.InputSystem;
namespace MyGame.Player
{
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float sprintMultiplier = 1.5f;
[SerializeField] private float jumpHeight = 2f;
[SerializeField] private float gravity = -15f;
[Header("Look")]
[SerializeField] private float lookSensitivity = 1f;
[SerializeField] private float maxLookAngle = 85f;
[SerializeField] private Transform cameraTransform;
private CharacterController controller;
private PlayerInput playerInput;
private Vector2 moveInput;
private Vector2 lookInput;
private bool sprintInput;
private bool jumpInput;
private Vector3 velocity;
private float xRotation;
private bool isGrounded;
private void Awake()
{
controller = GetComponent<CharacterController>();
playerInput = GetComponent<PlayerInput>();
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void OnEnable()
{
// Subscribe to Input System events
var actionMap = playerInput.actions;
actionMap["Move"].performed += OnMove;
actionMap["Move"].canceled += OnMove;
actionMap["Look"].performed += OnLook;
actionMap["Look"].canceled += OnLook;
actionMap["Jump"].performed += OnJump;
actionMap["Sprint"].performed += OnSprint;
actionMap["Sprint"].canceled += OnSprint;
}
private void OnDisable()
{
var actionMap = playerInput.actions;
actionMap["Move"].performed -= OnMove;
actionMap["Move"].canceled -= OnMove;
actionMap["Look"].performed -= OnLook;
actionMap["Look"].canceled -= OnLook;
actionMap["Jump"].performed -= OnJump;
actionMap["Sprint"].performed -= OnSprint;
actionMap["Sprint"].canceled -= OnSprint;
}
private void Update()
{
HandleMovement();
HandleLook();
}
private void HandleMovement()
{
isGrounded = controller.isGrounded;
if (isGrounded && velocity.y < 0)
{
velocity.y = -2f; // Small downward force to keep grounded
}
// Calculate move direction relative to camera
var forward = transform.forward;
var right = transform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
var currentSpeed = sprintInput ? moveSpeed * sprintMultiplier : moveSpeed;
var moveDirection = (forward * moveInput.y + right * moveInput.x) * currentSpeed;
// Apply gravity
velocity.y += gravity * Time.deltaTime;
// Jump
if (jumpInput && isGrounded)
{
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
jumpInput = false;
}
// Move
controller.Move((moveDirection + velocity) * Time.deltaTime);
}
private void HandleLook()
{
// Horizontal rotation (body)
transform.Rotate(Vector3.up * lookInput.x * lookSensitivity);
// Vertical rotation (camera only)
xRotation -= lookInput.y * lookSensitivity;
xRotation = Mathf.Clamp(xRotation, -maxLookAngle, maxLookAngle);
cameraTransform.localRotation = Quaternion.Euler(xRotation, 0f, 0f);
}
// Input callbacks
private void OnMove(InputAction.CallbackContext context) =>
moveInput = context.ReadValue<Vector2>();
private void OnLook(InputAction.CallbackContext context) =>
lookInput = context.ReadValue<Vector2>();
private void OnJump(InputAction.CallbackContext context) =>
jumpInput = true;
private void OnSprint(InputAction.CallbackContext context) =>
sprintInput = context.ReadValueAsButton();
}
}
Player Health System¶
using UnityEngine;
using UnityEngine.Events;
namespace MyGame.Player
{
public class PlayerHealth : MonoBehaviour, IDamageable
{
[Header("Health")]
[SerializeField] private int maxHealth = 100;
[SerializeField] private float invincibilityDuration = 0.5f;
[Header("Events")]
public UnityEvent<int, int> OnHealthChanged; // current, max
public UnityEvent OnDeath;
public UnityEvent OnDamaged;
public int CurrentHealth { get; private set; }
public bool IsAlive => CurrentHealth > 0;
public bool IsInvincible { get; private set; }
private void Start()
{
CurrentHealth = maxHealth;
OnHealthChanged?.Invoke(CurrentHealth, maxHealth);
}
public void TakeDamage(int damage)
{
if (!IsAlive || IsInvincible) return;
CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
OnHealthChanged?.Invoke(CurrentHealth, maxHealth);
OnDamaged?.Invoke();
if (CurrentHealth <= 0)
{
Die();
}
else
{
StartCoroutine(InvincibilityCoroutine());
}
}
public void Heal(int amount)
{
if (!IsAlive) return;
CurrentHealth = Mathf.Min(maxHealth, CurrentHealth + amount);
OnHealthChanged?.Invoke(CurrentHealth, maxHealth);
}
private void Die()
{
OnDeath?.Invoke();
GameManager.Instance?.SetState(GameState.GameOver);
}
private System.Collections.IEnumerator InvincibilityCoroutine()
{
IsInvincible = true;
yield return new WaitForSeconds(invincibilityDuration);
IsInvincible = false;
}
}
public interface IDamageable
{
void TakeDamage(int damage);
}
}
ScriptableObjects¶
Item Data¶
using UnityEngine;
namespace MyGame.Data
{
public enum ItemType
{
Consumable,
Equipment,
Quest,
Material
}
public enum Rarity
{
Common,
Uncommon,
Rare,
Epic,
Legendary
}
[CreateAssetMenu(fileName = "New Item", menuName = "Game/Items/Item Data")]
public class ItemData : ScriptableObject
{
[Header("Basic Info")]
public string itemName;
[TextArea(3, 5)]
public string description;
public Sprite icon;
public ItemType itemType;
public Rarity rarity;
[Header("Properties")]
public int maxStack = 99;
public int buyPrice;
public int sellPrice;
[Header("Effects")]
public int healthRestore;
public int damageBonus;
public int defenseBonus;
public Color GetRarityColor()
{
return rarity switch
{
Rarity.Common => Color.white,
Rarity.Uncommon => Color.green,
Rarity.Rare => Color.blue,
Rarity.Epic => new Color(0.5f, 0f, 0.5f), // Purple
Rarity.Legendary => new Color(1f, 0.5f, 0f), // Orange
_ => Color.white
};
}
}
}
Enemy Configuration¶
using UnityEngine;
namespace MyGame.Data
{
[CreateAssetMenu(fileName = "New Enemy", menuName = "Game/Enemies/Enemy Config")]
public class EnemyConfig : ScriptableObject
{
[Header("Identity")]
public string enemyName;
public GameObject prefab;
[Header("Stats")]
public int maxHealth = 50;
public int damage = 10;
public float moveSpeed = 3f;
public float attackRange = 2f;
public float attackCooldown = 1f;
[Header("AI")]
public float detectionRange = 10f;
public float chaseRange = 15f;
public bool canPatrol = true;
public float patrolWaitTime = 2f;
[Header("Rewards")]
public int experienceReward = 10;
public int scoreReward = 100;
public ItemData[] possibleDrops;
[Range(0f, 1f)]
public float dropChance = 0.3f;
}
}
Game Events (Observer Pattern)¶
using System.Collections.Generic;
using UnityEngine;
namespace MyGame.Data
{
// Parameterless event
[CreateAssetMenu(fileName = "New Game Event", menuName = "Game/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)
{
if (!listeners.Contains(listener))
listeners.Add(listener);
}
public void UnregisterListener(GameEventListener listener)
{
listeners.Remove(listener);
}
}
// Generic event with data
public abstract class GameEvent<T> : ScriptableObject
{
private readonly List<IGameEventListener<T>> listeners = new();
public void Raise(T value)
{
for (int i = listeners.Count - 1; i >= 0; i--)
{
listeners[i].OnEventRaised(value);
}
}
public void RegisterListener(IGameEventListener<T> listener)
{
if (!listeners.Contains(listener))
listeners.Add(listener);
}
public void UnregisterListener(IGameEventListener<T> listener)
{
listeners.Remove(listener);
}
}
public interface IGameEventListener<T>
{
void OnEventRaised(T value);
}
[CreateAssetMenu(fileName = "New Int Event", menuName = "Game/Events/Int Event")]
public class IntGameEvent : GameEvent<int> { }
[CreateAssetMenu(fileName = "New String Event", menuName = "Game/Events/String Event")]
public class StringGameEvent : GameEvent<string> { }
}
Event Listener Component¶
using UnityEngine;
using UnityEngine.Events;
namespace MyGame.Data
{
public class GameEventListener : MonoBehaviour
{
[SerializeField] private GameEvent gameEvent;
[SerializeField] private UnityEvent response;
private void OnEnable()
{
gameEvent?.RegisterListener(this);
}
private void OnDisable()
{
gameEvent?.UnregisterListener(this);
}
public void OnEventRaised()
{
response?.Invoke();
}
}
}
Object Pooling¶
using System.Collections.Generic;
using UnityEngine;
namespace MyGame.Systems
{
public class ObjectPool<T> where T : Component
{
private readonly T prefab;
private readonly Transform parent;
private readonly Queue<T> pool = new();
private readonly List<T> activeObjects = new();
public int ActiveCount => activeObjects.Count;
public int PoolCount => pool.Count;
public ObjectPool(T prefab, Transform parent, int initialSize = 10)
{
this.prefab = prefab;
this.parent = parent;
for (int i = 0; i < initialSize; i++)
{
CreateNew();
}
}
private T CreateNew()
{
var obj = Object.Instantiate(prefab, parent);
obj.gameObject.SetActive(false);
pool.Enqueue(obj);
return obj;
}
public T Get()
{
if (pool.Count == 0)
{
CreateNew();
}
var obj = pool.Dequeue();
obj.gameObject.SetActive(true);
activeObjects.Add(obj);
return obj;
}
public T Get(Vector3 position, Quaternion rotation)
{
var obj = Get();
obj.transform.SetPositionAndRotation(position, rotation);
return obj;
}
public void Return(T obj)
{
if (!activeObjects.Contains(obj)) return;
obj.gameObject.SetActive(false);
obj.transform.SetParent(parent);
activeObjects.Remove(obj);
pool.Enqueue(obj);
}
public void ReturnAll()
{
foreach (var obj in activeObjects.ToArray())
{
Return(obj);
}
}
}
// MonoBehaviour wrapper for inspector setup
public class BulletPool : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private int initialSize = 20;
private ObjectPool<Bullet> pool;
public static BulletPool Instance { get; private set; }
private void Awake()
{
Instance = this;
pool = new ObjectPool<Bullet>(bulletPrefab, transform, initialSize);
}
public Bullet GetBullet(Vector3 position, Quaternion rotation)
{
return pool.Get(position, rotation);
}
public void ReturnBullet(Bullet bullet)
{
pool.Return(bullet);
}
}
}
Audio Manager¶
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Audio;
namespace MyGame.Systems
{
public interface IAudioManager
{
void PlaySFX(AudioClip clip, float volume = 1f);
void PlaySFXAtPosition(AudioClip clip, Vector3 position, float volume = 1f);
void PlayMusic(AudioClip clip, bool loop = true);
void StopMusic();
void SetMasterVolume(float volume);
void SetMusicVolume(float volume);
void SetSFXVolume(float volume);
}
public class AudioManager : MonoBehaviour, IAudioManager
{
[Header("Audio Mixer")]
[SerializeField] private AudioMixer audioMixer;
[Header("Audio Sources")]
[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource sfxSource;
[Header("Pooling")]
[SerializeField] private int sfxPoolSize = 10;
private readonly Queue<AudioSource> sfxPool = new();
private readonly List<AudioSource> activeSfxSources = new();
private void Awake()
{
// Create SFX pool
for (int i = 0; i < sfxPoolSize; i++)
{
var source = gameObject.AddComponent<AudioSource>();
source.playOnAwake = false;
source.outputAudioMixerGroup = sfxSource.outputAudioMixerGroup;
sfxPool.Enqueue(source);
}
LoadVolumeSettings();
}
public void PlaySFX(AudioClip clip, float volume = 1f)
{
if (clip == null) return;
sfxSource.PlayOneShot(clip, volume);
}
public void PlaySFXAtPosition(AudioClip clip, Vector3 position, float volume = 1f)
{
if (clip == null) return;
var source = GetPooledSource();
if (source == null)
{
AudioSource.PlayClipAtPoint(clip, position, volume);
return;
}
source.transform.position = position;
source.clip = clip;
source.volume = volume;
source.spatialBlend = 1f; // 3D sound
source.Play();
StartCoroutine(ReturnToPoolAfterPlay(source, clip.length));
}
public void PlayMusic(AudioClip clip, bool loop = true)
{
if (clip == null) return;
musicSource.clip = clip;
musicSource.loop = loop;
musicSource.Play();
}
public void StopMusic()
{
musicSource.Stop();
}
public void SetMasterVolume(float volume)
{
audioMixer.SetFloat("MasterVolume", LinearToDecibel(volume));
PlayerPrefs.SetFloat("MasterVolume", volume);
}
public void SetMusicVolume(float volume)
{
audioMixer.SetFloat("MusicVolume", LinearToDecibel(volume));
PlayerPrefs.SetFloat("MusicVolume", volume);
}
public void SetSFXVolume(float volume)
{
audioMixer.SetFloat("SFXVolume", LinearToDecibel(volume));
PlayerPrefs.SetFloat("SFXVolume", volume);
}
private AudioSource GetPooledSource()
{
if (sfxPool.Count == 0) return null;
var source = sfxPool.Dequeue();
activeSfxSources.Add(source);
return source;
}
private System.Collections.IEnumerator ReturnToPoolAfterPlay(AudioSource source, float delay)
{
yield return new WaitForSeconds(delay);
source.Stop();
source.clip = null;
activeSfxSources.Remove(source);
sfxPool.Enqueue(source);
}
private void LoadVolumeSettings()
{
SetMasterVolume(PlayerPrefs.GetFloat("MasterVolume", 1f));
SetMusicVolume(PlayerPrefs.GetFloat("MusicVolume", 1f));
SetSFXVolume(PlayerPrefs.GetFloat("SFXVolume", 1f));
}
private float LinearToDecibel(float linear)
{
return linear > 0 ? Mathf.Log10(linear) * 20f : -80f;
}
}
}
Save System¶
using System;
using System.IO;
using UnityEngine;
namespace MyGame.Systems
{
public interface ISaveSystem
{
void Save<T>(string key, T data);
T Load<T>(string key, T defaultValue = default);
bool HasSave(string key);
void DeleteSave(string key);
void DeleteAllSaves();
}
[Serializable]
public class SaveData
{
public int level;
public int experience;
public int gold;
public float playTime;
public Vector3Serializable playerPosition;
public string[] unlockedItems;
public DateTime lastSaved;
}
[Serializable]
public struct Vector3Serializable
{
public float x, y, z;
public Vector3Serializable(Vector3 v)
{
x = v.x;
y = v.y;
z = v.z;
}
public Vector3 ToVector3() => new(x, y, z);
}
public class SaveSystem : MonoBehaviour, ISaveSystem
{
private string SavePath => Application.persistentDataPath;
public void Save<T>(string key, T data)
{
try
{
var json = JsonUtility.ToJson(data, true);
var path = GetFilePath(key);
File.WriteAllText(path, json);
Debug.Log($"Saved to {path}");
}
catch (Exception e)
{
Debug.LogError($"Failed to save {key}: {e.Message}");
}
}
public T Load<T>(string key, T defaultValue = default)
{
var path = GetFilePath(key);
if (!File.Exists(path))
{
return defaultValue;
}
try
{
var json = File.ReadAllText(path);
return JsonUtility.FromJson<T>(json);
}
catch (Exception e)
{
Debug.LogError($"Failed to load {key}: {e.Message}");
return defaultValue;
}
}
public bool HasSave(string key)
{
return File.Exists(GetFilePath(key));
}
public void DeleteSave(string key)
{
var path = GetFilePath(key);
if (File.Exists(path))
{
File.Delete(path);
}
}
public void DeleteAllSaves()
{
var files = Directory.GetFiles(SavePath, "*.json");
foreach (var file in files)
{
File.Delete(file);
}
}
private string GetFilePath(string key)
{
return Path.Combine(SavePath, $"{key}.json");
}
}
}
UI System¶
HUD Controller¶
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace MyGame.UI
{
public class HUDController : MonoBehaviour
{
[Header("Health")]
[SerializeField] private Slider healthBar;
[SerializeField] private TextMeshProUGUI healthText;
[Header("Score")]
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI highScoreText;
[Header("Animation")]
[SerializeField] private Animator scoreAnimator;
[SerializeField] private float healthLerpSpeed = 5f;
private float targetHealthPercent;
private void Start()
{
// Subscribe to events
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged += UpdateScore;
}
var playerHealth = FindObjectOfType<PlayerHealth>();
if (playerHealth != null)
{
playerHealth.OnHealthChanged.AddListener(UpdateHealth);
}
// Initialize
UpdateHighScore();
}
private void Update()
{
// Smooth health bar animation
if (healthBar != null)
{
healthBar.value = Mathf.Lerp(
healthBar.value,
targetHealthPercent,
healthLerpSpeed * Time.deltaTime
);
}
}
private void UpdateHealth(int current, int max)
{
targetHealthPercent = (float)current / max;
if (healthText != null)
{
healthText.text = $"{current}/{max}";
}
}
private void UpdateScore(int score)
{
if (scoreText != null)
{
scoreText.text = score.ToString("N0");
}
if (scoreAnimator != null)
{
scoreAnimator.SetTrigger("ScoreChanged");
}
UpdateHighScore();
}
private void UpdateHighScore()
{
if (highScoreText != null && GameManager.Instance != null)
{
highScoreText.text = $"Best: {GameManager.Instance.HighScore:N0}";
}
}
private void OnDestroy()
{
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged -= UpdateScore;
}
}
}
}
Pause Menu¶
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.InputSystem;
namespace MyGame.UI
{
public class PauseMenuController : MonoBehaviour
{
[Header("Panels")]
[SerializeField] private GameObject pausePanel;
[SerializeField] private GameObject settingsPanel;
[Header("Settings")]
[SerializeField] private Slider masterVolumeSlider;
[SerializeField] private Slider musicVolumeSlider;
[SerializeField] private Slider sfxVolumeSlider;
private IAudioManager audioManager;
private void Start()
{
pausePanel.SetActive(false);
settingsPanel.SetActive(false);
if (ServiceLocator.TryGet(out IAudioManager audio))
{
audioManager = audio;
}
LoadSettings();
}
public void OnPauseInput(InputAction.CallbackContext context)
{
if (context.performed)
{
TogglePause();
}
}
public void TogglePause()
{
if (GameManager.Instance == null) return;
if (GameManager.Instance.CurrentState == GameState.Playing)
{
Pause();
}
else if (GameManager.Instance.CurrentState == GameState.Paused)
{
Resume();
}
}
public void Pause()
{
pausePanel.SetActive(true);
settingsPanel.SetActive(false);
GameManager.Instance.SetState(GameState.Paused);
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
}
public void Resume()
{
pausePanel.SetActive(false);
settingsPanel.SetActive(false);
GameManager.Instance.SetState(GameState.Playing);
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
public void OpenSettings()
{
pausePanel.SetActive(false);
settingsPanel.SetActive(true);
}
public void CloseSettings()
{
settingsPanel.SetActive(false);
pausePanel.SetActive(true);
}
public void ReturnToMainMenu()
{
Time.timeScale = 1f;
GameManager.Instance?.LoadScene("MainMenu");
}
public void OnMasterVolumeChanged(float value)
{
audioManager?.SetMasterVolume(value);
}
public void OnMusicVolumeChanged(float value)
{
audioManager?.SetMusicVolume(value);
}
public void OnSFXVolumeChanged(float value)
{
audioManager?.SetSFXVolume(value);
}
private void LoadSettings()
{
masterVolumeSlider.value = PlayerPrefs.GetFloat("MasterVolume", 1f);
musicVolumeSlider.value = PlayerPrefs.GetFloat("MusicVolume", 1f);
sfxVolumeSlider.value = PlayerPrefs.GetFloat("SFXVolume", 1f);
}
}
}
Testing¶
Edit Mode Tests¶
using NUnit.Framework;
using MyGame.Data;
using UnityEngine;
namespace MyGame.Tests.EditMode
{
public class ItemDataTests
{
[Test]
public void GetRarityColor_Common_ReturnsWhite()
{
var item = ScriptableObject.CreateInstance<ItemData>();
item.rarity = Rarity.Common;
var color = item.GetRarityColor();
Assert.AreEqual(Color.white, color);
}
[Test]
public void GetRarityColor_Legendary_ReturnsOrange()
{
var item = ScriptableObject.CreateInstance<ItemData>();
item.rarity = Rarity.Legendary;
var color = item.GetRarityColor();
Assert.AreEqual(new Color(1f, 0.5f, 0f), color);
}
}
public class SaveDataTests
{
[Test]
public void Vector3Serializable_RoundTrip_PreservesValues()
{
var original = new Vector3(1.5f, 2.5f, 3.5f);
var serializable = new Vector3Serializable(original);
var result = serializable.ToVector3();
Assert.AreEqual(original, result);
}
[Test]
public void SaveData_JsonSerialization_Works()
{
var data = new SaveData
{
level = 5,
experience = 1000,
gold = 500,
playTime = 3600f,
playerPosition = new Vector3Serializable(new Vector3(10, 0, 20))
};
var json = JsonUtility.ToJson(data);
var restored = JsonUtility.FromJson<SaveData>(json);
Assert.AreEqual(data.level, restored.level);
Assert.AreEqual(data.experience, restored.experience);
Assert.AreEqual(data.gold, restored.gold);
Assert.AreEqual(data.playTime, restored.playTime);
}
}
}
Play Mode Tests¶
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using MyGame.Player;
namespace MyGame.Tests.PlayMode
{
public class PlayerHealthTests
{
private GameObject playerObject;
private PlayerHealth playerHealth;
[SetUp]
public void SetUp()
{
playerObject = new GameObject("Player");
playerHealth = playerObject.AddComponent<PlayerHealth>();
}
[TearDown]
public void TearDown()
{
Object.Destroy(playerObject);
}
[UnityTest]
public IEnumerator TakeDamage_ReducesHealth()
{
yield return null; // Wait for Start()
var initialHealth = playerHealth.CurrentHealth;
playerHealth.TakeDamage(10);
Assert.AreEqual(initialHealth - 10, playerHealth.CurrentHealth);
}
[UnityTest]
public IEnumerator TakeDamage_WhenHealthZero_TriggersDeathEvent()
{
yield return null;
bool deathTriggered = false;
playerHealth.OnDeath.AddListener(() => deathTriggered = true);
playerHealth.TakeDamage(1000);
Assert.IsTrue(deathTriggered);
Assert.IsFalse(playerHealth.IsAlive);
}
[UnityTest]
public IEnumerator Heal_IncreasesHealth()
{
yield return null;
playerHealth.TakeDamage(50);
var healthAfterDamage = playerHealth.CurrentHealth;
playerHealth.Heal(25);
Assert.AreEqual(healthAfterDamage + 25, playerHealth.CurrentHealth);
}
[UnityTest]
public IEnumerator Invincibility_PreventsConsecutiveDamage()
{
yield return null;
playerHealth.TakeDamage(10);
var healthAfterFirstHit = playerHealth.CurrentHealth;
playerHealth.TakeDamage(10); // Should be blocked
Assert.AreEqual(healthAfterFirstHit, playerHealth.CurrentHealth);
}
}
}
Build & Commands¶
Build Commands¶
# Build from command line
# Windows
"C:\Program Files\Unity\Hub\Editor\2022.3.0f1\Editor\Unity.exe" \
-quit -batchmode \
-projectPath "C:\Projects\MyGame" \
-buildTarget Win64 \
-buildPath "Builds/Windows/MyGame.exe"
# macOS
/Applications/Unity/Hub/Editor/2022.3.0f1/Unity.app/Contents/MacOS/Unity \
-quit -batchmode \
-projectPath ~/Projects/MyGame \
-buildTarget StandaloneOSX \
-buildPath Builds/macOS/MyGame.app
# Run tests
Unity -runTests \
-projectPath /path/to/project \
-testResults results.xml \
-testPlatform EditMode
# Create package
Unity -exportPackage Assets/MyPlugin MyPlugin.unitypackage
Editor Scripts¶
#if UNITY_EDITOR
using UnityEditor;
using UnityEngine;
namespace MyGame.Editor
{
public static class BuildScript
{
[MenuItem("Build/Build Windows")]
public static void BuildWindows()
{
var scenes = new[]
{
"Assets/_Project/Scenes/MainMenu.unity",
"Assets/_Project/Scenes/GameLevel.unity"
};
BuildPipeline.BuildPlayer(
scenes,
"Builds/Windows/MyGame.exe",
BuildTarget.StandaloneWindows64,
BuildOptions.None
);
}
[MenuItem("Build/Build WebGL")]
public static void BuildWebGL()
{
var scenes = new[]
{
"Assets/_Project/Scenes/MainMenu.unity",
"Assets/_Project/Scenes/GameLevel.unity"
};
BuildPipeline.BuildPlayer(
scenes,
"Builds/WebGL",
BuildTarget.WebGL,
BuildOptions.None
);
}
}
}
#endif
Best Practices¶
Performance Tips¶
- Object Pooling: Reuse objects instead of Instantiate/Destroy
- Avoid Find: Cache references in Awake/Start
- Update Optimization: Use events instead of polling in Update
- String Operations: Use StringBuilder, avoid concatenation
- Physics: Use layers and avoid complex colliders
- Draw Calls: Batch materials, use sprite atlases
Code Guidelines¶
- Single Responsibility: One component = one purpose
- Composition over Inheritance: Favor multiple components
- ScriptableObjects: Use for configuration and data
- Events: Decouple systems with UnityEvents or C# events
- Serialization: Mark fields [SerializeField] for inspector access
Common Mistakes to Avoid¶
// BAD: Finding objects every frame
void Update()
{
var player = GameObject.Find("Player"); // Expensive!
}
// GOOD: Cache references
private Transform player;
void Start() => player = GameObject.Find("Player").transform;
// BAD: String comparison
if (gameObject.tag == "Player") { }
// GOOD: Use CompareTag
if (gameObject.CompareTag("Player")) { }
// BAD: Allocating in Update
void Update()
{
var list = new List<Enemy>(); // GC allocation every frame!
}
// GOOD: Reuse collections
private readonly List<Enemy> enemies = new();
void Update()
{
enemies.Clear();
// Reuse...
}
Framework Comparison¶
| Feature | Unity | Unreal Engine | Godot |
|---|---|---|---|
| Language | C# | C++/Blueprints | GDScript/C# |
| Learning Curve | Medium | Steep | Easy |
| 2D Support | Excellent | Good | Excellent |
| 3D Support | Excellent | Excellent | Good |
| Mobile | Excellent | Good | Good |
| VR/AR | Excellent | Excellent | Limited |
| Performance | Good | Excellent | Good |
| Asset Store | Massive | Large | Growing |
| Open Source | No | Partial | Yes |
| Free Tier | Yes (< $100K) | Yes (< $1M) | Yes |