app.hypergames.hypersdk 1.4.22 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -42,13 +42,28 @@ namespace Hyper.Internal.Managers
42
42
 
43
43
  #region Public Properties (Internal API)
44
44
 
45
- public int scoreSubmissionTimeout { get; private set; } = 7;
45
+ public int scoreSubmissionTimeout { get; private set; } = 6;
46
46
  public int CurrentRetryCount => _currentRetryCount;
47
47
  public bool isSocketConnected { get; private set; }
48
48
  public bool tutorialFetchSuccess { get; private set; }
49
49
  public bool tutorialExistsButFailed { get; private set; }
50
50
  public bool tutorialDoesNotExist => !tutorialExistsButFailed && !tutorialFetchSuccess && !_isFetchingTutorial;
51
51
 
52
+ /// <summary>
53
+ /// Type of error that occurred during score submission.
54
+ /// </summary>
55
+ public enum SubmissionErrorType
56
+ {
57
+ None,
58
+ NetworkError,
59
+ TimeoutError,
60
+ CSRFTokenError,
61
+ ServerError,
62
+ EncryptionError
63
+ }
64
+
65
+ public SubmissionErrorType LastSubmissionError { get; private set; } = SubmissionErrorType.None;
66
+
52
67
  #endregion
53
68
 
54
69
  #region Private Configuration
@@ -350,7 +365,7 @@ namespace Hyper.Internal.Managers
350
365
 
351
366
  if (string.IsNullOrEmpty(_cachedSessionId))
352
367
  {
353
- HyperDebug.LogError("No valid session ID – cannot send resumeGame");
368
+ HyperDebug.LogError("[ResumeGame] No valid session ID – cannot send resumeGame");
354
369
  _sendGameResumedCoroutine = null;
355
370
  yield break;
356
371
  }
@@ -358,7 +373,7 @@ namespace Hyper.Internal.Managers
358
373
  // Don't send if socket is not connected - prevents wasting retry attempts
359
374
  if (!isSocketConnected)
360
375
  {
361
- HyperDebug.LogWarning("Cannot send gameResumed: WebSocket not connected");
376
+ HyperDebug.LogWarning("[ResumeGame] Cannot send gameResumed: WebSocket not connected");
362
377
  _sendGameResumedCoroutine = null;
363
378
  yield break;
364
379
  }
@@ -382,12 +397,12 @@ namespace Hyper.Internal.Managers
382
397
  };
383
398
 
384
399
  string json = JsonConvert.SerializeObject(message);
385
- HyperDebug.Log($"Sending game resumed message: {json}");
400
+ HyperDebug.Log($"[ResumeGame] Sending game resumed message (attempt {attempts}/{maxAttempts}): {json}");
386
401
  SendWebSocketMessage(json);
387
402
  }
388
403
  catch (Exception ex)
389
404
  {
390
- HyperDebug.LogError($"SendGameResumed attempt {attempts} failed: {ex.Message}");
405
+ HyperDebug.LogError($"[ResumeGame] SendGameResumed attempt {attempts} failed: {ex.Message}");
391
406
  }
392
407
 
393
408
  float elapsed = 0f;
@@ -398,6 +413,11 @@ namespace Hyper.Internal.Managers
398
413
  yield return null;
399
414
  elapsed += Time.unscaledDeltaTime;
400
415
  }
416
+
417
+ if (!_gameResumed)
418
+ {
419
+ HyperDebug.LogWarning($"[ResumeGame] No gameResumed acknowledgement from server within {timeout} seconds (attempt {attempts}/{maxAttempts}).");
420
+ }
401
421
  }
402
422
 
403
423
  yield return new WaitForSecondsRealtime(1.5f);
@@ -463,6 +483,13 @@ namespace Hyper.Internal.Managers
463
483
 
464
484
  public void OnSocketConnected()
