using UnityEngine; using Photon.Pun; using UnityEngine.SceneManagement; using TMPro; using UnityEngine.UI; // ส่วน HUD ของ GameplayManager; partial คือคลาสเดิม ไม่ต้องเพิ่ม Component public partial class GameplayManager { private RectTransform battleHud; private RectTransform shipHudLayer; private sealed class ShipNameplate { public PlayerController ship; public RectTransform root; public TMP_Text name; public Image fill; public TMP_Text emote; public float emoteUntil; } private readonly System.Collections.Generic.Dictionary shipNameplates = new System.Collections.Generic.Dictionary(); private readonly System.Collections.Generic.List staleNameplates = new System.Collections.Generic.List(); private float nextShipHudScan; private void LateUpdate() { if (shipHudLayer == null || !battleHud.gameObject.activeInHierarchy) return; if (Time.unscaledTime >= nextShipHudScan) { nextShipHudScan = Time.unscaledTime + .5f; foreach (var ship in FindObjectsByType(FindObjectsSortMode.None)) { int key = ship.GetInstanceID(); if (shipNameplates.ContainsKey(key)) continue; var plate = SpawnShipNameplateFromTemplate("ShipNameplate_" + key) ?? CreateShipNameplate("ShipNameplate_" + key); plate.ship = ship; shipNameplates[key] = plate; } } var view = Camera.main; if (view == null) { shipHudLayer.gameObject.SetActive(false); return; } shipHudLayer.gameObject.SetActive(true); var canvas = battleHud.GetComponentInParent(); var uiCamera = canvas.renderMode == RenderMode.ScreenSpaceOverlay ? null : canvas.worldCamera; staleNameplates.Clear(); foreach (var entry in shipNameplates) { var plate = entry.Value; if (plate.ship == null) { Destroy(plate.root.gameObject); staleNameplates.Add(entry.Key); continue; } Vector3 center = view.WorldToViewportPoint(plate.ship.transform.position); bool visible = !plate.ship.isDead && !plate.ship.HasMatchEnded && center.z > 0 && center.x >= 0 && center.x <= 1 && center.y >= 0 && center.y <= 1; plate.root.gameObject.SetActive(visible); if (!visible) continue; // Follow the ship pivot, not its rotating bounds: a fixed screen-space offset avoids bobbing. Vector3 screen = view.WorldToScreenPoint(plate.ship.transform.position); RectTransformUtility.ScreenPointToLocalPointInRectangle(shipHudLayer, screen, uiCamera, out Vector2 point); plate.root.anchoredPosition = point + Vector2.up * 36; // Keep names upright and a constant UI size regardless of ship rotation or camera zoom. bool mine = plate.ship.photonView.IsMine; string nickname = plate.ship.photonView.Owner != null ? plate.ship.photonView.Owner.NickName : "PILOT"; plate.name.text = (mine ? "YOU / " : "") + nickname; plate.name.color = mine ? new Color(.72f, 1f, .95f) : new Color(1f, .82f, .72f); float hp = Mathf.Clamp01(plate.ship.currentHp / Mathf.Max(1, plate.ship.maxHp)); plate.fill.rectTransform.localScale = new Vector3(hp, 1, 1); plate.fill.color = hp < .3f ? new Color(1f, .25f, .2f) : hp < .6f ? new Color(1f, .8f, .2f) : mine ? new Color(.2f, 1f, .7f) : new Color(1f, .45f, .35f); if (plate.emote != null) plate.emote.gameObject.SetActive(Time.unscaledTime < plate.emoteUntil); } foreach (int key in staleNameplates) shipNameplates.Remove(key); } // ป้ายชื่อเหนือยาน (สร้างด้วยโค้ดแบบเดิม) private ShipNameplate CreateShipNameplate(string objectName) { var root = BattleRect(objectName, shipHudLayer, 0, 0, 156, 38); var name = BattleLabel("Pilot", root, "", 0, 11, 156, 22, 15); name.richText = false; name.outlineWidth = .28f; name.outlineColor = new Color(0, 0, 0, 1f); var track = BattlePanel("HullTrack", root, 0, -7, 112, 8, new Color(.015f, .025f, .04f, .8f)); var fill = BattlePanel("HullFill", track.transform, -54, 0, 108, 4, Color.cyan); fill.rectTransform.pivot = new Vector2(0, .5f); var emote = CreateEmoteBubble(root); return new ShipNameplate { root = root, name = name, fill = fill, emote = emote }; } // ถ้ามีต้นแบบ ShipNameplate_Template ใน Scene ให้คัดลอกต้นแบบนั้น (หน้าตาตามที่แก้ในหน้า Edit) private ShipNameplate SpawnShipNameplateFromTemplate(string objectName) { var template = shipHudLayer.Find(ShipNameplateTemplateName); if (template == null) return null; var root = (RectTransform)EditableTemplate.Spawn(template.gameObject, shipHudLayer).transform; root.name = objectName; var name = FindPart(root, "Pilot"); var fill = FindPart(root, "HullTrack/HullFill"); if (name != null && fill != null) { var emote = FindPart(root, "Emote"); if (emote == null) emote = CreateEmoteBubble(root); emote.gameObject.SetActive(false); return new ShipNameplate { root = root, name = name, fill = fill, emote = emote }; } Destroy(root.gameObject); return null; } private RectTransform resultSurface; private TMP_Text resultHeadline, resultScore; private bool resultShown; private TMP_Text battleSkillStatus, battleRivalName; private Image combatStatusPanel; private TMP_Text[] combatStatusLabels; private int previousCombatStatus = -1; private RectTransform BattleRect(string name, Transform parent, float x, float y, float w, float h) { var rect = new GameObject(name, typeof(RectTransform)).GetComponent(); RegisterEditorCreated(rect.gameObject); rect.SetParent(parent, false); rect.anchorMin = rect.anchorMax = rect.pivot = new Vector2(0.5f, 0.5f); rect.anchoredPosition = new Vector2(x, y); rect.sizeDelta = new Vector2(w, h); return rect; } // ---------------- HUD ที่บันทึกลง Scene (แก้ในหน้า Edit ได้) ---------------- // ถ้าใน Scene มี BattleHUD อยู่แล้ว (สร้างด้วยเมนู Editable/1 บน GameplayManager) // โค้ดจะ "หยิบของเดิมมาใช้" แทนการสร้างใหม่ ตำแหน่ง/ขนาด/สี/ฟอนต์จึงเป็นตามที่จัดไว้ในหน้า Edit // ถ้าไม่มี BattleHUD ใน Scene โค้ดจะสร้างทุกอย่างเองเหมือนเดิมทุกประการ private const string ShipNameplateTemplateName = "ShipNameplate_Template"; private const string KillMessageTemplateName = "KillMessage_Template"; private bool hudAuthored; private static T FindPart(Transform root, string path) where T : Component { if (root == null) return null; var part = root.Find(path); return part != null ? part.GetComponent() : null; } private static T GetOrAdd(GameObject target) where T : Component { var component = target.GetComponent(); return component != null ? component : target.AddComponent(); } private Transform FindAuthoredBattleHud() { foreach (var canvas in FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None)) { if (canvas.gameObject.scene != gameObject.scene) continue; var hud = canvas.transform.Find("BattleHUD"); if (hud != null) return hud; } return null; } private static void RegisterEditorCreated(GameObject created) { #if UNITY_EDITOR if (!Application.isPlaying) UnityEditor.Undo.RegisterCreatedObjectUndo(created, "Build Editable Battle UI"); #endif } private Image BattlePanel(string name, Transform parent, float x, float y, float w, float h, Color color) { var img = BattleRect(name, parent, x, y, w, h).gameObject.AddComponent(); img.color = color; img.raycastTarget = false; return img; } private TMP_Text BattleLabel(string name, Transform parent, string value, float x, float y, float w, float h, int size) { var label = BattleRect(name, parent, x, y, w, h).gameObject.AddComponent(); if (matchTimerText != null) label.font = matchTimerText.font; label.text = value; label.fontSize = size; label.enableAutoSizing = true; label.fontSizeMin = 12; label.fontSizeMax = size; label.alignment = TextAlignmentOptions.Center; label.textWrappingMode = TextWrappingModes.NoWrap; label.overflowMode = TextOverflowModes.Ellipsis; label.color = Color.white; label.raycastTarget = false; return label; } private void PlaceBattleControl(Transform control, float x, float y, float scale = 1f) { if (control == null) return; control.SetParent(battleHud, false); var rect = control.GetComponent(); rect.anchorMin = rect.anchorMax = rect.pivot = new Vector2(0.5f, 0.5f); rect.anchoredPosition = new Vector2(x, y); rect.localScale = Vector3.one * scale; } private void BuildBattleHealth(string name, float x, bool local) { var card = BattlePanel(name, battleHud, x, 295, 360, 94, new Color(0.035f, 0.065f, 0.12f, 0.9f)); var title = BattleLabel("Pilot", card.transform, local ? "YOUR SHIP" : "WAITING FOR RIVAL", 0, 24, 332, 30, 21); title.richText = false; if (local) playerInfoText = title; else battleRivalName = title; var bar = BattlePanel("Hull", card.transform, 0, -14, 328, 25, new Color(0.12f, 0.15f, 0.22f)); var fill = BattlePanel("Fill", bar.transform, 0, 0, 328, 25, local ? Color.cyan : new Color(1f, 0.4f, 0.5f)); fill.rectTransform.pivot = new Vector2(0, 0.5f); fill.rectTransform.anchoredPosition = new Vector2(-164, 0); fill.rectTransform.localScale = new Vector3(0, 1, 1); var hp = BattleLabel("HullValue", bar.transform, "-- / --", 0, 0, 310, 25, 17); if (local) { p1HpText = hp; p1HpFill = fill.rectTransform; } else { p2HpText = hp; p2HpFill = fill.rectTransform; } } private void StyleBattleHUD() { if (matchTimerText == null || battleHud != null) return; if (hudAuthored) { BindAuthoredBattleHUD(); return; } var canvas = matchTimerText.canvas; foreach (string name in new[] { "Player1HUD", "Player2HUD" }) { var old = canvas.transform.Find(name); if (old != null) old.gameObject.SetActive(false); } battleHud = BattleRect("BattleHUD", canvas.transform, 0, 0, 1280, 720); // Keep the results dialog above the in-match controls. if (resultPanel != null) resultPanel.transform.SetAsLastSibling(); shipHudLayer = BattleRect("ShipNameplates", battleHud, 0, 0, 1280, 720); // Preserve the legacy canvas reference used by the minimap without showing a fixed player card. if (playerInfoText != null) playerInfoText.gameObject.SetActive(false); playerInfoText = BattleLabel("PlayerCanvasReference", battleHud, "", 0, 0, 1, 1, 12); playerInfoText.gameObject.SetActive(false); p1HpText = p2HpText = null; p1HpFill = p2HpFill = null; combatStatusPanel = BattlePanel("CombatStatuses", battleHud, 0, -264, 640, 86, new Color(.035f, .065f, .12f, .85f)); combatStatusLabels = new TMP_Text[6]; for (int i = 0; i < combatStatusLabels.Length; i++) { combatStatusLabels[i] = BattleLabel("State" + i, combatStatusPanel.transform, "", 0, 0, 198, 32, 17); combatStatusLabels[i].gameObject.SetActive(false); } combatStatusPanel.gameObject.SetActive(false); BattlePanel("MatchPlate", battleHud, 0, 291, 320, 102, new Color(0.035f, 0.065f, 0.12f, 0.25f)); PlaceBattleControl(matchTimerText.transform, 0, 314); matchTimerText.fontSize = 32; matchTimerText.text = "--:--"; matchTimerText.raycastTarget = false; PlaceBattleControl(scoreText.transform, 0, 281); scoreText.fontSize = 20; scoreText.color = new Color(0.23f, 0.82f, 0.92f); scoreText.text = "YOU 0 : 0 RIVAL"; scoreText.raycastTarget = false; BattleLabel("Objective", battleHud, "FIRST TO " + targetKills + " KILLS", 0, 252, 360, 24, 14); if (pingText != null) { PlaceBattleControl(pingText.transform, 0, 218); pingText.fontSize = 14; pingText.color = new Color(0.65f, 0.75f, 0.85f); pingText.raycastTarget = false; } HookSettingsButton(canvas.transform, true); BuildBattleControls(); FitBattleHUD(); } private void HookSettingsButton(Transform canvas, bool setLabel) { var exit = canvas.Find("TopCenter/Btn_Exit"); if (exit == null) return; // Keep this button in the authored Canvas so its RectTransform can be adjusted in the Scene view. var button = exit.GetComponent