一. 游戏规则
在河岸边有三个牧师和三个魔鬼,他们需要通过一艘小船过河。然而,船的容量有限,每次只能携带两个人,而且船上至少要有一个人才能过河。在任何一边,如果牧师的数量少于魔鬼的数量,那么魔鬼将会吃掉牧师。游戏的目标是找到一种方法,使得所有牧师和魔鬼都能安全过河。
二. 游戏效果
B站视频链接:3D游戏编程与设计第四次作业之魔鬼与牧师_哔哩哔哩_bilibili
最终实现的游戏效果如下:
3D游戏编程与设计第四次作业之魔鬼与牧师
三. 游戏实现过程
1. 明确玩家的行为
玩家动作 | 游戏反应 |
点击岸上的牧师 | 牧师上船 |
点击岸上的魔鬼 | 魔鬼上船 |
点击船上的牧师 | 牧师上岸 |
点击船上的魔鬼 | 魔鬼上岸 |
点击船(船上有人) | 船开到对岸 |
点击“Restart” | 重新开始游戏 |
2. 制作游戏对象的预制体
然后要先找好各个预制体在屏幕中所处的位置,然后设置好预制体的Position
3. 设计游戏的MVC框架
Model脚本:用于创建游戏对象,分别有几个游戏对象对应的脚本以及处理点击事件的Click和ClickAction,ClickAction是一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件。
Click是一个自定义的部件,包含ClickAction,将Click添加到对象上后,对象就可以在接收用户点击时调用DealClick()
View脚本:这里包含的是实现用户所看到的界面UserGUI
Control脚本:这里包含着很多作为游戏对象控制器的脚本,负责所有游戏对象的生成和变化,以及相应用户事件等
对应的MVC架构的UML图如下(按自己的理解画的,不知道对不对,包含——继承和引用两种关系):
4. 各个脚本的逻辑实现
Model脚本:
Boat.cs(其他游戏对象代码逻辑类似):
1. 创建一个名为boat的游戏对象,实例化为一个预制体,然后初始化游戏对象的位置;
2. 另外还要设置一些用于后续判断对象状态的变量,比如isRight为true的话说明船在右边,priestCount 和devilCount分别用于记录船上牧师和魔鬼的数量
3. 由于船遇到岸边要停下,所以给船增加碰撞组件BoxCollider;另外,当玩家点击船的时候船要向着对岸移动,因此还要添加一个上面自定义的Click组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Boat
{
public GameObject boat;//船对象
public Role[] roles;//船上的角色
public bool isRight;
public int priestCount, devilCount;
public Boat(Vector3 position) {
boat = GameObject.Instantiate(Resources.Load("Prefabs/boat", typeof(GameObject))) as GameObject;
boat.name = "boat";
boat.transform.position = position;
boat.transform.localScale = new Vector3(2.8f,0.4f,2);
roles = new Role[2];
isRight = false;
priestCount = devilCount = 0;
boat.AddComponent<BoxCollider>();
boat.AddComponent<Click>();
}
}
Click.cs
将Click添加到对象上后,对象就可以在接收用户点击时调用DealClick(),但是不同对象对于鼠标点击的相应不同(比如船和人物),所以还要写一个setClickAction()函数,它将一个包含DealClick具体实现的类对象传给clickAction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Click : MonoBehaviour
{
ClickAction clickAction;
public void setClickAction(ClickAction clickAction) {
this.clickAction = clickAction;
}
void OnMouseDown() {
clickAction.DealClick();
}
}
Control脚本 :
FirstController.cs
1. 这个脚本是对整个场景的控制,在实现游戏的时候我们只需创建一个空物体然后把这个脚本挂载在空物体上即可
2. 定义了一系列控制器如左岸控制器(leftShoreController)、右岸控制器(rightShoreController)、河流(river)、船控制器(boatController)、角色控制器数组(roleControllers)、移动控制器(moveController)对相应的游戏对象进行控制。
3. 继承了ISceneController, IUserAction两个接口类,具体实现这两个接口类里面的函数,IUserAction提供了用户动作的接口,ISceneController提供场景接口。
4. LoadResources()函数用于加载游戏资源,调用游戏控制器里面的create函数即可创建游戏对象并初始化位置,加载游戏资源时isRunning要设为true表示游戏运行状态。
5. Restart()函数用于重启游戏,每次都要重新运行太麻烦了,在界面创建一个Restart的按钮,点击的话就将已有的游戏对象销毁掉(排除掉保留的camera等),然后重新调用LoadResources()
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
public class FirstController : MonoBehaviour, ISceneController, IUserAction {
ShoreCtrl leftShoreController, rightShoreController;
River river;
BoatCtrl boatController;
RoleCtrl[] roleControllers;
MoveCtrl moveController;
bool isRunning;
//float time;
public void LoadResources() {
//role
roleControllers = new RoleCtrl[6];
for (int i = 0; i < 6; ++i) {
roleControllers[i] = new RoleCtrl();
roleControllers[i].CreateRole(Position.role_shore[i], i < 3 ? true : false, i);
}
//shore
leftShoreController = new ShoreCtrl();
leftShoreController.CreateShore(Position.left_shore);
leftShoreController.GetShore().shore.name = "left_shore";
rightShoreController = new ShoreCtrl();
rightShoreController.CreateShore(Position.right_shore);
rightShoreController.GetShore().shore.name = "right_shore";
//将人物添加并定位至左岸
foreach (RoleCtrl roleController in roleControllers)
{
roleController.GetRoleModel().role.transform.localPosition = leftShoreController.AddRole(roleController.GetRoleModel());
}
//boat
boatController = new BoatCtrl();
boatController.CreateBoat(Position.left_boat);
//river
river = new River(Position.river);
//move
moveController = new MoveCtrl();
isRunning = true;
//time = 60;
}
public void MoveBoat() {
if (isRunning == false || moveController.GetIsMoving()) return;
if (boatController.GetBoatModel().isRight) {
moveController.SetMove(Position.left_boat, boatController.GetBoatModel().boat);
}
else {
moveController.SetMove(Position.right_boat, boatController.GetBoatModel().boat);
}
boatController.GetBoatModel().isRight = !boatController.GetBoatModel().isRight;
}
public void MoveRole(Role roleModel) {
if (isRunning == false || moveController.GetIsMoving()) return;
if (roleModel.inBoat) {
if (boatController.GetBoatModel().isRight) {
moveController.SetMove(rightShoreController.AddRole(roleModel), roleModel.role);
}
else {
moveController.SetMove(leftShoreController.AddRole(roleModel), roleModel.role);
}
roleModel.onRight = boatController.GetBoatModel().isRight;
boatController.RemoveRole(roleModel);
}
else {
if (boatController.GetBoatModel().isRight == roleModel.onRight) {
if (roleModel.onRight) {
rightShoreController.RemoveRole(roleModel);
}
else {
leftShoreController.RemoveRole(roleModel);
}
moveController.SetMove(boatController.AddRole(roleModel), roleModel.role);
}
}
}
public void Check() {
if (isRunning == false) return;
this.gameObject.GetComponent<UserGUI>().gameMessage = "";
if (rightShoreController.GetShore().priestCount == 3) {
this.gameObject.GetComponent<UserGUI>().gameMessage = "Congratulations! Win!";
isRunning = false;
}
else {
int leftPriestCount, rightPriestCount, leftDevilCount, rightDevilCount;
leftPriestCount = leftShoreController.GetShore().priestCount + (boatController.GetBoatModel().isRight ? 0 : boatController.GetBoatModel().priestCount);
rightPriestCount = rightShoreController.GetShore().priestCount + (boatController.GetBoatModel().isRight ? boatController.GetBoatModel().priestCount : 0);
leftDevilCount = leftShoreController.GetShore().devilCount + (boatController.GetBoatModel().isRight ? 0 : boatController.GetBoatModel().devilCount);
rightDevilCount = rightShoreController.GetShore().devilCount + (boatController.GetBoatModel().isRight ? boatController.GetBoatModel().devilCount : 0);
if (leftPriestCount != 0 && leftPriestCount < leftDevilCount || rightPriestCount != 0 && rightPriestCount < rightDevilCount) {
this.gameObject.GetComponent<UserGUI>().gameMessage = "Pity! Game Over!";
isRunning = false;
}
}
}
void Awake() {
SSDirector.GetInstance().CurrentSceneController = this;
LoadResources();
this.gameObject.AddComponent<UserGUI>();
}
void Update() {
}
public void Restart() {
// 获取场景中的所有游戏对象
GameObject[] gameObjects = FindObjectsOfType<GameObject>();
// 销毁所有游戏对象
for (int i = 0; i < gameObjects.Length; i++)
{
// 排除一些你不想销毁的游戏对象
if (gameObjects[i].name != "Camera" && gameObjects[i].name !="GameObject" && gameObjects[i].name !="Directional Light")
{
// 使用Destroy或DestroyImmediate销毁游戏对象
Destroy(gameObjects[i]);
//DestroyImmediate(gameObjects[i]);
}
}
LoadResources();
}
}
IUserAction.cs
定义了玩家行为的接口:移动船,移动角色,重新开始游戏,查看游戏状态等,具体实现在FirstController.cs中
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//玩家行为
public interface IUserAction {
void MoveBoat();
void MoveRole(Role roleModel);
void Check();//查看游戏进行的状态,是否结束以及是否应该更新游戏状态
void Restart();
}
RoleCtrl.cs (游戏对象控制器以角色控制器为例,其他是类似逻辑)
1. 角色控制器主要控制角色的行为,由于角色被点击后要有响应,所以要实现ClickAction的接口,实现里面DealClick的接口
2. CreateRole用于创建实例化角色——牧师或者魔鬼,并为角色的点击部件增加一个实例化的接口(通过setClickAction())
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class RoleCtrl : ClickAction
{
Role roleModel;
IUserAction userAction;
public RoleCtrl() {
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
}
public void CreateRole(Vector3 position, bool isPriest, int id) {
if (roleModel != null) {
Object.DestroyImmediate(roleModel.role);
}
roleModel = new Role(position, isPriest, id);
roleModel.role.GetComponent<Click>().setClickAction(this);
}
public Role GetRoleModel() {
return roleModel;
}
public void DealClick() {
userAction.MoveRole(roleModel);
}
}
Move.cs
1. 这个脚本是实现了移动的组件
2.具体逻辑是:
·当前游戏对象的位置是否达到了目标位置destination,如果达到了,则将isMoving设置为false,并返回
·当前游戏对象的位置还没有达到目标位置,则将isMoving设置为true
·接下来,判断游戏对象的当前位置是否同时与目标位置在x轴和y轴上都不相等,如果是,则将游戏对象的位置移动向中间位置mid_destination,移动的速度为speed * Time.deltaTime
·如果游戏对象的当前位置与目标位置在x轴和y轴上有一个或两个相等,则将游戏对象的位置移动向目标位置destination,移动的速度为speed * Time.deltaTime
·总之,实现了游戏对象朝着目标位置移动
3. 对应的MoveCtrl.cs实现了对某个对象设置移动条件和状态(定义对象并为其添加Move组件即可)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move : MonoBehaviour
{
public bool isMoving = false;
public float speed = 5;
public Vector3 destination;
public Vector3 mid_destination;
// Update is called once per frame
void Update()
{
if (transform.localPosition == destination) {
//到达目的地,不再移动,返回
isMoving = false;
return;
}
isMoving = true;
if (transform.localPosition.x != destination.x && transform
.localPosition.y != destination.y) {
//与目的地的x和y都不相同则朝着中间位置移动
transform.localPosition = Vector3.MoveTowards(transform.localPosition, mid_destination, speed * Time.deltaTime);
}
else {
//其中一个相同了就朝着destination移动
transform.localPosition = Vector3.MoveTowards(transform.localPosition, destination, speed * Time.deltaTime);
}
}
}
Position.cs
定义了所有游戏对象的初始位置,可用Position.直接调用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Position //存储所有对象的位置
{
//固定位置(世界坐标)
public static Vector3 left_shore = new Vector3(-8,-3,0);
public static Vector3 right_shore = new Vector3(8,-3,0);
public static Vector3 river = new Vector3(0,-4,0);
public static Vector3 left_boat = new Vector3(-2.3f,-2.3f,-0.4f);
public static Vector3 right_boat = new Vector3(2.4f, -2.3f, -0.4f);
//角色相对于岸边的位置(相对坐标)
public static Vector3[] role_shore = new Vector3[] {new Vector3(0.4f,0.77f,0), new Vector3(0.2f,0.77f,0), new Vector3(0,0.77f,0), new Vector3(-0.2f,0.77f,0), new Vector3(-0.4f,0.77f,0), new Vector3(-0.6f,0.77f,0)};
//角色相对于船的位置(相对坐标)
public static Vector3[] role_boat = new Vector3[] {new Vector3(0.2f,3,0), new Vector3(-0.2f,3,0)};
}
View脚本:
UserGUI.cs
该脚本主要用于实现用户看到的界面,这里没啥解释的了
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UserGUI : MonoBehaviour
{
IUserAction userAction;
public string gameMessage ;
//public int time;
GUIStyle style, bigstyle;
// Start is called before the first frame update
void Start()
{
//time = 60;
userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
style = new GUIStyle();
style.normal.textColor = Color.white;
style.fontSize = 30;
bigstyle = new GUIStyle();
bigstyle.normal.textColor = Color.white;
bigstyle.fontSize = 50;
}
// Update is called once per frame
void OnGUI() {
userAction.Check();
GUI.Label(new Rect(250, 100, 50, 200), gameMessage, style);
if(GUI.Button(new Rect(50, Screen.height * 0.05f, 50, 50), "Restart")){
userAction.Restart();
};
}
}
5. 代码位置:gitee位置
参考师兄博客:师兄博客