3D游戏建模与设计5:基于Unity平台编写游戏:牧师与魔鬼(Priest And Devil)动作分离版本

1.游戏简介

  牧师与魔鬼是一款益智小游戏,在游戏中有两个角色:牧师与魔鬼。玩家需要设计上船方案帮助牧师过顺利河。邪恶的魔鬼会攻击善良的牧师,面对成群的魔鬼的攻击,必须有一定数量的牧师来抵御魔鬼的攻击,所以一定要保证牧师的数量不能少于魔鬼的数量。

2.游戏规则 

  游戏中有3个牧师以及3个魔鬼,无论何时何地,牧师的数量均不能少于魔鬼,若某一边(包括船上)的魔鬼数量大于牧师数量,则游戏失败。游戏可以通过船只运送牧师与魔鬼,船只一次性只能运送两名角色,牧师与魔鬼均可登船且船上可同时存在魔鬼与牧师,船只上必须有角色才能向对岸移动。玩家如果能在60秒的时间内探索出能让牧师顺利过河的方案并实施,则游戏胜利,若未能在60秒的时间内探索出能让牧师顺利过河的方案或某一时刻的某一岸上出现魔鬼数量大于牧师数量,则游戏失败。

3.游戏玩法

  玩家通过鼠标左键点击牧师(白色圆柱)、恶魔(黑色圆柱)可以完成它们上船、上岸的动作。玩家通过鼠标左键点击点击船(水面上的方块)可以让船向对岸移动。

4.游戏MVC的UML图

 BoatModel、LandModel、RiverModel、RoleModel为船只、河岸、河流、角色模型,用于模型的初始化。

MainControllor为主控制器,用于加载资源、开始游戏、重启游戏、判断游戏是否结束等。

IUserAction内含有用户能通过GUI进行的操作。

 BoatControllor、LandControllor、RiverControllor、RoleControllor用于船只、河岸、河流、角色模型的创建以及控制。

SceneControllor用于控制场景,游戏中只有一处场景。

UserGUI为用户控制相关的GUI等。

5.动作分离后的动作Action的UML图

SSActionManger:动作管理者基类并不直接作为动作管理者使用,它的存在是为调用者提供较简单和通用的接口。

CCActionManger:具体动作作为类中的一个对象从而管理动作,同时继承SSActionManager。

 SSAction:动作基类接口。

CCsequenceAction:实现组合动作,同时继承ISSActionCallBack。

CCMoveToAction:实现移动动作, 以speed的速度向target目的地移动。

ISSActionCallBack:回调函数,用于判断正在进行的动作是否完成。

IUsrAction和ClickAction:用户动作基类接口与点击动作基类接口

6.游戏编写

  本游戏基于Unity引擎编写,使用的系统为Windows10,使用的脚本文件语言为C#,首先需要确保保证电脑中安装Unity引擎,我的版本是2023.3.8f1c1,还需下载VSCode或VStudio用于编写代码,还需要Unity的Plastic SCM用于管理代码,具体安装方法这里不过多介绍。

接着我们打开Unity,找到项目,选择新项目。

选择3D,项目命名为Priest And Devil,保存地址我选择的是D盘。(启动版本管理可选可不选)

现在我们进入了Unity引擎界面,如图所示,什么内容也没有。

本项目中运用到的代码、文件非常多,为了保证界面的简洁性,建议在Assests项目栏中如下图建

立几个文件夹方便管理。

建立方法如下:在Assets栏中空白区用鼠标右键点击,选择Create中的Folder生成我们需要的游戏文件夹。

游戏中GameObject如下:

GameObject数目
牧师priest3
恶魔devil3
船只boat1
河流river1
河岸land2

 现在我们创建这些GameObject。

首先进入Materials文件夹,我们需要每个material的贴图(记得贴图得要是JPG模式)。

河岸land贴图

船只boat贴图

河流river贴图

牧师priest贴图

恶魔devil贴图

我们需要创建Material,创建方式如下:在Assets栏中空白区用鼠标右键点击,选择Create中的生成Material。

