FNAF 05 - Main Menu
In this video, we take another big step toward making our project feel like a real horror game by creating the full start menu experience. We’ll begin with a classic FNAF-style warning screen, complete with static noise and flashing text. Then we’ll build the animated start menu itself, featuring functional buttons, creepy background audio, and animatronic faces that flicker in and out on the right side of the screen.
This is the fifth 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:
MainMenuController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class MainMenuController : MonoBehaviour
{
[Header("References")]
public CanvasGroup warningPanel;
public CanvasGroup startMenuPanel;
public CanvasGroup storyPanel;
public GameObject staticPanel;
public GameObject flickerFace;
public AudioSource noise;
public AudioSource music;
[Header("Timing")]
public float warningFadeDuration = 1.5f;
public float warningDisplayTime = 5.0f;
public float storyFadeDuration = 1.5f;
public float storyDisplayTime = 8.0f;
// Start is called before the first frame update
void Start()
{
if(staticPanel != null)
{
staticPanel.SetActive(false);
}
if (startMenuPanel != null)
startMenuPanel.gameObject.SetActive(false);
if (storyPanel != null)
{
storyPanel.alpha = 0.0f;
storyPanel.gameObject.SetActive(true);
}
if (flickerFace != null)
{
flickerFace.gameObject.SetActive(false);
}
StartCoroutine(Co_Warning());
}
public void OnNewGame()
{
SceneManager.LoadSceneAsync("Noise_Test");
//StartCoroutine(Co_PlayStoryAndLoadGame());
}
public void OnExitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
IEnumerator Co_Warning()
{
//Fade In
float t = 0.0f;
while(t < warningFadeDuration)
{
t += Time.unscaledDeltaTime / warningFadeDuration;
warningPanel.alpha = t;
yield return null;
}
warningPanel.alpha = 1.0f;
//Wait
yield return new WaitForSeconds(warningDisplayTime);
//Fade Out
t = 0.0f;
while (t < warningFadeDuration)
{
t += Time.unscaledDeltaTime / warningFadeDuration;
warningPanel.alpha = 1.0f - t;
yield return null;
}
//Turn off the menu and enable the main menu
warningPanel.gameObject.SetActive(false);
startMenuPanel.gameObject.SetActive(true);
flickerFace.SetActive(true);
staticPanel.SetActive(true);
noise.Play();
music.Play();
}
IEnumerator Co_PlayStoryAndLoadGame()
{
if (startMenuPanel != null)
startMenuPanel.interactable = false;
if(storyPanel != null)
{
storyPanel.gameObject.SetActive(true);
//Fade In
float t = 0.0f;
while (t < storyFadeDuration)
{
t += Time.unscaledDeltaTime / storyFadeDuration;
storyPanel.alpha = t;
yield return null;
}
storyPanel.alpha = 1.0f;
}
yield return new WaitForSeconds(storyDisplayTime);
}
}
FlickerFaces.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class FlickerFaces : MonoBehaviour
{
public Image targetImage;
public Sprite baseSprite;
public Sprite[] flashingSprites;
public Vector2 fadeDurationRange = new Vector2(2.0f, 5.0f);
public Vector2 idleDelayRange = new Vector2(1.0f, 3.0f);
public float flashDuration = 0.25f;
Vector3 basePos;
Color imageColor;
int lastFlashIndex = -1;
private void Awake()
{
//Set alpha value to 0.0
if(targetImage)
{
imageColor = targetImage.color;
imageColor.a = 1.0f;
targetImage.color = imageColor;
}
}
private void OnEnable()
{
if(!targetImage)
{
enabled = false;
return;
}
StartCoroutine(Co_Run());
}
IEnumerator Co_Run()
{
while(true)
{
yield return new WaitForSeconds(Random.Range(idleDelayRange.x, idleDelayRange.y));
float fadeTime = Random.Range(fadeDurationRange.x, fadeDurationRange.y);
float alphaFadeValue = Random.Range(0.2f, 0.95f);
int numFades = Random.Range(1, 5);
for(int i = 0; i < numFades; i++)
{
//Base
targetImage.sprite = baseSprite;
yield return FadeTo(alphaFadeValue, fadeTime);
alphaFadeValue = Random.Range(0.2f, 0.95f);
fadeTime = Random.Range(fadeDurationRange.x, fadeDurationRange.y);
}
//Flash
Sprite flash = GetFlashSprite();
targetImage.sprite = flash;
SetTargetAlpha(1.0f);
yield return new WaitForSeconds(flashDuration);
//Base
targetImage.sprite = baseSprite;
}
}
IEnumerator FadeTo(float target, float duration)
{
float start = targetImage.color.a;
float t = 0f;
while (t < duration)
{
t += Time.unscaledDeltaTime;
float a = Mathf.Lerp(start, target, Mathf.Clamp01(t / duration));
SetTargetAlpha(a);
yield return null;
}
SetTargetAlpha(target);
}
void SetTargetAlpha( float alpha)
{
Color temp = targetImage.color;
Color newColor = new Color(temp.r, temp.g, temp.b, alpha);
targetImage.color = newColor;
}
Sprite GetFlashSprite()
{
if (flashingSprites.Length == 1) return flashingSprites[0];
int i = Random.Range(0, flashingSprites.Length);
if (i == lastFlashIndex) i = (i + 1) % flashingSprites.Length;
lastFlashIndex = i;
return flashingSprites[i];
}
}
MenuHoverArrow.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
public class MenuHoverArrow : MonoBehaviour, IPointerEnterHandler
{
public RectTransform arrow;
public Vector2 arrowStartLocation = new Vector2();
public void Start()
{
arrowStartLocation = arrow.transform.position;
}
public void OnPointerEnter(PointerEventData eventData)
{
if (arrow == null) return;
Vector2 targetPos = (transform as RectTransform).position;
arrow.position = new Vector2(arrowStartLocation.x, targetPos.y);
}
}