1. 程式人生 > >Unity3d學習之路-簡單打靶遊戲

Unity3d學習之路-簡單打靶遊戲

簡單打靶遊戲

遊戲規則與遊戲要求

  • 規則
    使用WSAD鍵或者上下左右鍵移動弓箭,滑鼠點選射箭。每一關有十支箭,需要用這十支箭打靶,達到目標分數即可進入下一關,每次射出箭後會變換一次風向。

  • 要求:

    • 靶物件為 5 環,按環計分
    • 箭物件,射中後要插在靶上,射中後,箭物件產生顫抖效果,持續0.8秒
    • 新增一個風向和強度標誌,提高難度

遊戲UML類圖

這次遊戲相比原來的框架多了主副相機相關的類和靶的碰撞器檢測類

UML

遊戲實現

靶建立部分

靶子是由5個圓柱體組成,每個圓柱體上新增一個Mesh Collider元件官方文件介紹,勾選isTrigger做觸發器使用。而且掛載了CollisionDetectionRingData指令碼。RingData指令碼上只有一個分數屬性,代表了這一環的分數。(如果覺得環應該有其他屬性,可以使用之前的序列化自定義編輯器元件)。5個圓柱體作為一個空物件的孩子。最後預製體圖如下。

預製體

弓移動部分

這部分實現了當使用者按下鍵盤的WSAD或上下左右鍵時,弓箭移動,並且保持相機與弓箭的相對位置不變,實現相機跟隨弓箭的效果,並且弓箭移動範圍不是無限的,所以限定了一個移動範圍

  • UserGUI

在UserGUI的Update中,當遊戲進行時每一幀獲取是否按下方向鍵,使用Input.GetAxis官方文件介紹,得到按下方向鍵後的虛擬軸中的值,然後通過介面去呼叫場景控制器中移動弓的方法。相關程式碼如下。

void Update()
{
    if(game_start && !action.GetGameover())
    {
        if
(Input.GetButtonDown("Fire1")) { action.Shoot(); } //獲取方向鍵的偏移量 float translationY = Input.GetAxis("Vertical"); float translationX = Input.GetAxis("Horizontal"); //移動弓箭 action.MoveBow(translationX, translationY); } }
  • FirstSceneController

場景的控制器,繼承了IUserAction介面,並實現其方法,其中MoveBow方法實現了根據獲取的虛擬軸的值移動弓。相關程式碼如下。

public void MoveBow(float offsetX, float offsetY)
{
    //遊戲未開始時候不允許移動弓
    if (game_over || !game_start)
    {
        return;
    }
    //弓是否超出限定的移動範圍
    if (bow.transform.position.x > 5)
    {
        bow.transform.position = new Vector3(5, bow.transform.position.y, bow.transform.position.z);
        return;
    }
    else if(bow.transform.position.x < -5)
    {
        bow.transform.position = new Vector3(-5, bow.transform.position.y, bow.transform.position.z);
        return;
    }
    else if (bow.transform.position.y < -3)
    {
        bow.transform.position = new Vector3(bow.transform.position.x, -3, bow.transform.position.z);
        return;
    }
    else if (bow.transform.position.y > 5)
    {
        bow.transform.position = new Vector3(bow.transform.position.x, 5, bow.transform.position.z);
        return;
    }

    //弓箭移動
    offsetY *= Time.deltaTime;
    offsetX *= Time.deltaTime;
    bow.transform.Translate(0, -offsetX, 0);
    bow.transform.Translate(0, 0, -offsetY);
}
  • CameraFlow

掛載在主相機上,根據初始的時候與弓的偏移量,在弓的位置變化的時候,保持偏移量不變,從而產生跟隨效果。這裡的bow在場景控制器中設定。

public class CameraFlow : MonoBehaviour
{
    public GameObject bow;               //跟隨的物體
    public float smothing = 5f;          //相機跟隨的速度
    Vector3 offset;                      //相機與物體相對偏移位置

    void Start()
    {
        offset = transform.position - bow.transform.position;
    }

    void FixedUpdate()
    {
        Vector3 target = bow.transform.position + offset;
        //攝像機自身位置到目標位置平滑過渡
        transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
    }
}

箭飛行部分

這部分實現了使用者按下滑鼠左鍵,然後箭實現飛行動作

  • ArrowFlyAction

箭的飛行動作,在Start中為箭的Rigidbody設定一個初始力,讓箭射出,在FixedUpdate中給箭一個持續的風力。(在開始時候給箭的預製體新增Rigidbody元件,不使用重力,並且預製體的Collider勾選isTrigger選項,作為觸發器使用)

public class ArrowFlyAction : SSAction
{
    public Vector3 force;                      //初始時候給箭的力
    public Vector3 wind;                       //風方向上的力
    private ArrowFlyAction() { }
    public static ArrowFlyAction GetSSAction(Vector3 wind)
    {
        ArrowFlyAction action = CreateInstance<ArrowFlyAction>();
        //給予箭z軸方向的力
        action.force = new Vector3(0, 0, 20);
        action.wind = wind;
        return action;
    }

    public override void Update(){}

    public override void FixedUpdate()
    {
        //風的力持續作用在箭身上
        this.gameobject.GetComponent<Rigidbody>().AddForce(wind, ForceMode.Force);

        //檢測是否被擊中或是超出邊界
        if (this.transform.position.z > 30 || this.gameobject.tag == "hit")
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,this.gameobject);
        }
    }
    public override void Start()
    {
        gameobject.transform.parent = null;
        gameobject.GetComponent<Rigidbody>().velocity = Vector3.zero;
        gameobject.GetComponent<Rigidbody>().AddForce(force, ForceMode.Impulse);
    }
}
  • ArrowFactory

