Unity 芯片拼图算法
很多游戏的养成系统中会有利用芯片或者碎片来合成特定道具的功能,或者来给玩家以额外的属性提升等,先截个图以便更好说明:
如上图,我们有各种各样形状迥异的碎片,上面只不过列举了其中一部分,现在,我们需要利用这些碎片非常恰好和完整的将左边这个棋盘格填满;当然了,这里并不是要让计算机来计算所有的填充办法,也不是要让计算机来自动的完成填充,而是要让玩家来选择这些碎片的具体放法,最终的目的都是要让这个棋盘格全部填满以解锁新的游戏道具或给游戏中的单位提升尽可能多的属性。这样玩家可以有充分的自由,好去思考和权衡自己当前碎片的库存情况,每个碎片带给玩家的属性提升情况,最终来确定自己应该如何去放。
我们先假设一下玩家放碎片的整个流程,例如,他先选中其中一个碎片,然后点击棋盘格中的一个空白的位置,最后这个碎片就会填充到他所指定的位置。听上去似乎没有任何的问题,但实际上,有很多细节我们都没有思考清楚。
1.他所选中的那个碎片到底能不能在点击的棋盘格位置放下呢?例如,现在只剩两格空格点了,而选择的确是一个三格的碎片,则无论如何点击也是不可能放下此碎片的,即使可能剩下五格,也不一定能保证放下特殊形状的三格碎片。
2.如果能放下碎片,那应该以碎片的哪个格子为基准点进行放置呢?观察下面几张图:
我选中的是同一个碎片,点击的都是棋盘格的中间那个格子,理论上就会有3种可能的放法,会根据你的碎片定义的基准点放置结果不同,如果碎片本身的格子数更多的话,放置的方式也会和碎片占有的格子数一样多。这本身不会产生任何问题,只要给碎片定义一个原点(基准点)不就好了,但有时候又会有这样的情况发生:
假如图上已经有两个碎片了,你还是像之前一样选中那个折角点击最中心那个格子,这是你发现只剩唯一一种放法,如果你之前定义的碎片的原点并不是折角的那个格子,那么你怎么样都放不上去了,但只要是个人都知道点击中间那个格子是有放法的。那这个时候就会有矛盾产生了,有多种放法的时候应该怎么放,如果定义原点的话只有唯一一种放法的时候很可能就放不上去了,那应该如何处理呢?
我的处理方式是:还是先给每一个碎片定义一个默认原点,但也不一定就要按这个原点的顺序去放置,只有当默认原点放置的方式失效时,才考虑其他的格子作为原点的放法。
基于以上的想法,就可以定义出碎片的基类了:
using System.Collections.Generic;
using UnityEngine; public class Fragment : MonoBehaviour
{
public int TypeID;
//默认原点
public List<Vector2Int> Pos;
//其他可能的原点组合
public List<List<Vector2Int>> ExPos;
public bool bSelected { get; set; }
public virtual void Init() { }
}
所有形状各异的碎片都继承自这个基类,根据他们的不同形状来重写初始化的方式,例如,上面那个折角碎片:
using System.Collections.Generic;
using UnityEngine; public class RTFragment : Fragment
{
public override void Init()
{
TypeID = ;
Pos = new List<Vector2Int>() { new Vector2Int(, ), new Vector2Int(-, ), new Vector2Int(, -) }; ExPos = new List<List<Vector2Int>>()
{
new List<Vector2Int>(){new Vector2Int(,),new Vector2Int(,),new Vector2Int(,-)},
new List<Vector2Int>(){new Vector2Int(,),new Vector2Int(,),new Vector2Int(-,)}
};
}
}
注意,这里即使是所谓的原点也并非是一个确定的点,而是一个相对的偏移值(平移值)的组合,通过这个组合来具体确定这个碎片的数学形状,因为你并不知道这个碎片到底会被玩家放在什么位置,其实如果你想偷懒,这里的ExPos也可以用纯碎的数学平移方式来计算:
protected List<List<Vector2Int>> InitExPos(List<Vector2Int> pos,List<Vector2Int> offse)
{
var ex = new List<List<Vector2Int>>();
foreach (var o in offse)
{
var temp = new List<Vector2Int>();
foreach (var p in pos)
{
temp.Add(p + o);
}
ex.Add(temp);
}
return ex;
}
那个ExPos就可以这么来计算:
ExPos = InitExPos(Pos, new List<Vector2Int>() { new Vector2Int(, ), new Vector2Int(, ) });
该碎片的另外两种可能只不过是在原点(折角点)的基础上向右和向上分别平移一个单位得到的组合。将上面的方法放置在基类当中,这样所有的子类就能根据自己的需要来计算ExPos。
有个这些碎片之后,它们现在可以随时放置在棋盘格中的任何位置,我们要开始考虑整一个棋盘格的结构了,以及要如何定义放入的碎片和碎片放置的位置。
初步的考虑是这样的,我们可以将棋盘格定义为一个矩阵。一开始,我们要确定它的大小,几行几列,以及它每个格子的状态,这个格子是已经放置了碎片还是没有放置,这所有的一切,都可以用一个矩阵来表示。
例如,上面的例子是一个3行3列的矩阵,我们只需要在矩阵中填充0或者1来判断这个位置上有没有放置碎片,一开始,没有放置任何碎片,则是一个零矩阵。
基础的结构可以这样定义:
public Vector2Int Size { get; private set; } public int[,] PuzzlePicture { get; private set; } public Dictionary<List<Vector2Int>, Fragment> PuzzleFragments = new Dictionary<List<Vector2Int>, Fragment>(); public void InitPuzzle(Vector2Int size)
{
Size = size;
PuzzlePicture = new int[Size.x, Size.y];
}
这里额外定义了一个字典用于保存和获取当前棋盘格中已有的碎片列表,它的键为该碎片在棋盘格中的位置列表。
添加碎片到棋盘格:
/// <summary>
/// 在棋盘格中添加碎片
/// </summary>
/// <param name="frag">选择的碎片</param>
/// <param name="offse">在棋盘格中的位置</param>
/// <returns>是否添加成功</returns>
public bool AddFragment(Fragment frag, Vector2Int offse)
{
var pos = new List<Vector2Int>();
var expos = new List<List<Vector2Int>>();
foreach (var p in frag.Pos)
{
pos.Add(p + offse);
}
foreach (var pg in frag.ExPos)
{
var temp = new List<Vector2Int>();
foreach (var p in pg)
{
temp.Add(p + offse);
}
expos.Add(temp);
}
var s = AddFragToPos(pos, expos);
if (s != null)
{
PuzzleFragments.Add(s, frag);
}
return s != null;
} List<Vector2Int> AddFragToPos(List<Vector2Int> pos, List<List<Vector2Int>> expos)
{
bool ms = true;
var spos = new List<Vector2Int>();
foreach (var p in pos)
{
ms = CheckPos(p);
if (!ms)
break;
}
if (!ms)
{
foreach (var pg in expos)
{
bool tb = true;
foreach (var p in pg)
{
tb = CheckPos(p);
if (!tb)
break;
}
if (tb)
{
foreach (var p in pg)
{
PuzzlePicture[p.x, p.y] = ;
}
spos = pg;
break;
}
else
{
if (expos[expos.Count - ] == pg)
{
spos = null;
}
}
}
}
else
{
foreach (var p in pos)
{
PuzzlePicture[p.x, p.y] = ;
}
spos = pos;
}
return spos;
}
稍微解释一下,外部的调用方法就是将具体要填充的点的位置计算出来,也是一个平移变换,然后传值到一个私有的计算方法中,在这里边先判断原始的点是否能完成填充,注意必须要原始组合点中的所有点都能填充进格子才行,第一次遍历纯粹是为了检查这一组的点是否符合要求,只有全部都符合要求才能进行第二次遍历改值,将矩阵对应位置的点改为1。如果原始一组点无法填充,则考虑其他的组合可能,方法同上。检查点状态的方法如下:
bool CheckPos(Vector2Int p)
{
return !(p.x < || p.x >= Size.x || p.y < || p.y >= Size.y || PuzzlePicture[p.x, p.y] != );
}
如果改组点中有任何一个点超出格子范围或者那个位置已经有填充了,这一组点的放置方式将失效。
从棋盘格中移除碎片:
/// <summary>
/// 从选定位置移除碎片
/// </summary>
/// <param name="offse">选中的位置</param>
/// <returns>是否成功</returns>
public bool RemoveFragment(Vector2Int offse)
{
bool s = false;
var spos = new List<Vector2Int>();
foreach (var pg in PuzzleFragments.Keys)
{
if (pg.Contains(offse))
{
s = RemoveFragToPos(pg);
spos = pg;
break;
}
}
if (s)
PuzzleFragments.Remove(spos);
return s;
} bool RemoveFragToPos(List<Vector2Int> pos)
{
foreach (var p in pos)
{
if (PuzzlePicture[p.x, p.y] == )
{
return false;
}
} foreach (var p in pos)
{
PuzzlePicture[p.x, p.y] = ;
} return true;
}
移除相对简单,不用考虑额外的可能性,有就移没有就不移。
下面是一个测试脚本:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; public class TestPuzzle : MonoBehaviour
{
private List<GridView> Grids = new List<GridView>(); private List<Fragment> Fragments = new List<Fragment>(); private PuzzleCtrl PuzzleCtrl; private Fragment CurSelectFrag;
public void Start()
{
PuzzleCtrl = GetComponent<PuzzleCtrl>();
PuzzleCtrl.InitPuzzle(new Vector2Int(, )); Fragments.AddRange(GetComponentsInChildren<Fragment>());
foreach (var f in Fragments)
{
f.Init(); var bt = f.GetComponent<Button>();
bt.onClick.AddListener(() =>
{
if (f.bSelected)
{
f.bSelected = false;
CurSelectFrag = null;
}
else
{
f.bSelected = true;
if (CurSelectFrag != null)
CurSelectFrag.bSelected = false;
CurSelectFrag = f;
}
});
} Grids.AddRange(GetComponentsInChildren<GridView>()); foreach (var g in Grids)
{
var bt = g.GetComponent<Button>();
bt.onClick.AddListener(() =>
{
if (CurSelectFrag == null)
{
PuzzleCtrl.RemoveFragment(g.pos);
}
else
{
PuzzleCtrl.AddFragment(CurSelectFrag, g.pos);
}
UpdateView(PuzzleCtrl.Size,PuzzleCtrl.PuzzlePicture);
});
}
} public void UpdateView(Vector2Int size,int[,] v)
{
for (int i = ; i < size.x; i++)
{
for (int j = ; j < size.y; j++)
{
foreach (var g in Grids)
{
if (g.pos.x == i && g.pos.y == j)
{
g.GetComponent<Image>().color = v[i, j] == ? new Color(, , , * 1.0f / ) : new Color(, , , * 1.0f / );
break;
}
}
}
}
}
}
接下来就可以愉快的进行拼图游戏了,猜猜这个图是用哪些元素拼出来的:
Unity 芯片拼图算法的更多相关文章
- [A*算法]基于Unity实现A*算法(二)
写在前面:上一篇当时是非常简单的了解一下A*,昨天还有一些问题没解决,就暂时把自己查阅的文坛摘抄了过来(毕竟人家写的比我要好的多 :> ) 今天终于解决了,就又写了这一篇,正好我自己再梳理一遍, ...
- Unity 之圆环算法
首先我们要明白圆环生成的原理,其实说白了并不是圆环,而是圆.因为我们使用的预制物体时Cube(物体本身是有大小的)难免会有发生实物的折叠看起来给人的感觉是圆环而已. 1.1 几何中我们要画一个圆,因为 ...
- Unity 梯子生成算法
Unity之生成梯子算法的实现. 1.通过预制物体动态生成角度可设置的梯子形状. 1.1 主要涉及到的数学知识点,角度与弧度的转化. 弧度=角度乘以π后再除以180 角度=弧度除以π再乘以180 1. ...
- Unity项目 - Boids集群模拟算法
1987年Craig W.Reynolds发表一篇名为<鸟群.牧群.鱼群:分布式行为模式>的论文,描述了一种非常简单的.以面向对象思维模拟群体类行为的方法,称之为 Boids ,Boids ...
- Google瓦片地图算法解析
基本概念: 地图瓦片地址:http://mt2.google.cn/vt/lyrs=m@167000000&hl=zh-CN&gl=cn&x=420&y=193& ...
- Unity Graphics(一):选择一个光照系统
原文链接 Choosing a Lighting Technique https://unity3d.com/learn/tutorials/topics/graphics/choosing-ligh ...
- 痞子衡嵌入式:恩智浦i.MX RT1xxx系列MCU硬件那些事(2.5)- 串行NOR Flash下载算法(IAR EWARM篇)
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是IAR开发环境下i.MXRT的串行NOR Flash下载算法设计. 在i.MXRT硬件那些事系列之<在串行NOR Flash XI ...
- 本科小白学ROS 和 SLAM(一):杂谈
本人最近才迷恋上ROS(Robot Operating System),准确的说应该是6月中旬,具体的记不清了(可能是年纪大了,容易健忘).对于一个电子DIY的狂热爱好者来说,我在校的梦想就是做一个属 ...
- 2018 AI产业界大盘点
2018 AI产业界大盘点 大事件盘点 “ 1.24——Facebook人工智能部门负责人Yann LeCun宣布卸任 Facebook人工智能研究部门(FAIR)的负责人Yann LeCun宣布卸 ...
随机推荐
- 怎么在三层架构中使用Quartz.Net开源项目(与数据库交互)
1.首先在项目中先创建一个控制台应用程序 2.然后右击项目中的[引用],可以[添加引用],也可以[管理NuGet程序包],作者使用的是[添加引用],添加本地应用.版本不同,所使用的方式不同.需要此版本 ...
- python—os模块
os模块(操作目录) 1 import os 2 os.rename('旧','新') #修改文件名 3 os.remove('') #删除文件 4 print(os.listdir('.')) # ...
- TensorFlow报错module 'tensorflow' has no attribute 'xxx'解决办法
原因:TensorFlow2.0版本修改了许多函数名字 tf.sub()更改为tf.subtract() tf.mul()更改为tf.multiply() tf.types.float32更改为tf. ...
- ssh-add和ssh-agent
注: 因为在ssh-agent异常关闭或者新开窗口是会导致ssh-add找不到私钥,导致添加的私钥无效,所以下面使用keychain管理 ssh-add 参数 -l 查看代理中的私钥 -L 查看代理中 ...
- 如何让Java应用成为杀不死的小强?(上篇)
各位坐稳扶好,我们要开车了.不过在开车之前,我们还是例行回顾一下上期分享的要点. 项庄舞剑意在沛公,而咱们上期主要借助应用服务器 Resin 的源码,体验了一次 JMX 的真实应用.鉴于 9012 年 ...
- PTA数据结构与算法题目集(中文) 7-10
PTA数据结构与算法题目集(中文) 7-10 7-10 公路村村通 (30 分) 现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低 ...
- 大曾Blogs使用说明书😊——Super ITZ
大曾Blogs使用说明书 先敲黑板,四句话: pipe搜索,简洁,用于跳转,博客园及csdn和github 博客园炫酷界面,用于查看主要博文 csdn所有博客汇总,查看详细信息 github项目源码汇 ...
- JavaScript中||和&&的运算
一般来讲 && 运算和 | | 运算得到的结果都是 true 和 false ,但是 js 中的有点不太一样.当进行 a&&b 和 a| |b (如 1&&am ...
- System.out.println()的真实含义
每一个人的Java学习之路上恐怕都是用以下代码开始的吧? public class Test { public static void main(String[] args) { System.out ...
- Java相同id的数据集合,合并数据为一条,并将几个字段内容合并为一个
Java实现,当然也可以数据库实现; /** * Created by shaozhiqi on 2019/7/31. */ public class TestUnion { @Test public ...