FNAF 09 - Animatronics

In this video, we finally complete the Security Office by adding fully functional doors and light controls, turning your scene into something that actually plays like a real FNAF-style game. Instead of rendering hundreds of “every possible combination” frames, we build the room the smart way: render key elements separately, stack them as layered sprites, and drive everything through a clean SecurityRoomManager that handles door states, light toggles, audio, and power draw. We also split the experience into a dedicated Game View and a separate Security Cam View, plus a new no-shader pan controller so your UI stays clickable while still letting the player look left and right.

You can copy and paste the complete code directly from the tutorial below, or…

Just want the full Unity package with everything pre-built?
Become a Patreon member at the Creator Level to download the entire project: https://www.patreon.com/glecakes

More From this Series

CameraPanController_NOSHADER.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CameraPanController_NOSHADER : MonoBehaviour
{
    public RectTransform gameView;                    // The RawImage that shows the room

    [Header("Pan Range")]
    public float minPan = -200;   // Full left
    public float maxPan = 200;    // Full right

    [Header("Pan Behaviour")]
    public float maxPanSpeed = 500f;               // Max units/second toward edge
    public float edgeWidth = 120f;                   // Pixels from screen edge = active zone

    private Vector3 initialPosition; 
    private float screenWidth;
    private float currentPan;                        // Value sent to shader

    void Start()
    {
        screenWidth = Screen.width;
        initialPosition = gameView.transform.localPosition;
        currentPan = 0.0f;
    }

    void Update()
    {
        float mouseX = Input.mousePosition.x;

        bool onLeftEdge = mouseX <= edgeWidth;
        bool onRightEdge = mouseX >= screenWidth - edgeWidth;

        if (onLeftEdge)
        {
            // How deep into the left zone are we? 0 at edge boundary, 1 at very edge.
            float depth = 1f - (mouseX / edgeWidth);  // 0..1
            float step = maxPanSpeed * depth * Time.deltaTime;

            // Gradually move toward minPan
            currentPan = Mathf.MoveTowards(currentPan, minPan, step);
        }
        else if (onRightEdge)
        {
            // How deep into the right zone are we? 0 at edge boundary, 1 at screen edge.
            float distFromRightEdge = screenWidth - mouseX;
            float depth = 1f - (distFromRightEdge / edgeWidth); // 0..1
            float step = maxPanSpeed * depth * Time.deltaTime;

            // Gradually move toward maxPan
            currentPan = Mathf.MoveTowards(currentPan, maxPan, step);
        }
        // If we're not in an edge zone, we do nothing:
        // currentPan stays where it is.

        // Clamp just in case and apply
        currentPan = Mathf.Clamp(currentPan, minPan, maxPan);
        gameView.localPosition = new Vector3(-currentPan, initialPosition.y, initialPosition.z);
    }
}

SecurityRoomManager.cs


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SecurityRoomManager : MonoBehaviour
{
    [SerializeField]
    Animator leftDoorAnimator;
    [SerializeField]
    Animator rightDoorAnimator;

    private bool leftLightOn = false;
    private bool rightLightOn = false;

    [Header("Security Room Images")]
    [SerializeField] UnityEngine.UI.Image bg_LeftLight;
    [SerializeField] UnityEngine.UI.Image bg_RightLight;
    [SerializeField] UnityEngine.UI.Image bg_FoxyLightLeftWindow;
    [SerializeField] UnityEngine.UI.Image bg_FoxyLightRightWindow;
    [SerializeField] UnityEngine.UI.Image foreground;

    [Header("Audio Clips")]
    [SerializeField] AudioSource audioSourceDoors;
    [SerializeField] AudioSource audioSourceLights;
    [SerializeField] AudioClip doorClosing_L;
    [SerializeField] AudioClip doorClosing_R;
    [SerializeField] AudioClip doorOpening_L;
    [SerializeField] AudioClip doorOpening_R;

    [Header("Lights Out Images")]
    [SerializeField] Sprite bg_lo_dark;
    [SerializeField] Sprite[] bg_lo_lightUpSprites;
    [SerializeField] Sprite bg_lo_blackout;

    private AnimatronicSystem animatronicSystem;
    private PowerManager powerManager;

    //Events
    public event Action OnRightDoorOpened;
    public event Action OnRightDoorClosed;
    public event Action OnLeftDoorOpened;
    public event Action OnLeftDoorClosed;

    // Start is called before the first frame update
    void Start()
    {
        animatronicSystem = GameObject.FindFirstObjectByType();
        audioSourceDoors = this.GetComponent();
        powerManager = GameObject.FindFirstObjectByType();
    }
    public bool IsRightDoorOpen()
    {
        return rightDoorAnimator.GetBool("IsOpen");
    }

    public void MakeRoomDark()
    {
        foreground.sprite = bg_lo_dark;
    }

    public void MakeRoomBlackOut()
    {
        foreground.sprite = bg_lo_blackout;
    }

    public int GetLenBright()
    {
        return bg_lo_lightUpSprites.Length;
    }

    public void MakeRoomBright(int value = 0)
    {
        value = Math.Clamp(value,0 , bg_lo_lightUpSprites.Length);
        foreground.sprite = bg_lo_lightUpSprites[value];
    }

    public bool IsLeftDoorOpen()
    {
        return leftDoorAnimator.GetBool("IsOpen");
    }

    public void SetRightBoolOpen()
    {
        rightDoorAnimator.SetBool("IsOpen", true);
    }
    public void SetRightBoolClosed()
    {
        rightDoorAnimator.SetBool("IsOpen", false);
    }

    public void ToggleLeftLight()
    {
        leftLightOn = !leftLightOn;
        if (leftLightOn)
        {
            if (animatronicSystem.PugRoomId == RoomConfig.RoomID.WESTDOOR || animatronicSystem.FoxyRoomId == RoomConfig.RoomID.WESTDOOR)
                bg_FoxyLightLeftWindow.gameObject.SetActive(true);
            else
            {
                bg_LeftLight.gameObject.SetActive(true);
            }
            powerManager.AddDraw("LeftLight", 0.15f);
            audioSourceLights.Play();
        }
        else
        {
            powerManager.RemoveDraw("LeftLight");
            bg_LeftLight.gameObject.SetActive(false);
            bg_FoxyLightLeftWindow.gameObject.SetActive(false);
            audioSourceLights.Stop();
        }
    }

    public void ToggleRightLight()
    {
        rightLightOn = !rightLightOn;
        if (rightLightOn)
        {
            if (animatronicSystem.PugRoomId == RoomConfig.RoomID.EASTDOOR || animatronicSystem.FoxyRoomId == RoomConfig.RoomID.EASTDOOR)
                bg_FoxyLightRightWindow.gameObject.SetActive(true);
            else
            {
                bg_RightLight.gameObject.SetActive(true);
            }
            powerManager.AddDraw("RightLight", 0.15f);
            audioSourceLights.Play();
        }
        else
        {
            powerManager.RemoveDraw("RightLight");
            bg_RightLight.gameObject.SetActive(false);
            bg_FoxyLightRightWindow.gameObject.SetActive(false);
            audioSourceLights.Stop();
        }
    }

      public void ToggleRightDoor()
    {
        if (!rightDoorAnimator.GetBool("Transitioning"))
        {
            rightDoorAnimator.SetTrigger("ToggleDoor");
            rightDoorAnimator.SetBool("Transitioning", true);
            if(rightDoorAnimator.GetBool("IsOpen"))
            {
                powerManager.AddDraw("RightDoor", 0.35f);
                audioSourceDoors.clip = doorClosing_R;
            }
            else
            {
                powerManager.RemoveDraw("RightDoor");
                audioSourceDoors.clip = doorOpening_R;
            }
            audioSourceDoors.Play();
        }
    }

    public void ToggleLeftDoor()
    {
        if (!leftDoorAnimator.GetBool("Transitioning"))
        {
            leftDoorAnimator.SetTrigger("ToggleDoor");
            leftDoorAnimator.SetBool("Transitioning", true);
            if (leftDoorAnimator.GetBool("IsOpen"))
            {
                powerManager.AddDraw("LeftDoor", 0.35f);
                audioSourceDoors.clip = doorClosing_L;
            }
            else
            {
                powerManager.RemoveDraw("LeftDoor");
                audioSourceDoors.clip = doorOpening_L;
            }
            audioSourceDoors.Play();
        }
    }
}

SecurityCamPanController.cs


/*
 * ---------------------------------------------------------------
 *  GLecakes Projects - FNAF Unity Tutorial Series
 *
 *  Created for educational use in building a 2D FNAF-style game.
 *  
 *  Find more tutorials, scripts, and project details at:
 *  Website: https://glecakes.com
 *  Patreon: https://www.patreon.com/glecakes
 *  
 *  Please do not redistribute without attribution.
 * ---------------------------------------------------------------
 */

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class SecurityCamPanController : MonoBehaviour
{
    public RectTransform gameView;                    // The RawImage that shows the room

    [Header("Pan Range")]
    public float minPan = -200;   // Full left
    public float maxPan = 200;    // Full right

    [Header("Pan Behaviour")]
    public bool isStatic = false;
    public float maxPanSpeed = 500f;               // Max units/second toward edge
    public float edgeWidth = 120f;                   // Pixels from screen edge = active zone

    private Vector3 initialPosition; 
    private float screenWidth;
    private float currentPan;                        // Value sent to shader

    void Start()
    {
        screenWidth = Screen.width;
        initialPosition = gameView.transform.localPosition;
        currentPan = 0.0f;
    }

    void Update()
    {
        if (isStatic)
        {
            return;
        }

        float mouseX = Input.mousePosition.x;

        bool onLeftEdge = mouseX <= edgeWidth;
        bool onRightEdge = mouseX >= screenWidth - edgeWidth;

        if (onLeftEdge)
        {
            // How deep into the left zone are we? 0 at edge boundary, 1 at very edge.
            float depth = 1f - (mouseX / edgeWidth);  // 0..1
            float step = maxPanSpeed * depth * Time.deltaTime;

            // Gradually move toward minPan
            currentPan = Mathf.MoveTowards(currentPan, minPan, step);
        }
        else if (onRightEdge)
        {
            // How deep into the right zone are we? 0 at edge boundary, 1 at screen edge.
            float distFromRightEdge = screenWidth - mouseX;
            float depth = 1f - (distFromRightEdge / edgeWidth); // 0..1
            float step = maxPanSpeed * depth * Time.deltaTime;

            // Gradually move toward maxPan
            currentPan = Mathf.MoveTowards(currentPan, maxPan, step);
        }
        // If we're not in an edge zone, we do nothing:
        // currentPan stays where it is.

        // Clamp just in case and apply
        currentPan = Mathf.Clamp(currentPan, minPan, maxPan);
        gameView.localPosition = new Vector3(-currentPan, initialPosition.y, initialPosition.z);
    }
}

RoomConfig.cs


using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "FNAF/Room Config", fileName = "RoomConfig_")]
public class RoomConfig : ScriptableObject
{
    [Serializable]
    public enum RoomID
    {
        STAGE,
        HALL,
        SUPPLY,
        WESTDOOR,
        EASTDOOR,
        OFFICE
    }

    [Header("Room Name")]
    public RoomID roomID;

    [Header("Room Visuals")]
    public Sprite roomEmpty;
    public Sprite roomPug;
    public Sprite roomFoxy;
    public Sprite roomPugFoxy;

    [Header("Camera")]
    public bool isStatic = false;
    public float panSpeed = 150.0f;

    [Header("Ambience")]
    public AudioClip ambientAudioClip;
    public float volume = 1.0f;
}

