Performance improvements

This commit is contained in:
2025-12-18 03:01:20 +08:00
parent c6ebe96a12
commit 1d134ffc40
8 changed files with 144 additions and 81 deletions

View File

@@ -35,7 +35,7 @@ namespace Services
} }
public UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates) { public UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates) {
List<Vector2Int> initialBombs = new List<Vector2Int>(); HashSet<Vector2Int> initialBombs = new HashSet<Vector2Int>();
foreach (Vector2Int p in bombCandidates) { foreach (Vector2Int p in bombCandidates) {
if (!GemUtils.IsInBounds(p, this.gameBoard)) continue; if (!GemUtils.IsInBounds(p, this.gameBoard)) continue;
@@ -47,7 +47,7 @@ namespace Services
initialBombs.Add(p); initialBombs.Add(p);
} }
return UniTask.FromResult(initialBombs.Distinct().ToList()); return UniTask.FromResult(initialBombs.ToList());
} }
public List<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem) { public List<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem) {
@@ -64,7 +64,7 @@ namespace Services
return positions; return positions;
} }
public void DetectBombSpawnFromLastSwap(List<Gem> currentMatches) { public void DetectBombSpawnFromLastSwap(HashSet<Gem> currentMatches) {
Vector2Int from = this.lastSwapFrom; Vector2Int from = this.lastSwapFrom;
Vector2Int to = this.lastSwapTo; Vector2Int to = this.lastSwapTo;
@@ -72,7 +72,7 @@ namespace Services
TryCreateBombSpawnAt(to, currentMatches); TryCreateBombSpawnAt(to, currentMatches);
} }
private void TryCreateBombSpawnAt(Vector2Int pivot, List<Gem> currentMatches) { private void TryCreateBombSpawnAt(Vector2Int pivot, HashSet<Gem> currentMatches) {
Gem pivotGem = this.gameBoard.GetGemAt(pivot); Gem pivotGem = this.gameBoard.GetGemAt(pivot);
if (pivotGem == null) if (pivotGem == null)
return; return;
@@ -81,7 +81,7 @@ namespace Services
if (pivotGem.Type == GemType.Bomb) if (pivotGem.Type == GemType.Bomb)
return; return;
if (currentMatches.All(g => g.Position != pivot)) if (currentMatches == null || !currentMatches.Contains(pivotGem))
return; return;
// Only create a bomb if pivot is part of a straight 4+ line of the SAME color. // Only create a bomb if pivot is part of a straight 4+ line of the SAME color.
@@ -108,14 +108,14 @@ namespace Services
HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>(); HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>();
Queue<Vector2Int> waveQueue = new Queue<Vector2Int>( Queue<Vector2Int> waveQueue = new Queue<Vector2Int>();
initialBombs.Where(p => foreach (Vector2Int position in initialBombs) {
{ if (GemUtils.IsInBounds(position, gameBoard)) {
if (!GemUtils.IsInBounds(p, gameBoard)) return false; Gem gem = gameBoard.GetGemAt(position);
Gem g = gameBoard.GetGemAt(p); if(gem is { Type: GemType.Bomb })
return g is { Type: GemType.Bomb }; waveQueue.Enqueue(position);
}) }
); }
while (waveQueue.Count > 0) while (waveQueue.Count > 0)
{ {

View File

@@ -36,6 +36,7 @@ namespace Services {
#region Variables #region Variables
private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>(); private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
private readonly Dictionary<Gem, GemView> gemToView = new Dictionary<Gem, GemView>();
private GameState currentState = GameState.Setup; private GameState currentState = GameState.Setup;
#endregion #endregion
@@ -81,13 +82,13 @@ namespace Services {
SpawnBackgroundTile(position); SpawnBackgroundTile(position);
int iterations = 0; int iterations = 0;
int gemToUse = -1; GemType gemToUse;
do { do {
gemToUse = RandomUtils.RandomGemTypeAsInt(); gemToUse = RandomUtils.RandomGemType();
iterations++; 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); SpawnCascade(gemsToSpawn);
@@ -108,6 +109,7 @@ namespace Services {
GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs); GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs);
gemView.Bind(gem, gemValue, isBomb: isBomb); gemView.Bind(gem, gemValue, isBomb: isBomb);
this.gemPresenters.Add(new GemPresenter(gem, gemView)); this.gemPresenters.Add(new GemPresenter(gem, gemView));
this.gemToView.Add(gem, gemView);
} }
private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) { private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
@@ -216,7 +218,10 @@ namespace Services {
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) { private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
List<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions); List<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions.Distinct().ToList());
HashSet<Vector2Int> uniqueMatchPositions = new HashSet<Vector2Int>(matchPositions);
List<Vector2Int> bombCandidates = uniqueMatchPositions.ToList();
List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, bombCandidates);
// If a bomb is part of the match, do NOT destroy matching pieces immediately. // 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. // Let the bomb's manhattan-distance explosion destroy them in sequence.
@@ -226,7 +231,7 @@ namespace Services {
DestroyAtAsync, DestroyAtAsync,
this.gameBoard); this.gameBoard);
foreach (Vector2Int p in matchPositions.Distinct()) foreach (Vector2Int p in uniqueMatchPositions)
await DestroyAtAsync(p); await DestroyAtAsync(p);
await UniTask.Delay(this.gameVariables.fillBoardDelayMs); await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
@@ -236,12 +241,19 @@ namespace Services {
} }
// For audio SFX // 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) if (willBreakAnyNonBombGem)
this.audioPresenter.OnMatch(this.gameVariables.matchSfx); this.audioPresenter.OnMatch(this.gameVariables.matchSfx);
// For score counting // For score counting
foreach (Vector2Int pos in matchPositions.Distinct().ToList()) { foreach (Vector2Int pos in uniqueMatchPositions) {
Gem gem = this.gameBoard.GetGemAt(pos); Gem gem = this.gameBoard.GetGemAt(pos);
if (gem == null) continue; if (gem == null) continue;
if (gem.Type == GemType.Bomb) continue; if (gem.Type == GemType.Bomb) continue;
@@ -267,14 +279,11 @@ namespace Services {
} }
private void ReleaseMatchedGems(Vector2Int position) { private void ReleaseMatchedGems(Vector2Int position) {
List<GemView> gemsViews = this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
Gem currentGem = this.gameBoard.GetGemAt(position); Gem currentGem = this.gameBoard.GetGemAt(position);
if (currentGem != null) if (currentGem != null)
{ {
GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem); if (!this.gemToView.TryGetValue(currentGem, out GemView gemView) || gemView == null)
if (gemView is null) {
return; return;
}
this.objectPool.Release(gemView); this.objectPool.Release(gemView);
RemovePresenterFor(gemView); RemovePresenterFor(gemView);
@@ -283,11 +292,10 @@ namespace Services {
} }
private async UniTask MoveGemsDown() { private async UniTask MoveGemsDown() {
while (true) List<FallMove> moves = new List<FallMove>();
{ while (true) {
moves.Clear();
// Build moves from a snapshot of the current grid state (no mid-wave chaining) // 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 x = 0; x < this.gameBoard.Width; x++)
{ {
for (int y = 1; y < this.gameBoard.Height; y++) for (int y = 1; y < this.gameBoard.Height; y++)
@@ -299,21 +307,21 @@ namespace Services {
if (gem == null) if (gem == null)
continue; continue;
Gem below = this.gameBoard.GetGemAt(to); if (this.gameBoard.GetGemAt(to) != null)
if (below != null)
continue; continue;
moves.Add((from, to, gem)); moves.Add(new FallMove {
from = from,
to = to,
gem = gem
});
} }
} }
if (moves.Count == 0) if (moves.Count == 0)
break; break;
// Apply all moves simultaneously for this wave foreach (FallMove move in moves) {
for (int i = 0; i < moves.Count; i++)
{
(Vector2Int from, Vector2Int to, Gem gem) move = moves[i];
this.gameBoard.SetGemAt(move.to, move.gem); this.gameBoard.SetGemAt(move.to, move.gem);
this.gameBoard.SetGemAt(move.from, null); this.gameBoard.SetGemAt(move.from, null);
move.gem.SetPosition(move.to); move.gem.SetPosition(move.to);
@@ -347,12 +355,12 @@ namespace Services {
{ {
Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y)); Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y));
if (currentGem == null) { if (currentGem == null) {
int gemToUse = RandomUtils.RandomGemTypeAsInt(); GemType gemToUse = RandomUtils.RandomGemType();
int iterations = 0; int iterations = 0;
while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100) while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100)
{ {
gemToUse = RandomUtils.RandomGemTypeAsInt(); gemToUse = RandomUtils.RandomGemType();
iterations++; iterations++;
} }
@@ -395,6 +403,8 @@ namespace Services {
public void Dispose() { public void Dispose() {
this.objectPool.Clear(); this.objectPool.Clear();
this.gemPresenters.Clear();
this.gemToView.Clear();
} }
} }
} }

View File

@@ -14,7 +14,7 @@ namespace Services.Interfaces
void SetLastSwap(Vector2Int from, Vector2Int to); void SetLastSwap(Vector2Int from, Vector2Int to);
void DetectBombSpawnFromLastSwap(List<Gem> currentMatches); void DetectBombSpawnFromLastSwap(HashSet<Gem> currentMatches);
List<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem); List<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem);
UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates); UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates);

