智能巡逻兵

作业七:智能巡逻兵

完整项目资源地址.

1.要求:

1.1游戏设计要求:

游戏设计要求:
1:创建一个地图和若干巡逻兵(使用动画);
2:每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前         位置为原点计算;
3:巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
4:巡逻兵在设定范围内感知到玩家,会自动追击玩家;
5:失去玩家目标后,继续巡逻;
6:计分:玩家每摆脱一名巡逻兵就加一分
7:	玩家将所有的水晶都收集完则游戏获胜;

8:玩家被捕获,则游戏失败。

1.2程序设计要求:

  • 使用订阅与发布模式传递消息:
  • 工厂模式生产巡逻兵

2:预制资源制作:

使用的资源包:
[链接](https://assetstore.unity.com/packages/3d/characters/humanoids/character-pack-free-sample-79870)

2.1玩家预制制作:

使用了资源包里的模型预制,只需要进行一定的设置即可:
要点:设置Tag: Player
设置Animator:
1:具体设置:
在这里插入图片描述
2:设置Animator:
1:在Assets窗口新建:Create-> Animator Controller:
2:右键击Animator窗口新建状态,点击状态后右击添加转移;
3:在Parameter窗口添加状态转移的变量:
4:点击状态转移的线条,在出现的窗口点击Condition下面的加号,添加转移变量的控制信息;
5:点击状态,在Inspector窗口的motion项目那里添加该状态执行的动作:

Player的Animator具体信息如下:
在这里插入图片描述
状态idle的设置:
在这里插入图片描述

AnyState->death的设置:
在这里插入图片描述

3:player:

在这里插入图片描述

2.2 Patrol预制的制作:

因为模型包里只有一个模型,所以Patrol使用的是同一个预制,但是为了区分,将它们的大小做了改变:

设置要点:

1:制作Animator:

1:状态图
在这里插入图片描述
2:idle的信息:
在这里插入图片描述

3:AnyState->attack转移在这里插入图片描述

2:在预制的第二层basic_rig添加Boxcllide组件:

将Trigger设置为true,同时设置好BoxCollide的Size;这是巡逻兵触发器的范围大小,即他能检测玩家的范围;
在这里插入图片描述

3:添加脚本:

一共有三个脚本需要添加到Patrol中,PatrolData.cs和Player.cs添加到第一层,然后PatrolCollide.cs 添加到子对象basic_rig上:

2.3 制作地图:

1:要点是在每个房间添加trigger对象,用来检索player的位置:
在这里插入图片描述

2:Trigger的设置:

Is Trigger : true
添加脚本,并设置脚本变量Sign的值,对应相应的房间号。

在这里插入图片描述

2.4 预制水晶:

相关的设置如下:
也有一个检测碰撞的脚本:

在这里插入图片描述

3:代码:

代码结构图:
在这里插入图片描述

3.1订阅发布者模式:

订阅者与发布者模式主要用于处理一些事件,如本游戏中的碰撞事件,以及玩家逃脱追捕这些事件。这些发生事件的类为发布者,即事件的来源;如程序中的CrytalCollision和PlayerCollide;
这些事件都是由一个消息发布媒体来发布,我们称为主题(渠道) ;如GameEvenManage;
订阅者就是对事件感兴趣的类,定义了消息相应的处理方法;对应FirstSceneController;
在这里插入图片描述

1:发布者:
  • 1:收集水晶:
    CrystalCollide:该脚本添加在水晶预制下,用来检测玩家与水晶的碰撞:
    如果碰撞者的标签时“player”就发布水晶减少的信息,同时将碰撞后将水晶对象设置为不活跃
public class CrystalCollide : MonoBehaviour
{
    void OnTriggerEnter(Collider collider)
    {
        //如果碰撞者的标签时“player”就发布水晶减少的信息
        if (collider.gameObject.tag == "Player" && this.gameObject.activeSelf)
        {   //碰撞后将水晶对象设置为不活跃
            this.gameObject.SetActive(false);
            //减少水晶数量
            Singleton<GameEventManager>.Instance.ReduceCrystalNum();
        }
    }
}

2:玩家被捕获:
该脚本文件添加到Patrol预制对象上:
//如果与玩家相撞,则游戏结束,设置Animator相应的参数变量值。
//Player状态机触发death Trigger,从AnyState转到death状态
//Patrol状态机触发shoot;转入Attack攻击状态。
//同时通过GameEvenManage发布游戏结束的信息。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//本文件添加到Patrol预制对象上
public class PlayerCollide : MonoBehaviour
{
    //如果与玩家相撞,则游戏结束,设置Animator相应的参数变量值。
    void OnCollisionEnter(Collision other)
    {
        //当玩家与侦察兵相撞
        if (other.gameObject.tag == "Player")
        {
            //Player状态机触发death Trigger,从AnyState转到death状态
            other.gameObject.GetComponent<Animator>().SetTrigger("death");
            //Patrol状态机触发shoot;转入Attack攻击状态。
            this.GetComponent<Animator>().SetTrigger("shoot");
            //发布游戏结束的信息
            Singleton<GameEventManager>.Instance.PlayerGameover();
        }
    }
}
2:主题:GameEvenManage:

所有的事件都通知到该类,由该类向订阅者发布消息:

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 CrystalEvent();
    public static event CrystalEvent CrystalChange;

    //玩家逃脱
    public void PlayerEscape()
    {
        if (ScoreChange != null)
        {
            ScoreChange();
        }
    }
    //玩家被捕
    public void PlayerGameover()
    {
        if (GameoverChange != null)
        {
            GameoverChange();
        }
    }
    //减少水晶数量
    public void ReduceCrystalNum()
    {
        if (CrystalChange != null)
        {
            CrystalChange();
        }
    }
}
3:订阅者:

