Assignment 7

智能巡逻兵

  • 提交要求:
  • 游戏设计要求:
    • 创建一个地图和若干巡逻兵(使用动画);
    • 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
    • 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
    • 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
    • 失去玩家目标后,继续巡逻;
    • 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束;
  • 程序设计要求:
    • 必须使用订阅与发布模式传消息
      • subject:OnLostGoal
      • Publisher: GameEventManager
      • Subscriber: FirstSceneController

        本次实验作业我参考了往年师兄的代码,借用了师兄们选用过的Ybot作为Player和Guard的模型。为了增加游戏的趣味性,我在游戏中加入了胜利条件:即玩家需要走到迷宫的尽头去拿到NPC要求的宝藏之后再原路返回送给NPC,若在路上被侦察兵发现则游戏失败。此外,为了增加游戏的收集要素,我增加了一个“装备”在地图的右上角,可抵消一次被侦察兵发现的次数。

源代码:UnityGame/AutomaticGuard at master · ShirohaBili/UnityGame (github.com)

演示视频:Assignment7 智能巡逻兵

因为本次实验代码量有点大,因此我就挑选一些我认为比较重要的代码进行讲解:

Animator

         这里我只设置了按空格可以有一个翻滚的动作,本来是设置了一个跳跃的,但因为跳跃会出问题,因此我又把它删掉了。实际上似乎只用跑的也行

PlayerCollide

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

public class PlayerCollide : MonoBehaviour {
    private IUserAction action = SSDirector.GetInstance().CurrentScenceController as IUserAction;
    void OnCollisionEnter(Collision other) {
        //当玩家与侦察兵相撞
        if (other.gameObject.tag == "Guard") {
            if (action.GetEquipment()) {
                // Destroy(other.gameObject);
                action.setEquipment(false);
            }
            else Singleton<GameEventManager>.Instance.PlayerGameover();
        }
        //当玩家捡到宝藏时
        if(other.gameObject.tag == "Treasure"){
            Destroy(other.gameObject);
            Singleton<GameEventManager>.Instance.PlayerWining();
        }
        //玩家交付宝藏给NPC,任务胜利
        if (other.gameObject.tag == "NPC"){
            if (action.GetWinning()) Singleton<GameEventManager>.Instance.TreasureReceive();
        }
        //玩家获取装备
        if(other.gameObject.tag == "Equipment"){
            Destroy(other.gameObject);
            Singleton<GameEventManager>.Instance.EquipmentReceive();
        }
    }
}

这一段代码利用消息订阅/发布的模式,实现了对游戏内各种要素的碰撞检验,即巡逻兵、NPC、宝物和装备。

GameEventManager

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

public class GameEventManager : MonoBehaviour {
    public delegate void ScoreEvent();
    public static event ScoreEvent ScoreChange;
    
    public delegate void GameoverEvent();
    public static event GameoverEvent GameoverChange;

    public delegate void GameWiningEvent();
    public static event GameWiningEvent GameWiningChange;

    public delegate void TreasureReceiveEvent();
    public static event TreasureReceiveEvent TreasureReceiveChange;

    public delegate void EquipmentReceiveEvent();
    public static event EquipmentReceiveEvent EquipmentReceiveChange;

    public void PlayerEscape() {
        if (ScoreChange != null) {
            ScoreChange();
        }
    }

    public void PlayerGameover(){
        if (GameoverChange != null) {
            GameoverChange();
        }
    }

    public void PlayerWining(){
        if (GameWiningChange !=null){
            GameWiningChange();
        }
    }

    public void TreasureReceive(){
        if (TreasureReceiveChange !=null){
            TreasureReceiveChange();
        }
    }

    public void EquipmentReceive(){
        if (EquipmentReceiveChange != null){
            EquipmentReceiveChange();
        }
    }
}


这一段代码实现了对所有游戏事件的代理机制。

Camera

我分别写了两个脚本来控制相机,一个控制相机跟着玩家模型移动,另一个控制相机根据玩家左右移动鼠标而移动

CameraController

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

public class CameraConrtoller : MonoBehaviour {
    public PlayerInput pi;
    public float horizontalSpeed = 100f;
    public float verticalSpeed = 80f;
    public float cameraDampValue = 0.5f;

    private GameObject playerHandle;
    private GameObject cameraHandle;
    private float tempEulerX;
    private GameObject model;
    private GameObject camera;

    private Vector3 cameraDampVelocity;
    