View File

@@ -6,7 +6,7 @@ using Structs;
namespace Services.Interfaces { namespace Services.Interfaces {
public interface IMatchService { public interface IMatchService {
List<Gem> CurrentMatches { get; } HashSet<Gem> CurrentMatches { get; }
UniTask<List<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions); UniTask<List<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions);
bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck); bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck);
void FindAllMatches(); void FindAllMatches();

View File

@@ -11,9 +11,8 @@ namespace Services {
public class MatchService : IMatchService { public class MatchService : IMatchService {
private readonly IGameBoard gameBoard; private readonly IGameBoard gameBoard;
private readonly HashSet<Gem> currentMatches = new HashSet<Gem>();
private List<Gem> currentMatches = new List<Gem>(); public HashSet<Gem> CurrentMatches => this.currentMatches;
public List<Gem> CurrentMatches => this.currentMatches;
public MatchService(IGameBoard gameBoard) { public MatchService(IGameBoard gameBoard) {
this.gameBoard = gameBoard; this.gameBoard = gameBoard;
@@ -52,9 +51,10 @@ namespace Services {
} }
public UniTask<List<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions) { public UniTask<List<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions) {
List<Vector2Int> matchPositions = new List<Vector2Int>(CurrentMatches.Count); List<Vector2Int> matchPositions = new List<Vector2Int>(this.currentMatches.Count);
for (int i = 0; i < CurrentMatches.Count; i++) { List<Gem> matches = this.currentMatches.ToList();
Gem match = CurrentMatches[i]; for (int i = 0; i < matches.Count; i++) {
Gem match = matches[i];
if (match == null) continue; if (match == null) continue;
Vector2Int pos = match.Position; Vector2Int pos = match.Position;
@@ -70,39 +70,59 @@ namespace Services {
public void FindAllMatches() { public void FindAllMatches() {
this.currentMatches.Clear(); this.currentMatches.Clear();
for (int x = 0; x < this.gameBoard.Width; x++) Gem[,] grid = this.gameBoard.GemsGrid;
for (int y = 0; y < this.gameBoard.Height; y++) { int boardWidth = this.gameBoard.Width;
Gem currentGem = this.gameBoard.GemsGrid[x, y]; int boardHeight = this.gameBoard.Height;
if (currentGem == null)
continue;
if (x > 0 && x < this.gameBoard.Width - 1) { // Horizontal runs
Gem leftGem = this.gameBoard.GemsGrid[x - 1, y]; for (int y = 0; y < boardHeight; y++) {
Gem rightGem = this.gameBoard.GemsGrid[x + 1, y]; int x = 0;
if (leftGem != null && rightGem != null) { while (x < boardWidth) {
if (leftGem.MatchColor == currentGem.MatchColor && rightGem.MatchColor == currentGem.MatchColor) { Gem start = grid[x, y];
this.currentMatches.Add(currentGem); if (start == null) { x++; continue; }
this.currentMatches.Add(leftGem);
this.currentMatches.Add(rightGem); 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) { if (runLen >= 3) {
Gem aboveGem = this.gameBoard.GemsGrid[x, y - 1]; for (int i = 0; i < runLen; i++)
Gem bellowGem = this.gameBoard.GemsGrid[x, y + 1]; this.currentMatches.Add(grid[x + i, y]);
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);
}
}
} }
x += runLen;
} }
}
if (this.currentMatches.Count > 0) // Vertical runs
this.currentMatches = this.currentMatches.Distinct().ToList(); 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;
}
}
} }
} }
} }

View File

@@ -0,0 +1,10 @@
using Services;
using UnityEngine;
namespace Structs {
public struct FallMove {
public Vector2Int from;
public Vector2Int to;
public Gem gem;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 420c7135e99442e280e7ac7439d5c702
timeCreated: 1765995012

View File

@@ -5,13 +5,33 @@ using Random = UnityEngine.Random;
namespace Utils { namespace Utils {
public static class RandomUtils { public static class RandomUtils {
public static int RandomGemTypeAsInt() { private static readonly GemType[] spawnableGems = BuildSpawnableGems();
GemType[] spawnableGems = Enum.GetValues(typeof(GemType))
.Cast<GemType>() private static GemType[] BuildSpawnableGems() {
.Where(gType => gType != GemType.Bomb) Array values = Enum.GetValues(typeof(GemType));
.ToArray(); int count = 0;
return Random.Range(0, spawnableGems.Length); 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)];
} }
} }
} }