app.hypergames.hypersdk 1.11.0-beta.2 → 1.11.0-beta.21
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 +146 -0
- package/Editor/Scripts/Inspectors/HyperSDKSettingsEditor.cs +6 -0
- package/HyperLoader.unity +2 -2
- package/Runtime/Internal/Core/HyperRuntime.cs +104 -9
- package/Runtime/Internal/Managers/DataManager.cs +2 -1
- package/Runtime/Internal/Managers/EventManager.cs +7 -1
- package/Runtime/Internal/Managers/GameManager.cs +88 -51
- package/Runtime/Internal/Managers/ReplayManager.cs +5 -1
- package/Runtime/Internal/Managers/TimeManager.cs +25 -8
- package/Runtime/Internal/Replay/ReplaySessionState.cs +66 -6
- package/Runtime/Internal/ScriptableObjects/HyperSDKSettings.cs +4 -0
- package/Runtime/Internal/UI/Components/ReplayController.cs +134 -11
- package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs +18 -0
- package/Runtime/Internal/UI/Components/ReplaySeekOverlay.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs +145 -0
- package/Runtime/Internal/UI/Modals/ReplayPausedModal.cs.meta +2 -0
- package/Runtime/Internal/UI/Modals/StartingInstructionModal.cs +1 -0
- package/Runtime/Public/Core/HyperSDK.cs +16 -0
- package/Runtime/Public/Replay/ReplayPlaybackEngine.cs +127 -3
- package/Runtime/Resources/Fonts/Nunito/Nunito-Bold SDF.asset +500 -457
- package/Runtime/Resources/Fonts/Wonder Boys/Wonder Boys.asset +394 -379
- package/Runtime/Resources/HyperSDKSettings.asset +1 -0
- package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png +0 -0
- package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Normal.png.meta +156 -0
- package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png +0 -0
- package/Runtime/Resources/Sprites/Buttons/Hyper Long White Button_Pressed.png.meta +156 -0
- package/Runtime/Resources/Sprites/Circle Loader/Property 1=1.png.meta +7 -7
- package/Runtime/Resources/Sprites/Circle Loader/Property 1=2.png.meta +7 -7
- package/Runtime/Resources/Sprites/Circle Loader/Property 1=3.png.meta +7 -7
- package/Runtime/Resources/Sprites/Circle Loader/Property 1=4.png.meta +7 -7
- package/Runtime/Resources/Sprites/Circle Loader/Property 1=5.png.meta +7 -7
- package/Runtime/Resources/Sprites/Replays/Replay.png +0 -0
- package/Runtime/Resources/Sprites/Replays/{Recording.png.meta → Replay.png.meta} +9 -9
- package/Runtime/Resources/UIPrefabs/ControlBar.prefab +5 -5
- package/Runtime/Resources/UIPrefabs/ReplayController.prefab +305 -183
- package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab +2194 -0
- package/Runtime/Resources/UIPrefabs/ReplayPausedModal.prefab.meta +7 -0
- package/Runtime/Resources/UIPrefabs/ReplaySeekOverlay.prefab +291 -6
- package/Runtime/Shared/Replay/IReplayPlaybackEngine.cs +3 -0
- package/Samples~/LaneRunner/LaneRunnerSample.unity +4 -3
- package/Samples~/LaneRunner/Prefabs/DoubleScorePickup.prefab +1 -1
- package/Samples~/LaneRunner/Prefabs/Obstacle.prefab +1 -1
- package/Samples~/LaneRunner/README.md +8 -1
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerPlayer.cs +18 -4
- package/Samples~/LaneRunner/Scripts/Gameplay/LaneRunnerSpawner.cs +3 -1
- package/Samples~/LaneRunner/Scripts/Hyper/LaneRunnerTournamentAdapter.cs +8 -0
- package/package.json +1 -1
- package/Runtime/Resources/Sprites/Replays/Recording.png +0 -0
|
@@ -32,6 +32,9 @@ namespace Hyper.Internal.Managers
|
|
|
32
32
|
private float _battleTimer;
|
|
33
33
|
private bool _isPracticeRunning;
|
|
34
34
|
private bool _isBattleRunning;
|
|
35
|
+
private bool _resumePracticeAfterPause;
|
|
36
|
+
private bool _resumeBattleAfterPause;
|
|
37
|
+
private bool _hasPausedGameplayTimerState;
|
|
35
38
|
|
|
36
39
|
private float _fiveSecondTimer = 5f;
|
|
37
40
|
private bool _isFiveSecondTimerRunning;
|
|
@@ -111,6 +114,7 @@ namespace Hyper.Internal.Managers
|
|
|
111
114
|
public void Reset()
|
|
112
115
|
{
|
|
113
116
|
_practiceTimer = 0;
|
|
117
|
+
ClearPausedGameplayTimerState();
|
|
114
118
|
_lastUnscaledTime = Time.unscaledTime;
|
|
115
119
|
}
|
|
116
120
|
|
|
@@ -120,6 +124,7 @@ namespace Hyper.Internal.Managers
|
|
|
120
124
|
_battleTimer = _gameManager != null ? _gameManager.Config.TimeLimit : 0f;
|
|
121
125
|
_isPracticeRunning = false;
|
|
122
126
|
_isBattleRunning = false;
|
|
127
|
+
ClearPausedGameplayTimerState();
|
|
123
128
|
_timeUpTriggered = false;
|
|
124
129
|
_timeUpPending = false;
|
|
125
130
|
_hasGameEnded = false;
|
|
@@ -129,18 +134,17 @@ namespace Hyper.Internal.Managers
|
|
|
129
134
|
|
|
130
135
|
public void ResumeGameTimersAfterPause()
|
|
131
136
|
{
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
{
|
|
138
|
-
StartPractice();
|
|
139
|
-
}
|
|
137
|
+
if (!_hasPausedGameplayTimerState) return;
|
|
138
|
+
|
|
139
|
+
_isPracticeRunning = _resumePracticeAfterPause;
|
|
140
|
+
_isBattleRunning = _resumeBattleAfterPause;
|
|
141
|
+
ClearPausedGameplayTimerState();
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
public void StartActiveGameTimer()
|
|
143
145
|
{
|
|
146
|
+
ClearPausedGameplayTimerState();
|
|
147
|
+
|
|
144
148
|
if (_gameManager.IsBattleMode)
|
|
145
149
|
{
|
|
146
150
|
StartSessionTimer();
|
|
@@ -158,6 +162,7 @@ namespace Hyper.Internal.Managers
|
|
|
158
162
|
{
|
|
159
163
|
_hasGameEnded = true;
|
|
160
164
|
_timeUpPending = false;
|
|
165
|
+
ClearPausedGameplayTimerState();
|
|
161
166
|
StopBattle();
|
|
162
167
|
StopPractice();
|
|
163
168
|
}
|
|
@@ -168,6 +173,11 @@ namespace Hyper.Internal.Managers
|
|
|
168
173
|
/// </summary>
|
|
169
174
|
public void PauseGameplayTimers()
|
|
170
175
|
{
|
|
176
|
+
if (_hasPausedGameplayTimerState) return;
|
|
177
|
+
|
|
178
|
+
_resumePracticeAfterPause = _isPracticeRunning;
|
|
179
|
+
_resumeBattleAfterPause = _isBattleRunning;
|
|
180
|
+
_hasPausedGameplayTimerState = true;
|
|
171
181
|
StopBattle();
|
|
172
182
|
StopPractice();
|
|
173
183
|
}
|
|
@@ -208,6 +218,13 @@ namespace Hyper.Internal.Managers
|
|
|
208
218
|
internal void StartBattle() => _isBattleRunning = true;
|
|
209
219
|
internal void StopBattle() => _isBattleRunning = false;
|
|
210
220
|
|
|
221
|
+
private void ClearPausedGameplayTimerState()
|
|
222
|
+
{
|
|
223
|
+
_resumePracticeAfterPause = false;
|
|
224
|
+
_resumeBattleAfterPause = false;
|
|
225
|
+
_hasPausedGameplayTimerState = false;
|
|
226
|
+
}
|
|
227
|
+
|
|
211
228
|
internal bool TrySetSessionDurationFromBackendRemainingTime(float remainingTimeSeconds)
|
|
212
229
|
{
|
|
213
230
|
if (_hasBackendSessionDuration)
|
|
@@ -24,6 +24,7 @@ namespace Hyper.Internal
|
|
|
24
24
|
public static bool IsSeekRestartPending => IsSeeking && !_seekPlaybackReady;
|
|
25
25
|
public static float SeekTarget01 { get; private set; }
|
|
26
26
|
public static bool HasEnded { get; private set; }
|
|
27
|
+
public static float GameplayStartProgress01 { get; private set; }
|
|
27
28
|
|
|
28
29
|
internal static string EditorReplayFilePath =>
|
|
29
30
|
Path.Combine(Application.persistentDataPath, "hyper-replay.ndjson");
|
|
@@ -31,6 +32,9 @@ namespace Hyper.Internal
|
|
|
31
32
|
public static event Action<float> SeekCompleted;
|
|
32
33
|
|
|
33
34
|
private static bool _seekPlaybackReady;
|
|
35
|
+
private static bool _watchAgainAutoStartPending;
|
|
36
|
+
private static bool _isSeekAudioMuted;
|
|
37
|
+
private static float _volumeBeforeSeek;
|
|
34
38
|
|
|
35
39
|
public static void Configure(
|
|
36
40
|
string playerName,
|
|
@@ -38,6 +42,7 @@ namespace Hyper.Internal
|
|
|
38
42
|
long originalRandomSeed,
|
|
39
43
|
string entryId = null)
|
|
40
44
|
{
|
|
45
|
+
RestoreAudioAfterSeek();
|
|
41
46
|
IsActive = true;
|
|
42
47
|
PlayerName = playerName ?? string.Empty;
|
|
43
48
|
AvatarUrl = avatarUrl ?? string.Empty;
|
|
@@ -51,7 +56,9 @@ namespace Hyper.Internal
|
|
|
51
56
|
IsSeeking = false;
|
|
52
57
|
SeekTarget01 = 0f;
|
|
53
58
|
_seekPlaybackReady = false;
|
|
59
|
+
_watchAgainAutoStartPending = false;
|
|
54
60
|
HasEnded = false;
|
|
61
|
+
GameplayStartProgress01 = 0f;
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
public static void SetMetadata(string playerName, string avatarUrl, long originalRandomSeed)
|
|
@@ -76,19 +83,28 @@ namespace Hyper.Internal
|
|
|
76
83
|
AvatarTexture = texture;
|
|
77
84
|
}
|
|
78
85
|
|
|
79
|
-
public static
|
|
86
|
+
public static bool SetProgress(float progress)
|
|
80
87
|
{
|
|
81
|
-
if (!IsActive) return;
|
|
82
|
-
if (IsSeeking && !_seekPlaybackReady) return;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
if (!IsActive) return false;
|
|
89
|
+
if (IsSeeking && !_seekPlaybackReady) return false;
|
|
90
|
+
float rawProgress = Math.Max(0f, Math.Min(1f, progress));
|
|
91
|
+
Progress01 = GameplayStartProgress01 >= 1f
|
|
92
|
+
? 0f
|
|
93
|
+
: Math.Max(0f, Math.Min(1f,
|
|
94
|
+
(rawProgress - GameplayStartProgress01) / (1f - GameplayStartProgress01)));
|
|
95
|
+
|
|
96
|
+
bool hasReachedGameplayStart = rawProgress + 0.000001f >= GameplayStartProgress01;
|
|
97
|
+
if (IsSeeking && hasReachedGameplayStart && Progress01 >= SeekTarget01)
|
|
86
98
|
{
|
|
87
99
|
IsSeeking = false;
|
|
88
100
|
_seekPlaybackReady = false;
|
|
89
101
|
PlaybackSpeed = UserPlaybackSpeed;
|
|
102
|
+
RestoreAudioAfterSeek();
|
|
90
103
|
SeekCompleted?.Invoke(PlaybackSpeed);
|
|
104
|
+
return true;
|
|
91
105
|
}
|
|
106
|
+
|
|
107
|
+
return false;
|
|
92
108
|
}
|
|
93
109
|
|
|
94
110
|
public static void SetPlaybackSpeed(float speed)
|
|
@@ -105,6 +121,7 @@ namespace Hyper.Internal
|
|
|
105
121
|
Progress01 = 1f;
|
|
106
122
|
if (IsSeeking) PlaybackSpeed = UserPlaybackSpeed;
|
|
107
123
|
IsSeeking = false;
|
|
124
|
+
RestoreAudioAfterSeek();
|
|
108
125
|
HasEnded = true;
|
|
109
126
|
}
|
|
110
127
|
|
|
@@ -117,6 +134,7 @@ namespace Hyper.Internal
|
|
|
117
134
|
PlaybackSpeed = SanitizeSpeed(seekSpeed);
|
|
118
135
|
IsSeeking = true;
|
|
119
136
|
_seekPlaybackReady = false;
|
|
137
|
+
MuteAudioForSeek();
|
|
120
138
|
}
|
|
121
139
|
|
|
122
140
|
public static void ResetForSeekRestart()
|
|
@@ -124,6 +142,7 @@ namespace Hyper.Internal
|
|
|
124
142
|
if (!IsActive || !IsSeeking) return;
|
|
125
143
|
Progress01 = 0f;
|
|
126
144
|
_seekPlaybackReady = false;
|
|
145
|
+
_watchAgainAutoStartPending = false;
|
|
127
146
|
HasEnded = false;
|
|
128
147
|
}
|
|
129
148
|
|
|
@@ -133,20 +152,42 @@ namespace Hyper.Internal
|
|
|
133
152
|
_seekPlaybackReady = true;
|
|
134
153
|
}
|
|
135
154
|
|
|
155
|
+
internal static void EnsureAudioMutedDuringSeek()
|
|
156
|
+
{
|
|
157
|
+
if (_isSeekAudioMuted)
|
|
158
|
+
AudioListener.volume = 0f;
|
|
159
|
+
}
|
|
160
|
+
|
|
136
161
|
public static void ResetForWatchAgain()
|
|
137
162
|
{
|
|
138
163
|
if (!IsActive) return;
|
|
164
|
+
RestoreAudioAfterSeek();
|
|
139
165
|
Progress01 = 0f;
|
|
140
166
|
PlaybackSpeed = 1f;
|
|
141
167
|
UserPlaybackSpeed = 1f;
|
|
142
168
|
IsSeeking = false;
|
|
143
169
|
SeekTarget01 = 0f;
|
|
144
170
|
_seekPlaybackReady = false;
|
|
171
|
+
_watchAgainAutoStartPending = true;
|
|
145
172
|
HasEnded = false;
|
|
146
173
|
}
|
|
147
174
|
|
|
175
|
+
public static void SetGameplayStartProgress(float progress)
|
|
176
|
+
{
|
|
177
|
+
if (!IsActive) return;
|
|
178
|
+
GameplayStartProgress01 = Math.Max(0f, Math.Min(1f, progress));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public static bool ConsumeWatchAgainAutoStart()
|
|
182
|
+
{
|
|
183
|
+
bool pending = _watchAgainAutoStartPending;
|
|
184
|
+
_watchAgainAutoStartPending = false;
|
|
185
|
+
return pending;
|
|
186
|
+
}
|
|
187
|
+
|
|
148
188
|
public static void Clear()
|
|
149
189
|
{
|
|
190
|
+
RestoreAudioAfterSeek();
|
|
150
191
|
IsActive = false;
|
|
151
192
|
PlayerName = string.Empty;
|
|
152
193
|
AvatarUrl = string.Empty;
|
|
@@ -160,7 +201,9 @@ namespace Hyper.Internal
|
|
|
160
201
|
IsSeeking = false;
|
|
161
202
|
SeekTarget01 = 0f;
|
|
162
203
|
_seekPlaybackReady = false;
|
|
204
|
+
_watchAgainAutoStartPending = false;
|
|
163
205
|
HasEnded = false;
|
|
206
|
+
GameplayStartProgress01 = 0f;
|
|
164
207
|
SeekCompleted = null;
|
|
165
208
|
}
|
|
166
209
|
|
|
@@ -169,6 +212,23 @@ namespace Hyper.Internal
|
|
|
169
212
|
return float.IsNaN(speed) || float.IsInfinity(speed) || speed <= 0f ? 1f : speed;
|
|
170
213
|
}
|
|
171
214
|
|
|
215
|
+
private static void MuteAudioForSeek()
|
|
216
|
+
{
|
|
217
|
+
if (_isSeekAudioMuted) return;
|
|
218
|
+
|
|
219
|
+
_volumeBeforeSeek = AudioListener.volume;
|
|
220
|
+
_isSeekAudioMuted = true;
|
|
221
|
+
AudioListener.volume = 0f;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private static void RestoreAudioAfterSeek()
|
|
225
|
+
{
|
|
226
|
+
if (!_isSeekAudioMuted) return;
|
|
227
|
+
|
|
228
|
+
AudioListener.volume = _volumeBeforeSeek;
|
|
229
|
+
_isSeekAudioMuted = false;
|
|
230
|
+
}
|
|
231
|
+
|
|
172
232
|
private static void ReleaseAvatarTexture()
|
|
173
233
|
{
|
|
174
234
|
if (AvatarTexture != null)
|
|
@@ -76,6 +76,10 @@ namespace Hyper.Internal
|
|
|
76
76
|
"instead of using default restart. Listen to HyperSDK.Event.OnCustomRestart in your game manager.")]
|
|
77
77
|
public bool useCustomRestartFlow = false;
|
|
78
78
|
|
|
79
|
+
[Tooltip("Use the game-owned custom restart flow for replay Watch Again and seeking. " +
|
|
80
|
+
"Leave disabled to rebuild the game through HyperLoader for deterministic replay state.")]
|
|
81
|
+
public bool useCustomReplayRestartFlow = false;
|
|
82
|
+
|
|
79
83
|
#endregion
|
|
80
84
|
|
|
81
85
|
#region Editor Testing
|
|
@@ -15,12 +15,15 @@ namespace Hyper.Internal
|
|
|
15
15
|
[SerializeField] private Slider progressSlider;
|
|
16
16
|
[SerializeField] private TextMeshProUGUI replayLabel;
|
|
17
17
|
[SerializeField] private RawImage playerAvatar;
|
|
18
|
-
[SerializeField] private Button
|
|
18
|
+
[SerializeField] private Button pauseButton;
|
|
19
19
|
[SerializeField] private Button playbackSpeedButton;
|
|
20
20
|
[SerializeField] private TextMeshProUGUI playbackSpeedLabel;
|
|
21
21
|
[SerializeField] private float[] playbackSpeeds = { 1.5f, 2f, 3f, 4f, 5f, 1f };
|
|
22
|
-
[SerializeField, Min(1f)] private float seekPlaybackSpeed =
|
|
22
|
+
[SerializeField, Min(1f)] private float seekPlaybackSpeed = 10f;
|
|
23
23
|
[SerializeField, Range(0.25f, 1f)] private float seekSnapshotScale = 0.55f;
|
|
24
|
+
[SerializeField, Range(0f, 0.5f)] private float seekPreparationProgress = 0.2f;
|
|
25
|
+
[SerializeField, Range(0f, 0.15f)] private float seekPreparationProgressVariation = 0.05f;
|
|
26
|
+
[SerializeField, Min(0.1f)] private float seekPreparationDuration = 3f;
|
|
24
27
|
[SerializeField] private GameObject seekLoadingOverlay;
|
|
25
28
|
[SerializeField] private RawImage seekSnapshotImage;
|
|
26
29
|
|
|
@@ -32,6 +35,12 @@ namespace Hyper.Internal
|
|
|
32
35
|
private Texture2D seekSnapshotTexture;
|
|
33
36
|
private Image seekOverlayBackground;
|
|
34
37
|
private Color seekOverlayBackgroundColor;
|
|
38
|
+
private ReplaySeekOverlay seekOverlay;
|
|
39
|
+
private static readonly System.Random SeekProgressRandom = new System.Random();
|
|
40
|
+
private static float displayedSeekProgress;
|
|
41
|
+
private static float firstSeekPreparationTarget;
|
|
42
|
+
private static float secondSeekPreparationTarget;
|
|
43
|
+
private static float seekPreparationCeiling;
|
|
35
44
|
|
|
36
45
|
private void Awake()
|
|
37
46
|
{
|
|
@@ -41,7 +50,7 @@ namespace Hyper.Internal
|
|
|
41
50
|
ConfigureSeekSnapshot();
|
|
42
51
|
ApplyInitialPlaybackSpeed();
|
|
43
52
|
ReplaySessionState.SeekCompleted += RestorePlaybackAfterSeek;
|
|
44
|
-
if (
|
|
53
|
+
if (pauseButton != null) pauseButton.onClick.AddListener(PauseReplay);
|
|
45
54
|
if (playbackSpeedButton != null) playbackSpeedButton.onClick.AddListener(CyclePlaybackSpeed);
|
|
46
55
|
}
|
|
47
56
|
|
|
@@ -79,6 +88,11 @@ namespace Hyper.Internal
|
|
|
79
88
|
SetSeekingPresentation(isSeeking);
|
|
80
89
|
}
|
|
81
90
|
|
|
91
|
+
if (isSeeking)
|
|
92
|
+
{
|
|
93
|
+
UpdateSeekProgress(true);
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
if (progressSlider != null && (seekInput == null || !seekInput.IsDragging))
|
|
83
97
|
{
|
|
84
98
|
float displayedProgress = isCapturingSeek
|
|
@@ -94,7 +108,7 @@ namespace Hyper.Internal
|
|
|
94
108
|
|
|
95
109
|
private void OnDestroy()
|
|
96
110
|
{
|
|
97
|
-
if (
|
|
111
|
+
if (pauseButton != null) pauseButton.onClick.RemoveListener(PauseReplay);
|
|
98
112
|
if (playbackSpeedButton != null) playbackSpeedButton.onClick.RemoveListener(CyclePlaybackSpeed);
|
|
99
113
|
ReplaySessionState.SeekCompleted -= RestorePlaybackAfterSeek;
|
|
100
114
|
seekInput?.Clear();
|
|
@@ -124,7 +138,7 @@ namespace Hyper.Internal
|
|
|
124
138
|
|
|
125
139
|
internal void SetReplayEnded()
|
|
126
140
|
{
|
|
127
|
-
if (
|
|
141
|
+
if (pauseButton != null) pauseButton.interactable = false;
|
|
128
142
|
if (playbackSpeedButton != null) playbackSpeedButton.interactable = false;
|
|
129
143
|
if (progressSlider != null) progressSlider.interactable = false;
|
|
130
144
|
if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(false);
|
|
@@ -158,10 +172,9 @@ namespace Hyper.Internal
|
|
|
158
172
|
SetSeekingPresentation(ReplaySessionState.IsSeeking);
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
private void
|
|
175
|
+
private void PauseReplay()
|
|
162
176
|
{
|
|
163
|
-
|
|
164
|
-
HyperRuntime.GameManager?.ExitReplayEarly();
|
|
177
|
+
HyperRuntime.GameManager?.PauseReplay();
|
|
165
178
|
}
|
|
166
179
|
|
|
167
180
|
private void CyclePlaybackSpeed()
|
|
@@ -190,6 +203,7 @@ namespace Hyper.Internal
|
|
|
190
203
|
|
|
191
204
|
float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
|
|
192
205
|
float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
|
|
206
|
+
InitializeSeekProgressPresentation();
|
|
193
207
|
StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
|
|
194
208
|
}
|
|
195
209
|
|
|
@@ -239,7 +253,6 @@ namespace Hyper.Internal
|
|
|
239
253
|
private void RestorePlaybackAfterSeek(float playbackSpeed)
|
|
240
254
|
{
|
|
241
255
|
float speed = SanitizePlaybackSpeed(playbackSpeed);
|
|
242
|
-
Time.timeScale = speed;
|
|
243
256
|
AlignPlaybackSpeedIndex(speed);
|
|
244
257
|
UpdatePlaybackSpeedLabel(speed);
|
|
245
258
|
SetSeekingPresentation(false);
|
|
@@ -261,6 +274,7 @@ namespace Hyper.Internal
|
|
|
261
274
|
private void SetSeekingPresentation(bool isSeeking)
|
|
262
275
|
{
|
|
263
276
|
ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
|
|
277
|
+
UpdateSeekProgress(isSeeking);
|
|
264
278
|
if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
|
|
265
279
|
if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
266
280
|
if (playbackSpeedButton != null)
|
|
@@ -268,7 +282,7 @@ namespace Hyper.Internal
|
|
|
268
282
|
playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
269
283
|
}
|
|
270
284
|
|
|
271
|
-
if (
|
|
285
|
+
if (pauseButton != null) pauseButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
272
286
|
if (!isSeeking) ClearSeekSnapshot();
|
|
273
287
|
}
|
|
274
288
|
|
|
@@ -341,7 +355,12 @@ namespace Hyper.Internal
|
|
|
341
355
|
private void AutoBindReferences()
|
|
342
356
|
{
|
|
343
357
|
if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
|
|
344
|
-
|
|
358
|
+
|
|
359
|
+
Button[] buttons = GetComponentsInChildren<Button>(true);
|
|
360
|
+
for (int i = 0; i < buttons.Length; i++)
|
|
361
|
+
{
|
|
362
|
+
if (pauseButton == null && buttons[i].name == "PauseButton") pauseButton = buttons[i];
|
|
363
|
+
}
|
|
345
364
|
|
|
346
365
|
TextMeshProUGUI[] labels = GetComponentsInChildren<TextMeshProUGUI>(true);
|
|
347
366
|
for (int i = 0; replayLabel == null && i < labels.Length; i++)
|
|
@@ -400,10 +419,114 @@ namespace Hyper.Internal
|
|
|
400
419
|
seekLoadingOverlay.transform.SetAsFirstSibling();
|
|
401
420
|
seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
|
|
402
421
|
if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
|
|
422
|
+
seekOverlay = seekLoadingOverlay.GetComponent<ReplaySeekOverlay>();
|
|
403
423
|
seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
|
|
404
424
|
wasSeeking = ReplaySessionState.IsSeeking;
|
|
405
425
|
}
|
|
406
426
|
|
|
427
|
+
private void UpdateSeekProgress(bool isSeeking)
|
|
428
|
+
{
|
|
429
|
+
if (seekOverlay == null) return;
|
|
430
|
+
|
|
431
|
+
float progress;
|
|
432
|
+
if (!isSeeking)
|
|
433
|
+
{
|
|
434
|
+
progress = 1f;
|
|
435
|
+
}
|
|
436
|
+
else if (isCapturingSeek || ReplaySessionState.IsSeekRestartPending)
|
|
437
|
+
{
|
|
438
|
+
AdvanceSeekPreparationProgress();
|
|
439
|
+
progress = displayedSeekProgress;
|
|
440
|
+
}
|
|
441
|
+
else
|
|
442
|
+
{
|
|
443
|
+
EnsureSeekProgressPresentation();
|
|
444
|
+
float target = ReplaySessionState.SeekTarget01;
|
|
445
|
+
float reconstructionProgress = target <= Mathf.Epsilon
|
|
446
|
+
? 1f
|
|
447
|
+
: ReplaySessionState.Progress01 / target;
|
|
448
|
+
float realProgress = Mathf.Lerp(
|
|
449
|
+
seekPreparationCeiling,
|
|
450
|
+
1f,
|
|
451
|
+
Mathf.Clamp01(reconstructionProgress));
|
|
452
|
+
AdvanceSeekReconstructionProgress(realProgress);
|
|
453
|
+
progress = displayedSeekProgress;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
seekOverlay.SetProgress(progress);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private void AdvanceSeekPreparationProgress()
|
|
460
|
+
{
|
|
461
|
+
EnsureSeekProgressPresentation();
|
|
462
|
+
float duration = Mathf.Max(0.1f, seekPreparationDuration);
|
|
463
|
+
float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
|
|
464
|
+
|
|
465
|
+
if (displayedSeekProgress < firstSeekPreparationTarget)
|
|
466
|
+
{
|
|
467
|
+
float speed = firstSeekPreparationTarget / (duration * 0.35f);
|
|
468
|
+
displayedSeekProgress = Mathf.MoveTowards(
|
|
469
|
+
displayedSeekProgress,
|
|
470
|
+
firstSeekPreparationTarget,
|
|
471
|
+
speed * deltaTime);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (displayedSeekProgress < secondSeekPreparationTarget)
|
|
476
|
+
{
|
|
477
|
+
float speed = (secondSeekPreparationTarget - firstSeekPreparationTarget) / (duration * 0.2f);
|
|
478
|
+
displayedSeekProgress = Mathf.MoveTowards(
|
|
479
|
+
displayedSeekProgress,
|
|
480
|
+
secondSeekPreparationTarget,
|
|
481
|
+
speed * deltaTime);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
float crawlResponse = 1f - Mathf.Exp(-deltaTime / (duration * 0.65f));
|
|
486
|
+
displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, seekPreparationCeiling, crawlResponse);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private void InitializeSeekProgressPresentation()
|
|
490
|
+
{
|
|
491
|
+
float center = Mathf.Clamp01(seekPreparationProgress);
|
|
492
|
+
float variation = Mathf.Max(0f, seekPreparationProgressVariation);
|
|
493
|
+
float minimum = Mathf.Clamp01(center - variation);
|
|
494
|
+
float maximum = Mathf.Clamp01(center + variation);
|
|
495
|
+
|
|
496
|
+
displayedSeekProgress = 0f;
|
|
497
|
+
seekPreparationCeiling = RandomRange(minimum, maximum);
|
|
498
|
+
firstSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.25f, 0.35f);
|
|
499
|
+
secondSeekPreparationTarget = seekPreparationCeiling * RandomRange(0.62f, 0.76f);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
private void EnsureSeekProgressPresentation()
|
|
503
|
+
{
|
|
504
|
+
if (seekPreparationCeiling > 0f) return;
|
|
505
|
+
InitializeSeekProgressPresentation();
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private static float RandomRange(float minimum, float maximum)
|
|
509
|
+
{
|
|
510
|
+
if (maximum <= minimum) return minimum;
|
|
511
|
+
return minimum + ((float)SeekProgressRandom.NextDouble() * (maximum - minimum));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private void AdvanceSeekReconstructionProgress(float realProgress)
|
|
515
|
+
{
|
|
516
|
+
float target = Mathf.Max(displayedSeekProgress, realProgress);
|
|
517
|
+
if (target >= 0.999f)
|
|
518
|
+
{
|
|
519
|
+
displayedSeekProgress = 1f;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
float deltaTime = Mathf.Min(Time.unscaledDeltaTime, 0.1f);
|
|
524
|
+
float gap = target - displayedSeekProgress;
|
|
525
|
+
float responseSpeed = Mathf.Lerp(5f, 14f, Mathf.Clamp01(gap * 4f));
|
|
526
|
+
float response = 1f - Mathf.Exp(-responseSpeed * deltaTime);
|
|
527
|
+
displayedSeekProgress = Mathf.Lerp(displayedSeekProgress, target, response);
|
|
528
|
+
}
|
|
529
|
+
|
|
407
530
|
private void ConfigureSeekSnapshot()
|
|
408
531
|
{
|
|
409
532
|
if (seekSnapshotImage == null)
|
|
@@ -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.RoundToInt(Mathf.Clamp01(progress01) * 100f);
|
|
15
|
+
progressText.text = $"{percentage}%";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
using TMPro;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
using UnityEngine.UI;
|
|
4
|
+
|
|
5
|
+
namespace Hyper.Internal
|
|
6
|
+
{
|
|
7
|
+
internal sealed class ReplayPausedModal : MonoBehaviour
|
|
8
|
+
{
|
|
9
|
+
[SerializeField] private TextMeshProUGUI scoreValue;
|
|
10
|
+
[SerializeField] private TextMeshProUGUI scoreOwnerName;
|
|
11
|
+
[SerializeField] private TextMeshProUGUI soundText;
|
|
12
|
+
[SerializeField] private Sprite soundIconOn;
|
|
13
|
+
[SerializeField] private Sprite soundIconOff;
|
|
14
|
+
[SerializeField] private Image soundIcon;
|
|
15
|
+
[SerializeField] private GameObject restartButton;
|
|
16
|
+
[SerializeField] private Button resumeButton;
|
|
17
|
+
[SerializeField] private Button exitButton;
|
|
18
|
+
[SerializeField] private Button soundButton;
|
|
19
|
+
[SerializeField] private Button restartActionButton;
|
|
20
|
+
[SerializeField] private GameObject exitLoadingCircle;
|
|
21
|
+
|
|
22
|
+
private bool handled;
|
|
23
|
+
|
|
24
|
+
private void Awake()
|
|
25
|
+
{
|
|
26
|
+
AutoBindButtons();
|
|
27
|
+
resumeButton?.onClick.AddListener(ResumeReplay);
|
|
28
|
+
exitButton?.onClick.AddListener(ExitReplay);
|
|
29
|
+
soundButton?.onClick.AddListener(ToggleSound);
|
|
30
|
+
restartActionButton?.onClick.AddListener(RestartReplay);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private void OnEnable()
|
|
34
|
+
{
|
|
35
|
+
handled = false;
|
|
36
|
+
SetButtonsInteractable(true);
|
|
37
|
+
exitLoadingCircle?.SetActive(false);
|
|
38
|
+
|
|
39
|
+
if (scoreValue != null)
|
|
40
|
+
{
|
|
41
|
+
scoreValue.text = (HyperRuntime.DataManager?.CurrentScore ?? 0).ToString("N0");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (scoreOwnerName != null)
|
|
45
|
+
{
|
|
46
|
+
scoreOwnerName.text = FormatScoreOwner(ReplaySessionState.PlayerName);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
UpdateSoundPresentation();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private void OnDestroy()
|
|
53
|
+
{
|
|
54
|
+
resumeButton?.onClick.RemoveListener(ResumeReplay);
|
|
55
|
+
exitButton?.onClick.RemoveListener(ExitReplay);
|
|
56
|
+
soundButton?.onClick.RemoveListener(ToggleSound);
|
|
57
|
+
restartActionButton?.onClick.RemoveListener(RestartReplay);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public void ResumeReplay()
|
|
61
|
+
{
|
|
62
|
+
if (handled) return;
|
|
63
|
+
handled = true;
|
|
64
|
+
SetButtonsInteractable(false);
|
|
65
|
+
HyperRuntime.GameManager?.ResumeReplay();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public void ExitReplay()
|
|
69
|
+
{
|
|
70
|
+
if (handled) return;
|
|
71
|
+
handled = true;
|
|
72
|
+
SetButtonsInteractable(false);
|
|
73
|
+
exitLoadingCircle?.SetActive(true);
|
|
74
|
+
HyperRuntime.GameManager?.ExitReplayEarly();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public void ToggleSound()
|
|
78
|
+
{
|
|
79
|
+
if (handled || HyperRuntime.DataManager == null) return;
|
|
80
|
+
|
|
81
|
+
int sound = HyperRuntime.DataManager.GetSound() == 1 ? 0 : 1;
|
|
82
|
+
HyperRuntime.DataManager.SetSound(sound);
|
|
83
|
+
UpdateSoundPresentation();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public void RestartReplay()
|
|
87
|
+
{
|
|
88
|
+
if (handled) return;
|
|
89
|
+
handled = true;
|
|
90
|
+
SetButtonsInteractable(false);
|
|
91
|
+
HyperRuntime.RestartReplaySession();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
internal static string FormatScoreOwner(string playerName)
|
|
95
|
+
{
|
|
96
|
+
string normalizedName = (playerName ?? string.Empty).Trim().TrimStart('@');
|
|
97
|
+
return string.IsNullOrEmpty(normalizedName)
|
|
98
|
+
? "PLAYER'S SCORE"
|
|
99
|
+
: $"@{normalizedName.ToUpperInvariant()}'S SCORE";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private void UpdateSoundPresentation()
|
|
103
|
+
{
|
|
104
|
+
bool isSoundOn = HyperRuntime.DataManager == null || HyperRuntime.DataManager.GetSound() == 1;
|
|
105
|
+
if (soundText != null) soundText.text = isSoundOn ? "Sound On" : "Sound Off";
|
|
106
|
+
if (soundIcon != null) soundIcon.sprite = isSoundOn ? soundIconOn : soundIconOff;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private void AutoBindButtons()
|
|
110
|
+
{
|
|
111
|
+
Button[] buttons = GetComponentsInChildren<Button>(true);
|
|
112
|
+
for (int i = 0; i < buttons.Length; i++)
|
|
113
|
+
{
|
|
114
|
+
switch (buttons[i].name)
|
|
115
|
+
{
|
|
116
|
+
case "Resume":
|
|
117
|
+
if (resumeButton == null) resumeButton = buttons[i];
|
|
118
|
+
break;
|
|
119
|
+
case "Exit":
|
|
120
|
+
if (exitButton == null) exitButton = buttons[i];
|
|
121
|
+
break;
|
|
122
|
+
case "SoundButton":
|
|
123
|
+
if (soundButton == null) soundButton = buttons[i];
|
|
124
|
+
break;
|
|
125
|
+
case "RestartButton":
|
|
126
|
+
if (restartActionButton == null) restartActionButton = buttons[i];
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (restartButton == null && restartActionButton != null)
|
|
132
|
+
{
|
|
133
|
+
restartButton = restartActionButton.gameObject;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private void SetButtonsInteractable(bool interactable)
|
|
138
|
+
{
|
|
139
|
+
if (resumeButton != null) resumeButton.interactable = interactable;
|
|
140
|
+
if (exitButton != null) exitButton.interactable = interactable;
|
|
141
|
+
if (soundButton != null) soundButton.interactable = interactable;
|
|
142
|
+
if (restartActionButton != null) restartActionButton.interactable = interactable;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -59,6 +59,7 @@ namespace Hyper.Internal
|
|
|
59
59
|
{
|
|
60
60
|
float elapsed = 0f;
|
|
61
61
|
float delay = Mathf.Max(0f, replayAutoStartDelay);
|
|
62
|
+
if (ReplaySessionState.ConsumeWatchAgainAutoStart()) delay *= 0.5f;
|
|
62
63
|
while (elapsed < delay)
|
|
63
64
|
{
|
|
64
65
|
if (Application.isFocused) elapsed += Time.unscaledDeltaTime;
|