【Unity3D开发】恶魔与牧师小游戏

本文同时发布至我的个人博客,点击进入我的个人博客阅读。本博客供技术交流与经验分享,可自由转载。转载请在评论区或私信简单通知,感谢!

V2.0更新说明

  1. 使用动作管理器,实现动作分离
  2. 修改了精灵上船位置的BUG

游戏简介

​ 《植物与僵尸》是一款模仿《牧师与恶魔》的益智小游戏。规则与《牧师与恶魔》相似,只不过牧师在游戏中变成了植物,而恶魔变成了僵尸。

​ 游戏规则简介如下:

成功条件:所有的植物与僵尸都完成过河。

制约条件:

  1. 若河岸的某一边,僵尸的数目大于植物的数目,则游戏失败。注意:船上的植物与僵尸也会计数, 若船在靠下方河岸,则计入下方河岸数目,反之亦然。
  2. 船只能搭载两个植物或僵尸。
  3. 船上需要有至少一人才能使船移动。

操作方法:点击植物或僵尸使其上船,点击GO按键使船移动。

​ 游戏效果:

参考资料

​ Simba_Scorpio的博客文章:http://blog.csdn.net/simba_scorpio/article/details/50846520

​ Unity官方文档:https://docs.unity3d.com/ScriptReference/

​ 游戏蛮牛文档:http://docs.manew.com/index.html

游戏架构

​ 游戏采用MVC架构来设计,主要目录如下:

​ 其中GameSceneController为控制类,负责生成以及控制所有游戏对象;SenceModelPersonModel为模型类,主要负责整体和局部的逻辑实现;PersonGoClick为用户接口类,主要实现每个游戏精灵以及GO按钮的用户交互。

重点展示

​ 关于逻辑实现,考虑的是数据结构尽量从简,以减少游戏消耗。所以这里采用最朴素的对象数组来储存逻辑对象。下面是SenceModel的关于这部分的部分代码:

public class SenceModel  {
    //六个游戏对象
    const int objCout = 6;
    //游戏对象数组
    PersonModel[] allPerson = new PersonModel[objCout];
    //初始化
    public void Reset()
    {
        for (int i = 0; i < objCout; i++)
        {
            allPerson[i] = new PersonModel();
            allPerson[i].IPersonState = PersonState.up;
            if (i < 3)
            {
                allPerson[i].IPersonStyle = PersonStyle.sunflower;
            }
            else
            {
                allPerson[i].IPersonStyle = PersonStyle.zombie;
            }
        }
    }
    //get访问器获取指定游戏对象
    public PersonModel getPersonModel(int num)
    {
        return allPerson[num];
    }
}

​ 游戏胜利判断较为简单,通过遍历上述对象数组allPerson,来分别获取上方河岸和下方河岸各自植物和僵尸的数量,然后作简单逻辑判断即可:

public GameState checkGameState() //GameState是自定义的一个表示游戏状态的枚举类型
    {
        //声明四个变量储存植物和僵尸的数量
        int upSunflower = 0, downSunflower = 0;
        int upZombie = 0, downZombie = 0;
        //遍历对象数组
        foreach(var person in allPerson)
        {
            if (_IBoatState == BoatState.up)
            {
                if (person.IPersonState != PersonState.down)
                {
                    if (person.IPersonStyle == PersonStyle.sunflower)
                        upSunflower++;
                    else
                        upZombie++;
                }
                else if (person.IPersonState == PersonState.down)
                {
                    if (person.IPersonStyle == PersonStyle.sunflower)
                        downSunflower++;
                    else
                        downZombie++;
                }
            }
            if (_IBoatState == BoatState.down)
            {
                if (person.IPersonState != PersonState.up)
                {
                    if (person.IPersonStyle == PersonStyle.sunflower)
                        downSunflower++;
                    else
                        downZombie++;
                }
                else if (person.IPersonState == PersonState.up)
                {
                    if (person.IPersonStyle == PersonStyle.sunflower)
                        upSunflower++;
                    else
                        upZombie++;
                }
            }
        }
        //判断游戏状态
        if ((upZombie > upSunflower && upSunflower != 0) ||
                  downZombie > downSunflower && downSunflower != 0)
            return GameState.fail;

        foreach (var person in allPerson)
        {
            if (person.IPersonState == PersonState.up || person.IPersonState == PersonState.onBoat)
                return GameState.ing;
        }
        return GameState.win;
    }

​ 至于对于游戏对象的控制,需要注意的是对上船/下船动作的条件限制:例如船不在对应的岸边,对象是无法上船的;船上的对象已有2个也是无法上船;上船时需要控制对象的位置,若船上已有人则需要到有空位的位置...对象控制部分新手容易忽视的一个问题是:船上的对象要随船移动,所以需要将船上的对象设置为船对象的子对象。同理,下船的适合则将该对象的父对象重新设置为gameObject

_person.transform.SetParent(boat.transform);

开发心得

​ 由于这是我第一次相对独立开发的Unity游戏项目(上次五子棋项目基本按照慕课网的五子棋教程实现),所以还是遇到了很多新手开发者遇到的坑,再次做下记录。

(一)Canvas带来的困惑

