using UnityEngine; /// /// 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. /// 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(); if (player != null) { _playerTransform = player.transform; _playerCamera = player.GetComponentInChildren(); } 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); } }