OperationStarfall — Pickups & Inventory

een modulair pickups en inventory systeem waarmee het team snel nieuwe power ups kan toevoegen zonder de kerncode aan te passen.


features

  • Direct vs. indirect pickups: sommige effecten treden direct op (bijv. heal), andere worden opgeslagen in de inventory en kun je later gebruiken.
  • Uitbreidbaar met 1 regel: nieuwe pickups voeg je toe via een mapping (enum → effect‑klasse).
  • Veelgebruikte effect typen:
    • Instant effect (bijv. health)
    • Timed effect (automatisch starten/stoppen met timer)
    • Usage‑based effect (maximaal N keer te gebruiken)
  • Ontkoppeld ontwerp: inventory kent geen details van de effecten. Alles verloopt via events.

Health pickup: “Health stijgt direct na oppakken.”

Growth pickup: “Avatar verdubbelt 5s in grootte en keert terug naar normaal.”

Inventory use: “Voorraad gebruiken vanuit inventory (indirect).”

Hoe het werkt

  • Een pickup (CollectPickup) detecteert een botsing met de speler en roept Inventory.AddItem(...) aan.
  • Bij “Direct” effecten: de inventory vuurt het event OnItemActivated af met de naam van het item.
  • Het EffectSystem luistert naar dat event, maakt dynamisch de juiste effect‑component aan (via een mapping), en roept Apply().
  • Effect‑klassen regelen zelf hun leven:
    • HealthEffect voert direct een actie uit en verwijdert zichzelf.
    • TimedEffect start een timer (coroutine), eindigt automatisch en verwijdert zichzelf.
    • UsageEffect telt het aantal gebruiken en verwijdert zichzelf na de laatste keer.

Code highlights

Context: event gedreven, data‑gedreven mapping, en effect klassen die zichzelf beheren (self‑contained). Snippets zijn ingekort voor leesbaarheid.

  1. Event → Dynamisch effect aanmaken (EffectSystem) Waarom goed: effecten registreren is 1 regel in de mapping. Inventory en effecten blijven losjes gekoppeld.
				
					public class EffectSystem : MonoBehaviour
{
    private Inventory _inventory;

    // Enum → Type mapping: voeg hier 1 regel toe voor een nieuwe pickup
    private Dictionary<PickupNames, Type> _itemToEffectMap = new(){
        {PickupNames.HealthEffect, typeof(HealthEffect)},
        {PickupNames.SpeedBoostEffect, typeof(TimedEffect)},
        {PickupNames.ShieldEffect, typeof(UsageEffect)},
        {PickupNames.DamageEffect, typeof(UsageEffect)},
        {PickupNames.TimedEffect, typeof(TimedEffect)},
        {PickupNames.UsageEffect, typeof(UsageEffect)},
        {PickupNames.GrowthEffect, typeof(GrowthEffect)},
    };

    void Start()
    {
        _inventory = GetComponent<Inventory>();
        if (_inventory != null)
            _inventory.OnItemActivated.AddListener(AddEffect);
    }

    void AddEffect(PickupNames targetEffect)
    {
        var effectType = _itemToEffectMap[targetEffect];
        var newEffect = gameObject.AddComponent(effectType) as BaseEffect;
        newEffect?.Apply();
    }
}
				
			
  1. Inventory — direct vs. indirect en voorraadbeheer Waarom goed: duidelijke scheiding tussen “nu toepassen” en “opslaan voor later”, met limieten en nette logging.
				
					public class Inventory : MonoBehaviour
{
    [SerializeField] private int maxItemCount = 10;
    private Dictionary<PickupNames, int> _itemInventory = new();
    public UnityEvent<PickupNames> OnItemActivated;

    public void AddItem(PickupEffectType pickupType, PickupNames itemName, int amount)
    {
        if (pickupType == PickupEffectType.Direct)
        {
            UseItem(itemName);           // direct toepassen
            return;
        }
        StoreItem(itemName, amount);     // anders opslaan
    }

    private void StoreItem(PickupNames itemName, int amount)
    {
        if (_itemInventory.ContainsKey(itemName) && _itemInventory[itemName] >= maxItemCount)
            return;

        _itemInventory[itemName] = GetItemCount(itemName) + amount;
    }

    public bool UseItemFromInventory(PickupNames itemName)
    {
        if (GetItemCount(itemName) <= 0) return false;
        _itemInventory[itemName]--;
        if (_itemInventory[itemName] <= 0) _itemInventory.Remove(itemName);
        UseItem(itemName);
        return true;
    }

    private void UseItem(PickupNames itemName) => OnItemActivated?.Invoke(itemName);
    public int GetItemCount(PickupNames itemName) => _itemInventory.TryGetValue(itemName, out var c) ? c : 0;
}
				
			
  1. TimedEffect — start/stop met coroutine Waarom goed: generiek patroon voor tijdelijke buffs. Herbruikbaar: alleen OnEffectStart/End overschrijven.
				
					public class TimedEffect : BaseEffect
{
    [SerializeField] protected float duration = 5f;
    protected Coroutine timerCoroutine;
    private bool isEffectActive;

    public override void Apply()
    {
        if (isEffectActive) return;
        OnEffectStart();
        timerCoroutine = StartCoroutine(EffectTimer());
    }

    protected virtual void OnEffectStart()
    {
        isEffectActive = true;
        // start logica (bijv. stats verhogen)
    }

    protected virtual void OnEffectEnd()
    {
        isEffectActive = false;
        Remove(); // verwijder component
    }

    private IEnumerator EffectTimer()
    {
        yield return new WaitForSeconds(duration);
        OnEffectEnd();
    }

    public override void Remove()
    {
        if (timerCoroutine != null) StopCoroutine(timerCoroutine);
        base.Remove();
    }
}
				
			

Voorbeeld van een concrete pickup op TimedEffect: GrowthEffect (avatar 2x zo groot voor de duur).

				
					public class GrowthEffect : TimedEffect
{
    private Vector3 originalScale;

    protected override void OnEffectStart()
    {
        base.OnEffectStart();
        originalScale = transform.localScale;
        transform.localScale = originalScale * 2f;
    }

    protected override void OnEffectEnd()
    {
        transform.localScale = originalScale;
        base.OnEffectEnd();
    }
}