游戏规则和背景我就不说了,首先我们要知道这个游戏中出现了那些对象:魔鬼,牧师,两个岸,一条河,一艘船。那么每个对象有什么行为呢,如下
魔鬼
1.上下船
2.当数量大于牧师时,可以杀死牧师,游戏结束
3.划船
牧师
1.上下船
2.划船
船
1.河岸间移动
至于岸,河就是个场景布置,没有什么行为。
在开始之前,首先我在Assets中创建了三个文件夹,Materials存放一些简单素材,Resources存放资源,Scripts存放脚本,Resources中有一个Prefabs文件用于存储预制。
行为确定了,我们先把具体模型创建出来,用立方体来做岸和河,用球来作为魔鬼和牧师,在做一些简单的装饰,得到这样的效果(毕竟没有艺术细胞)
我创造了五个类:岸,河,船,角色,上帝。河就只是简单的生成,确定位置;岸记录所在岸上的牧师数和魔鬼数;船有两个位置;上帝判断游戏的输赢,以及重来操作。它们的具体关系如图
由于角色的操作会对岸,船都有影响,所以我把岸、船座位角色的一部分,一边修改方便,同时,上帝判断输赢也需要对船、角色、岸进行控制,所以船、角色、岸又作为了上帝的组成部分,这里面存在岸、船被角色和上帝共同使用。
下面每个类具体说明
1.河流
这个不多说,直接上代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 河流 : MonoBehaviour {
// Use this for initialization
void Start () {
transform.position = new Vector3(-2.81f, 3.0f, 0.0f);
}
// Update is called once per frame
void Update () {
}
}
2.岸
因为有两个岸,为了区分,添加一个public int变量 num,可以在外部赋值,0为起始岸,1为终点岸。两个int类型NumberOfPriests、NumberOfDevils很好理解。下面是函数解释:
Initialization:初始化函数会把两个岸移到相应位置,将起始岸的牧师、魔鬼数都设为3
SetNumber:置数,两个参数Which,add,Which表示给魔鬼还是牧师的数目进行修改,add是具体添加或减小多少
GetNumberOfPriests:返回牧师数
GetNumberOfDevils:返回恶魔数
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 岸 : MonoBehaviour {
public int num;
int NumberOfPriests;
int NumberOfDevils;
// Use this for initialization
void Start () {
Initialization();
}
public void Initialization()
{
if (num == 0)
{
transform.position = new Vector3(13.2f, 3.47f, 0.0f);
NumberOfDevils = 3;
NumberOfPriests = 3;
}
else
{
transform.position = new Vector3(-18.1f, 3.47f, 0.0f);
}
if (num == 1)
{
NumberOfPriests = 0;
NumberOfDevils = 0;
} else
{
NumberOfPriests = 3;
NumberOfDevils = 3;
}
}
public void SetNumber(int Which, int add)
{
if (Which == 0)
NumberOfDevils += add;
else
NumberOfPriests += add;
}
public int GetNumberOfPriests()
{
return NumberOfPriests;
}
public int GetNumberOfDevils()
{
return NumberOfDevils;
}
}
3.船
因为船正在航行时,对其点击要求无效,所以定义了一个IsMoving来确定运动状态。同时用WhereToGo确定船运动的方向。sets数组显示作为情况,-1为无人,0为恶魔,1为牧师,2为暂时被占用,可能是正在上下船,where表示船目前在哪。
函数解释:
Initialization:对船的位置初始化,运动状态设为静止,朝终点岸方向,位置在起始岸,并把所有座位置为空
GetEmpty:当角色上船时被调用,找到当前为空的位置并返回,没有则返回-1
GetIsMoving:返回船的运动状态
Set:对座位进行状态修改,因为角色上下船需要时间,这个时候若是设置船座位已为空或者坐下了,鼠标点击那么船就直接跑了,因此这一段时间船不能移动,为了区分这个状态,设置2为暂时被使用
Empty:判断船是否为空
GetNumberOfPriests:返回船上坐的牧师
GetNumberOfDevils:返回船上坐的恶魔
IAmBusy: 船若是暂时被占用,则很忙,否则空闲
Idle:判断船是否准备好,即船不空且每个座位都没有被暂时使用
CanMove:判断船是否可以出发了
OnMouseDown:鼠标点击触发,如果可以出发,就改变船的运动状态
Stop:游戏结束时所有物体都不能被操作,因此需要Stop
GetWhere:获得船当前的位置
GetWhereToGo:获得船的移动方向
Update:当船的运动状态为运动时,此时需要进行判断,若运动位置已达到目标位置,则停止运动,并修改所在位置以及方向,否则继续移动,这里的移动我用了MoveTowards函数判断是否已达目的地用了例如if(transform.position == new Vector3(-8.0f, 5.0f, 0.0f))
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 船 : MonoBehaviour {
int IsMoving; // 若船为航行状态,将使点击无效
int WhereToGo; // 判断船向为起始岸还是终点岸
int[] sets = new int[2]; // 判断座位是否为空,2为正在占用状态,不予执行操作
int where;
void Start()
{
Initialization();
}
public void Initialization()
{
transform.position = new Vector3(3, 5, 0);
IsMoving = 0; // 0为未航行, 1为正在航行
WhereToGo = 1; // 0为朝起始岸,1为朝终点岸
for (int i = 0; i < 2; i++)
sets[i] = -1; // -1表示座位为空,0为坐的是魔鬼,1坐的是牧师,2是正在被占用
where = 0;
}
public int GetEmpty() // 哪个位置空就返回哪个位置,否则返回-1
{
for (int i = 0; i < 2; i++)
{
if (sets[i] == -1)
return i;
}
return -1;
}
public int GetIsMoving()
{
return IsMoving;
}
public void Set(int num, int value) // 修改座位情况
{
sets[num] = value;
}
public bool Empty()
{
for (int i = 0; i < 2; i++)
if (sets[i] != -1)
return false;
return true;
}
public int GetNumberOfPriests()
{
int num = 0;
for (int i = 0; i < 2; i++)
if (sets[i] == 1)
num++;
return num;
}
public bool IAmBusy()
{
for (int i = 0; i < 2; i++)
if (sets[i] == 2)
return true;
return false;
}
public int GetNumberOfDevils()
{
int num = 0;
for (int i = 0; i < 2; i++)
if (sets[i] == 0)
num++;
return num;
}
bool Idle() // 判断船是否准备好了好
{
if (Empty())
return false;
for (int i = 0; i < 2; i++)
if (sets[i] == 2)
return false;
return true;
}
public bool CanMove()
{
if (IsMoving == 1 || !Idle() || IsMoving == 2)
return false;
return true;
}
void OnMouseDown()
{
if (!CanMove())
return;
IsMoving = 1;
}
public void Stop()
{
IsMoving = 2;
}
public int GetWhere()
{
return where;
}
public int GetWhereToGo()
{
return WhereToGo;
}
void Update()
{
if (IsMoving == 1)
{
if (WhereToGo == 1)
{
if (transform.position == new Vector3(-8.0f, 5.0f, 0.0f))
{
IsMoving = 0;
WhereToGo = 0;
where = 1;
} else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(-8.0f, 5.0f, 0.0f), 5 * Time.deltaTime);
}
} else
{
if (transform.position == new Vector3(3, 5, 0))
{
IsMoving = 0;
WhereToGo = 1;
where = 0;
} else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(3.0f, 5.0f, 0.0f), 5 * Time.deltaTime);
}
}
}
}
}
3.角色
因为魔鬼与牧师基本相同,所以用num来给他们编号,同时用Ideneity来区分他们的身份。where表示角色的位置,角色上下船会改变船位置的状态,所以船也应该为角色的组成,同理,两个岸也应该是角色的组成。角色的运动状态用IsMoving表示,为了确定角色是否在船上有参数IsInBoat。点击角色,角色会有不同的动作,如静止、起始岸上下船、终点岸上下船,为了在Update函数中区分相应行为,用action来表示不同行为,WhichSet则表示角色上船坐的是哪个位置
Initialization:对位置初始化,一开始都在起始岸,静止,不在船上,同时,需要对起始岸、终点岸进行初始化
Stop:游戏结束,不能进行操作,所以对每个角色Stop
OnMouseDown:鼠标点击,若是角色正在移动,则无效,否则根据角色所处位置,是否在船上修改相应的action,同时设置与角色相关的位置为暂时占用状态。若是角色要上船,要看船是否有位置,若有则获得一个空位给WhichSet
Update:首先由于我将船设为了角色的组成部分,所以划船的时候会使船走了,角色还漂在水上。为了解决这个问题,我只好修改当船正在移动时,角色也相应移动,做到相对静止,这是划船行为。上下船与船移动实质差不多,都是找一个目标位置开始移动。在上下船成功后,需要将船上相应位置状态由2改为相应状态,同时将角色的action设为0,运动状态设为静止,修改所在地和是否在船上
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 角色 : MonoBehaviour {
public int num;
public int Ideneity; // 1表示牧师,0表示恶魔
int where; // 0在起始岸,1在终点岸
public 船 boat;
public 岸 start, end;
int IsMoving;
bool IsInBoat; // 是否在船上
int action;
int WhichSet; // 绝对坐在哪个位置
// Use this for initialization
void Start () {
Initialization();
}
public void Initialization()
{
transform.position = new Vector3(4 + num * 2, 6.0f, 0);
where = 0;
IsMoving = 0;
IsInBoat = false;
action = 0;
WhichSet = -1;
start.Initialization();
end.Initialization();
}
public void Stop()
{
IsMoving = 2;
}
void OnMouseDown()
{
if (IsMoving == 1 || IsMoving == 2)
return;
if (!IsInBoat)
WhichSet = boat.GetEmpty();
if (boat.GetIsMoving() == 1 || IsMoving == 1) // 船或人物正在移动不能上下船
{
action = 0;
return;
}
if (where == 0 && IsInBoat) // 起始岸下船
{
IsMoving = 1;
boat.Set(WhichSet, 2);
action = 1;
start.SetNumber(Ideneity, 1);
}
if (where == 0 && !IsInBoat && boat.GetWhere() == 0) // 起始岸上船
{
if (WhichSet == -1) // 船满了,上不了船
return;
action = 2;
boat.Set(WhichSet, 2);
IsMoving = 1;
start.SetNumber(Ideneity, -1);
}
if (where == 1 && IsInBoat) // 终点岸下船
{
IsMoving = 1;
boat.Set(WhichSet, 2);
action = 3;
end.SetNumber(Ideneity, 1);
}
if (where == 1 && !IsInBoat && boat.GetWhere() == 1) // 终点岸上船
{
if (WhichSet == -1) // 船满了,上不了船
return;
action = 4;
boat.Set(WhichSet, 2);
IsMoving = 1;
end.SetNumber(Ideneity, -1);
}
}
// Update is called once per frame
void Update ()
{
if (boat.GetIsMoving() == 1 && IsInBoat)
{
if (boat.GetWhereToGo() == 1)
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(-9 - WhichSet * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
where = 1;
} else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(4 + WhichSet * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
where = 0;
}
}
if (IsMoving == 1)
{
if (action == 1) // 起始岸下船
{
if (transform.position == new Vector3(4 + num * 2.0f, 6.0f, 0.0f))
{
action = 0;
IsMoving = 0;
where = 0;
IsInBoat = false;
boat.Set(WhichSet, -1);
} else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(4 + num * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
}
}
if (action == 2) // 起始岸上船
{
if (transform.position == new Vector3(2 + WhichSet * 2.0f, 6.0f, 0.0f))
{
action = 0;
IsMoving = 0;
where = 0;
IsInBoat = true;
boat.Set(WhichSet, Ideneity);
} else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(2 + WhichSet * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
}
}
if (action == 3) // 终点岸下船
{
if (transform.position == new Vector3(-23 + num * 2.0f, 6.0f, 0.0f))
{
action = 0;
IsMoving = 0;
where = 1;
IsInBoat = false;
boat.Set(WhichSet, -1);
}
else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(-23 + num * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
}
}
if (action == 4) // 终点岸上船
{
if (transform.position == new Vector3(-9 + WhichSet * 2.0f, 6.0f, 0.0f))
{
action = 0;
IsMoving = 0;
where = 1;
IsInBoat = true;
boat.Set(WhichSet, Ideneity);
}
else
{
transform.position = Vector3.MoveTowards(transform.position, new Vector3(-9 + WhichSet * 2.0f, 6.0f, 0.0f), 5 * Time.deltaTime);
}
}
}
}
}
4.上帝
作为上帝,一个简单的例子,重新开始函数就需要对一切对象初始化,所以自然每个对象(河流可以除外)都是上帝的组成,同时为了判断游戏是否胜利,是否输了,引入了一个GameOver来判断游戏进行状态
Initialization:将游戏状态设为未结束,并将各个对象初始化。
Stop:游戏结束所有对象都需要暂停
Reset:重新开始,自然需要初始化函数
OnGUI:若是Reset按钮被按了则重新开始,若游戏结束根据游戏结束情况在Label上显示相应的文字
IfWin():判断是否胜利。若船在运动或者船正在装载人,此时不应该判断。若终点岸牧师、魔鬼的数目加在一起为6则赢了。排除以后,判断游戏是否输了我是按船所在位置判断的,若是船在起始岸,若是船上的恶魔加上起始岸的恶魔大于船上的牧师加上起始岸的牧师,且牧师数不为0;或者终点岸恶魔大于终点岸牧师且牧师数不为0,则输,船在终点岸同理
Update:很简单,当游戏没有结束时,时刻监视游戏进行情况
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class 上帝 : MonoBehaviour {
public 岸 start;
public 岸 end;
public 角色 魔鬼1;
public 角色 魔鬼2;
public 角色 魔鬼3;
public 角色 牧师1;
public 角色 牧师2;
public 角色 牧师3;
public 船 boat;
int GameOver;
// Use this for initialization
void Start ()
{
Initialization();
}
void Initialization()
{
GameOver = 0;
start.Initialization();
end.Initialization();
魔鬼1.Initialization();
魔鬼2.Initialization();
魔鬼3.Initialization();
牧师1.Initialization();
牧师2.Initialization();
牧师3.Initialization();
boat.Initialization();
}
void Stop()
{
魔鬼1.Stop();
魔鬼2.Stop();
魔鬼3.Stop();
牧师1.Stop();
牧师2.Stop();
牧师3.Stop();
boat.Stop();
}
private void Reset()
{
Initialization();
}
private void OnGUI()
{
if (GUI.Button(new Rect(450, 400, 100, 50), "Reset"))
Reset();
if (GameOver == 1)
GUI.Label(new Rect(450, 50, 200, 200), "You win!");
else if (GameOver == 2)
GUI.Label(new Rect(450, 50, 200, 200), "You loose!");
}
void IfWin()
{
if (boat.GetIsMoving() == 1 || boat.IAmBusy())
return;
if (end.GetNumberOfDevils() + end.GetNumberOfPriests() == 6)
{
GameOver = 1;
Stop();
return;
}
if (boat.GetWhere() == 0) // 船开来了
{
if ((start.GetNumberOfDevils() + boat.GetNumberOfDevils() > start.GetNumberOfPriests() + boat.GetNumberOfPriests() && start.GetNumberOfPriests() + boat.GetNumberOfPriests() != 0) ||
end.GetNumberOfDevils() > end.GetNumberOfPriests() && end.GetNumberOfPriests() != 0)
{
Stop();
GameOver = 2;
}
} else
{
if ((start.GetNumberOfDevils() > start.GetNumberOfPriests() && start.GetNumberOfPriests()!= 0) ||
end.GetNumberOfDevils() + boat.GetNumberOfDevils() > end.GetNumberOfPriests() + boat.GetNumberOfPriests() && end.GetNumberOfPriests() + boat.GetNumberOfPriests() != 0)
{
Stop();
GameOver = 2;
}
}
}
// Update is called once per frame
void Update () {
if (GameOver == 0)
IfWin();
}
}
所有的脚本都写好了,创建一个空游戏对象——上帝,将所有对象都引入上帝中,再将脚本挂载到相对对象,再给相应public元素赋值,就可以正常运行这个游戏了。
但是还没完,这里要介绍以下MVC结构
MVC结构
制作游戏其实和拍电影一样,电影里面需要导演,导演的工作就是协调各个场记,给各个场记分发任务,场记在接到任务后通知演员、化妆师、特效师、布置场景。MVC就是用这种思想,有一个导演掌控全局,当需要这个场景的时候会通知场记,场记加载资源,这里用到了LoadResources,这就要求资源必须存放在Resources中的Prefabs中,因此需要我们把之前制作的游戏整体作为一个预制存入。MVC结构,必须要编写三个脚本SSDirector(导演),FirstController(场记,本游戏只需要一个场记),ISceneController(接口)。编写完后,将FirstController挂载到一个空游戏对象,这样就可以使得整个游戏仅有主摄像机和一个Empty对象, 其他对象都由代码动态生成。
SSDirector
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SSDirector : System.Object {
private static SSDirector _instance;
public ISceneController currentSceneController { get; set; }
public static SSDirector getInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
public int getFPS()
{
return Application.targetFrameRate;
}
public void setFPS(int fps)
{
Application.targetFrameRate = fps;
}
}
ISceneController
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public interface ISceneController {
void LoadResources();
void Pause();
void Resume();
}
FirstController
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstController : MonoBehaviour, ISceneController {
void Awake() {
SSDirector director = SSDirector.getInstance();
director.setFPS(60);
director.currentSceneController = this; // 导演把任务交给了这个场记
director.currentSceneController.LoadResources();
}
// Use this for initialization
public void LoadResources()
{
GameObject Global = Instantiate<GameObject>(Resources.Load<GameObject>("Prefabs/上帝"), Vector3.zero, Quaternion.identity);
Global.name = "上帝";
}
void Start()
{
}
public void Pause()
{
throw new NotImplementedException();
}
public void Resume()
{
throw new NotImplementedException();
}
}
这样,我们的游戏就大致完成了!
由于一开始没有弄清楚各个对象之间的相互关系,所以有些逻辑十分混乱,不够清晰,以后制作游戏一定会优先绘制关系图,分析每个类需要的函数、属性,这样能更快捷、高效地编程。
感谢阅读!