    void Awake() {
        cameraHandle = transform.parent.gameObject;
        playerHandle = cameraHandle.transform.parent.gameObject;
        model = playerHandle.GetComponent<ActorController>().model;
        camera = Camera.main.gameObject;
        tempEulerX = 20f;
    }

    // Update is called once per frame
    void FixedUpdate() {
        Vector3 tempModelEuler = model.transform.eulerAngles;
        playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);
        tempEulerX -= pi.Jup * verticalSpeed * Time.fixedDeltaTime;
        tempEulerX = Mathf.Clamp(tempEulerX, -35, 30);
        cameraHandle.transform.localEulerAngles = new Vector3(tempEulerX, 0, 0);
        model.transform.eulerAngles = tempModelEuler;

        camera.transform.position = Vector3.SmoothDamp(
            camera.transform.position, transform.position, 
            ref cameraDampVelocity, cameraDampValue);
        camera.transform.eulerAngles = transform.eulerAngles;
    }
}

CameraMove

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class CameraMove : MonoBehaviour
{
    public Transform target;    //相机追随目标
    public float xSpeed = 200;  //X轴方向拖动速度
    public float ySpeed = 200;  //Y轴方向拖动速度
    public float mSpeed = 10;   //放大缩小速度
    public float yMinLimit = -50; //在Y轴最小移动范围
    public float yMaxLimit = 50; //在Y轴最大移动范围
    public float distance = 2;  //相机视角距离
    public float minDinstance = 2; //相机视角最小距离
    public float maxDinstance = 10; //相机视角最大距离
    public float x = 0.0f;
    public float y = 0.0f;
    public float damping = 5.0f;
    public bool needDamping = true;
    public bool lockCursor = true;

    private bool m_cursorIsLocked = true;
 
 
    // Start is called before the first frame update
    void Start()
    {
        target = transform.parent.parent.gameObject.transform;
        Vector3 angle = transform.eulerAngles;
        x = angle.y;
        y = angle.x;
    }
 
    // Update is called once per frame
    void LateUpdate()
    {
        if (target)
        {
            // if (Input.GetMouseButton(1))
            // {
                x += Input.GetAxis("Mouse X") * xSpeed * 0.02f;
                y -= Input.GetAxis("Mouse Y") * ySpeed * 0.02f;
                y = ClamAngle(y, yMinLimit, yMaxLimit);
                    
            // }
            distance -= Input.GetAxis("Mouse ScrollWheel") * mSpeed;
            distance = Mathf.Clamp(distance, minDinstance, maxDinstance);
            Quaternion rotation = Quaternion.Euler(y, x, 0.0f);
            Vector3 disVector = new Vector3(0.0f, 0.0f, -distance);
            Vector3 position = rotation * disVector + target.position;
 
            if (needDamping)
            {
                transform.rotation = Quaternion.Lerp(transform.rotation, rotation, Time.deltaTime * damping);
                transform.position = Vector3.Lerp(transform.position, position, Time.deltaTime * damping);
            }
            else
            {
                transform.rotation = rotation;
                transform.position = position;
            }
            UpdateCursorLock();
        }
    }

    static float ClamAngle(float angle, float min, float max)
    {
        if (angle < -360)
        {
            angle += 360;
        }
        if(angle > 360)
        {
            angle -= 360;
        }
        return Mathf.Clamp(angle, min, max);
    }

        public void SetCursorLock(bool value)
    {
        lockCursor = value;
        if(!lockCursor)
        {//we force unlock the cursor if the user disable the cursor locking helper
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }
    }

    public void UpdateCursorLock()
    {
        //if the user set "lockCursor" we check & properly lock the cursos
        if (lockCursor)
            InternalLockUpdate();
    }

    private void InternalLockUpdate()
    {
        if(Input.GetKeyUp(KeyCode.Escape))
        {
            m_cursorIsLocked = false;
        }
        else if(Input.GetMouseButtonUp(0))
        {
            m_cursorIsLocked = true;
        }

        if (m_cursorIsLocked)
        {
            Cursor.lockState = CursorLockMode.Locked;
            Cursor.visible = false;
        }
        else if (!m_cursorIsLocked)
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }
    }

}

控制相机移动的方法相对简单一些,只需要设置一下跟随的目标,使用平滑的跟踪方式进行跟踪即可。控制相机旋转的方法相对复杂,我这里设置了拖动的最大上限,防止角度过大会出现的问题,并设置了可以利用滚轮调节相机的距离,便于玩家选择适合自己的视距。同时设计了隐藏鼠标指针的方法,点击画面隐藏鼠标指针,按ESC键可重新显示鼠标指针。

