605 lines
21 KiB
C#
605 lines
21 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|
||
|