學習Unity(5)小游戲?qū)嵗翈熍c魔鬼

游戲規(guī)則:

  • 你要運用智慧幫助3個牧師(方塊)和3個魔鬼(圓球)渡河蔗蹋。
  • 船最多可以載2名游戲角色旁振。
  • 船上有游戲角色時,你才可以點擊這個船钦睡,讓船移動到對岸墓律。
  • 當有一側(cè)岸的魔鬼數(shù)多余牧師數(shù)時(包括船上的魔鬼和牧師)膀估,魔鬼就會失去控制,吃掉牧師(如果這一側(cè)沒有牧師則不會失敵芊怼)察纯,游戲失敗。
  • 當所有游戲角色都上到對岸時针肥,游戲勝利饼记。

項目資源

https://github.com/csr632/Priests-and-devils

游戲截圖:

開始游戲
游戲失敗
游戲勝利

在Unity中體驗

從Github中下載我的項目
將我的Asserts文件夾覆蓋你的Unity項目中的Asserts文件夾祖驱。在你的Assets窗口中雙擊“ass”握恳,然后就可以點擊運行按鈕了!


游戲架構(gòu)

使用了MVC架構(gòu)捺僻。

  • 場景中的所有GameObject就是Model乡洼,它們受到Controller的控制,比如說牧師和魔鬼受到MyCharacterController類的控制匕坯,船受到BoatController類的控制束昵,河岸受到CoastController類的控制。
  • View就是UserGUI和ClickGUI葛峻,它們展示游戲結(jié)果锹雏,并提供用戶交互的渠道(點擊物體和按鈕)。
  • Controller:除了剛才說的MyCharacterController术奖、BoatController礁遵、CoastController以外轻绞,還有更高一層的Controller:FirstController(場景控制器),F(xiàn)irstController控制著這個場景中的所有對象佣耐,包括其加載政勃、通信、用戶輸入兼砖。
    最高層的Controller是Director類奸远,一個游戲中只能有一個實例,它控制著場景的創(chuàng)建讽挟、切換懒叛、銷毀、游戲暫停耽梅、游戲退出等等最高層次的功能薛窥。

Director

Director的定義:

public class Director : System.Object {
    private static Director _instance;
    public SceneController currentSceneController { get; set; }

    public static Director getInstance() {
        if (_instance == null) {
            _instance = new Director ();
        }
        return _instance;
    }
}

Director是最高層的控制器,運行游戲時始終只有一個實例褐墅,它掌控著場景的加載拆檬、切換等,也可以控制游戲暫停妥凳、結(jié)束等等。

雖然Director控制著場景答捕,但是它并不控制場景中的具體對象逝钥,控制場景對象的任務(wù)交給了SceneController(場景控制器),我們等一下會談到拱镐。

Director類使用了單例模式艘款。第一次調(diào)用Director.getInstance()時,會創(chuàng)建一個新的Director對象沃琅,保存在_instance哗咆,此后每次調(diào)用getInstance,都回返回_instance益眉。也就是說Director最多只有一個實例晌柬。這樣,我們在任何Script中的任何地方通過Director.getInstance()都能得到同一個Director對象郭脂,也就可以獲得同一個currentSceneController年碘,這樣我們就可以輕易實現(xiàn)類與類之間的通信,比如說我在其他控制器中就可以使用Director.getInstance().somethingHappen()來告訴導(dǎo)演某一件事情發(fā)生了展鸡,導(dǎo)演就可以在somethingHappen()方法中做出對應(yīng)的反應(yīng)屿衅。


SceneController接口

SceneController接口定義:

public interface SceneController {
    void loadResources ();
}

interface(接口)不能直接用來創(chuàng)建對象!必須先有一個類實現(xiàn)(繼承)它莹弊,在我的這個游戲中就是FirstController類涤久。
SceneController 是用來干什么的呢涡尘?它是導(dǎo)演控制場景控制器的渠道。在上面的Director 類中响迂,currentSceneController (FirstController類)就是SceneController的實現(xiàn)考抄,所以Director可以調(diào)用SceneController接口中的方法,來實現(xiàn)對場景的生殺予奪栓拜。

