牧师与魔鬼

1. 游戏规则

三名牧师与三名魔鬼来到了河的两岸,河上只有一条船,船上一次只能乘坐两个人。只有在船上有人时,船才能移动。

两岸中的任意一岸,只要魔鬼的数量大于牧师的数量,魔鬼就会吃掉牧师,游戏结束。

游戏胜利的条件是:所有牧师与魔鬼都到达对岸。

在本游戏中,黑山羊代表魔鬼,白绵羊代表牧师。


2. 素材准备

总体界面概览

image-20241012155703375

预制游戏对象

恶魔与牧师采用了Unity Asset Store中的黑山羊/白绵羊素材。船、石头与水是普通立方体。天空采用了skybox素材包。

预制体

材料

暂停游戏、继续游戏、重新开始、显示规则的按钮贴图。

UI贴图

游戏UML图

UML

由于商店中的羊是不规则建模,不能简单使用球形、立方体碰撞体等,因此我使用了一个胶囊碰撞体并调整参数,以便处理点击事件。

绵羊模型

碰撞体参数


3. 编写脚本

脚本代码有参考。

3.1 Models

陆地模型

功能:表示游戏中的陆地,包括起点和终点两块区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LandModel{
GameObject land;
Vector3[] positions;
int land_type; //-1为目的地,1为起始地
RoleModel[] roles = new RoleModel[6];

public LandModel(string land_type_string){
positions = new Vector3[] {new Vector3(5.3F,-0.5F,0),
new Vector3(6.1F,-0.5F,0),
new Vector3(6.9F,-0.5F,0),
new Vector3(7.7F,-0.5F,0),
new Vector3(8.5F,-0.5F,0),
new Vector3(9.3F,-0.5F,0)};

if (land_type_string == "start"){
land = Object.Instantiate(Resources.Load("Perfabs/Stone", typeof(GameObject)), new Vector3(8, -1.5F, 0), Quaternion.identity) as GameObject;
land_type = 1;
}
else if (land_type_string == "end"){
land = Object.Instantiate(Resources.Load("Perfabs/Stone", typeof(GameObject)), new Vector3(-8, -1.5F, 0), Quaternion.identity) as GameObject;
land_type = -1;
}
}
...
}

方法:

GetEmptyIndex():获取空位的索引。

AddRoleOnLand():在陆地上添加角色。

DeleteRoleByName():根据名字删除陆地上的角色。

Reset():重置陆地,清空角色。


船模型

功能:表示游戏中的船,负责角色的跨岸移动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BoatModel{
GameObject boat;
Vector3[] start_vacancy;
Vector3[] end_vacancy;
Move move;
Click click;
int boat_sign = 1;
RoleModel[] roles = new RoleModel[2];

public BoatModel(){
boat = Object.Instantiate(Resources.Load("Perfabs/Boat", typeof(GameObject)), new Vector3(4, -1.3F, 0), Quaternion.identity) as GameObject;
boat.name = "boat";
move = boat.AddComponent(typeof(Move)) as Move;
click = boat.AddComponent(typeof(Click)) as Click;
click.SetBoat(this);
start_vacancy = new Vector3[] { new Vector3(3.5F, -1, 0), new Vector3(4.5F, -1, 0) };
end_vacancy = new Vector3[] { new Vector3(-4.5F, -1, 0), new Vector3(-3.5F, -1, 0) };
}
}

方法

  • BoatMove():移动船只,并根据船的位置调整船上角色的朝向(起点时朝向60度,终点时朝向-120度)。
  • AddRoleOnBoat():在船上添加角色。
  • RemoveRoleFromBoat():角色从船上下到陆地时调整角色朝向。
  • Reset():重置船的位置并清空角色

角色模型

