Unity实现小游戏《牧师与魔鬼》动作分离版

游戏背景

Priests and Devils

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!

        牧师与魔鬼是一个帮助牧师和魔鬼乘船过河的益智游戏。船最多承载二人,且必须有人才能移动。当某一侧的魔鬼数量大于牧师时(包括并未上岸的船上角色),游戏失败。当三名牧师和三名魔鬼全都过河,游戏成功。

        玩法操作:点击角色来上下船,船上有角色时点击船移动到对岸。

游戏结构

        本游戏采用MVC框架实现,实质则是各功能的面向对象地设计与实现。根据Model、View和Controller来设计不同的类并将它们组织起来。

        Model负责的是各个游戏对象的属性和基本行为,包括人物角色(魔鬼、牧师),船,以及河的两岸。它们都需要有一定的方法去返回自身的信息。

        View是与用户交互的接口,需要接受来自用户的输入并对用户反馈相应信息,最终呈现出来的就是一个界面。

        Controller将Model和View连接器来,达到控制全局的目的。这不仅需要从model中获取相应的属性和信息,并且利用这些信息判断他们的位置,还需要从View中获取相应的用户输入,进行相应的物体移动,在Model和View之间充当桥梁的作用。 同时还需要判断游戏的进行和结束。

        由于Controller要实现的功能过于多了,逻辑判断和对象动作混杂在一起使得Controller变得冗杂模糊。所以实现动作分离来保证清晰,并使得游戏的可拓展性和维护性更高。

UML设计图(MVC+动作分离):

游戏实现

游戏对象

        共有牧师、魔鬼、河岸、船、河、光源(可选)。

对象模型呈现

        由于所有游戏对象都需要在运行后动态生成,我们需要先设置好摄像头和他们的位置与角度,从而实现对象的预制实现。在脚本实现之前先用方块建立河流、河岸等对象,调整其合适大小。用黑色长条方块表示魔鬼,用白色圆柱体表示牧师,船用一个压扁的胶囊体来替代(类似木筏)。效果如下:

脚本组织

Director

        从宏观上来说,Directer几乎掌管了游戏的所有事务。但其实他并没有什么复杂详细的功能,只是安排下层的Controller来处理。并且在这里的单例模式下,由一个director来统领整个游戏,保证了每一个场景都有着同一个Director。

public class Director : System.Object
{
    // 单例模式,只能有一个实例,通过getInstance访问
    private static Director _instance;
    // 管理着SceneController,通过它来间接管理场景中的对象
	public SceneController currentSceneController { get; set; }
	
	public static Director getInstance() {
		if (_instance == null) {
			_instance = new Director ();
		}
		return _instance;
	}
}

Controller

        这里的Controller概念较为广泛。夹杂了很多部分的代码。它们首先是能够将游戏对象实例化,并能够获取各个游戏对象的状态、属性和进行逻辑判断。

        以下是对游戏角色的Controller和裁判Controller类的一个示例:

public class CharacterController {
        GameObject character;
        ClickEvent click;
        public Move move;
        int state; // Boat = 1, bank = 0;
        int man; //Devil = 0, Priest = 1;
        int tag;
        BankController bankController;
        public CharacterController(string chr, int tag) {
            this.tag = tag;
            if (chr == "priest") {
                character = Object.Instantiate(Resources.Load("Prefabs/Priest", typeof(GameObject)), Vector3.zero, Quaternion.identity) as GameObject;
                man = 1;
            }
            else {
                character = Object.Instantiate(Resources.Load("Prefabs/Devil", typeof(GameObject)), Vector3.zero, Quaternion.identity) as GameObject;
                man = 0;
            }
            move = character.AddComponent(typeof(Move)) as Move;
            click = character.AddComponent(typeof(ClickEvent)) as ClickEvent;
            click.setChrController(this);
        }
        public void setName(string name) {}
        public void setPosition(Vector3 position) {}
        public void goMoving(Vector3 position) {}
        public string getName() {}
        public int getTag() {}
        public string getMan() {}
        public BankController getBank() {}
        public int getState() {}
        public void moveToBoat(BoatController boatController)  {}
        public void moveToBank(BankController bankController)  {}
        public void init() {}
    }

public class Judger : MonoBehaviour {
    public int checkGame(){} 
    public void setForbid(bool b) {} 
}