在這個游戲中SceneController的定義非常簡單座泳,因為這個游戲做得并不完整。我們剛才說過導(dǎo)演可以加載幕与、切換挑势、銷毀場景、暫停游戲啦鸣,所以SceneController 還可以規(guī)定void switchScene()潮饱、void destroyScene()void pause()這些方法诫给,供給導(dǎo)演來調(diào)用香拉。


Moveable

Moveable是一個可以掛載在GameObject上的類:

public class Moveable: MonoBehaviour {
    
    readonly float move_speed = 20;

    // change frequently
    int moving_status;  // 0->not moving, 1->moving to middle, 2->moving to dest
    Vector3 dest;
    Vector3 middle;

    void Update() {
        if (moving_status == 1) {
            transform.position = Vector3.MoveTowards (transform.position, middle, move_speed * Time.deltaTime);
            if (transform.position == middle) {
                moving_status = 2;
            }
        } else if (moving_status == 2) {
            transform.position = Vector3.MoveTowards (transform.position, dest, move_speed * Time.deltaTime);
            if (transform.position == dest) {
                moving_status = 0;
            }
        }
    }
    public void setDestination(Vector3 _dest) {
        dest = _dest;
        middle = _dest;
        if (_dest.y == transform.position.y) {  // boat moving
            moving_status = 2;
        }
        else if (_dest.y < transform.position.y) {  // character from coast to boat
            middle.y = transform.position.y;
        } else {                                // character from boat to coast
            middle.x = transform.position.x;
        }
        moving_status = 1;
    }

    public void reset() {
        moving_status = 0;
    }
}

GameObject掛載上Moveable以后,Controller就可以通過setDestination()方法輕松地讓GameObject移動起來中狂。

在這里我沒有讓物體直接移動到目的地dest凫碌,因為那樣可能會直接穿過河岸物體。我用middle來保存一個中間位置胃榕,讓物體先移動到middle盛险,再移動到dest,這就實現(xiàn)了一個折線的移動勋又,不會穿越河岸苦掘。moving_status記錄著目前該物體處于哪種移動狀態(tài)。


MyCharacterController

MyCharacterController封裝了一個GameObject楔壤,表示游戲角色(牧師或惡魔)鹤啡。

public class MyCharacterController {
    readonly GameObject character;
    readonly Moveable moveableScript;
    readonly ClickGUI clickGUI;
    readonly int characterType; // 0->priest, 1->devil

    // change frequently
    bool _isOnBoat;
    CoastController coastController;


    public MyCharacterController(string which_character) {
        
        if (which_character == "priest") {
            character = Object.Instantiate (Resources.Load ("Perfabs/Priest", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
            characterType = 0;
        } else {
            character = Object.Instantiate (Resources.Load ("Perfabs/Devil", typeof(GameObject)), Vector3.zero, Quaternion.identity, null) as GameObject;
            characterType = 1;
        }
        moveableScript = character.AddComponent (typeof(Moveable)) as Moveable;

        clickGUI = character.AddComponent (typeof(ClickGUI)) as ClickGUI;
        clickGUI.setController (this);
    }

    public void setName(string name) {
        character.name = name;
    }

    public void setPosition(Vector3 pos) {
        character.transform.position = pos;
    }

    public void moveToPosition(Vector3 destination) {
        moveableScript.setDestination(destination);
    }

    public int getType() {  // 0->priest, 1->devil
        return characterType;
    }

    public string getName() {
        return character.name;
    }

    public void getOnBoat(BoatController boatCtrl) {
        coastController = null;
        character.transform.parent = boatCtrl.getGameobj().transform;
        _isOnBoat = true;
    }

    public void getOnCoast(CoastController coastCtrl) {
        coastController = coastCtrl;
        character.transform.parent = null;
        _isOnBoat = false;
    }

    public bool isOnBoat() {
        return _isOnBoat;
    }

    public CoastController getCoastController() {
        return coastController;
    }

    public void reset() {
        moveableScript.reset ();
        coastController = (Director.getInstance ().currentSceneController as FirstController).fromCoast;
        getOnCoast (coastController);
        setPosition (coastController.getEmptyPosition ());
        coastController.getOnCoast (this);
    }
}

在構(gòu)造函數(shù)中實例化了一個perfab,創(chuàng)建GameObject蹲嚣,因此我們每new MyCharacterController()一次递瑰,場景中就會多一個游戲角色。
構(gòu)造函數(shù)還將clickGUI掛載到了這個角色上端铛,以監(jiān)測“鼠標點擊角色”的事件泣矛。

MyCharacterController還定義了一些方法提供給場景控制器來調(diào)用,方法名已經(jīng)能夠表明這個方法是做什么的了禾蚕。


BoatController和CoastController

BoatController和CoastController也類似MyCharacterController您朽,封裝了船GameObject和河岸GameObject。實現(xiàn)這兩個類的難度主要在于它們是一種“容器”,游戲角色要進入它們的空位中哗总。因此它們要提供getEmptyPosition()方法几颜,給出自己的空位,讓游戲角色能夠移動到合適的位置讯屈。

/*-----------------------------------CoastController------------------------------------------*/
public class CoastController {
    readonly GameObject coast;
    readonly Vector3 from_pos = new Vector3(9,1,0);
    readonly Vector3 to_pos = new Vector3(-9,1,0);
    readonly Vector3[] positions;
    readonly int to_or_from;    // to->-1, from->1

    // change frequently
    MyCharacterController[] passengerPlaner;

    public CoastController(string _to_or_from) {
        positions = new Vector3[] {new Vector3(6.5F,2.25F,0), new Vector3(7.5F,2.25F,0), new Vector3(8.5F,2.25F,0), 
            new Vector3(9.5F,2.25F,0), new Vector3(10.5F,2.25F,0), new Vector3(11.5F,2.25F,0)};

        passengerPlaner = new MyCharacterController[6];

        if (_to_or_from == "from") {
            coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), from_pos, Quaternion.identity, null) as GameObject;
            coast.name = "from";
            to_or_from = 1;
        } else {
            coast = Object.Instantiate (Resources.Load ("Perfabs/Stone", typeof(GameObject)), to_pos, Quaternion.identity, null) as GameObject;
            coast.name = "to";
            to_or_from = -1;
        }
    }

