項目展示
Github項目地址:Pacman
試玩下載:Pacman 吃豆人 提取碼brkv
涉及知識
- 切片制作 Animations
- 狀態機設置,any state切換,重寫狀態機
- 按鍵讀取進行整數距離的剛體移動
- 用射線檢測碰撞性
- 渲染順序問題
- 單、多路徑的實現
- 協程延時
- Button 按鍵功能
准備工作
Pixels Per Unit:多少像素相當於Unity一個單位,迷宮Maze大小232x256,
Pivot:設置貼圖的零點,Bettom Left左下角
物理化:牆,import package->custom package,導入已經設置好碰撞體的牆
pacman切圖,動畫片段:Sprite Mode->multiple,Pixels Per Unit=8,進行Sprite Editor,顯示其窗口。選擇Slice切片,Type為Grid By Cell Count,切割參數3行4列,Apply后可在Pacman下面看到切割好的12張照片。
動作制作:12張照片每3張為一個動作,分別是右,左,上,下,每次將3張拖入Hierarchy面板,保存在Animations文檔下,各自命名。可在Project面板Animations文件夾下包含4個動畫文件,說明每次保存的3張圖片生成一個動畫,還包含4個動畫機(但只需要設置一個。其余可刪除)
初始狀態機設置
狀態機:在主角Pacman內添加Animator組件,添加上述留下的動畫機,打開Animator頁面,看到4個組件,初始情況為:當游戲物體進入狀態機,默認狀態轉變為PacmanRight;后拖拽其他3個狀態到狀態機頁面
分析主角移動:僅僅能橫x縱y向移動,當持續按住某方向鍵位,速度為每0.3s移動 1 Unit
**
Any State:**切換連接4個狀態,點擊連線可看到說明:無論在任何狀態只要達到連線內條件,即轉變狀態到所指對象狀態
Any State 切換條件:在Parameters內添加float型DirX,DirY值用來判斷(持續按鍵產生的是浮點數)。例如PacmanRight的判斷,添加DirY,當DirY>0.1(浮點數不精確性質,留一定范圍)。並且取消狀態機Settings內Can Transition To(考慮到幀數問題,和重復播放初始動作問題),其次2D動畫將融合調0
其他:可以調節動畫的speed以調節播放該動作的速度
吃豆人 Pacman
吃豆人的實體化:
- 加碰撞器,circle collider,添加rigdbody2D,設置重力0
吃豆人的移動方法:
- 修改transform瞬移,修改坐標,多用在生成位置
- rigidbody2D移動,物理移動,推薦使用
具體實現移動過程:
- 調用
Vector2.MoveTowards(transform.position,dest,speed)
,使得返回起始點到目標點的中間值,另設temp
接收這個值;再對剛體進行移動操作GetComponent<Rigidbody2D>().MovePosition(temp);
- Vector2.MoveTowards(transform.position,dest,speed):表示以 浮點數型 speed 的速度,從起點 transform.position 移動到終點 dest ,返回的值為兩點坐標的中間值
- 初始時
transform.position = test
,故不會移動,因此需要按鍵檢測以改變dest
的值:Input.GetKey(KeyCode.UpArrow)
或者Input.GetKey(KeyCode.W)
實現讀取鍵位- 然后賦值
dest:(Vector2)transform.position + Vector2.up;
實現吃豆人每次單位移動:(Vector2)transform.position + Vector2.up
,表示 當前坐標position 加 向上一個單位量;每次讀取鍵盤方向信息,將當前坐標 + 某方向單位量 = 目的地位置
產生問題1:此時移動會造成吃豆人旋轉問題
原因:Pacman 與牆壁碰撞時Z軸坐標改變造成旋轉
解決:凍結Z軸 Rigdibody2D->Constraints->Freeze Rotation Z
產生問題2:卡槽間移動容易卡住,非規則移動
原因:問題在於按鍵過程時刻改變dest ,造成 temp = Vector2.MoveTowards()的時刻改變
解決:判斷當上一個dest抵達時才讀取新的鍵位if ((Vector2)transform.position == dest)
產生問題3:首次測試按鍵移動發現撞牆后就不可再移動
原因:抵達牆邊時,鍵盤讀取的dest到了牆以外,if判斷永遠無法transform.position==dest,無法在鍵盤讀取
解決:檢測目的地合法性
//檢查目的地是否合法 dir方向值(上述的Vector.XXX)
private bool Valid(Vector2 dir)
{
//pos 存儲當前位置(牆內的合法位置)
Vector2 pos = transform.position;
//從 當前值pos+方向值dir 的位置發射一條射線到Pacman 當前的位置pos
RaycastHit2D hit = Physics2D.Linecast(pos + dir, pos);
//射線打到的碰撞器 是否等於 吃豆人的碰撞器:
//若射線從牆中心(不合法位置)射出,hit.collider為牆的,不等於Pacman的,返回fault
//若射線從路面(合法位置)射出,hit.collider等於Pacman的,返回true
return (hit.collider == GetComponent<Collider2D>());
}
狀態機的切換
實現不同動作狀態機的切換:
- 獲取移動的方向:
Vector2 dir = dest - (Vector2)transform.position;
- 把獲取到的方向設置到狀態機:
GetComponent<Animator>().SetFloat("DirX", dir.x);
2D游戲Z軸問題:若在不同層級碰撞功能失效,若在同一層級,則存在渲染順序問題(誰覆蓋誰)
渲染順序問題:Sprite Renderer->Order in Layer
- 小的先渲染,大的后渲染:迷宮Maze 0,豆子pacdot 1,敵人 2~5,Pacman 6
- 小的被覆蓋,大的覆蓋:先渲染的存在於底層,后渲染的位於上層(類似ppt中的圖層)
豆子及敵人的創建、移動
豆子:
- Pacdot 即為豆子圖標,拖入頁面內創建對象
- 對其添加碰撞器 Box Collider 2D,設置為觸發器
- 對所有Pacdot添加腳本
Pacdot.cs
- 腳本內創建碰撞檢測 :
OntriggerEnter2D(Collider2D collision)
函數用來檢測觸發 Pacdot 的物體是否為Pacman ,是則銷毀Pacdot
- 腳本內創建碰撞檢測 :
敵人的創建:
- 重復切圖,合成動作,設置圖層,安放位置
- 關於狀態機,采用 重寫狀態機:
- 在 Animation文檔內 create->Animator Override Controller,設置狀態機參照 Controller 為 Pacman的,可看到 Original 為 Pacman內的動作,Override 內的就設置為每個敵人的不同動作(注意,刪除原有的狀態機使得物體的 Animator 內 Controller 找不到狀態機組件,此時需要將重寫后的狀態機設置到它身上
- 再設置 Rigidbody 和 CircleCollider(注意此處要設置為觸發器Trriger而不是碰撞器,范圍0.8
敵人的移動(單路徑):
- 創建與豆子坐標一致的、始末位置同點的閉合路徑(用空物體作路徑點即可),統一儲存於 way 結構內作為一條路徑
- 編寫
EnemyMove.cs
- 創建循環隊列保存所有路徑點
Transform[] WayPoint
,及index
標記敵人在前往哪個路徑點的途中; - 創建
FixedeUpdata()
,判斷:若怪物沒抵達目標位置,則從當前位置持續移動直到抵達(同Pacman的移動,但沒有輸入檢測),若抵達,則index++,前往下一個點;此外動畫狀態的檢測、切換也同Pacman的; - 設置觸發檢測:當檢測到觸發的物體是 Pacman ,則銷毀 Pacman
- 創建循環隊列保存所有路徑點
- 在Unity頁面將全部路徑點拖拽到怪物腳本的Way Point處實現賦值數組
敵人的移動(多路徑):高級的方法是AI,但本例采用多路線隨機分配實現
- 先創建對象
wayPointsGo
用來接收為預制體的路線Way
; - 創建表
wayPoints
,在 start() 內調用foreach方法,將wayPointGo
內的組件取出到t
,將t.position
坐標順序添加到wayPoints
表內(還需修改FixedUpdate()函數內: wayPoints[index] 此時為表,存儲的是坐標,無需再.position,后續的wayPoints.Length也改為了wayPoints.Count),由此實現了一條路徑; - 修改EnemyMove.cs,游戲對象 wayPointsGo 改為數組形式 實現存儲多路線
- 根據前面路徑Way預制體的制作方法,再次制作多條路徑
private void LoadApath(GameObject go)
{
//將wayPointsGo數組內某一路徑的子物體(路徑點)的Transform組件取出,依次將其position賦值到Ways表中
//修改為多路徑后隨機從5條路徑走
foreach (Transform t in wayPointsGo[Random.Range(0, 4)].transform)
{
wayPoints.Add(t.position);
}
}
- 創建LoadApath函數:首先清除上調路徑遺留再List中的信息,后foreach()加載路徑到List內,而后再Start內每次調用隨機,傳入隨機一條路徑。
產生問題1:不同怪物出門都與 Blinky紅色敵人 同一點問題
原因:因為做預制體way1,way2,...,wayn 時,路徑始末兩點坐標都是 Blinky紅色敵人 上方3個單位,所以其他敵人起始移動就會先進行穿牆到那個點
解決:修改 EnemyMove.cs
,創建一個坐標變量 startPos
用在存放每個敵人路徑的始末位置點,在 Start()
函數內初始化設置 satrtPos
為怪物起始坐標+向上3個單位;再后續 foreach()
內插入該點到List表頭,及在List末尾添加該點,但注意每次要調用LoadApath()函數要清除上一次路徑信息
//清空List內前次路徑的信息
wayPoints.Clear();
//添加首末路徑點到List內
wayPoints.Insert(0, startPos);
wayPoints.Add(startPos);
產生問題2:即便隨機路徑下不同怪物選到同路徑問題
原因:每個敵人進行 Random.Range(0,n)
隨機分配路徑時可能抽到一樣的隨機數
解決:添加 GameManager.cs
,調用如下代碼
public class GameManager : MonoBehaviour
{
private static GameManager _instance;
public static GameManager Instance
{
get
{
return _instance;
}
}
public List<int> usingIndex = new List<int>();
public List<int> rawIndex = new List<int> { 0, 1, 2, 3};
private void Awake()
{
_instance = this;
int tempCount = rawIndex.Count;
for (int i = 0; i < tempCount; i++)
{
int tempIndex = Random.Range(0, rawIndex.Count);
usingIndex.Add(rawIndex[tempIndex]);
rawIndex.RemoveAt(tempIndex);
}
}
}
//再修改EnemyMove,Start內
LoadApath(wayPointsGo[GameManager.Instance.usingIndex[GetComponent<SpriteRenderer>().sortingOrder - 2]]);
超級豆子
超級豆子的生成:
- GameManager.cs內:
- 創建pacdotGos,foreach()存儲所有豆子;
- 生成超級豆子:CreateSuperPacdot()
- 創建布爾變量isSuperPacman(初始為false);
- Pacdot.cs內:
- 修改碰撞觸發判定:if(是超吃豆人狀態){} else 被敵人消滅
- EnemyMove.cs內:
- 修改碰撞觸發判定:if(碰到的是超級吃豆人){} else 消滅吃豆人
超級豆子帶來的超級吃豆人狀態:敵人靜止,且可以被吃掉
- Pacman的超級狀態:OnEatSuperPacdot()
GameManager.cs
內添加布爾變量isSuperPacman
判定是否超級狀態- 當吃到超級豆子后啟用該函數,變更狀態標記
isSuperPacman = true
- 啟用靜止敵人函數
FreeEnemy()
(下有說明) - 實現保持超級狀態4s:協程延時
StartCoroutine(Recover());
(下面說明) - 4s時間結束后取消狀態(同在協程函數Recover()內進行)
- 敵人靜止: FreezeEnemy()
Blinky.GetComponent<EnemyMove>().enabled = false;
禁用怪物移動腳本的update方法Blinky.GetComponent<SpriteRenderer>().color = new Color(0.7f, 0.7f, 0.7f, 0.7f);
敵人圖標變暗淡
- 敵人被吃掉:
- 在敵人移動腳本
EnemyMove.cs
內檢測,若碰撞檢測到的 Pacman 是超級狀態GameManager.Instance.isSuperPacman
則自身回到出生點
- 在敵人移動腳本
協程延時:
- 協程的作用:當 啟動
OnEatSuperPacdot()
變更為超級狀態狀態時,啟用協程函數StartCoroutine(Recover())
,表示該協程函數與OnEateSuperPacdot()
同時啟用,並行運行 - 實現功能:在吃到超級豆子瞬間即開始計時,計時完后取消敵人靜止狀態
Dis_FreezeEnemy()
及恢復吃豆人狀態isSuperPacman = false
- 延時代碼:
yield return new WaitForSeconds(4f);
吃豆過程概述:
- 開局一段時間后生成超級豆子
- 豆子被吃掉后,從表內移除該豆子,銷毀對象,延時10s后准備下一個超級的生成,與此同時改變吃豆人狀態isSuperPacman=true(兩過程不干涉,並行執行)
- 若在超級吃豆人狀態期間吃到敵人,則敵人位置回歸到初始
- 持續超級吃豆人狀態4s后取消敵人的凍結,並調用狀態恢復函數
UI設計
Start 與 Exit 圖標:
- 創建
UI->Canvas
,作為UI工作區域; - 添加image作為logo;創建空物體命名StartPanel,包含2個
UI->text,start和exit
,修改字體,調整位置 - 創建空物體命名GamePanel,包含3個UI->text,remain,eaten,修改字體,score
- 倒計時321動畫:同理將素材文件Start切片,設置動作,修改每個動作間隔時長為1s(Animation->Samples)
- GameManager.cs 內持有UI面板及動畫、音樂
建立 Button 按鍵跳轉:
- 對
Start、Exit
兩UI添加Button(script)
組件,設置Target Graphic->Start,Exit
; - 在
GameManager.cs
內添加OnStartButton()
和OnExitButton()
對接按鍵腳本,如下圖代碼: - 啟用Button功能:對UI圖標添加
On Click->GameManager->OnStartButton
啟用該函數 - 其他:按鍵或者其他UI都必須存在於 畫布Canvas 內;畫布Canvas下一級是 畫板Panel ;在下一級就是各類UI組件
//當點擊 Start
public void OnStartButton()
{
//與點擊開始按鈕后同步進行的函數
StartCoroutine(PlayStartCountDown());
//Start聲音,聲音源在原點位置
AudioSource.PlayClipAtPoint(startClip, Vector3.zero);
//隱藏開始按鈕的頁面
startPanel.SetActive(false);
}
//點擊Exit后退出游戲
public void OnExitButton()
{
Application.Quit();
}
其他
網頁跳轉:
調用 Application.OpenURL("https://www.cnblogs.com/SouthBegonia/");
即可實現,可用在Button 也可用在其他觸發時間