很多游戲的養成系統中會有利用芯片或者碎片來合成特定道具的功能,或者來給玩家以額外的屬性提升等,先截個圖以便更好說明:
如上圖,我們有各種各樣形狀迥異的碎片,上面只不過列舉了其中一部分,現在,我們需要利用這些碎片非常恰好和完整的將左邊這個棋盤格填滿;當然了,這里並不是要讓計算機來計算所有的填充辦法,也不是要讓計算機來自動的完成填充,而是要讓玩家來選擇這些碎片的具體放法,最終的目的都是要讓這個棋盤格全部填滿以解鎖新的游戲道具或給游戲中的單位提升盡可能多的屬性。這樣玩家可以有充分的自由,好去思考和權衡自己當前碎片的庫存情況,每個碎片帶給玩家的屬性提升情況,最終來確定自己應該如何去放。
我們先假設一下玩家放碎片的整個流程,例如,他先選中其中一個碎片,然后點擊棋盤格中的一個空白的位置,最后這個碎片就會填充到他所指定的位置。聽上去似乎沒有任何的問題,但實際上,有很多細節我們都沒有思考清楚。
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 }
接下來就可以愉快的進行拼圖游戲了,猜猜這個圖是用哪些元素拼出來的: