學習Unity(9)打飛碟小游戲改進——使用適配器模式

改進描述

我們之前完成的飛碟游戲中,UFO是在兩點之間來回飛行,我們是通過修改position來使得飛碟運動起來的。

現在,為了練習對Unity物理引擎的使用和適配器模式的使用,我們想要加入另一種飛碟運動模式:物理運動模式,飛碟受到向下的力,向地面撞去。玩家要在飛碟撞到地面之前擊中飛碟才能得分,飛碟撞上地面則不得分。

并且,我們不僅要實現物理運動模式,還要保留著原本的普通運動模式,通過鼠標右鍵,用戶可以在兩種模式之間切換。

游戲截圖

正常運動模式下飛碟來回飛行
鼠標右鍵可以切換運動模式
物理模式下飛碟會緩緩掉落地面

在自己的電腦上運行這個游戲!

我的github下載項目資源,將所有文件放進你的項目的Assets文件夾(如果有重復則覆蓋),然后在U3D中雙擊“hw5”,就可以運行了!

實現物理模式的動作管理器

這次的改進有一點特別。正常運動模式不能刪除,而是與新的物理運動模式共存,我們要在游戲運行的時候來決定使用哪種運動模式。也就是說,原本的動作管理器類不能刪除,它們是管理正常運動模式的。我們還要再實現一個動作管理器,用來管理物理運動模式。最后想一種辦法將兩個動作管理器結合起來。
首先我們實現物理模式動作管理器:

public class PhysicsActionManager : MonoBehaviour {

    public void addForce(GameObject gameObj, Vector3 force) {
        ConstantForce originalForce = gameObj.GetComponent<ConstantForce>();
        if (originalForce) {
            originalForce.enabled = true;
            originalForce.force = force;
        } else {
            gameObj.AddComponent<Rigidbody>().useGravity = false;
            gameObj.AddComponent<ConstantForce>().force = force;
        }
    }

    public void removeForce(GameObject gameObj) {
        gameObj.GetComponent<ConstantForce>().enabled = false;
    }
}

這個管理器的實現非常簡單,只需要負責增加\移除ConstantForce組件就可以了。

要使物體受到力的影響,必須先讓他具有Rigidbody(剛體)組件。對物理引擎的使用,網上有很多教程。你可以查看官方文檔學習其他作者的博客


適配器模式

如何將兩種動作管理器有機地結合起來呢?讓FirstController(場景控制器)同時擁有兩個變量,分別指向這兩個動作管理器嗎?這樣不好,如果我們以后又要增加新的動作管理器呢?如果我們要增加新的飛碟工廠類呢?這樣的話FirstController就需要管理太多功能相同的部件了,FirstController會越來越臃腫,可擴展性很差。

我們希望FirstController只需要為同一個用途的所有組件保存1個變量

這就是為什么我們需要適配器模式。
讓我通過一個生活中的例子來解釋適配器模式:現在大部分的的平板電腦只有一個USB接口,現在我想在我的平板電腦上同時使用鍵盤和鼠標,怎么辦?很簡單,買一個這樣的USB擴展器:


USB擴展器就是一種適配器

將USB擴展器插在平板的USB接口上,然后將鍵盤、鼠標插在USB擴展器的USB接口上,你就可以同時使用鍵盤和鼠標了!

在這個例子中,我們的平板就像是FirstController,兩個輸入設備就像是兩個動作管理器。要將兩個動作管理器同時接入FirstController,我們要實現一個適配器,讓FirstController連接適配器,然后讓適配器連接兩個動作管理器。

我們先將FirstController中原本保存ActionManager的變量刪掉,然后添加這一行:

ActionManagerTarget actionManagerTarget;

ActionManagerTarget是一個接口,它就相當于平板電腦上的USB接口:

public interface ActionManagerTarget {
    void switchActionMode();
    
    void addAction(GameObject gameObj, Dictionary<string, object> option);

    void addActionForArr(GameObject[] Arr, Dictionary<string, object> option);

    void addActionForArr(UFOController[] Arr, Dictionary<string, object> option);

    void removeActionOf(GameObject obj, Dictionary<string, object> option);
}

然后實現一個適配器類ActionManagerAdapter,這個類要實現這個接口:

public class ActionManagerAdapter: ActionManagerTarget {
    FirstSceneActionManager normalAM;
    PhysicsActionManager PhysicsAM;

    int whichActionManager = 0; // 0->normal, 1->physics

    public ActionManagerAdapter(GameObject main) {
        normalAM = main.AddComponent<FirstSceneActionManager>();
        PhysicsAM = main.AddComponent<PhysicsActionManager>();
        whichActionManager = 0;
    }