Material的贴图方式如下:点击Material,将贴图用鼠标移动到Albedo前的小框中。

完成上述步骤后Materials文件夹应该如下:

现在我们创建Prefab,点击Assets中的Resouces文件夹,创建Prefabs文件夹并在文件夹内右键空白处,选择3D Object中的Prefab,如下图所示:

分别建立devil、boat、priest、land和river的Prefab,并贴上Material,贴Material的方式为:左键点击Prefab,右侧inspector栏会显示Prefab相关信息,将Material拉入空白处,如图所示:

创捷完后如图所示:

我们再导入天空盒,选择Window中的Asset Store。

点击Search online。

在Asset Store中搜索skybox并添加。

我选择的是这个:

回到 Unity,选择Window中的Package Manager。

 选择My Assets。

找到刚刚添加的skybox,点击import。

 再次点击import。

在Assets内可以看到天空盒的文件夹了。

我选择的是这个,把喜欢的拖动到游戏中便可以了。

 

现在编写脚本,打开Scripts文件夹,创建如下Controllers、View和Model几个文件夹:

Controllers内有如下几个脚本:

我们现在编写船只控制器BoatController脚本:

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

//
public class BoatController : ClickAction
{
    BoatModel boatModel;
    IUserAction userAction;

    public BoatModelController()
    {
        userAction = SSDirector.GetInstance().CurrentSenceController as IUserAction;
    }
    public void CreateBoat(Vector3 position)
    {
        if (boatModel == null)
            boatModel = new BoatModel();
        boatModel.Init(position);
        boatModel.boat.GetComponent<Click>().setClickAction(this);
    }

    public BoatModel GetBoatModel()
    {
        return boatModel;
    }

    //将角色加载到船只上,返回接下来角色计算后应该到达的位置
    public Vector3 AddRole(RoleModel roleModel)
    {
        //船上有两个位置,分别判断两个位置是否为空
        if (boatModel.roles[0] == null)
        {
            boatModel.roles[0] = roleModel;
            roleModel.isInBoat = true;
            roleModel.role.transform.parent = boatModel.boat.transform;
            if (roleModel.isPriest)
                boatModel.priestNum++;
            else
                boatModel.devilNum++;
            return PositionModel.boatRoles[0];

        }
        if (boatModel.roles[1] == null)
        {
            boatModel.roles[1] = roleModel;
            roleModel.isInBoat = true;
            roleModel.role.transform.parent = boatModel.boat.transform;
            if (roleModel.isPriest)
                boatModel.priestNum++;
            else
                boatModel.devilNum++;
            return PositionModel.boatRoles[1];
        }
        return roleModel.role.transform.localPosition;
    }

    //将角色从船上移除
    public void RemoveRole(RoleModel roleModel)
    {
        //船上有两个位置,分别判断两个位置当中有没有要移除的角色
        if (boatModel.roles[0] == roleModel)
        {
            boatModel.roles[0] = null;
            if (roleModel.isPriest)
                boatModel.priestNum--;
            else
                boatModel.devilNum--;
        }
        if (boatModel.roles[1] == roleModel)
        {
            boatModel.roles[1] = null;
            if (roleModel.isPriest)
                boatModel.priestNum--;
            else
                boatModel.devilNum--;
        }
    }

    //处理点击事件
    public void DealClick()
    {
        if (boatModel.roles[0] != null || boatModel.roles[1] != null)
            userAction.MoveBoat();
    }
}

我们现在编写裁判类控制器JudgeController脚本:

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

public class JudgeController : MonoBehaviour
{
    public FirstController mainController;
    public LandModel leftLandModel;
    public LandModel rightLandModel;
    public BoatModel boatModel;
    // 在游戏第一帧运行update前运行这个start函数
    void Start()
    {
        mainController = (FirstController)SSDirector.GetInstance().CurrentSenceController;
        this.leftLandModel = mainController.leftLandController.GetLandModel();
        this.rightLandModel = mainController.rightLandController.GetLandModel();
        this.boatModel = mainController.boatController.GetBoatModel();
    }

