Unity游戏制作——牧师与魔鬼 动作分离版

目录

一、前言

二、代码介绍

Model

View

Controller

Actions

动作事件接口  SSActionEvent

动作基类  SSAction

简单动作实现  CCMoveToAction

顺序动作组合类  CCSequenceAction

动作管理基类  SSActionManager

使用动作的组合  CCActionManager

FirstController

裁判类JudgeController

三、结束


一、前言

        这篇博客是上一篇博客的进阶版,上一个博客链接:Unity游戏制作——牧师与魔鬼-CSDN博客。这次主要是根据老师提供的代码示例,在上一个游戏的基础上:

  • 集成 CCAction,使得动作管理从场景控制器中分离。
  • 利用接口消息传递机制,设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。实现游戏结束判定从场景控制器中分离。

二、代码介绍

        第二版的游戏和上一版的具体区别是代码部分去除了原本在controller中的movehe movecrtl类,增加了一系列Action类和裁判类JudgeController。

……

修改结构后的UML图如下:

        项目整体结构如下:

        下面将主要介绍新增和修改的部分。

Model

        没有改变。

View

        没有改变,和上一版一样。

Controller

        (把Actions也算到控制部分,应该合理吧……)

        改动主要在这一部分,改变了原本的控制结构。

        在这一部分中,删除了原来控制物体移动的move和moveController,等价地替换成Actions(虽然在这个游戏中看起来变得复杂了,但是如果是较大型的游戏,就会让代码结构更加清晰,分工更明确)。同时修改了FirtstController,增加裁判类JudgeController。

Actions

动作事件接口  SSActionEvent
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType:int {Started, Completed}
public interface ISSActionCallback
{
    //回调函数
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        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 destroy = false;

    public GameObject gameObject { get; set; }
    public Transform transform { get; set; }
    public ISSActionCallback callback { get; set; }

    protected SSAction()
    {

    }

    // Start is called before the first frame update
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }

    // Update is called once per frame
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}

        SSAction是动作的基类,后续的动作要从它派生出去。作为父类,SSAction使用 virtual 申明虚方法,通过重写实现多态。这样继承者就明确使用 Start 和 Update 编程游戏对象行为。利用了接口上面ISSACtionCallback实现消息通知,避免与动作管理者直接依赖。

简单动作实现  CCMoveToAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCMoveToAction : SSAction
{
    //目的地
    public Vector3 target;
    //速度
    public float speed;

    private CCMoveToAction()
    {

    }

    public static CCMoveToAction GetSSAction(Vector3 target, float speed)
    {
        CCMoveToAction action = ScriptableObject.CreateInstance<CCMoveToAction>();
        action.target = target;
        action.speed = speed;
        return action;
    }

    // Start is called before the first frame update
    public override void Start()
    {
        
    }

    // Update is called once per frame
    public override void Update()
    {
        //判断是否符合移动条件
        if (this.gameObject == null || this.transform.localPosition == target)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this);
            return;
        }
        //移动
        this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);
    }
}

        实现一个简单的动作:将一个物体以特定速度移动到目标位置,并通知任务完成,期望管理程序自动回收运行对象。

顺序动作组合类  CCSequenceAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CCSequenceAction : SSAction, ISSActionCallback
{
    //动作序列
    public List<SSAction> sequence;
    //重复次数
    public int repeat = -1;
    //动作开始指针
    public int start = 0;

    //生产函数
    public static CCSequenceAction GetSSAction(int repeat, int start, List<SSAction> sequence)
    {
        CCSequenceAction action = ScriptableObject.CreateInstance<CCSequenceAction>();
        action.repeat = repeat;
        action.start = start;
        action.sequence = sequence;
        return action;
    }

    //对序列中的动作进行初始化
    public override void Start()
    {
        foreach (SSAction action in sequence)
        {
            action.gameObject = this.gameObject;
            action.transform = this.transform;
            action.callback = this;
            action.Start();
        }
    }

    //运行序列中的动作
    public override void Update()
    {
        if (sequence.Count == 0)
            return;
        if (start < sequence.Count)
        {
            sequence[start].Update();
        }
    }

    //回调处理,当有动作完成时触发
    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Completed,
        int Param = 0,
        string strParam = null,
        Object objectParam = null)
    {
        source.destroy = false;
        this.start++;
        if (this.start >= sequence.Count)
        {
            this.start = 0;
            if (repeat > 0)
                repeat--;
            if (repeat == 0)
            {
                this.destroy = true;
                this.callback.SSActionEvent(this);
            }
        }
    }

    void OnDestroy()
    {

    }
}

        在这个类中,让动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件。在重载的GetSSAction中,创建了一个动作顺序执行序列,用-1 表示无限循环,0表示开始动作。重载Start和Update方法来初始化并执行当前动作。实现的接口SSActionEvent 收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者。