    public void switchActionMode() {
        whichActionManager = 1-whichActionManager;
    }

    public void addAction(GameObject gameObj, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            normalAM.addRandomAction(gameObj, (float)option["speed"]);
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            PhysicsAM.addForce(gameObj, (Vector3)option["force"]);
        }
    }

    public void addActionForArr(GameObject[] Arr, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            float speed = (float)option["speed"];
            foreach (GameObject gameObj in Arr) {
                normalAM.addRandomAction(gameObj, speed);
            }
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            Vector3 force = (Vector3)option["force"];
            foreach (GameObject gameObj in Arr) {
                PhysicsAM.addForce(gameObj, force);
            }
        }
    }

    public void addActionForArr(UFOController[] Arr, Dictionary<string, object> option) {
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            float speed = (float)option["speed"];
            foreach (UFOController ctrl in Arr) {
                normalAM.addRandomAction(ctrl.getObj(), speed);
            }
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            Vector3 force = (Vector3)option["force"];
            foreach (UFOController ctrl in Arr) {
                PhysicsAM.addForce(ctrl.getObj(), force);
            }
        }
    }

    public void removeActionOf(GameObject gameObj, Dictionary<string, object> option){
        if (whichActionManager == 0)
        //  use normalAM
        {
            Debug.Log("use normalAM");
            normalAM.removeActionOf(gameObj);
        }

        else
        //  use PhysicsAM
        {
            Debug.Log("use PhysicsAM");
            PhysicsAM.removeForce(gameObj);
        }
    }
}

可以看出,我們在實現適配器的時候,將兩個動作管理器“焊死”在適配器上了,你還可以自己嘗試,實現一個可以“自由插拔”的適配器:)。

然后我們在FirstController的構造函數中實例化一個適配器(相當于將USB擴展器插在平板電腦上):

actionManagerTarget = new ActionManagerAdapter(gameObject);

最后不要忘了在Update中監測用戶鼠標的右鍵輸入,切換動作管理模式。最終的FirstController是這樣的:

public class FirstController : MonoBehaviour, SceneController
{
    Director director;

    UFOFactory UFOfactory;

    ExplosionFactory explosionFactory;

    ActionManagerTarget actionManagerTarget;

    bool switchAMInNextRound = false;

    Scorer scorer;

    DifficultyManager difficultyManager;

    float timeAfterRoundStart = 10;

    bool roundHasStarted = false;

    FirstCharacterController firstCharacterController;

    Text hint;

    void Awake()
    {
        // 掛載各種控制組件

        director = Director.getInstance();
        director.currentSceneController = this;

        // actionManager = gameObject.AddComponent<FirstSceneActionManager>();
        actionManagerTarget = new ActionManagerAdapter(gameObject);

        UFOfactory = gameObject.AddComponent<UFOFactory>();

        explosionFactory = gameObject.AddComponent<ExplosionFactory>();

        scorer = Scorer.getInstance();
        difficultyManager = DifficultyManager.getInstance();


        loadResources();
        Physics.IgnoreLayerCollision(LayerMask.NameToLayer("Shootable"), LayerMask.NameToLayer("Shootable"), true);
    }
    public void loadResources()
    {
        // 初始化場景中的物體
        firstCharacterController = new FirstCharacterController();
        Instantiate(Resources.Load("Terrain"));
        hint = (Instantiate(Resources.Load("ShowResult")) as GameObject).GetComponentInChildren<Text>();
        hint.text = "";
    }

    public void Start()
    {
        roundStart();
    }

    void Update()
    {
        if (roundHasStarted) {
            timeAfterRoundStart += Time.deltaTime;
        }

        if (roundHasStarted && checkAllUFOIsShot()) // 檢查是否所有UFO都已經被擊落
        {
            hint.text = "All UFO has crashed in this round! Next round in 3 sec";
            roundHasStarted = false;
            Invoke("roundStart", 3);
            difficultyManager.setDifficultyByScore(scorer.getScore());
        }
        else if (roundHasStarted && checkTimeOut()) // 檢查這一輪是否已經超時
        {
            hint.text = "Time out! Next round in 3 sec";
            roundHasStarted = false;
            foreach (UFOController ufo in UFOfactory.getUsingList())
            {
                actionManagerTarget.removeActionOf(ufo.getObj(), new Dictionary<string, object>());
            }
            UFOfactory.recycleAll();
            Invoke("roundStart", 3);
            difficultyManager.setDifficultyByScore(scorer.getScore());
        }
        if (Input.GetButtonDown("Fire2")) {
            hint.text = "Action of UFOs will change in the next round!";
            switchAMInNextRound = true;
        }
    }

