
Unity Csharp Scripting
- 75 installs
- 50 repo stars
- Updated June 18, 2026
- josiahsiegel/claude-plugin-marketplace
Helps with ai & agent building tasks.
About
unity-csharp-scripting is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- unity-csharp-scripting
- AI & Agent Building
- AI-coding skill
Unity Csharp Scripting by the numbers
- 75 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #5,472 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill unity-csharp-scriptingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 75 |
|---|---|
| repo stars | ★ 50 |
| Last updated | June 18, 2026 |
| Repository | josiahsiegel/claude-plugin-marketplace ↗ |
What it does
Helps with ai & agent building tasks.
Files
Unity C# Scripting Patterns
Overview
Core C# scripting reference for Unity development. Covers MonoBehaviour lifecycle, physics and collision APIs, animation scripting, audio, navigation, common design patterns, serialization, and ECS/DOTS coding patterns.
MonoBehaviour Lifecycle
Execution Order
Awake() -> OnEnable() -> Start() -> FixedUpdate() -> Update() -> LateUpdate() -> OnDisable() -> OnDestroy()| Method | When Called | Use For |
|---|---|---|
Awake() | Once, when object instantiates (before Start) | Self-initialization, caching references |
OnEnable() | Each time object becomes active | Subscribe to events, reset state |
Start() | Once, before first Update (after all Awake) | Cross-object initialization |
FixedUpdate() | Fixed timestep (default 0.02s) | Physics, Rigidbody movement |
Update() | Every frame | Input, non-physics logic |
LateUpdate() | After all Update calls | Camera follow, post-movement adjustments |
OnDisable() | When object deactivates | Unsubscribe events, save state |
OnDestroy() | When object is destroyed | Final cleanup, resource release |
Key Rules
- Awake runs even on disabled components (but not disabled GameObjects)
- Never rely on Awake/Start order between scripts -- use
[DefaultExecutionOrder(N)]or Script Execution Order settings - Use
OnValidate()for editor-time validation of serialized fields
Coroutines and Async
Coroutines
IEnumerator SpawnWaves(int count, float delay)
{
for (int i = 0; i < count; i++)
{
SpawnEnemy();
yield return new WaitForSeconds(delay);
}
}
// Start: Coroutine handle = StartCoroutine(SpawnWaves(5, 1f));
// Stop: StopCoroutine(handle); or StopAllCoroutines();| Yield Instruction | Behavior |
|---|---|
yield return null | Wait one frame |
yield return new WaitForSeconds(t) | Wait t seconds (affected by timeScale) |
yield return new WaitForSecondsRealtime(t) | Unscaled time |
yield return new WaitForFixedUpdate() | Wait for next FixedUpdate |
yield return new WaitForEndOfFrame() | After rendering |
yield return new WaitUntil(() => condition) | Wait until predicate is true |
yield return StartCoroutine(other) | Wait for nested coroutine |
Async/Await (Unity 6+ / UniTask)
For Unity 2023+/Unity 6, Awaitable is built-in. For older versions, use UniTask.
async Awaitable LoadLevelAsync(string sceneName)
{
await Awaitable.WaitForSecondsAsync(1f);
var op = SceneManager.LoadSceneAsync(sceneName);
while (!op.isDone)
{
progressBar.value = op.progress;
await Awaitable.NextFrameAsync();
}
}Events and Delegates
Event Pattern (Recommended)
// Publisher
public class Health : MonoBehaviour
{
public event System.Action<float> OnDamaged; // event keyword prevents external invocation
public event System.Action OnDeath;
public void TakeDamage(float amount)
{
currentHealth -= amount;
OnDamaged?.Invoke(amount);
if (currentHealth <= 0) OnDeath?.Invoke();
}
}
// Subscriber
void OnEnable() => health.OnDamaged += HandleDamage;
void OnDisable() => health.OnDamaged -= HandleDamage;
void HandleDamage(float amount) => /* react */;ScriptableObject Event Channels
Decouple systems without direct references. Create GameEvent as a ScriptableObject asset, invoke from publishers, and listen from subscribers via GameEventListener MonoBehaviours. See references/design-patterns.md for full implementation.
Physics API Quick Reference
Rigidbody Movement (3D)
| Task | Method | Where |
|---|---|---|
| Continuous force | rb.AddForce(dir * force) | FixedUpdate |
| Instant impulse | rb.AddForce(dir * force, ForceMode.Impulse) | FixedUpdate |
| Direct velocity | rb.linearVelocity = dir * speed | FixedUpdate |
| Kinematic move | rb.MovePosition(target) | FixedUpdate |
| Rotation | rb.MoveRotation(targetRot) | FixedUpdate |
Note: In Unity 6, Rigidbody.velocity is renamed to Rigidbody.linearVelocity.
Raycasting
if (Physics.Raycast(origin, direction, out RaycastHit hit, maxDistance, layerMask))
{
Debug.Log($"Hit {hit.collider.name} at {hit.point}");
}
// Use Physics.RaycastAll or Physics.RaycastNonAlloc for multiple hits
// 2D: Physics2D.Raycast, Physics2D.OverlapCircle, etc.Collision vs Trigger
| Callback | Requires | Use For |
|---|---|---|
OnCollisionEnter/Stay/Exit | Both have colliders, at least one Rigidbody, isTrigger=false | Physical impacts |
OnTriggerEnter/Stay/Exit | One collider has isTrigger=true, at least one Rigidbody | Zones, pickups, detection |
Always use CompareTag("Enemy") instead of other.tag == "Enemy" (avoids GC allocation).
Animation Scripting
[RequireComponent(typeof(Animator))]
public class CharacterAnimation : MonoBehaviour
{
static readonly int SpeedHash = Animator.StringToHash("Speed");
static readonly int JumpTrigger = Animator.StringToHash("Jump");
Animator _anim;
void Awake() => _anim = GetComponent<Animator>();
void Update()
{
_anim.SetFloat(SpeedHash, moveSpeed);
if (jumped) _anim.SetTrigger(JumpTrigger);
}
}Cache Animator.StringToHash results as static readonly fields to avoid per-frame hashing. Use Animation Events for gameplay-timed callbacks (footsteps, hit frames). For IK, implement OnAnimatorIK(int layerIndex) with SetIKPosition/Rotation/Weight.
Audio Quick Reference
[RequireComponent(typeof(AudioSource))]
public class SFXPlayer : MonoBehaviour
{
[SerializeField] AudioClip[] clips;
AudioSource _source;
void Awake() => _source = GetComponent<AudioSource>();
public void PlayRandom() => _source.PlayOneShot(clips[Random.Range(0, clips.Length)]);
}Use AudioSource.PlayOneShot() for overlapping SFX. Use AudioMixer with exposed parameters for volume control. Use snapshots for state transitions (combat vs. exploration).
Serialization
| Type | Serialized | Notes |
|---|---|---|
public fields | Yes | Visible in Inspector |
[SerializeField] private | Yes | Preferred -- maintains encapsulation |
[HideInInspector] public | Yes, hidden | Serialized but not shown |
[NonSerialized] public | No | Opt-out of serialization |
| Properties | No | Never serialized by Unity |
| Dictionaries | No | Use serialized list + rebuild, or Odin/SerializedDictionary |
| Interfaces | No | Use abstract ScriptableObject or SerializeReference |
Use [SerializeReference] for polymorphic serialization of interfaces and abstract types.
Common Design Patterns
| Pattern | When to Use | Approach |
|---|---|---|
| Singleton | Global managers (Audio, GameState) | ScriptableObject-based or lazy MonoBehaviour |
| Observer | Decoupled communication | C# events or SO event channels |
| Command | Input/undo systems | Command interface + history stack |
| State Machine | AI, player states | Enum + switch, or class-based states |
| Object Pool | Bullets, particles, enemies | Queue<T> with pre-instantiation |
| Service Locator | Testable global access | Static registry with interface keys |
For detailed implementations and code examples, see references/design-patterns.md.
ECS/DOTS Quick Reference
| Concept | Role |
|---|---|
| Entity | Lightweight ID (no MonoBehaviour) |
| IComponentData | Pure data struct on entities |
| SystemBase / ISystem | Logic operating on component queries |
| EntityQuery | Filter entities by component sets |
| Jobs (IJobEntity) | Multithreaded systems |
| Burst Compiler | SIMD-optimized native code |
For ECS architecture patterns and migration guidance, see references/design-patterns.md.
Additional Resources
Reference Files
- `references/design-patterns.md` -- Full implementations of singleton, observer, command, state machine, object pool, service locator, and ECS patterns
- `references/physics-animation-audio.md` -- Detailed physics setup (2D and 3D), advanced animation (blend trees, IK, root motion, Animation Rigging), and audio architecture
Unity C# Design Patterns - Complete Reference
ScriptableObject Singleton
Avoid MonoBehaviour singletons where possible. ScriptableObject-based singletons are testable and survive scene reloads:
// Generic SO singleton base
public abstract class SingletonSO<T> : ScriptableObject where T : ScriptableObject
{
static T _instance;
public static T Instance
{
get
{
if (_instance == null)
_instance = Resources.Load<T>(typeof(T).Name);
return _instance;
}
}
}
// Usage
[CreateAssetMenu(menuName = "Config/Game Settings")]
public class GameSettings : SingletonSO<GameSettings>
{
public float Gravity = -9.81f;
public int MaxEnemies = 50;
public AnimationCurve DifficultyCurve;
}If a MonoBehaviour singleton is truly needed (e.g., AudioManager that plays sounds), use the lazy initialization pattern:
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
}
void OnDestroy()
{
if (Instance == this) Instance = null;
}
}ScriptableObject Event Channels
Fully decoupled event system using SO assets. Publishers and subscribers reference the same SO asset, requiring no direct script references.
// Event with no parameters
[CreateAssetMenu(menuName = "Events/Game Event")]
public class GameEvent : ScriptableObject
{
readonly List<GameEventListener> _listeners = new();
public void Raise()
{
// Iterate backwards for safe removal during iteration
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised();
}
public void Register(GameEventListener listener) => _listeners.Add(listener);
public void Unregister(GameEventListener listener) => _listeners.Remove(listener);
}
// Listener component (attach to GameObjects that respond to events)
public class GameEventListener : MonoBehaviour
{
[SerializeField] GameEvent _event;
[SerializeField] UnityEvent _response;
void OnEnable() => _event.Register(this);
void OnDisable() => _event.Unregister(this);
public void OnEventRaised() => _response.Invoke();
}
// Typed event for data passing
[CreateAssetMenu(menuName = "Events/Float Event")]
public class FloatEvent : ScriptableObject
{
readonly List<FloatEventListener> _listeners = new();
public void Raise(float value)
{
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised(value);
}
public void Register(FloatEventListener l) => _listeners.Add(l);
public void Unregister(FloatEventListener l) => _listeners.Remove(l);
}Command Pattern (Input / Undo)
public interface ICommand
{
void Execute();
void Undo();
}
public class MoveCommand : ICommand
{
readonly Transform _target;
readonly Vector3 _delta;
Vector3 _previousPosition;
public MoveCommand(Transform target, Vector3 delta)
{
_target = target;
_delta = delta;
}
public void Execute()
{
_previousPosition = _target.position;
_target.position += _delta;
}
public void Undo() => _target.position = _previousPosition;
}
public class CommandManager
{
readonly Stack<ICommand> _undoStack = new();
readonly Stack<ICommand> _redoStack = new();
public void Execute(ICommand command)
{
command.Execute();
_undoStack.Push(command);
_redoStack.Clear();
}
public void Undo()
{
if (_undoStack.Count == 0) return;
var cmd = _undoStack.Pop();
cmd.Undo();
_redoStack.Push(cmd);
}
public void Redo()
{
if (_redoStack.Count == 0) return;
var cmd = _redoStack.Pop();
cmd.Execute();
_undoStack.Push(cmd);
}
}Class-Based State Machine
public interface IState
{
void Enter();
void Execute(); // Called each frame
void Exit();
}
public class StateMachine
{
IState _currentState;
readonly Dictionary<System.Type, IState> _states = new();
public void AddState(IState state) => _states[state.GetType()] = state;
public void ChangeState<T>() where T : IState
{
_currentState?.Exit();
_currentState = _states[typeof(T)];
_currentState.Enter();
}
public void Update() => _currentState?.Execute();
}
// Example states
public class IdleState : IState
{
readonly EnemyAI _ai;
public IdleState(EnemyAI ai) => _ai = ai;
public void Enter() => _ai.Animator.SetBool("IsIdle", true);
public void Execute()
{
if (_ai.CanSeePlayer())
_ai.StateMachine.ChangeState<ChaseState>();
}
public void Exit() => _ai.Animator.SetBool("IsIdle", false);
}
public class ChaseState : IState
{
readonly EnemyAI _ai;
public ChaseState(EnemyAI ai) => _ai = ai;
public void Enter() => _ai.Agent.SetDestination(_ai.Player.position);
public void Execute()
{
_ai.Agent.SetDestination(_ai.Player.position);
if (_ai.IsInAttackRange())
_ai.StateMachine.ChangeState<AttackState>();
else if (!_ai.CanSeePlayer())
_ai.StateMachine.ChangeState<IdleState>();
}
public void Exit() => _ai.Agent.ResetPath();
}Service Locator
Alternative to singletons that supports testing via interface substitution:
public static class ServiceLocator
{
static readonly Dictionary<System.Type, object> _services = new();
public static void Register<T>(T service) => _services[typeof(T)] = service;
public static T Get<T>() => (T)_services[typeof(T)];
public static bool TryGet<T>(out T service)
{
if (_services.TryGetValue(typeof(T), out var obj))
{
service = (T)obj;
return true;
}
service = default;
return false;
}
public static void Clear() => _services.Clear();
}
// Registration (e.g., in a bootstrap scene)
ServiceLocator.Register<IAudioService>(new AudioService());
ServiceLocator.Register<ISaveService>(new CloudSaveService());
// Usage
ServiceLocator.Get<IAudioService>().PlaySFX("explosion");Object Pool (Generic, Production-Ready)
public class ComponentPool<T> where T : Component
{
readonly Queue<T> _available = new();
readonly HashSet<T> _active = new();
readonly T _prefab;
readonly Transform _parent;
readonly int _maxSize;
public int ActiveCount => _active.Count;
public int AvailableCount => _available.Count;
public ComponentPool(T prefab, int preWarm, int maxSize = 1000, Transform parent = null)
{
_prefab = prefab;
_maxSize = maxSize;
_parent = parent;
for (int i = 0; i < preWarm; i++)
_available.Enqueue(CreateNew());
}
public T Get(Vector3 position, Quaternion rotation)
{
T item;
if (_available.Count > 0)
{
item = _available.Dequeue();
}
else if (_active.Count < _maxSize)
{
item = CreateNew();
}
else
{
Debug.LogWarning($"Pool exhausted for {_prefab.name}");
return null;
}
item.transform.SetPositionAndRotation(position, rotation);
item.gameObject.SetActive(true);
_active.Add(item);
return item;
}
public void Return(T item)
{
if (!_active.Remove(item)) return;
item.gameObject.SetActive(false);
_available.Enqueue(item);
}
public void ReturnAll()
{
foreach (var item in _active)
{
item.gameObject.SetActive(false);
_available.Enqueue(item);
}
_active.Clear();
}
T CreateNew()
{
var item = Object.Instantiate(_prefab, _parent);
item.gameObject.SetActive(false);
return item;
}
}ECS/DOTS Patterns
Component Data
// Components are pure data structs
public struct MoveSpeed : IComponentData
{
public float Value;
}
public struct Health : IComponentData
{
public float Current;
public float Max;
}
// Tag components (no data, used for filtering)
public struct EnemyTag : IComponentData { }System (ISystem - Burst Compatible)
[BurstCompile]
public partial struct MoveSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<MoveSpeed>>())
{
transform.ValueRW.Position += new float3(0, 0, speed.ValueRO.Value * dt);
}
}
}Jobs (Parallel Processing)
[BurstCompile]
public partial struct DamageJob : IJobEntity
{
public float DamageAmount;
void Execute(ref Health health, in EnemyTag tag)
{
health.Current -= DamageAmount;
}
}
// Schedule from a system
public partial struct DamageSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var job = new DamageJob { DamageAmount = 10f };
job.ScheduleParallel();
}
}MonoBehaviour to ECS Migration Checklist
1. Identify data (fields) -> IComponentData structs 2. Identify logic (Update methods) -> ISystem implementations 3. Convert prefabs -> Entity prefabs (SubScene workflow) 4. Replace GetComponent -> SystemAPI.GetComponent 5. Replace Instantiate -> EntityManager.Instantiate 6. Use Burst-compatible types (float3, quaternion, NativeArray) 7. Profile to verify the migration provides actual speedup
Physics, Animation, and Audio - Detailed Reference
Physics Setup (3D)
Rigidbody Configuration
| Property | Default | Guidelines |
|---|---|---|
| Mass | 1 | Use realistic ratios (player: 70, crate: 20, bullet: 0.01) |
| Drag | 0 | Increase for floating/hovering feel |
| Angular Drag | 0.05 | Increase to prevent endless spinning |
| Interpolate | None | Set to Interpolate for player-controlled objects to smooth rendering |
| Collision Detection | Discrete | Use Continuous for fast-moving objects (bullets) |
| Constraints | None | Freeze rotation axes for characters to prevent tipping |
Collider Types
| Collider | Performance | Use For |
|---|---|---|
| Box | Fastest | Crates, walls, doors |
| Sphere | Very Fast | Pickups, triggers, characters (capsule for tall) |
| Capsule | Fast | Characters, humanoids |
| Mesh (Convex) | Moderate | Irregularly shaped props (max 255 tris) |
| Mesh (Non-Convex) | Slow | Static environment only (not on Rigidbody) |
| Terrain | Moderate | Terrain Collider component on Terrain objects |
Physics Layers and Matrix
Configure collision layers in Edit > Project Settings > Physics:
Layer 8: Player
Layer 9: Enemy
Layer 10: Projectile
Layer 11: Environment
Layer 12: Trigger
Layer 13: RagdollDisable unnecessary collisions in the matrix (e.g., Projectile vs. Projectile, Ragdoll vs. Trigger).
Joint Types
| Joint | Use For | Key Settings |
|---|---|---|
| Fixed | Welding objects together | Break force/torque |
| Hinge | Doors, wheels, pendulums | Axis, limits, motor |
| Spring | Suspension, bouncy connections | Spring force, damper |
| Configurable | Complex constraints | Per-axis freedom control |
| Character | Player joints in ragdolls | Swing/twist limits |
Advanced Raycasting
// SphereCast for wider hit detection (e.g., aim assist)
if (Physics.SphereCast(origin, radius, direction, out RaycastHit hit, maxDistance, layerMask))
{
// hit.point, hit.normal, hit.collider
}
// OverlapSphere for area detection
Collider[] results = new Collider[32]; // Pre-allocate
int count = Physics.OverlapSphereNonAlloc(center, radius, results, layerMask);
for (int i = 0; i < count; i++)
ProcessTarget(results[i]);
// BoxCast for rectangular sweeps (melee attacks)
if (Physics.BoxCast(center, halfExtents, direction, out RaycastHit hit, orientation, maxDistance))
ApplyDamage(hit.collider);Always use NonAlloc variants in hot paths to avoid GC allocation.
Physics 2D
2D physics uses separate components: Rigidbody2D, BoxCollider2D, CircleCollider2D, CapsuleCollider2D, CompositeCollider2D, PolygonCollider2D.
Key Differences from 3D
| Aspect | 3D | 2D |
|---|---|---|
| Gravity | Physics.gravity (Vector3) | Physics2D.gravity (Vector2) |
| Rigidbody type | Dynamic/Kinematic/Static | Dynamic/Kinematic/Static |
| Collider shapes | Box, Sphere, Capsule, Mesh | Box, Circle, Capsule, Polygon, Edge, Composite |
| Callbacks | OnCollisionEnter(Collision) | OnCollisionEnter2D(Collision2D) |
| Raycasting | Physics.Raycast | Physics2D.Raycast |
| Layers | Same layer system | Same layer system |
2D Rigidbody Types
| Type | Use For |
|---|---|
| Dynamic | Physics-driven movement (enemies, projectiles) |
| Kinematic | Script-driven movement (platforms, players in some designs) |
| Static | Immovable environment (walls, ground) -- no Rigidbody2D needed, just collider |
2D-Specific Patterns
// Ground check for platformers
bool IsGrounded()
{
return Physics2D.BoxCast(
collider.bounds.center,
collider.bounds.size,
0f,
Vector2.down,
0.1f,
groundLayerMask
).collider != null;
}
// One-way platforms
// Use PlatformEffector2D component with "Use One Way" enabled
// Composite Collider for tilemaps
// Add CompositeCollider2D + Rigidbody2D(Static) to tilemap object
// Set TilemapCollider2D "Used By Composite" = trueAnimation System
Animator Controller Structure
Animator Controller
├── Layers
│ ├── Base Layer (weight: 1.0)
│ │ ├── States: Idle, Walk, Run, Jump
│ │ ├── Transitions (with conditions)
│ │ └── Blend Tree (Walk/Run based on Speed)
│ └── Upper Body Layer (weight: 0.7, override)
│ ├── States: Idle, Attack, Block
│ └── Avatar Mask: Upper Body only
├── Parameters
│ ├── Speed (Float)
│ ├── IsGrounded (Bool)
│ ├── Jump (Trigger)
│ └── AttackIndex (Int)
└── Sub-State Machines
└── Combat (groups Attack, Block, Dodge states)Blend Trees
| Type | Axes | Use For |
|---|---|---|
| 1D | Speed | Walk/Run blend |
| 2D Simple Directional | X, Y | 4-directional locomotion |
| 2D Freeform Directional | X, Y | 8+ directional locomotion |
| 2D Freeform Cartesian | X, Y | Complex parameter combinations |
| Direct | Multiple | Face blendshape control |
Transition Settings
| Setting | Default | Recommendation |
|---|---|---|
| Has Exit Time | true | Disable for responsive gameplay |
| Exit Time | 0.75 | Set to last frame if using exit time |
| Transition Duration | 0.25s | 0.1-0.15s for responsive, 0.2-0.3s for smooth |
| Transition Offset | 0 | Non-zero to start mid-animation |
| Interruption Source | None | Current State for interruptible attacks |
Animation Events
// Called from Animation Event in the clip
public void OnFootstep()
{
audioSource.PlayOneShot(footstepClips[Random.Range(0, footstepClips.Length)]);
}
public void OnAttackHit()
{
// Enable hit detection for this frame
weaponCollider.enabled = true;
}
public void OnAttackEnd()
{
weaponCollider.enabled = false;
}Animation Events fire at specific frame timestamps. Use for gameplay-critical moments (hit frames, footsteps, spell cast points).
Inverse Kinematics (IK)
void OnAnimatorIK(int layerIndex)
{
if (lookTarget != null)
{
animator.SetLookAtWeight(1f, 0.3f, 0.6f, 1f, 0.5f);
animator.SetLookAtPosition(lookTarget.position);
}
if (rightHandTarget != null)
{
animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1f);
animator.SetIKPosition(AvatarIKGoal.RightHand, rightHandTarget.position);
animator.SetIKRotation(AvatarIKGoal.RightHand, rightHandTarget.rotation);
}
}Use IK for: hand placement on weapons/ledges, foot placement on uneven terrain, head look-at targets. For complex rigs, use the Animation Rigging package.
Root Motion
Enable "Apply Root Motion" on Animator for animation-driven movement. The OnAnimatorMove() callback gives full control:
void OnAnimatorMove()
{
// Apply root motion through CharacterController or Rigidbody
Vector3 deltaPosition = animator.deltaPosition;
characterController.Move(deltaPosition);
transform.rotation *= animator.deltaRotation;
}Audio Architecture
Audio Hierarchy
Scene
├── AudioListener (on Main Camera)
├── Music Manager (DontDestroyOnLoad)
│ ├── AudioSource (Music Track A)
│ └── AudioSource (Music Track B) -- for crossfading
├── Ambient Manager
│ └── AudioSource (ambient loops)
└── SFX (on game objects)
├── Player AudioSource
├── Enemy AudioSources
└── Environment AudioSourcesAudioMixer Setup
Master Mixer
├── Music Group
│ └── Volume exposed as "MusicVolume"
├── SFX Group
│ ├── Weapons subgroup
│ ├── Footsteps subgroup
│ └── UI subgroup
└── Ambient Group
└── Volume exposed as "AmbientVolume"Expose parameters for volume sliders:
// Set mixer volume (logarithmic scale)
public void SetMusicVolume(float linearValue)
{
float dbValue = linearValue > 0.001f ? Mathf.Log10(linearValue) * 20f : -80f;
audioMixer.SetFloat("MusicVolume", dbValue);
}Mixer Snapshots
Create snapshots for different game states:
- Default: Normal mix
- Combat: Lower music, boost SFX
- Paused: Duck all, slight reverb
- Underwater: Low-pass filter, muffled
combatSnapshot.TransitionTo(0.5f); // Blend over 0.5 secondsSpatial Audio
// 3D sound setup
AudioSource source = GetComponent<AudioSource>();
source.spatialBlend = 1f; // Full 3D
source.rolloffMode = AudioRolloffMode.Custom;
source.maxDistance = 50f;
source.minDistance = 1f;
source.dopplerLevel = 0f; // Usually disable for gamesAudio Clip Import Settings
| Setting | Music | SFX (Short) | SFX (Long) | Voice |
|---|---|---|---|---|
| Load Type | Streaming | Decompress On Load | Compressed In Memory | Compressed In Memory |
| Compression | Vorbis | PCM or ADPCM | Vorbis | Vorbis |
| Sample Rate | 44100 | 22050-44100 | 44100 | 22050 |
| Channels | Stereo | Mono (3D) | Mono (3D) | Mono |
Music Crossfade
public class MusicManager : MonoBehaviour
{
[SerializeField] AudioSource sourceA, sourceB;
AudioSource _current;
public async Awaitable CrossfadeTo(AudioClip newClip, float duration = 1f)
{
var next = _current == sourceA ? sourceB : sourceA;
next.clip = newClip;
next.volume = 0f;
next.Play();
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.unscaledDeltaTime;
float t = elapsed / duration;
_current.volume = 1f - t;
next.volume = t;
await Awaitable.NextFrameAsync();
}
_current.Stop();
_current = next;
}
}NavMesh and Pathfinding
Setup
1. Mark walkable surfaces as Navigation Static 2. Open Navigation window (Window > AI > Navigation) 3. Bake NavMesh with appropriate agent radius/height 4. Add NavMeshAgent component to moving entities
Agent Configuration
| Property | Purpose | Typical Values |
|---|---|---|
| Speed | Movement speed | 3.5 (walk), 6 (run) |
| Angular Speed | Turn rate | 120-360 deg/s |
| Acceleration | Speed ramp-up | 8-12 |
| Stopping Distance | Stop before target | 0.5-2 units |
| Auto Braking | Slow near destination | true for patrol, false for chase |
| Obstacle Avoidance | Quality level | Medium for most, High for player |
| Area Mask | Walkable areas | Configure per-agent type |
NavMesh Scripting
NavMeshAgent agent = GetComponent<NavMeshAgent>();
// Move to target
agent.SetDestination(targetPosition);
// Check if arrived
bool hasArrived = !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance;
// Off-mesh links (jumping, climbing)
if (agent.isOnOffMeshLink)
{
// Custom animation/movement over the link
await AnimateOffMeshLink(agent);
agent.CompleteOffMeshLink();
}
// Dynamic obstacles
// Use NavMeshObstacle on moving objects that block paths
// Set Carve = true for reliable blockingNavMesh for 2D
Unity's NavMesh is 3D-only. For 2D pathfinding, use:
- NavMeshPlus (community package) -- adapts Unity NavMesh for 2D
- *A Pathfinding Project** (Aron Granberg) -- popular third-party solution
- Custom A* on a grid -- for tile-based games