Static, Game Over, & Restart
In this video, we bring the death sequence of FNAF to life by adding animated static, a proper Game Over screen, and a clean restart system. We’ll start by creating noisy texture frames in Photoshop to mimic old VHS static, then import them into Unity and organize them with StaticFrameGroups
and StaticSets
for maximum flexibility. With the StaticNoisePlayer
handling the animation, you’ll see how easy it is to reuse the same static system later for menus and cameras.
Next, we’ll build the GameOverController
that ties everything together: static plays for a few seconds after a jump scare, then fades into a Game Over panel, and finally waits for player input before restarting the scene. Along the way, we’ll also make sure the office background always resets cleanly, so the game feels polished and consistent.
This is the fourth episode in our series where we’re building a complete 2D FNAF-style game from scratch using real programming and logic — no shortcuts, just solid Unity skills.
You can copy and paste the complete code directly from the tutorial below, or…
Just want the Unity Package with everything pre-built? Become a Patreon member at the Creator Level to download the entire Unity package:
StaticFrameGroup.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName ="FNAF/Static/Frame Group", fileName = "SFG_NewGroup")]
public class StaticFrameGroup : ScriptableObject
{
[Tooltip("Individual frames for this group.")]
public Texture2D[] frames;
[Tooltip("Duration of each frame.")]
public float frameDuration = 0.05f;
[Tooltip("Randomize frame order each pass.")]
public bool shuffle = true;
public bool IsValid()
{
return frames != null && frames.Length > 0;
}
}
StaticSet.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "FNAF/Static/Set", fileName = "SS_NewSet")]
public class StaticSet : ScriptableObject
{
[Tooltip("Set name")]
public string setName = "Static Set";
[Tooltip("Order of groups to play.")]
public StaticFrameGroup[] groups;
[Tooltip("Repeat groups?")]
public bool loop = true;
public bool IsValid()
{
if (groups == null || groups.Length == 0)
return false;
foreach( StaticFrameGroup group in groups )
{
if (group == null || !group.IsValid())
return false;
}
return true;
}
}
StaticNoisePlayer.cs
using System.Collections;
using System.Collections.Generic;
using TreeEditor;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
public class StaticNoisePlayer : MonoBehaviour
{
public RawImage targetImage;
public StaticSet set;
public bool playOnEnable = true;
Coroutine playCoroutine;
private void Reset()
{
targetImage = GetComponentInChildren();
}
private void OnEnable()
{
if (playOnEnable && set != null && set.IsValid())
Play();
}
private void OnDisable()
{
Stop();
}
public void Play()
{
Stop();
if (set != null && set.IsValid())
{
playCoroutine = StartCoroutine(Co_Play());
}
}
public void Stop()
{
if(playCoroutine != null)
{
StopCoroutine(playCoroutine);
}
playCoroutine = null;
}
IEnumerator Co_Play()
{
while (true)
{
for (int i = 0; i < set.groups.Length; i++)
{
StaticFrameGroup group = set.groups[i];
int l = group.frames.Length;
if (l == 0) yield break;
int[] order = BuildOrder(l, group.shuffle);
for (int f = 0; f < order.Length; f++)
{
Texture2D tex = group.frames[order[f]];
if (tex != null && targetImage != null)
{
targetImage.texture = tex;
}
yield return new WaitForSeconds(group.frameDuration);
}
}
if (!set.loop)
break;
playCoroutine = null;
}
}
int[] BuildOrder(int length, bool shuffle)
{
var order = new int[length];
for (int i = 0;i < length;i++)
{
order[i] = i;
}
if (!shuffle || length < 2) return order;
for (int i = length -1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
//New in C# 7, kinda like how python unpacks Tuples.
(order[i], order[j]) = (order[j], order[i]);
}
return order;
}
}
GameOverController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class GameOverController : MonoBehaviour
{
[Header("Static")]
public GameObject staticPanel;
public CanvasGroup staticGroup;
public StaticNoisePlayer staticPlayer;
public AudioSource staticAudio;
[Header("Game Over")]
public GameObject gameOverPanel;
public CanvasGroup gameOverGroup;
public RawImage gameOverImage;
[Header("Timing")]
public float staticDuration = 5.0f;
public float fadeDuration = 1.25f;
bool running;
Coroutine flowCoroutine;
private void Awake()
{
if (staticPanel)
staticPanel.SetActive(false);
if (staticGroup)
staticGroup.alpha = 0.0f;
if (gameOverPanel)
gameOverPanel.SetActive(false);
if (gameOverGroup)
gameOverGroup.alpha = 0.0f;
}
public void BeginGameOver()
{
if (running)
return;
running = true;
flowCoroutine = StartCoroutine(Co_GameOver());
}
IEnumerator Co_GameOver()
{
//Bring up static
if (staticPanel)
staticPanel.SetActive(true);
if (staticGroup)
staticGroup.alpha = 1.0f;
if (staticPlayer != null)
staticPlayer.Play();
if (staticAudio)
staticAudio.Play();
//Wait
yield return new WaitForSeconds(staticDuration);
//Game Over Panel
if(gameOverPanel)
gameOverPanel.SetActive(true);
//Cross fade
float t = 0.0f;
while(running && t < fadeDuration)
{
t += Time.unscaledDeltaTime;
float a = Mathf.Clamp01(t / fadeDuration);
staticGroup.alpha = 1.0f - a;
staticAudio.volume = 1.0f - a;
gameOverGroup.alpha = a;
yield return null;
}
staticPlayer.Stop();
staticPanel.SetActive(false);
staticAudio.Stop();
while (!Input.anyKeyDown && !Input.GetMouseButtonDown(0))
yield return null;
ReloadActiveScene();
Cleanup();
}
void ReloadActiveScene()
{
Scene scene = SceneManager.GetActiveScene();
SceneManager.LoadScene(scene.name);
}
void Cleanup()
{
running = false;
flowCoroutine = null;
}
}