Unity3D游戏编程-智能巡逻兵

Unity3D游戏编程-智能巡逻兵

一、作业要求

游戏设计要求:

  1. 创建一个地图和若干巡逻兵(使用动画);
  2. 每个巡逻兵走一个3~5个边的凸多边型,位置数据是相对地址。即每次确定下一个目标位置,用自己当前位置为原点计算;
  3. 巡逻兵碰撞到障碍物,则会自动选下一个点为目标;
  4. 巡逻兵在设定范围内感知到玩家,会自动追击玩家;
  5. 失去玩家目标后,继续巡逻;
  6. 计分:玩家每次甩掉一个巡逻兵计一分,与巡逻兵碰撞游戏结束。

程序设计要求:

  1. 必须使用订阅与发布模式传消息
  2. 工厂模式生产巡逻兵

二、项目配置

Windows 10
Unity 2020.3.17f1c1

三、项目演示

(1)视频演示

点击此处可以前往
可开启字幕观看。

(2)项目下载

下载Assets文件夹
点击此处可以前往gitee

(3)文字说明

  1. 创建unity专案后,将保存的文件夹中的Assets替换成在上面项目下载的Assets文件夹
  2. 打开专案,然后点选Assets > Scenes的mySence加载场景
  3. 运行即可开始游戏
  4. 角色控制是“WASD”,镜头控制是“↑←↓→”,按下左shift同时进行角色控制可以跑步
  5. 每逃离一个巡逻兵的追捕加一分,碰撞到巡逻兵就会结束游戏

(4)项目截图

请添加图片描述
请添加图片描述
请添加图片描述


四、前置内容

(1)课程对"模型与动画"的基本练习

为了熟悉操作, 我把本节内容老师给出的例子做了一遍, 详细的内容在下面的博客中。
点击此处前往

(2)MVC模式

MVC模式在每次作业中都是程序的重要结构,具体内容可以参考回去之前的作业:
点击此处前往


(3)工厂模式

工厂模式在以往飞碟游戏中有过应用,此处进行一个重温。
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
工厂模式给我感觉有点像线程池,预先创建了一些对象,当需要用的时候才会取出。


(4)Singleton

Unity里的单例模式。一些东西在整个游戏中只有一个而你又想可以方便地随时访问它,这时就可以利用单例模式。
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
在上次飞碟游戏中也有涉及到单例模式。


(5)消息订阅/发布模式

观察者(Observer)模式:多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。

观察者模式包括以下角色

  • 发布者(事件源)/Publisher:事件或消息的拥有者
  • 主题(渠道)/Subject:消息发布媒体
  • 接收器(地址)/Handle:处理消息的方法
  • 订阅者(接收者)/Subscriber:对主题感兴趣的人

观察者模式的特点

  • 发布者与订阅者没有直接的耦合
  • 发布者与订阅者没有直接关联,可以 多对多 通讯
  • 在MVC设计中,是实现模型与视图分离的重要手段

引用课程的例子:明星希望把自己的动态及时通知粉丝,但她(他)并不喜欢一一通知粉丝,所以用 微信公众号 或 微博 通知粉丝,这样,粉丝仅需知道明星的公众号,然后关注它。

  • Receiver/Subscriber(粉丝) 依赖 具体的公众号Subject(通过Handle接收),而不是 sender/Publisher。
  • 一个公众号可以自动通知很多 receivers

而在多数场景中(Handle的) listener 接口仅包含一个接受消息的方法,因此 C# 对这种情况下的订阅/发布模式做了语言级别的实现,称为事件-代理机制。

对于消息发送方
在这里插入图片描述
对于消息接收方
在这里插入图片描述

五、实现过程和方法(算法)

(1)巡逻兵

(1-1)GuardData

一个巡逻兵所包含的参数有:

public class GuardData : MonoBehaviour {
    public GameObject model;
    public float walkSpeed = 1.3f;         //行走速度
    public float runSpeed = 1.8f;          //奔跑速度
    public int sign;                      //标志巡逻兵在哪一块区域
    public bool isFollow = false;         //是否跟随玩家
    public int playerSign = -1;           //当前玩家所在区域标志
    public Vector3 start_position;        //当前巡逻兵初始位置   

    [SerializeField]
    private Animator anim;
    private Rigidbody rigid;

    void Awake() {
        anim = model.GetComponent<Animator>();//获取animator控制
        rigid = GetComponent<Rigidbody>();  //获取刚体控制
    }

    public void OnGround() {
        anim.SetBool("OnGround", true);
    }
    
    public void OnGroundEnter() {
        
    }
}
(1-2)GuardFactory

