unity3D游戏——魔鬼与牧师(Devil and Priest)动作分离版本的实现

目录

前言

魔鬼与牧师游戏的MVC实现回顾

MVC的UML设计图

Models

View

Controllers

动作分离基本思路

动作分离版本的UML设计图

动作回调函数接口(ISSActionCallback)

动作基类(SSAction)

简单平移动作子类(CCMoveToAction)

组合移动动作子类(CCSequenceAction)

动作管理基类(SSActionManager)

本游戏项目(魔鬼与牧师)的动作管理类(CCActionManager)

裁判类(JudgeController)

其它操作

结语


前言

魔鬼与牧师(Devil and Priest)是一个经典的智力解谜游戏,玩家需要帮助三位魔鬼和三位牧师,将所有的牧师带过河,同时还要确保牧师不会被魔鬼吃掉。

下面是游戏的规则和背景故事:

规则:
1. 在河的一边有三个魔鬼、三个牧师以及一条船。
2. 船最多可以承载两个角色(牧师或魔鬼)且必须承载一个角色。
3. 当河的一侧的魔鬼数量大于牧师数量时,魔鬼会吃掉牧师。
4. 玩家需要将所有的牧师都安全地运送到另一侧的河岸,而且保证没有牧师被魔鬼吃掉。

背景故事:
有三个牧师和三个魔鬼被困在一座孤岛上,他们想要回到对岸。但是,这座孤岛上有一条河,河上只有一艘小船,而且这条河非常危险,同时魔鬼会吃掉牧师。他们需要找到一种方法使所有的牧师都能够安全地过河。

玩家需要根据规则,使用合适的策略移动牧师和魔鬼,以确保所有的牧师都能够安全地过河。游戏中的挑战在于玩家需要平衡每一步的移动,避免牧师被魔鬼吃掉,并且在规定的条件下完成游戏。

魔鬼与牧师游戏是一个简单而有趣的逻辑解谜游戏,可以帮助玩家锻炼思维能力和问题解决能力。

首先我们先来看一下这个游戏的视频演示吧:

魔鬼与牧师的视频演示

本次项目的代码地址为:

Github地址​​​​​​​

魔鬼与牧师游戏的MVC实现回顾

在上一个实验(上一个实验的博客地址)中,我们利用基础的MVC架构来实现了魔鬼与牧师的游戏,在这里我们将会简单地回顾一下魔鬼与牧师游戏的MVC实现,具体的内容实现请参考上一篇博客内容。

MVC的UML设计图

我们在实现游戏时,将会创建model、controller以及view的脚本文件,分别实现相对应的功能。同时最核心的脚本代码就是controller里面的firstcontroller,同时还有导演类、用户交互类以及GUI类等等,我们首先看一下该游戏项目的UML设计图吧:

Models

Models负责的是各个游戏对象的属性和基本行为,包括人物角色(魔鬼、牧师),船,以及河的两岸。船和岸是作为一个容器,需要有容纳人物的位置,也需要记录每个位置对应的xyz坐标。而且它们都需要有一定的方法去返回自身的信息,比如在左还是右、是第几个人物、返回对象类型的方法等等。同时还需要使用一个models中的position类记录一下各个游戏对象的位置信息,以供后面的类中调用。

View

View就是与用户交互的接口,其中需要接受来自用户的点击信息,并且根据点击的物体不同而传递不同的信息。还有就是反馈,游戏开始或结束需要反馈相应的信息给用户,也就是一个简单的GUI界面。比如游戏结束返回Game Over信息,游戏胜利就返回you win信息,以及倒计时游戏的剩余时间等。

Controllers

控制器Controllers的目的就是需要将M和V连接起来,从而控制全局,不仅需要从Models中获取相应的信息,并且利用这些信息判断他们的位置,还需要从View中获取相应的用户输入,进行相应的物体移动,在Models和View之间充当桥梁的作用。同时在主要的控制器FirstController还需要判断游戏的进行程度,也就是需要判断输赢,并且将输赢信息通过回调函数返回给View,从而达到反馈的目的。总而言之,控制器就是整个游戏得以进行的大脑,控制了整个游戏的进行逻辑以及实现。

动作分离基本思路

从上一个实验我们可以知道,使用MVC架构虽然比较直观,但是会发现我们的控制器Controllers比较臃肿,比较复杂,承担的任务比较重。所以是绝对有必要给控制器分配几个“手下”,为其承担一部分的任务的。那么,应该如何分配“手下”呢?而到了这里就是今天的主题啦——动作

