3D游戏编程与设计 HW 4.5 牧师与恶魔(动作分离版)
完整游戏过程可见以下视频:
https://www.bilibili.com/video/BV1gv411y7xz/
完整代码可见以下仓库:
https://gitee.com/beilineili/game3-d
1.作业要求
2.游戏制作
① 设计思路
- 在上一版的牧师与恶魔中,场记管的事情太多,不仅要处理用户交互事件,还要进行游戏对象加载、游戏规则实现、运动实现等工作,显得非常臃肿。对这个问题,一个最直观的想法就是让更多的人(角色)来管理不同方面的工作。显然,这就是面向对象基于职责的思考。
- 例如:就和踢足球一样,自己踢5人场,一个裁判就够了,但如果是国际比赛,就需要主裁判、边裁判、电子裁判等角色通过消息协同来完成更为复杂的工作。
- 在之前的牧师与恶魔的游戏制作中,我们使用FirstController来控制游戏中人物的动作。
- 本次作业,我们对上一版的牧师与恶魔更新,将动作从FirstController中分离出来。为了用一组简单的动作组合成复杂的动作,我们采用 cocos2d 的方案,建立与 CCAtion 类似的类。先上设计图:
设计思路如下:
- 通过门面模式(控制器模式)输出组合好的几个动作,供原来程序调用。
- 通过组合模式实现将基本动作组合
- 接口回调(函数回调)实现管理者与被管理者解耦
- 通过模板方法,让使用者减少对动作管理过程细节的要求
② 设计代码
一、ActionController.cs
1)动作基类 SSAction
- 需要用户实现方法 Start 和 Update ,分别用于动作初始化和实现动作逻辑
// 所有动作的基类
public class SSAction : ScriptableObject { //不需要绑定GameObject对象的可编程基类
public bool enable = true; //是否进行
public bool destroy = false; //是否删除
//需要进行运动的游戏对象
public GameObject GameObject { get; set; }
public Transform Transform { get; set; }
//动作执行完后要通知的对象
public ISSActionCallback Callback { get; set; }
// 申明虚方法,通过重写实现多态,由继承者来明确行为
public virtual void Start() {
throw new System.NotImplementedException();
}
public virtual void Update() {
throw new System.NotImplementedException();
}
}
2)基础动作 移动子类 SSMoveToAction (牧师与恶魔游戏只需设计直线运动)
//实现移动的基本动作
public class SSMoveToAction : SSAction {
//目的地
public Vector3 target;
//速度
public float speed;
private SSMoveToAction() { }
public static SSMoveToAction GetSSMoveToAction(Vector3 goal, float speed) {
SSMoveToAction action = CreateInstance<SSMoveToAction>();
action.target = goal;
action.speed = speed;
return action;
}
//声明重写父类虚函数,不需要任何初始化操作
public override void Start() { }
//游戏对象以速度speed向target直线运动
public override void Update() {
Transform.position = Vector3.MoveTowards(Transform.position, target, speed * Time.deltaTime);
if (Transform.position == target) {
destroy = true;
//动作完成时通过callback告诉动作管理者
Callback.ActionDone(this);
}
}
}
3)组合动作 SequenceAction
- 牧师与恶魔里的动作,即上下船/岸动作,可以抽象为一个直角折线运动。这时,就需要两步直线运动,所以要实现组合动作。
- 组合动作类实现一个动作组合序列,顺序播放动作
public class SequenceAction: SSAction, ISSActionCallback {
//存储多个顺序执行的动作
public List<SSAction> sequence;
//动作执行次数,为负数则要重复执行
public int repeat = -1;
//当前执行的动作
public int currentActionIndex = 0;
//创建一个动作顺序执行序列
public static SequenceAction GetSequenceAction(int repeat, int currentActionIndex, List<SSAction> sequence) {
SequenceAction action = CreateInstance<SequenceAction>();
action.sequence = sequence;
action.repeat = repeat;
action.currentActionIndex = currentActionIndex;
return action;
}
//执行当前动作
public override void Update() {
if (sequence.Count == 0) return;
if (currentActionIndex < sequence.Count) {
sequence[currentActionIndex].Update();
}
}
public void ActionDone(SSAction source) {
source.destroy = false;
currentActionIndex++; //下一个动作
if (currentActionIndex >= sequence.Count) { //如果已经完成一次循环
currentActionIndex = 0;
if (repeat > 0) repeat--;
if (repeat == 0) { //如果已经完成,通知动作管理者
destroy = true;
Callback.ActionDone(this);
}
}
}
public override void Start() {
//为每个动作注入当前游戏对象,将自己作为动作事件的接收者
foreach(SSAction action in sequence) {
action.GameObject = GameObject;
action.Transform = Transform;
action.Callback = this;
action.Start();
}
}
//如果被注销,应该释放自己管理的动作
void OnDestroy() {
foreach(SSAction action in sequence) {
DestroyObject(action);
}
}
}
4)动作管理 ActionManager
- 管理动作的执行,它来调度调配所有的动作的执行,决定游戏对象做某个动作或是一连串动作
//动作对象管理器的基类
//继承ISSActionCallback获取完成动作时的反馈信息
public class ActionManager : MonoBehaviour, ISSActionCallback {
//动作字典
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
//等待执行的动作列表
private List<SSAction> waitingAdd = new List<SSAction>();
//等待删除动作的key的列表
private List<int> waitingDelete = new List<int>();
protected void Update() {
foreach(SSAction action in waitingAdd) {
actions[action.GetInstanceID()] = action;
}
waitingAdd.Clear();
//执行每一个动作
foreach(KeyValuePair<int, SSAction> kv in actions) {
SSAction action = kv.Value;
if (action.destroy) {
waitingDelete.Add(action.GetInstanceID());
} else if (action.enable) {
action.Update();
}
}
//删除已完成动作
foreach(int key in waitingDelete) {
SSAction action = actions[key];
actions.Remove(key);
DestroyObject(action);
}
waitingDelete.Clear();
}
//添加动作
public void AddAction(GameObject gameObject, SSAction action, ISSActionCallback callback) {
action.GameObject = gameObject;
action.Transform = gameObject.transform;
action.Callback = callback;
waitingAdd.Add(action);
action.Start();
}
public void ActionDone(SSAction source) { }
}
二、FirstSceneController.cs
- 增加一个场记动作管理者类,避免场记代码冗余,它含有控制船只和人物运动的方法
- 移动船:一个动作,从当前位置以speed的速率移动到destination
- 移动角色:两个动作,从当前位置以speed的速率移动到middlePos,再以speed的速率移动到destination
public class FirstSceneActionManager : ActionManager {
public void MoveBoat(BoatController boatController) {
SSMoveToAction action = SSMoveToAction.GetSSMoveToAction(boatController.GetDestination(), boatController.boat.movingSpeed);
AddAction(boatController.boat._Boat, action, this);
}
public void MoveCharacter(MyNamespace.CharacterController characterCtrl, Vector3 destination) {
Vector3 currentPos = characterCtrl.character.Role.transform.position;
Vector3 middlePos = currentPos;
if (destination.y > currentPos.y) middlePos.y = destination.y;
else {
middlePos.x = destination.x;
}
//两个动作,两段移动
SSAction action1 = SSMoveToAction.GetSSMoveToAction(middlePos, characterCtrl.character.movingSpeed);
SSAction action2 = SSMoveToAction.GetSSMoveToAction(destination, characterCtrl.character.movingSpeed);
//动作队列完成上船或上岸动作
SSAction seqAction = SequenceAction.GetSequenceAction(1, 0, new List<SSAction> { action1, action2 });
AddAction(characterCtrl.character.Role, seqAction, this);
}
}
三、Check.cs(裁判类)
- 增加了一个裁判类,用来判定游戏是否结束。一旦游戏结束,UserGUI即会得到信息来打印出胜或败的游戏结果
- 将之前在 FirstController.cs 里的 Check 函数删去,在 FirstController 的 Awake 函数对裁判类实例进行初始化。
public class Check : MonoBehaviour {
public FirstController sceneController;
protected void Start() {
sceneController = (FirstController)Director.GetInstance().CurrentSecnController;
sceneController.gameStatusManager = this;
}
public int CheckGame() {
//0-游戏继续,1-失败,2-成功
int rightPriests = (sceneController.rightCoastCtrl.GetCharacterNum())[0];
int rightDevils = (sceneController.rightCoastCtrl.GetCharacterNum())[1];
int leftPriests = (sceneController.leftCoastCtrl.GetCharacterNum())[0];
int leftDevils = (sceneController.leftCoastCtrl.GetCharacterNum())[1];
//所有角色都过河了
if (leftPriests + leftDevils == 6) return 2;
if (sceneController.boatCtrl.boat.Location == Location.right) {
rightPriests += sceneController.boatCtrl.GetCharacterNum()[0];
rightDevils += sceneController.boatCtrl.GetCharacterNum()[1];
} else {
leftPriests += sceneController.boatCtrl.GetCharacterNum()[0];
leftDevils += sceneController.boatCtrl.GetCharacterNum()[1];
}
// Lose,有一边恶魔数量多过牧师
if ((rightPriests < rightDevils && rightPriests > 0) ||
(leftPriests < leftDevils && leftPriests > 0)) {
return 1;
}
return 0; //游戏还没结束
}
}
③ 修改已有代码
一、Moveable.cs
- 将Moveable.cs删除,有动作管理类就不再需要移动控制器了。
二、Interface.cs
1)增加 动作事件回调接口 ISSActionCallback
- ActionDone 用于通知更高级的对象动作已执行完毕
public interface ISSActionCallback {
void ActionDone(SSAction source);
}
三、Boat.cs 和 Character.cs
- 将 Model 里的 Boat 和 Character 中与运动相关的方法删除,并添加speed
public class Boat {
public readonly Vector3 departure;
public readonly Vector3 destination;
public readonly Vector3[] departures;
public readonly Vector3[] destinations;
public readonly float movingSpeed = 20;
public CharacterController[] passenger = new CharacterController[2];
public GameObject boat_ { get; set; }
public Location Location { get; set; }
public Boat() {
// 船的起点和终点位置的坐标
departure = new Vector3(5, 1, 0);
destination = new Vector3(-5, 1, 0);
Location = Location.right;
// 船上空位置的坐标
departures = new Vector3[] {new Vector3(4.5f, 1.5f, 0), new Vector3(5.5f, 1.5f, 0) };
destinations = new Vector3[] {new Vector3(-5.5f, 1.5f, 0),new Vector3(-4.5f, 1.5f, 0) };
// 用预制初始化船
boat_ = Object.Instantiate(Resources.Load("Prefab/Boat", typeof(GameObject)),departure, Quaternion.identity, null) as GameObject;
boat_.name = "boat";
boat_.AddComponent(typeof(UserGUI));
}
}
四、FirstController.cs
- 加入动作管理类 ActionManager
private FirstSceneActionManager actionManager;
void Start() {
actionManager = GetComponent<FirstSceneActionManager>();
}
- 修改MoveBoat 和 CharacterClicked,使用动作管理器 ActionManager 来实现
public void MoveBoat() {
if (boatCtrl.IsEmpty()) return;
//修改船的移动动作和过程处理
actionManager.MoveBoat(boatCtrl);
boatCtrl.SetPos();
UserGUI.status = CheckGameOver();
}
五、GameController
- 删除和移动动作相关的代码,改成直接设置 pos
3.游戏截图
- 从 Asset Store 中下载并添加了天空盒,以及流动的水元素
资源:流动的水 - 游戏开始界面
- 游戏结束界面