箭的工廠,在場景控制器需要箭的時候,從空閒的箭佇列拿箭或者例項化新的箭放在場景中。

public class ArrowFactory : MonoBehaviour {

    public GameObject arrow = null;                             //弓箭預製體
    private List<GameObject> used = new List<GameObject>();     //正在被使用的弓箭
    private Queue<GameObject> free = new Queue<GameObject>();   //空閒的弓箭佇列
    public FirstSceneController sceneControler;                 //場景控制器

    public GameObject GetArrow()
    {
        if (free.Count == 0)
        {
            arrow = Instantiate(Resources.Load<GameObject>("Prefabs/arrow"));
        }
        else
        {
            arrow = free.Dequeue();
            //如果是曾經射出過的箭
            if(arrow.tag == "hit")
            {
                arrow.GetComponent<Rigidbody>().isKinematic = false;
                //箭頭設定為可見
                arrow.transform.GetChild(0).gameObject.SetActive(true);
                arrow.tag = "arrow";
            }
            arrow.gameObject.SetActive(true);
        }

        sceneControler = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
        Transform temp = sceneControler.bow.transform.GetChild(2);
        //設定新射出去的箭的位置在弓箭上
        arrow.transform.position = temp.transform.position;
        arrow.transform.parent = sceneControler.bow.transform;
        used.Add(arrow);
        return arrow;
    }

