牧师与魔鬼动作分离版
一、简介
这篇博客继承自牧师与魔鬼,在该游戏原有代码的基础上,实现动作及动作管理器与其他部分的分离。
二、代码框架结构变化
删除原来控制物体移动的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;
}
}
}