通常會有一個起點一個終點,既然這邊是接電線,起點就是電源,終點就用電燈,當路徑從起點到終點有達成連線的時候,遊戲就完成了。
製作這個我覺得可以有幾種做法,一種就是亂數安排路徑,好處是每次結果都會不同,但是壞處就是難度比較無法掌控;另一種就是預先把版面設計幾種,好處就是可以掌控電線的路徑圖案跟難度,壞處就是要花時間把版面設定完畢,而且變化不大,重玩幾次就可以背下答案。
這邊使用的亂數安排路徑,因為比較省時間,另外也可以使用之前做過的迷宮產生器來產生路線資料。
實作測試 (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