这次的项目是弄一个牧师与恶魔的小游戏,游戏规则和背景:三个牧师和三个恶魔需要过一条河,河上只有一条船,船上最多可以载两个人,而船上最少有一个人的时候才可以开船,当六个人都到达河对岸的时候,游戏胜利,但是,当任意一边的恶魔的数目比牧师的数目多时,恶魔会攻击牧师,游戏失败。
首先,就是贴图的下载和prefab的制作,这里就不多说了。然后,成品如下:
游戏的实现采用MVC结构,全部的游戏对象和场景都由代码来实现。
首先,就从导演类开始实现,具体也没什么可说的,代码如下:
public class Director : System.Object {
private static Director _instance;
public SceneController currentSceneController { set; get; }
public static Director getInstance()
{
if(_instance == null)
{
_instance = new Director();
}
return _instance;
}
}
接下来,实现两个接口,分别是场景控制的接口和对用户动作进行反应的接口。
//游戏场景控制器
public interface SceneController {
void LoadResources();
}
public interface UserAction
{
void moveBoat();
void isClickCha(ChaController chaController);
void restart();
}
然后,就是对游戏场景中有动作的对象添加控制,在这个游戏中,有动作的对象有船(过河)、牧师(上船、上岸)和魔鬼(上船、上岸),可以看出,牧师和魔鬼的动作其实是一样的,所以,可以将他们归为同一类,就称为人类吧。之后,考虑到我们需要一个办法来判定游戏是否胜利,于是,就把两岸也当做是一个有动作的对象,他们的动作主要是判定两边的人数是否平衡,确定游戏是否结束。
为了方便处理对象的移动,这里增加多了一个类来处理物体的移动。实现如下:
//对象移动处理
public class MoveStatus : MonoBehaviour {
float speed = 15;//移动的速度
//用来检测移动的状态。由于上船或下船的时候,有两个移动段。
//0代表在开始位置,1代表从开始位置移到转折点,2代表从转折点移动到目的地
public int move_status;
//目的地的地址
Vector3 destination;
//转折点的地址
Vector3 halfDest;
private void Update()
{
//从开始移动到转折点,到转折点后,改变状态
if(move_status == 1)
{
transform.position = Vector3.MoveTowards(transform.position, halfDest, speed * Time.deltaTime);
if(transform.position == halfDest)
{
move_status = 2;
}
}
//从转折点到目的地,到目的地后,改变状态
else if(move_status == 2)
{
transform.position = Vector3.MoveTowards(transform.position, destination, speed * Time.deltaTime);
if(transform.position == destination)
{
move_status = 0;
}
}
}
//重置移动状态
public void Reset()
{
move_status = 0;
}
//设置目的地
public void setDestination(Vector3 dest)
{
destination = dest;
move_status = 1;
//如果高度一样,证明不用经过转折点,就是,在水面上船的移动
if (dest.y == transform.position.y)
{
move_status = 2;
}
//上岸时转折处理
else if(dest.y > transform.position.y)
{
halfDest.x = transform.position.x;
halfDest.y = dest.y;
}
//上船时转折处理
else
{
halfDest.y = transform.position.y;
halfDest.x = dest.x;
}
}
}
下面就是对船这个对象的Controller,对这个对象,需要记录的就三个属性:船的移动、船上人的位置、船现在所处的位置(左边还是右边)。
先来看一下这个类的字段:
GameObject boat;//对象
MoveStatus moveable;//移动
Vector3[] from_pos;//船上的能坐人的位置,右边
Vector3[] to_pos;//船上能坐人的位置,左边
int onWhere;//标志位,船在左边还是右边(1 - 右边, -1 - 左边)
ChaController[] people = new ChaController[2];//船上的人物
由于船的移动其实就只有向左移动和向右移动,通过标志位,可以确定船要移动的方向,而船的位置移动实现可通过moveable字段来实现。如下:
/// <summary>
/// 船的移动。每移动一次,改变标志位
/// </summary>
public void Move()
{
if(onWhere == -1)
{
moveable.setDestination(new Vector3(5, 1.15f, -2.5f));
onWhere = 1;
}
else
{
moveable.setDestination(new Vector3(-5, 1.15f, -2.5f));
onWhere = -1;
}
}
其他的就是需要对船的一些状态的获取,具体类的实现如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class BoatController {
GameObject boat;//对象
MoveStatus moveable;//移动
Vector3[] from_pos;//船上的能坐人的位置,右边
Vector3[] to_pos;//船上能坐人的位置,左边
int onWhere;//标志位,船在左边还是右边(1 - 右边, -1 - 左边)
ChaController[] people = new ChaController[2];//船上的人物
public BoatController(GameObject t_boat)
{
onWhere = 1;//初始在右边
from_pos = new Vector3[] { new Vector3(4.5f, 1.65f, -2), new Vector3(5.5f, 1.65f, -2) };
to_pos = new Vector3[] { new Vector3(-5.5f, 1.65f, -2), new Vector3(-4.5f, 1.65f, -2) };
boat = t_boat;
moveable = boat.AddComponent(typeof(MoveStatus)) as MoveStatus;
boat.AddComponent(typeof(ClickGUI));
}
/// <summary>
/// 船的移动。每移动一次,改变标志位
/// </summary>
public void Move()
{
if(onWhere == -1)
{
moveable.setDestination(new Vector3(5, 1.15f, -2.5f));
onWhere = 1;
}
else
{
moveable.setDestination(new Vector3(-5, 1.15f, -2.5f));
onWhere = -1;
}
}
//获取船上的空位
public int getEmptyIndex()
{
for(int i = 0; i < people.Length; i++)
{
if (people[i] == null) return i;
}
return -1;
}
//判断船是否没人
public bool isEmpty()
{
foreach(ChaController chac in people)
{
if (chac != null) return false;
}
return true;
}
//获取没人的位置的坐标
public Vector3 getEmptyPos()
{
int index = getEmptyIndex();
if(onWhere == -1)
{
return to_pos[index];
}
else
{
return from_pos[index];
}
}
//人物对象上船
public void getOnBoat(ChaController chac)
{
int index = getEmptyIndex();
people[index] = chac;
}
/// <summary>
/// 人物对象下船
/// </summary>
/// <param name="name">下船的人的名字</param>
/// <returns>返回这个对象的控制器</returns>
public ChaController getOffBoat(string name)
{
for(int i = 0; i < people.Length; i++)
{
if(people[i] != null && people[i].getChaName() == name)
{
ChaController chac = people[i];
people[i] = null;
return chac;
}
}
return null;
}
public GameObject getGameobj()
{
return boat;
}
public int getOnWhere()
{
return onWhere;
}
//获取船上各身分人物的数量
public int[] getChaNum()
{
int[] count = { 0, 0 };
foreach (ChaController chac in people)
{
if (chac != null)
{
int add = chac.getChaType() == 0 ? 0 : 1;
count[add]++;
}
}
return count;
}
//重置
public void reset()
{
moveable.Reset();
if (onWhere == -1) Move();
people = new ChaController[2];
}
}
然后,就是对人物对象进行动作管理,人物的动作有上船和下船,另外,需要一个标志位来确定这个人物是牧师还是恶魔,以方便判定游戏的失败条件。还需要随时监控鼠标的点击,当点击该人物对象的时候,人物对象要进行上船或者下船。当然,还需要判定人物是否在船上。具体实现如下:
public class ChaController {
GameObject character;//游戏人物控制器
MoveStatus moveable;//人物动作
ClickGUI clickGUI;//点击事件
private int whatCharacter;//人物的角色(牧师还是魔鬼)
private bool onBoat;//人数是否在船上
LandController landController;
//人物控制器的初始化
public ChaController(GameObject chac, int status)
{
character = chac;
whatCharacter = status;
moveable = character.AddComponent(typeof(MoveStatus)) as MoveStatus;
clickGUI = character.AddComponent(typeof(ClickGUI)) as ClickGUI;
clickGUI.setController(this);
}
public void setName(string name)
{
character.name = name;
}
//设置人物的位置
public void setPosition(Vector3 position)
{
character.transform.position = position;
}
//人物移动事件
public void movePosition(Vector3 dest)
{
moveable.setDestination(dest);
}
public int getChaType()
{
return whatCharacter;
}
public string getChaName()
{
return character.name;
}
//人物上船。设置船为人物的父对象
public void getOnBoat(BoatController boat)
{
landController = null;
character.transform.parent = boat.getGameobj().transform;
onBoat = true;
}
//人物下船
public void getOnLand(LandController land)
{
landController = land;
character.transform.parent = null;
onBoat = false;
}
public bool isOnBoat()
{
return onBoat;
}
public LandController getLandCont()
{
return landController;
}
//重置
public void Reset()
{
moveable.Reset();
landController = (Director.getInstance().currentSceneController as FirstController).fromLand;
getOnLand(landController);
setPosition(landController.getEmptyPosition());
landController.getOnLand(this);
}
}
接下来,就是对鼠标事件的监控。
public class ClickGUI : MonoBehaviour {
UserAction action;
ChaController chac;
//设置点击的人物
public void setController(ChaController chaController)
{
chac = chaController;
}
private void Start()
{
action = Director.getInstance().currentSceneController as UserAction;
}
//鼠标点击事件
private void OnMouseUp()
{
try
{
action.isClickCha(chac);
}
catch(Exception e)
{
Debug.Log(e.ToString());
}
finally
{
Debug.Log("Clicking:" + gameObject.name);
}
}
private void OnGUI()
{
//开船事件。由于用鼠标点击的时候,总是点不中,就直接用button来了
if ((Director.getInstance().currentSceneController as FirstController).isOver() == 0)
{
GUIStyle buttonStyle = new GUIStyle("button");
buttonStyle.fontSize = 30;
buttonStyle.normal.textColor = Color.blue;
if (GUI.Button(new Rect(Screen.width / 2 - 60, Screen.height / 2 - 25, 120, 50), "set sail", buttonStyle))
{
action.moveBoat();
}
}
//加点儿游戏的文字说明
GUIStyle gUIStyle = new GUIStyle();
gUIStyle.fontSize = 40;
gUIStyle.normal.textColor = Color.green;
GUI.Label(new Rect(Screen.width / 2 - 150, 10, 300, 40), "Priests And Devils", gUIStyle);
GUI.Label(new Rect(10, 200, 300, 300), "Priests and Devils is a " +
"game in which you will help the Priests and Devils to cross the river." +
" 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 this game, you can click on them to move them and click the button to move the boat to the other direction. " +
"If the priests are out numbered by 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!");
}
}
下面,再增加一个陆地(也就是两岸)的监测类,用来获取岸上牧师的人数和魔鬼的人数,还有就是人物上岸和离岸的处理。如下:
public class LandController {
GameObject land;//对象
Vector3[] pos;//用于放置人物对象的位置
int side;//标志位,1 表示这是右边的陆地,-1表示这是左边的位置
ChaController[] people;//陆地上的任务对象
//初始化
public LandController(GameObject t_land, int t_status)
{
pos = new Vector3[] {new Vector3(6.5f,2.5f,-2), new Vector3(7.5f,2.5f,-2), new Vector3(8.5f,2.5f,-2),
new Vector3(9.5f,2.5f,-2), new Vector3(10.5f,2.5f,-2), new Vector3(11.5f,2.5f,-2)};
people = new ChaController[6];
land = t_land;
side = t_status;
}
//获取人物位置数组中,没有人物的位置下标
public int getEmptyIndex()
{
for(int i = 0; i < people.Length; i++)
{
if (people[i] == null) return i;
}
return -1;
}
//获取人物位置数组中,没有人物的位置坐标
public Vector3 getEmptyPosition()
{
Vector3 position = pos[getEmptyIndex()];
position.x *= side;
return position;
}
//人物上岸,找个没人的位置给他
public void getOnLand(ChaController cha)
{
int index = getEmptyIndex();
people[index] = cha;
}
//人物上船
public ChaController getOffLand(string person)
{
for(int i = 0; i < people.Length; i++)
{
if(people[i] != null && people[i].getChaName() == person)
{
ChaController chaCon = people[i];
people[i] = null;
return chaCon;
}
}
return null;
}
//获取当前陆地的标识(左边还是右边)
public int getSide()
{
return side;
}
//获取陆地上牧师的数目和魔鬼的数目
public int[] getChaNum()
{
int[] count = {0,0};
foreach(ChaController chac in people)
{
if(chac != null)
{
int add = chac.getChaType() == 0 ? 0 : 1;
count[add]++;
}
}
return count;
}
//重置
public void reset()
{
people = new ChaController[6];
}
}
在上面那些类都处理完毕之后,就可以真正地开始弄游戏场景了,在总的场景控制类中,首先需要导入游戏资源,搭建游戏场景,这个可以在Awake函数中实现。然后,就是玩家开始玩游戏,场景控制器需要处理玩家的请求,选择谁上船,是否开船等,另外,还需要随时监听游戏是否已经结束。代码如下:
public class FirstController : MonoBehaviour, SceneController, UserAction {
UserGUI userGUI;
public LandController fromLand;
public LandController toLand;
public BoatController boat;
private ChaController[] people;
private Transform background;
//初始化游戏场景和配置。
void Awake()
{
Director.getInstance().currentSceneController = this;
userGUI = gameObject.AddComponent<UserGUI>() as UserGUI;
people = new ChaController[6];
LoadResources();
}
//创建对象的辅助函数
private GameObject createObject(string name, Vector3 position)
{
return (GameObject)Object.Instantiate(Resources.Load("Prefabs/" + name), position, Quaternion.identity);
}
//导入各种预制体资源
public void LoadResources()
{
//背景设置(其实就是一个方块加点贴图。能力限制,就这么弄了)
background = Instantiate<Transform>(Resources.Load<Transform>("Prefabs/backGround"), new Vector3(0, 6, 3), Quaternion.identity);
background.name = "background";
background.localScale += new Vector3(35, 20, 2);
background.Rotate(new Vector3(10, 0, 180));
//导入陆地、河流和船
GameObject river = createObject("River", new Vector3(0, 0, -2));
river.name = "river";
GameObject leftLand = createObject("Land", new Vector3(-10, 0.5f, -2));
leftLand.name = "leftLand";
GameObject rightLand = createObject("Land", new Vector3(10, 0.5f, -2));
rightLand.name = "rightLand";
GameObject t_boat = createObject("Boat", new Vector3(5, 1.15f, -2.5f));
t_boat.name = "boat";
//设置控制器
fromLand = new LandController(rightLand, 1);
toLand = new LandController(leftLand, -1);
boat = new BoatController(t_boat);
//导入游戏人物对象并设置控制器
for (int i = 0; i < 3; i++)
{
GameObject temp = createObject("devil", Vector3.zero);
ChaController cha = new ChaController(temp, 1);
cha.setName("devil" + i);
cha.setPosition(fromLand.getEmptyPosition());
cha.getOnLand(fromLand);
fromLand.getOnLand(cha);
people[i] = cha;
}
for (int i = 0; i < 3; i++)
{
GameObject temp = createObject("Priests", Vector3.zero);
ChaController cha = new ChaController(temp, 0);
cha.setName("priest" + i);
cha.setPosition(fromLand.getEmptyPosition());
cha.getOnLand(fromLand);
fromLand.getOnLand(cha);
people[i + 3] = cha;
}
}
//船的移动,当船上有人时,船才可以移动。。
public void moveBoat()
{
if (!boat.isEmpty())
boat.Move();
userGUI.Status = isOver();
}
/// <summary>
/// 如果点了某个人物,判断其是在船上还是陆地上
/// 如果在船上,则上岸;否则,上船
/// </summary>
/// <param name="chac">某个人</param>
public void isClickCha(ChaController chac)
{
//上岸
if(chac.isOnBoat())
{
LandController whichLand;
if (boat.getOnWhere() == -1)
whichLand = toLand;
else
whichLand = fromLand;
boat.getOffBoat(chac.getChaName());
chac.movePosition(whichLand.getEmptyPosition());
chac.getOnLand(whichLand);
whichLand.getOnLand(chac);
}
//上船
else
{
LandController whichLand = chac.getLandCont();
if (boat.getEmptyIndex() == -1) return;
if (whichLand.getSide() != boat.getOnWhere()) return;
whichLand.getOffLand(chac.getChaName());
chac.movePosition(boat.getEmptyPos());
chac.getOnBoat(boat);
boat.getOnBoat(chac);
}
userGUI.Status = isOver();//判断游戏是否已经达到了结束的条件
}
/// <summary>
/// 判断游戏是否结束
/// </summary>
/// <returns>2 - 赢了; 1 - 输了; 0 - 还没结束</returns>
public int isOver()
{
int fromP = 0;//右边牧师人数
int fromD = 0;//右边魔鬼人数
int toP = 0;//左边牧师人数
int toD = 0;//左边魔鬼人数
//获取右边对应的人数
int[] fromCount = fromLand.getChaNum();
fromP += fromCount[0];
fromD += fromCount[1];
//获取左边对应的人数
int[] toCount = toLand.getChaNum();
toP += toCount[0];
toD += toCount[1];
//如果左边有六个人,证明全部安全过河,你赢了
if (toP + toD == 6) return 2;
//将船上人的数目也加上去
int[] boatCount = boat.getChaNum();
if(boat.getOnWhere() == -1)
{
toP += boatCount[0];
toD += boatCount[1];
}
else
{
fromP += boatCount[0];
fromD += boatCount[1];
}
//如果任意一边牧师人数少于魔鬼,你输了
if (fromP < fromD && fromP > 0 || toP < toD && toP > 0) return 1;
return 0;
}
//重置游戏
public void restart()
{
boat.reset();
fromLand.reset();
toLand.reset();
foreach (ChaController chac in people) chac.Reset();
}
}
至此,代码实现方面就完成了,接下来,只需在Unity3D场景中create一个空对象,将所有继承了MonoBehavior的类的脚本挂在这个空对象上,然后,调整好摄像机的位置和拍摄角度,就可以运行游戏了。
当然,现在这个实现的还是很简单的,你可以考虑一些改进:
- 游戏场景美化,牧师、船、恶魔等可以下载一些好看的模块来代替,河流和可以做成是流动的。
- 加上时间限制,玩家需在规定时间内完成游戏。
- 增加背景音乐等。