Unity項目 - 吃豆人Pacman

項目展現

Github項目地址:Pacmangit

涉及知識

  • 切片製做 Animations
  • 狀態機設置,any state切換,重寫狀態機
  • 按鍵讀取進行整數距離的剛體移動
  • 用射線檢測碰撞性
  • 渲染順序問題
  • 單、多路徑的實現
  • 協程延時
  • Button 按鍵功能

準備工做

Pixels Per Unit:多少像素至關於Unity一個單位,迷宮Maze大小232x256,github

Pivot:設置貼圖的零點,Bettom Left左下角數組

物理化:牆,import package->custom package,導入已經設置好碰撞體的牆dom

pacman切圖,動畫片斷:Sprite Mode->multiple,Pixels Per Unit=8,進行Sprite Editor,顯示其窗口。選擇Slice切片,Type爲Grid By Cell Count,切割參數3行4列,Apply後可在Pacman下面看到切割好的12張照片。ide

動做製做: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以調節播放該動做的速度ui

吃豆人 Pacman

吃豆人的實體化

  • 加碰撞器,circle collider,添加rigdbody2D,設置重力0

吃豆人的移動方法

  1. 修改transform瞬移,修改座標,多用在生成位置
  2. rigidbody2D移動,物理移動,推薦使用

具體實現移動過程:

  1. 調用 Vector2.MoveTowards(transform.position,dest,speed) ,使得返回起始點到目標點的中間值,另設 temp 接收這個值;再對剛體進行移動操做GetComponent<Rigidbody2D>().MovePosition(temp);
    • Vector2.MoveTowards(transform.position,dest,speed):表示以 浮點數型 speed 的速度,從起點 transform.position 移動到終點 dest ,返回的值爲兩點座標的中間值
  2. 初始時 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中的圖層)

豆子及敵人的建立、移動

豆子:

  1. Pacdot 即爲豆子圖標,拖入頁面內建立對象
  2. 對其添加碰撞器 Box Collider 2D,設置爲觸發器
  3. 對全部Pacdot添加腳本 Pacdot.cs
    • 腳本內建立碰撞檢測OntriggerEnter2D(Collider2D collision) 函數用來檢測觸發 Pacdot 的物體是否爲Pacman ,是則銷燬Pacdot

敵人的建立:

  1. 重複切圖,合成動做,設置圖層,安放位置
  2. 關於狀態機,採用 重寫狀態機
    • 在 Animation文檔內 create->Animator Override Controller,設置狀態機參照 Controller 爲 Pacman的,可看到 Original 爲 Pacman內的動做,Override 內的就設置爲每一個敵人的不一樣動做(注意,刪除原有的狀態機使得物體的 Animator 內 Controller 找不到狀態機組件,此時須要將重寫後的狀態機設置到它身上
  3. 再設置 Rigidbody 和 CircleCollider(注意此處要設置爲觸發器Trriger而不是碰撞器,範圍0.8

敵人的移動(單路徑):

  1. 建立與豆子座標一致的、始末位置同點的閉合路徑(用空物體做路徑點便可),統一儲存於 way 結構內做爲一條路徑
  2. 編寫 EnemyMove.cs
    • 建立循環隊列保存全部路徑點 Transform[] WayPoint ,及index 標記敵人在前往哪一個路徑點的途中;
    • 建立 FixedeUpdata() ,判斷:若怪物沒抵達目標位置,則從當前位置持續移動直到抵達(同Pacman的移動,但沒有輸入檢測),若抵達,則index++,前往下一個點;此外動畫狀態的檢測、切換也同Pacman的;
    • 設置觸發檢測:當檢測到觸發的物體是 Pacman ,則銷燬 Pacman
  3. 在Unity頁面將所有路徑點拖拽到怪物腳本的Way Point處實現賦值數組

敵人的移動(多路徑):高級的方法是AI,但本例採用多路線隨機分配實現

  1. 先建立對象 wayPointsGo 用來接收爲預製體的路線 Way
  2. 建立 wayPoints ,在 start() 內調用foreach方法,將 wayPointGo 內的組件取出到 t,將 t.position 座標順序添加到wayPoints 表內(還需修改FixedUpdate()函數內: wayPoints[index] 此時爲表,存儲的是座標,無需再.position,後續的wayPoints.Length也改成了wayPoints.Count),由此實現了一條路徑;
  3. 修改EnemyMove.cs,遊戲對象 wayPointsGo 改成數組形式 實現存儲多路線
  4. 根據前面路徑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內:
    1. 建立pacdotGos,foreach()存儲全部豆子;
    2. 生成超級豆子:CreateSuperPacdot()
    3. 建立布爾變量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 圖標:

  1. 建立 UI->Canvas,做爲UI工做區域;
  2. 添加image做爲logo;建立空物體命名StartPanel,包含2個 UI->text,start和exit ,修改字體,調整位置
  3. 建立空物體命名GamePanel,包含3個UI->text,remain,eaten,修改字體,score
  4. 倒計時321動畫:同理將素材文件Start切片,設置動做,修改每一個動做間隔時長爲1s(Animation->Samples)
  5. GameManager.cs 內持有UI面板及動畫、音樂

創建 Button 按鍵跳轉:

  1. Start、Exit 兩UI添加 Button(script)組件,設置 Target Graphic->Start,Exit
  2. GameManager.cs內添加 OnStartButton()OnExitButton()對接按鍵腳本,以下圖代碼:
  3. 啓用Button功能:對UI圖標添加 On Click->GameManager->OnStartButton 啓用該函數
  4. 其餘:按鍵或者其餘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 也可用在其餘觸發時間

相關文章
相關標籤/搜索