动作分离版本的UML设计图

首先,我们还是需要先看一下该游戏项目的UML设计图,该UML设计图是基于动作分离版本的,在前一个实验的基础上增加了CCAction的动作类,同时对控制器Controllers做了一定的修改,下面是本次实验的UML设计图:

动作回调函数接口(ISSActionCallback)

在动作执行完毕后,都会执行回调函数执行别的操作,以及实现与这个动作相关的代码。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//枚举了动作事件的类型:开始和完成
public enum SSActionEventType:int { Started, Competeted }

//一个接口,用于在动作完成时进行回调
public interface ISSActionCallback
{
	//source为一个SSAction类型的参数,表示触发事件的动作对象
	//events为一个SSActionEventType类型的参数,表示事件类型,默认为Competeted
	void SSActionEvent(SSAction source, 
		SSActionEventType events = SSActionEventType.Competeted,
		int intParam = 0 , 
		string strParam = null, 
		Object objectParam = null);
}

动作基类(SSAction)

在上一个实验中,我们学习了解到物体运动(也就是所说的动作),无非就是物体空间属性的改变,而我们目前所接触到的只有三种:平移、旋转、缩放。所以为了简化实现,我们可以直接定义一个类,可是又不能把它定死,万一要用到一个类的时候还要去区分是三种动作的哪一种,那不是很麻烦吗?所以我们就需要抽象出来一个动作的基类:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//动作基类
//将平移、旋转、缩放抽象出一个动作基类
public class SSAction : ScriptableObject {

	//用于标记该动作是否被启用
	public bool enable = true;
	//用于标记该动作是否被摧毁
	public bool destory = false;

	//表示该动作作用的对象
	public GameObject gameobject { get; set; }
	//表示该动作所作用的对象的变换组件
	public Transform transform { get; set; }
	//表示该动作完成后的回调接口
	public ISSActionCallback callback { get; set; }

	//构造函数
	protected SSAction () {}

	// Use this for initialization
	public virtual void Start () {
		//抛出异常,表示在子类中必须完成相应的实现
		throw new System.NotImplementedException ();
	}
	
	// Update is called once per frame
	public virtual void Update () {
		//抛出异常,表示在子类中必须完成相应的实现
		throw new System.NotImplementedException ();
	}
		
}

该动作类只是一个简单的基类,将缩放、平移、旋转抽象成了一个类,在该类中,我们并没有具体表明这是一个什么动作,而是定义设置好了这个动作是否被启用、销毁、使用该动作的对象以及一些虚函数,用于在具体实现的动作子类中重写从而实现该动作的一些特性。这样子就可以将各个动作进行区分实现了。

简单平移动作子类(CCMoveToAction)

在本游戏项目中,由于对象的运动比较简单,只涉及到了平移的运动操作,而没有旋转、缩放等运动,所以在这里我们只需要实现平移动作的子类即可,而如果在另一个项目中有涉及到旋转、缩放等运动,我们就需要实现这两个子类了。但是在这里我们只需要实现简单移动动作子类即可:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//在本游戏中(魔鬼与牧师),由于只涉及到平移操作,而没有旋转、缩放操作
//所以我们只需要实现一个平移类即可
//主要负责运动
public class CCMoveToAction : SSAction
{
	//表示动作的目标位置
	public Vector3 target;
	//表示动作移动的速度
	public float speed;

	//构造函数
	private CCMoveToAction(){}

	//创建以及返回一个CCMoveToAction对象
	public static CCMoveToAction GetSSAction(Vector3 target, float speed){
		CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction> ();
		action.target = target;
		action.speed = speed;
		return action;
	}

	public override void Update ()
	{
		//在每一帧更新时,将游戏对象的位置逐渐移动到目标位置target,
		//并根据移动是否完成来设置destory属性和调用回调接口的SSActionEvent()方法
		if (this.gameobject == null||this.transform.localPosition == target) {
			//waiting for destroy
			this.destory = true;  
			this.callback.SSActionEvent (this);
			return;
		}
		this.transform.localPosition = Vector3.MoveTowards (this.transform.localPosition, target, speed * Time.deltaTime);
	}

	//重写了Start函数,但是没有具体实现
	public override void Start () {

	}
}

在该移动子类中,我们设置了目标位置和移动速度,然后在更新函数Update中,不断地更新移动后的新的位置即可,同时还需要更新是否摧毁等标记,以及调用回调函数来进行相应的操作。

组合移动动作子类(CCSequenceAction)

可能你会有所疑问——为什么还需要一个组合移动动作子类?上面不是说该游戏项目中只有一个移动的简单动作吗?其实确实如此,但是移动动作也不是简单的直接移动,而是需要分段式地移动,就好像走路,你不可能一直直走而不拐弯。所以这个组合移动动作子类的意思即是移动动作需要进行两段划分。因为在该游戏项目中,角色与船的高度y坐标不相同,角色在岸上的位置是高于船的,所以我们需要先将角色进行向右平移,移动到船的上空后再向下平移,装载到船上。所以这个就是我们需要这一个组合移动动作子类的原因。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//组合动作类,将事物的动作划分为多个小的动作
public class CCSequenceAction : SSAction, ISSActionCallback
{
	//一个List类型的数据,存储一系列SSAction对象,表示需要执行的动作序列
	public List<SSAction> sequence;
	//表示动作序列需要重复的次数,默认为-1,表示无限重复
	public int repeat = -1;
	//表示当前执行的动作在序列中的索引
	public int start = 0;

	//用于创建CCSequenceAction对象,
	//接受重复次数repeat、起始索引start和动作序列sequence作为参数,
	//返回一个CCSequenceAction对象
	public static CCSequenceAction GetSSAction(int repeat, int start , List<SSAction> sequence){
		CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction> ();
		action.repeat = repeat;
		action.sequence= sequence;
		action.start = start;
		return action;
	}
	
	// Update is called once per frame
	//重写当前的Update函数
	public override void Update ()
	{
		//如果sequence中的动作为空,那么直接返回
		if (sequence.Count == 0) return;  
		//如果当前还有未执行的动作,就再调用当前动作的Update方法
		if (start < sequence.Count) {
			sequence [start].Update ();
		}
	}

	//实现了ISSActionCallback接口的方法,
	//当一个动作完成时,会调用该方法,
	//将当前动作的destory属性设置为false,表示不立即销毁,
	//将索引start加1,判断是否需要重复执行动作序列或者执行完毕,
	//并根据情况设置destory属性为true以及调用回调接口的SSActionEvent()方法。
	public void SSActionEvent (SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null)
	{
		source.destory = false;
		this.start++;
		if (this.start >= sequence.Count) {
			this.start = 0;
			if (repeat > 0) repeat--;
			if (repeat == 0) {
				this.destory = true;
				this.callback.SSActionEvent (this); }
		}
	}

	//重写了基类的Start方法,
	//在该方法中,遍历动作序列sequence,
	//并为每个动作设置gameobject、transform和callback属性,最后调用各个动作的Start方法。
	// Use this for initialization
	public override void Start () {
		foreach (SSAction action in sequence) {
			action.gameobject = this.gameobject;
			action.transform = this.transform;
			action.callback = this;
			action.Start ();
		}
	}

	//遍历动作序列sequence,并且销毁每一个动作
	void OnDestory() {
		foreach(SSAction action in sequence){
			Destroy(action);
		}
	}
}

在这一个类中,我们就是设置了一个列表,将一个移动动作划分为了几个简单的移动动作,然后将这些移动动作放在列表中。

Start函数主要就是遍历动作序列,并且为每个动作设置gameobject、transform和callback属性,最后调用各个动作的Start方法。

Update函数则是不断遍历动作序列,如果sequence中的动作为空,那么直接返回,如果当前还有未执行的动作,就再调用当前动作的Update方法。

在每一个简单的动作执行完毕后,都会不断地调用回调函数,此时会执行SSActionEvent这一个函数,检查repeat是否为0,如果不为0,就再继续回调。

动作管理基类(SSActionManager)

我们在实现了前面的一些基本动作后,我们就需要考虑如何调用这一些函数,然后使得角色运动起来了。在动作管理基类中,其实就是将许多的动作集中在一起,即使是不同的运动动作、不同的角色的运动、不同的游戏项目的运动,我们都可以将它们的运动放在同一个基类中,即将所有的运动动作整合在一起,然后再顺序地调用这些动作即可。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//动作管理基类
//将许多个动作整合到一起,然后顺序调用
public class SSActionManager : MonoBehaviour {

