魔鬼与牧师MVC实现
魔鬼与牧师
游戏介绍
play the game
网址:魔鬼与牧师
列出游戏中提及的事物
魔鬼,牧师,船,河流,两边的陆地
用表格列出玩家动作表(规则表)
动作 | 条件 | 结果 |
---|---|---|
单击魔鬼或牧师 | 游戏未结束,船没有移动,上船需要位于单击对象的一边且没有满员,下船需要靠岸 | 上船或下船 |
单击GO | 船未移动且至少有一人 | 船移动到另一端 |
MVC
模型(Model):数据对象及关系。模型包括场景中的所有GameObject,它们都由控制器控制。
控制器(Controller):接受用户事件,控制模型的变化。包括控制魔鬼牧师活动的控制器,船控制器,河岸控制器,场景控制器,以及一个最高层的Director控制器。
界面(View):显示模型,将人机交互事件交给控制器处理处收 Input 事件,包括用户交互的UI界面,提供与用户交互的渠道。
代码分析
Controller
Director——指挥中心
Director是最高层的控制器,运行游戏时始终只有一个实例,它控制场景的加载、切换,游戏进程状态等等,可以说,是控制器的控制器。
我们要想让游戏顺利的运行,最重要的就是控制住只有一个Director实例,因为只有这样,我们才可以保证所有的脚本中获得的都是同一个Director对象,这样,也就能通过Director进行类之间的通信。代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Director : System.Object
{
public MySceneControl currentSceneControl { get; set; }
private static Director director;
public static Director getInstance()
{
if (director == null)
{
director = new Director();
}
return director;
}
}
SceneController——场景控制器接口
SceneController接口主要是Director控制场景控制器的渠道,但是在这里,只是一个接口,后续会用一个类来继承它并给出具体方法。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface MySceneControl
{
void GenGameObjects();
}
IUserAction——用户动作控制接口
IUserAction的作用是给用户GUI提供控制接口,通过前面的分析可以知道,我们需要后台判断游戏的状态,需要获取剩余时间,同时还需要移动小船。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum GameState { WIN, FAILED, NOT_ENDED,TIME_UP }
public interface IUserAction
{
void MoveBoat();
GameState getGameState();
float Gettime();
}
On_Off——角色控制器
On_off是角色移动的控制器,主要包括上下船,上下岸的过程。这个控制器主要接受的是点击的信号,只有点击角色时才会触发。
我们首先定义一个枚举变量用于设置状态,并且定义一个函数用于返回当前的角色的状态。在开始时,还是将场景控制器设置为我们刚刚定义的场景控制器。
private FirstSceneControl firstSceneControl;
enum Position { ON_BOAT, ON_SHORE }
Position find_Position(int id)
{
if (id >= 6) return Position.ON_BOAT;
return Position.ON_SHORE;
}
void Start()
{
firstSceneControl = (FirstSceneControl)Director.getInstance().currentSceneControl;
}
下面,我们定义当鼠标点击时的处理反应函数,这里只给出一个岸边的处理,另外一个岸是基本完全对称的处理方式。我们使用id+6的编号对已经上船的物体进行重新编号,这样便于我们在别的函数中使用。同时,需要在下船的时候使用id-6对下船的物体再次恢复编号。
if (firstSceneControl.b_state == FirstSceneControl.BoatState.STOPRIGHT)//左边
{
if (firstSceneControl.On_Shore_r.ContainsKey(id))
{
if (find_Position(id) == Position.ON_SHORE && firstSceneControl.boat_capicity != 0)
{
firstSceneControl.On_Boat.Add(id + 6, firstSceneControl.On_Shore_r[id]);//上船
firstSceneControl.On_Shore_r.Remove(id);
this.name = (id + 6).ToString();
this.transform.parent = firstSceneControl.boat.transform;
firstSceneControl.boat_capicity--;//空位-1
}
}
if (find_Position(id) == Position.ON_BOAT)//在船上
{
firstSceneControl.On_Shore_r.Add(id - 6, firstSceneControl.On_Boat[id]);//上岸
firstSceneControl.On_Boat.Remove(id);//下船
this.name = (id - 6).ToString();
this.transform.parent = null;
firstSceneControl.boat_capicity++;//船空位+1
}
}
在这次处理之前,我们需要注意要求中的一项:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件! 所以,我们需要在接收到点击时就进行判断。
if (firstSceneControl.b_state == FirstSceneControl.BoatState.MOVING) return;//在移动的时候不能进行其他操作片
这样,我们就完成了对于点击物体的控制器。
FirstSceneControl——场景控制器实例
前面我们定义了场景控制器的接口和一些需要使用的别的控制器等等,这里,我们需要先定义一些需要用到的变量,下面详细解释一下变量的含义及用法。
public enum BoatState { MOVING, STOPLEFT, STOPRIGHT }
public Dictionary<int, GameObject> On_Boat = new Dictionary<int, GameObject>();
public Dictionary<int, GameObject> On_Shore_r = new Dictionary<int, GameObject>();
public Dictionary<int, GameObject> On_Shore_l = new Dictionary<int, GameObject>();//类似于map的结构
public GameState game_state;
public GameObject Shore_l;
public GameObject Shore_r;
public GameObject boat;
readonly float gab = 3f;
public int boat_capicity;
public BoatState b_state;
bool boatAction = false;
int boatmoving = 0;
public float timer = 60f;
BoatState为一个船状态枚举变量,包括了停在左边、右边以及移动中。下面定义了三个Dictionary,用于存储目前在船上、左岸以及右岸的物体,这个结构类似一个map一样的结构。同时,我们会发现,刚刚我们使用了数字定义id可以直接用于编号。game_state定义了一个变量用于记录当前游戏的状态,然后定义了左岸右岸以及船的实体。gab则是一个变量用于控制人物之间的间距。boat_capicity是用于记录当前船上的空位,而下面的b_state用于记录船的状态。boatAction用于发出移动信号给Update,boat moving用于记录移动的方向,主要用于在移动时进行连贯式的移动。最后timer用于记录剩余时间。
下面介绍一下需要使用的函数。
movingboat——船的移动
船的移动我们需要先检测是否有人在船上,然后检测船的状态,如果没有在移动则可以移动。
public void MoveBoat()
{
if (boat_capicity == 2) return;
if (b_state == BoatState.STOPLEFT) boatmoving = 1;
if (b_state == BoatState.STOPRIGHT) boatmoving = 2;
boatAction = true;
}
Gettime and state——用于返回时间和状态
返回当前的时间与当前状态
public float Gettime()
{
return timer;
}
public GameState getGameState()
{
return game_state;
}
get_num——用于统计魔鬼与牧师个数
该函数通过对id的统计,主要用于判断游戏状态。
public int get_num(Dictionary<int, GameObject> dict, int ch)
{
var keys = dict.Keys;
int d_num = 0;
int p_num = 0;
foreach (int i in keys)
{
if (i < 3 || (i >= 6 && i <= 8))
{
p_num++;
}
else
{
d_num++;
}
}
return (ch == 1 ? p_num : d_num);
}
check——判断游戏状态
通过成功判断,失败判断以及超时判断来对当前游戏状态进行判断。
GameState check()
{
if (On_Shore_l.Count == 6)
{
return GameState.WIN;
}
else if (b_state == BoatState.STOPLEFT)
{
if (get_num(On_Boat, 1) + get_num(On_Shore_l, 1) != 0 && get_num(On_Boat, 1) + get_num(On_Shore_l, 1) < (get_num(On_Boat, -1) + get_num(On_Shore_l, -1)))
{
//牧师不为0,且牧师数小于恶魔数
return GameState.FAILED;
}
if (get_num(On_Shore_r, 1) != 0 && get_num(On_Shore_r, 1) < get_num(On_Shore_r, -1))
{
//这步是为了修正对岸的情况,以免出现卡bug情况
return GameState.FAILED;
}
}
else if (b_state == BoatState.STOPRIGHT)
{
if (get_num(On_Boat, 1) + get_num(On_Shore_r, 1) != 0 && get_num(On_Boat, 1) + get_num(On_Shore_r, 1) < (get_num(On_Boat, -1) + get_num(On_Shore_r, -1)))
{
//牧师不为0,且牧师数小于恶魔数
return GameState.FAILED;
}
if (get_num(On_Shore_l, 1) != 0 && get_num(On_Shore_l, 1) < get_num(On_Shore_l, -1))
{
//这步是为了修正对岸的情况,以免出现卡bug情况
return GameState.FAILED;
}
}
if (timer > 0) timer -= Time.deltaTime;
else return GameState.TIME_UP;
return GameState.NOT_ENDED;
}
GenGameObjects——构建游戏对象
构建游戏对象其实应该属于Model,但是这里为Model的控制器,这里控制了魔鬼与牧师、船、岸的生成位置并赋予了魔鬼与牧师脚本On_Off,这里使用了一个循环来将魔鬼与牧师编号并添加到右岸上。
for (int i = 0; i < 3; i++)
{
On_Shore_r.Add(i, Instantiate<GameObject>(temp_priest, new Vector3(12.5f + i * gab, -7.5f, 0), Quaternion.identity));
On_Shore_r[i].name = i.ToString();
}
for (int i = 3; i < 6; i++)
{
On_Shore_r.Add(i, Instantiate<GameObject>(temp_devil, new Vector3(12.5f + i * gab, -7.5f, 0), Quaternion.identity));
On_Shore_r[i].name = i.ToString();
}
剩下的就是一些简单的GameObject的运用,这里就不再赘述。
Update——帧更新
update为每帧的更新函数,主要用于状态判断、船的移动和位置修正,首先我们需要在每帧都进行判断来保证游戏状态是同步正确的,船可以从一侧移到另一侧就是利用了帧更新,除此之外函数还在每帧都确定人物的位置改变。
首先展示游戏状态和船状态的判断代码,游戏状态用check函数即可,而船状态判断直接根据当前船的位置判断即可。
{
game_state = check();
if (game_state == GameState.NOT_ENDED)//判断状态
{
if (boat.transform.position.x < 7.4f && boat.transform.position.x > -7.4f)//有0.07误差防止出错
{
b_state = BoatState.MOVING;
}
else if (boat.transform.position.x > 7.4f)
{
b_state = BoatState.STOPRIGHT;
}
else
{
b_state = BoatState.STOPLEFT;
}
下面,是角色位置的修正,主要是岸上角色位置修正以及船上角色位置的修正。
for (int i = 0; i < 6; i++)
{
if (On_Shore_l.ContainsKey(i)) On_Shore_l[i].transform.position = new Vector3(-12.5f - i * gab, -7.5f, 0);
if (On_Shore_r.ContainsKey(i)) On_Shore_r[i].transform.position = new Vector3(12.5f + i * gab, -7.5f, 0);//位置修正
}
int signed = 1;
for (int i = 6; i < 12; i++)
{
if (On_Boat.ContainsKey(i))//在船上的角色位置修正
{
On_Boat[i].transform.localPosition = new Vector3(signed * 0.3f, 1, 0);
signed = -signed;
}
}
}
最后,是船移动的实现,这里只放出左边移动到右边的代码,右边到左边只需要对称实现即可。
if (boatmoving==1 && (b_state == BoatState.MOVING || b_state == BoatState.STOPLEFT))
{
boat.transform.position = Vector3.MoveTowards(boat.transform.position, new Vector3(7.47f, -9.5f, 0), 10 * Time.deltaTime);
if (boat.transform.position == new Vector3(7.47f, -9.5f, 0))
{
boatmoving = 0;
boatAction = false;
}
}
View and Model
View和Model部分主要呈现的就是程序的UI,我将主要的代码写成了一个文件UserGUI。该文件核心就是利用Vector3进行调整各种gameobject的位置,同时根据不同的游戏状态进行不同的UI上的反应。
首先我们先进行字体和静态object的设置:
GUIStyle winstyle = new GUIStyle();
winstyle.normal.textColor = new Color(0,1,0);
winstyle.fontSize = 80;
GUIStyle losestyle = new GUIStyle();
losestyle.normal.textColor = new Color(1, 0, 0);
losestyle.fontSize = 80;
GUIStyle longtimestyle = new GUIStyle();
longtimestyle.normal.textColor = new Color(0, 1, 0);
longtimestyle.fontSize = 55;
GUIStyle middletimestyle = new GUIStyle();
middletimestyle.normal.textColor = new Color(1, 1, 0);
middletimestyle.fontSize = 55;
GUIStyle shorttimestyle = new GUIStyle();
shorttimestyle.normal.textColor = new Color(1, 0, 0);
shorttimestyle.fontSize = 55;
GUIStyle titlestyle = new GUIStyle();
titlestyle.normal.textColor = new Color(1, 1, 0);
titlestyle.fontSize = 100;
GUI.Label(new Rect(260, 5, 40, 40), "Priests And Devils", titlestyle);
然后,设置time显示。
if (action.getGameState() == GameState.NOT_ENDED) timers = action.Gettime();
else if (action.getGameState() == GameState.TIME_UP) timers = 0;
if (timers > 20 && timers < 40)
{
GUI.Label(new Rect(30, 130, 40, 40), "Time:", middletimestyle);
GUI.Label(new Rect(190, 130, 40, 40), timers.ToString(), middletimestyle);
}
else if (timers >= 40)
{
GUI.Label(new Rect(30, 130, 40, 40), "Time:", longtimestyle);
GUI.Label(new Rect(190, 130, 40, 40), timers.ToString(), longtimestyle);
}
else
{
GUI.Label(new Rect(30, 130, 40, 40), "Time:", shorttimestyle);
GUI.Label(new Rect(190, 130, 40, 40), timers.ToString(), shorttimestyle);
}
下面,是GO点击按键的设置。
if (GUI.Button(new Rect(527, 300, 260, 120), "GO!") && action.getGameState() == GameState.NOT_ENDED)
{
action.MoveBoat();
}
最后,是对游戏状态的反应。
else if (action.getGameState() == GameState.WIN)
{
GUI.Label(new Rect(500, 170, 200, 200), "You Win!",winstyle);
GUI.color = Color.green;
}
else if (action.getGameState() == GameState.FAILED)
{
GUI.Label(new Rect(500, 170, 200, 200), "You Lose!",losestyle);
GUI.color = Color.red;
}
else if (action.getGameState() == GameState.TIME_UP)
{
GUI.Label(new Rect(500, 170, 400, 400), "Time Up!", losestyle);
}
}
到此,编码工作已基本完成。
模型使用
本游戏使用了unity asset商店中的免费模型:unity_assets
成品
总结
魔鬼与牧师这个小游戏看起来并不难,但制作起来十分复杂,通过MVC结构的分解,我们可以有效的简化代码难度,但是在其中update函数的设置还是需要仔细斟酌。在利用MVC编程时候,控制器的实现是一个关键点,也可以说,95%的时间都是在做控制器,在最后的View中就是调用控制器接口来反映到UI上。在制作控制器时候,要把握我需要给界面提供哪些接口,也就是说,用最少的接口函数解决最多的问题。
总的来说,本次小游戏制作的过程不仅提高了我的编码能力,更重要的是编码思维。其实小游戏还能有更多的功能等待实现,例如简单的AI,退出暂停功能等等。随着后续课程的学习,我相信可以将这个小游戏做到更加完美。