Examples — Unity¶
Self-contained mini-projects. Each is small enough to build in one sitting and teaches one thing well.
1. Smooth third-person camera follow¶
Assets/Scripts/Camera/SmoothFollow.cs:
using UnityEngine;
public class SmoothFollow : MonoBehaviour {
[SerializeField] Transform target;
[SerializeField] Vector3 offset = new(0, 5, -8);
[SerializeField] float positionLerp = 8f;
[SerializeField] float rotationLerp = 6f;
void LateUpdate() {
if (!target) return;
Vector3 desiredPos = target.position + target.rotation * offset;
transform.position = Vector3.Lerp(transform.position, desiredPos, positionLerp * Time.deltaTime);
Quaternion desiredRot = Quaternion.LookRotation(target.position + Vector3.up * 1.5f - transform.position);
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRot, rotationLerp * Time.deltaTime);
}
}
Notes: in LateUpdate (after the player's Update has moved them); decouple position and rotation lerp speeds — both feel different and you'll want them tunable separately.
2. Object pool for bullets¶
using UnityEngine;
using UnityEngine.Pool;
public class BulletSpawner : MonoBehaviour {
[SerializeField] Bullet bulletPrefab;
IObjectPool<Bullet> pool;
void Awake() {
pool = new ObjectPool<Bullet>(
createFunc: () => { var b = Instantiate(bulletPrefab); b.Pool = pool; return b; },
actionOnGet: b => b.gameObject.SetActive(true),
actionOnRelease: b => b.gameObject.SetActive(false),
actionOnDestroy: b => Destroy(b.gameObject),
collectionCheck: false,
defaultCapacity: 32,
maxSize: 200
);
}
public Bullet Spawn(Vector3 pos, Quaternion rot) {
var b = pool.Get();
b.transform.SetPositionAndRotation(pos, rot);
return b;
}
}
public class Bullet : MonoBehaviour {
public IObjectPool<Bullet> Pool { get; set; }
[SerializeField] float lifetime = 3f;
[SerializeField] float speed = 30f;
void OnEnable() => CancelInvoke(nameof(Recycle));
void OnEnable() : void => Invoke(nameof(Recycle), lifetime);
void Update() => transform.position += transform.forward * speed * Time.deltaTime;
void Recycle() => Pool.Release(this);
}
Pattern: pool, prefab knows its pool, returns itself on lifetime expiry.
3. ScriptableObject inventory¶
[CreateAssetMenu(menuName = "Game/Item")]
public class Item : ScriptableObject {
public string displayName;
public Sprite icon;
public int maxStack = 99;
}
[CreateAssetMenu(menuName = "Game/Inventory")]
public class Inventory : ScriptableObject {
public List<Slot> slots = new();
public event System.Action OnChanged;
[System.Serializable]
public class Slot { public Item item; public int count; }
public bool Add(Item item, int n) {
var slot = slots.Find(s => s.item == item && s.count < item.maxStack);
if (slot != null) {
int add = Mathf.Min(n, item.maxStack - slot.count);
slot.count += add;
n -= add;
}
while (n > 0) {
int add = Mathf.Min(n, item.maxStack);
slots.Add(new Slot { item = item, count = add });
n -= add;
}
OnChanged?.Invoke();
return true;
}
}
The inventory is a singleton-by-asset-reference. Drag the same Inventory SO into player and UI. They both see the same data; both subscribe to OnChanged. No Instance static needed.
4. State machine via interfaces¶
For an enemy with idle / chase / attack:
public interface IState {
void Enter();
void Tick();
void Exit();
}
public class StateMachine {
IState current;
public void Change(IState next) {
current?.Exit();
current = next;
current.Enter();
}
public void Tick() => current?.Tick();
}
Each state is a small class implementing IState, taking the enemy as a constructor arg. Cleaner than nested switch statements.
5. Animator-driven movement¶
Setup:
- Player has Animator referencing a Controller asset.
- Controller has parameters: Speed (float), Jump (trigger), Grounded (bool).
- A 1D blend tree on the base layer driven by Speed: idle (0) → walk (3) → run (6).
Code:
void Update() {
Vector3 horizontal = new(rb.linearVelocity.x, 0, rb.linearVelocity.z);
animator.SetFloat("Speed", horizontal.magnitude);
animator.SetBool("Grounded", isGrounded);
if (jumpPressed && isGrounded) animator.SetTrigger("Jump");
}
Animator "Apply Root Motion" is off for code-driven movement, on for animation-driven (in-place mocap moves the character).
6. UI Toolkit health bar¶
UI Toolkit (UXML/USS, similar to HTML/CSS) is the modern UI. uGUI (the Canvas system) still works but UI Toolkit is the path forward for non-world-space UI.
UXML:
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="hud" class="hud">
<ui:VisualElement name="health-bg" class="bar-bg">
<ui:VisualElement name="health-fill" class="bar-fill" />
</ui:VisualElement>
<ui:Label name="score" text="Score: 0" />
</ui:VisualElement>
</ui:UXML>
C#:
using UnityEngine;
using UnityEngine.UIElements;
public class HUD : MonoBehaviour {
[SerializeField] UIDocument document;
VisualElement healthFill;
Label scoreLabel;
void Awake() {
var root = document.rootVisualElement;
healthFill = root.Q<VisualElement>("health-fill");
scoreLabel = root.Q<Label>("score");
}
public void SetHealth01(float t) => healthFill.style.width = Length.Percent(Mathf.Clamp01(t) * 100);
public void SetScore(int s) => scoreLabel.text = $"Score: {s}";
}
USS handles styling. Hot reload in the UI Builder (Window > UI Toolkit > UI Builder).
7. URP custom shader for dissolve¶
Shader Graph (URP):
1. Create a Shader Graph (Lit). Assign to a material on a mesh.
2. Add a Texture2D property _Noise (a noise texture from PolyHaven works).
3. Add a Float property _Threshold (0–1).
4. In the graph: sample _Noise. Subtract _Threshold. Plug into the master node's Alpha Clip Threshold — or to Alpha with Surface = Transparent, Alpha Clip on.
5. Add an Edge color: Step(_Noise - _Threshold, 0.05) masks a thin band around the dissolve front. Multiply by an emission color.
Drive _Threshold from script:
This is the bones of every "magic disappear" / Thanos snap effect.
8. Save and load with JSON¶
[System.Serializable]
public class SaveData {
public string playerName;
public int level;
public Vector3 position;
}
public static class SaveSystem {
static string Path => Application.persistentDataPath + "/save.json";
public static void Save(SaveData data) =>
System.IO.File.WriteAllText(Path, JsonUtility.ToJson(data, true));
public static SaveData Load() {
if (!System.IO.File.Exists(Path)) return null;
return JsonUtility.FromJson<SaveData>(System.IO.File.ReadAllText(Path));
}
}
Application.persistentDataPath resolves correctly on every platform (Windows AppData, Mac Application Support, iOS/Android sandboxed paths). JsonUtility is fast and Unity-aware (Vector3 etc.) but doesn't support dictionaries; use Newtonsoft.Json (built-in package now) for richer serialization.