FNAF 08 - Animatronics

In this video, we bring the animatronics to life — the moment every FNAF-style game truly begins.
Your characters now move room-to-room on their own, following timed paths that draw them closer to the Security Office one step at a time. Each movement triggers a full reaction across the game: the cameras update, the backgrounds change, and static hides what you were never meant to see.

With ambient audio added to every room, hover sounds on the map, and a new camera-panning system that lets you tilt your view left and right, the building finally feels alive… and watching you back. This episode transforms the cameras from a simple UI into a fully reactive surveillance system — one that players will rely on as the animatronics inch closer in the dark.

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

AnimatronicSystem.cs


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

public class AnimatronicSystem : MonoBehaviour
{
    [Header("Animatronic Jump Scares")]
    [SerializeField] JumpScareSequence pugJumpScare;
    [SerializeField] JumpScareSequence foxyJumpScare;

    //If you end up with many Animatronics, you could use a bitmask to account for every state.
    [Serializable]
    public enum RoomState
    {
        Empty,
        Pug,
        Foxy,
        PugAndFoxy
    }

    [Serializable]
    public enum RoomID
    {
        STAGE,
        HALL,
        SUPPLY,
        WESTDOOR,
        EASTDOOR,
        OFFICE
    }

    public RoomID PugRoomId { get; private set;}
    public RoomID PugNextRoomId { get; private set; }
    public RoomID FoxyRoomId { get; private set; }
    public RoomID FoxyNextRoomId { get; private set; }

    bool pugPaused = false;
    bool foxyPaused = false;

    float _pugTimer;
    float _foxyTimer;
    float _timerVariability = 1.5f;
    float _transitionDuration = 5.0f;
    float _timeToRoomChange = 10.0f; //Time between moves in seconds

    public event Action OnAnimatronicPreparingToMove;
    public event Action OnAnimatronicMoved;
    public event Action OnJumpScare;

    private void Awake()
    {
        PugRoomId = RoomID.STAGE;
        FoxyRoomId = RoomID.STAGE;
        _pugTimer = _timeToRoomChange;
        _foxyTimer = _timeToRoomChange;
    }

    private void Start()
    {
        PowerManager pm = FindFirstObjectByType();
        if(pm != null)
        {
            pm.OnPowerOut += OnPowerOut;
        }
    }

    private void OnPowerOut()
    {
        //Disable the entire thing and shutdown.
        this.gameObject.SetActive(false);
    }

    // Update is called once per frame
    void Update()
    {
        _pugTimer -= Time.deltaTime;
        _foxyTimer -= Time.deltaTime;

        if(!pugPaused && _pugTimer <= 0.0f)
        {  
            CalcPugNextLocation();
            Debug.Log($"Preparing to move Pug to: {PugNextRoomId}");
            OnAnimatronicPreparingToMove.Invoke(PugRoomId);
            pugPaused = true;
            StartCoroutine(CR_UnfreezePug());
         }

        if(!foxyPaused && _foxyTimer <= 0.0f)
        {
            _foxyTimer = _timeToRoomChange + UnityEngine.Random.Range(0.0f, _timerVariability);
            CalcFoxyNextLocation();
            Debug.Log($"Preparing to move Foxy to: {FoxyNextRoomId}");
            OnAnimatronicPreparingToMove.Invoke(FoxyRoomId);
            foxyPaused = true;
            StartCoroutine(CR_UnfreezeFoxy());
        }
    }

    private IEnumerator CR_UnfreezePug()
    {
        yield return new WaitForSeconds(_transitionDuration);
        _pugTimer = _timeToRoomChange + UnityEngine.Random.Range(0.0f, _timerVariability);
        pugPaused = false;
        OnAnimatronicMoved.Invoke(PugRoomId);
        PugRoomId = PugNextRoomId;

        if(PugRoomId == RoomID.OFFICE)
        {
            OnJumpScare.Invoke();
            pugJumpScare.Play();
        }
    }

