Timeline UI

This commit is contained in:
ThomasCharester committed 2026-10-27 20:47:16 +03:00
1 parent c8626a9806
commit 2697212f05
177 files changed
+52513 -41627

No files matched your search

@@ -24,12 +24,12 @@ namespace Code.OfficeGame.BattleMode.Actions.Attacks
_attackData = (AttackActionConfig) attackData.Configs.SingleOrDefault(x=>x.ActionType == ActionType);
}
public override async UniTask<BattleActionDecisionResultCode> DecideAction()
public override async UniTask<BattleActionDecisionResult> DecideAction()
{
_currentFrame = 0;
Finished = false;
return BattleActionDecisionResultCode.Decided;
return new() { ResultCode = BattleActionDecisionResultCode.Decided, FrameDuration = _attackData.TotalFrames};
}
public override BattleActionType ActionType => BattleActionType.PlayerAttackMMA;
@@ -66,7 +66,6 @@ namespace Code.OfficeGame.BattleMode.Actions.Attacks
Debug.Log($"Попадание в {enemyCollider.name} на {_currentFrame}-м кадре! Урон: {_attackData.Damage}");
var entityActor = enemyCollider.GetComponentInParent<EntityTimelineActor>();
entityActor.ForceDecideAction(BattleActionType.Stugger);
}
public void CompleteOrInterruptAction()
@@ -7,7 +7,7 @@ namespace Code.OfficeGame.BattleMode.Actions
{
public bool Finished { get; protected set; }
public abstract void Tick(float tickDelta);
public abstract UniTask<BattleActionDecisionResultCode> DecideAction();
public abstract UniTask<BattleActionDecisionResult> DecideAction();
public abstract BattleActionType ActionType { get; }
}
}
@@ -0,0 +1,8 @@
namespace Code.OfficeGame.BattleMode.Actions
{
public struct BattleActionDecisionResult
{
public BattleActionDecisionResultCode ResultCode;
public int FrameDuration;
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 392d78f7e24c4a4bbceea3776e00272c
timeCreated: 1793113452
@@ -2,6 +2,7 @@ using System.Threading;
using Code.Game.Gameplay.Player;
using Code.Infrastructure.Camera;
using Code.Infrastructure.Input;
using Code.OfficeGame.BattleMode.Timeline;
using Cysharp.Threading.Tasks;
using KinematicCharacterController;
using Reflex.Attributes;
@@ -129,7 +130,7 @@ namespace Code.OfficeGame.BattleMode.Actions
private CancellationTokenSource _waitForEnterTokenSource;
private BattleActionDecisionResultCode _decisionResultCode;
public override async UniTask<BattleActionDecisionResultCode> DecideAction()
public override async UniTask<BattleActionDecisionResult> DecideAction()
{
_waitForEnterTokenSource = new CancellationTokenSource();
_decisionResultCode = BattleActionDecisionResultCode.CancelDecision;
@@ -156,11 +157,13 @@ namespace Code.OfficeGame.BattleMode.Actions
Finished = _decisionResultCode != BattleActionDecisionResultCode.Decided;
return _decisionResultCode;
return new() { ResultCode = _decisionResultCode, FrameDuration = _totalFrames};
}
public override BattleActionType ActionType => BattleActionType.Move;
private int _totalFrames;
private void CancelPosition(InputAction.CallbackContext obj)
{
_decisionResultCode = BattleActionDecisionResultCode.CancelDecision;
@@ -180,12 +183,40 @@ namespace Code.OfficeGame.BattleMode.Actions
_corners = _path.corners;
_currentCornerIndex = _corners.Length > 1 ? 1 : 0;
_decisionResultCode = BattleActionDecisionResultCode.Decided;
_totalFrames = CalculatePathFrames(_corners);
}
else
{
_decisionResultCode = BattleActionDecisionResultCode.Deciding;
_totalFrames = 0;
}
else _decisionResultCode = BattleActionDecisionResultCode.Deciding;
_waitForEnterTokenSource?.Cancel();
}
private int CalculatePathFrames(Vector3[] corners)
{
if (corners == null || corners.Length < 2)
return 0;
float totalDistanceXZ = 0f;
for (int i = 1; i < corners.Length; i++)
{
Vector3 segment = corners[i] - corners[i - 1];
segment.y = 0f;
totalDistanceXZ += segment.magnitude;
}
float effectiveDistance = Mathf.Max(0f, totalDistanceXZ - StoppingDistance);
float stepPerFrame = _movementController.MaxStableMoveSpeed * BattleTimeline.TickDelta;
if (stepPerFrame <= 0.0001f)
return 0;
// Округляем вверх, так как последний неполный шаг тоже занимает 1 целый кадр
return Mathf.CeilToInt(effectiveDistance / stepPerFrame);
}
private void SelectPosition(InputAction.CallbackContext input)
{
var position = _cameraStateMachine.Camera.transform.position;
@@ -25,12 +25,13 @@ namespace Code.OfficeGame.BattleMode.Actions
_entityHitBoxes = GetComponent<EntityHitBoxes>();
_actionParameters = actionData.Configs.SingleOrDefault(x => x.ActionType == ActionType);
}
public override async UniTask<BattleActionDecisionResultCode> DecideAction()
public override async UniTask<BattleActionDecisionResult> DecideAction()
{
_currentFrame = 0;
Finished = false;
return BattleActionDecisionResultCode.Decided;
return new() { ResultCode = BattleActionDecisionResultCode.Decided, FrameDuration = _actionParameters.TotalFrames};
}
public override BattleActionType ActionType => BattleActionType.Stugger;
@@ -11,13 +11,17 @@ namespace Code.OfficeGame.BattleMode.Actions
if (_waitFrames <= 0) Finished = true;
}
public override async UniTask<BattleActionDecisionResultCode> DecideAction()
public override async UniTask<BattleActionDecisionResult> DecideAction()
{
Finished = false;
_waitFrames = 120;
return BattleActionDecisionResultCode.Decided;
return new()
{
ResultCode = BattleActionDecisionResultCode.Decided,
FrameDuration = _waitFrames
};
}
public override BattleActionType ActionType => BattleActionType.Wait;
@@ -1,6 +1,8 @@
using System.Collections.Generic;
using System.Threading;
using Code.OfficeGame.BattleMode.Timeline;
using Cysharp.Threading.Tasks;
using R3;
using Unity.VisualScripting;
using UnityEngine;
@@ -10,9 +12,27 @@ namespace Code.OfficeGame.BattleMode.Actions
{
public BattleAction CurrentAction { get; protected set;}
public bool Finished => CurrentAction == null || CurrentAction.Finished;
private readonly Subject<TimelineBattleAction> _onActionSet = new();
public Observable<TimelineBattleAction> OnActionSet => _onActionSet;
protected void SetCurrentAction(BattleAction action, int durationInFrames)
{
CurrentAction = action;
_onActionSet.OnNext(new TimelineBattleAction
{
EntityId = gameObject.GetEntityId(),
Frame = durationInFrames
});
}
private void OnDestroy()
{
_onActionSet.Dispose();
}
public abstract UniTask DecideAction(CancellationToken ctx);
public abstract void ForceDecideAction(BattleActionType actionType);
public abstract UniTaskVoid ForceDecideAction(BattleActionType actionType);
[DoNotSerialize] public Animator animator;
@@ -16,6 +16,8 @@ namespace Code.OfficeGame.BattleMode.Actions.Player
BattleActionType decidedAction = BattleActionType.None;
BattleActionDecisionResult decision = new();
while (decidedAction == BattleActionType.None || ctx.IsCancellationRequested)
{
Cursor.lockState = CursorLockMode.Confined;
@@ -23,29 +25,30 @@ namespace Code.OfficeGame.BattleMode.Actions.Player
await _battleActionDecisionPanel.Open(ctx);
decidedAction = await _battleActionDecisionPanel.GetDecision(ctx);
var decision = BattleActionDecisionResultCode.Deciding;
while (decidedAction != BattleActionType.None
&& decision == BattleActionDecisionResultCode.Deciding
&& !ctx.IsCancellationRequested)
{
Cursor.lockState = CursorLockMode.Locked;
await _battleActionDecisionPanel.Close(ctx);
decision = await BattleActions[decidedAction].DecideAction();
if (decision == BattleActionDecisionResultCode.CancelDecision)
if (decision.ResultCode == BattleActionDecisionResultCode.CancelDecision)
decidedAction = BattleActionType.None;
if (decision.ResultCode != BattleActionDecisionResultCode.Deciding)
break;
}
}
CurrentAction = BattleActions[decidedAction];
SetCurrentAction(BattleActions[decidedAction], decision.FrameDuration);
}
public override void ForceDecideAction(BattleActionType actionType)
public override async UniTaskVoid ForceDecideAction(BattleActionType actionType)
{
CurrentAction = BattleActions[actionType];
CurrentAction.DecideAction();
var decision = await CurrentAction.DecideAction();
SetCurrentAction(BattleActions[actionType], decision.FrameDuration);
}
}
}
@@ -10,10 +10,12 @@ using Code.Infrastructure.SceneLoader;
using Code.OfficeGame.BattleMode.Actions;
using Code.OfficeGame.BattleMode.Actions.Player;
using Code.OfficeGame.BattleMode.Timeline;
using Code.OfficeGame.BattleMode.UI;
using Code.OfficeGame.Player;
using Cysharp.Threading.Tasks;
using KinematicCharacterController;
using Reflex.Core;
using Reflex.Injectors;
using UnityEngine;
using UnityEngine.InputSystem;
@@ -28,7 +30,7 @@ namespace Code.Game.OfficeGame.GameStates
public override AppStates MyState => AppStates.BattleState;
private BattleTimeline _battleTimeline = new BattleTimeline();
private BattleTimeline _battleTimeline;
private CancellationTokenSource _battleTokenSource = new CancellationTokenSource();
public override async UniTask EnterState()
@@ -52,6 +54,8 @@ namespace Code.Game.OfficeGame.GameStates
{
_battleTokenSource = new();
_battleTimeline = ConstructorInjector.Construct(typeof(BattleTimeline), container) as BattleTimeline;
Physics.simulationMode = SimulationMode.Script;
KinematicCharacterSystem.EnsureCreation();
KinematicCharacterSystem.Settings.AutoSimulation = false;
@@ -66,8 +70,10 @@ namespace Code.Game.OfficeGame.GameStates
entity.animator.enabled = false;
}
}
_battleTimeline?.RunBattleLoopAsync(entities,_battleTokenSource.Token).Forget();
_battleTimeline.RunBattleLoopAsync(entities,_battleTokenSource.Token).Forget();
await container.Resolve<BattleTimelinePanel>().Open();
}
private void TogglePlayerBattleMode(Container container, bool enabled)
@@ -95,6 +101,7 @@ namespace Code.Game.OfficeGame.GameStates
var container = Context.SceneContainerManager.ScenesContainers[SceneId.Testing];
TogglePlayerBattleMode(container, true);
await container.Resolve<BattleTimelinePanel>().Close();
// T
var entities =
@@ -1,7 +1,9 @@
using System.Collections.Generic;
using System.Threading;
using Code.OfficeGame.BattleMode.Actions;
using Code.OfficeGame.BattleMode.UI;
using Cysharp.Threading.Tasks;
using R3;
using UnityEngine;
namespace Code.OfficeGame.BattleMode.Timeline
@@ -11,23 +13,53 @@ namespace Code.OfficeGame.BattleMode.Timeline
public const int TicksPerSecond = 60;
public const float TickDelta = 1f / TicksPerSecond;
public int CurrentBattleFrame { get; private set; }
private readonly ReactiveProperty<int> _currentBattleFrame = new(0);
public ReadOnlyReactiveProperty<int> CurrentBattleFrame => _currentBattleFrame;
private readonly BattleTimelinePanel _timelinePanel;
private List<EntityTimelineActor> _entities;
private DisposableBag _battleSubscriptions;
public async UniTask RunBattleLoopAsync(List<EntityTimelineActor> entities, CancellationToken ct)
public BattleTimeline(BattleTimelinePanel timelinePanel)
{
_timelinePanel = timelinePanel;
}
public async UniTask RunBattleLoopAsync(List<EntityTimelineActor> entities, CancellationToken ctx)
{
_entities = entities;
CurrentBattleFrame = 0;
_currentBattleFrame.Value = 0;
await DecideActions(_entities, ct);
// Контейнер подписок на время текущего боя (автоматически очистится при выходе из метода)
_battleSubscriptions = new DisposableBag();
while (!IsBattleOver() && !ct.IsCancellationRequested)
try
{
List<EntityTimelineActor> entitiesNeedOrders = await PlayTimelineUntilPause(ct);
_currentBattleFrame
.Subscribe(_timelinePanel.OnFrameTick)
.AddTo(ref _battleSubscriptions);
await DecideActions(entitiesNeedOrders, ct);
foreach (var entity in entities)
entity.OnActionSet
.Subscribe(action =>
{
// Переводим длительность действия в абсолютный кадр окончания на таймлайне
action.Frame += _currentBattleFrame.Value;
_timelinePanel.SetAction(action);
})
.AddTo(ref _battleSubscriptions);
await DecideActions(_entities, ctx);
while (!IsBattleOver() && !ctx.IsCancellationRequested)
{
List<EntityTimelineActor> entitiesNeedOrders = await PlayTimelineUntilPause(ctx);
await DecideActions(entitiesNeedOrders, ctx);
}
}
finally
{
_battleSubscriptions.Dispose();
}
}
@@ -65,28 +97,21 @@ namespace Code.OfficeGame.BattleMode.Timeline
private void SimulateSingleFrame(List<EntityTimelineActor> outReadyEntities)
{
CurrentBattleFrame++;
_currentBattleFrame.Value++;
foreach (var entity in _entities)
{
entity.CurrentAction.Tick(TickDelta);
}
foreach (var entity in _entities)
{
entity.animator.Update(TickDelta);
}
Physics.Simulate(TickDelta);
Physics.SyncTransforms();
foreach (var entity in _entities)
{
if (entity.Finished)
{
outReadyEntities.Add(entity);
}
}
Physics.Simulate(TickDelta);
Physics.SyncTransforms();
}
private bool IsBattleOver() => false;
@@ -1,29 +1,91 @@
using System.Collections.Generic;
using System.Threading;
using Code.Infrastructure.UI;
using Code.OfficeGame.BattleMode.Timeline;
using Cysharp.Threading.Tasks;
using LitMotion;
using LitMotion.Extensions;
using UnityEngine;
namespace Code.OfficeGame.BattleMode.UI
{
public class BattleTimelinePanel : UIPanel
{
[SerializeField] private int maxFrames = 250;
[SerializeField] private GameObject timelineKeyPrefab;
[SerializeField] private float duration = 0.35f;
[SerializeField] private BattleActionDecisionButton actionButtonPrefab;
[SerializeField] private RectTransform timelineItself;
// TODO Вырезать или пощадить
private readonly Dictionary<EntityId, int> _entityEndFrames = new();
private readonly Dictionary<EntityId, RectTransform> _entityKeys = new();
private int _lastKnownCurrentFrame;
private List<BattleActionDecisionButton> _actionButtons = new();
private MotionHandle _motionHandle;
public void SetAction(TimelineBattleAction action)
{
_entityEndFrames[action.EntityId] = action.Frame;
if (!_entityKeys.TryGetValue(action.EntityId, out RectTransform keyRect))
{
keyRect = (RectTransform)Instantiate(timelineKeyPrefab, timelineItself).transform;
_entityKeys[action.EntityId] = keyRect;
}
UpdateKeyPosition(keyRect, action.Frame - _lastKnownCurrentFrame);
}
public void OnFrameTick(int currentFrame)
{
_lastKnownCurrentFrame = currentFrame;
foreach (var (entityId, endFrame) in _entityEndFrames)
{
if (_entityKeys.TryGetValue(entityId, out RectTransform keyRect))
{
int remainingFrames = Mathf.Max(0, endFrame - currentFrame);
UpdateKeyPosition(keyRect, remainingFrames);
}
}
}
private void UpdateKeyPosition(RectTransform keyRect, int relativeFrame)
{
var rectWidth = timelineItself.rect.width;
var frameSize = rectWidth / maxFrames;
keyRect.anchoredPosition = new Vector2(frameSize * relativeFrame - rectWidth / 2f, 0f);
}
public override async UniTask Open(CancellationToken cts = default)
{
gameObject.SetActive(true);
float startY = -((RectTransform)transform).rect.height;
float targetY = 0f;
_motionHandle = LMotion.Create(startY, targetY, duration)
.WithEase(Ease.OutCubic)
.WithScheduler(MotionScheduler.UpdateIgnoreTimeScale)
.BindToAnchoredPositionY((RectTransform)transform);
await _motionHandle.ToUniTask(cts);
}
public override async UniTask Close(CancellationToken cts = default)
{
float startY = 0;
float targetY = ((RectTransform)transform).rect.height;
_motionHandle = LMotion.Create(startY, targetY, duration)
.WithEase(Ease.InBack)
.WithScheduler(MotionScheduler.UpdateIgnoreTimeScale)
.BindToAnchoredPositionY((RectTransform)transform);
await _motionHandle.ToUniTask(cts);
gameObject.SetActive(false);
}
}
}