From 668bd03f632f3e39b7833f4d998b334828641dff Mon Sep 17 00:00:00 2001 From: Jesus Castro Date: Thu, 18 Dec 2025 03:44:51 +0800 Subject: [PATCH] Optimization and Documentation --- Assets/Scripts/Services/AudioService.cs | 2 +- Assets/Scripts/Services/BombService.cs | 23 ++- Assets/Scripts/Services/GameBoardService.cs | 155 ++++++++++++++---- Assets/Scripts/Services/InputService.cs | 2 +- .../Services/Interfaces/IBombService.cs | 43 ++++- .../Services/Interfaces/IGameBoardService.cs | 2 +- .../Services/Interfaces/IMatchService.cs | 20 ++- .../Services/Interfaces/IScoreService.cs | 2 +- Assets/Scripts/Services/MatchService.cs | 6 +- Assets/Scripts/Services/ScoreService.cs | 2 +- 10 files changed, 204 insertions(+), 53 deletions(-) diff --git a/Assets/Scripts/Services/AudioService.cs b/Assets/Scripts/Services/AudioService.cs index fcc3c4e..6d0900c 100644 --- a/Assets/Scripts/Services/AudioService.cs +++ b/Assets/Scripts/Services/AudioService.cs @@ -25,7 +25,7 @@ namespace Services public void PlaySound(AudioClip clip) { if (clip == null) return; - if (this.source == null) return; // In case called before Initialize in some edge setup + if (this.source == null) return; // In case it's called before Initialize in some edge setup this.source.PlayOneShot(clip); } diff --git a/Assets/Scripts/Services/BombService.cs b/Assets/Scripts/Services/BombService.cs index c7f821e..89a5af0 100644 --- a/Assets/Scripts/Services/BombService.cs +++ b/Assets/Scripts/Services/BombService.cs @@ -20,7 +20,6 @@ namespace Services private Vector2Int lastSwapTo; private BombSpawnRequest? pendingBombSpawn; - public BombSpawnRequest? PendingBombSpawn => this.pendingBombSpawn; public BombService(GameVariables gameVariables, IGameBoard gameBoard) { this.gameVariables = gameVariables; @@ -33,8 +32,8 @@ namespace Services ClearPendingBombs(); } - - public UniTask> GetInitialBombs(List protectedPositions, List bombCandidates) { + + public UniTask> GetInitialBombs(List protectedPositions, HashSet bombCandidates) { HashSet initialBombs = new HashSet(); foreach (Vector2Int p in bombCandidates) { if (!GemUtils.IsInBounds(p, this.gameBoard)) continue; @@ -52,10 +51,10 @@ namespace Services public List ApplyPendingBombSpawns(Action spawnGem) { List positions = new List(); - BombSpawnRequest? bombSpawnRequest = PendingBombSpawn; + BombSpawnRequest? bombSpawnRequest = this.pendingBombSpawn; if (bombSpawnRequest != null) { - BombSpawnRequest bombRequest = PendingBombSpawn.GetValueOrDefault(); + BombSpawnRequest bombRequest = this.pendingBombSpawn.GetValueOrDefault(); positions.Add(bombRequest.Position); spawnGem(bombRequest.Position, bombRequest.Color, true); } @@ -84,7 +83,7 @@ namespace Services if (currentMatches == null || !currentMatches.Contains(pivotGem)) return; - // Only create a bomb if pivot is part of a straight 4+ line of the SAME color. + // Only create a bomb if the pivot is part of a straight 4+ line of the SAME color. int longestLine = GetLongestMatchedLineThroughPivot(pivot, pivotGem.MatchColor); if (longestLine < 4) return; @@ -181,7 +180,7 @@ namespace Services } } - // Destroy everything for this wave (non-bombs in range + the detonating bombs themselves) + // Destroy everything for this wave (non-bombs in range and the detonating bombs themselves) foreach (Vector2Int p in toDestroyNow) await destroyAtAsync(p); @@ -216,21 +215,21 @@ namespace Services private int CountSameColorInDirection(Vector2Int start, Vector2Int direction, GemType color) { int count = 0; - Vector2Int oivot = start + direction; + Vector2Int pivot = start + direction; - while (oivot.x >= 0 && oivot.x < this.gameBoard.Width && oivot.y >= 0 && oivot.y < this.gameBoard.Height) { - Gem g = this.gameBoard.GetGemAt(oivot); + while (pivot.x >= 0 && pivot.x < this.gameBoard.Width && pivot.y >= 0 && pivot.y < this.gameBoard.Height) { + Gem g = this.gameBoard.GetGemAt(pivot); if (g == null || g.Type == GemType.Bomb || g.MatchColor != color) break; count++; - oivot += direction; + pivot += direction; } return count; } - public void ClearPendingBombs() { + private void ClearPendingBombs() { this.pendingBombSpawn = null; } } diff --git a/Assets/Scripts/Services/GameBoardService.cs b/Assets/Scripts/Services/GameBoardService.cs index 10ecf16..a4cac9a 100644 --- a/Assets/Scripts/Services/GameBoardService.cs +++ b/Assets/Scripts/Services/GameBoardService.cs @@ -9,7 +9,6 @@ using ScriptableObjects; using Services.Interfaces; using Structs; using UnityEngine; -using UnityEngine.UIElements; using Utils; using VContainer.Unity; using Views; @@ -30,7 +29,6 @@ namespace Services { private readonly ScorePresenter scorePresenter; private readonly AudioPresenter audioPresenter; - private readonly Transform gemsHolder; private readonly Transform backgroundHolder; #endregion @@ -49,7 +47,6 @@ namespace Services { IScoreService scoreService, ScorePresenter scorePresenter, AudioPresenter audioPresenter, - Transform gemsHolder, Transform backgroundHolder) { this.gameVariables = gameVariables; this.gameBoard = gameBoard; @@ -59,10 +56,12 @@ namespace Services { this.scoreService = scoreService; this.scorePresenter = scorePresenter; this.audioPresenter = audioPresenter; - this.gemsHolder = gemsHolder; this.backgroundHolder = backgroundHolder; } + /// + /// Global tick. Update replacement of VContainer-managed objects. + /// public void Tick() { foreach (GemPresenter gemPresenter in gemPresenters) { gemPresenter.Tick(this.gameVariables.gemSpeed); @@ -71,8 +70,9 @@ namespace Services { this.scorePresenter.Tick(this.gameVariables.scoreSpeed); } - //Instantiates background tiles and calls SpawnGems - //Uses MatchService.MatchesAt to avoid matching Gems + /// + /// Initiates the game board by placing gems randomly. + /// public void Setup() { List gemsToSpawn = new List(); for (int x = 0; x < this.gameBoard.Width; x++) @@ -91,9 +91,22 @@ namespace Services { gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), gemToUse)); } - SpawnCascade(gemsToSpawn); + SpawnCascade(gemsToSpawn).Forget(); } + /// + /// Create a Gem object and place it on the GameBoard. + /// + /// + /// Position on the GameBoard to place the Gem. + /// + /// + /// Type of Gem to create. + /// + /// + /// Whether the Gem is a Bomb or not. + /// + /// private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) { GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs); Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType); @@ -102,6 +115,15 @@ namespace Services { return gem; } + /// + /// Spawns a GemView object and binds it to the Gem object. + /// + /// + /// Gem to bind to the GemView. + /// + /// + /// Whether the Gem is a Bomb or not. + /// private void SpawnGemGameObject(Gem gem, bool isBomb = false) { GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gem.Type, gem.Position, isBomb ? 0 : this.gameVariables.dropHeight); gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type; @@ -112,6 +134,18 @@ namespace Services { this.gemToView.Add(gem, gemView); } + /// + /// Calls SetGemAt and SpawnGemGameObject. + /// + /// + /// Position on the GameBoard to place the Gem. + /// + /// + /// Type of Gem to create. + /// + /// + /// Whether the Gem is a Bomb or not. + /// private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) { if(isBomb) ReleaseMatchedGems(position); @@ -119,13 +153,28 @@ namespace Services { SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb); } + /// + /// Spawns a background tile at the given position. Only for setup. + /// + /// + /// Position on the GameBoard to place the tile. + /// private void SpawnBackgroundTile(Vector2 position) { GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity); backgroundTile.transform.SetParent(this.backgroundHolder); backgroundTile.name = "BG Tile - " + position.x + ", " + position.y; } - //Listens to InputService OnSwapRequest + /// + /// Attempts to swap two gems on the GameBoard. + /// + /// + /// Original position of the gem to swap. + /// + /// + /// Destination position of the gem to swap. + /// + /// public async UniTask TrySwap(Vector2Int from, Vector2Int to) { if (this.currentState != GameState.Move) return false; @@ -158,10 +207,17 @@ namespace Services { return true; } - public async UniTask TrySwitch(Vector2Int position) { + /// + /// For debug purposes. Switches the Gem at the given position to a different GemType. + /// + /// + /// Position of the Gem to switch. + /// + /// + public void TrySwitch(Vector2Int position) { Gem gem = this.gameBoard.GetGemAt(position); if(gem == null) - return false; + return; GemType[] normalTypes = Enum.GetValues(typeof(GemType)) .Cast() @@ -169,7 +225,7 @@ namespace Services { .ToArray(); if (normalTypes.Length == 0) - return false; + return; bool nextIsBomb; GemType nextTypeOrMatchColor; @@ -198,12 +254,20 @@ namespace Services { } } - // Replace both model+view by releasing current and respawning at same position. + // Replace both model+view by releasing current and respawning at the same position. ReleaseMatchedGems(position); SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb); - return true; } + /// + /// Helper function for TrySwap. Swaps the Gems in the GameBoard. + /// + /// + /// Original position of the gem to swap. + /// + /// + /// Destination position of the gem to swap. + /// private void ApplySwap(Vector2Int from, Vector2Int to) { Gem fromGem = this.gameBoard.GetGemAt(from); Gem toGem = this.gameBoard.GetGemAt(to); @@ -216,22 +280,23 @@ namespace Services { this.gameBoard.SetGemAt(to, fromGem); } + /// + /// Destroys all matches on the GameBoard. + /// + /// + /// Gems we don't want to instantly destroy. + /// private async UniTask DestroyMatchesAsync(List protectedPositions) { - List matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions); - - HashSet uniqueMatchPositions = new HashSet(matchPositions); - List bombCandidates = uniqueMatchPositions.ToList(); - List initialBombs = await this.bombService.GetInitialBombs(protectedPositions, bombCandidates); + HashSet matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions); + List initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions); - // If a bomb is part of the match, do NOT destroy matching pieces immediately. - // Let the bomb's manhattan-distance explosion destroy them in sequence. if (initialBombs.Count > 0) { await this.bombService.DetonateChainAsync( initialBombs, DestroyAtAsync, this.gameBoard); - foreach (Vector2Int p in uniqueMatchPositions) + foreach (Vector2Int p in matchPositions) await DestroyAtAsync(p); await UniTask.Delay(this.gameVariables.fillBoardDelayMs); @@ -242,7 +307,7 @@ namespace Services { // For audio SFX bool willBreakAnyNonBombGem = false; - foreach (Vector2Int pos in uniqueMatchPositions) { + foreach (Vector2Int pos in matchPositions) { Gem g = this.gameBoard.GetGemAt(pos); if (g != null && g.Type != GemType.Bomb) { willBreakAnyNonBombGem = true; @@ -253,18 +318,25 @@ namespace Services { this.audioPresenter.OnMatch(this.gameVariables.matchSfx); // For score counting - foreach (Vector2Int pos in uniqueMatchPositions) { + foreach (Vector2Int pos in matchPositions) { Gem gem = this.gameBoard.GetGemAt(pos); if (gem == null) continue; if (gem.Type == GemType.Bomb) continue; - this.scoreService.ScoreCheck(gem.ScoreValue); + this.scoreService.AddScore(gem.ScoreValue); ReleaseMatchedGems(pos); } await UniTask.Delay(this.gameVariables.fillBoardDelayMs); await MoveGemsDown(); } + /// + /// Destroys the Gem at the given position. + /// + /// + /// Position of the Gem to destroy. + /// + /// private UniTask DestroyAtAsync(Vector2Int pos) { Gem gem = this.gameBoard.GetGemAt(pos); if (gem == null) @@ -273,11 +345,17 @@ namespace Services { if (gem.Type == GemType.Bomb) this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx); - this.scoreService.ScoreCheck(gem.ScoreValue); + this.scoreService.AddScore(gem.ScoreValue); ReleaseMatchedGems(pos); return UniTask.CompletedTask; } + /// + /// Returns the GemView to the Object Pool and sets the Position in the GameBoard to null. + /// + /// + /// Position of the Gem to release. + /// private void ReleaseMatchedGems(Vector2Int position) { Gem currentGem = this.gameBoard.GetGemAt(position); if (currentGem != null) @@ -291,11 +369,13 @@ namespace Services { } } + /// + /// Moves Gems Down to free spaces. + /// private async UniTask MoveGemsDown() { List moves = new List(); while (true) { moves.Clear(); - // Build moves from a snapshot of the current grid state (no mid-wave chaining) for (int x = 0; x < this.gameBoard.Width; x++) { for (int y = 1; y < this.gameBoard.Height; y++) @@ -333,6 +413,9 @@ namespace Services { await FillBoard(); } + /// + /// Fills the GameBoard with gems. If there are resulting matches, process them. + /// private async UniTask FillBoard() { await RefillBoard(); @@ -340,13 +423,15 @@ namespace Services { if (this.matchService.CurrentMatches.Count > 0) { await UniTask.Delay(this.gameVariables.fillBoardDelayMs); - // In cascades, there is no "creating slot" bomb protection. await DestroyMatchesAsync(new List()); } else { this.currentState = GameState.Move; } } + /// + /// Refills the GameBoard with gems. + /// private async UniTask RefillBoard() { List gemsToSpawn = new List(); for (int x = 0; x < this.gameBoard.Width; x++) @@ -358,13 +443,13 @@ namespace Services { GemType gemToUse = RandomUtils.RandomGemType(); int iterations = 0; - while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100) + while (this.matchService.MatchesAt(new Vector2Int(x, y), gemToUse) && iterations < 100) { gemToUse = RandomUtils.RandomGemType(); iterations++; } - gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), (GemType)gemToUse)); + gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), gemToUse)); } } } @@ -372,6 +457,12 @@ namespace Services { await SpawnCascade(gemsToSpawn); } + /// + /// Spawns gems in cascade. + /// + /// + /// List of gems to spawn. + /// private async UniTask SpawnCascade(List gemsToSpawn) { List> groups = gemsToSpawn @@ -392,6 +483,12 @@ namespace Services { this.currentState = GameState.Move; } + /// + /// Removes the Presenter for the given GemView from the list to avoid calling its Tick method. + /// + /// + /// GemView to remove from the List of presenters. + /// private void RemovePresenterFor(GemView gemView) { if (gemView is null) { return; diff --git a/Assets/Scripts/Services/InputService.cs b/Assets/Scripts/Services/InputService.cs index f6295dc..1bbf735 100644 --- a/Assets/Scripts/Services/InputService.cs +++ b/Assets/Scripts/Services/InputService.cs @@ -100,7 +100,7 @@ namespace Services { || touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary; - // Ended/Canceled => not down (we still report position) + // Ended/Canceled => not down (we still report the position) if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) isDown = false; diff --git a/Assets/Scripts/Services/Interfaces/IBombService.cs b/Assets/Scripts/Services/Interfaces/IBombService.cs index be590a4..8283f4c 100644 --- a/Assets/Scripts/Services/Interfaces/IBombService.cs +++ b/Assets/Scripts/Services/Interfaces/IBombService.cs @@ -10,14 +10,51 @@ using UnityEngine; namespace Services.Interfaces { public interface IBombService { - public BombSpawnRequest? PendingBombSpawn { get; } - + /// + /// Caches last swap action. + /// + /// + /// Original location of the gem. + /// + /// + /// Destination location of the gem. + /// void SetLastSwap(Vector2Int from, Vector2Int to); + /// + /// Try to spawn a bomb at the last swap location. + /// + /// + /// List of current matches. + /// void DetectBombSpawnFromLastSwap(HashSet currentMatches); List ApplyPendingBombSpawns(Action spawnGem); - UniTask> GetInitialBombs(List protectedPositions, List bombCandidates); + /// + /// Get a List of bombs that we will detonate. + /// + /// + /// Protected positions, bombs that we don't want to destroy. + /// + /// + /// Possible bombs. + /// + /// + UniTask> GetInitialBombs(List protectedPositions, HashSet bombCandidates); + + /// + /// Detonate the bomb(s) part of the match. If there are other bombs within the radius, they will be detonated too sequentially. + /// + /// + /// List of bombs to detonate. + /// + /// + /// Destroy function reference. + /// + /// + /// Gameboard reference. + /// + /// UniTask DetonateChainAsync( IReadOnlyList initialBombs, Func destroyAtAsync, diff --git a/Assets/Scripts/Services/Interfaces/IGameBoardService.cs b/Assets/Scripts/Services/Interfaces/IGameBoardService.cs index a35eab8..3518681 100644 --- a/Assets/Scripts/Services/Interfaces/IGameBoardService.cs +++ b/Assets/Scripts/Services/Interfaces/IGameBoardService.cs @@ -6,6 +6,6 @@ namespace Services.Interfaces { void Setup(); UniTask TrySwap(Vector2Int from, Vector2Int to); - UniTask TrySwitch(Vector2Int position); + void TrySwitch(Vector2Int position); } } \ No newline at end of file diff --git a/Assets/Scripts/Services/Interfaces/IMatchService.cs b/Assets/Scripts/Services/Interfaces/IMatchService.cs index 4873357..94ed3f6 100644 --- a/Assets/Scripts/Services/Interfaces/IMatchService.cs +++ b/Assets/Scripts/Services/Interfaces/IMatchService.cs @@ -7,7 +7,25 @@ using Structs; namespace Services.Interfaces { public interface IMatchService { HashSet CurrentMatches { get; } - UniTask> GetMatchPositionsAsync(List protectedPositions); + /// + /// Get positions of all matches that are not protected. + /// + /// + /// Protected positions, bombs that we don't want to destroy. + /// + /// + UniTask> GetMatchPositionsAsync(List protectedPositions); + + /// + /// Checks if there are any matches at the given position and type. + /// + /// + /// Position on the gameBoard to check. + /// + /// + /// Type of gem to check. + /// + /// bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck); void FindAllMatches(); } diff --git a/Assets/Scripts/Services/Interfaces/IScoreService.cs b/Assets/Scripts/Services/Interfaces/IScoreService.cs index 401de0d..9c534ae 100644 --- a/Assets/Scripts/Services/Interfaces/IScoreService.cs +++ b/Assets/Scripts/Services/Interfaces/IScoreService.cs @@ -4,6 +4,6 @@ namespace Services.Interfaces { public interface IScoreService { event Action OnScoreChanged; int Score { get; } - void ScoreCheck(int value); + void AddScore(int value); } } \ No newline at end of file diff --git a/Assets/Scripts/Services/MatchService.cs b/Assets/Scripts/Services/MatchService.cs index 7c6a6f6..8ce7efa 100644 --- a/Assets/Scripts/Services/MatchService.cs +++ b/Assets/Scripts/Services/MatchService.cs @@ -49,9 +49,9 @@ namespace Services { return false; } - - public UniTask> GetMatchPositionsAsync(List protectedPositions) { - List matchPositions = new List(this.currentMatches.Count); + + public UniTask> GetMatchPositionsAsync(List protectedPositions) { + HashSet matchPositions = new HashSet(); List matches = this.currentMatches.ToList(); for (int i = 0; i < matches.Count; i++) { Gem match = matches[i]; diff --git a/Assets/Scripts/Services/ScoreService.cs b/Assets/Scripts/Services/ScoreService.cs index 35a20c7..af786cc 100644 --- a/Assets/Scripts/Services/ScoreService.cs +++ b/Assets/Scripts/Services/ScoreService.cs @@ -7,7 +7,7 @@ namespace Services { private int score = 0; public int Score => this.score; public event Action OnScoreChanged; - public void ScoreCheck(int value) { + public void AddScore(int value) { this.score += value; OnScoreChanged?.Invoke(this.score);