3D游戏编程 作业三 牧师与魔鬼

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

开发3D游戏,比起游戏的规则玩法,我觉得更重要的是如何让玩家身临其境,代入其中。因此,我会学习前辈的代码结构,并在此基础上,尝试更多的3D视觉化的效果。

简答题传送门:简答题
asset文件:asset
参考博客:学长博客


一、任务要求

​​​​在这里插入图片描述


二、 任务分析

1.游戏规则

三个魔鬼与三个牧师在河的一端,只通过一艘小船到达河的对岸。小船只能承载最多两人,必须有人掌舵才能移动。如果一端的牧师少于魔鬼,牧师将被杀死,游戏失败;如果全员移动到对岸,游戏成功。

2.游戏对象

牧师,魔鬼,船,河岸,河流

3.动作表

动作状态结果
点击牧师或魔鬼船在对应的河岸牧师或魔鬼上船
点击牧师或魔鬼牧师或魔鬼在船上牧师或魔鬼下船
点击船小船上至少有一个人小船移动

二、 项目展示

前面mvc架构跟游戏逻辑没什么好说的,大家大差不差,网上也一堆差不多的,而第三部分才是我个人想实现的挑战跟突破

1.mvc架构

mvc结构图

导演 SSDirector

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

场记接口 ISceneController

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

场记 Controllor

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 ();
    public void LoadResources();              //创建水,陆地,角色,船
    public void MoveBoat();                  //移动船
    public void MoveRole(RoleModel role);    //移动角色
    public void Restart();
    public int Check();//检测游戏状态
}

交互观众接口 IUserAction

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

交互观众 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()
    {
        //规则展示
        if (GUI.Button(new Rect(10, 10, 60, 30), "Rule", new GUIStyle("button")))
        {
            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||sign == 2)
        {
            string say;
            say = sign==1?"你输了":"你赢了";
            GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 100), say);
            if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "重开")){
                action.Restart();
                sign = 0;
            }
        }
    }
}

模型 model

通过在命名空间下进行类的定义,再引用命名空间,就可以实现所有模型的引用,而不需要写多个c#文件

namespace mygame
{

    public interface ISceneController                      //加载场景
    public interface IUserAction                          //用户互动会发生的事件
    public class SSDirector : System.Object //导演
    public class LandModel//陆地模型
    public class BoatModel  //船模型
    public class RoleModel //人物模型
    public class Move : MonoBehaviour//移动辅助类
    public class Click : MonoBehaviour//点击辅助类
}

预设展示

预设展示

2.游戏逻辑实现

加载

将control类直接拖拽到摄像头上,即可运行。加载过程中,我对boat的加载引用了this.transform.parent,这里是因为之后的摄像头移动需要,暂且按下不表。

    public void LoadResources()              //创建水,陆地,角色,船
    {
        GameObject water = Instantiate(Resources.Load("Water", typeof(GameObject)), Vector3.zero, Quaternion.identity) as GameObject;
        water.name = "water";       
        start_land = new LandModel("start");
        end_land = new LandModel("end");
        boat = new BoatModel((Transform)this.transform.parent);
        roles = new RoleModel[6];

        for (int i = 0; i < 3; i++)
        {
            RoleModel role = new RoleModel("priest");
            role.SetName("priest" + i);
            role.SetPosition(start_land.GetEmptyPosition());
            role.GoLand(start_land);
            start_land.AddRole(role);
            roles[i] = role;
        }

        for (int i = 0; i < 3; i++)
        {
            RoleModel role = new RoleModel("devil");
            role.SetName("devil" + i);
            role.SetPosition(start_land.GetEmptyPosition());
            role.GoLand(start_land);
            start_land.AddRole(role);
            roles[i + 3] = role;
        }
    }

其中模型在加载的时候使用下列方式添加move和click这俩个类,这样的方式等价于直接将脚本拖拽到对应的游戏对象上。

 move = boat.AddComponent(typeof(Move)) as Move;
 click = boat.AddComponent(typeof(Click)) as Click;

游戏运行

click与move赋予了模型点击后响应的能力。
click的点击实现如下,其中两个函数都在control中实现

void OnMouseDown()
        {
            if (boat == null && role == null) return;
            if (boat != null&&boat.GetMove()==0)
                action.MoveBoat();
            else if(role != null&&role.GetMove()==0)
                action.MoveRole(role);
        }

船跟河岸都有一个游戏对象的数组,同时也存储了对应的游戏位置,移动时进行对应的判断,让游戏对象在船和两个河岸的对应数组间移动即可。很多代码细节封装成了函数,此处不再一一赘述。

    public void MoveBoat()                  //移动船
    {
        if (boat.IsEmpty() || user_gui.sign != 0) return;
        boat.BoatMove();
        user_gui.sign = Check();
    }

    public void MoveRole(RoleModel role)    //移动角色
    {
        if (user_gui.sign != 0) return;
        if (role.IsOnBoat())
        {
            LandModel land;
            if (boat.GetBoatSign() == -1)
                land = end_land;
            else
                land = start_land;
            boat.DeleteRoleByName(role.GetName());
            role.GoLand(land);
            role.movetoland(land);
            land.AddRole(role);
        }
        else
        {                                
            LandModel land = role.GetLandModel();
            if (boat.GetEmptyNumber() == -1 || land.GetLandSign() != boat.GetBoatSign()) return;   //船没有空位,也不是船停靠的陆地,就不上船
            land.DeleteRoleByName(role.GetName());
            role.GoBoat(boat);
            role.movetoboat(boat);
            boat.AddRole(role);
        }
        user_gui.sign = Check();
    }

游戏结束判断

游戏状态以0运行,1失败,2获胜的数表示。

 public int Check()
    {
        int start_priest = (start_land.GetRoleNum())[0];
        int start_devil = (start_land.GetRoleNum())[1];
        int end_priest = (end_land.GetRoleNum())[0];
        int end_devil = (end_land.GetRoleNum())[1];

        if (end_priest + end_devil == 6)     //获胜
            return 2;

        int[] boat_role_num = boat.GetRoleNumber();
        if (boat.GetBoatSign() == 1)         //在开始岸和船上的角色
        {
            start_priest += boat_role_num[0];
            start_devil += boat_role_num[1];
        }
        else                                  //在结束岸和船上的角色
        {
            end_priest += boat_role_num[0];
            end_devil += boat_role_num[1];
        }
        if (start_priest > 0 && start_priest < start_devil) //失败
        {      
            return 1;
        }
        if (end_priest > 0 && end_priest < end_devil)        //失败
        {
            return 1;
        }
        return 0;                                             //未完成
    }

这些数会返回给UserGUI判断

        if (sign == 1||sign == 2)
        {
            string say;
            say = sign==1?"你输了":"你赢了";
            GUI.Box (new Rect (Screen.width / 2 - 100, Screen.height / 2 + 50, 200, 100), say);
            if (GUI.Button (new Rect (Screen.width / 2 - 80, Screen.height / 2, 160, 20), "重开")){
                action.Restart();
                sign = 0;
            }
        }

3.3d效果实现

上次作业是上手unity,那这次的就是上手mvc跟学会空间系的灵活应用。
模型的布置以(0,0,0)为中心对称排布,这样的话方便了对岸跟当前河岸的位置处理,只需要取反即可。

人物布置与移动

人物的布置的话,我按照个人的审美进行错落的排布,同时把船简化成方块,这样就能只进行水平上的移动。我把上下船的动作解耦成五步:转向中间区域,移动向中间区域,转向目标,移动向目标,站定后转向。

