Game development has unique patterns: tight performance budgets, frame-rate sensitivity, and the component-entity model that differs from standard software architecture. Claude Code understands Unity’s execution model — Update vs FixedUpdate, object pooling to avoid GC spikes, the new ECS/DOTS system for performance-critical code, and coroutines vs async/await in Unity’s context.
Component Architecture
Create a player controller component.
Features: movement with character controller, dashing with cooldown,
wall jumping, coyote time, and jump buffering.
// PlayerController.cs
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
[SerializeField] private float moveSpeed = 6f;
[SerializeField] private float dashSpeed = 20f;
[SerializeField] private float dashDuration = 0.15f;
[SerializeField] private float dashCooldown = 1f;
[Header("Jumping")]
[SerializeField] private float jumpHeight = 3f;
[SerializeField] private float gravity = -20f;
[SerializeField] private float coyoteTime = 0.15f; // Time after leaving ground to still jump
[SerializeField] private float jumpBufferTime = 0.1f; // Time before landing to queue jump
private CharacterController _cc;
private Vector3 _velocity;
private float _coyoteTimer;
private float _jumpBufferTimer;
private float _dashCooldownTimer;
private float _dashTimer;
private bool _isDashing;
private Vector3 _dashDirection;
private bool IsGrounded => _cc.isGrounded;
private bool CanCoyoteJump => _coyoteTimer > 0 && !IsGrounded;
private bool HasJumpBuffer => _jumpBufferTimer > 0;
private bool CanDash => _dashCooldownTimer <= 0 && !_isDashing;
private void Awake()
{
_cc = GetComponent<CharacterController>();
}
private void Update()
{
UpdateTimers();
if (_isDashing)
{
HandleDash();
return; // Don't apply normal movement during dash
}
HandleMovement();
HandleJump();
HandleDashInput();
ApplyGravity();
_cc.Move(_velocity * Time.deltaTime);
}
private void UpdateTimers()
{
if (IsGrounded)
{
_coyoteTimer = coyoteTime;
if (_velocity.y < 0) _velocity.y = -2f; // Snap to ground
}
else
{
_coyoteTimer -= Time.deltaTime;
}
_jumpBufferTimer -= Time.deltaTime;
_dashCooldownTimer -= Time.deltaTime;
_dashTimer -= Time.deltaTime;
}
private void HandleMovement()
{
float horizontal = Input.GetAxisRaw("Horizontal");
float vertical = Input.GetAxisRaw("Vertical");
Vector3 move = (transform.right * horizontal + transform.forward * vertical).normalized;
_velocity.x = move.x * moveSpeed;
_velocity.z = move.z * moveSpeed;
}
private void HandleJump()
{
if (Input.GetButtonDown("Jump"))
_jumpBufferTimer = jumpBufferTime;
bool canJump = IsGrounded || CanCoyoteJump;
if (canJump && HasJumpBuffer)
{
// v = sqrt(2 * jumpHeight * |gravity|)
_velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
_coyoteTimer = 0; // Consume coyote time
_jumpBufferTimer = 0; // Consume buffer
}
// Variable jump height — release early for lower jump
if (Input.GetButtonUp("Jump") && _velocity.y > 0)
_velocity.y *= 0.5f;
}
private void HandleDashInput()
{
if (Input.GetKeyDown(KeyCode.LeftShift) && CanDash)
{
float h = Input.GetAxisRaw("Horizontal");
float v = Input.GetAxisRaw("Vertical");
_dashDirection = (transform.right * h + transform.forward * v).normalized;
if (_dashDirection == Vector3.zero)
_dashDirection = transform.forward;
_isDashing = true;
_dashTimer = dashDuration;
_dashCooldownTimer = dashCooldown;
}
}
private void HandleDash()
{
_velocity = _dashDirection * dashSpeed;
_velocity.y = 0;
_cc.Move(_velocity * Time.deltaTime);
if (_dashTimer <= 0)
{
_isDashing = false;
_velocity = Vector3.zero;
}
}
private void ApplyGravity()
{
if (!IsGrounded)
_velocity.y += gravity * Time.deltaTime;
}
}
Object Pooling
Create a generic object pool to avoid garbage collection spikes
from instantiating/destroying bullets, particles, and enemies.
// ObjectPool.cs
using System.Collections.Generic;
using UnityEngine;
public class ObjectPool<T> where T : Component
{
private readonly T _prefab;
private readonly Transform _parent;
private readonly Queue<T> _pool = new Queue<T>();
public ObjectPool(T prefab, int initialSize, Transform parent = null)
{
_prefab = prefab;
_parent = parent;
// Pre-warm the pool
for (int i = 0; i < initialSize; i++)
{
T obj = CreateNew();
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
}
public T Get(Vector3 position, Quaternion rotation)
{
T obj = _pool.Count > 0 ? _pool.Dequeue() : CreateNew();
obj.transform.SetPositionAndRotation(position, rotation);
obj.gameObject.SetActive(true);
return obj;
}
public void Return(T obj)
{
obj.gameObject.SetActive(false);
_pool.Enqueue(obj);
}
private T CreateNew()
{
return Object.Instantiate(_prefab, _parent);
}
}
// Usage: BulletSpawner.cs
public class BulletSpawner : MonoBehaviour
{
[SerializeField] private Bullet bulletPrefab;
[SerializeField] private int poolSize = 50;
private ObjectPool<Bullet> _pool;
private void Awake()
{
_pool = new ObjectPool<Bullet>(bulletPrefab, poolSize, transform);
}
public void Shoot(Vector3 origin, Vector3 direction)
{
Bullet bullet = _pool.Get(origin, Quaternion.LookRotation(direction));
bullet.Initialize(direction, () => _pool.Return(bullet)); // Return callback
}
}
Unity ECS (DOTS)
Convert our enemy AI movement to ECS for handling 1000+ enemies at 60fps.
Enemies move toward the player position.
// EnemyComponents.cs
using Unity.Entities;
using Unity.Mathematics;
// Component data — structs, no heap allocations
public struct EnemyTag : IComponentData { }
public struct MoveSpeed : IComponentData
{
public float Value;
}
public struct TargetPosition : IComponentData
{
public float3 Value;
}
// EnemyMoveSystem.cs
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Burst;
[BurstCompile] // Compile to native code via Burst compiler
public partial struct EnemyMoveSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// Runs on all entities with LocalTransform + MoveSpeed + TargetPosition + EnemyTag
// Parallelized across CPU cores via the job system
foreach (var (transform, speed, target) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<MoveSpeed>, RefRO<TargetPosition>>()
.WithAll<EnemyTag>())
{
float3 direction = math.normalize(target.ValueRO.Value - transform.ValueRO.Position);
float3 movement = direction * speed.ValueRO.Value * deltaTime;
transform.ValueRW.Position += movement;
// Face movement direction
if (math.lengthsq(direction) > 0.001f)
{
transform.ValueRW.Rotation = quaternion.LookRotationSafe(direction, math.up());
}
}
}
}
1000 enemies moving to the player: with MonoBehaviour, ~8ms per frame. With DOTS + Burst, ~0.3ms. The scheduling and Burst compilation happen automatically.
CLAUDE.md for Unity Projects
## Unity Project Setup
- Unity 2023.3 LTS, URP pipeline
- Target platforms: PC (first), then console (no unsafe code)
- Architecture: MonoBehaviour for game feel/input, ECS for simulation (enemies, projectiles, particles)
- Scene organization: separate scenes for different game states, additive loading for levels
- Scripts namespace: com.studio.gamename
## Performance Rules
- No `new` allocations in Update() loops — pre-allocate or pool
- String formatting: StringBuilder, not string.Format in hot paths
- Physics queries: cache ContactFilter2D, use NonAlloc variants (Physics.OverlapSphereNonAlloc)
- No FindObjectOfType in Update — cache in Awake/Start
- Profile with Unity Profiler before optimizing — don't guess
## Commands
- Build: File → Build Settings → Build or `Unity -batchmode -buildTarget StandaloneWindows64 -projectPath . -buildPath build/`
Editor Tooling
Write a Unity Editor window that batch processes sprite sprites:
finds all sprites in selected folder, sets import settings
(filter mode, compression, max texture size) uniformly.
// Editor/SpriteImportTool.cs
#if UNITY_EDITOR
using UnityEngine;
using UnityEditor;
using System.IO;
public class SpriteImportTool : EditorWindow
{
private string _targetFolder = "Assets/Sprites";
private FilterMode _filterMode = FilterMode.Point;
private TextureImporterCompression _compression = TextureImporterCompression.Compressed;
private int _maxSize = 512;
[MenuItem("Tools/Sprite Import Tool")]
public static void ShowWindow()
{
GetWindow<SpriteImportTool>("Sprite Import Tool");
}
private void OnGUI()
{
GUILayout.Label("Batch Sprite Settings", EditorStyles.boldLabel);
_targetFolder = EditorGUILayout.TextField("Folder", _targetFolder);
_filterMode = (FilterMode)EditorGUILayout.EnumPopup("Filter Mode", _filterMode);
_compression = (TextureImporterCompression)EditorGUILayout.EnumPopup("Compression", _compression);
_maxSize = EditorGUILayout.IntField("Max Size", _maxSize);
if (GUILayout.Button("Apply to All Sprites"))
{
string[] guids = AssetDatabase.FindAssets("t:Texture2D", new[] { _targetFolder });
int processed = 0;
AssetDatabase.StartAssetEditing(); // Batch import for performance
try
{
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
var importer = AssetImporter.GetAtPath(path) as TextureImporter;
if (importer == null || importer.textureType != TextureImporterType.Sprite) continue;
importer.filterMode = _filterMode;
importer.textureCompression = _compression;
importer.maxTextureSize = _maxSize;
importer.SaveAndReimport();
processed++;
}
}
finally
{
AssetDatabase.StopAssetEditing();
}
EditorUtility.DisplayDialog("Done", $"Processed {processed} sprites.", "OK");
}
}
}
#endif
For the performance profiling tools that show you whether ECS is needed, see the performance optimization guide. For WebAssembly when you need to run game logic in the browser, see the WebAssembly guide. The Claude Skills 360 bundle includes game development skill sets for Unity C# patterns, ECS migration, and editor tooling. Start with the free tier to try Unity script generation.