天天看點

Unity3D -- Hit UFO adapter模式

目錄

    • 一、遊戲内容
    • 二、UML圖設計
    • 三、遊戲的實作
        • 1. DiskFactory
        • 2. FlyAction
        • 3. SSActionManager
        • 4. PhyUFOFlyAction
        • 5. PhyFlyActionManager
        • 6. ISceneController
        • 7. UserGUI
    • 四、 運作界面與代碼傳送門

一、遊戲内容

改進飛碟(Hit UFO)遊戲

遊戲内容要求:

  1. 按 adapter模式 設計圖修改飛碟遊戲
  2. 使它同時支援實體運動與運動學(變換)運動

二、UML圖設計

Unity3D -- Hit UFO adapter模式

三、遊戲的實作

1. DiskFactory

飛碟工廠實作對不同飛碟的生産、管理以及回收。需要注意的是,這裡使用的是帶緩存的單執行個體工廠模式。

  • 單執行個體:運用模闆,可以為每個 MonoBehaviour子類建立一個對象的執行個體:

    Singleten<T>

  • 帶緩存的工廠:由于對飛碟的多次建立與銷毀開銷很大,是以我們使用帶緩存的工廠來避免頻繁的建立與銷毀操作。當一個飛碟需要被銷毀時,工廠并不直接銷毀他,而是将其标記為“空閑”,表示他已經不被場景使用了。在新的飛碟被需要時,工廠不會直接去執行個體化一個新的飛碟,而是從被标記“空閑”的飛碟中選擇可用的執行個體,隻有在目前沒有可用的執行個體時,工廠才會去真正執行個體化一個新的飛碟。這樣一來就能減少遊戲不斷建立與銷毀遊戲對象的極大開銷。```
public class DiskFactory : MonoBehaviour {
    private int diskID = 0;
    private List<DiskModel>[] disks = new List<DiskModel>[3]; //共有三種飛碟
    bool[,] diskStatus = new bool[3, 20];   //每種飛碟最多有二十個
    int[] size = new int[3];    //儲存目前已建立出來的每種飛碟的數量private List<DiskModel> ddisk = new List<DiskModel>();
​
public void Start() {
    for (int i = 0; i < 3; i++) {
        size[i] = 0;                                 
        disks[i] = new List<DiskModel>();
    }
    for (int j = 0; j < 3; j++) {
        for (int i = 0; i < 20; i++) {
            diskStatus[j, i] = true;
        }
    }
}
​
//随機擷取一種空閑飛碟,如果飛碟不足則建立一個執行個體
public DiskModel getDisk() {
​
    //随機從三種預制中選擇一個飛碟外觀
    int type = Random.Range(0, 3);
    DiskModel disk;
​
    //嘗試擷取已經被建立但目前處于空閑态的飛碟
    for (int i = 0; i < size[type]; i++) {
        if (diskStatus[type, i] == false) {
            diskStatus[type, i] = true;
            disks[type][i].disk.SetActive(true);
            disk = disks[type][i];
            disk.setDiskID(diskID);
            diskID++;
            //取出時飛碟不能夠有爆炸特效
            disk.disk.GetComponent<ParticleSystem>().Stop();
            return disk;
        }
    }
​
    //目前沒有可用的空閑飛碟,需要建立
    disk = new DiskModel(type, diskID);
    diskID++;
    disks[type].Add(disk);
    diskStatus[type, size[type]] = true;
    size[type]++;
    disk.disk.GetComponent<ParticleSystem>().Stop();
    return disk;
}
​
//回收飛碟
public void FreeDisk(DiskModel disk) {
    int type = disk.getType();
    int ID = disk.getDiskID();
​
    for (int i = 0; i < size[type]; i++) {
        if (disk.getDiskID() == disks[type][i].getDiskID()) {
            diskStatus[type, i] = false;
            return;
        }
    }
}
​
//根據 gameobject 的 instanceID 來查找飛碟
public DiskModel findDisk(int InstanceID) {
    for(int i = 0; i < 3; i++) {
        for(int j = 0; j < size[i]; j++) {
            if (disks[i][j].disk.GetInstanceID() == InstanceID) return disks[i][j];
        }
    }
    return null;
}
}
           

2. FlyAction

lyAction的基類

SSAction

在之前的部落格已經介紹過。

FlyAction實際上隻是一個簡單的直線動作,但是與CCMoveAction不同的是其參數是飛行的角度和速度。

public class FlyAction : SSAction{                           
    private Vector3 angle;
    private float speed;//根據飛行角度和速度擷取一個FlyAction
    public static FlyAction GetSSAction(Vector3 angle, float speed) {
    FlyAction action = CreateInstance<FlyAction>();
    action.angle = angle;
    action.speed = speed;
    return action;
}
​
//實作簡單的直線飛行
public override void Update() {
    transform.position += angle * Time.deltaTime * speed;
}
​
public override void Start() {
    Update();
}}
           

3. SSActionManager

FlyActionManager 是本次遊戲的動作管理者,管理此次控制飛碟運動的多個Action。

FlyActionManager 具有的功能:

  • 使用Ruler為每個 Action 設定合适的飛行角度和速度
  • 為輸入的飛碟尋找一個空閑的 Action 進行飛行動作
  • 在每次 Update 時 FlyActionManager 對被使用的所有 action 進行 Update 調用
public class FlyActionManager : SSActionManager, ActionAdapter {
    public FlyAction[] actions = new FlyAction[20]; //最多建立20個action
    public int[] diskID = new int[20];              //action 所應用的飛碟的ID
    public ISceneController controller;            
    private Ruler ruler;    //根據不同 round 對飛碟的性能參數做設定
    int round;protected void Start() {
    controller = (ISceneController)SSDirector.getInstance().currentSceneController;
    controller.actionManager = this;
    ruler = new Ruler();
    for(int i = 0; i < 20; i++) {
        diskID[i] = -1; //開始時沒有可用飛碟,ID 均為-1
    }
}
​
public void Update() {
    for(int i = 0; i < 20; i++) {
        if (diskID[i] != -1) {
            //執行有附着在飛碟上的 action
            actions[i].Update();
        }
    }
}
​
public void setRound(int round) {
    this.round = round;
}
​
public void UFOFly(DiskModel disk) {
    ruler.setRound(round);
    disk.disk.transform.position = ruler.getStart();//設定飛碟的出現位置
    int index = 0;
    for (; diskID[index] != -1; index++) ;//找到空閑的 Action
    actions[index] = FlyAction.GetSSAction(ruler.getAngle(), ruler.getSpeed());
    diskID[index] = disk.getDiskID();
    disk.disk.GetComponent<Rigidbody>().useGravity = false;
    disk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
    this.RunAction(disk.disk, actions[index], this);
}
​
public void freeAction(DiskModel disk) {
    for(int i = 0; i < 20; i++) {
        //當飛碟不再需要時,actionManager可以簡單的将對應的action設為空閑,而非直接删除 action
        if(diskID[i] == disk.getDiskID()) {
            diskID[i] = -1;
            break;
        }
    }
}}
           

4. PhyUFOFlyAction

控制飛碟的實體學運動,與運動學類似,但要對遊戲對象的rigidbody 進行操作。

public class PhyUFOFlyAction : SSAction {
    private Vector3 angle;  //飛行角度
    float speed;            //飛行初速度private PhyUFOFlyAction() { }
public static PhyUFOFlyAction GetSSAction(Vector3 angle, float speed) {
    //初始化物體将要運動的初速度向量
    PhyUFOFlyAction action = CreateInstance<PhyUFOFlyAction>();
    action.angle = angle;
    action.speed = speed;
    return action;
}
​
public override void Start() {
    //使用重力以及給一個初速度
    gameobject.GetComponent<Rigidbody>().velocity = angle * speed;
    gameobject.GetComponent<Rigidbody>().useGravity = true;
}}
           

5. PhyFlyActionManager

public class PhyFlyActionManager : SSActionManager, ActionAdapter {public PhyUFOFlyAction[] actions = new PhyUFOFlyAction[20]; //最多建立20個action
public int[] diskID = new int[20];              //action 所應用的飛碟的ID
public ISceneController controller;
private Ruler ruler;                            //根據不同 round 對飛碟的性能參數做設定
int round;
​
protected void Start() {
    controller = (ISceneController)SSDirector.getInstance().currentSceneController;
    controller.actionManager = this;
    ruler = new Ruler();
    for (int i = 0; i < 20; i++) {
        diskID[i] = -1; //開始時沒有可用飛碟,ID 均為-1
    }
}
​
public void setRound(int round) {
    this.round = round;
}
​
public void UFOFly(DiskModel disk) {
    ruler.setRound(round);
    disk.disk.transform.position = ruler.getStart();//設定飛碟的出現位置
    int index = 0;
    for (; diskID[index] != -1; index++) ;//找到空閑的 Action
​
    Vector3 angle = Vector3.left;
    int flag = Random.Range(0, 2);
    if (flag == 1) angle *= -1;
​
    actions[index] = PhyUFOFlyAction.GetSSAction(angle, ruler.getSpeed());//從ruler中擷取初速度和飛行角度,加速度為10
    diskID[index] = disk.getDiskID();
    this.RunAction(disk.disk, actions[index], this);
}
​
public void freeAction(DiskModel disk) {
    disk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
    disk.disk.GetComponent<Rigidbody>().useGravity = false;
​
    for (int i = 0; i < 20; i++) {
        //當飛碟不再需要時,actionManager可以簡單的将對應的action設為空閑,而非直接删除 action
        if (diskID[i] == disk.getDiskID()) {
            diskID[i] = -1;
            break;
        }
    }
}}
           

6. ISceneController

其主要功能為:

  • 在飛碟數量小于目前 round 值規定時,向場景中發送飛碟;
  • 對場景中所有飛碟進行位置判斷,如果超出視野範圍則“銷毀”飛碟;
  • 判斷玩家的射擊操作,擊中飛碟後進行爆炸特效、增加分數、銷毀飛碟等一系列處理;
public class ISceneController : MonoBehaviour, IUserAction {
    public ActionAdapter actionManager;
    public DiskFactory diskFactory;private List<DiskModel> currentDisks = new List<DiskModel>();
    private int diskCount = 0;  //目前場景中的飛碟數量
    private int[] maxCount = { 3, 5, 8 };   //每個round中飛碟需要維持的數目,數目不足時将發送新的飛碟
    private int round = 0;  //目前遊戲的 round
    private int currentTrial = 0;//目前遊戲的 trail
    private int score = 0;  //獲得的分數
    private int[] scoreOfRound = { 10, 20, 30 };   //每個round需要達成的分數目标
    private bool playing = false;
    private int life = 100;  //血量
​
    public UserGUI userGUI;
​
void Start() {
    SSDirector director = SSDirector.getInstance();
    director.currentSceneController = this;
    diskFactory = Singleton<DiskFactory>.Instance;
    actionManager = gameObject.AddComponent<PhyFlyActionManager>() as ActionAdapter;
    actionManager.setRound(round);
    userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
}
​
void Update() {
    //發送飛碟
    if (playing) {
        if (diskCount < maxCount[round]) {
            sendDisk();
        }
        //檢查目前遊戲狀态是否允許更新
        checkStatus();
        removeRemoteDisk();
​
        //檢查玩家的射擊操作
        if (Input.GetButtonDown("Fire1")) {
            Vector3 mp = Input.mousePosition; 
            Hit(mp);
        }
    }
}
​
public void startGame() {
    life = 100;
    playing = true;
}
​
public int getScore() {
    return score;
}
​
public int getLife() {
    return life;
}
​
public int getTrail() {
    return currentTrial;
}
​
public int getRound() {
    return round;
}
​
//檢查目前遊戲狀态
//檢查目前trail是否足以進入下一 round
//檢查目前round是否足夠結束遊戲
public void checkStatus() {
    //此時的分數大于設定的門檻值,遊戲進入下一階段,分數清零重新計算
    if (score >= scoreOfRound[round]) {
        currentTrial++;
        score = 0;
        
        //當遊戲的trail大于3時進入下一 round
        if (currentTrial >= 3) {
            round++;
            life = 100;//當遊戲進入到新的round生命值回複
            if (round >= 3) winGame();
            currentTrial = 0;              
            actionManager.setRound(round);
        }
    }
}
​
//判斷飛碟是否已經離開視野
private bool outOfSight(Vector3 pos) {
    return pos.x > 35 || pos.x < -35
        || pos.y > 35 || pos.y < -35
        || pos.z > 10 || pos.z < -300;
}
​
//檢查目前所有被使用的飛碟是否已經飛出視野
//将飛出視野的飛碟“銷毀”
//每銷毀一個飛碟就将目前飛碟數量減一,遊戲将自動補齊缺少的飛碟
private void removeRemoteDisk() {
    for (int i = 0; i < diskCount; i++) {
        GameObject tmp = currentDisks[i].disk;
        if (outOfSight(tmp.transform.position)) {
            tmp.SetActive(false);
            diskFactory.FreeDisk(currentDisks[i]);
            actionManager.freeAction(currentDisks[i]);
            currentDisks.Remove(currentDisks[i]);
            diskCount--;
            life--;
        }
    }
}
​
//發送飛碟
private void sendDisk() {
    diskCount++;
    DiskModel disk = diskFactory.getDisk(); //從工廠擷取新的飛碟
    currentDisks.Add(disk);                 //将新飛碟加入到目前的清單
    actionManager.UFOFly(disk);             //令飛碟進行移動
}
​
//檢查玩家是否射中飛碟
public void Hit(Vector3 pos) {
    Ray ray = Camera.main.ScreenPointToRay(pos);
    RaycastHit[] hits;
    hits = Physics.RaycastAll(ray);
    DiskModel hitDisk;
​
    for (int i = 0; i < hits.Length; i++) {
        RaycastHit hit = hits[i];
        hitDisk = diskFactory.findDisk(hit.collider.gameObject.GetInstanceID());
​
        //射線打中物體
        if (hitDisk != null) {
            score += hitDisk.score;
​
            //顯示爆炸粒子效果
            hitDisk.disk.GetComponent<ParticleSystem>().Play();
​
            //等0.5秒後執行回收飛碟
            StartCoroutine(WaitingParticle(0.50f, diskFactory, hitDisk));
            break;
        }
    }
}
​
public void winGame() {
    playing = false;
    Debug.Log("you win");
}
​
//暫停幾秒後回收飛碟
IEnumerator WaitingParticle(float wait_time, DiskFactory diskFactory, DiskModel hitDisk) {
    yield return new WaitForSeconds(wait_time);
    if (hitDisk.disk.active == true) {
        hitDisk.disk.SetActive(false);
        hitDisk.disk.GetComponent<ParticleSystem>().Stop();
        hitDisk.disk.GetComponent<Rigidbody>().velocity = Vector3.zero;
        hitDisk.disk.GetComponent<Rigidbody>().useGravity = false;
        currentDisks.Remove(hitDisk);
        actionManager.freeAction(hitDisk);
        diskFactory.FreeDisk(hitDisk);
        diskCount--;
    }
}}
           

7. UserGUI

UI的作用比較簡單,主要是實作顯示目前分數、血量、關卡,以及在關卡之間切換的功能。我在這裡主要是參考了師兄師姐的實作,然後按照自己的規則做了一些改動。

public class UserGUI : MonoBehaviour {
    private IUserAction action;//每個GUI的style
    GUIStyle bold_style = new GUIStyle();
    GUIStyle text_style = new GUIStyle();
    GUIStyle over_style = new GUIStyle();
    bool show;
    int round;
    bool changeRound;
    bool playing = true;
​
void Start() {
    show = true;
    changeRound = false;
    action = SSDirector.getInstance().currentSceneController as IUserAction;
}
​
void OnGUI() {
    if (!playing) {
        if (action.getLife() < 0) {
            GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "YOU LOSE");
        }
        else {
            GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "YOU WIN");
        }
        return;
    }
​
    bold_style.normal.textColor = new Color(1, 0, 0);
    bold_style.fontSize = 16;
    text_style.normal.textColor = new Color(0, 0, 0, 1);
    text_style.fontSize = 16;
    over_style.normal.textColor = new Color(1, 0, 0);
    over_style.fontSize = 25;
​
    if (action.getLife() < 0) {
        playing = false;
    }
​
    if (changeRound) {
        GUI.Label(new Rect(Screen.width / 2 - 120, Screen.width / 2 - 220, 400, 100), " N E X T   R  O U N D ", over_style);
        if (GUI.Button(new Rect(0, 0, Screen.width, Screen.width), "press to continue")) {
            changeRound = false;
            action.startGame();
        }
    }
    else {
        if (show) {
            GUI.Label(new Rect(Screen.width / 2 - 170, Screen.width / 2 - 180, 400, 100), "大量UFO出現,點選它們即可消滅,快來加入戰鬥吧", text_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 40, Screen.width / 2 - 120, 100, 50), "開始")) {
                show = false;
                action.startGame();
            }
        }
        else {
            GUI.Label(new Rect(Screen.width - 120, 20, 200, 50), "score:", text_style);
            GUI.Label(new Rect(Screen.width - 70, 20, 200, 50), action.getScore().ToString(), bold_style);
​
            GUI.Label(new Rect(Screen.width - 120, 50, 200, 50), "trial:", text_style);
            GUI.Label(new Rect(Screen.width - 70, 50, 200, 50), (action.getTrail() + 1).ToString(), bold_style);
​
            GUI.Label(new Rect(Screen.width - 120, 80, 50, 50), "life:", text_style);
            GUI.Label(new Rect(Screen.width - 70, 80, 50, 50), action.getLife().ToString(), bold_style);
​
            if (action.getRound() > round) {
                round = action.getRound();
                if (round > 2) playing = false;
                changeRound = true;
            }
        }
    }
}}
           

四、 運作界面與代碼傳送門

遊戲視訊位址

代碼傳送門

Unity3D -- Hit UFO adapter模式
Unity3D -- Hit UFO adapter模式

繼續閱讀