app.hypergames.hypersdk 1.11.0-beta.9 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +15 -0
  3. package/HyperLoader.unity +2 -2
  4. package/Runtime/Internal/Bootstrap/HyperLoader.cs +5 -1
  5. package/Runtime/Internal/Core/HyperRuntime.cs +63 -1
  6. package/Runtime/Internal/Managers/BackendManager.cs +26 -3
  7. package/Runtime/Internal/Managers/DataManager.cs +8 -0
  8. package/Runtime/Internal/Managers/EventManager.cs +7 -1
  9. package/Runtime/Internal/Managers/GameManager.cs +1 -1
  10. package/Runtime/Internal/Managers/ReplayManager.cs +4 -1
  11. package/Runtime/Internal/Managers/TimeManager.cs +21 -6
  12. package/Runtime/Internal/Replay/ReplaySessionState.cs +15 -4
  13. package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +10 -0
  14. package/Runtime/Internal/UI/Components/ReplayController.cs +205 -27
  15. package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +1 -1
  16. package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs +18 -0
  17. package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs.meta +2 -0
  18. package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs +9 -1
  19. package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +1 -1
  20. package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +71 -2
  21. package/Runtime/Resources/Fonts/Wonder Boys/Wonder Boys.asset +394 -379
  22. package/Runtime/Resources/HyperSDKSettings.asset +2 -0
  23. package/Runtime/Resources/Sprites/Circle Loader/Property 1=1.png.meta +7 -7
  24. package/Runtime/Resources/Sprites/Circle Loader/Property 1=2.png.meta +7 -7
  25. package/Runtime/Resources/Sprites/Circle Loader/Property 1=3.png.meta +7 -7
  26. package/Runtime/Resources/Sprites/Circle Loader/Property 1=4.png.meta +7 -7
  27. package/Runtime/Resources/Sprites/Circle Loader/Property 1=5.png.meta +7 -7
  28. package/Runtime/Resources/UIPrefabs/ReplayController.prefab +295 -15
  29. package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +291 -6
  30. package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +1 -1
  31. package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +1 -1
  32. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +1 -1
  33. package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +9 -2
  34. package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +1 -0
  35. package/package.json +1 -1
  36. /package/Runtime/{Public → Shared}/PlayerDeviceClassification.cs +0 -0
  37. /package/Runtime/{Public → Shared}/PlayerDeviceClassification.cs.meta +0 -0