GuardActionManager

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

public class GuardActionManager : SSActionManager, ISSActionCallback {
    private GuardPatrolAction patrol;
    private GameObject player;
    public void GuardPatrol(GameObject guard, GameObject _player) {
        player = _player;
        patrol = GuardPatrolAction.GetSSAction(guard.transform.position);
        this.RunAction(guard, patrol, this);
    }

    public void SSActionEvent(
        SSAction source, SSActionEventType events = SSActionEventType.Competeted,
        int intParam = 0, GameObject objectParam = null) {
        if (intParam == 0) {
            //追逐
            GuardFollowAction follow = GuardFollowAction.GetSSAction(player);
            this.RunAction(objectParam, follow, this);
        } else {
            //巡逻
            GuardPatrolAction move = GuardPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<GuardData>().start_position);
            this.RunAction(objectParam, move, this);
            Singleton<GameEventManager>.Instance.PlayerEscape();
        }
    }
}

巡逻兵察觉到玩家进来就变成追逐模式,玩家逃离则继续巡逻。

GuardPatrol

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

public class GuardPatrolAction : SSAction {
    private enum Dirction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                 
    private float move_length;                 
    private bool move_sign = true;              
    private Dirction dirction = Dirction.EAST;  

    private GuardData data;
    private Animator anim;
    private Rigidbody rigid;
    private Vector3 planarVec; // 平面移动向量
    private GuardPatrolAction() { }

    public override void Start() {
        data = gameobject.GetComponent<GuardData>();
        anim = gameobject.GetComponent<Animator>();
        rigid = gameobject.GetComponent<Rigidbody>();
        //播放走路动画
        anim.SetFloat("forward", 1.0f);
    }
    public static GuardPatrolAction GetSSAction(Vector3 location) {
        GuardPatrolAction action = CreateInstance<GuardPatrolAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;
        //设定移动矩形的边长
        action.move_length = Random.Range(5, 6);
        return action;
    }

    public override void Update() {
        //保留供物理引擎调用
        planarVec = gameobject.transform.forward * data.walkSpeed;
    }

    public override void FixedUpdate() {
        //巡逻
        Gopatrol();
        //玩家进入该区域,巡逻结束,开始追逐
        if (data.playerSign == data.sign) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }

    void Gopatrol() {
        if (move_sign) {
            //不需要转向则设定一个目的地,按照矩形移动
            switch (dirction) {
                case Dirction.EAST:
                    pos_x -= move_length;
                    break;
                case Dirction.NORTH:
                    pos_z += move_length;
                    break;
                case Dirction.WEST:
                    pos_x += move_length;
                    break;
                case Dirction.SOUTH:
                    pos_z -= move_length;
                    break;
            }
            move_sign = false;
        }
        this.transform.LookAt(new Vector3(pos_x, 0, pos_z));
        float distance = Vector3.Distance(transform.position, new Vector3(pos_x, 0, pos_z));

        if (distance > 0.9) {
            rigid.velocity = new Vector3(planarVec.x, rigid.velocity.y, planarVec.z);
        } else {
            dirction = dirction + 1;
            if(dirction > Dirction.SOUTH) {
                dirction = Dirction.EAST;
            }
            move_sign = true;
        }
    }
}

这一脚本控制巡逻兵的动作,按题目要求,设置巡逻兵按一个矩形移动,但或许是因为Ybot自带的动画的问题,巡逻兵会卡在一个地方不动一段时间,而后恢复正常。

GuardFactory

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

public class GuardFactory : MonoBehaviour {
    private GameObject guard = null;                               //巡逻兵
    private List<GameObject> used = new List<GameObject>();        //正在使用的巡逻兵列表
    private Vector3[] vec = new Vector3[9];                        //每个巡逻兵的初始位置

    public List<GameObject> GetPatrols() {
        int[] pos_x = { -6, 4, 13 };
        int[] pos_z = { -4, 6, -13 };
        int index = 0;
        for(int i=0;i < 3;i++) {
            for(int j=0;j < 3;j++) {
                vec[index] = new Vector3(pos_x[i], 0, pos_z[j] + 5);
                index++;
            }
        }
        for(int i = 0; i < 8; i++) {
            guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
            guard.transform.position = vec[i];
            guard.GetComponent<GuardData>().sign = i + 1;
            guard.GetComponent<GuardData>().start_position = vec[i];
            guard.GetComponent<Animator>().SetFloat("forward", 1);
            used.Add(guard);
        }   
        return used;
    }
}

