From 1d134ffc40a25f8f199cfea40f524270dcb315f4 Mon Sep 17 00:00:00 2001 From: Jesus Castro Date: Thu, 18 Dec 2025 03:01:20 +0800 Subject: [PATCH] Performance improvements --- Assets/Scripts/Services/BombService.cs | 26 +++--- Assets/Scripts/Services/GameBoardService.cs | 62 +++++++------ .../Services/Interfaces/IBombService.cs | 2 +- .../Services/Interfaces/IMatchService.cs | 2 +- Assets/Scripts/Services/MatchService.cs | 86 ++++++++++++------- Assets/Scripts/Structs/FallMove.cs | 10 +++ Assets/Scripts/Structs/FallMove.cs.meta | 3 + Assets/Scripts/Utils/RandomUtils.cs | 34 ++++++-- 8 files changed, 144 insertions(+), 81 deletions(-) create mode 100644 Assets/Scripts/Structs/FallMove.cs create mode 100644 Assets/Scripts/Structs/FallMove.cs.meta diff --git a/Assets/Scripts/Services/BombService.cs b/Assets/Scripts/Services/BombService.cs index d3bd9df..c7f821e 100644 --- a/Assets/Scripts/Services/BombService.cs +++ b/Assets/Scripts/Services/BombService.cs @@ -35,7 +35,7 @@ namespace Services } public UniTask> GetInitialBombs(List protectedPositions, List bombCandidates) { - List initialBombs = new List(); + HashSet initialBombs = new HashSet(); foreach (Vector2Int p in bombCandidates) { if (!GemUtils.IsInBounds(p, this.gameBoard)) continue; @@ -47,7 +47,7 @@ namespace Services initialBombs.Add(p); } - return UniTask.FromResult(initialBombs.Distinct().ToList()); + return UniTask.FromResult(initialBombs.ToList()); } public List ApplyPendingBombSpawns(Action spawnGem) { @@ -64,7 +64,7 @@ namespace Services return positions; } - public void DetectBombSpawnFromLastSwap(List currentMatches) { + public void DetectBombSpawnFromLastSwap(HashSet currentMatches) { Vector2Int from = this.lastSwapFrom; Vector2Int to = this.lastSwapTo; @@ -72,7 +72,7 @@ namespace Services TryCreateBombSpawnAt(to, currentMatches); } - private void TryCreateBombSpawnAt(Vector2Int pivot, List currentMatches) { + private void TryCreateBombSpawnAt(Vector2Int pivot, HashSet currentMatches) { Gem pivotGem = this.gameBoard.GetGemAt(pivot); if (pivotGem == null) return; @@ -81,7 +81,7 @@ namespace Services if (pivotGem.Type == GemType.Bomb) return; - if (currentMatches.All(g => g.Position != pivot)) + if (currentMatches == null || !currentMatches.Contains(pivotGem)) return; // Only create a bomb if pivot is part of a straight 4+ line of the SAME color. @@ -108,14 +108,14 @@ namespace Services HashSet processedBombs = new HashSet(); - Queue waveQueue = new Queue( - initialBombs.Where(p => - { - if (!GemUtils.IsInBounds(p, gameBoard)) return false; - Gem g = gameBoard.GetGemAt(p); - return g is { Type: GemType.Bomb }; - }) - ); + Queue waveQueue = new Queue(); + foreach (Vector2Int position in initialBombs) { + if (GemUtils.IsInBounds(position, gameBoard)) { + Gem gem = gameBoard.GetGemAt(position); + if(gem is { Type: GemType.Bomb }) + waveQueue.Enqueue(position); + } + } while (waveQueue.Count > 0) { diff --git a/Assets/Scripts/Services/GameBoardService.cs b/Assets/Scripts/Services/GameBoardService.cs index 9967f70..10ecf16 100644 --- a/Assets/Scripts/Services/GameBoardService.cs +++ b/Assets/Scripts/Services/GameBoardService.cs @@ -36,6 +36,7 @@ namespace Services { #region Variables private readonly List gemPresenters = new List(); + private readonly Dictionary gemToView = new Dictionary(); private GameState currentState = GameState.Setup; #endregion @@ -81,13 +82,13 @@ namespace Services { SpawnBackgroundTile(position); int iterations = 0; - int gemToUse = -1; + GemType gemToUse; do { - gemToUse = RandomUtils.RandomGemTypeAsInt(); + gemToUse = RandomUtils.RandomGemType(); iterations++; - } while (this.matchService.MatchesAt(position.ToVector2Int(), (GemType)gemToUse) && iterations < 100); + } while (this.matchService.MatchesAt(position.ToVector2Int(), gemToUse) && iterations < 100); - gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), (GemType)gemToUse)); + gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), gemToUse)); } SpawnCascade(gemsToSpawn); @@ -108,6 +109,7 @@ namespace Services { GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs); gemView.Bind(gem, gemValue, isBomb: isBomb); this.gemPresenters.Add(new GemPresenter(gem, gemView)); + this.gemToView.Add(gem, gemView); } private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) { @@ -216,7 +218,10 @@ namespace Services { private async UniTask DestroyMatchesAsync(List protectedPositions) { List matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions); - List initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions.Distinct().ToList()); + + HashSet uniqueMatchPositions = new HashSet(matchPositions); + List bombCandidates = uniqueMatchPositions.ToList(); + List initialBombs = await this.bombService.GetInitialBombs(protectedPositions, bombCandidates); // 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. @@ -226,7 +231,7 @@ namespace Services { DestroyAtAsync, this.gameBoard); - foreach (Vector2Int p in matchPositions.Distinct()) + foreach (Vector2Int p in uniqueMatchPositions) await DestroyAtAsync(p); await UniTask.Delay(this.gameVariables.fillBoardDelayMs); @@ -236,12 +241,19 @@ namespace Services { } // For audio SFX - bool willBreakAnyNonBombGem = matchPositions.Select(pos => this.gameBoard.GetGemAt(pos)).Where(gem => gem != null).Any(gem => gem.Type != GemType.Bomb); + bool willBreakAnyNonBombGem = false; + foreach (Vector2Int pos in uniqueMatchPositions) { + Gem g = this.gameBoard.GetGemAt(pos); + if (g != null && g.Type != GemType.Bomb) { + willBreakAnyNonBombGem = true; + break; + } + } if (willBreakAnyNonBombGem) this.audioPresenter.OnMatch(this.gameVariables.matchSfx); // For score counting - foreach (Vector2Int pos in matchPositions.Distinct().ToList()) { + foreach (Vector2Int pos in uniqueMatchPositions) { Gem gem = this.gameBoard.GetGemAt(pos); if (gem == null) continue; if (gem.Type == GemType.Bomb) continue; @@ -267,14 +279,11 @@ namespace Services { } private void ReleaseMatchedGems(Vector2Int position) { - List gemsViews = this.gemsHolder.GetComponentsInChildren().ToList(); Gem currentGem = this.gameBoard.GetGemAt(position); if (currentGem != null) { - GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem); - if (gemView is null) { + if (!this.gemToView.TryGetValue(currentGem, out GemView gemView) || gemView == null) return; - } this.objectPool.Release(gemView); RemovePresenterFor(gemView); @@ -283,11 +292,10 @@ namespace Services { } private async UniTask MoveGemsDown() { - while (true) - { + List moves = new List(); + while (true) { + moves.Clear(); // Build moves from a snapshot of the current grid state (no mid-wave chaining) - List<(Vector2Int from, Vector2Int to, Gem gem)> moves = new List<(Vector2Int, Vector2Int, Gem)>(); - for (int x = 0; x < this.gameBoard.Width; x++) { for (int y = 1; y < this.gameBoard.Height; y++) @@ -299,21 +307,21 @@ namespace Services { if (gem == null) continue; - Gem below = this.gameBoard.GetGemAt(to); - if (below != null) + if (this.gameBoard.GetGemAt(to) != null) continue; - moves.Add((from, to, gem)); + moves.Add(new FallMove { + from = from, + to = to, + gem = gem + }); } } if (moves.Count == 0) break; - - // Apply all moves simultaneously for this wave - for (int i = 0; i < moves.Count; i++) - { - (Vector2Int from, Vector2Int to, Gem gem) move = moves[i]; + + foreach (FallMove move in moves) { this.gameBoard.SetGemAt(move.to, move.gem); this.gameBoard.SetGemAt(move.from, null); move.gem.SetPosition(move.to); @@ -347,12 +355,12 @@ namespace Services { { Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y)); if (currentGem == null) { - int gemToUse = RandomUtils.RandomGemTypeAsInt(); + GemType gemToUse = RandomUtils.RandomGemType(); int iterations = 0; while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100) { - gemToUse = RandomUtils.RandomGemTypeAsInt(); + gemToUse = RandomUtils.RandomGemType(); iterations++; } @@ -395,6 +403,8 @@ namespace Services { public void Dispose() { this.objectPool.Clear(); + this.gemPresenters.Clear(); + this.gemToView.Clear(); } } } \ No newline at end of file diff --git a/Assets/Scripts/Services/Interfaces/IBombService.cs b/Assets/Scripts/Services/Interfaces/IBombService.cs index 65068f9..be590a4 100644 --- a/Assets/Scripts/Services/Interfaces/IBombService.cs +++ b/Assets/Scripts/Services/Interfaces/IBombService.cs @@ -14,7 +14,7 @@ namespace Services.Interfaces void SetLastSwap(Vector2Int from, Vector2Int to); - void DetectBombSpawnFromLastSwap(List currentMatches); + void DetectBombSpawnFromLastSwap(HashSet currentMatches); List ApplyPendingBombSpawns(Action spawnGem); UniTask> GetInitialBombs(List protectedPositions, List bombCandidates); diff --git a/Assets/Scripts/Services/Interfaces/IMatchService.cs b/Assets/Scripts/Services/Interfaces/IMatchService.cs index b2350b6..4873357 100644 --- a/Assets/Scripts/Services/Interfaces/IMatchService.cs +++ b/Assets/Scripts/Services/Interfaces/IMatchService.cs @@ -6,7 +6,7 @@ using Structs; namespace Services.Interfaces { public interface IMatchService { - List CurrentMatches { get; } + HashSet CurrentMatches { get; } UniTask> GetMatchPositionsAsync(List protectedPositions); bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck); void FindAllMatches(); diff --git a/Assets/Scripts/Services/MatchService.cs b/Assets/Scripts/Services/MatchService.cs index 66240de..7c6a6f6 100644 --- a/Assets/Scripts/Services/MatchService.cs +++ b/Assets/Scripts/Services/MatchService.cs @@ -11,9 +11,8 @@ namespace Services { public class MatchService : IMatchService { private readonly IGameBoard gameBoard; - - private List currentMatches = new List(); - public List CurrentMatches => this.currentMatches; + private readonly HashSet currentMatches = new HashSet(); + public HashSet CurrentMatches => this.currentMatches; public MatchService(IGameBoard gameBoard) { this.gameBoard = gameBoard; @@ -52,9 +51,10 @@ namespace Services { } public UniTask> GetMatchPositionsAsync(List protectedPositions) { - List matchPositions = new List(CurrentMatches.Count); - for (int i = 0; i < CurrentMatches.Count; i++) { - Gem match = CurrentMatches[i]; + List matchPositions = new List(this.currentMatches.Count); + List matches = this.currentMatches.ToList(); + for (int i = 0; i < matches.Count; i++) { + Gem match = matches[i]; if (match == null) continue; Vector2Int pos = match.Position; @@ -70,39 +70,59 @@ namespace Services { public void FindAllMatches() { this.currentMatches.Clear(); - for (int x = 0; x < this.gameBoard.Width; x++) - for (int y = 0; y < this.gameBoard.Height; y++) { - Gem currentGem = this.gameBoard.GemsGrid[x, y]; - if (currentGem == null) - continue; + Gem[,] grid = this.gameBoard.GemsGrid; + int boardWidth = this.gameBoard.Width; + int boardHeight = this.gameBoard.Height; - if (x > 0 && x < this.gameBoard.Width - 1) { - Gem leftGem = this.gameBoard.GemsGrid[x - 1, y]; - Gem rightGem = this.gameBoard.GemsGrid[x + 1, y]; - if (leftGem != null && rightGem != null) { - if (leftGem.MatchColor == currentGem.MatchColor && rightGem.MatchColor == currentGem.MatchColor) { - this.currentMatches.Add(currentGem); - this.currentMatches.Add(leftGem); - this.currentMatches.Add(rightGem); - } - } + // Horizontal runs + for (int y = 0; y < boardHeight; y++) { + int x = 0; + while (x < boardWidth) { + Gem start = grid[x, y]; + if (start == null) { x++; continue; } + + GemType color = start.MatchColor; + + int runLen = 1; + while (x + runLen < boardWidth) { + Gem next = grid[x + runLen, y]; + if (next == null || next.MatchColor != color) break; + runLen++; } - if (y > 0 && y < this.gameBoard.Height - 1) { - Gem aboveGem = this.gameBoard.GemsGrid[x, y - 1]; - Gem bellowGem = this.gameBoard.GemsGrid[x, y + 1]; - if (aboveGem != null && bellowGem != null) { - if (aboveGem.MatchColor == currentGem.MatchColor && bellowGem.MatchColor == currentGem.MatchColor) { - this.currentMatches.Add(currentGem); - this.currentMatches.Add(aboveGem); - this.currentMatches.Add(bellowGem); - } - } + if (runLen >= 3) { + for (int i = 0; i < runLen; i++) + this.currentMatches.Add(grid[x + i, y]); } + + x += runLen; } + } - if (this.currentMatches.Count > 0) - this.currentMatches = this.currentMatches.Distinct().ToList(); + // Vertical runs + for (int x = 0; x < boardWidth; x++) { + int y = 0; + while (y < boardHeight) { + Gem start = grid[x, y]; + if (start == null) { y++; continue; } + + GemType color = start.MatchColor; + + int runLen = 1; + while (y + runLen < boardHeight) { + Gem next = grid[x, y + runLen]; + if (next == null || next.MatchColor != color) break; + runLen++; + } + + if (runLen >= 3) { + for (int i = 0; i < runLen; i++) + this.currentMatches.Add(grid[x, y + i]); + } + + y += runLen; + } + } } } } \ No newline at end of file diff --git a/Assets/Scripts/Structs/FallMove.cs b/Assets/Scripts/Structs/FallMove.cs new file mode 100644 index 0000000..6cfc685 --- /dev/null +++ b/Assets/Scripts/Structs/FallMove.cs @@ -0,0 +1,10 @@ +using Services; +using UnityEngine; + +namespace Structs { + public struct FallMove { + public Vector2Int from; + public Vector2Int to; + public Gem gem; + } +} \ No newline at end of file diff --git a/Assets/Scripts/Structs/FallMove.cs.meta b/Assets/Scripts/Structs/FallMove.cs.meta new file mode 100644 index 0000000..5ce6c31 --- /dev/null +++ b/Assets/Scripts/Structs/FallMove.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 420c7135e99442e280e7ac7439d5c702 +timeCreated: 1765995012 \ No newline at end of file diff --git a/Assets/Scripts/Utils/RandomUtils.cs b/Assets/Scripts/Utils/RandomUtils.cs index 306692c..0fa5b63 100644 --- a/Assets/Scripts/Utils/RandomUtils.cs +++ b/Assets/Scripts/Utils/RandomUtils.cs @@ -5,13 +5,33 @@ using Random = UnityEngine.Random; namespace Utils { public static class RandomUtils { - public static int RandomGemTypeAsInt() { - GemType[] spawnableGems = Enum.GetValues(typeof(GemType)) - .Cast() - .Where(gType => gType != GemType.Bomb) - .ToArray(); - - return Random.Range(0, spawnableGems.Length); + private static readonly GemType[] spawnableGems = BuildSpawnableGems(); + + private static GemType[] BuildSpawnableGems() { + Array values = Enum.GetValues(typeof(GemType)); + int count = 0; + + for (int i = 0; i < values.Length; i++) { + if ((GemType)values.GetValue(i) != GemType.Bomb) + count++; + } + + GemType[] result = new GemType[count]; + int write = 0; + + for (int i = 0; i < values.Length; i++) { + GemType t = (GemType)values.GetValue(i); + if (t == GemType.Bomb) + continue; + + result[write++] = t; + } + + return result; + } + + public static GemType RandomGemType() { + return spawnableGems[Random.Range(0, spawnableGems.Length)]; } } } \ No newline at end of file