一、游戏说明文本与基本规则
Priests and Devils
Priests and Devils is a puzzle game in which you will help the Priests and Devils to cross the river within the time limit. There are 3 priests and 3 devils at one side of the river. They all want to get to the other side of this river, but there is only one boat and this boat can only carry two persons each time. And there must be one person steering the boat from one side to the other side. In the flash game,you can click on them to move them and click the go button to move the devils on either side of the river, they get killed and the game is over. You can try it in many > ways. Keep all priests alive! Good luck!
简单说明一下就是:
在河的一岸有3个牧师和3个魔鬼,他们需要借助船渡到河的对岸,船最多同时承载2个游戏角色。
你的任务是:让所有游戏角色都渡到河的对岸,游戏胜利。
需要注意:当一侧岸上的魔鬼数量大于牧师数量时,魔鬼就会吃掉这个岸上的牧师,游戏失败。
游戏中提及的事物:魔鬼,牧师,船,两岸。
![](https://i-blog.csdnimg.cn/blog_migrate/7833c138cf5fa135f35360688eac5edb.png)
二、设计结构和对应UML图
MVC:
我们在设计该游戏时使用MVC架构(模型、界面、控制器)。
MVC是界面人机交互程序设计的一种架构模式。它把程序分为三个部分:
模型(Model):数据对象及关系,游戏对象、空间关系,在游戏中,一切实体都可以被视为是模型,这种实体可以是具体的,也可以是抽象的。
控制器(Controller):接受用户事件,控制模型的变化,一个场景一个主控制器,至少实现与玩家交互的接口(IPlayerAction,实现或管理运动
界面(View):显示模型,将人机交互事件交给控制器处理,处收 Input 事件,渲染 GUI ,接收事件。
我们将刚才提及的游戏中的事物或者说游戏对象都分为模型Model,他们受到Controller控制器的控制进行游戏行为,具体的模型类有:Boat(船)、Land(河岸/陆地)、Move(处理游戏对象的移动)、Position(处理游戏对象的初始坐标)、Role(调整游戏对象方向方便移动);
控制器类需要接受用户事件并控制模型的变化,比如Boat模型就会有BoatController,BoatController会控制一切以船为对象的行为,比如:处理船被点击时的事件、往船上添加一名乘客,往船上减少一名乘客。必须要注意的是,直接控制模型的控制器之间是没有耦合关系的,也就是说,BoatController只能知道当前船上有多少名乘客,但是不知道他们都是谁(这个信息应当由各RoleController保存),还有,因为移动船要同时移动船上的所有乘客,但是BoatController不能直接移动乘客,因此实际上真正移动船的逻辑不能直接写在BoatController里(具体应该写在哪下文说)。Move模型就会有MoveController,MoveController控制了谁要移动,而且负责判断当前是否有模型正在移动。另外还有一类很重要的控制器就是场景控制器和导演。游戏就像是一场话剧,话剧会有多个场景,同时有一个导演贯穿了所有场景,导演控制器必须被写成单例模式,这就确保了各控制器属于同一场“话剧”。每一个场景都会有一个单独的场景控制器,但是在这个游戏里,只有唯一一个场景(FirstController)。场景控制器管理所有该场景内的模型控制器,并且实现他们的综合行为。就这个游戏而言,FirstController会生成并且管理:一个BoatController,两个LandController,六个RoleController,一个MoveController。所有该场景内的综合行为都会在这个场景控制器内实现。比如:移动船,涉及到船和乘客的同时移动;移动角色,涉及到离岸、登船、人状态的转变三方信息;判断当前游戏是否成功或者失败。最后,以I开头的控制器都是接口,用于规范同一类型的控制器应当具有的行为,比如IObjectContoller应当被所有模型的控制器继承、IScenceController应当被所有场景的控制器继承、IUserAction负责与视图方面沟通的接口。
界面类显示游戏结果和提供用户交互渠道,具体的界面类为UserGUI(用户交互界面)管理比如说:标题、重新开始的按钮、游戏成功或者失败时的提示。
uml图如下:
三、具体实现
项目文件结构如下:
其中Resources为游戏对象的预制和材质,里面有四个游戏对象的预制及其材质
Scenes为游戏所用的场景,由于我们在该游戏中并没有切换场景,所以只需要使用默认场景即可
Scripts是代码部分也是我们设计中最重要的部分,我们按照MVC架构组织代码结构
Model部分:
Boat类:
public class Boat
{
public GameObject boat;//船对象
public Boat(Vector3 initPos){
boat = GameObject.Instantiate(Resources.Load("Prefabs/boat", typeof(GameObject))) as GameObject;
boat.transform.position = initPos;
boat.AddComponent<Click>();
}
}
Click类:
public class Click : MonoBehaviour
{
IObjectController clickAction;
public void setClickAction(IObjectController clickAction) {
this.clickAction = clickAction;
}
void OnMouseDown() {
clickAction.DealClick();
}
}
Land类:
public class Land
{
public GameObject land;
public Land(Vector3 initPos){
land = GameObject.Instantiate(Resources.Load("Prefabs/land", typeof(GameObject))) as GameObject;
land.transform.position = initPos;
}
}
Move类:
public class Move : MonoBehaviour
{
public bool isMoving;
public float speed = 5;
public Vector3 destination;
void Update()
{
if (transform.localPosition == destination) {
isMoving = false;
return;
}
isMoving = true;
transform.localPosition = Vector3.MoveTowards(transform.localPosition, destination, speed * Time.deltaTime);
}
}
Position类:
public class Position
{
public static Vector3 boatLeftPos = new Vector3(-3.5f, -1, 12);
public static Vector3 boatRightPos = new Vector3(3.5f, -1, 12);
public static Vector3[] roleLeftPos = new Vector3[6]{new Vector3(-11, 0, 12),
new Vector3(-10, 0, 12),
new Vector3(-9, 0, 12),
new Vector3(-8, 0, 12),
new Vector3(-7, 0, 12),
new Vector3(-6, 0, 12)};
public static Vector3[] roleRightPos = new Vector3[6]{new Vector3(6, 0, 12),
new Vector3(7, 0, 12),
new Vector3(8, 0, 12),
new Vector3(9, 0, 12),
new Vector3(10, 0, 12),
new Vector3(11, 0, 12)};
public static Vector3[] landInitPos = new Vector3[2]{new Vector3(-8, -1, 12),
new Vector3(8, -1, 12)};
public static Vector3[] seatLeftPos = new Vector3[3]{new Vector3(-4.5f, 0, 12),
new Vector3(-3.5f, 0, 12),
new Vector3(-2.5f, 0, 12)};
public static Vector3[] seatRightPos = new Vector3[3]{new Vector3(2.5f, 0, 12),
new Vector3(3.5f, 0, 12),
new Vector3(4.5f, 0, 12)};
}
Role类:
public class Role
{
public GameObject role;
public Role(int roleType, Vector3 initPos){
string path = "Prefabs/" + ((roleType == FirstController.PRIEST) ? "priest" : "devil");
role = GameObject.Instantiate(Resources.Load(path, typeof(GameObject))) as GameObject;
role.transform.position = initPos;
role.AddComponent<Click>();
}
}
View部分:
UserGUI类:
public class UserGUI : MonoBehaviour
{
IUserAction userAction;
GUIStyle msgStyle, titleStyle;
void Start()
{
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
msgStyle = new GUIStyle();
msgStyle.normal.textColor = Color.white;
msgStyle.alignment = TextAnchor.MiddleCenter;
msgStyle.fontSize = 30;
titleStyle = new GUIStyle();
titleStyle.normal.textColor = Color.white;
titleStyle.alignment = TextAnchor.MiddleCenter;
titleStyle.fontSize = 60;
}
void OnGUI() {
// 重新开始的按钮
if(GUI.Button(new Rect(Screen.width*0.4f, Screen.height*0.65f, Screen.width*0.2f, Screen.height*0.1f), "Restart")){
userAction.Restart();
}
// 检查是否正确
GUI.Label(new Rect(0, 0, Screen.width, Screen.height*0.2f), "Preists and Devils", titleStyle);
if(userAction.GetGameState() == FirstController.WIN){
GUI.Label(new Rect(0, Screen.height*0.8f, Screen.width, Screen.height*0.2f), "You Win.", msgStyle);
}
else if(userAction.GetGameState() == FirstController.FAILED){
GUI.Label(new Rect(0, Screen.height*0.8f, Screen.width, Screen.height*0.2f), "You failed.", msgStyle);
}
}
}
Controller部分:
BoatController类:
public class BoatController : IObjectController
{
public bool onLeftside;
IUserAction userAction;
public int[] seat;
public Boat boatModel;
public BoatController(){
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
seat = new int[3];
Reset();
}
public void Reset(){
onLeftside = true;
for(int i = 0; i < 3; i++){
seat[i] = -1;
}
}
public int embark(int roleID){
for(int i = 0; i < 3; i++){
if(seat[i] == -1){
seat[i] = roleID;
return i;
}
}
return -1;
}
public int getEmptySeat(){
for(int i = 0; i < 3; i++){
if(seat[i] == -1){
return i;
}
}
return -1;
}
public void disembark(int roleID){
for(int i = 0; i < 3; i++){
if(seat[i] == roleID){
seat[i] = -1;
return;
}
}
}
public void CreateModel(){
boatModel = new Boat(Position.boatLeftPos);
boatModel.boat.GetComponent<Click>().setClickAction(this);
}
public void DealClick(){
userAction.MoveBoat();
}
public GameObject GetModelGameObject(){
return boatModel.boat;
}
}
FirstController类:
public class FirstController : MonoBehaviour, ISceneController, IUserAction
{
public static int LEFTLAND = 0;
public static int RIGHTLAND = 1;
public static int BOAT = 2;
public static int PRIEST = 0;
public static int DEVIL = 1;
public static int PLAYING = 0;
public static int WIN = 1;
public static int FAILED = 2;
BoatController BoatCtrl;
RoleController[] RoleCtrl = new RoleController[6];
LandController[] LandCtrl = new LandController[2];
MoveController MoveCtrl;
int[] rolesID = new int[6]{0,1,2,3,4,5};
int gameState;
void Awake(){
SSDirector director = SSDirector.GetInstance();
director.CurrentSceneController = this;
director.CurrentSceneController.Initialize();
}
public void Initialize(){
//如果有,则释放原有的GameObject
for(int i = 0; i < 6; i++){
if(RoleCtrl[i] != null){
Destroy(RoleCtrl[i].GetModelGameObject());
}
}
for(int i = 0; i < 2; i++){
if(LandCtrl[i] != null){
Destroy(LandCtrl[i].GetModelGameObject());
}
}
if(BoatCtrl != null){
Destroy(BoatCtrl.GetModelGameObject());
}
// 加载控制器和模型
BoatCtrl = new BoatController();
BoatCtrl.CreateModel();
for(int i = 0; i < 6; i++){
int roleType = (i < 3) ? PRIEST : DEVIL;
RoleCtrl[i] = new RoleController(roleType, rolesID[i]);
RoleCtrl[i].CreateModel();
}
LandCtrl[0] = new LandController(LEFTLAND, rolesID);
LandCtrl[1] = new LandController(RIGHTLAND, rolesID);
LandCtrl[0].CreateModel();
LandCtrl[1].CreateModel();
MoveCtrl = new MoveController();
//开始游戏
gameState = PLAYING;
}
//将角色的ID转换成数组的下标
int IDToNumber(int ID){
for(int i = 0; i < 6; i++){
if(rolesID[i] == ID){
return i;
}
}
return -1;
}
//点击船时执行
public void MoveBoat(){
if(gameState != PLAYING || MoveCtrl.IsMoving()) return;
CheckAndSetGameState();
if(BoatCtrl.onLeftside){
MoveCtrl.SetMove(BoatCtrl.GetModelGameObject(), Position.boatRightPos);
for(int i = 0; i < 3; i++){
if(BoatCtrl.seat[i] != -1){
RoleController r = RoleCtrl[IDToNumber(BoatCtrl.seat[i])];
MoveCtrl.SetMove(r.GetModelGameObject(), Position.seatRightPos[i]);
}
}
}
else{
MoveCtrl.SetMove(BoatCtrl.GetModelGameObject(), Position.boatLeftPos);
for(int i = 0; i < 3; i++){
if(BoatCtrl.seat[i] != -1){
RoleController r = RoleCtrl[IDToNumber(BoatCtrl.seat[i])];
MoveCtrl.SetMove(r.GetModelGameObject(), Position.seatLeftPos[i]);
}
}
}
BoatCtrl.onLeftside = !BoatCtrl.onLeftside;
}
//点击角色时执行
public void MoveRole(int id){
int num = IDToNumber(id);
if(gameState != PLAYING || MoveCtrl.IsMoving()) return;
int seat;
switch(RoleCtrl[num].roleState){
case 0: // LEFTLAND
if(!BoatCtrl.onLeftside) return;
LandCtrl[0].LeaveLand(id);
seat = BoatCtrl.embark(id);
RoleCtrl[num].GoTo(BOAT);
if(seat == -1) return;
MoveCtrl.SetMove(RoleCtrl[num].GetModelGameObject(), Position.seatLeftPos[seat]);
break;
case 1: // RIGHTLAND
if(BoatCtrl.onLeftside) return;
LandCtrl[1].LeaveLand(id);
seat = BoatCtrl.embark(id);
RoleCtrl[num].GoTo(BOAT);
if(seat == -1) return;
MoveCtrl.SetMove(RoleCtrl[num].GetModelGameObject(), Position.seatRightPos[seat]);
break;
case 2: //BOAT
if(BoatCtrl.onLeftside){
seat = LandCtrl[0].getEmptySeat();
BoatCtrl.disembark(id);
LandCtrl[0].GoOnLand(id);
RoleCtrl[num].GoTo(LEFTLAND);
MoveCtrl.SetMove(RoleCtrl[num].GetModelGameObject(), Position.roleLeftPos[seat]);
}
else{
seat = LandCtrl[1].getEmptySeat();
BoatCtrl.disembark(id);
LandCtrl[1].GoOnLand(id);
RoleCtrl[num].GoTo(RIGHTLAND);
MoveCtrl.SetMove(RoleCtrl[num].GetModelGameObject(), Position.roleRightPos[seat]);
}
break;
default: break;
}
}
//判断游戏状态
public void CheckAndSetGameState(){
if(gameState != PLAYING) return;
//判断是否失败
int[,] rolePos = new int[2, 3]{{0, 0, 0}, {0, 0, 0}};
foreach(RoleController r in RoleCtrl){
rolePos[r.roleType, r.roleState]++;
}
if((rolePos[0,0]>0 && rolePos[0,0]<rolePos[1,0]) ||
(rolePos[0,1]>0 && rolePos[0,1]<rolePos[1,1]) ||
(rolePos[0,2]>0 && rolePos[0,2] < rolePos[1,2])){
gameState = FAILED;
return;
}
//判断是否成功
foreach(RoleController r in RoleCtrl){
if(r.roleType == 0 && r.roleState != RIGHTLAND){
return;
}
}
gameState = WIN;
return;
}
//Reset按钮执行的功能
public void Restart(){
Initialize();
gameState = PLAYING;
}
//获取游戏当前状态
public int GetGameState(){
return gameState;
}
}
IObjectController类:
public interface IObjectController
{
void DealClick();
void CreateModel();
void Reset();
GameObject GetModelGameObject();
}
ISceneController类:
public interface ISceneController
{
void Initialize();
}
IUserAction类:
public interface IUserAction {
void MoveBoat();
void MoveRole(int id);
int GetGameState();
void Restart();
}
LandController类:
public class LandController: IObjectController
{
public int side;
public int[] seat;
IUserAction userAction;
Land landModel;
public LandController(int side, int[] rolesID){
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
this.side = side;
this.seat = new int[6]{-1,-1,-1,-1,-1,-1};
if(this.side == FirstController.LEFTLAND){
for(int i = 0; i < 6; i++){
this.seat[i] = rolesID[i];
}
}
}
public bool LeaveLand(int roleID){
for(int i = 0; i < 6; i++){
if(seat[i] == roleID){
seat[i] = -1;
return true;
}
}
return false;
}
public bool GoOnLand(int roleID){
for(int i = 0; i < 6; i++){
if(seat[i] == -1){
seat[i] = roleID;
return true;
}
}
return false;
}
public int getEmptySeat(){
for(int i = 0; i < 6; i++){
if(seat[i] == -1){
return i;
}
}
return -1;
}
public void CreateModel(){
landModel = new Land(Position.landInitPos[side]);
}
public void Reset(){}
public void DealClick(){}
public GameObject GetModelGameObject(){
return landModel.land;
}
}
MoveController类:
public class MoveController
{
GameObject moveObject;
public bool IsMoving(){
return(this.moveObject != null && this.moveObject.GetComponent<Move>().isMoving == true);
}
public void SetMove(GameObject moveObject, Vector3 destination) {
// 设置一个新的移动
Move test;
this.moveObject = moveObject;
if (!moveObject.TryGetComponent<Move>(out test)) {
moveObject.AddComponent<Move>();
}
this.moveObject.GetComponent<Move>().destination = destination;
}
}
RoleController类:
public class RoleController : IObjectController
{
public int roleType;
public int roleState;
public int id;
Role roleModel;
IUserAction userAction;
public RoleController(int roleType, int id){
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
this.roleType = roleType;
this.id = id;
Reset();
}
public void Reset(){
this.roleState = FirstController.LEFTLAND;
}
public void GoTo(int pos){
this.roleState = pos;
}
public void CreateModel(){
roleModel = new Role(roleType, Position.roleLeftPos[id]);
roleModel.role.GetComponent<Click>().setClickAction(this);
}
public void DealClick(){
userAction.MoveRole(this.id);
}
public GameObject GetModelGameObject(){
return roleModel.role;
}
}
SSSirector类:
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;
}
}
为了进一步说明代码是如何运作的,以下我就游戏行为“先点击一个牧师上船,再点击船移动,被判定游戏失败”为例介绍代码的工作流程。
- IFirstController中的Awake函数和UserGUI中的start函数被执行,前者初始化了第一个场景中的所有3D对象,后者初始化了2D的GUI
- 点击一个牧师,点击事件被某牧师模型(Role.cs)的Click组件(Click.cs)中的OnMouseDown函数捕获到
- 该点击事件被传递到该牧师对应的RoleController.cs中(具体传递的机制请查看RoleController.cs中的CreatModel函数)
- RoleController.cs调用DealClick函数解决该点击事件。但是以上已经解释过RoleController.cs实际上不能真正解决这个点击事件,因此DealClick的做法是通过唯一的导演找到管理他的场景控制器,请场景控制器解决这个问题。
- 场景控制器调用指定的函数(在这里就是MoveRole函数),调动所有相关的模型控制器,真正处理该点击事件
- 再点击船,此时的响应逻辑跟上述的完全一样,船模型中的点击事件会被一步步传递到场景控制器中,由场景控制器的MoveBoat函数处理该点击事件
- MoveBoat函数执行时会检查当前游戏的状态,此时判断游戏失败,则会把场景控制器中的gameState变量设置为FAILED并不再允许处理点击事件
- UserGUI中的OnGUI函数会一直监听场景控制器中的gameState的值,当它发现这个值变成FAILED后就会在绘制出“you failed”的文字。
四、游戏展示
游戏演示视频:unity 3d 牧师与魔鬼小游戏作业_哔哩哔哩bilibili
参考师兄的博客:Unity3D小游戏——牧师与魔鬼 - LoongChan - 博客园 (cnblogs.com)
写的很好,谢谢师兄!