连连看所属算法分析与实现
2018-03-13 本文已影响0人
JervieQin
需求
(1)随机生成游戏界面;
(2)选择两个相同图案的图片,并以不超过两个转弯的连线将它们连接起来,便可以消除这对图片,每一局用户需要消除所有的图片;
(3)当没有可以消除的图案时,可以使用重置功能;
(4)选择提示功能时,游戏自动消除一对相同的图片;
(5)实现联网对战
可能存在的问题
生成界面存在的问题
- 界面中棋子必须成对出现
- 初始界面必须保证有解
连接时存在的问题
- 折线次数的判断
- 配对图片的判断
- 连接路径上保证没有阻碍
联网同步问题
- 同步何种数据来体现对战
构思
- 成对问题: 可以准备两个数组分别存放匹配对,每次各取一个赋给sprite renderer。
- 初始界面保证有解问题: 可以先随机生成一个棋盘,再用自动匹配算法进行匹配,如果无解则重新生成棋盘。
- 折线次数问题: 可以用
- 判断一对棋子是否可以消除时,即最小折线次数小于等于2,可以用广度优先搜索;在自动求解时,可以用深度优先搜索。
- 图片选项配对问题:可以通过判断名称是否匹配。
- 连线路径上无障碍:确保路径上的sprite renderer为空即可。
- 简单实现对战,只要实现一个进度条,同步对方的完成度即可。
棋盘生成算法分析
匹配算法分析
显然这个类型的游戏,判断折线次数内匹配的算法时最核心的,其他都问题不大。下面我们来仔细分解一下这个问题。
$$方法一:分类判断法
首先我们用分类的思想类分析这个问题,这样更有助于我们深入理解。现在要求是折线数少于3次完成连接,那么我们很容易划分出三种情况:0折连,1折连,2折连。
0折连情况(即直连)
这种情况两个选中目标的横坐标或者纵坐标是一样的。只要再判断连线上没有阻碍即可。
1折连情况
此情况的两个目标横纵坐标都不能相等,相当于矩形上的对角点。所以要判断两个点是否满足1折连就只要确定另外两个对角点上的任一点同时对A和B满足0折连的情况即可。
2折连情况
向四周查找空格区域,直到“撞墙”,然后记录点C。再看点C是否满足与B 1折连情况。
$$方法二:广度优先算法
把所有满足情况的点都放在一个集合中,然后只要判断B在不在集合中就行了。具体是这样的:
- 准备两个数组,组A是存放满足条件的点;组B是存放暂时满足情况的点。
- 先把A放到组A。
- 然后把所有与组A 0折连的点放到组B,然后把组B拷贝到组A再清空。折数+1
- 重复3,直到B在组A中或者折数超过2。
$$方法三:A*寻路算法
虽然A*是解决静态网路寻路问题中求解最短路径的算法,但是其实在这里也可以有很好的应用。(不过我还没实现过,大家可以试试,可以在评论区甩代码链接分享下)
代码参考:
1.棋盘初始化(主题实现)
//初始化布局
public void InitializeLayout()
{
float offset = sample.bounds.size.x;
layout = new GameObject[Height, Width];
//初始化空棋盘
for (int i = 0; i < Height; i++)
for (int j = 0; j < Width; j++)
{
GameObject tile = Instantiate(emptytile, new Vector3(parent.position.x + (offset * j), parent.position.y + (offset * i), 0), Quaternion.identity, parent);
tile.name = (i * 10 + j).ToString();
layout[i, j] = tile;
}
//矫正位置偏差
if (SceneManager.GetActiveScene().name.Equals("Single"))
{
if (currentLevel <= 5)
parent.position = parentPOS[currentLevel];
else
parent.position = parentPOS[5];
}
//给棋盘放上棋子
GiveSprites();
}
void GiveSprites()
{
//实现图片的成对放置:遍历棋盘,当前tile没有精灵则贴上精灵并且成对的在棋盘随机位置放上对应精灵
for (int i = 1; i < Height - 1; i++)
for (int j = 1; j < Width - 1; j++)
{
if (layout[i, j].GetComponent<SpriteRenderer>().sprite == null)
{
layout[i, j].GetComponent<SpriteRenderer>().sprite = GetRandomSprite();
GiveSpriteInRandomXY();
}
}
}
//棋子图片随机付给没贴图的棋子,防止摆放太整齐
void GiveSpriteInRandomXY()
{
int x = UnityEngine.Random.Range(100, (Height - 1) * 100);
x /= 100;
int y = UnityEngine.Random.Range(100, (Width - 1) * 100);
y /= 100;
//随机定位一个位置
Sprite thisSprite = layout[x, y].GetComponent<SpriteRenderer>().sprite;
//检查是否有空
if (thisSprite == null)
layout[x, y].GetComponent<SpriteRenderer>().sprite = GetRandomSprite();
else if (thisSprite != null)
GiveSpriteInRandomXY();
}
//随机选取棋子图片
Sprite GetRandomSprite()
{
Sprite s = null;
int index = -1;
//在s1中获取一张精灵
if (currentName == "")
{
int maxrange = s1.Length * 100;
index = UnityEngine.Random.Range(0, maxrange);
index /= 100;
s = s1[index];
currentName = s.name;
}
//在s2中获取一张精灵
else
{
string name1 = currentName;
for (int i = 0; i < s1.Length; i++)
{
if (s1[i].name.Equals(name1))
{
s = s1[i];
currentName = "";
break;
}
}
}
return s;
}
//重新开始
public void Play()
{
if (CurrentLevel <= 5)
{ //根据当前关卡显示棋盘大小
Width = ReadLevelRowCol()[0];
Height = ReadLevelRowCol()[1];
}
if (CurrentLevel > 5)
{
Width = 7;
Height = 8;
}
GetComponent<GameManager>().PlayAgain();
}
//打乱棋盘
public void ConfuseLayout()
{
/*当棋盘无解时打乱棋盘*/
remaining.Clear();
//1.先把剩余sprite收走,存入数组
for (int i = 1; i < Height - 1; i++)
for (int j = 1; j < Width - 1; j++)
if (layout[i, j].GetComponent<SpriteRenderer>().sprite != null)
{
remaining.Add(layout[i, j].GetComponent<SpriteRenderer>().sprite);
layout[i, j].GetComponent<SpriteRenderer>().sprite = null;
}
//2.然后把它分发到棋盘上
ReGive();
}
private void ReGive()
{
int x = UnityEngine.Random.Range(100, (Height - 1) * 100);
x /= 100;
int y = UnityEngine.Random.Range(100, (Width - 1) * 100);
y /= 100;
while (remaining.Count > 0)
{
if (layout[x, y].GetComponent<SpriteRenderer>().sprite == null)
{
layout[x, y].GetComponent<SpriteRenderer>().sprite = remaining[0];
remaining.RemoveAt(0);
}
else
ReGive();
}
}
2.判定(主体实现)
private void Update()
{
SelectDetact();
}
void SelectDetact()
{
if (Input.GetMouseButtonDown(0))
{
position = Input.mousePosition;
Ray ray = Camera.main.ScreenPointToRay(position);
RaycastHit hit;
if (Physics.Raycast(ray, out hit))
{
SpriteRenderer hited = hit.transform.GetComponent<SpriteRenderer>();
//如果第一次点击则记录名字
if (firstSelect == null)
{
firstSelect = hited;
if (firstSelect.sprite == null)
firstSelect = null;
else //显示年代
{
firstSelect.gameObject.GetComponent<Tile>().ShowFrame(true);
//记录原先的图,如果选择失败要回退
firstSprite = firstSelect.sprite;
firstSelect.sprite = layout.ShowDynasty(firstSelect.sprite.name);
}
}
else
{
nextSelect = hited;
//排除无效选择
if (firstSelect.sprite == null || nextSelect.sprite == null || firstSelect.transform.position==nextSelect.transform.position )
{
//选择置空
firstSelect.gameObject.GetComponent<Tile>().ShowFrame(false);
//图片回退
firstSelect.sprite = firstSprite;
//移除选择记录
firstSelect = null;
nextSelect = null;
return;
}
//记录
nextSprite = nextSelect.sprite;
nextSelect.sprite = layout.ShowDynasty(nextSelect.sprite.name);
nextSelect.gameObject.GetComponent<Tile>().ShowFrame(true);
//判断选择对是否匹配
string name1 = firstSelect.sprite.name;
string name2 = nextSelect.sprite.name;
if (name1.Equals(name2) && JudgeConnection(firstSelect, nextSelect))
//消除
StartCoroutine(MatchSuccess());
else
StartCoroutine(MatchFail());
}
}
}
}
bool JudgeConnection(SpriteRenderer a, SpriteRenderer b)
{
int count = 0;
lst1.Add(a);
while (!lst1.Contains(b) && count < 3)
{
foreach (SpriteRenderer s in lst1)
{
//0折连且路空则加入lst2
lst2.AddRange(DirectConnect(s));
}
//lst2 -> lst1 并清空
for (int i = 0; i < lst2.Count; i++)
if (!lst1.Contains(lst2[i]))
lst1.Add(lst2[i]);
lst2.Clear();
count++;
}
return lst1.Contains(b) ? true : false;
}
List<SpriteRenderer> DirectConnect(SpriteRenderer a)
{
List<SpriteRenderer> temp = new List<SpriteRenderer>();
temp.AddRange(FindDirect(Vector2.up, a));
temp.AddRange(FindDirect(Vector2.left, a));
temp.AddRange(FindDirect(Vector2.right, a));
temp.AddRange(FindDirect(Vector2.down, a));
return temp;
}
List<SpriteRenderer> FindDirect(Vector2 dirc, SpriteRenderer a)
{
SpriteRenderer start = a;
List<SpriteRenderer> cast = new List<SpriteRenderer>();
RaycastHit hit;
SpriteRenderer sr;
while (Physics.Raycast(start.transform.position, dirc, out hit))
{
sr = hit.transform.GetComponent<SpriteRenderer>();
if (hit.transform.GetComponent<SpriteRenderer>().Equals(nextSelect))
{
cast.Add(sr);
break;
}
else
{
if (JudgeRoad(hit.transform.GetComponent<SpriteRenderer>().sprite))
{
cast.Add(sr);
start = sr;
}
else
break;
}
}
////把所有在A,B形成矩形范围内的点都加入waypoints以便寻路
//foreach (SpriteRenderer s in cast)
//{
// if (InArea(s, firstSelect, nextSelect))
// waypoints.Add(s.transform.position);
//}
return cast;
}
bool JudgeRoad(Sprite s)
{
if (s == null)
return true;
else
return false;
}
/// <summary>
/// 匹配成功动动作
/// </summary>
IEnumerator MatchSuccess()
{
//匹配成功音效
SoundManager.instance.SetClip("success");
yield return new WaitForSeconds(0.2f);
if (remainingCount > 2)
{
remainingCount -= 2;
if (SceneManager.GetActiveScene().name.Equals("Online"))
owningSlider.GetComponent<Progress>().CmdMinuse();
}
//结算界面
else
{
if (SceneManager.GetActiveScene().name.Equals("Online"))
owningSlider.GetComponent<Progress>().CmdOver();
else
{
OverPanel("win");
if (SceneManager.GetActiveScene().name.Equals("Single"))
gameObject.GetComponent<LayoutInitailization>().CurrentLevel++;
}
}
firstSelect.GetComponent<ParticleSystem>().Play();
nextSelect.GetComponent<ParticleSystem>().Play();
firstSelect.sprite = null;
nextSelect.sprite = null;
lst1.Clear();
OverAction();
}
IEnumerator MatchFail()
{
//匹配失败音效
SoundManager.instance.SetClip("fail");
yield return new WaitForSeconds(0.2f);
//还原图片
firstSelect.sprite = firstSprite;
nextSelect.sprite = nextSprite;
lst1.Clear();
OverAction();
}
void OverAction()
{
//选择置空
firstSelect.gameObject.GetComponent<Tile>().ShowFrame(false);
nextSelect.gameObject.GetComponent<Tile>().ShowFrame(false);
firstSelect = null;
nextSelect = null;
}
public void OverPanel(string v)
{
restartPanel.SetActive(true);
playingButton.SetActive(false);
image.sprite = v.Equals("fail") ? fail : win;
SoundManager.instance.SetClip(v.Equals("fail") ? "over" : "win");
if (changebtn[0] != null)
{
changebtn[0].SetActive(v.Equals("fail"));
changebtn[1].SetActive(v.Equals("win"));
timenow = 10000;
}
}
public void PlayAgain()
{
//更新行列值
countRecord = (GetComponent<LayoutInitailization>().Height - 2) * (GetComponent<LayoutInitailization>().Width - 2);
remainingCount = countRecord;
//更新UI
restartPanel.SetActive(false);
playingButton.SetActive(true);
timenow = 51;
time.text = "51";
//开始倒计时
StopCoroutine("TimeCount");
StartCoroutine("TimeCount");
pa.DestroyChildren();
//布局
layout.InitializeLayout();
}
}
【https://github.com/jewis123/AntiqueCoin/blob/master/LinkUpScriptExample】