Files
ft/Client/Assets/Scripts/UI/PinballUncountable/PinballUncountablePlaySystem.cs
2026-06-29 21:18:33 +08:00

605 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 UniRx;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using asap.core;
using System.Threading.Tasks;
namespace PinballUncountable
{
public class PinballUncountablePlaySystem : MonoBehaviour, IComparer<ushort>
{
[SerializeField] private PinballUncountableExitFxCtrl[] exits;
[SerializeField] private float phyOffset = 0.5f;
// 解析后的轨迹数据ID -> 帧数组
private Dictionary<ushort, PinballUncountableFrameData[]> _trajectoryData;
// ID -> ExitNumber 映射表
private Dictionary<ushort, ushort> _idToExitNumber;
private Dictionary<ushort, (ushort hit1, ushort hit2)> _idToHitCount;
// 所有可用的 ID 列表,用于随机选择
private List<ushort> _availableIds;
// 物理球 -> 出口索引,用于回收时发布 EventExit
private Dictionary<Rigidbody2D, int> _physicalBallToExitIdx;
private readonly (ushort, ushort) EmptyHit = (0, 0);
private void Awake()
{
var manager = GContext.container.Resolve<Manager>();
manager.EventAggregator.GetEvent<EventHitDeathZone>().Subscribe(RecyclePhysicalBall).AddTo(this);
_trajectoryData = new Dictionary<ushort, PinballUncountableFrameData[]>();
_idToExitNumber = new Dictionary<ushort, ushort>();
_idToHitCount = new Dictionary<ushort, (ushort hit1, ushort hit2)>();
_availableIds = new List<ushort>();
_physicalBallToExitIdx = new Dictionary<Rigidbody2D, int>();
_objectPool = new Queue<PinballUncountableSimBall>(InitialPoolCapacity);
_activeBalls = new List<PinballUncountableSimBall>();
_physicalObjectPool = new Queue<Rigidbody2D>(InitialPoolCapacity);
_activePhysicalBalls = new List<Rigidbody2D>();
ParseBinaryData();
}
#region
[Header("Data")]
[Tooltip("拖入 trajectories.bin 二进制文件")]
public TextAsset binaryData;
/// <summary>
/// 解析二进制文件,将轨迹数据缓存到内存
/// </summary>
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();
}
}
/// <summary>
/// 将 ColliderType 转换为对应的字符串
/// </summary>
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<PinballUncountableSimBall> _objectPool;
// 活跃对象列表
private List<PinballUncountableSimBall> _activeBalls;
// 对象池初始容量
private const int InitialPoolCapacity = 50;
/// <summary>
/// 从对象池获取或创建小球,并开始播放指定 ID 的轨迹
/// </summary>
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<Manager>().OnBallCreate(hit1, hit2, exitIdx);
ball.StartReplay(frames, exitIdx);
return (hit1, hit2, exitIdx);
}
/// <summary>
/// 回收单个完成回放的小球到对象池,并在对应 exit 生成物理小球
/// </summary>
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);
}
}
/// <summary>
/// 从对象池获取小球组件,若池为空则动态扩容
/// </summary>
private PinballUncountableSimBall GetBallFromPool()
{
if (_objectPool.Count > 0)
{
return _objectPool.Dequeue();
}
// 池为空,动态扩容
GameObject go = Instantiate(ballPrefab.gameObject, transform);
go.SetActive(false);
PinballUncountableSimBall ball = go.GetComponent<PinballUncountableSimBall>();
if (ball == null)
{
Debug.LogError("[BallReplaySystemBinary] ballPrefab does not contain BallReplaySingleBinary component!");
Destroy(go);
return null;
}
return ball;
}
/// <summary>
/// 回收所有活跃对象到对象池
/// </summary>
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<Rigidbody2D> _physicalObjectPool;
// 活跃对象列表
private List<Rigidbody2D> _activePhysicalBalls;
private readonly Vector2 _initialForce = Vector2.down;
/// <summary>
/// 从对象池获取或创建小球
/// </summary>
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<TrailRenderer>().Clear();
}
/// <summary>
/// 回收单个完成回放的小球到对象池
/// </summary>
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>();
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<Rigidbody2D>();
RecyclePhysicalBall(ball);
}
/// <summary>
/// 从对象池获取小球组件,若池为空则动态扩容
/// </summary>
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<Rigidbody2D>(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
/// <summary>
/// 根据 ID 数组播放指定的小球轨迹
/// </summary>
/// <param name="ids">小球记录 ID 数组</param>
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<ushort> validIds = new List<ushort>();
List<ushort> invalidIds = new List<ushort>();
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<Manager>();
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 });
}
/// <summary>
/// 随机播放 1 个小球轨迹
/// </summary>
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 });
}
/// <summary>
/// 随机播放 20 个小球轨迹(如果总数不足 20则播放全部
/// </summary>
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<ushort> shuffledIds = new List<ushort>(_availableIds);
ShuffleList(shuffledIds);
int[] ids = new int[count];
for (int i = 0; i < count; i++)
{
ids[i] = shuffledIds[i];
}
Play(ids);
}
/// <summary>
/// 随机播放 20 个小球轨迹(如果总数不足 20则播放全部
/// </summary>
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<ushort> shuffledIds = new List<ushort>(_availableIds);
ShuffleList(shuffledIds);
int[] ids = new int[count];
for (int i = 0; i < count; i++)
{
ids[i] = shuffledIds[i];
}
Play(ids);
}
/// <summary>
/// 播放所有小球轨迹
/// </summary>
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);
}
/// <summary>
/// Fisher-Yates 洗牌算法
/// </summary>
private void ShuffleList<T>(List<T> 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;
}
}
}