MiniGame 之 扫雷实现

本文是 扫雷(MiniGame) 的一个实现样例(使用 Unity/C#),主要以代码为主,辅以一点简单的注解

实现

样例中的扫雷实现主要是两个类型(BombGame 和 BombGrid),下面是完整代码:

// desc bomb game implementation
// maintainer hugoyuusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public enum BombGridState
{// unfixed statesUnFlag = 0,Flag,// fixed statesEmpty,Number,Bomb,
}public enum BombGameResult
{Gaming,Success,Failed,
}public class BombGridData
{public bool isBomb;public BombGridState state;public BombGrid grid;public BombGridData(bool isBomb_, BombGridState state_, BombGrid grid_){isBomb = isBomb_;state = state_;grid = grid_;}
}public class BombGame : MonoBehaviour
{[SerializeField]int m_rowCount = 5;[SerializeField]int m_colCount = 5;[SerializeField]int m_bombCount = 5;[SerializeField]float m_gridWidth = 40;[SerializeField]float m_gridHeight = 40;[SerializeField]bool m_allowGridPropagation = true;[SerializeField]GameObject m_bombGridPrefab;[SerializeField]GameObject m_successButton;[SerializeField]GameObject m_failedButton;List<BombGridData> m_bombGridDatas = new List<BombGridData>();void Awake(){Debug.Assert(m_bombGridPrefab);}void Start(){InitGame();}#region logic for bomb game createpublic void InitGame(){CreateGrids();InitBombs();CheckGame();}void CreateGrids(){// clear old gridsfor (int i = 0; i < m_bombGridDatas.Count; ++i){if (m_bombGridDatas[i].grid){Destroy(m_bombGridDatas[i].grid);}}m_bombGridDatas.Clear();// create new gridsfor (int i = 0; i < m_rowCount; ++i){for (int j = 0; j < m_colCount; ++j){var gridGO = Instantiate(m_bombGridPrefab, new Vector3(j * m_gridWidth, -i * m_gridHeight, 0), Quaternion.identity);var index = i * m_colCount + j;gridGO.name = "Grid" + index.ToString();gridGO.transform.SetParent(transform, false);Debug.Assert(gridGO);var grid = gridGO.GetComponent<BombGrid>();Debug.Assert(grid);grid.SetData(this, index);grid.SetState(BombGridState.UnFlag);m_bombGridDatas.Add(new BombGridData(false, BombGridState.UnFlag, grid));}}// adjust grids position by tweak parent positiontransform.localPosition = new Vector3(-0.5f * (m_colCount - 1) * m_gridWidth, 0.5f * (m_rowCount - 1) * m_gridHeight);}void InitBombs(){var bombLeft = m_bombCount;while (bombLeft > 0){int randIndex = Random.Range(0, m_bombGridDatas.Count);if (!m_bombGridDatas[randIndex].isBomb){m_bombGridDatas[randIndex].isBomb = true;--bombLeft;}}}#endregion#region logic for check game resultBombGameResult GetGameResult(){// check fail firstfor (int i = 0; i < m_bombGridDatas.Count; ++i){if (m_bombGridDatas[i].state == BombGridState.Bomb){return BombGameResult.Failed;}}// check gaming thenfor (int i = 0; i < m_bombGridDatas.Count; ++i){if (m_bombGridDatas[i].state == BombGridState.UnFlag){return BombGameResult.Gaming;}if (!m_bombGridDatas[i].isBomb && m_bombGridDatas[i].state == BombGridState.Flag){return BombGameResult.Gaming;}}// successreturn BombGameResult.Success;}void CheckGame(){var gameResult = GetGameResult();switch (gameResult){case BombGameResult.Success:m_successButton.gameObject.SetActive(true);m_failedButton.gameObject.SetActive(false);break;case BombGameResult.Failed:m_successButton.gameObject.SetActive(false);m_failedButton.gameObject.SetActive(true);break;default:m_successButton.gameObject.SetActive(false);m_failedButton.gameObject.SetActive(false);break;}}#endregion#region logic for get grid around bomb numint CheckGridInternal(int bombGridRow, int bombGridCol){if (bombGridRow >= 0 && bombGridRow < m_rowCount &&bombGridCol >= 0 && bombGridCol < m_colCount){int bombGridIndex = bombGridRow * m_colCount + bombGridCol;return m_bombGridDatas[bombGridIndex].isBomb ? 1 : 0;}return 0;}int CheckGrid(int bombGridIndex){int num = 0;int row = bombGridIndex / m_colCount;int col = bombGridIndex % m_colCount;num += CheckGridInternal(row - 1, col - 1);num += CheckGridInternal(row - 1, col);num += CheckGridInternal(row - 1, col + 1);num += CheckGridInternal(row, col - 1);num += CheckGridInternal(row, col + 1);num += CheckGridInternal(row + 1, col - 1);num += CheckGridInternal(row + 1, col);num += CheckGridInternal(row + 1, col + 1);return num;}#endregion#region logic for grid operationsvoid PropagationGridRecur(int row, int col){int bombGridIndex = row * m_colCount + col;if (bombGridIndex >= 0 && bombGridIndex < m_bombGridDatas.Count){var data = m_bombGridDatas[bombGridIndex];if (data.state == BombGridState.UnFlag){if (!data.isBomb){var num = CheckGrid(bombGridIndex);data.state = num > 0 ? BombGridState.Number : BombGridState.Empty;Debug.Assert(data.grid);data.grid.SetState(data.state, num);if (num <= 0){// when num <= 0, keep propagationPropagationGridRecur(row - 1, col - 1);PropagationGridRecur(row - 1, col);PropagationGridRecur(row - 1, col + 1);PropagationGridRecur(row, col - 1);PropagationGridRecur(row, col + 1);PropagationGridRecur(row + 1, col - 1);PropagationGridRecur(row + 1, col);PropagationGridRecur(row + 1, col + 1);}}}}}void PropagationGrid(int bombGridIndex){if (m_allowGridPropagation){int row = bombGridIndex / m_colCount;int col = bombGridIndex % m_colCount;PropagationGridRecur(row - 1, col - 1);PropagationGridRecur(row - 1, col);PropagationGridRecur(row - 1, col + 1);PropagationGridRecur(row, col - 1);PropagationGridRecur(row, col + 1);PropagationGridRecur(row + 1, col - 1);PropagationGridRecur(row + 1, col);PropagationGridRecur(row + 1, col + 1);}}public void ClickGrid(int bombGridIndex){if (bombGridIndex >= 0 && bombGridIndex < m_bombGridDatas.Count){var data = m_bombGridDatas[bombGridIndex];if (data.state == BombGridState.UnFlag || data.state == BombGridState.Flag){if (data.isBomb){data.state = BombGridState.Bomb;Debug.Assert(data.grid);data.grid.SetState(data.state);}else{var num = CheckGrid(bombGridIndex);data.state = num > 0 ? BombGridState.Number : BombGridState.Empty;Debug.Assert(data.grid);data.grid.SetState(data.state, num);if (num <= 0){// when num <= 0, propagationPropagationGrid(bombGridIndex);}}CheckGame();}}}public void FlagGrid(int bombGridIndex){if (bombGridIndex >= 0 && bombGridIndex < m_bombGridDatas.Count){var data = m_bombGridDatas[bombGridIndex];if (data.state == BombGridState.UnFlag || data.state == BombGridState.Flag){if (data.state == BombGridState.UnFlag){data.state = BombGridState.Flag;Debug.Assert(data.grid);data.grid.SetState(data.state);}else if (data.state == BombGridState.Flag){data.state = BombGridState.UnFlag;Debug.Assert(data.grid);data.grid.SetState(data.state);}CheckGame();}}}#endregion
}
// desc bomb grid implementation
// maintainer hugoyuusing System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;public class BombGrid : MonoBehaviour
{#region display related data[SerializeField]Text m_text;[SerializeField]Image m_img;[SerializeField]Button m_btn;#endregion#region game related dataBombGame m_owner;int m_index = -1;#endregionvoid Awake(){Debug.Assert(m_text && m_img && m_btn);// we use editor to assign callback now}#region logic for set grid datapublic void SetData(BombGame owner, int index){m_owner = owner;m_index = index;}#endregion#region logic for set grid statepublic void SetState(BombGridState state, int param = 0){switch (state){case BombGridState.UnFlag:{m_text.text = "";m_img.gameObject.SetActive(false);var colors = m_btn.colors;colors.normalColor = Color.white;colors.highlightedColor = Color.white;m_btn.colors = colors;}break;case BombGridState.Flag:{m_text.text = "";m_img.gameObject.SetActive(true);var colors = m_btn.colors;colors.normalColor = Color.white;colors.highlightedColor = Color.white;m_btn.colors = colors;}break;case BombGridState.Empty:{m_text.text = "";m_img.gameObject.SetActive(false);var colors = m_btn.colors;colors.normalColor = Color.green;colors.highlightedColor = Color.green;m_btn.colors = colors;}break;case BombGridState.Number:{m_text.text = param.ToString();m_img.gameObject.SetActive(false);var colors = m_btn.colors;colors.normalColor = Color.white;colors.highlightedColor = Color.white;m_btn.colors = colors;}break;case BombGridState.Bomb:{m_text.text = "";m_img.gameObject.SetActive(false);var colors = m_btn.colors;colors.normalColor = Color.black;colors.highlightedColor = Color.black;m_btn.colors = colors;}break;}}#endregion#region logic for grid operationpublic void OnPointerClick(BaseEventData pointerData){var pointerClickData = pointerData as PointerEventData;if (pointerClickData != null){if (pointerClickData.pointerId == -1){// left clickif (m_owner){m_owner.ClickGrid(m_index);}}else if (pointerClickData.pointerId == -2){// right clickif (m_owner){m_owner.FlagGrid(m_index);}}}}#endregion
}
注解
  • BombGame 实现游戏的主体逻辑, BombGrid 实现扫雷的格子表现和操作
  • 在一般的程序开发中(不仅仅是游戏开发),逻辑与表现的分离是一种较好的开发原则(MVC 模式是一种相关的体现),如果以上面的代码为例来说的话, BombGrid 的实现应该尽量不要涉及扫雷的实际游戏逻辑(理想情况下应该都由 BombGame 来负责实现)
  • 样例代码中出于简明的原因并未做进一步的抽象,实际开发中我们可以通过接口,基类等方式做进一步的代码解耦
  • BombGame 使用了一维数组存储游戏数据,实际而言是有些反直觉的(同时代码中也涉及了一些相关处理),更符合思维的一种方式是使用多维数组
  • BombGame 中随机布雷的逻辑实际并不能做到雷的均匀分布,这里有编码上的权衡(获得均匀分布的收益和实现均匀分布的代价)