[Fix] ECS

This commit is contained in:
2026-06-07 01:12:10 +07:00
parent 285c11597a
commit 5a58c9031a
16 changed files with 225 additions and 36 deletions
+13 -2
View File
@@ -69,6 +69,7 @@ namespace Minesweeper.Commands
}
var state = gameStateService.Current;
var generatedOnThisCommand = false;
if (state != GameState.Preparing && state != GameState.Playing)
{
return;
@@ -85,6 +86,8 @@ namespace Minesweeper.Commands
{
return;
}
generatedOnThisCommand = true;
}
var result = boardService.OpenCell(command.X, command.Y);
@@ -106,7 +109,15 @@ namespace Minesweeper.Commands
gameStateService.SetState(GameState.Playing);
}
boardEcsSyncService.SyncBoard(boardService);
if (generatedOnThisCommand)
{
boardEcsSyncService.SyncBoard(boardService);
}
else
{
boardEcsSyncService.SyncCells(result.ChangedCells, boardService);
}
boardEcsSyncService.SyncGameState(gameStateService.Current, boardService.IsGenerated);
}
}
@@ -145,7 +156,7 @@ namespace Minesweeper.Commands
return;
}
boardEcsSyncService.SyncBoard(boardService);
boardEcsSyncService.SyncCells(result.ChangedCells, boardService);
boardEcsSyncService.SyncGameState(gameStateService.Current, boardService.IsGenerated);
}
}
+5 -1
View File
@@ -1,19 +1,23 @@
using System.Collections.Generic;
namespace Minesweeper.Core
{
public readonly struct BoardActionResult
{
public BoardActionResult(bool changed, bool hitMine, bool won, bool invalid)
public BoardActionResult(bool changed, bool hitMine, bool won, bool invalid, IReadOnlyList<BoardCellData> changedCells = null)
{
Changed = changed;
HitMine = hitMine;
Won = won;
Invalid = invalid;
ChangedCells = changedCells;
}
public bool Changed { get; }
public bool HitMine { get; }
public bool Won { get; }
public bool Invalid { get; }
public IReadOnlyList<BoardCellData> ChangedCells { get; }
public static BoardActionResult NoChange => new BoardActionResult(false, false, false, false);
public static BoardActionResult InvalidAction => new BoardActionResult(false, false, false, true);
+29 -4
View File
@@ -7,6 +7,7 @@ namespace Minesweeper.Core
{
private readonly IGameSettingsService settingsService;
private readonly Random random = new Random();
private readonly List<BoardCellData> changedCells = new List<BoardCellData>();
private CellData[,] cells;
public BoardService(IGameSettingsService settingsService)
@@ -19,7 +20,9 @@ namespace Minesweeper.Core
public int MinesCount { get; private set; }
public bool IsGenerated { get; private set; }
public int OpenedSafeCellsCount { get; private set; }
public int FlaggedCellsCount { get; private set; }
public int SafeCellsCount => Width * Height - MinesCount;
public IReadOnlyList<BoardCellData> LastChangedCells => changedCells;
public void InitializeEmptyBoard()
{
@@ -27,7 +30,9 @@ namespace Minesweeper.Core
Height = settingsService.SizeY;
MinesCount = Math.Min(settingsService.MinesCount, Width * Height - 1);
OpenedSafeCellsCount = 0;
FlaggedCellsCount = 0;
IsGenerated = false;
changedCells.Clear();
cells = new CellData[Width, Height];
for (var x = 0; x < Width; x++)
@@ -58,6 +63,7 @@ namespace Minesweeper.Core
public BoardActionResult OpenCell(int x, int y)
{
EnsureInitialized();
changedCells.Clear();
if (!IsGenerated || !IsInside(x, y))
{
@@ -73,7 +79,8 @@ namespace Minesweeper.Core
if (cell.IsMine)
{
cell.IsOpened = true;
return new BoardActionResult(true, true, false, false);
AddChangedCell(cell);
return new BoardActionResult(true, true, false, false, changedCells);
}
if (cell.NeighborMines == 0)
@@ -85,12 +92,13 @@ namespace Minesweeper.Core
OpenSafeCell(cell);
}
return new BoardActionResult(true, false, IsWin(), false);
return new BoardActionResult(true, false, IsWin(), false, changedCells);
}
public BoardActionResult ToggleFlag(int x, int y)
{
EnsureInitialized();
changedCells.Clear();
if (!IsInside(x, y))
{
@@ -103,8 +111,19 @@ namespace Minesweeper.Core
return BoardActionResult.NoChange;
}
cell.IsFlagged = !cell.IsFlagged;
return new BoardActionResult(true, false, false, false);
if (cell.IsFlagged)
{
cell.IsFlagged = false;
FlaggedCellsCount--;
}
else
{
cell.IsFlagged = true;
FlaggedCellsCount++;
}
AddChangedCell(cell);
return new BoardActionResult(true, false, false, false, changedCells);
}
public bool IsInside(int x, int y)
@@ -273,6 +292,12 @@ namespace Minesweeper.Core
cell.IsOpened = true;
OpenedSafeCellsCount++;
AddChangedCell(cell);
}
private void AddChangedCell(CellData cell)
{
changedCells.Add(ToData(cell));
}
private bool IsWin()
+2
View File
@@ -9,7 +9,9 @@ namespace Minesweeper.Core
int MinesCount { get; }
bool IsGenerated { get; }
int OpenedSafeCellsCount { get; }
int FlaggedCellsCount { get; }
int SafeCellsCount { get; }
IReadOnlyList<BoardCellData> LastChangedCells { get; }
void InitializeEmptyBoard();
bool GenerateAfterFirstClick(int safeX, int safeY);
+92 -11
View File
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Minesweeper.Core;
using Minesweeper.ECS.Components;
using Unity.Collections;
@@ -7,6 +8,10 @@ namespace Minesweeper.ECS
{
public sealed class BoardEcsSyncService : IBoardEcsSyncService
{
private readonly Dictionary<int, Entity> cellsByIndex = new Dictionary<int, Entity>();
private int syncedWidth;
private int syncedHeight;
public void ClearBoard()
{
if (!TryGetEntityManager(out var entityManager))
@@ -15,6 +20,9 @@ namespace Minesweeper.ECS
}
ClearCells(entityManager);
cellsByIndex.Clear();
syncedWidth = 0;
syncedHeight = 0;
}
public void SyncBoard(IBoardService boardService)
@@ -24,14 +32,27 @@ namespace Minesweeper.ECS
return;
}
ClearCells(entityManager);
if (syncedWidth != boardService.Width || syncedHeight != boardService.Height || cellsByIndex.Count != boardService.Width * boardService.Height)
{
ClearCells(entityManager);
cellsByIndex.Clear();
syncedWidth = boardService.Width;
syncedHeight = boardService.Height;
}
else
{
ClearChangedTags(entityManager);
}
var boardEntity = GetOrCreateSingleton<BoardConfigComponent>(entityManager);
entityManager.SetComponentData(boardEntity, new BoardConfigComponent
{
Width = boardService.Width,
Height = boardService.Height,
MinesCount = boardService.MinesCount
MinesCount = boardService.MinesCount,
OpenedSafeCellsCount = boardService.OpenedSafeCellsCount,
FlaggedCellsCount = boardService.FlaggedCellsCount,
IsGenerated = ToByte(boardService.IsGenerated)
});
var archetype = entityManager.CreateArchetype(typeof(CellComponent));
@@ -39,16 +60,45 @@ namespace Minesweeper.ECS
for (var i = 0; i < cells.Count; i++)
{
var cell = cells[i];
var entity = entityManager.CreateEntity(archetype);
entityManager.SetComponentData(entity, new CellComponent
var index = ToIndex(cell.X, cell.Y, boardService.Width);
if (!cellsByIndex.TryGetValue(index, out var entity) || !entityManager.Exists(entity))
{
X = cell.X,
Y = cell.Y,
IsMine = ToByte(cell.IsMine),
IsOpened = ToByte(cell.IsOpened),
IsFlagged = ToByte(cell.IsFlagged),
NeighborMines = cell.NeighborMines
});
entity = entityManager.CreateEntity(archetype);
cellsByIndex[index] = entity;
}
SetCell(entityManager, entity, cell, boardService.Width, false);
}
}
public void SyncCells(IReadOnlyList<BoardCellData> cells, IBoardService boardService)
{
if (cells == null || cells.Count == 0 || !TryGetEntityManager(out var entityManager))
{
return;
}
var boardEntity = GetOrCreateSingleton<BoardConfigComponent>(entityManager);
entityManager.SetComponentData(boardEntity, new BoardConfigComponent
{
Width = boardService.Width,
Height = boardService.Height,
MinesCount = boardService.MinesCount,
OpenedSafeCellsCount = boardService.OpenedSafeCellsCount,
FlaggedCellsCount = boardService.FlaggedCellsCount,
IsGenerated = ToByte(boardService.IsGenerated)
});
ClearChangedTags(entityManager);
for (var i = 0; i < cells.Count; i++)
{
var cell = cells[i];
var index = ToIndex(cell.X, cell.Y, boardService.Width);
if (cellsByIndex.TryGetValue(index, out var entity) && entityManager.Exists(entity))
{
SetCell(entityManager, entity, cell, boardService.Width, true);
}
}
}
@@ -87,6 +137,37 @@ namespace Minesweeper.ECS
query.Dispose();
}
private static void ClearChangedTags(EntityManager entityManager)
{
var query = entityManager.CreateEntityQuery(typeof(CellChangedTag));
entityManager.RemoveComponent<CellChangedTag>(query);
query.Dispose();
}
private static void SetCell(EntityManager entityManager, Entity entity, BoardCellData cell, int width, bool markChanged)
{
entityManager.SetComponentData(entity, new CellComponent
{
X = cell.X,
Y = cell.Y,
Index = ToIndex(cell.X, cell.Y, width),
IsMine = ToByte(cell.IsMine),
IsOpened = ToByte(cell.IsOpened),
IsFlagged = ToByte(cell.IsFlagged),
NeighborMines = cell.NeighborMines
});
if (markChanged && !entityManager.HasComponent<CellChangedTag>(entity))
{
entityManager.AddComponent<CellChangedTag>(entity);
}
}
private static int ToIndex(int x, int y, int width)
{
return y * width + x;
}
private static Entity GetOrCreateSingleton<T>(EntityManager entityManager) where T : unmanaged, IComponentData
{
var query = entityManager.CreateEntityQuery(typeof(T));
@@ -7,5 +7,8 @@ namespace Minesweeper.ECS.Components
public int Width;
public int Height;
public int MinesCount;
public int OpenedSafeCellsCount;
public int FlaggedCellsCount;
public byte IsGenerated;
}
}
@@ -0,0 +1,8 @@
using Unity.Entities;
namespace Minesweeper.ECS.Components
{
public struct CellChangedTag : IComponentData
{
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5702c8f40c71e7444a70e7aa73d8a9db
@@ -6,9 +6,11 @@ namespace Minesweeper.ECS.Components
{
public int X;
public int Y;
public int Index;
public byte IsMine;
public byte IsOpened;
public byte IsFlagged;
public int NeighborMines;
}
}
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Minesweeper.Core;
namespace Minesweeper.ECS
@@ -6,6 +7,7 @@ namespace Minesweeper.ECS
{
void ClearBoard();
void SyncBoard(IBoardService boardService);
void SyncCells(IReadOnlyList<BoardCellData> cells, IBoardService boardService);
void SyncGameState(GameState state, bool hasFirstClick);
}
}
@@ -70,7 +70,7 @@ namespace Minesweeper.Presentation.Presenters
{
topPanelPresenter.SetCellPressActive(false);
commandDispatcher.Dispatch(new OpenCellCommand(x, y));
RefreshBoard();
RefreshChangedCellsOrBoard();
topPanelPresenter.RefreshCounters();
UpdateBoardInput();
}
@@ -78,7 +78,7 @@ namespace Minesweeper.Presentation.Presenters
private void OnCellFlagRequested(int x, int y)
{
commandDispatcher.Dispatch(new ToggleFlagCommand(x, y));
RefreshBoard();
RefreshChangedCellsOrBoard();
topPanelPresenter.RefreshCounters();
UpdateBoardInput();
}
@@ -156,6 +156,17 @@ namespace Minesweeper.Presentation.Presenters
boardView.Refresh(readModel.GetCells(), IsFinalState());
}
private void RefreshChangedCellsOrBoard()
{
if (IsFinalState())
{
RefreshBoard();
return;
}
boardView.RefreshCells(readModel.GetChangedCells(), false);
}
private bool IsFinalState()
{
var state = gameStateService.Current;
@@ -20,7 +20,7 @@ namespace Minesweeper.Presentation.ReadModels
public int Width => boardService.Width > 0 ? boardService.Width : settingsService.SizeX;
public int Height => boardService.Height > 0 ? boardService.Height : settingsService.SizeY;
public int MinesCount => boardService.MinesCount > 0 ? boardService.MinesCount : settingsService.MinesCount;
public int FlaggedCellsCount => CountFlaggedCells();
public int FlaggedCellsCount => boardService.FlaggedCellsCount;
public int RemainingMinesCount => MinesCount - FlaggedCellsCount;
public bool TryGetCell(int x, int y, out BoardCellData cell)
@@ -33,19 +33,10 @@ namespace Minesweeper.Presentation.ReadModels
return boardService.GetCells();
}
private int CountFlaggedCells()
public IReadOnlyList<BoardCellData> GetChangedCells()
{
var cells = boardService.GetCells();
var count = 0;
for (var i = 0; i < cells.Count; i++)
{
if (cells[i].IsFlagged)
{
count++;
}
}
return count;
return boardService.LastChangedCells;
}
}
}
@@ -14,5 +14,6 @@ namespace Minesweeper.Presentation.ReadModels
bool TryGetCell(int x, int y, out BoardCellData cell);
IReadOnlyList<BoardCellData> GetCells();
IReadOnlyList<BoardCellData> GetChangedCells();
}
}
+47 -3
View File
@@ -25,6 +25,7 @@ namespace Minesweeper.Presentation.Views
private readonly Dictionary<int, CellView> cellsByCoordinate = new Dictionary<int, CellView>();
private readonly Stack<CellView> pooledCells = new Stack<CellView>();
private IReadOnlyList<BoardCellData> currentCells;
private readonly List<BoardCellData> currentCellsCache = new List<BoardCellData>();
private bool inputEnabled = true;
private bool currentRevealUnflaggedMines;
private bool resizeRefreshPending;
@@ -82,7 +83,7 @@ namespace Minesweeper.Presentation.Views
Clear();
currentBoardWidth = width;
currentBoardHeight = height;
currentCells = cells;
CacheCurrentCells(cells);
currentRevealUnflaggedMines = revealUnflaggedMines;
var layoutWasEnabled = gridLayoutGroup != null && gridLayoutGroup.enabled;
SetGridLayoutEnabled(false);
@@ -109,12 +110,23 @@ namespace Minesweeper.Presentation.Views
public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
{
currentCells = cells;
CacheCurrentCells(cells);
currentRevealUnflaggedMines = revealUnflaggedMines;
RefreshCells(cells, revealUnflaggedMines);
}
public void RefreshCells(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
{
if (cells == null)
{
return;
}
for (var i = 0; i < cells.Count; i++)
{
var cell = cells[i];
UpdateCachedCell(cell);
if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
{
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
@@ -250,12 +262,19 @@ namespace Minesweeper.Presentation.Views
{
view = pooledCells.Pop();
view.transform.SetParent(gridLayoutGroup.transform, false);
view.transform.SetAsLastSibling();
view.Initialize(cell.X, cell.Y);
view.gameObject.SetActive(true);
return view;
}
return cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
if (view != null)
{
view.transform.SetAsLastSibling();
}
return view;
}
private void Clear()
@@ -273,6 +292,31 @@ namespace Minesweeper.Presentation.Views
}
cellsByCoordinate.Clear();
currentCellsCache.Clear();
currentCells = null;
}
private void CacheCurrentCells(IReadOnlyList<BoardCellData> cells)
{
currentCellsCache.Clear();
if (cells != null)
{
for (var i = 0; i < cells.Count; i++)
{
currentCellsCache.Add(cells[i]);
}
}
currentCells = currentCellsCache;
}
private void UpdateCachedCell(BoardCellData cell)
{
var index = cell.Y * currentBoardWidth + cell.X;
if (index >= 0 && index < currentCellsCache.Count)
{
currentCellsCache[index] = cell;
}
}
private void PoolCell(CellView cell)
@@ -17,6 +17,7 @@ namespace Minesweeper.Presentation.Views
void Hide();
void Rebuild(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines);
void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines);
void RefreshCells(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines);
void SetInputEnabled(bool enabled);
}
}
@@ -16,6 +16,7 @@ namespace Minesweeper.Presentation.Views
public void Hide() { }
public void Rebuild(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines) { }
public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines) { }
public void RefreshCells(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines) { }
public void SetInputEnabled(bool enabled) { }
}
}