巡逻兵工厂创建一组巡逻兵对象, 因此需要有一个队列放置创建好的巡逻兵object并且用作返回。

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]);
                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);//把这个巡逻兵放入到used队列
        }   
        return used;
    }
}
(1-3)巡逻兵的行为

GuardActionManager对巡逻兵的动作类进行了调用:

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);//创建"跟随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();//因为玩家逃脱,调用PlayerEscape修改得分
        }
    }
}

巡逻兵动作实际上在GuardPatrolAction.cs与GuardFollowAction.cs中实现。


GuardPatrolAction使得巡逻兵进行巡逻。

  1. 初始化的时候, GuardPatrolAction加载参数, 并且让巡逻兵播放行走的动画
  2. GuardPatrolAction的动作产生会随机出一个边长数值,令巡逻兵沿着某个方向走随机距离
  3. GuardPatrolAction的fixupdate会使得巡逻兵开始巡逻, 当player当前所在格与巡逻兵的所在格相同时, 因为巡逻兵的动作变更, 巡逻结束, 返回动作完成信息。

GuardFollowAction使得巡逻兵进行追逐。

  1. 初始化的时候, GuardFollowAction加载参数, 并且让巡逻兵播放奔跑的动画
  2. GuardFollowAction令巡逻兵朝着(Lookat)player的方向移动
  3. 如果player脱离了当前巡逻兵所在的格子, 则巡逻兵的动作变更, 追逐结束, 返回动作完成信息。

(2)玩家player对象

(2-1)playerInput

对于player对象以及镜头的控制,首先要得到键盘的输入,这部分的内容在playerInput.cs中。

  1. 判断键盘"W/S"有无按下, 调整前后平移的参数
  2. 判断键盘"A/D"有无按下, 调整左右平移的参数
  3. 判断键盘"↑/↓"有无按下, 调整镜头上下移动的参数
  4. 判断键盘"←/→"有无按下, 调整镜头左右移动的参数
  5. 判断键盘“left shift”有无按下

然后ActorController类, 就会根据playerInput里面的参数(主要依赖参数Dmag、Dvec、run), 执行对player对象的移动控制和动画的播放。
而CameraContoller类则会根据playerInput里面关于镜头的参数(主要依赖参数Jup、Jright), 执行对镜头对象的移动。

(2-2)ActorController

ActorController根据参数调整动画混合树,使得player对象进行行走或奔跑动作,并且计算移动距离的向量。

    void Update() {
        //修改动画混合树
        float targetRunMulti = pi.run ? 2.0f : 1.0f;
        anim.SetFloat("forward", pi.Dmag * Mathf.Lerp(anim.GetFloat("forward"), targetRunMulti, 0.3f));
        
        //转向
        if(pi.Dmag > 0.01f) {
            Vector3 targetForward = Vector3.Slerp(model.transform.forward, pi.Dvec, 0.2f);
            model.transform.forward = targetForward;
        }
        if(!lockPlanar) {
            planarVec = pi.Dmag * model.transform.forward * walkSpeed * (pi.run ? runMultiplier : 1.0f);
        }
        
    }
(2-3)CameraContolle

CameraContolle除了要根据键盘的输入,调整镜头角度之外,还要调整player的朝向。

    void FixedUpdate() {
        Vector3 tempModelEuler = model.transform.eulerAngles;
        playerHandle.transform.Rotate(Vector3.up, pi.Jright * horizontalSpeed * Time.fixedDeltaTime);//调整player朝向
        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;
    }

(3)AreaCollide

AreaCollide 负责侦测玩家当前进入到哪一个格子, 修改参数playerSign(这个参数在FirstSceneController中), 由此别的函数侦测到Sign的变化, 对player进行追逐。

(4)PlayerCollide

当玩家碰撞到巡逻兵时,要求发布事件PlayerGameover。

(5)Director

Director的建立基本是默认的单例、懒汉模式,与之前的作业程序编写一样。
Director在本次作业的程序SSDirector.cs中。

(6)FirstSceneController

FirstSceneController一如既往地负责游戏第一个场景的布景、演员的上下场、管理动作。
FirstSceneController除了继承了MonoBehaviour,还继承了IUserAction、ISceneController,在文件Interface.cs就有这两个抽象类的描述:

public interface ISceneController {
    void LoadResources();
}

public interface IUserAction {
    //得到分数
    int GetScore();
    //得到游戏结束标志
    bool GetGameover();
    //重新开始
    void Restart();
}