465
485
  {
486
+ if (HyperRuntime.Timer.SessionTimeRemaining <= 0)
487
+ {
488
+ HyperDebug.LogWarning("Session time remaining is 0 - closing WebSocket connection");
489
+ CloseWebSocket();
490
+ return;
491
+ }
492
+
466
493
  // Mark connected before any follow-up actions (resumeGame, UI updates, etc.)
467
494
  isSocketConnected = true;
468
495
  _isManuallyClosed = false;
@@ -518,14 +545,16 @@ namespace Hyper.Internal.Managers
518
545
  _sendGameStartedCoroutine = null;
519
546
  }
520
547
 
521
- TryUpdateCSRFToken(message);
548
+ HyperDebug.Log("[WebSocket] Received gameStarted event from server.");
549
+ TryUpdateCSRFToken(message, "gameStarted");
522
550
  }
523
551
 
524
552
  public void OnSocketGameResumed(string message)
525
553
  {
526
554
  _gameResumed = true;
555
+ HyperDebug.Log("[WebSocket] Received gameResumed event from server.");
527
556
 
528
- TryUpdateCSRFToken(message);
557
+ TryUpdateCSRFToken(message, "gameResumed");
529
558
  ResumeScoreSending();
530
559
  }
531
560
 
@@ -538,21 +567,23 @@ namespace Hyper.Internal.Managers
538
567
  {
539
568
  if (_lastSentBatch != null && _lastSentBatch.Count > 0)
540
569
  {
541
- _lastScoreData = _lastSentBatch.Last();
570
+ _lastScoreData = _lastSentBatch[_lastSentBatch.Count - 1];
571
+ HyperDebug.Log($"[Score] Server acknowledged score update. CumulativeScore={_lastScoreData.cumulativeScore}, IsFinal={_lastScoreData.isFinal}");
542
572
  _eventManager?.InvokeWebSocketScoreSubmitted(_lastScoreData);
543
573
  }
544
574
 
545
575
  if (_isWaitingForFinalScoreResponse)
546
576
  {
577
+ HyperDebug.Log("[Score] Final score response received from server.");
547
578
  _finalScoreSubmitted = true;
548
579
  _isWaitingForFinalScoreResponse = false;
549
580
  return;
550
581
  }
551
582
 
552
- TryUpdateCSRFToken(message);
583
+ TryUpdateCSRFToken(message, "scoreUpdated");
553
584
  }
554
585
 
555
- private void TryUpdateCSRFToken(string json)
586
+ private void TryUpdateCSRFToken(string json, string context = null)
556
587
  {
557
588
  try
558
589
  {
@@ -560,11 +591,17 @@ namespace Hyper.Internal.Managers
560
591
  if (parsed.TryGetValue("csrfToken", out object tokenObj))
561
592
  {
562
593
  _csrfToken = tokenObj.ToString();
594
+ int tokenLength = string.IsNullOrEmpty(_csrfToken) ? 0 : _csrfToken.Length;
595
+ HyperDebug.Log($"[CSRF] Token updated from '{context ?? "unknown"}'. Length={tokenLength}");
596
+ }
597
+ else
598
+ {
599
+ HyperDebug.LogWarning($"[CSRF] No csrfToken field present in message when updating from '{context ?? "unknown"}'.");
563
600
  }
564
601
  }
565
602
  catch (Exception ex)
566
603
  {
567
- HyperDebug.LogError($"Failed to parse CSRF token: {ex.Message}");
604
+ HyperDebug.LogError($"[CSRF] Failed to parse CSRF token from '{context ?? "unknown"}': {ex.Message}");
568
605
  }
569
606
  }
570
607
 
