Unity3d学习之路-牧师与魔鬼

Unity3d学习之路-牧师与魔鬼


游戏基本介绍

  • 游戏规则:

    Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game, you can click on them to move them and click the go button to move the boat to the other direction. If the priests are out numbered by the devils on either side of the river, they get killed and the game is over. You can try it in many > ways. Keep all priests alive! Good luck!

  • 游戏中提及的事物(Objects):
    魔鬼,牧师,船,河流,两边的陆地

  • 用表格列出玩家动作表(规则表):

玩家动作 执行条件 执行结果
点击牧师/魔鬼 游戏未结束,船没有正在移动,与船在相同的一边 牧师/魔鬼移动
点击船 游戏未结束且船上至少有一人 船移动
  • 本游戏架构参考下图:

MVC
MVC模式真的很好用,用过都说好,不过本次游戏只创建了三个脚本,所以接口和导演是和GameModels放在同一个脚本的同一命名空间下。

游戏制作流程

  • 搭建场景
    在Unity官方的Asset Store输入需要查找的资源,找到了免费的模型且模型带有动画(Asset Store中有很多免费的资源但是注意版权哦),然后布置场景,把它们放在场景中,记录他们不同状态的时候的坐标,方便接下来使用。

  • 创建各类构建基本的架构

    1.SSDirector
    利用单例模式创建导演,和Cocos2d-x一样一个游戏导演只能有一个,这里继承于System.Object,保持导演类一直存在,不被Unity内存管理而管理,导演类类似于生活中的导演,安排场景,场景切换,都是靠它来指挥。

    public class SSDirector : System.Object
    {
        private static SSDirector _instance;             //导演类的实例
        public ISceneController CurrentScenceController { get; set; }
        public static SSDirector GetInstance()           
        {
            if (_instance == null)
            {
                _instance = new SSDirector();
            }
            return _instance;
        }
    }

2.ISceneController
这是一个场景的控制器的接口,算是导演与场景控制器沟通的接口,利用这个接口,得知当前场景是由哪个控制,然后向场景控制器传达要求等。

   public interface ISceneController                      
{
    void LoadResources();                                  //加载场景
}

3.IUserAction
用户进行操作后与游戏中发生响应的接口,用户通过键盘、鼠标等对游戏发出指令,这个指令会触发游戏中的一些行为,(比如在这个游戏中,点击角色让角色移动,这个角色移动就是用户动作后触发的行为),由IUserAction来声明

    public interface IUserAction                           //用户互动会发生的事件
    {
        void MoveBoat();                                   //移动船
        void Restart();                                    //重新开始
        void MoveRole(RoleModel role);                     //移动角色
        int Check();                                       //检测游戏结束
    }

4.FirstController
这是一个控制器,对场景中的具体对象进行操作(其实也是使用GameModels给出的函数进行控制),可以看到这个控制器继承了两个接口类并实现了它们的方法,控制器是场景中各游戏对象行为改变的核心

public class Controllor : MonoBehaviour, ISceneController, IUserAction
{
    public LandModel start_land;            //开始陆地
    public LandModel end_land;              //结束陆地
    public BoatModel boat;                  //船
    private RoleModel[] roles;              //角色
    UserGUI user_gui;

    void Start ()
    {
        SSDirector director = SSDirector.GetInstance();      //得到导演实例
        director.CurrentScenceController = this;             //设置当前场景控制器
        user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;  //添加UserGUI脚本作为组件
        LoadResources();                                     //加载资源
    }

    //创建水,陆地,角色,船
    //移动船
    //移动角色
    //重新开始游戏
    //检测游戏是否结束
}

5.UserGUI
建立用户的交互界面,比如按钮和标签,

public class UserGUI : MonoBehaviour {
    private IUserAction action;
    public int sign = 0;

    bool isShow = false;
    void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    }
    void OnGUI()
    {
        GUIStyle text_style;
        GUIStyle button_style;
        text_style = new GUIStyle()
        {
            fontSize = 30
        };
        button_style = new GUIStyle("button")
        {
            fontSize = 15
        };
        if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", button_style))
        {
            if (isShow)
                isShow = false;
            else
                isShow = true;
        }
        if(isShow)
        {
            GUI.Label(new Rect(Screen.width / 2 - 85, 10, 200, 50), "让全部牧师和恶魔都渡河");
            GUI.Label(new Rect(Screen.width / 2 - 120, 30, 250, 50), "每一边恶魔数量都不能多于牧师数量");
            GUI.Label(new Rect(Screen.width / 2 - 85, 50, 250, 50), "点击牧师、恶魔、船移动");
        }
        if (sign == 1)
        {
            GUI.Label(new Rect(Screen.width / 2-90, Screen.height / 2-120, 100, 50), "Gameover!", text_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 70, Screen.height / 2, 100, 50), "Restart", button_style))
            {
                action.Restart();
                sign = 0;
            }
        }
        else if (sign == 2)
        {
            GUI.Label(new Rect(Screen.width / 2 - 80, Screen.height / 2 - 120, 100, 50), "You Win!", text_style);
            if (GUI.Button(new Rect(Screen.width / 2 - 70, Screen.height / 2, 100, 50), "Restart", button_style))
            {
                action.Restart();
                sign = 0;
            }
        }
    }
}

