很多游戏的养成系统中会有利用芯片或者碎片来合成特定道具的功能,或者来给玩家以额外的属性提升等,先截个图以便更好说明:
如上图,我们有各种各样形状迥异的碎片,上面只不过列举了其中一部分,现在,我们需要利用这些碎片非常恰好和完整的将左边这个棋盘格填满;当然了,这里并不是要让计算机来计算所有的填充办法,也不是要让计算机来自动的完成填充,而是要让玩家来选择这些碎片的具体放法,最终的目的都是要让这个棋盘格全部填满以解锁新的游戏道具或给游戏中的单位提升尽可能多的属性。这样玩家可以有充分的自由,好去思考和权衡自己当前碎片的库存情况,每个碎片带给玩家的属性提升情况,最终来确定自己应该如何去放。
我们先假设一下玩家放碎片的整个流程,例如,他先选中其中一个碎片,然后点击棋盘格中的一个空白的位置,最后这个碎片就会填充到他所指定的位置。听上去似乎没有任何的问题,但实际上,有很多细节我们都没有思考清楚。
1.他所选中的那个碎片到底能不能在点击的棋盘格位置放下呢?例如,现在只剩两格空格点了,而选择的确是一个三格的碎片,则无论如何点击也是不可能放下此碎片的,即使可能剩下五格,也不一定能保证放下特殊形状的三格碎片。
2.如果能放下碎片,那应该以碎片的哪个格子为基准点进行放置呢?观察下面几张图:
我选中的是同一个碎片,点击的都是棋盘格的中间那个格子,理论上就会有3种可能的放法,会根据你的碎片定义的基准点放置结果不同,如果碎片本身的格子数更多的话,放置的方式也会和碎片占有的格子数一样多。这本身不会产生任何问题,只要给碎片定义一个原点(基准点)不就好了,但有时候又会有这样的情况发生:
假如图上已经有两个碎片了,你还是像之前一样选中那个折角点击最中心那个格子,这是你发现只剩唯一一种放法,如果你之前定义的碎片的原点并不是折角的那个格子,那么你怎么样都放不上去了,但只要是个人都知道点击中间那个格子是有放法的。那这个时候就会有矛盾产生了,有多种放法的时候应该怎么放,如果定义原点的话只有唯一一种放法的时候很可能就放不上去了,那应该如何处理呢?
我的处理方式是:还是先给每一个碎片定义一个默认原点,但也不一定就要按这个原点的顺序去放置,只有当默认原点放置的方式失效时,才考虑其他的格子作为原点的放法。
基于以上的想法,就可以定义出碎片的基类了:
1 using System.Collections.Generic; 2 using UnityEngine; 3 4 public class Fragment : MonoBehaviour 5 { 6 public int TypeID; 7 //默认原点 8 public List<Vector2Int> Pos; 9 //其他可能的原点组合 10 public List<List<Vector2Int>> ExPos; 11 public bool bSelected { get; set; } 12 public virtual void Init() { } 13 }
所有形状各异的碎片都继承自这个基类,根据他们的不同形状来重写初始化的方式,例如,上面那个折角碎片:
1 using System.Collections.Generic; 2 using UnityEngine; 3 4 public class RTFragment : Fragment 5 { 6 public override void Init() 7 { 8 TypeID = 7; 9 Pos = new List<Vector2Int>() { new Vector2Int(0, 0), new Vector2Int(-1, 0), new Vector2Int(0, -1) }; 10 11 ExPos = new List<List<Vector2Int>>() 12 { 13 new List<Vector2Int>(){new Vector2Int(0,0),new Vector2Int(1,0),new Vector2Int(1,-1)}, 14 new List<Vector2Int>(){new Vector2Int(0,0),new Vector2Int(0,1),new Vector2Int(-1,1)} 15 }; 16 } 17 }
注意,这里即使是所谓的原点也并非是一个确定的点,而是一个相对的偏移值(平移值)的组合,通过这个组合来具体确定这个碎片的数学形状,因为你并不知道这个碎片到底会被玩家放在什么位置,其实如果你想偷懒,这里的ExPos也可以用纯碎的数学平移方式来计算:
1 protected List<List<Vector2Int>> InitExPos(List<Vector2Int> pos,List<Vector2Int> offse) 2 { 3 var ex = new List<List<Vector2Int>>(); 4 foreach (var o in offse) 5 { 6 var temp = new List<Vector2Int>(); 7 foreach (var p in pos) 8 { 9 temp.Add(p + o); 10 } 11 ex.Add(temp); 12 } 13 return ex; 14 }
那个ExPos就可以这么来计算:
ExPos = InitExPos(Pos, new List<Vector2Int>() { new Vector2Int(0, 1), new Vector2Int(1, 0) });
该碎片的另外两种可能只不过是在原点(折角点)的基础上向右和向上分别平移一个单位得到的组合。将上面的方法放置在基类当中,这样所有的子类就能根据自己的需要来计算ExPos。
有个这些碎片之后,它们现在可以随时放置在棋盘格中的任何位置,我们要开始考虑整一个棋盘格的结构了,以及要如何定义放入的碎片和碎片放置的位置。
初步的考虑是这样的,我们可以将棋盘格定义为一个矩阵。一开始,我们要确定它的大小,几行几列,以及它每个格子的状态,这个格子是已经放置了碎片还是没有放置,这所有的一切,都可以用一个矩阵来表示。
例如,上面的例子是一个3行3列的矩阵,我们只需要在矩阵中填充0或者1来判断这个位置上有没有放置碎片,一开始,没有放置任何碎片,则是一个零矩阵。
基础的结构可以这样定义:
1 public Vector2Int Size { get; private set; } 2 3 public int[,] PuzzlePicture { get; private set; } 4 5 public Dictionary<List<Vector2Int>, Fragment> PuzzleFragments = new Dictionary<List<Vector2Int>, Fragment>(); 6 7 public void InitPuzzle(Vector2Int size) 8 { 9 Size = size; 10 PuzzlePicture = new int[Size.x, Size.y]; 11 }
这里额外定义了一个字典用于保存和获取当前棋盘格中已有的碎片列表,它的键为该碎片在棋盘格中的位置列表。
添加碎片到棋盘格:
1 /// <summary> 2 /// 在棋盘格中添加碎片 3 /// </summary> 4 /// <param name="frag">选择的碎片</param> 5 /// <param name="offse">在棋盘格中的位置</param> 6 /// <returns>是否添加成功</returns> 7 public bool AddFragment(Fragment frag, Vector2Int offse) 8 { 9 var pos = new List<Vector2Int>(); 10 var expos = new List<List<Vector2Int>>(); 11 foreach (var p in frag.Pos) 12 { 13 pos.Add(p + offse); 14 } 15 foreach (var pg in frag.ExPos) 16 { 17 var temp = new List<Vector2Int>(); 18 foreach (var p in pg) 19 { 20 temp.Add(p + offse); 21 } 22 expos.Add(temp); 23 } 24 var s = AddFragToPos(pos, expos); 25 if (s != null) 26 { 27 PuzzleFragments.Add(s, frag); 28 } 29 return s != null; 30 } 31 32 List<Vector2Int> AddFragToPos(List<Vector2Int> pos, List<List<Vector2Int>> expos) 33 { 34 bool ms = true; 35 var spos = new List<Vector2Int>(); 36 foreach (var p in pos) 37 { 38 ms = CheckPos(p); 39 if (!ms) 40 break; 41 } 42 if (!ms) 43 { 44 foreach (var pg in expos) 45 { 46 bool tb = true; 47 foreach (var p in pg) 48 { 49 tb = CheckPos(p); 50 if (!tb) 51 break; 52 } 53 if (tb) 54 { 55 foreach (var p in pg) 56 { 57 PuzzlePicture[p.x, p.y] = 1; 58 } 59 spos = pg; 60 break; 61 } 62 else 63 { 64 if (expos[expos.Count - 1] == pg) 65 { 67 spos = null; 68 } 69 } 70 } 71 } 72 else 73 { 74 foreach (var p in pos) 75 { 76 PuzzlePicture[p.x, p.y] = 1; 77 } 78 spos = pos; 79 } 80 return spos; 81 }
稍微解释一下,外部的调用方法就是将具体要填充的点的位置计算出来,也是一个平移变换,然后传值到一个私有的计算方法中,在这里边先判断原始的点是否能完成填充,注意必须要原始组合点中的所有点都能填充进格子才行,第一次遍历纯粹是为了检查这一组的点是否符合要求,只有全部都符合要求才能进行第二次遍历改值,将矩阵对应位置的点改为1。如果原始一组点无法填充,则考虑其他的组合可能,方法同上。检查点状态的方法如下:
1 bool CheckPos(Vector2Int p) 2 { 3 return !(p.x < 0 || p.x >= Size.x || p.y < 0 || p.y >= Size.y || PuzzlePicture[p.x, p.y] != 0); 4 }
如果改组点中有任何一个点超出格子范围或者那个位置已经有填充了,这一组点的放置方式将失效。
从棋盘格中移除碎片:
1 /// <summary> 2 /// 从选定位置移除碎片 3 /// </summary> 4 /// <param name="offse">选中的位置</param> 5 /// <returns>是否成功</returns> 6 public bool RemoveFragment(Vector2Int offse) 7 { 8 bool s = false; 9 var spos = new List<Vector2Int>(); 10 foreach (var pg in PuzzleFragments.Keys) 11 { 12 if (pg.Contains(offse)) 13 { 14 s = RemoveFragToPos(pg); 15 spos = pg; 16 break; 17 } 18 } 19 if (s) 20 PuzzleFragments.Remove(spos); 21 return s; 22 } 23 24 bool RemoveFragToPos(List<Vector2Int> pos) 25 { 26 foreach (var p in pos) 27 { 28 if (PuzzlePicture[p.x, p.y] == 0) 29 { 30 return false; 31 } 32 } 33 34 foreach (var p in pos) 35 { 36 PuzzlePicture[p.x, p.y] = 0; 37 } 38 39 return true; 40 }
移除相对简单,不用考虑额外的可能性,有就移没有就不移。
下面是一个测试脚本:
1 using System.Collections.Generic; 2 using UnityEngine; 3 using UnityEngine.UI; 4 5 public class TestPuzzle : MonoBehaviour 6 { 7 private List<GridView> Grids = new List<GridView>(); 8 9 private List<Fragment> Fragments = new List<Fragment>(); 10 11 private PuzzleCtrl PuzzleCtrl; 12 13 private Fragment CurSelectFrag; 14 public void Start() 15 { 16 PuzzleCtrl = GetComponent<PuzzleCtrl>(); 17 PuzzleCtrl.InitPuzzle(new Vector2Int(3, 3)); 18 19 Fragments.AddRange(GetComponentsInChildren<Fragment>()); 20 foreach (var f in Fragments) 21 { 22 f.Init(); 23 24 var bt = f.GetComponent<Button>(); 25 bt.onClick.AddListener(() => 26 { 27 if (f.bSelected) 28 { 29 f.bSelected = false; 30 CurSelectFrag = null; 31 } 32 else 33 { 34 f.bSelected = true; 35 if (CurSelectFrag != null) 36 CurSelectFrag.bSelected = false; 37 CurSelectFrag = f; 38 } 39 }); 40 } 41 42 Grids.AddRange(GetComponentsInChildren<GridView>()); 43 44 foreach (var g in Grids) 45 { 46 var bt = g.GetComponent<Button>(); 47 bt.onClick.AddListener(() => 48 { 49 if (CurSelectFrag == null) 50 { 51 PuzzleCtrl.RemoveFragment(g.pos); 52 } 53 else 54 { 55 PuzzleCtrl.AddFragment(CurSelectFrag, g.pos); 56 } 57 UpdateView(PuzzleCtrl.Size,PuzzleCtrl.PuzzlePicture); 58 }); 59 } 60 } 61 62 public void UpdateView(Vector2Int size,int[,] v) 63 { 64 for (int i = 0; i < size.x; i++) 65 { 66 for (int j = 0; j < size.y; j++) 67 { 68 foreach (var g in Grids) 69 { 70 if (g.pos.x == i && g.pos.y == j) 71 { 72 g.GetComponent<Image>().color = v[i, j] == 0 ? new Color(0, 0, 0, 169 * 1.0f / 255) : new Color(1, 1, 1, 233 * 1.0f / 255); 73 break; 74 } 75 } 76 } 77 } 78 } 79 }
接下来就可以愉快的进行拼图游戏了,猜猜这个图是用哪些元素拼出来的: