using UniRx; using System.Collections.Generic; using UnityEngine; using System.IO; using asap.core; using System.Threading.Tasks; namespace PinballUncountable { public class PinballUncountablePlaySystem : MonoBehaviour, IComparer { [SerializeField] private PinballUncountableExitFxCtrl[] exits; [SerializeField] private float phyOffset = 0.5f; // 解析后的轨迹数据:ID -> 帧数组 private Dictionary _trajectoryData; // ID -> ExitNumber 映射表 private Dictionary _idToExitNumber; private Dictionary _idToHitCount; // 所有可用的 ID 列表,用于随机选择 private List _availableIds; // 物理球 -> 出口索引,用于回收时发布 EventExit private Dictionary _physicalBallToExitIdx; private readonly (ushort, ushort) EmptyHit = (0, 0); private void Awake() { var manager = GContext.container.Resolve(); manager.EventAggregator.GetEvent().Subscribe(RecyclePhysicalBall).AddTo(this); _trajectoryData = new Dictionary(); _idToExitNumber = new Dictionary(); _idToHitCount = new Dictionary(); _availableIds = new List(); _physicalBallToExitIdx = new Dictionary(); _objectPool = new Queue(InitialPoolCapacity); _activeBalls = new List(); _physicalObjectPool = new Queue(InitialPoolCapacity); _activePhysicalBalls = new List(); ParseBinaryData(); } #region 轨迹数据处理 [Header("Data")] [Tooltip("拖入 trajectories.bin 二进制文件")] public TextAsset binaryData; /// /// 解析二进制文件,将轨迹数据缓存到内存 /// private void ParseBinaryData() { if (binaryData == null || binaryData.bytes == null || binaryData.bytes.Length == 0) { Debug.LogError("[BallReplaySystemBinary] binaryData is null or empty!"); return; } try { uint fileCount; using (var ms = new MemoryStream(binaryData.bytes)) using (var reader = new BinaryReader(ms)) { // 读取文件总数 (uint32, 4 bytes) fileCount = reader.ReadUInt32(); for (int i = 0; i < fileCount; i++) { // 读取记录头 ushort id = reader.ReadUInt16(); // ID (uint16, 2 bytes) byte exitNumber = reader.ReadByte(); // ExitNumber (uint8, 1 byte) uint frameCount = reader.ReadUInt32(); // FrameCount (uint32, 4 bytes) (ushort hit1, ushort hit2) discHitCount = (0, 0); // 读取帧数据 var frames = new PinballUncountableFrameData[frameCount]; for (int f = 0; f < frameCount; f++) { float posX = reader.ReadSingle(); // PositionX (float32, 4 bytes) float posY = reader.ReadSingle(); // PositionY (float32, 4 bytes) byte colliderType = reader.ReadByte(); // ColliderType (uint8, 1 byte) if (colliderType == 1) discHitCount.hit1++; else if (colliderType == 2) discHitCount.hit2++; frames[f] = new PinballUncountableFrameData( new Vector2(posX, posY), ConvertColliderType(colliderType) ); } // 存入字典 _trajectoryData[id] = frames; _availableIds.Add(id); _idToExitNumber[id] = exitNumber; _idToHitCount[id] = discHitCount; } } Debug.Log($"[BallReplaySystemBinary] Successfully parsed {fileCount} trajectories."); } catch (System.Exception e) { Debug.LogError($"[BallReplaySystemBinary] Failed to parse binary data: {e.Message}"); _trajectoryData.Clear(); _availableIds.Clear(); } } /// /// 将 ColliderType 转换为对应的字符串 /// private string ConvertColliderType(byte colliderType) { return colliderType switch { 1 => "Disc_01", 2 => "Disc_02", _ => null }; } #endregion #region 模拟球对象池 [Header("Prefab")] [Tooltip("小球预制体,必须包含 BallReplaySingleBinary 组件")] public PinballUncountableSimBall ballPrefab; public Vector2 OffScreenPosition = new Vector2(-1000, -1000); // 对象池:空闲的 BallReplaySingleBinary 组件 private Queue _objectPool; // 活跃对象列表 private List _activeBalls; // 对象池初始容量 private const int InitialPoolCapacity = 50; /// /// 从对象池获取或创建小球,并开始播放指定 ID 的轨迹 /// private (int hit1, int hit2, int exitIdx) SpawnAndPlayBall(ushort id) { PinballUncountableSimBall ball = GetBallFromPool(); _activeBalls.Add(ball); // 订阅回放完成回调,以便立即回收对象 ball.OnReplayFinished = RecycleBall; PinballUncountableFrameData[] frames = _trajectoryData[id]; // 从 LUT 获取 exit 编号(数据源 是 1-based,转换为 0-based) var exitIdx = _idToExitNumber.TryGetValue(id, out var exit) ? exit - 1 : 0; (ushort hit1, ushort hit2) = _idToHitCount.TryGetValue(id, out var hit) ? hit : EmptyHit; GContext.container.Resolve().OnBallCreate(hit1, hit2, exitIdx); ball.StartReplay(frames, exitIdx); return (hit1, hit2, exitIdx); } /// /// 回收单个完成回放的小球到对象池,并在对应 exit 生成物理小球 /// private async void RecycleBall(PinballUncountableSimBall ball, int exitIdx) { try { if (ball == null) return; // 移除回调避免内存泄漏 ball.OnReplayFinished = null; // 从活跃列表中移除 _activeBalls.Remove(ball); // 重置状态并入池 ball.gameObject.SetActive(false); await Awaiters.NextFrame; _objectPool.Enqueue(ball); ball.transform.position = OffScreenPosition; // Debug.Log($"[BallReplaySystemBinary] Recycled ball at exit {exitNumber}."); // 在对应的 exit 位置生成物理小球 SpawnAndPlayPhysicalBall(exitIdx); exits[exitIdx].TryPlayFx(); } catch (System.Exception e) { Debug.LogError(e); } } /// /// 从对象池获取小球组件,若池为空则动态扩容 /// private PinballUncountableSimBall GetBallFromPool() { if (_objectPool.Count > 0) { return _objectPool.Dequeue(); } // 池为空,动态扩容 GameObject go = Instantiate(ballPrefab.gameObject, transform); go.SetActive(false); PinballUncountableSimBall ball = go.GetComponent(); if (ball == null) { Debug.LogError("[BallReplaySystemBinary] ballPrefab does not contain BallReplaySingleBinary component!"); Destroy(go); return null; } return ball; } /// /// 回收所有活跃对象到对象池 /// private void RecycleAllActiveBalls() { foreach (var ball in _activeBalls) { if (ball != null) { ball.StopReplay(); ball.gameObject.SetActive(false); _objectPool.Enqueue(ball); } } _activeBalls.Clear(); } #endregion #region 物理球对象池 [Header("Prefab")] public Rigidbody2D physicalBallPrefab; private Queue _physicalObjectPool; // 活跃对象列表 private List _activePhysicalBalls; private readonly Vector2 _initialForce = Vector2.down; /// /// 从对象池获取或创建小球 /// private void SpawnAndPlayPhysicalBall(int exitNumber) { exitNumber = Mathf.Clamp(exitNumber, 0, exits.Length - 1); Rigidbody2D ball = GetPhysicalBallFromPool(exits[exitNumber].transform); ball.gameObject.SetActive(true); _activePhysicalBalls.Add(ball); _physicalBallToExitIdx[ball] = exitNumber; ball.AddForce(_initialForce, ForceMode2D.Impulse); ball.GetComponentInChildren().Clear(); } /// /// 回收单个完成回放的小球到对象池 /// public void RecyclePhysicalBall(Rigidbody2D ball) { if (ball == null) return; // 从活跃列表中移除 _activePhysicalBalls.Remove(ball); // 回收时发布球出口事件(保留 exit id) if (_physicalBallToExitIdx.TryGetValue(ball, out var exitIdx)) { _physicalBallToExitIdx.Remove(ball); var manager = GContext.container.Resolve(); manager.EventAggregator.Publish(new EventExit { ExitIdx = exitIdx }); } // 重置状态并入池 // ball.StopReplay(); ball.gameObject.SetActive(false); _physicalObjectPool.Enqueue(ball); ball.transform.position = Vector3.zero; // Debug.Log($"[BallReplaySystemBinary] Recycled physical ball {ball.name}."); } public void RecyclePhysicalBall(EventHitDeathZone e) { var ball = e.Ball.GetComponent(); RecyclePhysicalBall(ball); } /// /// 从对象池获取小球组件,若池为空则动态扩容 /// private Rigidbody2D GetPhysicalBallFromPool(Transform exit) { var targetPos = exit.position + Vector3.down * phyOffset; if (_physicalObjectPool.Count > 0) { var res = _physicalObjectPool.Dequeue(); res.transform.position = targetPos; return res; } // 池为空,动态扩容 GameObject go = Instantiate(physicalBallPrefab.gameObject, targetPos, Quaternion.identity); go.SetActive(false); if (!go.TryGetComponent(out var ball)) { Debug.LogError("[BallReplaySystemBinary] ballPrefab does not contain RigidBody2D component!"); Destroy(go); return null; } return ball; } #endregion private int _evEventId, _evRound, _evMultiplier, _evTotalCount, _evDiscHitCount1, _evDiscHitCount2; #region 播放控制 /// /// 根据 ID 数组播放指定的小球轨迹 /// /// 小球记录 ID 数组 public async void Play(int[] ids) { try { if (ids == null || ids.Length == 0) { Debug.LogWarning("[BallReplaySystemBinary] Play() called with null or empty ids array."); return; } if (_trajectoryData == null || _trajectoryData.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available."); return; } // 回收所有活跃对象 // RecycleAllActiveBalls(); List validIds = new List(); List invalidIds = new List(); foreach (int id in ids) { if (id < 0 || id > ushort.MaxValue) { invalidIds.Add((ushort)id); continue; } ushort shortId = (ushort)id; if (_trajectoryData.ContainsKey(shortId)) { validIds.Add(shortId); } else { invalidIds.Add(shortId); } } // 输出不存在的 ID 警告 if (invalidIds.Count > 0) { Debug.LogWarning($"[BallReplaySystemBinary] The following IDs do not exist in binary data: {string.Join(", ", invalidIds)}"); } // 如果没有有效 ID,直接返回 if (validIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No valid IDs to play."); return; } // 随机播放有效 ID var manager = GContext.container.Resolve(); validIds.Sort(this); _evTotalCount = 0; _evDiscHitCount1 = 0; _evDiscHitCount2 = 0; foreach (ushort id in validIds) { var (disc1, disc2, exitIdx) = SpawnAndPlayBall(id); _evTotalCount += manager.TableContext.GetPointMagFromExitIndex(exitIdx); _evDiscHitCount1 += disc1; _evDiscHitCount2 += disc2; await Task.Delay(System.TimeSpan.FromSeconds(1f / 25)); } _evEventId = manager.TableContext.EventId; _evRound = manager.TableContext.GetRoundFromScore(manager.Score); _evMultiplier = validIds.Count; // Debug.Log($"[PinballUncountable] EventTracking: ball"); // Debug.Log($"[PinballUncountable] EventId: {_evEventId}"); // Debug.Log($"[PinballUncountable] Round: {_evRound}"); // Debug.Log($"[PinballUncountable] Multiplier: {_evMultiplier}"); // Debug.Log($"[PinballUncountable] TotalCount: {_evTotalCount}"); // Debug.Log($"[PinballUncountable] DiscHitCount1: {_evDiscHitCount1}"); // Debug.Log($"[PinballUncountable] DiscHitCount2: {_evDiscHitCount2}"); #if AGG using (var e = GEvent.GameEvent("event_pinballuncountable")) { e.AddContent("event_id", _evEventId) .AddContent("round", _evRound) .AddContent("multiplier", _evMultiplier) .AddContent("total_count", _evTotalCount) .AddContent("disc_hit_count1", _evDiscHitCount1) .AddContent("disc_hit_count2", _evDiscHitCount2); } #endif } catch (System.Exception e) { Debug.LogError(e); } } public void PlayId(int id) { if (_availableIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available for PlaySingle."); return; } Play(new int[] { id }); } /// /// 随机播放 1 个小球轨迹 /// public void PlaySingle() { if (_availableIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available for PlaySingle."); return; } int randomIndex = Random.Range(0, _availableIds.Count); ushort id = _availableIds[randomIndex]; Play(new int[] { id }); } /// /// 随机播放 20 个小球轨迹(如果总数不足 20,则播放全部) /// public void PlayTwenty() { if (_availableIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available for PlayTwenty."); return; } int count = Mathf.Min(20, _availableIds.Count); // 如果总数不足20,直接播放全部 if (_availableIds.Count <= 20) { PlayAll(); return; } // 随机选择 20 个不重复的 ID List shuffledIds = new List(_availableIds); ShuffleList(shuffledIds); int[] ids = new int[count]; for (int i = 0; i < count; i++) { ids[i] = shuffledIds[i]; } Play(ids); } /// /// 随机播放 20 个小球轨迹(如果总数不足 20,则播放全部) /// public void PlayHundred() { if (_availableIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available."); return; } int count = Mathf.Min(100, _availableIds.Count); // 如果总数不足20,直接播放全部 if (_availableIds.Count <= 100) { PlayAll(); return; } List shuffledIds = new List(_availableIds); ShuffleList(shuffledIds); int[] ids = new int[count]; for (int i = 0; i < count; i++) { ids[i] = shuffledIds[i]; } Play(ids); } /// /// 播放所有小球轨迹 /// public void PlayAll() { if (_availableIds.Count == 0) { Debug.LogWarning("[BallReplaySystemBinary] No trajectory data available for PlayAll."); return; } int[] ids = new int[_availableIds.Count]; for (int i = 0; i < _availableIds.Count; i++) { ids[i] = _availableIds[i]; } Play(ids); } /// /// Fisher-Yates 洗牌算法 /// private void ShuffleList(List list) { int n = list.Count; for (int i = n - 1; i > 0; i--) { int j = Random.Range(0, i + 1); (list[i], list[j]) = (list[j], list[i]); } } #endregion private void OnDestroy() { // 清理引用 // Destroy all sim balls in the pool while (_objectPool?.Count > 0) { var ball = _objectPool.Dequeue(); if (ball != null) Destroy(ball.gameObject); } // Destroy all active sim balls foreach (var ball in _activeBalls) { if (ball != null) Destroy(ball.gameObject); } // Destroy all physical balls in the pool while (_physicalObjectPool?.Count > 0) { var ball = _physicalObjectPool.Dequeue(); if (ball != null) Destroy(ball.gameObject); } // Destroy all active physical balls foreach (var ball in _activePhysicalBalls) { if (ball != null) Destroy(ball.gameObject); } // Clear collections _objectPool?.Clear(); _activeBalls?.Clear(); _physicalObjectPool?.Clear(); _activePhysicalBalls?.Clear(); _trajectoryData?.Clear(); _idToExitNumber?.Clear(); _idToHitCount?.Clear(); _availableIds?.Clear(); _physicalBallToExitIdx?.Clear(); } public int Compare(ushort x, ushort y) { return Random.value > 0.5f ? -1 : 1; } } }