unity 实现游戏——牧师与魔鬼

unity 实现游戏——牧师与魔鬼

一、游戏介绍

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!

点击试玩

本次作业以此游戏为原型,在unity中实现类似的效果。

二、实现效果

在这里插入图片描述

三、实现过程
1. 定义玩家行为
玩家动作表
玩家动作效果备注
点击牧师或魔鬼牧师或魔鬼在船与岸之间切换必须点击船上或船所靠岸上的对象
点击船只船从一边的岸移动到另一边船上至少有一人,至多有两人
2. 做好各个对象的预制
游戏中的预制对象

在这里插入图片描述

3. 基于MVC框架确定脚本结构

(1)Models
包含两个部分:

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

在这里插入图片描述
(2)Controllers
这是整个项目中的控制部分,由它负责所有游戏对象的生成和变化,以及相应用户事件。
其中最核心的部分是FirstController,它的声明如下:
在这里插入图片描述
可见,它分别继承了IsceneController和IUserAction这两个接口类,前者定义了控制整个场景资源的接口,后者定义用户操作的接口。
在这里插入图片描述
(3)Views
定义了用户看到的界面
在这里插入图片描述

4. 逐步实现每一个脚本

建议在着手写代码前先手动搭建一下游戏的UI,确定各个对象的位置、大小等参数。
仅挑选其中较关键的部分进行介绍,完整代码已上传到github仓库。
(1)Models

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

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

Click与ClickAction
public class Click : MonoBehaviour
{
    ClickAction clickAction;
    public void setClickAction(ClickAction clickAction) {
        this.clickAction = clickAction;
    }
    void OnMouseDown() {
        clickAction.DealClick();
    }
}
public interface ClickAction
{
    void DealClick();
}

ClickAction是一个接口类,定义了唯一的一个接口DealClick(),用来处理点击事件。
Click是一个自定义的部件,包含ClickAction,将Click添加到对象上后,对象就可以在接收用户点击时调用DealClick()。
由于角色和船的鼠标响应显然是不同的,所以它们调用的DealClick()实现必然也不同,所以需要setClickAction,它将一个包含DealClick具体实现的类对象传给clickAction。

位置
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)};
}

(2)Controllers

船控制器
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();
        }
    }
}

船控制器主要做几件事情:

  • 创建船
  • 将角色从岸上移到船上
  • 将角色从船上移到岸上
  • 实现船的DealClick()函数

将角色从岸上移到船上时会返回角色安置在船上的相对位置,这个位置有什么作用呢?
作用就是作为参数传给Move,后者实现对象的移动(要有移动的效果,而不是闪现)
所以类似的,将角色从船上移到岸上时也需要返回角色安置在岸上的相对位置,不过这个不在船控制器中完成,而是在岸控制器中完成。

Move部件和Move控制器
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);
        }
    }
}

Move是一个部件,控制对象的移动,主要有三个重要的变量,当前位置、目标位置和中转位置,Move的目的就是呈现出对象往目的位置移动的过程,中转位置是必要的,否则可能会出现类似穿墙的情况。
Move控制器设置中转位置和目标位置,使得对象开始移动。

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

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

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;
    }
}
public interface ISceneController
{
    void LoadResources();
}
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 = 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 = "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;
            }
        }
    }
}


(3)Views

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

至此所有重要的代码已经介绍完毕。

四、项目链接

github地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值