Jiahonzheng's Blog

Unity 牧师与魔鬼

字数统计: 2.1k阅读时长: 9 min
2019/09/21 Share

阅读以下游戏脚本:

Priests and Devils

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!

程序需要满足的要求:

  • Play the game ( http://www.flash-game.net/game/2535/priests-and-devils.html )。
  • 列出游戏中提及的事物(Objects)。
  • 用表格列出玩家动作表(规则表),注意,动作越少越好。
  • 请将游戏中对象做成预制。
  • 在 GenGameObjects 中创建长方形、正方形、球及其色彩代表游戏中的对象。
  • 使用 C# 集合类型有效组织对象。
  • 整个游戏仅主摄像机 和 一个 Empty 对象, 其他对象必须代码动态生成!!! 。 整个游戏不许出现 Find 游戏对象, SendMessage 这类突破程序结构的通讯耦合语句。 违背本条准则,不给分
  • 请使用课件架构图编程,不接受非 MVC 结构程序
  • 注意细节,例如:船未靠岸,牧师与魔鬼上下船运动中,均不能接受用户事件!

项目地址github.com/Jiahonzheng/Unity-3D-Learning

在线演示demo.jiahonzheng.cn/Priests-and-Devils

演示视频Unity 牧师与魔鬼

GameObject

游戏涉及到的游戏对象(GameObject):Priests、Devils、Boat、Coast、River 。

玩家动作表

由于游戏规则并不算复杂,我们可以得到以下简单的玩家动作表。

动作 结果
点击 Character 人物 对应人物上船(离岸)、下船(上岸)
点击 Boat 船只 船只运动(过河)

Prefabs

游戏对象及其预设的关系如下。

GameObject Prefab 名称 3D Object 类型
Priest 牧师 Priest Cube
Devil 魔鬼 Devil Sphere
Boat 船只 Boat Cube
Coast 河岸 Stone Cube
River 河流 Water Cube

为了使得游戏对象变得更为美观,我们为其添加了 Material 贴图,以下是该游戏的预设资源预览。

基于职责的设计

设计一个游戏如同组织一场话剧,既然要搞话剧或游戏,就至少需要以下角色(划分职责):

  • 导演,1名(仅要一个)。
    • 具体类型:Director
    • 职责:把握全局,控制场景的切换。
  • 场记若干,话剧有很多场,每场需要一个。
    • 抽象类型:ISceneController
    • 职责:每一场的场记,控制布景、演员的上下场、管理动作等执行。
  • 吃瓜群众,1个。
    • 抽象类型:IUserAction
    • 职责:边吃瓜子边和场记聊天。

我们使用面向对象技术设计游戏,其核心是基于职责的设计。为此,我们设计 Director 类,其职责大致如下:

  • 获取当前游戏的场景
  • 管理游戏全局状态
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
namespace PriestsAndDevils
{
public class Director : System.Object
{
// Singlton instance.
private static Director instance;
public ISceneController currentSceneController { get; set; }
public bool running { get; set; }
public int fps
{
get
{
return Application.targetFrameRate;
}
set
{
Application.targetFrameRate = value;
}
}

public static Director GetInstance()
{
return instance ?? (instance = new Director());
}
}
}

由于本游戏场景不多,我们只有一位场记 GameController ,它通过 ISceneController 接口与导演 Director 交互。与此同时,我们玩家(“吃瓜群众”),需要与 GameController 互动,因此我们设计了 IUserAction 用户交互接口,也实现了用户行为与游戏系统规则计算的分离,实现解耦合

以下是 ISceneControllerIUserAction 接口的代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace PriestsAndDevils
{
public interface ISceneController
{
void LoadResources();
}

public interface IUserAction
{
void ClickBoat();
void ClickCharacter(CharacterController c);
void Reset();
}
}

以下是 GameController 的整体代码实现(有删改)。

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
33
34
35
36
37
38
39
40
namespace PriestsAndDevils
{
public class GameController : MonoBehaviour, ISceneController, IUserAction
{
void Awake()
{
// Set the current scene controller.
Director director = Director.GetInstance();
director.CurrentSceneController = this;
// Add GUI.
gui = gameObject.AddComponent<GameGUI>() as GameGUI;
// Load the resources.
LoadResources();
}

// Load the resources.
public void LoadResources()
{
GenGameObjects();
}

// It is called when player clicks the boat.
public void ClickBoat()
{
// ......
}

// It is called when player clicks a character.
public void ClickCharacter(CharacterController character)
{
// ......
}

// It is called when player resets the game.
public void Reset()
{
// ......
}
}
}

我们注意到,在场景被加载(awake)时,它也会自动注入导演,设置当前场景。

MVC

MVC是界面人机交互程序设计的一种架构模式,它把程序分为三个部分:

  • 模型(Model
    • 管理游戏对象、空间关系。
  • 控制器(Controller
    • 一个场景一个主控制器。
    • 至少实现与玩家交互的接口(IUserAction),接受用户事件,控制 Model 的变化。
    • 实现、管理 GameObject 的运动。
  • 界面(View
    • 渲染视图,接收并转发用户事件(点击等)至 Controller 处理。

Model

在实现中,我设计并实现了这些 ModelCharacterBoatCoast

Character 中,我们维护牧师和魔鬼的 nameLocationisOnboard

1
2
// 用于描述游戏对象的位置:位于左岸、位于右岸。
public enum Location { Left, Right };

BoatCoast 中,我们除了维护其 nameLocation 信息,还维护了空位乘客信息,具体请参考代码实现。

Controller

针对每一个 Model ,我实现了对应的 Controller ,用于控制对应游戏对象的运动,值得注意的是,它们都继承于 Moveable 类。

  • Moveable
    • SetDestination:使对应游戏对象运动至指定位置。
    • Reset:在重置游戏时使用。
  • CharacterController
    • GoAboard:当玩家点击牧师或魔鬼,使之上船时,被调用。
    • GoAshore:当玩家点击牧师或魔鬼,使之上岸时,被调用。
  • BoatController
    • Move:当玩家点击船只,使之运动时,被调用。
    • GoAboard:配合 CharacterControllerGoAboard 使用。
    • GoAshore:配合 CharacterControllerGoAshore 使用。
  • CoastController
    • GoAboard:配合 CharacterControllerGoAboard 使用。
    • GoAshore:配合 CharacterControllerGoAshore 使用。

GameController 中,我们通过调用子控制器,实现对游戏对象的运动控制。在 GameController 中,我使用了集合数据类型来管理游戏对象。

1
2
3
4
5
6
7
8
9
10
public class GameController : MonoBehaviour, ISceneController, IUserAction
{
public CoastController leftCoast;
public CoastController rightCoast;
public BoatController boat;
// 使用 集合数据类型 管理游戏对象。
public List<CharacterController> characters = new List<CharacterController>(6);
private GameGUI gui;
// ......
}

View

我们使用 GameGUI 作为 View 。在 Start 函数中,我们通过 Director 的全局单例,获取到了当前的视图控制器,即 GameController 。在 OnGUI 函数中,我们渲染视图;在 OnUpdate 函数中,我们监听捕捉用户的鼠标左键(Fire1)的点击事件,最后转发至 Controller 处理事件。

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
33
34
35
36
37
38
39
40
41
namespace PriestsAndDevils
{
public class GameGUI : MonoBehaviour
{
public Result result;
private IUserAction action;

// Use this for initialization
void Start()
{
result = Result.NOT_FINISHED;
action = Director.GetInstance().CurrentSceneController as IUserAction;
}

void OnGUI()
{
// ......
}

void Update()
{
if (Input.GetButtonDown("Fire1"))
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f))
{
var todo = hit.collider;
var character = todo.GetComponent<CharacterController>();
if (character)
{
action.ClickCharacter(character);
}
else if (todo.transform.name == "Boat")
{
action.ClickBoat();
}
}
}
}
}
}

为了获取用户所点击的 GameObject ,我们使用了射线捕捉技术:连接摄像机和用户在 ViewPort 所点击的点,构造一条射线,然后射线所照射的对象,即为我们想要获取的游戏对象。

GenGameObjects

我们在 GameController (与课件的 FirstController 对应)实现了 ISceneController 接口,实现了 LoadResources 方法。

1
2
3
4
5
// Load the resources.
public void LoadResources()
{
GenGameObjects();
}

在该方法中,我们调用了 GenGameObjects 方法,来生成各种 GameObject ,并为其添加对应的 MonoBehaviour

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
33
34
35
36
37
38
39
40
41
42
43
44
45
// It generates the GameObjects.
private void GenGameObjects()
{
// Generate River.
{
GameObject temp = Utils.Instantiate("Prefabs/Water", new Vector3(0, 0.5f, 0));
temp.name = "River";
}
// Generate LeftCoast.
{
GameObject temp = Utils.Instantiate("Prefabs/Stone", Coast.departure);
leftCoast = temp.AddComponent<CoastController>();
temp.name = leftCoast.name = "LeftCoast";
leftCoast.location = Location.Left;
}
// Generate RightCoast.
{
GameObject temp = Utils.Instantiate("Prefabs/Stone", Coast.destination);
rightCoast = temp.AddComponent<CoastController>();
temp.name = rightCoast.name = "RightCoast";
rightCoast.location = Location.Right;
}
// Generate Boat.
{
GameObject temp = Utils.Instantiate("Prefabs/Boat", Boat.departure);
boat = temp.AddComponent<BoatController>();
temp.name = boat.name = "Boat";
}
// Generate Priests.
for (int i = 0; i < 3; ++i)
{
GameObject temp = Utils.Instantiate("Prefabs/Priest", Coast.destination);
characters[i] = temp.AddComponent<CharacterController>();
temp.name = characters[i].name = "Priest" + i;
characters[i].GoAshore(rightCoast);
}
// Generate Devils.
for (int i = 0; i < 3; ++i)
{
GameObject temp = Utils.Instantiate("Prefabs/Devil", Coast.destination);
characters[i + 3] = temp.AddComponent<CharacterController>();
temp.name = characters[i + 3].name = "Devil" + i;
characters[i + 3].GoAshore(rightCoast);
}
}

演示

在线演示demo.jiahonzheng.cn/Priests-and-Devils

演示视频Unity 牧师与魔鬼

以下是游戏初始页面。

以下是玩家胜利的页面。

以下是玩家失败的页面。

CATALOG
  1. 1. GameObject
  2. 2. 玩家动作表
  3. 3. Prefabs
  4. 4. 基于职责的设计
  5. 5. MVC
    1. 5.1. Model
    2. 5.2. Controller
    3. 5.3. View
  6. 6. GenGameObjects
  7. 7. 演示