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

Unity3d學習之路-簡單打飛碟小遊戲

簡單打飛碟小遊戲


遊戲規則與遊戲要求

  • 規則
    滑鼠點選飛碟,即可獲得分數,不同飛碟分數不一樣,飛碟的初始位置與飛行速度隨機,隨著分數增加,遊戲難度增加。初始時每個玩家都有6條生命,漏打飛碟扣除一條生命,直到生命為0遊戲結束。

  • 要求:

    • 使用帶快取的工廠模式管理不同飛碟的生產與回收,該工廠必須是場景單例項的!具體實現見參考資源 Singleton 模板類
    • 近可能使用前面 MVC 結構實現人機互動與遊戲模型分離
  • 擴充套件:

    • 用自定義元件定義幾種飛碟,編輯並賦予飛碟一些屬性,做成預製

遊戲UML類圖

  • 很多類都是使用之前的程式碼,修改過或者新建的類,在UML中表示出來了
    UML類圖

遊戲實現過程

動作部分

  • FlyActionManager

    飛碟飛行的動作管理類,當場景控制器需要飛碟飛行的時候,呼叫動作管理類的方法,讓飛碟飛行

public class FlyActionManager : SSActionManager
{

    public UFOFlyAction fly;                            //飛碟飛行的動作
    public FirstController scene_controller;             //當前場景的場景控制器

    protected void Start()
    {
        scene_controller = (FirstController)SSDirector.GetInstance().CurrentScenceController;
        scene_controller.action_manager = this
; } //飛碟飛行 public void UFOFly(GameObject disk, float angle, float power) { fly = UFOFlyAction.GetSSAction(disk.GetComponent<DiskData>().direction, angle, power); this.RunAction(disk, fly, this); } }
  • UFOFlyAction

    給飛碟一個方向和一個力,然後飛碟模擬做有向下加速度的飛行動作,直到飛碟不在相機範圍內,就停止動作。可以設定飛碟位置y方向的值,當它小於多少的時候,不再飛行,然後等待場景控制器和飛碟工廠進行配合回收飛碟。其實可以使用Rigidbody元件,開啟Unity的物理模擬效果。

public class UFOFlyAction : SSAction
{
    public float gravity = -5;                                 //向下的加速度
    private Vector3 start_vector;                              //初速度向量
    private Vector3 gravity_vector = Vector3.zero;             //加速度的向量,初始時為0
    private float time;                                        //已經過去的時間
    private Vector3 current_angle = Vector3.zero;               //當前時間的尤拉角

    private UFOFlyAction() { }
    public static UFOFlyAction GetSSAction(Vector3 direction, float angle, float power)
    {
        //初始化物體將要運動的初速度向量
        UFOFlyAction action = CreateInstance<UFOFlyAction>();
        if (direction.x == -1)
        {
            action.start_vector = Quaternion.Euler(new Vector3(0, 0, -angle)) * Vector3.left * power;
        }
        else
        {
            action.start_vector = Quaternion.Euler(new Vector3(0, 0, angle)) * Vector3.right * power;
        }
        return action;
    }

    public override void Update()
    {
        //計算物體的向下的速度,v=at
        time += Time.fixedDeltaTime;
        gravity_vector.y = gravity * time;

        //位移模擬
        transform.position += (start_vector + gravity_vector) * Time.fixedDeltaTime;
        current_angle.z = Mathf.Atan((start_vector.y + gravity_vector.y) / start_vector.x) * Mathf.Rad2Deg;
        transform.eulerAngles = current_angle;

        //如果物體y座標小於-10,動作就做完了
        if (this.transform.position.y < -10)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);      
        }
    }

    public override void Start() { }
}

飛碟預製與重用部分

  • 用自定義元件定義幾種飛碟

使用序列化的方法,將DiskData的屬性顯示在Inspector中。需要我們新建一個指令碼,並且繼承Editor,在類的前面新增:[CustomEditor(typeof(DiskData))],這裡的DiskData就是要實現自定義元件的類。在這之後新增[CanEditMultipleObjects],實現了多個物件可以不同的修改。如果沒有這個標籤,那麼在Inspector修改之後,擁有這個DiskData作為元件的預製體所有修改都會同步。SerializedProperty是我們需要序列化的屬性,通過EditorGUILayout的不同的方法,可以在Inspector中用不同方式呈現我們序列化的屬性。序列化的屬性的呈現方式需要在OnInspectorGUI中進行編寫。

//指令碼DiskData
public class DiskData : MonoBehaviour
{
    public int score = 1;                               //射擊此飛碟得分
    public Color color = Color.white;                   //飛碟顏色
    public Vector3 direction;                           //飛碟初始的位置
    public Vector3 scale = new Vector3( 1 ,0.25f, 1);   //飛碟大小
}

//指令碼MyDiskEditor
[CustomEditor(typeof(DiskData))]
[CanEditMultipleObjects]
public class MyDiskEditor: Editor
{
    SerializedProperty score;                              //分數
    SerializedProperty color;                              //顏色
    SerializedProperty scale;                              //大小

    void OnEnable()
    {
        //序列化物件後獲得各個值
        score = serializedObject.FindProperty("score");
        color = serializedObject.FindProperty("color");
        scale = serializedObject.FindProperty("scale");
    }

    public override void OnInspectorGUI()
    {
        //更新serializedProperty,始終在OnInspectorGUI的開頭執行此操作
        serializedObject.Update();
        //設定滑動條
        EditorGUILayout.IntSlider(score, 0, 5, new GUIContent("score"));

        if (!score.hasMultipleDifferentValues)
        {
            //顯示進度條
            ProgressBar(score.intValue / 5f, "score");
        }
        //顯示值
        EditorGUILayout.PropertyField(color);
        EditorGUILayout.PropertyField(scale);
        //將更改應用於serializedProperty,始終在OnInspectorGUI的末尾執行此操作
        serializedObject.ApplyModifiedProperties();
    }
    private void ProgressBar(float value, string label)
    {
        Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
        EditorGUI.ProgressBar(rect, value, label);
        //中間留一個空行
        EditorGUILayout.Space();
    }
}

如果自定義元件只是運用在一個預製體上,也可以使用其他方法簡化程式碼,具體操作請看官方文件。我設計了三種飛碟,把DiskData指令碼掛載在每個飛碟預製體上就可以對它們進行設定了,最後實現效果如下圖。

自定義元件

  • DiskFactory

飛碟工廠類,根據回合的不同實現隨機發送不同的飛碟。在場景控制器需要某種飛碟的時候,飛碟工廠從倉庫(List <DiskData> free)中獲取這種飛碟,如果倉庫中沒有,則新的例項化一個飛碟,然後新增到正在使用的飛碟列表中。當場景控制器發現飛碟被打中或者飛碟掉出攝像機視野外,將執行回收飛碟。(ps: 這裡也留了一個問題為啥List中儲存DiskData,而不直接儲存GameObject,我覺得是DiskData可以用gameObject屬性直接得到GameObject物件,而GameObject物件要用GetComponent<DiskData>()方法去查詢DiskData元件,而GetComponent<>()比直接獲取屬性慢的多,使用DiskData頻率可能高於直接使用GameObject物件,所以優選儲存DiskData)。。。然而獲取飛碟的時候我返回了GameObject,而且在場景控制器我也是儲存的GameObject :)

public class DiskFactory : MonoBehaviour
{
    public GameObject disk_prefab = null;                 //飛碟預製體
    private List<DiskData> used = new List<DiskData>();   //正在被使用的飛碟列表
    private List<DiskData> free = new List<DiskData>();   //空閒的飛碟列表