@@ -683,16 +720,60 @@ namespace Hyper.Internal.Managers
683
720
  {
684
721
  _gameManager?.GameOverModalComponent?.Init();
685
722
  _currentRetryCount = 0;
723
+ LastSubmissionError = SubmissionErrorType.None;
724
+
725
+ // Keep connection alive during game end sequence
726
+ // WebSocket connections can be closed by proxies/firewalls if idle for too long (typically 30-60 seconds)
727
+ // During gameplay, scores are sent every second, keeping the connection active
728
+ // But at game end, we stop sending scores, so the connection might go idle
729
+ // We ensure the connection is active right before submission by checking and reconnecting if needed
730
+ if (!isSocketConnected)
731
+ {
732
+ HyperDebug.LogWarning("WebSocket disconnected at game end - attempting to reconnect before submission");
733
+ InitializeWebSocket();
734
+
735
+ // Wait a bit for connection to establish
736
+ float reconnectWait = 0f;
737
+ const float maxReconnectWait = 3f;
738
+ while (!isSocketConnected && reconnectWait < maxReconnectWait)
739
+ {
740
+ yield return new WaitForSecondsRealtime(0.1f);
741
+ reconnectWait += 0.1f;
742
+ }
743
+
744
+ if (isSocketConnected)
745
+ {
746
+ HyperDebug.Log("Connection restored before score submission");
747
+ }
748
+ }
749
+ else
750
+ {
751
+ // Connection is active - ensure it stays active by verifying it's still connected
752
+ // The act of checking and the upcoming gameResumed call will keep it active
753
+ HyperDebug.Log("Connection active - maintaining during game end sequence");
754
+ }
686
755
 
687
756
  while (_currentRetryCount < MAX_RETRY_COUNT)
688
757
  {
689
758
  if (_hasSessionEnded)
690
759
  {
760
+ LastSubmissionError = SubmissionErrorType.NetworkError;
691
761
  _gameManager?.ShowRetryTimeExpiredModal();
692
762
  _finalScoreSubmissionCoroutine = null;
693
763
  yield break;
694
764
  }
695
765
 
766
+ // Check if we received a late response during retry wait
767
+ // This handles the case where backend responds after timeout but before next retry
768
+ if (_finalScoreSubmitted)
769
+ {
770
+ LastSubmissionError = SubmissionErrorType.None;
771
+ _gameManager?.GameOverModalComponent?.ScoreHasBeenSubmitted();
772
+ StopWebSocket();
773
+ _finalScoreSubmissionCoroutine = null;
774
+ yield break;
775
+ }
776
+
696
777
  var finalScoreData = _dataManager?.CurrentScoreData;
697
778
  finalScoreData.isFinal = true;
698
779
 
@@ -703,17 +784,19 @@ namespace Hyper.Internal.Managers
703
784
 
704
785
  if (string.IsNullOrEmpty(encryptedScoreData))
705
786
  {
706
- HyperDebug.LogError("Encryption failed");
787
+ LastSubmissionError = SubmissionErrorType.EncryptionError;
788
+ HyperDebug.LogError("Encryption failed [ENCRYPTION_ERROR]");
707
789
  yield break;
708
790
  }
709
791
 
792
+ // Reset gameResumed flag for this attempt
793
+ _gameResumed = false;
794
+
795
+ // Ensure game is resumed to get new CSRF token before final score submission
710
796
  if (_sendGameResumedCoroutine == null)
711
797
  {
712
798
  _sendGameResumedCoroutine = StartCoroutine(SendGameResumedCoroutine());
713
-
714
- //? Ensure game is resumed to get new CSRF token before final score submission
715
799
  yield return _sendGameResumedCoroutine;
716
-
717
800
  _sendGameResumedCoroutine = null;
718
801
  }
719
802
  else
@@ -723,7 +806,24 @@ namespace Hyper.Internal.Managers
723
806
 
724
807
  if (!isSocketConnected)
725
808
  {
726
- HyperDebug.LogError("WebSocket not connected - cannot submit final score");
809
+ LastSubmissionError = SubmissionErrorType.NetworkError;
810
+ HyperDebug.LogError("WebSocket not connected - cannot submit final score [NETWORK_ERROR]");
811
+ _currentRetryCount++;
812
+ _gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
813
+
814
+ if (_currentRetryCount < MAX_RETRY_COUNT)
815
+ {
816
+ yield return new WaitForSecondsRealtime(1f);
817
+ }
818
+ continue;
819
+ }
820
+
821
+ // Verify that gameResumed succeeded (CSRF token was refreshed)
822
+ // If _gameResumed is still false, the CSRF token wasn't refreshed and submission will fail
823
+ if (!_gameResumed)
824
+ {
825
+ LastSubmissionError = SubmissionErrorType.CSRFTokenError;
826
+ HyperDebug.LogWarning("gameResumed did not succeed - CSRF token may be stale. Retrying entire submission process (including gameResumed)... [CSRF_TOKEN_ERROR]");
727
827
  _currentRetryCount++;
728
828
  _gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
729
829
 
@@ -731,13 +831,19 @@ namespace Hyper.Internal.Managers
731
831
  {
732
832
  yield return new WaitForSecondsRealtime(1f);
733
833
  }
834
+
734
835
  continue;
735
836
  }
736
837
 
737
838
  // Sending final score via WebSocket
738
839
  _finalScoreSubmitted = false;
739
840
  _isWaitingForFinalScoreResponse = true;
740
- _lastSentBatch = new List<ScoreData> { finalScoreData };
841
+
842
+ _lastSentBatch.Clear();
843
+ if (finalScoreData != null)
844
+ {
845
+ _lastSentBatch.Add(finalScoreData);
846
+ }
741
847
 
742
848
  bool sendFailed = false;
743
849
  try
@@ -748,7 +854,8 @@ namespace Hyper.Internal.Managers
748
854
  }
749
855
  catch (Exception ex)
750
856
  {
751
- HyperDebug.LogError($"Failed to send final score via WebSocket: {ex.Message}");
857
+ LastSubmissionError = SubmissionErrorType.NetworkError;
858
+ HyperDebug.LogError($"Failed to send final score via WebSocket: {ex.Message} [NETWORK_ERROR]");
752
859
  // Keep _isWaitingForFinalScoreResponse = true since we're still in final score submission mode
753
860
  // and will retry. We're still waiting for a final score response.
754
861
  sendFailed = true;
@@ -804,6 +911,8 @@ namespace Hyper.Internal.Managers
804
911
  // Timeout occurred, but keep _isWaitingForFinalScoreResponse = true
805
912
  // because we're still in final score submission mode and will retry.
806
913
  // If backend responds during retry wait, we'll catch it.
914
+ LastSubmissionError = SubmissionErrorType.TimeoutError;
915
+ HyperDebug.LogWarning($"Score submission timeout after {timeout} seconds [TIMEOUT_ERROR]");
807
916
  _currentRetryCount++;
808
917
  _gameManager?.GameOverModalComponent?.UpdateAttemptCount(_currentRetryCount, MAX_RETRY_COUNT);
809
918
 
@@ -814,6 +923,8 @@ namespace Hyper.Internal.Managers
814
923
  }
815
924
  }