FirstSceneController除了要对游戏第一场景进行初始化,还要实现上面这些接口。
FirstSceneController负责管理以下这些参数以及他们相关的动作。

    public GuardFactory guard_factory;                               //巡逻者工厂
    public ScoreRecorder recorder;                                   //计分器
    public GuardActionManager action_manager;                        //运动管理器
    public int playerSign = -1;                                      //当前玩家所处哪个格子
    public GameObject player;                                        //玩家对象
    public UserGUI gui;                                              //交互界面

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

FirstSceneController的Awake()函数就对以上这些参数进行初始化,并且加载资源:

    void Awake() {
        SSDirector director = SSDirector.GetInstance();//设置director
        director.CurrentScenceController = this;
        guard_factory = Singleton<GuardFactory>.Instance;//设置巡逻者工厂
        action_manager = gameObject.AddComponent<GuardActionManager>() as GuardActionManager;//设置运动管理器
        gui = gameObject.AddComponent<UserGUI>() as UserGUI;//设置gui
        LoadResources();    //加载资源(地图, 角色)
        recorder = Singleton<ScoreRecorder>.Instance;//设置计分器
    }

    public void LoadResources() {
        Instantiate(Resources.Load<GameObject>("Prefabs/Plane"));//加载地图
        player = Instantiate(
            Resources.Load("Prefabs/Player"), 
            new Vector3(10, 0, -10), Quaternion.identity) as GameObject;//加载玩家
        guards = guard_factory.GetPatrols();//从工厂获得巡逻兵队列

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

其他的函数:

  1. GetScore()返回recorder的GetScore()函数结果, 返回分数
  2. GetGameover()直接返回参数的game_over
  3. Restart()直接加载游戏初始状态的场景

(7)ScoreRecorder

ScoreRecorder就是拿来记录分数, 增加分数。

public class ScoreRecorder : MonoBehaviour {
    public FirstSceneController sceneController;
    public int score = 0;

    void Start() {
        sceneController = (FirstSceneController)SSDirector.GetInstance().CurrentScenceController; //取得导演
        sceneController.recorder = this;
    }
    public int GetScore() {//返回score的值
        return score;
    }
    public void AddScore() {//增加score
        score++;
    }
}

(8)代理机制-消息发布:GameEventManager

GameEventManager负责发布事件,它实现了Interface.cs中的:

public interface IGameStatusOp {
    void PlayerEscape();
    void PlayerGameover();
}

负责发布游戏的两种状态消息, 第一种是玩家逃离巡逻兵, 第二种是玩家被巡逻兵逮捕。
第一种情况, 需要修改得分
第二种情况, 需要修改bool-game_over

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

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

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

对应地,消息的订阅者就是FirstSceneController,FirstSceneController根据消息,控制游戏状态。

//FirstScenecontroller.cs
 void OnEnable() {
        GameEventManager.ScoreChange += AddScore;
        GameEventManager.GameoverChange += Gameover;
    }
    void OnDisable() {
        GameEventManager.ScoreChange -= AddScore;
        GameEventManager.GameoverChange -= Gameover;
    }

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

    void Gameover() {
        game_over = true;
    }


六、参考资料

  1. 【Unity3D】智能巡逻兵
  2. 【Unity技巧】使用单例模式Singleton
  3. 单例模式\工厂模式
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要实现Unity敌人自动巡逻,可以按照以下步骤操作: 1.创建一个敌人游戏对象,并添加一个NavMeshAgent组件。NavMeshAgent组件可以让敌人在导航网格上移动。 2.在场景中创建一个空对象作为巡逻点,将其命名为“Waypoints”。将巡逻点放置在地图上,以便敌人可以沿着它们移动。可以在Waypoints对象上创建子对象,以便更好地组织巡逻点。 3.编写一个脚本来控制敌人的巡逻行为。该脚本应该附加到敌人游戏对象上。在脚本中,可以使用NavMeshAgent组件来控制敌人的移动。 4.在脚本中定义一个数组来存储巡逻点。可以使用FindGameObjectsWithTag()函数来查找Waypoints对象,并获取其子对象作为巡逻点。 5.在脚本中定义一个变量来存储当前巡逻点的索引。在敌人接近一个巡逻点时,可以将索引增加1,以便敌人移动到下一个巡逻点。如果敌人已到达最后一个巡逻点,则应将索引重置为0,以便敌人开始从头再次巡逻。 6.在Update()函数中,可以检查敌人是否接近当前巡逻点。如果敌人已接近当前巡逻点,则应将索引增加1,并将敌人移动到下一个巡逻点。 7.在脚本中添加一些额外的逻辑,以便敌人可以在巡逻过程中检测并攻击玩家或其他敌人。 这些步骤可以帮助您实现Unity敌人的自动巡逻。需要注意的是,在实现过程中可能会遇到一些问题,需要根据具体情况进行调整和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值