Unity3D游戏编程-牧师与恶魔 动作分离版
文章目录
作业要求
牧师与魔鬼的动作分离版。
设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。
项目配置
Windows 10
Unity 2020.3.17f1c1
项目演示
视频演示
项目下载
下载Assets文件夹
点击此处可以前往gitee
文字说明
- 创建unity专案后,将保存的文件夹中的Assets替换成在上面项目下载的Assets文件夹
- 打开专案,然后点选Assets中的ass加载场景
- 运行即可开始游戏
- 白色长方体为牧师,红色圆球为恶魔
项目截图
因为实际游玩效果与上一次作业相近,因此不再在此放出截图,可以参考上次作业的博客:
点击此处前往
实现过程和方法(算法)
裁判类Judge
裁判类实际上是把原来FirstController中的check_game_over单独分离出来成为一个类,而check_game_over中主要需要这三个对象的属性来进行判断:
CoastController fromCoast;
CoastController toCoast;
BoatController boat;
因此在Judge类中就有这三个私有对象,以及类的构造函数:
private CoastController fromCoast;
private CoastController toCoast;
private BoatController boat;
public Judge(CoastController cfrom, CoastController cto, BoatController b){
fromCoast = cfrom;
toCoast = cto;
boat = b;
}
在FirstController中就创建Judge类对象,并且进行初始化:
judge= new Judge(fromCoast, toCoast, boat);
而check_game_over函数就与原来的没有太大差异,FirstController中就可以调用judge.check_game_over()来获取游戏状态。
遇到的问题
在编写judge这个类的时候,参考了其他类的构造而继承了MonoBehaviour,但是编译会报警告说MonoBehaviour的类不可以用new创建,于是judge类直接作为单独的类不继承。
动作分离
动作分离的目的是将运动的共性提取出来,用一个管理器统一管理,提高代码的复用性和可读性。而在此游戏项目中,就是要去掉baseCode中Moveable这个类,而选用动作管理器来实现。
为了更好的理解,从FristController开始分析。FristController中创建了FirstSceneActionManager这个类的对象,如下:
private FirstSceneActionManager actionManager;
于是观察FirstSceneActionManager:
FirstSceneActionManager
public class FirstSceneActionManager:ActionManager {
public void moveBoat(BoatController boat) {
MoveToAction action = MoveToAction.getAction(boat.getDestination(), boat.movingSpeed);
this.addAction(boat.getGameobj(), action, this);
}
public void moveCharacter(MyCharacterController characterCtrl, Vector3 destination) {
Vector3 currentPos = characterCtrl.getPos();
Vector3 middlePos = currentPos;
if (destination.y > currentPos.y) { //from low(boat) to high(coast)
middlePos.y = destination.y;
} else { //from high(coast) to low(boat)
middlePos.x = destination.x;
}
ObjAction action1 = MoveToAction.getAction(middlePos, characterCtrl.movingSpeed);
ObjAction action2 = MoveToAction.getAction(destination, characterCtrl.movingSpeed);
ObjAction seqAction = SequenceAction.getAction(1, 0, new List<ObjAction>{action1, action2});
this.addAction(characterCtrl.getGameobj(), seqAction, this);
}
}
注:moveBoat()中boat.getDestination()、boat.getGameobj()是BaseCode中BoatController的函数,前者作用返回船目前所在岸的对岸(目的地),后者是返回这个船对象本身。
moveCharacter()中characterCtrl.getPos()、characterCtrl.getGameobj()类似,是BaseCode中MyCharacterController的函数,前者返回角色当前位置,后者返回角色对象本身。
FirstController就是通过FirstSceneActionManager来管理所有动作,可以看到它里面包括了:
- 移动船只moveBoat,调用MoveToAction.getAction()对动作初始化,MoveToAction是让一个对象直线移动的动作类:
public static MoveToAction getAction(Vector3 target, float speed) {
MoveToAction action = ScriptableObject.CreateInstance<MoveToAction>();
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.whoToNotify.actionDone(this);
}
}
个人感觉比如说动作A,它的类是MoveToAction,而动作A就要用getAction来进行初始化构造(目的地,速度),实际上在界面上的移动就依赖updata中MoveTowards来完成。
初始化了动作之后,利用this.addAction()执行动作,其参数就是(执行动作的对象,执行什么动作,通知哪个类我的动作已完成)。
因为MoveToAction是一次性动作,在updata的最后会使用this.whoToNotify.actionDone(this)告知动作已完成,这个动作就会被系统回收。
- 移动角色,因为角色如果是上船会需要两段移动,所以用了两次MoveToAction作为组合动作seqAction。组合动作利用SequenceAction.getAction()进行初始化,参数为(循环的次数,从哪一个动作开始做起,动作列表),Start()是将目前所轮到的动作的参数进行赋值,然后利用this.addAction()执行目前所轮到的动作。而在最后,需要逐一为列表中的动作都执行actionDone()。
public static SequenceAction getAction(int repeat, int currentActionIndex, List<ObjAction> sequence) {
SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentActionIndex = currentActionIndex;
return action;
}
public override void Start() {
foreach(ObjAction action in sequence) {
action.gameObject = this.gameObject;
action.transform = this.transform;
action.whoToNotify = this;
action.Start();
}
}
public override void Update() {
if (sequence.Count == 0)return;
if (currentActionIndex < sequence.Count) {
sequence[currentActionIndex].Update();
}
}
public void actionDone(ObjAction source) {
source.destroy = false;
this.currentActionIndex++;
if (this.currentActionIndex >= sequence.Count) {
this.currentActionIndex = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true;
this.whoToNotify.actionDone(this);
}
}
}
FirstSceneActionManager继承了ActionManager,addAction()是在ActionManager中实现。FirstSceneActionManage的作用是更好地让FirstController简洁地进行调用ActionManager函数。
ActionManager与ObjAction
ActionManager是动作对象管理器的基类,实现了所有动作的基本管理。而ObjAction是所有动作的基类,ActionManager就是通过ObjAction这个接口来管理动作的。于是有关系ObjAction > ActionManager > FirstSceneActionManager。
ActionManager与ObjAction这两个类可以作为模板,基本固定。
ActionCallback
ActionCallback是一个接口,实现了这个接口的类,就可以知道到“某个动作已完成”(动作一完成actionDone方法就会被调用),并对这个事件做出反应。
public interface ActionCallback {
void actionDone(ObjAction source);
}
SequenceAction、ActionManager中就有actionDone()这个类的实现。
然后就可以在FirstController中调用FirstSceneActionManager的moveBoat和moveCharacter实现船只和角色移动了:
两条语句注释的是前一个作业利用moveable类的表示形式,下面是这次作业用actionManager的表示形式
//characterCtrl.moveToPosition (whichCoast.getEmptyPosition ());
actionManager.moveCharacter(characterCtrl, whichCoast.getEmptyPosition ());
//boat.Move ();
actionManager.moveBoat(boat);
并且经过对比更容易看懂这张图(动作管理器的设计):