app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.2
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.
- package/CHANGELOG.md +34 -0
- package/Editor/Scripts/Setup/HyperSDKInstaller.cs +42 -0
- package/Editor/Scripts/Tools/HyperGameModeMenu.cs +27 -7
- package/Runtime/Internal/AssemblyInfo.cs +1 -0
- package/Runtime/Internal/Bootstrap/HyperLoader.cs +186 -8
- package/Runtime/Internal/Core/HyperRuntime.cs +82 -0
- package/Runtime/Internal/Core/HyperRuntimeHost.cs +6 -0
- package/Runtime/Internal/Data/URLData.cs +18 -1
- package/Runtime/Internal/Managers/BackendManager.cs +281 -24
- package/Runtime/Internal/Managers/DataManager.cs +207 -21
- package/Runtime/Internal/Managers/GameManager.cs +365 -17
- package/Runtime/Internal/Managers/ReplayManager.cs +117 -0
- package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
- package/Runtime/Internal/Managers/TimeManager.cs +54 -12
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs +182 -0
- package/Runtime/Internal/Replay/ReplayActivityRecorder.cs.meta +2 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs +181 -0
- package/Runtime/Internal/Replay/ReplaySessionState.cs.meta +2 -0
- package/Runtime/Internal/Replay.meta +8 -0
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +5 -0
- package/Runtime/Internal/UI/Animation/LoadingTextAnimation.cs +15 -1
- package/Runtime/Internal/UI/Components/ControlBar.cs +25 -2
- package/Runtime/Internal/UI/Components/PauseButton.cs +4 -1
- package/Runtime/Internal/UI/Components/ReplayController.cs +527 -0
- package/Runtime/Internal/UI/Components/ReplayController.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs +89 -0
- package/Runtime/Internal/UI/Components/ReplayMarkerPulse.cs.meta +2 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs +183 -0
- package/Runtime/Internal/UI/Components/ReplayPresentationCanvas.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/LoadingFailedModal.cs +12 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs +61 -0
- package/Runtime/Internal/UI/Modals/ReplayOverModal.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +38 -29
- package/Runtime/Public/AssemblyInfo.cs +3 -0
- package/Runtime/Public/AssemblyInfo.cs.meta +2 -0
- package/Runtime/Public/Core/HyperSDK.cs +305 -109
- package/Runtime/Public/Hyper.asmdef +2 -1
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +555 -0
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs.meta +2 -0
- package/Runtime/Public/Replay.meta +8 -0
- package/Runtime/Resources/Fonts/Nunito/Nunito-Light SDF.asset +528 -521
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset +340 -0
- package/Runtime/Resources/Materials/HyperReplayScanlines.preset.meta +8 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat +55 -0
- package/Runtime/Resources/Materials/ReplayScanlineOverlay.mat.meta +8 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader +150 -0
- package/Runtime/Resources/Shaders/ReplayScanlineOverlay.shader.meta +10 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Fallback avatar.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Recording.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay frame.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png +0 -0
- package/Runtime/Resources/Sprites/Replays/Replay modal icon.png.meta +156 -0
- package/Runtime/Resources/Sprites/Replays.meta +8 -0
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +169 -2
- package/Runtime/Resources/UIPrefabs/GamePausedModal_Battle.prefab +1 -1
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab +1269 -0
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab +90 -0
- package/Runtime/Resources/UIPrefabs/ReplayInputBlocker.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab +958 -0
- package/Runtime/Resources/UIPrefabs/ReplayOverModal.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +189 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/SnackBar.prefab +2 -2
- package/Runtime/Shared/AssemblyInfo.cs +5 -0
- package/Runtime/Shared/AssemblyInfo.cs.meta +11 -0
- package/Runtime/Shared/DTOs/Enums.cs +8 -3
- package/Runtime/Shared/DTOs/Score/ScoreSnapshot.cs +13 -1
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +37 -0
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs.meta +11 -0
- package/Runtime/Shared/Replay.meta +8 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity +1603 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity.meta +7 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat +86 -0
- package/Samples~/LaneRunner/Materials/DoubleScore.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Ground.mat +84 -0
- package/Samples~/LaneRunner/Materials/Ground.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Lane.mat +84 -0
- package/Samples~/LaneRunner/Materials/Lane.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat +84 -0
- package/Samples~/LaneRunner/Materials/LaneMarker.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat +84 -0
- package/Samples~/LaneRunner/Materials/Obstacle.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials/Player.mat +84 -0
- package/Samples~/LaneRunner/Materials/Player.mat.meta +8 -0
- package/Samples~/LaneRunner/Materials.meta +8 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +152 -0
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab.meta +7 -0
- package/Samples~/LaneRunner/Prefabs.meta +8 -0
- package/Samples~/LaneRunner/README.md +35 -0
- package/Samples~/LaneRunner/README.md.meta +7 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs +9 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerCommand.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs +93 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerGame.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +155 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +110 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs +41 -0
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerTrackItem.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Gameplay.meta +8 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs +62 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerReplayAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +64 -0
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs.meta +2 -0
- package/Samples~/LaneRunner/Scripts/Hyper.meta +8 -0
- package/Samples~/LaneRunner/Scripts.meta +8 -0
- package/Tests.meta +8 -0
- package/package.json +8 -1
|
@@ -16,13 +16,27 @@ namespace Hyper.Internal
|
|
|
16
16
|
{
|
|
17
17
|
StartCoroutine(AnimateLoadingText());
|
|
18
18
|
}
|
|
19
|
+
|
|
20
|
+
internal void SetBaseText(string value)
|
|
21
|
+
{
|
|
22
|
+
if (string.IsNullOrWhiteSpace(value)) return;
|
|
23
|
+
|
|
24
|
+
loadingBaseText = value;
|
|
25
|
+
if (loadingText != null)
|
|
26
|
+
{
|
|
27
|
+
loadingText.text = loadingBaseText;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
private IEnumerator AnimateLoadingText()
|
|
21
32
|
{
|
|
22
33
|
int i = 0;
|
|
23
34
|
while (true)
|
|
24
35
|
{
|
|
25
|
-
loadingText
|
|
36
|
+
if (loadingText != null)
|
|
37
|
+
{
|
|
38
|
+
loadingText.text = loadingBaseText + loadingStates[i];
|
|
39
|
+
}
|
|
26
40
|
i = (i + 1) % loadingStates.Length;
|
|
27
41
|
yield return new WaitForSecondsRealtime(dotInterval);
|
|
28
42
|
}
|
|
@@ -40,6 +40,8 @@ namespace Hyper.Internal
|
|
|
40
40
|
private TextMeshProUGUI _timerTextComponent;
|
|
41
41
|
private LivesDisplay _livesDisplay;
|
|
42
42
|
private HealthBarDisplay _healthBarDisplay;
|
|
43
|
+
private GameObject _pauseButtonRoot;
|
|
44
|
+
private GameObject _replayMarker;
|
|
43
45
|
|
|
44
46
|
// Layout constants
|
|
45
47
|
private const float CENTERED_LAYOUT_SCORE_Y = 31f;
|
|
@@ -76,7 +78,9 @@ namespace Hyper.Internal
|
|
|
76
78
|
ConfigureLayout();
|
|
77
79
|
ConfigureTheme();
|
|
78
80
|
|
|
79
|
-
|
|
81
|
+
ConfigureReplayState();
|
|
82
|
+
|
|
83
|
+
connectionStatusIcon?.onClick.AddListener(ShowConnectionLostStatus);
|
|
80
84
|
}
|
|
81
85
|
|
|
82
86
|
private void ConfigureControlBarBackground()
|
|
@@ -93,7 +97,7 @@ namespace Hyper.Internal
|
|
|
93
97
|
|
|
94
98
|
private void OnDestroy()
|
|
95
99
|
{
|
|
96
|
-
connectionStatusIcon
|
|
100
|
+
connectionStatusIcon?.onClick.RemoveListener(ShowConnectionLostStatus);
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
private void CacheTextComponents()
|
|
@@ -314,10 +318,29 @@ namespace Hyper.Internal
|
|
|
314
318
|
|
|
315
319
|
private void ShowConnectionLostStatus()
|
|
316
320
|
{
|
|
321
|
+
if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
|
|
317
322
|
// Show the connection lost status
|
|
318
323
|
_gameManager.ShowConnectionLost();
|
|
319
324
|
}
|
|
320
325
|
|
|
326
|
+
private void ConfigureReplayState()
|
|
327
|
+
{
|
|
328
|
+
Transform[] children = GetComponentsInChildren<Transform>(true);
|
|
329
|
+
for (int i = 0; i < children.Length; i++)
|
|
330
|
+
{
|
|
331
|
+
if (children[i].name == "PauseButton") _pauseButtonRoot = children[i].gameObject;
|
|
332
|
+
else if (children[i].name == "Replay Marker") _replayMarker = children[i].gameObject;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
bool replay = HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode;
|
|
336
|
+
if (_pauseButtonRoot != null) _pauseButtonRoot.SetActive(!replay);
|
|
337
|
+
if (_replayMarker != null)
|
|
338
|
+
{
|
|
339
|
+
_replayMarker.SetActive(replay);
|
|
340
|
+
}
|
|
341
|
+
if (connectionStatusIcon != null) connectionStatusIcon.gameObject.SetActive(!replay);
|
|
342
|
+
}
|
|
343
|
+
|
|
321
344
|
public void UpdateScoreLabel()
|
|
322
345
|
{
|
|
323
346
|
int currentScore = HyperRuntime.DataManager.CurrentScore;
|
|
@@ -2,6 +2,7 @@ using Hyper.Internal.Managers;
|
|
|
2
2
|
using UnityEngine;
|
|
3
3
|
using UnityEngine.EventSystems;
|
|
4
4
|
using UnityEngine.UI;
|
|
5
|
+
using Hyper.Shared;
|
|
5
6
|
|
|
6
7
|
namespace Hyper.Internal
|
|
7
8
|
{
|
|
@@ -31,6 +32,7 @@ namespace Hyper.Internal
|
|
|
31
32
|
|
|
32
33
|
public void OnPointerDown(PointerEventData eventData)
|
|
33
34
|
{
|
|
35
|
+
if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
|
|
34
36
|
if (!_isTypeRush)
|
|
35
37
|
{
|
|
36
38
|
_gameManager.PauseGame();
|
|
@@ -39,10 +41,11 @@ namespace Hyper.Internal
|
|
|
39
41
|
|
|
40
42
|
public void OnPointerUp(PointerEventData eventData)
|
|
41
43
|
{
|
|
44
|
+
if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.IsReplayMode) return;
|
|
42
45
|
if (_isTypeRush)
|
|
43
46
|
{
|
|
44
47
|
_gameManager.PauseGame();
|
|
45
48
|
}
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
|
-
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections;
|
|
3
|
+
using Hyper.Shared;
|
|
4
|
+
using TMPro;
|
|
5
|
+
using UnityEngine;
|
|
6
|
+
using UnityEngine.EventSystems;
|
|
7
|
+
using UnityEngine.UI;
|
|
8
|
+
|
|
9
|
+
namespace Hyper.Internal
|
|
10
|
+
{
|
|
11
|
+
internal sealed class ReplayController : MonoBehaviour
|
|
12
|
+
{
|
|
13
|
+
internal const int AvatarTextureSize = 128;
|
|
14
|
+
|
|
15
|
+
[SerializeField] private Slider progressSlider;
|
|
16
|
+
[SerializeField] private TextMeshProUGUI replayLabel;
|
|
17
|
+
[SerializeField] private RawImage playerAvatar;
|
|
18
|
+
[SerializeField] private Button exitButton;
|
|
19
|
+
[SerializeField] private Button playbackSpeedButton;
|
|
20
|
+
[SerializeField] private TextMeshProUGUI playbackSpeedLabel;
|
|
21
|
+
[SerializeField] private float[] playbackSpeeds = { 1.5f, 2f, 3f, 4f, 5f, 1f };
|
|
22
|
+
[SerializeField, Min(1f)] private float seekPlaybackSpeed = 30f;
|
|
23
|
+
[SerializeField, Range(0.25f, 1f)] private float seekSnapshotScale = 0.55f;
|
|
24
|
+
[SerializeField] private GameObject seekLoadingOverlay;
|
|
25
|
+
[SerializeField] private RawImage seekSnapshotImage;
|
|
26
|
+
|
|
27
|
+
private int playbackSpeedIndex;
|
|
28
|
+
private ReplaySeekInput seekInput;
|
|
29
|
+
private bool wasSeeking;
|
|
30
|
+
private bool isCapturingSeek;
|
|
31
|
+
private float pendingSeekTarget;
|
|
32
|
+
private Texture2D seekSnapshotTexture;
|
|
33
|
+
private Image seekOverlayBackground;
|
|
34
|
+
private Color seekOverlayBackgroundColor;
|
|
35
|
+
|
|
36
|
+
private void Awake()
|
|
37
|
+
{
|
|
38
|
+
AutoBindReferences();
|
|
39
|
+
ConfigureProgressSlider();
|
|
40
|
+
ConfigureSeekOverlay();
|
|
41
|
+
ConfigureSeekSnapshot();
|
|
42
|
+
ApplyInitialPlaybackSpeed();
|
|
43
|
+
ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
|
|
44
|
+
if (exitButton != null) exitButton.onClick.AddListener(ExitReplay);
|
|
45
|
+
if (playbackSpeedButton != null) playbackSpeedButton.onClick.AddListener(CyclePlaybackSpeed);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private void Start()
|
|
49
|
+
{
|
|
50
|
+
string playerName = ReplaySessionState.PlayerName ?? string.Empty;
|
|
51
|
+
playerName = playerName.TrimStart('@');
|
|
52
|
+
if (replayLabel != null) replayLabel.text = $"Replay of @{playerName}";
|
|
53
|
+
if (progressSlider != null)
|
|
54
|
+
{
|
|
55
|
+
float displayedProgress = ReplaySessionState.IsSeeking
|
|
56
|
+
? ReplaySessionState.SeekTarget01
|
|
57
|
+
: ReplaySessionState.Progress01;
|
|
58
|
+
progressSlider.SetValueWithoutNotify(displayedProgress);
|
|
59
|
+
}
|
|
60
|
+
UpdatePlaybackSpeedLabel(ReplaySessionState.UserPlaybackSpeed);
|
|
61
|
+
SetSeekingPresentation(ReplaySessionState.IsSeeking);
|
|
62
|
+
|
|
63
|
+
if (playerAvatar != null && ReplaySessionState.AvatarTexture != null)
|
|
64
|
+
{
|
|
65
|
+
playerAvatar.texture = ReplaySessionState.AvatarTexture;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private void Update()
|
|
70
|
+
{
|
|
71
|
+
bool isSeeking = ReplaySessionState.IsSeeking;
|
|
72
|
+
if (wasSeeking && !isSeeking && !ReplaySessionState.HasEnded)
|
|
73
|
+
{
|
|
74
|
+
RestorePlaybackAfterSeek(ReplaySessionState.PlaybackSpeed);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (wasSeeking != isSeeking)
|
|
78
|
+
{
|
|
79
|
+
SetSeekingPresentation(isSeeking);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (progressSlider != null && (seekInput == null || !seekInput.IsDragging))
|
|
83
|
+
{
|
|
84
|
+
float displayedProgress = isCapturingSeek
|
|
85
|
+
? pendingSeekTarget
|
|
86
|
+
: isSeeking
|
|
87
|
+
? ReplaySessionState.SeekTarget01
|
|
88
|
+
: ReplaySessionState.Progress01;
|
|
89
|
+
progressSlider.SetValueWithoutNotify(displayedProgress);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
wasSeeking = isSeeking;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private void OnDestroy()
|
|
96
|
+
{
|
|
97
|
+
if (exitButton != null) exitButton.onClick.RemoveListener(ExitReplay);
|
|
98
|
+
if (playbackSpeedButton != null) playbackSpeedButton.onClick.RemoveListener(CyclePlaybackSpeed);
|
|
99
|
+
ReplaySessionState.SeekCompleted -= RestorePlaybackAfterSeek;
|
|
100
|
+
seekInput?.Clear();
|
|
101
|
+
ClearSeekSnapshot();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private void ApplyInitialPlaybackSpeed()
|
|
105
|
+
{
|
|
106
|
+
if (!ReplaySessionState.IsActive || playbackSpeeds == null || playbackSpeeds.Length == 0)
|
|
107
|
+
{
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
float speed = ReplaySessionState.IsSeeking
|
|
112
|
+
? ReplaySessionState.UserPlaybackSpeed
|
|
113
|
+
: SanitizePlaybackSpeed(playbackSpeeds[0]);
|
|
114
|
+
if (!ReplaySessionState.IsSeeking)
|
|
115
|
+
{
|
|
116
|
+
ReplaySessionState.SetPlaybackSpeed(speed);
|
|
117
|
+
Time.timeScale = speed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
playbackSpeedIndex = playbackSpeeds.Length > 1 ? 1 : 0;
|
|
121
|
+
AlignPlaybackSpeedIndex(speed);
|
|
122
|
+
UpdatePlaybackSpeedLabel(speed);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
internal void SetReplayEnded()
|
|
126
|
+
{
|
|
127
|
+
if (exitButton != null) exitButton.interactable = false;
|
|
128
|
+
if (playbackSpeedButton != null) playbackSpeedButton.interactable = false;
|
|
129
|
+
if (progressSlider != null) progressSlider.interactable = false;
|
|
130
|
+
if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(false);
|
|
131
|
+
ClearSeekSnapshot();
|
|
132
|
+
if (progressSlider != null) progressSlider.SetValueWithoutNotify(1f);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
internal void ResetForReplayRestart()
|
|
136
|
+
{
|
|
137
|
+
float playbackSpeed = ReplaySessionState.UserPlaybackSpeed;
|
|
138
|
+
if (!ReplaySessionState.IsSeeking && playbackSpeeds != null && playbackSpeeds.Length > 0)
|
|
139
|
+
{
|
|
140
|
+
playbackSpeed = SanitizePlaybackSpeed(playbackSpeeds[0]);
|
|
141
|
+
ReplaySessionState.SetPlaybackSpeed(playbackSpeed);
|
|
142
|
+
Time.timeScale = playbackSpeed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
isCapturingSeek = false;
|
|
146
|
+
pendingSeekTarget = ReplaySessionState.IsSeeking
|
|
147
|
+
? ReplaySessionState.SeekTarget01
|
|
148
|
+
: 0f;
|
|
149
|
+
wasSeeking = ReplaySessionState.IsSeeking;
|
|
150
|
+
|
|
151
|
+
if (progressSlider != null)
|
|
152
|
+
{
|
|
153
|
+
progressSlider.SetValueWithoutNotify(pendingSeekTarget);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
UpdatePlaybackSpeedLabel(playbackSpeed);
|
|
157
|
+
AlignPlaybackSpeedIndex(playbackSpeed);
|
|
158
|
+
SetSeekingPresentation(ReplaySessionState.IsSeeking);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private void ExitReplay()
|
|
162
|
+
{
|
|
163
|
+
if (exitButton != null) exitButton.interactable = false;
|
|
164
|
+
HyperRuntime.GameManager?.ExitReplayEarly();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private void CyclePlaybackSpeed()
|
|
168
|
+
{
|
|
169
|
+
if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
|
|
170
|
+
playbackSpeeds == null || playbackSpeeds.Length == 0)
|
|
171
|
+
{
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
float speed = SanitizePlaybackSpeed(playbackSpeeds[playbackSpeedIndex]);
|
|
176
|
+
playbackSpeedIndex = (playbackSpeedIndex + 1) % playbackSpeeds.Length;
|
|
177
|
+
|
|
178
|
+
ReplaySessionState.SetPlaybackSpeed(speed);
|
|
179
|
+
Time.timeScale = speed;
|
|
180
|
+
UpdatePlaybackSpeedLabel(speed);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private void SeekTo(float targetProgress)
|
|
184
|
+
{
|
|
185
|
+
if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
|
|
186
|
+
ReplaySessionState.IsSeeking || isCapturingSeek)
|
|
187
|
+
{
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
|
|
192
|
+
float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
|
|
193
|
+
StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private IEnumerator CaptureAndSeek(float targetProgress, float resumeSpeed, float fastForwardSpeed)
|
|
197
|
+
{
|
|
198
|
+
isCapturingSeek = true;
|
|
199
|
+
pendingSeekTarget = Mathf.Clamp01(targetProgress);
|
|
200
|
+
|
|
201
|
+
yield return new WaitForEndOfFrame();
|
|
202
|
+
|
|
203
|
+
Texture2D capturedFrame = null;
|
|
204
|
+
try
|
|
205
|
+
{
|
|
206
|
+
capturedFrame = ScreenCapture.CaptureScreenshotAsTexture();
|
|
207
|
+
if (capturedFrame != null)
|
|
208
|
+
{
|
|
209
|
+
seekSnapshotTexture = CreateScaledSnapshot(capturedFrame, seekSnapshotScale);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch (Exception exception)
|
|
213
|
+
{
|
|
214
|
+
HyperDebug.LogWarning($"[Replay] Unable to capture seek snapshot: {exception.Message}");
|
|
215
|
+
}
|
|
216
|
+
finally
|
|
217
|
+
{
|
|
218
|
+
if (capturedFrame != null) Destroy(capturedFrame);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (seekSnapshotImage != null && seekSnapshotTexture != null)
|
|
222
|
+
{
|
|
223
|
+
seekSnapshotImage.texture = seekSnapshotTexture;
|
|
224
|
+
seekSnapshotImage.gameObject.SetActive(true);
|
|
225
|
+
SetSeekOverlayBackgroundVisible(false);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try
|
|
229
|
+
{
|
|
230
|
+
SetSeekingPresentation(true);
|
|
231
|
+
HyperRuntime.SeekReplaySession(pendingSeekTarget, resumeSpeed, fastForwardSpeed);
|
|
232
|
+
}
|
|
233
|
+
finally
|
|
234
|
+
{
|
|
235
|
+
isCapturingSeek = false;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private void RestorePlaybackAfterSeek(float playbackSpeed)
|
|
240
|
+
{
|
|
241
|
+
float speed = SanitizePlaybackSpeed(playbackSpeed);
|
|
242
|
+
Time.timeScale = speed;
|
|
243
|
+
AlignPlaybackSpeedIndex(speed);
|
|
244
|
+
UpdatePlaybackSpeedLabel(speed);
|
|
245
|
+
SetSeekingPresentation(false);
|
|
246
|
+
wasSeeking = false;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private void AlignPlaybackSpeedIndex(float speed)
|
|
250
|
+
{
|
|
251
|
+
if (playbackSpeeds == null || playbackSpeeds.Length == 0) return;
|
|
252
|
+
|
|
253
|
+
for (int i = 0; i < playbackSpeeds.Length; i++)
|
|
254
|
+
{
|
|
255
|
+
if (!Mathf.Approximately(SanitizePlaybackSpeed(playbackSpeeds[i]), speed)) continue;
|
|
256
|
+
playbackSpeedIndex = (i + 1) % playbackSpeeds.Length;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private void SetSeekingPresentation(bool isSeeking)
|
|
262
|
+
{
|
|
263
|
+
ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
|
|
264
|
+
if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
|
|
265
|
+
if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
266
|
+
if (playbackSpeedButton != null)
|
|
267
|
+
{
|
|
268
|
+
playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (exitButton != null) exitButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
272
|
+
if (!isSeeking) ClearSeekSnapshot();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private static float SanitizePlaybackSpeed(float speed)
|
|
276
|
+
{
|
|
277
|
+
return float.IsNaN(speed) || float.IsInfinity(speed) || speed <= 0f ? 1f : speed;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private void UpdatePlaybackSpeedLabel(float speed)
|
|
281
|
+
{
|
|
282
|
+
if (playbackSpeedLabel == null && playbackSpeedButton != null)
|
|
283
|
+
{
|
|
284
|
+
playbackSpeedLabel = playbackSpeedButton.GetComponentInChildren<TextMeshProUGUI>(true);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (playbackSpeedLabel != null)
|
|
288
|
+
{
|
|
289
|
+
playbackSpeedLabel.text = $"{speed:0.#}x";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
internal static Texture2D CreateCircularAvatar(Texture2D source, int size)
|
|
294
|
+
{
|
|
295
|
+
var result = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
|
296
|
+
{
|
|
297
|
+
name = "Replay Avatar (Circular)",
|
|
298
|
+
filterMode = FilterMode.Bilinear,
|
|
299
|
+
wrapMode = TextureWrapMode.Clamp
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
Color[] pixels = new Color[size * size];
|
|
303
|
+
float sourceAspect = source.width / (float)source.height;
|
|
304
|
+
float edgeFeather = 2.5f / size;
|
|
305
|
+
|
|
306
|
+
for (int y = 0; y < size; y++)
|
|
307
|
+
{
|
|
308
|
+
float v = (y + 0.5f) / size;
|
|
309
|
+
for (int x = 0; x < size; x++)
|
|
310
|
+
{
|
|
311
|
+
float u = (x + 0.5f) / size;
|
|
312
|
+
float centeredX = u - 0.5f;
|
|
313
|
+
float centeredY = v - 0.5f;
|
|
314
|
+
float distanceFromCenter = Mathf.Sqrt(centeredX * centeredX + centeredY * centeredY);
|
|
315
|
+
float edgeCoverage = Mathf.Clamp01((0.5f - distanceFromCenter) / edgeFeather);
|
|
316
|
+
edgeCoverage = edgeCoverage * edgeCoverage * (3f - 2f * edgeCoverage);
|
|
317
|
+
|
|
318
|
+
if (edgeCoverage <= 0f)
|
|
319
|
+
{
|
|
320
|
+
pixels[y * size + x] = Color.clear;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
float sampleU = sourceAspect > 1f
|
|
325
|
+
? 0.5f + centeredX / sourceAspect
|
|
326
|
+
: u;
|
|
327
|
+
float sampleV = sourceAspect < 1f
|
|
328
|
+
? 0.5f + centeredY * sourceAspect
|
|
329
|
+
: v;
|
|
330
|
+
Color color = source.GetPixelBilinear(sampleU, sampleV);
|
|
331
|
+
color.a *= edgeCoverage;
|
|
332
|
+
pixels[y * size + x] = color;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
result.SetPixels(pixels);
|
|
337
|
+
result.Apply(false, true);
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private void AutoBindReferences()
|
|
342
|
+
{
|
|
343
|
+
if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
|
|
344
|
+
if (exitButton == null) exitButton = GetComponentInChildren<Button>(true);
|
|
345
|
+
|
|
346
|
+
TextMeshProUGUI[] labels = GetComponentsInChildren<TextMeshProUGUI>(true);
|
|
347
|
+
for (int i = 0; replayLabel == null && i < labels.Length; i++)
|
|
348
|
+
{
|
|
349
|
+
if (labels[i].name == "Subtext") replayLabel = labels[i];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
RawImage[] images = GetComponentsInChildren<RawImage>(true);
|
|
353
|
+
for (int i = 0; playerAvatar == null && i < images.Length; i++)
|
|
354
|
+
{
|
|
355
|
+
if (images[i].name == "Player Avatar") playerAvatar = images[i];
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private void ConfigureProgressSlider()
|
|
360
|
+
{
|
|
361
|
+
if (progressSlider == null) return;
|
|
362
|
+
|
|
363
|
+
progressSlider.minValue = 0f;
|
|
364
|
+
progressSlider.maxValue = 1f;
|
|
365
|
+
progressSlider.wholeNumbers = false;
|
|
366
|
+
progressSlider.interactable = true;
|
|
367
|
+
float displayedProgress = ReplaySessionState.IsSeeking
|
|
368
|
+
? ReplaySessionState.SeekTarget01
|
|
369
|
+
: ReplaySessionState.Progress01;
|
|
370
|
+
progressSlider.SetValueWithoutNotify(displayedProgress);
|
|
371
|
+
|
|
372
|
+
seekInput = progressSlider.GetComponent<ReplaySeekInput>();
|
|
373
|
+
if (seekInput == null) seekInput = progressSlider.gameObject.AddComponent<ReplaySeekInput>();
|
|
374
|
+
seekInput.Initialize(progressSlider, SeekTo);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private void ConfigureSeekOverlay()
|
|
378
|
+
{
|
|
379
|
+
if (seekLoadingOverlay == null)
|
|
380
|
+
{
|
|
381
|
+
Transform existing = transform.Find("ReplaySeekOverlay");
|
|
382
|
+
if (existing != null) seekLoadingOverlay = existing.gameObject;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (seekLoadingOverlay == null)
|
|
386
|
+
{
|
|
387
|
+
GameObject prefab = Resources.Load<GameObject>("UIPrefabs/ReplaySeekOverlay");
|
|
388
|
+
seekLoadingOverlay = Instantiate(prefab, transform, false);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
RectTransform rect = seekLoadingOverlay.GetComponent<RectTransform>();
|
|
392
|
+
if (rect != null)
|
|
393
|
+
{
|
|
394
|
+
rect.anchorMin = Vector2.zero;
|
|
395
|
+
rect.anchorMax = Vector2.one;
|
|
396
|
+
rect.offsetMin = Vector2.zero;
|
|
397
|
+
rect.offsetMax = Vector2.zero;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
seekLoadingOverlay.transform.SetAsFirstSibling();
|
|
401
|
+
seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
|
|
402
|
+
if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
|
|
403
|
+
seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
|
|
404
|
+
wasSeeking = ReplaySessionState.IsSeeking;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private void ConfigureSeekSnapshot()
|
|
408
|
+
{
|
|
409
|
+
if (seekSnapshotImage == null)
|
|
410
|
+
{
|
|
411
|
+
Transform existing = transform.Find("ReplaySeekSnapshot");
|
|
412
|
+
if (existing != null) seekSnapshotImage = existing.GetComponent<RawImage>();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (seekSnapshotImage == null)
|
|
416
|
+
{
|
|
417
|
+
var snapshot = new GameObject(
|
|
418
|
+
"ReplaySeekSnapshot",
|
|
419
|
+
typeof(RectTransform),
|
|
420
|
+
typeof(CanvasRenderer),
|
|
421
|
+
typeof(RawImage));
|
|
422
|
+
snapshot.transform.SetParent(transform, false);
|
|
423
|
+
seekSnapshotImage = snapshot.GetComponent<RawImage>();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
RectTransform rect = seekSnapshotImage.rectTransform;
|
|
427
|
+
rect.anchorMin = Vector2.zero;
|
|
428
|
+
rect.anchorMax = Vector2.one;
|
|
429
|
+
rect.offsetMin = Vector2.zero;
|
|
430
|
+
rect.offsetMax = Vector2.zero;
|
|
431
|
+
seekSnapshotImage.raycastTarget = false;
|
|
432
|
+
seekSnapshotImage.transform.SetAsFirstSibling();
|
|
433
|
+
if (seekLoadingOverlay != null) seekLoadingOverlay.transform.SetSiblingIndex(1);
|
|
434
|
+
seekSnapshotImage.gameObject.SetActive(false);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private static Texture2D CreateScaledSnapshot(Texture2D source, float scale)
|
|
438
|
+
{
|
|
439
|
+
float sanitizedScale = Mathf.Clamp(scale, 0.25f, 1f);
|
|
440
|
+
int width = Mathf.Max(1, Mathf.RoundToInt(source.width * sanitizedScale));
|
|
441
|
+
int height = Mathf.Max(1, Mathf.RoundToInt(source.height * sanitizedScale));
|
|
442
|
+
RenderTexture temporary = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
|
|
443
|
+
RenderTexture previous = RenderTexture.active;
|
|
444
|
+
Texture2D result = null;
|
|
445
|
+
|
|
446
|
+
try
|
|
447
|
+
{
|
|
448
|
+
Graphics.Blit(source, temporary);
|
|
449
|
+
RenderTexture.active = temporary;
|
|
450
|
+
result = new Texture2D(width, height, TextureFormat.RGB24, false)
|
|
451
|
+
{
|
|
452
|
+
name = "Replay Seek Snapshot",
|
|
453
|
+
filterMode = FilterMode.Bilinear,
|
|
454
|
+
wrapMode = TextureWrapMode.Clamp
|
|
455
|
+
};
|
|
456
|
+
result.ReadPixels(new Rect(0f, 0f, width, height), 0, 0, false);
|
|
457
|
+
result.Apply(false, true);
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
catch
|
|
461
|
+
{
|
|
462
|
+
if (result != null) Destroy(result);
|
|
463
|
+
throw;
|
|
464
|
+
}
|
|
465
|
+
finally
|
|
466
|
+
{
|
|
467
|
+
RenderTexture.active = previous;
|
|
468
|
+
RenderTexture.ReleaseTemporary(temporary);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private void ClearSeekSnapshot()
|
|
473
|
+
{
|
|
474
|
+
SetSeekOverlayBackgroundVisible(true);
|
|
475
|
+
if (seekSnapshotImage != null)
|
|
476
|
+
{
|
|
477
|
+
seekSnapshotImage.texture = null;
|
|
478
|
+
seekSnapshotImage.gameObject.SetActive(false);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (seekSnapshotTexture == null) return;
|
|
482
|
+
Destroy(seekSnapshotTexture);
|
|
483
|
+
seekSnapshotTexture = null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private void SetSeekOverlayBackgroundVisible(bool isVisible)
|
|
487
|
+
{
|
|
488
|
+
if (seekOverlayBackground == null) return;
|
|
489
|
+
Color color = seekOverlayBackgroundColor;
|
|
490
|
+
if (!isVisible) color.a = 0.5f;
|
|
491
|
+
seekOverlayBackground.color = color;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
internal sealed class ReplaySeekInput : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
|
496
|
+
{
|
|
497
|
+
private Slider slider;
|
|
498
|
+
private Action<float> onSeekRequested;
|
|
499
|
+
|
|
500
|
+
internal bool IsDragging { get; private set; }
|
|
501
|
+
|
|
502
|
+
internal void Initialize(Slider targetSlider, Action<float> callback)
|
|
503
|
+
{
|
|
504
|
+
slider = targetSlider;
|
|
505
|
+
onSeekRequested = callback;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
internal void Clear()
|
|
509
|
+
{
|
|
510
|
+
IsDragging = false;
|
|
511
|
+
onSeekRequested = null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public void OnPointerDown(PointerEventData eventData)
|
|
515
|
+
{
|
|
516
|
+
if (slider == null || !slider.interactable) return;
|
|
517
|
+
IsDragging = true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public void OnPointerUp(PointerEventData eventData)
|
|
521
|
+
{
|
|
522
|
+
if (!IsDragging) return;
|
|
523
|
+
IsDragging = false;
|
|
524
|
+
onSeekRequested?.Invoke(slider.normalizedValue);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|