    //回收箭
    public void FreeArrow(GameObject arrow)
    {
        for (int i = 0; i < used.Count; i++)
        {
            if (arrow.GetInstanceID() == used[i].gameObject.GetInstanceID())
            {
                used[i].gameObject.SetActive(false);
                free.Enqueue(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }
}
  • FirstSceneController

使用者按下滑鼠左鍵後,場景控制器從箭工廠得到箭,然後生成一個風力,傳遞給動作管理器,讓箭飛行,並開啟副相機。相關程式碼如下。

public void Shoot()
{
    if((!game_over || game_start) && arrow_num <= 10)
    {
        arrow = arrow_factory.GetArrow();
        arrow_queue.Add(arrow);
        //風方向
        Vector3 wind = new Vector3(wind_directX, wind_directY, 0);
        //動作管理器實現箭飛行
        action_manager.ArrowFly(arrow, wind);
        //副相機開啟
        child_camera.GetComponent<ChildCamera>().StartShow();
        arrow_num++;
    }
}
  • ChildCamera

在設定的時間內,顯示副相機,可以讓使用者看清楚射出去箭在靶上的位置,掛載在副相機上。(初始設定副相機不顯示)

public class ChildCamera : MonoBehaviour
{   
    public bool isShow = false;                   //是否顯示副攝像機
    public float leftTime;                        //顯示時間

    void Update()
    {
        if (isShow)
        {
            leftTime -= Time.deltaTime;
            if (leftTime <= 0)
            {
                this.gameObject.SetActive(false);
                isShow = false;
            }
        }
    }

    public void StartShow()
    {
        this.gameObject.SetActive(true);
        isShow = true;
        leftTime = 2f;
    }
}

箭中靶後部分

當箭射中靶子之後,會出現箭顫抖的效果,並且檢測箭射中哪一環

  • CollisionDetection

箭分為兩個部分,箭頭和箭身,當箭頭進入每一環碰撞器的時候,會消失。然後根據觸發了哪一環的碰撞器,來計分。

public class CollisionDetection : MonoBehaviour
{
    public FirstSceneController scene_controller;         //場景控制器
    public ScoreRecorder recorder;                        //記錄員

    void Start()
    {
        scene_controller = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    void OnTriggerEnter(Collider arrow_head)
    { 
        //得到箭身
        Transform arrow = arrow_head.gameObject.transform.parent;
        if (arrow == null)
        {
            return;
        }
        if(arrow.tag == "arrow")
        {
            //箭身速度為0,不受物理影響
            arrow.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
            arrow.GetComponent<Rigidbody>().isKinematic = true;
            recorder.Record(this.gameObject);
            //箭頭消失
            arrow_head.gameObject.gameObject.SetActive(false); ;
            arrow.tag = "hit";
        }
    }
}
  • ArrowTremble

箭中靶後,通過回撥函式告訴動作管理器,去執行箭顫抖動作。箭顫抖是通過短時間內上下快速移動實現的。

public class ArrowTremble : SSAction
{
    float radian = 0;                             // 弧度  
    float per_radian = 3f;                        // 每次變化的弧度  
    float radius = 0.01f;                         // 半徑  
    Vector3 old_pos;                              // 開始時候的座標  
    public float left_time = 0.8f;                 //動作持續時間

    private ArrowTremble() { }

    public override void Start()
    {
        //將最初的位置儲存  
        old_pos = transform.position;             
    }

    public static ArrowTremble GetSSAction()
    {
        ArrowTremble action = CreateInstance<ArrowTremble>();
        return action;
    }
    public override void Update()
    {
        left_time -= Time.deltaTime;
        if (left_time <= 0)
        {
            //顫抖後回到初始位置
            transform.position = old_pos;
            this.destroy = true;
            this.callback.SSActionEvent(this);
        }

        // 弧度每次增加
        radian += per_radian;
        //y軸的位置變化,上下顫抖
        float dy = Mathf.Cos(radian) * radius; 
        transform.position = old_pos + new Vector3(0, dy, 0);
    }
    public override void FixedUpdate()
    {
    }
}
  • SSActionManager

之前的ISSActionCallback介面終於有用到了,在箭飛行後會執行一個回撥函式SSActionEvent,傳遞現在中靶的GameObject。實現這個回撥函式就可以讓箭顫抖動作開始了。部分程式碼如下。

public void SSActionEvent(SSAction source, GameObject arrow = null)
{
    //回撥函式,如果是箭飛行動作做完,則做箭顫抖動作
    if(arrow != null)
    {
        ArrowTremble tremble = ArrowTremble.GetSSAction();
        this.RunAction(arrow, tremble, this);
    }
    else
    {
        //場景控制器減少一支箭
        FirstSceneController scene_controller = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController;
        scene_controller.ReduceArrow();
    }
}
  • FirstSceneController

從上面可以看到在箭顫抖動作做完後,動作管理器呼叫了場景控制器的ReduceArrow方法,弓箭減少一支,並且生成新的風向。(這部分在之後有修改,見補充改進部分),部分程式碼如下。

public void ReduceArrow()
{
    recorder.arrow_number--;
    if (recorder.arrow_number <= 0 && recorder.score < recorder.target_score)
    {
        game_over = true;
        return;
    }
    //生成新的風向
    wind_directX = Random.Range(-(round + 1), (round + 1));
    wind_directY = Random.Range(-(round + 1), (round + 1));
    CreateWind();
}

其他

  • FirstSceneController

GUI基本是延用了上次遊戲的風格,增加了一個風向文字,通過判定風力值的大小來顯示是哪個方向的風,風力幾級。UserGUI可以通過IUserAction介面得到風力文字,實現是在場景控制器中。部分程式碼如下。

//根據風的方向生成文字
public void CreateWind()
{
    string Horizontal = "", Vertical = "", level = "";
    if (wind_directX > 0)
    {
        Horizontal = "西";
    }
    else if (wind_directX <= 0)
    {
        Horizontal = "東";
    }
    if (wind_directY > 0)
    {
        Vertical = "南";
    }
    else if (wind_directY <= 0)
    {
        Vertical = "北";
    }
    if ((wind_directX + wind_directY) / 2 > -1 && (wind_directX + wind_directY) / 2 < 1)
    {
        level = "1 級";
    }
    else if ((wind_directX + wind_directY) / 2 > -2 && (wind_directX + wind_directY) / 2 < 2)
    {
        level = "2 級";
    }
    else if ((wind_directX + wind_directY) / 2 > -3 && (wind_directX + wind_directY) / 2 < 3)
    {
        level = "3 級";
    }
    else if ((wind_directX + wind_directY) / 2 > -5 && (wind_directX + wind_directY) / 2 < 5)
    {
        level = "4 級";
    }

    wind = Horizontal + Vertical + "風" + " " + level;
}

實現效果

GIF

補充改進

正常遊戲應該是射出箭之後立即弓箭數減少,等待中靶之後判斷遊戲是否應該繼續或結束。在之前的版本中,判斷遊戲應該處於哪種狀態我是寫在FirstSceneController的Update中,現在我改為在箭顫抖動作做完之後再進行判斷,這樣實現的遊戲效果就比較好了。修改FirstSceneController的ReduceArrowa函式,更名為CheckGamestatus。箭數量減少程式碼寫在發射箭的函式中。

  • FirstSceneController(部分程式碼)
void Update ()
{
    if(game_start)
    {
        for (int i = 0; i < arrow_queue.Count; i++)
        {
            GameObject temp = arrow_queue[i];
            //場景中超過5只箭或者超出邊界則回收箭
            if (temp.transform.position.z > 30 || arrow_queue.Count > 5)
            {
                arrow_factory.FreeArrow(arrow_queue[i]);
                arrow_queue.Remove(arrow_queue[i]);
            }
        }
    }
}
public void Shoot()
{
    if((!game_over || game_start) && arrow_num <= 10)
    {
        arrow = arrow_factory.GetArrow();
        arrow_queue.Add(arrow);
        //風方向
        Vector3 wind = new Vector3(wind_directX, wind_directY, 0);
        //動作管理器實現箭飛行
        action_manager.ArrowFly(arrow, wind);
        //副相機開啟
        child_camera.GetComponent<ChildCamera>().StartShow();
        //使用者能射出的箭數量減少
        recorder.arrow_number--;
        //場景中箭數量增加
        arrow_num++;
    }
}
public void CheckGamestatus()
{
    if (recorder.arrow_number <= 0 && recorder.score < recorder.target_score)
    {
        game_over = true;
        return;
    }
    else if (recorder.arrow_number <= 0 && recorder.score >= recorder.target_score)
    {
        round++;
        arrow_num = 0;
        if (round == 4)
        {
            game_over = true;
        }
        //回收所有的箭
        for (int i = 0; i < arrow_queue.Count; i++)
        {
            arrow_factory.FreeArrow(arrow_queue[i]);
        }
        arrow_queue.Clear();
        recorder.arrow_number = 10;
        recorder.score = 0;
        recorder.target_score = targetscore[round];
    }
    //生成新的風向
    wind_directX = Random.Range(-(round + 1), (round + 1));
    wind_directY = Random.Range(-(round + 1), (round + 1));
    CreateWind();
}

小結

本次遊戲開始時候覺得很複雜,然後把遊戲的每個部分分解來做,最後合起來的時候很幸運沒有出現大的bug,都是按照預期來執行,之前的MVC架構擴充套件性果然很好。就算之後發現有些遊戲邏輯實現的不太好,要修改也很方便。(ps:最後一關是需要50分,意味著十次都必須打中紅心,其實很難的,畢竟不想做恭喜通關介面了。逃)

完整專案請點選傳送門,Assets/Scenes/中的myScene是本次遊戲場景