目录
一、前言
这篇博客是上一篇博客的进阶版,上一个博客链接: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场记结束,实现游戏结束判定从场景控制器中分离。
三、结束
至此修改后的项目介绍完成,运行的方法和上一版一样,运行效果也没变,因此不再做视频演示。