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

本文围绕《牧师与魔鬼》游戏开发展开,介绍与普通版区别,将动作从代码分离并设计裁判类,降低耦合、便于维护。阐述代码实现,包括动作基类、回调函数等。还提到裁判类作用及问题解决办法,最后分享制作心得,提升了游戏开发技能。

与普通版的区别

这次游戏项目与第一版的区别在于,将动作从代码中分离了出来,并设计一个裁判类实时地监管游戏,这样做会有很多好处:

  • 降低了不同功能之间的耦合性,代码的复用性更好。
  • 通过门面模式的设计,程序更加容易进行维护。

代码实现

SSAction 动作基类。

定义了两个虚函数 Start 和 Update,后续的动作类都继承动作基类并实现这两个虚函数。

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();
    }
}

ISSActionCallback 回调函数。

SSActionEvent 是回调函数,动作完成后需要通知主控制器完成结果,设计这样的类更加方便事件的调度。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SSActionEventType : int { Started, Competeted } 
public interface ISSActionCallback
{
    // Start is called before the first frame update
    // void Start()
    // {
        
    // }

    // // Update is called once per frame
    // void Update()
    // {
        
    // }

    public void SSActionEvent(SSAction source, 
                    SSActionEventType events = SSActionEventType.Competeted, 
                    int intParam = 0, string strParam = null, Object objectParam = null);
}

SSActionManager 动作管理基类

Update 中实现了所有动作的基本管理。具体操作是遍历动作字典中的所有动作,查看它们的信息,进行相应的销毁或更新操作。

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>();
    // Start is called before the first frame update
    protected void Start()
    {
        
    }

    // Update is called once per frame
    protected void Update()
    {
        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();
            }
        }
        foreach(int key in waitingDelete){
            SSAction ac = actions[key];
            actions.Remove(key);
            Destroy(ac);
        }
        waitingDelete.Clear();
    }

    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager){
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }
}

CCMoveToAction 简单移动实现。

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

public class CCMoveToAction : SSAction
{
    public Vector3 target;   // 移动后的目标位置
    public float speed;

    private CCMoveToAction(){

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

    // Update is called once per frame
    public override void Update()
    {
        this.transform.localPosition = Vector3.MoveTowards(this.transform.localPosition, target, speed * Time.deltaTime);
        // 如果游戏对象不存在或者当前位置已在目标位置上,则不移动
        if(this.transform.localPosition == target || this.gameobject == null){
            this.destroy = true;    // 标记为销毁
            this.callback.SSActionEvent(this);   // 回调函数
            return;
        }
        

    }

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

CCSequenceAction 组合动作实现

通过一个列表存储组合动作中的各个子动作,并按存放的顺序依次执行。根据参数 repeat 判断是否要重复执行组合动作。

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;
    // Start is called before the first frame update
    public override void Start()
    {
        // 初始化列表中的动作
        foreach(SSAction action in sequence){
            action.gameobject = this.gameobject;
            action.transform = this.transform;
            action.callback = this;
            action.Start();
        }
    }

    // Update is called once per frame
    public override void Update()
    {
        if(sequence.Count <= 0){
            return;
        }
        if(sequence.Count > 0 && start < sequence.Count){
            sequence[start].Update();
        }
        else{
            return;
        }
    }

    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 void SSActionEvent(SSAction source, 
                    SSActionEventType events = SSActionEventType.Competeted, 
                    int intParam = 0, string strParam = null, Object objectParam = null){
            source.destroy = false;
            this.start++;

            if(this.start >= sequence.Count){
                this.start = 0;
                if(this.repeat > 0){
                    this.repeat--;
                }
                else{
                    this.destroy = true;
                    this.callback.SSActionEvent(this);
                }
            }
    }

    void OnDestroy(){

    }
}

CCActionManager 动作组合

MoveBoat 移动船,这是单独的一个动作,对应为 CCMoveToAction

MoveRole 移动人物,这是一个组合动作(折线运动),对应为 CCSequenceAction

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

public class CCActionManager : SSActionManager, ISSActionCallback
{
    
    public CCMoveToAction boatMovement;
    public CCSequenceAction roleMovement;
    public FirstController controller;
    private bool isMoving = false;

    protected new void Start()
    {
        controller = (FirstController)SSDirector.GetInstance().CurrentSceneController;
        controller.actionManager = this;
    }

    public bool CheckMoving()
    {
        return isMoving;
    }

    public void MoveBoat(GameObject boat, Vector3 target, float speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        boatMovement = CCMoveToAction.GetSSAction(target, speed);
        this.RunAction(boat, boatMovement, this);
    }

    public void MoveRole(GameObject role, Vector3 middle_pos, Vector3 target, float speed)
    {
        if (isMoving)
            return;
        isMoving = true;
        SSAction ac1 = CCMoveToAction.GetSSAction(middle_pos, speed);
        SSAction ac2 = CCMoveToAction.GetSSAction(target, speed);
        roleMovement = CCSequenceAction.GetSSAction(0, 0, new List<SSAction> {CCMoveToAction.GetSSAction(middle_pos, speed), CCMoveToAction.GetSSAction(target, speed)});
        this.RunAction(role, roleMovement, this);
    }

