Initial commit: Client Doc docs Server Tools
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XGame
|
||||
{
|
||||
[Serializable]
|
||||
public class CharacterConfigData
|
||||
{
|
||||
public string defaultCharacterId;
|
||||
public Vector3 spawnPosition;
|
||||
public Vector3 spawnEuler;
|
||||
public List<CharacterConfigItem> characters = new List<CharacterConfigItem>();
|
||||
|
||||
public CharacterConfigItem GetDefault()
|
||||
{
|
||||
CharacterConfigItem item = Find(defaultCharacterId);
|
||||
return item ?? (characters.Count > 0 ? characters[0] : null);
|
||||
}
|
||||
|
||||
public CharacterConfigItem Find(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < characters.Count; i++)
|
||||
{
|
||||
CharacterConfigItem item = characters[i];
|
||||
if (item != null && string.Equals(item.id, id, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class CharacterConfigItem
|
||||
{
|
||||
public string id;
|
||||
public string displayName;
|
||||
public string prefabPath;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a02dad566ae24fdcb50e5fbc6e4c42c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,324 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UObject = UnityEngine.Object;
|
||||
|
||||
namespace XGame
|
||||
{
|
||||
public class CharacterSwitchController : MonoBehaviour
|
||||
{
|
||||
private const string ConfigResourcePath = "Config/CharacterConfig";
|
||||
private const string SelectedCharacterKey = "CharacterSwitch.SelectedCharacterId";
|
||||
private const string PrefabPath = "Assets/Game/Art/UI/Prefab/UI_CharacterSwitch.prefab";
|
||||
|
||||
private CharacterConfigData config;
|
||||
private CharacterConfigItem currentConfig;
|
||||
private GameObject currentCharacter;
|
||||
private Transform characterRoot;
|
||||
private string errorText;
|
||||
|
||||
// UGUI 引用(替换原 IMGUI 绘制)
|
||||
private GameObject uiPanel; // 实例化的 UI 根
|
||||
private GameObject selectPanel; // Panel_Select
|
||||
private Transform listChars; // List_Chars
|
||||
private GameObject itemTemplate; // Item_Char
|
||||
private TMP_Text errorLabel; // Txt_Error
|
||||
private TMP_Text toggleLabel; // Txt_Toggle
|
||||
private readonly List<Button> itemButtons = new List<Button>();
|
||||
private readonly List<string> itemIds = new List<string>();
|
||||
|
||||
public string CurrentCharacterId
|
||||
{
|
||||
get { return currentConfig == null ? string.Empty : currentConfig.id; }
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
LoadConfig();
|
||||
EnsureRoot();
|
||||
LoadInitialSelection();
|
||||
StartCoroutine(LoadUIAsync());
|
||||
}
|
||||
|
||||
public void SwitchTo(string characterId)
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
LoadConfig();
|
||||
}
|
||||
|
||||
CharacterConfigItem next = config == null ? null : config.Find(characterId);
|
||||
if (next == null)
|
||||
{
|
||||
errorText = "Character not found: " + characterId;
|
||||
Debug.LogWarning(errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
SwitchTo(next);
|
||||
}
|
||||
|
||||
private void LoadConfig()
|
||||
{
|
||||
TextAsset textAsset = Resources.Load<TextAsset>(ConfigResourcePath);
|
||||
if (textAsset == null)
|
||||
{
|
||||
errorText = "Missing character config: Resources/" + ConfigResourcePath + ".json";
|
||||
Debug.LogError(errorText);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
config = JsonUtility.FromJson<CharacterConfigData>(textAsset.text);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorText = "Parse character config failed: " + ex.Message;
|
||||
Debug.LogError(errorText);
|
||||
config = null;
|
||||
}
|
||||
|
||||
if (config == null || config.characters == null || config.characters.Count == 0)
|
||||
{
|
||||
errorText = "Character config is empty.";
|
||||
Debug.LogError(errorText);
|
||||
config = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureRoot()
|
||||
{
|
||||
if (characterRoot != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject rootObject = GameObject.Find("CharacterRoot");
|
||||
if (rootObject == null)
|
||||
{
|
||||
rootObject = new GameObject("CharacterRoot");
|
||||
}
|
||||
|
||||
characterRoot = rootObject.transform;
|
||||
if (characterRoot.parent != transform)
|
||||
{
|
||||
characterRoot.SetParent(transform, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadInitialSelection()
|
||||
{
|
||||
if (config == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string selectedId = PlayerPrefs.GetString(SelectedCharacterKey, string.Empty);
|
||||
CharacterConfigItem item = config.Find(selectedId) ?? config.GetDefault();
|
||||
if (item != null)
|
||||
{
|
||||
currentConfig = item;
|
||||
PlayerPrefs.SetString(SelectedCharacterKey, item.id);
|
||||
PlayerPrefs.Save();
|
||||
}
|
||||
}
|
||||
|
||||
private void SwitchTo(CharacterConfigItem item)
|
||||
{
|
||||
if (currentCharacter != null)
|
||||
{
|
||||
Destroy(currentCharacter);
|
||||
currentCharacter = null;
|
||||
}
|
||||
|
||||
currentConfig = item;
|
||||
errorText = string.Empty;
|
||||
|
||||
PlayerPrefs.SetString(SelectedCharacterKey, item.id);
|
||||
PlayerPrefs.Save();
|
||||
RefreshLobbyAvatar();
|
||||
}
|
||||
|
||||
private void RefreshLobbyAvatar()
|
||||
{
|
||||
LobbyWorldController lobby = FindObjectOfType<LobbyWorldController>();
|
||||
if (lobby != null)
|
||||
{
|
||||
lobby.RefreshSelectedFigure();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- UGUI ----------------
|
||||
|
||||
// 异步加载 UI 预设:按需从 CDN 下载,避免同步加载读到旧 APK 底包。
|
||||
private IEnumerator LoadUIAsync()
|
||||
{
|
||||
string prefabPath = PrefabPath;
|
||||
if (prefabPath.StartsWith("Assets/"))
|
||||
{
|
||||
prefabPath = prefabPath.Substring(7);
|
||||
}
|
||||
|
||||
UObject asset = null;
|
||||
yield return XResLoader.coLoadRes(prefabPath, typeof(GameObject), o => asset = o);
|
||||
GameObject prefab = asset as GameObject;
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogError("Load UI_CharacterSwitch failed: " + PrefabPath);
|
||||
yield break;
|
||||
}
|
||||
|
||||
uiPanel = Instantiate(prefab);
|
||||
// 角色切换是普通功能界面 → 挂到 UIRoot/Normal 层(见 UI 挂载规则)
|
||||
UIManager.AttachTo(UILayer.Normal, uiPanel);
|
||||
|
||||
CacheUI();
|
||||
BuildCharacterButtons();
|
||||
}
|
||||
|
||||
private void CacheUI()
|
||||
{
|
||||
if (uiPanel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform sel = uiPanel.transform.FindTransform("Panel_Select");
|
||||
selectPanel = sel == null ? null : sel.gameObject;
|
||||
listChars = uiPanel.transform.FindTransform("List_Chars");
|
||||
Transform item = uiPanel.transform.FindTransform("Item_Char");
|
||||
itemTemplate = item == null ? null : item.gameObject;
|
||||
Transform err = uiPanel.transform.FindTransform("Txt_Error");
|
||||
errorLabel = err == null ? null : err.GetComponent<TMP_Text>();
|
||||
Transform tog = uiPanel.transform.FindTransform("Txt_Toggle");
|
||||
toggleLabel = tog == null ? null : tog.GetComponent<TMP_Text>();
|
||||
|
||||
Transform toggleBtn = uiPanel.transform.FindTransform("Btn_Toggle");
|
||||
if (toggleBtn != null)
|
||||
{
|
||||
Button b = toggleBtn.GetComponent<Button>();
|
||||
if (b != null)
|
||||
{
|
||||
b.onClick.RemoveListener(OnToggleClicked);
|
||||
b.onClick.AddListener(OnToggleClicked);
|
||||
}
|
||||
}
|
||||
|
||||
if (selectPanel != null)
|
||||
{
|
||||
selectPanel.SetActive(false);
|
||||
}
|
||||
UpdateToggleLabel();
|
||||
}
|
||||
|
||||
private void BuildCharacterButtons()
|
||||
{
|
||||
itemButtons.Clear();
|
||||
itemIds.Clear();
|
||||
if (itemTemplate == null || listChars == null || config == null || config.characters == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < config.characters.Count; i++)
|
||||
{
|
||||
CharacterConfigItem item = config.characters[i];
|
||||
if (item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
GameObject row = Instantiate(itemTemplate, listChars);
|
||||
row.name = "Item_Char_" + item.id;
|
||||
row.SetActive(true);
|
||||
|
||||
Transform label = row.transform.FindTransform("Txt_Char");
|
||||
if (label != null)
|
||||
{
|
||||
TMP_Text t = label.GetComponent<TMP_Text>();
|
||||
if (t != null)
|
||||
{
|
||||
t.text = string.IsNullOrEmpty(item.displayName) ? item.id : item.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
Button btn = row.GetComponent<Button>();
|
||||
if (btn != null)
|
||||
{
|
||||
string captured = item.id;
|
||||
btn.onClick.RemoveAllListeners();
|
||||
btn.onClick.AddListener(() => OnCharClicked(captured));
|
||||
itemButtons.Add(btn);
|
||||
itemIds.Add(item.id);
|
||||
}
|
||||
}
|
||||
|
||||
RefreshSelection();
|
||||
RefreshError();
|
||||
}
|
||||
|
||||
private void OnToggleClicked()
|
||||
{
|
||||
if (selectPanel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
selectPanel.SetActive(!selectPanel.activeSelf);
|
||||
}
|
||||
|
||||
private void OnCharClicked(string characterId)
|
||||
{
|
||||
SwitchTo(characterId);
|
||||
RefreshSelection();
|
||||
RefreshError();
|
||||
UpdateToggleLabel();
|
||||
if (selectPanel != null)
|
||||
{
|
||||
selectPanel.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// 高亮当前选中项(当前项绿色加粗,其余白色)
|
||||
private void RefreshSelection()
|
||||
{
|
||||
for (int i = 0; i < itemButtons.Count; i++)
|
||||
{
|
||||
Button btn = itemButtons[i];
|
||||
if (btn == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bool selected = currentConfig != null && currentConfig.id == itemIds[i];
|
||||
TMP_Text t = btn.GetComponentInChildren<TMP_Text>(true);
|
||||
if (t != null)
|
||||
{
|
||||
t.color = selected ? new Color(0.34f, 0.84f, 0.55f, 1f) : new Color(0.96f, 0.97f, 1f, 1f);
|
||||
t.fontStyle = selected ? FontStyles.Bold : FontStyles.Normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshError()
|
||||
{
|
||||
if (errorLabel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
errorLabel.text = errorText ?? string.Empty;
|
||||
errorLabel.gameObject.SetActive(!string.IsNullOrEmpty(errorText));
|
||||
}
|
||||
|
||||
private void UpdateToggleLabel()
|
||||
{
|
||||
if (toggleLabel == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
toggleLabel.text = string.IsNullOrEmpty(CurrentCharacterId) ? "角色" : "角色: " + CurrentCharacterId;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7aa234eb9d494595a42577fc322ede94
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user