前言
玩遊戲也能學習知識?還記得高中時的化學元素常見金屬活動性屬性表嗎?一起來複習一下:鉀K,鈣Ca,鈉Na,鎂Mg,鋁Al,鋅Zn,鐵Fe, 錫Ni,鉛Sn,氫(H),銅Cu,汞Hg,銀Ag,鉑Pt,金Au。 一股很熟悉的味道有沒有?一起來看看化學元素和遊戲之間發生的碰撞吧~
一,遊戲介紹和效果展示
2048 一款益智小遊戲,遊戲的規則十分簡單,簡單易上手的數字小遊戲,閑來無事,自己制作一個,卻怎麼也到沒有成功到2048。
老規矩,先看下按照此博文一步步操作完成的效果吧~

二,開發前的準備工作
2.1 建立工程
打開Unity Hub 點選“建立”,在彈窗中輸入 --> "項目名稱" --> 選擇"項目位置" --> "項目版本控制系統" 選不選都行,不需要在雲端備份的話就不要選(我一般不選)
,選了沒用過的話,會自動幫你安裝PlasticSCM到本地。最後點選建立即可。
2.2 導入素材
将提前準備好的素材(圖檔和聲音)導入工程。(文末會提供下載下傳位址)
- 方式一: 在"Project"面闆右鍵 --> "Improt Package" --> "Custom Package..." --> 選擇自己下載下傳下來的UnityPackage --> 最後點選"Import" 導入。
- 方式二: 直接将下載下傳還的UnityPackage,拖拽到項目中,然後點選"Import" 導入:
2048化學元素版?一文教你Unity零基礎制作2048 !前言一,遊戲介紹和效果展示二,開發前的準備工作三,遊戲開發進行中四,遊戲完成源碼分享結語
導入項目後工程目錄如下:
三,遊戲開發進行中
3.1 遊戲場景搭建
- 設定分辨率:點選Game視圖分辨率選框,設定為(1080x1920),沒有的話點選加号自行添加一個,或者使用一個豎屏的就可以。
2048化學元素版?一文教你Unity零基礎制作2048 !前言一,遊戲介紹和效果展示二,開發前的準備工作三,遊戲開發進行中四,遊戲完成源碼分享結語 - 建立背景:右鍵UI --> Image 建立後,重命名為BG,将其錨點設定為鋪滿,源檔案指定為素材"play_bg_forest_dark":
- 修改Canvas:将自動建立出來的Canvas的Canvas Scaler 屬性設定如下:
- 建立格子: 在“Canvas”下建立Image,重命名為“MapBg”,将其寬高設定為(1720,1720),然後修改其顔色為淺藍色,透明度相應調低,效果如下:(自己覺得好看就可以了)
然後在“MapBg”下再次建立一個Image,重命名為“gezi”,将其寬高設定為(400,388),調整其顔色為(22,0,192,30),還是一樣調整到自己喜歡的顔色即可。然後“Ctrl + D” 複制15個格子出來:
最後在“MapBg”上添加"Grid Layout Group",屬性設定如下圖,将格子鋪滿到地圖背景上:
- 建立數字池: 右鍵建立一個空物體作為加載數字的父物體,并命名為“NumPool”,所有屬性預設即可。
- 建立數字預制體: 右鍵Image重命名為Num,寬高調整為和格子一樣大(400,380),然後将其拽到Resources檔案夾下,作為預制體,然後右鍵删除場景中Num即可:
- 遊戲結束面闆:建立Image命名為“UIFinsh”,鋪滿背景。在“UIFinsh”下在建立一個Text和一個Button,作為遊戲結束顯示文本和重新開始按鈕:
2048化學元素版?一文教你Unity零基礎制作2048 !前言一,遊戲介紹和效果展示二,開發前的準備工作三,遊戲開發進行中四,遊戲完成源碼分享結語
3.2 核心代碼編寫
場景終于搭建完了,下面開始編寫腳本吧,完整代碼在四中給出,這裡隻講解實作思路,檢視核心邏輯。
- 在Project下建立Scripts檔案夾,然後建立"Manager.cs" 和 "Number.cs" 腳本。
- 基礎邏輯就是:預設生成兩個數字,接收使用者輸入,管理器觸發移動邏輯,數字移動并校驗合并,移動後校驗是否遊戲結束,若結束處理遊戲結束摞,若未結束在自動生成一個數字:
graph TD
預設生成數字 --> 管理器接收使用者輸入
--> 管理器觸發數字移動邏輯 --> 數字移動并校驗合并
--> 移動後校驗遊戲結束 --結束--> 處理遊戲結束邏輯
移動後校驗遊戲結束 --繼續--> 預設生成數字
- Manager中接收使用者輸入的方式一種是在移動端的滑動,一種是在PC端的按下剪頭或WASD上下左右移動:
- Manager收到使用者輸入後的控制遊戲數字移動邏輯:
- Num類收到Manager的移動,合并邏輯處理:
- 最後将Manager挂載到“BG”,并對公有變量'PoolManager'和'UIFinsh'指派,如下圖即可:
- 将Number腳本挂載到上面制作的預制體“Num”上即可。
此時所有的遊戲邏輯就都完成,運作遊戲就可以玩耍了~
3.3 遊戲音效處理
音效是這一個遊戲的很重要的部分,一個好的音效可以讓使用者得到更好的回報,是以再簡單的遊戲也要有背景音和音效給使用者一個好的互動體驗。
- 背景音樂:在“BG”上,添加元件“Audio Source”,将音頻檔案“bg_1”指派給AudioClip并勾選Loop,即可完成自動循環播放背景音樂:
- 數字音效:同理在“Num”預制體上添加元件“Audio Source”,将音頻檔案“num”指派給AudioClip,其他屬性預設即可。
四,遊戲完成源碼分享
4.1 Manager遊戲管理類
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Manager : MonoBehaviour
{
public static Manager _isnstance; //單例模式的引用
public Transform poolManager; //生成數字的池子
private GameObject numPrefab; //數字的預制體
public Number[,] numbers = new Number[4, 4]; //儲存方格中的數組
//正在移動中的Num
public List<Number> isMovingNum = new List<Number>();
public bool hasMove = false; //是否有數字發生了移動
public GameObject UIFinsh; //遊戲結束頁面
void Awake()
{
_isnstance = this;
}
void Start()
{
numPrefab = Resources.Load<GameObject>("Num");
// 開始遊戲
ReStartBtn();
// 遊戲結束面闆按鈕監聽,重新開始
UIFinsh.GetComponentInChildren<Button>().onClick.AddListener(ReStartBtn);
}
// 重新開始
void ReStartBtn()
{
isMovingNum.Clear();
numbers = new Number[4, 4];
for (int i = poolManager.childCount - 1; i >= 0; i--)
{
Destroy(poolManager.GetChild(i).gameObject);
}
hasMove = false;
//遊戲開始生成兩個數字
CreateNun();
CreateNun();
UIFinsh.SetActive(false);
}
void CreateNun()
{
GameObject go = Instantiate(numPrefab);
go.transform.parent = poolManager;
go.transform.localScale = Vector3.one;
}
#region 檢測鍵盤和觸摸輸入
void Update()
{
//--------- 移動端檢測邏輯 ---------
//有觸摸點,且滑動
if (isMovingNum.Count == 0)
{
if (Input.touchCount > 0 && Input.GetTouch(0).phase == TouchPhase.Moved)
{
int dieX = 0;
int dieY = 0;
//擷取滑動的距離
Vector2 touchDelPos = Input.GetTouch(0).deltaPosition;
if (Mathf.Abs(touchDelPos.x) > Mathf.Abs(touchDelPos.y))
{
//滑動距離
if (touchDelPos.x > 10)
{
dieX = 1;
}
else
if (touchDelPos.x < -10)
{
dieX = -1;
}
}
else
{
if (touchDelPos.y > 10)
{
dieY = 1;
}
else if (touchDelPos.y < -10)
{
dieY = -1;
}
}
MoveNum(dieX, dieY);
}
}
//--------- PC端檢測邏輯 ---------
if (isMovingNum.Count == 0)
{
int dieX = 0;
int dieY = 0;
if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
{
dieX = -1;
}
else
if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
{
dieX = 1;
}
else
if (Input.GetKeyDown(KeyCode.W) || Input.GetKeyDown(KeyCode.UpArrow))
{
dieY = 1;
}
else
if (Input.GetKeyDown(KeyCode.S) || Input.GetKeyDown(KeyCode.DownArrow))
{
dieY = -1;
}
MoveNum(dieX, dieY);
}
if (hasMove && isMovingNum.Count == 0) //生成新的數字
{
CreateNun();
hasMove = false;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
if (numbers[i, j] != null)
{
numbers[i, j].OneMove = false;
}
}
}
}
}
#endregion
#region 遊戲邏輯
/// <summary>
/// 數字移動方法
/// </summary>
/// <param name="directionX"></param>
/// <param name="directionY"></param>
public void MoveNum(int directionX, int directionY)
{
if (directionX == 1) //向右移動
{
//首先将空格填滿 最右側列不需做判斷
for (int j = 0; j < 4; j++)
{
for (int i = 2; i >= 0; i--)
{
if (numbers[i, j] != null) //格子中有物體(數字),,調用移動方法
{
numbers[i, j].Move(directionX, directionY);
}
}
}
}
else
//===========向左移動==================
if (directionX == -1)
{
for (int j = 0; j < 4; j++)
{
for (int i = 1; i < 4; i++)
{ //最左側的一列 [0,0] [0,1] [0,2] [0,3]
if (numbers[i, j] != null)
{
numbers[i, j].Move(directionX, directionY);
}
}
}
}
else
//===========向上移動==================
if (directionY == 1)
{
for (int i = 0; i < 4; i++)
{
for (int j = 2; j >= 0; j--)
{
if (numbers[i, j] != null)
{
numbers[i, j].Move(directionX, directionY);
}
}
}
}
else
//===========向下移動==================
if (directionY == -1)
{
for (int i = 3; i >= 0; i--)
{
for (int j = 0; j < 4; j++)
{
if (numbers[i, j] != null) //有物體(數字)就移動
{
numbers[i, j].Move(directionX, directionY);
}
}
}
}
}
/// <summary>
/// 判斷是否是空格的方法
/// </summary>
/// <param name="x">數組索引X</param>
/// <param name="y">數組索引Y</param>
/// <returns></returns>
public bool isEmpty(int x, int y)
{
if (x < 0 || x > 3 || y < 0 || y > 3)
{
return false;
}
else if (numbers[x, y] != null)
{
return false;
}
return true;
}
/// <summary>
/// 判斷遊戲是否結束
/// </summary>
/// <returns>傳回true則遊戲結束</returns>
public bool isDead()
{
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
if (numbers[i, j] == null)
{
return false;
}
}
}
for (int j = 0; j < 4; j++)
{
for (int i = 0; i < 3; i++)
{
if (numbers[i, j].value == numbers[i + 1, j].value)
{
return false;
}
}
}
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 3; j++)
{
if (numbers[i, j].value == numbers[i, j + 1].value)
{
return false;
}
}
}
return true;
}
#endregion
/// <summary>
/// 遊戲結束
/// </summary>
/// <param name="isSuccess">false:輸,true:赢</param>
public void ShowUIFinsh(bool isSuccess)
{
UIFinsh.SetActive(true);
if (isSuccess)
{
UIFinsh.GetComponentInChildren<Text>().text = "遊戲成功";
}
else
{
UIFinsh.GetComponentInChildren<Text>().text = "遊戲失敗";
}
}
}
4.2 Number數字處理類
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine;
using UnityEngine.UI;
using Vector3 = UnityEngine.Vector3;
public class Number : MonoBehaviour
{
//在二維數組中的位置X,Y
public int posX;
public int posY;
private int offsetX = -620; //顯示偏移,Y,,,
private int offsetY = -620;
private int space = 420; // 間距
private bool isMoving = false; //動畫是否播放過的計數
public int value; //産生數字是幾
private bool toDestroy; //判斷數字是否銷毀
public bool OneMove = false; //辨別數字是否合并過一次
// Use this for initialization
void Start()
{
// 80%成2的機率,更改本身的Sprite名字,以更換圖檔
value = Random.value > 0.2f ? 2 : 4;
this.GetComponent<Image>().sprite = LoadSprite();
do
{
posX = Random.Range(0, 4);
posY = Random.Range(0, 4);
} while (Manager._isnstance.numbers[posX, posY] != null);
transform.localPosition = GetLocalPos();
// 存放數字本身到數組中,表示此位置有數字不能生成新的數字
Manager._isnstance.numbers[posX, posY] = this;
if (Manager._isnstance.isDead())
{
// 遊戲失敗
Manager._isnstance.ShowUIFinsh(false);
}
}
// Update is called once per frame
void Update()
{
//播放一次動畫
if (!isMoving)
{
if (transform.localPosition != GetLocalPos())
{
isMoving = true;
StartCoroutine(MoveAni());
}
}
}
// 移動動畫
IEnumerator MoveAni()
{
Debug.Log("移動動畫...");
float t = 0;
for (int i = 0; i < 10; i++)
{
transform.localPosition = Vector3.Lerp(transform.localPosition, GetLocalPos(), t);
t += 0.1f;
yield return new WaitForEndOfFrame();
}
// 移動結束的回調
MoveOver();
}
#region 遊戲核心移動算法
/// <summary>
/// 核心,移動方法(有空格,有物體是否一樣)
/// </summary>
public void Move(int directionX, int directionY)
{
//Debug.Log("測試");
//==========向右移動==================
if (directionX == 1)
{
int index = 1; // 空格标志
while (Manager._isnstance.isEmpty(posX+index,posY))
{
index++;
}
// 有空格的移動
if (index>1)
{
if (!Manager._isnstance.isMovingNum.Contains(this))
{ // 保證不會重複添加物體(數字)到清單,
Manager._isnstance.isMovingNum.Add(this);
}
//移動一次,就生成兩個數字的标志符
Manager._isnstance.hasMove = true;
//向空格位置移動
Manager._isnstance.numbers[posX, posY] = null;
posX = posX + index - 1;
Manager._isnstance.numbers[posX, posY] = this;
}
//有相同數字的移動
if (posX < 3 && value == Manager._isnstance.numbers[posX+1,posY].value &&
!Manager._isnstance.numbers[posX+1,posY].OneMove)
{
// 隻合并一次的标志
Manager._isnstance.numbers[posX + 1, posY].OneMove = true;
// 移動的标志,(生成新的物體(數字))
Manager._isnstance.hasMove = true;
// 動畫播放的限定(有數字在清單中就不會重複播放第二次動畫)
// 不會重複添加物體(數字)到清單,
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
// 碰到一樣的數字,講位置設為空 并銷毀本身辨別(true),
// 再将其位置上的值變為2倍,(更換成新的數字)
toDestroy = true;
Manager._isnstance.numbers[posX, posY] = null;
Manager._isnstance.numbers[posX + 1, posY].value *= 2;
posX += 1;
}
}else
//===========向左移動==================
if (directionX == -1)
{
int index = 1;
while (Manager._isnstance.isEmpty(posX - index, posY))
{
index++;
}
//有空格的移動
if (index > 1)
{
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
Manager._isnstance.numbers[posX, posY] = null;
posX = posX - index + 1;
Manager._isnstance.numbers[posX, posY] = this;
}
//碰到相同數字的移動
if (posX > 0 && value == Manager._isnstance.numbers[posX - 1, posY].value &&
!Manager._isnstance.numbers[posX - 1, posY].OneMove)
{
Manager._isnstance.numbers[posX - 1, posY].OneMove = true;
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
toDestroy = true;
Manager._isnstance.numbers[posX, posY] = null;
Manager._isnstance.numbers[posX - 1, posY].value *= 2;
posX -= 1;
}
}else
//===========向上移動==================
if (directionY == 1)
{
int index = 1; //空格标志
while (Manager._isnstance.isEmpty(posX , posY + index))
{
index++;
}
//有空格的移動
if (index > 1)
{
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
Manager._isnstance.numbers[posX, posY] = null;
posY = posY + index - 1;
Manager._isnstance.numbers[posX, posY] = this;
}
//有相同位置的移動
if (posY < 3 && value == Manager._isnstance.numbers[posX , posY + 1].value && !Manager._isnstance.numbers[posX, posY + 1].OneMove)
{
Manager._isnstance.numbers[posX , posY + 1].OneMove = true;
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
toDestroy = true;
Manager._isnstance.numbers[posX, posY] = null;
Manager._isnstance.numbers[posX , posY + 1].value *= 2;
posY += 1;
}
}else
//===========向下移動==================
if (directionY == -1)
{
int index = 1; //空格标志位
while (Manager._isnstance.isEmpty(posX, posY - index))
{
index++;
}
//有空格的移動
if (index > 1)
{
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
Manager._isnstance.numbers[posX, posY] = null;
posY = posY - index + 1;
Manager._isnstance.numbers[posX, posY] = this;
}
//有相同數字的移動
if (posY > 0 && value == Manager._isnstance.numbers[posX, posY - 1].value && !Manager._isnstance.numbers[posX, posY - 1].OneMove)
{
Manager._isnstance.numbers[posX, posY -1].OneMove = true;
Manager._isnstance.hasMove = true;
if (!Manager._isnstance.isMovingNum.Contains(this))
{
Manager._isnstance.isMovingNum.Add(this);
}
toDestroy = true;
Manager._isnstance.numbers[posX, posY] = null;
Manager._isnstance.numbers[posX, posY - 1].value *= 2;
posY -= 1;
}
}
}
#endregion
/// <summary>
/// 動畫結束,标志改為false
/// </summary>
public void MoveOver()
{
isMoving = false;
//若碰到了相同的數字 銷毀自己,和改變另一個圖檔(數字)
if (toDestroy)
{
Destroy(this.gameObject);
value = Manager._isnstance.numbers[posX, posY].value;
Manager._isnstance.numbers[posX, posY].GetComponent<Image>().sprite = LoadSprite();
//遊戲成功
if (value == 4096)
{
Manager._isnstance.ShowUIFinsh(true);
}
}
Manager._isnstance.isMovingNum.Remove(this);
}
Vector3 GetLocalPos()
{
return new Vector3(offsetX + posX * space, offsetY + posY * space, 0);
}
/// <summary>
/// 根據數字加載對應圖檔
/// </summary>
/// <returns></returns>
Sprite LoadSprite()
{
return Resources.Load<Sprite>(value.ToString());
}
}
結語
本文從建立項目開始一步一步帶你完成所有步驟,還在等什麼呢?三連支援一下吧~