    public int getEmptyIndex() {
        for (int i = 0; i < passengerPlaner.Length; i++) {
            if (passengerPlaner [i] == null) {
                return i;
            }
        }
        return -1;
    }

    public Vector3 getEmptyPosition() {
        Vector3 pos = positions [getEmptyIndex ()];
        pos.x *= to_or_from;
        return pos;
    }

    public void getOnCoast(MyCharacterController characterCtrl) {
        int index = getEmptyIndex ();
        passengerPlaner [index] = characterCtrl;
    }

    public MyCharacterController getOffCoast(string passenger_name) {   // 0->priest, 1->devil
        for (int i = 0; i < passengerPlaner.Length; i++) {
            if (passengerPlaner [i] != null && passengerPlaner [i].getName () == passenger_name) {
                MyCharacterController charactorCtrl = passengerPlaner [i];
                passengerPlaner [i] = null;
                return charactorCtrl;
            }
        }
        Debug.Log ("cant find passenger on coast: " + passenger_name);
        return null;
    }

    public int get_to_or_from() {
        return to_or_from;
    }

    public int[] getCharacterNum() {
        int[] count = {0, 0};
        for (int i = 0; i < passengerPlaner.Length; i++) {
            if (passengerPlaner [i] == null)
                continue;
            if (passengerPlaner [i].getType () == 0) {  // 0->priest, 1->devil
                count[0]++;
            } else {
                count[1]++;
            }
        }
        return count;
    }

    public void reset() {
        passengerPlaner = new MyCharacterController[6];
    }
}

/*-----------------------------------BoatController------------------------------------------*/
public class BoatController {
    readonly GameObject boat;
    readonly Moveable moveableScript;
    readonly ClickGUI clickGUI;
    readonly Vector3 fromPosition = new Vector3 (5, 1, 0);
    readonly Vector3 toPosition = new Vector3 (-5, 1, 0);
    readonly Vector3[] from_positions;
    readonly Vector3[] to_positions;

    // change frequently
    int to_or_from; // to->-1; from->1
    MyCharacterController[] passenger = new MyCharacterController[2];