发布的消息最终会通知到订阅者这里,订阅者定义了相应的方法对发生的事件进行处理:程序中的订阅者是FirstScenceController;下面是订阅者的部分代码:

    //订阅事件:并添加相应的处理方法
    void OnEnable()
    {
        GameEventManager.ScoreChange += AddScore;
        GameEventManager.GameoverChange += Gameover;
        GameEventManager.CrystalChange += ReduceCrystalNumber;
    }
    //取消订阅该事件
    void OnDisable()
    {
        GameEventManager.ScoreChange -= AddScore;
        GameEventManager.GameoverChange -= Gameover;
        GameEventManager.CrystalChange -= ReduceCrystalNumber;
    }
    //水晶碰撞的处理方法
    void ReduceCrystalNumber()
    {
        recorder.ReduceCrystal();
    }
    //分数变化的处理方法
    void AddScore()
    {
        recorder.AddScore();
    }
    //游戏结束的处理方法:
    void Gameover()
    {
        game_over = true;
        patrol_factory.StopPatrol();
        action_manager.DestroyAllAction();
    }
3.2:添加到预制中的其它代码:
1:巡逻兵检测玩家的脚本: PatrolCollide:该代码添加在Patrol的子对象basic_rig上;

因为Patrol对象为IsTrigger;所以当玩家进入范围时,会触发事件OnTriggerEnter;当玩家离开boxColiide范围时,会触发OnTriggerExit事件;
玩家进入侦察兵追捕范围,
//将跟随玩家设置为true
//并获取玩家的对象信息,赋给PatrolData的player 成员变量

如果玩家摆脱跟随,撤销PatrolDAta中的player信息;

public class PatrolCollide : MonoBehaviour
{
    //检测到玩家
    void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.tag == "Player")
        {
            //玩家进入侦察兵追捕范围
            //将跟随玩家设置为true
            //并获取玩家的对象信息,赋给PatrolData的player成员变量
            this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = true;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = collider.gameObject;
        }
    }
    void OnTriggerExit(Collider collider)
    {
        //玩家摆脱跟随,撤销PatrolDAta中的player信息;
        if (collider.gameObject.tag == "Player")
        {
            this.gameObject.transform.parent.GetComponent<PatrolData>().follow_player = false;
            this.gameObject.transform.parent.GetComponent<PatrolData>().player = null;
        }
    }
}
2:每个Patrol基本信息的记录:PatrolData,添加在Patrol上:
public class PatrolData : MonoBehaviour
{
    public int sign;                      //标志巡逻兵在哪一块区域
    public bool follow_player = false;    //是否跟随玩家
    public int wall_sign = -1;            //当前玩家所在区域标志
    public GameObject player;             //玩家游戏对象
    public Vector3 start_position;        //当前巡逻兵初始位置     
}

3:玩家位置检测: AreaCollide.cs

该脚本添加在地图的Trigger子对象上,每个房间有一个Trigger,当玩家进入该房间时,触发器出发,更新玩家的位置信息。

玩家进入范围:更新FirstScenceController中的玩家位置信息为当前房间标识;

public class AreaCollide : MonoBehaviour
{//房间号的标识
    public int sign = 0;
    FirstSceneController sceneController;
    private void Start()
    {
        sceneController = SSDirector.GetInstance().CurrentScenceController as FirstSceneController;
    }
    void OnTriggerEnter(Collider collider)
    {
        //标记玩家进入自己的区域
        if (collider.gameObject.tag == "Player")
        {
            //更新FirstScenceController中的玩家位置信息
            sceneController.wall_sign = sign;
        }
    }
}
3.3 工厂模式,Factory