@@ -12,35 +12,59 @@ namespace Hyper.Internal
12
12
  {
13
13
  internal const int AvatarTextureSize = 128;
14
14
 
15
- [SerializeField] private Slider progressSlider;
15
+ [SerializeField] private Slider seekableSlider;
16
+ [SerializeField] private Slider readOnlySlider;
16
17
  [SerializeField] private TextMeshProUGUI replayLabel;
17
18
  [SerializeField] private RawImage playerAvatar;
18
19
  [SerializeField] private Button pauseButton;
19
20
  [SerializeField] private Button playbackSpeedButton;
20
21
  [SerializeField] private TextMeshProUGUI playbackSpeedLabel;
21
- [SerializeField] private float[] playbackSpeeds = { 1.5f, 2f, 3f, 4f, 5f, 1f };
22
- [SerializeField, Min(1f)] private float seekPlaybackSpeed = 30f;
22
+ [SerializeField] private float[] playbackSpeeds = { 1f, 1.5f, 2f, 0.5f };
23
+ [SerializeField, Min(1f)] private float seekPlaybackSpeed = 15f;
24
+ [SerializeField, Min(0f)] private float readOnlyProgressSmoothTime = 0.08f;
23
25
  [SerializeField, Range(0.25f, 1f)] private float seekSnapshotScale = 0.55f;
26
+ [SerializeField, Range(0f, 0.5f)] private float seekPreparationProgress = 0.2f;
27
+ [SerializeField, Range(0f, 0.15f)] private float seekPreparationProgressVariation = 0.05f;
28
+ [SerializeField, Min(0.1f)] private float seekPreparationDuration = 3f;
24
29
  [SerializeField] private GameObject seekLoadingOverlay;
25
30
  [SerializeField] private RawImage seekSnapshotImage;
26
31
 
27
32
  private int playbackSpeedIndex;
33
+ private Slider progressSlider;
28
34
  private ReplaySeekInput seekInput;
35
+ private bool seekingAllowed;
29
36
  private bool wasSeeking;
30
37
  private bool isCapturingSeek;
31
38
  private float pendingSeekTarget;
39
+ private float displayedReadOnlyProgress;
40
+ private float readOnlyProgressVelocity;
32
41
  private Texture2D seekSnapshotTexture;
33
42
  private Image seekOverlayBackground;
34
43
  private Color seekOverlayBackgroundColor;
44
+ private ReplaySeekOverlay seekOverlay;
45
+ private static readonly System.Random SeekProgressRandom = new System.Random();
46
+ private static float displayedSeekProgress;
47
+ private static float firstSeekPreparationTarget;
48
+ private static float secondSeekPreparationTarget;
49
+ private static float seekPreparationCeiling;
35
50
 
36
51
  private void Awake()
37
52
  {
53
+ seekingAllowed = HyperRuntime.Settings != null && HyperRuntime.Settings.allowReplaySeeking;
38
54
  AutoBindReferences();
39
- ConfigureProgressSlider();
40
- ConfigureSeekOverlay();
41
- ConfigureSeekSnapshot();
55
+ ConfigureProgressSliders();
56
+ if (seekingAllowed)
57
+ {
58
+ ConfigureSeekOverlay();
59
+ ConfigureSeekSnapshot();
60
+ }
61
+ else if (seekLoadingOverlay != null)
62
+ {
63
+ seekLoadingOverlay.SetActive(false);
64
+ }
65
+
42
66
  ApplyInitialPlaybackSpeed();
43
- ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
67
+ if (seekingAllowed) ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
44
68
  if (pauseButton != null) pauseButton.onClick.AddListener(PauseReplay);
45
69
  if (playbackSpeedButton != null) playbackSpeedButton.onClick.AddListener(CyclePlaybackSpeed);
46
70
  }
@@ -52,13 +76,13 @@ namespace Hyper.Internal
52
76
  if (replayLabel != null) replayLabel.text = $"Replay of @{playerName}";
53
77
  if (progressSlider != null)
54
78
  {
55
- float displayedProgress = ReplaySessionState.IsSeeking
79
+ float displayedProgress = seekingAllowed && ReplaySessionState.IsSeeking
56
80
  ? ReplaySessionState.SeekTarget01
57
81
  : ReplaySessionState.Progress01;
58
- progressSlider.SetValueWithoutNotify(displayedProgress);
82
+ SetProgressSliderValue(displayedProgress);
59
83
  }
60
84
  UpdatePlaybackSpeedLabel(ReplaySessionState.UserPlaybackSpeed);
61
- SetSeekingPresentation(ReplaySessionState.IsSeeking);
85
+ SetSeekingPresentation(seekingAllowed && ReplaySessionState.IsSeeking);
62
86
 
63
87
  if (playerAvatar != null && ReplaySessionState.AvatarTexture != null)
64
88
  {
@@ -68,7 +92,7 @@ namespace Hyper.Internal
68
92
 
69
93
  private void Update()
70
94
  {
71
- bool isSeeking = ReplaySessionState.IsSeeking;
95
+ bool isSeeking = seekingAllowed && ReplaySessionState.IsSeeking;
72
96
  if (wasSeeking && !isSeeking && !ReplaySessionState.HasEnded)
73
97
  {
74
98
  RestorePlaybackAfterSeek(ReplaySessionState.PlaybackSpeed);
@@ -79,6 +103,11 @@ namespace Hyper.Internal
79
103
  SetSeekingPresentation(isSeeking);
80
104
  }
81
105
 
106
+ if (isSeeking)
107
+ {
108
+ UpdateSeekProgress(true);
109
+ }
110
+
82
111
  if (progressSlider != null && (seekInput == null || !seekInput.IsDragging))
83
112
  {
84
113
  float displayedProgress = isCapturingSeek
@@ -86,7 +115,7 @@ namespace Hyper.Internal
86
115
  : isSeeking
87
116
  ? ReplaySessionState.SeekTarget01
88
117
  : ReplaySessionState.Progress01;
89
- progressSlider.SetValueWithoutNotify(displayedProgress);
118
+ SetProgressSliderValue(displayedProgress, !seekingAllowed);
90
119
  }
91
120
 
92
121
  wasSeeking = isSeeking;
@@ -129,7 +158,7 @@ namespace Hyper.Internal
129
158
  if (progressSlider != null) progressSlider.interactable = false;
130
159
  if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(false);
131
160
  ClearSeekSnapshot();
132
- if (progressSlider != null) progressSlider.SetValueWithoutNotify(1f);
161
+ SetProgressSliderValue(1f);
133
162
  }
134
163
 
135
164
  internal void ResetForReplayRestart()
@@ -148,10 +177,7 @@ namespace Hyper.Internal
148
177
  : 0f;
149
178
  wasSeeking = ReplaySessionState.IsSeeking;
150
179
 
151
- if (progressSlider != null)
152
- {
153
- progressSlider.SetValueWithoutNotify(pendingSeekTarget);
154
- }
180
+ SetProgressSliderValue(pendingSeekTarget);
155
181
 
156
182
  UpdatePlaybackSpeedLabel(playbackSpeed);
157
183
  AlignPlaybackSpeedIndex(playbackSpeed);
@@ -181,7 +207,7 @@ namespace Hyper.Internal
181
207
 
182
208
  private void SeekTo(float targetProgress)
183
209
  {
184
- if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
210
+ if (!seekingAllowed || !ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
185
211
  ReplaySessionState.IsSeeking || isCapturingSeek)
186
212
  {
187
213
  return;
@@ -189,6 +215,7 @@ namespace Hyper.Internal
189
215
 
190
216
  float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
191
217
  float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
218
+ InitializeSeekProgressPresentation();
192
219
  StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
193
220
  }
194
221
 
@@ -238,7 +265,6 @@ namespace Hyper.Internal
238
265
  private void RestorePlaybackAfterSeek(float playbackSpeed)
239
266
  {
240
267
  float speed = SanitizePlaybackSpeed(playbackSpeed);
241
- Time.timeScale = speed;
242
268
  AlignPlaybackSpeedIndex(speed);
243
269
  UpdatePlaybackSpeedLabel(speed);
244
270
  SetSeekingPresentation(false);
@@ -259,9 +285,14 @@ namespace Hyper.Internal
259
285
 
260
286
  private void SetSeekingPresentation(bool isSeeking)
261
287
  {
288
+ isSeeking &= seekingAllowed;
262
289
  ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
290
+ UpdateSeekProgress(isSeeking);
263
291
  if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
264
- if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
292
+ if (progressSlider != null)
293
+ {
294
+ progressSlider.interactable = seekingAllowed && !isSeeking && !ReplaySessionState.HasEnded;
295
+ }
265
296
  if (playbackSpeedButton != null)
266
297
  {
267
298
  playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
@@ -285,7 +316,7 @@ namespace Hyper.Internal
285
316
 
286
317
  if (playbackSpeedLabel != null)
287
318
  {
288
- playbackSpeedLabel.text = $"{speed:0.#}x";
319
+ playbackSpeedLabel.text = $"{speed:0.##}x";
289
320
  }
290
321
  }
291
322
 
@@ -339,8 +370,6 @@ namespace Hyper.Internal
339
370
 
340
371
  private void AutoBindReferences()
341
372
  {
342
- if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
343
-
344
373
  Button[] buttons = GetComponentsInChildren<Button>(true);
345
374
  for (int i = 0; i < buttons.Length; i++)
346
375
  {
@@ -360,24 +389,69 @@ namespace Hyper.Internal
360
389
  }
361
390
  }
362
391
 
363
- private void ConfigureProgressSlider()
392
+ private void ConfigureProgressSliders()
364
393
  {
394
+ progressSlider = seekingAllowed ? seekableSlider : readOnlySlider;
395
+
396
+ if (seekableSlider != null)
397
+ {
398
+ seekableSlider.gameObject.SetActive(seekingAllowed);
399
+ }
400
+
401
+ if (readOnlySlider != null)
402
+ {
403
+ readOnlySlider.gameObject.SetActive(!seekingAllowed);
404
+ }
405
+
365
406
  if (progressSlider == null) return;
366
407
 
367
408
  progressSlider.minValue = 0f;
368
409
  progressSlider.maxValue = 1f;
369
410
  progressSlider.wholeNumbers = false;
370
- progressSlider.interactable = true;
371
- float displayedProgress = ReplaySessionState.IsSeeking
411
+ progressSlider.interactable = seekingAllowed;
412
+ float displayedProgress = seekingAllowed && ReplaySessionState.IsSeeking
372
413
  ? ReplaySessionState.SeekTarget01
373
414
  : ReplaySessionState.Progress01;
374
- progressSlider.SetValueWithoutNotify(displayedProgress);
415
+ SetProgressSliderValue(displayedProgress);
416
+
417
+ if (!seekingAllowed) return;
375
418
 
376
419
  seekInput = progressSlider.GetComponent<ReplaySeekInput>();
377
420
  if (seekInput == null) seekInput = progressSlider.gameObject.AddComponent<ReplaySeekInput>();
378
421
  seekInput.Initialize(progressSlider, SeekTo);
379
422
  }
380
423
 
424
+ private void SetProgressSliderValue(float targetProgress, bool smoothReadOnly = false)
425
+ {
426
+ if (progressSlider == null) return;
427
+
428
+ targetProgress = Mathf.Clamp01(targetProgress);
429
+ if (!smoothReadOnly || readOnlyProgressSmoothTime <= 0f ||
430
+ targetProgress < displayedReadOnlyProgress)
431
+ {
432
+ displayedReadOnlyProgress = targetProgress;
433
+ readOnlyProgressVelocity = 0f;
434
+ }
435
+ else
436
+ {
437
+ displayedReadOnlyProgress = Mathf.SmoothDamp(
438
+ displayedReadOnlyProgress,
439
+ targetProgress,
440
+ ref readOnlyProgressVelocity,
441
+ readOnlyProgressSmoothTime,
442
+ Mathf.Infinity,
443
+ Time.unscaledDeltaTime);
444
+
445
+ if (Mathf.Abs(targetProgress - displayedReadOnlyProgress) < 0.0001f)
446
+ {
447
+ displayedReadOnlyProgress = targetProgress;
448
+ readOnlyProgressVelocity = 0f;
449
+ }
450
+ }
451
+
452
+ progressSlider.SetValueWithoutNotify(displayedReadOnlyProgress);
453
+ }
454
+
381
455
  private void ConfigureSeekOverlay()
382
456
  {
383
457
  if (seekLoadingOverlay == null)
@@ -404,10 +478,114 @@ namespace Hyper.Internal
404
478
  seekLoadingOverlay.transform.SetAsFirstSibling();
405
479
  seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
406
480
  if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
481
+ seekOverlay = seekLoadingOverlay.GetComponent<ReplaySeekOverlay>();
407
482
  seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
408
483
  wasSeeking = ReplaySessionState.IsSeeking;
409
484
  }
410
485
 
486
+ private void UpdateSeekProgress(bool isSeeking)
487
+ {
488
+ if (seekOverlay == null) return;
489
+
490
+ float progress;
491
+ if (!isSeeking)
492
+ {
493
+ progress = 1f;
494
+ }
495
+ else if (isCapturingSeek || ReplaySessionState.IsSeekRestartPending)
496
+ {
497
+ AdvanceSeekPreparationProgress();
498
+ progress = displayedSeekProgress;
499
+ }
500
+ else
501
+ {
502
+ EnsureSeekProgressPresentation();
503
+ float target = ReplaySessionState.SeekTarget01;
504
+ float reconstructionProgress = target <= Mathf.Epsilon
505
+ ? 1f
506
+ : ReplaySessionState.Progress01 / target;
507
+ float realProgress = Mathf.Lerp(
508
+ seekPreparationCeiling,
509
+ 1f,
510
+ Mathf.Clamp01(reconstructionProgress));
511
+ AdvanceSeekReconstructionProgress(realProgress);
512
+ progress = displayedSeekProgress;
513
+ }
514
+
515
+ seekOverlay.SetProgress(progress);
516
+ }
517
+
518
+ private void AdvanceSeekPreparationProgress()
519
+ {
520
+ EnsureSeekProgressPresentation();
521
+ float duration = Mathf.Max(0.1f, seekPreparationDuration);
522
+ float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
523
+
524
+ if (displayedSeekProgress < firstSeekPreparationTarget)
525
+ {
526
+ float speed = firstSeekPreparationTarget / (duration * 0.35f);
527
+ displayedSeekProgress = Mathf.MoveTowards(
528
+ displayedSeekProgress,
529
+ firstSeekPreparationTarget,
530
+ speed * deltaTime);
531
+ return;
532
+ }
533
+
534
+ if (displayedSeekProgress < secondSeekPreparationTarget)
535
+ {
536
+ float speed = (secondSeekPreparationTarget - firstSeekPreparationTarget) / (duration * 0.2f);
537
+ displayedSeekProgress = Mathf.MoveTowards(
538
+ displayedSeekProgress,
539
+ secondSeekPreparationTarget,
540
+ speed * deltaTime);
541
+ return;
542
+ }
543
+
544
+ float crawlResponse = 1f - Mathf.Exp(-deltaTime / (duration * 0.65f));
545
+ displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, seekPreparationCeiling, crawlResponse);
546
+ }
547
+
548
+ private void InitializeSeekProgressPresentation()
549
+ {
550
+ float center = Mathf.Clamp01(seekPreparationProgress);
551
+ float variation = Mathf.Max(0f, seekPreparationProgressVariation);
552
+ float minimum = Mathf.Clamp01(center - variation);
553
+ float maximum = Mathf.Clamp01(center + variation);
554
+
555
+ displayedSeekProgress = 0f;
556
+ seekPreparationCeiling = RandomRange(minimum, maximum);
557
+ firstSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.25f, 0.35f);
558
+ secondSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.62f, 0.76f);
559
+ }
560
+
561
+ private void EnsureSeekProgressPresentation()
562
+ {
563
+ if (seekPreparationCeiling > 0f) return;
564
+ InitializeSeekProgressPresentation();
565
+ }
566
+
567
+ private static float RandomRange(float minimum, float maximum)
568
+ {
569
+ if (maximum <= minimum) return minimum;
570
+ return minimum + ((float)SeekProgressRandom.NextDouble() * (maximum - minimum));
571
+ }
572
+
573
+ private void AdvanceSeekReconstructionProgress(float realProgress)
574
+ {
575
+ float target = Mathf.Max(displayedSeekProgress, realProgress);
576
+ if (target >= 0.999f)
577
+ {
578
+ displayedSeekProgress = 1f;
579
+ return;
580
+ }
581
+
582
+ float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
583
+ float gap = target - displayedSeekProgress;
584
+ float responseSpeed = Mathf.Lerp(5f, 14f, Mathf.Clamp01(gap * 4f));
585
+ float response = 1f - Mathf.Exp(-responseSpeed * deltaTime);
586
+ displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, target, response);
587
+ }
588
+
411
589
  private void ConfigureSeekSnapshot()
412
590
  {
413
591
  if (seekSnapshotImage == null)
@@ -110,7 +110,7 @@ namespace Hyper.Internal
110
110
  CanvasScaler scaler = GetComponent<CanvasScaler>();
111
111
  scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
112
112
  scaler.referenceResolution = new Vector2(1080f, 2160f);
113
- scaler.matchWidthOrHeight = isPortrait ? 1f : 0.593f;
113
+ scaler.matchWidthOrHeight = isPortrait ? 1f : (PlayerDeviceClassification.PlayerDeviceIsPC ? 0.75f : 0.593f);
114
114
  }
115
115
 
116
116
  private void EnsurePresentationObjects()
@@ -0,0 +1,18 @@
1
+ using TMPro;
2
+ using UnityEngine;
3
+
4
+ namespace Hyper.Internal
5
+ {
6
+ internal sealed class ReplaySeekOverlay : MonoBehaviour
7
+ {
8
+ [SerializeField] private TextMeshProUGUI progressText;
9
+
10
+ internal void SetProgress(float progress01)
11
+ {
12
+ if (progressText == null) return;
13
+
14
+ int percentage = Mathf.FloorToInt(Mathf.Clamp01(progress01) * 10f) * 10;
15
+ progressText.text = $"{percentage}%";
16
+ }
17
+ }
18
+ }
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: a7d0eb352f3c4d64a687b8c4509e1fa2
@@ -1,3 +1,4 @@
1
+ using System.Globalization;
1
2
  using TMPro;
2
3
  using UnityEngine;
3
4
  using UnityEngine.UI;
@@ -38,7 +39,7 @@ namespace Hyper.Internal
38
39
 
39
40
  if (scoreValue != null)
40
41
  {
41
- scoreValue.text = (HyperRuntime.DataManager?.CurrentScore ?? 0).ToString("N0");
42
+ scoreValue.text = FormatFinalScore(ReplaySessionState.FinalScore);
42
43
  }
43
44
 
44
45
  if (scoreOwnerName != null)
@@ -99,6 +100,13 @@ namespace Hyper.Internal
99
100
  : $"@{normalizedName.ToUpperInvariant()}'S SCORE";
100
101
  }
