牧师与魔鬼动作分离版

    科技2026-03-09  9

    牧师与魔鬼动作分离版

    一、简介

    这篇博客继承自牧师与魔鬼,在该游戏原有代码的基础上,实现动作及动作管理器与其他部分的分离。

    二、代码框架结构变化

    删除原来控制物体移动的move和moveController,替代为以下部分 另外增加一名裁判,以及对场记FirstController作修改(修改后依然只需要将FirstController脚本挂载到空对象上即可运行游戏)。

    它们的关系如下:

    三、代码解释

    接下来对新增或修改的部分代码进行解释。

    首先是Actions(动作)部分

    1. SSAction & ISSActionCallback

    动作基类和回调函数接口

    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是动作的基类,单个动作类和组合动作类都继承自它。 ScriptableObject 是不需要绑定 GameObject 对象的可编程基类,这些对象受 Unity 引擎场景管理。 所以需要在SSAction类的定义中增加对游戏对象的绑定。

    SSAction还包含了一个回调函数的接口ISSActionCallback,该接口定义如下

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

    这里简单解释一下这个接口的作用以及为什么要使用这个接口。

    在我们的实现中有动作类和动作管理类,动作类与游戏对象绑定,控制游戏对象的移动,动作管理类则管理动作的产生和消除。

    当一个动作结束时,动作管理器需要做出反应,比如将IsMoving设为false,这表示又可以接受新的动作了。有两种方式的实现:第一种实现是在动作结束时直接将isMoving设为false,另一种方式是动作本身不修改IsMoving,而是向动作管理器传递一个动作结束的消息,再由动作管理器来设置IsMoving的值。第一个版本的牧师与魔鬼采用了第一种方法,在我们这个版本中则采用了第二种方法。

    采用消息传递的方式优点是显而易见的,一些当前的动作状态本该由动作管理器管理,而不应该交由某一个动作去控制,动作只需做好它本身的移动物体的任务即可。

    所以,每一个SSAction都有一个ISSActionCallback接口callback,SSAction的管理者实现了ISSActionCallback接口,并且将自身赋给callback,当SSAction动作完成时,调用callback的SSActionEvent函数,即相当于调用动作管理者,令动作管理者做出相应的反应。

    2. SSActionManager

    动作管理者基类 动作管理者基类并不直接作为动作管理者使用,它的存在是为调用者提供较简单和通用的接口,我们真正用到的CCActionManager继承自它。

    SSActionManager管理等待被加入的动作队列 ‘waitingAdd’ 和等待被删除的动作队列 ‘waitingDelete’ ,在每一次更新中主要做以下事情:

    将waitingAdd中的动作保存到字典中运行字典中的动作,将完成的动作加入到waitingDelete队列中

    这里可能有个问题,同时运行字典中的所有动作?这不会导致混乱吗?

    实际上在这个游戏中不会,因为动作管理器保证了队列中最多同时只有一个动作存在(如果isMoving为true即有动作正在进行,则新的动作会被丢弃)

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

    (其实这个游戏根本不需要用到这么复杂的框架,只是为了学习框架才用了起来)

    销毁waitingDelete队列中的动作实例对象 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() { } }
    3. 单个动作与组合动作

    如上所说,它们都继承自动作基类SSAction,各有各的表现。单个动作就是只有一个动作,比如船只的移动;组合动作是多个动作,比如角色的移动分为两个部分,横向移动和纵向移动。

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

    组合动作类同时还继承了上面说的回调函数接口ISSActionCallback,因为对于组合动作中的单个动作,组合动作对象就是它们的动作管理者,要为它们其中任何一个的结束做出反应(执行下一个动作,直到所有动作都执行完则再向它自己的动作管理器传递消息)。

    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() { } }
    4. 动作管理器CCActionManager

    实现了移动船只和移动角色

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

    然后是控制部分

    1. 裁判类

    裁判类将原来FirstController中的check函数抽离出来,专门做游戏状态的检测,在游戏结束时通过回调通知FirstController场记。

    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)SSDirector.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.priestCount == 3) { mainController.JudgeCallback(false, "You Win!"); return; } else { int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum; leftPriestNum = leftShoreModel.priestCount + (boatModel.isRight ? 0 : boatModel.priestCount); leftDevilNum = leftShoreModel.devilCount + (boatModel.isRight ? 0 : boatModel.devilCount); if (leftPriestNum != 0 && leftPriestNum < leftDevilNum) { mainController.JudgeCallback(false, "Game Over!"); return; } rightPriestNum = rightShoreModel.priestCount + (boatModel.isRight ? boatModel.priestCount : 0); rightDevilNum = rightShoreModel.devilCount + (boatModel.isRight ? boatModel.devilCount : 0); if (rightPriestNum != 0 && rightPriestNum < rightDevilNum) { mainController.JudgeCallback(false, "Game Over!"); return; } } } }
    2. FirstController

    场记(主控制器) 在原来的基础上将check外包给裁判类(不再调用它)。 以及实现了裁判类的回调函数,当裁判类判定游戏结束时作出处理。 在初始化时,将动作管理器和裁判都加载到游戏对象上。

    public class FirstController : MonoBehaviour, ISceneController, IUserAction { public CCActionManager actionManager; public ShoreCtrl leftShoreController, rightShoreController; public River river; public BoatCtrl boatController; public RoleCtrl[] roleControllers; //public MoveCtrl moveController; 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); } //shore leftShoreController = new ShoreCtrl(); leftShoreController.CreateShore(Position.left_shore); leftShoreController.GetShore().shore.name = "left_shore"; rightShoreController = new ShoreCtrl(); rightShoreController.CreateShore(Position.right_shore); rightShoreController.GetShore().shore.name = "right_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); //move //moveController = new MoveCtrl(); 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, 5); 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() { } void Awake() { SSDirector.GetInstance().CurrentSceneController = this; LoadResources(); this.gameObject.AddComponent<UserGUI>(); this.gameObject.AddComponent<CCActionManager>(); this.gameObject.AddComponent<JudgeController>(); } void Update() { if (isRunning) { time -= Time.deltaTime; this.gameObject.GetComponent<UserGUI>().time = (int)time; } } }

    四、项目链接

    github链接

    Processed: 0.016, SQL: 9