    public GameObject GetDisk(int round)
    {
        int choice = 0;
        int scope1 = 1, scope2 = 4, scope3 = 7;           //隨機的範圍
        float start_y = -10f;                             //剛例項化時的飛碟的豎直位置
        string tag;
        disk_prefab = null;

        //根據回合,隨機選擇要飛出的飛碟
        if (round == 1)
        {
            choice = Random.Range(0, scope1);
        }
        else if(round == 2)
        {
            choice = Random.Range(0, scope2);
        }
        else
        {
            choice = Random.Range(0, scope3);
        }
        //將要選擇的飛碟的tag
        if(choice <= scope1)
        {
            tag = "disk1";
        }
        else if(choice <= scope2 && choice > scope1)
        {
            tag = "disk2";
        }
        else
        {
            tag = "disk3";
        }
        //尋找相同tag的空閒飛碟
        for(int i=0;i<free.Count;i++)
        {
            if(free[i].tag == tag)
            {
                disk_prefab = free[i].gameObject;
                free.Remove(free[i]);
                break;
            }
        }
        //如果空閒列表中沒有,則重新例項化飛碟
        if(disk_prefab == null)
        {
            if (tag == "disk1")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk1"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else if (tag == "disk2")
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk2"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            else
            {
                disk_prefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk3"), new Vector3(0, start_y, 0), Quaternion.identity);
            }
            //給新例項化的飛碟賦予其他屬性
            float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
            disk_prefab.GetComponent<Renderer>().material.color = disk_prefab.GetComponent<DiskData>().color;
            disk_prefab.GetComponent<DiskData>().direction = new Vector3(ran_x, start_y, 0);
            disk_prefab.transform.localScale = disk_prefab.GetComponent<DiskData>().scale;
        }
        //新增到使用列表中
        used.Add(disk_prefab.GetComponent<DiskData>());
        return disk_prefab;
    }

    //回收飛碟
    public void FreeDisk(GameObject disk)
    {
        for(int i = 0;i < used.Count; i++)
        {
            if (disk.GetInstanceID() == used[i].gameObject.GetInstanceID())
            {
                used[i].gameObject.SetActive(false);
                free.Add(used[i]);
                used.Remove(used[i]);
                break;
            }
        }
    }
}

飛碟遊戲場景控制器

  • FirstController

    遊戲有三種狀態,遊戲開始,遊戲中,遊戲結束。(最好用列舉吧,我用了三個bool變數…)。遊戲開始:設定一個定時器,定時從工廠那裡獲取飛碟,並且傳送飛碟,一次只拿一個並立即傳送。(有看過師兄的程式碼是一次就從工廠拿多個,然後慢慢傳送飛碟,如果飛碟完了又一次性拿多個)。遊戲中:根據定時器,時間到了就獲取飛碟併發送。當分數到達10分,將會多增加一種飛碟隨機發送這兩種並且縮短髮送間隔,達到25分則三種飛碟隨機發送,傳送概率不一樣。遊戲結束:顯示最高分,提供重新開始按鈕。

    • 使用者點選螢幕傳送射線

      如果打中則啟用爆炸粒子效果,一段時間後,飛碟工廠再回收。這裡使用了協程的概念,我覺得和多執行緒的意思很像,StartCoroutine開啟一個協程,StartCoroutine後面的程式碼和新的協程一起執行,使用yield暫停協程的執行,yield return的值是代表什麼時候繼續協程的執行,這樣在yield return後面的程式碼將延遲一點時間執行。更詳細的解釋請看官方文件。(ps: 這裡有一點迷的就是,加上協程後,使用者點選一次螢幕會執行兩次Hit函式,所以我用了判斷物體是否被打中,打中了就不要再繼續執行後面程式碼)

    • 傳送飛碟

      從飛碟佇列中取出一個飛碟,然後重新設定它的位置(因為拿到的可能是使用過的飛碟)。傳送的時候檢測未射中的飛碟列表,是否已經飛出鏡頭外了,如果是使用者減一條生命。這裡直接呼叫了user_gui的方法,其實有點違背了MVC,所以如果要改進可以讓計分員記錄生命,這樣可以合併為一個場景中專門記錄數值的類。

部分程式碼如下:

 void Start ()
 {
     SSDirector director = SSDirector.GetInstance();     
     director.CurrentScenceController = this;             
     disk_factory = Singleton<DiskFactory>.Instance;
     score_recorder = Singleton<ScoreRecorder>.Instance;
     action_manager = gameObject.AddComponent<FlyActionManager>() as FlyActionManager;
     user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;
 }
 //發射射線
