【遊戲開發】unity教程4 打飛碟小遊戲

github傳送門:https://github.com/dongzizhu/unity3DLearning/tree/master/hw4/Disk

視頻傳送門:https://space.bilibili.com/472759319

打飛碟小遊戲

這次的代碼架構同樣採用了MVC模式,與之前的牧師與魔鬼基本相同,這裏就不重複敘述了,感興趣的可以看上上篇博文

這裏主要還是介紹一下firstController的變化以及新應用的工廠模式和真正負責飛碟移動的Emit類。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using MyGame;
using UnityEngine.SceneManagement;

public class FirstController : MonoBehaviour, ISceneController, IUserAction {

    public ActionManager MyActionManager { get; set; }
    public DiskFactory factory { get; set; }
    public RecordController scoreRecorder;
    public UserGUI user;    
    
    void Awake() {
        Director diretor = Director.getInstance();
        diretor.sceneCtrl = this;                              
    }

    // Use this for initialization
    void Start() {
        Begin();
    }
	
	// Update is called once per frame
	void Update () {
        
	}

    public void Begin() {
        MyActionManager = gameObject.AddComponent<ActionManager>() as ActionManager;
        scoreRecorder = gameObject.AddComponent<RecordController>();
        user = gameObject.AddComponent<UserGUI>();
        user.Begin();
    }

    public void Hit(DiskController diskCtrl) {   
        // 0=playing 1=lose 2=win 3=cooling     
        if (user.game == 0) {            
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            
            if (Physics.Raycast(ray, out hit)) {
                //hit.collider.gameObject.SetActive(false);
                Debug.Log("Hit");
                factory.freeDisk(hit.collider.gameObject);
                hit.collider.gameObject.GetComponent<DiskController>().hit = true;
                scoreRecorder.add(hit.collider.gameObject.GetComponent<DiskController>());
            }

        }
    }

    public void PlayDisk() {
        MyActionManager.playDisk(user.round);
    }

    public void Restart() {
        SceneManager.LoadScene("scene");
    }

    public int Check() {
        return 0;
    }
}

FirstController同樣是負責着所有其他的controller和userGUI,這都和之前相同;新加入的DiskFactory我們一會兒再介紹。這裏主要講一下Hit函數。所謂ScreenPointToRay就是從Camera出發連接到鼠標點擊位置的一條射線,然後如果射線經過了我們目標的GameObject,就算是擊中了。當一個飛碟被擊中時,我們首先將這個Object的Active設爲False,從而將擊中的消息傳回給Action;然後FreeDisk是將這個實例從放到free列表中等待下一次調用(其實在FreeDisk中我們已經有了設置Active的操作,這裏將其註釋在這裏是爲了提醒我們它的重要性)。不知道free列表是什麼東西沒關係,我們繼續看DiskFactory的代碼。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyGame;

public class DiskFactory : MonoBehaviour {
    private static DiskFactory _instance;
    public FirstController sceneControler { get; set; }
    GameObject diskPrefab;
    public DiskController diskData;
    public List<GameObject> used;
    public List<GameObject> free;
    // Use this for initialization

    public static DiskFactory getInstance() {
        return _instance;
    }

    private void Awake() {
        if (_instance == null) {
            _instance = Singleton<DiskFactory>.Instance;
            _instance.used = new List<GameObject>();
            _instance.free = new List<GameObject>();
            diskPrefab = Instantiate(Resources.Load<GameObject>("Prefabs/disk"), new Vector3(40, 0, 0), Quaternion.identity);
        }
    }


    public void Start() {
        sceneControler = (FirstController)Director.getInstance().sceneCtrl;
        sceneControler.factory = _instance;      
    }

    public GameObject getDisk(int round) { // 0=playing 1=lose 2=win 3=cooling
        if (sceneControler.scoreRecorder.Score >= round * 4) {
            if (sceneControler.user.round < 3) {
                sceneControler.user.round++;
                sceneControler.user.num = 0;
                sceneControler.scoreRecorder.Score = 0;
            }
            else {
                sceneControler.user.game = 2; // 贏了
                return null;
            }
        }
        else {
            if (sceneControler.user.num >= 10) {
                sceneControler.user.game = 1; // 輸了
                return null;
            }            
        }

        GameObject newDisk;
        RoundController diskOfCurrentRound = new RoundController(sceneControler.user.round);        
        if (free.Count == 0) {// if no free disk, then create a new disk
            newDisk = GameObject.Instantiate(diskPrefab) as GameObject;
            newDisk.AddComponent<ClickGUI>();
            diskData = newDisk.AddComponent<DiskController>();
        }
        else {// else let the first free disk be the newDisk
            newDisk = free[0];
            free.Remove(free[0]);
            newDisk.SetActive(true);
        }
        diskData = newDisk.GetComponent<DiskController>();
        diskData.color = diskOfCurrentRound.color;
        //Debug.Log(diskData);

        newDisk.transform.localScale = diskOfCurrentRound.scale * diskPrefab.transform.localScale;
        newDisk.GetComponent<Renderer>().material.color = diskData.color;
        
        used.Add(newDisk);
        return newDisk;
    }

    public void freeDisk(GameObject disk1) {
        used.Remove(disk1);
        disk1.SetActive(false);
        free.Add(disk1);
        return;
    }

    public void Restart() {
        used.Clear();
        free.Clear();
    }
}

這就是所謂的工廠模式了。當遊戲對象的創建與銷燬成本較高,且遊戲涉及大量遊戲對象的創建與銷燬時,必須考慮減少銷燬次數,比如這次的打飛碟遊戲,或者像其他類型的射擊遊戲,其中子彈或者中彈對象的創建與銷燬是很頻繁的。工廠模式將已經創建好正在使用的實例存在一個used列表中,然後當使用完成(被擊中)就將其放在free列表中,等待下一次調用;當我們需要一個新的實例的時候,首先檢查free列表,當其中沒有限制的實例時我們才創建一個新的。getDisk和freeDisk就實現了上面所敘述的邏輯,是核心的代碼。

最後是Emit類。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using MyGame;

public class Emit : SSAction
{
    public FirstController sceneControler = (FirstController)Director.getInstance().sceneCtrl;
    public Vector3 target;     
    public float speed;     
    private float distanceToTarget;    
    float startX;
    float targetX;
    float targetY;

    public override void Start() {
        speed = sceneControler.user.round * 5;
        GameObject.GetComponent<DiskController>().speed = speed;

        startX = 6 - Random.value * 12;

        if (Random.value > 0.5) {
            targetX = 36 - Random.value * 36;
            targetY = 25 - Random.value * 25;
        }
        else {
            targetX = -36 + Random.value * 36;
            targetY = -25 + Random.value * 25;
        }

        this.Transform.position = new Vector3(startX, 0, 0);
        target = new Vector3(targetX, targetY, 30);
        //Debug.Log(target);
        distanceToTarget = Vector3.Distance(this.Transform.position, target);
    }

    public static Emit GetSSAction() {
        Emit action = ScriptableObject.CreateInstance<Emit>();
        return action;
    }

    public override void Update() {
        Vector3 targetPos = target;
        if(!GameObject.activeSelf){
            this.destroy = true;
            return;
        }

        //facing the target
        GameObject.transform.LookAt(targetPos);

        //calculate the starting angel  
        float angle = Mathf.Min(1, Vector3.Distance(GameObject.transform.position, targetPos) / distanceToTarget) * 45;
        GameObject.transform.rotation = GameObject.transform.rotation * Quaternion.Euler(Mathf.Clamp(-angle, -42, 42), 0, 0);
        float currentDist = Vector3.Distance(GameObject.transform.position, target);
        //Debug.Log("****************************");
        //Debug.Log(startX);
        //Debug.Log(target);
        //Debug.Log("****************************");
        GameObject.transform.Translate(Vector3.forward * Mathf.Min(speed * Time.deltaTime, currentDist));
        if (this.Transform.position == target) {
            sceneControler.scoreRecorder.miss();
            Debug.Log("here in miss!!");
            GameObject.SetActive(false);
            GameObject.transform.position = new Vector3(startX, 0, 0);
            sceneControler.factory.freeDisk(GameObject);
            this.destroy = true;
            this.Callback.ActionDone(this);
        }
    }
}

我們在Start函數中保證了飛碟出現的位置和目標方向的隨機性。然後在Update函數中首先計算移動的角度,然後根據速度給出當前的位移,然後進行一次判斷,如果當前位置已經是終點了,那麼我們首先setActive告訴外層的actionManager之前的運動可以取消了,然後將當前的實例free掉。在Update函數開始返回前也需要設置一下destroy是爲了在hit後也可以告訴actionManager取消當前運動,與後面那個並不是重複操作。

最後我們來看一眼actionControl的核心ActionMananger,其他的就不全部貼上來了。

public class ActionManager : SSActionManager {
    public FirstController sceneController;
    public DiskFactory diskFactory;
    public RecordController scoreRecorder;
    public Emit EmitDisk;
    public GameObject Disk;
    int count = 0;

    protected void Start() {
        sceneController = (FirstController)Director.getInstance().sceneCtrl;
        diskFactory = sceneController.factory;
        scoreRecorder = sceneController.scoreRecorder;
        sceneController.MyActionManager = this;
    }

    protected new void Update() {
        if (sceneController.user.round <= 3 && sceneController.user.game == 0) {
            count++;
            if (count == 60 * sceneController.user.round) {
                playDisk(sceneController.user.round);
                sceneController.user.num++;
                count = 0;
            }
            base.Update();
        }
    }

    public void playDisk(int round) {
        EmitDisk = Emit.GetSSAction();
        Disk = diskFactory.getDisk(round);
        this.AddAction(Disk, EmitDisk, this);
        Disk.GetComponent<DiskController>().action = EmitDisk;
    }
    
}

Update實現了每60幀*round後發出一個飛碟。之所以這樣設計是因爲最後一輪的飛碟速度太快,這樣能夠適當降低遊戲難度。playDisk函數就是從工廠中獲取一個飛碟,然後和下一個應該出現的飛碟的移動方向和特徵一起傳給AcitionManager。

另外爲了有空戰的感覺,我還加入了在AssetStore下載的StarField天空盒,最終的效果如下圖所示。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章