用来使用预制批量生产Patrol对象和Crystal对象;通过GetPatrol和GetCrystal两个方法得到批量的对象,通过StopPatrol群体控制patrol 的Animator状态机动作。

public class PropFactory : MonoBehaviour
{
    private GameObject patrol = null;                              //巡逻兵
    private List<GameObject> used = new List<GameObject>();        //正在被使用的巡逻兵
    private GameObject crystal = null;                             //水晶
    private List<GameObject> usedcrystal = new List<GameObject>();      //正在被使用的水晶
    private float range = 12;                                      //水晶生成的坐标范围
    private Vector3[] vec = new Vector3[9];                        //保存每个巡逻兵的初始位置

    public FirstSceneController sceneControler;                    //场景控制器

    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]);
                index++;
            }
        }
        for(int i=0; i < 9; i++)
        {
            patrol = Instantiate(Resources.Load<GameObject>("Prefabs/Patrol"));
            patrol.transform.position = vec[i];
            patrol.GetComponent<PatrolData>().sign = i + 1;
            patrol.GetComponent<PatrolData>().start_position = vec[i];
            used.Add(patrol);
        }   
        return used;
    }


    public List<GameObject> GetCrystal()
    {
        for(int i=0;i<12;i++)
        {
            crystal = Instantiate(Resources.Load<GameObject>("Prefabs/Crystal"));
            float ranx = Random.Range(-range, range);
            float ranz = Random.Range(-range, range);
            crystal.transform.position = new Vector3(ranx, 0, ranz);
            usedcrystal.Add(crystal);
        }

        return usedcrystal;
    }
    public void StopPatrol()
    {
        //切换所有侦查兵的动画
        for (int i = 0; i < used.Count; i++)
        {
            used[i].gameObject.GetComponent<Animator>().SetBool("run", false);
        }
    }
}
3.4:Patrol的动作脚本:
1:SSAction:

这是巡逻兵所有动作的父类,巡逻兵主要有巡逻和跟踪两个动作,都是从该类派生出去的,便于巡逻兵动作的统一管理和调用:

public class SSAction : ScriptableObject
{
    public bool enable = true;                      //是否正在进行此动作
    public bool destroy = false;                    //是否需要被销毁
    public GameObject gameobject;                   //动作对象
    public Transform transform;                     //动作对象的transform
    public ISSActionCallback callback;              //动作完成后的消息通知者

    protected SSAction() { }
    //子类可以使用下面这两个函数
    public virtual void Start()
    {
        throw new System.NotImplementedException();
    }
    public virtual void Update()
    {
        throw new System.NotImplementedException();
    }
}
2:巡逻动作:GoPatrolAction

按着东北西南的方向顺序,和一定的移动距离进行巡逻,一开始就设置状态机的进入Run动画状态,然后就按特定的路线巡逻。

public class GoPatrolAction : SSAction
{
    private enum Dirction { EAST, NORTH, WEST, SOUTH };
    private float pos_x, pos_z;                 //移动前的初始x和z方向坐标
    private float move_length;                  //移动的长度
    private float move_speed = 1.2f;            //移动速度
    private bool move_sign = true;              //是否到达目的地
    private Dirction dirction = Dirction.EAST;  //移动的方向
    private PatrolData data;                    //侦察兵的数据
    

    private GoPatrolAction() { }
    public static GoPatrolAction GetSSAction(Vector3 location)
    {
        GoPatrolAction action = CreateInstance<GoPatrolAction>();
        action.pos_x = location.x;
        action.pos_z = location.z;
        //设定移动矩形的边长
        action.move_length = Random.Range(4, 7);
        return action;
    }
    public override void Update()
    {
        //防止碰撞发生后的旋转
        if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
        }            
        if (transform.position.y != 0)
        {
            transform.position = new Vector3(transform.position.x, 0, transform.position.z);
        }
        //侦察移动
        Gopatrol();
        //如果侦察兵需要跟随玩家并且玩家就在侦察兵所在的区域,侦查动作结束
        if (data.follow_player && data.wall_sign == data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,0,this.gameobject);
        }
    }
    public override void Start()
    {
        this.gameobject.GetComponent<Animator>().SetBool("run", true);
        data  = this.gameobject.GetComponent<PatrolData>();
    }

    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)
        {
            transform.position = Vector3.MoveTowards(this.transform.position, new Vector3(pos_x, 0, pos_z), move_speed * Time.deltaTime);
        }
        else
        {
            dirction = dirction + 1;
            if(dirction > Dirction.SOUTH)
            {
                dirction = Dirction.EAST;
            }
            move_sign = true;
        }
    }
}