DoorHelper.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DoorHelper : MonoBehaviour
{
    private Animator doorAnimator;

    // Start is called before the first frame update
    void Start()
    {
        doorAnimator = this.GetComponent();
    }

    public void SetBoolOpen()
    {
        doorAnimator.SetBool("Transitioning", false);
        doorAnimator.SetBool("IsOpen", true);
    }

    public void SetBoolClosed()
    {
        doorAnimator.SetBool("Transitioning", false);
        doorAnimator.SetBool("IsOpen", false);
    }
}

BlackOutController.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BlackOutController : MonoBehaviour
{
    [Header("Managers & Controllers")]
    public CameraManager cameraManager;
    public JumpScareSequence jumpScareSequence;
    public PowerManager powerManager;
    public SecurityRoomManager securityRoomManager;

    [Header("Audio References")]
    public AudioSource as_breaker_switch;
    public AudioSource as_animatronic_long_walk;
    public AudioSource as_generator_slow_die;
    public AudioSource as_power_outage_music;
    public AudioSource as_animatronic_3_last_steps;

    // Start is called before the first frame update
    void Start()
    {
        securityRoomManager = GameObject.FindFirstObjectByType();
        powerManager.OnPowerOut += OnPowerOut;
    }

    public void OnPowerOut()
    {
        StartCoroutine(C_BlackOutSequence());
    }

    IEnumerator C_BlackOutSequence()
    {
        //Wait to allow other things to react to the blackout sequence.
        //This is actually a very poor way to do this, it would be better
        //If we implemented a manager over the room and have its state controlled by a 
        //class that hides all this messiness I'm doing with timing... maybe next time...
        
        yield return new WaitForSeconds(1.0f);
        //Open Doors
        if(!securityRoomManager.IsRightDoorOpen())
        {
            securityRoomManager.ToggleRightDoor();
        }

        if (!securityRoomManager.IsLeftDoorOpen())
        {
            securityRoomManager.ToggleLeftDoor();
        }

        //Force doors open;
        yield return new WaitForSeconds(0.2f);
        //Swap room texture to dark blue
        securityRoomManager.MakeRoomDark();
        as_breaker_switch.Play();

        //Play low-pitched generator whine
        as_generator_slow_die.Play();
        float time_To_footsteps = as_generator_slow_die.clip.length - as_animatronic_long_walk.clip.length;
        yield return new WaitForSeconds(time_To_footsteps);
        //Play footsteps getting closer
        as_animatronic_long_walk.Play();
        yield return new WaitForSeconds(as_animatronic_long_walk.clip.length);
        //Play carnival music
        as_power_outage_music.Play();
        //Flicker face ?In time with music?
        yield return StartCoroutine(RandomFaceFlicker(as_power_outage_music.clip.length - 7.5f));
        yield return new WaitForSeconds(1.5f);
        //Change room texture to dark black
        as_breaker_switch.Play();
        securityRoomManager.MakeRoomBlackOut();
        //Play footsteps
        yield return new WaitForSeconds(1.5f);
        as_animatronic_3_last_steps.Play();
        yield return new WaitForSeconds(as_animatronic_3_last_steps.clip.length);
        //Jumpscare
        jumpScareSequence.Play();
    }

    IEnumerator RandomFaceFlicker(float length)
    {
        float acc_time = 0.0f;
        int currentIndex = securityRoomManager.GetLenBright() - 1;

        while (acc_time < length)
        {
            //Should we swap the face?
            if(currentIndex == 0)
            {
                //50 / 50 chance to hold on dark room
                if(Random.Range(0,2) == 0)
                {
                    acc_time += Time.deltaTime;
                    yield return null;
                }
            }

            //How long to swap?
            float waitTime = Random.Range(0.01f, 0.2f);
            yield return new WaitForSeconds(waitTime);
            //What should we swap to? Equal chance to swap among all options. 
            //Probabilities are : {0.833, 0.16665, 0.16665}
            currentIndex = Random.Range(0, securityRoomManager.GetLenBright());
            securityRoomManager.MakeRoomBright(currentIndex);
            acc_time += waitTime;
            yield return null;
        }
        //Swap back to off screen.
        securityRoomManager.MakeRoomDark();
    }
}