Overview

A first-person psychological horror game made in Unity 6. It started as a class project for GDD-211 and turned into something I'm genuinely proud of. I was the only Unity developer on the team — every room, system, script, and interaction is my work.

The game has nine interactable types, multi-state door logic, a dialogue system, an enemy AI with raycast-based defeat detection, two full ending sequences with camera clamping and fade transitions, a ScreenFader singleton used for scene-safe teleportation, and a final cutscene that coordinates six simultaneous systems in one coroutine.

Unity 6 C# ProBuilder Coroutines Raycasting Enemy AI Horror

What I Built

// Interaction system — raycast + enum-based type dispatch
Two scripts power everything the player interacts with. PlayerInteractor fires a raycast each frame, stores a reference to the nearest Interactable, and calls Interact() on key press. Nine InteractableTypes cover every use case in the game.
if (Physics.Raycast(_cam.transform.position, _cam.transform.forward,
    out RaycastHit hit, interactRange, interactLayer))
{
    _current = hit.collider.GetComponent<Interactable>();
    interactPrompt.SetActive(_current != null);
}
if (Input.GetKeyDown(KeyCode.E) && _current != null)
    _current.Interact(this);
// Enemy AI — locked-axis movement, activation gating, defeat via raycast
Timothy moves toward the player on the XZ plane only, smoothly rotating with Slerp. He starts inactive and is unlocked by a progression flag. The gun defeat uses a raycast rather than a physics trigger so it works reliably regardless of his exact position.
void Update()
{
    if (!_active) return;
    Vector3 target = _player.position;
    target.y = transform.position.y;   // locked Y axis
    Vector3 dir = (target - transform.position).normalized;
    transform.rotation = Quaternion.Slerp(transform.rotation,
        Quaternion.LookRotation(dir), rotationSpeed * Time.deltaTime);
    transform.position = Vector3.MoveTowards(
        transform.position, target, chaseSpeed * Time.deltaTime);
}
// ScreenFader singleton — fade-teleport used throughout
Every scene transition, room teleport, and ending fade goes through one singleton. DontDestroyOnLoad keeps it alive across scenes.
public IEnumerator FadeAndTeleport(Transform player, Transform destination)
{
    yield return StartCoroutine(FadeOut());
    player.position = destination.position;
    player.rotation = destination.rotation;
    yield return StartCoroutine(FadeIn());
}
// Final door cutscene — six systems, one coroutine
Spawning the figure, slowing player speed, flickering the lantern, rotating the door, activating Timothy, starting chase music — all timed together via yield WaitForSeconds within a single coroutine chain.
StartCoroutine(FlickerLight(lanternLight, 1.2f));
yield return new WaitForSeconds(0.8f);
StartCoroutine(RotateDoor(finalDoor, -110f, duration: 1.0f));
yield return new WaitForSeconds(1.2f);
_timothyAI.Activate();
AudioManager.Instance.PlayChaseMusic();

Links