动作管理基类  SSActionManager
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSActionManager : MonoBehaviour
{
    //动作集,以字典形式存在
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
    //等待被加入的动作队列(动作即将开始)
    private List<SSAction> waitingAdd = new List<SSAction>();
    //等待被删除的动作队列(动作已完成)
    private List<int> waitingDelete = new List<int>();

    protected void Update()
    {
        //将waitingAdd中的动作保存
        foreach (SSAction ac in waitingAdd)
            actions[ac.GetInstanceID()] = ac;
        waitingAdd.Clear();

        //运行被保存的事件
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID());
            }else if (ac.enable)
            {
                ac.Update();
            }
        }

        //销毁waitingDelete中的动作
        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    //准备运行一个动作,将动作初始化,并加入到waitingAdd
    public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback manager)
    {
        action.gameObject = gameObject;
        action.transform = gameObject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

    // Start is called before the first frame update
    protected void Start()
    {

    }

}

        这是动作对象管理器的基类,实现了所有动作的基本管理。该基类创建 MonoBehaiviour 管理一个动作集合,动作做完自动回收动作,维护了一个动作字典。这里有一个问题是,字典是线程不安全的,会对我们的游戏有影响吗?答案是不会,因为这个游戏中只有一个线程,动作管理器用isMoving参数作为锁拒绝了并行的可能。

        SSActionManger还提供了添加新动作的方法 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 controller;

    protected new void Start()
    {
        controller = (FirstController)Director.GetInstance().CurrentSceneController;
        controller.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);
    }

    //回调函数
    public void SSActionEvent(SSAction source,
    SSActionEventType events = SSActionEventType.Completed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        isMoving = false;
    }
}

       干实事的家伙。 继承上面的动作管理基类,实现回调函数的接口。实现了具体的移动船和移动人的动作,在动作完成后通过回调函数通知管理者动作已结束,isMoving=false。isMoving可以理解为锁,避免了多个动作一起的可能性。

FirstController

        作为控制的主体,FirstController当然会有修改:

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

public class FirstController : MonoBehaviour, SceneController, UserAction
{
    public CCActionManager actionManager;
    public ShoreCtrl leftShoreController, rightShoreController;
    public River river;
    public BoatCtrl boatController;
    public RoleCtrl[] roleControllers;
    public bool isRunning;
    public float time;

    public void JudgeCallback(bool isRuning, string message)
    {
        this.gameObject.GetComponent<UserGUI>().gameMessage = message;
        this.gameObject.GetComponent<UserGUI>().time = (int)time;
        this.isRunning = isRunning;
    }

    public void LoadResources()
    {
        //role
        roleControllers = new RoleCtrl[6];
        for (int i = 0; i < 6; ++i)
        {
            roleControllers[i] = new RoleCtrl();
            roleControllers[i].CreateRole(Position.role_shore[i], i < 3 ? true : false, i);
        }

        // 加载此岸和彼岸
        leftShoreController = new ShoreCtrl();
        leftShoreController.CreateShore(Position.left_shore);
        leftShoreController.GetShore().shore.name = "this_shore";
        rightShoreController = new ShoreCtrl();
        rightShoreController.CreateShore(Position.right_shore);
        rightShoreController.GetShore().shore.name = "other_shore";

        // 将人物添加并定位至左边 
        foreach (RoleCtrl roleController in roleControllers)
        {
            roleController.GetRoleModel().role.transform.localPosition = leftShoreController.AddRole(roleController.GetRoleModel());
        }
        //boat
        boatController = new BoatCtrl();
        boatController.CreateBoat(Position.left_boat);

        //river
        river = new River(Position.river);

        isRunning = true;
        time = 60;
    }