​ 没有完整接触过Unity组件常常会受Canvas影响,当创建一个Button时自动会生成一个Canvas作为其父对象。其实Canvas的主要功能就是负责UI布局和渲染的抽象空间,所有的UI都必须在此元素下。通过设置其UI Scale Mode可以实现游戏程序的自适应屏幕缩放。这里不做过多赘述,作者的Canvas设置如下图,仅供参考。需要注意的是用一个场景下所有的UI对象最好都声明在同一个Canvas父对象之下,否则可能会发生按钮click失效这种情况。

(二)对MVC架构的设计

​ 架构的设计对后期开发效率的影响十分巨大,这次我因为架构并不合理而踩了许多坑。首先,我对控制类GameSceneController没有做好单例控制,这一点在大型游戏开发中是很不对的,会带来对性能要求的提高。其次我没有意识到将GameSceneController声明到一个自定义的公共命名空间能够带来的便利,如下是一个比较好的示范:

namespace Com.Mygame {  
      
    public class GameSceneController: System.Object {  
          
        private static GameSceneController _instance;  
        private BaseCode _base_code;  //底层逻辑类
          
        public static GameSceneController GetInstance() {  
            if (null == _instance) {  
                _instance = new GameSceneController();  
            }  
            return _instance;  
        }  
          
        public BaseCode getBaseCode() {  
            return _base_code;  
        }  
          
        internal void setBaseCode(BaseCode bc) {  
            if (null == _base_code) {  
                _base_code = bc;  
            }  
        }  
    }  
}  

​ 将GameSceneController声明到一个自定义的公共命名空间,其它脚本只要添加using Com.Mygame就看以使用控制类对象了,而且也能将我们自定义的类与系统类区分,也便于大型开发项目的管理。这次我由于没有这样做,导致在后期开发中添加了许多冗余繁杂的类接口来保证功能需求,带来了许多麻烦。

(三)时时刻刻注意实例化与初始化

​ 基本上,每个类都需要一个初始化函数,来初始化所有字段属性。这一点新手开发者虽然知道但常常会遗忘。其次,对于某些类如果没有设置声明时自动初始化,要记得实例化的同时手动做好初始化。这种问题虽然能在后期Debug中比较容易地发现,但是极大地影响开发者的效率。


V2.0相关

关于动作分离

​ 使用简单的工厂模式,其原理时:添加动作管理器ActionManagerActionManager设置分配动作给游戏对象的方法(moveToLeftUpmoveToRightUp等)。具体的分配动作的方法是:自定义一个对象组件MoveToAction,通过向对象添加组件和删除组件的方式来控制游戏对象的动作进行和动作结束。动作分离的好处是:代码结构清晰,GameSceneController不再需要了解动作的具体实现,而只需要通知ActionManager想特定的对象添加特定的动作即可。在大型的游戏工程中,一个动作的实现往往是相对复杂的,所以动作分离十分重要。

实现过程

(一)定义ActionManager

​ 定义ActionManager类,并添加需要用到的静态字段,用于表示动作的到达位置与速度。

    public class ActionManager : System.Object
    {
        private static ActionManager _IActionManager;
        static Vector3 LEFTUP = new Vector3(-2.5f, 0.2f, 0);
        static Vector3 RIGHTUP = new Vector3(-1.5f, 0.2f, 0);
        static Vector3 LEFTDOWN = new Vector3(-2.5f, -0.8f, 0);
        static Vector3 RIGHTDOWN = new Vector3(-1.5f, -0.8f, 0);
        static Vector3 BOATUP = new Vector3(-2, -0.4f, 0);
        static Vector3 BOATDOWN = new Vector3(-2, -1.4f, 0);
        static float SPEED = 4f;
    }

(二)自定义MoveToAction组件

​ 自定义MoveToAction动作组件,其主要功能是包含一个实现让特定对象移动到特定地点的动作。需要注意的是,在工程中年我们常常先自定义一个动作基类,因为在游戏工程中往往需要各种各样的动作,我们将所有的动作定义为动作基类的子类,这符合封装的原则。MoveToAction需要实现两个主要方法,一个是setMoveTo(Vector3 target, float speed),用于设置动作的目标位置与移动速度,另一个是重写MonoBehaviourUpdate()方法,该方法会在游戏的每一帧执行,用以动作的具体实现和检查动作是否完成。

    public class MyAction : MonoBehaviour
    {
       //...
    }

    public class MoveToAction : MyAction
    {
        Vector3 _target;
        float _speed;
        ActionState _actionState = ActionState.ing;

        public void setMoveTo(Vector3 target, float speed)
        {
            //do
        }

        private void Update()
        {
            //do
        }
    }

(三)应用动作函数

​ 在ActionManager中实现你所需要的各种动作的应用,下面是一个例子。场景控制类只需调用这些动作应用函数即可实现对象的动作:

    public void moveToLeftUp(GameObject obj)
        {
            MoveToAction action = obj.AddComponent<MoveToAction>();
            if (action.actionState == ActionState.ing)
                action.setMoveTo(LEFTUP, SPEED);
        }

写在后面

​ 游戏的实现还是有很多由于作为新手而作出的不理智选择,有很多值得改进的地方。此文也有许多不严谨处,欢迎大家多加指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容