    // 每一帧都会运行这个update函数
    void Update()
    {
        if (!mainController.isRuning)
            return;
        if (mainController.time <= 0)
        {
            mainController.JudgeCallback(false, "Game Over!");
            return;
        }
        this.gameObject.GetComponent<UserGUI>().gameMessage = "";
        //判断是否已经胜利
        if (rightLandModel.priestNum == 3)
        {
            mainController.JudgeCallback(false, "You Win!");
            return;
        }
        else
        {
            //若任意一侧,牧师数量不为0且牧师数量少于恶魔数量,则游戏失败
            int leftPriestNum; //左岸牧师数量
            int leftDevilNum; //左岸恶魔数量
            int rightPriestNum; //右岸牧师数量
            int rightDevilNum; //右岸恶魔数量
            leftPriestNum = leftLandModel.priestNum + (boatModel.isRight ? 0 : boatModel.priestNum);
            leftDevilNum = leftLandModel.devilNum + (boatModel.isRight ? 0 : boatModel.devilNum);
            if (leftPriestNum != 0 && leftPriestNum < leftDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
            rightPriestNum = rightLandModel.priestNum + (boatModel.isRight ? boatModel.priestNum : 0);
            rightDevilNum = rightLandModel.devilNum + (boatModel.isRight ? boatModel.devilNum : 0);
            if (rightPriestNum != 0 && rightPriestNum < rightDevilNum)
            {
                mainController.JudgeCallback(false, "Game Over!");
                return;
            }
        }
    }
}

我们现在编写河岸控制器LandController脚本:

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

public class LandController
{
    private LandModel landModel;
    
    //加载陆地模型
    public void CreateLand(string name, Vector3 position)
    {
        if (landModel==null)
            landModel = new LandModel();
        landModel.Init(name, position);
        landModel.priestNum = landModel.devilNum = 0;
    } 
   
    //调用陆地模型
    public LandModel GetLandModel()
    {
        return landModel;
    }

    //将人物添加到岸上,返回角色在岸上的相对坐标
    public Vector3 AddRole(RoleModel roleModel)
    {
        if (roleModel.isPriest)
            landModel.priestNum++;
        else
            landModel.devilNum++;
        roleModel.role.transform.parent = landModel.land.transform;
        roleModel.isInBoat = false;
        return PositionModel.roles[roleModel.tag];
    }

    //将角色从岸上移除
    public void RemoveRole(RoleModel roleModel)
    {
        if (roleModel.isPriest)
            landModel.priestNum--;
        else
            landModel.devilNum--;
    }
}

我们现在编写主控制器MainController脚本:

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

public class MainController : MonoBehaviour, SceneController, IUserAction
{
    public CCActionManager actionManager;
    public LandModelController rightLandController;                        //右岸控制器
    public LandModelController leftLandController;                         //左岸控制器
    public RiverModel riverModel;                                              //河流Model
    public BoatController boatController;                                  //船控制器
    public RoleController[] roleControllers;                         //人物控制器集合
    //private MoveController moveController;                                      //移动控制器
    public bool isRuning;                                                      //游戏进行状态
    public float time;                                                         //游戏进行时间

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


    //导入资源
    public void LoadResources()
    {
        //人物初始化
        roleControllers = new RoleModelController[6];
        for (int i = 0; i < 6; i++)
        {
            roleControllers[i] = new RoleModelController();
            roleControllers[i].CreateRole(PositionModel.roles[i], i < 3 ? true : false, i);
        }
        //左右岸初始化
        leftLandController = new LandModelController();
        leftLandController.CreateLand("left_land", PositionModel.left_land);
        rightLandController = new LandModelController();
        rightLandController.CreateLand("right_land", PositionModel.right_land);
        //将人物添加并定位至左岸  
        foreach (RoleModelController roleModelController in roleControllers)
        {
            roleModelController.GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleModelController.GetRoleModel());
        }
        //河流Model实例化
        riverModel = new RiverModel(PositionModel.river);
        //船初始化
        boatController = new BoatModelController();
        boatController.CreateBoat(PositionModel.left_boat);
        //移动控制器实例化
        //moveController = new MoveController();
        //数据初始化
        isRuning = false;
        time = 60;
    }

