Compare commits
11 Commits
f92a5623fb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a5c7096cea | |||
| 993cca2943 | |||
| e2409106d4 | |||
| 73db75d40a | |||
| aae09b5696 | |||
| 3956a6ffab | |||
| 667a39c260 | |||
| 668bd03f63 | |||
| 1d134ffc40 | |||
| c6ebe96a12 | |||
| 577f78f413 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -96,4 +96,6 @@ InitTestScene*.unity*
|
|||||||
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
|
/[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta
|
||||||
|
|
||||||
# Auto-generated scenes by play mode tests
|
# Auto-generated scenes by play mode tests
|
||||||
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
|
/[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity*
|
||||||
|
|
||||||
|
*.zip
|
||||||
@@ -12,6 +12,7 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 9f37e854902a46cb8bd927cf84ab450c, type: 3}
|
m_Script: {fileID: 11500000, guid: 9f37e854902a46cb8bd927cf84ab450c, type: 3}
|
||||||
m_Name: GameVariables
|
m_Name: GameVariables
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
|
debugMode: 1
|
||||||
bgTilePrefabs: {fileID: 2914066502361773997, guid: 3f39182b81f944a4d93213431acb41c3,
|
bgTilePrefabs: {fileID: 2914066502361773997, guid: 3f39182b81f944a4d93213431acb41c3,
|
||||||
type: 3}
|
type: 3}
|
||||||
gemsPrefabs:
|
gemsPrefabs:
|
||||||
@@ -64,5 +65,8 @@ MonoBehaviour:
|
|||||||
bombDelay: 2
|
bombDelay: 2
|
||||||
bombRadius: 2
|
bombRadius: 2
|
||||||
dropHeight: 2
|
dropHeight: 2
|
||||||
gemSpeed: 4
|
gemSpeed: 5
|
||||||
scoreSpeed: 3
|
scoreSpeed: 3
|
||||||
|
cascadeDelayMs: 150
|
||||||
|
swapDelayMs: 600
|
||||||
|
fillBoardDelayMs: 500
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ using UnityEngine;
|
|||||||
namespace ScriptableObjects {
|
namespace ScriptableObjects {
|
||||||
[CreateAssetMenu(fileName = "GameVariables", menuName = "Game Variables")]
|
[CreateAssetMenu(fileName = "GameVariables", menuName = "Game Variables")]
|
||||||
public class GameVariables : ScriptableObject {
|
public class GameVariables : ScriptableObject {
|
||||||
|
[Header("Debug")]
|
||||||
|
[Tooltip("Turns Gem switching on/off")]
|
||||||
|
public bool debugMode;
|
||||||
|
|
||||||
[Header("Prefabs")]
|
[Header("Prefabs")]
|
||||||
public GameObject bgTilePrefabs;
|
public GameObject bgTilePrefabs;
|
||||||
public GemTypeValues[] gemsPrefabs;
|
public GemTypeValues[] gemsPrefabs;
|
||||||
@@ -28,5 +32,10 @@ namespace ScriptableObjects {
|
|||||||
|
|
||||||
[Header("Score")]
|
[Header("Score")]
|
||||||
public float scoreSpeed = 5;
|
public float scoreSpeed = 5;
|
||||||
|
|
||||||
|
[Header("Delays")]
|
||||||
|
public int cascadeDelayMs = 150;
|
||||||
|
public int swapDelayMs = 600;
|
||||||
|
public int fillBoardDelayMs = 250;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ namespace Services
|
|||||||
public void PlaySound(AudioClip clip)
|
public void PlaySound(AudioClip clip)
|
||||||
{
|
{
|
||||||
if (clip == null) return;
|
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);
|
this.source.PlayOneShot(clip);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ namespace Services
|
|||||||
private Vector2Int lastSwapFrom;
|
private Vector2Int lastSwapFrom;
|
||||||
private Vector2Int lastSwapTo;
|
private Vector2Int lastSwapTo;
|
||||||
|
|
||||||
private BombSpawnRequest? pendingBombSpawn;
|
private List<BombSpawnRequest> pendingBombSpawns = new List<BombSpawnRequest>();
|
||||||
public BombSpawnRequest? PendingBombSpawn => this.pendingBombSpawn;
|
|
||||||
|
|
||||||
public BombService(GameVariables gameVariables, IGameBoard gameBoard) {
|
public BombService(GameVariables gameVariables, IGameBoard gameBoard) {
|
||||||
this.gameVariables = gameVariables;
|
this.gameVariables = gameVariables;
|
||||||
@@ -33,9 +32,9 @@ namespace Services
|
|||||||
|
|
||||||
ClearPendingBombs();
|
ClearPendingBombs();
|
||||||
}
|
}
|
||||||
|
|
||||||
public UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, List<Vector2Int> bombCandidates) {
|
public UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, HashSet<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,15 +46,13 @@ 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) {
|
||||||
List<Vector2Int> positions = new List<Vector2Int>();
|
List<Vector2Int> positions = new List<Vector2Int>();
|
||||||
BombSpawnRequest? bombSpawnRequest = PendingBombSpawn;
|
|
||||||
|
|
||||||
if (bombSpawnRequest != null) {
|
foreach (BombSpawnRequest bombRequest in this.pendingBombSpawns) {
|
||||||
BombSpawnRequest bombRequest = PendingBombSpawn.GetValueOrDefault();
|
|
||||||
positions.Add(bombRequest.Position);
|
positions.Add(bombRequest.Position);
|
||||||
spawnGem(bombRequest.Position, bombRequest.Color, true);
|
spawnGem(bombRequest.Position, bombRequest.Color, true);
|
||||||
}
|
}
|
||||||
@@ -64,7 +61,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 +69,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,25 +78,42 @@ 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.
|
// Create a bomb if it's a 4+ line OR an L/T shape (both horizontal and vertical >= 3)
|
||||||
int longestLine = GetLongestMatchedLineThroughPivot(pivot, pivotGem.MatchColor);
|
bool isEligibleForBomb = IsEligibleForBomb(pivot, pivotGem.MatchColor);
|
||||||
if (longestLine < 4)
|
if (!isEligibleForBomb)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Prevent duplicates for the same cell.
|
// Prevent duplicates for the same cell.
|
||||||
if (this.pendingBombSpawn.GetValueOrDefault().Position == pivot)
|
if (this.pendingBombSpawns.Any(b => b.Position == pivot))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.pendingBombSpawn = new BombSpawnRequest(pivot, pivotGem.MatchColor);
|
this.pendingBombSpawns.Add(new BombSpawnRequest(pivot, pivotGem.MatchColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsEligibleForBomb(Vector2Int pivot, GemType color) {
|
||||||
|
int horizontal = 1 + CountSameColorInDirection(pivot, Vector2Int.left, color)
|
||||||
|
+ CountSameColorInDirection(pivot, Vector2Int.right, color);
|
||||||
|
|
||||||
|
int vertical = 1 + CountSameColorInDirection(pivot, Vector2Int.up, color)
|
||||||
|
+ CountSameColorInDirection(pivot, Vector2Int.down, color);
|
||||||
|
|
||||||
|
// Straight line of 4 or more
|
||||||
|
if (horizontal >= 4 || vertical >= 4)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// L or T shape: both directions have at least a 3-match intersecting at this pivot
|
||||||
|
if (horizontal >= 3 && vertical >= 3)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async UniTask DetonateChainAsync(
|
public async UniTask DetonateChainAsync(
|
||||||
IReadOnlyList<Vector2Int> initialBombs,
|
IReadOnlyList<Vector2Int> initialBombs,
|
||||||
Func<Vector2Int, UniTask> destroyAtAsync,
|
Func<Vector2Int, UniTask> destroyAtAsync)
|
||||||
IGameBoard gameBoard)
|
|
||||||
{
|
{
|
||||||
if (initialBombs == null || initialBombs.Count == 0)
|
if (initialBombs == null || initialBombs.Count == 0)
|
||||||
return;
|
return;
|
||||||
@@ -108,86 +122,66 @@ 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, this.gameBoard)) {
|
||||||
if (!GemUtils.IsInBounds(p, gameBoard)) return false;
|
Gem gem = this.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)
|
||||||
{
|
{
|
||||||
// current wave (per bomb)
|
Vector2Int bombPos = waveQueue.Dequeue();
|
||||||
List<Vector2Int> waveBombs = new List<Vector2Int>();
|
|
||||||
while (waveQueue.Count > 0)
|
|
||||||
{
|
|
||||||
Vector2Int b = waveQueue.Dequeue();
|
|
||||||
if (processedBombs.Contains(b))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!GemUtils.IsInBounds(b, gameBoard))
|
if (processedBombs.Contains(bombPos))
|
||||||
continue;
|
|
||||||
|
|
||||||
Gem g = gameBoard.GetGemAt(b);
|
|
||||||
if (g is not { Type: GemType.Bomb })
|
|
||||||
continue;
|
|
||||||
|
|
||||||
processedBombs.Add(b);
|
|
||||||
waveBombs.Add(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (waveBombs.Count == 0)
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// delay once per wave
|
if (!GemUtils.IsInBounds(bombPos, this.gameBoard))
|
||||||
if (waveDelayMs > 0)
|
continue;
|
||||||
await UniTask.Delay(waveDelayMs);
|
|
||||||
|
|
||||||
HashSet<Vector2Int> nextWaveBombs = new HashSet<Vector2Int>();
|
Gem g = this.gameBoard.GetGemAt(bombPos);
|
||||||
|
if (g is not { Type: GemType.Bomb })
|
||||||
|
continue;
|
||||||
|
|
||||||
|
processedBombs.Add(bombPos);
|
||||||
|
|
||||||
|
// delay once per bomb
|
||||||
|
await UniTask.Delay(waveDelayMs);
|
||||||
HashSet<Vector2Int> toDestroyNow = new HashSet<Vector2Int>();
|
HashSet<Vector2Int> toDestroyNow = new HashSet<Vector2Int>();
|
||||||
|
|
||||||
for (int i = 0; i < waveBombs.Count; i++)
|
// destroy self when it detonates
|
||||||
|
toDestroyNow.Add(bombPos);
|
||||||
|
|
||||||
|
foreach (Vector2Int position in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius))
|
||||||
{
|
{
|
||||||
Vector2Int bombPos = waveBombs[i];
|
if (!GemUtils.IsInBounds(position, this.gameBoard))
|
||||||
|
continue;
|
||||||
|
|
||||||
// destroy self when it detonates
|
if (position == bombPos)
|
||||||
toDestroyNow.Add(bombPos);
|
continue;
|
||||||
|
|
||||||
foreach (Vector2Int p in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius))
|
Gem cellGem = this.gameBoard.GetGemAt(position);
|
||||||
|
if (cellGem == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (cellGem.Type == GemType.Bomb)
|
||||||
{
|
{
|
||||||
if (!GemUtils.IsInBounds(p, gameBoard))
|
// bombs in range are NOT destroyed now. triggered to explode in a later "step".
|
||||||
continue;
|
if (!processedBombs.Contains(position))
|
||||||
|
waveQueue.Enqueue(position);
|
||||||
|
|
||||||
if (p == bombPos)
|
continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
Gem cellGem = gameBoard.GetGemAt(p);
|
|
||||||
if (cellGem == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (cellGem.Type == GemType.Bomb)
|
|
||||||
{
|
|
||||||
// bombs in range are NOT destroyed now. triggered to explode in a later wave.
|
|
||||||
if (!processedBombs.Contains(p))
|
|
||||||
nextWaveBombs.Add(p);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-bomb gem gets destroyed by this bomb
|
|
||||||
toDestroyNow.Add(p);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-bomb gem gets destroyed by this bomb
|
||||||
|
toDestroyNow.Add(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Destroy everything for this wave (non-bombs in range + the detonating bombs themselves)
|
// Destroy everything for this specific bomb detonation
|
||||||
foreach (Vector2Int p in toDestroyNow)
|
foreach (Vector2Int p in toDestroyNow)
|
||||||
await destroyAtAsync(p);
|
await destroyAtAsync(p);
|
||||||
|
|
||||||
// Schedule the next wave (triggered bombs)
|
|
||||||
foreach (Vector2Int b in nextWaveBombs)
|
|
||||||
waveQueue.Enqueue(b);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,34 +198,24 @@ namespace Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int GetLongestMatchedLineThroughPivot(Vector2Int pivot, GemType color) {
|
|
||||||
int horizontal = 1 + CountSameColorInDirection(pivot, Vector2Int.left, color)
|
|
||||||
+ CountSameColorInDirection(pivot, Vector2Int.right, color);
|
|
||||||
|
|
||||||
int vertical = 1 + CountSameColorInDirection(pivot, Vector2Int.up, color)
|
|
||||||
+ CountSameColorInDirection(pivot, Vector2Int.down, color);
|
|
||||||
|
|
||||||
return Mathf.Max(horizontal, vertical);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int CountSameColorInDirection(Vector2Int start, Vector2Int direction, GemType color) {
|
private int CountSameColorInDirection(Vector2Int start, Vector2Int direction, GemType color) {
|
||||||
int count = 0;
|
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) {
|
while (pivot.x >= 0 && pivot.x < this.gameBoard.Width && pivot.y >= 0 && pivot.y < this.gameBoard.Height) {
|
||||||
Gem g = this.gameBoard.GetGemAt(oivot);
|
Gem g = this.gameBoard.GetGemAt(pivot);
|
||||||
if (g == null || g.Type == GemType.Bomb || g.MatchColor != color)
|
if (g == null || g.Type == GemType.Bomb || g.MatchColor != color)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
count++;
|
count++;
|
||||||
oivot += direction;
|
pivot += direction;
|
||||||
}
|
}
|
||||||
|
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearPendingBombs() {
|
private void ClearPendingBombs() {
|
||||||
this.pendingBombSpawn = null;
|
this.pendingBombSpawns.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,6 @@ using ScriptableObjects;
|
|||||||
using Services.Interfaces;
|
using Services.Interfaces;
|
||||||
using Structs;
|
using Structs;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.UIElements;
|
|
||||||
using Utils;
|
using Utils;
|
||||||
using VContainer.Unity;
|
using VContainer.Unity;
|
||||||
using Views;
|
using Views;
|
||||||
@@ -30,12 +29,12 @@ namespace Services {
|
|||||||
private readonly ScorePresenter scorePresenter;
|
private readonly ScorePresenter scorePresenter;
|
||||||
private readonly AudioPresenter audioPresenter;
|
private readonly AudioPresenter audioPresenter;
|
||||||
|
|
||||||
private readonly Transform gemsHolder;
|
|
||||||
private readonly Transform backgroundHolder;
|
private readonly Transform backgroundHolder;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#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
|
||||||
|
|
||||||
@@ -48,7 +47,6 @@ namespace Services {
|
|||||||
IScoreService scoreService,
|
IScoreService scoreService,
|
||||||
ScorePresenter scorePresenter,
|
ScorePresenter scorePresenter,
|
||||||
AudioPresenter audioPresenter,
|
AudioPresenter audioPresenter,
|
||||||
Transform gemsHolder,
|
|
||||||
Transform backgroundHolder) {
|
Transform backgroundHolder) {
|
||||||
this.gameVariables = gameVariables;
|
this.gameVariables = gameVariables;
|
||||||
this.gameBoard = gameBoard;
|
this.gameBoard = gameBoard;
|
||||||
@@ -58,10 +56,12 @@ namespace Services {
|
|||||||
this.scoreService = scoreService;
|
this.scoreService = scoreService;
|
||||||
this.scorePresenter = scorePresenter;
|
this.scorePresenter = scorePresenter;
|
||||||
this.audioPresenter = audioPresenter;
|
this.audioPresenter = audioPresenter;
|
||||||
this.gemsHolder = gemsHolder;
|
|
||||||
this.backgroundHolder = backgroundHolder;
|
this.backgroundHolder = backgroundHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Global tick. Update replacement of VContainer-managed objects.
|
||||||
|
/// </summary>
|
||||||
public void Tick() {
|
public void Tick() {
|
||||||
foreach (GemPresenter gemPresenter in gemPresenters) {
|
foreach (GemPresenter gemPresenter in gemPresenters) {
|
||||||
gemPresenter.Tick(this.gameVariables.gemSpeed);
|
gemPresenter.Tick(this.gameVariables.gemSpeed);
|
||||||
@@ -70,8 +70,9 @@ namespace Services {
|
|||||||
this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
|
this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Instantiates background tiles and calls SpawnGems
|
/// <summary>
|
||||||
//Uses MatchService.MatchesAt to avoid matching Gems
|
/// Initiates the game board by placing gems randomly.
|
||||||
|
/// </summary>
|
||||||
public void Setup() {
|
public void Setup() {
|
||||||
List<Gem> gemsToSpawn = new List<Gem>();
|
List<Gem> gemsToSpawn = new List<Gem>();
|
||||||
for (int x = 0; x < this.gameBoard.Width; x++)
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
||||||
@@ -81,18 +82,31 @@ 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).Forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a Gem object and place it on the GameBoard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Position on the GameBoard to place the Gem.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="gemType">
|
||||||
|
/// Type of Gem to create.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="isBomb">
|
||||||
|
/// Whether the Gem is a Bomb or not.
|
||||||
|
/// </param>
|
||||||
|
/// <returns></returns>
|
||||||
private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) {
|
private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) {
|
||||||
GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
|
GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
|
||||||
Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
|
Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
|
||||||
@@ -101,6 +115,15 @@ namespace Services {
|
|||||||
return gem;
|
return gem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns a GemView object and binds it to the Gem object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="gem">
|
||||||
|
/// Gem to bind to the GemView.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="isBomb">
|
||||||
|
/// Whether the Gem is a Bomb or not.
|
||||||
|
/// </param>
|
||||||
private void SpawnGemGameObject(Gem gem, bool isBomb = false) {
|
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 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;
|
gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type;
|
||||||
@@ -108,8 +131,21 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calls SetGemAt and SpawnGemGameObject.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Position on the GameBoard to place the Gem.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="gemType">
|
||||||
|
/// Type of Gem to create.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="isBomb">
|
||||||
|
/// Whether the Gem is a Bomb or not.
|
||||||
|
/// </param>
|
||||||
private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
|
private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
|
||||||
if(isBomb)
|
if(isBomb)
|
||||||
ReleaseMatchedGems(position);
|
ReleaseMatchedGems(position);
|
||||||
@@ -117,13 +153,28 @@ namespace Services {
|
|||||||
SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
|
SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns a background tile at the given position. Only for setup.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Position on the GameBoard to place the tile.
|
||||||
|
/// </param>
|
||||||
private void SpawnBackgroundTile(Vector2 position) {
|
private void SpawnBackgroundTile(Vector2 position) {
|
||||||
GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
|
GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
|
||||||
backgroundTile.transform.SetParent(this.backgroundHolder);
|
backgroundTile.transform.SetParent(this.backgroundHolder);
|
||||||
backgroundTile.name = "BG Tile - " + position.x + ", " + position.y;
|
backgroundTile.name = "BG Tile - " + position.x + ", " + position.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Listens to InputService OnSwapRequest
|
/// <summary>
|
||||||
|
/// Attempts to swap two gems on the GameBoard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="from">
|
||||||
|
/// Original position of the gem to swap.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="to">
|
||||||
|
/// Destination position of the gem to swap.
|
||||||
|
/// </param>
|
||||||
|
/// <returns></returns>
|
||||||
public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
|
public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
|
||||||
if (this.currentState != GameState.Move)
|
if (this.currentState != GameState.Move)
|
||||||
return false;
|
return false;
|
||||||
@@ -138,14 +189,14 @@ namespace Services {
|
|||||||
|
|
||||||
ApplySwap(from, to);
|
ApplySwap(from, to);
|
||||||
|
|
||||||
await UniTask.Delay(600);
|
await UniTask.Delay(this.gameVariables.swapDelayMs);
|
||||||
this.bombService.SetLastSwap(from, to);
|
this.bombService.SetLastSwap(from, to);
|
||||||
this.matchService.FindAllMatches();
|
this.matchService.FindAllMatches();
|
||||||
this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
|
this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
|
||||||
|
|
||||||
if (this.matchService.CurrentMatches.Count == 0) {
|
if (this.matchService.CurrentMatches.Count == 0) {
|
||||||
ApplySwap(to, from);
|
ApplySwap(to, from);
|
||||||
await UniTask.Delay(600);
|
await UniTask.Delay(this.gameVariables.swapDelayMs);
|
||||||
this.currentState = GameState.Move;
|
this.currentState = GameState.Move;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -156,10 +207,20 @@ namespace Services {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async UniTask<bool> TrySwitch(Vector2Int position) {
|
/// <summary>
|
||||||
|
/// For debug purposes. Switches the Gem at the given position to a different GemType.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Position of the Gem to switch.
|
||||||
|
/// </param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public void TrySwitch(Vector2Int position) {
|
||||||
|
if (!this.gameVariables.debugMode)
|
||||||
|
return;
|
||||||
|
|
||||||
Gem gem = this.gameBoard.GetGemAt(position);
|
Gem gem = this.gameBoard.GetGemAt(position);
|
||||||
if(gem == null)
|
if(gem == null)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
GemType[] normalTypes = Enum.GetValues(typeof(GemType))
|
GemType[] normalTypes = Enum.GetValues(typeof(GemType))
|
||||||
.Cast<GemType>()
|
.Cast<GemType>()
|
||||||
@@ -167,7 +228,7 @@ namespace Services {
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
if (normalTypes.Length == 0)
|
if (normalTypes.Length == 0)
|
||||||
return false;
|
return;
|
||||||
|
|
||||||
bool nextIsBomb;
|
bool nextIsBomb;
|
||||||
GemType nextTypeOrMatchColor;
|
GemType nextTypeOrMatchColor;
|
||||||
@@ -196,12 +257,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);
|
ReleaseMatchedGems(position);
|
||||||
SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
|
SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper function for TrySwap. Swaps the Gems in the GameBoard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="from">
|
||||||
|
/// Original position of the gem to swap.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="to">
|
||||||
|
/// Destination position of the gem to swap.
|
||||||
|
/// </param>
|
||||||
private void ApplySwap(Vector2Int from, Vector2Int to) {
|
private void ApplySwap(Vector2Int from, Vector2Int to) {
|
||||||
Gem fromGem = this.gameBoard.GetGemAt(from);
|
Gem fromGem = this.gameBoard.GetGemAt(from);
|
||||||
Gem toGem = this.gameBoard.GetGemAt(to);
|
Gem toGem = this.gameBoard.GetGemAt(to);
|
||||||
@@ -214,45 +283,62 @@ namespace Services {
|
|||||||
this.gameBoard.SetGemAt(to, fromGem);
|
this.gameBoard.SetGemAt(to, fromGem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys all matches on the GameBoard.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="protectedPositions">
|
||||||
|
/// Gems we don't want to instantly destroy.
|
||||||
|
/// </param>
|
||||||
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
||||||
List<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
|
HashSet<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
|
||||||
List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions.Distinct().ToList());
|
List<Vector2Int> 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) {
|
if (initialBombs.Count > 0) {
|
||||||
await this.bombService.DetonateChainAsync(
|
await this.bombService.DetonateChainAsync(
|
||||||
initialBombs,
|
initialBombs,
|
||||||
DestroyAtAsync,
|
DestroyAtAsync);
|
||||||
this.gameBoard);
|
|
||||||
|
|
||||||
foreach (Vector2Int p in matchPositions.Distinct())
|
foreach (Vector2Int p in matchPositions)
|
||||||
await DestroyAtAsync(p);
|
await DestroyAtAsync(p);
|
||||||
|
|
||||||
await UniTask.Delay(600);
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
||||||
|
|
||||||
await MoveGemsDown();
|
await MoveGemsDown();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 matchPositions) {
|
||||||
|
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 matchPositions) {
|
||||||
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;
|
||||||
|
|
||||||
this.scoreService.ScoreCheck(gem.ScoreValue);
|
this.scoreService.AddScore(gem.ScoreValue);
|
||||||
ReleaseMatchedGems(pos);
|
ReleaseMatchedGems(pos);
|
||||||
}
|
}
|
||||||
await UniTask.Delay(250);
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
||||||
await MoveGemsDown();
|
await MoveGemsDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Destroys the Gem at the given position.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pos">
|
||||||
|
/// Position of the Gem to destroy.
|
||||||
|
/// </param>
|
||||||
|
/// <returns></returns>
|
||||||
private UniTask DestroyAtAsync(Vector2Int pos) {
|
private UniTask DestroyAtAsync(Vector2Int pos) {
|
||||||
Gem gem = this.gameBoard.GetGemAt(pos);
|
Gem gem = this.gameBoard.GetGemAt(pos);
|
||||||
if (gem == null)
|
if (gem == null)
|
||||||
@@ -261,20 +347,23 @@ namespace Services {
|
|||||||
if (gem.Type == GemType.Bomb)
|
if (gem.Type == GemType.Bomb)
|
||||||
this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);
|
this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);
|
||||||
|
|
||||||
this.scoreService.ScoreCheck(gem.ScoreValue);
|
this.scoreService.AddScore(gem.ScoreValue);
|
||||||
ReleaseMatchedGems(pos);
|
ReleaseMatchedGems(pos);
|
||||||
return UniTask.CompletedTask;
|
return UniTask.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the GemView to the Object Pool and sets the Position in the GameBoard to null.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="position">
|
||||||
|
/// Position of the Gem to release.
|
||||||
|
/// </param>
|
||||||
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);
|
||||||
@@ -282,45 +371,69 @@ namespace Services {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Moves Gems Down to free spaces.
|
||||||
|
/// </summary>
|
||||||
private async UniTask MoveGemsDown() {
|
private async UniTask MoveGemsDown() {
|
||||||
int nullCounter = 0;
|
List<FallMove> moves = new List<FallMove>();
|
||||||
for (int x = 0; x < this.gameBoard.Width; x++)
|
while (true) {
|
||||||
{
|
moves.Clear();
|
||||||
for (int y = 0; y < this.gameBoard.Height; y++)
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
||||||
{
|
{
|
||||||
Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x, y));
|
for (int y = 1; y < this.gameBoard.Height; y++)
|
||||||
if (currentGem == null)
|
|
||||||
{
|
{
|
||||||
nullCounter++;
|
Vector2Int from = new Vector2Int(x, y);
|
||||||
}
|
Vector2Int to = new Vector2Int(x, y - 1);
|
||||||
else if (nullCounter > 0)
|
|
||||||
{
|
Gem gem = this.gameBoard.GetGemAt(from);
|
||||||
currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter));
|
if (gem == null)
|
||||||
this.gameBoard.SetGemAt(currentGem.Position, currentGem);
|
continue;
|
||||||
this.gameBoard.SetGemAt(new Vector2Int(x,y), null);
|
|
||||||
|
if (this.gameBoard.GetGemAt(to) != null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
moves.Add(new FallMove {
|
||||||
|
from = from,
|
||||||
|
to = to,
|
||||||
|
gem = gem
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nullCounter = 0;
|
|
||||||
|
if (moves.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
foreach (FallMove move in moves) {
|
||||||
|
this.gameBoard.SetGemAt(move.to, move.gem);
|
||||||
|
this.gameBoard.SetGemAt(move.from, null);
|
||||||
|
move.gem.SetPosition(move.to);
|
||||||
|
}
|
||||||
|
|
||||||
|
await UniTask.Delay(this.gameVariables.cascadeDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
await UniTask.Delay(600);
|
|
||||||
await FillBoard();
|
await FillBoard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fills the GameBoard with gems. If there are resulting matches, process them.
|
||||||
|
/// </summary>
|
||||||
private async UniTask FillBoard() {
|
private async UniTask FillBoard() {
|
||||||
await RefillBoard();
|
await RefillBoard();
|
||||||
|
|
||||||
this.matchService.FindAllMatches();
|
this.matchService.FindAllMatches();
|
||||||
if (this.matchService.CurrentMatches.Count > 0) {
|
if (this.matchService.CurrentMatches.Count > 0) {
|
||||||
await UniTask.Delay(600);
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
||||||
|
|
||||||
// In cascades, there is no "creating slot" bomb protection.
|
|
||||||
await DestroyMatchesAsync(new List<Vector2Int>());
|
await DestroyMatchesAsync(new List<Vector2Int>());
|
||||||
} else {
|
} else {
|
||||||
this.currentState = GameState.Move;
|
this.currentState = GameState.Move;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refills the GameBoard with gems.
|
||||||
|
/// </summary>
|
||||||
private async UniTask RefillBoard() {
|
private async UniTask RefillBoard() {
|
||||||
List<Gem> gemsToSpawn = new List<Gem>();
|
List<Gem> gemsToSpawn = new List<Gem>();
|
||||||
for (int x = 0; x < this.gameBoard.Width; x++)
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
||||||
@@ -329,16 +442,16 @@ 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), gemToUse) && iterations < 100)
|
||||||
{
|
{
|
||||||
gemToUse = RandomUtils.RandomGemTypeAsInt();
|
gemToUse = RandomUtils.RandomGemType();
|
||||||
iterations++;
|
iterations++;
|
||||||
}
|
}
|
||||||
|
|
||||||
gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), (GemType)gemToUse));
|
gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), gemToUse));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,6 +459,12 @@ namespace Services {
|
|||||||
await SpawnCascade(gemsToSpawn);
|
await SpawnCascade(gemsToSpawn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns gems in cascade.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="gemsToSpawn">
|
||||||
|
/// List of gems to spawn.
|
||||||
|
/// </param>
|
||||||
private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
|
private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
|
||||||
{
|
{
|
||||||
List<IGrouping<int, Gem>> groups = gemsToSpawn
|
List<IGrouping<int, Gem>> groups = gemsToSpawn
|
||||||
@@ -359,13 +478,19 @@ namespace Services {
|
|||||||
SpawnGemGameObject(gem);
|
SpawnGemGameObject(gem);
|
||||||
|
|
||||||
if (i < groups.Count - 1)
|
if (i < groups.Count - 1)
|
||||||
await UniTask.Delay(150);
|
await UniTask.Delay(this.gameVariables.cascadeDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(this.currentState == GameState.Setup)
|
if(this.currentState == GameState.Setup)
|
||||||
this.currentState = GameState.Move;
|
this.currentState = GameState.Move;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes the Presenter for the given GemView from the list to avoid calling its Tick method.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="gemView">
|
||||||
|
/// GemView to remove from the List of presenters.
|
||||||
|
/// </param>
|
||||||
private void RemovePresenterFor(GemView gemView) {
|
private void RemovePresenterFor(GemView gemView) {
|
||||||
if (gemView is null) {
|
if (gemView is null) {
|
||||||
return;
|
return;
|
||||||
@@ -377,6 +502,8 @@ namespace Services {
|
|||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
this.objectPool.Clear();
|
this.objectPool.Clear();
|
||||||
|
this.gemPresenters.Clear();
|
||||||
|
this.gemToView.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,7 +100,7 @@ namespace Services {
|
|||||||
|| touch.phase == TouchPhase.Moved
|
|| touch.phase == TouchPhase.Moved
|
||||||
|| touch.phase == TouchPhase.Stationary;
|
|| 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)
|
if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled)
|
||||||
isDown = false;
|
isDown = false;
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,56 @@ using System.Collections.Generic;
|
|||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using Enums;
|
using Enums;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
using Structs;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Services.Interfaces
|
namespace Services.Interfaces
|
||||||
{
|
{
|
||||||
public interface IBombService {
|
public interface IBombService {
|
||||||
public BombSpawnRequest? PendingBombSpawn { get; }
|
/// <summary>
|
||||||
|
/// Caches last swap action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="from">
|
||||||
|
/// Original location of the gem.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="to">
|
||||||
|
/// Destination location of the gem.
|
||||||
|
/// </param>
|
||||||
void SetLastSwap(Vector2Int from, Vector2Int to);
|
void SetLastSwap(Vector2Int from, Vector2Int to);
|
||||||
|
|
||||||
void DetectBombSpawnFromLastSwap(List<Gem> currentMatches);
|
/// <summary>
|
||||||
|
/// Try to spawn a bomb at the last swap location.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="currentMatches">
|
||||||
|
/// List of current matches.
|
||||||
|
/// </param>
|
||||||
|
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);
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a List of bombs that we will detonate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="protectedPositions">
|
||||||
|
/// Protected positions, bombs that we don't want to destroy.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="bombCandidates">
|
||||||
|
/// Possible bombs.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// List of Bombs.
|
||||||
|
/// </returns>
|
||||||
|
UniTask<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, HashSet<Vector2Int> bombCandidates);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detonate the bomb(s) part of the match. If there are other bombs within the radius, they will be detonated too sequentially.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="initialBombs">
|
||||||
|
/// List of bombs to detonate.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="destroyAtAsync">
|
||||||
|
/// Destroy function reference.
|
||||||
|
/// </param>
|
||||||
UniTask DetonateChainAsync(
|
UniTask DetonateChainAsync(
|
||||||
IReadOnlyList<Vector2Int> initialBombs,
|
IReadOnlyList<Vector2Int> initialBombs,
|
||||||
Func<Vector2Int, UniTask> destroyAtAsync,
|
Func<Vector2Int, UniTask> destroyAtAsync);
|
||||||
IGameBoard gameBoard);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,6 @@ namespace Services.Interfaces {
|
|||||||
void Setup();
|
void Setup();
|
||||||
|
|
||||||
UniTask<bool> TrySwap(Vector2Int from, Vector2Int to);
|
UniTask<bool> TrySwap(Vector2Int from, Vector2Int to);
|
||||||
UniTask<bool> TrySwitch(Vector2Int position);
|
void TrySwitch(Vector2Int position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,12 +2,33 @@ using System.Collections.Generic;
|
|||||||
using Cysharp.Threading.Tasks;
|
using Cysharp.Threading.Tasks;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using Enums;
|
using Enums;
|
||||||
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);
|
/// <summary>
|
||||||
|
/// Get positions of all matches that are not protected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="protectedPositions">
|
||||||
|
/// Protected positions, bombs that we don't want to destroy.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// HashSet of unprotected matches.
|
||||||
|
/// </returns>
|
||||||
|
UniTask<HashSet<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if there are any matches at the given position and type.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="positionToCheck">
|
||||||
|
/// Position on the gameBoard to check.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="gemTypeToCheck">
|
||||||
|
/// Type of gem to check.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>
|
||||||
|
/// True if there are matches, false otherwise.
|
||||||
|
/// </returns>
|
||||||
bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck);
|
bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck);
|
||||||
void FindAllMatches();
|
void FindAllMatches();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace Services.Interfaces {
|
|||||||
public interface IScoreService {
|
public interface IScoreService {
|
||||||
event Action<int> OnScoreChanged;
|
event Action<int> OnScoreChanged;
|
||||||
int Score { get; }
|
int Score { get; }
|
||||||
void ScoreCheck(int value);
|
void AddScore(int value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
using Presenter;
|
|
||||||
using Services.Interfaces;
|
using Services.Interfaces;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using VContainer.Unity;
|
using VContainer.Unity;
|
||||||
using Views;
|
|
||||||
|
|
||||||
namespace Services
|
namespace Services
|
||||||
{
|
{
|
||||||
public class LevelEntryPoint : IStartable
|
public class LevelEntryPoint : IStartable
|
||||||
{
|
{
|
||||||
private readonly IObjectPool<GemView> gemViewPool;
|
|
||||||
private readonly IGameBoardService gameBoardService;
|
private readonly IGameBoardService gameBoardService;
|
||||||
private readonly IInputService inputService;
|
private readonly IInputService inputService;
|
||||||
private readonly AudioPresenter audioPresenter;
|
|
||||||
|
|
||||||
public LevelEntryPoint(IObjectPool<GemView> gemViewPool, IGameBoardService gameBoardService, IInputService inputService, AudioPresenter audioPresenter)
|
public LevelEntryPoint(IGameBoardService gameBoardService, IInputService inputService)
|
||||||
{
|
{
|
||||||
this.gemViewPool = gemViewPool;
|
|
||||||
this.gameBoardService = gameBoardService;
|
this.gameBoardService = gameBoardService;
|
||||||
this.inputService = inputService;
|
this.inputService = inputService;
|
||||||
this.audioPresenter = audioPresenter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ using Cysharp.Threading.Tasks;
|
|||||||
using Enums;
|
using Enums;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
using Services.Interfaces;
|
using Services.Interfaces;
|
||||||
using Structs;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace Services {
|
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;
|
||||||
@@ -50,11 +48,12 @@ namespace Services {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public UniTask<List<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions) {
|
public UniTask<HashSet<Vector2Int>> GetMatchPositionsAsync(List<Vector2Int> protectedPositions) {
|
||||||
List<Vector2Int> matchPositions = new List<Vector2Int>(CurrentMatches.Count);
|
HashSet<Vector2Int> matchPositions = new HashSet<Vector2Int>();
|
||||||
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 +69,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using Services.Interfaces;
|
using Services.Interfaces;
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
namespace Services {
|
namespace Services {
|
||||||
public class ScoreService : IScoreService {
|
public class ScoreService : IScoreService {
|
||||||
private int score = 0;
|
private int score;
|
||||||
public int Score => this.score;
|
public int Score => this.score;
|
||||||
public event Action<int> OnScoreChanged;
|
public event Action<int> OnScoreChanged;
|
||||||
public void ScoreCheck(int value) {
|
public void AddScore(int value) {
|
||||||
this.score += value;
|
this.score += value;
|
||||||
|
|
||||||
OnScoreChanged?.Invoke(this.score);
|
OnScoreChanged?.Invoke(this.score);
|
||||||
|
|||||||
10
Assets/Scripts/Structs/FallMove.cs
Normal file
10
Assets/Scripts/Structs/FallMove.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Services;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Structs {
|
||||||
|
public struct FallMove {
|
||||||
|
public Vector2Int from;
|
||||||
|
public Vector2Int to;
|
||||||
|
public Gem gem;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Scripts/Structs/FallMove.cs.meta
Normal file
3
Assets/Scripts/Structs/FallMove.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 420c7135e99442e280e7ac7439d5c702
|
||||||
|
timeCreated: 1765995012
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using Enums;
|
using Enums;
|
||||||
using Models.Interfaces;
|
using Models.Interfaces;
|
||||||
using Services;
|
|
||||||
using Structs;
|
using Structs;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
|
||||||
using Enums;
|
using Enums;
|
||||||
using Random = UnityEngine.Random;
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
namespace Utils {
|
namespace Utils {
|
||||||
public static class RandomUtils {
|
public static class RandomUtils {
|
||||||
public static int RandomGemTypeAsInt() {
|
private static readonly GemType[] SPAWNABLE_GEMS = 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 SPAWNABLE_GEMS[Random.Range(0, SPAWNABLE_GEMS.Length)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,11 +19,11 @@ namespace Views {
|
|||||||
|
|
||||||
private CancellationTokenSource spawnScaleCts;
|
private CancellationTokenSource spawnScaleCts;
|
||||||
|
|
||||||
public void Bind(Gem gem, GemTypeValues gemvalue, bool isBomb = false) {
|
public void Bind(Gem gem, GemTypeValues gemValue, bool isBomb = false) {
|
||||||
this.gem = gem;
|
this.gem = gem;
|
||||||
this.gameObject.SetActive(true);
|
this.gameObject.SetActive(true);
|
||||||
|
|
||||||
SetupGem(isBomb, gemvalue.gemSprite);
|
SetupGem(isBomb, gemValue.gemSprite);
|
||||||
PlaySpawnScale();
|
PlaySpawnScale();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
57
README.md
Normal file
57
README.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Unity Match-3: VContainer Edition
|
||||||
|
|
||||||
|
A modern Match-3 puzzle game built in Unity, focusing on clean architecture, dependency injection, and data-driven design. This project serves as a template for scalable mobile game development.
|
||||||
|
|
||||||
|
## 🏗 Architecture & Patterns
|
||||||
|
|
||||||
|
### Dependency Injection with VContainer
|
||||||
|
This project utilizes **VContainer** for high-performance dependency injection. By using a `LifetimeScope`, we decouple our game logic (Grid Management, Scoring, Input) from the Unity Lifecycle, making the codebase more modular and easier to test.
|
||||||
|
|
||||||
|
|
||||||
|
### Data-Driven Gameplay
|
||||||
|
All balancing and configuration parameters are centralized in a **ScriptableObject** called `GameVariables`. This allows designers to tweak the game feel in real-time without touching code.
|
||||||
|
|
||||||
|
**Adjustable Parameters include:**
|
||||||
|
* Debug Mode
|
||||||
|
* Tile prefabs
|
||||||
|
* Spawnable object
|
||||||
|
* Spawnable explosion prefab animation
|
||||||
|
* Sprite
|
||||||
|
* Score value
|
||||||
|
* Grid Dimensions
|
||||||
|
* Audio SFX
|
||||||
|
* Special Tiles
|
||||||
|
* Animation speed and delays
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎮 Features
|
||||||
|
|
||||||
|
* **Grid Logic:** Efficient recursive algorithms for detecting matches and handling "gravity" (falling tiles).
|
||||||
|
* **Decoupled Systems:** UI, Sound, and Gameplay systems communicate via injected interfaces.
|
||||||
|
* **Scriptable Configuration:** Easily swap between "Easy" and "Hard" mode by swapping the `GameVariables` asset.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Tech Stack
|
||||||
|
|
||||||
|
* **Engine:** Unity 2022.3+
|
||||||
|
* **DI Framework:** [VContainer](https://vcontainer.hadashikakeru.jp/)
|
||||||
|
* **UniTask:** C# (Task-based async for animations)
|
||||||
|
* **Data:** ScriptableObjects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
* Unity Hub & Unity 2022.3 LTS or newer.
|
||||||
|
* VContainer package.
|
||||||
|
* UniTask package.
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
1. Open the project in Unity.
|
||||||
|
2. Navigate to Assets/Scenes/ and open `Scene_Submission`
|
||||||
|
3. Locate `GameVariables` asset in `Assets` to modify gameplay values.
|
||||||
Reference in New Issue
Block a user