FNAF 07 - Power Manager
In this video, we build the Power Manager system — the lifeblood of every FNAF-style game. Your power now drains over time, with a working usage meter that shows how much energy each action costs. As the night goes on, every choice you make — opening the map, using the doors, switching lights — brings you closer to one terrifying outcome: the blackout.
When power hits 0%, the screen fades, the lights die, and something starts moving in the dark. This episode combines the Power Manager and the full Blackout sequence, turning the resource system into a real horror mechanic that keeps players watching that meter until the very end.
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 Unity package:
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();
}
}
PowerUI.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PowerUI : MonoBehaviour
{
[Header("References")]
public UnityEngine.UI.Image powerUIPanel;
public PowerManager powerManager;
public TMPro.TMP_Text powerRemainingText;
public UnityEngine.UI.Image[] batteryUsageBars;
// Start is called before the first frame update
void Start()
{ }
private void OnEnable()
{
if (powerManager != null)
{
powerManager.OnPowerChanged += OnPowerChanged;
powerManager.OnUsageChanged += OnUsageChanged;
powerManager.OnPowerOut += OnPowerOut;
}
}
private void OnDisable()
{
if(powerManager != null)
{
powerManager.OnPowerChanged -= OnPowerChanged;
powerManager.OnUsageChanged -= OnUsageChanged;
}
}
void UpdateUsageUI(int usage)
{
for(int i = 0; i < batteryUsageBars.Length; i++)
{
if (i < usage)
batteryUsageBars[i].enabled = true;
else
batteryUsageBars[i].enabled = false;
}
}
void UpdatePowerUI(int power)
{
powerRemainingText.text = $"{power}%";
}
void OnPowerChanged(int power)
{
//Debug.Log("Power changed.");
UpdatePowerUI(power);
}
void OnUsageChanged(int usage)
{
//Debug.Log("Usage changed.");
UpdateUsageUI(usage);
}
void OnPowerOut()
{
powerUIPanel.gameObject.SetActive(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;
[Header("BG Texture References")]
public Texture2D tex_securityRoom_normal;
public Texture2D tex_securityRoom_noPower;
public Texture2D tex_securityRoom_anim_01;
public Texture2D tex_securityRoom_anim_02;
public Texture2D tex_securityRoom_backOut;
[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()
{
powerManager.OnPowerOut += OnPowerOut;
}
// Update is called once per frame
void Update()
{ }
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 null;
//Swap room texture to dark blue
cameraManager.SetRoomBackground(tex_securityRoom_noPower);
as_breaker_switch.Play();
//Open doors (Not implemented yet)
//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();
cameraManager.SetRoomBackground(tex_securityRoom_backOut);
//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;
Texture2D[] swapArray = { tex_securityRoom_noPower, tex_securityRoom_anim_01, tex_securityRoom_anim_02 };
int currentIndex = swapArray.Length - 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, swapArray.Length);
cameraManager.SetRoomBackground(swapArray[currentIndex]);
acc_time += waitTime;
yield return null;
}
//Swap back to off screen.
cameraManager.SetRoomBackground(swapArray[0]);
}
}