added dialogue manager and radar
This commit is contained in:
17
Assets/Scripts/DialogueLine.cs
Normal file
17
Assets/Scripts/DialogueLine.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One "page" of dialogue: a speaker name + an array of text lines.
|
||||||
|
/// Fill these out in the Inspector on a DialogueNPC component.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class DialogueLine
|
||||||
|
{
|
||||||
|
[Tooltip("Name shown above the text box. Leave blank to hide the name bar.")]
|
||||||
|
public string speakerName = "???";
|
||||||
|
|
||||||
|
[Tooltip("Each entry is one page of dialogue. Press E to advance.")]
|
||||||
|
[TextArea(2, 6)]
|
||||||
|
public string[] pages = { "..." };
|
||||||
|
}
|
||||||
2
Assets/Scripts/DialogueLine.cs.meta
Normal file
2
Assets/Scripts/DialogueLine.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 414879952353f4d46ae2bc71d8ccdee4
|
||||||
278
Assets/Scripts/DialogueManager.cs
Normal file
278
Assets/Scripts/DialogueManager.cs
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton. Manages the dialogue UI box.
|
||||||
|
/// Attach to any persistent GameObject in the scene (e.g. a "Managers" object),
|
||||||
|
/// or drop it on the Player — it will survive fine either way.
|
||||||
|
///
|
||||||
|
/// Controls:
|
||||||
|
/// E or Space or Enter — advance to next page / close
|
||||||
|
/// Escape — close immediately
|
||||||
|
///
|
||||||
|
/// While dialogue is open:
|
||||||
|
/// - Cursor is unlocked and visible
|
||||||
|
/// - FirstPersonController mouse look is blocked (via IsOpen flag)
|
||||||
|
/// </summary>
|
||||||
|
public class DialogueManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ── Singleton ─────────────────────────────────────────────────────
|
||||||
|
public static DialogueManager Instance { get; private set; }
|
||||||
|
|
||||||
|
// ── Inspector ─────────────────────────────────────────────────────
|
||||||
|
[Header("Box Layout")]
|
||||||
|
[Tooltip("Height of the dialogue box as a fraction of screen height.")]
|
||||||
|
public float boxHeightFraction = 0.22f;
|
||||||
|
public float boxMargin = 24f;
|
||||||
|
public float innerPad = 18f;
|
||||||
|
|
||||||
|
[Header("Colours — Cruelty Squad palette")]
|
||||||
|
public Color colBoxBg = new Color(0.04f, 0.06f, 0.04f, 0.94f);
|
||||||
|
public Color colBoxBorder = new Color(0.18f, 0.75f, 0.22f, 0.90f);
|
||||||
|
public Color colNameBg = new Color(0.10f, 0.28f, 0.10f, 1.00f);
|
||||||
|
public Color colNameText = new Color(0.25f, 1.00f, 0.30f, 1.00f);
|
||||||
|
public Color colBodyText = new Color(0.78f, 0.90f, 0.78f, 1.00f);
|
||||||
|
public Color colHintText = new Color(0.35f, 0.55f, 0.35f, 0.85f);
|
||||||
|
public Color colPageDots = new Color(0.28f, 0.65f, 0.30f, 0.80f);
|
||||||
|
|
||||||
|
[Header("Text")]
|
||||||
|
public int nameFontSize = 13;
|
||||||
|
public int bodyFontSize = 14;
|
||||||
|
public int hintFontSize = 10;
|
||||||
|
|
||||||
|
[Header("Typewriter Effect")]
|
||||||
|
public bool useTypewriter = true;
|
||||||
|
public float charsPerSecond = 40f;
|
||||||
|
|
||||||
|
// ── Public state ──────────────────────────────────────────────────
|
||||||
|
public bool IsOpen { get; private set; }
|
||||||
|
|
||||||
|
// ── Private ───────────────────────────────────────────────────────
|
||||||
|
private DialogueLine[] _lines;
|
||||||
|
private int _lineIndex;
|
||||||
|
private int _pageIndex;
|
||||||
|
|
||||||
|
// Typewriter
|
||||||
|
private float _charTimer;
|
||||||
|
private int _visibleChars;
|
||||||
|
private bool _pageComplete;
|
||||||
|
|
||||||
|
private Texture2D _white;
|
||||||
|
|
||||||
|
// ── Current text shortcuts ─────────────────────────────────────────
|
||||||
|
private string CurrentSpeaker => (_lines != null && _lineIndex < _lines.Length)
|
||||||
|
? _lines[_lineIndex].speakerName : "";
|
||||||
|
private string CurrentPage => (_lines != null && _lineIndex < _lines.Length
|
||||||
|
&& _pageIndex < _lines[_lineIndex].pages.Length)
|
||||||
|
? _lines[_lineIndex].pages[_pageIndex] : "";
|
||||||
|
private int TotalPages => (_lines != null && _lineIndex < _lines.Length)
|
||||||
|
? _lines[_lineIndex].pages.Length : 1;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this) { Destroy(gameObject); return; }
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
_white = Texture2D.whiteTexture;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
public void StartDialogue(DialogueLine[] lines)
|
||||||
|
{
|
||||||
|
if (lines == null || lines.Length == 0) return;
|
||||||
|
|
||||||
|
_lines = lines;
|
||||||
|
_lineIndex = 0;
|
||||||
|
_pageIndex = 0;
|
||||||
|
IsOpen = true;
|
||||||
|
|
||||||
|
BeginPage();
|
||||||
|
|
||||||
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
Cursor.visible = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BeginPage()
|
||||||
|
{
|
||||||
|
_charTimer = 0f;
|
||||||
|
_visibleChars = useTypewriter ? 0 : int.MaxValue;
|
||||||
|
_pageComplete = !useTypewriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!IsOpen) return;
|
||||||
|
|
||||||
|
// Advance typewriter
|
||||||
|
if (useTypewriter && !_pageComplete)
|
||||||
|
{
|
||||||
|
_charTimer += Time.deltaTime;
|
||||||
|
_visibleChars = Mathf.FloorToInt(_charTimer * charsPerSecond);
|
||||||
|
if (_visibleChars >= CurrentPage.Length)
|
||||||
|
{
|
||||||
|
_visibleChars = CurrentPage.Length;
|
||||||
|
_pageComplete = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance or close on E / Space / Return
|
||||||
|
bool advance = Input.GetKeyDown(KeyCode.E)
|
||||||
|
|| Input.GetKeyDown(KeyCode.Space)
|
||||||
|
|| Input.GetKeyDown(KeyCode.Return);
|
||||||
|
bool cancel = Input.GetKeyDown(KeyCode.Escape);
|
||||||
|
|
||||||
|
if (cancel)
|
||||||
|
{
|
||||||
|
CloseDialogue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (advance)
|
||||||
|
{
|
||||||
|
// If typewriter is still running, skip to end of page first
|
||||||
|
if (useTypewriter && !_pageComplete)
|
||||||
|
{
|
||||||
|
_visibleChars = CurrentPage.Length;
|
||||||
|
_pageComplete = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try next page in current line
|
||||||
|
_pageIndex++;
|
||||||
|
if (_pageIndex < TotalPages)
|
||||||
|
{
|
||||||
|
BeginPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try next line
|
||||||
|
_lineIndex++;
|
||||||
|
_pageIndex = 0;
|
||||||
|
if (_lineIndex < _lines.Length)
|
||||||
|
{
|
||||||
|
BeginPage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All done
|
||||||
|
CloseDialogue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseDialogue()
|
||||||
|
{
|
||||||
|
IsOpen = false;
|
||||||
|
_lines = null;
|
||||||
|
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
Cursor.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (!IsOpen || _lines == null) return;
|
||||||
|
|
||||||
|
float sw = Screen.width;
|
||||||
|
float sh = Screen.height;
|
||||||
|
float bh = sh * boxHeightFraction;
|
||||||
|
float bw = sw - boxMargin * 2f;
|
||||||
|
float by = sh - bh - boxMargin;
|
||||||
|
float bx = boxMargin;
|
||||||
|
|
||||||
|
// ── Outer border ─────────────────────────────────────────────
|
||||||
|
float border = 2f;
|
||||||
|
DrawTex(new Rect(bx - border, by - border, bw + border * 2f, bh + border * 2f), colBoxBorder);
|
||||||
|
|
||||||
|
// ── Main background ───────────────────────────────────────────
|
||||||
|
DrawTex(new Rect(bx, by, bw, bh), colBoxBg);
|
||||||
|
|
||||||
|
// ── Left accent stripe ────────────────────────────────────────
|
||||||
|
DrawTex(new Rect(bx, by, 4f, bh), colBoxBorder);
|
||||||
|
|
||||||
|
float cx = bx + innerPad + 6f; // content X (after the stripe + pad)
|
||||||
|
float cy = by + innerPad;
|
||||||
|
float cw = bw - innerPad * 2f - 6f;
|
||||||
|
|
||||||
|
// ── Speaker name bar ──────────────────────────────────────────
|
||||||
|
string speaker = CurrentSpeaker;
|
||||||
|
float nameH = 0f;
|
||||||
|
if (!string.IsNullOrWhiteSpace(speaker))
|
||||||
|
{
|
||||||
|
GUIStyle nameStyle = new GUIStyle();
|
||||||
|
nameStyle.fontSize = nameFontSize;
|
||||||
|
nameStyle.fontStyle = FontStyle.Bold;
|
||||||
|
nameStyle.normal.textColor = colNameText;
|
||||||
|
nameStyle.alignment = TextAnchor.MiddleLeft;
|
||||||
|
|
||||||
|
Vector2 ns = nameStyle.CalcSize(new GUIContent(speaker));
|
||||||
|
nameH = ns.y + 6f;
|
||||||
|
float namePadX = 10f;
|
||||||
|
|
||||||
|
DrawTex(new Rect(cx - 2f, cy, ns.x + namePadX * 2f + 4f, nameH), colNameBg);
|
||||||
|
GUI.Label(new Rect(cx + namePadX, cy + 3f, ns.x + 4f, ns.y), speaker, nameStyle);
|
||||||
|
cy += nameH + 8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Body text (typewriter) ────────────────────────────────────
|
||||||
|
string fullText = CurrentPage;
|
||||||
|
string visibleText = useTypewriter
|
||||||
|
? fullText.Substring(0, Mathf.Min(_visibleChars, fullText.Length))
|
||||||
|
: fullText;
|
||||||
|
|
||||||
|
GUIStyle bodyStyle = new GUIStyle();
|
||||||
|
bodyStyle.fontSize = bodyFontSize;
|
||||||
|
bodyStyle.fontStyle = FontStyle.Normal;
|
||||||
|
bodyStyle.normal.textColor = colBodyText;
|
||||||
|
bodyStyle.wordWrap = true;
|
||||||
|
bodyStyle.richText = true;
|
||||||
|
|
||||||
|
float bodyH = bh - innerPad * 2f - nameH - 8f - 20f; // leave room for hint
|
||||||
|
GUI.Label(new Rect(cx, cy, cw, bodyH), visibleText, bodyStyle);
|
||||||
|
|
||||||
|
// ── Page indicator dots ───────────────────────────────────────
|
||||||
|
int total = TotalPages;
|
||||||
|
if (total > 1)
|
||||||
|
{
|
||||||
|
float dotR = 4f;
|
||||||
|
float dotGap = dotR * 2f + 4f;
|
||||||
|
float dotsW = total * dotGap;
|
||||||
|
float dotX = bx + bw * 0.5f - dotsW * 0.5f;
|
||||||
|
float dotY = by + bh - innerPad - dotR;
|
||||||
|
|
||||||
|
for (int i = 0; i < total; i++)
|
||||||
|
{
|
||||||
|
Color dc = (i == _pageIndex) ? colBoxBorder : colPageDots;
|
||||||
|
DrawTex(new Rect(dotX + i * dotGap, dotY - dotR, dotR * 2f, dotR * 2f), dc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Advance hint ──────────────────────────────────────────────
|
||||||
|
string hintStr = _pageComplete
|
||||||
|
? (_lineIndex >= _lines.Length - 1 && _pageIndex >= TotalPages - 1
|
||||||
|
? "[ E ] Close"
|
||||||
|
: "[ E ] Next")
|
||||||
|
: "[ E ] Skip";
|
||||||
|
|
||||||
|
GUIStyle hintStyle = new GUIStyle();
|
||||||
|
hintStyle.fontSize = hintFontSize;
|
||||||
|
hintStyle.fontStyle = FontStyle.Bold;
|
||||||
|
hintStyle.normal.textColor = colHintText;
|
||||||
|
hintStyle.alignment = TextAnchor.LowerRight;
|
||||||
|
|
||||||
|
GUI.Label(new Rect(bx, by, bw - innerPad, bh - 6f), hintStr, hintStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void DrawTex(Rect r, Color c)
|
||||||
|
{
|
||||||
|
Color prev = GUI.color;
|
||||||
|
GUI.color = c;
|
||||||
|
GUI.DrawTexture(r, _white);
|
||||||
|
GUI.color = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/DialogueManager.cs.meta
Normal file
2
Assets/Scripts/DialogueManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 82597f905b4b351499cb80cf88b7e69e
|
||||||
135
Assets/Scripts/DialogueNPC.cs
Normal file
135
Assets/Scripts/DialogueNPC.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attach this to any world object (NPC, terminal, sign, corpse, etc.)
|
||||||
|
/// to make it interactable. The player presses E when looking at it
|
||||||
|
/// from within interactRange to start the dialogue.
|
||||||
|
///
|
||||||
|
/// The component draws a small world-space "[E]" prompt via OnGUI
|
||||||
|
/// when the player is close enough and looking at the object.
|
||||||
|
/// </summary>
|
||||||
|
public class DialogueNPC : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Dialogue")]
|
||||||
|
public DialogueLine[] lines = new DialogueLine[]
|
||||||
|
{
|
||||||
|
new DialogueLine { speakerName = "STRANGER", pages = new[] { "Hey." } }
|
||||||
|
};
|
||||||
|
|
||||||
|
[Header("Interaction")]
|
||||||
|
[Tooltip("Maximum distance from which the player can interact.")]
|
||||||
|
public float interactRange = 3.5f;
|
||||||
|
[Tooltip("Maximum angle (degrees) between player forward and direction to this object.")]
|
||||||
|
public float interactAngle = 45f;
|
||||||
|
[Tooltip("Layer mask used for the line-of-sight raycast.")]
|
||||||
|
public LayerMask occlusionMask = ~0;
|
||||||
|
|
||||||
|
[Header("Prompt Style")]
|
||||||
|
public string promptText = "[E] Talk";
|
||||||
|
public Color colPrompt = new Color(0.20f, 0.95f, 0.40f, 1f);
|
||||||
|
public Color colPromptBg = new Color(0f, 0f, 0f, 0.70f);
|
||||||
|
|
||||||
|
// ── Private ──────────────────────────────────────────────────────
|
||||||
|
private Transform _playerTransform;
|
||||||
|
private Camera _playerCamera;
|
||||||
|
private bool _promptVisible;
|
||||||
|
private Texture2D _white;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
_white = Texture2D.whiteTexture;
|
||||||
|
|
||||||
|
// Find the player by component
|
||||||
|
var player = FindObjectOfType<FirstPersonController>();
|
||||||
|
if (player != null)
|
||||||
|
{
|
||||||
|
_playerTransform = player.transform;
|
||||||
|
_playerCamera = player.GetComponentInChildren<Camera>();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[DialogueNPC] '{name}': Could not find FirstPersonController in scene.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (_playerTransform == null || DialogueManager.Instance == null) return;
|
||||||
|
if (DialogueManager.Instance.IsOpen) { _promptVisible = false; return; }
|
||||||
|
|
||||||
|
_promptVisible = CanInteract();
|
||||||
|
|
||||||
|
if (_promptVisible && Input.GetKeyDown(KeyCode.E))
|
||||||
|
DialogueManager.Instance.StartDialogue(lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
bool CanInteract()
|
||||||
|
{
|
||||||
|
if (_playerTransform == null || _playerCamera == null) return false;
|
||||||
|
|
||||||
|
// Distance check
|
||||||
|
float dist = Vector3.Distance(_playerTransform.position, transform.position);
|
||||||
|
if (dist > interactRange) return false;
|
||||||
|
|
||||||
|
// Angle check — is the player roughly facing this object?
|
||||||
|
Vector3 dir = (transform.position - _playerCamera.transform.position).normalized;
|
||||||
|
float dot = Vector3.Dot(_playerCamera.transform.forward, dir);
|
||||||
|
if (dot < Mathf.Cos(interactAngle * Mathf.Deg2Rad)) return false;
|
||||||
|
|
||||||
|
// Line-of-sight (optional — fire a ray toward us)
|
||||||
|
if (Physics.Raycast(_playerCamera.transform.position, dir, out RaycastHit hit, interactRange, occlusionMask))
|
||||||
|
{
|
||||||
|
// Allow if the hit object is us or a child of us
|
||||||
|
if (!hit.transform.IsChildOf(transform) && hit.transform != transform)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
if (!_promptVisible || _playerCamera == null) return;
|
||||||
|
|
||||||
|
// Project to screen
|
||||||
|
Vector3 worldPos = transform.position + Vector3.up * 0.5f; // slightly above pivot
|
||||||
|
Vector3 screenPos = _playerCamera.WorldToScreenPoint(worldPos);
|
||||||
|
|
||||||
|
if (screenPos.z <= 0f) return; // behind camera
|
||||||
|
|
||||||
|
// Flip Y (GUI vs screen coords)
|
||||||
|
float sx = screenPos.x;
|
||||||
|
float sy = Screen.height - screenPos.y;
|
||||||
|
|
||||||
|
GUIStyle style = new GUIStyle();
|
||||||
|
style.fontSize = 13;
|
||||||
|
style.fontStyle = FontStyle.Bold;
|
||||||
|
style.normal.textColor = colPrompt;
|
||||||
|
style.alignment = TextAnchor.MiddleCenter;
|
||||||
|
|
||||||
|
Vector2 size = style.CalcSize(new GUIContent(promptText));
|
||||||
|
float padX = 8f;
|
||||||
|
float padY = 4f;
|
||||||
|
float bgW = size.x + padX * 2f;
|
||||||
|
float bgH = size.y + padY * 2f;
|
||||||
|
Rect bgRect = new Rect(sx - bgW * 0.5f, sy - bgH * 0.5f, bgW, bgH);
|
||||||
|
Rect txRect = new Rect(sx - size.x * 0.5f, sy - size.y * 0.5f, size.x, size.y);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
Color prev = GUI.color;
|
||||||
|
GUI.color = colPromptBg;
|
||||||
|
GUI.DrawTexture(bgRect, _white);
|
||||||
|
GUI.color = prev;
|
||||||
|
|
||||||
|
GUI.Label(txRect, promptText, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = new Color(0f, 1f, 0.4f, 0.25f);
|
||||||
|
Gizmos.DrawWireSphere(transform.position, interactRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/DialogueNPC.cs.meta
Normal file
2
Assets/Scripts/DialogueNPC.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c1bad8b87ccb4db4996e0dd891a2224a
|
||||||
@@ -77,9 +77,13 @@ public class FirstPersonController : MonoBehaviour
|
|||||||
controller.Move(new Vector3(0f, velocity.y, 0f) * Time.deltaTime);
|
controller.Move(new Vector3(0f, velocity.y, 0f) * Time.deltaTime);
|
||||||
|
|
||||||
bool inventoryOpen = inventory != null && inventory.IsOpen;
|
bool inventoryOpen = inventory != null && inventory.IsOpen;
|
||||||
if (!inventoryOpen)
|
bool dialogueOpen = DialogueManager.Instance != null && DialogueManager.Instance.IsOpen;
|
||||||
|
if (!inventoryOpen && !dialogueOpen)
|
||||||
HandleMouseLook();
|
HandleMouseLook();
|
||||||
|
|
||||||
|
// Freeze movement during dialogue
|
||||||
|
if (dialogueOpen) return;
|
||||||
|
|
||||||
if (Input.GetKeyDown(KeyCode.Escape))
|
if (Input.GetKeyDown(KeyCode.Escape))
|
||||||
{
|
{
|
||||||
Cursor.lockState = CursorLockMode.None;
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
|||||||
259
Assets/Scripts/RadarHUD.cs
Normal file
259
Assets/Scripts/RadarHUD.cs
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws a circular radar in the top-left corner of the screen.
|
||||||
|
/// Shows enemies (red) and pickups (yellow-green) relative to the player.
|
||||||
|
/// Rotates so "up" on the radar = player's forward direction.
|
||||||
|
///
|
||||||
|
/// Attach to the Player GameObject alongside Player.cs / PlayerHUD.cs.
|
||||||
|
/// Cruelty Squad aesthetic: deliberately ugly, glitchy-looking.
|
||||||
|
/// </summary>
|
||||||
|
public class RadarHUD : MonoBehaviour
|
||||||
|
{
|
||||||
|
// ─── Inspector ────────────────────────────────────────────────────
|
||||||
|
[Header("Layout")]
|
||||||
|
public float edgePadX = 18f;
|
||||||
|
public float edgePadY = 18f;
|
||||||
|
public float radarRadius = 60f; // visual radius of the radar disc (px)
|
||||||
|
public float worldRange = 40f; // world-space units the radar covers
|
||||||
|
|
||||||
|
[Header("Scan")]
|
||||||
|
[Tooltip("How often (seconds) to re-scan the scene for enemies/pickups.")]
|
||||||
|
public float scanInterval = 0.25f;
|
||||||
|
|
||||||
|
[Header("Colours")]
|
||||||
|
public Color colBackground = new Color(0.04f, 0.10f, 0.04f, 0.88f);
|
||||||
|
public Color colRim = new Color(0.18f, 0.70f, 0.22f, 0.90f);
|
||||||
|
public Color colGrid = new Color(0.18f, 0.70f, 0.22f, 0.20f);
|
||||||
|
public Color colForwardTick = new Color(0.18f, 0.70f, 0.22f, 0.80f);
|
||||||
|
public Color colEnemy = new Color(0.95f, 0.12f, 0.12f, 1.00f);
|
||||||
|
public Color colPickup = new Color(0.90f, 0.85f, 0.05f, 1.00f);
|
||||||
|
public Color colNPC = new Color(0.20f, 0.95f, 0.40f, 1.00f);
|
||||||
|
public Color colPlayerDot = new Color(1.00f, 1.00f, 1.00f, 1.00f);
|
||||||
|
|
||||||
|
[Header("Blip Sizes (px)")]
|
||||||
|
public float enemyBlipSize = 5f;
|
||||||
|
public float pickupBlipSize = 4f;
|
||||||
|
public float npcBlipSize = 4f;
|
||||||
|
public float playerDotSize = 5f;
|
||||||
|
|
||||||
|
[Header("Pulse")]
|
||||||
|
[Tooltip("Enemies pulse their blip opacity.")]
|
||||||
|
public float blipPulseSpeed = 4f;
|
||||||
|
|
||||||
|
// ─── Private ──────────────────────────────────────────────────────
|
||||||
|
private Texture2D _white;
|
||||||
|
|
||||||
|
// Cached lists — refreshed on scanInterval
|
||||||
|
private readonly List<Transform> _enemies = new List<Transform>();
|
||||||
|
private readonly List<Transform> _pickups = new List<Transform>();
|
||||||
|
private readonly List<Transform> _npcs = new List<Transform>();
|
||||||
|
private float _nextScan;
|
||||||
|
|
||||||
|
// Centre and radius of the drawn disc in screen pixels (set each OnGUI call)
|
||||||
|
private Vector2 _centre;
|
||||||
|
private float _r;
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
_white = Texture2D.whiteTexture;
|
||||||
|
ScanScene();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (Time.time >= _nextScan)
|
||||||
|
{
|
||||||
|
ScanScene();
|
||||||
|
_nextScan = Time.time + scanInterval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanScene()
|
||||||
|
{
|
||||||
|
_enemies.Clear();
|
||||||
|
foreach (var e in FindObjectsOfType<HumanoidEnemy>())
|
||||||
|
if (e != null) _enemies.Add(e.transform);
|
||||||
|
|
||||||
|
_pickups.Clear();
|
||||||
|
foreach (var p in FindObjectsOfType<PickupItem>())
|
||||||
|
if (p != null) _pickups.Add(p.transform);
|
||||||
|
|
||||||
|
_npcs.Clear();
|
||||||
|
foreach (var n in FindObjectsOfType<DialogueNPC>())
|
||||||
|
if (n != null) _npcs.Add(n.transform);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
_r = radarRadius;
|
||||||
|
// Radar disc centre: top-left corner + padding + radius
|
||||||
|
float cx = edgePadX + _r;
|
||||||
|
float cy = edgePadY + _r;
|
||||||
|
_centre = new Vector2(cx, cy);
|
||||||
|
|
||||||
|
// ── Background disc ──────────────────────────────────────────
|
||||||
|
DrawDisc(cx, cy, _r, colBackground);
|
||||||
|
|
||||||
|
// ── Grid rings (2 concentric) ─────────────────────────────────
|
||||||
|
DrawDiscOutline(cx, cy, _r * 0.5f, colGrid, 1f);
|
||||||
|
|
||||||
|
// ── Crosshair lines (faint) ────────────────────────────────────
|
||||||
|
Color gridCol = colGrid;
|
||||||
|
DrawTex(new Rect(cx - _r, cy - 0.5f, _r * 2f, 1f), gridCol);
|
||||||
|
DrawTex(new Rect(cx - 0.5f, cy - _r, 1f, _r * 2f), gridCol);
|
||||||
|
|
||||||
|
// ── Outer rim ────────────────────────────────────────────────
|
||||||
|
DrawDiscOutline(cx, cy, _r, colRim, 1.5f);
|
||||||
|
|
||||||
|
// ── Player forward tick ────────────────────────────────────────
|
||||||
|
// A small notch at the top of the disc = player's forward
|
||||||
|
DrawTex(new Rect(cx - 1.5f, cy - _r + 1f, 3f, 6f), colForwardTick);
|
||||||
|
|
||||||
|
// ── Blips ─────────────────────────────────────────────────────
|
||||||
|
float yaw = transform.eulerAngles.y; // player yaw (world Y)
|
||||||
|
|
||||||
|
float enemyPulse = Mathf.Lerp(0.45f, 1f,
|
||||||
|
(Mathf.Sin(Time.time * blipPulseSpeed) + 1f) * 0.5f);
|
||||||
|
|
||||||
|
// Pickups first (rendered under everything)
|
||||||
|
foreach (var t in _pickups)
|
||||||
|
{
|
||||||
|
if (t == null) continue;
|
||||||
|
Vector2 blipPos;
|
||||||
|
if (WorldToRadar(t.position, yaw, out blipPos))
|
||||||
|
DrawBlip(blipPos.x, blipPos.y, pickupBlipSize, colPickup, 1f, square: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NPCs — green dots
|
||||||
|
foreach (var t in _npcs)
|
||||||
|
{
|
||||||
|
if (t == null) continue;
|
||||||
|
Vector2 blipPos;
|
||||||
|
if (WorldToRadar(t.position, yaw, out blipPos))
|
||||||
|
DrawBlip(blipPos.x, blipPos.y, npcBlipSize, colNPC, 1f, square: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemies
|
||||||
|
Color enemyDraw = new Color(colEnemy.r, colEnemy.g, colEnemy.b, colEnemy.a * enemyPulse);
|
||||||
|
foreach (var t in _enemies)
|
||||||
|
{
|
||||||
|
if (t == null) continue;
|
||||||
|
Vector2 blipPos;
|
||||||
|
if (WorldToRadar(t.position, yaw, out blipPos))
|
||||||
|
DrawBlip(blipPos.x, blipPos.y, enemyBlipSize, enemyDraw, 1f, square: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player dot (always at centre)
|
||||||
|
DrawBlip(cx, cy, playerDotSize, colPlayerDot, 1f, square: false);
|
||||||
|
|
||||||
|
// ── Label ─────────────────────────────────────────────────────
|
||||||
|
GUIStyle labelStyle = new GUIStyle();
|
||||||
|
labelStyle.fontSize = 8;
|
||||||
|
labelStyle.fontStyle = FontStyle.Bold;
|
||||||
|
labelStyle.normal.textColor = new Color(colRim.r, colRim.g, colRim.b, 0.65f);
|
||||||
|
GUI.Label(new Rect(cx - _r, cy + _r + 3f, _r * 2f, 12f), "RADAR", labelStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Conversion ───────────────────────────────────────────────────
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a world position to a radar screen position.
|
||||||
|
/// Returns false if the point is outside the radar range.
|
||||||
|
/// </summary>
|
||||||
|
bool WorldToRadar(Vector3 worldPos, float playerYawDeg, out Vector2 screenPos)
|
||||||
|
{
|
||||||
|
Vector3 delta = worldPos - transform.position;
|
||||||
|
float dx = delta.x;
|
||||||
|
float dz = delta.z;
|
||||||
|
|
||||||
|
// Rotate so player forward = up on radar
|
||||||
|
float rad = playerYawDeg * Mathf.Deg2Rad;
|
||||||
|
float rx = dx * Mathf.Cos(rad) - dz * Mathf.Sin(rad);
|
||||||
|
float ry = dx * Mathf.Sin(rad) + dz * Mathf.Cos(rad);
|
||||||
|
|
||||||
|
// Normalise to radar pixel space
|
||||||
|
float px = _centre.x + (rx / worldRange) * _r;
|
||||||
|
float py = _centre.y - (ry / worldRange) * _r; // screen Y is flipped
|
||||||
|
|
||||||
|
// Clamp to disc edge if outside range
|
||||||
|
float dist = new Vector2(rx, ry).magnitude;
|
||||||
|
if (dist > worldRange)
|
||||||
|
{
|
||||||
|
// Show as a dim edge blip
|
||||||
|
float ex = _centre.x + (rx / dist) * (_r - 3f);
|
||||||
|
float ey = _centre.y - (ry / dist) * (_r - 3f);
|
||||||
|
screenPos = new Vector2(ex, ey);
|
||||||
|
return true; // still draw, but at edge
|
||||||
|
}
|
||||||
|
|
||||||
|
screenPos = new Vector2(px, py);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Drawing helpers ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
void DrawBlip(float x, float y, float size, Color col, float alpha, bool square)
|
||||||
|
{
|
||||||
|
Color c = new Color(col.r, col.g, col.b, col.a * alpha);
|
||||||
|
float half = size * 0.5f;
|
||||||
|
if (square)
|
||||||
|
{
|
||||||
|
DrawTex(new Rect(x - half, y - half, size, size), c);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Approximate circle with a 3x3 cross pattern
|
||||||
|
DrawTex(new Rect(x - half, y - half * 0.4f, size, size * 0.4f), c);
|
||||||
|
DrawTex(new Rect(x - half * 0.4f, y - half, size * 0.4f, size), c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Draws a filled circle approximated by a square (fast IMGUI).</summary>
|
||||||
|
void DrawDisc(float cx, float cy, float r, Color col)
|
||||||
|
{
|
||||||
|
// Draw as a stack of horizontal lines to get a rough circle
|
||||||
|
int steps = Mathf.CeilToInt(r * 2f);
|
||||||
|
for (int i = 0; i < steps; i++)
|
||||||
|
{
|
||||||
|
float t = (i / (float)steps) * 2f - 1f; // -1 to 1
|
||||||
|
float hw = Mathf.Sqrt(Mathf.Max(0f, 1f - t * t)) * r;
|
||||||
|
float y = cy + t * r;
|
||||||
|
DrawTex(new Rect(cx - hw, y, hw * 2f, 1f), col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Draws a thin circle outline.</summary>
|
||||||
|
void DrawDiscOutline(float cx, float cy, float r, Color col, float thickness)
|
||||||
|
{
|
||||||
|
int segments = Mathf.Max(32, Mathf.CeilToInt(r * 3f));
|
||||||
|
for (int i = 0; i < segments; i++)
|
||||||
|
{
|
||||||
|
float a0 = (i / (float)segments) * Mathf.PI * 2f;
|
||||||
|
float a1 = ((i + 1) / (float)segments) * Mathf.PI * 2f;
|
||||||
|
|
||||||
|
float x0 = cx + Mathf.Cos(a0) * r;
|
||||||
|
float y0 = cy + Mathf.Sin(a0) * r;
|
||||||
|
float x1 = cx + Mathf.Cos(a1) * r;
|
||||||
|
float y1 = cy + Mathf.Sin(a1) * r;
|
||||||
|
|
||||||
|
// Draw a tiny rect along each segment
|
||||||
|
float len = Mathf.Sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
|
||||||
|
if (len < 0.01f) continue;
|
||||||
|
|
||||||
|
DrawTex(new Rect(Mathf.Min(x0, x1) - thickness * 0.5f,
|
||||||
|
Mathf.Min(y0, y1) - thickness * 0.5f,
|
||||||
|
Mathf.Abs(x1 - x0) + thickness,
|
||||||
|
Mathf.Abs(y1 - y0) + thickness), col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawTex(Rect r, Color c)
|
||||||
|
{
|
||||||
|
Color prev = GUI.color;
|
||||||
|
GUI.color = c;
|
||||||
|
GUI.DrawTexture(r, _white);
|
||||||
|
GUI.color = prev;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/RadarHUD.cs.meta
Normal file
2
Assets/Scripts/RadarHUD.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dadd7df554e38bc4b8bd7a86ca5c8d82
|
||||||
73
DEV_NOTES.md
73
DEV_NOTES.md
@@ -38,6 +38,10 @@ Pickups are standalone GameObjects placed anywhere in the scene — **not** chil
|
|||||||
| `WeaponManager` | Player | Instantiates weapon prefabs, handles slot switching |
|
| `WeaponManager` | Player | Instantiates weapon prefabs, handles slot switching |
|
||||||
| `BootsEffect` | Player | Watches inventory for Bunny Hop Boots equipped state, applies stamina boost |
|
| `BootsEffect` | Player | Watches inventory for Bunny Hop Boots equipped state, applies stamina boost |
|
||||||
| `StaminaBoostPickup` | Pickup GameObjects | Legacy — superseded by BootsEffect. Left in for reference. |
|
| `StaminaBoostPickup` | Pickup GameObjects | Legacy — superseded by BootsEffect. Left in for reference. |
|
||||||
|
| `RadarHUD` | Player | Mini-map radar disc (IMGUI), top-left corner — shows enemies, NPCs, pickups |
|
||||||
|
| `DialogueLine` | (data class) | Serializable speaker + pages of text, populated in Inspector on DialogueNPC |
|
||||||
|
| `DialogueNPC` | Any interactable object | World prompt + E-to-talk trigger, references DialogueLine array |
|
||||||
|
| `DialogueManager` | Any persistent GameObject | Singleton — renders the dialogue box, handles input and cursor lock |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -259,7 +263,71 @@ Each weapon prefab gets its own values, so Gun Splat can sit differently from an
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Equippable Items — `BootsEffect`
|
## 9. Radar HUD — `RadarHUD`
|
||||||
|
|
||||||
|
Attach to the **Player** alongside `PlayerHUD`. Draws a circular mini-map in the top-left corner. Rotates so "up" always faces the player's forward direction.
|
||||||
|
|
||||||
|
**Blip legend:**
|
||||||
|
| Blip | Colour | Shape | Source component |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Player | White | Round | (always centre) |
|
||||||
|
| Enemy | Red (pulsing) | Round | `HumanoidEnemy` |
|
||||||
|
| NPC | Green | Round | `DialogueNPC` |
|
||||||
|
| Pickup | Yellow | Square | `PickupItem` |
|
||||||
|
|
||||||
|
Points beyond `worldRange` are clamped to the disc edge so off-screen threats always show a direction.
|
||||||
|
|
||||||
|
**Key Inspector fields:**
|
||||||
|
| Field | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `radarRadius` | Visual size of the disc in pixels (default 60) |
|
||||||
|
| `worldRange` | World-space units covered (default 40) |
|
||||||
|
| `scanInterval` | How often the scene is re-scanned for objects (default 0.25s) |
|
||||||
|
|
||||||
|
No tags or layers required — blips are found by component type.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Dialogue — `DialogueNPC` + `DialogueManager`
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Add `DialogueManager` to any persistent GameObject in the scene (e.g. a "Managers" empty). One instance required per scene.
|
||||||
|
2. Add `DialogueNPC` to any world object you want to be talkable.
|
||||||
|
3. In the Inspector on `DialogueNPC`, expand the `Lines` array and fill in speaker names and pages.
|
||||||
|
|
||||||
|
### `DialogueNPC` Inspector fields
|
||||||
|
| Field | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `lines[]` | Array of `DialogueLine` entries — each has a speaker name + pages of text |
|
||||||
|
| `interactRange` | Max distance for the E prompt to appear (default 3.5 units) |
|
||||||
|
| `interactAngle` | Max degrees off-centre the player can be looking (default 45°) |
|
||||||
|
| `occlusionMask` | Layer mask for the line-of-sight raycast |
|
||||||
|
| `promptText` | Label shown in the world-space prompt bubble (default `[E] Talk`) |
|
||||||
|
|
||||||
|
### Dialogue box controls
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| E / Space / Enter | Advance page (or skip typewriter) |
|
||||||
|
| Escape | Close immediately |
|
||||||
|
|
||||||
|
### `DialogueManager` Inspector fields
|
||||||
|
| Field | Notes |
|
||||||
|
|---|---|
|
||||||
|
| `useTypewriter` | Enable/disable letter-by-letter reveal (default on) |
|
||||||
|
| `charsPerSecond` | Typewriter speed (default 40) |
|
||||||
|
| `boxHeightFraction` | Box height as fraction of screen height (default 0.22) |
|
||||||
|
|
||||||
|
Rich text tags (`<b>`, `<i>`, `<color=red>`) work inside page text — good for glitchy/stylised dialogue.
|
||||||
|
|
||||||
|
While the dialogue box is open, mouse look and movement are frozen and the cursor is unlocked automatically.
|
||||||
|
|
||||||
|
### NPCs on the radar
|
||||||
|
Any GameObject with a `DialogueNPC` component automatically appears as a **green dot** on the `RadarHUD`. No extra tagging needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Equippable Items — `BootsEffect`
|
||||||
|
|
||||||
**Add `BootsEffect` to the Player** for the Bunny Hop Boots to work.
|
**Add `BootsEffect` to the Player** for the Bunny Hop Boots to work.
|
||||||
|
|
||||||
@@ -332,6 +400,7 @@ if (inv.HasItem("Medkit")) {
|
|||||||
- **Starting gun** — any `SimpleGun` that's a child of the Camera at Start gets auto-registered but starts inactive. Player needs to find it as a pickup in the scene.
|
- **Starting gun** — any `SimpleGun` that's a child of the Camera at Start gets auto-registered but starts inactive. Player needs to find it as a pickup in the scene.
|
||||||
- **Inventory is IMGUI** — not Canvas. Performance fine at this scale. All data logic is decoupled from drawing so a Canvas swap later would be straightforward.
|
- **Inventory is IMGUI** — not Canvas. Performance fine at this scale. All data logic is decoupled from drawing so a Canvas swap later would be straightforward.
|
||||||
- **`SimpleGun` assumes it's always active** — non-SimpleGun weapons will slot-switch fine but need their own input handling to shoot.
|
- **`SimpleGun` assumes it's always active** — non-SimpleGun weapons will slot-switch fine but need their own input handling to shoot.
|
||||||
|
- **Dialogue LOS raycast** — uses `occlusionMask` defaulting to `~0` (all layers). If geometry is blocking the prompt unexpectedly, trim the mask on the `DialogueNPC` component.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -345,7 +414,7 @@ if (inv.HasItem("Medkit")) {
|
|||||||
| Left Click | Shoot |
|
| Left Click | Shoot |
|
||||||
| Right Click | Context menu (inventory open) |
|
| Right Click | Context menu (inventory open) |
|
||||||
| R | Reload |
|
| R | Reload |
|
||||||
| E | Pick up nearby item |
|
| E | Pick up nearby item / advance dialogue |
|
||||||
| I | Open / close inventory |
|
| I | Open / close inventory |
|
||||||
| F | Equip selected item |
|
| F | Equip selected item |
|
||||||
| Scroll Wheel | Cycle weapons |
|
| Scroll Wheel | Cycle weapons |
|
||||||
|
|||||||
142
README.md
Normal file
142
README.md
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
# OGG
|
||||||
|
|
||||||
|
A janky boomer shooter built in Unity. Cruelty Squad aesthetics, boomer shooter mechanics. Deliberately ugly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Assets/
|
||||||
|
Scripts/ — all game code
|
||||||
|
Items/ — ItemDefinition ScriptableObjects
|
||||||
|
Models/ — 3D assets
|
||||||
|
Prefabs/ — weapon and enemy prefabs
|
||||||
|
Scenes/ — game scenes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Systems
|
||||||
|
|
||||||
|
### Player
|
||||||
|
|
||||||
|
**`FirstPersonController.cs`**
|
||||||
|
Standard FPS controller. WASD + mouse look, sprint with Left Shift, jump with Space. Mouse look and movement are automatically frozen when the inventory or dialogue is open.
|
||||||
|
|
||||||
|
**`Player.cs`**
|
||||||
|
Tracks health and stamina. Stamina drains while sprinting and regenerates at rest. Exposes `HealthFraction` and `StaminaFraction` for the HUD.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Combat
|
||||||
|
|
||||||
|
**`SimpleGun.cs`** / **`WeaponManager.cs`** / **`WeaponViewmodel.cs`**
|
||||||
|
Weapon firing, switching, and first-person viewmodel rendering. Weapons are defined as prefabs with an `ItemDefinition` ScriptableObject. A custom editor tool automates prefab generation and includes a live positioning workflow for viewmodel placement.
|
||||||
|
|
||||||
|
**`WeaponBob.cs`**
|
||||||
|
Applies positional bob to the viewmodel while moving.
|
||||||
|
|
||||||
|
**`EnemyHealth.cs`** / **`EnemyHeadHitbox.cs`**
|
||||||
|
Enemies take damage from gunfire. Headshots via a dedicated hitbox component are one-shot kills; body shots require multiple hits.
|
||||||
|
|
||||||
|
**`CameraShake.cs`**
|
||||||
|
Triggered on fire and on taking damage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Enemies
|
||||||
|
|
||||||
|
**`HumanoidEnemy.cs`**
|
||||||
|
Procedurally generated humanoid enemies with three types:
|
||||||
|
|
||||||
|
| Type | Role |
|
||||||
|
|---|---|
|
||||||
|
| Grunt | Balanced melee + ranged |
|
||||||
|
| Runner | Fast melee rusher |
|
||||||
|
| Brute | Slow, heavy, high damage |
|
||||||
|
|
||||||
|
Enemies chase, attack in melee range, and shoot at range. AI uses Unity's `CharacterController` with custom state logic (idle → chase → attack).
|
||||||
|
|
||||||
|
**`EnemySpawner.cs`**
|
||||||
|
Spawns enemies into the scene.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Inventory
|
||||||
|
|
||||||
|
**`Inventory.cs`**
|
||||||
|
Grid-based inventory opened with Tab. Supports weapons and equippable non-weapon items. Right-click context menu for equipping/dropping. Pauses mouse look when open.
|
||||||
|
|
||||||
|
**`ItemDefinition.cs`**
|
||||||
|
ScriptableObject defining an item: name, icon, type, weapon prefab reference, and equippable stats.
|
||||||
|
|
||||||
|
**`PickupItem.cs`**
|
||||||
|
Attach to any world object to make it a pickup. Spins and bobs in the world. Supports full `ItemDefinition` or a simple name fallback.
|
||||||
|
|
||||||
|
**`StaminaBoostPickup.cs`** / **`BootsEffect.cs`**
|
||||||
|
"Bunny Hop Boots" equippable item. Grants a passive stamina boost rather than Quake-style bunny hopping — more accessible, still rewarding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### HUD
|
||||||
|
|
||||||
|
**`PlayerHUD.cs`**
|
||||||
|
Draws health and stamina bars in the bottom-left corner using Unity's immediate-mode GUI (`OnGUI`). Bars smooth-interpolate, pulse red at critical values, and have 25% segment tick marks. Includes a colour-coded speedometer (green → yellow → red).
|
||||||
|
|
||||||
|
**`RadarHUD.cs`**
|
||||||
|
Circular mini-map in the top-left corner. Rotates with the player so "up" always equals forward.
|
||||||
|
|
||||||
|
| Blip | Colour | Shape |
|
||||||
|
|---|---|---|
|
||||||
|
| Player | White | Round |
|
||||||
|
| Enemy (`HumanoidEnemy`) | Red (pulsing) | Round |
|
||||||
|
| NPC (`DialogueNPC`) | Green | Round |
|
||||||
|
| Pickup (`PickupItem`) | Yellow | Square |
|
||||||
|
|
||||||
|
Enemies beyond radar range are clamped to the disc edge. Scene is re-scanned every 0.25 seconds.
|
||||||
|
|
||||||
|
**`SimpleCrosshair.cs`**
|
||||||
|
Minimal crosshair overlay.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Dialogue
|
||||||
|
|
||||||
|
**`DialogueNPC.cs`**
|
||||||
|
Attach to any world object (NPC, terminal, sign, etc.) to make it interactable. When the player looks at it within range, a floating `[E] Talk` prompt appears in world space. Press E to open the dialogue box.
|
||||||
|
|
||||||
|
Configurable per object:
|
||||||
|
- `interactRange` — how close the player needs to be
|
||||||
|
- `interactAngle` — how directly they need to be looking
|
||||||
|
- `lines[]` — array of speaker/page blocks (fill in the Inspector)
|
||||||
|
|
||||||
|
**`DialogueManager.cs`** *(singleton)*
|
||||||
|
Renders the dialogue box at the bottom of the screen. Typewriter text effect, page dot indicators, speaker name bar. Unlocks the cursor while open and re-locks it on close.
|
||||||
|
|
||||||
|
Controls while dialogue is open:
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| E / Space / Enter | Advance page (or skip typewriter) |
|
||||||
|
| Escape | Close immediately |
|
||||||
|
|
||||||
|
Rich text tags (`<b>`, `<color=red>`, `<i>`) work in dialogue body text.
|
||||||
|
|
||||||
|
**`DialogueLine.cs`**
|
||||||
|
Serializable data class. One `DialogueLine` = one speaker with an array of text pages. Multiple `DialogueLine` entries per NPC chain speakers together.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Cheatsheet
|
||||||
|
|
||||||
|
| What | Component | Where to attach |
|
||||||
|
|---|---|---|
|
||||||
|
| Player movement | `FirstPersonController` | Player GameObject |
|
||||||
|
| Health / stamina | `Player` | Player GameObject |
|
||||||
|
| HUD bars + speedo | `PlayerHUD` | Player GameObject |
|
||||||
|
| Mini-map radar | `RadarHUD` | Player GameObject |
|
||||||
|
| Dialogue system | `DialogueManager` | Any persistent GameObject (e.g. a "Managers" empty) |
|
||||||
|
| Make something talkable | `DialogueNPC` | The NPC / object |
|
||||||
|
| Make something a pickup | `PickupItem` | The pickup object |
|
||||||
|
| Make something an enemy | `HumanoidEnemy` + `EnemyHealth` | Enemy GameObject |
|
||||||
Reference in New Issue
Block a user