    private IEnumerator CR_UnfreezeFoxy()
    {
        yield return new WaitForSeconds(_transitionDuration);
        _foxyTimer = _timeToRoomChange + UnityEngine.Random.Range(0.0f, _timerVariability);
        foxyPaused = false;
        OnAnimatronicMoved.Invoke(FoxyRoomId);
        FoxyRoomId = FoxyNextRoomId;

        if(FoxyRoomId == RoomID.OFFICE)
        {
            OnJumpScare.Invoke();
            foxyJumpScare.Play();
        }
    }

    public RoomState GetRoomState(RoomID roomId)
    {
        bool hasPug = (PugRoomId == roomId);
        bool hasFoxy = (FoxyRoomId == roomId);

        if (!hasPug && !hasFoxy) return RoomState.Empty;
        if (hasPug && !hasFoxy) return RoomState.Pug;
        if (!hasPug && hasFoxy) return RoomState.Foxy;
        return RoomState.PugAndFoxy;
    }

    bool CalcPugNextLocation()
    {
        RoomID from = PugRoomId;
        RoomID to = GetNextPugRoom(from);
        if (to == from)
            return false;
        PugNextRoomId = to;
        return true;
    }

    bool CalcFoxyNextLocation()
    {
        RoomID from = FoxyRoomId;
        RoomID to = GetNextFoxyRoom(from);
        if (to == from)
            return false;

        FoxyNextRoomId = to;
        return true;
    }

    void ResetPug()
    {
        PugRoomId = RoomID.STAGE;
    }

    void ResetFoxy()
    {
        FoxyRoomId = RoomID.STAGE;
    }
    
    RoomID GetNextPugRoom(RoomID current)
    {
        switch(current)
        {
            case RoomID.STAGE:
                return RoomID.HALL;
            case RoomID.HALL:
                return UnityEngine.Random.value < 0.5f ? (UnityEngine.Random.value < 0.5f ? RoomID.EASTDOOR : RoomID.WESTDOOR) : RoomID.SUPPLY;
            case RoomID.SUPPLY:
                return RoomID.HALL;
            case RoomID.WESTDOOR:
                return UnityEngine.Random.value < 0.5f ? RoomID.HALL : RoomID.OFFICE;
            case RoomID.EASTDOOR:
                return UnityEngine.Random.value < 0.5f ? RoomID.HALL : RoomID.OFFICE;
            case RoomID.OFFICE:
                return RoomID.OFFICE;
            default:
                return current;
        }
    }

    RoomID GetNextFoxyRoom(RoomID current)
    {
        switch (current)
        {
            case RoomID.STAGE:
                return RoomID.HALL;
            case RoomID.HALL:
                return UnityEngine.Random.value < 0.5f ? (UnityEngine.Random.value < 0.5f ? RoomID.EASTDOOR : RoomID.WESTDOOR) : RoomID.SUPPLY;
            case RoomID.SUPPLY:
                return RoomID.HALL;
            case RoomID.WESTDOOR:
                return UnityEngine.Random.value < 0.5f ? RoomID.HALL : RoomID.OFFICE;
            case RoomID.EASTDOOR:
                return UnityEngine.Random.value < 0.5f ? RoomID.HALL : RoomID.OFFICE;
            case RoomID.OFFICE:
                return RoomID.OFFICE;
            default:
                return current;
        }
    }
}

CameraPanController.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class CameraPanController : MonoBehaviour
{
    [Header("Material Controls")]
    public RawImage monitorImage;                   
    public string shaderProperty = "_camAngle"; 

    [Header("Pan Range (tiny)")]
    public float minPan = -0.005f;       
    public float maxPan = 0.005f;               

    [Header("Pan Behaviour")]
    public float maxPanSpeed = 0.002f;            
    public float edgeWidth = 120f;                
    
    private Material monitorMaterial;
    private float screenWidth;
    private float currentPan;                        // Value sent to shader

    void Start()
    {
        monitorMaterial = monitorImage.material;
        screenWidth = Screen.width;

        currentPan = (minPan + maxPan) * 0.5f;
        monitorMaterial.SetFloat(shaderProperty, currentPan);
    }

    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 = Mathf.Clamp(currentPan, minPan, maxPan);
        monitorMaterial.SetFloat(shaderProperty, currentPan);
    }
}