沿用了前几个作业中的工厂模式,设置了一个巡逻兵工厂,这里需要说明的是,sign变量的设置是为了检验玩家是否和巡逻兵在一个区域内,若在,则追逐,不在则巡逻。

FirstSceneController

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


public class FirstSceneController : MonoBehaviour, IUserAction, ISceneController {
    public GuardFactory guard_factory;                               //巡逻者工厂
    public ScoreRecorder recorder;                                   //记录员
    public GuardActionManager action_manager;                        //运动管理器
    public int playerSign = -1;                                      //当前玩家所处哪个格子
    public GameObject player;                                        //玩家
    public UserGUI gui;                                             //交互界面
    public GameObject treasure;
    public GameObject npc;      
    public GameObject equipment;                                      

    private List<GameObject> guards;                                 //场景中巡逻者列表
    private bool game_over = false;                                  //游戏结束
    private bool Wining = false;
    private bool receive = false;
    private bool equipmentRec = false;
    private bool equipmentGet = false;

    
    void Awake() {
        SSDirector director = SSDirector.GetInstance();
        director.CurrentScenceController = this;
        guard_factory = Singleton<GuardFactory>.Instance;
        action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;
        gui = gameObject.AddComponent<UserGUI>() as UserGUI;
        LoadResources();
        recorder = Singleton<ScoreRecorder>.Instance;
    }

    void Update() {
        for (int i = 0; i < guards.Count; i++) {
            guards[i].gameObject.GetComponent<GuardData>().playerSign = playerSign;
        }
    }


    public void LoadResources() {
        Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));
        player = Instantiate(
            Resources.Load("Prefabs/Player"), 
            new Vector3(10, 0, -10), Quaternion.identity) as GameObject;
        
        treasure = Instantiate(Resources.Load("Prefabs/treasure"), new Vector3(0f,0.33f,-19f), Quaternion.identity) as GameObject;
        npc = Instantiate(Resources.Load("Prefabs/NPC"), new Vector3(12f,0f,-12f),Quaternion.identity) as GameObject;
        equipment = Instantiate(Resources.Load("Prefabs/equipment"), new Vector3(9f,1f,10f),Quaternion.identity) as GameObject;
        guards = guard_factory.GetPatrols();

        for (int i = 0; i < guards.Count; i++) {
            action_manager.GuardPatrol(guards[i], player);
        }
    }

    public int GetScore() {
        return recorder.GetScore();
    }

    public bool GetGameover() {
        return game_over;
    }

    public bool GetWinning(){
        return Wining;
    }

    public bool GetReceive(){
        return receive;
    }

    public bool GetEquipment(){
        return equipmentRec;
    }

    public bool HadEquipment(){
        return equipmentGet;
    }

    public void Restart() {
        SceneManager.LoadScene("Scenes/mySence");
    }

    void OnEnable() {
        GameEventManager.ScoreChange += AddScore;
        GameEventManager.GameoverChange += Gameover;
        GameEventManager.GameWiningChange += GameWining;
        GameEventManager.TreasureReceiveChange += ReceiveChange;
        GameEventManager.EquipmentReceiveChange += EquipmentChange;
    }
    void OnDisable() {
        GameEventManager.ScoreChange -= AddScore;
        GameEventManager.GameoverChange -= Gameover;
        GameEventManager.GameWiningChange -= GameWining;
        GameEventManager.TreasureReceiveChange -= ReceiveChange;
        GameEventManager.EquipmentReceiveChange -= EquipmentChange;
    }

    void AddScore() {
        recorder.AddScore();
    }

    void Gameover() {
        game_over = true;
    }

    void GameWining(){
        Wining = true;
    }

    void ReceiveChange(){
        receive = true;
    }

    void EquipmentChange(){
        equipmentRec = true;
        equipmentGet = true;
    }

    public void setEquipment(bool status){
        equipmentRec = status;
    }
}

本脚本充当了整个游戏项目的初始化管理者的角色,同时也担当了订阅/发布模式中的接收者的角色,管理着本游戏的全部游戏事件。

剩下的一些脚本如FSM,UserGUI等不是特别难理解,因此我没有将它列出来,若有需要的话,请自行翻阅源代码。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值