3.追捕动作:PatrolFollowAction:

时刻设置朝向和移动为向着目标玩家:

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

public class PatrolFollowAction : SSAction
{
    private float speed = 2f;            //跟随玩家的速度
    private GameObject player;           //玩家
    private PatrolData data;             //侦查兵数据

    private PatrolFollowAction() { }
    public static PatrolFollowAction GetSSAction(GameObject player)
    {
        PatrolFollowAction action = CreateInstance<PatrolFollowAction>();
        action.player = player;
        return action;
    }

    public override void Update()
    {
        if (transform.localEulerAngles.x != 0 || transform.localEulerAngles.z != 0)
        {
            transform.localEulerAngles = new Vector3(0, transform.localEulerAngles.y, 0);
        }
        if (transform.position.y != 0)
        {
            transform.position = new Vector3(transform.position.x, 0, transform.position.z);
        }
         
        Follow();
        //如果侦察兵没有跟随对象,或者需要跟随的玩家不在侦查兵的区域内
        if (!data.follow_player || data.wall_sign != data.sign)
        {
            this.destroy = true;
            this.callback.SSActionEvent(this,1,this.gameobject);
        }
    }
    public override void Start()
    {
        data = this.gameobject.GetComponent<PatrolData>();
    }
    void Follow()
    {
        transform.position = Vector3.MoveTowards(this.transform.position, player.transform.position, speed * Time.deltaTime);
        this.transform.LookAt(player.transform.position);
    }
}

4:patrol动作管理类SSActionManage:

处理动作的生成和撤销,进行巡逻或者跟随动作的切换:

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

public class SSActionManager : MonoBehaviour, ISSActionCallback
{
    private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();    //将执行的动作的字典集合
    private List<SSAction> waitingAdd = new List<SSAction>();                       //等待去执行的动作列表
    private List<int> waitingDelete = new List<int>();                              //等待删除的动作的key                

    protected void Update()
    {
        foreach (SSAction ac in waitingAdd)
        {
            actions[ac.GetInstanceID()] = ac;
        }
        waitingAdd.Clear();

        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            if (ac.destroy)
            {
                waitingDelete.Add(ac.GetInstanceID());
            }
            else if (ac.enable)
            {
                //运动学运动更新
                ac.Update();
            }
        }

        foreach (int key in waitingDelete)
        {
            SSAction ac = actions[key];
            actions.Remove(key);
            DestroyObject(ac);
        }
        waitingDelete.Clear();
    }
 
    public void RunAction(GameObject gameobject, SSAction action, ISSActionCallback manager)
    {
        action.gameobject = gameobject;
        action.transform = gameobject.transform;
        action.callback = manager;
        waitingAdd.Add(action);
        action.Start();
    }

    public void SSActionEvent(SSAction source, int intParam = 0, GameObject objectParam = null)
    {
        if(intParam == 0)
        {
            //侦查兵跟随玩家
            PatrolFollowAction follow = PatrolFollowAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().player);
            this.RunAction(objectParam, follow, this);
        }
        else
        {
            //侦察兵按照初始位置开始继续巡逻
            GoPatrolAction move = GoPatrolAction.GetSSAction(objectParam.gameObject.GetComponent<PatrolData>().start_position);
            this.RunAction(objectParam, move, this);
            //玩家逃脱
            Singleton<GameEventManager>.Instance.PlayerEscape();
        }
    }


    public void DestroyAll()
    {
        foreach (KeyValuePair<int, SSAction> kv in actions)
        {
            SSAction ac = kv.Value;
            ac.destroy = true;
        }
    }
}

3.5:摄像机跟随脚本:CameraFlow:

将摄像机设置为跟随玩家移动,并平滑移动:

public class CameraFlow : MonoBehaviour
{
    public GameObject follow;            //跟随的物体
    public float smothing = 5f;          //相机跟随的速度
    Vector3 offset;                      //相机与物体相对偏移位置

    void Start()
    {
        offset = transform.position - follow.transform.position;
    }

    void FixedUpdate()
    {
        Vector3 target = follow.transform.position + offset;
        //摄像机自身位置到目标位置平滑过渡
        transform.position = Vector3.Lerp(transform.position, target, smothing * Time.deltaTime);
    }
}

3.6 完整的项目代码:

在这里插入图片描述

4:运行:

游戏视频展示如下:

bandicam 2020-12-03 21-43-48-221

5:项目地址:

完整项目资源地址.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值