游戏对象与图形基础
这是有游戏编程的第四次作业,对MVC深入
说明文档
本次实验完成了所有基本要求,尽量将步骤展示出。
闪光点:
新增类图以及详细的代码注释来解析动作分离
对游戏改造时出现的问题进行分析和解决
作业内容
1.
基本操作演练【建议做】
2.
编程实践
- 牧师与魔鬼 动作分离版
- 【2019开始的新要求】:设计一个裁判类,当游戏达到结束条件时,通知场景控制器游戏结束
需求分析
这一次的作业建立在上一次作业的基础上,在这次的设计里面,我们需要把动作的管理抽取为一个裁判的角色,进行进一步的角色专职管理。
首先看看上一次作业的UML
类图,我们可以看到:
MoveController
控制着游戏对象的移动,但是运动不论是基本还是组合动作都耦合在一起,同时,裁判的判断职责以及具体控制对象的移动的逻辑都在FirstController
中,这将导致FirstController
的职责不再单一,我们考虑使用恰当的接口设计来使得这些角色进一步分离。
基于以上判断,动作分离版的新特性可列举如下:
- 使用专门的对象来管理运动,包括基本动作、组合动作。并且,不需要再像旧版一样每个对象都加上
Move
组件。
- 游戏中新增裁判类,当游戏达到结束条件时,通知场景控制器游戏结束。在旧版中,
FirstController
使用Check
函数判断游戏是否结束。现在将游戏的胜利与失败交给裁判管理,场景控制器新增处理裁判的反馈的回调函数即可,进一步解耦合职责。
新版的设计与实现
新版的设计思想里借鉴了老师课件,我觉得总结的很好,我摘抄并加上自己的理解展示如下:
- 通过门面模式(控制器模式)来输出几个预先设计好的动作,供场景控制器使用
- 好处:动作的具体组合逻辑变成动作模块的内部事务,不需要场景控制器操心具体运动
- 在新版设计中,这个门面也就是
CCActionManager
。
- 通过组合模式实现动作组合,按照组合模式的设计方法
- 必须要有一个接口表示事物的共性和行为,例如新版设计中的
SSAction
,表示动作,它可以表示基本动作也可以表示组合基本动作。
- 基本动作:用户设计的基本动作类,如
CCMoveToAction
。
- 组合动作:由(基本或组合)动作组合的类,如
CCSequenceAction
。
- 接口回调(函数回调)实现管理者与被管理者解耦
- 如组合对象实现一个事件抽象接口(
ISSActionCallback
),作为监听器监听子动作的事件。
- 被组合对象使用监听器传递消息给管理者。至于管理者如何控制子动作顺序或者逻辑就是实现这个监听器接口的负责人说了算,但”别人敢实现,我就敢用“。
- 前两点是很重要的,子动作序列操控着物体移动,而顺序便是最开始定义的,然而每一次移动之间的衔接如何进行呢?其实就是通过接口回调通知组合动作对象说:嘿,我这个子动作完成了,你可以接着下一个子动作了。
- 通过模板方法,让使用者减少对动作管理过程细节的要求
-
SSActionManager
作为CCActionManager
基类。
最终,我们将可以很方便的定义动作并实现动作的自用组合,程序更易于适应需求变化和易于维护,对象更容易被复用。
旧版的代码结构图以及类图可参考我上一篇作业。
上面就是动作管理者的实现。除此之外,我们还设计一个裁判类,裁判类其实是一个控制器:
public class JudgeController : MonoBehaviour {
public FirstController mainController;
public LandModel leftLandModel;
public LandModel rightLandModel;
public BoatModel boatModel;
// Start is called before the first frame update
void Start() {
mainController = (FirstController)SSDirector.GetInstance().CurrentSenceController;
this.leftLandModel = mainController.leftLandRoleController.GetLandModel();
this.rightLandModel = mainController.rightLandRoleController.GetLandModel();
this.boatModel = mainController.boatModelController.GetBoatModel();
}
// Update is called once per frame
void Update() {
if (!mainController.isRunning) {
return;
}
// 如果时间消耗尽
if (mainController.time <= 0) {
mainController.JudgeCallback(false, "Game Over!");
}
// 否则游戏还在进行
this.gameObject.GetComponent<UserGUI>().gameMessage = "";
// 判断是否已经胜利
if (leftLandModel.priestNum == 3) {
mainController.JudgeCallback(false, "You Win!");
return;
} else {
int leftPriestNum, leftDevilNum, rightPriestNum, rightDevilNum;
leftPriestNum = leftLandModel.priestNum + (boatModel.onRight ? 0 : boatModel.priestNum);
leftDevilNum = leftLandModel.devilNum + (boatModel.onRight ? 0 : boatModel.devilNum);
if (leftPriestNum != 0 && leftDevilNum > leftPriestNum) {
mainController.JudgeCallback(false, "Game Over!");
return;
}
rightPriestNum = rightLandModel.priestNum + (boatModel.onRight ? boatModel.priestNum : 0);
rightDevilNum = rightLandModel.devilNum + (boatModel.onRight ? boatModel.devilNum : 0);
if (rightPriestNum != 0 && rightDevilNum > rightPriestNum) {
mainController.JudgeCallback(false, "Game Over!");
return;
}
}
}
}
那么对应的,我们的场景控制器FirstController
也要进一步修改,来接入裁判和动作管理,代码有点长,我类似diff
一个列出变化,突出这一次的新设计的接入。
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
// 新增动作管理类
public CCActionManager actionManager;
// 不在需要
// public MoveController moveController;
// 新增函数,用于让裁判通知游戏状态以及消息
public void JudgeCallback(bool isRunning, string msg) {
this.gameObject.GetComponent<UserGUI>().gameMessage = msg;
this.gameObject.GetComponent<UserGUI>().time = (int)time;
this.isRunning = isRunning;
}
public void LoadResources() {
// 移动控制器实例化已经不需要
// moveController = new MoveController();
}
public void MoveBoat() {
if ((!isRunning) || actionManager.IsMoving()) {
return;
}
Vector3 target = boatModelController.GetBoatModel().onRight ? PositionModel.left_boat : PositionModel.right_boat;
// 直接交给动作管理进行
actionManager.MoveBoat(boatModelController.GetBoatModel().boat, target, 5);
// 方向取反
boatModelController.GetBoatModel().onRight = !boatModelController.GetBoatModel().onRight;
}
public void MoveRole(RoleModel roleModel) {
if ((!isRunning) || actionManager.IsMoving()) {
return;
}
Vector3 destination, mid_destination;
if (roleModel.onBoat) {
// 在船上
if (boatModelController.GetBoatModel().onRight) {
destination = rightLandRoleController.AddRole(roleModel);
} else {
destination = leftLandRoleController.AddRole(roleModel);
}
if (roleModel.role.transform.localPosition.y > destination.y) {
mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
} else {
mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
}
// 直接交给动作管理进行
actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
// 角色的方向不能每次取反,因为可以在同一边重复上下船
roleModel.onRight = boatModelController.GetBoatModel().onRight;
boatModelController.RemoveRole(roleModel);
} else {
// 在陆地并且船未满
if (boatModelController.GetBoatModel().onRight == roleModel.onRight && boatModelController.GetBoatModel().devilNum + boatModelController.GetBoatModel().priestNum < 2) {
if (roleModel.onRight) {
rightLandRoleController.RemoveRole(roleModel);
} else {
leftLandRoleController.RemoveRole(roleModel);
}
destination = boatModelController.AddRole(roleModel);
if (roleModel.role.transform.localPosition.y > destination.y) {
mid_destination = new Vector3(destination.x, roleModel.role.transform.localPosition.y, destination.z);
} else {
mid_destination = new Vector3(roleModel.role.transform.localPosition.x, destination.y, destination.z);
}
// 直接交给动作管理进行
actionManager.MoveRole(roleModel.role, mid_destination, destination, 5);
}
}
}
public void Restart() {
// 必须实例化一个新的裁判,否则使用的是历史数据
Destroy(this.gameObject.GetComponent<JudgeController>());
this.gameObject.AddComponent<JudgeController>();
}
private void Awake() {
SSDirector.GetInstance().CurrentSenceController = this;
LoadResources();
this.gameObject.AddComponent<UserGUI>();
this.gameObject.AddComponent<CCActionManager>();
this.gameObject.AddComponent<JudgeController>();
}
private void Update() {
if (isRunning) {
time -= Time.deltaTime;
this.gameObject.GetComponent<UserGUI>().time = (int)time;
// 不再具体管控判断和运动逻辑
}
}
}
其他改动: