unity 3D 牧师与魔鬼小游戏MVC.Ver

MVC架构概述

在游戏设计中,MVC(Model-View-Controller)结构也可以用来组织代码和逻辑,尤其是对于游戏的用户界面(UI)和游戏逻辑部分,通常称为MVC游戏设计模式。以下是MVC游戏设计模式的主要组成部分和如何运作的概述:

1. Model(模型):游戏中的模型代表了游戏的数据和业务逻辑。这包括玩家的角色、敌人、游戏规则、关卡数据等。模型管理和存储这些数据,并提供用于访问和操作数据的方法。模型通常是游戏的核心,但与用户界面和输入无关。

2. View(视图):游戏中的视图包括所有可见的元素,如游戏画面、UI元素、分数板等。视图负责将模型的数据可视化呈现给玩家。视图还可以接收玩家的输入(例如点击、触摸、键盘输入)并将其传递给控制器。在游戏中,视图通常由游戏场景、粒子效果、UI元素和3D模型组成。

3. Controller(控制器):游戏中的控制器充当模型和视图之间的中介,处理用户输入和协调模型和视图之间的交互。控制器接收来自视图的用户输入,并根据业务逻辑调用模型来更新游戏状态,然后将新的状态反映在视图中。控制器还可以处理游戏的各种规则、逻辑和事件。

MVC游戏设计模式的运作过程如下:

1. 玩家与游戏视图进行交互,例如点击按钮、移动角色或执行操作。

2. 游戏视图将用户输入传递给控制器。

3. 控制器接受用户输入,根据游戏规则和业务逻辑处理输入,并调用模型来更新游戏状态。

4. 模型更新游戏数据和状态。

5. 视图观察模型的变化,并根据新的游戏状态来更新游戏画面和UI。

这种结构有助于游戏开发人员分离游戏逻辑、数据和界面,使游戏更易于开发和维护。它还有助于实现游戏的可扩展性,因为你可以更轻松地添加新的视图或模型,而不必改变现有的游戏逻辑。这种分离也有助于实现多平台支持,因为游戏逻辑通常可以保持不变,只需根据不同平台的需求创建不同的视图。

游戏基本规则

简单说明:

在河的一岸有3个牧师和3个魔鬼,他们需要借助船渡到河的对岸,船最多同时承载2个游戏角色。

你的任务是:让所有牧师角色都渡到河的对岸,游戏胜利。 

需要注意:当一侧岸上的魔鬼数量大于牧师数量时,魔鬼就会吃掉这个岸上的牧师,游戏失败。

游戏中提及的事物:魔鬼,牧师,船,两岸。

设计结构和对应UML图

Model:

Role、Boat、Shore、River、Click(自定义的部件Click,用于处理点击事件)、ClickAction(一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件)、Position((处理游戏对象的初始坐标))

Controller:

SSDirector、ISceneController、IUserAction分别定义了导演类(只有一个实例)、资源控制接口和用户操作接口。

在这个游戏里,只有唯一一个场景(FirstController)。场景控制器管理所有该场景内的模型控制器,并且实现他们的综合行为。就这个游戏而言,FirstController会生成并且管理:一个BoatController,两个ShoreController,六个RoleController,一个MoveController。所有该场景内的综合行为都会在这个场景控制器内实现。比如:移动船,涉及到船和乘客的同时移动;移动角色,涉及到离岸、登船、人状态的转变三方信息;判断当前游戏是否成功或者失败。

以I开头的控制器都是接口,用于规范同一类型的控制器应当具有的行为,比如IObjectContoller应当被所有模型的控制器继承、IScenceController应当被所有场景的控制器继承、IUserAction负责与视图方面沟通的接口。

游戏具体实现

基本结构

Resources为游戏对象的预制和材质

Scenes为游戏所用的场景

由于我们在该游戏中并没有切换场景,所以只需要使用默认场景即可

Scripts脚本结构

(1)Models
包含两个部分:

  • 包含各个对象的模块,每个模块中定义了对象的类
  • 包含自定义的部件Click,用于处理点击事件

(2)Controllers
这是整个项目中的控制部分,由它负责所有游戏对象的生成和变化,以及相应用户事件。
其中最核心的部分是FirstController.

(3)Views
定义了用户看到的界面

代码实现

Models

角色Role(priest / devil):

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Role 
{
    public GameObject role;//model对应的游戏对象
    public bool isPriest;
    public bool inBoat;
    public bool onRight;
    public int id;
    
    public Role (Vector3 position, bool isPriest, int id) {
        this.isPriest = isPriest;
        this.id = id;
        onRight = false;
        inBoat = false;
        role = GameObject.Instantiate(Resources.Load("Prefabs/" + (isPriest ? "priest" : "devil"), typeof(GameObject))) as GameObject;
        role.transform.localScale = new Vector3(1,1.2f,1);
        role.transform.position = position;
        role.name = "role" + id;
        role.AddComponent<Click>();
        role.AddComponent<BoxCollider>();
    }
}

船Boat:

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>();
        
    }
}

河流River:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class River
{
    public GameObject river;
    public River(Vector3 position) {
        river = GameObject.Instantiate(Resources.Load("Prefabs/river", typeof(GameObject))) as GameObject;
        river.transform.localScale = new Vector3(8,2.5f,2);
        river.transform.position = position;
        river.name = "river";
    }
}

河岸Shore:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Shore 
{
    public GameObject shore;
    public int priestCount, devilCount;
    public Shore (Vector3 position){
        shore = GameObject.Instantiate(Resources.Load("Prefabs/left_shore", typeof(GameObject))) as GameObject;
        shore.transform.localScale = new Vector3(8,4.8f,2);
        shore.transform.position = position;
        priestCount = devilCount = 0;
    }
}

Click与ClickAction:

船和角色都是游戏中用户可以通过点击操作控制的对象,所以它们都附加了Click部件(这个部件是自定义的)和碰撞部件。

ClickAction是一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件。
Click是一个自定义的部件,包含ClickAction,将Click添加到对象上后,对象就可以在接收用户点击时调用DealClick()。
由于角色和船的鼠标响应显然是不同的,所以它们调用的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();
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ClickAction
{
    void DealClick();
}

Controllers:

角色控制器RoleCtrl:

  1. RoleCtrl 类包含了以下成员变量:

    • roleModel:用于存储游戏角色的数据模型。
    • userAction:用于存储实现了 IUserAction 接口的对象。
  2. 构造函数 RoleCtrl()

    • 初始化 userAction,将其设置为当前场景的用户操作接口。这里使用了 SSDirector.GetInstance().CurrentSceneController 来获取当前场景的控制器,并将其转换为 IUserAction 接口。
  3. CreateRole(Vector3 position, bool isPriest, int id) 方法:

    • 用于创建游戏角色。
    • 如果 roleModel 不为空,会立即销毁之前的角色对象。
    • 创建一个新的角色对象 roleModel,并传递给它的构造函数一个位置 position、角色类型 isPriest(是牧师还是恶魔)和角色的ID id
    • 最后,将角色的点击事件处理方法设置为当前的 RoleCtrl 实例,以便处理点击事件。
  4. GetRoleModel() 方法:

    • 用于获取 roleModel,即当前的游戏角色数据模型。
  5. DealClick() 方法:

    • 用于处理点击事件。
    • 调用 userActionMoveRole() 方法,将当前的 roleModel 传递给它,以触发游戏中角色的移动操作。

总的来说,实现了以下功能:

  • 创建和控制游戏中的角色对象。
  • 处理点击事件,触发游戏中角色的移动操作。
  • 将用户的点击操作与游戏中的角色交互进行了关联。
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);
    }
}

