Unit 4: Programming

CSE434 — Game Development In 3D 9 min read

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 Player is Rigidbody + 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: Update runs once per rendered frame at a variable rate, so per-frame quantities must be scaled by Time.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 public fields, or private fields marked [SerializeField]. Properties and static fields are not serialized.
  • Reference costs: GameObject.Find, FindObjectOfType and GetComponent are search operations — cache them in Awake/Start, never call them in Update.

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, new arrays, 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, Quaternion and Color are structs (copied on assignment), so transform.position.x = 5f fails to compile — you must assign a whole new Vector3.
  • Coroutines: methods returning IEnumerator that 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, Func and the event keyword provide the language-level basis for the Observer pattern (§V).
CSHARP
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.deltaTime gives metres per frame, so movement is identical at 30 fps and 144 fps; omitting deltaTime makes 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 SendMessage are 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 a IState; concrete states implementing Enter(), Tick(), Exit().
  • Problem solved: replaces a nested if (isGrounded && !isDashing && ...) chain whose branch count grows combinatorially with the number of flags.
CSHARP
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) < 12f moves Patrol to Chase.
  • Character controllers: Idle, Run, Jump, Dash, Wallslide; each state owns its own gravity and input rules, so a Dash state 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 Grounded sub-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.

CSHARP
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 != this check is essential, otherwise re-entering scene 1 creates a second manager and audio plays twice.

B. Appropriate uses and hazards

  1. Legitimate uses: genuinely unique, long-lived services — GameManager (score, pause), AudioManager, SaveSystem, InputManager, object pools.
  2. 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 Awake is 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.

CSHARP
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 in OnDisable.
CSHARP
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 MissingReferenceException on the next invoke.

B. Application and trade-offs

  • Decoupling gained: Health knows nothing of the health bar, the blood-splatter shader, the audio cue or the analytics logger; adding a fifth reaction requires no edit to Health.
  • UnityEvent variant: [SerializeField] UnityEvent onDied lets 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; Invoke on 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 not Update, and they have no transform.
  • Memory advantage: 500 goblin prefabs referencing one EnemyStats asset store the stat block once, versus 500 copies of the same fields on 500 MonoBehaviours.
CSHARP
[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 FloatVariable asset holding player health, referenced by both the HUD and the damage system — removes the need for a singleton.
  • Event channels: a GameEventSO asset with a Raise() method and a listener list implements Observer without direct object references, so objects in different scenes can communicate.
  • Pluggable behaviour: an abstract AbilitySO with public abstract void Activate(GameObject user) and subclasses FireballSO, 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.