三消類游戲一直是游戲市場上經(jīng)久不衰的休閑游戲,該類型也是源自于經(jīng)典的俄羅斯方塊玩法的一部分。三消游戲需要交換游戲中相鄰的兩個方格,以讓3個或更多相同的方格連成直線,一旦連線成功,則消除這些連成線的同色方格,并使用新的方格進(jìn)行填充,填充后如果還存在連線就可以達(dá)成Combo或多倍加分!
本教程就為大家分享如何在Unity中制作這樣一款三消游戲的完整過程,從創(chuàng)建底板填充方格開始,到統(tǒng)計步數(shù)并計算游戲得分,來自己做一款三消游戲。
準(zhǔn)備工作
將項目初始資源導(dǎo)入Unity項目,資源目錄如下:
?
其中分別包含要用于游戲的動畫、音效、字體、預(yù)制件、場景、腳本及圖片資源。
創(chuàng)建游戲底板
打開Game場景,新建空游戲?qū)ο竺麨锽oardManager,該對象將用于生成游戲底板,并填充方格。然后將Scripts/Board and Grid文件夾下的BoardManager腳本拖拽至剛剛創(chuàng)建的BoardManager游戲?qū)ο笊希?/span>
?
BoardManager腳本代碼如下:
public static BoardManager instance; // 1
public List<Sprite> characters = new List<Sprite>(); // 2
public GameObject tile; // 3
public int xSize, ySize; // 4
private GameObject[,] tiles; // 5
public bool IsShifting { get; set; } // 6
void Start () {
instance = GetComponent<BoardManager>(); // 7
Vector2 offset = tile.GetComponent<SpriteRenderer>().bounds.size;
CreateBoard(offset.x, offset.y); // 8
}
private void CreateBoard (float xOffset, float yOffset) {
tiles = new GameObject[xSize, ySize]; // 9
float startX = transform.position.x; // 10
float startY = transform.position.y;
for (int x = 0; x < xSize; x++) { // 11
for (int y = 0; y < ySize; y++) {
GameObject newTile = Instantiate(tile, new Vector3(startX + (xOffset * x), startY + (yOffset * y), 0), tile.transform.rotation);
tiles[x, y] = newTile;
}
}
}
?
- BoardManager腳本聲明了一個單例名為instance,便于其它腳本訪問該腳本。
- characters是方格需要用到的圖片列表。
- tile是用于初始化方格底板的預(yù)制件。
- xSize及ySize是底板橫向及縱向的數(shù)量。
- tiles是保存底板方格的二維數(shù)組。
- IsShifting函數(shù)用于檢測是否需要填充方格。
- Start函數(shù)用于初始化BoardManager腳本實例。
- CreateBoard函數(shù)用于創(chuàng)建底板,參數(shù)為方初始化格圖片的寬度及高度,根據(jù)此前定義的底板方格數(shù)量及底板方格預(yù)制件,來初始化整個底板。
在層級視圖中選中BoardManager對象,然后在檢視視圖中將BoardManager腳本的Characters元素數(shù)量設(shè)為7,然后將Sprites/Characters文件夾下的圖片綁定到數(shù)組元素。最后將Prefabs文件夾下的Tile預(yù)制件綁定到腳本的Tile字段,將BoardManager腳本的X Size與Y Size分別設(shè)為8、12。完成后如下圖:
?
然后運(yùn)行場景,可以看到底板能夠正常創(chuàng)建,但出現(xiàn)了偏移:
?
這是因為底板方格從左下角開始最先生成,而首個方格坐標(biāo)為BoardManager對象坐標(biāo)。下面調(diào)整BoardManager對象的坐標(biāo)為(-2.66, -3.83, 0),讓BoardManager坐標(biāo)位于屏幕左下角。
?
隨機(jī)生成底板
打開BoardManager腳本,在CreateBoard方法中新增以下代碼:
?
newTile.transform.parent = transform; // 1
Sprite newSprite = characters[Random.Range(0, characters.Count)]; // 2
newTile.GetComponent<SpriteRenderer>().sprite = newSprite; // 3
?以上代碼的作用是將所有底板方格的父對象均設(shè)置為BoardManager,保持層級視圖干凈整潔,并從之前定義的數(shù)組中隨機(jī)選取一張圖片來初始化方格。現(xiàn)在運(yùn)行游戲,效果如下:
?
上面生成的方格還有些小問題,就是一開始就出現(xiàn)了連續(xù)的可消除方格,下面就來解決這個問題。
避免初始化重復(fù)方格
底板方格按從下到上從左到右的順序創(chuàng)建,所以在創(chuàng)建新方格前要對相鄰的方格進(jìn)行判斷。
?
上圖所示的循環(huán)會從左下方開始遍歷方格,每次迭代都會獲取當(dāng)前方格左側(cè)及下方的方格,然后通過隨機(jī)選取這兩個方格,來保證不會在初始化底板時出現(xiàn)3個及以上相連的同一方格。更改CreateBoard方法代碼為如下:
?
private void CreateBoard (float xOffset, float yOffset) {
tiles = new GameObject[xSize, ySize];
float startX = transform.position.x;
float startY = transform.position.y;
Sprite[] previousLeft = new Sprite[ySize]; // Add this line
Sprite previousBelow = null; // Add this line
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
GameObject newTile = Instantiate(tile, new Vector3(startX + (xOffset * x), startY + (yOffset * y), 0), tile.transform.rotation);
tiles[x, y] = newTile;
newTile.transform.parent = transform; // Add this line
List<Sprite> possibleCharacters = new List<Sprite>();
possibleCharacters.AddRange(characters);
possibleCharacters.Remove(previousLeft[y]);
possibleCharacters.Remove(previousBelow);
Sprite newSprite = possibleCharacters[Random.Range(0, possibleCharacters.Count)];
newTile.GetComponent<SpriteRenderer>().sprite = newSprite;
previousLeft[y] = newSprite;
previousBelow = newSprite;
}
}
}
?
運(yùn)行游戲,不會出現(xiàn)重復(fù)相連的3個方格了:
?
交換方格
下面來實現(xiàn)選中并交換相鄰的方格。打開Tile腳本,其中Select方法用于選中方格后替換方格圖片并播放選中音效,Deselect方法用于恢復(fù)選中方格的圖片,并提示當(dāng)前未選中任意方格。SwapSprite方法用于交換兩個相鄰方格,即替換兩個Sprite的紋理,然后播放交換音效。這里通過按下鼠標(biāo)左鍵來操作方格,代碼如下:
?
void Awake() {
render = GetComponent<SpriteRenderer>();
}
private void Select() {
isSelected = true;
render.color = selectedColor;
previousSelected = gameObject.GetComponent<Tile>();
SFXManager.instance.PlaySFX(Clip.Select);
}
private void Deselect() {
isSelected = false;
render.color = Color.white;
previousSelected = null;
}
void OnMouseDown() {
// Not Selectable conditions
if (render.sprite == null || BoardManager.instance.IsShifting) {
return;
}
if (isSelected) { // Is it already selected?
Deselect();
} else {
if (previousSelected == null) { // Is it the first tile selected?
Select();
} else {
if (GetAllAdjacentTiles().Contains(previousSelected.gameObject)) { // Is it an adjacent tile?
SwapSprite(previousSelected.render);
previousSelected.ClearAllMatches();
previousSelected.Deselect();
ClearAllMatches();
} else {
previousSelected.GetComponent<Tile>().Deselect();
Select();
}
}
}
}
public void SwapSprite(SpriteRenderer render2) {
if (render.sprite == render2.sprite) {
return;
}
Sprite tempSprite = render2.sprite;
render2.sprite = render.sprite;
render.sprite = tempSprite;
SFXManager.instance.PlaySFX(Clip.Swap);
GUIManager.instance.MoveCounter--; // Add this line here
}
?
這里還需要保證僅相鄰的方格才能進(jìn)行交換,在Tile腳本中添加以下兩個方法:
?
private GameObject GetAdjacent(Vector2 castDir) {
RaycastHit2D hit = Physics2D.Raycast(transform.position, castDir);
if (hit.collider != null) {
return hit.collider.gameObject;
}
return null;
}
private List<GameObject> GetAllAdjacentTiles() {
List<GameObject> adjacentTiles = new List<GameObject>();
for (int i = 0; i < adjacentDirections.Length; i++) {
adjacentTiles.Add(GetAdjacent(adjacentDirections[ i ]));
}
return adjacentTiles;
}
?
其中GetAdjacent方法用于檢測某個固定方向是否存在方格,如果有,則返回此方格。GetAllAdjacentTiles方法則調(diào)用GetAdjacent來生成圍繞當(dāng)前方格的列表,該循環(huán)將遍歷各個方向與當(dāng)前方格相鄰的方格,并返回列表,以保證方格僅能與其相鄰方格進(jìn)行交換。
保存代碼后運(yùn)行場景,效果如下:
?
檢測相同方格進(jìn)行消除
消除可以拆解為幾個步驟,首先判斷是否出現(xiàn)3個及以上相連的同樣方格,如果有,則消除已匹配的方格,并填充新方格。然后重復(fù)此步驟直至沒有有效匹配。
在Tile腳本中新增以下代碼:
?
private List<GameObject> FindMatch(Vector2 castDir) {
List<GameObject> matchingTiles = new List<GameObject>();
RaycastHit2D hit = Physics2D.Raycast(transform.position, castDir);
while (hit.collider != null && hit.collider.GetComponent<SpriteRenderer>().sprite == render.sprite) {
matchingTiles.Add(hit.collider.gameObject);
hit = Physics2D.Raycast(hit.collider.transform.position, castDir);
}
return matchingTiles;
}
private void ClearMatch(Vector2[] paths) {
List<GameObject> matchingTiles = new List<GameObject>();
for (int i = 0; i < paths.Length; i++) { matchingTiles.AddRange(FindMatch(paths[i])); }
if (matchingTiles.Count >= 2) {
for (int i = 0; i < matchingTiles.Count; i++) {
matchingTiles[i].GetComponent<SpriteRenderer>().sprite = null;
}
matchFound = true;
}
}
private bool matchFound = false;
public void ClearAllMatches() {
if (render.sprite == null)
return;
?
FindMatch方法接收一個Vector2參數(shù),用于表示所有射線投射的方向,新建GameObject列表來保存所有匹配條件的方格,從方格朝參數(shù)方向投射射線,直至射線未碰撞到任何方格或與當(dāng)前方格不一致時停止,然后返回匹配條件的Sprite列表。
ClearMatch方法會按照給定路徑尋找相同的方格,并相應(yīng)消除所有匹配的方格。即判斷FindMatch方法返回的列表中,是否有相連為直線的3個及以上相同方格。如果有,則將matchFound設(shè)為True。ClearAllMatch方法會在找到滿足條件的匹配后,刪除所有匹配的方格。
運(yùn)行游戲,效果如下:
?
填充空白方格
在消除方格后,還需要為其填充新的方格。在BoardManager腳本中加入以下代碼:
?
public IEnumerator FindNullTiles() {
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
if (tiles[x, y].GetComponent<SpriteRenderer>().sprite == null) {
yield return StartCoroutine(ShiftTilesDown(x, y));
break;
}
}
}
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
tiles[x, y].GetComponent<Tile>().ClearAllMatches();
}
}
}
private IEnumerator ShiftTilesDown(int x, int yStart, float shiftDelay = .03f) {
IsShifting = true;
List<SpriteRenderer> renders = new List<SpriteRenderer>();
int nullCount = 0;
for (int y = yStart; y < ySize; y++) {
SpriteRenderer render = tiles[x, y].GetComponent<SpriteRenderer>();
if (render.sprite == null) {
nullCount++;
}
renders.Add(render);
}
for (int i = 0; i < nullCount; i++) {
GUIManager.instance.Score += 50; // Add this line here
yield return new WaitForSeconds(shiftDelay);
for (int k = 0; k < renders.Count - 1; k++) {
renders[k].sprite = renders[k + 1].sprite;
renders[k + 1].sprite = GetNewSprite(x, ySize - 1);
}
}
IsShifting = false;
}
private Sprite GetNewSprite(int x, int y) {
List<Sprite> possibleCharacters = new List<Sprite>();
possibleCharacters.AddRange(characters);
if (x > 0) {
possibleCharacters.Remove(tiles[x - 1, y].GetComponent<SpriteRenderer>().sprite);
}
if (x < xSize - 1) {
possibleCharacters.Remove(tiles[x + 1, y].GetComponent<SpriteRenderer>().sprite);
}
if (y > 0) {
possibleCharacters.Remove(tiles[x, y - 1].GetComponent<SpriteRenderer>().sprite);
}
return possibleCharacters[Random.Range(0, possibleCharacters.Count)];
}
?
其中FindNullTiles方法用于查找是否存在空的方格,如果有,則調(diào)用ShiftTilesDown方法將周圍的方格填充進(jìn)來,該方法有三個參數(shù),分別是X索引,Y索引以及延遲時間,X、Y決定了哪一塊方格需要移動,這里僅實現(xiàn)向下填充,所以X值是固定了,僅Y值會變。GetNewSprite方法將生成新的方塊來填滿整個底板。
?
連擊
新填充的方格可能會再次出現(xiàn)符合條件的匹配,所以新填充底板后要再次進(jìn)行判斷。再找到匹配后再次匹配成功,就是一次連擊。所以在上面的FindNullTiles方法中,通過以下代碼循環(huán)判斷是否出現(xiàn)匹配:
?
for (int x = 0; x < xSize; x++) {
for (int y = 0; y < ySize; y++) {
tiles[x, y].GetComponent<Tile>().ClearAllMatches();
}
}
?現(xiàn)在運(yùn)行游戲,效果如下:
?
添加計步器與分?jǐn)?shù)
下面實現(xiàn)玩家步數(shù)記錄,并統(tǒng)計游戲分?jǐn)?shù)。打開Scripts/Managers文件夾下的GUIManager腳本,該腳本用于管理游戲UI,顯示步數(shù)及分?jǐn)?shù)文本。腳本代碼如下:
?
public static GUIManager instance;
public GameObject gameOverPanel;
public Text yourScoreTxt;
public Text highScoreTxt;
public Text scoreTxt;
public Text moveCounterTxt;
private int score, moveCounter;
void Awake() {
instance = GetComponent<GUIManager>();
moveCounter = 99;
}
// Show the game over panel
public void GameOver() {
GameManager.instance.gameOver = true;
gameOverPanel.SetActive(true);
if (score > PlayerPrefs.GetInt("HighScore")) {
PlayerPrefs.SetInt("HighScore", score);
highScoreTxt.text = "New Best: " + PlayerPrefs.GetInt("HighScore").ToString();
} else {
highScoreTxt.text = "Best: " + PlayerPrefs.GetInt("HighScore").ToString();
}
yourScoreTxt.text = score.ToString();
}
public int Score {
get {
return score;
}
set {
score = value;
scoreTxt.text = score.ToString();
}
}
public int MoveCounter {
get {
return moveCounter;
}
set {
moveCounter = value;
if (moveCounter <= 0) {
moveCounter = 0;
StartCoroutine(WaitForShifting());
}
moveCounterTxt.text = moveCounter.ToString();
}
}
private IEnumerator WaitForShifting() {
yield return new WaitUntil(() => !BoardManager.instance.IsShifting);
yield return new WaitForSeconds(.25f);
GameOver();
}
?
在Awake中獲取腳本引用,并初始化步數(shù)。Score及MoveCounter函數(shù)用于在每次更新分?jǐn)?shù)值或步數(shù)時,UI界面上的文本也會同時更新。當(dāng)步數(shù)減少至0時,游戲結(jié)束。此時會通過WaitForShifting協(xié)程在等待0.25秒后調(diào)用GameOver方法,并在GameOver方法中顯示游戲結(jié)束面板。這里的等待是為了確保所有連擊都被計算在總分內(nèi)。
?
總結(jié)
到這里本篇教程就結(jié)束了,當(dāng)然大家還可以在理解游戲機(jī)制后添加更多的玩法,包括限時結(jié)算模式、增加不同關(guān)卡與底板類型、連擊的積分計算規(guī)則,或是為消除方格添加一些酷炫的粒子效果等等。后面就留給大家自行擴(kuò)展與發(fā)揮了!
-
回復(fù)(0) 回復(fù) (0)