    //移动船
    public void MoveBoat()
    {
        //判断当前游戏是否在进行,同时是否有对象正在移动
        if ((!isRuning) || actionManager.IsMoving())
            return;
        //判断船在左侧还是右侧
        Vector3 destination = boatController.GetBoatModel().isRight ? PositionModel.left_boat : PositionModel.right_boat;
        actionManager.MoveBoat(boatController.GetBoatModel().boat, destination, 5);
/*        if (boatRoleController.GetBoatModel().isRight)
            moveController.SetMove(PositionModel.left_boat, boatRoleController.GetBoatModel().boat);
        else
            moveController.SetMove(PositionModel.right_boat, boatRoleController.GetBoatModel().boat);*/
        //移动后,将船的位置取反
        boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;
    }

    //移动人物    
    public void MoveRole(RoleModel roleModel)
    {
        //判断当前游戏是否在进行,同时是否有对象正在移动
        if ((!isRuning) || actionManager.IsMoving())
            return;

        Vector3 destination, mid_destination;
        if (roleModel.isInBoat)
        {
            //若人在船上,则将其移向岸上
            if (boatController.GetBoatModel().isRight)
                destination = rightLandController.AddRole(roleModel);
            else
                destination = leftLandController.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);
            roleModel.isRight = boatController.GetBoatModel().isRight;
            boatController.RemoveRole(roleModel);
        }
        else
        {
            //若人在岸上,则将其移向船
            if (boatController.GetBoatModel().isRight == roleModel.isRight)
            {
                if (roleModel.isRight)
                {
                    rightLandController.RemoveRole(roleModel);
                }
                else
                {
                    leftLandController.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 MyStart()
    {
        //对各数据进行初始化
        time = 60;
        leftLandController.CreateLand("left_land", PositionModel.left_land);
        rightLandController.CreateLand("right_land", PositionModel.right_land);
        for (int i = 0; i < 6; i++)
        {
            roleControllers[i].CreateRole(PositionModel.roles[i], i < 3 ? true : false, i);
            roleControllers[i].GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleControllers[i].GetRoleModel());
        }
        boatController.CreateBoat(PositionModel.left_boat);
        isRuning = true;
    }

    //游戏重置
    public void Restart()
    {
        //对各数据进行初始化
        time = 60;
        leftLandController.CreateLand("left_land", PositionModel.left_land);
        rightLandController.CreateLand("right_land", PositionModel.right_land);
        for (int i = 0; i < 6; i++)
        {
            roleControllers[i].CreateRole(PositionModel.roles[i], i < 3 ? true : false, i);
            roleControllers[i].GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleControllers[i].GetRoleModel());
        }
        boatController.CreateBoat(PositionModel.left_boat);
        isRuning = true;
    }


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

    void Update()
    {
        if (isRuning)
        {
            time -= Time.deltaTime;
            this.gameObject.GetComponent<UserGUI>().time = (int)time;
        }
    }

}

 我们现在编写移动控制器MoveController脚本:

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

public class MoveCamera : MonoBehaviour
{
    void Start()
    {
        transform.position = new Vector3((float)1.11,1, (float)-18);        
    }
}

 我们现在编写角色控制器RoleController脚本:

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

public class RoleController :ClickAction
{
    RoleModel roleModel;                                       
    IUserAction userAction;                                     

    //用户行为类userAction初始化
    public RoleModelController()
    {
        userAction = SSDirector.GetInstance().CurrentSenceController as IUserAction;
    }
 