功能:表示游戏中的角色,包含牧师和魔鬼两种角色,负责角色的移动及朝向控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RoleModel{
GameObject role;
int role_sign;
Click click;
Move move;
bool on_boat;

LandModel land_model = (SSDirector.GetInstance().CurrentScenceController as Controllor).start_land;

public RoleModel(string role_name){
if (role_name == "priest"){
role = Object.Instantiate(Resources.Load("Perfabs/Priest", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -120, 0)) as GameObject;
role_sign = 0;
}
else{
role = Object.Instantiate(Resources.Load("Perfabs/Devil", typeof(GameObject)), Vector3.zero, Quaternion.Euler(0, -120, 0)) as GameObject;
role_sign = 1;
}
move = role.AddComponent(typeof(Move)) as Move;
click = role.AddComponent(typeof(Click)) as Click;

click.SetRole(this);
}

方法

  • Move():让角色移动到指定位置。
  • MoveToLand():将角色从船上移到陆地。
  • MoveToBoat():将角色从陆地移到船上。
  • SetFaceToStartLand()SetFaceToEndLand():设置角色在起始岸和终点岸的朝向。
  • Reset():重置角色位置和朝向。

计时器

功能:用于记录游戏时间。

成员变量:记录小时、分钟、秒等。

方法

  • Update():每秒更新时间。
  • Reset():重置时间。
  • StopTiming():暂停计时。
  • beginTiming():开始计时

移动

功能:控制角色的移动动画。

方法

  • MovePosition():通过插值的方式移动角色或船到指定位置

鼠标点击

功能:控制用户点击的交互行为。

方法

  • OnMouseDown():点击船时移动船,点击角色时移动角色。


3.2 Controller

该类继承了MonoBehaviour, ISceneController, IUserAction类,这个类负责整个游戏的控制逻辑,包括资源加载、移动船只、移动角色、检查游戏状态、以及重新开始游戏。

加载资源

创建了水、两块陆地、船、三个牧师以及三个恶魔,并规定它们的位置。

移动船只

1
2
3
4
5
6
7
8
9
public void MoveBoat(){
if (judge.IsEmptyBoat(boat) || !judge.isPlaying()) {
return;
}
boat.BoatMove();
int gamestate = judge.Check(start_land, destination, boat);
if (gamestate > 0) timer.StopTiming();
}

该函数用于控制船的移动。只有在船上有角色并且游戏处于进行状态时,船才能移动。

移动后会触发 Judge.Check 来检测当前游戏状态。

移动角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public void MoveRole(RoleModel role){
if (!judge.isPlaying()) return;

if (judge.IsOnBoat(role)){
LandModel land;
if (boat.GetBoatSign() == -1)
land = destination;
else
land = start_land;

boat.DeleteRoleByName(role.GetName());
role.Move(land.GetEmpty());
role.MoveToLand(land);
land.AddRoleOnLand(role);

if (land == start_land)
role.SetFaceToStartLand();
else
role.SetFaceToEndLand();
} else {
LandModel land = role.GetLandModel();
if (boat.GetEmptyIndex() == -1 || land.GetLandType() != boat.GetBoatSign()) return;

land.DeleteRoleByName(role.GetName());
role.Move(boat.GetEmpty());
role.MoveToBoat(boat);
boat.AddRoleOnBoat(role);
}
int gamestate = judge.Check(start_land, destination, boat);
if (gamestate > 0) timer.StopTiming();
}

角色的移动逻辑较为复杂:

  • 如果角色在船上,点击后它会回到船停靠的陆地。
  • 如果角色在陆地上且船有空位,并且船停在它所在的岸边,角色会上船。
  • 每次移动角色后,都会调用 Judge.Check 来检测游戏状态。
  • 根据角色上岸的陆地,调整角色的朝向。
  • 最终达成的效果是:当角色在两岸上时,朝向固定方向。当角色被船载着移动时,角色朝向船的移动方向。

3.3 Judge

Judge 类的主要功能是用于判断游戏状态(胜利、失败、进行中等),并实现了几个与游戏状态和规则相关的方法。

  • 判断船是否为空
    • 通过遍历船上的角色,检查船是否为空。如果船上的角色数组中有任何非空的元素,返回 false,否则返回 true
  • 判断角色是否在船上
    • 通过调用 RoleModel 中的 getIsOnBoat 方法,检查某个角色是否在船上。
  • 判断游戏是否结束
    • 首先获取起点和终点陆地上牧师和魔鬼的数量
    • 胜利条件:终点(destination)上的角色数量达到 6(表示所有角色都已成功过河)
    • 船上角色的数量更新:根据船的位置,将船上的角色数量加到起点或终点的数量上
    • 失败条件:在任意一侧(起点或终点),如果牧师的数量大于0并且小于魔鬼的数量(牧师会被魔鬼吃掉),游戏失败
    • 如果不满足胜利或失败条件,游戏继续进行


SSAction 是动作的基类,CCMoveToAction 实现了简单的移动,CCSequenceAction 实现了顺序执行多个动作。

ActionManager 负责管理所有动作的执行与更新。

SceneController 实现回调,处理动作完成后的逻辑

4. 视频演示

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2024 Boreascup

请我喝杯咖啡吧~

支付宝
微信