public void Hit(Vector3 pos)
{
    Ray ray = Camera.main.ScreenPointToRay(pos);
    RaycastHit[] hits;
    hits = Physics.RaycastAll(ray);
    bool not_hit = false;
    for (int i = 0; i < hits.Length; i++)
    {
        RaycastHit hit = hits[i];
        //射線打中物體
        if (hit.collider.gameObject.GetComponent<DiskData>() != null)
        {
            //射中的物體要在沒有打中的飛碟列表中
            for (int j = 0; j < disk_notshot.Count; j++)
            {
                if (hit.collider.gameObject.GetInstanceID() == disk_notshot[j].gameObject.GetInstanceID())
                {
                    not_hit = true;
                }
            }
            if(!not_hit)
            {
                return;
            }
            disk_notshot.Remove(hit.collider.gameObject);
            //記分員記錄分數
            score_recorder.Record(hit.collider.gameObject);
            //顯示爆炸粒子效果
            Transform explode = hit.collider.gameObject.transform.GetChild(0);
            explode.GetComponent<ParticleSystem>().Play();
            //等0.08秒後執行回收飛碟
            StartCoroutine(WaitingParticle(0.08f, hit, disk_factory, hit.collider.gameObject));
            break;
        }
    }
}
//暫停幾秒後回收飛碟
IEnumerator WaitingParticle(float wait_time, RaycastHit hit, DiskFactory disk_factory, GameObject obj)
{
    yield return new WaitForSeconds(wait_time);
    //等待之後執行的動作  
    hit.collider.gameObject.transform.position = new Vector3(0, -9, 0);
    disk_factory.FreeDisk(obj);
}

//傳送飛碟
private void SendDisk()
{
    float position_x = 16;                       
    if (disk_queue.Count != 0)
    {
        GameObject disk = disk_queue.Dequeue();
        disk_notshot.Add(disk);
        disk.SetActive(true);
        //設定被隱藏了或是新建的飛碟的位置
        float ran_y = Random.Range(1f, 4f);
        float ran_x = Random.Range(-1f, 1f) < 0 ? -1 : 1;
        disk.GetComponent<DiskData>().direction = new Vector3(ran_x, ran_y, 0);
        Vector3 position = new Vector3(-disk.GetComponent<DiskData>().direction.x * position_x, ran_y, 0);
        disk.transform.position = position;
        //設定飛碟初始所受的力和角度
        float power = Random.Range(10f, 15f);
        float angle = Random.Range(15f, 28f);
        action_manager.UFOFly(disk,angle,power);
    }

    for (int i = 0; i < disk_notshot.Count; i++)
    {
        GameObject temp = disk_notshot[i];
        //飛碟飛出攝像機視野也沒被打中
        if (temp.transform.position.y < -10 && temp.gameObject.activeSelf == true)
        {
            disk_factory.FreeDisk(disk_notshot[i]);
            disk_notshot.Remove(disk_notshot[i]);
            //玩家血量-1
            user_gui.ReduceBlood();
        }
    }
}

一般射擊都是從點選處射出子彈,帶有碰撞器,當碰撞器與飛碟碰撞器觸碰時候,應該是開啟子彈上的粒子效果才對,然後播放完畢後子彈消失。


記分員

  • ScoreRecorder

記錄分數和重置分數

public class ScoreRecorder : MonoBehaviour
{
    public int score;                   //分數
    void Start ()
    {
        score = 0;
    }
    //記錄分數
    public void Record(GameObject disk)
    {
        int temp = disk.GetComponent<DiskData>().score;
        score = temp + score;
        //Debug.Log(score);
    }
    //重置分數
    public void Reset()
    {
        score = 0;
    }
}

遊戲實現截圖

開始介面

遊戲中

遊戲結束

小結

這次遊戲使用工廠物件實現了預製體例項化後的重用,提高了遊戲效能,還加入了獲取滑鼠輸入,增加了與使用者的互動。在寫部落格的過程中發現了很多程式碼中存在的問題,也發現了許多遊戲中需要改進的地方。

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