PowerManager.cs


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

public class PowerManager : MonoBehaviour
{
    public float startingValue = 100.0f;
    public float baseDrainRate = 0.15f;

    private Dictionary activeRates = new Dictionary();
    private bool isRunning = true;
    private bool powerOutageRaised = false;

    public event Action OnPowerOut;
    public event Action OnPowerChanged;
    public event Action OnUsageChanged;

    [SerializeField] private float currentPower;
    [SerializeField] private int currentPowerInteger;

    private void Awake()
    {
        currentPower = Mathf.Clamp(startingValue, 0, 100);
        currentPowerInteger = Mathf.RoundToInt(currentPower);
        CheckPowerIntChange();
    }

    private void Start()
    {
        OnPowerChanged?.Invoke(currentPowerInteger);
        OnUsageChanged?.Invoke(GetUsage());
    }

    // Update is called once per frame
    void Update()
    {
        if(isRunning || !powerOutageRaised)
        {
            //Remove base rate 
            float powerDrawRate = baseDrainRate;

            //Remove drains
            foreach(string name in activeRates.Keys )
            {
                powerDrawRate += activeRates[name];
            }

            currentPower = Mathf.Max(0.0f, currentPower - (powerDrawRate * Time.deltaTime));
            CheckPowerIntChange();

            if (!powerOutageRaised && currentPower <= 0.0f)
            {
                powerOutageRaised = true;
                OnPowerOut?.Invoke();
            }
        }
    }

    public void AddDraw(string id, float drainPerSec)
    {
        if (string.IsNullOrEmpty(id)) return;
        activeRates[id] = drainPerSec;
        OnUsageChanged?.Invoke(GetUsage());
    }

    public int GetUsage()
    {
        int currentUsage = 0;
        //if the system is running, usage is always 1
        if (isRunning)
            currentUsage += 1;

        if (activeRates.Count > 0)
        {
            currentUsage = Mathf.Clamp(currentUsage, currentUsage, 5);
        }
        //Get the number of objects drawing power, max is 5
        return currentUsage;
    }

    public void RemoveDraw(string id)
    {
        if(string.IsNullOrEmpty(id)) return;
        activeRates.Remove(id);
        OnUsageChanged?.Invoke(GetUsage());
    }

    private void CheckPowerIntChange()
    {
        int rounded = Mathf.RoundToInt(currentPower);
        if(rounded != currentPowerInteger)
        {
            currentPowerInteger = rounded;
            OnPowerChanged?.Invoke(currentPowerInteger);
        }
    }

    public void Reset()
    {
        activeRates.Clear();
        powerOutageRaised = false;
        currentPower = Mathf.Clamp(startingValue, 0.0f, 100.0f);
        isRunning = true;
        currentPowerInteger = Mathf.RoundToInt(currentPower);
        OnPowerChanged?.Invoke(currentPowerInteger);
        OnPowerOut?.Invoke();
    }
}

CameraManager.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 UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Collections;
using Unity.VisualScripting;
using static AnimatronicSystem;

public class CameraManager : MonoBehaviour
{
    public RawImage background;
    public Texture2D mainRoom;
    private Material backgroundMaterial;
    private CameraAreaController lastSelected;
    private AnimatronicSystem animatronicSystem;
    [SerializeField]
    private StaticNoisePlayer mapStaticNoisePlayer;
    private Dictionary registeredRooms = new Dictionary();
    private AnimatronicSystem.RoomID currentRoomID;
    [SerializeField]
    private AudioClip mouseOverClip;
    [SerializeField]
    private AudioClip mouseClickClip;
    private AudioSource roomBackgroundAudioSource;

    public void Start()
    {
        currentRoomID = RoomID.OFFICE;
        backgroundMaterial = background.material;
        backgroundMaterial.SetTexture("_background", mainRoom);
        animatronicSystem = FindFirstObjectByType();
        if (animatronicSystem == null)
        {
            Debug.Log("CameraManager: Unable to find Animatronic System in scene.");
        }
        else
        {
            animatronicSystem.OnAnimatronicPreparingToMove += OnAnimatronicPreparingToMove;
            animatronicSystem.OnAnimatronicMoved += OnAnimatronicMoved;
            animatronicSystem.OnJumpScare += OnJumpScare;
        }
        //Search for the static noise player even if it is inactive.
        if (mapStaticNoisePlayer == null)
        {
            Debug.Log("CameraManager: MapStaticNoisePlayer ref not set in scene.");
        }
        roomBackgroundAudioSource = this.GetComponent();
    }

    private void OnJumpScare()
    {
        //Disable static & Reset room to security room
        mapStaticNoisePlayer.gameObject.SetActive(false);
        ResetRoom();
    }

    private void OnAnimatronicPreparingToMove(AnimatronicSystem.RoomID roomID)
    {
        Debug.Log($"CameraManager.OnRoomStateChanged called");
        if(roomID == RoomID.OFFICE)
        {
            //We shouldn't get here. If the animatronic is in the office we should be dead!
            return;
        }
        CameraAreaController areaController = registeredRooms[roomID];
        areaController.isTransitioning = true;

        //Are we currently viewing the room that is transitioning?
        if(currentRoomID == roomID)
        {
            mapStaticNoisePlayer.gameObject.SetActive(true);
        }
        else
        {
            mapStaticNoisePlayer.gameObject.SetActive(false);
        }
    }

    public void OnAnimatronicMoved(AnimatronicSystem.RoomID roomID)
    {
        //Update room visual to show the correct animatronic state
        //Turn off transitioning.
        if (registeredRooms.ContainsKey(roomID))
        {
            registeredRooms[roomID].isTransitioning = false;
        }
    }

    public bool RegisterRoom(CameraAreaController areaController)
    {
        if (registeredRooms.ContainsKey(areaController.roomID))
        {
            Debug.Log($"Duplicate room registered: {areaController.name} : {areaController.roomID}");
            return false;
        }
        registeredRooms.Add(areaController.roomID, areaController);
        return true;
    }

    public void ResetRoom()
    {
        backgroundMaterial.SetTexture("_background", mainRoom);
    }

    public void SetRoomBackground(Texture2D texture)
    {
        backgroundMaterial.SetTexture("_background", texture);
    }

    public void SelectCamera(CameraAreaController selected)
    {
        AudioSource.PlayClipAtPoint(mouseClickClip, Vector3.zero);
        if (selected.roomAudio != null)
        {
            roomBackgroundAudioSource.clip = selected.roomAudio;
            roomBackgroundAudioSource.Play();
        }
        currentRoomID = selected.roomID;
        AnimatronicSystem.RoomState state = animatronicSystem.GetRoomState(currentRoomID);

        if (registeredRooms[currentRoomID].isTransitioning)
        {
            mapStaticNoisePlayer.gameObject.SetActive(true);
        }
        //Make sure static is off for now!
        else
        {
            mapStaticNoisePlayer.gameObject.SetActive(false);
        }

        switch (state)
        {
            case AnimatronicSystem.RoomState.Empty:
                backgroundMaterial.SetTexture("_background", selected.roomEmptySprite);
                break;
            case AnimatronicSystem.RoomState.Pug:
                backgroundMaterial.SetTexture("_background", selected.roomPugSprite);
                break;
            case AnimatronicSystem.RoomState.Foxy:
                backgroundMaterial.SetTexture("_background", selected.roomFoxySprite);
                break;
            case AnimatronicSystem.RoomState.PugAndFoxy:
                backgroundMaterial.SetTexture("_background", selected.roomPugFoxySprite);
                break;
        }

        if (lastSelected != null && lastSelected != selected)
            lastSelected.StopBlinking();

        selected.StartBlinking();
        lastSelected = selected;
    }

    public void OnMouseEnter()
    {
        AudioSource.PlayClipAtPoint(mouseOverClip, Vector3.zero);
    }
}