COIT20271 · Week 4 Lab Tutorial

🌾 Farm Life

Build a complete farm game — cultivate crops, feed chickens, and chop trees using C# scripting in Unity.

MonoBehaviour Lifecycle
Coroutines
Interfaces
Animator Controller
State Machines
6 Scripts
Introduction

What You'll Build

In this tutorial you'll build Farm Life — a top-down farm game where a player character walks around a farm, plants and harvests crops, feeds hungry chickens, and chops down trees. It is designed to use every scripting concept from the Week 4 lecture in a natural, visible way.

🎯
Learning Goal

By the end of this tutorial you will have written 6 scripts that communicate through a shared interface — the same pattern used in professional Unity projects.

The Three Farm Activities

ActivityMechanicKey Scripting Concept
🌱 Cultivate Plant seeds → wait → harvest ready crops Coroutine, Enum state machine, Material swap
🐔 Feed Chickens Pick up feed → walk to chicken → press E Inventory, Animator Trigger, OnTriggerEnter
🪓 Chop Trees Hold E near tree → progress bar fills → tree falls Time.deltaTime, UI Slider, Destroy()

Week 4 Concepts in Action

Lecture ConceptWhere it appears in Farm Life
Awake()Every interactable caches its components here
Start()GameManager sets up UI; chickens start wandering
Update()Player movement, tree chop progress, timer countdown
LateUpdate()Camera follows player after all movement resolves
CoroutineCrop growing (10 sec), chicken re-hunger (30 sec), tree fall
[SerializeField]Grow times, chop duration, scores — all Inspector-tunable
GetComponentCached in Awake() on every script
OnTriggerEnter/StayDetecting player near crops, chickens, trees
interfaceIInteractable — one interaction system for all objects
⏱️
Time Estimate

This tutorial covers two lab sessions. Week 4: Asset setup, scene, PlayerFarmer, CropPatch. Week 5: Chicken, Tree, GameManager, polish and extensions.

Step 0

Asset Store Setup

Before writing a single line of code, import the free assets below. These give you a character, a farm environment, and animals — so you can focus entirely on scripting rather than 3D modelling.

⚠️
Import in this order

Import the character package first, then the environment, then animals. This prevents namespace conflicts and keeps your Project window clean.

🧑‍🌾
Character + Camera Rig
Starter Assets – Third Person
Unity's official starter pack. Always version-compatible. Includes Idle, Walk, Run, Jump animations and a built-in camera rig.
🏡
Farm Environment
Low Poly Farm Pack Lite
Includes soil patches, fences, barn, trees, ground tiles. Low poly means fast import and good performance on all machines.
🐔
Chicken
Low Poly Animals (free)
Simple low-poly chicken model with Idle and Eat animation clips. Confirm the rig type is Humanoid or Generic before importing.
💊
Fallback (if stuck)
Unity Primitives Only
If any import fails: Capsule = player, coloured Cubes = crops and trees, Sphere = chicken. You can do the full tutorial with primitives.

How to Import Each Asset

  1. 1
    Open the Asset Store in Unity

    Go to Window → Asset Store. This opens your browser. Sign in with your Unity ID if prompted.

  2. 2
    Search for the exact asset name

    Search "Starter Assets Third Person". Click the result. Click "Add to My Assets" then "Open in Unity".

  3. 3
    Import via Package Manager

    Unity opens the Package Manager window automatically. Click Import. Keep all files ticked. Wait for the import to finish completely before importing the next asset.

  4. 4
    Verify the import

    In the Project window, you should see a new folder for the asset. Expand it — you should see Models, Prefabs, Animations, and Materials folders.

📝
Licence Note

Asset Store free assets are licenced for use in Unity projects only. Do not redistribute the raw asset files outside of your built game.

Step 1

Scene Setup

Work through this checklist top-to-bottom. By the end you should have a playable scene with a character walking around — before writing a single script.

1A — Install Cinemachine (Required)

The Starter Assets character camera relies on Cinemachine — a Unity package that must be installed before the prefab will work correctly.

🚫
Do not skip this step

If Cinemachine is missing, Unity will show "The type or namespace 'Cinemachine' could not be found" compile errors and the PlayerArmature prefab will appear broken with missing script warnings. Install Cinemachine first, then import Starter Assets.

1B — Open the Starter Assets Sample Scene

Rather than building the camera system from scratch — which involves several fragile manual steps — use the sample scene that ships with Starter Assets. It has the character, camera, and input system already correctly wired together. You then save it as your own scene and build the farm on top of it.

💡
Why start from the sample scene?

The Starter Assets camera system involves three components that must be configured together: a Main Camera with a Cinemachine Brain, a PlayerFollowCamera virtual camera, and the PlayerInput component on the character. Getting any one of these slightly wrong causes "No cameras rendering" or broken movement. The sample scene has all of this already done correctly.

  1. 1
    Find the sample scene in the Project window

    Navigate to Assets → StarterAssets → ThirdPersonController → Scenes. You should see a scene called Playground (or similar). Double-click it to open it.

  2. 2
    Press Play to confirm it works

    Press Play. The character should be visible in the Game view and you should be able to move with WASD and look with the mouse. This confirms the character, camera, and input are all working. Press Play again to stop.

  3. 3
    Save it as your FarmScene

    Go to File → Save As. Navigate to your Assets/Scenes/ folder. Name the file FarmScene. Click Save. You now have your own copy — the original Playground scene is unchanged.

  4. 4
    Delete the sample obstacles

    In the Hierarchy, select and delete anything that is not the character, the camera, or the lighting. This typically means deleting a Obstacles or Geometry root GameObject that contains the sample platform/course. Leave PlayerArmature, PlayerFollowCamera, Main Camera, and Directional Light in place.

  5. 5
    Press Play again to confirm the clean scene still works

    Press Play. The character should still move and the camera should still follow — the character will fall into a void, which is fine. This confirms deleting the obstacles did not break anything. Press Play to stop.

⚠️
Can't find the Scenes folder inside StarterAssets?

Some versions of the package use a different folder path. Try: Assets → StarterAssets → Scenes or search the Project window for Playground. If there is no sample scene at all, go to the Package Manager → Starter Assets → click Import into Project to bring in the sample content.

1C — Ground and Terrain

1D — Reposition the Character on the Ground

🔴
Pink / Magenta materials on the character?

Render pipeline mismatch. The Starter Assets URP version needs a URP project, and the Built-in version needs a Built-in project. Check your project type in Edit → Project Settings → Graphics. If the Scriptable Render Pipeline Asset field is empty, you have a Built-in project — download the Built-in version of Starter Assets. If it has a URP asset assigned, download the URP version.

1E — Farm Activity Zones

1F — Tags and Layers

Unity requires tags to be registered before use. Add these now:

1G — UI Canvas

Scene Checkpoint

Your scene should now have: a walking character with camera, a farmyard plane, 5 soil patches, 3 chickens, 4 trees, a feed bag, and a UI canvas with score/timer/prompt/slider.

Step 2

Script Architecture

Before writing any code, understand how the six scripts communicate. This is the most important concept in the tutorial.

GameManager.cs score · timer · UI refs · game state CropPatch.cs Empty→Planted→Growing→Ready Chicken.cs Hungry→Fed→Hungry (coroutine) Tree.cs chop progress · fall · Destroy implements IInteractable PlayerFarmer.cs movement · input · calls Interact() Inventory.cs feedCount · woodCount · cropCount «interface» IInteractable Interact() · GetPromptText() manages / reads calls method on

The Key Design: IInteractable

All three farm objects (CropPatch, Chicken, Tree) implement the same interface. The player doesn't need to know what it's standing next to — it just calls Interact() and the correct behaviour happens. This is called polymorphism and it keeps PlayerFarmer.cs simple and clean.

Without an interface (messy)

  • if tag == "CropPatch" → cropPatch.Plant()
  • if tag == "Chicken" → chicken.Feed()
  • if tag == "Tree" → tree.Chop()
  • Adding a new object = editing PlayerFarmer

With an interface (clean)

  • IInteractable target = GetNearbyObject()
  • target.Interact(this) — done!
  • The object handles its own logic
  • Adding new objects = zero changes to PlayerFarmer
Script 0 of 6

IInteractable.cs — The Interface

This is not a MonoBehaviour — it is a plain C# interface. It defines a contract: any object that can be interacted with by the player must provide these two methods. It is tiny but architecturally important.

IInteractable.cs
Interface — not a MonoBehaviour · no GameObject required
Interface
interface polymorphism design pattern
📁
Create this file

In Project window → right-click → Create → C# Script → name it exactly IInteractable. Delete the generated content and replace it with the code below.

IInteractable.cs C#
// IInteractable.cs
// ─────────────────────────────────────────────────────────────────────────────
// This is a C# interface — not a MonoBehaviour.
// Any script that can be interacted with by the player must implement
// this interface. The player calls Interact() without caring what type
// of object it is talking to (CropPatch, Chicken, or Tree).
// ─────────────────────────────────────────────────────────────────────────────

public interface IInteractable
{
    // Called when the player presses E while standing near this object.
    // The PlayerFarmer reference is passed in so the object can access
    // the player's inventory or position if needed.
    void Interact(PlayerFarmer player);

    // Returns the prompt text shown on screen when the player is nearby.
    // Example: "Press E to Plant", "Press E to Feed Chicken"
    string GetPromptText();
}
💡
Teaching Note: Why an interface and not a base class?

CropPatch, Chicken, and Tree all already inherit from MonoBehaviour. C# does not allow multiple inheritance of classes — but a class can implement multiple interfaces. An interface is the right tool here.

Script 1 of 6

Inventory.cs

A simple component that lives on the Player and tracks everything they are carrying. Other scripts ask the player's inventory whether they have enough feed, or tell it to add harvested crops.

1
Inventory.cs
Attach to: PlayerArmature
MonoBehaviour
[SerializeField] public methods encapsulation
Inventory.cs C#
// Inventory.cs
// ─────────────────────────────────────────────────────────────────────────────
// Tracks everything the player is carrying: feed bags, harvested crops,
// and chopped wood. Other scripts call the public methods to add or
// spend items rather than accessing the counts directly.
// ─────────────────────────────────────────────────────────────────────────────

using UnityEngine;

public class Inventory : MonoBehaviour
{
    // ── Starting quantities (tunable in the Inspector) ──────────────────
    [SerializeField] private int startingFeedBags = 0;

    // ── Private counts — other scripts use the public methods below ─────
    private int feedBags;
    private int cropsHarvested;
    private int woodChopped;

    // ── Properties — read-only access for other scripts ─────────────────
    public int FeedBags      => feedBags;
    public int CropsHarvested => cropsHarvested;
    public int WoodChopped   => woodChopped;

    // ────────────────────────────────────────────────────────────────────
    // Awake: initialise quantities from Inspector values
    // ────────────────────────────────────────────────────────────────────
    void Awake()
    {
        feedBags = startingFeedBags;
    }

    // ── Feed Bag methods ─────────────────────────────────────────────────

    public void AddFeedBag()
    {
        feedBags++;
        Debug.Log($"Picked up feed bag. Total: {feedBags}");
    }

    // Returns true if the player had a feed bag to spend, false if empty.
    public bool SpendFeedBag()
    {
        if (feedBags <= 0)
        {
            Debug.Log("No feed bags left!");
            return false;
        }
        feedBags--;
        Debug.Log($"Used feed bag. Remaining: {feedBags}");
        return true;
    }

    // ── Crop methods ─────────────────────────────────────────────────────

    public void AddCrop(int value)
    {
        cropsHarvested++;
        // Notify the GameManager to update score
        GameManager.Instance.AddScore(value);
        Debug.Log($"Harvested crop worth {value} points. Total crops: {cropsHarvested}");
    }

    // ── Wood methods ─────────────────────────────────────────────────────

    public void AddWood(int value)
    {
        woodChopped++;
        GameManager.Instance.AddScore(value);
        Debug.Log($"Chopped wood worth {value} points. Total wood: {woodChopped}");
    }
}
Script 2 of 6

PlayerFarmer.cs

The central hub. Handles movement input and detects when the player is near an interactable object. When the player presses E, it calls Interact() on whatever object they are standing next to.

2
PlayerFarmer.cs
Attach to: PlayerArmature
MonoBehaviour
Awake() Update() Physics.OverlapSphere GetComponent caching interface call
PlayerFarmer.cs C#
// PlayerFarmer.cs
// ─────────────────────────────────────────────────────────────────────────────
// Attached to the PlayerArmature. Handles interaction input and
// keeps track of which IInteractable the player is currently near.
// Movement is handled by the Starter Assets controller — we only
// add the farming interaction layer here.
// ─────────────────────────────────────────────────────────────────────────────

using UnityEngine;
using TMPro;

public class PlayerFarmer : MonoBehaviour
{
    // ── Inspector References ─────────────────────────────────────────────
    // promptText is the single TMP_Text you created in the Canvas.
    // There is no separate "panel" — we show/hide the text object directly.
    [SerializeField] private TMP_Text promptText;

    // interactionRadius controls how close the player must be to trigger a prompt.
    // Tune this in the Inspector — 2.0 is a good starting value.
    [SerializeField] private float interactionRadius = 2.0f;

    // ── Cached Components ────────────────────────────────────────────────
    private Inventory inventory;

    // ── State ────────────────────────────────────────────────────────────
    private IInteractable nearbyInteractable;

    // Mobile chop hold state — read by Tree.cs each frame
    [HideInInspector] public bool isMobileChopping = false;

    // ── Property ─────────────────────────────────────────────────────────
    public Inventory Inventory => inventory;

    // ────────────────────────────────────────────────────────────────────
    // Awake: cache the Inventory component on this same GameObject
    // ────────────────────────────────────────────────────────────────────
    void Awake()
    {
        inventory = GetComponent<Inventory>();
        if (inventory == null)
            Debug.LogError("PlayerFarmer: Inventory component missing from this GameObject!");
    }

    // ────────────────────────────────────────────────────────────────────
    // Start: hide the prompt text at game start
    // ────────────────────────────────────────────────────────────────────
    void Start()
    {
        HidePrompt();
    }

    // ────────────────────────────────────────────────────────────────────
    // Update: every frame, use Physics.OverlapSphere to find the closest
    // interactable object within interactionRadius.
    //
    // Why OverlapSphere instead of OnTriggerEnter?
    // The Starter Assets PlayerArmature uses a CharacterController, which
    // has unreliable trigger behaviour. OverlapSphere is a direct physics
    // query that works correctly regardless of how the player is set up.
    // No special collider setup is needed on the player.
    // ────────────────────────────────────────────────────────────────────
    void Update()
    {
        FindNearestInteractable();

        if (nearbyInteractable != null)
        {
            ShowPrompt(nearbyInteractable.GetPromptText());

            // Keyboard: press E once to interact
            if (Input.GetKeyDown(KeyCode.E))
                nearbyInteractable.Interact(this);
        }
        else
        {
            HidePrompt();
        }

        // FeedBag pickup: check separately each frame
        CheckFeedBagPickup();
    }

    // ────────────────────────────────────────────────────────────────────
    // FindNearestInteractable: scan all colliders within interactionRadius
    // and set nearbyInteractable to the closest IInteractable found.
    // ────────────────────────────────────────────────────────────────────
    private void FindNearestInteractable()
    {
        // QueryTriggerInteraction.Collide means we detect both trigger
        // and non-trigger colliders — works with any Asset Store prefab
        Collider[] nearby = Physics.OverlapSphere(
            transform.position,
            interactionRadius,
            Physics.AllLayers,
            QueryTriggerInteraction.Collide
        );

        IInteractable closest  = null;
        float         closestD = float.MaxValue;

        foreach (Collider col in nearby)
        {
            // Skip the player's own colliders
            if (col.transform.root == transform.root) continue;

            IInteractable interactable = col.GetComponentInParent<IInteractable>();
            if (interactable == null) continue;

            float dist = Vector3.Distance(transform.position, col.transform.position);
            if (dist < closestD)
            {
                closestD = dist;
                closest  = interactable;
            }
        }

        nearbyInteractable = closest;
    }

    // ────────────────────────────────────────────────────────────────────
    // CheckFeedBagPickup: auto-collect any FeedBag the player walks over
    // ────────────────────────────────────────────────────────────────────
    private void CheckFeedBagPickup()
    {
        Collider[] nearby = Physics.OverlapSphere(transform.position, 1.0f);
        foreach (Collider col in nearby)
        {
            if (col.CompareTag("FeedBag"))
            {
                inventory.AddFeedBag();
                Destroy(col.gameObject);
            }
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // Mobile button methods — called by on-screen UI buttons
    // ────────────────────────────────────────────────────────────────────
    public void MobileInteract()
    {
        if (nearbyInteractable != null)
            nearbyInteractable.Interact(this);
    }

    public void MobileChopBegin() { isMobileChopping = true;  }
    public void MobileChopEnd()   { isMobileChopping = false; }

    // ────────────────────────────────────────────────────────────────────
    // Prompt helpers
    // ────────────────────────────────────────────────────────────────────
    private void ShowPrompt(string message)
    {
        if (promptText == null) return;
        promptText.gameObject.SetActive(true);
        promptText.text = message;
    }

    private void HidePrompt()
    {
        if (promptText != null)
            promptText.gameObject.SetActive(false);
    }

    // Called by Tree.cs before it destroys itself
    public void ClearNearbyInteractable()
    {
        nearbyInteractable = null;
        HidePrompt();
    }
}
🔧
Inspector Setup for PlayerFarmer — only two fields

Prompt Text: drag the TMP_Text prompt object from your Canvas into this field. Interaction Radius: leave at 2.0 to start — increase it if the prompt feels unresponsive, decrease it if it triggers too early. No Sphere Collider is needed on the player.

Script 3 of 6

CropPatch.cs

The star of Week 4. This script manages the full lifecycle of a crop from bare soil to a ready-to-harvest plant using an enum state machine and coroutines for the timed growing stages.

State Machine

Empty
brown soil
Press E
Planted
dark soil
10 sec
Growing
yellow sprout
8 sec
Ready
green glow
Press E
Empty
resets
3
CropPatch.cs
Attach to: each soil patch prefab in the scene
MonoBehaviour
Awake() Coroutine Enum state machine IInteractable Material swap [SerializeField]
CropPatch.cs C#
// CropPatch.cs
// ─────────────────────────────────────────────────────────────────────────────
// Manages the lifecycle of a single crop patch.
// Implements IInteractable so the player can plant and harvest
// by pressing E when nearby.
// ─────────────────────────────────────────────────────────────────────────────

using System.Collections;
using UnityEngine;

public class CropPatch : MonoBehaviour, IInteractable
{
    // ── State Machine ────────────────────────────────────────────────────
    // An enum cleanly names each stage — much better than using magic
    // integers (0, 1, 2, 3) which are hard to read and maintain.
    public enum CropState
    {
        Empty,     // bare soil, ready to plant
        Planted,   // seed in ground, starting to grow
        Growing,   // sprouting — yellow visual
        Ready      // fully grown — green, can be harvested
    }

    // ── Inspector Settings ───────────────────────────────────────────────
    [SerializeField] private float    plantedDuration = 10f; // seconds until Growing
    [SerializeField] private float    growingDuration = 8f;  // seconds until Ready
    [SerializeField] private int      cropValue       = 10;  // score awarded on harvest
    [SerializeField] private string   cropName        = "Wheat";

    // ── Materials for each visual state ──────────────────────────────────
    // Drag these in from the Project window in the Inspector.
    [SerializeField] private Material matEmpty;    // brown soil
    [SerializeField] private Material matPlanted;  // dark, moist soil
    [SerializeField] private Material matGrowing;  // yellow/green sprout
    [SerializeField] private Material matReady;    // bright green, ready

    // ── Cached Components ────────────────────────────────────────────────
    private Renderer  patchRenderer;  // the mesh renderer on this soil patch
    private Coroutine growCoroutine;  // stored so we can stop it if needed

    // ── Current State ────────────────────────────────────────────────────
    private CropState currentState = CropState.Empty;

    // ────────────────────────────────────────────────────────────────────
    // Awake: cache the Renderer component.
    // Use GetComponentInChildren — Asset Store prefabs almost always put
    // the mesh (and its Renderer) on a child object, not the root.
    // GetComponent would miss it and return null every time.
    // ────────────────────────────────────────────────────────────────────
    void Awake()
    {
        // Search this object AND all children for a Renderer
        patchRenderer = GetComponentInChildren<Renderer>();

        if (patchRenderer == null)
            Debug.LogError($"CropPatch on {name}: No Renderer found on this object or any child!");
    }

    // ────────────────────────────────────────────────────────────────────
    // Start: ensure the patch starts in the Empty visual state
    // ────────────────────────────────────────────────────────────────────
    void Start()
    {
        SetVisualState(CropState.Empty);
    }

    // ────────────────────────────────────────────────────────────────────
    // IInteractable implementation
    // ────────────────────────────────────────────────────────────────────

    public string GetPromptText()
    {
        // Return the right prompt depending on the current state
        return currentState switch
        {
            CropState.Empty   => "Press E to Plant",
            CropState.Planted => "Growing... please wait",
            CropState.Growing => "Almost ready...",
            CropState.Ready   => $"Press E to Harvest {cropName}",
            _                  => ""
        };
    }

    public void Interact(PlayerFarmer player)
    {
        switch (currentState)
        {
            case CropState.Empty:
                Plant();        // start the growing process
                break;

            case CropState.Ready:
                Harvest(player); // give crop to player, reset patch
                break;

            // Planted/Growing states: do nothing — just show the prompt
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // Plant: transition from Empty to Planted, start growing coroutine
    // ────────────────────────────────────────────────────────────────────
    private void Plant()
    {
        currentState = CropState.Planted;
        SetVisualState(CropState.Planted);
        Debug.Log($"{name}: Planted. Growing in {plantedDuration} seconds.");

        // Start the coroutine and store the reference so we could stop it
        growCoroutine = StartCoroutine(GrowSequence());
    }

    // ────────────────────────────────────────────────────────────────────
    // GrowSequence: the coroutine that advances the crop through stages.
    //
    // yield return new WaitForSeconds(t) pauses HERE and resumes after
    // t seconds — Update() and other coroutines keep running normally.
    // ────────────────────────────────────────────────────────────────────
    private IEnumerator GrowSequence()
    {
        // ── Stage 1: Planted → Growing ──────────────────────────────────
        yield return new WaitForSeconds(plantedDuration);

        currentState = CropState.Growing;
        SetVisualState(CropState.Growing);
        Debug.Log($"{name}: Now growing. Ready in {growingDuration} seconds.");

        // ── Stage 2: Growing → Ready ────────────────────────────────────
        yield return new WaitForSeconds(growingDuration);

        currentState = CropState.Ready;
        SetVisualState(CropState.Ready);
        Debug.Log($"{name}: {cropName} is ready to harvest!");
    }

    // ────────────────────────────────────────────────────────────────────
    // Harvest: add crop to inventory, reset patch to Empty
    // ────────────────────────────────────────────────────────────────────
    private void Harvest(PlayerFarmer player)
    {
        Debug.Log($"{name}: Harvested {cropName} for {cropValue} points!");
        player.Inventory.AddCrop(cropValue);

        // Reset the patch back to Empty so it can be planted again
        currentState = CropState.Empty;
        SetVisualState(CropState.Empty);
    }

    // ────────────────────────────────────────────────────────────────────
    // SetVisualState: swap the material to match the current state.
    // This provides instant, visible feedback without any animation.
    // ────────────────────────────────────────────────────────────────────
    private void SetVisualState(CropState state)
    {
        if (patchRenderer == null) return;

        patchRenderer.material = state switch
        {
            CropState.Empty   => matEmpty,
            CropState.Planted => matPlanted,
            CropState.Growing => matGrowing,
            CropState.Ready   => matReady,
            _                  => matEmpty
        };
    }
}
🎨
Creating the four Materials

Project window → right-click → Create → Material. Name them Mat_SoilEmpty, Mat_SoilPlanted, Mat_Growing, Mat_Ready. Set their Albedo colours to brown, dark-brown, yellow, and bright-green respectively. Drag them into the CropPatch Inspector fields.

Script 4 of 6

Chicken.cs

Chickens wander randomly when hungry and stand still after being fed. Feeding requires the player to have a feed bag in their inventory. The chicken re-hungers after 30 seconds via a coroutine.

State Machine

Hungry
wandering
Press E + feed bag
Fed
idle / eating
30 sec coroutine
Hungry
wanders again
4
Chicken.cs
Attach to: each chicken prefab in the scene
MonoBehaviour
Coroutine Animator.SetTrigger IInteractable Random wander Inventory check
Chicken.cs C#
// Chicken.cs
// ─────────────────────────────────────────────────────────────────────────────
// A hungry chicken wanders randomly. The player feeds it by pressing E
// while holding a feed bag. After 30 seconds the chicken gets hungry again.
// ─────────────────────────────────────────────────────────────────────────────

using System.Collections;
using UnityEngine;

public class Chicken : MonoBehaviour, IInteractable
{
    // ── Inspector Settings ───────────────────────────────────────────────
    [SerializeField] private float hungerDuration    = 30f; // seconds before hungry again
    [SerializeField] private float wanderRadius      = 3f;  // how far the chicken strays
    [SerializeField] private float wanderInterval    = 3f;  // seconds between wander moves
    [SerializeField] private float wanderSpeed       = 1.5f;// movement speed while wandering
    [SerializeField] private int   feedScore         = 15;  // score for feeding this chicken

    // ── Cached Components ────────────────────────────────────────────────
    private Animator animator;

    // ── State ────────────────────────────────────────────────────────────
    private bool isHungry = true;
    private Vector3 homePosition; // where the chicken started
    private Vector3 wanderTarget;  // current wander destination

    // ────────────────────────────────────────────────────────────────────
    // Awake: cache the Animator component
    // ────────────────────────────────────────────────────────────────────
    void Awake()
    {
        animator = GetComponent<Animator>();
        homePosition = transform.position;
        wanderTarget = homePosition;
    }

    // ────────────────────────────────────────────────────────────────────
    // Start: begin the wander coroutine immediately
    // ────────────────────────────────────────────────────────────────────
    void Start()
    {
        StartCoroutine(WanderRoutine());
    }

    // ────────────────────────────────────────────────────────────────────
    // Update: move toward the current wander target each frame
    // ────────────────────────────────────────────────────────────────────
    void Update()
    {
        if (!isHungry) return; // fed chickens stand still

        float distToTarget = Vector3.Distance(transform.position, wanderTarget);

        if (distToTarget > 0.1f)
        {
            // Move toward wander target using Time.deltaTime for frame-rate independence
            transform.position = Vector3.MoveTowards(
                transform.position,
                wanderTarget,
                wanderSpeed * Time.deltaTime
            );

            // Face the direction of movement
            Vector3 dir = (wanderTarget - transform.position).normalized;
            if (dir != Vector3.zero)
                transform.rotation = Quaternion.LookRotation(dir);
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // WanderRoutine: every wanderInterval seconds, pick a new random
    // destination within wanderRadius of the home position.
    // ────────────────────────────────────────────────────────────────────
    private IEnumerator WanderRoutine()
    {
        while (true) // loop forever — chicken always wanders when hungry
        {
            yield return new WaitForSeconds(wanderInterval);

            if (isHungry)
            {
                // Pick a random point within the wander radius
                Vector2 randomCircle = Random.insideUnitCircle * wanderRadius;
                wanderTarget = homePosition + new Vector3(randomCircle.x, 0, randomCircle.y);
            }
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // IInteractable implementation
    // ────────────────────────────────────────────────────────────────────

    public string GetPromptText()
    {
        return isHungry
            ? "Press E to Feed Chicken (needs feed bag)"
            : "Chicken is happy!";
    }

    public void Interact(PlayerFarmer player)
    {
        if (!isHungry)
        {
            Debug.Log("This chicken is already fed.");
            return;
        }

        // Attempt to spend a feed bag from the player's inventory
        bool hasFeed = player.Inventory.SpendFeedBag();

        if (!hasFeed)
        {
            Debug.Log("You need a feed bag to feed the chicken. Find one on the farm!");
            return;
        }

        // Feed successful!
        Feed(player);
    }

    // ────────────────────────────────────────────────────────────────────
    // Feed: mark as fed, play animation, start the re-hunger timer
    // ────────────────────────────────────────────────────────────────────
    private void Feed(PlayerFarmer player)
    {
        isHungry = false;

        // Trigger the Eat animation on the Animator Controller
        // The string "Eat" must match the Trigger parameter name exactly.
        if (animator != null)
            animator.SetTrigger("Eat");

        // Award the player score directly via GameManager
        GameManager.Instance.AddScore(feedScore);

        Debug.Log($"Chicken fed! Will get hungry again in {hungerDuration} seconds.");

        // Start the countdown to hunger again
        StartCoroutine(HungerTimer());
    }

    // ────────────────────────────────────────────────────────────────────
    // HungerTimer: after hungerDuration seconds, the chicken is hungry again
    // ────────────────────────────────────────────────────────────────────
    private IEnumerator HungerTimer()
    {
        yield return new WaitForSeconds(hungerDuration);

        isHungry = true;
        Debug.Log($"{name} is hungry again!");
    }
}
🎬
Setting up the Chicken Animator

Create an Animator Controller for the chicken. Add two states: Idle (default) and Eat. Add a Trigger parameter named Eat. Make a transition from Idle → Eat with the condition Eat triggered, and a transition back from Eat → Idle with Has Exit Time = true (so the eating animation plays fully before returning to Idle).

Script 5 of 6

Tree.cs

The tree uses a hold-to-chop mechanic — the player holds E and a progress bar fills up. This introduces Time.deltaTime in a very tangible way, and drives a UI Slider directly from a 3D script.

5
Tree.cs
Attach to: each tree prefab in the scene
MonoBehaviour
Time.deltaTime UI Slider Coroutine IInteractable Destroy() Hold-input pattern
Tree.cs C#
// Tree.cs
// ─────────────────────────────────────────────────────────────────────────────
// The player holds E to chop a tree. A UI Slider fills as chopping progresses.
// When the progress reaches 1.0, the tree falls and is destroyed.
//
// Key concept: Time.deltaTime makes chopping speed frame-rate independent.
// Without it, the chop speed would vary wildly between fast and slow machines.
// ─────────────────────────────────────────────────────────────────────────────

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

public class Tree : MonoBehaviour, IInteractable
{
    // ── Inspector Settings ───────────────────────────────────────────────
    [SerializeField] private float  chopDuration   = 4f;  // seconds to fully chop
    [SerializeField] private int    woodValue      = 20;  // score for chopping this tree
    [SerializeField] private float  fallDuration   = 1.2f;// seconds the tree takes to "fall"
    [SerializeField] private Slider chopProgressBar;    // the UI Slider — drag in from Canvas

    // ── State ────────────────────────────────────────────────────────────
    private float chopProgress    = 0f;    // 0.0 = untouched, 1.0 = fully chopped
    private bool  isBeingChopped  = false; // is the player currently holding E?
    private bool  isFalling       = false; // prevent double-chop once falling
    private PlayerFarmer currentPlayer;    // the player that started chopping

    // ────────────────────────────────────────────────────────────────────
    // Start: initialise the progress bar (it should be hidden at game start)
    // ────────────────────────────────────────────────────────────────────
    void Start()
    {
        if (chopProgressBar != null)
        {
            chopProgressBar.value = 0f;
            chopProgressBar.gameObject.SetActive(false);
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // Update: advance chop progress while E is held (keyboard) OR while
    // the mobile chop button is held (isMobileChopping on PlayerFarmer).
    //
    // chopProgress += Time.deltaTime / chopDuration
    //   → Time.deltaTime is the time since the last frame (e.g. 0.016s at 60fps)
    //   → Dividing by chopDuration normalises it: 1 second of holding = 1/chopDuration
    //   → After chopDuration seconds of holding, chopProgress reaches 1.0
    // ────────────────────────────────────────────────────────────────────
    void Update()
    {
        if (isFalling) return; // already done — stop processing

        // Accept input from keyboard (PC) OR mobile button flag
        bool chopInputHeld = Input.GetKey(KeyCode.E)
                          || (currentPlayer != null && currentPlayer.isMobileChopping);

        if (isBeingChopped)
        {
            if (chopInputHeld)
            {
                // Advance progress (frame-rate independent)
                chopProgress += Time.deltaTime / chopDuration;
                chopProgress = Mathf.Clamp01(chopProgress); // keep between 0 and 1

                // Drive the UI Slider
                if (chopProgressBar != null)
                    chopProgressBar.value = chopProgress;

                // Trigger the fall when fully chopped
                if (chopProgress >= 1f)
                    StartCoroutine(FallAndDestroy());
            }
            else
            {
                // Player released — pause chopping but keep progress
                isBeingChopped = false;
                if (chopProgressBar != null)
                    chopProgressBar.gameObject.SetActive(false);
            }
        }
    }

    // ────────────────────────────────────────────────────────────────────
    // IInteractable implementation
    // ────────────────────────────────────────────────────────────────────

    public string GetPromptText()
    {
        if (isFalling) return "Timber!";
        if (chopProgress > 0f)
            return $"Hold E to continue chopping ({(int)(chopProgress * 100)}%)";
        return "Hold E to Chop Tree";
    }

    public void Interact(PlayerFarmer player)
    {
        if (isFalling) return;

        // Pressing E starts (or resumes) chopping
        // Update() will then check GetKey(E) each frame
        isBeingChopped = true;
        currentPlayer  = player;

        if (chopProgressBar != null)
            chopProgressBar.gameObject.SetActive(true);

        Debug.Log($"{name}: Chopping started ({(int)(chopProgress * 100)}% complete)");
    }

    // ────────────────────────────────────────────────────────────────────
    // FallAndDestroy: tree "falls" (tilts), then is destroyed.
    // We use a coroutine to wait for the visual effect before Destroy().
    // ────────────────────────────────────────────────────────────────────
    private IEnumerator FallAndDestroy()
    {
        isFalling = true;
        isBeingChopped = false;

        // Hide the progress bar
        if (chopProgressBar != null)
            chopProgressBar.gameObject.SetActive(false);

        // Award the player their wood score
        if (currentPlayer != null)
            currentPlayer.Inventory.AddWood(woodValue);

        Debug.Log($"{name}: Timber! Falling over {fallDuration} seconds.");

        // Animate the tree tilting over using rotation over time
        float elapsed = 0f;
        Quaternion startRot = transform.rotation;
        Quaternion endRot   = Quaternion.Euler(0, 0, 90f); // falls sideways

        while (elapsed < fallDuration)
        {
            elapsed += Time.deltaTime;
            transform.rotation = Quaternion.Lerp(startRot, endRot, elapsed / fallDuration);
            yield return null; // wait one frame, then continue the loop
        }

        // Clear the player's interaction target before destroying
        if (currentPlayer != null)
            currentPlayer.ClearNearbyInteractable();

        // Destroy the tree GameObject
        // Note: script execution stops after this call
        Destroy(gameObject);
    }
}
🎚️
Why Time.deltaTime / chopDuration?

If you just wrote chopProgress += 0.01f, the chop speed would depend on the frame rate — fast on a gaming PC, unbearably slow on a laptop. Dividing by chopDuration makes it exactly that many seconds regardless of hardware.

Script 6 of 6

GameManager.cs

The GameManager is a singleton — there is exactly one in the scene, and any script can reach it via GameManager.Instance. It runs the countdown timer, tracks the total score, and shows the game-over screen.

6
GameManager.cs
Attach to: an empty GameObject named "GameManager" in the scene
MonoBehaviour
Singleton pattern Start() Update() TMP_Text UI Game state
GameManager.cs C#
// GameManager.cs
// ─────────────────────────────────────────────────────────────────────────────
// A singleton manager that controls the game session: timer, score, and UI.
// Any script in the scene can call GameManager.Instance.AddScore(n) to
// award points without needing a direct reference to this GameObject.
// ─────────────────────────────────────────────────────────────────────────────

using UnityEngine;
using TMPro;

public class GameManager : MonoBehaviour
{
    // ── Singleton ────────────────────────────────────────────────────────
    // A static property — belongs to the CLASS, not any instance.
    // Set in Awake so it's available before Start() runs anywhere.
    public static GameManager Instance { get; private set; }

    // ── Inspector References ─────────────────────────────────────────────
    [SerializeField] private TMP_Text  scoreText;
    [SerializeField] private TMP_Text  timerText;
    [SerializeField] private GameObject gameOverPanel;
    [SerializeField] private TMP_Text  finalScoreText;

    // ── Game Settings ────────────────────────────────────────────────────
    [SerializeField] private float gameDuration = 120f; // 2 minutes

    // ── Runtime State ────────────────────────────────────────────────────
    private int   totalScore = 0;
    private float timeRemaining;
    private bool  gameIsOver = false;

    // ────────────────────────────────────────────────────────────────────
    // Awake: set up the singleton reference.
    // This runs before any Start(), so Instance is ready for everyone.
    // ────────────────────────────────────────────────────────────────────
    void Awake()
    {
        // If an Instance already exists (e.g. from a previous scene load), destroy this one
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }
        Instance = this;
    }

    // ────────────────────────────────────────────────────────────────────
    // Start: initialise the game session
    // ────────────────────────────────────────────────────────────────────
    void Start()
    {
        timeRemaining = gameDuration;
        totalScore    = 0;
        gameIsOver    = false;

        if (gameOverPanel != null)
            gameOverPanel.SetActive(false);

        UpdateUI();
    }

    // ────────────────────────────────────────────────────────────────────
    // Update: count down the timer each frame
    // ────────────────────────────────────────────────────────────────────
    void Update()
    {
        if (gameIsOver) return;

        timeRemaining -= Time.deltaTime;

        if (timeRemaining <= 0f)
        {
            timeRemaining = 0f;
            EndGame();
        }

        UpdateTimerDisplay();
    }

    // ────────────────────────────────────────────────────────────────────
    // Public API — called by Inventory, Chicken, etc.
    // ────────────────────────────────────────────────────────────────────

    public void AddScore(int points)
    {
        if (gameIsOver) return;
        totalScore += points;
        UpdateScoreDisplay();
        Debug.Log($"Score: {totalScore} (+{points})");
    }

    // ────────────────────────────────────────────────────────────────────
    // UI helpers
    // ────────────────────────────────────────────────────────────────────

    private void UpdateUI()
    {
        UpdateScoreDisplay();
        UpdateTimerDisplay();
    }

    private void UpdateScoreDisplay()
    {
        if (scoreText != null)
            scoreText.text = $"Score: {totalScore}";
    }

    private void UpdateTimerDisplay()
    {
        if (timerText == null) return;

        // Format as MM:SS for readability
        int minutes = (int)(timeRemaining / 60);
        int seconds = (int)(timeRemaining % 60);
        timerText.text = $"{minutes:00}:{seconds:00}";

        // Turn red in the last 30 seconds as a visual warning
        timerText.color = timeRemaining <= 30f
            ? new Color(0.9f, 0.2f, 0.1f)
            : Color.white;
    }

    private void EndGame()
    {
        gameIsOver = true;
        Debug.Log($"Game Over! Final score: {totalScore}");

        if (gameOverPanel != null)
            gameOverPanel.SetActive(true);

        if (finalScoreText != null)
            finalScoreText.text = $"Final Score\n{totalScore}";

        // Pause the game
        Time.timeScale = 0f;
    }
}
🔑
The Singleton Pattern Explained

GameManager.Instance is a static property — it belongs to the class itself, not any object. Any script anywhere in the scene can call GameManager.Instance.AddScore(10) without needing a serialised reference. This is the most common way to access a manager in Unity.

Step 3

Inspector Wiring Checklist

All six scripts are written. Now connect them to the scene through the Inspector.

PlayerArmature

💡
No special collider setup needed on the player

The updated PlayerFarmer uses Physics.OverlapSphere to scan for nearby objects each frame — no trigger collider, no Rigidbody, no OnTriggerEnter. The interactable objects just need any collider (which Asset Store prefabs already have). The only field to fill in the Inspector is the Prompt Text.

Each CropPatch

Each Chicken

Each Tree

FeedBag

🎒
No script needed on the FeedBag

The PlayerFarmer script handles pickup automatically using Physics.OverlapSphere — when the player walks within 1 unit of any object tagged FeedBag, it disappears and the inventory count increases. You only need to ensure the object has a collider and the correct tag.

GameManager

Step 4

Testing Your Game

Test each activity independently before testing the whole game together.

Test 1 — Crop Planting

Test 2 — Feeding Chickens

Test 3 — Chopping Trees

Common Issues

ProblemLikely CauseFix
Press E but nothing happens, no promptPlayerFarmer script not attached, or Prompt Text field emptySelect PlayerArmature → confirm PlayerFarmer component is listed → drag Prompt Text TMP object into its Prompt Text field
Prompt text never showsPrompt Text field not assigned in PlayerFarmer InspectorSelect PlayerArmature → PlayerFarmer component → drag the TMP prompt text object from Canvas into Prompt Text field
"No Renderer found" on CropPatchAsset Store prefab has its mesh on a child object — GetComponent misses itScript now uses GetComponentInChildren — re-copy the updated CropPatch.cs from this tutorial
NullReferenceException on CropPatchMaterial fields not assigned in InspectorDrag all four materials into the CropPatch fields
Chicken doesn't animateAnimator Controller not assigned or "Eat" Trigger name mismatchCheck Animator Controller is on the chicken and Trigger is spelled "Eat" exactly
Progress bar doesn't appearChop Progress Bar field not wired in Tree InspectorDrag the Slider from Canvas into the Tree's Chop Progress Bar field
Score doesn't updateGameManager.Instance is null — GameManager script not in sceneAdd an empty GameObject named GameManager with the GameManager script
Game doesn't pause on endTime.timeScale = 0 won't stop Unscaled coroutinesThis is expected — Time.timeScale pauses physics and Update but not all coroutines
Step 5 — Deployment

📱 Building for Android

Putting your game on an actual Android device is one of the most satisfying moments in this course. Unity handles the heavy lifting — you configure a few settings, add mobile input controls, then build and deploy.

⚠️
Do this before the lab session

Steps 5A and 5B (installing modules and enabling Developer Mode on your phone) can take 10–15 minutes. Do them before you sit down to build.

5A — Install Android Build Support in Unity Hub

  1. 1
    Open Unity Hub

    Go to Installs tab → find your Unity version → click the ⚙️ gear icon next to it → Add Modules.

  2. 2
    Select the Android modules

    Tick: Android Build Support and inside it tick both Android SDK & NDK Tools and OpenJDK. Click Install. This downloads ~1.5 GB — use your home WiFi, not the lab.

  3. 3
    Verify the install

    Reopen Unity. Go to File → Build Settings. You should see Android listed as a platform. If it shows a warning icon, the module is still installing.

5B — Enable Developer Mode on Your Android Device

  1. 1
    Find your Build Number

    Settings → About PhoneSoftware Information → find Build Number. (Location varies by manufacturer — search "enable developer mode" + your phone model if needed.)

  2. 2
    Tap Build Number 7 times

    Each tap shows a countdown. After 7 taps: "You are now a developer!" appears.

  3. 3
    Enable USB Debugging

    Settings → Developer Options (now visible) → turn on USB Debugging.

  4. 4
    Connect via USB and trust the computer

    Plug your phone into your PC/Mac via USB. A prompt appears on the phone: "Allow USB debugging?" → tap Allow. Tick "Always allow from this computer."

5C — Switch Platform to Android in Unity

📦
Texture Compression

In Build Settings, set Texture Compression to ASTC — this gives the best quality/size ratio on modern Android devices (all phones since ~2016 support it).

5D — Add Mobile Input Controls

Keyboard keys don't exist on a touchscreen. You need two things: a virtual joystick for movement and an on-screen Interact button for pressing E.

Virtual Joystick (Movement)

The Starter Assets package already includes a mobile joystick UI. Enable it:

On-Screen Interact Button

Add a button to the Canvas that calls the player's MobileInteract() method:

Hold-to-Chop Button (for Tree)

Tree chopping needs a hold, not a tap. Use the Button's EventTrigger component instead of OnClick:

💡
Why EventTrigger for chop but OnClick for interact?

OnClick() fires once on release — perfect for plant/harvest/feed (single action). PointerDown/PointerUp fires on press and release separately — necessary for chop which needs to know the button is being held.

Show/Hide Mobile Buttons Based on Platform

You want the E-key prompt on PC but the on-screen button on mobile. A simple script handles this:

MobilePlatformUI.cs C# — Attach to Canvas
// MobilePlatformUI.cs
// ─────────────────────────────────────────────────────────────────────────────
// Shows the on-screen buttons only when running on a touch device.
// Attach to the Canvas. Drag the mobile button panel into mobileControlsPanel.
// ─────────────────────────────────────────────────────────────────────────────

using UnityEngine;

public class MobilePlatformUI : MonoBehaviour
{
    [SerializeField] private GameObject mobileControlsPanel; // parent of E button, chop button
    [SerializeField] private GameObject keyboardPromptPanel; // "Press E to ..." text panel

    void Awake()
    {
        // Application.isMobilePlatform is true on Android and iOS at runtime.
        // In the Editor it is false — so keyboard controls stay active while testing.
        bool isMobile = Application.isMobilePlatform;

        if (mobileControlsPanel  != null) mobileControlsPanel.SetActive(isMobile);
        if (keyboardPromptPanel  != null) keyboardPromptPanel.SetActive(!isMobile);
    }
}
🧪
Testing mobile input in the Editor

Install Unity Remote 5 (free on the Play Store / App Store). With your phone connected via USB: Edit → Project Settings → Editor → Device: any Android device. Press Play in Unity — your phone's screen mirrors the game and sends touch input back to Unity in real time. Much faster than building a full APK each time.

5E — Build and Deploy

🔐
If your phone says "Install Blocked"

Go to Settings → Apps → Special Access → Install Unknown Apps → find your file manager or ADB → enable it. This allows installing APKs outside the Play Store.

Common Android Build Errors

ErrorCauseFix
No Android device foundUSB Debugging not enabled, or wrong USB modeOn phone: swipe down → USB notification → select "File Transfer (MTP)" not "Charging only"
SDK not foundAndroid Build Support module installed but SDK path not setEdit → Preferences → External Tools → Android → tick "Unity" for SDK, NDK, JDK
Gradle build failedPackage name has uppercase or spacesPlayer Settings → Other Settings → Package Name must be com.name.game lowercase only
Installation failed (INSTALL_FAILED_UPDATE_INCOMPATIBLE)Old version on phone has different signatureUninstall the old version from your phone, then build again
Game installs but controls don't workMobile buttons not wired to PlayerFarmer, or wrong method selected in OnClickRe-check Inspector wiring for InteractButton and ChopButton EventTrigger components
Screen orientation wrong (portrait instead of landscape)Orientation not set in Player SettingsPlayer Settings → Resolution → Default Orientation → Landscape Left
📱
Your game is on Android!

You've gone from a C# script to a deployed mobile game. This is the complete pipeline every professional Unity developer uses — from prototype to device.

Going Further

Extension Challenges

Finished early? These extensions are ordered from easiest to most challenging.

⭐ DifficultyChallengeNew Concept
Add a second crop type (e.g. Pumpkin) with a different grow time and valueDuplicate prefab, adjust SerializeField values
Display the player's feed count and crop count on the UITMP_Text, referencing Inventory from GameManager
⭐⭐Spawn a new FeedBag every 20 seconds at the farmhouseCoroutine + Instantiate(prefab)
⭐⭐Chickens lay an egg (small sphere) after being fed that the player can collect for bonus pointsCoroutine spawning a prefab at a position
⭐⭐Trees respawn after 60 seconds in the same positionCoroutine on a tree manager, Instantiate at stored position
⭐⭐⭐Add a watering mechanic — crops only grow if watered within the first 5 seconds of plantingSecond interaction type, condition check in GrowSequence
⭐⭐⭐Add a day/night cycle using the Directional Light's rotation — crops grow faster during the dayLighting, time-of-day variable, modifying coroutine wait time
⭐⭐⭐⭐Add a simple shop at the farmhouse — sell harvested crops for gold coins using a UI panelUI Button, ScriptableObject for item data, currency system
🎉
You've completed Farm Life!

You have built a complete game loop with 6 communicating scripts, a shared interface, coroutines, an Animator, a singleton manager, and a UI system — every scripting concept from Week 4 in a single project. Well done!