    void roundStart()
    {   
        // 開始新的一輪
        if (switchAMInNextRound) {
            switchAMInNextRound = false;
            actionManagerTarget.switchActionMode();
        }

        roundHasStarted = true;
        timeAfterRoundStart = 0;
        UFOController[] ufoCtrlArr = UFOfactory.produceUFOs(difficultyManager.getUFOAttributes(), difficultyManager.UFONumber);
        for (int i = 0; i < ufoCtrlArr.Length; i++)
        {
            ufoCtrlArr[i].appear();
            ufoCtrlArr[i].setPosition(getRandomUFOPosition());
        }

        actionManagerTarget.addActionForArr(ufoCtrlArr, new Dictionary<string, object>() {
            {"speed", ufoCtrlArr[0].attr.speed},
            {"force", difficultyManager.getGravity()}
        });
        hint.text = "";
    }

    bool checkTimeOut()
    {
        if (timeAfterRoundStart > difficultyManager.currentSendInterval)
        {
            return true;
        }
        return false;
    }

    bool checkAllUFOIsShot()
    {
        return UFOfactory.getUsingList().Count == 0;
    }

    public void UFOIsShot(UFOController UFOCtrl)
    {
        // 響應UFO被擊中的事件
        scorer.record(difficultyManager.getDifficulty());
        actionManagerTarget.removeActionOf(UFOCtrl.getObj(), new Dictionary<string, object>());
        UFOfactory.recycle(UFOCtrl);
        explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
    }

    public void GroundIsShot(Vector3 pos) {
        // 響應地面被擊中的事件(直接產生一個爆炸)
        explosionFactory.explodeAt(pos);
    }

    public void UFOCrash(UFOController UFOCtrl) {
        actionManagerTarget.removeActionOf(UFOCtrl.getObj(), new Dictionary<string, object>());
        UFOfactory.recycle(UFOCtrl);
        explosionFactory.explodeAt(UFOCtrl.getObj().transform.position);
    }

    public Vector3 getRandomUFOPosition() {
        Vector3 relativeToCharacter = new Vector3(Random.Range(-10, 10), Random.Range(10, 15), Random.Range(-10, 10));
        return firstCharacterController.getPosition()+relativeToCharacter;
    }
}

注意我沒有在監測到鼠標右鍵輸入以后馬上切換動作管理模式,而是通過一點小技巧,延遲到下一輪開始的時候再切換。這是因為如果立刻切換,這一輪的“動作取消”會出很大的問題。你仔細想想,這一輪開始的時候,我們使用正常動作管理器給每個飛碟添加了一個普通的動作,而取消動作的時候卻使用物理動作管理器!這樣,飛碟上的普通動作就無法被回收,下一輪開始的時候飛碟依然在來回移動。

ActionManagerAdapter使用了一個非常靈活的方式來接收參數:Dictionary<string, object> option 其中的object可以傳遞任何類型的值,甚至是int、float原始類型。因為FirstController不知道當前的運動模式是什么,不知道應該給ActionManagerAdapter傳遞speed參數還是force參數,于是干脆兩個都傳進去,讓ActionManagerAdapter自己選擇:

actionManagerTarget.addActionForArr(ufoCtrlArr, new Dictionary<string, object>() {
            {"speed", ufoCtrlArr[0].attr.speed},
            {"force", difficultyManager.getGravity()}
        });

適配器模式補充說明

適配器模式定義

適配器模式(Adapter Pattern) :將一個接口轉換成客戶希望的另一個接口,適配器模式使接口不兼容的那些類可以一起工作,其別名為包裝器(Wrapper)。

需要接入2個類,而客戶類只提供1個接口,這也是一種“接口不兼容”。

適配器模式的組成

  • Target:目標抽象類(USB接口)
  • Adapter:適配器類(USB擴展器)
  • Adaptee:適配者類(鼠標、鍵盤、U盤)
  • Client:客戶類(平板電腦)

適配器的作用,除了我們剛才所說的,將多個類接入同一個接口以外,還有轉接“不兼容”接口的作用。比如說,如果我們想將U盤插入USB-typeC接口中,我們要買另一種適配器:

USB轉接器也是一種適配器

這個適配器也解決了“接口不兼容”的問題。當“客戶類提供的接口”與“適配者類”不兼容的時候,可以實現一個適配器,讓適配器實現“客戶類提供的接口”,并在這個適配器中調用“適配者類”的方法。

如果還想深入學習有關適配器模式的內容,可以看看這個網站


謝謝閱讀!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容