Chua Ze Ming (0354368)
Programming
DogController.cs – Patrol and Reactive Chase AI (2D)
This script handles AI for a dog character that patrols between waypoints, detects the player using raycasting, and reacts with a bark-then-chase sequence. If the player escapes, the dog returns to its patrol route.
Patrol Behavior:
* On `Start()`, the dog searches for sibling GameObjects named with **"waypoint"** (e.g., `Waypoint1`, `Waypoint2`, `Waypoint3`, etc.) under the same parent.
* It cycles through these waypoints in order, moving from one to the next.
* At each waypoint, the dog pauses for `waitTimeAtPoint` seconds before continuing.
Detection & Response:
* A raycast is cast forward based on the dog’s current facing direction.
* If the ray hits a collider on the `detectionMask` and the object is tagged `"Player"`, it triggers the response behavior.
Bark State:
* Dog stops and triggers the `Bark` animation.
* It waits for a short period (`barkDuration`) while continuing to raycast.
* If the player leaves detection range during this time, the dog cancels the chase and resumes patrolling.
Chase Behavior:
* If the player remains detected after barking, the dog begins to chase.
* It moves toward the player's horizontal position at `sprintSpeed`.
* If the player is lost (no longer detected via raycast), the dog exits the chase.
Return & Resume Patrol:
* After losing the player, the dog runs back to its **original start position**.
* If it detects the player again mid-return, it interrupts and re-enters the bark state.
* Once back, it resumes the patrol cycle from where it left off.
Additional Notes:
* Uses `Rigidbody2D.MovePosition` for frame-independent movement.
* Facing direction is handled by flipping `transform.localScale.x`.
* Coroutines manage state transitions (patrol, bark, chase, return) to prevent overlapping behaviors.
* Animator must support triggers for: `"Bark"`, `"Sprint"`, `"Unagro"`, and `"Return"`.
This setup is intended for 2D platformers or side-scrolling games where enemies need predictable patrol patterns but reactive behavior when a player comes near. It's lightweight, extensible, and works well with minimal setup.
DogController.cs – Patrol and Reactive Chase AI (2D)
This script handles AI for a dog character that patrols between waypoints, detects the player using raycasting, and reacts with a bark-then-chase sequence. If the player escapes, the dog returns to its patrol route.
Patrol Behavior:
* On `Start()`, the dog searches for sibling GameObjects named with **"waypoint"** (e.g., `Waypoint1`, `Waypoint2`, `Waypoint3`, etc.) under the same parent.
* It cycles through these waypoints in order, moving from one to the next.
* At each waypoint, the dog pauses for `waitTimeAtPoint` seconds before continuing.
Detection & Response:
* A raycast is cast forward based on the dog’s current facing direction.
* If the ray hits a collider on the `detectionMask` and the object is tagged `"Player"`, it triggers the response behavior.
Bark State:
* Dog stops and triggers the `Bark` animation.
* It waits for a short period (`barkDuration`) while continuing to raycast.
* If the player leaves detection range during this time, the dog cancels the chase and resumes patrolling.
Chase Behavior:
* If the player remains detected after barking, the dog begins to chase.
* It moves toward the player's horizontal position at `sprintSpeed`.
* If the player is lost (no longer detected via raycast), the dog exits the chase.
Return & Resume Patrol:
* After losing the player, the dog runs back to its **original start position**.
* If it detects the player again mid-return, it interrupts and re-enters the bark state.
* Once back, it resumes the patrol cycle from where it left off.
Additional Notes:
* Uses `Rigidbody2D.MovePosition` for frame-independent movement.
* Facing direction is handled by flipping `transform.localScale.x`.
* Coroutines manage state transitions (patrol, bark, chase, return) to prevent overlapping behaviors.
* Animator must support triggers for: `"Bark"`, `"Sprint"`, `"Unagro"`, and `"Return"`.
This setup is intended for 2D platformers or side-scrolling games where enemies need predictable patrol patterns but reactive behavior when a player comes near. It's lightweight, extensible, and works well with minimal setup.
OwlController.cs – Swooping Enemy Patrol & Dive Attack AI (2D)
This script controls an enemy owl character with behavior similar to the DogController: it patrols between waypoints, detects the player, and reacts. The main difference is how it attacks—by diving straight toward the player from the air, with a temporary collision bypass (phasing layer).
🔁 Patrol Behavior (Same as DogController):
On Start(), finds sibling objects with "waypoint" in their name under the same parent (e.g., Waypoint1, Waypoint2, Waypoint3).
Moves between patrol points using Rigidbody2D.MovePosition, waiting at each for waitTimeAtPoint seconds.
Patrol continues looping unless interrupted by player detection.
👁️ Detection – View Cone + Line of Sight:
Uses a PolygonCollider2D (assigned as viewCone) as a detection field.
Every frame, checks for players inside the cone using Physics2D.OverlapCollider.
If the player is detected inside the cone, it performs a raycast check for clear line of sight.
If both cone + sight checks succeed, the owl initiates a dive attack.
🕊️ Dive Attack Behavior:
When diving:
Sets the owl to a custom "Phasing" layer so it can pass through obstacles if needed.
Continues diving toward the player’s current position using MovePosition.
Dive ends if it reaches within 1 unit of the player or if it loses line of sight mid-dive.
After the dive, owl returns to its last patrol waypoint.
↩ Return to Patrol:
Once the dive ends (either by hit or line-of-sight loss), the owl returns to the current patrol point.
When it arrives, it switches back to the "Default" collision layer and resumes patrol behavior.
🔄 State Management:
Uses flags (isDiving, isReturning) to prevent overlapping behavior.
Uses coroutines for:
Patrol (Patrol())
Dive (DiveAttack())
Return (ReturnToPatrolPoint())
⚙ Requirements:
Requires Rigidbody2D, Animator, and a child Transform with a PolygonCollider2D assigned to viewCone.
Layers: Must set up Default and Phasing layers properly in your project for collision rules.
This system gives the owl a distinct aerial "swoop" behavior that works well in 2D platformers or stealth-action games. It's modular, easily extended, and maintains clean separation of detection, attack, and patrol logic.
Guard Primary Diction (red) and chase detection (yellow)
Guard Ground Check (Green) to prevent walking off edges
This script controls the guard enemy in our game. It handles patrolling, detecting the player, chasing the player, and returning to patrol if the player is lost.
At the start of the game, the script sets up audio sources, loads references to components like the Rigidbody2D and Animator, and collects all patrol waypoints (any child object with "waypoint" in its name). If it finds more than one waypoint, the guard begins patrolling.
While patrolling, the guard moves toward a target waypoint at a slow speed. Once it reaches the waypoint, it waits for a short time before heading to the next. If the guard gets stuck (e.g. blocked by something), it will teleport to the waypoint after a set amount of time using a timeout system. The sprite also flips to face the direction it’s walking, and the walking animation speed is updated.
Every frame, the guard checks if the player is inside its view cone using a collider. If a player is detected inside the cone, the script does an additional raycast to check if the player is actually visible (not behind a wall). If both checks pass, the guard stops patrolling and enters alert mode.
When the guard enters alert mode, it stops moving and plays an alert sound. It stays in this state for a short time (like a reaction delay). If the player is still visible afterward, the guard starts chasing. If not, it goes back to patrolling.
While chasing, the guard moves toward the player at a faster speed. It keeps checking whether it still has a clear line of sight and if the player is within a certain distance. If either check fails, the guard gives up and returns to patrolling. It also plays a looping chase sound during this phase and flips the sprite to face the player. The guard only moves if there’s ground ahead, to prevent falling off edges.
If the player is lost or escapes, the guard stops the chase sound, resets the state, and resumes patrolling from the current position.
To prevent falling off platforms, the guard uses a ground check at its feet to see if there's walkable ground ahead before moving forward. This check uses a downward raycast.
For debugging in the Unity Editor, the script draws visual lines and shapes to show the view cone, aggro radius, current line to player (if chasing), and ground check ray. This helps with tuning detection and movement behavior during development.
This script manages the player’s health, death handling, and checkpoint tracking.
At runtime, currentHP is initialized to maxHP. The player’s respawn point is set to a default checkpoint if provided, otherwise the player’s starting position is used.
The TakeDamage() method reduces health by 1. If health drops to 0 or below, the level is reloaded. If health is still above 0, the player is repositioned at the last checkpoint.
Scene reload behavior depends on the sceneToReload string. If left empty, the current scene is reloaded using its build index.
SetCheckpoint() updates the last active checkpoint and its index. Only higher-indexed checkpoints overwrite the current one. This ensures the player only progresses forward.
The player's position is reset to the last checkpoint when respawning after taking damage (but not on full death).
Heal(int amount) restores health, clamped to maxHP.
GetCurrentHP() returns the current health value.
GetCheckpointIndex() returns the index of the last checkpoint reached.
This script controls the behavior of checkpoints in the game. When the player touches a checkpoint, it updates their respawn location and changes the checkpoint's appearance to indicate it’s active.
Each checkpoint has an index, a spawn point (an empty child transform), and two sprites — one for the inactive state and one for the active state.
On start (Awake), the script sets the sprite to the inactive one.
When the player enters the checkpoint’s collider (OnTriggerEnter2D), the script checks:
Is the collider the player?
Is this checkpoint not already activated?
Is this checkpoint further than the player's last activated checkpoint?
If all are true, the checkpoint becomes active.
When activated:
The checkpoint tells the player to save this checkpoint index and spawn point.
The sprite is changed to the "active" one to visually indicate that it has been used.
A debug message is printed to help track activation during development.
This system ensures the player only updates to newer checkpoints and provides a visual cue using sprite swapping.
This script handles damage when the player touches an enemy's hitbox. It should be attached to a child GameObject under each enemy, with a Trigger Collider set up.
When the player enters the trigger area (OnTriggerEnter2D), the script checks if the object has the "Player" tag.
If it is the player, the script gets the PlayerHealth component and calls TakeDamage() on it.
This setup allows enemies to damage the player simply by touching them — no extra logic needed on the enemy itself. You just need to place this hitbox object (with a collider) under each enemy.
This script is responsible for handling level transitions. It is attached to a GameObject with a trigger collider placed at the end of the level.
When the player enters the trigger, the script checks whether the current level is marked as the final level using the isFinalLevel boolean. If it is, the scene is changed to the main menu. Otherwise, it loads the next level based on the nextSceneName string set in the Inspector.
The transition is handled using SceneManager.LoadScene. A debug message is also printed when the player reaches the exit.
This script handles collectible items that recharge the player's boost meter.
The script is attached to a pickup GameObject with a trigger collider. When the player enters the trigger, it checks for a PlayerBoost component. If found, it adds a fixed charge amount (chargeAmount) to the player's boost meter and then destroys the pickup object.
The chargeAmount can be adjusted in the Inspector to control how much boost the item gives. The pickup can also be disabled instead of destroyed if object pooling is used
This script manages the player's boost meter, tracking how much boost energy is available and handling logic for adding and spending boost.
The current value stores the current boost level, while max defines the maximum capacity.
Add(float amount) increases the boost by a specified amount, clamped to the maximum.
Spend(float amount) reduces the boost if there is enough energy available. It returns true if the operation was successful, or false otherwise.
CanSpend(float amount) checks if there is enough energy to perform an action.
GetPercent() returns the current boost level as a normalized percentage between 0 and 1. This is mainly used for UI fill bars.
AuraController is a player character controller built on top of a custom KinematicObject base. It supports standard 2D movement (left/right), sprinting, and jumping, and is designed to integrate with the Unity Input System and animation state handling.
🎮 Input Handling
This controller uses Unity’s new Input System and looks up three actions:
"Player/Move" → Reads movement input (Vector2)
"Player/Jump" → Handles jump press and release
"Player/Sprint" → Enables a movement speed multiplier while held
All actions are registered and enabled in Awake() via InputSystem.actions.FindAction(...).
🦶 Movement Logic
Movement input is read each frame, and a sprint multiplier is applied if the sprint key is held:
float speed = maxSpeed * (isSprinting ? sprintMultiplier : 1f);
targetVelocity is calculated in ComputeVelocity(), and horizontal sprite flipping is applied based on move.x.
The controller inherits from KinematicObject, which likely manages collision and movement resolution via velocity and targetVelocity.
🪂 Jump State Machine
Jumping is managed with a finite state machine through the JumpState enum:
Grounded: Default idle state when touching the ground.
PrepareToJump: Triggered when the jump key is pressed.
Jumping: Executes jump and transitions to InFlight once airborne.
InFlight: Continues mid-air until grounded again.
Landed: Intermediary state before going back to Grounded.
Jump cancellation (variable height jumps) is supported by checking for jump release and halving upward velocity if still rising.
🛠 Summary:
Modular design using inheritance (KinematicObject)
New Input System compatible
Handles basic movement, sprinting, jump arcs with cutoff, and animation hooks
Clean separation between input handling, state management, and physics calculation
This component implements a lightweight parallax effect by offsetting background layers based on the camera’s movement. It’s intended for use in layered 2D environments (e.g., platformers, side-scrollers) to simulate depth using movement and sprite layering.
🔧 How It Works
On Awake(), the script stores:
The initial camera position
The layer’s original world position
On LateUpdate(), it calculates how far the camera has moved since start (cameraDelta), then applies that offset to the layer’s position — scaled on each axis by a public movementScale.
Vector3 cameraDelta = _camera.position - _initialCameraPos;
transform.position = _initialLayerPos + Vector3.Scale(cameraDelta, movementScale);
This lets each layer move proportionally slower or faster than the camera, depending on how you configure the scale.
⚙️ movementScale
movementScale.x and .y determine how much the layer moves relative to the camera.
Smaller values → slower movement → appears further back
Typical usage:
Far background: (0.2, 0.2, 0)
Midground: (0.5, 0.5, 0)
Foreground: (0.8, 0.8, 0)
Static (UI-style or fixed): (1, 1, 0)
🧱 Layering with SpriteRenderer.sortingOrder
To match the depth visually:
Use SpriteRenderer.sortingOrder to layer the sprites front-to-back.
Conventionally:
Background: lower sorting order (e.g., -10)
Foreground: higher sorting order (e.g., 10)
The movement speed (movementScale) and render order (sortingOrder) should correlate — things that move slower should also render behind faster layers.
This script controls the visual representation of the player’s boost meter using a UI bar composed of two parts: a stretchable fill and a fixed-size cap.
barFill: A 1-pixel wide image that stretches horizontally to represent the fill amount.
cap: A fixed-width image (usually rounded) placed at the end of the bar.
capWidth: Width of the cap image in pixels.
maxBarWidth: Maximum width of the full bar (when boost is 100%).
In Update(), the script reads the current boost level from the PlayerBoost component using GetPercent(), which returns a value from 0 to 1.
UpdateBar(float percent) calculates the total width based on the boost percent. If the width is less than or equal to the cap width, only the cap is shown (bar fill is hidden). If the width exceeds the cap width, the bar fill is stretched to match the remaining space, and the cap is positioned at the end.
This creates a clean, responsive UI bar with a rounded edge, while maintaining accurate fill proportional to the player's current boost.
This script displays the player’s current health using heart icons in the UI.
player: Reference to the PlayerHealth component to read current HP.
heartPrefab: A UI prefab (typically an image) representing one heart.
In every frame (Update()), the script does the following:
Clears all existing heart icons from the UI container (this script is placed on a horizontal layout group or similar).
Retrieves the player’s current HP using GetCurrentHP().
Instantiates one heart prefab for each point of HP.
This approach ensures the number of visible hearts always reflects the player’s current health.
This script scrolls background layers to create a parallax effect in the main menu.
The script expects multiple child GameObjects, each with a SpriteRenderer. These should be placed side by side to form a seamless horizontal loop.
In Start(), the script caches all child transforms into the tiles array and calculates the width of a single tile using the bounds of the first child's sprite.
In Update(), the entire parent object is moved left based on scrollSpeed.
Each tile is checked to see if it has moved off-screen to the left (past the camera view). If so, that tile is repositioned to the right of the current rightmost tile. This creates a continuous horizontal scroll loop.
The speed of movement is adjustable per layer to achieve a multi-layer parallax effect.
This script handles displaying exposition text on screen, typically used at the start or end of a level.
textFile is a .txt asset containing the lines of exposition, set in the Inspector.
textDisplay is a TextMeshProUGUI component where the text will be shown.
nextSceneName defines the scene to load after the exposition ends.
In Start(), the script loads the text file (if assigned) by splitting it into lines and queuing them.
In Update(), the script checks for input — Enter key or left mouse click. When pressed, it triggers DisplayNextLine().
DisplayNextLine() shows the next line of text from the queue. When all lines have been displayed, it loads the next scene using SceneManager.LoadScene.
All lines are trimmed and blank lines are ignored during loading. The exposition only starts once a valid file is loaded.
This script handles NPC interaction logic, including proximity detection, showing prompts, displaying dialogue lines from a text file, and disabling the NPC after the conversation ends.
promptUI: A UI element shown when the player is within range but not talking (e.g. "Press Enter to talk").
dialogueUI: The main dialogue panel that appears during conversation.
dialogueText: TextMeshPro field for displaying dialogue lines.
dialogueFile: A .txt file containing the dialogue content (one line per entry).
playerInRange: Becomes true when the player enters the NPC's trigger collider.
isTalking: Tracks whether the dialogue is active.
lines: A queue that stores trimmed dialogue lines split from the text file.
On Start(), both the prompt and dialogue UI are hidden.
On Update(), if the player is in range and presses Enter:
If not already talking, it starts the dialogue.
If already talking, it shows the next line.
StartDialogue() clears any existing lines, splits the text file into a queue, hides the prompt, shows the dialogue UI, and displays the first line.
DisplayNextLine() dequeues and displays the next line. If the queue is empty, EndDialogue() is called.
EndDialogue() hides the dialogue UI, resets state, and disables the NPC GameObject.
OnTriggerEnter2D() sets playerInRange to true when the player enters the collider and shows the prompt if not already talking.
OnTriggerExit2D() sets playerInRange to false, hides the prompt, and ends the dialogue if the player leaves mid-conversation.
This script links a Unity Slider UI to an AudioMixer volume setting and keeps it synced across sessions and multiple sliders.
audioMixer: Reference to the AudioMixer where the volume parameter is stored.
exposedParam: Name of the volume parameter in the mixer (default is "MasterVolume").
slider: The UI Slider that the user interacts with.
Start()
If the slider isn't assigned, it grabs the Slider component on the GameObject.
Loads the saved volume (from PlayerPrefs) or defaults to full volume (1.0).
Converts that volume to decibels (dB) using log10(value) * 20 and applies it to the AudioMixer.
Adds a listener so that when the slider is moved, SetVolume() gets called.
Registers this slider with VolumeSync so it can sync with others.
SetVolume(float value)
Called when the slider is changed.
Converts the linear volume (0.0 to 1.0) into decibels and sets the mixer value.
Updates all linked sliders through VolumeSync.
Saves the new value into PlayerPrefs.
UpdateSliderFromMixer()
Reads the current decibel value from the AudioMixer.
Converts it back to linear (for the UI slider).
Updates the slider without triggering another volume update (isUpdating flag prevents loops).
All audio in the game, including guard-related sound effects, is routed into a Unity Audio Mixer.
The guard's AudioSource is assigned to a Mixer Group that falls under the Master group.
This allows global control of volume via the mixer without touching each individual AudioSource.
The Master volume is controlled by a UI Slider linked to the VolumeSlider script.
The script references the Audio Mixer and adjusts an exposed parameter named MasterVolume.
The Slider value ranges from 0.0 to 1.0.
This value is converted to decibels before being applied to the mixer.
The slider uses a logarithmic scale for audio accuracy:
Slider value (0.0001 to 1.0) is converted to decibels.
The decibel value is set in the mixer via the exposed parameter.
This allows for natural-sounding volume changes (human hearing is logarithmic).
The last selected volume is saved using PlayerPrefs.
On startup, the slider loads the saved value and updates the mixer accordingly.
This ensures consistent audio levels across sessions.
If there are multiple sliders (e.g. in options and pause menu), the VolumeSync system ensures all stay updated when one changes.
This script manages navigation between multiple UI menu pages using a stack-based approach. It allows forward navigation to specific pages (Settings, Credits, Controls) and backward navigation via a GoBack() function.
The system controls the following UI groups:
mainMenuGroup
settingsGroup
creditsGroup
controlsGroup
Each group is assumed to be a GameObject containing the corresponding UI elements.
A stack (pageStack) keeps track of navigation history:
When a new page is shown, it is pushed to the stack.
When the back function is called, the current page is popped off, and the previous one is reactivated.
This allows proper "back" behavior across menus.
ShowMainMenu(), ShowSettings(), ShowCredits(), ShowControls()
Directly open the specified menu page. Internally uses ShowPage().
GoBack()
Goes back to the previous page by popping the top of the stack and reactivating the one underneath.
ShowPage(GameObject page)
Deactivates the current page and shows the new one. Pushes it to the stack.
ShowOnly(GameObject page)
Used on Start() to ensure only the main menu is shown at launch. Clears the stack and resets to a known state.
On Start(), the script activates only the main menu and hides the rest to prevent multiple pages being visible at once.
Each character (Dog, Owl, Aura) uses an Animator component to visually reflect their current state. The animation transitions are driven by simple flags or parameters set directly in the respective controller scripts.
🐶 Dog Animation Controller
The DogController uses trigger-based animations to reflect key behavioral states:
"Bark" – triggered when player is detected
"Sprint" – triggered before chase begins
"Unagro" – when player escapes during bark phase
"Return" – when returning to patrol after a chase
These transitions are cleanly tied to specific points in the AI coroutine flow, which makes the animation logic straightforward and event-driven.
🦉 Owl Animation Controller
The OwlController follows a similar pattern to Dog, using triggers and booleans in response to dive attack states:
Likely triggers for:
"Dive"
"Return" or "Recover"
These are not explicitly shown in code but can be hooked in via the DiveAttack() and ReturnToPatrolPoint() coroutines.
The owl's animator can stay fairly minimal, as the behavior is binary: patrolling or diving.
✨ Aura (Player) Animation Controller
The AuraController is more complex and state-dependent. Animation parameters are updated in real-time every frame inside ComputeVelocity():
IsGrounded → bool
velocityX → float (absolute horizontal speed)
velocityY → float (vertical velocity, useful for airborne states)
landTrigger → trigger, used when falling transitions back to grounded
This setup allows fine-grained transitions:
Idle ↔ Walk ↔ Run (via velocityX)
Jump ↔ Fall ↔ Land (via velocityY + IsGrounded)
Sprint animation could optionally be layered in using velocityX threshold + isSprinting flag
Level Design
1st draft
2nd draft
3rd draft