一、游戏规则
- 游戏面板上有一定数量(偶数个)的方块,每个方块都有一个特定的图标或文字符号。
- 游戏开始时,所有方块都是背面朝上隐藏的。
- 玩家需要点击两个方块来翻开它们。如果这两个方块的标记相同,则这两个方块会保持翻 开状态;否则,在短暂展示后自动翻回来。
- 当所有方块都被成功匹配后,游戏结束。
二、游戏功能
- 游戏面板的动态创建和布局,可手动设置行列;
- 方块的翻转动画效果;
- 方块匹配逻辑的实现;
- 游戏结束的判定和处理;
- 可以根据需要进行进一步的功能扩展。
- 游戏包含一个主页面和游戏页面,可从首页点击开始游戏和点击游戏页按钮返回首页
- 进行数据持久化,并在游戏首页显示所记录的游戏次数、上次游戏成绩和一个数量上限为 10的历史成绩排行榜
三、实现结果
四、实现过程
4.1 方块基类
这个类中用协程实现了方块翻转动画的方法。
public class Tile : MonoBehaviour
{
private GameObject front; // 正面,显示图标
private GameObject back; // 背面,隐藏图标
private Sprite icon; // 图标
[SerializeField]
private bool isFlipped = false; // 标识方块是否翻转
private void Awake()
{
front = transform.Find("Front").gameObject;
back = transform.Find("Back").gameObject;
}
/// <summary>
/// 设置图标
/// </summary>
/// <param name="newIcon"></param>
public void SetIcon(Sprite newIcon)
{
icon = newIcon;
front.GetComponent<Image>().sprite = icon;
}
/// <summary>
/// 获取图标
/// </summary>
/// <returns></returns>
public Sprite GetIcon()
{
return icon;
}
/// <summary>
/// 初始化方块
/// </summary>
public void OnInit()
{
isFlipped = false;
front.SetActive(isFlipped);
back.SetActive(!isFlipped);
transform.rotation = Quaternion.Euler(0, 0, 0);
}
/// <summary>
/// 翻转方块
/// </summary>
public void Flip()
{
if(!isFlipped) StartCoroutine(FlipAnimation());
else StartCoroutine(FlipBackAnimation());
}
/// <summary>
/// 翻转动画,从背面翻转到正面
/// </summary>
/// <returns></returns>
private IEnumerator FlipAnimation()
{
float duration = 0.5f; // 动画持续时间
float elapsed = 0f; // 经过时间
// 翻转动画过程
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float angle = Mathf.Lerp(0, 90, elapsed / duration);
transform.rotation = Quaternion.Euler(0, angle, 0);
yield return null;
}
// 更新翻转状态
isFlipped = !isFlipped;
front.SetActive(isFlipped);
back.SetActive(!isFlipped);
// 继续翻转动画过程
elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float angle = Mathf.Lerp(90, 180, elapsed / duration);
transform.rotation = Quaternion.Euler(0, angle, 0);
yield return null;
}
}
/// <summary>
/// 翻转动画,从正面翻转到背面
/// </summary>
/// <returns></returns>
private IEnumerator FlipBackAnimation()
{
float duration = 0.5f; // 动画持续时间
float elapsed = 0f; // 经过时间
// 翻转动画过程
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float angle = Mathf.Lerp(180, 90, elapsed / duration);
transform.rotation = Quaternion.Euler(0, angle, 0);
yield return null;
}
// 更新翻转状态
isFlipped = !isFlipped;
front.SetActive(isFlipped);
back.SetActive(!isFlipped);
// 继续翻转动画过程
elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float angle = Mathf.Lerp(90, 0, elapsed / duration);
transform.rotation = Quaternion.Euler(0, angle, 0);
yield return null;
}
}
/// <summary>
/// 判断方块是否被翻转
/// </summary>
/// <returns></returns>
public bool IsFlipped()
{
return isFlipped;
}
}
4.2 面板基类
这个类为抽象类,每次创建新面板实例时,绑定组件和注册事件。每次调用展示面板方法时,进行初始化。
/// <summary>
/// 面板基类
/// </summary>
public abstract class PanelBase : MonoBehaviour
{
/// <summary>
/// 初始化面板
/// </summary>
public abstract void OnInit(params object[] args);
/// <summary>
/// 绑定面板组件
/// </summary>
public abstract void BindComponent();
/// <summary>
/// 注册事件
/// </summary>
public abstract void AddEvent();
/// <summary>
/// 显示面板
/// </summary>
public virtual void OnShow(params object[] args)
{
this.gameObject.SetActive(true);
OnInit(args);
}
/// <summary>
/// 隐藏面板
/// </summary>
public virtual void OnHide()
{
this.gameObject.SetActive(false);
}
public virtual void Start()
{
BindComponent();
AddEvent();
OnShow();
}
}
4.3 面板管理类
这个类为单例模式,全局唯一管理已经创建的面板,使用了对象池,在关闭界面时隐藏界面的实例,而不是删除被GC回收,避免频繁创建和删除实例。
/// <summary>
/// 面板管理类
/// </summary>
public class PanelManager
{
private Dictionary<string, PanelBase> panels = new Dictionary<string, PanelBase>();
private Transform canvas = GameObject.Find("Canvas").transform;
private static PanelManager _instance;
public static PanelManager Instance
{
get
{
if (_instance == null)
{
_instance = new PanelManager();
}
return _instance;
}
}
/// <summary>
/// 增加面板
/// </summary>
/// <typeparam name="T"></typeparam>
public void AddPanel<T>(params object[] args) where T : PanelBase
{
string name = typeof(T).Name;
if (panels.ContainsKey(name))
{
panels[name].OnShow(args);
}
else
{
GameObject gameObject = Resources.Load<GameObject>("Prefabs/" + name);
GameObject panel = GameObject.Instantiate(gameObject, canvas);
PanelBase panelBase = panel.AddComponent<T>();
panels.Add(typeof(T).Name, panelBase);
}
}
/// <summary>
/// 移除面板
/// </summary>
/// <typeparam name="T"></typeparam>
public void RemovePanel<T>() where T : PanelBase
{
string name = typeof(T).Name;
if (panels.ContainsKey(name))
{
panels[name].OnHide();
}
}
}
4.4 主菜单界面类
继承面板基类,每次打开初始化游戏记录,有开始游戏和退出游戏两个按钮。
/// <summary>
/// 主菜单
/// </summary>
public class MainMenu : PanelBase
{
private Button beginBtn;
private Button exitBtn;
private Text scoreText;
private Text countText;
private Text rankText;
public override void BindComponent()
{
beginBtn = transform.Find("Bg/BeginBtn").GetComponent<Button>();
exitBtn = transform.Find("Bg/ExitBtn").GetComponent<Button>();
scoreText = transform.Find("Bg/ScoreText").GetComponent<Text>();
countText = transform.Find("Bg/CountText").GetComponent<Text>();
rankText = transform.Find("Bg/RankText").GetComponent<Text>();
}
public override void AddEvent()
{
beginBtn.onClick.AddListener(() =>
{
PanelManager.Instance.RemovePanel<MainMenu>();
PanelManager.Instance.AddPanel<GamePanel>();
});
exitBtn.onClick.AddListener(() =>
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
});
}
public override void OnInit(params object[] args)
{
scoreText.text = "上次游玩分数:" + PlayerPrefs.GetInt("Score", 0).ToString();
countText.text = "游玩次数:" + PlayerPrefs.GetInt("Count", 0).ToString();
rankText.text = "";
List<Record> records = JsonConvert.DeserializeObject<List<Record>>(PlayerPrefs.GetString("Records"));
if (records != null)
{
for(int i = 0; i < 10; i++)
{
if (i >= records.Count) break;
rankText.text += records[i].ToString() + "\n";
}
}
}
}
4.5 游戏界面类
继承面板基类,管理整个小游戏的进程,可在调用打开这个面板时设置四个参数,行数,列数,翻牌加分,错误扣分。使用了PlayerPrefs和Newtonsoft.Json进行数据持久化。
public class GamePanel : PanelBase
{
private Button backBtn;
private GameObject tilePrefab;
private Transform group;
[SerializeField]
private int rows = 2;
[SerializeField]
private int columns = 6;
private Sprite[] icons;
private int iconNum = 10;
private List<Tile> tiles = new List<Tile>();
private Tile firstSelectedTile;
private Tile secondSelectedTile;
private int score = 0;
private int sucScore = 10;
private int failScore = -3;
private Text scoreText;
public override void BindComponent()
{
backBtn = transform.Find("Bg/BackBtn").GetComponent<Button>();
tilePrefab = Resources.Load<GameObject>("Prefabs/Tile");
group = transform.Find("Bg/Group").GetComponent<Transform>();
scoreText = transform.Find("Bg/ScoreText").GetComponent<Text>();
icons = new Sprite[iconNum];
for (int i = 0; i < iconNum; i++)
icons[i] = Resources.Load<Sprite>("Sprites/Tile" + (i + 1));
}
public override void AddEvent()
{
backBtn.onClick.AddListener(()=>
{
RecordScore();
PanelManager.Instance.AddPanel<MainMenu>();
PanelManager.Instance.RemovePanel<GamePanel>();
});
}
public override void OnInit(params object[] args)
{
if(args.Length >= 2)
{
rows = (int)args[0];
columns = (int)args[1];
if(args.Length >= 4)
{
sucScore = (int)args[2];
failScore = (int)args[3];
}
}
SetScore(-score);
if (rows * columns / 2 > iconNum)
{
Debug.LogError("图标数量不足");
OnHide();
PanelManager.Instance.AddPanel<MainMenu>();
return;
}
if (rows * columns % 2 != 0)
{
Debug.LogError("方块数量必须为偶数");
OnHide();
PanelManager.Instance.AddPanel<MainMenu>();
return;
}
SetGrid();
CreateGameBoard();
AssignIcons();
firstSelectedTile = null;
secondSelectedTile = null;
}
/// <summary>
/// 设置排列
/// </summary>
private void SetGrid()
{
GridLayoutGroup g = group.GetComponent<GridLayoutGroup>();
g.constraint = GridLayoutGroup.Constraint.FixedRowCount;
g.constraintCount = rows;
}
/// <summary>
/// 创建多个方块
/// </summary>
private void CreateGameBoard()
{
if(tiles.Count == 0)
{
for (int i = 0; i < rows * columns; i++)
{
CreateTile();
}
}
else
{
//如果要创建的方块数量大于当前方块数量,则需要创建新的方块
if(rows * columns > tiles.Count)
{
for(int i = tiles.Count; i < rows * columns; i++)
{
CreateTile();
}
}
else
{
//如果创建的方块数量小于当前方块数量,则需要令多余的方块隐藏
for(int i = tiles.Count - 1; i >= rows * columns; i--)
{
tiles[i].gameObject.SetActive(false);
}
}
for(int i = 0; i < rows * columns; i++)
{
tiles[i].gameObject.SetActive(true);
tiles[i].OnInit();
}
}
}
/// <summary>
/// 创建方块
/// </summary>
private void CreateTile()
{
GameObject tileObject = Instantiate(tilePrefab, group);
Tile tile = tileObject.AddComponent<Tile>();
tile.GetComponent<Button>().onClick.AddListener(() => OnTileClicked(tile));
tiles.Add(tile);
}
/// <summary>
/// 分配方块图标
/// </summary>
private void AssignIcons()
{
List<Sprite> iconsList = new List<Sprite>();
for (int i = 0;i<rows*columns/2;i++)
{
iconsList.Add(icons[i]);
iconsList.Add(icons[i]);
}
ListRandom<Sprite>(iconsList);
for (int i = 0; i < rows * columns; i++)
{
tiles[i].SetIcon(iconsList[i]);
}
}
/// <summary>
/// 点击方块
/// </summary>
/// <param name="clickedTile"></param>
private void OnTileClicked(Tile clickedTile)
{
if (firstSelectedTile == null)
{
firstSelectedTile = clickedTile;
firstSelectedTile.Flip();
}
else if (secondSelectedTile == null && clickedTile != firstSelectedTile)
{
secondSelectedTile = clickedTile;
secondSelectedTile.Flip();
StartCoroutine(CheckMatch());
}
}
/// <summary>
/// 检查匹配
/// </summary>
/// <returns></returns>
private IEnumerator CheckMatch()
{
yield return new WaitForSeconds(1f);
if (firstSelectedTile.GetIcon() == secondSelectedTile.GetIcon())
{
SetScore(sucScore);
firstSelectedTile = null;
secondSelectedTile = null;
}
else
{
SetScore(failScore);
firstSelectedTile.Flip();
secondSelectedTile.Flip();
firstSelectedTile = null;
secondSelectedTile = null;
}
if (CheckGameEnd())
{
RecordScore();
PanelManager.Instance.AddPanel<WinPanel>();
OnHide();
}
}
/// <summary>
/// 判断游戏是否结束
/// </summary>
/// <returns></returns>
private bool CheckGameEnd()
{
for(int i=0;i<rows*columns;i++)
{
if (!tiles[i].IsFlipped())
{
return false;
}
}
return true;
}
/// <summary>
/// 设置得分
/// </summary>
/// <param name="num"></param>
private void SetScore(int num)
{
score += num;
scoreText.text = "得分:" + score;
}
/// <summary>
/// 记录分数
/// </summary>
private void RecordScore()
{
PlayerPrefs.SetInt("Score", score);
int count = PlayerPrefs.GetInt("Count", 0);
count++;
PlayerPrefs.SetInt("Count", count);
List<Record> records = JsonConvert.DeserializeObject<List<Record>>(PlayerPrefs.GetString("Records"));
if (records == null) records = new List<Record>();
records.Add(new Record(score));
records.Sort((a, b) => b.score - a.score);
PlayerPrefs.SetString("Records", JsonConvert.SerializeObject(records));
}
/// <summary>
/// 随机打乱顺序
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sources"></param>
private static void ListRandom<T>(List<T> sources)
{
System.Random rd = new System.Random();
int index = 0;
T temp;
for (int i = 0; i < sources.Count; i++)
{
index = rd.Next(0, sources.Count - 1);
if (index != i)
{
temp = sources[i];
sources[i] = sources[index];
sources[index] = temp;
}
}
}
}
4.6 获胜界面类
继承了面板基类,总共两个按钮,继续游戏和返回主菜单。
public class WinPanel : PanelBase
{
private Button continueBtn;
private Button backBtn;
public override void BindComponent()
{
continueBtn = transform.Find("Bg/ContinueBtn").GetComponent<Button>();
backBtn = transform.Find("Bg/BackBtn").GetComponent<Button>();
}
public override void AddEvent()
{
continueBtn.onClick.AddListener(()=>
{
PanelManager.Instance.AddPanel<GamePanel>();
OnHide();
});
backBtn.onClick.AddListener(()=>
{
PanelManager.Instance.AddPanel<MainMenu>();
OnHide();
});
}
public override void OnInit(params object[] args)
{
}
}
4.7 记录数据类
里面有一个int型参数的构造函数,时间自动获取当前时间,重写了ToString方法。
/// <summary>
/// 记录类,用于存储时间和分数
/// </summary>
public class Record
{
public string time;
public int score;
public Record(int score)
{
time = System.DateTime.Now.ToString("G");
this.score = score;
}
public override string ToString()
{
return $"{time} | {score}";
}
}
五、测试过程
5.1 场景
5.2 美术资源
5.3 测试方法
获取到面板管理类的实例来打开主菜单的界面。
public class ThirdTest : TestBase
{
public override void TestMethod()
{
PanelManager.Instance.AddPanel<MainMenu>();
//扩展(row=2,col=4)
//PanelManager.Instance.AddPanel<GamePanel>(2,4);
//扩展(row=2,col=4,sucScore=5,failScore=-2)
//PanelManager.Instance.AddPanel<GamePanel>(2,4,5,-2);
}
}