    //创建角色模型
    public void CreateRole(Vector3 position, bool isPriest, int tag)
    {
        if (roleModel == null)
            roleModel = new RoleModel();
        roleModel.Init(position, isPriest, tag);
        roleModel.role.GetComponent<Click>().setClickAction(this);
    }

    //调用角色模型
    public RoleModel GetRoleModel()
    {
        return roleModel;
    }
  
    //处理点击事件
    public void DealClick()
    {
        userAction.MoveRole(roleModel);
    }
}

我们现在编写场景控制器SceneController脚本:

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

//加载资源
public interface SceneController
{
    void LoadResources();    
}

我们现在编写系统控制器SceneController脚本:

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

public class SSDirector : System.Object
{
    private static SSDirector _instance;
    public ISceneController CurrentSenceController { get; set; }
    public static SSDirector GetInstance()
    {
        if (_instance == null)
        {
            _instance = new SSDirector();
        }
        return _instance;
    }
}

到此为止Controllers文件夹内的脚本编写完成。

Models内有如下几个脚本:

我们现在编写船只模型BoatModel:

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

public class BoatModel {
    public GameObject boat;                     //船对象
    public RoleModel[] roles;                   //船上的角色的指针
    public bool isRight;                        //判断船在左侧还是右侧
    public int priestNum, devilNum;             //船上牧师与恶魔的数量

    public void Init(Vector3 position)
    {
        if (boat != null)
            Object.DestroyImmediate(boat);
        priestNum = devilNum = 0;
        roles = new RoleModel[2];
        boat = GameObject.Instantiate(Resources.Load("Prefabs/boat", typeof(GameObject))) as GameObject;
        boat.name = "boat";
        boat.transform.position = position;
        boat.transform.localScale = new Vector3(4, (float)1.5, 3);
        boat.AddComponent<BoxCollider>();
        boat.AddComponent<Click>();
        isRight = false;
    }
}

 我们现在编写河岸模型LandModel脚本:

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

public class LandModel
{
    public GameObject land;                     //岸的游戏对象
    public int priestNum, devilNum;             //岸上牧师与恶魔的数量

    public void Init(string name, Vector3 position)
    {
        if (land != null)
            Object.DestroyImmediate(land);
        priestNum = devilNum = 0;
        land = GameObject.Instantiate(Resources.Load("Prefabs/land", typeof(GameObject))) as GameObject;
        land.name = name;
        land.transform.position = position;
        land.transform.localScale = new Vector3(13, 5, 3);
    }
}

我们现在编写角色模型RoleModel脚本:

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

public class RoleModel{ 
    public GameObject role;             //角色的游戏对象
    public bool isPriest;               //区分角色是牧师还是恶魔
    public int tag;                     //给对象标号,方便查找
    public bool isRight;                //区分角色是在左侧还是右侧
    public bool isInBoat;               //区分角色是在船上还是在岸上

    //初始化函数
    public void Init(Vector3 position, bool isPriest, int tag)
    {
        if (role != null)
            Object.DestroyImmediate(role);
        this.isPriest = isPriest;
        this.tag = tag;
        isRight = false;
        isInBoat = false;
        role = GameObject.Instantiate(Resources.Load("Prefabs/" + (isPriest ? "priest" : "devil"), typeof(GameObject))) as GameObject;
        role.transform.localScale = new Vector3(1, 1, 1);
        role.transform.position = position;
        role.name = "role" + tag;
        role.AddComponent<Click>();
        role.AddComponent<BoxCollider>();
    }
}

我们现在编写河流模型RiverModel脚本:

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

public class RiverModel
{
    private GameObject river;               //河流的游戏对象

