Unity牧师与魔鬼小游戏
前言
这是中大计算机学院3D游戏编程课的一次作业,在这里分享一下设计思路。 主要代码上传到了gitee上,请按照后文的操作运行。 项目地址:https://gitee.com/cuizx19308024/unity-games/tree/master/hw2 成果视频:https://www.bilibili.com/video/BV16g411F7yF?spm_id_from=333.999.0.0
游戏说明
游戏规则
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 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!
游戏中提及的事物
牧师、魔鬼、船、河、两边的陆地
用表格列出玩家的动作表
玩家动作 | 执行条件 | 执行结果 |
---|
点击牧师/魔鬼 | 游戏进行中,且可以上岸/上船 | 上岸/上船 | 点击船 | 游戏进行中且船上有1或2个人 | 开船到另一边 |
项目组成和运行环境
由于文件过大,本项目仅上传了Assets文件夹。可以直接新建一个Unity项目,然后将Assets文件夹替换。 其中包括了Resources文件夹,包括Prefab对象预置、Material材料预制和Script脚本文件。使用时可以将Scripts文件夹中的Controller.cs 拖动到对象ScriptFactory 上。
设计与实现
MVC基本框架
本次将MVC三层分别放在了Models.cs ,Controller.cs 和UserGUI.cs 三个文件下。结构上与老师给过的结构基本相同。
- 接口、导演。包括
ISceneController 和IUserAction 两个接口,分别定义场景操作和用户操作。导演类SSDirector 采用单例模式,进行切换片场和类之间通信的操作。 - 模型。包括
RoleModel ,BoatModel ,LandModel 三个类,分别定义了其中可执行的操作和属性。 - 辅助脚本。包括
Movable 和Clickable 类,定义了移动的方式和在屏幕上的显示效果,并设置了点击模型的事件处理器,将点击信息发送给Controller来处理。 - 控制器。本次只有一个控制器,可以很方便地控制各model之间的操作和游戏逻辑判定。
- UI界面。UI界面可以接受游戏外的一些操作,如重新开始。
接口、导演
分别定义场景操作和用户操作:
//加载场景接口
public interface ISceneController
{
void LoadResources();
}
//用户操作接口
public interface IUserAction
{
void MoveBoat(); //移动船
void Restart(); //重新开始
void MoveRole(RoleModel role); //移动角色
int GameJudge(); //检测游戏结束
}
//导演类
public class SSDirector : System.Object
{
private static SSDirector _instance;
public ISceneController CurrentScenceController { get; set; }
public static SSDirector GetInstance()
{
if (_instance == null)
{
_instance = new SSDirector();
}
return _instance;
}
}
模型
由于代码过长,这里只说几个关键点:
- 角色模型
RoleModel :GameObject role;
int role_sign; //0为牧师,1为魔鬼
bool on_boat; //是否在船上
LandModel land = (SSDirector.GetInstance().CurrentScenceController as Controller).src_land;//所在的陆地
Clickable click;//给model赋予属性,可点击、可移动,相当于添加了脚本。
Movable move;
这里定义了游戏对象,并定义了所在的大陆和是否在船上,方便与其他两个模型关联。public RoleModel(int id, Vector3 pos)
{
if (id == 0)
{
role_sign = 0;
role = Object.Instantiate(Resources.Load("Prefabs/Priest", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
}
else
{
role_sign = 1;
role = Object.Instantiate(Resources.Load("Prefabs/Ghost", typeof(GameObject)), pos, Quaternion.identity) as GameObject;
}
move = role.AddComponent(typeof(Movable)) as Movable; //添加属性
click = role.AddComponent(typeof(Clickable)) as Clickable;
click.SetRole(this);
}
初始化对象,根据输入的pos确定游戏对象位置,并加载资源。对于move和click,相当于添加两个脚本,让它们可移动、可点击。public void ToLand(LandModel land)//上岸的移动
{
Vector3 pos = land.GetEmptyPosition();
move.MoveTo(pos);
this.land = land;
on_boat = false;
}
public void ToBoat(BoatModel boat)//上船的移动
{
Vector3 pos = boat.GetEmptyPosition();
move.MoveTo(pos);
this.land = null;
on_boat = true;
}
注意上船和上岸之后,要对本地的boat和land关联属性惊醒修改,并用MoveTo() 函数通知UI界面调整位置。 - 陆地模型
LandModel :public int land_mark;//src为1,des为-1。
这里用1和-1来标记大陆,可以直接用land_mark来计算对称的位置,比较方便。 此外,由于陆地不可以移动和点击,因此不需要处理相关的移动函数,只需要在逻辑层统计好陆地上现有的角色即可,并用AddRole() 和RemoveRole() 函数管理即可。 - 船模型
BoatModel : 首先,需要一个Total() 函数统计船上的人数,之后需要研究Move() 函数,注意注释:public void Move()//移动船只的同时,调用角色的函数Move,同时将角色移到指定位置。
{
if (boat_mark == -1)
{
move.MoveTo(new Vector3(4.5F, 0.5F, 0));
for (int i = 0; i < 2; i++)
{
if (roles[i] != null)
{
roles[i].Move(src_empty_pos[i]);
}
}
boat_mark = 1;
}
else
{
move.MoveTo(new Vector3(-4.5F, 0.5F, 0));
for (int i = 0; i < 2; i++)
{
if (roles[i] != null)
{
roles[i].Move(des_empty_pos[i]);
}
}
boat_mark = -1;
}
}
尤其要注意船在两边的时候船上空位的位置是不同的。
辅助脚本
Movable 类:public void MoveTo(Vector3 pos);//定义了移动到指定位置的UI动画,如何平移。
void Update();//自带的函数,可以逐帧决定如何移动。
Clickable 类:public class Clickable : MonoBehaviour
{
IUserAction action;
RoleModel role = null;
public void SetRole(RoleModel role)
{
this.role = role;
}
private void Start()
{
action = SSDirector.GetInstance().CurrentScenceController as IUserAction;//获取控制器
}
private void OnMouseDown()//点击时调用控制器中的相关函数,用gameObject的name来区分对象。
{
if (gameObject.name == "boat")
{
action.MoveBoat();
}
else
{
action.MoveRole(role);
}
}
}
控制器
这里只说明关键点:
start() 函数:// Start is called before the first frame update
void Start()
{
SSDirector director = SSDirector.GetInstance();
director.CurrentScenceController = this; //脚本在此处运行,故现让Controller指向自己。
user_gui = gameObject.AddComponent<UserGUI>() as UserGUI;//添加GUI属性
LoadResources();
}
LoadResources() 函数中,分别调用了类的构造函数进行初始化,尤其注意初始化模型之间关联的属性,要保持一致,可以参考注释。- 移动船和移动角色:
public void MoveBoat()
{
if (boat.Total() == 0 || user_gui.status != 0)
{//空船不可以移动,且必须为游戏进行状态
return;
}
boat.Move();
user_gui.status = GameJudge();//判断游戏结束
}
public void MoveRole(RoleModel role)
{
if (user_gui.status != 0)
{
return;
}
if (role.IsOnBoat())//人在船上,上岸
{
LandModel land;//确定上哪边的岸
if (boat.GetBoatMark() == -1)
{
land = des_land;
}
else
{
land = src_land;
}
boat.RemoveRole(role.GetName());//船操作
role.ToLand(land);//角色操作
land.AddRole(role);//陆地操作
}
else
{
LandModel land = role.GetLandModel();
if (boat.Total() == 2 || land.GetLandMark() != boat.GetBoatMark())
{//船已满或船不在此岸边
return;
}
land.RemoveRole(role.GetName());
role.ToBoat(boat);
boat.AddRole(role);
}
user_gui.status = GameJudge();
}
- 判断游戏结束。注意,游戏规则是开船之后,判断岸边魔鬼和牧师的数量。但实际上,在船靠一便停的时候,这一边的魔鬼和牧师数量多少是无所谓的,因此要等船开走后才能判断这边的数量关系:
if (boat.GetBoatMark() == 1)//由于在这一边船还没开,因此只需检测另一边的数量即可。
{
if (des_priest < des_ghost && des_priest > 0)
{//失败
return -1;
}
}
else
{
if (src_priest < src_ghost && src_priest > 0)
{//失败
return -1;
}
}
UI界面
注意这里的重新开始按钮,需要关联所对应的Controller,而这个Controller会从导演类获得。 action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
运行结果
请参考展示视频:传送门
|