app.hypergames.hypersdk 1.10.0 → 1.11.0-beta.1
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 +25 -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 +58 -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 +350 -15
- package/Runtime/Internal/Managers/ReplayManager.cs +111 -0
- package/Runtime/Internal/Managers/ReplayManager.cs.meta +11 -0
- package/Runtime/Internal/Managers/TimeManager.cs +56 -14
- 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 +501 -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 +54 -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
|
@@ -0,0 +1,501 @@
|
|
|
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
|
+
private void ExitReplay()
|
|
136
|
+
{
|
|
137
|
+
if (exitButton != null) exitButton.interactable = false;
|
|
138
|
+
HyperRuntime.GameManager?.ExitReplayEarly();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private void CyclePlaybackSpeed()
|
|
142
|
+
{
|
|
143
|
+
if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
|
|
144
|
+
playbackSpeeds == null || playbackSpeeds.Length == 0)
|
|
145
|
+
{
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
float speed = SanitizePlaybackSpeed(playbackSpeeds[playbackSpeedIndex]);
|
|
150
|
+
playbackSpeedIndex = (playbackSpeedIndex + 1) % playbackSpeeds.Length;
|
|
151
|
+
|
|
152
|
+
ReplaySessionState.SetPlaybackSpeed(speed);
|
|
153
|
+
Time.timeScale = speed;
|
|
154
|
+
UpdatePlaybackSpeedLabel(speed);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private void SeekTo(float targetProgress)
|
|
158
|
+
{
|
|
159
|
+
if (!ReplaySessionState.IsActive || ReplaySessionState.HasEnded ||
|
|
160
|
+
ReplaySessionState.IsSeeking || isCapturingSeek)
|
|
161
|
+
{
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
float resumeSpeed = SanitizePlaybackSpeed(ReplaySessionState.UserPlaybackSpeed);
|
|
166
|
+
float fastForwardSpeed = SanitizePlaybackSpeed(seekPlaybackSpeed);
|
|
167
|
+
StartCoroutine(CaptureAndSeek(targetProgress, resumeSpeed, fastForwardSpeed));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private IEnumerator CaptureAndSeek(float targetProgress, float resumeSpeed, float fastForwardSpeed)
|
|
171
|
+
{
|
|
172
|
+
isCapturingSeek = true;
|
|
173
|
+
pendingSeekTarget = Mathf.Clamp01(targetProgress);
|
|
174
|
+
|
|
175
|
+
yield return new WaitForEndOfFrame();
|
|
176
|
+
|
|
177
|
+
Texture2D capturedFrame = null;
|
|
178
|
+
try
|
|
179
|
+
{
|
|
180
|
+
capturedFrame = ScreenCapture.CaptureScreenshotAsTexture();
|
|
181
|
+
if (capturedFrame != null)
|
|
182
|
+
{
|
|
183
|
+
seekSnapshotTexture = CreateScaledSnapshot(capturedFrame, seekSnapshotScale);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (Exception exception)
|
|
187
|
+
{
|
|
188
|
+
HyperDebug.LogWarning($"[Replay] Unable to capture seek snapshot: {exception.Message}");
|
|
189
|
+
}
|
|
190
|
+
finally
|
|
191
|
+
{
|
|
192
|
+
if (capturedFrame != null) Destroy(capturedFrame);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (seekSnapshotImage != null && seekSnapshotTexture != null)
|
|
196
|
+
{
|
|
197
|
+
seekSnapshotImage.texture = seekSnapshotTexture;
|
|
198
|
+
seekSnapshotImage.gameObject.SetActive(true);
|
|
199
|
+
SetSeekOverlayBackgroundVisible(false);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
try
|
|
203
|
+
{
|
|
204
|
+
SetSeekingPresentation(true);
|
|
205
|
+
HyperRuntime.SeekReplaySession(pendingSeekTarget, resumeSpeed, fastForwardSpeed);
|
|
206
|
+
}
|
|
207
|
+
finally
|
|
208
|
+
{
|
|
209
|
+
isCapturingSeek = false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private void RestorePlaybackAfterSeek(float playbackSpeed)
|
|
214
|
+
{
|
|
215
|
+
float speed = SanitizePlaybackSpeed(playbackSpeed);
|
|
216
|
+
Time.timeScale = speed;
|
|
217
|
+
AlignPlaybackSpeedIndex(speed);
|
|
218
|
+
UpdatePlaybackSpeedLabel(speed);
|
|
219
|
+
SetSeekingPresentation(false);
|
|
220
|
+
wasSeeking = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private void AlignPlaybackSpeedIndex(float speed)
|
|
224
|
+
{
|
|
225
|
+
if (playbackSpeeds == null || playbackSpeeds.Length == 0) return;
|
|
226
|
+
|
|
227
|
+
for (int i = 0; i < playbackSpeeds.Length; i++)
|
|
228
|
+
{
|
|
229
|
+
if (!Mathf.Approximately(SanitizePlaybackSpeed(playbackSpeeds[i]), speed)) continue;
|
|
230
|
+
playbackSpeedIndex = (i + 1) % playbackSpeeds.Length;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private void SetSeekingPresentation(bool isSeeking)
|
|
236
|
+
{
|
|
237
|
+
ReplayPresentationCanvas.SetSeekingLayer(isSeeking);
|
|
238
|
+
if (seekLoadingOverlay != null) seekLoadingOverlay.SetActive(isSeeking);
|
|
239
|
+
if (progressSlider != null) progressSlider.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
240
|
+
if (playbackSpeedButton != null)
|
|
241
|
+
{
|
|
242
|
+
playbackSpeedButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (exitButton != null) exitButton.interactable = !isSeeking && !ReplaySessionState.HasEnded;
|
|
246
|
+
if (!isSeeking) ClearSeekSnapshot();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private static float SanitizePlaybackSpeed(float speed)
|
|
250
|
+
{
|
|
251
|
+
return float.IsNaN(speed) || float.IsInfinity(speed) || speed <= 0f ? 1f : speed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private void UpdatePlaybackSpeedLabel(float speed)
|
|
255
|
+
{
|
|
256
|
+
if (playbackSpeedLabel == null && playbackSpeedButton != null)
|
|
257
|
+
{
|
|
258
|
+
playbackSpeedLabel = playbackSpeedButton.GetComponentInChildren<TextMeshProUGUI>(true);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (playbackSpeedLabel != null)
|
|
262
|
+
{
|
|
263
|
+
playbackSpeedLabel.text = $"{speed:0.#}x";
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
internal static Texture2D CreateCircularAvatar(Texture2D source, int size)
|
|
268
|
+
{
|
|
269
|
+
var result = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
|
270
|
+
{
|
|
271
|
+
name = "Replay Avatar (Circular)",
|
|
272
|
+
filterMode = FilterMode.Bilinear,
|
|
273
|
+
wrapMode = TextureWrapMode.Clamp
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
Color[] pixels = new Color[size * size];
|
|
277
|
+
float sourceAspect = source.width / (float)source.height;
|
|
278
|
+
float edgeFeather = 2.5f / size;
|
|
279
|
+
|
|
280
|
+
for (int y = 0; y < size; y++)
|
|
281
|
+
{
|
|
282
|
+
float v = (y + 0.5f) / size;
|
|
283
|
+
for (int x = 0; x < size; x++)
|
|
284
|
+
{
|
|
285
|
+
float u = (x + 0.5f) / size;
|
|
286
|
+
float centeredX = u - 0.5f;
|
|
287
|
+
float centeredY = v - 0.5f;
|
|
288
|
+
float distanceFromCenter = Mathf.Sqrt(centeredX * centeredX + centeredY * centeredY);
|
|
289
|
+
float edgeCoverage = Mathf.Clamp01((0.5f - distanceFromCenter) / edgeFeather);
|
|
290
|
+
edgeCoverage = edgeCoverage * edgeCoverage * (3f - 2f * edgeCoverage);
|
|
291
|
+
|
|
292
|
+
if (edgeCoverage <= 0f)
|
|
293
|
+
{
|
|
294
|
+
pixels[y * size + x] = Color.clear;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
float sampleU = sourceAspect > 1f
|
|
299
|
+
? 0.5f + centeredX / sourceAspect
|
|
300
|
+
: u;
|
|
301
|
+
float sampleV = sourceAspect < 1f
|
|
302
|
+
? 0.5f + centeredY * sourceAspect
|
|
303
|
+
: v;
|
|
304
|
+
Color color = source.GetPixelBilinear(sampleU, sampleV);
|
|
305
|
+
color.a *= edgeCoverage;
|
|
306
|
+
pixels[y * size + x] = color;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
result.SetPixels(pixels);
|
|
311
|
+
result.Apply(false, true);
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
private void AutoBindReferences()
|
|
316
|
+
{
|
|
317
|
+
if (progressSlider == null) progressSlider = GetComponentInChildren<Slider>(true);
|
|
318
|
+
if (exitButton == null) exitButton = GetComponentInChildren<Button>(true);
|
|
319
|
+
|
|
320
|
+
TextMeshProUGUI[] labels = GetComponentsInChildren<TextMeshProUGUI>(true);
|
|
321
|
+
for (int i = 0; replayLabel == null && i < labels.Length; i++)
|
|
322
|
+
{
|
|
323
|
+
if (labels[i].name == "Subtext") replayLabel = labels[i];
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
RawImage[] images = GetComponentsInChildren<RawImage>(true);
|
|
327
|
+
for (int i = 0; playerAvatar == null && i < images.Length; i++)
|
|
328
|
+
{
|
|
329
|
+
if (images[i].name == "Player Avatar") playerAvatar = images[i];
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
private void ConfigureProgressSlider()
|
|
334
|
+
{
|
|
335
|
+
if (progressSlider == null) return;
|
|
336
|
+
|
|
337
|
+
progressSlider.minValue = 0f;
|
|
338
|
+
progressSlider.maxValue = 1f;
|
|
339
|
+
progressSlider.wholeNumbers = false;
|
|
340
|
+
progressSlider.interactable = true;
|
|
341
|
+
float displayedProgress = ReplaySessionState.IsSeeking
|
|
342
|
+
? ReplaySessionState.SeekTarget01
|
|
343
|
+
: ReplaySessionState.Progress01;
|
|
344
|
+
progressSlider.SetValueWithoutNotify(displayedProgress);
|
|
345
|
+
|
|
346
|
+
seekInput = progressSlider.GetComponent<ReplaySeekInput>();
|
|
347
|
+
if (seekInput == null) seekInput = progressSlider.gameObject.AddComponent<ReplaySeekInput>();
|
|
348
|
+
seekInput.Initialize(progressSlider, SeekTo);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private void ConfigureSeekOverlay()
|
|
352
|
+
{
|
|
353
|
+
if (seekLoadingOverlay == null)
|
|
354
|
+
{
|
|
355
|
+
Transform existing = transform.Find("ReplaySeekOverlay");
|
|
356
|
+
if (existing != null) seekLoadingOverlay = existing.gameObject;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (seekLoadingOverlay == null)
|
|
360
|
+
{
|
|
361
|
+
GameObject prefab = Resources.Load<GameObject>("UIPrefabs/ReplaySeekOverlay");
|
|
362
|
+
seekLoadingOverlay = Instantiate(prefab, transform, false);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
RectTransform rect = seekLoadingOverlay.GetComponent<RectTransform>();
|
|
366
|
+
if (rect != null)
|
|
367
|
+
{
|
|
368
|
+
rect.anchorMin = Vector2.zero;
|
|
369
|
+
rect.anchorMax = Vector2.one;
|
|
370
|
+
rect.offsetMin = Vector2.zero;
|
|
371
|
+
rect.offsetMax = Vector2.zero;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
seekLoadingOverlay.transform.SetAsFirstSibling();
|
|
375
|
+
seekOverlayBackground = seekLoadingOverlay.GetComponent<Image>();
|
|
376
|
+
if (seekOverlayBackground != null) seekOverlayBackgroundColor = seekOverlayBackground.color;
|
|
377
|
+
seekLoadingOverlay.SetActive(ReplaySessionState.IsSeeking);
|
|
378
|
+
wasSeeking = ReplaySessionState.IsSeeking;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private void ConfigureSeekSnapshot()
|
|
382
|
+
{
|
|
383
|
+
if (seekSnapshotImage == null)
|
|
384
|
+
{
|
|
385
|
+
Transform existing = transform.Find("ReplaySeekSnapshot");
|
|
386
|
+
if (existing != null) seekSnapshotImage = existing.GetComponent<RawImage>();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (seekSnapshotImage == null)
|
|
390
|
+
{
|
|
391
|
+
var snapshot = new GameObject(
|
|
392
|
+
"ReplaySeekSnapshot",
|
|
393
|
+
typeof(RectTransform),
|
|
394
|
+
typeof(CanvasRenderer),
|
|
395
|
+
typeof(RawImage));
|
|
396
|
+
snapshot.transform.SetParent(transform, false);
|
|
397
|
+
seekSnapshotImage = snapshot.GetComponent<RawImage>();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
RectTransform rect = seekSnapshotImage.rectTransform;
|
|
401
|
+
rect.anchorMin = Vector2.zero;
|
|
402
|
+
rect.anchorMax = Vector2.one;
|
|
403
|
+
rect.offsetMin = Vector2.zero;
|
|
404
|
+
rect.offsetMax = Vector2.zero;
|
|
405
|
+
seekSnapshotImage.raycastTarget = false;
|
|
406
|
+
seekSnapshotImage.transform.SetAsFirstSibling();
|
|
407
|
+
if (seekLoadingOverlay != null) seekLoadingOverlay.transform.SetSiblingIndex(1);
|
|
408
|
+
seekSnapshotImage.gameObject.SetActive(false);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private static Texture2D CreateScaledSnapshot(Texture2D source, float scale)
|
|
412
|
+
{
|
|
413
|
+
float sanitizedScale = Mathf.Clamp(scale, 0.25f, 1f);
|
|
414
|
+
int width = Mathf.Max(1, Mathf.RoundToInt(source.width * sanitizedScale));
|
|
415
|
+
int height = Mathf.Max(1, Mathf.RoundToInt(source.height * sanitizedScale));
|
|
416
|
+
RenderTexture temporary = RenderTexture.GetTemporary(width, height, 0, RenderTextureFormat.ARGB32);
|
|
417
|
+
RenderTexture previous = RenderTexture.active;
|
|
418
|
+
Texture2D result = null;
|
|
419
|
+
|
|
420
|
+
try
|
|
421
|
+
{
|
|
422
|
+
Graphics.Blit(source, temporary);
|
|
423
|
+
RenderTexture.active = temporary;
|
|
424
|
+
result = new Texture2D(width, height, TextureFormat.RGB24, false)
|
|
425
|
+
{
|
|
426
|
+
name = "Replay Seek Snapshot",
|
|
427
|
+
filterMode = FilterMode.Bilinear,
|
|
428
|
+
wrapMode = TextureWrapMode.Clamp
|
|
429
|
+
};
|
|
430
|
+
result.ReadPixels(new Rect(0f, 0f, width, height), 0, 0, false);
|
|
431
|
+
result.Apply(false, true);
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
catch
|
|
435
|
+
{
|
|
436
|
+
if (result != null) Destroy(result);
|
|
437
|
+
throw;
|
|
438
|
+
}
|
|
439
|
+
finally
|
|
440
|
+
{
|
|
441
|
+
RenderTexture.active = previous;
|
|
442
|
+
RenderTexture.ReleaseTemporary(temporary);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private void ClearSeekSnapshot()
|
|
447
|
+
{
|
|
448
|
+
SetSeekOverlayBackgroundVisible(true);
|
|
449
|
+
if (seekSnapshotImage != null)
|
|
450
|
+
{
|
|
451
|
+
seekSnapshotImage.texture = null;
|
|
452
|
+
seekSnapshotImage.gameObject.SetActive(false);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (seekSnapshotTexture == null) return;
|
|
456
|
+
Destroy(seekSnapshotTexture);
|
|
457
|
+
seekSnapshotTexture = null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private void SetSeekOverlayBackgroundVisible(bool isVisible)
|
|
461
|
+
{
|
|
462
|
+
if (seekOverlayBackground == null) return;
|
|
463
|
+
Color color = seekOverlayBackgroundColor;
|
|
464
|
+
if (!isVisible) color.a = 0.5f;
|
|
465
|
+
seekOverlayBackground.color = color;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
internal sealed class ReplaySeekInput : MonoBehaviour, IPointerDownHandler, IPointerUpHandler
|
|
470
|
+
{
|
|
471
|
+
private Slider slider;
|
|
472
|
+
private Action<float> onSeekRequested;
|
|
473
|
+
|
|
474
|
+
internal bool IsDragging { get; private set; }
|
|
475
|
+
|
|
476
|
+
internal void Initialize(Slider targetSlider, Action<float> callback)
|
|
477
|
+
{
|
|
478
|
+
slider = targetSlider;
|
|
479
|
+
onSeekRequested = callback;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
internal void Clear()
|
|
483
|
+
{
|
|
484
|
+
IsDragging = false;
|
|
485
|
+
onSeekRequested = null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
public void OnPointerDown(PointerEventData eventData)
|
|
489
|
+
{
|
|
490
|
+
if (slider == null || !slider.interactable) return;
|
|
491
|
+
IsDragging = true;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
public void OnPointerUp(PointerEventData eventData)
|
|
495
|
+
{
|
|
496
|
+
if (!IsDragging) return;
|
|
497
|
+
IsDragging = false;
|
|
498
|
+
onSeekRequested?.Invoke(slider.normalizedValue);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
using System.Collections;
|
|
2
|
+
using UnityEngine;
|
|
3
|
+
using UnityEngine.UI;
|
|
4
|
+
|
|
5
|
+
namespace Hyper.Internal
|
|
6
|
+
{
|
|
7
|
+
[RequireComponent(typeof(Graphic))]
|
|
8
|
+
internal sealed class ReplayMarkerPulse : MonoBehaviour
|
|
9
|
+
{
|
|
10
|
+
[SerializeField, Range(0f, 1f)] private float minimumAlpha = 0.3f;
|
|
11
|
+
[SerializeField] private float fadeDuration = 0.8f;
|
|
12
|
+
|
|
13
|
+
private Graphic _graphic;
|
|
14
|
+
private Coroutine _pulseCoroutine;
|
|
15
|
+
|
|
16
|
+
private void Awake()
|
|
17
|
+
{
|
|
18
|
+
_graphic = GetComponent<Graphic>();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private void OnEnable()
|
|
22
|
+
{
|
|
23
|
+
HyperRuntime.EventManager.OnGameStart += StartPulsing;
|
|
24
|
+
HyperRuntime.EventManager.OnGameEnded += StopPulsing;
|
|
25
|
+
SetAlpha(minimumAlpha);
|
|
26
|
+
|
|
27
|
+
if (HyperRuntime.GameManager != null && HyperRuntime.GameManager.HasGameStarted)
|
|
28
|
+
{
|
|
29
|
+
StartPulsing();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private void OnDisable()
|
|
34
|
+
{
|
|
35
|
+
if (HyperRuntime.EventManager != null)
|
|
36
|
+
{
|
|
37
|
+
HyperRuntime.EventManager.OnGameStart -= StartPulsing;
|
|
38
|
+
HyperRuntime.EventManager.OnGameEnded -= StopPulsing;
|
|
39
|
+
}
|
|
40
|
+
StopPulsing();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private void StartPulsing()
|
|
44
|
+
{
|
|
45
|
+
if (_pulseCoroutine != null) StopCoroutine(_pulseCoroutine);
|
|
46
|
+
_pulseCoroutine = StartCoroutine(Pulse());
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private void StopPulsing()
|
|
50
|
+
{
|
|
51
|
+
if (_pulseCoroutine != null)
|
|
52
|
+
{
|
|
53
|
+
StopCoroutine(_pulseCoroutine);
|
|
54
|
+
_pulseCoroutine = null;
|
|
55
|
+
}
|
|
56
|
+
SetAlpha(1f);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private IEnumerator Pulse()
|
|
60
|
+
{
|
|
61
|
+
float duration = Mathf.Max(0.05f, fadeDuration);
|
|
62
|
+
while (true)
|
|
63
|
+
{
|
|
64
|
+
yield return Fade(minimumAlpha, 1f, duration);
|
|
65
|
+
yield return Fade(1f, minimumAlpha, duration);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private IEnumerator Fade(float from, float to, float duration)
|
|
70
|
+
{
|
|
71
|
+
float elapsed = 0f;
|
|
72
|
+
while (elapsed < duration)
|
|
73
|
+
{
|
|
74
|
+
elapsed += Time.unscaledDeltaTime;
|
|
75
|
+
SetAlpha(Mathf.Lerp(from, to, elapsed / duration));
|
|
76
|
+
yield return null;
|
|
77
|
+
}
|
|
78
|
+
SetAlpha(to);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private void SetAlpha(float alpha)
|
|
82
|
+
{
|
|
83
|
+
if (_graphic == null) return;
|
|
84
|
+
Color color = _graphic.color;
|
|
85
|
+
color.a = alpha;
|
|
86
|
+
_graphic.color = color;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|