MVC架构概述
在游戏设计中,MVC(Model-View-Controller)结构也可以用来组织代码和逻辑,尤其是对于游戏的用户界面(UI)和游戏逻辑部分,通常称为MVC游戏设计模式。以下是MVC游戏设计模式的主要组成部分和如何运作的概述:
1. Model(模型):游戏中的模型代表了游戏的数据和业务逻辑。这包括玩家的角色、敌人、游戏规则、关卡数据等。模型管理和存储这些数据,并提供用于访问和操作数据的方法。模型通常是游戏的核心,但与用户界面和输入无关。
2. View(视图):游戏中的视图包括所有可见的元素,如游戏画面、UI元素、分数板等。视图负责将模型的数据可视化呈现给玩家。视图还可以接收玩家的输入(例如点击、触摸、键盘输入)并将其传递给控制器。在游戏中,视图通常由游戏场景、粒子效果、UI元素和3D模型组成。
3. Controller(控制器):游戏中的控制器充当模型和视图之间的中介,处理用户输入和协调模型和视图之间的交互。控制器接收来自视图的用户输入,并根据业务逻辑调用模型来更新游戏状态,然后将新的状态反映在视图中。控制器还可以处理游戏的各种规则、逻辑和事件。
MVC游戏设计模式的运作过程如下:
1. 玩家与游戏视图进行交互,例如点击按钮、移动角色或执行操作。
2. 游戏视图将用户输入传递给控制器。
3. 控制器接受用户输入,根据游戏规则和业务逻辑处理输入,并调用模型来更新游戏状态。
4. 模型更新游戏数据和状态。
5. 视图观察模型的变化,并根据新的游戏状态来更新游戏画面和UI。
这种结构有助于游戏开发人员分离游戏逻辑、数据和界面,使游戏更易于开发和维护。它还有助于实现游戏的可扩展性,因为你可以更轻松地添加新的视图或模型,而不必改变现有的游戏逻辑。这种分离也有助于实现多平台支持,因为游戏逻辑通常可以保持不变,只需根据不同平台的需求创建不同的视图。
游戏基本规则
简单说明:
在河的一岸有3个牧师和3个魔鬼,他们需要借助船渡到河的对岸,船最多同时承载2个游戏角色。
你的任务是:让所有牧师角色都渡到河的对岸,游戏胜利。
需要注意:当一侧岸上的魔鬼数量大于牧师数量时,魔鬼就会吃掉这个岸上的牧师,游戏失败。
游戏中提及的事物:魔鬼,牧师,船,两岸。
设计结构和对应UML图
Model:
Role、Boat、Shore、River、Click(自定义的部件Click,用于处理点击事件)、ClickAction(一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件)、Position((处理游戏对象的初始坐标))
Controller:
SSDirector、ISceneController、IUserAction分别定义了导演类(只有一个实例)、资源控制接口和用户操作接口。
在这个游戏里,只有唯一一个场景(FirstController)。场景控制器管理所有该场景内的模型控制器,并且实现他们的综合行为。就这个游戏而言,FirstController会生成并且管理:一个BoatController,两个ShoreController,六个RoleController,一个MoveController。所有该场景内的综合行为都会在这个场景控制器内实现。比如:移动船,涉及到船和乘客的同时移动;移动角色,涉及到离岸、登船、人状态的转变三方信息;判断当前游戏是否成功或者失败。
以I开头的控制器都是接口,用于规范同一类型的控制器应当具有的行为,比如IObjectContoller应当被所有模型的控制器继承、IScenceController应当被所有场景的控制器继承、IUserAction负责与视图方面沟通的接口。
游戏具体实现
基本结构
Resources为游戏对象的预制和材质
Scenes为游戏所用的场景
由于我们在该游戏中并没有切换场景,所以只需要使用默认场景即可
Scripts脚本结构
(1)Models
包含两个部分:
- 包含各个对象的模块,每个模块中定义了对象的类
- 包含自定义的部件Click,用于处理点击事件
(2)Controllers
这是整个项目中的控制部分,由它负责所有游戏对象的生成和变化,以及相应用户事件。
其中最核心的部分是FirstController.
(3)Views
定义了用户看到的界面
代码实现
Models
角色Role(priest / devil):
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Role
{
public GameObject role;//model对应的游戏对象
public bool isPriest;
public bool inBoat;
public bool onRight;
public int id;
public Role (Vector3 position, bool isPriest, int id) {
this.isPriest = isPriest;
this.id = id;
onRight = false;
inBoat = false;
role = GameObject.Instantiate(Resources.Load("Prefabs/" + (isPriest ? "priest" : "devil"), typeof(GameObject))) as GameObject;
role.transform.localScale = new Vector3(1,1.2f,1);
role.transform.position = position;
role.name = "role" + id;
role.AddComponent<Click>();
role.AddComponent<BoxCollider>();
}
}
船Boat:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boat
{
public GameObject boat;//船对象
public Role[] roles;//船上的角色
public bool isRight;
public int priestCount, devilCount;
public Boat(Vector3 position) {
boat = GameObject.Instantiate(Resources.Load("Prefabs/boat", typeof(GameObject))) as GameObject;
boat.name = "boat";
boat.transform.position = position;
boat.transform.localScale = new Vector3(2.8f,0.4f,2);
roles = new Role[2];
isRight = false;
priestCount = devilCount = 0;
boat.AddComponent<BoxCollider>();
boat.AddComponent<Click>();
}
}
河流River:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class River
{
public GameObject river;
public River(Vector3 position) {
river = GameObject.Instantiate(Resources.Load("Prefabs/river", typeof(GameObject))) as GameObject;
river.transform.localScale = new Vector3(8,2.5f,2);
river.transform.position = position;
river.name = "river";
}
}
河岸Shore:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Shore
{
public GameObject shore;
public int priestCount, devilCount;
public Shore (Vector3 position){
shore = GameObject.Instantiate(Resources.Load("Prefabs/left_shore", typeof(GameObject))) as GameObject;
shore.transform.localScale = new Vector3(8,4.8f,2);
shore.transform.position = position;
priestCount = devilCount = 0;
}
}
Click与ClickAction:
船和角色都是游戏中用户可以通过点击操作控制的对象,所以它们都附加了Click部件(这个部件是自定义的)和碰撞部件。
ClickAction是一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件。
Click是一个自定义的部件,包含ClickAction,将Click添加到对象上后,对象就可以在接收用户点击时调用DealClick()。
由于角色和船的鼠标响应显然是不同的,所以它们调用的DealClick()实现必然也不同,所以需要setClickAction,它将一个包含DealClick具体实现的类对象传给clickAction。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Click : MonoBehaviour
{
ClickAction clickAction;
public void setClickAction(ClickAction clickAction) {
this.clickAction = clickAction;
}
void OnMouseDown() {
clickAction.DealClick();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ClickAction
{
void DealClick();
}
Controllers:
角色控制器RoleCtrl:
-
RoleCtrl
类包含了以下成员变量:roleModel
:用于存储游戏角色的数据模型。userAction
:用于存储实现了IUserAction
接口的对象。
-
构造函数
RoleCtrl()
:- 初始化
userAction
,将其设置为当前场景的用户操作接口。这里使用了SSDirector.GetInstance().CurrentSceneController
来获取当前场景的控制器,并将其转换为IUserAction
接口。
- 初始化
-
CreateRole(Vector3 position, bool isPriest, int id)
方法:- 用于创建游戏角色。
- 如果
roleModel
不为空,会立即销毁之前的角色对象。 - 创建一个新的角色对象
roleModel
,并传递给它的构造函数一个位置position
、角色类型isPriest
(是牧师还是恶魔)和角色的IDid
。 - 最后,将角色的点击事件处理方法设置为当前的
RoleCtrl
实例,以便处理点击事件。
-
GetRoleModel()
方法:- 用于获取
roleModel
,即当前的游戏角色数据模型。
- 用于获取
-
DealClick()
方法:- 用于处理点击事件。
- 调用
userAction
的MoveRole()
方法,将当前的roleModel
传递给它,以触发游戏中角色的移动操作。
总的来说,实现了以下功能:
- 创建和控制游戏中的角色对象。
- 处理点击事件,触发游戏中角色的移动操作。
- 将用户的点击操作与游戏中的角色交互进行了关联。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoleCtrl : ClickAction
{
Role roleModel;
IUserAction userAction;
public RoleCtrl() {
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
}
public void CreateRole(Vector3 position, bool isPriest, int id) {
if (roleModel != null) {
Object.DestroyImmediate(roleModel.role);
}
roleModel = new Role(position, isPriest, id);
roleModel.role.GetComponent<Click>().setClickAction(this);
}
public Role GetRoleModel() {
return roleModel;
}
public void DealClick() {
userAction.MoveRole(roleModel);
}
}
船控制器BoatCtrl:
-
BoatCtrl
类包含了以下成员变量:boatModel
:用于存储游戏船只的数据模型。userAction
:用于存储实现了IUserAction
接口的对象。
-
构造函数
BoatCtrl()
:- 初始化
userAction
,将其设置为当前场景的用户操作接口。这里使用了SSDirector.GetInstance().CurrentSceneController
来获取当前场景的控制器,并将其转换为IUserAction
接口。
- 初始化
-
CreateBoat(Vector3 position)
方法:- 用于创建游戏船只。
- 如果
boatModel
不为空,会立即销毁之前的船只对象。 - 创建一个新的船只对象
boatModel
,并传递给它的构造函数一个位置position
。 - 最后,将船只的点击事件处理方法设置为当前的
BoatCtrl
实例,以便处理点击事件。
-
GetBoatModel()
方法:- 用于获取
boatModel
,即当前的游戏船只数据模型。
- 用于获取
-
AddRole(Role roleModel)
方法:- 用于将角色从岸上移动到船上。
- 根据船上的位置,确定是否有空位可以放置角色。
- 如果有空位,将角色添加到船上,设置角色在船上的标志,并更新船上的牧师和恶魔数量。
- 返回角色在船上的新位置。
-
RemoveRole(Role roleModel)
方法:- 用于将角色从船上移动到岸上。
- 遍历船上的位置,查找包含指定角色的位置,然后将该位置清空,并更新船上的牧师和恶魔数量。
-
DealClick()
方法:- 用于处理船只的点击事件。
- 如果船上有角色,就调用
userAction
的MoveBoat()
方法,以触发船只的移动操作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BoatCtrl : ClickAction
{
Boat boatModel;
IUserAction userAction;
public BoatCtrl() {
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
}
public void CreateBoat(Vector3 position) {
if (boatModel != null) {
Object.DestroyImmediate(boatModel.boat);
}
boatModel = new Boat(position);
boatModel.boat.GetComponent<Click>().setClickAction(this);
}
public Boat GetBoatModel() {
return boatModel;
}
//将角色从岸上移动到船上,返回接下来角色应该到达的位置??
public Vector3 AddRole(Role roleModel) {
int index = -1;
if (boatModel.roles[0] == null) index = 0;
else if (boatModel.roles[1] == null) index = 1;
if (index == -1) return roleModel.role.transform.localPosition;
boatModel.roles[index] = roleModel;
roleModel.inBoat = true;
roleModel.role.transform.parent = boatModel.boat.transform;
if (roleModel.isPriest) boatModel.priestCount++;
else boatModel.devilCount++;
return Position.role_boat[index];
}
//将角色从船上移到岸上
public void RemoveRole(Role roleModel) {
for (int i = 0; i < 2; ++i){
if (boatModel.roles[i] == roleModel) {
boatModel.roles[i] = null;
if (roleModel.isPriest) boatModel.priestCount--;
else boatModel.devilCount--;
break;
}
}
}
public void DealClick() {
if (boatModel.roles[0] != null || boatModel.roles[1] != null) {
userAction.MoveBoat();
}
}
}
河岸控制器ShoreCtrl:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ShoreCtrl
{
Shore shoreModel;
public void CreateShore(Vector3 position) {
if (shoreModel == null) {
shoreModel = new Shore(position);
}
}
public Shore GetShore() {
return shoreModel;
}
//将角色添加到岸上,返回角色在岸上的相对坐标
public Vector3 AddRole(Role roleModel) {
roleModel.role.transform.parent = shoreModel.shore.transform;
roleModel.inBoat = false;
if (roleModel.isPriest) shoreModel.priestCount++;
else shoreModel.devilCount++;
return Position.role_shore[roleModel.id];
}
//将角色从岸上移除
public void RemoveRole(Role roleModel) {
if (roleModel.isPriest) shoreModel.priestCount--;
else shoreModel.devilCount--;
}
}
Move和MoveCtrl:
Move是一个部件,控制对象的移动,主要有三个重要的变量,当前位置、目标位置和中转位置,Move的目的就是呈现出对象往目的位置移动的过程,中转位置是必要的,否则可能会出现类似穿墙的情况。
Move控制器设置中转位置和目标位置,使得对象开始移动。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move : MonoBehaviour
{
public bool isMoving = false;
public float speed = 5;
public Vector3 destination;
public Vector3 mid_destination;
// Update is called once per frame
void Update()
{
if (transform.localPosition == destination) {
isMoving = false;
return;
}
isMoving = true;
if (transform.localPosition.x != destination.x && transform
.localPosition.y != destination.y) {
transform.localPosition = Vector3.MoveTowards(transform.localPosition, mid_destination, speed * Time.deltaTime);
}
else {
transform.localPosition = Vector3.MoveTowards(transform.localPosition, destination, speed * Time.deltaTime);
}
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveCtrl
{
GameObject moveObject;
public bool GetIsMoving() {
return (moveObject != null && moveObject.GetComponent<Move>().isMoving == true);
}
public void SetMove(Vector3 destination, GameObject moveObject) {
Move test;
this.moveObject = moveObject;
if (!moveObject.TryGetComponent<Move>(out test)) {
moveObject.AddComponent<Move>();
}
this.moveObject.GetComponent<Move>().destination = destination;
if (this.moveObject.transform.localPosition.y > destination.y) {
this.moveObject.GetComponent<Move>().mid_destination = new Vector3(destination.x, this.moveObject.transform.localPosition.y, destination.z);
}
else {
this.moveObject.GetComponent<Move>().mid_destination = new Vector3(this.moveObject.transform.localPosition.x, destination.y, destination.z);
}
}
}
SSDirector、ISceneController、IUserAction:
分别定义了导演类(只有一个实例)、资源控制接口和用户操作接口。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object
{
static SSDirector _instance;
public ISceneController CurrentSceneController {get; set;}
public static SSDirector GetInstance() {
if (_instance == null) {
_instance = new SSDirector();
}
return _instance;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController
{
void LoadResources();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface IUserAction {
void MoveBoat();
void MoveRole(Role roleModel);
void Check();
}
场景控制器FirstController:
FirstController可谓是所有控制器的主管,它指挥控制器们的行为,与此同时,它还负责在游戏中加载资源、创建对象,以及实现了用户操作的响应、更新游戏界面、保存游戏状态,等等。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
ShoreCtrl leftShoreController, rightShoreController;
River river;
BoatCtrl boatController;
RoleCtrl[] roleControllers;
MoveCtrl moveController;
bool isRunning;
float time;
public void LoadResources() {
//role
roleControllers = new RoleCtrl[6];
for (int i = 0; i < 6; ++i) {
roleControllers[i] = new RoleCtrl();
roleControllers[i].CreateRole(Position.role_shore[i], i < 3 ? true : false, i);
}
//shore
leftShoreController = new ShoreCtrl();
leftShoreController.CreateShore(Position.left_shore);
leftShoreController.GetShore().shore.name = "left_shore";
rightShoreController = new ShoreCtrl();
rightShoreController.CreateShore(Position.right_shore);
rightShoreController.GetShore().shore.name = "right_shore";
//将人物添加并定位至左岸
foreach (RoleCtrl roleController in roleControllers)
{
roleController.GetRoleModel().role.transform.localPosition = leftShoreController.AddRole(roleController.GetRoleModel());
}
//boat
boatController = new BoatCtrl();
boatController.CreateBoat(Position.left_boat);
//river
river = new River(Position.river);
//move
moveController = new MoveCtrl();
isRunning = true;
time = 120;
}
public void MoveBoat() {
if (isRunning == false || moveController.GetIsMoving()) return;
if (boatController.GetBoatModel().isRight) {
moveController.SetMove(Position.left_boat, boatController.GetBoatModel().boat);
}
else {
moveController.SetMove(Position.right_boat, boatController.GetBoatModel().boat);
}
boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;
}
public void MoveRole(Role roleModel) {
if (isRunning == false || moveController.GetIsMoving()) return;
if (roleModel.inBoat) {
if (boatController.GetBoatModel().isRight) {
moveController.SetMove(rightShoreController.AddRole(roleModel), roleModel.role);
}
else {
moveController.SetMove(leftShoreController.AddRole(roleModel), roleModel.role);
}
roleModel.onRight = boatController.GetBoatModel().isRight;
boatController.RemoveRole(roleModel);
}
else {
if (boatController.GetBoatModel().isRight == roleModel.onRight) {
if (roleModel.onRight) {
rightShoreController.RemoveRole(roleModel);
}
else {
leftShoreController.RemoveRole(roleModel);
}
moveController.SetMove(boatController.AddRole(roleModel), roleModel.role);
}
}
}
public void Check() {
if (isRunning == false) return;
this.gameObject.GetComponent<UserGUI>().gameMessage = "";
if (rightShoreController.GetShore().priestCount == 3) {
this.gameObject.GetComponent<UserGUI>().gameMessage = "You Win!";
isRunning = false;
}
else {
int leftPriestCount, rightPriestCount, leftDevilCount, rightDevilCount;
leftPriestCount = leftShoreController.GetShore().priestCount + (boatController.GetBoatModel().isRight ? 0 : boatController.GetBoatModel().priestCount);
rightPriestCount = rightShoreController.GetShore().priestCount + (boatController.GetBoatModel().isRight ? boatController.GetBoatModel().priestCount : 0);
leftDevilCount = leftShoreController.GetShore().devilCount + (boatController.GetBoatModel().isRight ? 0 : boatController.GetBoatModel().devilCount);
rightDevilCount = rightShoreController.GetShore().devilCount + (boatController.GetBoatModel().isRight ? boatController.GetBoatModel().devilCount : 0);
if (leftPriestCount != 0 && leftPriestCount < leftDevilCount || rightPriestCount != 0 && rightPriestCount < rightDevilCount) {
this.gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
isRunning = false;
}
}
}
void Awake() {
SSDirector.GetInstance().CurrentSceneController = this;
LoadResources();
this.gameObject.AddComponent<UserGUI>();
}
void Update() {
if (isRunning) {
time -= Time.deltaTime;
this.gameObject.GetComponent<UserGUI>().time = (int)time;
if (time <= 0) {
this.gameObject.GetComponent<UserGUI>().time = 0;
this.gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
isRunning = false;
}
}
}
}
Views:
用于在游戏中显示用户界面的脚本 UserGUI
。它负责在游戏画面中显示游戏信息,包括游戏的标题、消息以及剩余时间。以下是代码的功能解释:
-
UserGUI
类继承了MonoBehaviour
,这是Unity脚本的基类。 -
IUserAction userAction
:用于存储实现了IUserAction
接口的对象,这是用于处理用户操作的接口。 -
public string gameMessage
:用于存储游戏的消息,如游戏胜利或失败的消息。 -
public int time
:用于存储游戏中的剩余时间。 -
在
Start()
方法中进行了初始化:- 设置初始时间为120秒。
- 获取当前场景的控制器,并将其转换为
IUserAction
接口。 - 创建两个
GUIStyle
,style
用于一般文本显示,bigstyle
用于较大的文本。
-
OnGUI()
方法用于在游戏界面上绘制用户界面元素:- 调用
userAction.Check()
方法,可能用于检查游戏的状态或事件。 - 使用
GUI.Label()
在游戏界面上绘制文字标签:- 第一个标签用于显示游戏标题 "Preists and Devils",使用
bigstyle
进行显示。 - 第二个标签用于显示游戏消息,消息内容存储在
gameMessage
变量中,使用style
进行显示。 - 第三个标签用于显示剩余时间,显示 "Time: " 加上剩余时间,使用
style
进行显示。
- 第一个标签用于显示游戏标题 "Preists and Devils",使用
- 调用
这段代码的作用是在游戏中显示游戏标题、消息和剩余时间,以向玩家提供游戏的信息和反馈。通过在 OnGUI()
方法中绘制GUI元素,玩家可以实时了解游戏状态和进展。其中,gameMessage
和 time
的值会根据游戏的进展和规则进行更新。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
IUserAction userAction;
public string gameMessage ;
public int time;
GUIStyle style, bigstyle;
// Start is called before the first frame update
void Start()
{
time = 120;
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 30;
bigstyle = new GUIStyle();
bigstyle.normal.textColor = Color.white;
bigstyle.fontSize = 50;
}
// Update is called once per frame
void OnGUI() {
userAction.Check();
GUI.Label(new Rect(160, Screen.height * 0.1f, 50, 200), "Preists and Devils", bigstyle);
GUI.Label(new Rect(250, 100, 50, 200), gameMessage, style);
GUI.Label(new Rect(0,0,100,50), "Time: " + time, style);
}
}