816
925
 
926
+ // All retries exhausted - log the final error type for debugging
927
+ HyperDebug.LogError($"Score submission failed after {MAX_RETRY_COUNT} attempts. Error type: {LastSubmissionError}");
817
928
  _isWaitingForFinalScoreResponse = false;
818
929
  _gameManager?.GameOverModalComponent?.AllAttemptsHaveFailed();
819
930
  _finalScoreSubmissionCoroutine = null;
@@ -842,7 +953,10 @@ namespace Hyper.Internal.Managers
842
953
  #region Score Submission (WebSocket)
843
954
 
844
955
  private Coroutine _scoreSenderCoroutine;
845
- private List<ScoreData> _lastSentBatch = new List<ScoreData>();
956
+ // Stores standalone copies of the last batch that was sent over the socket.
957
+ // We explicitly clone these so they are not affected by the DataManager's object pooling.
958
+ private readonly List<ScoreData> _lastSentBatch = new List<ScoreData>();
959
+ private readonly List<ScoreData> _scoreSendBuffer = new List<ScoreData>();
846
960
  private ScoreData _lastScoreData;
847
961
  private readonly WaitForSeconds _scoreSendInterval = new WaitForSeconds(1f);
848
962
  private bool _isWaitingForFinalScoreResponse;
@@ -872,21 +986,20 @@ namespace Hyper.Internal.Managers
872
986
  {
873
987
  while (true)
874
988
  {
875
- List<ScoreData> scoresToSend = null;
989
+ var scoresToSend = _scoreSendBuffer;
990
+ scoresToSend.Clear();
876
991
 
877
- try
992
+ if (_dataManager?.PendingScores.Count > 0)
878
993
  {
879
- if (_dataManager?.PendingScores.Count > 0)
880
- {
881
- scoresToSend = new List<ScoreData>(_dataManager?.PendingScores);
882
- _dataManager?.PendingScores.Clear();
883
- SendScoresOverWebSocket(scoresToSend);
884
- }
994
+ // Copy pending scores into our reusable buffer, then clear the source list
995
+ scoresToSend.AddRange(_dataManager?.PendingScores);
996
+ _dataManager?.PendingScores.Clear();
997
+ HyperDebug.Log($"[Score] Preparing to send {scoresToSend.Count} score sample(s) over WebSocket. LastPendingCumulativeScore={scoresToSend[scoresToSend.Count - 1].cumulativeScore}");
998
+ SendScoresOverWebSocket(scoresToSend);
885
999
  }
886
- finally
1000
+ else
887
1001
  {
888
- scoresToSend?.Clear();
889
- scoresToSend = null;
1002
+ HyperDebug.Log("[Score] No pending scores to send in this tick.");
890
1003
  }
891
1004
 
892
1005
  yield return _scoreSendInterval;
@@ -901,14 +1014,66 @@ namespace Hyper.Internal.Managers
901
1014
  // and ensures we wait for reconnection to get fresh token via SendGameResumed()
902
1015
  if (!isSocketConnected)
903
1016
  {
904
- HyperDebug.LogWarning("Cannot send scores: WebSocket not connected");
1017
+ int lastScore = scores[scores.Count - 1].cumulativeScore;
1018
+ HyperDebug.LogWarning($"[Score] Cannot send scores: WebSocket not connected. LastBatchCumulativeScore={lastScore} [ErrorType=NetworkError]");
905
1019
  return;
906
1020
  }
907
1021
 
908
1022
  try
909
1023
  {
910
- _lastSentBatch = new List<ScoreData>(scores);
911
- var scoreData = scores.Last();
1024
+ // Clone the last batch into standalone ScoreData instances
1025
+ // so that pooling/reset in DataManager does not zero out the values we log/use here.
1026
+ _lastSentBatch.Clear();
1027
+
1028
+ ScoreData scoreData = null;
1029
+ if (scores.Count > 0)
1030
+ {
1031
+ // We only need the last score point for submission, but we keep the whole batch
1032
+ // for events so listeners can inspect the last acknowledged score accurately.
1033
+ for (int i = 0; i < scores.Count; i++)
1034
+ {
1035
+ var source = scores[i];
1036
+ if (source == null) continue;
1037
+
1038
+ var cloned = new ScoreData
1039
+ {
1040
+ cumulativeScore = source.cumulativeScore,
1041
+ isFinal = source.isFinal,
1042
+ timestampUtc = source.timestampUtc
1043
+ };
1044
+
1045
+ if (source.scoreSnapshot != null)
1046
+ {
1047
+ var snapshot = source.scoreSnapshot;
1048
+ var clonedSnapshot = new ScoreSnapshot
1049
+ {
1050
+ scoreTime = snapshot.scoreTime,
1051
+ scoreDelta = snapshot.scoreDelta
1052
+ };
1053
+
1054
+ if (snapshot.gameplayModifiers != null)
1055
+ {
1056
+ var mods = snapshot.gameplayModifiers;
1057
+ var clonedMods = new GameplayModifiers
1058
+ {
1059
+ comboModifiers = mods.comboModifiers,
1060
+ globalModifiers = mods.globalModifiers,
1061
+ activePowerUps = mods.activePowerUps != null
1062
+ ? new List<string>(mods.activePowerUps)
1063
+ : null
1064
+ };
1065
+
1066
+ clonedSnapshot.gameplayModifiers = clonedMods;
1067
+ }
1068
+
1069
+ cloned.scoreSnapshot = clonedSnapshot;
1070
+ }
1071
+
1072
+ _lastSentBatch.Add(cloned);
1073
+ }
1074
+
1075
+ scoreData = scores[scores.Count - 1];
1076
+ }
912
1077
 
913
1078
  string rawScoreJson = JsonConvert.SerializeObject(scoreData);
914
1079
  var compressedScoreData = JsonCompressor.CompressJson(rawScoreJson);
@@ -917,25 +1082,27 @@ namespace Hyper.Internal.Managers
917
1082
 
918
1083
  if (string.IsNullOrEmpty(encryptedData))
919
1084
  {
920
- HyperDebug.LogError("Encryption failed");
1085
+ int lastScore = scoreData != null ? scoreData.cumulativeScore : -1;
1086
+ HyperDebug.LogError($"[Score] Encryption failed for per-second submission. CumulativeScore={lastScore} [ErrorType=EncryptionError]");
921
1087
  return;
922
1088
  }
923
1089
 
924
1090
  var message = new WebSocketScoreMessage(_cachedSessionId, _csrfToken, encryptedData);
925
1091
  string json = JsonConvert.SerializeObject(message);
1092
+ int csrfLength = string.IsNullOrEmpty(_csrfToken) ? 0 : _csrfToken.Length;
1093
+ int cumulativeScore = scoreData != null ? scoreData.cumulativeScore : -1;
1094
+ HyperDebug.Log($"[Score] Sending per-second score. CumulativeScore={cumulativeScore}, BatchSize={scores.Count}, SessionId={_cachedSessionId}, CsrfLength={csrfLength}");
926
1095
  SendWebSocketMessage(json);
927
-
928
- #if UNITY_EDITOR
929
- _lastScoreData = _lastSentBatch.Last();
930
- _lastSentBatch.Clear();
931
- #endif
932
1096
  }
933
1097
  catch (Exception ex)
934
1098
  {
935
- HyperDebug.LogError($"Failed to send scores via WebSocket: {ex.Message}");
1099
+ int lastScore = scores[scores.Count - 1].cumulativeScore;
1100
+ HyperDebug.LogError($"[Score] Failed to send scores via WebSocket. LastBatchCumulativeScore={lastScore} [ErrorType=NetworkError, Exception={ex.Message}]");
936
1101
  }
937
1102
  finally
938
1103
  {
1104
+ if (_dataManager == null) _dataManager = HyperRuntime.DataManager;
1105
+ _dataManager?.ReturnScoreDataToPool(scores);
939
1106
  scores?.Clear();
940
1107
  }
941
1108
  }
@@ -44,7 +44,7 @@ namespace Hyper.Internal.Managers
44
44
  public void UpdateScore(int score)
45
45
  {
46
46
  if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
47
-
47
+
48
48
  if (_gameManager.HasGameEnded) return;
49
49
 
50
50
  CurrentScore = score;
@@ -56,23 +56,26 @@ namespace Hyper.Internal.Managers
56
56
 
57
57
  float currentGameSecond = Mathf.Floor(currentTimestamp);
58
58
 
59
- CurrentScoreData = new ScoreData
59
+ // Reuse a single ScoreData graph for CurrentScoreData to avoid per-call allocations
60
+ if (CurrentScoreData == null)
60
61
  {
61
- cumulativeScore = CurrentScore,
62
- timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
63
- isFinal = true,
64
- scoreSnapshot = new ScoreSnapshot
65
- {
66
- scoreTime = currentGameSecond,
67
- scoreDelta = CurrentScore - _lastCapturedScore,
68
- gameplayModifiers = new GameplayModifiers
69
- {
70
- comboModifiers = ComboMultiplier,
71
- globalModifiers = GlobalMultiplier,
72
- activePowerUps = ActivePowerups.ToList()
73
- }
74
- }
75
- };
62
+ CurrentScoreData = _currentScoreDataBuffer;
63
+ CurrentScoreData.scoreSnapshot = _currentSnapshotBuffer;
64
+ CurrentScoreData.scoreSnapshot.gameplayModifiers = _currentModifiersBuffer;
65
+ }
66
+
67
+ CurrentScoreData.cumulativeScore = CurrentScore;
68
+ CurrentScoreData.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
69
+ CurrentScoreData.isFinal = true;
70
+
71
+ var snapshot = CurrentScoreData.scoreSnapshot;
72
+ snapshot.scoreTime = currentGameSecond;
73
+ snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
74
+
75
+ var modifiers = snapshot.gameplayModifiers;
76
+ modifiers.comboModifiers = ComboMultiplier;
77
+ modifiers.globalModifiers = GlobalMultiplier;
78
+ modifiers.activePowerUps = ActivePowerups;
76
79
 
77
80
  if (score > GetBestScore())
78
81
  {
@@ -130,6 +133,7 @@ namespace Hyper.Internal.Managers
130
133
  internal List<ScoreData> PendingScores { get; private set; } = new List<ScoreData>();
131
134
  internal TutorialData TutorialData { get; private set; }
132
135
  internal bool HasReferencedTutorialResources { get; private set; }
136
+ private const int SCORE_POOL_CAPACITY = 15;
133
137
 
134
138
  internal void ConfigurePlayerSoundSetting()
135
139
  {
@@ -152,7 +156,7 @@ namespace Hyper.Internal.Managers
152
156
  internal void StartCapturingScores()
153
157
  {
154
158
  if (_gameManager == null) _gameManager = HyperRuntime.GameManager;
155
-
159
+
156
160
  if (_scoreCaptureCoroutine == null)
157
161
  {
158
162
  _scoreCaptureCoroutine = StartCoroutine(CaptureScoreEverySecond());
@@ -200,6 +204,16 @@ namespace Hyper.Internal.Managers
200
204
  private float _lastCapturedGameSecond = -1f;
201
205
  private int _lastCapturedScore = 0;
202
206
 
207
+ // Reusable object graph for CurrentScoreData to avoid allocations in UpdateScore()
208
+ private readonly ScoreData _currentScoreDataBuffer = new ScoreData();
209
+ private readonly ScoreSnapshot _currentSnapshotBuffer = new ScoreSnapshot();
210
+ private readonly GameplayModifiers _currentModifiersBuffer = new GameplayModifiers();
211
+
212
+ // Pools used for per-second score snapshots captured into PendingScores
213
+ private readonly Queue<ScoreData> _scoreDataPool = new Queue<ScoreData>();
214
+ private readonly Queue<ScoreSnapshot> _scoreSnapshotPool = new Queue<ScoreSnapshot>();
215
+ private readonly Queue<GameplayModifiers> _gameplayModifiersPool = new Queue<GameplayModifiers>();
216
+
203
217
  #endregion
204
218
 
205
219
  #region Score Capture Logic
@@ -216,23 +230,24 @@ namespace Hyper.Internal.Managers
216
230
 
217
231
  if (currentGameSecond > _lastCapturedGameSecond)
218
232
  {
219
- ScoreData newScore = new ScoreData
220
- {
221
- cumulativeScore = CurrentScore,
222
- timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
223
- isFinal = false,
224
- scoreSnapshot = new ScoreSnapshot
225
- {
226
- scoreTime = currentGameSecond,
227
- scoreDelta = CurrentScore - _lastCapturedScore,
228
- gameplayModifiers = new GameplayModifiers
229
- {
230
- comboModifiers = ComboMultiplier,
231
- globalModifiers = GlobalMultiplier,
232
- activePowerUps = ActivePowerups.ToList()
233
- }
234
- }
235
- };
233
+ // Get objects from pools to avoid allocations in this hot path
234
+ var newScore = GetPooledScoreData();
235
+ var snapshot = GetPooledScoreSnapshot();
236
+ var modifiers = GetPooledGameplayModifiers();
237
+
238
+ newScore.cumulativeScore = CurrentScore;
239
+ newScore.timestampUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
240
+ newScore.isFinal = false;
241
+
242
+ snapshot.scoreTime = currentGameSecond;
243
+ snapshot.scoreDelta = CurrentScore - _lastCapturedScore;
244
+
245
+ modifiers.comboModifiers = ComboMultiplier;
246
+ modifiers.globalModifiers = GlobalMultiplier;
247
+ modifiers.activePowerUps = ActivePowerups;
248
+
249
+ snapshot.gameplayModifiers = modifiers;
250
+ newScore.scoreSnapshot = snapshot;
236
251
 
237
252
  PendingScores.Add(newScore);
238
253
 
@@ -244,6 +259,84 @@ namespace Hyper.Internal.Managers
244
259
  }
245
260
  }
246
261
 
262
+ private ScoreData GetPooledScoreData()
263
+ {
264
+ return _scoreDataPool.Count > 0 ? _scoreDataPool.Dequeue() : new ScoreData();
265
+ }
266
+
267
+ private ScoreSnapshot GetPooledScoreSnapshot()
268
+ {
269
+ return _scoreSnapshotPool.Count > 0 ? _scoreSnapshotPool.Dequeue() : new ScoreSnapshot();
270
+ }
271
+
272
+ private GameplayModifiers GetPooledGameplayModifiers()
273
+ {
274
+ return _gameplayModifiersPool.Count > 0 ? _gameplayModifiersPool.Dequeue() : new GameplayModifiers();
275
+ }
276
+
277
+ internal void ReturnScoreDataToPool(List<ScoreData> scores)
278
+ {
279
+ if (scores == null) return;
280
+
281
+ for (int i = 0; i < scores.Count; i++)
282
+ {
283
+ ReturnScoreDataToPool(scores[i]);
284
+ }
285
+ }
286
+
287
+ private void ReturnScoreDataToPool(ScoreData data)
288
+ {
289
+ if (data == null) return;
290
+
291
+ if (data.scoreSnapshot != null)
292
+ {
293
+ ReturnScoreSnapshotToPool(data.scoreSnapshot);
294
+ data.scoreSnapshot = null;
295
+ }
296
+
297
+ data.cumulativeScore = 0;
298
+ data.isFinal = false;
299
+ data.timestampUtc = null;
300
+
301
+ if (_scoreDataPool.Count < SCORE_POOL_CAPACITY)
302
+ {
303
+ _scoreDataPool.Enqueue(data);
304
+ }
305
+ }
306
+
307
+ private void ReturnScoreSnapshotToPool(ScoreSnapshot snapshot)
308
+ {
309
+ if (snapshot == null) return;
310
+
311
+ if (snapshot.gameplayModifiers != null)
312
+ {
313
+ ReturnGameplayModifiersToPool(snapshot.gameplayModifiers);
314
+ snapshot.gameplayModifiers = null;
315
+ }
316
+
317
+ snapshot.scoreTime = 0f;
318
+ snapshot.scoreDelta = 0;
319
+
320
+ if (_scoreSnapshotPool.Count < SCORE_POOL_CAPACITY)
321
+ {
322
+ _scoreSnapshotPool.Enqueue(snapshot);
323
+ }
324
+ }
325
+
326
+ private void ReturnGameplayModifiersToPool(GameplayModifiers modifiers)
327
+ {
328
+ if (modifiers == null) return;
329
+
330
+ modifiers.comboModifiers = 0f;
331
+ modifiers.globalModifiers = 0f;
332
+ modifiers.activePowerUps = null;
333
+
334
+ if (_gameplayModifiersPool.Count < SCORE_POOL_CAPACITY)
335
+ {
336
+ _gameplayModifiersPool.Enqueue(modifiers);
337
+ }
338
+ }
339
+
247
340
  private void SetBestScore(ScoreData scoreData, string sessionId)
248
341
  {
249
342
  BestScoreWrapper bestScore = new BestScoreWrapper(scoreData, sessionId);
@@ -38,6 +38,9 @@ namespace Hyper.Internal
38
38
  [Tooltip("Choose the start CTA overlay shown before gameplay begins. Hyper requires one of these overlays - do not add your own start buttons.")]
39
39
  public StartCTA startCTA = StartCTA.TapToPlay;
40
40
 
41
+ [Tooltip("If Start CTA is set to Swipe to Play, choose the swipe direction used for the visual cue.")]
42
+ public SwipeDirection swipeDirection = SwipeDirection.Up;
43
+
41
44
  #endregion
42
45
 
43
46
  #region Control Bar Settings