牧师与魔鬼 动作分离版
上一次作业中游戏对象牧师、魔鬼、船体都包含有MoveBehavior,由它们对应的控制器负责控制自身的移动。在这一次的作业中我实现了一个动作管理器来管理这些游戏对象的移动,通过场记(SceneController)来调用动作管理器,这样将动作与游戏对象解耦,也实现了更好的代码复用,尤其是当多个游戏对象有相同的动作时。这样场记就负责管理游戏对象的状态、协调游戏对象间的通信而无需管理游戏对象的动作执行。
动作管理部分的UML图
动作虚基类
这是所有动作类的基类,protected声明构造函数从而避免用户new出一个基类对象,使用Start和Update虚方法,继承者通过重写实现多态,内部有gameObject和transform属性,用来绑定执行该运动的游戏对象。enabled用来标记动作是否能够执行,destroy用来表示动作执行结束应该销毁。
public class SSAction: ScriptableObject
{
public bool enabled = true;
public bool destory = false;
public GameObject gameObject { get; set; }
public Transform transform { get; set; }
public ISSActionCallBack callback { get; set; }
protected SSAction() { }
public virtual void Start() { throw new System.NotImplementedException(); }
public virtual void Update() { throw new System.NotImplementedException(); }
}
动作事件基本类型及回调接口
定义动作事件执行完毕的回调接口,所有事件管理者都应该实现这个接口,包括组合事件和事件管理器。这里由于不需要复杂的回调,我把接口简化了,只需两个参数即可。SSActionEventType是一个枚举类型,用来标记调用回调函数的动作执行的状态。
//动作事件基本类型
public enum SSActionEventType: int { Started, Completed };
//动作事件回调接口
public interface ISSActionCallBack
{
void SSActionEvent(SSAction sourse, SSActionEventType events = SSActionEventType.Completed);
}
动作管理基类
动作管理基类里面右两个List,一个存等待添加的动作对象,一个存待删除的动作对象的id,在每次Update函数执行的时候会检查waitingAdd List里面是否有待添加对象,有的话就加进字典;同时检查待删除对象,然后执行对应的删除操作。这样当动作实例运行完,destroy变为true的时候,就会被加进待删除列表,随后便被移出字典。动作管理基类还提供了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>();
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.destory)
{
waitingDelete.Add(ac.GetInstanceID());
}else if(ac.enabled){
ac.Update();
}
}
foreach (int key in waitingDelete)
{
SSAction ac = actions[key];
actions.Remove(key);
DestroyObject(ac);
}
waitingDelete.Clear();
}
protected void Start() { }
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallBack manager)
{
action.gameObject = gameObject;
action.callback = manager;
action.transform = gameObject.transform;
waitingAdd.Add(action);
action.Start();
}
}
具体动作管理类
这个类主要用于与场景控制器交互,可以在场景控制器的start函数里用GetComponent将它作为场控的一员,主要负责接收场控的命令,可以暴露出一些接口,用于场控调用以执行所需的动作。如下面代码中我暴露了moveBoat和moveCharacter接口,这样场控便可以轻易“命令”动作管理者要执行什么动作。
public class CCActionManager: SSActionManager, ISSActionCallBack
{
public FirstController sceneController;
public void moveBoat(GameObject boat, int to_or_from)
{
if (to_or_from == -1)
{
Debug.Log("船应该向右移");
CCBoatMove boatMoveToRight = CCBoatMove.GetSSAction(new Vector3(5, 1, 0));
this.RunAction(boat, boatMoveToRight, this);
}
else
{
Debug.Log("船应该向左移");
CCBoatMove boatMoveToLeft = CCBoatMove.GetSSAction(new Vector3(-5, 1, 0));
this.RunAction(boat, boatMoveToLeft, this);
}
}
public void moveCharacter(GameObject obj, Vector3 dest) {
CCCharacterMove characterMove = CCCharacterMove.GetSSAction(dest);
this.RunAction(obj, characterMove, this);
}
protected new void Start()
{
sceneController = (FirstController)Director.getInstance().currentSceneController;
sceneController.actionManager = this;
}
protected new void Update()
{
base.Update();
}
public void SSActionEvent(SSAction sourse, SSActionEventType events = SSActionEventType.Completed)
{
Debug.Log("Completed!");
}
}
船体移动动作
public class CCBoatMove : SSAction
{
public Vector3 target;
public float speed = 20;
public static CCBoatMove GetSSAction(Vector3 target)
{
CCBoatMove action = ScriptableObject.CreateInstance<CCBoatMove>();
action.target = target;
return action;
}
public override void Start()
{
Debug.Log("开始移动");
this.transform.position = Vector3.MoveTowards(this.transform.position, this.target, this.speed * Time.deltaTime);
}
public override void Update() {
if (this.transform.position == target)
{
Debug.Log("移动结束");
this.destory = true;
this.callback.SSActionEvent(this);
}
else
{
this.transform.position = Vector3.MoveTowards(this.transform.position, this.target, this.speed * Time.deltaTime);
}
}
}
角色移动动作
public class CCCharacterMove : SSAction
{
private Vector3 target;
private Vector3 mid;
int status = 0; // 0 for not moving; 1 for moving to middle; 2 for moving to dest
public float speed = 20;
public static CCCharacterMove GetSSAction(Vector3 target)
{
CCCharacterMove action = ScriptableObject.CreateInstance<CCCharacterMove>();
action.target = target;
action.mid = target;
return action;
}
public override void Start()
{
Debug.Log("开始移动");
if (target.y == transform.position.y)
{
status = 2;
}
else if (target.y < transform.position.y)
{
mid.y = transform.position.y;
}
else
{
mid.x = transform.position.x;
}
status = 1;
}
public override void Update()
{
if (status == 1)
{
transform.position = Vector3.MoveTowards(transform.position, mid, speed * Time.deltaTime);
if (transform.position == mid)
{
status = 2;
}
}
else if (status == 2)
{
transform.position = Vector3.MoveTowards(transform.position, target, speed * Time.deltaTime);
if (transform.position == target)
{
status = 0;
Debug.Log("移动结束");
this.destory = true;
this.callback.SSActionEvent(this);
}
}
}
}
由此,当场记判断需要移动船只的时候,只需执行:
actionManager.moveBoat(boat.getGameObject(), boat.getToOrFrom());
需要移动角色的时候,只需:
actionManager.moveCharacter(characterController.getGameObject(), boat.getEmptyPosition());
完整代码见传送门