[Fix] Board Optimization, Fix Screen Loading

This commit is contained in:
2026-06-07 00:57:20 +07:00
parent e487795e6f
commit 285c11597a
9 changed files with 117 additions and 94 deletions
+4 -4
View File
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: b4e8d5c36f36bb443b640a85df3e7077, type: 3} m_Script: {fileID: 11500000, guid: b4e8d5c36f36bb443b640a85df3e7077, type: 3}
m_Name: MinesweeperGameConfig m_Name: MinesweeperGameConfig
m_EditorClassIdentifier: Assembly-CSharp::Minesweeper.Config.MinesweeperGameConfig m_EditorClassIdentifier: Assembly-CSharp::Minesweeper.Config.MinesweeperGameConfig
<MinSizeX>k__BackingField: 2 <MinSizeX>k__BackingField: 3
<MaxSizeX>k__BackingField: 50 <MaxSizeX>k__BackingField: 30
<MinSizeY>k__BackingField: 2 <MinSizeY>k__BackingField: 3
<MaxSizeY>k__BackingField: 50 <MaxSizeY>k__BackingField: 30
+4 -5
View File
@@ -629,13 +629,12 @@ MonoBehaviour:
autoInjectGameObjects: [] autoInjectGameObjects: []
gameConfig: {fileID: 11400000, guid: 4c24a7c2a548eff4fb21fa4a4bf3e741, type: 2} gameConfig: {fileID: 11400000, guid: 4c24a7c2a548eff4fb21fa4a4bf3e741, type: 2}
uiConfig: {fileID: 11400000, guid: c8b7785713c7c8b49b853f7e5028a4fa, type: 2} uiConfig: {fileID: 11400000, guid: c8b7785713c7c8b49b853f7e5028a4fa, type: 2}
screenCatalog:
<MainMenuPanelPrefab>k__BackingField: {fileID: 7682962739562644362, guid: 66407cd7142d6a945b37ca8dc5e7c6b7, type: 3}
<BoardGridPrefab>k__BackingField: {fileID: 463985621338212375, guid: 91c5885a4fbe47540abf4bfd814a32d0, type: 3}
<PausePanelPrefab>k__BackingField: {fileID: 2814582388565546678, guid: ec358f6fec19e3b469f516bd1ade70cd, type: 3}
<ResultPanelPrefab>k__BackingField: {fileID: 6869455415096409219, guid: 73d5a09dc8885e64a8c20a68ea82c5dc, type: 3}
contentRoot: {fileID: 1373940536} contentRoot: {fileID: 1373940536}
topPanelView: {fileID: 1101876296} topPanelView: {fileID: 1101876296}
mainMenuViewPrefab: {fileID: 0}
boardViewPrefab: {fileID: 0}
pauseViewPrefab: {fileID: 0}
resultViewPrefab: {fileID: 0}
--- !u!4 &1287266282 --- !u!4 &1287266282
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -1,16 +0,0 @@
using System;
using UnityEngine;
namespace Minesweeper.Config
{
[Serializable]
public sealed class MinesweeperScreenCatalog
{
[field: SerializeField] public GameObject MainMenuPanelPrefab { get; private set; }
[field: SerializeField] public GameObject BoardGridPrefab { get; private set; }
[field: SerializeField] public GameObject PausePanelPrefab { get; private set; }
[field: SerializeField] public GameObject ResultPanelPrefab { get; private set; }
public bool IsValid => MainMenuPanelPrefab != null && BoardGridPrefab != null && PausePanelPrefab != null && ResultPanelPrefab != null;
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6d0009ea7e70ee548a09cd95d482ec83
-8
View File
@@ -1,8 +0,0 @@
fileFormatVersion: 2
guid: 210d08f5d5948674fa0df51ed6a785b0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -17,9 +17,12 @@ namespace Minesweeper.Infrastructure
{ {
[SerializeField] private MinesweeperGameConfig gameConfig; [SerializeField] private MinesweeperGameConfig gameConfig;
[SerializeField] private MinesweeperUiConfig uiConfig; [SerializeField] private MinesweeperUiConfig uiConfig;
[SerializeField] private MinesweeperScreenCatalog screenCatalog = new MinesweeperScreenCatalog();
[SerializeField] private Transform contentRoot; [SerializeField] private Transform contentRoot;
[SerializeField] private TopPanelView topPanelView; [SerializeField] private TopPanelView topPanelView;
[SerializeField] private MainMenuView mainMenuViewPrefab;
[SerializeField] private BoardView boardViewPrefab;
[SerializeField] private PauseView pauseViewPrefab;
[SerializeField] private ResultView resultViewPrefab;
protected override void Configure(IContainerBuilder builder) protected override void Configure(IContainerBuilder builder)
{ {
@@ -124,12 +127,12 @@ namespace Minesweeper.Infrastructure
private MinesweeperScreenRefs SpawnScreens() private MinesweeperScreenRefs SpawnScreens()
{ {
if (contentRoot == null || screenCatalog == null || !screenCatalog.IsValid) return new MinesweeperScreenBootstrapper().Spawn(
{ contentRoot,
return default; mainMenuViewPrefab,
} boardViewPrefab,
pauseViewPrefab,
return new MinesweeperScreenBootstrapper().Spawn(contentRoot, screenCatalog); resultViewPrefab);
} }
} }
} }
@@ -1,4 +1,3 @@
using Minesweeper.Config;
using Minesweeper.Presentation.Views; using Minesweeper.Presentation.Views;
using UnityEngine; using UnityEngine;
@@ -11,7 +10,12 @@ namespace Minesweeper.Presentation.Factories
private const string PausePanelName = "PausePanel"; private const string PausePanelName = "PausePanel";
private const string ResultPanelName = "ResultPanel"; private const string ResultPanelName = "ResultPanel";
public MinesweeperScreenRefs Spawn(Transform contentRoot, MinesweeperScreenCatalog catalog) public MinesweeperScreenRefs Spawn(
Transform contentRoot,
MainMenuView mainMenuPrefab,
BoardView boardPrefab,
PauseView pausePrefab,
ResultView resultPrefab)
{ {
if (contentRoot == null) if (contentRoot == null)
{ {
@@ -19,47 +23,42 @@ namespace Minesweeper.Presentation.Factories
return default; return default;
} }
if (catalog == null || !catalog.IsValid) if (mainMenuPrefab == null || boardPrefab == null || pausePrefab == null || resultPrefab == null)
{ {
Debug.LogError("Minesweeper screen bootstrap failed: screen catalog prefab references are incomplete."); Debug.LogError("Minesweeper screen bootstrap failed: screen prefab references are incomplete.");
return default; return default;
} }
ClearContent(contentRoot); ClearContent(contentRoot);
var mainMenu = SpawnScreen(catalog.MainMenuPanelPrefab, contentRoot, MainMenuPanelName, 0); var mainMenuView = SpawnScreen(mainMenuPrefab, contentRoot, MainMenuPanelName, 0);
var board = SpawnScreen(catalog.BoardGridPrefab, contentRoot, BoardGridName, 1); var boardView = SpawnScreen(boardPrefab, contentRoot, BoardGridName, 1);
var pause = SpawnScreen(catalog.PausePanelPrefab, contentRoot, PausePanelName, 2); var pauseView = SpawnScreen(pausePrefab, contentRoot, PausePanelName, 2);
var result = SpawnScreen(catalog.ResultPanelPrefab, contentRoot, ResultPanelName, 3); var resultView = SpawnScreen(resultPrefab, contentRoot, ResultPanelName, 3);
var mainMenuView = RequireComponent<MainMenuView>(mainMenu.transform, MainMenuPanelName);
if (mainMenuView != null) if (mainMenuView != null)
{ {
mainMenuView.BindRoot(mainMenu); mainMenuView.BindRoot(mainMenuView.gameObject);
} }
var boardView = RequireComponent<BoardView>(board.transform, BoardGridName);
var pauseView = RequireComponent<PauseView>(pause.transform, PausePanelName);
var resultView = RequireComponent<ResultView>(result.transform, ResultPanelName);
var refs = new MinesweeperScreenRefs( var refs = new MinesweeperScreenRefs(
mainMenuView, mainMenuView,
boardView, boardView,
pauseView, pauseView,
resultView); resultView);
mainMenu.SetActive(false); mainMenuView.gameObject.SetActive(false);
board.SetActive(false); boardView.gameObject.SetActive(false);
pause.SetActive(false); pauseView.gameObject.SetActive(false);
result.SetActive(false); resultView.gameObject.SetActive(false);
return refs; return refs;
} }
private static GameObject SpawnScreen(GameObject prefab, Transform parent, string expectedName, int siblingIndex) private static T SpawnScreen<T>(T prefab, Transform parent, string expectedName, int siblingIndex) where T : Component
{ {
var instance = Object.Instantiate(prefab, parent, false); var instance = Object.Instantiate(prefab, parent, false);
instance.name = expectedName; instance.gameObject.name = expectedName;
instance.transform.SetSiblingIndex(siblingIndex); instance.transform.SetSiblingIndex(siblingIndex);
Stretch(instance.GetComponent<RectTransform>()); Stretch(instance.GetComponent<RectTransform>());
return instance; return instance;
@@ -75,17 +74,6 @@ namespace Minesweeper.Presentation.Factories
} }
} }
private static T RequireComponent<T>(Transform root, string ownerName) where T : Component
{
var component = root.GetComponent<T>();
if (component == null)
{
Debug.LogError($"Minesweeper screen bootstrap failed: '{ownerName}' is missing {typeof(T).Name}.");
}
return component;
}
private static void Stretch(RectTransform rectTransform) private static void Stretch(RectTransform rectTransform)
{ {
if (rectTransform == null) if (rectTransform == null)
+48 -13
View File
@@ -23,6 +23,7 @@ namespace Minesweeper.Presentation.Views
[SerializeField] private MinesweeperUiConfig uiConfig; [SerializeField] private MinesweeperUiConfig uiConfig;
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 IReadOnlyList<BoardCellData> currentCells; private IReadOnlyList<BoardCellData> currentCells;
private bool inputEnabled = true; private bool inputEnabled = true;
private bool currentRevealUnflaggedMines; private bool currentRevealUnflaggedMines;
@@ -83,14 +84,27 @@ namespace Minesweeper.Presentation.Views
currentBoardHeight = height; currentBoardHeight = height;
currentCells = cells; currentCells = cells;
currentRevealUnflaggedMines = revealUnflaggedMines; currentRevealUnflaggedMines = revealUnflaggedMines;
var layoutWasEnabled = gridLayoutGroup != null && gridLayoutGroup.enabled;
SetGridLayoutEnabled(false);
try
{
ConfigureGrid(width, height); ConfigureGrid(width, height);
for (var i = 0; i < cells.Count; i++) for (var i = 0; i < cells.Count; i++)
{ {
CreateCell(cells[i], cellViewFactory); var cell = cells[i];
var view = CreateCell(cell, cellViewFactory);
if (view != null)
{
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, currentContentPadding, revealUnflaggedMines);
}
}
}
finally
{
SetGridLayoutEnabled(layoutWasEnabled);
} }
Refresh(cells, revealUnflaggedMines);
} }
public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines) public void Refresh(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
@@ -207,17 +221,17 @@ namespace Minesweeper.Presentation.Views
return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight))); return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight)));
} }
private void CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory) private CellView CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
{ {
if (gridLayoutGroup == null) if (gridLayoutGroup == null)
{ {
return; return null;
} }
var view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform); var view = GetOrCreateCell(cell, cellViewFactory);
if (view == null) if (view == null)
{ {
return; return null;
} }
view.SetInputEnabled(inputEnabled); view.SetInputEnabled(inputEnabled);
@@ -226,6 +240,22 @@ namespace Minesweeper.Presentation.Views
view.PressStarted += OnCellPressStarted; view.PressStarted += OnCellPressStarted;
view.PressEnded += OnCellPressEnded; view.PressEnded += OnCellPressEnded;
cellsByCoordinate[ToKey(cell.X, cell.Y)] = view; 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() private void Clear()
@@ -238,19 +268,24 @@ namespace Minesweeper.Presentation.Views
cell.FlagRequested -= OnCellFlagRequested; cell.FlagRequested -= OnCellFlagRequested;
cell.PressStarted -= OnCellPressStarted; cell.PressStarted -= OnCellPressStarted;
cell.PressEnded -= OnCellPressEnded; cell.PressEnded -= OnCellPressEnded;
PoolCell(cell);
} }
} }
cellsByCoordinate.Clear(); cellsByCoordinate.Clear();
if (gridLayoutGroup == null)
{
return;
} }
for (var i = gridLayoutGroup.transform.childCount - 1; i >= 0; i--) private void PoolCell(CellView cell)
{ {
Destroy(gridLayoutGroup.transform.GetChild(i).gameObject); cell.gameObject.SetActive(false);
pooledCells.Push(cell);
}
private void SetGridLayoutEnabled(bool enabled)
{
if (gridLayoutGroup != null)
{
gridLayoutGroup.enabled = enabled;
} }
} }
+30 -6
View File
@@ -39,6 +39,7 @@ namespace Minesweeper.Presentation.Views
{ {
this.x = x; this.x = x;
this.y = y; this.y = y;
gameObject.name = $"bt_{x}_{y}";
} }
public void SetInputEnabled(bool enabled) public void SetInputEnabled(bool enabled)
@@ -52,7 +53,6 @@ namespace Minesweeper.Presentation.Views
public void Render(BoardCellData cell, MinesweeperUiConfig config, float pixelsPerUnitMultiplier, float contentPadding, bool revealUnflaggedMines) public void Render(BoardCellData cell, MinesweeperUiConfig config, float pixelsPerUnitMultiplier, float contentPadding, bool revealUnflaggedMines)
{ {
gameObject.name = $"bt_{cell.X}_{cell.Y}_{cell.DisplayValue}";
isOpened = cell.IsOpened; isOpened = cell.IsOpened;
var revealMine = revealUnflaggedMines && cell.IsMine && !cell.IsFlagged; var revealMine = revealUnflaggedMines && cell.IsMine && !cell.IsFlagged;
@@ -90,14 +90,14 @@ namespace Minesweeper.Presentation.Views
if (contentImage != null) if (contentImage != null)
{ {
contentImage.gameObject.SetActive(showFlag || (showMine && !showMineAsText)); SetActiveIfChanged(contentImage.gameObject, showFlag || (showMine && !showMineAsText));
if (showFlag) if (showFlag)
{ {
contentImage.sprite = config.FlagSprite; SetSpriteIfChanged(contentImage, config.FlagSprite);
} }
else if (showMine) else if (showMine)
{ {
contentImage.sprite = config.MineSprite; SetSpriteIfChanged(contentImage, config.MineSprite);
} }
contentImage.color = Color.white; contentImage.color = Color.white;
@@ -105,12 +105,36 @@ namespace Minesweeper.Presentation.Views
if (label != null) if (label != null)
{ {
label.gameObject.SetActive(showNumber || showMineAsText); SetActiveIfChanged(label.gameObject, showNumber || showMineAsText);
label.text = showMineAsText ? "M" : showNumber ? cell.NeighborMines.ToString() : string.Empty; SetTextIfChanged(label, showMineAsText ? "M" : showNumber ? cell.NeighborMines.ToString() : string.Empty);
label.color = config.GetNumberTextColor(cell.NeighborMines); label.color = config.GetNumberTextColor(cell.NeighborMines);
} }
} }
private static void SetActiveIfChanged(GameObject target, bool active)
{
if (target.activeSelf != active)
{
target.SetActive(active);
}
}
private static void SetSpriteIfChanged(Image image, Sprite sprite)
{
if (image.sprite != sprite)
{
image.sprite = sprite;
}
}
private static void SetTextIfChanged(TMP_Text text, string value)
{
if (text.text != value)
{
text.text = value;
}
}
public void OnPointerClick(PointerEventData eventData) public void OnPointerClick(PointerEventData eventData)
{ {
if (!inputEnabled) if (!inputEnabled)