一、 项目配置
-
首先创建一个新项目,选择3D模板
-
新项目的文件结构如下:
相较于上一次作业,少了Moveable脚本,多了Judge和Action两个文件
-
Assets/Resources下存放的是项目动态加载所需的图片以及预制,预制包括按要求制作成预制的牧师、魔鬼、船、河流和河岸,图片则是用于GUI装饰
-
Assets/Materials
-
Assets/Scripts中则存放的是项目代码,各个类之间遵守MVC架构
-
-
由于图片是动态加载,故需要将在Inspector菜单中,将图片设置为可读可写状态,才能保证图片能被正常加载。
-
最后将FirstController代码拖到Main Camera中,Ctrl+B即可运行。
二、 实现过程和方法
1. 总体设计思路
-
此次作业主要有两个任务:
-
在原有的基础上新增一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束,也就是把负责判断游戏状态的职责从场景控制器中分离出去。
-
新增动作管理器类,独立管理游戏对象的动作,也就是把负责管理控制游戏对象动作的职责从场景控制器中分离出去。
-
-
这两个改进都是面向对象基于职责的思想,让场景控制器专注于本身应该实现的职责,如处理用户交互时间,加载游戏资源等,而把一些更具体专门的功能分离给更特定的对象,从而避免对象的臃肿,各个对象实现特定的功能,然后通过消息协同完成更为复杂的工作。
2. 代码分析
下面分别根据两个任务对相较于上一次作业有修改的地方进行分析。
-
新增裁判类
-
首先要先定义一个新的类——Judge,把原来在FirstController中实现SceneController接口的checkGameStatus()函数移到Judge类中,作为其一个公用成员函数,以供FirstController调用。
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Judge : MonoBehaviour{ private Land startLand;// 起始河岸 private Land endLand;// 终点河岸 private Boat boat;// 船 public Judge(Land _startLand, Land _endLand, Boat _boat){ startLand = _startLand; endLand = _endLand; boat = _boat; } public int checkGameState() { // 船在出发点 int startNumOfPriest = startLand.getNumOfPriest(); int startNumOfDevil = startLand.getNumOfDevil(); int endNumOfPriest = endLand.getNumOfPriest(); int endNumOfDevil = endLand.getNumOfDevil(); int boatNumOfPriest = boat.getNumOfPriest(); int boatNumOfDevil = boat.getNumOfDevil(); if(endNumOfPriest + endNumOfDevil + boatNumOfPriest + boatNumOfDevil == 6){ return 2; } if(boat.getCurPosition() == 1){ if((startNumOfPriest < startNumOfDevil && startNumOfPriest > 0)||(endNumOfPriest + boatNumOfPriest < endNumOfDevil + boatNumOfDevil && endNumOfPriest + boatNumOfPriest > 0)){ return 1; } } else{ if((startNumOfPriest + boatNumOfPriest < startNumOfDevil + boatNumOfDevil && startNumOfPriest + boatNumOfPriest > 0) || (endNumOfPriest < endNumOfDevil && endNumOfPriest > 0)){ return 1; } } return 0; } }
-
然后SceneController接口中便不再需要checkGameState函数
using System.Collections; using System.Collections.Generic; using UnityEngine; public interface SceneController { void loadResources (); //void checkGameState(); }
-
最后在FirstController新增一个Judge类对象,并在需要的地方调用它的成员函数checkGameState()
-
-
新增动作管理类
-
这一部分可以根据教学网站上的提示,先定义动作基类SSAction
public class SSAction : ScriptableObject{ //动作 public bool enable = true; //是否正在进行此动作 public bool destroy = false; //是否需要被销毁 public GameObject gameobject{get;set;} //动作对象 public Transform transform{get;set;} //动作对象的transform public ISSActionCallback callback{get;set;} //回调函数 protected SSAction() { } //保证SSAction不会被new public virtual void Start() //子类可以使用这两个函数 { throw new System.NotImplementedException(); } public virtual void Update() { throw new System.NotImplementedException(); } }
-
然后定义一个继承自SSAction的具体动作类,实现的是将一个物体移动到目标位置
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>();//让unity自己创建一个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.callback.SSActionEvent(this); //告诉动作管理或动作组合这个动作已完成 } } public override void Start() { //移动动作建立时候不做任何事情 } }
-
接着定义动作事件接口ISSActionCallBack,作为动作和动作管理者的接口,所有动作事件管理者都必须实现这个接口以实现事件的调度,当动作完成时,动作对象会调用这个接口,通知动作管理器对象动作已经完成,以便管理器能对下一个动作进行处理。
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); }
-
有了动作基类和动作事件接口,就可以定义一个动作组合序列类CCSequenceAction,由于组合动作实际上就是按顺序完成每一个动作,因此也需要实现ISSActionCallback接口,以便动作序列中的每一个小动作完成之后可以通知它,然后处理下一个小动作。
public class SequenceAction : SSAction, ISSActionCallback { public List<SSAction> sequence; //动作的列表 public int repeat = -1; //-1就是无限循环做组合中的动作 public int start = 0; //当前做的动作的索引 public static SequenceAction GetSSAcition(int repeat, int start, List<SSAction> sequence) { SequenceAction action = ScriptableObject.CreateInstance<SequenceAction>();//让unity自己创建一个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(); //一个组合中的一个动作执行完后会调用接口,所以这里看似没有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); //告诉组合动作的管理对象组合做完了 } } } public override void Start() { foreach (SSAction action in sequence) { action.gameobject = this.gameobject; action.transform = this.transform; action.callback = this; //组合动作的每个小的动作的回调是这个组合动作 action.Start(); } } void OnDestroy() { //如果组合动作做完第一个动作突然不要它继续做了,那么后面的具体的动作需要被释放 } }
-
定义好了动作基类,也就可以实现动作管理器了,先定义一个动作管理器基类SSActionManager,实现对SequenceAction和SSAction对象的管理,如为动作类传递游戏对象,决定动作执行的顺序,切换动作等等。
public class SSActionManager : MonoBehaviour, ISSActionCallback //action管理器 { private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>(); //将执行的动作的字典集合,int为key,SSAction为value 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; //获取动作实例的ID作为key } 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) { //牧师与魔鬼的游戏对象移动完成后就没有下一个要做的动作了,所以回调函数为空 } }
-
最后就可以定义一个继承自游戏管理器基类SSActionManager的自定义的游戏管理器用于管理本游戏的游戏对象动作,由于本游戏的游戏对象只有移动这一个动作,所以实际上就是对SSMoveToAction对象的管理。
public class MySceneActionManager : SSActionManager //本游戏管理器 { private SSMoveToAction moveBoatToEndOrStart; //移动船到结束岸,移动船到开始岸 private SequenceAction moveRoleToLandorBoat; //移动角色到陆地,移动角色到船上 public FirstController sceneController; protected new void Start() { sceneController = (FirstController)Director.getInstance().currentSceneController; sceneController.actionManager = this; } public void moveBoat(GameObject boat, Vector3 target, float speed) { moveBoatToEndOrStart = SSMoveToAction.GetSSAction(target, speed);//创建移动动作 this.RunAction(boat, moveBoatToEndOrStart, this);//执行动作 } public void moveCharacter(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);//执行动作序列 } }
-
完成好游戏动作管理器的定义,就已经实现了把动作管理的职责分配到了动作管理器,接下来在FirstController中进行修改
-
添加自定义的动作管理器并将其作为一个组件
-
修改了场景控制器中对用户动作的响应函数,对于实现游戏对象动作,不再是调用游戏对象,而是将游戏对象以及动作执行所需参数,如移动的起始、目标位置传入动作管理者对象中实现。
// 动作分离版新增,获取游戏对象到达目的地的路径上的中间位置,以便实现折线移动 public Vector3 getMiddlePosition(GameObject gameObject,Vector3 dest){ Vector3 middle = dest; if (dest.y < gameObject.transform.position.y) { // character from coast to boat middle.y = gameObject.transform.position.y; } else { // character from boat to coast middle.x = gameObject.transform.position.x; } return middle; } // 动作分离版修改,修改了原有的goButtonIsClicked public void goButtonIsClicked(){ if (!boat.isEmpty()){ actionManager.moveBoat(boat.getGameObject(),boat.boatMoveToPosition(),boat.move_speed); //动作分离版本改变 state = judge.checkGameState(); } } // 动作分离版修改,修改了原有的characterIsClicked public void characterIsClicked(Character ch){ // 角色在船上 if(ch.getIsOnBoat()){ if(boat.getCurPosition() == 0){ Vector3 end_pos = startLand.getOnLand(ch);// //ch.moveToPosition(startLand.getOnLand(ch));// 原来的 //Vector3 end_pos = land.GetEmptyPosition(); //动作分离版本改变 Vector3 middle_pos = getMiddlePosition(ch.getGameObject(),end_pos); //动作分离版本改变 actionManager.moveCharacter(ch.getGameObject(), middle_pos, end_pos, ch.move_speed); //动作分离版本改变 ch.getOnLand(startLand); } else{ Vector3 end_pos = endLand.getOnLand(ch); Vector3 middle_pos = getMiddlePosition(ch.getGameObject(),end_pos); //动作分离版本改变 actionManager.moveCharacter(ch.getGameObject(), middle_pos, end_pos, ch.move_speed); //动作分离版本改变 //ch.moveToPosition(endLand.getOnLand(ch)); ch.getOnLand(endLand); } ch.getOffBoat(); boat.removePassenger(ch); } // 角色在岸上 else{ if(!boat.isFull()){ if(ch.getIsFinished() && boat.getCurPosition() == 1){ ch.getOnBoat(boat); endLand.leaveLand(ch); Vector3 end_pos = boat.getEmptyPosition(); Vector3 middle_pos = getMiddlePosition(ch.getGameObject(),end_pos); //ch.moveToPosition(boat.getEmptyPosition()); actionManager.moveCharacter(ch.getGameObject(), middle_pos, end_pos, ch.move_speed); //动作分离版本改变 boat.addPassenger(ch); } if(!ch.getIsFinished() && boat.getCurPosition() == 0){ ch.getOnBoat(boat); startLand.leaveLand(ch); Vector3 end_pos = boat.getEmptyPosition(); Vector3 middle_pos = getMiddlePosition(ch.getGameObject(),end_pos); actionManager.moveCharacter(ch.getGameObject(), middle_pos, end_pos, ch.move_speed); //动作分离版本改变 //ch.moveToPosition(boat.getEmptyPosition()); boat.addPassenger(ch); } } } }
-
-
三、总结
本次作业依据面向对象基于职责的思想,在上一次作业的基础上对代码结构进行了修改,将游戏状态的判断和动作管理的功能从场景控制器中分离出来,使得各个对象的职责分工更加明确。
对于课程网站上所提供的动作管理器的实现,可以视作一个框架,通过这一框架,我认为可以更加有效地组织管理复杂的动作,也提高了动作实现的代码的可重用性,尽管在本游戏中,游戏对象只有移动这一简单的动作或由两个移动组合的移动动作序列,用这一框架似乎有些“大材小用”,但我认为当游戏涉及的动作多且复杂时,就体现了它的优势——可以通过动作管理器统一的对游戏对象进行管理,而不用像之前一样,为每个需要动作的游戏对象添加特定动作脚本作为组件。
至于裁判类的添加,正是基于职责思想的体现,各个不同的对象有特定的职责,实现特定的功能,这样子不仅设计的时候简单直观,在实现的时候也可以相对独立地实现各个模块,最终整个游戏的实现就是靠各个对象之间的通信合作,各司其职
四、效果展示
(由于此次作业只是修改代码的实现,并没有对最终游戏效果产生影响,因此仍然使用上一次作业的效果展示视频)