Skip to main content

๐Ÿ—ก๏ธ Lesson 7.4: Pickups & Equipping (Mini-Project)

Time to make Module 7 playable. You have an IDamageable contract (7.1), hit detection (7.2), and a data-driven inventory (7.3). This capstone joins them: walk over a glowing pickup and it slides into your inventory grid; equip a sword and your attacks suddenly hit harder; press drop and the item pops back into the world. By the end you'll have a small loop that feels like a real action game.

๐ŸŽฏ Learning Objectives

By the end of this lesson, you will be able to:

  • Build a world pickup that adds its ItemData to the inventory on trigger, then removes itself
  • Handle a full or partial pickup gracefully using Add's returned leftover
  • Model equippable items and apply an equipped weapon's effect (its damage)
  • Feed the equipped weapon's damage into the hitscan attack from Lesson 7.2
  • Implement dropping: remove from the inventory and spawn a pickup back in the world
  • Assemble a complete pick-up โ†’ carry โ†’ equip โ†’ fight loop

Estimated Time: 50 minutes  ยท  Prerequisite: Lessons 7.1โ€“7.3 (the full combat & inventory stack)

In This Lesson

The Loop We're Building

Everything in this lesson is glue between systems you already own. Here is the whole flow at a glance โ€” notice how each arrow lands on a piece from an earlier lesson:

flowchart LR W["World Pickup
(trigger collider + ItemData)"] -- "player enters" --> ADD["Inventory.Add(item, amount)"] ADD -- "all collected?" --> DESTROY["Destroy pickup"] ADD --> PING["OnInventoryChanged โ†’
grid refreshes (7.3)"] PING --> EQUIP["Player equips a weapon
from a slot"] EQUIP --> STAT["Equipment applies
weapon damage"] STAT --> FIRE["HitscanWeapon.Fire()
uses that damage (7.2)"] FIRE --> DMG["IDamageable.TakeDamage() (7.1)"] EQUIP -. "press drop" .-> DROP["Inventory.Remove +
spawn pickup in world"] DROP --> W

Figure 1: The Module 7 loop. Pickups feed the inventory (7.3), equipping sets the damage a hitscan attack (7.2) deals through IDamageable (7.1), and dropping closes the circle.

World Pickups

A pickup is a GameObject in the world carrying an ItemData reference and a trigger collider. When the player walks into it, it adds itself to their inventory and disappears. Because we return the leftover from Add, we can even handle a bag that's almost full.

using UnityEngine;

[RequireComponent(typeof(Collider))]
public class Pickup : MonoBehaviour
{
    [SerializeField] ItemData item;
    [SerializeField] int amount = 1;

    void Reset()
    {
        // convenience: make the collider a trigger the moment you add this
        GetComponent<Collider>().isTrigger = true;
    }

    void OnTriggerEnter(Collider other)
    {
        // only the player picks things up
        if (!other.TryGetComponent<Inventory>(out var inventory)) return;

        int leftover = inventory.Add(item, amount);

        if (leftover == 0)
            Destroy(gameObject);          // fully collected
        else
            amount = leftover;            // bag was full โ€” leave the rest on the ground
    }
}

That is the entire pickup. It knows nothing about the UI, damage, or equipping โ€” it just calls Inventory.Add (Lesson 7.3) and trusts the event to refresh the grid. If the bag couldn't take everything, the pickup keeps the remainder so the player can come back for it. Below is what the moment of pickup looks like: the item leaves the world and its icon lands in the next free slot.

A world pickup entering an inventory slot On the left, a glowing potion pickup sits on the ground with a soft radial glow beneath it. A curved dashed arrow labelled on touch, add to inventory sweeps to the right, where an inventory row of four slots is shown. The first slot is filling with the same potion icon and a small count of one, while the remaining three slots are empty. In the world Health Potion pickup on touch โ†’ Inventory.Add() In the inventory 1
Figure 2: The pickup moment (faithfully recreated). Touching the pickup calls Inventory.Add; the event refreshes the grid and the potion appears in the first free slot.

โš ๏ธ Give the player a Rigidbody, or triggers stay silent

OnTriggerEnter only fires if at least one of the two objects has a Rigidbody. Your player almost certainly has one (or a CharacterController, which also works). If pickups never trigger, that missing Rigidbody is the usual culprit โ€” it's the classic Fundamentals physics gotcha.

Equipping: Items with Effects

Some items don't just sit in the bag โ€” you use them. A weapon changes your damage; armor changes your defense. We extend the data model so an item can describe its effect. The cleanest approach that fits this course: make a WeaponItemData that inherits from ItemData and adds the numbers a weapon needs.

using UnityEngine;

[CreateAssetMenu(fileName = "New Weapon", menuName = "Inventory/Weapon")]
public class WeaponItemData : ItemData
{
    [Header("Weapon")]
    public float damage = 25f;
    public float range = 100f;
}

Because WeaponItemData is an ItemData, it slots into the same inventory with no changes โ€” the potion and the sword live side by side in the grid. When the player equips one, an Equipment component remembers it and applies its stats:

using System;
using UnityEngine;

public class Equipment : MonoBehaviour
{
    public WeaponItemData EquippedWeapon { get; private set; }
    public event Action<WeaponItemData> OnWeaponChanged;

    public void Equip(ItemData item)
    {
        // only weapons can be equipped
        if (item is not WeaponItemData weapon) return;

        EquippedWeapon = weapon;
        OnWeaponChanged?.Invoke(weapon);   // UI / model swap can react
        Debug.Log($"Equipped {weapon.displayName} ({weapon.damage} dmg)");
    }

    public void Unequip()
    {
        EquippedWeapon = null;
        OnWeaponChanged?.Invoke(null);
    }
}

The is not WeaponItemData weapon pattern (C# pattern matching) both checks the type and gives you the typed variable in one line โ€” a potion silently fails to equip, a weapon goes through. Equipping raises OnWeaponChanged so a HUD icon or a visible weapon model can swap, following the same event discipline as the rest of the module.

๐Ÿ“– Definition

Applying an effect: "equipping" simply means some component starts reading a chosen item's data. A weapon's effect is that your attack uses its damage instead of a default. Nothing magic โ€” just a reference the attack consults.

Equipped Weapon Drives Damage

Here is the payoff that ties combat to inventory. In Lesson 7.2 the HitscanWeapon had a fixed damage. Now it asks the Equipment component what's equipped and uses that weapon's numbers โ€” so picking up and equipping a better sword genuinely makes you hit harder.

using UnityEngine;

public class PlayerAttack : MonoBehaviour
{
    [SerializeField] Transform muzzle;
    [SerializeField] LayerMask hittable;
    [SerializeField] Equipment equipment;
    [SerializeField] float unarmedDamage = 5f;   // fists, when nothing equipped

    // call from an Input Action (Module 3), e.g. on Attack performed
    public void Attack()
    {
        var weapon = equipment.EquippedWeapon;
        float damage = weapon != null ? weapon.damage : unarmedDamage;
        float range  = weapon != null ? weapon.range  : 2f;

        if (Physics.Raycast(muzzle.position, muzzle.forward, out RaycastHit hit, range, hittable))
            hit.collider.GetComponent<IDamageable>()?.TakeDamage(damage);   // 7.1 + 7.2
    }
}

Read what just happened: the pickup filled the inventory (7.3), equipping chose a weapon, and the attack pulled that weapon's damage into the exact raycast-plus-IDamageable call from Lessons 7.1 and 7.2. Four lessons, one line of consequence. Unarmed you poke for 5 at 2 units; equip the sword and you swing for 25 at its range.

โœ… Pro Tip

Reading the damage at attack time (rather than copying it into a field when you equip) means swapping weapons is instant and always correct โ€” there's no stale cached value to keep in sync. The equipped item is the single source of truth.

Dropping Items

Dropping closes the loop: remove one from the inventory and spawn a pickup back in the world in front of the player. It reuses Inventory.Remove (7.3) and the same Pickup prefab pickups already use.

using UnityEngine;

public class ItemDropper : MonoBehaviour
{
    [SerializeField] Inventory inventory;
    [SerializeField] Pickup pickupPrefab;   // a generic pickup we configure per-drop
    [SerializeField] Transform dropPoint;   // slightly in front of the player

    public void Drop(ItemData item)
    {
        if (!inventory.Remove(item, 1)) return;   // nothing to drop

        var dropped = Instantiate(pickupPrefab, dropPoint.position, Quaternion.identity);
        dropped.Configure(item, 1);               // small setter on Pickup
    }
}

Add a tiny Configure(ItemData, int) method to Pickup that sets its item and amount so a dropped item becomes a valid world pickup again โ€” walk back over it and it re-enters the bag. That symmetry (drop spawns the very thing pickups consume) is what makes the world feel consistent.

๐Ÿ’ก Save it for later. Because items are ScriptableObjects and slots are [Serializable], this whole inventory is ready for Module 5's JSON save system โ€” you'd store each slot's item id and count, and rebuild the list on load. Data-driven design pays off across modules.

Mini-Project: The Full Loop

Put it all together into a small playable scene. This is the deliverable for Module 7.

  1. Scene setup: a floor, your player (with Inventory, Equipment, PlayerAttack, ItemDropper, and a Rigidbody or CharacterController), and the inventory grid UI from Lesson 7.3.
  2. Author items: a HealthPotion (ItemData) and a Sword (WeaponItemData, 25 damage). Place a few Pickup objects around the floor referencing them.
  3. Targets: drop in a couple of Barrels and an Enemy from Lesson 7.1, each with a Health component and a health bar.
  4. Wire input (Module 3): move the player, an Attack action, an Equip action (equip the first weapon found in the inventory), and a Drop action.
  5. Play the loop: walk over pickups โ†’ they appear in the grid โ†’ equip the sword โ†’ attack a barrel and watch it take 25 (not 5) โ†’ drop the potion and see it reappear on the floor.

๐ŸŽฏ Definition of done

  • Walking over a pickup adds it to the grid and the pickup disappears (or leaves a remainder if the bag is full).
  • Equipping the sword changes your attack's damage from 5 to 25 โ€” verified on a barrel's health bar.
  • Dropping removes the item from the grid and spawns a re-collectable pickup in front of you.
  • No system references another directly against the flow: pickups and inventory talk through Add; UI listens to OnInventoryChanged; the attack reads Equipment.

Hands-on Challenge

๐Ÿ‹๏ธ Exercise 1: Consumables โ€” use the potion

Objective: Close the combat loop by healing.

  1. Add a ConsumableItemData : ItemData with a healAmount field, and make the Health Potion one.
  2. Add a Use input action that, for the selected slot, calls Health.Heal(healAmount) on the player and then Inventory.Remove(item, 1).
  3. Take damage from an enemy, drink a potion, and watch your health bar refill while the potion count drops by one.
๐Ÿ’ก Hint: the potion heals but never leaves the bag

Order matters and both steps must run: heal first, then Remove. If Remove returns false, you had none to use โ€” guard the heal behind a successful count check with CountOf.

โœ… Success check

Using a potion at full health should either be blocked or waste the potion by design (your choice) โ€” but the count and the health bar must always agree with what actually happened.

๐Ÿ‹๏ธ Exercise 2: A weapon rack

Make two weapons โ€” a Dagger (10 dmg, short range) and a Greatsword (40 dmg, longer range) โ€” as WeaponItemData assets and place pickups for both. Confirm that equipping each visibly changes how much damage a barrel takes per hit, with no change to PlayerAttack: the data drives everything. Bonus: show the equipped weapon's name on the HUD by listening to Equipment.OnWeaponChanged.

๐ŸŽฏ Quick Quiz

Question 1: A pickup's OnTriggerEnter never fires when the player walks into it. What's the most likely cause?

Question 2: Why does WeaponItemData inherit from ItemData?

Question 3: Why does PlayerAttack read equipment.EquippedWeapon.damage at the moment of attack rather than caching it on equip?

Summary

๐ŸŽ‰ Key Takeaways

  • A pickup is a trigger collider + ItemData; it calls Inventory.Add and uses the returned leftover to handle a full bag.
  • Equippable items extend ItemData (e.g. WeaponItemData) so they share the same inventory while carrying their own stats.
  • An Equipment component holds the equipped weapon and raises OnWeaponChanged; equipping is just "a component starts reading this item."
  • The attack reads the equipped weapon's damage live, joining inventory (7.3) to the hitscan + IDamageable pipeline (7.1, 7.2).
  • Dropping removes from the inventory and spawns a re-collectable pickup, closing the loop.
  • Every join is through data and events โ€” no system reaches across the flow, so the whole module stays decoupled.

๐Ÿš€ What's Next?

That wraps Module 7 โ€” your game can now hurt things, hold things, and let the player gear up. You've spent two systems-heavy modules (AI, then Combat & Inventory) making games work. Next we make them look the part. In Lesson 8.1: URP Lighting: Real-time & Baked we open the Graphics track: light types, real-time vs. baked lighting, and how URP turns a flat gray scene into one with mood and depth.

๐Ÿ—ก๏ธ A real gameplay loop

Pick up, carry, equip, fight, drop โ€” five lessons of systems working as one. This is what "intermediate" means: not bigger scripts, but pieces that fit together cleanly.