456 lines
17 KiB
C#
456 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using Code.Game.Entities;
|
|
using Code.Game.OfficeGame.BattleMode;
|
|
using Code.Infrastructure.Camera;
|
|
using Code.OfficeGame.BattleMode.Actions;
|
|
using Code.OfficeGame.BattleMode.Actions.Deciders;
|
|
using Code.OfficeGame.BattleMode.Snapshots;
|
|
using Code.OfficeGame.BattleMode.UI;
|
|
using Cysharp.Threading.Tasks;
|
|
using R3;
|
|
using UnityEngine;
|
|
|
|
namespace Code.OfficeGame.BattleMode.Timeline
|
|
{
|
|
public class BattleTimeline
|
|
{
|
|
public const int TicksPerSecond = 60;
|
|
public const float TickDelta = 1f / TicksPerSecond;
|
|
|
|
private readonly ReactiveProperty<int> _currentBattleFrame = new(0);
|
|
public ReadOnlyReactiveProperty<int> CurrentBattleFrame => _currentBattleFrame;
|
|
|
|
private readonly BattleTimelinePanel _timelinePanel;
|
|
private DisposableBag _battleSubscriptions;
|
|
|
|
private Dictionary<BattleActionType, BattleAction> _battleActions;
|
|
private Dictionary<DeciderType, BattleActionDecider> _battleActionDeciders;
|
|
|
|
private Dictionary<EntityId, EntityProperties> _entities;
|
|
private Dictionary<EntityId, BattleActionData> _battleActionDatas = new();
|
|
private Dictionary<EntityId, BattleActionDecisionResult> _actionDecisionResults = new();
|
|
|
|
private SnapshotUtilityNonStatic _snapshotUtility = new();
|
|
|
|
private BattleActionDecisionPanel _battleActionDecisionPanel;
|
|
private CameraStateMachine _cameraStateMachine;
|
|
|
|
private SemaphoreSlim _preview = new(1,1);
|
|
|
|
public BattleTimeline(
|
|
BattleTimelinePanel timelinePanel,
|
|
IEnumerable<BattleAction> battleActions,
|
|
IEnumerable<BattleActionDecider> battleActionDeciders,
|
|
BattleActionDecisionPanel battleActionDecisionPanel,
|
|
CameraStateMachine cameraStateMachine)
|
|
{
|
|
_cameraStateMachine = cameraStateMachine;
|
|
_battleActionDecisionPanel = battleActionDecisionPanel;
|
|
_timelinePanel = timelinePanel;
|
|
|
|
_battleActions = new();
|
|
foreach (var battleAction in battleActions)
|
|
{
|
|
battleAction.SetBattleTimeline(this);
|
|
_battleActions.Add(battleAction.ActionType, battleAction);
|
|
}
|
|
|
|
_battleActionDeciders = battleActionDeciders.ToDictionary(x => x.DeciderType);
|
|
}
|
|
|
|
public async UniTask RunBattleLoopAsync(List<EntityProperties> entities, CancellationToken ctx)
|
|
{
|
|
_entities = entities.ToDictionary(x => x.GetEntityId());
|
|
_currentBattleFrame.Value = 0;
|
|
|
|
_battleSubscriptions = new DisposableBag();
|
|
|
|
try
|
|
{
|
|
_currentBattleFrame
|
|
.Subscribe(_timelinePanel.OnFrameTick)
|
|
.AddTo(ref _battleSubscriptions);
|
|
|
|
await DecideActions(_entities.Keys.ToArray(), ctx);
|
|
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
EntityId[] entitiesNeedOrders = await PlayTimelineUntilPause(ctx);
|
|
|
|
await DecideActions(entitiesNeedOrders, ctx);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_battleSubscriptions.Dispose();
|
|
}
|
|
}
|
|
|
|
private async UniTask<EntityId[]> PlayTimelineUntilPause(CancellationToken ctx)
|
|
{
|
|
float accumulator = 0f;
|
|
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
await UniTask.Yield(PlayerLoopTiming.Update, ctx);
|
|
|
|
accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f);
|
|
|
|
while (accumulator >= TickDelta)
|
|
{
|
|
accumulator -= TickDelta;
|
|
|
|
_currentBattleFrame.Value++;
|
|
|
|
var readyEntities = SimulateSingleFrame(_entities);
|
|
|
|
if (readyEntities.Length > 0)
|
|
{
|
|
foreach (var entityId in readyEntities)
|
|
{
|
|
Debug.Log($"Entity {entityId} is real ready");
|
|
|
|
_battleActions[_actionDecisionResults[entityId].ActionType]
|
|
.Finish(
|
|
_entities[entityId],
|
|
_battleActionDatas[entityId],
|
|
_actionDecisionResults[entityId]);
|
|
|
|
_battleActionDatas.Remove(entityId);
|
|
_actionDecisionResults.Remove(entityId);
|
|
}
|
|
|
|
return readyEntities;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// TODO ЗАПУСКАТЬ ДВА ЦИКЛА ПОЛНЫЙ И ДО ПРЕРЫВАНИЯ
|
|
private async UniTask PreviewBattleLoopAsync(int frameCount,
|
|
Dictionary<EntityId, EntityProperties> entities,
|
|
CancellationToken ctx)
|
|
{
|
|
if (frameCount <= 0)
|
|
{
|
|
_preview.Release(1);
|
|
throw new Exception("FrameCount must be > 0");
|
|
}
|
|
|
|
_snapshotUtility.GetSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
try
|
|
{
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
_snapshotUtility.RestoreFromSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
await PlayTimelineForCertainFrames(frameCount, ctx).AttachExternalCancellation(ctx);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_snapshotUtility.RestoreFromSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
foreach (var (_, entity) in entities)
|
|
{
|
|
entity.EntityPreviewVisuals.HidePreviewState();
|
|
}
|
|
|
|
_preview.Release(1);
|
|
}
|
|
}
|
|
private async UniTask PreviewBattleLoopAsync(EntityId entityId,
|
|
Dictionary<EntityId, EntityProperties> entities,
|
|
CancellationToken ctx)
|
|
{
|
|
_snapshotUtility.GetSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
try
|
|
{
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
_snapshotUtility.RestoreFromSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
await PlayTimelineUntilThatSpecific(entityId, ctx).AttachExternalCancellation(ctx);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_snapshotUtility.RestoreFromSnapshots(
|
|
entities,
|
|
_actionDecisionResults,
|
|
_battleActionDatas);
|
|
|
|
foreach (var (_, entity) in entities)
|
|
{
|
|
entity.EntityPreviewVisuals.HidePreviewState();
|
|
}
|
|
|
|
_preview.Release(1);
|
|
}
|
|
}
|
|
|
|
private async UniTask PlayTimelineForCertainFrames(int frameCount, CancellationToken ctx)
|
|
{
|
|
float accumulator = 0f;
|
|
int counter = frameCount;
|
|
|
|
while (counter > 0 && !ctx.IsCancellationRequested)
|
|
{
|
|
bool isCanceled = await UniTask.Yield(PlayerLoopTiming.Update, ctx).SuppressCancellationThrow();
|
|
if (isCanceled) break;
|
|
|
|
accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f);
|
|
|
|
while (accumulator >= TickDelta && counter > 0 && !ctx.IsCancellationRequested)
|
|
{
|
|
// Debug.Log($"Simulating {counter}/{frameCount}");
|
|
accumulator -= TickDelta;
|
|
counter--;
|
|
|
|
var readyEntities = SimulateSingleFrame(_entities);
|
|
|
|
if (readyEntities.Length > 0)
|
|
{
|
|
foreach (var entityId in readyEntities)
|
|
{
|
|
var battleActionType = _actionDecisionResults[entityId].ActionType;
|
|
Debug.Log($"Entity {entityId} is preview ready {battleActionType}");
|
|
|
|
_battleActions[battleActionType]
|
|
.Finish(
|
|
_entities[entityId],
|
|
_battleActionDatas[entityId],
|
|
_actionDecisionResults[entityId]);
|
|
|
|
_battleActionDatas.Remove(entityId);
|
|
_actionDecisionResults.Remove(entityId);
|
|
}
|
|
// return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private async UniTask PlayTimelineUntilThatSpecific(EntityId specificEntityId, CancellationToken ctx)
|
|
{
|
|
float accumulator = 0f;
|
|
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
bool isCanceled = await UniTask.Yield(PlayerLoopTiming.Update, ctx).SuppressCancellationThrow();
|
|
if (isCanceled) break;
|
|
|
|
accumulator += Mathf.Min(Time.unscaledDeltaTime, 0.1f);
|
|
|
|
while (accumulator >= TickDelta && !ctx.IsCancellationRequested)
|
|
{
|
|
// Debug.Log($"Simulating {counter}/{frameCount}");
|
|
accumulator -= TickDelta;
|
|
|
|
var readyEntities = SimulateSingleFrame(_entities);
|
|
|
|
if (readyEntities.Length > 0)
|
|
{
|
|
foreach (var entityId in readyEntities)
|
|
{
|
|
var battleActionType = _actionDecisionResults[entityId].ActionType;
|
|
Debug.Log($"Entity {entityId} is preview ready {battleActionType}");
|
|
|
|
_battleActions[_actionDecisionResults[entityId].ActionType]
|
|
.Finish(
|
|
_entities[entityId],
|
|
_battleActionDatas[entityId],
|
|
_actionDecisionResults[entityId]);
|
|
|
|
_battleActionDatas.Remove(entityId);
|
|
_actionDecisionResults.Remove(entityId);
|
|
}
|
|
if(readyEntities.Contains(specificEntityId)) return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private EntityId[] SimulateSingleFrame(Dictionary<EntityId, EntityProperties> inEntities)
|
|
{
|
|
foreach (var (entityId, entity) in inEntities)
|
|
{
|
|
if (!_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult)
|
|
|| !_battleActionDatas.TryGetValue(entityId, out var battleActionData))
|
|
continue;
|
|
|
|
_battleActions[actionDecisionResult.ActionType]
|
|
.Tick(
|
|
TickDelta,
|
|
entity,
|
|
actionDecisionResult,
|
|
battleActionData);
|
|
|
|
entity.animator.Update(TickDelta);
|
|
}
|
|
|
|
KinematicCharacterController.KinematicCharacterSystem.Simulate(
|
|
TickDelta,
|
|
KinematicCharacterController.KinematicCharacterSystem.CharacterMotors,
|
|
KinematicCharacterController.KinematicCharacterSystem.PhysicsMovers
|
|
);
|
|
|
|
Physics.Simulate(TickDelta);
|
|
Physics.SyncTransforms();
|
|
|
|
var readyEntities = _battleActionDatas
|
|
.Where(x => x.Value.Finished)
|
|
.Select(x => x.Key)
|
|
.ToArray();
|
|
|
|
return readyEntities;
|
|
}
|
|
|
|
private async UniTask DecideActions(EntityId[] entities, CancellationToken ctx)
|
|
{
|
|
foreach (var entityId in entities)
|
|
{
|
|
var (decisionResult, battleActionData) = await DecideAction(ctx, _entities[entityId]);
|
|
|
|
_actionDecisionResults[entityId] = decisionResult;
|
|
_battleActionDatas[entityId] = battleActionData;
|
|
}
|
|
}
|
|
|
|
private async UniTask<(BattleActionDecisionResult, BattleActionData)> DecideAction(CancellationToken ctx,
|
|
EntityProperties entity)
|
|
|
|
{
|
|
_battleActionDecisionPanel.SetEntityInfo(entity.battleActionTypes);
|
|
|
|
BattleActionType decidedActionType = BattleActionType.None;
|
|
|
|
BattleActionDecisionResult decisionResult = null;
|
|
BattleActionData actionData = null;
|
|
|
|
var entityId = entity.GetEntityId();
|
|
|
|
BattleActionData entityActionDataSnapshot = null;
|
|
BattleActionDecisionResult entityResultSnapshot = null;
|
|
|
|
if (_battleActionDatas.TryGetValue(entityId, out var battleActionData))
|
|
entityActionDataSnapshot = battleActionData.GetSnapshot();
|
|
|
|
if (_actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult))
|
|
{
|
|
entityResultSnapshot = actionDecisionResult.GetSnapshot();
|
|
_timelinePanel.SetPreviousActionText(entityResultSnapshot.ActionType.ToString());
|
|
}
|
|
|
|
while (decidedActionType == BattleActionType.None || ctx.IsCancellationRequested)
|
|
{
|
|
Cursor.lockState = CursorLockMode.Confined;
|
|
|
|
((CameraBattleModeState)_cameraStateMachine.CurrentBaseState).battleDeciding = true;
|
|
|
|
await _battleActionDecisionPanel.Open(ctx);
|
|
|
|
decidedActionType = await _battleActionDecisionPanel.GetDecision(ctx);
|
|
|
|
if (decidedActionType == BattleActionType.None) continue;
|
|
|
|
var deciderType = _battleActions[decidedActionType].DeciderType;
|
|
|
|
_battleActionDeciders[deciderType].MountDecider(entity);
|
|
|
|
CancellationTokenSource previewCancellationToken = new();
|
|
|
|
while (!ctx.IsCancellationRequested)
|
|
{
|
|
Cursor.lockState = CursorLockMode.Locked;
|
|
|
|
((CameraBattleModeState)_cameraStateMachine.CurrentBaseState).battleDeciding = false;
|
|
|
|
await _battleActionDecisionPanel.Close(ctx);
|
|
|
|
decisionResult = await _battleActionDeciders[deciderType].GetDecision();
|
|
decisionResult.ActionType = decidedActionType;
|
|
|
|
previewCancellationToken.Cancel();
|
|
previewCancellationToken.Dispose();
|
|
|
|
await _preview.WaitAsync(ctx);
|
|
|
|
_preview.Release();
|
|
|
|
actionData = _battleActions[decidedActionType].MountAndGetFrames(entity, decisionResult);
|
|
|
|
if (decisionResult.ResultCode == BattleActionDecisionResultCode.Deciding)
|
|
{
|
|
_actionDecisionResults[entityId] = decisionResult;
|
|
_battleActionDatas[entityId] = actionData;
|
|
|
|
previewCancellationToken = new();
|
|
|
|
await _preview.WaitAsync(previewCancellationToken.Token);
|
|
|
|
PreviewBattleLoopAsync(entityId, _entities, previewCancellationToken.Token)
|
|
.Forget();
|
|
}
|
|
else if (decisionResult.ResultCode == BattleActionDecisionResultCode.CancelDecision)
|
|
{
|
|
decidedActionType = BattleActionType.None;
|
|
_battleActionDatas.Remove(entityId);
|
|
_actionDecisionResults.Remove(entityId);
|
|
break;
|
|
}
|
|
else break;
|
|
}
|
|
|
|
_battleActionDeciders[deciderType].UnmountDecider();
|
|
}
|
|
|
|
_battleActionDatas[entityId] = entityActionDataSnapshot;
|
|
_actionDecisionResults[entityId] = entityResultSnapshot;
|
|
|
|
if (decisionResult == null || actionData == null) throw new ArgumentNullException(nameof(decisionResult));
|
|
|
|
return (decisionResult, actionData);
|
|
}
|
|
|
|
public void ForceDecideAction(
|
|
EntityId entityId,
|
|
BattleActionData forcedBattleActionData,
|
|
BattleActionDecisionResult forcedBattleActionDataResult)
|
|
{
|
|
if(_battleActionDatas.TryGetValue(entityId, out var battleActionData)
|
|
&& _actionDecisionResults.TryGetValue(entityId, out var actionDecisionResult))
|
|
_battleActions[_actionDecisionResults[entityId].ActionType]
|
|
.Finish(
|
|
_entities[entityId],
|
|
battleActionData,
|
|
actionDecisionResult);
|
|
|
|
_battleActionDatas[entityId] = forcedBattleActionData;
|
|
_actionDecisionResults[entityId] = forcedBattleActionDataResult;
|
|
|
|
Debug.Log($"Задано действие {forcedBattleActionDataResult.ActionType} для {entityId}");
|
|
}
|
|
}
|
|
} |