船控制器BoatCtrl:

  1. BoatCtrl 类包含了以下成员变量:

    • boatModel:用于存储游戏船只的数据模型。
    • userAction:用于存储实现了 IUserAction 接口的对象。
  2. 构造函数 BoatCtrl()

    • 初始化 userAction,将其设置为当前场景的用户操作接口。这里使用了 SSDirector.GetInstance().CurrentSceneController 来获取当前场景的控制器,并将其转换为 IUserAction 接口。
  3. CreateBoat(Vector3 position) 方法:

    • 用于创建游戏船只。
    • 如果 boatModel 不为空,会立即销毁之前的船只对象。
    • 创建一个新的船只对象 boatModel,并传递给它的构造函数一个位置 position
    • 最后,将船只的点击事件处理方法设置为当前的 BoatCtrl 实例,以便处理点击事件。
  4. GetBoatModel() 方法:

    • 用于获取 boatModel,即当前的游戏船只数据模型。
  5. AddRole(Role roleModel) 方法:

    • 用于将角色从岸上移动到船上。
    • 根据船上的位置,确定是否有空位可以放置角色。
    • 如果有空位,将角色添加到船上,设置角色在船上的标志,并更新船上的牧师和恶魔数量。
    • 返回角色在船上的新位置。
  6. RemoveRole(Role roleModel) 方法:

    • 用于将角色从船上移动到岸上。
    • 遍历船上的位置,查找包含指定角色的位置,然后将该位置清空,并更新船上的牧师和恶魔数量。
  7. DealClick() 方法:

    • 用于处理船只的点击事件。
    • 如果船上有角色,就调用 userActionMoveBoat() 方法,以触发船只的移动操作。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BoatCtrl : ClickAction
{
    Boat boatModel;
    IUserAction userAction;

    public BoatCtrl() {
        userAction = SSDirector.GetInstance().CurrentSceneController as IUserAction;
    }
    public void CreateBoat(Vector3 position) {
        if (boatModel != null) {
            Object.DestroyImmediate(boatModel.boat);
        }
        boatModel = new Boat(position);
        boatModel.boat.GetComponent<Click>().setClickAction(this);
    }

    public Boat GetBoatModel() {
        return boatModel;
    }

    //将角色从岸上移动到船上,返回接下来角色应该到达的位置??
    public Vector3 AddRole(Role roleModel) {
        int index = -1;
        if (boatModel.roles[0] == null) index = 0;
        else if (boatModel.roles[1] == null) index = 1;

        if (index == -1) return roleModel.role.transform.localPosition;

        boatModel.roles[index] = roleModel;
        roleModel.inBoat = true;
        roleModel.role.transform.parent = boatModel.boat.transform;
        if (roleModel.isPriest) boatModel.priestCount++;
        else boatModel.devilCount++;
        return Position.role_boat[index];
    }

    //将角色从船上移到岸上
    public void RemoveRole(Role roleModel) {
        for (int i = 0; i < 2; ++i){
            if (boatModel.roles[i] == roleModel) {
                boatModel.roles[i] = null;
                if (roleModel.isPriest) boatModel.priestCount--;
                else boatModel.devilCount--;
                break;
            }
        }
    }

    public void DealClick() {
        if (boatModel.roles[0] != null || boatModel.roles[1] != null) {
            userAction.MoveBoat();
        }
    }
}

河岸控制器ShoreCtrl:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShoreCtrl
{
    Shore shoreModel;
    public void CreateShore(Vector3 position) {
        if (shoreModel == null) {
            shoreModel = new Shore(position);
        }
    }
    public Shore GetShore() {
        return shoreModel;
    }

    //将角色添加到岸上,返回角色在岸上的相对坐标
    public Vector3 AddRole(Role roleModel) {
        roleModel.role.transform.parent = shoreModel.shore.transform;
        roleModel.inBoat = false;
        if (roleModel.isPriest) shoreModel.priestCount++;
        else shoreModel.devilCount++;
        return Position.role_shore[roleModel.id];
    }

    //将角色从岸上移除
    public void RemoveRole(Role roleModel) {
        if (roleModel.isPriest) shoreModel.priestCount--;
        else shoreModel.devilCount--;
    }
}

Move和MoveCtrl:

Move是一个部件,控制对象的移动,主要有三个重要的变量,当前位置、目标位置和中转位置,Move的目的就是呈现出对象往目的位置移动的过程,中转位置是必要的,否则可能会出现类似穿墙的情况。
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) {
            transform.localPosition = Vector3.MoveTowards(transform.localPosition, mid_destination, speed * Time.deltaTime);
        }
        else {
            transform.localPosition = Vector3.MoveTowards(transform.localPosition, destination, speed * Time.deltaTime);
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveCtrl 
{
    GameObject moveObject;
    public bool GetIsMoving() {
        return (moveObject != null && moveObject.GetComponent<Move>().isMoving == true);
    }

    public void SetMove(Vector3 destination, GameObject moveObject) {
        Move test;
        this.moveObject = moveObject;
        if (!moveObject.TryGetComponent<Move>(out test)) {
            moveObject.AddComponent<Move>();
        }

        this.moveObject.GetComponent<Move>().destination = destination;
        if (this.moveObject.transform.localPosition.y > destination.y) {
            this.moveObject.GetComponent<Move>().mid_destination = new Vector3(destination.x, this.moveObject.transform.localPosition.y, destination.z);
        }
        else {
            this.moveObject.GetComponent<Move>().mid_destination = new Vector3(this.moveObject.transform.localPosition.x, destination.y, destination.z);
        }
    }
}

SSDirector、ISceneController、IUserAction:

分别定义了导演类(只有一个实例)、资源控制接口和用户操作接口。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SSDirector : System.Object
{
    static SSDirector _instance;
    public ISceneController CurrentSceneController {get; set;}
    public static SSDirector GetInstance() {
        if (_instance == null) {
            _instance = new SSDirector();
        }
        return _instance;
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface ISceneController
{
    void LoadResources();
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public interface IUserAction {
    void MoveBoat();
    void MoveRole(Role roleModel);
    void Check();
}

场景控制器FirstController:

FirstController可谓是所有控制器的主管,它指挥控制器们的行为,与此同时,它还负责在游戏中加载资源、创建对象,以及实现了用户操作的响应、更新游戏界面、保存游戏状态,等等。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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 = 120;


    }

    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 = "You 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 = "Game Over!";
                isRunning = false;
            }
        }
    }

    void Awake() {
        SSDirector.GetInstance().CurrentSceneController = this;
        LoadResources();
        this.gameObject.AddComponent<UserGUI>();
    }

    void Update() {
        if (isRunning) {
            time -= Time.deltaTime;
            this.gameObject.GetComponent<UserGUI>().time = (int)time;
            if (time <= 0) {
                this.gameObject.GetComponent<UserGUI>().time = 0;
                this.gameObject.GetComponent<UserGUI>().gameMessage = "Game Over!";
                isRunning = false;
            }
        }
    }
}

Views:

用于在游戏中显示用户界面的脚本 UserGUI。它负责在游戏画面中显示游戏信息,包括游戏的标题、消息以及剩余时间。以下是代码的功能解释:

  1. UserGUI 类继承了 MonoBehaviour,这是Unity脚本的基类。

  2. IUserAction userAction:用于存储实现了 IUserAction 接口的对象,这是用于处理用户操作的接口。

  3. public string gameMessage:用于存储游戏的消息,如游戏胜利或失败的消息。

  4. public int time:用于存储游戏中的剩余时间。

  5. Start() 方法中进行了初始化:

    • 设置初始时间为120秒。
    • 获取当前场景的控制器,并将其转换为 IUserAction 接口。
    • 创建两个 GUIStylestyle 用于一般文本显示,bigstyle 用于较大的文本。
  6. OnGUI() 方法用于在游戏界面上绘制用户界面元素:

    • 调用 userAction.Check() 方法,可能用于检查游戏的状态或事件。
    • 使用 GUI.Label() 在游戏界面上绘制文字标签:
      • 第一个标签用于显示游戏标题 "Preists and Devils",使用 bigstyle 进行显示。
      • 第二个标签用于显示游戏消息,消息内容存储在 gameMessage 变量中,使用 style 进行显示。
      • 第三个标签用于显示剩余时间,显示 "Time: " 加上剩余时间,使用 style 进行显示。

这段代码的作用是在游戏中显示游戏标题、消息和剩余时间,以向玩家提供游戏的信息和反馈。通过在 OnGUI() 方法中绘制GUI元素,玩家可以实时了解游戏状态和进展。其中,gameMessagetime 的值会根据游戏的进展和规则进行更新。

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 = 120;
        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(160, Screen.height * 0.1f, 50, 200), "Preists and Devils", bigstyle);
        GUI.Label(new Rect(250, 100, 50, 200), gameMessage, style);
        GUI.Label(new Rect(0,0,100,50), "Time: " + time, style);
    }
}

游戏演示

视频:unity 3D 牧师与魔鬼小游戏MVC.Ver_哔哩哔哩_bilibili

参考博客:unity 实现游戏——牧师与魔鬼-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值