101
102
 
103
+ internal static string FormatFinalScore(int? finalScore)
104
+ {
105
+ return finalScore.HasValue
106
+ ? finalScore.Value.ToString("N0", CultureInfo.InvariantCulture)
107
+ : "--";
108
+ }
109
+
102
110
  private void UpdateSoundPresentation()
103
111
  {
104
112
  bool isSoundOn = HyperRuntime.DataManager == null || HyperRuntime.DataManager.GetSound() == 1;
@@ -14,7 +14,7 @@ namespace Hyper.Internal
14
14
  [SerializeField] private BlinkingText blinkingText;
15
15
  [SerializeField] private RectTransform swipeGif;
16
16
  [SerializeField] private RectTransform handSpriteInGif;
17
- [SerializeField] private float replayAutoStartDelay = 1f;
17
+ [SerializeField] private float replayAutoStartDelay = 2f;
18
18
  private GameManager _gameManager;
19
19
  private StartCTA _currentCTA;
20
20
  private SwipeDirection _swipeDirection;
@@ -29,6 +29,13 @@ namespace Hyper
29
29
  private double _stoppedElapsed;
30
30
  private bool _isSuspended;
31
31
  private bool _completionPending;
32
+ private bool _gameplayClockStarted;
33
+ private double _gameplaySourceStartTime;
34
+ private double _gameplayElapsedSeconds;
35
+ private double _gameplayStepSeconds;
36
+ private double _gameplayPauseDurationAtStart;
37
+ private long _gameplayTick;
38
+ private double _currentTickDelta;
32
39
 
33
40
  public event Action<float, float> OnValues2;
34
41
  public event Action<float> OnValue;
@@ -76,6 +83,13 @@ namespace Hyper
76
83
  _endedRaised = false;
77
84
  _completionPending = false;
78
85
  _nextActionPrepared = false;
86
+ _gameplayClockStarted = false;
87
+ _gameplaySourceStartTime = 0d;
88
+ _gameplayElapsedSeconds = 0d;
89
+ _gameplayStepSeconds = 0d;
90
+ _gameplayPauseDurationAtStart = 0d;
91
+ _gameplayTick = 0L;
92
+ _currentTickDelta = 0d;
79
93
  DurationSeconds = CalculateDuration(_ndjson);
80
94
  double gameplayStartSeconds = CalculateGameplayStart(_ndjson, out bool hasGameplayReadyMarker);
81
95
  HasGameplayReadyMarker = hasGameplayReadyMarker;
@@ -122,13 +136,19 @@ namespace Hyper
122
136
  double.IsInfinity(fixedSimulationDeltaSeconds)
123
137
  ? 0d
124
138
  : Math.Max(0d, fixedSimulationDeltaSeconds);
139
+ _currentTickDelta = delta;
125
140
  _elapsedSeconds += delta;
141
+ if (_gameplayClockStarted && delta > 0d)
142
+ {
143
+ if (_gameplayStepSeconds <= 0d) _gameplayStepSeconds = delta;
144
+ _gameplayElapsedSeconds += delta;
145
+ _gameplayTick++;
146
+ }
126
147
 
127
148
  int processed = 0;
128
149
  while (IsPlaying && !_completionPending && _hasNext && processed++ < MaxActivitiesPerTick)
129
150
  {
130
- double elapsed = GetElapsedSeconds();
131
- double dueTime = _next.Time - _sourceStartTime - _removedPauseDuration;
151
+ GetSchedulingTime(_next, out double elapsed, out double dueTime);
132
152
  PrepareNextActionIfDue(elapsed, dueTime);
133
153
  if (elapsed + 0.000001d < dueTime) return;
134
154
 
@@ -154,6 +174,13 @@ namespace Hyper
154
174
  _isSuspended = false;
155
175
  _completionPending = false;
156
176
  _nextActionPrepared = false;
177
+ _gameplayClockStarted = false;
178
+ _gameplaySourceStartTime = 0d;
179
+ _gameplayElapsedSeconds = 0d;
180
+ _gameplayStepSeconds = 0d;
181
+ _gameplayPauseDurationAtStart = 0d;
182
+ _gameplayTick = 0L;
183
+ _currentTickDelta = 0d;
157
184
  }
158
185
 
159
186
  public void Suspend()
@@ -224,6 +251,7 @@ namespace Hyper
224
251
  OnGameStarted?.Invoke();
225
252
  break;
226
253
  case GameplayReady:
254
+ StartGameplayClock(activity.Time);
227
255
  OnGameplayReady?.Invoke();
228
256
  break;
229
257
  case GamePaused:
@@ -255,6 +283,47 @@ namespace Hyper
255
283
  OnActionPreparing?.Invoke(_next.Action);
256
284
  }
257
285
 
286
+ private void StartGameplayClock(double sourceTime)
287
+ {
288
+ _gameplayClockStarted = true;
289
+ _gameplaySourceStartTime = sourceTime;
290
+ _gameplayElapsedSeconds = 0d;
291
+ _gameplayStepSeconds = _currentTickDelta;
292
+ _gameplayPauseDurationAtStart = _removedPauseDuration;
293
+ _gameplayTick = 0L;
294
+ }
295
+
296
+ private void GetSchedulingTime(ReplayActivity activity, out double elapsed, out double dueTime)
297
+ {
298
+ if (!_gameplayClockStarted)
299
+ {
300
+ elapsed = GetElapsedSeconds();
301
+ dueTime = activity.Time - _sourceStartTime - _removedPauseDuration;
302
+ return;
303
+ }
304
+
305
+ double removedGameplayPauses = Math.Max(0d,
306
+ _removedPauseDuration - _gameplayPauseDurationAtStart);
307
+ double rawDueTime = Math.Max(0d,
308
+ activity.Time - _gameplaySourceStartTime - removedGameplayPauses);
309
+
310
+ if (_gameplayStepSeconds <= 0d)
311
+ {
312
+ elapsed = _gameplayElapsedSeconds;
313
+ dueTime = rawDueTime;
314
+ return;
315
+ }
316
+
317
+ // Gameplay can only consume an activity on a fixed step. Mapping the
318
+ // timestamp to its nearest GPR-relative tick prevents sub-step JSON
319
+ // precision noise from moving an input to the following physics step.
320
+ long dueTick = Math.Max(0L, (long)Math.Round(
321
+ rawDueTime / _gameplayStepSeconds,
322
+ MidpointRounding.AwayFromZero));
323
+ elapsed = _gameplayTick * _gameplayStepSeconds;
324
+ dueTime = dueTick * _gameplayStepSeconds;
325
+ }
326
+
258
327
  private static bool IsLifecycleAction(string action)
259
328
  {
260
329
  return action == GameStarted || action == GamePaused ||