    public RiverModel(Vector3 position)
    {
        river = Object.Instantiate(Resources.Load("Prefabs/river", typeof(GameObject))) as GameObject;
        river.name = "river";
        river.transform.position = position;
        river.transform.localScale = new Vector3(15, 2, 3);
    }
}

我们现在编写位置模型PositionModel脚本:

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

public class PositionModel
{
    public static Vector3 right_land = new Vector3(15, -4, 0);                  //右侧岸的位置
    public static Vector3 left_land = new Vector3(-13, -4, 0);                  //左侧岸的位置
    public static Vector3 river = new Vector3(1, -(float)5.5, 0);               //河流的位置
    public static Vector3 right_boat = new Vector3(7, -(float)3.8, 0);          //船在右边的位置
    public static Vector3 left_boat = new Vector3(-5, -(float)3.8, 0);          //船在左边的位置
    //角色在岸上的相对位置(6个)
    public static Vector3[] roles = new Vector3[]{new Vector3((float)-0.2, (float)0.7, 0) ,new Vector3((float)-0.1, (float)0.7,0),
    new Vector3(0, (float)0.7,0),new Vector3((float)0.1, (float)0.7,0),new Vector3((float)0.2, (float)0.7,0),new Vector3((float)0.3, (float)0.7,0)};
    //角色在船上的相对位置(2个)
    public static Vector3[] boatRoles = new Vector3[] { new Vector3((float)-0.1, (float)1.2, 0), new Vector3((float)0.2, (float)1.2, 0) };
}

我们现在编写点击事件Click脚本:

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

public class Click : MonoBehaviour
{
    ClickAction clickAction;

    public void setClickAction(ClickAction clickAction)
    {
        this.clickAction = clickAction;
    }

    void OnMouseDown()
    {
        clickAction.DealClick();
    }
}

到此为止Models文件夹内的脚本编写完成。

View内有如下一个脚本:

我们现在编写控制用户GUL的UserGUI脚本:

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

public class UserGUI : MonoBehaviour
{
    private IUserAction userAction;
    public string gameMessage;
    public int time;
    public bool ButtionRestartIsShow;
    void Start()
    {
        gameMessage = "";
        time = 60;
        userAction = SSDirector.GetInstance().CurrentSenceController as IUserAction;
        ButtionRestartIsShow = false;
    }

    void OnGUI()
    {
        //小字体初始化
        GUIStyle style = new GUIStyle();
        style.normal.textColor = Color.white;
        style.fontSize = 30;

        //大字体初始化
        GUIStyle bigStyle = new GUIStyle();
        bigStyle.normal.textColor = Color.white;
        bigStyle.fontSize = 50;

        GUI.Label(new Rect(375, 30, 50, 200), "牧师与魔鬼", bigStyle);

        GUI.Label(new Rect(320, 100, 50, 200), gameMessage, style);

        GUI.Label(new Rect(0, 0, 100, 50), "Time: " + time, style);


        if (GUI.Button(new Rect(470, 160, 100, 50), "Start"))
        {
            userAction.MyStart();
            ButtionRestartIsShow = true;
        }

        if(ButtionRestartIsShow == true)
        {
            if (GUI.Button(new Rect(850, 50, 100, 50), "Retart"))
            {
                userAction.Restart();
            }
        }
    }
}

到此为止View文件夹内的脚本编写完成。

在上一次MVC版本中的牧师与魔鬼,我们主要使用MainController、MoveController等控制器来实现对游戏内角色的移动,由于我们的游戏牧师与魔鬼的规模较小,单纯使用MVC方式编写游戏是可行的,但是对于更加大型的游戏,将所有的动作放在控制器内运行,这对于后续代码的修改以及维护非常困难,且控制器的负担十分大,为了减少这些弊端对游戏的影响,我们可以将 GameObject 的动作通过动作管理器将其分离出 控制器,将 GameObject 的动作方法调用转移给动作管理器Action进行管理。

Action内有如下一个脚本:

为了更好理解动作分离并编写Actions内的几个脚本,我们先编写一下角色动作表:

角色动作前提条件
牧师从左岸登船左岸有至少一个牧师&&船在左岸&&船上有空位
魔鬼从左岸登船左岸有至少一个魔鬼&&船在左岸&&船上有空位
牧师从右岸登船右岸有至少一个牧师&&船在右岸&&船上有空位
魔鬼从右岸登船右岸有至少一个魔鬼&&船在右岸&&船上有空位
牧师从左岸下船船上有至少一个牧师&&船在左岸
魔鬼从左岸下船船上有至少一个魔鬼&&船在左岸
牧师从右岸下船船上有至少一个牧师&&船在右岸
魔鬼从右岸下船船上有至少一个魔鬼&&船在右岸
船向对岸移动船上至少有一个牧师或者魔鬼

有了用户动作表,我们现在可以编写脚本了。

先编写ISSAction脚本,在动作分离版本中,将动作其抽离为一个 SSAction 类,当这个物体需要进行某些动作时,再实时进行动作的创建,并进行Update()。并且为其定义一个 destroy 属性,当动作完成后自动将这个动作实例标记为可以删除的对象,等待游戏引擎自动回收这个对象,减少对系统资源的占用。

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; }      //动作对象的transform
    public ISSActionCallback callback { get; set; }  //回调函数

    protected SSAction()   //保证SSAction不会被new
    {

    }

    // Start is called before the first frame update
    public virtual void Start()   //子类可以使用Start()和Update()这两个函数
    {
        throw new System.NotImplementedException();
    }

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

再编写ISSActionCallBack脚本,当一个动作完成之后,我们需要通知其他的部分(如动作管理器或其他的动作)当前的动作已经完成,可以进行下一步的操作。因此添加一个SSActionCallbackInterface 类,使得在动作完成之后再进行一个回调操作,实现通知其他部分的这个功能。

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

public enum SSActionEventType : int { Started, Competed }
public interface ISSActionCallback
{
    //回调函数
    void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        int intParam = 0,
        string strParam = null,
        Object objectParam = null);
}

我们现在编写CCMoveToAction脚本,CCMoveToAction 继承于 SSAction,并给动作管理器提供了GetSSAction的接口。当动作管理器调用这个方法的时候,可以直接生成一个动作的脚本,此时只需要将这个脚本赋予给某个物体,并由动作管理器进行动作的调度之后,即可产生物体做出这个动作的效果。

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脚本,有些动作并不只有一个部分组成,它可能是一个动作的序列(如牧师和魔鬼在上下船的运动)。因此 SSActionManger 继承了CCMoveToAction ,在动作序列中,当一个动作完成之后,则需要通过回调函数执行下一个动作,当所有动作执行完成之后才需要将这个动作的实例给标记为可删除的对象。

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(); //一个组合中的一个动作执行完后会调用接口,所以这里看似没有start++实则是在回调接口函数中实现
        }
    }

    //回调处理,当有动作完成时触发
    public void SSActionEvent(SSAction source,
        SSActionEventType events = SSActionEventType.Competed,
        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()
    {
        //如果组合动作做完第一个动作突然不要它继续做了,那么后面的具体的动作需要被释放
    }
}

我们现在编写SSActionManager脚本,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; //获取动作实例的ID作为key
        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()
    {

    }

}

我们现在编写CCActionManger脚本,由于动作管理器的设计不局限于一个项目的使用,因此在这个项目中继承 SSActionManager 再设计一个具体的CCActionManger类,使得其可以在这个项目中产生具体的动作实例,再进行调度操作。

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)SSDirector.GetInstance().CurrentSenceController;
        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.Competed,
    int intParam = 0,
    string strParam = null,
    Object objectParam = null)
    {
        isMoving = false;
    }
}

由于我们角色和船只的移动是通过鼠标点击实现的,我们还需要编写ClickAction管理点击动作,具体脚本代码如下:

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

public interface ClickAction
{
    void DealClick();
}

此外,我们还用IUAction记录了用户的动作表,代码如下:

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

public interface IUserAction
{
    void MoveBoat();
    void MoveRole(RoleModel roleModel);
    void MyStart();
    void Restart();
}

至此,我们成功编写完成了动作分离版本的牧师与魔鬼!!!

7.游戏演示

3D游戏设计与建模作业4 基于Unity编写游戏牧师与魔鬼(动作分离版)_哔哩哔哩bilibili

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值