分离动作

        动作的基础在于这个抽象基类SSAction。需要后续的具体动作类来重写,具有一定的拓展性和易用性。在后面调用动作时,便能够快速且正确地调用对应的动作子类。

public class SSAction : ScriptableObject {

    public bool enable = true;
    public bool destroy = false;
    public GameObject gameObject{get;set;}
    public Transform transform{get;set;}
    public ISSActionCallback callBack{get;set;}
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

        回调函数接口ISSActionCallback用于动作类间地消息传递。采用消息传递的方式优点是显而易见的,一些当前的动作状态本该由动作管理器管理,而不应该交由某一个动作去控制,动作只需做好它本身的移动物体的任务即可。

        每一个SSAction都有一个ISSActionCallback接口callback,SSAction的管理者实现了ISSActionCallback接口,并且将自身赋给callback,当SSAction动作完成时,调用callback的SSActionEvent函数,即相当于调用动作管理者,令动作管理者做出相应的反应。

public interface ISSActionCallback
{
    void SSActionEvent(SSAction action);
}

        平移动作类SSMoveToAction的任务就是将物体以一定的速度平移到目标位置,然后将自己标记为可销毁(任务完成),然后调用回调函数去通知信息。 

public class SSMoveToAction : SSAction
{
    public Vector3 target;
    public float speed;
    private SSMoveToAction() { }
    public static SSMoveToAction GetSSAction(Vector3 target, float speed)
    {
        SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
        action.target = target;
        action.speed = speed;
        return action;
    }
    public override void Update(){}
    public override void Start(){}
}

        虽然这个游戏只有平移一个动作,但是一个对象要完成他的一个完整动作需要一连串地平移(水平和垂直),所以我们还需要一个组合动作类 CCSequenceAction。

public class CCSequenceAction : SSAction, ISSActionCallback
{
    public List<SSAction> sequence; 
    public int repeat = -1;
    public int start = 0;
    public static CCSequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
    {
        CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
        action.repeat = repeat;
        action.sequence = sequence;
        action.start = start;
        return action;
    }
    public override void Update(){}
    public void SSActionEvent(SSAction action){}
    public override void Start(){}
    void OnDestroy(){}
}

        动作的调用和管理则由动作管理类来实现。它维护一些队列和字典来管理要调用的动作,然后提供接口给Controller来进行动作的真正实现。

public class SSActionManager : MonoBehaviour
{
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    private List<SSAction> waitingForAdd = new List<SSAction>();
    private List<int> waitingForDelete = new List<int>();
    protected void Update()
    {
        foreach (SSAction action in waitingForAdd)
        {
            actions[action.GetInstanceID()] = action;
        }
        
        waitingForAdd.Clear();

        foreach (KeyValuePair<int,SSAction> pair in actions)
        {
            SSAction action = pair.Value;
            if (action.destroy)
            {
                waitingForDelete.Add(action.GetInstanceID());
            } else if (action.enable)
            {
                action.Update();
            }
        }

        foreach (int key in waitingForDelete)
        {
            SSAction action = actions[key];
            actions.Remove(key);
            Destroy(action);
        }
        
        waitingForDelete.Clear();
    }
    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback callback){}
}

public class ScenceActionManager : SSActionManager, ISSActionCallback {
    SSMoveToAction boatAction;
    CCSequenceAction characterAction;
    Controller controller;
    Judger judger;
    private void Start()
    {
        controller = Director.getInstance().currentSceneController as Controller;
        controller.actionManager = this;
        judger = controller.judger;
    }
    public void moveBoat(GameObject boat, Vector3 pos, float speed) {}
    public void moveCharacter(GameObject chr, Vector3 pos, float speed) {}
    public void SSActionEvent(SSAction action) {
        judger.setForbid(false);
    }
}

        实际上就是用两个函数分别实现船的简单运动,以及人物的组合运动,然后调用RunAction函数,使得Controller能够直接调用。

        至此,整个游戏的基本设计思路已经介绍完毕。详细代码请看:.GitHub - Miao-Da/3d-game

        实质上,由于修改量巨大,这个游戏还未做到完全的动作分离,其中的运动模式只适用于这个游戏。但游戏功能是基本完善并符合要求的了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值