Kijk Niet Zo Boos – (VR Experience)

Kijk Niet Zo Boos is een VR-ervaring waarin sociale spanning, angst en het vermijden van oogcontact centraal staan. door naar bepaalde personen of dreigingen (zoals ‘Kees’) te kijken, loopt de angst van de speler op. Het dwingt de speler om letterlijk weg te kijken om vooruit te komen, wat een unieke fysieke interactie in Virtual Reality oplevert.

Hoe het werkt

De voortbeweging in Kijk Niet Zo Boos maakt gebruik van een op maat gemaakt Sequentieel Teleportatie Systeem, gebouwd bovenop de standaard Unity XR Interaction Toolkit. Het systeem dwingt de speler om een specifiek pad te volgen en voegt extra gameplay-mechanieken toe aan de voortbeweging.

  • De TeleportProgressService fungeert als de centrale beheerder van de route. Het houdt bij op welk ankerpunt de speler zich bevindt en bepaalt of de speler naar een volgend punt mag bewegen.
  • Teleportatiepunten worden beheerd door een SequentialTeleportAnchorGate. Deze controleert bij de service of het punt actief mag zijn op basis van de huidige voortgang van de speler.
  • Daarnaast zijn er ankers met unieke regels, zoals BreakUntilLookAway. Deze punten worden pas geactiveerd wanneer de speler er niet naar kijkt (een Weeping Angel-achtige mechaniek), wat de spanning en het thema van de game versterkt.

Code highlights

Context: Een gecentraliseerd progressiesysteem gecombineerd met modulaire poorten (gates) en zichtbaarheidsregels (visibility listeners). Snippets zijn ingekort voor leesbaarheid.

1. Centrale Progressie Tracker (TeleportProgressService) Waarom goed: Het houdt de flow van de speler globaal bij via een zero-based index en stuurt events uit wanneer de speler succesvol teleporteert. Het valideert of een stap logisch is (CanAdvanceTo).

				
					public class TeleportProgressService : MonoBehaviour
{
    public static TeleportProgressService Instance { get; private set; }
    public int CurrentIndex { get; private set; } = -1;
    public event Action<int> ProgressChanged;
    public bool CanAdvanceTo(int targetIndex)
    {
        return targetIndex == CurrentIndex + 1;
    }
    public bool TryAdvanceTo(int targetIndex)
    {
        if (!CanAdvanceTo(targetIndex))
            return false;
        CurrentIndex = targetIndex;
        ProgressChanged?.Invoke(CurrentIndex);
        return true;
    }
}
				
			

2. Progressie-Afhankelijke Ankers (SequentialTeleportAnchorGate) Waarom goed: Het breidt de standaard TeleportationAnchor van de XRI Toolkit uit door te luisteren naar de TeleportProgressService. Het schakelt ankers dynamisch in of uit op basis van de huidige flow, waardoor spelers niet per ongeluk stukken kunnen overslaan.

				
					[RequireComponent(typeof(TeleportationAnchor))]
public class SequentialTeleportAnchorGate : MonoBehaviour
{
    [SerializeField] private int _sequenceIndex;
    [SerializeField] private bool _disableVisited = true;
    private TeleportationAnchor _anchor;

    private void OnEnable()
    {
        TeleportProgressService.Instance.ProgressChanged += OnProgressChanged;
        _anchor.teleporting.AddListener(OnTeleporting);
        RefreshState();
    }

    private void RefreshState()
    {
        var progressService = TeleportProgressService.Instance;
        bool isNext = progressService.CanAdvanceTo(_sequenceIndex);
        bool isVisited = _sequenceIndex <= progressService.CurrentIndex;

        // Anker is alleen actief als het de volgende is in de reeks (of als we mogen backtracken)
        bool allowBySequence = isNext || (!_disableVisited && isVisited);
        _anchor.enabled = allowBySequence;
    }

    private void OnTeleporting(TeleportingEventArgs args)
    {
        TeleportProgressService.Instance.TryAdvanceTo(_sequenceIndex);
    }
}
				
			

3. Gameplay Integratie (BreakUntilLookAway) Waarom goed: Het combineert beweging met de thematiek van de game. Een teleportpunt blijft “gebroken” totdat de speler er precies één stap vandaan is én er actief van wegkijkt (getriggerd door een VisibilityListener). Dit dwingt de speler om hun omgeving op een specifieke manier te verkennen.

				
					public class BreakUntilLookAway : MonoBehaviour
{
    [SerializeField] private VisibilityListener _visibilityListener;
    [SerializeField] private int _sequenceIndex;
    private bool _unlocked;

    private void OnEnable()
    {
        _visibilityListener.OnInvisible.AddListener(OnInvisible);
        TeleportProgressService.Instance.ProgressChanged += OnProgressChanged;
    }

    private void OnInvisible()
    {
        if (_unlocked) return;

        // Ontgrendel alleen als de speler precies één anker achter dit anker staat
        if (TeleportProgressService.Instance.CurrentIndex == _sequenceIndex - 1)
        {
            _unlocked = true;
            // Activeer het teleportpunt visueel
        }
    }
}
				
			

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();
    }
}