Unity Coin Pickups and Collectibles: Triggers, Score and Effects

Illustration: spinning glowing coins floating above a platform with sparkle particles

Coins, gems, health packs and ammo boxes are some of the first things every game needs. They’re also a great place to learn triggers, events and object pooling. This guide builds Unity coin pickups and collectibles: a trigger-based pickup, a score counter that updates the UI, a bobbing spin animation, sound and particles that don’t get cut off, and pooling for games that spawn lots of them.

Setting Up the Coin

  1. Create the visual: a sprite for 2D or a simple cylinder or model for 3D.
  2. Add a collider (Sphere Collider or CircleCollider2D) and tick Is Trigger. Pickups shouldn’t block movement.
  3. Make sure the player has a Rigidbody (or Rigidbody2D, or a CharacterController in 3D). Trigger messages need a Rigidbody on at least one of the two objects. Coins themselves don’t need one.
  4. Put the player on a layer or give it the Player tag so pickups can tell it apart from enemies.

Save the coin as a prefab so changes apply everywhere.

The Pickup Script

using UnityEngine;

public class Pickup : MonoBehaviour
{
    [SerializeField] int value = 1;
    [SerializeField] AudioClip sound;
    [SerializeField] ParticleSystem burstPrefab;

    bool collected;

    void OnTriggerEnter(Collider other)     => TryCollect(other.gameObject);
    void OnTriggerEnter2D(Collider2D other) => TryCollect(other.gameObject);

    void TryCollect(GameObject who)
    {
        if (collected || !who.CompareTag("Player")) return;
        collected = true;

        ScoreKeeper.Add(value);

        if (sound) AudioSource.PlayClipAtPoint(sound, transform.position);
        if (burstPrefab) Instantiate(burstPrefab, transform.position, Quaternion.identity);

        gameObject.SetActive(false);
    }
}

Important details:

  • The collected flag. A player with two colliders, or a frame where both enter at once, can trigger the method twice. The flag guarantees one pickup per coin.
  • CompareTag is faster than tag == and doesn’t allocate.
  • Both 2D and 3D handlers. Only the one matching your physics will ever be called, so the same script works in both kinds of game.
  • SetActive(false) instead of Destroy, so the object can be pooled or re-enabled on respawn.

A Score Counter With Events

The pickup shouldn’t know about the UI. It tells a score keeper, and the UI listens for changes:

using System;

public static class ScoreKeeper
{
    public static int Coins { get; private set; }
    public static event Action<int> Changed;

    public static void Add(int amount)
    {
        Coins += amount;
        Changed?.Invoke(Coins);
    }

    public static void Reset()
    {
        Coins = 0;
        Changed?.Invoke(Coins);
    }
}

A static class is the simplest option for a small game. Call ScoreKeeper.Reset() when a level starts, especially if you turn off domain reload in Enter Play Mode Options, because static values survive between play sessions then. For a bigger project you might prefer a manager component or a ScriptableObject; the game manager patterns guide compares them.

Showing the Score in the UI

using TMPro;
using UnityEngine;

public class CoinCounterUI : MonoBehaviour
{
    [SerializeField] TMP_Text label;

    void OnEnable()
    {
        ScoreKeeper.Changed += Refresh;
        Refresh(ScoreKeeper.Coins);
    }

    void OnDisable() => ScoreKeeper.Changed -= Refresh;

    void Refresh(int coins) => label.SetText("{0}", coins);
}

Subscribing in OnEnable and unsubscribing in OnDisable is essential with static events. If you forget to unsubscribe, a destroyed UI from a previous scene stays subscribed and throws MissingReferenceException when the next coin is collected. The text only updates when the score changes, not every frame. The TextMeshPro guide covers adding a coin icon inside the text with a sprite asset.

Bob and Spin Animation

A small script makes coins feel alive without an Animator:

using UnityEngine;

public class BobAndSpin : MonoBehaviour
{
    [SerializeField] float bobHeight = 0.15f;
    [SerializeField] float bobSpeed = 2f;
    [SerializeField] float spinSpeed = 120f;

    Vector3 start;
    float offset;

    void OnEnable()
    {
        start = transform.localPosition;
        offset = Random.value * 10f; // so coins don't move in sync
    }