6.GameModels
游戏场景中可交互的游戏模型,在这个游戏中我建立了角色模型和船模型以及陆地模型给控制器留出函数供其调用

  • 这里先讲一下关于Tag:
    Tag可以用来识别游戏对象,在Inspector中名字的下面可以看到Tag,官方文档中对于Tag的好处描述得很清楚,这里我是用每个GameObject的name来区分不同的对象的,因为GameObject比较少,如果比较多的话建议还是使用Tag。

  • LandModel(陆地模型)
    1.先来想一下陆地模型应该具备什么属性吧,首先陆地有两块,所以我们需要一个标志位来记录是开始的陆地还是结束陆地,然后陆地的位置(因为陆地被创建后就不动了只用一次,所以我在创建时直接赋值给陆地的position所以没有记录),以及陆地上的角色(用角色模型的数组来记录),每个角色的位置(Vector3的数组来记录)。

        GameObject land;                        //陆地对象
        Vector3[] positions;                    //保存每个角色放在陆地上的位置
        int land_sign;                          //到达陆地标志为-1,开始陆地标志为1
        RoleModel[] roles = new RoleModel[6];   //陆地上有的角色

2.陆地应该有的函数:应该知道在自己陆地上牧师和魔鬼各自的数量,从陆地上移除一个角色,添加一个角色,知道陆地上哪一个是第一个空的位置(角色登陆的时候放上去),游戏结束的重置等。

因为陆地的摆放是按照z轴对称的,所以x坐标是相反数,得到陆地空位置的时候就可以乘陆地的标志就行,这是一个比较巧妙的方法

    public Vector3 GetEmptyPosition()               //得到陆地上空位置
    {
        Vector3 pos = positions[GetEmptyNumber()];
        pos.x = land_sign * pos.x;                  //因为两个陆地是x坐标对称
        return pos;
    }
  • BoatModel(船模型)
    1.那再来想一下船模型应该具有什么属性:船在开始/结束陆地旁的位置,在开始/结束陆地旁船上可以载客的两个位置(用Vector3的数组表示),船上载有的角色(用角色模型的数组来记录),标记船在开始陆地还是结束陆地的旁边。
        int boat_sign = 1;                              //船在开始还是结束陆地
        RoleModel[] roles = new RoleModel[2];           //船上的角色
        GameObject boat;                                          
        Vector3[] start_empty_pos;      //船在开始陆地的空位位置
        Vector3[] end_empty_pos;        //船在结束陆地的空位位置

2.船应该有的函数:与陆地拥有的函数类似,增加了一个移动船的函数,让船到设定的位置

public void BoatMove()      //封装了一个MovePosition,传进要移动到的位置就可以移动啦
{
    if (boat_sign == -1)
    {
        move.MovePosition(new Vector3(38, -3.9F, -2.9F));
        boat_sign = 1;
    }
    else
    {
        move.MovePosition(new Vector3(-1.4F, -4, -3));
        boat_sign = -1;
    }
}
  • RoleModel(角色模型)
    1.一个角色的属性:标志角色是牧师还是恶魔,标志是否在船上
    2.角色模型的函数:去到陆地/船上(其实就是把哪个作为父节点,并且修改是否在船上标志),其他就是基本的get/set函数。
    3.这里讲解一下如何加载预制体吧:
    • 首先用Resources.Load函数,第一个参数是你的预制体的路径(注意:路径名使用正斜杠“/”,如果使用反斜杠“\”会不正常运行),第二个参数是要返回的对象类型,当然你也可以只写第一个参数但是需要强制转换成为其他对象类型。Resources.Load函数让我们用一个对象来保存我们加载好的预制体,但预制体没有载入到场景中,还未进行实例化。
    • 然后使用Object.Instantiate函数,它传入一个你想要初始化的对象,可以设置位置,角度,和父节点的Transform等,这样一个实例化的预制体就出现在场景中了,更多的参数了解请移步官方文档
public class RoleModel
{
    GameObject role;
    int role_sign;             //0为牧师,1为恶魔
    Click click;               //点击触发的脚本
    bool on_boat;              //是否在船上       
    Move move;                 //移动的脚本
    PlayAnimation play_ani;    //动画脚本
    LandModel land_model = (SSDirector.GetInstance().CurrentScenceController as Controllor).start_land;     //所在陆地模型

    public RoleModel(string role_name)
    {
        if (role_name == "priest")
        {
            role = Object.Instantiate(Resources.Load("Priest", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -90, 0)) as GameObject;
            role_sign = 0;
        }
        else
        {
            role = Object.Instantiate(Resources.Load("Devil", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -90, 0)) as GameObject;
            role_sign = 1;
        }
        move = role.AddComponent(typeof(Move)) as Move;
        click = role.AddComponent(typeof(Click)) as Click;
        play_ani = role.AddComponent(typeof(PlayAnimation)) as PlayAnimation;
        click.SetRole(this);
    }