    public BoatController() {
        to_or_from = 1;

        from_positions = new Vector3[] { new Vector3 (4.5F, 1.5F, 0), new Vector3 (5.5F, 1.5F, 0) };
        to_positions = new Vector3[] { new Vector3 (-5.5F, 1.5F, 0), new Vector3 (-4.5F, 1.5F, 0) };

        boat = Object.Instantiate (Resources.Load ("Perfabs/Boat", typeof(GameObject)), fromPosition, Quaternion.identity, null) as GameObject;
        boat.name = "boat";

        moveableScript = boat.AddComponent (typeof(Moveable)) as Moveable;
        clickGUI = boat.AddComponent (typeof(ClickGUI)) as ClickGUI;
    }


    public void Move() {
        if (to_or_from == -1) {
            moveableScript.setDestination(fromPosition);
            to_or_from = 1;
        } else {
            moveableScript.setDestination(toPosition);
            to_or_from = -1;
        }
    }

    public int getEmptyIndex() {
        for (int i = 0; i < passenger.Length; i++) {
            if (passenger [i] == null) {
                return i;
            }
        }
        return -1;
    }

    public bool isEmpty() {
        for (int i = 0; i < passenger.Length; i++) {
            if (passenger [i] != null) {
                return false;
            }
        }
        return true;
    }

    public Vector3 getEmptyPosition() {
        Vector3 pos;
        int emptyIndex = getEmptyIndex ();
        if (to_or_from == -1) {
            pos = to_positions[emptyIndex];
        } else {
            pos = from_positions[emptyIndex];
        }
        return pos;
    }

    public void GetOnBoat(MyCharacterController characterCtrl) {
        int index = getEmptyIndex ();
        passenger [index] = characterCtrl;
    }

    public MyCharacterController GetOffBoat(string passenger_name) {
        for (int i = 0; i < passenger.Length; i++) {
            if (passenger [i] != null && passenger [i].getName () == passenger_name) {
                MyCharacterController charactorCtrl = passenger [i];
                passenger [i] = null;
                return charactorCtrl;
            }
        }
        Debug.Log ("Cant find passenger in boat: " + passenger_name);
        return null;
    }

    public GameObject getGameobj() {
        return boat;
    }

    public int get_to_or_from() { // to->-1; from->1
        return to_or_from;
    }

    public int[] getCharacterNum() {
        int[] count = {0, 0};
        for (int i = 0; i < passenger.Length; i++) {
            if (passenger [i] == null)
                continue;
            if (passenger [i].getType () == 0) {    // 0->priest, 1->devil
                count[0]++;
            } else {
                count[1]++;
            }
        }
        return count;
    }

    public void reset() {
        moveableScript.reset ();
        if (to_or_from == -1) {
            Move ();
        }
        passenger = new MyCharacterController[2];
    }
}

另外一個需要注意的是MyCharacterController蛋哭、BoatController、CoastController有一些方法名是重復(fù)的涮母,比如說getOnBoat在MyCharacterController和BoatController中都有(BoatController中的GetOnBoat是我當時手抖了谆趾,第一個字母應(yīng)該小寫)∨驯荆看起來似乎功能有點重復(fù)沪蓬,為什么不只用一個函數(shù)操控游戲角色的上船呢?原因是不要在一個類中操作另一個類来候,那會加強兩個類之間的耦合性跷叉。MyCharacterController中的getOnBoat()只應(yīng)該操作MyCharacterController中的成員,BoatController中的GetOnBoat()只應(yīng)該操作BoatController中的成員营搅。
我們在FirstController中想讓游戲角色上船的時候云挟,兩個類的getOnBoat都要調(diào)用:

whichCoast.getOffCoast(characterCtrl.getName());
characterCtrl.moveToPosition (boat.getEmptyPosition());
characterCtrl.getOnBoat (boat);
boat.GetOnBoat (characterCtrl);

UserAction

這個接口實際上使用了門面模式。
FirstController必須要實現(xiàn)這個接口才能對用戶的輸入做出反應(yīng)转质。

public interface UserAction {
    void moveBoat();
    void characterIsClicked(MyCharacterController characterCtrl);
    void restart();
}

在這個游戲中园欣,對用戶輸入做出反應(yīng),有這三個方法就夠了休蟹。
UserAction是如何得到用戶的輸入的呢俊庇?原來,在ClickGUI和UserGUI這兩個類中鸡挠,都保存了一個UserAction的引用。當ClickGUI監(jiān)測到用戶點擊GameObject的時候搬男,就會調(diào)用這個引用的characterIsClicked方法拣展,這樣FirstController就知道哪一個游戲角色被點擊了。UserGUI同理缔逛,只不過它監(jiān)測的是“用戶點擊Restart按鈕”的事件备埃。

