Files
FreewayGamesTest/Assets/Runtime/Presentation/Views/BoardView.cs
T

298 lines
9.9 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 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;
ConfigureGrid(width, height);
for (var i = 0; i < cells.Count; i++)
{
CreateCell(cells[i], cellViewFactory);
}
Refresh(cells, revealUnflaggedMines);
}
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 void CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
{
if (gridLayoutGroup == null)
{
return;
}
var view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
if (view == null)
{
return;
}
view.SetInputEnabled(inputEnabled);
view.OpenRequested += OnCellOpenRequested;
view.FlagRequested += OnCellFlagRequested;
view.PressStarted += OnCellPressStarted;
view.PressEnded += OnCellPressEnded;
cellsByCoordinate[ToKey(cell.X, cell.Y)] = view;
}
private void Clear()
{
foreach (var cell in cellsByCoordinate.Values)
{
if (cell != null)
{
cell.OpenRequested -= OnCellOpenRequested;
cell.FlagRequested -= OnCellFlagRequested;
cell.PressStarted -= OnCellPressStarted;
cell.PressEnded -= OnCellPressEnded;
}
}
cellsByCoordinate.Clear();
if (gridLayoutGroup == null)
{
return;
}
for (var i = gridLayoutGroup.transform.childCount - 1; i >= 0; i--)
{
Destroy(gridLayoutGroup.transform.GetChild(i).gameObject);
}
}
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;
}
}
}