编队系统大更新 基本快搞好了 准备做敌人和分数

This commit is contained in:
FloatGaming
2025-11-10 22:41:13 +08:00
parent 7837c6adf0
commit 941b294fce
98 changed files with 8933 additions and 98 deletions
@@ -1,6 +1,10 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class teamUIController : MonoBehaviour
{
@@ -9,6 +13,480 @@ public class teamUIController : MonoBehaviour
private set;
}
// --- 新增:队伍槽 ID 列表(从上到下 5 个友军) ---
[Header("Runtime ally configuration")]
[Tooltip("IDs for the 5 ally slots (top to bottom). These are used by PopulateAllySOsFromIds to resolve SOs into currentAllySOs.")]
public List<int> allySlotIds = new List<int> { 0, 0, 0, 0, 0 };
// --- 新增:解析后的当前五个 SO 引用(按顺序) ---
private TeamCharacterDataInfo[] currentAllySOs = new TeamCharacterDataInfo[5];
// previous active flags for detecting external changes
private bool[] prevAllyActive = new bool[5];
// 两个路径字段:一个用于编辑器查找(可以填写 Assets/Resources/so/ally 或磁盘绝对路径),
// 一个用于运行时代码查找(Resources 相对路径,例如 so/ally
[Header("SO folder paths")]
[Tooltip("Editor-only: project path or absolute disk path to the folder that contains SOs. Example: Assets/Resources/so/ally")]
public string editorSOFolderPath = "Assets/Resources/so/ally";
[Tooltip("Runtime: Resources subfolder path (no 'Resources/' prefix). Example: so/ally")]
public string runtimeResourcesFolderPath = "so/ally";
// Optional per-ally object to toggle together with isAllyX_active. If null falls back to objectFather_allyXX.
[Header("Ally runtime objects (optional)")]
public GameObject ally01_object;
public GameObject ally02_object;
public GameObject ally03_object;
public GameObject ally04_object;
public GameObject ally05_object;
// 兼容旧字段(可选)
[Header("Editor folder load (Editor only)")]
[Tooltip("Deprecated: kept for compatibility. Use editorSOFolderPath or runtimeResourcesFolderPath instead.")]
public string selectedProjectFolderPath = string.Empty;
/// <summary>
/// 在运行/编辑器中调用:根据 allySlotIds(5 个 int)在指定文件夹中搜索对应的 TeamCharacterDataInfo 资产。
/// Editor: 使用 editorSOFolderPath / selectedProjectFolderPathRuntime: 使用 runtimeResourcesFolderPath / selectedProjectFolderPath。
/// </summary>
public void PopulateAllySOsFromIds()
{
if (allySlotIds == null || allySlotIds.Count < 5)
{
Debug.LogWarning("allySlotIds 未正确设置,自动填充为 5 个占位 ID");
allySlotIds = new List<int> { 0, 0, 0, 0, 0 };
}
#if UNITY_EDITOR
// Choose editor folder path preferentially from editorSOFolderPath, fallback to selectedProjectFolderPath
string folderToUse = !string.IsNullOrEmpty(editorSOFolderPath) ? editorSOFolderPath : selectedProjectFolderPath;
// Ensure folderToUse points inside the project Assets folder when possible
string projectRelative = null;
if (!string.IsNullOrEmpty(folderToUse))
{
string projectPath = folderToUse.Replace("\\", "/");
if (projectPath.StartsWith(Application.dataPath))
{
projectRelative = "Assets" + projectPath.Substring(Application.dataPath.Length);
}
else if (projectPath.StartsWith("Assets/"))
{
projectRelative = projectPath;
}
else
{
Debug.LogWarning("editorSOFolderPath 必须位于项目的 Assets 文件夹下以便进行查询(建议填写 Assets/Resources/so/ally)。回退到全局列表查找。路径也可填写为磁盘绝对路径。");
}
}
// Collect TeamCharacterDataInfo and AllyHero_SO assets under the folder (or project-wide)
List<Object> discovered = new List<Object>();
if (!string.IsNullOrEmpty(projectRelative))
{
string[] guids = AssetDatabase.FindAssets("t:TeamCharacterDataInfo", new[] { projectRelative });
foreach (var g in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(g);
var so = AssetDatabase.LoadAssetAtPath<TeamCharacterDataInfo>(assetPath);
if (so != null) discovered.Add(so);
}
string[] guids2 = AssetDatabase.FindAssets("t:AllyHero_SO", new[] { projectRelative });
foreach (var g in guids2)
{
string assetPath = AssetDatabase.GUIDToAssetPath(g);
var so = AssetDatabase.LoadAssetAtPath<AllyHero_SO>(assetPath);
if (so != null) discovered.Add(so);
}
}
else
{
// no valid folder chosen -> search entire project
string[] guids = AssetDatabase.FindAssets("t:TeamCharacterDataInfo");
foreach (var g in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(g);
var so = AssetDatabase.LoadAssetAtPath<TeamCharacterDataInfo>(assetPath);
if (so != null) discovered.Add(so);
}
string[] guids2 = AssetDatabase.FindAssets("t:AllyHero_SO");
foreach (var g in guids2)
{
string assetPath = AssetDatabase.GUIDToAssetPath(g);
var so = AssetDatabase.LoadAssetAtPath<AllyHero_SO>(assetPath);
if (so != null) discovered.Add(so);
}
}
TeamCharacterList globalList = null;
if (teamSettingPanel.Instance != null)
globalList = teamSettingPanel.Instance.teamCharacterList;
for (int i = 0; i < 5; i++)
{
int id = allySlotIds[i];
TeamCharacterDataInfo found = null;
// 如果 slot ID 为 0 -> 关闭对应 ally UI 并跳过
if (id == 0)
{
currentAllySOs[i] = null;
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
Debug.Log($"[teamUIController] slot {i+1} id=0 -> disabled");
continue;
}
// 只要 asset 的路径或名称包含 id 即可引用(Editor)
for (int j = 0; j < discovered.Count; j++)
{
var obj = discovered[j];
if (obj == null) continue;
string path = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(path) && path.Contains(id.ToString()))
{
// if it's already TeamCharacterDataInfo, use it
if (obj is TeamCharacterDataInfo tc)
{
found = tc;
Debug.Log($"[teamUIController] (Editor) matched asset by path: {path} for id={id} slot={i+1}");
break;
}
// if it's AllyHero_SO, map to a runtime TeamCharacterDataInfo instance
if (obj is AllyHero_SO ah)
{
var mapped = ScriptableObject.CreateInstance<TeamCharacterDataInfo>();
mapped.CharacterID = ah.ally_heroID;
mapped.CharacterName = ah.ally_heroName;
mapped.CharacterCardSprite = ah.ally_heroImage;
mapped.CharacterTeamSprite = ah.ally_heroProfile;
mapped.name = ah.name;
found = mapped;
Debug.Log($"[teamUIController] (Editor) matched AllyHero_SO by path: {path} for id={id} slot={i+1}");
break;
}
}
}
// 回退到全局 teamCharacterList
if (found == null && globalList != null && globalList.characters != null)
{
for (int j = 0; j < globalList.characters.Count; j++)
{
var so = globalList.characters[j];
if (so == null) continue;
// globalList 中也以 name 包含 id 为准
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
{
found = so;
break;
}
}
}
currentAllySOs[i] = found;
if (found == null)
{
Debug.LogWarning($"[teamUIController] 未找到 id={id} 对应的 SO (slot {i + 1})");
// disable UI slot when not found
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
}
else
{
Debug.Log($"[teamUIController] (Editor) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID})");
// 确保对应 UI slot 被开启
switch (i)
{
case 0: isAlly01_active = true; break;
case 1: isAlly02_active = true; break;
case 2: isAlly03_active = true; break;
case 3: isAlly04_active = true; break;
case 4: isAlly05_active = true; break;
}
ToggleAllyObject(i, true);
DebugCharacterSO(found, i+1, true);
// Also attempt to set the teammate character image from an underlying AllyHero_SO if available
try
{
// first try to find AllyHero_SO by the same id
var ahList = Resources.LoadAll<AllyHero_SO>("");
AllyHero_SO matched = null;
foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; }
if (matched != null)
{
var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage;
SetTeammateCharacterImage(i, sprite);
}
else
{
// fallback: if current TeamCharacterDataInfo has a TeamSprite, use that
var tc = found as TeamCharacterDataInfo;
if (tc != null && tc.CharacterTeamSprite != null)
SetTeammateCharacterImage(i, tc.CharacterTeamSprite);
}
}
catch { }
}
}
#else
// Runtime: prefer runtimeResourcesFolderPath, fallback to selectedProjectFolderPath
string resourcesPath = ResolveResourcesRelativePath(!string.IsNullOrEmpty(runtimeResourcesFolderPath) ? runtimeResourcesFolderPath : selectedProjectFolderPath);
List<Object> runtimeDiscovered = new List<Object>();
if (!string.IsNullOrEmpty(resourcesPath))
{
resourcesPath = resourcesPath.Trim('/');
try
{
var arr = Resources.LoadAll(resourcesPath);
if (arr != null && arr.Length > 0)
runtimeDiscovered.AddRange(arr);
// If nothing loaded from the specific folder, fallback to loading all and filter by name later
if (runtimeDiscovered.Count == 0)
{
var arrAll = Resources.LoadAll("");
if (arrAll != null && arrAll.Length > 0)
{
runtimeDiscovered.AddRange(arrAll);
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll returned 0 in '{resourcesPath}', fell back to loading all ScriptableObjects ({arrAll.Length})");
}
}
Debug.Log($"[teamUIcontroller] (Runtime) Loaded {runtimeDiscovered.Count} objects from Resources/{resourcesPath}");
}
catch (System.Exception ex)
{
Debug.LogWarning($"[teamUIController] (Runtime) Resources.LoadAll failed for '{resourcesPath}': {ex.Message}");
var arrAll = Resources.LoadAll("");
if (arrAll != null && arrAll.Length > 0)
runtimeDiscovered.AddRange(arrAll);
}
}
else
{
var arr = Resources.LoadAll("");
if (arr != null && arr.Length > 0)
runtimeDiscovered.AddRange(arr);
Debug.Log($"[teamUIController] (Runtime) Loaded {arr?.Length ?? 0} objects from Resources (project-wide)");
}
TeamCharacterList globalList = null;
if (teamSettingPanel.Instance != null)
globalList = teamSettingPanel.Instance.teamCharacterList;
for (int i = 0; i < 5; i++)
{
int id = allySlotIds[i];
TeamCharacterDataInfo found = null;
// 如果 slot ID 为 0 -> 关闭对应 ally UI 并跳过
if (id == 0)
{
currentAllySOs[i] = null;
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
// also toggle associated object
ToggleAllyObject(i, false);
Debug.Log($"[teamUIController] (Runtime) slot {i+1} id=0 -> disabled");
continue;
}
// 只要资源名包含 id 即可引用(Runtime)。支持 TeamCharacterDataInfo 与 AllyHero_SO
string runtimeSourceInfo = string.Empty;
for (int j = 0; j < runtimeDiscovered.Count; j++)
{
var obj = runtimeDiscovered[j];
if (obj == null) continue;
// name match first
if (!string.IsNullOrEmpty(obj.name) && obj.name.Contains(id.ToString()))
{
if (obj is TeamCharacterDataInfo tc)
{
found = tc; break;
}
if (obj is AllyHero_SO ah)
{
var mapped = ScriptableObject.CreateInstance<TeamCharacterDataInfo>();
mapped.CharacterID = ah.ally_heroID;
mapped.CharacterName = ah.ally_heroName;
mapped.CharacterCardSprite = ah.ally_heroImage;
mapped.CharacterTeamSprite = ah.ally_heroProfile;
found = mapped; break;
}
}
// fallback: if object is TeamCharacterDataInfo, check its CharacterID
if (obj is TeamCharacterDataInfo tco && tco.CharacterID == id)
{
found = tco; break;
}
// fallback: AllyHero_SO CharacterID equivalent
if (obj is AllyHero_SO ah2 && ah2.ally_heroID == id)
{
var mapped2 = ScriptableObject.CreateInstance<TeamCharacterDataInfo>();
mapped2.CharacterID = ah2.ally_heroID;
mapped2.CharacterName = ah2.ally_heroName;
mapped2.CharacterCardSprite = ah2.ally_heroImage;
mapped2.CharacterTeamSprite = ah2.ally_heroProfile;
found = mapped2; break;
}
}
// 回退到全局 teamCharacterList
if (found == null && globalList != null && globalList.characters != null)
{
for (int j = 0; j < globalList.characters.Count; j++)
{
var so = globalList.characters[j];
if (so == null) continue;
if (!string.IsNullOrEmpty(so.name) && so.name.Contains(id.ToString()))
{
found = so;
break;
}
if (so.CharacterID == id)
{
found = so;
break;
}
}
}
// final fallback: try loading by resource path/id if resourcesPath provided
if (found == null && !string.IsNullOrEmpty(resourcesPath))
{
try
{
var byId = Resources.Load<TeamCharacterDataInfo>($"{resourcesPath}/{id}");
if (byId != null) found = byId;
if (found != null) runtimeSourceInfo = $"Resources.Load: {resourcesPath}/{id}";
}
catch { }
}
currentAllySOs[i] = found;
if (found == null)
{
Debug.LogWarning($"[teamUIController] (Runtime) 未找到 id={id} 对应的 SO (slot {i + 1})");
switch (i)
{
case 0: isAlly01_active = false; break;
case 1: isAlly02_active = false; break;
case 2: isAlly03_active = false; break;
case 3: isAlly04_active = false; break;
case 4: isAlly05_active = false; break;
}
ToggleAllyObject(i, false);
}
else
{
// try to get a readable source for the matched object
if (string.IsNullOrEmpty(runtimeSourceInfo))
{
runtimeSourceInfo = found.name + " (mapped)";
}
Debug.Log($"[teamUIcontroller] (Runtime) slot {i + 1} resolved to SO: {found.name} (id={found.CharacterID}) Source={runtimeSourceInfo}");
switch (i)
{
case 0: isAlly01_active = true; break;
case 1: isAlly02_active = true; break;
case 2: isAlly03_active = true; break;
case 3: isAlly04_active = true; break;
case 4: isAlly05_active = true; break;
}
ToggleAllyObject(i, true);
DebugCharacterSO(found, i+1, false);
// attempt to set teammate image from underlying AllyHero_SO if possible
try
{
var ahList = Resources.LoadAll<AllyHero_SO>("");
AllyHero_SO matched = null;
foreach (var a in ahList) if (a != null && a.ally_heroID == id) { matched = a; break; }
if (matched != null)
{
var sprite = matched.ally_heroProfile != null ? matched.ally_heroProfile : matched.ally_heroImage;
SetTeammateCharacterImage(i, sprite);
}
else
{
var tc = found as TeamCharacterDataInfo;
if (tc != null && tc.CharacterTeamSprite != null)
SetTeammateCharacterImage(i, tc.CharacterTeamSprite);
}
}
catch { }
}
}
#endif
}
// Resolve various forms of selectedProjectFolderPath into a Resources.Load relative path (no 'Resources/' prefix, no extension)
private string ResolveResourcesRelativePath(string selectedPath)
{
if (string.IsNullOrEmpty(selectedPath)) return string.Empty;
string p = selectedPath.Replace("\\", "/");
// If user provided a path starting with "Assets/Resources/", strip to get relative
int idx = p.IndexOf("Assets/Resources/");
if (idx >= 0)
{
return p.Substring(idx + "Assets/Resources/".Length).Trim('/');
}
// If user provided an absolute path that contains Application.dataPath and Resources
string appData = Application.dataPath.Replace("\\", "/");
idx = p.IndexOf(appData + "/Resources/");
if (idx >= 0)
{
return p.Substring((appData + "/Resources/").Length).Trim('/');
}
// If user already provided a Resources relative path (like "so/ally"), return it trimmed
if (p.StartsWith("Resources/")) p = p.Substring("Resources/".Length);
return p.Trim('/');
}
// Debug helper to print key fields of a TeamCharacterDataInfo
private void DebugCharacterSO(TeamCharacterDataInfo so, int slotIndex, bool isEditor)
{
if (so == null) return;
string mode = isEditor ? "Editor" : "Runtime";
string spriteCard = so.CharacterCardSprite != null ? so.CharacterCardSprite.name : "(null)";
string spriteTeam = so.CharacterTeamSprite != null ? so.CharacterTeamSprite.name : "(null)";
Debug.Log($"[teamUIController] ({mode}) Slot {slotIndex} -> CharacterID={so.CharacterID}, Name={so.CharacterName}, Skill={so.CharacterSkillName}, MaxHealth={so.CharacterMaxHealth}, CardSprite={spriteCard}, TeamSprite={spriteTeam}");
}
/// <summary>
/// 返回当前解析到的 SO 列表(数组副本)
/// </summary>
public TeamCharacterDataInfo[] GetCurrentAllySOs()
{
return (TeamCharacterDataInfo[])currentAllySOs.Clone();
}
[Header("基本文本")]
[Tooltip("title")]
public TextMeshProUGUI songNametitle;
@@ -22,7 +500,7 @@ public class teamUIController : MonoBehaviour
[SerializeField] private int enemyCounterMax; // 本局游戏敌人最大数
[SerializeField] private int enemyCurrentCount; // 现在的敌人顺序
[Tooltip("当前总分")]
public Text currentTotalScore;
public TextMeshProUGUI currentTotalScore;
[Header("Combo判定最低要求")]
public ComboJudgeType comboJudgeType = ComboJudgeType.Perfect;
@@ -232,13 +710,32 @@ public class teamUIController : MonoBehaviour
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
PopulateAllySOsFromIds();
// initialize previous flags so Update can detect changes
prevAllyActive[0] = isAlly01_active;
prevAllyActive[1] = isAlly02_active;
prevAllyActive[2] = isAlly03_active;
prevAllyActive[3] = isAlly04_active;
prevAllyActive[4] = isAlly05_active;
// Ensure score text references are resolved so ScoreManager and AllyCombatant can write to them
ResolveScoreTextReferences();
}
// Update is called once per frame
void Update()
{
// detect external changes to isAllyX_active and toggle objects accordingly
bool[] current = new bool[] { isAlly01_active, isAlly02_active, isAlly03_active, isAlly04_active, isAlly05_active };
for (int i = 0; i < 5; i++)
{
if (prevAllyActive[i] != current[i])
{
ToggleAllyObject(i, current[i]);
prevAllyActive[i] = current[i];
}
}
}
private void FixedUpdate()
@@ -289,5 +786,108 @@ public class teamUIController : MonoBehaviour
}
}
// helper to toggle ally object by slot index
private void ToggleAllyObject(int slotIndex, bool active)
{
GameObject target = null;
switch (slotIndex)
{
case 0: target = ally01_object ?? objectFather_ally01; break;
case 1: target = ally02_object ?? objectFather_ally02; break;
case 2: target = ally03_object ?? objectFather_ally03; break;
case 3: target = ally04_object ?? objectFather_ally04; break;
case 4: target = ally05_object ?? objectFather_ally05; break;
}
if (target != null)
target.SetActive(active);
}
// Try to ensure the teammateXX_current_scoreText fields are assigned; if null, search the corresponding parent for TMP/Text
private void ResolveScoreTextReferences()
{
void TryResolve(ref TextMeshProUGUI field, GameObject parent, string slotName)
{
if (field != null) return;
if (parent != null)
{
var tmp = parent.GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
field = tmp;
Debug.Log($"[teamUIController] Resolved {slotName} TMP from parent {parent.name} -> {tmp.gameObject.name}");
return;
}
var legacy = parent.GetComponentInChildren<Text>(true);
if (legacy != null)
{
// if only legacy Text exists, try to create a TMP component to avoid type mismatch
var go = legacy.gameObject;
var created = go.GetComponent<TextMeshProUGUI>() ?? go.AddComponent<TextMeshProUGUI>();
created.text = legacy.text;
field = created;
Debug.LogWarning($"[teamUIController] Found legacy Text for {slotName} under {parent.name}. Added/used TMP component on {go.name} and copied text.");
return;
}
}
Debug.LogWarning($"[teamUIController] Could not resolve {slotName} TMP (parent '{parent?.name}')");
}
TryResolve(ref teammate01_current_scoreText, objectFather_ally01, "teammate01_current_scoreText");
TryResolve(ref teammate02_current_scoreText, objectFather_ally02, "teammate02_current_scoreText");
TryResolve(ref teammate03_current_scoreText, objectFather_ally03, "teammate03_current_scoreText");
TryResolve(ref teammate04_current_scoreText, objectFather_ally04, "teammate04_current_scoreText");
TryResolve(ref teammate05_current_scoreText, objectFather_ally05, "teammate05_current_scoreText");
// total score: if assigned field is null, try to find any TMP in scene named "currentTotalScore" or under this object
if (currentTotalScore == null)
{
var tmp = GetComponentInChildren<TextMeshProUGUI>(true);
if (tmp != null)
{
currentTotalScore = tmp;
Debug.Log($"[teamUIController] Resolved currentTotalScore from child {tmp.gameObject.name}");
}
}
}
/// <summary>
/// 返回相邻的友军槽索引(左右),边侧只有一侧
/// </summary>
public int[] GetAdjacentAllyIndices(int slotIndex)
{
var list = new List<int>();
if (slotIndex - 1 >= 0) list.Add(slotIndex - 1);
if (slotIndex + 1 <= 4) list.Add(slotIndex + 1);
return list.ToArray();
}
/// <summary>
/// 返回指定槽位对应的友军 GameObject(优先返回 allyX_object,否则返回 objectFather_allyXX),可能为 null
/// </summary>
public GameObject GetAllyObjectBySlot(int slotIndex)
{
switch (slotIndex)
{
case 0: return ally01_object ?? objectFather_ally01;
case 1: return ally02_object ?? objectFather_ally02;
case 2: return ally03_object ?? objectFather_ally03;
case 3: return ally04_object ?? objectFather_ally04;
case 4: return ally05_object ?? objectFather_ally05;
default: return null;
}
}
// helper to set teammate character image based on slot index
private void SetTeammateCharacterImage(int slotIndex, Sprite sprite)
{
if (sprite == null) return;
switch (slotIndex)
{
case 0: if (teammate01_characterImage != null) teammate01_characterImage.sprite = sprite; break;
case 1: if (teammate02_characterImage != null) teammate02_characterImage.sprite = sprite; break;
case 2: if (teammate03_characterImage != null) teammate03_characterImage.sprite = sprite; break;
case 3: if (teammate04_characterImage != null) teammate04_characterImage.sprite = sprite; break;
case 4: if (teammate05_characterImage != null) teammate05_characterImage.sprite = sprite; break;
}
}
}