    public void SSActionEvent(SSAction source,
                    SSActionEventType events = SSActionEventType.Competeted,
                    int intParam = 0,
                    string strParam = null,
                    Object objectParam = null)
    {
        isMoving = false;
    }
}

JudgeController 裁判类(新增)

裁判类的作用是判断游戏进行的状态并通知主控制器,简单来说就是将原来 FirstController 中的 Check 抽离出来用一个类单独实现。需要在每一帧判断当前游戏的输赢,因此需要输入相关的变量进行判断。

在 Update 中实现判断的逻辑,以达到实施监测的效果。

这里有一个问题,我们需要利用到判断的结果在主控制器中进行相应的处理(打印信息等等),但 Update 函数不返回任何值(void类型)。解决的方法是,在FirstCotroller 中设计一个回调函数JudgeResultCallBack,在裁判类中调用 JudgeResultCallBack 以返回判断结果。
 

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

public class JudgeController : MonoBehaviour
{
    public FirstController sceneController;
    public Land rightLand;
    public Land leftLand;
    public Boat boat;

    // Start is called before the first frame update
    void Start()
    {
        this.sceneController = (FirstController)SSDirector.GetInstance().CurrentSceneController;
        this.rightLand = sceneController.rightLandController.GetLand();
        this.leftLand = sceneController.leftLandController.GetLand();
        this.boat = sceneController.boatController.GetBoatModel();
    }

    // Update is called once per frame
    void Update()
    {
        if(sceneController.isRunning == false){
            return;
        }
        this.gameObject.GetComponent<UserGUI>().gameMessage = "";
        if(rightLand.priestCount == 3){
            // win, callback
            sceneController.JudgeResultCallBack("You win!!");
            return;
        }
        else{
            int leftPriestCount, rightPriestCount, leftDevilCount, rightDevilCount;
            leftPriestCount = leftLand.priestCount + (boat.isRight ? 0 : boat.priestCount);
            rightPriestCount = 3 - leftPriestCount;
            leftDevilCount = leftLand.devilCount + (boat.isRight ? 0: boat.devilCount);
            rightDevilCount = 3 - leftDevilCount;

            if((leftPriestCount != 0 && leftPriestCount < leftDevilCount) || (rightPriestCount != 0 && rightPriestCount < rightDevilCount)){
                // lose
                sceneController.JudgeResultCallBack("Game over!!");
                return;
            }
        }

    }

}

FirstController 场景控制器

与上一次项目的主要不同是,这次的场景控制器并不具体实现移动和检查的逻辑,而是交由相应的功能类实现。

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

public class FirstController : MonoBehaviour, ISceneController, IUserAction {
    public LandControl leftLandController, rightLandController;
    public River river;
    public BoatControl boatController;
    public RoleControl[] roleControllers;
    public MoveCtrl moveController;
    public bool isRunning;
    public float time;
    public CCActionManager actionManager;
    public float speed = 5;

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

        leftLandController = new LandControl();
        leftLandController.CreateLand(Position.left_land);
        leftLandController.GetLand().land.name = "left_land";
        rightLandController = new LandControl();
        rightLandController.CreateLand(Position.right_land);
        rightLandController.GetLand().land.name = "right_land";

        foreach (RoleControl roleController in roleControllers)
        {
            roleController.GetRoleModel().role.transform.localPosition = leftLandController.AddRole(roleController.GetRoleModel());
        }

        boatController = new BoatControl();
        boatController.CreateBoat(Position.left_boat);
        river = new River(Position.river);
        moveController = new MoveCtrl();

        isRunning = true;
        time = 60;
    }

    public void MoveBoat() {
        if (isRunning == false || actionManager.CheckMoving() == true) return;
        Vector3 target;
        if(boatController.GetBoatModel().isRight){
            target = Position.left_boat;
        }
        else{
            target = Position.right_boat;
        }
        actionManager.MoveBoat(boatController.GetBoatModel().boat, target, speed);
        boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;
    }

    public void MoveRole(Role roleModel) {
        if (isRunning == false || actionManager.CheckMoving() == true) return;
        Vector3 middle_pos;
        Vector3 target;
        if(roleModel.inBoat){
            if(boatController.GetBoatModel().isRight){
                target = rightLandController.AddRole(roleModel);
            }
            else{
                target = leftLandController.AddRole(roleModel);
            }

            // if(roleModel.role.transform.localPosition.y > target.y){
            //     middle_pos = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);
            // }
            // else{
            //     middle_pos = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);
            // }
            middle_pos = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);
            actionManager.MoveRole(roleModel.role, middle_pos, target, speed);
            roleModel.onRight = boatController.GetBoatModel().isRight;
            boatController.RemoveRole(roleModel);
        }

        else{
            if (boatController.GetBoatModel().isRight == roleModel.onRight){
                if (roleModel.onRight) {
                    rightLandController.RemoveRole(roleModel);
                }
                else {
                    leftLandController.RemoveRole(roleModel);
                }
                
                target = boatController.AddRole(roleModel);

                // if(roleModel.role.transform.localPosition.y > target.y){
                //     middle_pos = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);
                // }
                // else{
                //     middle_pos = new Vector3(roleModel.role.transform.localPosition.x, target.y, target.z);
                // }
                middle_pos = new Vector3(target.x, roleModel.role.transform.localPosition.y, target.z);
                actionManager.MoveRole(roleModel.role, middle_pos, target, speed);
            }
        }
    }

    public void Check() {

    }

    public void JudgeResultCallBack(string result){
        this.gameObject.GetComponent<UserGUI>().gameMessage = result;
        this.isRunning = false;

    }

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

    void Update() {

    }
}

制作心得

总的来说,制作《牧师与魔鬼》这款游戏是一项充满挑战的任务,但也是一次有趣和有益的经历。通过熟练使用Unity引擎,合理设计游戏,分离动作和不断测试和改进,我成功地将这个创意变成了一个可玩的游戏。这个过程不仅提高了我的游戏开发技能,还让我明白了游戏制作的复杂性和乐趣。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值