牧师与魔鬼 动作分离版
目标
建立动作管理器,使动作抽象出来,可以应用到任何游戏对象上,以此提高代码复用性。
UML图
核心设计
将GenGameObject类物体移动的代码放在MoveToAction的类中,然后用ActionManager类对物体对象的动作进行管理,也就是说GenGameObject类的Update()不再进行物体的移动,而是在MoveToAction中执行。
举个例子,船的移动,在GenGameObject.cs的代码如下:
private FirstSceneActionManager actionManager;
public void moveBoat()
{
if (numberOnBoat == 0) return;
for(int i=0; i<2; i++)
{
if(objectOnBoat[i] != null) { objectOnBoat[i].transform.parent = boat.transform; }
}
if(boat.transform.position == leftBoatPos)
{
actionManager.moveBoat(boat, rightBoatPos, speed);
}
else if(boat.transform.position == rightBoatPos)
{
actionManager.moveBoat(boat, leftBoatPos, speed);
}
}
可以看见,我们是通过调用actionManager.moveBoat来移动船只。actionManager是FirstSceneActionManager类型的。先让我们来看看FirstSceneActionManager.cs的代码。
public class FirstSceneActionManager : ActionManager {
public void moveBoat(GameObject boat, Vector3 destination, float speed)
{
MoveToAction action = MoveToAction.getAction(destination, speed);
this.addAction(boat, action);
}
public void moveCharacter(GameObject character, Vector3 destination, float speed)
{
Vector3 middlePos = character.transform.position;
if(middlePos.y > destination.y) { middlePos.x = destination.x; }
else { middlePos.y = destination.y; }
ObjectAction action1 = MoveToAction.getAction(middlePos, speed);
ObjectAction action2 = MoveToAction.getAction(destination, speed);
ObjectAction seqAction = SequenceAction.getAction(0, new List<ObjectAction> { action1, action2 });
this.addAction(character, seqAction);
}
}
FirstSceneActionManager继承了ActionManager,前面说了,ActionManager是对物体动作的一个管理器,既然如此,为什么还要FirstSceneActionManager呢?目的就是为了让不同物体的动作可以更加具体化,例如:船的移动是通过moveBoat()控制,牧师与魔鬼的移动是moveCharacter()控制的。
现在我们已经大概了解了动作分离的操作,简而言之就是在GenGameObject中通过动作管理器执行具体的动作。接下来看看具体的实现。
针对FirstSceneActionManager的moveBoat()方法,先是声明定义了一个MoveToAction类型的action变量,看看MoveToAction的getAction()做了什么事:
//MoveToAction.cs
public static MoveToAction getAction(Vector3 position, float speed)
{
MoveToAction action = ScriptableObject.CreateInstance<MoveToAction>();
action.position = position;
action.speed = speed;
return action;
}
该函数创建了一个MoveToAction的实例,然后为该实例的position、speed属性进行赋值。这两个属性是MoveToAction继承ObjectAction而来的,而ObjectAction是所有动作的一个基类,其代码简单,不再赘述,唯一需要注意的是ObjectAction类继承了ScriptableObject,ScriptableObject是不需要绑定GameObject对象的可编程基类。
回到FirstSceneManager.cs的moveBoat(),该函数第二步是调用this.addAction(boat, action),该方法继承自ActionManager。
//ActionManager.cs
public void addAction(GameObject gameObject, ObjectAction action)
{
action.target = gameObject;
action.transform = gameObject.transform;
waitingAdd.Add(action);
action.Start();
}
该方法设置了action的target、transform属性,然后向waitingAdd中添加了action对象,action.Start()的作用稍后会提到。waitingAdd中存放的action在Update()得到执行。
//ActionManager.cs
protected void Update()
{
foreach (ObjectAction ac in waitingAdd) { actions[ac.GetInstanceID()] = ac; }
waitingAdd.Clear();
foreach(KeyValuePair<int, ObjectAction> kv in actions)
{
ObjectAction ac = kv.Value;
if (ac.destroy)
{
waitingDelete.Add(ac.GetInstanceID());
}
else
{
ac.Update();
}
}
OnActionComplete();
}
Update()先将waitingAdd中的所有action放在actions变量中,然后通过循环执行ac.Update()实现每帧的动作变化,当ac.destroy为true,意味着该动作完成(请看MoveToAction.cs的Update()),并将该动作放进waitingDelete里。Update()的最后一条语句执行OnActionComplete(),是对waitingDelete里的对象进行删除。
至此我们已经知道了船是怎么通过动作管理器来实现移动的了,剩下的SequenceAction.cs是用来组合动作的,用于移动牧师与魔鬼,因为牧师与魔鬼的移动是先水平移动,再竖直方向移动。来看看SequenceAction的Update函数:
public override void Update()
{
if (sequence.Count <= currentActionIndex)
{
this.destroy = true;
return;
}
sequence[currentActionIndex].Update();
OnActionComplete();
}
sequence中存放两个动作,通过currentActionIndex的值,控制哪个动作得到执行。currentActionIndex的初始值为0,此时sequence[currentActionIndex].Update()意味着执行第一个动作,每次Update()最后都执行OnActionComplete(),判断当前执行的动作是否已结束(通过destroy属性判断,同MoveToAction),如果已结束,就让currentActionIndex++,让下一帧开始执行下一个动作。当sequence.Count <= currentActionIndex,说明所有动作都结束了,并将destroy设为true,让管理者删除这个组合动作。最后要说说上文所说的ActionManager.cs中addAction()的最后一行代码的作用,就是为了设置组合动作中每一个动作的target、transform属性,详见SequenceAction.cs的Start()。
完整代码
https://github.com/Limsanity/Priests_and_Devils_v2
小结
加入了动作管理器之后,我们的代码层次性就更明显,更好维护了。MVC结构和动作管理器的好处真得自己敲一次才能体会出来,建议大家看多个人的博客,比较学习。