美文网首页Unity游戏开发入门UnityUnity3D游戏开发
学习Unity(5)小游戏实例——牧师与魔鬼

学习Unity(5)小游戏实例——牧师与魔鬼

作者: csRyan | 来源:发表于2017-03-14 00:40 被阅读1494次

    游戏规则:

    • 你要运用智慧帮助3个牧师(方块)和3个魔鬼(圆球)渡河。
    • 船最多可以载2名游戏角色。
    • 船上有游戏角色时,你才可以点击这个船,让船移动到对岸。
    • 当有一侧岸的魔鬼数多余牧师数时(包括船上的魔鬼和牧师),魔鬼就会失去控制,吃掉牧师(如果这一侧没有牧师则不会失败),游戏失败。
    • 当所有游戏角色都上到对岸时,游戏胜利。

    项目资源

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

    游戏截图:

    开始游戏 游戏失败 游戏胜利

    在Unity中体验

    从Github中下载我的项目
    将我的Asserts文件夹覆盖你的Unity项目中的Asserts文件夹。在你的Assets窗口中双击“ass”,然后就可以点击运行按钮了!


    游戏架构

    使用了MVC架构。

    • 场景中的所有GameObject就是Model,它们受到Controller的控制,比如说牧师和魔鬼受到MyCharacterController类的控制,船受到BoatController类的控制,河岸受到CoastController类的控制。
    • View就是UserGUI和ClickGUI,它们展示游戏结果,并提供用户交互的渠道(点击物体和按钮)。
    • Controller:除了刚才说的MyCharacterController、BoatController、CoastController以外,还有更高一层的Controller:FirstController(场景控制器),FirstController控制着这个场景中的所有对象,包括其加载、通信、用户输入。
      最高层的Controller是Director类,一个游戏中只能有一个实例,它控制着场景的创建、切换、销毁、游戏暂停、游戏退出等等最高层次的功能。

    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是最高层的控制器,运行游戏时始终只有一个实例,它掌控着场景的加载、切换等,也可以控制游戏暂停、结束等等。

    虽然Director控制着场景,但是它并不控制场景中的具体对象,控制场景对象的任务交给了SceneController(场景控制器),我们等一下会谈到。

    Director类使用了单例模式。第一次调用Director.getInstance()时,会创建一个新的Director对象,保存在_instance,此后每次调用getInstance,都回返回_instance。也就是说Director最多只有一个实例。这样,我们在任何Script中的任何地方通过Director.getInstance()都能得到同一个Director对象,也就可以获得同一个currentSceneController,这样我们就可以轻易实现类与类之间的通信,比如说我在其他控制器中就可以使用Director.getInstance().somethingHappen()来告诉导演某一件事情发生了,导演就可以在somethingHappen()方法中做出对应的反应。


    SceneController接口

    SceneController接口定义:

    public interface SceneController {
        void loadResources ();
    }
    

    interface(接口)不能直接用来创建对象!必须先有一个类实现(继承)它,在我的这个游戏中就是FirstController类。
    SceneController 是用来干什么的呢?它是导演控制场景控制器的渠道。在上面的Director 类中,currentSceneController (FirstController类)就是SceneController的实现,所以Director可以调用SceneController接口中的方法,来实现对场景的生杀予夺。

    在这个游戏中SceneController的定义非常简单,因为这个游戏做得并不完整。我们刚才说过导演可以加载、切换、销毁场景、暂停游戏,所以SceneController 还可以规定void switchScene()void destroyScene()void pause()这些方法,供给导演来调用。


    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,这就实现了一个折线的移动,不会穿越河岸。moving_status记录着目前该物体处于哪种移动状态。


    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);
        }
    }
    

    在构造函数中实例化了一个perfab,创建GameObject,因此我们每new MyCharacterController()一次,场景中就会多一个游戏角色。
    构造函数还将clickGUI挂载到了这个角色上,以监测“鼠标点击角色”的事件。

    MyCharacterController还定义了一些方法提供给场景控制器来调用,方法名已经能够表明这个方法是做什么的了。


    BoatController和CoastController

    BoatController和CoastController也类似MyCharacterController,封装了船GameObject和河岸GameObject。实现这两个类的难度主要在于它们是一种“容器”,游戏角色要进入它们的空位中。因此它们要提供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有一些方法名是重复的,比如说getOnBoat在MyCharacterController和BoatController中都有(BoatController中的GetOnBoat是我当时手抖了,第一个字母应该小写)。看起来似乎功能有点重复,为什么不只用一个函数操控游戏角色的上船呢?原因是不要在一个类中操作另一个类,那会加强两个类之间的耦合性。MyCharacterController中的getOnBoat()只应该操作MyCharacterController中的成员,BoatController中的GetOnBoat()只应该操作BoatController中的成员。
    我们在FirstController中想让游戏角色上船的时候,两个类的getOnBoat都要调用:

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

    UserAction

    这个接口实际上使用了门面模式。
    FirstController必须要实现这个接口才能对用户的输入做出反应。

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

    在这个游戏中,对用户输入做出反应,有这三个方法就够了。
    UserAction是如何得到用户的输入的呢?原来,在ClickGUI和UserGUI这两个类中,都保存了一个UserAction的引用。当ClickGUI监测到用户点击GameObject的时候,就会调用这个引用的characterIsClicked方法,这样FirstController就知道哪一个游戏角色被点击了。UserGUI同理,只不过它监测的是“用户点击Restart按钮”的事件。

    门面模式的好处:通过一套接口(UserAction)来定义Controller与GUI交互的渠道,这样实现Controller类的程序员只需要实现UserAction接口,他的代码就可以被任何支持这个接口的GUI类所使用;实现GUI类的程序员也不需要知道Controller的实现方式,它只需要调用接口中的方法,后面的事情就交给Controller吧!


    ClickGUI

    ClickGUI类是用来监测用户点击,并调用SceneController进行响应的。

    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的对象,它实现了UserAction接口。ClickGUI与FirstController打交道,就是通过UserAction接口的API。ClickGUI不知道这些API是怎么被实现的,但它知道FirstController类一定有这些方法。


    可以做的扩展:

    • 游戏失败以后不能再响应用户点击的事件,用户只能点击Restart。
    • 增加计时的功能(这应该由SceneController来控制)。
    • 增加暂停/恢复游戏的功能(这应该由Director来控制)。
    • 在开始游戏之前做一个欢迎界面,与用户进行交互(这就是另一个场景了)。
    • 让用户可以在游戏中切换到欢迎界面,再切换回游戏界面的时候,游戏状态要和之前一样(场景的切换)。用户可以在游戏中放弃游戏,回到欢迎页面(场景的销毁)。
    • 让用户能够在欢迎界面指定有几个牧师几个恶魔,然后开始游戏。(运行时决定场景的创建)
    • 增加一种更难的模式,开始3秒以后牧师和恶魔外观相同,玩家需要凭借记忆来操作。
    • 美化游戏对象!

    相关文章

      网友评论

        本文标题:学习Unity(5)小游戏实例——牧师与魔鬼

        本文链接:https://www.haomeiwen.com/subject/fwkxnttx.html