門面模式的好處:通過一套接口(UserAction)來定義Controller與GUI交互的渠道,這樣實現(xiàn)Controller類的程序員只需要實現(xiàn)UserAction接口褐奴,他的代碼就可以被任何支持這個接口的GUI類所使用按脚;實現(xiàn)GUI類的程序員也不需要知道Controller的實現(xiàn)方式,它只需要調(diào)用接口中的方法敦冬,后面的事情就交給Controller吧辅搬!


ClickGUI

ClickGUI類是用來監(jiān)測用戶點擊,并調(diào)用SceneController進行響應(yīng)的脖旱。

public class ClickGUI : MonoBehaviour {
    UserAction action;
    MyCharacterController characterController;

    public void setController(MyCharacterController characterCtrl) {
        characterController = characterCtrl;
    }

    void Start() {
        action = Director.getInstance ().currentSceneController as UserAction;
    }

    void OnMouseDown() {
        if (gameObject.name == "boat") {
            action.moveBoat ();
        } else {
            action.characterIsClicked (characterController);
        }
    }
}

我們可以看到UserAction action實際上是FirstController的對象堪遂,它實現(xiàn)了UserAction接口介蛉。ClickGUI與FirstController打交道,就是通過UserAction接口的API溶褪。ClickGUI不知道這些API是怎么被實現(xiàn)的币旧,但它知道FirstController類一定有這些方法。


可以做的擴展:

  • 游戲失敗以后不能再響應(yīng)用戶點擊的事件猿妈,用戶只能點擊Restart吹菱。
  • 增加計時的功能(這應(yīng)該由SceneController來控制)。
  • 增加暫停/恢復(fù)游戲的功能(這應(yīng)該由Director來控制)彭则。
  • 在開始游戲之前做一個歡迎界面鳍刷,與用戶進行交互(這就是另一個場景了)。
  • 讓用戶可以在游戲中切換到歡迎界面贰剥,再切換回游戲界面的時候倾剿,游戲狀態(tài)要和之前一樣(場景的切換)。用戶可以在游戲中放棄游戲蚌成,回到歡迎頁面(場景的銷毀)前痘。
  • 讓用戶能夠在歡迎界面指定有幾個牧師幾個惡魔,然后開始游戲担忧。(運行時決定場景的創(chuàng)建)
  • 增加一種更難的模式芹缔,開始3秒以后牧師和惡魔外觀相同,玩家需要憑借記憶來操作瓶盛。
  • 美化游戲?qū)ο螅?/li>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末最欠,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子惩猫,更是在濱河造成了極大的恐慌芝硬,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件轧房,死亡現(xiàn)場離奇詭異拌阴,居然都是意外死亡,警方通過查閱死者的電腦和手機奶镶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門迟赃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人厂镇,你說我怎么就攤上這事纤壁。” “怎么了捺信?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵酌媒,是天一觀的道長。 經(jīng)常有香客問我,道長馍佑,這世上最難降的妖魔是什么斋否? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮拭荤,結(jié)果婚禮上茵臭,老公的妹妹穿的比我還像新娘。我一直安慰自己舅世,他們只是感情好旦委,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著雏亚,像睡著了一般缨硝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上罢低,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天查辩,我揣著相機與錄音,去河邊找鬼网持。 笑死宜岛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的功舀。 我是一名探鬼主播萍倡,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辟汰!你這毒婦竟也來了列敲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤帖汞,失蹤者是張志新(化名)和其女友劉穎戴而,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體翩蘸,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡填硕,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鹿鳖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡壮莹,死狀恐怖翅帜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情命满,我是刑警寧澤涝滴,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響歼疮,放射性物質(zhì)發(fā)生泄漏杂抽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一韩脏、第九天 我趴在偏房一處隱蔽的房頂上張望缩麸。 院中可真熱鬧,春花似錦赡矢、人聲如沸杭朱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弧械。三九已至,卻和暖如春空民,著一層夾襖步出監(jiān)牢的瞬間刃唐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工界轩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留画饥,地道東北人。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓耸棒,卻偏偏與公主長得像荒澡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子与殃,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容