Unit 4: Programming
I. Orientation: Code as the Behavioural Layer of a 3D Game
A 3D game engine supplies rendering, physics, audio and asset pipelines; programming supplies the behaviour that binds them. In Unity (released 2005; C# as the surviving scripting language after UnityScript was deprecated in 2017), gameplay code is written as components attached to GameObjects in a scene, and the engine drives them through a fixed callback sequence rather than a main() loop you own. Unit 4 covers the language you write in, the reusable structural patterns that keep that code from collapsing as the project grows, and Unity's data-container asset type, ScriptableObject.
Defining properties and conventions assumed throughout:
- Component-based composition: behaviour is assembled by attaching many small scripts to an object, not by deep inheritance trees. A
PlayerisRigidbody+Collider+PlayerMovement+Health. - Engine-owned control flow: you write callbacks (
Awake,Start,Update,FixedUpdate,LateUpdate,OnDestroy); the engine calls them. Never block inside one. - Frame-time thinking:
Updateruns once per rendered frame at a variable rate, so per-frame quantities must be scaled byTime.deltaTime(seconds since last frame). - Determinism boundary: physics runs in
FixedUpdate(default 0.02 s, i.e. 50 Hz) so force integration is stable. - Serialization contract: the Inspector shows and saves
publicfields, orprivatefields marked[SerializeField]. Properties and static fields are not serialized. - Reference costs:
GameObject.Find,FindObjectOfTypeandGetComponentare search operations — cache them inAwake/Start, never call them inUpdate.
II. Scripting Language — C# in the Unity Runtime
A. Definition and role
A scripting language in a game engine is the high-level language in which gameplay logic is authored, compiled or interpreted separately from the engine's native (C++) core, so designers and programmers can iterate without rebuilding the engine.
- Two-layer architecture: engine internals in C++ for speed; gameplay in C# for safety and iteration speed. Calls across the boundary (e.g.
transform.position) are marshalled, so they cost more than pure C# arithmetic. - Compilation path: C# → IL (Intermediate Language) → executed by Mono, or converted to C++ by IL2CPP and compiled natively for iOS, WebGL and consoles.
- Managed memory: the garbage collector reclaims unused heap objects; allocations inside
Update(string concatenation,newarrays, LINQ) cause GC spikes and frame stutter. - Contrast with other engines: Unreal pairs C++ with the visual Blueprint scripting graph; Godot uses GDScript, a Python-like interpreted language. The pattern is the same — a fast core plus an ergonomic scripting surface.
B. Core language features used in gameplay code
- Classes deriving from
MonoBehaviour: only these can be attached to a GameObject and receive engine callbacks. - Value vs reference types:
Vector3,QuaternionandColorare structs (copied on assignment), sotransform.position.x = 5ffails to compile — you must assign a whole newVector3. - Coroutines: methods returning
IEnumeratorthat suspend across frames, used for timed sequences without blocking. - Attributes:
[SerializeField],[Range(0,10)],[Header("Movement")],[RequireComponent(typeof(Rigidbody))]shape the Inspector and enforce dependencies. - Events and delegates:
Action,Funcand theeventkeyword provide the language-level basis for the Observer pattern (§V).
public class PlayerMovement : MonoBehaviour
{
[SerializeField] private float speed = 6f; // m/s, editable in Inspector
private Rigidbody rb; // cached reference
void Awake() => rb = GetComponent<Rigidbody>();
void Update() // input: variable rate
{
float h = Input.GetAxis("Horizontal"); // -1..+1
float v = Input.GetAxis("Vertical");
transform.Translate(new Vector3(h, 0f, v) * speed * Time.deltaTime);
}
void FixedUpdate() => rb.AddForce(Physics.gravity, ForceMode.Acceleration);
}- Symbol check:
speed * Time.deltaTimegives metres per frame, so movement is identical at 30 fps and 144 fps; omittingdeltaTimemakes speed frame-rate dependent.
C. Applications and limitations
- Applications: input handling, AI decision logic, UI wiring, animation triggers, save/load, procedural spawning.
- Limitations: heavy per-frame numerical work (thousands of agents, mesh generation) belongs in the Job System/Burst or compute shaders; C# reflection and
SendMessageare convenient but slow enough to avoid in hot paths.
III. Game Architecture Pattern — State
A. Statement and structure
The State pattern lets an object alter its behaviour when its internal condition changes, by delegating behaviour to interchangeable state objects instead of testing flags. A finite state machine (FSM) is defined by the tuple (S, s₀, Σ, δ) — states, initial state, inputs, transition function δ: S × Σ → S.
- Participants: a
Context(e.g.EnemyAI) holding a reference to aIState; concrete states implementingEnter(),Tick(),Exit(). - Problem solved: replaces a nested
if (isGrounded && !isDashing && ...)chain whose branch count grows combinatorially with the number of flags.
public interface IState { void Enter(); void Tick(); void Exit(); }
public class EnemyAI : MonoBehaviour
{
private IState current;
public void ChangeState(IState next)
{
current?.Exit(); // e.g. stop the walk animation
current = next;
current.Enter(); // e.g. play the attack windup
}
void Update() => current?.Tick();
}B. Application in 3D gameplay
- Enemy AI:
Patrol → Chase → Attack → Flee. Transition condition:Vector3.Distance(transform.position, player.position) < 12fmoves Patrol to Chase. - Character controllers:
Idle,Run,Jump,Dash,Wallslide; each state owns its own gravity and input rules, so aDashstate can simply ignore gravity. - Animator parity: Unity's Animator state machine is the same pattern in visual form; keeping code states and animator states one-to-one prevents desynchronised visuals.
- Variants: a stack-based FSM (push/pop) supports "return to previous state" after a stun; a hierarchical FSM groups
Groundedsub-states so shared logic is written once.
C. Limitations
- State explosion: n states admit up to n(n−1) transitions; beyond ~10 states, behaviour trees or GOAP scale better.
- Shared data: states need access to context fields, so the context must expose them, weakening encapsulation.
IV. Game Architecture Pattern — Singleton
A. Statement and structure
The Singleton pattern guarantees a class has exactly one instance and provides a global access point to it. In Unity it is typically a MonoBehaviour that survives scene loads.
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); // persists across SceneManager.LoadScene
}
}- Access:
AudioManager.Instance.PlaySfx(clip)from anywhere, with no Inspector wiring. - Duplicate guard: the
Instance != thischeck is essential, otherwise re-entering scene 1 creates a second manager and audio plays twice.
B. Appropriate uses and hazards
- Legitimate uses: genuinely unique, long-lived services —
GameManager(score, pause),AudioManager,SaveSystem,InputManager, object pools. - Hazards: global mutable state hides dependencies (a class's needs no longer appear in its constructor or Inspector); tight coupling makes unit testing and scene-level reuse hard; initialization order between two singletons that reference each other in
Awakeis undefined.
- Mitigations: expose only a narrow interface; prefer
[SerializeField]references or ScriptableObject-based service objects (§VI) where an asset can carry the shared data instead of a static field.
V. Game Architecture Pattern — Observer
A. Statement and structure
The Observer pattern defines a one-to-many dependency in which a subject notifies registered observers of state changes without knowing their concrete types — the mechanism behind event-driven UI and achievement systems.
public class Health : MonoBehaviour
{
public event Action<int, int> OnHealthChanged; // (current, max)
public event Action OnDied;
[SerializeField] private int max = 100;
private int current;
public void TakeDamage(int amount)
{
current = Mathf.Max(0, current - amount);
OnHealthChanged?.Invoke(current, max); // fires for every listener
if (current == 0) OnDied?.Invoke();
}
}- Subscription lifecycle: subscribe in
OnEnable, unsubscribe inOnDisable.
void OnEnable() => health.OnHealthChanged += UpdateBar;
void OnDisable() => health.OnHealthChanged -= UpdateBar;- Why unsubscribe: a surviving delegate holds a reference to a destroyed listener, causing a leak and a
MissingReferenceExceptionon the next invoke.
B. Application and trade-offs
- Decoupling gained:
Healthknows nothing of the health bar, the blood-splatter shader, the audio cue or the analytics logger; adding a fifth reaction requires no edit toHealth. - UnityEvent variant:
[SerializeField] UnityEvent onDiedlets designers wire responses in the Inspector, at the cost of slower invocation and no compile-time type checking. - Costs: control flow becomes non-local and hard to trace in a debugger; ordering among observers is unspecified;
Invokeon a null event without?.throws.
VI. Scriptable Objects — Data as Assets
A. Definition and purpose
A ScriptableObject is a serializable class whose instances live as asset files in the project rather than as components on GameObjects, giving one shared copy of data in memory independent of any scene.
- Creation:
[CreateAssetMenu(fileName = "Weapon", menuName = "Game/Weapon")]adds an entry to the Assets ▸ Create menu. - Callbacks available:
OnEnable,OnDisable,OnValidate,Awake— but notUpdate, and they have notransform. - Memory advantage: 500 goblin prefabs referencing one
EnemyStatsasset store the stat block once, versus 500 copies of the same fields on 500 MonoBehaviours.
[CreateAssetMenu(menuName = "Game/Weapon")]
public class WeaponData : ScriptableObject
{
public string weaponName = "Rifle";
public float damage = 25f; // hit points per shot
public float fireRate = 8f; // shots per second
public GameObject muzzleFlashPrefab;
}B. Uses in a 3D project
- Configuration data: weapon, enemy, item and level tables that designers edit as assets with no code change and no scene dirty-marking.
- Shared runtime variables: a
FloatVariableasset holding player health, referenced by both the HUD and the damage system — removes the need for a singleton. - Event channels: a
GameEventSOasset with aRaise()method and a listener list implements Observer without direct object references, so objects in different scenes can communicate. - Pluggable behaviour: an abstract
AbilitySOwithpublic abstract void Activate(GameObject user)and subclassesFireballSO,HealSO— new abilities are new assets, a strategy pattern in asset form.
C. Limitations and cautions
- Editor vs build persistence: changes made to a ScriptableObject at runtime persist in the Editor after exiting Play mode, but are discarded in a built game — so never use them as the save file; write to JSON or
PlayerPrefs. - Shared-state bugs: mutating a ScriptableObject holding current health affects every object referencing it; keep runtime state on the instance and static definitions in the asset, or copy with
Instantiate(asset). - Not a component: cannot receive collisions, coroutines started on it need a MonoBehaviour host, and it must be referenced by an asset or scene object to avoid being stripped from the build.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →