Files
5.24BuYu/Assets/MyGame/aa/UI/Game/XJMH/GoldColumnController.cs
T
2026-10-07 05:28:36 +08:00

543 lines
21 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 星际迷航(XJMH)金币柱控制器:座位每次加金币时,在座位炮台旁生成一根从下往上生长的金币柱,
/// 柱头红/绿交替、顶部显示本次得分(万/千/亿缩写),多根并排最多 maxColumns 根。
///
/// 移植自 PW 小游戏的 PW_GoldColumn / PW_GoldColumnMgr:
/// - 每座位一个队列,每帧最多补 1 根(连杀时排队上升);
/// - 首根绿头(与 PW 的 ColumnType 初值 true 取反一致),之后红绿交替;
/// - 队头停留 holdDuration 后淡出,其余整体向座位方向平移补位;
/// - 队伍数量 > crowdThreshold 时队头不再额外停顿(快速滚动)。
///
/// 模板:面板根节点下已放好的 PW_GoldColumn_Self / PW_GoldColumn_Other 两个实例,
/// 由 Init 自动查找并隐藏,运行时克隆复用。
/// </summary>
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<GoldColumnItem> _poolSelf = new List<GoldColumnItem>();
private readonly List<GoldColumnItem> _poolOther = new List<GoldColumnItem>();
private readonly List<GoldColumnItem>[] _list = new List<GoldColumnItem>[4];
private readonly Queue<PendingColumn>[] _queue = new Queue<PendingColumn>[4];
// 红/绿交替标记:初值 true,取柱时先取反 → 首根绿头(与 PW 一致)
private readonly bool[] _redFlag = new bool[4];
private float _barWidth = 25f;
private bool _inited;
/// <summary>待生成的柱(入队时就把座位位置/炮倍固化,避免座位被销毁后的空引用)</summary>
private struct PendingColumn
{
public long earn;
public bool isSelf;
public Vector2 basePos; // 座位在面板本地(锚点 0.5,0)的锚点坐标
public float ratio; // 炮倍(>=1)
public float sign; // 堆叠方向:座位在面板左 → -1,右 → +1
}
// ==================== 初始化 / 清理 ====================
/// <summary>绑定面板根节点并查找两个模板(自动 SetActive(false))。</summary>
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<GoldColumnItem>();
_queue[i] = new Queue<PendingColumn>();
_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.#}");
}
/// <summary>退房/隐藏时调用:杀掉所有动画、回收/销毁全部柱、清空队列(模板与控制器保留,可继续复用)。</summary>
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();
}
// ==================== 对外接口 ====================
/// <summary>座位加金币时调用(入队,由 Update 逐帧限流生成)。</summary>
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;
}
/// <summary>队头淡出 → 回收 → 其余柱整体向座位平移补位。</summary>
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);
});
}
/// <summary>队头淡出完成后:剩余柱按新序号补位(用 basePos 绝对定位,避免多次补位误差累积)。</summary>
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<Image>(),
headRed = FindChildByName(rt, "1")?.gameObject,
headGreen = FindChildByName(rt, "2")?.gameObject,
// ⭐ 原版预制体 Num 上是 UGUI Text(m_Text "999万"),不是 TMP;两者都取,谁有写谁
numText = numNode != null ? numNode.GetComponent<Text>() : null,
numTmp = numNode != null ? numNode.GetComponent<TextMeshProUGUI>() : null,
};
if (numNode != null && item.numText == null && item.numTmp == null)
{
// 兜底:Text/TMP 挂在 Num 的子物体上
item.numText = numNode.GetComponentInChildren<Text>(true);
item.numTmp = numNode.GetComponentInChildren<TextMeshProUGUI>(true);
}
// ⭐ 模板为了让自己在编辑器里不露出来可能把 fillAmount 设小 → 强制整条可见;柱高全靠 sizeDelta.y
if (item.rootImg != null)
{
item.rootImg.type = Image.Type.Simple;
item.rootImg.fillAmount = 1f;
}
// ⭐ 柱是纯表现,不能挡棋盘点击
item.graphics = go.GetComponentsInChildren<Graphic>(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);
}
// ==================== 工具 ====================
/// <summary>写 Num 文本(原版 Lua: self.num:SetText),UGUI Text 与 TMP 都兼容。</summary>
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;
}
/// <summary>把 Graphic 的透明度按 a(0~1)乘回原值。</summary>
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;
}
}
/// <summary>数字缩写(万/千/亿),移植自 PW_GoldColumn:ChangeUnit。</summary>
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();
}
/// <summary>递归按名字查找子物体。</summary>
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;
}
/// <summary>单个金币柱实例。</summary>
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; // 淡入/淡出
}
}