    public int GetSign() { return role_sign;}
    public LandModel GetLandModel(){return land_model;}
    public string GetName() { return role.name; }
    public bool IsOnBoat() { return on_boat; }
    public void SetName(string name) { role.name = name; }
    public void SetPosition(Vector3 pos) { role.transform.position = pos; }

    public void PlayGameOver()
    {
        play_ani.Play();
    }

    public void PlayIdle()
    {
        play_ani.NotPlay();
    }

    public void Move(Vector3 vec)
    {
        move.MovePosition(vec);
    }

    public void GoLand(LandModel land)
    {  
        role.transform.parent = null;
        land_model = land;
        on_boat = false;
    }

    public void GoBoat(BoatModel boat)
    {
        role.transform.parent = boat.GetBoat().transform;
        land_model = null;          
        on_boat = true;
    }

    public void Reset()
    {
        land_model = (SSDirector.GetInstance().CurrentScenceController as Controllor).start_land;
        GoLand(land_model);
        SetPosition(land_model.GetEmptyPosition());
        land_model.AddRole(this);
    }
}

7.其他辅助脚本
这些脚本挂载预制体上(挂载方式是使用AddComponent函数),帮助实现一些动作

  • Move脚本
    先让角色水平移动再垂直移动或是先垂直移动再实现角色水平移动,通过每一帧检测是否移动
   public class Move : MonoBehaviour
   {
       float move_speed = 250;                   //移动速度
       int move_sign = 0;                        //0是不动,1水平移动,2竖直移动
       Vector3 end_pos;
       Vector3 middle_pos;

       void Update()
       {
           if (move_sign == 1)
           {
               transform.position = Vector3.MoveTowards(transform.position, middle_pos, move_speed * Time.deltaTime);
               if (transform.position == middle_pos)
                   move_sign = 2;
           }
           else if (move_sign == 2)
           {
               transform.position = Vector3.MoveTowards(transform.position, end_pos, move_speed * Time.deltaTime);
               if (transform.position == end_pos)
                   move_sign = 0;           
           }
       }
       public void MovePosition(Vector3 position)
       {
           end_pos = position;
           if (position.y == transform.position.y)         //船只会水平移动
           {  
               move_sign = 2;
           }
           else if (position.y < transform.position.y)      //角色从陆地到船
           {
               middle_pos = new Vector3(position.x, transform.position.y, position.z);
           }
           else                                          //角色从船到陆地
           {
               middle_pos = new Vector3(transform.position.x, position.y, position.z);
           }
           move_sign = 1;
       }
   }
  • Click脚本
    检测船和角色是否被点击(这里别忘了,只有物体加上了Collider才能实现检测点击事件发生哦),人形模型推荐使用Capsule Collider,船的话我使用的是Box Collider,关于Collider的介绍请移步官方文档,这里多说一句,因为当鼠标被按下的时候,unity会在你鼠标点击的那个点,向游戏场景内发送一条射线,这条射线如果和碰撞检测器也就是Collider触碰的时候就知道你是点击到拥有这个Collider组件的游戏对象上。
public class Click : MonoBehaviour
{
    IUserAction action;
    RoleModel role = null;
    BoatModel boat = null;
    public void SetRole(RoleModel role)
    {
        this.role = role;
    }
    public void SetBoat(BoatModel boat)
    {
        this.boat = boat;
    }
    void Start()
    {
        action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    }
    void OnMouseDown()
    {
        if (boat == null && role == null) return;
        if (boat != null)
            action.MoveBoat();
        else if(role != null)
            action.MoveRole(role);
    }
}
  • PlayAnimation脚本
    进行动画播放
public class PlayAnimation : MonoBehaviour
{
    public void Play()                                  //播放gameover状态的动画
    {
        Animator anim = this.GetComponent<Animator>();
        anim.SetBool("isgameover", true);
    }
    public void NotPlay()                              //播放Idle状态的动画
    {
        Animator anim = this.GetComponent<Animator>();
        anim.SetBool("isgameover", false);
    }
}
  • 完成MVC的搭建和函数实现,游戏就基本完成了,把Controllor.cs挂载到相机上,然后点击运行,一个牧师与魔鬼的游戏就实现啦

Animator Controllor

我从Asset Store下载的魔鬼模型和牧师模型以及自带了Animator组件,而且动作已经有了,可是缺少Animator Controllor,那么怎样去实现动画播放呢,这里放上一个不负责任的链接供大家参考:http://www.cnblogs.com/hammerc/p/4828774.html


实现截图

photo
gif


具体的代码实现可以去我的Github:传送门

鸣谢之前师兄留下的博客供我参考:传送门

猜你喜欢

转载自blog.csdn.net/C486C/article/details/79795708