Unity牧师与魔鬼小游戏(动作分离版)
前言
这是中大计算机学院3D游戏编程课的一次作业,在这里分享一下设计思路。 主要代码上传到了gitee上,请按照后文的操作运行。 项目地址:https://gitee.com/cuizx19308024/unity-games/tree/master/hw3 上一次的项目地址:https://gitee.com/cuizx19308024/unity-games/tree/master/hw2 成果视频:https://www.bilibili.com/video/BV16g411F7yF?spm_id_from=333.999.0.0 上一篇的博客地址:https://blog.csdn.net/kiriyama23333/article/details/120625461?spm=1001.2014.3001.5501
改动说明
在上一个项目中,我们把所有的动作、裁判和游戏控制全部交给了Controller来完成。但实际上,当游戏规模较大时,这种架构会变得难以维护。因此,本次的改动是将动作单独分离出来实现,将这些动作通过门面模式交给动作控制管理器来管理,外界可以通过调用动作管理器中的函数来控制动作的发生,将管理和实现分离。此外,还增加了一个裁判类,用于作游戏结束的判定。
新增ActionController
回调函数接口
这里采用接口来表示,每一个动作完成后,调用者会进行的操作,这一操作被放置在了回调函数里。
public enum SSActionEventType : int { Started, Competeted }
//接口
public interface ISSActionCallback {
void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted, int intParam = 0, string strParam = null, Object objectParam = null);
}
动作基类
这里通过基类和虚函数来规定子类需要实现的操作,通过“可进行”和“可删除”两个变量来控制动作。
//动作基类
public class SSAction : ScriptableObject {
public bool enable = true; //是否可进行
public bool destroy = false; //是否已完成
public GameObject gameobject; //动作对象
public Transform transform; //动作对象的transform
public ISSActionCallback callback; //回调函数
/*防止用户自己new对象*/
protected SSAction() { }
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
单个动作
注意单个动作的以下两个操作:
//这里利用Unity自带的构造函数创建对象,并初始化
public static SSMoveToAction GetSSAction(Vector3 _target, float _speed) {
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
action.target = _target;
action.speed = _speed;
return action;
}
public override void Update() {
this.transform.position = Vector3.MoveTowards(this.transform.position, target, speed * Time.deltaTime);
//动作完成,通知动作管理者或动作组合
if (this.transform.position == target) {
this.destroy = true;
this.callback.SSActionEvent(this);
}
}
组合动作
组合动作包括:
public List<SSAction> sequence; //动作的列表
public int repeat = -1; //-1就是无限循环
public int start = 0; //当前做的动作的标号
注意以下几个函数:
//让序列中的每一个动作都执行
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.Competeted, int intParam = 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); //通知管理者
}
}
}
动作管理器基类
动作管理器是一个门面,它可以自己实现对于动作的控制,并可以提供接口给外部函数调用。 尤其注意这里的等待添加和等待删除的队列,由于每次update的过程中不可能立刻对新增动作和已执行的动作进行处理,因此需要一个队列来进行缓冲。被删除的动作对象也由Unity的Destroy进行操作。 对于门面模式,这里提供了函数RunAction() ,外界仅需指定游戏对象、动作和回调处理器即可由这个类完成这些动作的实现。
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>(); //等待删除的key列表
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);
Object.Destroy(ac);//让Unity帮着删除
}
waitingDelete.Clear();
}
//外界只需要调用动作管理类的RunAction函数即可完成动作。
public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager) {
action.gameobject = gameobject;
action.transform = gameobject.transform;
action.callback = manager;
waitingAdd.Add(action);
action.Start();
}
}
这次游戏的动作管理器
注意,这里对移动船和移动对象又进行了封装,现在用户只需提供对象、终点和速度即可。
public void moveBoat(GameObject boat, Vector3 end, float speed) {
boatMove = SSMoveToAction.GetSSAction(end, speed);
this.RunAction(boat, boatMove, this);
}
public void moveRole(GameObject role, Vector3 middle, Vector3 end, float speed) {
//两段移动
SSAction action1 = SSMoveToAction.GetSSAction(middle, speed);
SSAction action2 = SSMoveToAction.GetSSAction(end, speed);
//两个动作,都只重复一次
roleMove = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });
this.RunAction(role, roleMove, this);
}
新增GameCheck
只需将原有的判断函数移动到类中即可,作一些变量的修改:
public class GameCheck : MonoBehaviour {
public Controller sceneController;
protected void Start() {
sceneController = (Controller)SSDirector.GetInstance().CurrentScenceController;
}
public int GameJudge()
{
int src_priest = sceneController.src_land.GetTotal(0);
int src_ghost = sceneController.src_land.GetTotal(1);
int des_priest = sceneController.des_land.GetTotal(0);
int des_ghost = sceneController.des_land.GetTotal(1);
if (des_priest + des_ghost == 6)
{ //全部到终点,获胜
return 1;
}
if (sceneController.boat.GetBoatMark() == 1)//由于在这一边船还没开,因此只需检测另一边的数量即可。
{
if (des_priest < des_ghost && des_priest > 0)
{//失败
return -1;
}
}
else
{
if (src_priest < src_ghost && src_priest > 0)
{//失败
return -1;
}
}
return 0;//游戏继续进行
}
}
修改原有代码
- 删除了
Movable 类,因为已经实现了控制器,不需要将其设计为属性。 - 替换了
BoatModel 和RoleModel 类中的Movable相关操作,全部改为用GhostBoatActionManager 类的对象进行操作。Model只需指定地点和速度,调用这个类中的相关函数进行移动即可。 - 将原有的
GameJudge() 操作替换为checker.GameJudge() ,删除类中原有的GameJudge() 函数。此外,还需要修改接口IUserAction ,因为判断游戏结束已经交给裁判决定了。 Controller 类初始化时新增了对GhostBoatActionManager 对象和GameCheck 对象的初始化,和UserGUI 一样,都是作为一个属性被添加到这个游戏对象上面,相当于给这个游戏对象附上了那些脚本。
实验结果
与上次的实验结果相同,参见展示视频。
|