    void Update()
    {
        float y = Mathf.Sin((Time.time + offset) * bobSpeed) * bobHeight;
        transform.localPosition = start + new Vector3(0f, y, 0f);
        transform.Rotate(0f, spinSpeed * Time.deltaTime, 0f, Space.World);
    }
}

Put this on the visual child, not the root, so the trigger collider stays still. In 2D, swap the spin for a sprite animation of a turning coin; the 2D sprite animation guide shows how.

For hundreds of coins on screen, consider doing the bob in a shader or a single manager script, since many small Update calls add up.

Sound and Particles That Don’t Cut Off

A common bug: you put an AudioSource on the coin and call Play(), then disable the coin, and the sound stops instantly. Disabling or destroying an object stops its audio and particles.

  • AudioSource.PlayClipAtPoint spawns a temporary object that plays the clip and cleans itself up. Simple and fine for occasional pickups. It’s always 3D, so for 2D games it may sound quiet; use a central audio player instead.
  • A separate particle prefab spawned at the coin’s position, with Stop Action set to Destroy (or Disable when pooled) in the Main module, so it removes itself.
  • Vary the pitch slightly for repeated coin sounds. Ten identical “ding” sounds in a row grate quickly. A random pitch between 0.95 and 1.1 fixes it.

Different Pickup Types

For health, ammo, keys and power-ups, swap the hardcoded score call for something more flexible. A neat way is an abstract effect the pickup applies:

using UnityEngine;

public abstract class PickupEffect : ScriptableObject
{
    public abstract bool Apply(GameObject collector); // return false to refuse (e.g. full health)
}

[CreateAssetMenu(menuName = "Pickups/Heal")]
public class HealEffect : PickupEffect
{
    [SerializeField] int amount = 25;
    public override bool Apply(GameObject collector)
    {
        var health = collector.GetComponent<Health>();
        if (health == null || health.IsFull) return false;
        health.Heal(amount);
        return true;
    }
}

The pickup then holds a PickupEffect field and calls Apply, only disabling itself when it returns true. Designers create effect assets in the Project window and drag them onto pickups. Your Health component would expose IsFull and Heal; the health bar tutorial has a matching one.

Pooling Dropped Coins

If enemies drop coins or you spawn them in bursts, instantiating and destroying causes garbage collection spikes. Use Unity’s built-in ObjectPool<T>: get a coin from the pool when dropping, and release it back instead of disabling it when collected. The object pooling guide shows the full pattern. Coins placed by hand in a level don’t need pooling.

When coins come out of a pool, reset the collected flag in OnEnable, otherwise reused coins can’t be picked up again.

🔧 Make pickups pop

Particle packs, coin models and juicy feedback tools are cheap on the Asset Store. Here’s our shortlist of Asset Store tools worth buying.

Troubleshooting

OnTriggerEnter not called on the coin

The player needs a Rigidbody or CharacterController, the coin’s collider needs Is Trigger, and 2D and 3D physics can’t be mixed.

Coin counted twice

The player has two colliders, or the method runs twice in a frame. Use a collected flag.

Pickup sound doesn’t play

The AudioSource was on the coin you disabled. Use PlayClipAtPoint or a separate audio manager.

MissingReferenceException in the UI after changing scenes

The UI didn’t unsubscribe from the static event. Unsubscribe in OnDisable.

Score doesn’t reset between plays

Static values persist when domain reload is disabled. Call Reset at level start.

FAQ

Should coins have a Rigidbody?

Static coins don’t need one as long as the player has one. Coins that fall or bounce after dropping need a Rigidbody plus a separate solid collider.

Tag or layer to detect the player?

Either works. CompareTag is simple. Layers let you also filter collisions in the physics matrix so enemies never touch pickups.

How do I make coins fly to the player?

Add a magnet: when the player is within a radius, move the coin toward the player each frame with MoveTowards, and collect on arrival.

How do I save collected coins?

Give each placed coin a unique ID and store collected IDs in your save data, then disable those coins when the level loads.

Is PlayClipAtPoint bad for performance?

It creates and destroys a GameObject each time. Fine for occasional sounds; for rapid pickups use a pooled audio player.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *