app.hypergames.hypersdk 1.8.19 → 1.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/HyperLoader.unity +3 -3
  3. package/Runtime/Internal/Managers/DataManager.cs +38 -2
  4. package/Runtime/Internal/Managers/GameManager.cs +34 -23
  5. package/Runtime/Internal/UI/Animation/SlideUp.cs +6 -1
  6. package/Runtime/Internal/UI/Components/RoundedRawImage.cs +146 -0
  7. package/Runtime/Internal/UI/Components/RoundedRawImage.cs.meta +11 -0
  8. package/Runtime/Internal/UI/Modals/GamePausedModal.cs +2 -0
  9. package/Runtime/Internal/UI/Modals/TutorialButton.cs +10 -0
  10. package/Runtime/Internal/UI/Modals/TutorialLoaderModal.cs +28 -2
  11. package/Runtime/Internal/UI/Modals/TutorialModal.cs +721 -47
  12. package/Runtime/Plugins/WebGL/HyperTutorialCachePlugin.jslib +35 -53
  13. package/Runtime/Resources/Materials/RoundedRawImage.mat +65 -0
  14. package/Runtime/Resources/Materials/RoundedRawImage.mat.meta +8 -0
  15. package/Runtime/Resources/Materials.meta +8 -0
  16. package/Runtime/Resources/RenderTextures/TutorialVideoTexture P.renderTexture +3 -3
  17. package/Runtime/Resources/Shaders/RoundedRawImage.shader +142 -0
  18. package/Runtime/Resources/Shaders/RoundedRawImage.shader.meta +10 -0
  19. package/Runtime/Resources/Shaders/RoundedStrip.shader +152 -0
  20. package/Runtime/Resources/Shaders/RoundedStrip.shader.meta +10 -0
  21. package/Runtime/Resources/Shaders.meta +8 -0
  22. package/Runtime/Resources/Sprites/Frames/Square.png +0 -0
  23. package/Runtime/Resources/Sprites/Frames/Square.png.meta +186 -0
  24. package/Runtime/Resources/Sprites/TutorialModal/Button_Back.png.meta +1 -1
  25. package/Runtime/Resources/Sprites/TutorialModal/Button_Next.png.meta +1 -1
  26. package/Runtime/Resources/Sprites/TutorialModal/Knob.png.meta +1 -1
  27. package/Runtime/Resources/Sprites/TutorialModal/Tutorial_Background_Portrait.png.meta +3 -3
  28. package/Runtime/Resources/UIPrefabs/Content.prefab +1672 -0
  29. package/Runtime/Resources/UIPrefabs/Content.prefab.meta +7 -0
  30. package/Runtime/Resources/UIPrefabs/IndicatorCircle.prefab +2 -2
  31. package/Runtime/Resources/UIPrefabs/TutorialLandscapeModal.prefab +121 -51
  32. package/Runtime/Resources/UIPrefabs/TutorialPortraitModal.prefab +497 -1469
  33. package/Runtime/Shared/DTOs/TutorialData.cs +18 -1
  34. package/package.json +1 -1
@@ -6,13 +6,22 @@ using UnityEngine;
6
6
  using UnityEngine.UI;
7
7
  using UnityEngine.Video;
8
8
  using UnityEngine.Networking;
9
+ using Hyper.Internal.Animation;
9
10
  using Hyper.Internal.Managers;
10
11
  using Hyper.Shared;
12
+ using TMPro;
11
13
 
12
14
  namespace Hyper.Internal
13
15
  {
14
16
  internal class TutorialModal : MonoBehaviour
15
17
  {
18
+ // Keep the seek wiring available while the tutorial video uses a passive progress bar.
19
+ private const bool VideoSeekingEnabled = false;
20
+ private const float LoaderFadeOutDelay = 2.15f;
21
+ private const float SlideInDuration = 0.33f;
22
+ private const float ContentFadeInDuration = 0.28f;
23
+ private const float ContentFadeOutDuration = 0.18f;
24
+ private const float HiddenPreloadPlayDuration = 0.75f;
16
25
 
17
26
  #if UNITY_WEBGL && !UNITY_EDITOR
18
27
  [DllImport("__Internal")]
@@ -23,20 +32,25 @@ namespace Hyper.Internal
23
32
  private CanvasGroup tutorialCG;
24
33
 
25
34
  [SerializeField] TutorialLoaderModal tutorialLoaderModal;
35
+ [SerializeField] private CanvasGroup tutorialContentGroup;
26
36
 
27
37
  [Header("Display Components")]
28
38
  [SerializeField] private RawImage tutorialDisplayP;
29
39
  [SerializeField] private RawImage tutorialDisplayL;
30
40
  [SerializeField] private VideoPlayer videoPlayer;
41
+ [SerializeField] private RawImage tutorialImageDisplay;
42
+ [SerializeField] private TMP_Text descriptionText;
43
+
44
+ #if UNITY_EDITOR
45
+ [Header("Editor Tutorial Test")]
46
+ [SerializeField] private EditorTutorialCard[] editorTutorialCards;
47
+ [SerializeField] private Vector2 editorLoadDelayRange = new Vector2(1f, 2f);
48
+ #endif
31
49
 
32
50
  [Header("Video Textures")]
33
51
  [SerializeField] private RenderTexture tutorialVideoTextureP;
34
52
  [SerializeField] private RenderTexture tutorialVideoTextureL;
35
53
 
36
- [Header("Aspect Ratio Fitters")]
37
- [SerializeField] private AspectRatioFitter aspectRatioFitterP;
38
- [SerializeField] private AspectRatioFitter aspectRatioFitterL;
39
-
40
54
  [Header("UI Controls")]
41
55
  [SerializeField] private Slider timeSlider;
42
56
  [SerializeField] private Button backButton;
@@ -46,16 +60,38 @@ namespace Hyper.Internal
46
60
  [SerializeField] private Transform indicatorParent;
47
61
  [SerializeField] private GameObject indicatorPrefab;
48
62
  private Sprite[] tutorialImages;
63
+ private readonly List<TutorialCardData> tutorialCards = new List<TutorialCardData>();
49
64
  private RawImage currentDisplay;
50
65
  private int currentImageIndex = 0;
66
+ private int currentCardIndex = 0;
51
67
  private bool isDraggingSlider = false;
52
68
  private float cachedVideoLength = 0f;
53
69
  private Coroutine updateVideoTimeCo = null;
70
+ private Coroutine editorTutorialLoadCo = null;
71
+ private Coroutine hiddenPreloadCloseCo = null;
54
72
  private TutorialData tutorialData;
73
+ private bool isClosing;
74
+ private bool hasInvokedTutorialLoadComplete;
75
+ private bool hasPreparedTutorialContent;
76
+ private bool isDirectContentOpen;
77
+ private bool contentRevealScheduled;
78
+ private HyperTween.TweenHandle contentRevealDelayTween;
55
79
 
56
80
  private EventManager _eventManager;
57
81
  private DataManager _dataManager;
58
82
 
83
+ #if UNITY_EDITOR
84
+ [Serializable]
85
+ private class EditorTutorialCard
86
+ {
87
+ public TutorialType type = TutorialType.video;
88
+ public VideoClip videoClip;
89
+ public Sprite image;
90
+ [TextArea] public string description;
91
+ public int position;
92
+ }
93
+ #endif
94
+
59
95
  private void Awake()
60
96
  {
61
97
  _eventManager = HyperRuntime.EventManager;
@@ -63,31 +99,32 @@ namespace Hyper.Internal
63
99
 
64
100
  slideUp = GetComponent<SlideUp>();
65
101
  tutorialCG = GetComponent<CanvasGroup>();
102
+ Hide();
66
103
 
67
104
  tutorialLoaderModal.gameObject.SetActive(true);
68
105
 
69
106
  _eventManager.OnTutorialResourcesReferenced += ShowTutorialBasedOnType;
107
+ _eventManager.OnTutorialLoadingComplete += HandleTutorialLoadingComplete;
70
108
  }
71
109
 
72
110
  void OnEnable()
73
111
  {
74
112
  SetupSliderControls();
75
113
 
114
+ if (hasPreparedTutorialContent) return;
115
+
76
116
  if (_dataManager.HasReferencedTutorialResources)
77
117
  {
78
- // ShowTutorialBasedOnType();
79
- if (tutorialData != null && tutorialData.type == TutorialType.video)
80
- {
81
- PlayCurrentVideo();
82
- }
83
- else if(tutorialData != null && tutorialData.type == TutorialType.images)
84
- {
85
- ShowImagesTutorial(tutorialData);
86
- }
118
+ ShowTutorialData(_dataManager.TutorialData);
87
119
  }
88
120
  else
89
121
  {
90
- _eventManager.InvokeTutorialFetchFailedEvent();
122
+ #if UNITY_EDITOR
123
+ if (TryStartEditorTutorialLoad())
124
+ {
125
+ return;
126
+ }
127
+ #endif
91
128
  }
92
129
  }
93
130
 
@@ -99,11 +136,47 @@ namespace Hyper.Internal
99
136
 
100
137
  public void Show()
101
138
  {
139
+ isClosing = false;
140
+ isDirectContentOpen = false;
141
+ contentRevealScheduled = false;
142
+ CancelHiddenPreloadClose();
143
+ HideTutorialContent();
102
144
  tutorialCG.alpha = 1f;
103
145
  tutorialCG.interactable = true;
104
146
  tutorialCG.blocksRaycasts = true;
105
147
 
106
- slideUp.SlideUpAnimation();
148
+ slideUp.SlideUpAnimation(SlideInDuration);
149
+
150
+ if (CanShowPreparedTutorialImmediately())
151
+ {
152
+ ShowPreparedTutorialImmediately();
153
+ return;
154
+ }
155
+
156
+ if (tutorialLoaderModal != null)
157
+ {
158
+ tutorialLoaderModal.gameObject.SetActive(true);
159
+ }
160
+
161
+ if (_dataManager != null && _dataManager.HasReferencedTutorialResources)
162
+ {
163
+ ShowTutorialData(_dataManager.TutorialData);
164
+ }
165
+ #if UNITY_EDITOR
166
+ else
167
+ {
168
+ if (!TryStartEditorTutorialLoad())
169
+ {
170
+ _eventManager.InvokeTutorialFetchFailedEvent();
171
+ }
172
+ }
173
+ #endif
174
+ #if !UNITY_EDITOR
175
+ else
176
+ {
177
+ HyperRuntime.BackendManager.GetTutorial(() => { });
178
+ }
179
+ #endif
107
180
  }
108
181
 
109
182
  private void Hide()
@@ -111,6 +184,143 @@ namespace Hyper.Internal
111
184
  tutorialCG.alpha = 0f;
112
185
  tutorialCG.interactable = false;
113
186
  tutorialCG.blocksRaycasts = false;
187
+ // HideTutorialContent();
188
+ }
189
+
190
+ private bool CanShowPreparedTutorialImmediately()
191
+ {
192
+ return hasPreparedTutorialContent && tutorialData != null;
193
+ }
194
+
195
+ private void ShowPreparedTutorialImmediately()
196
+ {
197
+ isDirectContentOpen = true;
198
+
199
+ if (tutorialLoaderModal != null)
200
+ {
201
+ tutorialLoaderModal.gameObject.SetActive(false);
202
+ }
203
+
204
+ FadeTutorialContentIn(SlideInDuration);
205
+ ShowTutorialData(tutorialData);
206
+ }
207
+
208
+ private void ShowTutorialContentImmediate()
209
+ {
210
+ if (tutorialContentGroup == null) return;
211
+
212
+ tutorialContentGroup.DOKill();
213
+ tutorialContentGroup.gameObject.SetActive(true);
214
+ tutorialContentGroup.alpha = 1f;
215
+ tutorialContentGroup.interactable = true;
216
+ tutorialContentGroup.blocksRaycasts = true;
217
+ }
218
+
219
+ private void FadeTutorialContentIn(float delay = 0f)
220
+ {
221
+ if (tutorialContentGroup == null) return;
222
+
223
+ contentRevealScheduled = true;
224
+ contentRevealDelayTween?.Kill();
225
+ contentRevealDelayTween = null;
226
+ tutorialContentGroup.DOKill();
227
+ tutorialContentGroup.gameObject.SetActive(true);
228
+ tutorialContentGroup.alpha = 0f;
229
+ tutorialContentGroup.interactable = false;
230
+ tutorialContentGroup.blocksRaycasts = false;
231
+
232
+ if (delay > 0f)
233
+ {
234
+ contentRevealDelayTween = HyperTween.DelayedCall(delay, StartTutorialContentFade)
235
+ .SetUpdate(true);
236
+ return;
237
+ }
238
+
239
+ StartTutorialContentFade();
240
+ }
241
+
242
+ private void StartTutorialContentFade()
243
+ {
244
+ if (tutorialContentGroup == null) return;
245
+
246
+ contentRevealDelayTween = null;
247
+ tutorialContentGroup.DOKill();
248
+ tutorialContentGroup.gameObject.SetActive(true);
249
+ tutorialContentGroup.interactable = true;
250
+ tutorialContentGroup.blocksRaycasts = true;
251
+ var fadeTween = tutorialContentGroup.DOFade(1f, ContentFadeInDuration)
252
+ .SetEase(HyperTween.Ease.OutQuad)
253
+ .SetUpdate(true);
254
+ }
255
+
256
+ private void HandleTutorialLoadingComplete()
257
+ {
258
+ if (!isActiveAndEnabled || isClosing) return;
259
+
260
+ hasPreparedTutorialContent = true;
261
+
262
+ if (IsHiddenPreload())
263
+ {
264
+ ScheduleHiddenPreloadClose();
265
+ return;
266
+ }
267
+
268
+ if (isDirectContentOpen)
269
+ {
270
+ if (!contentRevealScheduled)
271
+ {
272
+ FadeTutorialContentIn(SlideInDuration);
273
+ }
274
+ return;
275
+ }
276
+
277
+ if (tutorialContentGroup == null) return;
278
+ if (contentRevealScheduled) return;
279
+
280
+ FadeTutorialContentIn(LoaderFadeOutDelay);
281
+ }
282
+
283
+ private void HideTutorialContent()
284
+ {
285
+ if (tutorialContentGroup == null) return;
286
+
287
+ contentRevealScheduled = false;
288
+ contentRevealDelayTween?.Kill();
289
+ contentRevealDelayTween = null;
290
+ tutorialContentGroup.DOKill();
291
+ tutorialContentGroup.alpha = 0f;
292
+ tutorialContentGroup.interactable = false;
293
+ tutorialContentGroup.blocksRaycasts = false;
294
+ }
295
+
296
+ private bool IsHiddenPreload()
297
+ {
298
+ return tutorialCG != null && tutorialCG.alpha <= 0.001f && !tutorialCG.blocksRaycasts && !tutorialCG.interactable;
299
+ }
300
+
301
+ private void ScheduleHiddenPreloadClose()
302
+ {
303
+ CancelHiddenPreloadClose();
304
+ hiddenPreloadCloseCo = StartCoroutine(CloseHiddenPreloadAfterPreview());
305
+ }
306
+
307
+ private IEnumerator CloseHiddenPreloadAfterPreview()
308
+ {
309
+ yield return new WaitForSecondsRealtime(HiddenPreloadPlayDuration);
310
+
311
+ hiddenPreloadCloseCo = null;
312
+
313
+ if (!isActiveAndEnabled || isClosing || !IsHiddenPreload()) yield break;
314
+
315
+ FinishClose();
316
+ }
317
+
318
+ private void CancelHiddenPreloadClose()
319
+ {
320
+ if (hiddenPreloadCloseCo == null) return;
321
+
322
+ StopCoroutine(hiddenPreloadCloseCo);
323
+ hiddenPreloadCloseCo = null;
114
324
  }
115
325
 
116
326
  private void SetupSliderControls()
@@ -119,19 +329,25 @@ namespace Hyper.Internal
119
329
 
120
330
  timeSlider.minValue = 0f;
121
331
  timeSlider.wholeNumbers = false;
122
- timeSlider.onValueChanged.AddListener(OnTimeSliderChanged);
332
+ timeSlider.interactable = VideoSeekingEnabled;
333
+ timeSlider.onValueChanged.RemoveListener(OnTimeSliderChanged);
123
334
 
124
- // Add event triggers for drag detection
125
335
  var eventTrigger = timeSlider.gameObject.GetComponent<UnityEngine.EventSystems.EventTrigger>();
336
+ if (eventTrigger != null)
337
+ {
338
+ eventTrigger.triggers.Clear();
339
+ }
340
+
341
+ if (!VideoSeekingEnabled) return;
342
+
343
+ timeSlider.onValueChanged.AddListener(OnTimeSliderChanged);
344
+
345
+ eventTrigger = timeSlider.gameObject.GetComponent<UnityEngine.EventSystems.EventTrigger>();
126
346
  if (eventTrigger == null)
127
347
  {
128
348
  eventTrigger = timeSlider.gameObject.AddComponent<UnityEngine.EventSystems.EventTrigger>();
129
349
  }
130
350
 
131
- // Clear existing triggers
132
- eventTrigger.triggers.Clear();
133
-
134
- // Pointer down (begin drag)
135
351
  var pointerDownEntry = new UnityEngine.EventSystems.EventTrigger.Entry
136
352
  {
137
353
  eventID = UnityEngine.EventSystems.EventTriggerType.PointerDown
@@ -139,7 +355,6 @@ namespace Hyper.Internal
139
355
  pointerDownEntry.callback.AddListener((_) => OnSliderDragStart());
140
356
  eventTrigger.triggers.Add(pointerDownEntry);
141
357
 
142
- // Pointer up (end drag)
143
358
  var pointerUpEntry = new UnityEngine.EventSystems.EventTrigger.Entry
144
359
  {
145
360
  eventID = UnityEngine.EventSystems.EventTriggerType.PointerUp
@@ -150,11 +365,15 @@ namespace Hyper.Internal
150
365
 
151
366
  private void OnSliderDragStart()
152
367
  {
368
+ if (!VideoSeekingEnabled) return;
369
+
153
370
  isDraggingSlider = true;
154
371
  }
155
372
 
156
373
  private void OnSliderDragEnd()
157
374
  {
375
+ if (!VideoSeekingEnabled) return;
376
+
158
377
  isDraggingSlider = false;
159
378
 
160
379
  if (videoPlayer != null && videoPlayer.isPrepared)
@@ -171,8 +390,24 @@ namespace Hyper.Internal
171
390
  {
172
391
  //Debug.Log("Showing tutorial based on type");
173
392
  tutorialData = _dataManager.TutorialData;
393
+ ShowTutorialData(tutorialData);
394
+ }
174
395
 
175
- if (tutorialData.type == TutorialType.video)
396
+ private void ShowTutorialData(TutorialData data)
397
+ {
398
+ if (data == null)
399
+ {
400
+ _eventManager.InvokeTutorialFetchFailedEvent();
401
+ return;
402
+ }
403
+
404
+ tutorialData = data;
405
+
406
+ if (tutorialData.cards != null && tutorialData.cards.Length > 0)
407
+ {
408
+ ShowMixedTutorial(tutorialData);
409
+ }
410
+ else if (tutorialData.type == TutorialType.video)
176
411
  {
177
412
  ShowVideoTutorial(tutorialData);
178
413
  }
@@ -187,6 +422,74 @@ namespace Hyper.Internal
187
422
  }
188
423
  }
189
424
 
425
+ #if UNITY_EDITOR
426
+ internal bool TryStartEditorTutorialLoad()
427
+ {
428
+ TutorialData editorData = CreateEditorTutorialData();
429
+ if (editorData == null) return false;
430
+
431
+ if (editorTutorialLoadCo != null)
432
+ {
433
+ StopCoroutine(editorTutorialLoadCo);
434
+ }
435
+
436
+ editorTutorialLoadCo = StartCoroutine(LoadEditorTutorialAfterDelay(editorData));
437
+ return true;
438
+ }
439
+
440
+ private TutorialData CreateEditorTutorialData()
441
+ {
442
+ if (editorTutorialCards == null || editorTutorialCards.Length == 0) return null;
443
+
444
+ List<TutorialCardData> editorCards = new List<TutorialCardData>();
445
+
446
+ for (int i = 0; i < editorTutorialCards.Length; i++)
447
+ {
448
+ EditorTutorialCard editorCard = editorTutorialCards[i];
449
+ if (editorCard == null || editorCard.type == TutorialType.None) continue;
450
+
451
+ if (editorCard.type == TutorialType.video && editorCard.videoClip == null) continue;
452
+ if (editorCard.type == TutorialType.images && editorCard.image == null) continue;
453
+
454
+ editorCards.Add(new TutorialCardData
455
+ {
456
+ type = editorCard.type,
457
+ videoClip = editorCard.videoClip,
458
+ image = editorCard.image,
459
+ description = editorCard.description,
460
+ position = editorCard.position > 0 ? editorCard.position : i + 1
461
+ });
462
+ }
463
+
464
+ if (editorCards.Count == 0) return null;
465
+
466
+ editorCards.Sort((a, b) => a.position.CompareTo(b.position));
467
+
468
+ return new TutorialData
469
+ {
470
+ type = editorCards[0].type,
471
+ cards = editorCards.ToArray()
472
+ };
473
+ }
474
+
475
+ private IEnumerator LoadEditorTutorialAfterDelay(TutorialData editorData)
476
+ {
477
+ tutorialLoaderModal.gameObject.SetActive(true);
478
+ tutorialDisplayP.gameObject.SetActive(false);
479
+ tutorialDisplayL.gameObject.SetActive(false);
480
+ videoPlayer.enabled = false;
481
+
482
+ float minDelay = Mathf.Max(0f, Mathf.Min(editorLoadDelayRange.x, editorLoadDelayRange.y));
483
+ float maxDelay = Mathf.Max(minDelay, Mathf.Max(editorLoadDelayRange.x, editorLoadDelayRange.y));
484
+ float delay = UnityEngine.Random.Range(minDelay, maxDelay);
485
+
486
+ yield return new WaitForSecondsRealtime(delay);
487
+
488
+ editorTutorialLoadCo = null;
489
+ ShowTutorialData(editorData);
490
+ }
491
+ #endif
492
+
190
493
  private RawImage SelectOptimalDisplay(float aspectRatio)
191
494
  {
192
495
  bool isLandscape = aspectRatio > 1.0f;
@@ -196,20 +499,304 @@ namespace Hyper.Internal
196
499
 
197
500
  if (isLandscape)
198
501
  {
199
- //Debug.Log($"Content is landscape (aspect: {aspectRatio:F2}), using landscape display");
200
502
  tutorialDisplayL.gameObject.SetActive(true);
201
503
  return tutorialDisplayL;
202
504
  }
203
505
  else
204
506
  {
205
- //Debug.Log($"Content is portrait (aspect: {aspectRatio:F2}), using portrait display");
206
507
  tutorialDisplayP.gameObject.SetActive(true);
207
508
  return tutorialDisplayP;
208
509
  }
209
510
  }
210
511
 
512
+ private void ShowMixedTutorial(TutorialData tutorialData)
513
+ {
514
+ if (tutorialData.cards == null || tutorialData.cards.Length == 0)
515
+ {
516
+ _eventManager.InvokeTutorialFetchFailedEvent();
517
+ return;
518
+ }
519
+
520
+ hasInvokedTutorialLoadComplete = false;
521
+ tutorialCards.Clear();
522
+ tutorialCards.AddRange(tutorialData.cards);
523
+ tutorialCards.Sort((a, b) => a.position.CompareTo(b.position));
524
+ currentCardIndex = 0;
525
+ currentImageIndex = 0;
526
+
527
+ if (tutorialDisplayP != null) tutorialDisplayP.gameObject.SetActive(false);
528
+ if (tutorialDisplayL != null) tutorialDisplayL.gameObject.SetActive(false);
529
+
530
+ if (TryLoadCardImagesFromUrls())
531
+ {
532
+ return;
533
+ }
534
+
535
+ SetupMixedTutorialCards();
536
+ }
537
+
538
+ private bool TryLoadCardImagesFromUrls()
539
+ {
540
+ bool hasRemoteImage = false;
541
+
542
+ for (int i = 0; i < tutorialCards.Count; i++)
543
+ {
544
+ TutorialCardData card = tutorialCards[i];
545
+ if (card.type == TutorialType.images && card.image == null && !string.IsNullOrEmpty(card.imageUrl))
546
+ {
547
+ hasRemoteImage = true;
548
+ break;
549
+ }
550
+ }
551
+
552
+ if (!hasRemoteImage) return false;
553
+
554
+ StartCoroutine(LoadCardImagesFromUrlsCoroutine(SetupMixedTutorialCards));
555
+ return true;
556
+ }
557
+
558
+ private IEnumerator LoadCardImagesFromUrlsCoroutine(Action onComplete)
559
+ {
560
+ for (int i = 0; i < tutorialCards.Count; i++)
561
+ {
562
+ TutorialCardData card = tutorialCards[i];
563
+ if (card.type != TutorialType.images || card.image != null || string.IsNullOrEmpty(card.imageUrl)) continue;
564
+
565
+ using (UnityWebRequest webRequest = UnityWebRequestTexture.GetTexture(card.imageUrl))
566
+ {
567
+ yield return webRequest.SendWebRequest();
568
+
569
+ if (webRequest.result == UnityWebRequest.Result.Success)
570
+ {
571
+ Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
572
+ texture.filterMode = FilterMode.Bilinear;
573
+ texture.Compress(false);
574
+
575
+ card.image = Sprite.Create(
576
+ texture,
577
+ new Rect(0, 0, texture.width, texture.height),
578
+ new Vector2(0.5f, 0.5f),
579
+ 100f,
580
+ 0,
581
+ SpriteMeshType.Tight
582
+ );
583
+ }
584
+ }
585
+ }
586
+
587
+ onComplete?.Invoke();
588
+ }
589
+
590
+ private void SetupMixedTutorialCards()
591
+ {
592
+ RemoveInvalidTutorialCards();
593
+
594
+ if (tutorialCards.Count == 0)
595
+ {
596
+ _eventManager.InvokeTutorialFetchFailedEvent();
597
+ return;
598
+ }
599
+
600
+ backButton.gameObject.SetActive(true);
601
+ nextButton.gameObject.SetActive(true);
602
+ indicatorParent.gameObject.SetActive(true);
603
+
604
+ UpdateIndicators(tutorialCards.Count);
605
+ StartCoroutine(ApplyInitialIndicatorHighlightNextFrame());
606
+ DisplayCurrentCard(true);
607
+ }
608
+
609
+ private void RemoveInvalidTutorialCards()
610
+ {
611
+ for (int i = tutorialCards.Count - 1; i >= 0; i--)
612
+ {
613
+ TutorialCardData card = tutorialCards[i];
614
+ bool hasVideo = card.type == TutorialType.video && (card.videoClip != null || !string.IsNullOrEmpty(card.videoUrl));
615
+ bool hasImage = card.type == TutorialType.images && card.image != null;
616
+
617
+ if (!hasVideo && !hasImage)
618
+ {
619
+ tutorialCards.RemoveAt(i);
620
+ }
621
+ }
622
+ }
623
+
624
+
625
+
626
+ private void DisplayCurrentCard(bool isInitialDisplay)
627
+ {
628
+ if (currentCardIndex < 0 || currentCardIndex >= tutorialCards.Count) return;
629
+
630
+ StopVideoPlayback();
631
+ TutorialCardData card = tutorialCards[currentCardIndex];
632
+
633
+ if (descriptionText != null)
634
+ {
635
+ descriptionText.text = card.description ?? string.Empty;
636
+ }
637
+
638
+ if (card.type == TutorialType.video)
639
+ {
640
+ DisplayVideoCard(card, isInitialDisplay);
641
+ }
642
+ else
643
+ {
644
+ DisplayImageCard(card, isInitialDisplay);
645
+ }
646
+
647
+ HighlightIndicator(currentCardIndex);
648
+ UpdateNavigationButtons(tutorialCards.Count, currentCardIndex);
649
+ }
650
+
651
+ private void DisplayVideoCard(TutorialCardData card, bool isInitialDisplay)
652
+ {
653
+ if (videoPlayer == null)
654
+ {
655
+ _eventManager.InvokeTutorialFetchFailedEvent();
656
+ return;
657
+ }
658
+
659
+ if (tutorialImageDisplay != null)
660
+ tutorialImageDisplay.gameObject.SetActive(false);
661
+
662
+ // Start with portrait display as default; will switch to landscape in OnCardVideoPrepared if needed
663
+ tutorialDisplayP.gameObject.SetActive(true);
664
+ tutorialDisplayL.gameObject.SetActive(false);
665
+ currentDisplay = tutorialDisplayP;
666
+
667
+ videoPlayer.enabled = true;
668
+ videoPlayer.isLooping = true;
669
+ videoPlayer.prepareCompleted -= OnVideoPrepared;
670
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
671
+ videoPlayer.loopPointReached -= OnLoopPointReached;
672
+ videoPlayer.loopPointReached += OnLoopPointReached;
673
+
674
+ if (card.videoClip != null)
675
+ {
676
+ videoPlayer.source = VideoSource.VideoClip;
677
+ videoPlayer.clip = card.videoClip;
678
+ }
679
+ else if (!string.IsNullOrEmpty(card.videoUrl))
680
+ {
681
+ videoPlayer.source = VideoSource.Url;
682
+ videoPlayer.url = card.videoUrl;
683
+ }
684
+ else
685
+ {
686
+ _eventManager.InvokeTutorialFetchFailedEvent();
687
+ return;
688
+ }
689
+
690
+ videoPlayer.prepareCompleted += OnCardVideoPrepared;
691
+ videoPlayer.Prepare();
692
+
693
+ timeSlider.gameObject.SetActive(true);
694
+ timeSlider.interactable = VideoSeekingEnabled;
695
+ }
696
+
697
+ private void OnCardVideoPrepared(VideoPlayer vp)
698
+ {
699
+ vp.prepareCompleted -= OnCardVideoPrepared;
700
+ PreventIOSFullscreen();
701
+
702
+ if (currentDisplay == null)
703
+ {
704
+ _eventManager.InvokeTutorialFetchFailedEvent();
705
+ return;
706
+ }
707
+
708
+ bool isLandscape = vp.width > vp.height;
709
+ RawImage targetDisplay = isLandscape ? tutorialDisplayL : tutorialDisplayP;
710
+ RenderTexture targetTexture = isLandscape ? tutorialVideoTextureL : tutorialVideoTextureP;
711
+ if (targetTexture == null) targetTexture = isLandscape ? tutorialVideoTextureP : tutorialVideoTextureL;
712
+
713
+ // Switch to the correct display
714
+ tutorialDisplayP.gameObject.SetActive(targetDisplay == tutorialDisplayP);
715
+ tutorialDisplayL.gameObject.SetActive(targetDisplay == tutorialDisplayL);
716
+ currentDisplay = targetDisplay;
717
+
718
+ if (targetTexture != null)
719
+ {
720
+ currentDisplay.texture = targetTexture;
721
+ videoPlayer.targetTexture = targetTexture;
722
+ }
723
+
724
+ double len = videoPlayer.length;
725
+ if (len <= 0 && videoPlayer.clip != null) len = videoPlayer.clip.length;
726
+ cachedVideoLength = (float)Mathf.Max(0f, (float)len);
727
+
728
+ timeSlider.minValue = 0f;
729
+ timeSlider.maxValue = cachedVideoLength;
730
+ timeSlider.value = 0f;
731
+
732
+ if (updateVideoTimeCo != null) StopCoroutine(updateVideoTimeCo);
733
+ updateVideoTimeCo = StartCoroutine(UpdateVideoTime());
734
+
735
+ InvokeTutorialLoadingCompleteOnce();
736
+ videoPlayer.Play();
737
+ }
738
+
739
+ private void DisplayImageCard(TutorialCardData card, bool isInitialDisplay)
740
+ {
741
+ videoPlayer.enabled = false;
742
+ timeSlider.gameObject.SetActive(false);
743
+
744
+ if (tutorialDisplayP != null)
745
+ tutorialDisplayP.gameObject.SetActive(false);
746
+ if (tutorialDisplayL != null)
747
+ tutorialDisplayL.gameObject.SetActive(false);
748
+
749
+ if (tutorialImageDisplay == null || card.image == null)
750
+ {
751
+ _eventManager.InvokeTutorialFetchFailedEvent();
752
+ return;
753
+ }
754
+
755
+ tutorialImageDisplay.gameObject.SetActive(true);
756
+ tutorialImageDisplay.texture = card.image.texture;
757
+
758
+ if (isInitialDisplay)
759
+ {
760
+ InvokeTutorialLoadingCompleteOnce();
761
+ }
762
+ }
763
+
764
+ private void InvokeTutorialLoadingCompleteOnce()
765
+ {
766
+ if (hasInvokedTutorialLoadComplete) return;
767
+
768
+ hasPreparedTutorialContent = true;
769
+ hasInvokedTutorialLoadComplete = true;
770
+ _eventManager.InvokeTutorialLoadingCompleteEvent();
771
+ }
772
+
773
+ private void StopVideoPlayback()
774
+ {
775
+ if (updateVideoTimeCo != null)
776
+ {
777
+ StopCoroutine(updateVideoTimeCo);
778
+ updateVideoTimeCo = null;
779
+ }
780
+
781
+ if (videoPlayer == null) return;
782
+
783
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
784
+ videoPlayer.Stop();
785
+ videoPlayer.targetTexture = null;
786
+ }
787
+
788
+
789
+
211
790
  private void ShowVideoTutorial(TutorialData tutorialData)
212
791
  {
792
+ if (videoPlayer == null)
793
+ {
794
+ _eventManager.InvokeTutorialFetchFailedEvent();
795
+ return;
796
+ }
797
+
798
+ videoPlayer.enabled = true;
799
+
213
800
  bool videoFound = false;
214
801
 
215
802
  if (tutorialData.videoClip != null)
@@ -234,6 +821,7 @@ namespace Hyper.Internal
234
821
 
235
822
  else
236
823
  {
824
+ videoPlayer.prepareCompleted -= OnCardVideoPrepared;
237
825
  videoPlayer.prepareCompleted += OnVideoPrepared;
238
826
  videoPlayer.loopPointReached += OnLoopPointReached;
239
827
  }
@@ -258,15 +846,6 @@ namespace Hyper.Internal
258
846
  currentDisplay.texture = targetTexture;
259
847
  videoPlayer.targetTexture = targetTexture;
260
848
 
261
- AspectRatioFitter fitter = (currentDisplay == tutorialDisplayL) ?
262
- aspectRatioFitterL : aspectRatioFitterP;
263
-
264
- if (fitter != null)
265
- {
266
- fitter.aspectRatio = Mathf.Max(0.01f, videoAspectRatio);
267
- fitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
268
- }
269
-
270
849
  timeSlider.gameObject.SetActive(true);
271
850
  backButton.gameObject.SetActive(false);
272
851
  nextButton.gameObject.SetActive(false);
@@ -278,6 +857,7 @@ namespace Hyper.Internal
278
857
  timeSlider.minValue = 0f;
279
858
  timeSlider.maxValue = cachedVideoLength;
280
859
  timeSlider.value = 0f;
860
+ timeSlider.interactable = VideoSeekingEnabled;
281
861
 
282
862
  if (updateVideoTimeCo != null) StopCoroutine(updateVideoTimeCo);
283
863
  updateVideoTimeCo = StartCoroutine(UpdateVideoTime());
@@ -324,6 +904,7 @@ namespace Hyper.Internal
324
904
  {
325
905
  tutorialImages = tutorialData.images;
326
906
  SetupImageTutorial();
907
+ _eventManager.InvokeTutorialLoadingCompleteEvent();
327
908
  }
328
909
  else if (tutorialData.imageUrls != null && tutorialData.imageUrls.Length > 0)
329
910
  {
@@ -377,15 +958,6 @@ namespace Hyper.Internal
377
958
  float aspectRatio = (float)texture.width / texture.height;
378
959
 
379
960
  currentDisplay = SelectOptimalDisplay(aspectRatio);
380
-
381
- AspectRatioFitter fitter = (currentDisplay == tutorialDisplayL) ?
382
- aspectRatioFitterL : aspectRatioFitterP;
383
-
384
- if (fitter != null)
385
- {
386
- fitter.aspectRatio = Mathf.Max(0.01f, aspectRatio);
387
- fitter.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
388
- }
389
961
  }
390
962
 
391
963
  private IEnumerator LoadImagesFromUrlsCoroutine(string[] urls, Action<Sprite[]> onComplete)
@@ -407,7 +979,7 @@ namespace Hyper.Internal
407
979
  if (webRequest.result == UnityWebRequest.Result.Success)
408
980
  {
409
981
  Texture2D texture = DownloadHandlerTexture.GetContent(webRequest);
410
- texture.filterMode = FilterMode.Point;
982
+ texture.filterMode = FilterMode.Bilinear;
411
983
  texture.Compress(false);
412
984
 
413
985
  Sprite sprite = Sprite.Create(
@@ -447,6 +1019,20 @@ namespace Hyper.Internal
447
1019
 
448
1020
  private void OnDisable()
449
1021
  {
1022
+ isDirectContentOpen = false;
1023
+ contentRevealScheduled = false;
1024
+ isClosing = false;
1025
+ CancelHiddenPreloadClose();
1026
+ HideTutorialContent();
1027
+
1028
+ #if UNITY_EDITOR
1029
+ if (editorTutorialLoadCo != null)
1030
+ {
1031
+ StopCoroutine(editorTutorialLoadCo);
1032
+ editorTutorialLoadCo = null;
1033
+ }
1034
+ #endif
1035
+
450
1036
  if (updateVideoTimeCo != null)
451
1037
  {
452
1038
  StopCoroutine(updateVideoTimeCo);
@@ -454,6 +1040,14 @@ namespace Hyper.Internal
454
1040
  }
455
1041
  }
456
1042
 
1043
+ private void OnDestroy()
1044
+ {
1045
+ if (_eventManager == null) return;
1046
+
1047
+ _eventManager.OnTutorialResourcesReferenced -= ShowTutorialBasedOnType;
1048
+ _eventManager.OnTutorialLoadingComplete -= HandleTutorialLoadingComplete;
1049
+ }
1050
+
457
1051
  private void DisplayImage(int index)
458
1052
  {
459
1053
  if (tutorialImages == null || index < 0 || index >= tutorialImages.Length)
@@ -471,6 +1065,12 @@ namespace Hyper.Internal
471
1065
 
472
1066
  public void NextImage()
473
1067
  {
1068
+ if (tutorialCards.Count > 0)
1069
+ {
1070
+ NextContent();
1071
+ return;
1072
+ }
1073
+
474
1074
  if (currentImageIndex < tutorialImages.Length - 1)
475
1075
  {
476
1076
  currentImageIndex++;
@@ -480,6 +1080,12 @@ namespace Hyper.Internal
480
1080
 
481
1081
  public void PreviousImage()
482
1082
  {
1083
+ if (tutorialCards.Count > 0)
1084
+ {
1085
+ PreviousContent();
1086
+ return;
1087
+ }
1088
+
483
1089
  if (currentImageIndex > 0)
484
1090
  {
485
1091
  currentImageIndex--;
@@ -487,19 +1093,77 @@ namespace Hyper.Internal
487
1093
  }
488
1094
  }
489
1095
 
1096
+ public void NextContent()
1097
+ {
1098
+ if (currentCardIndex >= tutorialCards.Count - 1) return;
1099
+
1100
+ currentCardIndex++;
1101
+ DisplayCurrentCard(false);
1102
+ }
1103
+
1104
+ public void PreviousContent()
1105
+ {
1106
+ if (currentCardIndex <= 0) return;
1107
+
1108
+ currentCardIndex--;
1109
+ DisplayCurrentCard(false);
1110
+ }
1111
+
490
1112
  public void Close()
491
1113
  {
1114
+ if (isClosing) return;
1115
+
1116
+ isClosing = true;
1117
+ CancelHiddenPreloadClose();
1118
+ tutorialCG.interactable = false;
1119
+ tutorialCG.blocksRaycasts = false;
1120
+
1121
+ if (tutorialContentGroup == null || tutorialContentGroup.alpha <= 0.001f)
1122
+ {
1123
+ FinishClose();
1124
+ return;
1125
+ }
1126
+
1127
+ tutorialContentGroup.DOKill();
1128
+ tutorialContentGroup.interactable = false;
1129
+ tutorialContentGroup.blocksRaycasts = false;
1130
+ tutorialContentGroup.DOFade(0f, ContentFadeOutDuration)
1131
+ .SetEase(HyperTween.Ease.InQuad)
1132
+ .SetUpdate(true)
1133
+ .OnComplete(FinishClose);
1134
+ }
1135
+
1136
+ private void FinishClose()
1137
+ {
1138
+ if (tutorialContentGroup != null)
1139
+ {
1140
+ tutorialContentGroup.alpha = 0f;
1141
+ }
1142
+
492
1143
  if (videoPlayer != null) videoPlayer.enabled = false;
493
1144
  slideUp.SlideDownAnimation();
494
1145
  }
495
1146
 
496
1147
  private void UpdateNavigationButtons()
497
1148
  {
498
- nextButton.interactable = currentImageIndex < tutorialImages.Length - 1;
499
- backButton.interactable = currentImageIndex > 0;
1149
+ UpdateNavigationButtons(tutorialImages != null ? tutorialImages.Length : 0, currentImageIndex);
1150
+ }
1151
+
1152
+ private void UpdateNavigationButtons(int totalCount, int currentIndex)
1153
+ {
1154
+ bool hasMultipleCards = totalCount > 1;
1155
+ nextButton.gameObject.SetActive(hasMultipleCards);
1156
+ backButton.gameObject.SetActive(hasMultipleCards);
1157
+ nextButton.interactable = currentIndex < totalCount - 1;
1158
+ backButton.interactable = currentIndex > 0;
500
1159
  }
501
1160
 
502
1161
  private void UpdateIndicators()
1162
+ {
1163
+ UpdateIndicators(tutorialImages != null ? tutorialImages.Length : 0);
1164
+ }
1165
+
1166
+ private void UpdateIndicators(int totalCount)
503
1167
  {
504
1168
 
505
1169
  foreach (Transform child in indicatorParent)
@@ -507,10 +1171,18 @@ namespace Hyper.Internal
507
1171
  Destroy(child.gameObject);
508
1172
  }
509
1173
 
510
- for (int i = 0; i < tutorialImages.Length; i++)
1174
+ for (int i = 0; i < totalCount; i++)
511
1175
  {
512
1176
  Instantiate(indicatorPrefab, indicatorParent);
513
1177
  }
1178
+
1179
+ indicatorParent.gameObject.SetActive(totalCount > 1);
1180
+ }
1181
+
1182
+ private IEnumerator ApplyInitialIndicatorHighlightNextFrame()
1183
+ {
1184
+ yield return null;
1185
+ HighlightIndicator(currentCardIndex);
514
1186
  }
515
1187
 
516
1188
  private void HighlightIndicator(int index)
@@ -559,6 +1231,8 @@ namespace Hyper.Internal
559
1231
 
560
1232
  private void OnTimeSliderChanged(float value)
561
1233
  {
1234
+ if (!VideoSeekingEnabled) return;
1235
+
562
1236
  // var tutorialData = HyperRuntime.GameManager.IsPortrait()
563
1237
  // ? HyperRuntime.DataManager.tutorialData
564
1238
  // : HyperRuntime.DataManager.landscapeTutorial;