通常會有一個起點一個終點,既然這邊是接電線,起點就是電源,終點就用電燈,當路徑從起點到終點有達成連線的時候,遊戲就完成了。
製作這個我覺得可以有幾種做法,一種就是亂數安排路徑,好處是每次結果都會不同,但是壞處就是難度比較無法掌控;另一種就是預先把版面設計幾種,好處就是可以掌控電線的路徑圖案跟難度,壞處就是要花時間把版面設定完畢,而且變化不大,重玩幾次就可以背下答案。
這邊使用的亂數安排路徑,因為比較省時間,另外也可以使用之前做過的迷宮產生器來產生路線資料。
實作測試 (WebGL build) (滑鼠點擊方塊讓方塊旋轉,有通電的電線會改變顏色,當電線從電池到電燈有連通,遊戲結束)
1、路線路徑
這個版本是四方形棋盤格,明顯可以看出來類似迷宮,所以就把之前做過的迷宮產生器(簡易亂數迷宮產生1(Depth first search))拿來這邊產生版面資料使用,不過稍做點小修改只需要回傳的迷宮資料。
public class DepthFirstSearchMaze : MonoBehaviour { public class Cell { public int[] wall = new int[4]; //n,e,s,w 0=wall, 1=passage public int x, y; } public Cell[][] CreateMaze(int width, int height) { //初始化陣列大小 cellArr = new Cell[height][]; for(int i = 0; i < height; ++i) { cellArr[i] = new Cell[width]; for(int j = 0; j < width; ++j) { cellArr[i][j] = new Cell() {y = i, x = j}; } } //亂數取一個格子作為起始點 Cell startCell = cellArr[Random.Range(0,height)][Random.Range(0,width)]; //開始建立迷宮 CreatePath(startCell, new List<Cell>()); } void CreatePath(Cell start, List<Cell> visitedList) { //把此格加入已踩過列表 visitedList.Add(start); //取得可以使用的方向列表,該方向是牆面,並且沒有超出陣列大小 List<int> directions = new List<int>(); if(start.wall[0] == 0 && start.y != height-1) directions.Add(0); if(start.wall[1] == 0 && start.x != width-1) directions.Add(1); if(start.wall[2] == 0 && start.y != 0) directions.Add(2); if(start.wall[3] == 0 && start.x != 0) directions.Add(3); int count = directions.Count; for(int i = 0; i < count; ++i) { //從方向列表亂數取一個方向 int dir = directions[Random.Range(0, directions.Count)]; directions.Remove(dir); //把該方向移出列表 //取得下一個方向的Cell資料 int addY = (dir == 0) ? 1 : (dir == 2) ? -1 : 0; int addX = (dir == 1) ? 1 : (dir == 3) ? -1 : 0; int nextY = start.y + addY; int nextX = start.x + addX; Cell nextCell = cellArr[nextY][nextX]; //檢查是否踩過 if(visitedList.Contains(nextCell)) { continue; //Skip this direction } //建立通道 start.wall[dir] = 1; nextCell.wall[(dir+2)%4] = 1; //到下一個方向的Cell繼續 CreatePath (nextCell, visitedList); } } }
2、格子
接著來製作格子用的Component,用來裝四周路徑跟四周方塊的資料,有些跟迷宮方塊的資料重複,也可以修改使用,不過這邊就獨立出來好了。
public class Block : MonoBehaviour { public SpriteRenderer[] lineImages; //線段圖片 public Block[] blockNeighbors; //此方塊四個方向的鄰居 public int[] wirePath; //四個方向電線路徑,0沒有,1有 //用Raycast去抓四周的方塊 public void FindNeighbors() { //先把自己的Collider關掉 blockNeighbors = new Block[4]; BoxCollider2D b = transform.GetComponent<BoxCollider2D>(); if (b != null) b.enabled = false; //往四個方向抓物件 RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.up); if (hit) blockNeighbors[0] = hit.transform.GetComponent<Block>(); hit = Physics2D.Raycast(transform.position, Vector2.right); if (hit) blockNeighbors[1] = hit.transform.GetComponent<Block>(); hit = Physics2D.Raycast(transform.position, Vector2.down); if (hit) blockNeighbors[2] = hit.transform.GetComponent<Block>(); hit = Physics2D.Raycast(transform.position, Vector2.left); if (hit) blockNeighbors[3] = hit.transform.GetComponent<Block>(); if (b != null) b.enabled = true; } //設定電線路徑 public void SetPath(int[] path) { for (int i = 0; i < 4; ++i) { wirePath = path; if (i < lineImages.Length && lineImages[i] != null) lineImages[i].gameObject.SetActive(wirePath[i] == 1); } } //增加電線路徑 public void AddPath(int[] path) { for (int i = 0; i < 4; ++i) { wirePath[i] |= path[i]; if (i < lineImages.Length && lineImages[i] != null) lineImages[i].gameObject.SetActive(wirePath[i] == 1); } } //設定電線的圖片顏色,這邊就用暗紅跟亮紅來代表有無通電,這邊只有單純改Sprite的顏色,如果要有更好看的效果就需要自己加上電流等等 public void SetConnect(bool toggle) { foreach (SpriteRenderer sr in lineImages) { if (sr != null) sr.color = (toggle) ? new Color32(255, 0, 0, 255) : new Color32(106, 0, 0, 255); } } //從local方向的群組,依據目前方塊旋轉的角度,轉換成目前顯示在畫面上的world方向 public int[] GetWorldWireDir() { int rotate = Mathf.RoundToInt(this.transform.localEulerAngles.z) / 90; int[] wires = new int[4]; for (int i = 0; i < 4; ++i) { int index = (i + rotate) % 4; wires[i] = (index >= wirePath.Length) ? 0 : wirePath[index]; } return wires; } }
接著製作GameObject物件,這邊簡單使用一個單色圖片做背景,然後用四個方向的條狀方塊來代替電線,把這個物件做成Prefab,物件都加上一個BoxCollider2D跟Block的Component,把四條代表電線的圖案方塊拉到lineImages裡面。
接著用這個Prefab拉物件到場景上,在畫面上排出5x5的陣列,這邊我是從左下角開始往右排,再一層一層往上增加,這邊因為物件已經做成Prefab了,所以基本上該有的Component跟設定也都不用額外再做了。
最後再加上兩個做為起點跟終點的物件,這邊用個電池跟電燈,這兩個物件同樣加上BoxCollider2D跟Block的Component,其中沒有設定數值,到時候在板子運作的時候來做。
3、板面
最後製作板面把系統完成,用來初始化整個迷宮資料跟畫面顯示的圖片,初始方塊跟終點方塊也在這邊設定,因為這兩個方塊獨立陣列之外,所以就在這邊一起設定了,這邊有些隨意做,寫死的陣列大小跟初始化資料,不過還好有達到目的。
public class Board : MonoBehaviour { public Block start; //起始方塊,這邊是電池 public Block end; //終點方塊,這邊是電燈 public Block[] blocks; //中間所有的方塊,依序排列 public DepthFirstSearchMaze mazeGenerator; //迷宮產生器的Component,記得把Component抓進來 private bool isGameOver = false; //遊戲使否結束 private bool animFinished = true; //是否有方塊在旋轉 void Start() { //建立一張5x5迷宮 DepthFirstSearchMaze.Cell[][] cellArr = mazeGenerator.CreateMaze(5, 5); for (int y = 0; y < 5; ++y) { for (int x = 0; x < 5; ++x) { //設定方塊的電線圖片 int index = y * 5 + x; blocks[index].FindNeighbors(); //尋找跟此方塊四周相連的方塊 blocks[index].SetPath(cellArr[y][x].wall); //依據迷宮資料設定方塊電線的路徑 //亂數旋轉方塊角度 int dir = Random.Range(0, 4); blocks[index].transform.localEulerAngles = new Vector3(0, 0, dir * 90); } } //起始點跟終點,這邊起點跟終點不是在陣列中,所以手動設定 start.FindNeighbors(); start.SetPath(new int[4] {0, 1, 0, 0}); //起點是往右連到迷宮方塊 start.blockNeighbors[1].AddPath(new int[4] { 0, 0, 0, 1 }); //起點右邊的方塊需要增加一條往左連到起點的路徑 end.FindNeighbors(); end.SetPath(new int[4] {0, 0, 0, 1}); //終點是往左連到迷宮方塊 end.blockNeighbors[3].AddPath(new int[4] { 0, 1, 0, 0 }); //終點左邊的方塊需要增加一條往右連到終點的路徑 //檢查連線 CheckConnect(start, new List<Block>()); } void Update() { //滑鼠點左鍵,旋轉方塊,並檢查連線 if (Input.GetKeyDown(KeyCode.Mouse0)) { RaycastHit2D hit = Physics2D.Raycast(Camera.main.ScreenToWorldPoint(Input.mousePosition), Vector2.zero); if (hit) { Block target = hit.transform.GetComponent<Block>(); if (target != null) { if (target == start || target == end) return; //如果點到起點跟終點就不動作 //如果遊戲結束或是方塊還在旋轉就不動作 if (!isGameOver && animFinished) StartCoroutine(RotateAnim(target)); } } } } private IEnumerator RotateAnim(Block target) { animFinished = false; float angleTo = target.transform.localEulerAngles.z + 90; //低機率偶而旋轉會有小錯誤轉一整圈,不過無所謂 foreach (Block b in blocks) b.SetConnect(false); //重設所有的連線,讓所有的電線都先暗掉 CheckConnect(start, new List<Block>(), target); //重新檢查連線,跳過正在旋轉的方塊 while (true) { float angle = Mathf.MoveTowards(target.transform.localEulerAngles.z, angleTo, 270 * Time.deltaTime); target.transform.localRotation = Quaternion.Euler(new Vector3(0, 0, angle)); if (angle == angleTo) break; yield return null; } //方塊旋轉完畢再檢查一次 if (CheckConnect(start, new List<Block>())) { //連結成功,遊戲結束 Debug.Log("Game Over"); isGameOver = true; } animFinished = true; } private bool CheckConnect(Block block, List<Block> checkedList, Block skipBlock = null) { //從起點的Block開始檢查連結 bool result = false; for (int i = 0; i < 4; ++i) //北東南西 { if (i >= block.blockNeighbors.Length || block.blockNeighbors[i] == null) continue; //沒有該方向的鄰居格子 if (checkedList.Contains(block.blockNeighbors[i])) continue; //這個格子檢查過了 if (skipBlock != null && skipBlock == block.blockNeighbors[i]) continue; //跳過這個格子 int groupID = block.GetWorldWireDir()[i]; //往下個方向的電線編號 Block nextBlock = block.blockNeighbors[i]; //往下個方向的格子 int invertDir = (i + 2) % 4; //相反方向 //如果下個格子的反方向電線編號是0(沒有設定),或跟這個格子連過去的編號不同(這邊用flags檢查,如果有兩三種顏色的電線就可以用),就等於沒有連結成功 int nextBlockInvertDirGroupID = nextBlock.GetWorldWireDir()[invertDir]; if (groupID == 0 || nextBlockInvertDirGroupID == 0 || (nextBlockInvertDirGroupID & groupID) == 0) continue; //有連結成功,設定通電 nextBlock.SetConnect(true); //只有連結成功的加入已經檢查過名單 checkedList.Add(nextBlock); //如果這個格子是終點,代表頭尾連結完畢 if (nextBlock == end) result = true; //繼續往下檢查 if (CheckConnect(nextBlock, checkedList, skipBlock)) result = true; } return result; //就算中途有連通,也是全部檢查完後才會回傳結果 } }
最後把Board物件設置好,把起點跟終點方塊配置上去,然後Block照順序排列,這邊同樣把所有物件都Parent到這個板子物件下,單純只是整理一下。
到此便全部完成,同樣遊戲結束還需要製作結束畫面,以及這邊電線的方塊非常的陽春,也沒有任何特效,要讓整個畫面更好看還需要不少努力,同時也要調整一些Code來配合才行。
目前用亂數跑起來感覺還算可以,不過當然還是比不上手工製作的板面,也比較沒有騙人的路徑,不過單純這樣玩玩還算堪用就是了。
如果有任何錯誤歡迎提出。
No comments:
Post a Comment