    public void MoveBoat()
    {
        if (isRunning == false || actionManager.IsMoving())
            return;

        Vector3 destination = boatController.GetBoatModel().isRight ? Position.left_boat : Position.right_boat;
        actionManager.MoveBoat(boatController.GetBoatModel().boat, destination, 5);

        boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;

    }

    public void MoveRole(Role roleModel)
    {
        if (isRunning == false || actionManager.IsMoving())
            return;
        Vector3 destination, mid_destination;
        if (roleModel.inBoat)
        {

            if (boatController.GetBoatModel().isRight)
                destination = rightShoreController.AddRole(roleModel);
            else
                destination = leftShoreController.AddRole(roleModel);
            if (roleModel.role.transform.localPosition.y > destination.y)
                mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
            else
                mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);

            actionManager.MoveRole(roleModel.role, mid_destination, destination, 10);
            roleModel.onRight = boatController.GetBoatModel().isRight;
            boatController.RemoveRole(roleModel);
        }
        else
        {

            if (boatController.GetBoatModel().isRight == roleModel.onRight)
            {
                if (roleModel.onRight)
                {
                    rightShoreController.RemoveRole(roleModel);
                }
                else
                {
                    leftShoreController.RemoveRole(roleModel);
                }
                destination = boatController.AddRole(roleModel);
                if (roleModel.role.transform.localPosition.y > destination.y)
                    mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
                else
                    mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
                actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
            }
        }
    }

    public void Check(){    }

    public void RestartGame()
    {
        if (GUI.Button(new Rect(0, 35, 100, 30), "Restart"))
        {
            // 重新加载游戏场景,只有一个场景,那编号就是0
            SceneManager.LoadScene(0);
        }
    }

    void Awake()
    {
        Director.GetInstance().CurrentSceneController = this;
        LoadResources();
        this.gameObject.AddComponent<UserGUI>();
        this.gameObject.AddComponent<CCActionManager>();
        this.gameObject.AddComponent<JudgeController>();
    }

    // Update is called once per frame
    void Update()
    {
        if (isRunning)
        {
            time -= Time.deltaTime;
            this.gameObject.GetComponent<UserGUI>().time = (int)time;
        }
    }
}

        和上一版相比,主要是修改了控制船和控制角色的逻辑。抽出了check函数单独作为裁判类。

裁判类JudgeController

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

public class JudgeController : MonoBehaviour
{
    public FirstController mainController;
    public Shore leftShoreModel;
    public Shore rightShoreModel;
    public Boat boatModel;
    
    // Start is called before the first frame update
    void Start()
    {
        mainController = (FirstController)Director.GetInstance().CurrentSceneController;
        this.leftShoreModel = mainController.leftShoreController.GetShore();
        this.rightShoreModel = mainController.rightShoreController.GetShore();
        this.boatModel = mainController.boatController.GetBoatModel();
    }

    // Update is called once per frame
    void Update()
    {
        if (!mainController.isRunning)
            return;
        if (mainController.time <= 0)
        {
            mainController.JudgeCallback(false, "Game Over!");
            return;
        }
        this.gameObject.GetComponent<UserGUI>().gameMessage = "";
        //判断是否已经胜利
        if (rightShoreModel.pastorCount == 3)
        {
            mainController.JudgeCallback(false, "You Win!");
            return;
        }
        else
        {
            
            int leftPastorNum, leftDevilNum, rightPastorNum, rightDevilNum;
            leftPastorNum = leftShoreModel.pastorCount + (boatModel.isRight ? 0 : boatModel.pastorCount);
            leftDevilNum = leftShoreModel.devilCount + (boatModel.isRight ? 0 : boatModel.devilCount);
            if (leftPastorNum != 0 && leftPastorNum < leftDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
            rightPastorNum = rightShoreModel.pastorCount + (boatModel.isRight ? boatModel.pastorCount : 0);
            rightDevilNum = rightShoreModel.devilCount + (boatModel.isRight ? boatModel.devilCount : 0);
            if (rightPastorNum != 0 && rightPastorNum < rightDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
        }
    }
}

        从原来FirstController中的check函数抽离出来实现的裁判类,检测游戏状态,在游戏结束时通过回调通知FirstController场记结束,实现游戏结束判定从场景控制器中分离。

三、结束

        至此修改后的项目介绍完成,运行的方法和上一版一样,运行效果也没变,因此不再做视频演示。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值