牧师与魔鬼(动作分离版)

发布时间 2023-10-27 11:41:10作者: 一指流沙zjh

牧师与魔鬼(动作分离版)

前言

上节课我们已经完成了牧师与魔鬼的MVC分离版,这次我们继续完善代码结构,提高代码的复用性
游戏实现视频:点击跳转小破站
项目地址:https://github.com/yangjx77/3DGameDesign/tree/main/实验6

设计模式原理

动作管理器就是一个对象,管理整个场景中所有的动作。
一个SceneController(场景管理器)只配备一个动作管理器对象。
不管是游戏角色的移动还是船的移动,都归这个对象管;
动作管理器可以添加动作(添加的时候要指定动作所作用的GameObject),监测已经完成的动作并清除。

代码结构剖析

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完成交互。