void roleUpdate(){
            if(move_sign == 1){// 移动前转向
                transform.rotation = Quaternion.RotateTowards(transform.rotation, mid_qua, rotaspeed*Time.deltaTime);
                if(transform.rotation==mid_qua)move_sign=2;
            }
            else if(move_sign == 2){//移动到中间
                transform.position = Vector3.MoveTowards(transform.position, mid_position, mspeed * Time.deltaTime);
                if (transform.position == mid_position)move_sign=3;
            }
            else if(move_sign == 3){//在中间转向
                transform.rotation = Quaternion.RotateTowards(transform.rotation, end_qua, rotaspeed*Time.deltaTime);
                if(transform.rotation==end_qua)move_sign=4;
            }
            else if(move_sign == 4){//移动到最终目的地
                transform.position = Vector3.MoveTowards(transform.position, end_position, mspeed * Time.deltaTime);
                if (transform.position == end_position)move_sign=5;
            }
            else if(move_sign == 5){//在最终目的地转向
                transform.rotation = Quaternion.RotateTowards(transform.rotation, final_qua, rotaspeed*Time.deltaTime);
                if(transform.rotation==final_qua)move_sign=0;
            }
        }
        // -90前 0右 90后 180左 
        public void movetoland(LandModel land){
            Vector3 position = land.GetEmptyPosition();
            mid_position = new Vector3(position.x,transform.position.y,transform.position.z);
            end_position = new Vector3(position.x,transform.position.y,position.z);
            if(land.GetLandSign() == 1){//走下初始大陆上的转向方式
                mid_qua = Quaternion.Euler(0,90,0);
                if(position.z>transform.position.z)end_qua= Quaternion.Euler(0,0,0);
                else end_qua = Quaternion.Euler(0,180,0);
                final_qua  = Quaternion.Euler(0,-90,0);
            }
            else{//走下对面大陆上的转向方式
                mid_qua = Quaternion.Euler(0,-90,0);
                if(position.z>transform.position.z)end_qua= Quaternion.Euler(0,0,0);
                else end_qua = Quaternion.Euler(0,180,0);
                final_qua  = Quaternion.Euler(0,90,0);
            }
            kind = 1;
            move_sign = 1;
        }

        public void movetoboat(BoatModel boat){
            Vector3 position = boat.GetEmptyPosition();
            mid_position = new Vector3(transform.position.x,transform.position.y,position.z);
            end_position = new Vector3(position.x,transform.position.y,position.z);
            if(boat.GetBoatSign() == 1){//船在初始大陆上的转向方式
                if(position.z>transform.position.z)mid_qua = Quaternion.Euler(0,0,0);
                else mid_qua = Quaternion.Euler(0,180,0);
                end_qua = final_qua = Quaternion.Euler(0,-90,0);
            }
            else{//船在对面大陆上的转向方式
                if(position.z>transform.position.z)mid_qua = Quaternion.Euler(0,0,0);
                else mid_qua = Quaternion.Euler(0,180,0);
                end_qua = final_qua = Quaternion.Euler(0,90,0);
            }
            kind = 1;
            move_sign = 1;
        }

船的移动

船的移动本身不复杂,人物通过将parent挂载在船上就可与船进行平移,难的是镜头的转换。经过多次尝试,我在(0,0,0)处放置空对象,而摄像头挂载在对象下,这样就可以通过固定角度旋转空对象,实现镜头切换的效果,同时我还调整了减慢的参数。

        void boatUpdate(){
            if(move_sign==1){
                //渐慢的镜头旋转
                maincamera.rotation = Quaternion.RotateTowards(maincamera.rotation, cameraq, rotaspeed*Time.deltaTime);
                if(rotaspeed>0)rotaspeed -= 120*Time.deltaTime>rotaspeed?rotaspeed: 120*Time.deltaTime;
                transform.position = Vector3.MoveTowards(transform.position, end_position,mspeed * Time.deltaTime);
                if(transform.position==end_position)move_sign=0;
            }
        }
        
        public void boatmove(Vector3 position,Transform camera){
            end_position = position;
            maincamera = camera;
            //摄像头翻转
            if(camera.rotation==Quaternion.Euler(0,0,0))cameraq = Quaternion.Euler(180,0,180);
            else cameraq = Quaternion.Euler(0,0,0);
            kind = 2;
            move_sign=1;
            rotaspeed = 200;
            mspeed = 2;
        }

三、 展示效果

1.展示图

演示图

2.展示视频

牧师与魔鬼 演示

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值