333 lines
11 KiB
C#
333 lines
11 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Minesweeper.Config;
|
|
using Minesweeper.Core;
|
|
using Minesweeper.Presentation.Factories;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Minesweeper.Presentation.Views
|
|
{
|
|
public sealed class BoardView : MonoBehaviour, IBoardView
|
|
{
|
|
private const float ResizeRefreshDelaySeconds = 0.5f;
|
|
private const float ResizeSizeEpsilon = 0.5f;
|
|
private const float MaximumCellPixelsPerUnitMultiplier = 1f;
|
|
private const float ContentPaddingReferenceCellSize = 202f;
|
|
private const float ContentPaddingReferencePadding = 15f;
|
|
private const float MinimumContentPadding = 1f;
|
|
|
|
[SerializeField] private GameObject root;
|
|
[SerializeField] private RectTransform boardPanel;
|
|
[SerializeField] private GridLayoutGroup gridLayoutGroup;
|
|
[SerializeField] private MinesweeperUiConfig uiConfig;
|
|
|
|
private readonly Dictionary<int, CellView> cellsByCoordinate = new Dictionary<int, CellView>();
|
|
private readonly Stack<CellView> pooledCells = new Stack<CellView>();
|
|
private IReadOnlyList<BoardCellData> currentCells;
|
|
private bool inputEnabled = true;
|
|
private bool currentRevealUnflaggedMines;
|
|
private bool resizeRefreshPending;
|
|
private int currentBoardWidth;
|
|
private int currentBoardHeight;
|
|
private float currentContentPadding = MinimumContentPadding;
|
|
private float currentPixelsPerUnitMultiplier = 1f;
|
|
private float resizeStableAt;
|
|
private Vector2 lastObservedLayoutSize;
|
|
|
|
private GameObject Root => root != null ? root : gameObject;
|
|
|
|
public event Action CellPressStarted;
|
|
public event Action CellPressEnded;
|
|
public event Action PauseRequested;
|
|
public event Action<int, int> CellOpenRequested;
|
|
public event Action<int, int> CellFlagRequested;
|
|
|
|
private void Awake()
|
|
{
|
|
if (root == null)
|
|
{
|
|
root = gameObject;
|
|
}
|
|
|
|
if (boardPanel == null)
|
|
{
|
|
boardPanel = transform as RectTransform;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
TrackResize();
|
|
}
|
|
|
|
public void BindConfig(MinesweeperUiConfig config)
|
|
{
|
|
uiConfig = config;
|
|
}
|
|
|
|
public void Show()
|
|
{
|
|
Root.SetActive(true);
|
|
ResetResizeTracking();
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
Root.SetActive(false);
|
|
}
|
|
|
|
public void Rebuild(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines)
|
|
{
|
|
Clear();
|
|
currentBoardWidth = width;
|
|
currentBoardHeight = height;
|
|
currentCells = cells;
|
|
currentRevealUnflaggedMines = revealUnflaggedMines;
|
|
var layoutWasEnabled = gridLayoutGroup != null && gridLayoutGroup.enabled;
|
|
SetGridLayoutEnabled(false);
|
|
|
|
try
|
|
{
|
|
ConfigureGrid(width, height);
|
|
|
|
for (var i = 0; i < cells.Count; i++)
|
|
{
|
|
var cell = cells[i];
|
|
var view = CreateCell(cell, cellViewFactory);
|
|
if (view != null)
|
|
{
|
|
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
SetGridLayoutEnabled(layoutWasEnabled);
|
|
}
|
|
}
|
|
|
|
public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
|
|
{
|
|
currentCells = cells;
|
|
currentRevealUnflaggedMines = revealUnflaggedMines;
|
|
|
|
for (var i = 0; i < cells.Count; i++)
|
|
{
|
|
var cell = cells[i];
|
|
if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
|
|
{
|
|
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetInputEnabled(bool enabled)
|
|
{
|
|
inputEnabled = enabled;
|
|
foreach (var cell in cellsByCoordinate.Values)
|
|
{
|
|
cell.SetInputEnabled(enabled);
|
|
}
|
|
}
|
|
|
|
private void TrackResize()
|
|
{
|
|
if (boardPanel == null || !boardPanel.gameObject.activeInHierarchy || currentCells == null || currentBoardWidth <= 0 || currentBoardHeight <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var layoutSize = GetLayoutSourceSize();
|
|
if (layoutSize.x <= 0f || layoutSize.y <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (HasSizeChanged(layoutSize, lastObservedLayoutSize))
|
|
{
|
|
lastObservedLayoutSize = layoutSize;
|
|
resizeStableAt = Time.unscaledTime + ResizeRefreshDelaySeconds;
|
|
resizeRefreshPending = true;
|
|
return;
|
|
}
|
|
|
|
if (resizeRefreshPending && Time.unscaledTime >= resizeStableAt)
|
|
{
|
|
resizeRefreshPending = false;
|
|
RefreshLayout();
|
|
}
|
|
}
|
|
|
|
private void RefreshLayout()
|
|
{
|
|
ConfigureGrid(currentBoardWidth, currentBoardHeight);
|
|
Refresh(currentCells, currentRevealUnflaggedMines);
|
|
}
|
|
|
|
private void ConfigureGrid(int width, int height)
|
|
{
|
|
if (gridLayoutGroup == null || boardPanel == null || uiConfig == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
var layoutSize = GetLayoutSourceSize();
|
|
var panelWidth = layoutSize.x > 0f ? layoutSize.x : uiConfig.ReferenceCellSize * width;
|
|
var panelHeight = layoutSize.y > 0f ? layoutSize.y : uiConfig.ReferenceCellSize * height;
|
|
var cellSize = CalculateCellSize(panelWidth, panelHeight, width, height);
|
|
var padding = cellSize * uiConfig.BoardPaddingRatio;
|
|
var spacing = cellSize * uiConfig.GridSpacingRatio;
|
|
|
|
boardPanel.anchorMin = Vector2.zero;
|
|
boardPanel.anchorMax = Vector2.one;
|
|
boardPanel.offsetMin = new Vector2(padding, padding);
|
|
boardPanel.offsetMax = new Vector2(-padding, -padding);
|
|
|
|
gridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
|
gridLayoutGroup.constraintCount = width;
|
|
gridLayoutGroup.padding = new RectOffset();
|
|
gridLayoutGroup.spacing = new Vector2(spacing, spacing);
|
|
gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize);
|
|
currentPixelsPerUnitMultiplier = Mathf.Min(MaximumCellPixelsPerUnitMultiplier, uiConfig.ReferenceCellSize / cellSize);
|
|
currentContentPadding = CalculateContentPadding(cellSize);
|
|
lastObservedLayoutSize = layoutSize;
|
|
}
|
|
|
|
private Vector2 GetLayoutSourceSize()
|
|
{
|
|
var parentRect = boardPanel.parent as RectTransform;
|
|
var rect = parentRect != null ? parentRect.rect : boardPanel.rect;
|
|
return rect.size;
|
|
}
|
|
|
|
private void ResetResizeTracking()
|
|
{
|
|
resizeRefreshPending = false;
|
|
if (boardPanel != null)
|
|
{
|
|
lastObservedLayoutSize = GetLayoutSourceSize();
|
|
}
|
|
}
|
|
|
|
private float CalculateCellSize(float panelWidth, float panelHeight, int width, int height)
|
|
{
|
|
var widthUnits = width + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, width - 1) * uiConfig.GridSpacingRatio;
|
|
var heightUnits = height + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, height - 1) * uiConfig.GridSpacingRatio;
|
|
var cellByWidth = panelWidth / widthUnits;
|
|
var cellByHeight = panelHeight / heightUnits;
|
|
return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight)));
|
|
}
|
|
|
|
private CellView CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
|
|
{
|
|
if (gridLayoutGroup == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var view = GetOrCreateCell(cell, cellViewFactory);
|
|
if (view == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
view.SetInputEnabled(inputEnabled);
|
|
view.OpenRequested += OnCellOpenRequested;
|
|
view.FlagRequested += OnCellFlagRequested;
|
|
view.PressStarted += OnCellPressStarted;
|
|
view.PressEnded += OnCellPressEnded;
|
|
cellsByCoordinate[ToKey(cell.X, cell.Y)] = view;
|
|
return view;
|
|
}
|
|
|
|
private CellView GetOrCreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
|
|
{
|
|
CellView view;
|
|
if (pooledCells.Count > 0)
|
|
{
|
|
view = pooledCells.Pop();
|
|
view.transform.SetParent(gridLayoutGroup.transform, false);
|
|
view.Initialize(cell.X, cell.Y);
|
|
view.gameObject.SetActive(true);
|
|
return view;
|
|
}
|
|
|
|
return cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
|
|
}
|
|
|
|
private void Clear()
|
|
{
|
|
foreach (var cell in cellsByCoordinate.Values)
|
|
{
|
|
if (cell != null)
|
|
{
|
|
cell.OpenRequested -= OnCellOpenRequested;
|
|
cell.FlagRequested -= OnCellFlagRequested;
|
|
cell.PressStarted -= OnCellPressStarted;
|
|
cell.PressEnded -= OnCellPressEnded;
|
|
PoolCell(cell);
|
|
}
|
|
}
|
|
|
|
cellsByCoordinate.Clear();
|
|
}
|
|
|
|
private void PoolCell(CellView cell)
|
|
{
|
|
cell.gameObject.SetActive(false);
|
|
pooledCells.Push(cell);
|
|
}
|
|
|
|
private void SetGridLayoutEnabled(bool enabled)
|
|
{
|
|
if (gridLayoutGroup != null)
|
|
{
|
|
gridLayoutGroup.enabled = enabled;
|
|
}
|
|
}
|
|
|
|
private void OnCellOpenRequested(int x, int y)
|
|
{
|
|
CellOpenRequested?.Invoke(x, y);
|
|
}
|
|
|
|
private void OnCellFlagRequested(int x, int y)
|
|
{
|
|
CellFlagRequested?.Invoke(x, y);
|
|
}
|
|
|
|
private void OnCellPressStarted()
|
|
{
|
|
CellPressStarted?.Invoke();
|
|
}
|
|
|
|
private void OnCellPressEnded()
|
|
{
|
|
CellPressEnded?.Invoke();
|
|
}
|
|
|
|
private void OnPauseClicked()
|
|
{
|
|
PauseRequested?.Invoke();
|
|
}
|
|
|
|
private static float CalculateContentPadding(float cellSize)
|
|
{
|
|
return Mathf.Max(MinimumContentPadding, cellSize * ContentPaddingReferencePadding / ContentPaddingReferenceCellSize);
|
|
}
|
|
|
|
private static bool HasSizeChanged(Vector2 current, Vector2 previous)
|
|
{
|
|
return Mathf.Abs(current.x - previous.x) > ResizeSizeEpsilon || Mathf.Abs(current.y - previous.y) > ResizeSizeEpsilon;
|
|
}
|
|
|
|
private static int ToKey(int x, int y)
|
|
{
|
|
return (y << 16) ^ x;
|
|
}
|
|
}
|
|
}
|