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宣布卸 ...
随机推荐
- Ubuntu查看文件格式(后缀名)
在文件目录执行: $ file filename #filename表示要查看的文件名
- EXPLAIN 查看 SQL 执行计划
EXPLAIN 查看 SQL 执行计划.分析索引的效率: id:id 列数字越大越先执行: 如果说数字一样大,那么就从上往下依次执行,id列为null的就表是这是一个结果集,不需要使用它来进行查询. ...
- JavaScript-原始值和引用值
一.原始值和引用值的概念 在 ECMAScript 中,变量可以存在两种类型的值,即原始值和引用值. 1.1 原始值 (1)原始值指的是 原始类型 的值,也叫 基本类型,例如 Number.Stirn ...
- PTA | 1012 数字分类 (20分)
给定一系列正整数,请按要求对数字进行分类,并输出以下 5 个数字: A1 = 能被 5 整除的数字中所有偶数的和: A2 = 将被 5 除后余 1 的数字按给出顺序进行交错求和,即计算 n1−n2+n ...
- 【php】面向对象(四)
知识点:ai一. a => abstract(抽象类) a) 抽象类的修饰符,修饰类和成员方法 b) 注意:被修饰的类不能被实例化,被修饰的方法不能有程序体 c) 如果某一个类使用abstrac ...
- MTK Android Driver :Lcm
MTK Android Driver :lcm 1.怎样新建一个LCD驱动 LCD模组主要包括LCD显示屏和驱动IC.比如LF040DNYB16a模组的驱动IC型号为NT35510.要在MTK6577 ...
- C语言移动一个点
#include"stdio.h"#include"windows.h"#include"conio.h"#define M 3#defin ...
- scala_spark实践3
Spark 读写HBase优化 读数据 可以采用RDD的方式读取HBase数据: val conf = HBaseConfiguration.create() conf.set(TableInputF ...
- Dempster–Shafer theory(D-S证据理论)初探
1. 证据理论的发展历程 Dempster在1967年的文献<多值映射导致的上下文概率>中提出上.下概率的概念,并在一系列关于上下概率的文献中进行了拓展和应用,其后又在文献<贝叶斯推 ...
- 开源运动的"圣经"——《大教堂与集市》读书笔记
作者:Eric S. Raymond 一.黑客圈简史 1.早期 (1)MIT 与 ITS "黑客"一词大约就起源于MIT的计算机文化. 从PDP-1时代开始,黑客文化的命运就和DE ...