Unity3D小游戏——巡逻兵

网上已经有许多关于巡逻兵的作业博客,但是经过我自己运行后感觉很多要么代码量非常大,要么很简陋bug很多,我决定自己重新全部写一遍,用最少的代码完成本次作业,给后来的学弟学妹一些帮助。所有需要自己写的代码加起来不到三百行。为此我优化了好多天。

 运行方式,代码下载后即可直接运行,不过要注意需要打开Asserts中的floor场景

 

 

 

 本次实现用到以上七个代码文件。

UserGUI用于实现分数展示和开始按钮,它唯一需要与其他类交互的就是让导演开始游戏和从导演找到控制器,从控制器中拿到得分。

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

public class UserGUI : MonoBehaviour{

    private Director myDirector; 
    private GUIStyle msgStyle;
    private string showlabel;
    private float btn_x, btn_y, label_x, label_y;
    private float btn_width, btn_height, label_width, label_height;

    // 对GUI上的按钮和文字进行初始化
    void Start(){

        myDirector = Director.getinstance();

        msgStyle = new GUIStyle();
        msgStyle.normal.textColor = Color.black;
        msgStyle.alignment = TextAnchor.MiddleCenter;
        msgStyle.fontSize = 30;

        btn_width = 100;
        btn_height = 50;
        btn_x = Screen.width / 2 - btn_width / 2;
        btn_y = Screen.height / 2 - btn_height / 2;

        label_x = 50;
        label_y = 10;
        label_width = 100;
        label_height = 50;
    }


    private void OnGUI(){
        // 显示当前得分
        showlabel = $"Score:{myDirector.getCurrentController().score.get()}";
        GUI.Label(new Rect(label_x, label_y, label_width, label_height), showlabel, msgStyle);
        // 游戏停止时显示开始按钮
        if(myDirector.getPlaying() == false){
            if (GUI.Button(new Rect(btn_x, btn_y, btn_width, btn_height), "begin")){
                myDirector.startGame();
            }
        }
    }
}

导演类是系统对象,导演类只干两件事情:记录当前的控制器和管理游戏状态(有游戏中和结束两种状态),当游戏开始或者游戏结束时,导演通知控制器做出相应动作。

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

public class Director : System.Object{

    private static Director _instance;
    public Controller currentController = null;
    private bool isPlaying = false;
    
    public bool getPlaying(){
        return isPlaying;
    }

    public void startGame(){
        currentController.setGameStart();
        isPlaying = true;
    }

    public void overGame(){
        currentController.setGameOver();
        isPlaying = false;
    }

    public void setCurrentController(Controller c){
        currentController = c;
    }

    public Controller getCurrentController(){
        return currentController;
    }

    public static Director getinstance(){
        if (_instance == null){
            _instance = new Director();
        }
        return _instance;
    }
}

控制器管理着三个游戏对象:产生守卫的工厂,主角,游戏得分,在游戏开始和结束时,控制器会对这三个对象做一些初始化和收尾工作。管理器最重要的是Update函数,它会判断当前主角处于哪一个守卫的领地,如果发生了变化,就说明主角从一个领地移动到了另一个领地,然后通知原领地的守卫变成巡逻状态,新领地的守卫变成追逐状态。如果玩家通过一个有守卫守护的领地则加一分(同一领地出和入不能在同一地方,否则不加分,这就是prev变量的作用)

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

public class Controller : MonoBehaviour {

    private Director myDirector = Director.getinstance(); // 指挥本控制器的导演
    private Factory myFactroy = Factory.getinstance(); // 产生守卫的工厂
    public GameObject player; // 主角
    public Score score = new Score(); // 游戏得分

    private int curr; // 玩家当前处于的格子
    private int prev; // 玩家经过的上一个格子

    void Awake(){
        myDirector.setCurrentController(this);
        player = Instantiate(Resources.Load<GameObject>("player"));
        player.GetComponent<SimpleCharacterControl>().enabled = false;
    }

    public void setGameStart(){
        score.clear();
        myFactroy.initActors();
        player.transform.rotation = Quaternion.identity;
        player.transform.position = new Vector3(0f, 0f, 0f);
        player.GetComponent<SimpleCharacterControl>().enabled = true;
        curr = prev = -1;
    }

    public void setGameOver(){
        myFactroy.deleteActors();
        player.GetComponent<SimpleCharacterControl>().enabled = false;
    }

    public void Update(){
        if(!myDirector.getPlaying()) return;
        Vector3 playerPos = player.transform.position;
        int tmp = myFactroy.locate(playerPos);
        if(tmp != curr){
            if(curr != -1){
                myFactroy.select(curr).GetComponent<guardaction>().setChasing(false);
            }
            if(tmp != -1){
                myFactroy.select(tmp).GetComponent<guardaction>().setChasing(true);
            }
            // 如果玩家进入一个有守卫的区域且从另一个出口出去后,加一分
            if(tmp != prev){
                score.add();
            }
            prev = curr;
            curr = tmp;
        }
    }
}

工厂类中除了创建和删除守卫外,还记录了所有守卫的领地的信息,支持根据坐标查找守卫。

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

public class Factory : MonoBehaviour{

    private static Factory _instance = null;
    private List<GameObject> actorList = new List<GameObject>();

    private Vector3[] fieldBase = new Vector3[]{new Vector3(8f, 0f, -8f),
                                                new Vector3(8f, 0f, 8f),
                                                new Vector3(-8f, 0f, 8f),
                                                new Vector3(-24f, 0f, 8f),
                                                new Vector3(-24f, 0f, -8f),
                                                new Vector3(-24f, 0f, -24f),
                                                new Vector3(-8f, 0f, -24f),
                                                new Vector3(8f, 0f, -24f)};

    private Vector3 fieldSize = new Vector3(16f,0f,16f);

    public static Factory getinstance(){
        if(_instance == null){
            _instance = new Factory();
        }
        return _instance;
    }

    // 创建所有护卫
    public void initActors(){
        for(int i = 0; i < 8; i++){
            GameObject obj = Instantiate(Resources.Load<GameObject>("guard"), fieldBase[i] + fieldSize / 2, Quaternion.identity);
            actorList.Add(obj);
        }
    }

    // 清空所有护卫
    public void deleteActors(){
        for(int i = 0; i < actorList.ToArray().Length; i++){
            Destroy(actorList[i]);
        }
        actorList.Clear();
    }

    // 根据某一个位置选择监视此区域的护卫的id
    public int locate(Vector3 loc){
        if(loc.x > 8 && loc.z <= 8 && loc.z >= -8) return 0;
        if(loc.x > 8 && loc.z > 8) return 1;
        if(loc.x >= -8 && loc.x <= 8 && loc.z > 8) return 2;
        if(loc.x < -8 && loc.z > 8) return 3;
        if(loc.x < -8 && loc.z <= 8 && loc.z >= -8) return 4;
        if(loc.x < -8 && loc.z < -8) return 5;
        if(loc.x >= -8 && loc.x <= 8 && loc.z < -8) return 6;
        if(loc.x > 8 && loc.z < 8) return 7;
        return -1;
    }

    // 根据id选择gameObejct
    public GameObject select(int id){
        return actorList[id];
    }
}

守卫类,这个类会挂载到守卫预制体上,守卫会走一个凸四边形,当主角进入该守卫的领地时,管理器会通知守卫(修改其isChasing变量),守卫会根据这个状态变量决定其行为,如果为false,则守卫选择下一个巡逻点作为目标,如果为true,守卫选择主角的位置作为目标。

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

public class guardaction : MonoBehaviour {

    public float delta = 1f; // 判断到达目标位置的阈值
    public float range = 3f; // 巡逻半径
    
    private int order; // 守卫的下一个巡逻点
    private bool isChasing; // 当前是否正在追逐玩家
    private GameObject player; // 玩家
    private Vector3[] pos; // 守卫的巡逻点
    private Vector3 target; // 守卫运动的目标位置

    // 初始化所有private变量
    void Start () {
        order = 0;
        isChasing = false;
        player = Director.getinstance().currentController.player;
        Vector3 localPos = this.transform.position;
        pos = new Vector3[4];
        pos[0] = localPos + new Vector3(range, 0f, range);
        pos[1] = localPos + new Vector3(-range, 0f, range);
        pos[2] = localPos + new Vector3(-range, 0f, -range);
        pos[3] = localPos + new Vector3(range, 0f, -range);
        target = pos[order];
    }

    // 守卫每一帧调用该函数实现运动
    private void FixedUpdate(){
        // 根据状态(isChasing)设置守卫的运动目标
        target = isChasing ? player.transform.position : pos[order];
        // 足够靠近目标时,根据状态(isChasing)决定选择下一个目标或是结束游戏
        if((this.transform.position - target).magnitude < delta){
            if(!isChasing){
                order = (order + 1) % 4;
                target = pos[order];
            }
            else{
                Director.getinstance().overGame();
            }
        }
        // 朝向目标点并前进
        Vector3 dir = target - this.transform.position;
        this.transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
        this.transform.position += dir * Time.deltaTime;
    }

    // controller通过该函数激活或者灭活本守卫
    public void setChasing(bool chase){
        this.isChasing = chase;
    }
}

相机控制器,锁定相机跟踪拍摄主角的后背,但是试验后发现相机中心为头顶正上方,能看到天空时,整个画面看起来会更舒服一点。

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

public class CameraController : MonoBehaviour{
    
    public float distanceUp; // 摄像机离主角的高度
    public float distanceAway; // 摄像机离主角的水平距离
    public float focusUp; // 摄像机拍摄玩家头顶的高度
    public float smooth; // 位置平滑移动值
    private Transform target = null;

    // 在start中为摄像机绑定玩家
    void Start(){
        target = GameObject.FindGameObjectWithTag("Player").transform;
    }

    // 每一帧更新相机的位置
    void LateUpdate(){
        //相机的位置
        Vector3 disPos = target.position + Vector3.up * distanceUp - target.forward * distanceAway;
        transform.position = Vector3.Lerp(transform.position, disPos, Time.deltaTime * smooth);
        //相机的角度
        transform.LookAt(target.position + Vector3.up * focusUp);
    }
}

得分类,简单的读取修改。

public class Score{
    
    private int score;

    public Score(){clear();}
    public void clear(){ score = 0; }
    public void add(){ score++; }
    public int get(){ return score; }
}

地图如下,主角会出生在中心方格,其余八个方格都会刷新出守卫,每次穿过一个格子得一分。

 

 本文仅使用了师兄的博客https://blog.csdn.net/sodifferent/article/details/102732306中的预制体(人物)。在此鸣谢。

视频链接

代码链接,下载后可以直接运行

posted @ 2021-12-02 23:40  LoongChan  阅读(151)  评论(0编辑  收藏  举报