using System; using System.Collections.Generic; using DG.Tweening; using TMPro; using UnityEngine; using UnityEngine.UI; /// /// 星际迷航(XJMH)金币柱控制器:座位每次加金币时,在座位炮台旁生成一根从下往上生长的金币柱, /// 柱头红/绿交替、顶部显示本次得分(万/千/亿缩写),多根并排最多 maxColumns 根。 /// /// 移植自 PW 小游戏的 PW_GoldColumn / PW_GoldColumnMgr: /// - 每座位一个队列,每帧最多补 1 根(连杀时排队上升); /// - 首根绿头(与 PW 的 ColumnType 初值 true 取反一致),之后红绿交替; /// - 队头停留 holdDuration 后淡出,其余整体向座位方向平移补位; /// - 队伍数量 > crowdThreshold 时队头不再额外停顿(快速滚动)。 /// /// 模板:面板根节点下已放好的 PW_GoldColumn_Self / PW_GoldColumn_Other 两个实例, /// 由 Init 自动查找并隐藏,运行时克隆复用。 /// public class GoldColumnController : MonoBehaviour { // ==================== 可调参数("阈值") ==================== [Header("柱体上限 / 排列")] [Tooltip("每座位最多同时存在的金币柱数量")] public int maxColumns = 5; [Tooltip("队伍长度超过此值时,队头不再额外停顿(快速滚动)")] public int crowdThreshold = 3; // ⚠️ 这六个位置参数由面板脚本 GameMainPanel_XJMH 每次出柱前同步 // (无后缀=左座位、Right=右座位),请在面板的 GameMainPanel_XJMH 组件上调,不要在这里调(这里改了会被覆盖)。 // 左右判定:座位在面板中线左侧(sign<0)用无后缀那组,右侧(sign>0)用 Right 那组。 [HideInInspector] public float deltaX = 40f; [HideInInspector] public float offsetX = 100f; [HideInInspector] public float offsetY = 0f; [HideInInspector] public float deltaXRight = 40f; [HideInInspector] public float offsetXRight = 100f; [HideInInspector] public float offsetYRight = 0f; [Header("高度映射(沿用 PW 公式)")] [Tooltip("高度封顶值:score/scoreDivisor 达到此值即顶格")] public float heightCap = 255f; [Tooltip("得分系数:mul = (earn / 炮倍) / scoreDivisor")] public float scoreDivisor = 2f; [Tooltip("满格高度 = 面板高度 × 该比例")] public float barHeightToPanelRatio = 0.47f; [Tooltip(">0 时忽略 PW 公式,直接按 earn/该值 归一化(满格金额);=0 时用 PW 公式 earn/座位炮倍")] public long fullValue = 0; [Tooltip(">0 时直接作为满格高度(像素),覆盖上面的比例")] public float maxBarHeightOverride = 0f; [Header("时长(秒)")] [Tooltip("生长 + 淡入时长")] public float growDuration = 0.3f; [Tooltip("队头停留时长(单根独占时)")] public float holdDuration = 2f; [Tooltip("补位平移时长")] public float shiftDuration = 0.17f; [Tooltip("淡出时长")] public float fadeDuration = 0.3f; [Tooltip("队伍不超过 crowdThreshold 时,新队头的额外停顿")] public float crowdDelay = 1f; [Header("对象池")] [Tooltip("每种预制体缓存的空闲柱数量上限(超出直接销毁)")] public int preloadNum = 4; [Header("模板名(面板根节点下的子物体名)")] public string selfTemplateName = "PW_GoldColumn_Self"; public string otherTemplateName = "PW_GoldColumn_Other"; // ==================== 运行时状态 ==================== private RectTransform _panelRoot; private GameObject _selfTemplate; private GameObject _otherTemplate; private readonly List _poolSelf = new List(); private readonly List _poolOther = new List(); private readonly List[] _list = new List[4]; private readonly Queue[] _queue = new Queue[4]; // 红/绿交替标记:初值 true,取柱时先取反 → 首根绿头(与 PW 一致) private readonly bool[] _redFlag = new bool[4]; private float _barWidth = 25f; private bool _inited; /// 待生成的柱(入队时就把座位位置/炮倍固化,避免座位被销毁后的空引用) private struct PendingColumn { public long earn; public bool isSelf; public Vector2 basePos; // 座位在面板本地(锚点 0.5,0)的锚点坐标 public float ratio; // 炮倍(>=1) public float sign; // 堆叠方向:座位在面板左 → -1,右 → +1 } // ==================== 初始化 / 清理 ==================== /// 绑定面板根节点并查找两个模板(自动 SetActive(false))。 public void Init(RectTransform panelRoot) { _panelRoot = panelRoot; _inited = false; if (_panelRoot == null) { Debug.LogWarning("[GoldColumn] panelRoot 为空,金币柱不启用"); return; } _selfTemplate = FindChildByName(_panelRoot, selfTemplateName)?.gameObject; _otherTemplate = FindChildByName(_panelRoot, otherTemplateName)?.gameObject; if (_selfTemplate == null || _otherTemplate == null) { Debug.LogWarning($"[GoldColumn] 未在面板下找到模板 {selfTemplateName}({_selfTemplate != null}) / " + $"{otherTemplateName}({_otherTemplate != null}),金币柱不启用"); return; } // 模板本身只是资源,隐藏掉;取原始柱宽 _selfTemplate.SetActive(false); _otherTemplate.SetActive(false); var tmplRt = _selfTemplate.transform as RectTransform; if (tmplRt != null && tmplRt.sizeDelta.x > 0f) _barWidth = tmplRt.sizeDelta.x; for (int i = 0; i < _list.Length; i++) { _list[i] = new List(); _queue[i] = new Queue(); _redFlag[i] = true; } _poolSelf.Clear(); _poolOther.Clear(); _inited = true; Debug.Log($"[GoldColumn] Init: self={_selfTemplate.name} other={_otherTemplate.name} " + $"barW={_barWidth} panelH={_panelRoot.rect.height:0.#}"); } /// 退房/隐藏时调用:杀掉所有动画、回收/销毁全部柱、清空队列(模板与控制器保留,可继续复用)。 public void Cleanup() { for (int pos = 0; pos < _list.Length; pos++) { var list = _list[pos]; if (list == null) continue; for (int i = 0; i < list.Count; i++) DestroyItem(list[i]); list.Clear(); _queue[pos]?.Clear(); _redFlag[pos] = true; } // ⭐ 池里的克隆也要销毁:原来只清列表不销毁,克隆体会留在面板下变成无人引用、 // 也不会再复用的"孤儿"(原版 ExitGame → DestroyColumnPool 是逐个 Release 销毁的) for (int i = 0; i < _poolSelf.Count; i++) DestroyItem(_poolSelf[i]); for (int i = 0; i < _poolOther.Count; i++) DestroyItem(_poolOther[i]); _poolSelf.Clear(); _poolOther.Clear(); } private void OnDestroy() { Cleanup(); } // ==================== 对外接口 ==================== /// 座位加金币时调用(入队,由 Update 逐帧限流生成)。 public void ShowOne(SitPosData sit, long earn, bool isSelf) { if (!_inited || sit == null || earn <= 0) return; int pos = sit.posID; if (pos < 0 || pos >= _list.Length) return; Vector3 local = _panelRoot.InverseTransformPoint(sit.transform.position); float ratio = sit.consumeGoldNum > 0 ? sit.consumeGoldNum : 1f; // 柱锚点是 (0.5,0):锚点坐标 = 本地坐标 - 锚点参考点(面板底边中点) Rect r = _panelRoot.rect; Vector2 anchorRef = new Vector2(r.xMin + r.width * 0.5f, r.yMin); _queue[pos].Enqueue(new PendingColumn { earn = earn, isSelf = isSelf, basePos = new Vector2(local.x - anchorRef.x, local.y - anchorRef.y), ratio = ratio, sign = local.x < 0f ? -1f : 1f, }); } // ==================== 逐帧驱动 ==================== private void Update() { if (!_inited) return; for (int pos = 0; pos < _list.Length; pos++) { var list = _list[pos]; // ⭐ 每座位每帧最多补 1 根(复刻 PW CheckData,连杀才会排队上升,不能写成 while) if (_queue[pos].Count > 0 && list.Count < maxColumns) SpawnOne(pos); // 队头计时 if (list.Count > 0) { var head = list[0]; if (!head.fading) { head.holdRemaining -= Time.deltaTime; if (head.holdRemaining <= 0f) FadeOutHead(pos); } } } } private void SpawnOne(int pos) { var pending = _queue[pos].Dequeue(); var template = pending.isSelf ? _selfTemplate : _otherTemplate; var item = GetItem(template, pending.isSelf); var list = _list[pos]; item.isSelf = pending.isSelf; item.pos = pos; item.sign = pending.sign; item.basePos = pending.basePos; item.index = list.Count; item.fading = false; item.holdRemaining = float.MaxValue; // 非队头不倒计时 // 红/绿交替:取反后赋给红头(首根为绿,与 PW 一致) _redFlag[pos] = !_redFlag[pos]; bool showRed = _redFlag[pos]; if (item.headRed != null) item.headRed.SetActive(showRed); if (item.headGreen != null) item.headGreen.SetActive(!showRed); // 文本(原版 Lua: self.num:SetText(ChangeUnit(num))) SetNum(item, ChangeUnit(pending.earn)); // 位置:X = 座位位置 + 方向 × (首柱外偏 + 序号间距);Y = offsetY(0=贴面板底边,柱子往上长) // ⭐ 左右座位各一套偏移:sign<0=座位在面板左 → 无后缀那组;sign>0=右 → Right 那组 float offX = pending.sign < 0f ? offsetX : offsetXRight; float dltX = pending.sign < 0f ? deltaX : deltaXRight; float offY = pending.sign < 0f ? offsetY : offsetYRight; float x = pending.basePos.x + pending.sign * (offX + item.index * dltX); float y = offY; item.rt.anchoredPosition = new Vector2(x, y); // 高度:默认沿用 PW 公式 score=earn/炮倍 → mul=score/scoreDivisor → 封顶归一化; // fullValue>0 时改用「满格金额」(earn/fullValue) 归一化,便于快速调参 float score = pending.earn / pending.ratio; float mul = score / Mathf.Max(0.0001f, scoreDivisor); float norm = fullValue > 0 ? Mathf.Clamp01((float)pending.earn / fullValue) : Mathf.Clamp01(Mathf.Min(mul, heightCap) / Mathf.Max(1f, heightCap)); float panelH = _panelRoot.rect.height; if (panelH <= 1f) panelH = Screen.height; // 兜底:面板高度取不到时用屏幕高 float maxBarHeight = maxBarHeightOverride > 0f ? maxBarHeightOverride : panelH * barHeightToPanelRatio; float aimHeight = norm * maxBarHeight; // 诊断:高度不变化时看这行数值 Debug.Log($"[GoldColumn] pos={pos} earn={pending.earn} ratio={pending.ratio} score={score:0.##} " + $"mul={mul:0.##} norm={norm:0.####} panelH={panelH:0.#} maxBarH={maxBarHeight:0.#} aimH={aimHeight:0.#} " + $"barW={_barWidth} text={item.numText != null} tmp={item.numTmp != null}"); // 生长(动画 sizeDelta.y,不能动 fillAmount:柱头/数字锚在父顶边,靠父变高上移) // ⭐ 尺寸/位移用各自独立的 tween:补位平移时只打断位移,绝不动尺寸,否则柱高会卡在半途 item.sizeTween?.Kill(); item.moveTween?.Kill(); item.alphaTween?.Kill(); item.moveTween = null; item.rt.sizeDelta = new Vector2(_barWidth, 0f); item.alpha = 0f; ApplyAlpha(item, 0f); item.sizeTween = item.rt.DOSizeDelta(new Vector2(_barWidth, aimHeight), growDuration).SetEase(Ease.OutCubic); item.alphaTween = DOTween.To(() => item.alpha, a => { item.alpha = a; ApplyAlpha(item, a); }, 1f, growDuration) .SetEase(Ease.OutQuad); list.Add(item); // 单根独占时走 holdDuration(PW:EnterAction 里 index==1 → 2 秒) if (item.index == 0) item.holdRemaining = holdDuration; } /// 队头淡出 → 回收 → 其余柱整体向座位平移补位。 private void FadeOutHead(int pos) { var list = _list[pos]; if (list.Count == 0) return; var head = list[0]; if (head.fading) return; // 已在淡出中 // ⭐ 队头淡出期间仍留在列表里占 0 号位:这 0.3s 内新柱的序号从 1 起, // 不会生成到 0 号位跟正在淡出的柱重叠;等淡完(复刻 PW onEnd)再从列表移除 + 回收 + 补位 head.fading = true; head.sizeTween?.Kill(); head.moveTween?.Kill(); head.alphaTween?.Kill(); head.alphaTween = DOTween.To(() => head.alpha, a => { head.alpha = a; ApplyAlpha(head, a); }, 0f, fadeDuration) .SetEase(Ease.InQuad) .OnComplete(() => { var l = _list != null && pos < _list.Length ? _list[pos] : null; if (l != null) { if (l.Count > 0 && l[0] == head) l.RemoveAt(0); else l.Remove(head); } RecycleItem(head); ShiftAfterHead(pos); }); } /// 队头淡出完成后:剩余柱按新序号补位(用 basePos 绝对定位,避免多次补位误差累积)。 private void ShiftAfterHead(int pos) { var list = _list[pos]; if (list == null) return; for (int i = 0; i < list.Count; i++) { var item = list[i]; item.index = i; // ⭐ 与 SpawnOne 同规则:按该柱所在座位的左/右(sign)选对应偏移 float offX = item.sign < 0f ? offsetX : offsetXRight; float dltX = item.sign < 0f ? deltaX : deltaXRight; float targetX = item.basePos.x + item.sign * (offX + i * dltX); item.moveTween?.Kill(); item.moveTween = item.rt.DOAnchorPosX(targetX, shiftDuration).SetEase(Ease.Linear); } // 新队头停顿(PW:队伍数量 > 3 → 0,否则 crowdDelay) if (list.Count > 0) { var newHead = list[0]; newHead.fading = false; newHead.holdRemaining = list.Count > crowdThreshold ? 0f : crowdDelay; } } // ==================== 对象池 ==================== private GoldColumnItem GetItem(GameObject template, bool isSelf) { var pool = isSelf ? _poolSelf : _poolOther; GoldColumnItem item = null; if (pool.Count > 0) { item = pool[pool.Count - 1]; pool.RemoveAt(pool.Count - 1); } else { item = CreateItem(template); } item.go.SetActive(true); return item; } private GoldColumnItem CreateItem(GameObject template) { var go = Instantiate(template, _panelRoot); go.name = template.name + "_Clone"; var rt = go.transform as RectTransform; var numNode = FindChildByName(rt, "Num"); var item = new GoldColumnItem { go = go, rt = rt, rootImg = go.GetComponent(), headRed = FindChildByName(rt, "1")?.gameObject, headGreen = FindChildByName(rt, "2")?.gameObject, // ⭐ 原版预制体 Num 上是 UGUI Text(m_Text "999万"),不是 TMP;两者都取,谁有写谁 numText = numNode != null ? numNode.GetComponent() : null, numTmp = numNode != null ? numNode.GetComponent() : null, }; if (numNode != null && item.numText == null && item.numTmp == null) { // 兜底:Text/TMP 挂在 Num 的子物体上 item.numText = numNode.GetComponentInChildren(true); item.numTmp = numNode.GetComponentInChildren(true); } // ⭐ 模板为了让自己在编辑器里不露出来可能把 fillAmount 设小 → 强制整条可见;柱高全靠 sizeDelta.y if (item.rootImg != null) { item.rootImg.type = Image.Type.Simple; item.rootImg.fillAmount = 1f; } // ⭐ 柱是纯表现,不能挡棋盘点击 item.graphics = go.GetComponentsInChildren(true); item.origAlpha = new float[item.graphics.Length]; for (int i = 0; i < item.graphics.Length; i++) { item.graphics[i].raycastTarget = false; item.origAlpha[i] = item.graphics[i].color.a; } return item; } private void RecycleItem(GoldColumnItem item) { if (item == null) return; item.sizeTween?.Kill(); item.moveTween?.Kill(); item.alphaTween?.Kill(); item.moveTween = null; var pool = item.isSelf ? _poolSelf : _poolOther; if (pool.Count < preloadNum) { item.go.SetActive(false); item.fading = false; item.holdRemaining = float.MaxValue; pool.Add(item); } else { Destroy(item.go); } } private void DestroyItem(GoldColumnItem item) { if (item == null) return; item.sizeTween?.Kill(); item.moveTween?.Kill(); item.alphaTween?.Kill(); if (item.go != null) Destroy(item.go); } // ==================== 工具 ==================== /// 写 Num 文本(原版 Lua: self.num:SetText),UGUI Text 与 TMP 都兼容。 private static void SetNum(GoldColumnItem item, string text) { if (item == null) return; if (item.numText != null) item.numText.text = text; if (item.numTmp != null) item.numTmp.text = text; } /// 把 Graphic 的透明度按 a(0~1)乘回原值。 private void ApplyAlpha(GoldColumnItem item, float a) { if (item.graphics == null) return; for (int i = 0; i < item.graphics.Length; i++) { var g = item.graphics[i]; if (g == null) continue; var c = g.color; c.a = item.origAlpha[i] * a; g.color = c; } } /// 数字缩写(万/千/亿),移植自 PW_GoldColumn:ChangeUnit。 private static string ChangeUnit(long num) { if (num >= 10000000L) { double v = Math.Floor(num * 100.0 / 100000000.0) / 100.0; return v.ToString("0.00") + "亿"; } if (num >= 1000000L) { long v = (long)Math.Floor(num / 10000.0); return v + "万"; } if (num >= 100000L) { double v = Math.Floor(num * 10.0 / 10000.0) / 10.0; return v.ToString("0.0") + "万"; } if (num >= 10000L) { double v = Math.Floor(num * 100.0 / 10000.0) / 100.0; return v.ToString("0.00") + "万"; } if (num >= 1000L) { double v = Math.Floor(num * 100.0 / 1000.0) / 100.0; return v.ToString("0.00") + "千"; } return num.ToString(); } /// 递归按名字查找子物体。 private static Transform FindChildByName(Transform parent, string name) { if (parent == null || string.IsNullOrEmpty(name)) return null; if (parent.name == name) return parent; for (int i = 0; i < parent.childCount; i++) { var result = FindChildByName(parent.GetChild(i), name); if (result != null) return result; } return null; } /// 单个金币柱实例。 private class GoldColumnItem { public GameObject go; public RectTransform rt; public Image rootImg; public GameObject headRed; public GameObject headGreen; // Num 节点:原版预制体挂的是 UGUI Text(PW 的 SetText 就是它),这里同时兼容 TMP public Text numText; public TextMeshProUGUI numTmp; public Graphic[] graphics; public float[] origAlpha; public bool isSelf; public int pos; public int index; public float sign; public Vector2 basePos; public float holdRemaining; public bool fading; public float alpha; public Tween sizeTween; // 生长/尺寸 public Tween moveTween; // 补位平移 public Tween alphaTween; // 淡入/淡出 } }