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