一.改进目标
- 为了用一组简单的动作组合成复杂的动作,我们采用 cocos2d 的方案,建立与 CCAtion 类似的类。设计思路如下:
1.设计一个抽象类作为游戏动作的基类;
2.设计一个动作管理器类管理一组游戏动作的实现类;
3.通过回调,实现动作完成时的通知。
这样的目的是让程序可以方便的定义动作并实现动作的自由组合,使得:
1.程序更能适应需求变化;
2.对象更容易被复用;
3.程序更易于维护。
- 具体对于本游戏来说,我们需要把每个需要移动的游戏对象的移动方法提取出来,建立一个动作管理器来管理不同的移动方法。例如对于Move类,当游戏对象需要移动时候,游戏对象自己调用Move类中的方法让自己移动。而动作分离版,则剥夺了游戏对象自己调用动作的能力,建立一个动作管理器,通过场景控制器把需要移动的游戏对象传递给动作管理器,让动作管理器去移动游戏对象。
- 改进后动作管理部分类图如下
二.代码分析
编程目标:设计牧师与魔鬼动作分离版,并设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
接下来是改进部分的代码分析
在原代码的基础上我添加了动作管理器,以下是具体实现。
-
动作基类SSAction类
根据UML图,SSAction类是所有动作的基类,SSAction继承了ScriptableObject,ScriptableObject 是不需要绑定 GameObject 对象的可编程基类。这些对象受 Unity 引擎场景管理。其子类有Sequence
Action和MoveToAction,根据门面模式将其功能抽象为SSActionManager。除此之外,通过protected 防止用户自己 new 对象;使用 virtual 申明虚方法,通过重写实现多态。这样继承者就明确使用 Start 和 Update 编程游戏对象行为;利用接口(ISSACtionCallback)实现消息通知,避免与动作管理者直接依赖。
public class SSAction : ScriptableObject
{
public bool enable = true;
public bool destroy = false;
public GameObject gameobject;
public Transform transform;
public ISSActionCallback callback;
protected SSAction() { }
public virtual void Start()
{
throw new System.NotImplementedException();
}
public virtual void Update()
{
throw new System.NotImplementedException();
}
}
-
简单动作实现MoveToAction
实现具体动作,将一个物体移动到目标位置,并通知任务完成。让 Unity 创建动作类,确保内存正确回收。动作完成,则期望管理程序自动回收运行对象,并发出事件通知管理者。
public class SSMoveToAction : SSAction
{
public Vector3 target;
public float speed;
private SSMoveToAction() { }
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 override void Start()
{
}
}
-
顺序动作组合类实现SequenceAction
实现一个动作组合序列,顺序播放动作让动作组合继承抽象动作,能够被进一步组合;实现回调接受,能接收被组合动作的事件创建一个动作顺序执行序列,-1 表示无限循环,start 开始动作。Update方法执行执行当前动作;SSActionEvent 收到当前动作执行完成,推下一个动作,如果完成一次循环,减次数。如完成,通知该动作的管理者;Start 执行动作前,为每个动作注入当前动作游戏对象,并将自己作为动作事件的接收者;OnDestory 如果自己被注销,应该释放自己管理的动作。
public class SequenceAction : SSAction, ISSActionCallback
{
public List<SSAction> sequence;
public int repeat = -1;
public int start = 0;
public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence)
{
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}
public override void Update()
{
if (sequence.Count == 0) return;
if (start < sequence.Count)
{
sequence[start].Update();
}
}
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);
}
}
}
public override void Start()
{
foreach (SSAction action in sequence)
{
action.gameobject = this.gameobject;
action.transform = this.transform;
action.callback = this;
action.Start();
}
}
void OnDestroy()
{
}
}
-
动作事件接口定义
接口作为接收通知对象的抽象类型。事件类型定义,使用了枚举变量;定义了事件处理接口,所有事件管理者都必须实现这个接口,来实现事件调度。所以,组合事件需要实现它,事件管理器也必须实现它。
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);
}
-
动作管理基类 SSActionManager
这是动作对象管理器的基类,实现了所有动作的基本管理。使用上述的移动方法,实现游戏对象与动作的绑定,确定回调函数消息的接收对象。管理动作之间的切换。
public class SSActionManager : MonoBehaviour, ISSActionCallback
{
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()
{
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);
DestroyObject(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();
}
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Competeted,
int intParam = 0, string strParam = null, Object objectParam = null)
{
}
}
-
场景下动作管理SceneActionManager
当前场景下的动作管理的具体实现,在场景控制类中调用它的方法,实现对当前场景的动作管理。其中需要管理两个动作,一个是场景中船的移动,这是一个单独的移动,我们只需要知道船的当前位置和需要移动到的位置就可以调用上面动作管理积累的函数实现船的移动动作了。还有一个动作是人物的移动,这是一系列的动作,需要先将人平移到船上方的位置,再将人移动到陆地/船上。这需要两个动作,也就是需要调用顺序动作的函数来实现这个动作,我们定义好这两个动作,然后在调用动作管理基类的函数就可以实现人的移动动作了。
public class MySceneActionManager : SSActionManager
{
private SSMoveToAction moveBoatToEndOrStart;
private SequenceAction moveRoleToLandorBoat;
public Controller sceneController;
protected void Start()
{
sceneController = (Controller)SSDirector.GetInstance().CurrentScenceController;
sceneController.actionManager = this;
}
public void moveBoat(GameObject boat, Vector3 target, float speed)
{
moveBoatToEndOrStart = SSMoveToAction.GetSSAction(target, speed);
this.RunAction(boat, moveBoatToEndOrStart, this);
}
public void moveRole(GameObject role, Vector3 middle_pos, Vector3 end_pos, float speed)
{
SSAction action1 = SSMoveToAction.GetSSAction(middle_pos, speed);
SSAction action2 = SSMoveToAction.GetSSAction(end_pos, speed);
moveRoleToLandorBoat = SequenceAction.GetSSAcition(1, 0, new List<SSAction> { action1, action2 });
this.RunAction(role, moveRoleToLandorBoat, this);
}
}
- 场景控制与模型控制之中的修改
首先是船移动对应的修改,在原来的版本中,我们直接在模型控制中对船进行操作,让它移动,所以船类中BoatMove函数原先直接移动船,而现在我们将动作分离,所以BoatMove函数只需要返回需要移动到的位置,然后交给动作控制器处理进行移动。
public Vector3 BoatMove()
{
if (boat_sign == -1)
{
boat_sign = 1;
return new Vector3 (25, -2.5F, 0);
}
else
{
boat_sign = -1;
return new Vector3 (-25, -2.5F, 0);
}
}
在场景控制中我们就可以调用船的BoatMove函数得知需要移动到的位置,然后再通过动作管理实现移动的动作。
public void MoveBoat()
{
if (boat.IsEmpty() || user_gui.sign != 0) return;
actionManager.moveBoat (boat.getGameObject(),boat.BoatMove(),boat.move_speed);
user_gui.sign = judge.Check();
}
在场景控制中实现人物移动大致和之前相同,只是将移动的动作交给动作管理,调用动作管理中的moveRole函数就可以执行一系列的人物移动动作。
public void MoveRole(RoleModel role)
{
if (user_gui.sign != 0) return;
if (role.IsOnBoat())
{
LandModel land;
if (boat.GetBoatSign() == -1)
land = end_land;
else
land = start_land;
boat.DeleteRoleByName(role.GetName());
actionManager.moveRole (role.getGameObject(),new Vector3(role.getGameObject().transform.position.x,land.GetEmptyPosition ().y,land.GetEmptyPosition ().z),land.GetEmptyPosition (),role.move_speed);
role.GoLand(land);
land.AddRole(role);
}
else
{
LandModel land = role.GetLandModel();
if (boat.GetEmptyNumber() == -1 || land.GetLandSign() != boat.GetBoatSign()) return;
land.DeleteRoleByName(role.GetName());
actionManager.moveRole (role.getGameObject(),new Vector3(boat.GetEmptyPosition().x,role.getGameObject().transform.position.y,boat.GetEmptyPosition().z),boat.GetEmptyPosition (),role.move_speed);
role.GoBoat(boat);
boat.AddRole(role);
}
user_gui.sign = judge.Check();
}
因为有了动作管理器,所以我们就可以将之前的move类删去。
最后是设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。
通过裁判类获取游戏状态并通知场景控制器,而场景控制器又通知UI,在UI中查看游戏状态判断是否结束游戏即可。
在场景控制器中调用judge.Check()即可实现游戏结束。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using modelcon;
public class Judge {
LandModel start_land;
LandModel end_land;
BoatModel boat;
public Judge(LandModel start_,LandModel end_,BoatModel boat_)
{
start_land = start_;
end_land = end_;
boat = boat_;
}
public int Check()
{
int start_priest = (start_land.GetRoleNum())[0];
int start_devil = (start_land.GetRoleNum())[1];
int end_priest = (end_land.GetRoleNum())[0];
int end_devil = (end_land.GetRoleNum())[1];
if (end_priest + end_devil == 6)
return 2;
int[] boat_role_num = boat.GetRoleNumber();
if (boat.GetBoatSign() == 1)
{
start_priest += boat_role_num[0];
start_devil += boat_role_num[1];
}
else
{
end_priest += boat_role_num[0];
end_devil += boat_role_num[1];
}
if (start_priest > 0 && start_priest < start_devil)
{
return 1;
}
if (end_priest > 0 && end_priest < end_devil)
{
return 1;
}
return 0;
}
}
再为游戏添加skybox,最终的成果图如下:
github地址:PriestsAndDevilsV2
游戏展示:牧师与魔鬼V2
在最后感谢师兄的博客供我参考!!!