	//为一个字典,用于存储所有的SSAction对象,以其实例ID为键。
	private Dictionary <int, SSAction> actions = new Dictionary <int, SSAction> ();
	//一个列表,用于存储等待添加的SSAction对象
	private List <SSAction> waitingAdd = new List<SSAction> ();
	//一个列表,用于存储等待删除的SSAction对象 
	private List<int> waitingDelete = new List<int> ();

	// Update is called once per frame
	protected void Update () {
		//首先将所有等待添加的SSAction对象添加到actions字典中
		foreach (SSAction ac in waitingAdd){
			actions[ac.GetInstanceID ()] = ac;
		}

		//清空等待添加的列表
		waitingAdd.Clear ();

		//遍历actions字典中的每一个SSAction对象
		foreach (KeyValuePair <int, SSAction> kv in actions) {
			SSAction ac = kv.Value;
			//如果action的destory属性为true时,将该对象添加到等待删除的队列中
			if (ac.destory) { 
				waitingDelete.Add(ac.GetInstanceID()); // release action
			}
			//如果action的enable为true时,将调用Update函数更新该action 
			else if (ac.enable) { 
				ac.Update (); // update action
			}
		}

		//遍历等待删除的对象列表,将所有的对象销毁,并且清空该等待删除的列表
		foreach (int key in waitingDelete) {
			SSAction ac = actions[key]; 
			actions.Remove(key); 
			Object.Destroy(ac);
		}
		waitingDelete.Clear ();
	}

	//为外部的一个接口函数,将参数进行绑定
	public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
		action.gameobject = gameobject;
		action.transform = gameobject.transform;
		action.callback = manager;

		//将该动作添加到等待添加的对象列表中
		//同时调用对象的Start方法来进行初始化
		waitingAdd.Add (action);
		action.Start ();
	}


	// Use this for initialization
	protected void Start () {
		
	}
}

在该动作管理基类中,我们创建了一个字典用于存储所有的SSAction对象,一个列表用于存储等待添加的SSAction对象,一个列表用于存储等待删除的SSAction对象。主要是在Update函数中,将所有等待添加的SSAction对象添加到actions字典中,将所有等待被删除的SSAction对象销毁,同时提供了一个RunAction函数,用于外部对象可以添加动作。

本游戏项目(魔鬼与牧师)的动作管理类(CCActionManager)

上面我们已经实现了一个管理动作运动的动作管理基类,那么在我们本次的游戏项目中,我们就需要实现一个具体的符合该游戏项目的动作管理类了,其实就是上面父类的一个具体子类。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//该游戏魔鬼与牧师的动作管理的具体实现类
public class CCActionManager : SSActionManager, ISSActionCallback
{
    //是否正在进行运动
    private bool isMoving = false;
    //表示船移动动作类(只需要往左或者右移动,不需要组合)
    public CCMoveToAction moveBoatAction;
    //角色移动动作类(需要组合——先往右平移,然后往下面移动)
    public CCSequenceAction moveRoleAction;
    //控制游戏运行的主控制器
    public FirstController mainController;

    //重写基类的Start方法
    protected new void Start()
    {
        //获取主控制器的引用,并且将当前的控制器设置为主控制器
        mainController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
        mainController.actionManager = this;
    }

    //获取该游戏是否正在进行运动的状态
    public bool IsMoving()
    {
        return isMoving;
    }

    //移动船
    //创建移动船的动作并且将其添加到船上
    public void MoveBoat(GameObject boat, Vector3 target, float speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        moveBoatAction = CCMoveToAction.GetSSAction(target, speed);
        this.RunAction(boat, moveBoatAction, this);
    }

    //移动人
    //创建人移动的动作,并且通过组合多个动作来实现从起点到中间位置然后再到目标位置的移动
    public void MoveRole(GameObject role, Vector3 mid_destination, Vector3 destination, int speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        moveRoleAction = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> { CCMoveToAction.GetSSAction(mid_destination, speed), CCMoveToAction.GetSSAction(destination, speed) });
        this.RunAction(role, moveRoleAction, this);
    }

    //回调函数
    //当一个动作完成后,将会调用该方法
    //将isMoving设置为false,表示动作的运动已经完成
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Competeted,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        isMoving = false;
    }
}

在该管理动作类中,我们主要是实现了移动船和移动角色的两个简单运动,然后调用RunAction函数,使得控制器可以直接调用,一步到位。同时我们还需要重定义一下回调函数,当一个动作完成后就会调用该回调函数,同时更新标记,表示该动作的运动已经完成。

裁判类(JudgeController)

在上面的实现中,我们已经完成了动作的分离,将运动的实现从控制器中分离了出来,那么按照实验要求,我们还需要完成一个裁判类,主要用于判断游戏是否结束、胜利等逻辑。具体的游戏规则在上面的前言中已经给出。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class JudgeController : MonoBehaviour
{
    //控制游戏的主控制器
    public FirstController mainController;
    //游戏的左岸
    public Land leftLandModel;
    //游戏的右岸
    public Land rightLandModel;
    //游戏的船模型
    public Boat boatModel;

    // Start is called before the first frame update
    //在游戏开始时调用,主要用于获取主要控制器、左岸、右岸、船的引用
    void Start()
    {
        mainController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
        this.leftLandModel = mainController.leftLandController.GetLand();
        this.rightLandModel = mainController.rightLandController.GetLand();
        this.boatModel = mainController.boatController.GetBoatModel();
    }

    // Update is called once per frame
    void Update()
    {
        //首先检查游戏是否在运行,如果不是就返回
        if (!mainController.isRunning)
            return;
        //检查游戏时间是否已经变为0,如果是就返回,并且调用回调函数同时传递Game Over以及false参数
        //同时将游戏是否运行设置为false
        if (mainController.time <= 0)
        {
            mainController.JudgeCallback(false, "Game Over!");
            mainController.isRunning=false;
            return;
        }
        this.gameObject.GetComponent<UserGUI>().gameMessage = "";

        //判断游戏是否已经胜利
        //如果右岸上的牧师数量已经达到了三个,那么游戏胜利,同时利用回调函数传递You Win以及false参数
        //同时将游戏是否运行设置为false
        if (rightLandModel.priestCount == 3)
        {
            mainController.JudgeCallback(false, "You Win!");
            mainController.isRunning=false;
            return;
        }
        else
        {
            //如果左岸上的牧师数量不为0,而且左岸上的牧师数量小于魔鬼数量,那么判断游戏失败
            //同时返回Game Over以及false参数
            //同时将游戏是否运行设置为false
            int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum;
            leftPriestNum = leftLandModel.priestCount + (boatModel.isRight ? 0 : boatModel.priestCount);
            leftDevilNum = leftLandModel.devilCount + (boatModel.isRight ? 0 : boatModel.devilCount);
            if (leftPriestNum != 0 && leftPriestNum < leftDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                mainController.isRunning=false;
                return;
            }
            rightPriestNum = rightLandModel.priestCount + (boatModel.isRight ? boatModel.priestCount : 0);
            rightDevilNum = rightLandModel.devilCount + (boatModel.isRight ? boatModel.devilCount : 0);
            //如果右岸上的牧师数量不为0,而且右岸上的牧师数量小于魔鬼数量,那么判断游戏失败
            //同时返回Game Over以及false参数
            //同时将游戏是否运行设置为false
            if (rightPriestNum != 0 && rightPriestNum < rightDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                mainController.isRunning=false;
                return;
            }
        }
    }
}

在裁判类的Update函数中,我们首先判断游戏时间是否归零,如果是就直接判断游戏结束,同时更新参数以及传递信号给GUI。随后我们继续判断游戏是否达到胜利或者失败的规则,如果是,那么更新相对应的参数以及传递信号给GUI完成交互。

其它操作

在实现了上面的动作分离后,我们还需要对我们上一个实验实现的游戏项目进行一定的修改,才可以将游戏项目运行起来。我们需要将Controllers中的Move以及MoveController删除,在主控制器中将移动船、移动角色的操作交给动作管理类来实现,控制器直接调用,其它的代码保持不变即可。同时我们还可以为我们的游戏项目添加一定的天空盒等渲染。

结语

在本次实验中,我们实现了与上一个实验完全相同的项目游戏,我们的大多数的代码实现也是基本相同的,但是我们在上一个实验的基础上,添加了动作分离,将对象的运动分离到了CCAction中,从控制器中分离了出来,大大减轻了控制器的任务以及代码量。同时使得游戏的实现效率更高,代码的书写逻辑更加流畅、直观。而且明显动作分离是更加适合于一些更加复杂的运动项目的,可以将这些动作自由组合,而不是直接写死在控制器中,机动性更加强。

在完成了本次实验后,我对动作的分离以及集合的原理有了更加深入的理解和认识。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值