using UnityEngine; // หน้าตั้งค่าที่ใช้ร่วมกันระหว่างล็อบบี้และสนามรบ; ไม่หยุดเวลาเกมออนไลน์ // ถ้าใน Scene มีต้นแบบ BattleSettingsTemplate (สร้างด้วยเมนู Editable) จะคัดลอกต้นแบบนั้นมาใช้ // หน้าตาจึงเป็นตามที่จัดในหน้า Edit; ถ้าไม่มีต้นแบบ จะสร้างด้วยโค้ดแบบเดิม public class BattleSettingsPanel : MonoBehaviour { public const string TemplateName = "BattleSettingsTemplate"; private static BattleSettingsPanel current; public static bool IsOpen => current != null; public static float AimSensitivity => Mathf.Clamp(PlayerPrefs.GetFloat("AimSensitivity", .7f), .25f, 1.5f); private System.Action leave; private System.Action logout; private GameObject logoutConfirmation; private GameObject authoredLogoutConfirmation; private RectTransform panel; private GameObject confirmation; private GameObject authoredConfirmation; // onLeave = ปุ่ม LEAVE MATCH (ในสนามรบ), onLogout = ปุ่ม LOG OUT (ในล็อบบี้) public static void Show(System.Action onLeave = null, System.Action onLogout = null) { if (current != null) return; var template = FindTemplate(); if (template != null) { var copy = EditableTemplate.Spawn(template, null); copy.name = "BattleSettings"; current = copy.AddComponent(); current.leave = onLeave; current.logout = onLogout; if (current.BindAuthored()) return; Debug.LogWarning("BattleSettingsTemplate is missing SettingsPanel; using the code-built settings panel.", template); current = null; Destroy(copy); } var root = CreateRoot("BattleSettings"); current = root.AddComponent(); current.leave = onLeave; current.logout = onLogout; current.Build(); } private static GameObject CreateRoot(string name) { var root = new GameObject(name, typeof(Canvas), typeof(UnityEngine.UI.CanvasScaler), typeof(UnityEngine.UI.GraphicRaycaster)); var canvas = root.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvas.sortingOrder = 200; var scaler = root.GetComponent(); scaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1280,720); scaler.matchWidthOrHeight = .5f; return root; } private static GameObject FindTemplate() { foreach (var root in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects()) if (root.name == TemplateName) return root; return null; } private RectTransform Rect(string name, Transform parent, float x,float y,float w,float h) { var item = new GameObject(name,typeof(RectTransform)); var rect = item.GetComponent(); rect.SetParent(parent,false); rect.sizeDelta = new Vector2(w,h); rect.anchoredPosition = new Vector2(x,y); return rect; } // ชื่อวัตถุไม่ซ้ำกัน เพื่อให้ต้นแบบใน Scene หาเจอได้ (หน้าตา/การทำงานเหมือนเดิม) private TMPro.TextMeshProUGUI Text(Transform parent,string value,float x,float y,float w=500,string name="Label") { var text=Rect(name,parent,x,y,w,44).gameObject.AddComponent(); text.text=value; text.fontSize=23; text.alignment=TMPro.TextAlignmentOptions.Center; text.raycastTarget=false; return text; } private void Button(Transform parent,string value,float x,float y,System.Action action) { var rect=Rect(value,parent,x,y,220,48); rect.gameObject.AddComponent().color=new Color(.08f,.25f,.34f); rect.gameObject.AddComponent().onClick.AddListener(()=>action()); Text(rect,value,0,0,210); } private void Row(string title,string key,float defaultValue,float min,float max,float y,System.Action changed) { var label=Text(panel,title,-155,y,230,title+" Label"); var track=Rect(title,panel,125,y,260,16); track.gameObject.AddComponent().color=new Color(.15f,.24f,.32f); var slider=track.gameObject.AddComponent(); var handle=Rect("Handle",track,0,0,26,34); var image=handle.gameObject.AddComponent(); image.color=new Color(.3f,.85f,1); slider.handleRect=handle; slider.targetGraphic=image; slider.minValue=min; slider.maxValue=max; HookSlider(slider,label,title,key,defaultValue,min,max,changed); } private static void HookSlider(UnityEngine.UI.Slider slider,TMPro.TMP_Text label,string title,string key,float defaultValue,float min,float max,System.Action changed) { float value=Mathf.Clamp(PlayerPrefs.GetFloat(key,defaultValue),min,max); slider.SetValueWithoutNotify(value); if(label != null) label.text=title+" "+Mathf.RoundToInt(value*100)+"%"; slider.onValueChanged.AddListener(v=>{ PlayerPrefs.SetFloat(key,v); changed?.Invoke(v); if(label != null) label.text=title+" "+Mathf.RoundToInt(v*100)+"%"; }); } private void Build() { var shade=Rect("ModalShade",transform,0,0,0,0); shade.anchorMin=Vector2.zero; shade.anchorMax=Vector2.one; shade.offsetMin=shade.offsetMax=Vector2.zero; shade.gameObject.AddComponent().color=new Color(0,0,0,.8f); panel=Rect("SettingsPanel",transform,0,0,640,560); panel.gameObject.AddComponent().color=new Color(.035f,.065f,.12f); Text(panel,"SETTINGS",0,225,500,"Title"); Row("MASTER","MasterVolume",1,0,1,140,v=>AudioManager.Instance?.SetMasterVolume(v)); Row("MUSIC","MusicVolume",.65f,0,1,75,v=>AudioManager.Instance?.SetMusicVolume(v)); Row("EFFECTS","SFXVolume",.8f,0,1,10,v=>AudioManager.Instance?.SetSFXVolume(v)); Row("AIM SPEED","AimSensitivity",.7f,.25f,1.5f,-55,null); Text(panel,MessageFor(leave),0,-120,600,"Message").fontSize=18; Button(panel,"BACK",leave == null && logout == null ? 0 : -125,-205,Close); if(leave != null) Button(panel,"LEAVE MATCH",125,-205,Confirm); else if(logout != null) Button(panel,"LOG OUT",125,-205,ConfirmLogout); } private bool ShowsLogout => leave == null && logout != null; private static string MessageFor(System.Action leave) => leave == null ? "Settings are saved on this device." : "Online match continues while this menu is open."; // ผูกปุ่ม/สไลเดอร์ของต้นแบบที่คัดลอกมา (ไม่เปลี่ยนตำแหน่ง/สีที่จัดไว้) private bool BindAuthored() { panel = transform.Find("SettingsPanel") as RectTransform; if (panel == null) return false; BindRow("MASTER","MasterVolume",1,0,1,v=>AudioManager.Instance?.SetMasterVolume(v)); BindRow("MUSIC","MusicVolume",.65f,0,1,v=>AudioManager.Instance?.SetMusicVolume(v)); BindRow("EFFECTS","SFXVolume",.8f,0,1,v=>AudioManager.Instance?.SetSFXVolume(v)); BindRow("AIM SPEED","AimSensitivity",.7f,.25f,1.5f,null); var message = FindText(panel,"Message"); if(message != null) message.text = MessageFor(leave); BindButton(panel,"BACK",Close); var leaveButton = panel.Find("LEAVE MATCH"); if(leaveButton != null) { leaveButton.gameObject.SetActive(leave != null); if(leave != null) BindButton(panel,"LEAVE MATCH",Confirm); } var logoutButton = panel.Find("LOG OUT"); if(logoutButton == null && ShowsLogout) { // ต้นแบบเก่าที่ยังไม่มีปุ่ม LOG OUT: เพิ่มให้ และเลื่อน BACK ไปซ้ายไม่ให้ทับกัน var back = panel.Find("BACK") as RectTransform; if(back != null && Mathf.Abs(back.anchoredPosition.x) < 1f) back.anchoredPosition = new Vector2(-125, back.anchoredPosition.y); Button(panel,"LOG OUT",125,-205,ConfirmLogout); } else if(logoutButton != null) { logoutButton.gameObject.SetActive(ShowsLogout); if(ShowsLogout) BindButton(panel,"LOG OUT",ConfirmLogout); } var confirmLogout = panel.Find("ConfirmLogout"); if(confirmLogout != null) { authoredLogoutConfirmation = confirmLogout.gameObject; authoredLogoutConfirmation.SetActive(false); BindButton(confirmLogout,"CANCEL",()=>authoredLogoutConfirmation.SetActive(false)); BindButton(confirmLogout,"LOG OUT",()=>{var action=logout; Close(); action?.Invoke();}); } var confirm = panel.Find("ConfirmLeave"); if(confirm != null) { authoredConfirmation = confirm.gameObject; authoredConfirmation.SetActive(false); BindButton(confirm,"CANCEL",()=>authoredConfirmation.SetActive(false)); BindButton(confirm,"LEAVE",()=>{var action=leave; Close(); action?.Invoke();}); } return true; } private void BindRow(string title,string key,float defaultValue,float min,float max,System.Action changed) { var track = panel.Find(title); var slider = track != null ? track.GetComponent() : null; if(slider == null) return; // ช่วงค่าเป็นกติกาเกม จึงตั้งจากโค้ดเสมอ slider.minValue=min; slider.maxValue=max; HookSlider(slider,FindText(panel,title+" Label"),title,key,defaultValue,min,max,changed); } private static TMPro.TMP_Text FindText(Transform parent,string name) { var item = parent.Find(name); return item != null ? item.GetComponent() : null; } private static void BindButton(Transform parent,string name,System.Action action) { var item = parent.Find(name); var button = item != null ? item.GetComponent() : null; if(button != null) button.onClick.AddListener(()=>action()); } private void Confirm() { if(authoredConfirmation != null) { authoredConfirmation.SetActive(true); return; } if(confirmation != null) return; var rect=Rect("ConfirmLeave",panel,0,0,640,560); confirmation=rect.gameObject; confirmation.AddComponent().color=new Color(.035f,.065f,.12f); Text(rect,"LEAVE THIS MATCH?",0,95,500,"Title"); Text(rect,"The current match will end for both players.",0,25,610,"Message").fontSize=20; Button(rect,"CANCEL",-125,-95,()=>Destroy(confirmation)); Button(rect,"LEAVE",125,-95,()=>{var action=leave; Close(); action?.Invoke();}); } private void ConfirmLogout() { if(authoredLogoutConfirmation != null) { authoredLogoutConfirmation.SetActive(true); return; } if(logoutConfirmation != null) return; var rect=Rect("ConfirmLogout",panel,0,0,640,560); logoutConfirmation=rect.gameObject; logoutConfirmation.AddComponent().color=new Color(.035f,.065f,.12f); Text(rect,"LOG OUT?",0,95,500,"Title"); Text(rect,"You will return to the login screen.",0,25,610,"Message").fontSize=20; Button(rect,"CANCEL",-125,-95,()=>Destroy(logoutConfirmation)); Button(rect,"LOG OUT",125,-95,()=>{var action=logout; Close(); action?.Invoke();}); } private void Close() { PlayerPrefs.Save(); Destroy(gameObject); } void OnDestroy() { if(current==this) current=null; } #if UNITY_EDITOR // ใช้จากเมนู Editable บน GameplayManager / LobbyManager: สร้างต้นแบบหน้าตั้งค่าไว้ใน Scene ให้แก้ได้ public static void BuildEditableTemplate(bool inBattle) { if (Application.isPlaying) return; if (FindTemplate() != null) { Debug.LogWarning(TemplateName + " already exists in this scene."); return; } var root = CreateRoot(TemplateName); UnityEditor.Undo.RegisterCreatedObjectUndo(root, "Build Settings Template"); var builder = root.AddComponent(); builder.leave = inBattle ? () => { } : (System.Action)null; builder.logout = inBattle ? (System.Action)null : () => { }; builder.Build(); if (inBattle) { builder.Confirm(); if (builder.confirmation != null) builder.confirmation.SetActive(false); } else { builder.ConfirmLogout(); if (builder.logoutConfirmation != null) builder.logoutConfirmation.SetActive(false); } DestroyImmediate(builder); root.AddComponent(); root.SetActive(false); UnityEditor.Selection.activeGameObject = root; UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(root.scene); Debug.Log(TemplateName + " created. Tick it on to edit, the game hides it automatically when you press Play.", root); } #endif }