Files
match3-unity/Assets/Scripts/Services/GameBoardService.cs

379 lines
14 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Cysharp.Threading.Tasks;
using Enums;
using Models.Interfaces;
using Presenter;
using ScriptableObjects;
using Services.Interfaces;
using Structs;
using UnityEngine;
using UnityEngine.UIElements;
using Utils;
using VContainer.Unity;
using Views;
using Object = UnityEngine.Object;
namespace Services {
public class GameBoardService : IGameBoardService, ITickable, IDisposable {
#region Inject
private readonly GameVariables gameVariables;
private readonly IGameBoard gameBoard;
private readonly IObjectPool<GemView> objectPool;
private readonly IMatchService matchService;
private readonly IBombService bombService;
private readonly IScoreService scoreService;
private readonly ScorePresenter scorePresenter;
private readonly AudioPresenter audioPresenter;
private readonly Transform gemsHolder;
private readonly Transform backgroundHolder;
#endregion
#region Variables
private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
private GameState currentState = GameState.Setup;
#endregion
public GameBoardService(
GameVariables gameVariables,
IGameBoard gameBoard,
IObjectPool<GemView> objectPool,
IMatchService matchService,
IBombService bombService,
IScoreService scoreService,
ScorePresenter scorePresenter,
AudioPresenter audioPresenter,
Transform gemsHolder,
Transform backgroundHolder) {
this.gameVariables = gameVariables;
this.gameBoard = gameBoard;
this.objectPool = objectPool;
this.matchService = matchService;
this.bombService = bombService;
this.scoreService = scoreService;
this.scorePresenter = scorePresenter;
this.audioPresenter = audioPresenter;
this.gemsHolder = gemsHolder;
this.backgroundHolder = backgroundHolder;
}
public void Tick() {
foreach (GemPresenter gemPresenter in gemPresenters) {
gemPresenter.Tick(this.gameVariables.gemSpeed);
}
this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
}
//Instantiates background tiles and calls SpawnGems
//Uses MatchService.MatchesAt to avoid matching Gems
public void Setup() {
List<Gem> gemsToSpawn = new List<Gem>();
for (int x = 0; x < this.gameBoard.Width; x++)
for (int y = 0; y < this.gameBoard.Height; y++)
{
Vector2 position = new Vector2(x, y);
SpawnBackgroundTile(position);
int iterations = 0;
int gemToUse = -1;
do {
gemToUse = RandomUtils.RandomGemTypeAsInt();
iterations++;
} while (this.matchService.MatchesAt(position.ToVector2Int(), (GemType)gemToUse) && iterations < 100);
gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), (GemType)gemToUse));
}
SpawnCascade(gemsToSpawn);
}
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);
this.gameBoard.SetGemAt(position, gem);
return gem;
}
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;
GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs);
gemView.Bind(gem, gemValue, isBomb: isBomb);
this.gemPresenters.Add(new GemPresenter(gem, gemView));
}
private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
if(isBomb)
ReleaseMatchedGems(position);
SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
}
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
public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
if (this.currentState != GameState.Move)
return false;
if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard))
return false;
if (!from.IsAdjacent(to))
return false;
this.currentState = GameState.Wait;
ApplySwap(from, to);
await UniTask.Delay(600);
this.bombService.SetLastSwap(from, to);
this.matchService.FindAllMatches();
this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
if (this.matchService.CurrentMatches.Count == 0) {
ApplySwap(to, from);
await UniTask.Delay(600);
this.currentState = GameState.Move;
return false;
}
List<Vector2Int> protectedPositions = this.bombService.ApplyPendingBombSpawns(SetAndSpawnGem);
await DestroyMatchesAsync(protectedPositions);
this.currentState = GameState.Move;
return true;
}
public async UniTask<bool> TrySwitch(Vector2Int position) {
Gem gem = this.gameBoard.GetGemAt(position);
if(gem == null)
return false;
GemType[] normalTypes = Enum.GetValues(typeof(GemType))
.Cast<GemType>()
.Where(t => t != GemType.Bomb)
.ToArray();
if (normalTypes.Length == 0)
return false;
bool nextIsBomb;
GemType nextTypeOrMatchColor;
if (gem.Type != GemType.Bomb) {
int index = Array.IndexOf(normalTypes, gem.Type);
if (index < 0) index = 0;
if (index < normalTypes.Length - 1) {
nextIsBomb = false;
nextTypeOrMatchColor = normalTypes[index + 1];
} else {
nextIsBomb = true;
nextTypeOrMatchColor = normalTypes[0];
}
} else {
int idx = Array.IndexOf(normalTypes, gem.MatchColor);
if (idx < 0) idx = 0;
if (idx < normalTypes.Length - 1) {
nextIsBomb = true;
nextTypeOrMatchColor = normalTypes[idx + 1];
} else {
nextIsBomb = false;
nextTypeOrMatchColor = normalTypes[0];
}
}
// Replace both model+view by releasing current and respawning at same position.
ReleaseMatchedGems(position);
SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
return true;
}
private void ApplySwap(Vector2Int from, Vector2Int to) {
Gem fromGem = this.gameBoard.GetGemAt(from);
Gem toGem = this.gameBoard.GetGemAt(to);
// swap their stored positions
fromGem.SetPosition(to);
toGem.SetPosition(from);
// update grid
this.gameBoard.SetGemAt(from, toGem);
this.gameBoard.SetGemAt(to, fromGem);
}
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
List<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions.Distinct().ToList());
// 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);
await UniTask.Delay(600);
await MoveGemsDown();
return;
}
// For audio SFX
bool willBreakAnyNonBombGem = matchPositions.Select(pos => this.gameBoard.GetGemAt(pos)).Where(gem => gem != null).Any(gem => gem.Type != GemType.Bomb);
if (willBreakAnyNonBombGem)
this.audioPresenter.OnMatch(this.gameVariables.matchSfx);
// For score counting
foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
Gem gem = this.gameBoard.GetGemAt(pos);
if (gem == null) continue;
if (gem.Type == GemType.Bomb) continue;
this.scoreService.ScoreCheck(gem.ScoreValue);
ReleaseMatchedGems(pos);
}
await UniTask.Delay(250);
await MoveGemsDown();
}
private UniTask DestroyAtAsync(Vector2Int pos) {
Gem gem = this.gameBoard.GetGemAt(pos);
if (gem == null)
return UniTask.CompletedTask;
if (gem.Type == GemType.Bomb)
this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);
this.scoreService.ScoreCheck(gem.ScoreValue);
ReleaseMatchedGems(pos);
return UniTask.CompletedTask;
}
private void ReleaseMatchedGems(Vector2Int position) {
List<GemView> gemsViews = this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
Gem currentGem = this.gameBoard.GetGemAt(position);
if (currentGem != null)
{
GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem);
if (gemView is null) {
return;
}
this.objectPool.Release(gemView);
RemovePresenterFor(gemView);
this.gameBoard.SetGemAt(position, null);
}
}
private async UniTask MoveGemsDown() {
int nullCounter = 0;
for (int x = 0; x < this.gameBoard.Width; x++)
{
for (int y = 0; y < this.gameBoard.Height; y++)
{
Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x, y));
if (currentGem == null)
{
nullCounter++;
}
else if (nullCounter > 0)
{
currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter));
this.gameBoard.SetGemAt(currentGem.Position, currentGem);
this.gameBoard.SetGemAt(new Vector2Int(x,y), null);
}
}
nullCounter = 0;
}
await UniTask.Delay(600);
await FillBoard();
}
private async UniTask FillBoard() {
await RefillBoard();
this.matchService.FindAllMatches();
if (this.matchService.CurrentMatches.Count > 0) {
await UniTask.Delay(600);
// In cascades, there is no "creating slot" bomb protection.
await DestroyMatchesAsync(new List<Vector2Int>());
} else {
this.currentState = GameState.Move;
}
}
private async UniTask RefillBoard() {
List<Gem> gemsToSpawn = new List<Gem>();
for (int x = 0; x < this.gameBoard.Width; x++)
{
for (int y = 0; y < this.gameBoard.Height; y++)
{
Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y));
if (currentGem == null) {
int gemToUse = RandomUtils.RandomGemTypeAsInt();
int iterations = 0;
while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100)
{
gemToUse = RandomUtils.RandomGemTypeAsInt();
iterations++;
}
gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), (GemType)gemToUse));
}
}
}
await SpawnCascade(gemsToSpawn);
}
private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
{
List<IGrouping<int, Gem>> groups = gemsToSpawn
.GroupBy(gem => gem.Position.y)
.OrderBy(group => group.Key)
.ToList();
for (int i = 0; i < groups.Count; i++)
{
foreach (Gem gem in groups[i])
SpawnGemGameObject(gem);
if (i < groups.Count - 1)
await UniTask.Delay(150);
}
if(this.currentState == GameState.Setup)
this.currentState = GameState.Move;
}
private void RemovePresenterFor(GemView gemView) {
if (gemView is null) {
return;
}
GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
this.gemPresenters.Remove(presenter);
}
public void Dispose() {
this.objectPool.Clear();
}
}
}