Unity实现智能巡逻兵

Unity实现智能巡逻兵

前言

这是中大计算机学院3D游戏编程课的一次作业,在这里分享一下设计思路。
主要代码上传到了gitee上,请按照后文的操作运行。
项目地址:https://gitee.com/cuizx19308024/unity-games/tree/master/hw5
成果视频:https://www.bilibili.com/video/BV13F41187AD?spm_id_from=333.851.dynamic.content.click

游戏说明

游戏内容要求

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

游戏设计要求

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

项目组成和运行环境

环境配置

  1. 这里导入了第三方库Starter Assets(第三人称)来获取模型和动画,因此需要提前导入这个包,通过Window->Package Manager来管理这些包:
    在这里插入图片描述

  2. 导入这个包之后会出现缺失Cinemachine的错误,因此还需要再导入这个包(不用通过Assets Store下载,直接再Unity Registry中搜索):
    在这里插入图片描述

  3. 由于输入系统不兼容的问题,这里使用旧版输入系统,需要在Edit->Project Settings中对输入系统进行修改:
    在这里插入图片描述

  4. 由于之后的代码中需要用到SampleScene来加载,因此需要将这个Scene加入到BuildPath中。通过File->Build Settings进行修改:
    在这里插入图片描述

  5. 由于游戏中会使用Shift键,因此这里最好修改一下自己电脑的系统设置,防止总切换输入法,影响体验。
    在这里插入图片描述

项目结构

这次的项目包括素材、代码、第三方库和场景:
在这里插入图片描述

  1. 动画部分,包含PlayerController。是根据第三方库的动画剪辑素材,自己制作的Animator,用于控制动画流程。

  2. Material部分,一些渲染的素材。

  3. 预制部分,包括Player、Guard和Cell的预制。这些预制中包含了应当有的脚本、组件(动画、刚体、碰撞体等)。
    在这里插入图片描述

  4. 场景部分,只包含此次的场景。不过在重新开始功能中,需要重新加载这个场景。

  5. 代码部分,下文会说明。

  6. 第三方库部分,这里只是借用了模型、材料和动画剪辑,其余部分全部自己完成。

代码结构

本次的代码结构如下图所示:
在这里插入图片描述

  1. 分为三个部分,GuardControllers、MainControllers、PlayerControllers。
  2. 在主控部分基本与之前的项目相同,这里不赘述。
  3. 巡逻兵控制采用了动作分离模式,两个动作分别是走动和跑动(GuardWalkAction和GuardRunAction),还包括了动作管理器GuardActionManager。
  4. 巡逻兵的产生采用了工厂模式(GuardFactory和GuardData)。
  5. 玩家控制器的PlayerController是一个通过用户输入控制人物移动的角色控制器,不涉及设计模式。
  6. 玩家和巡逻兵、主控器之间的通信采用了订阅/发布模式。
    玩家的SSEventPublishier是基类,PlayerEventPublishier是子类,可以将游戏结束或进出区域的信息发送给巡逻兵或主控制器。
    巡逻兵的IEventHandler是接口,由GuardData来实现这个接口(因为这里有控制Guard运动的数据,可以直接在这里进行修改)。
    主控器一直被放在脚本中,因此可以随时通信。

模型和动画预制

Cell预制

采用12*12的场地,在周围分别搭建围墙,作为每一个cell。当运行时可以直接加载多个cell,设置好位置并加载到地图中。
在这里插入图片描述

动画制作

  1. 制作动画状态。这里只有走、跑、站立三个状态。首先需要导入动画剪辑,然后需要设定一些变量来控制这些状态:
    在这里插入图片描述

  2. 状态转换需要根据变量的变化进行转换,例如:
    在这里插入图片描述

Player预制

  1. 从第三方库中导入Model,然后添加Material。
    在这里插入图片描述

  2. 添加自己制作的Animator:
    在这里插入图片描述

  3. 添加刚体,方便控制运动。注意,需要修改旋转阻力为2,否则会导致人物停下之后仍然在旋转。此外,还应当锁定Y方向移动和绕X、Z轴的旋转,防止人物倒下或因碰撞上浮。
    在这里插入图片描述

  4. 添加碰撞体,注意这里可以调整一下碰撞体的大小和位置。
    在这里插入图片描述

  5. 添加脚本,这里只需要添加角色控制器和发布者即可。
    在这里插入图片描述

Guard预制

  1. 直接将Player稍加修改。保留了Model、Animator、RigidBody和Collider,替换了Material为红色。
    在这里插入图片描述

  2. 添加脚本,这里需要添加GuardData。
    在这里插入图片描述

代码分析

注意:篇幅限制,这里只分析关键代码。之前项目中重复的代码将不再分析。

FirstController主控制器

  1. 加载资源:
    /*ISceneController接口相关函数*/
    public void LoadResources(){
        //加载房间
        for(int i=0;i<9;i++){
            GameObject cell = Instantiate(Resources.Load<GameObject>("Prefabs/Cell"));
            int a = i % 3;
            int b = i / 3;
            cell.transform.position = new Vector3(12 * a, 0, 12 * b);
            cells.Add(cell);
        }
        
        //加载玩家
        player = Instantiate(Resources.Load("Prefabs/Player"), new Vector3(24, 0, 24), Quaternion.identity) as GameObject;
        playerEventPublisher = player.GetComponent<PlayerEventPublisher>();     
    
        //加载巡逻兵
        guards = guardFactory.GetGuards();
    
        //让每个巡逻兵都订阅玩家的事件发布者
        //并让每个巡逻兵调用动作管理器开始走动
        for (int i = 0; i < guards.Count; i++) {
            IEventHandler e = guards[i].GetComponent<GuardData>() as IEventHandler;
            playerEventPublisher.AddListener(e);
            actionManager.GuardWalk(guards[i], player);
        }
    }
    
  2. 重新开始,这里直接重新生成这个场景(注意前面的环境配置第4条操作,否则会报错):
    public void Restart() {
        //重新生成这个场景即可
        SceneManager.LoadScene("Scenes/SampleScene");
    }
    

PlayerController角色控制器

  1. 动画控制,这里的三个变量分别对应动画控制器中的三个变量。
    //获取方向键输入
    Vector3 vel = rigidbody.velocity;
    float v = Input.GetAxis("Vertical");
    float h = Input.GetAxis("Horizontal");
    bool isMove = (Mathf.Abs(v) > 0.05 || Mathf.Abs(h) > 0.05);//防止误触
    
    //判断游戏是否进行,来确定是否可以采用走路、跑步动画
    animator.SetBool("isLive", isLive);
    
    //如果有方向键变化,则设置动画为走
    if(isMove){
        animator.SetBool("isWalk", true);
    }
    if(!isMove){
        animator.SetBool("isWalk", false);
    }
    
    //Shift切换走跑动作
    if(Input.GetKeyDown("left shift")){
        bool ret = animator.GetBool("isRun");
        animator.SetBool("isRun", !ret);
        speed = run_speed;
    }
    
  2. 角色移动控制:
    //在游戏进行状态下,控制角色移动
    if(isMove && isLive){
        //获得相机的正对方向
        camera_rotate = camera.transform.eulerAngles.y / 180 * Mathf.PI;
    
        //给刚体一个速度,让角色向镜头方向,并综合方向键进行移动
        float sr = Mathf.Sin(camera_rotate);
        float cr = Mathf.Cos(camera_rotate);
        rigidbody.velocity = new Vector3((v * sr + h * cr) * speed, 0, (v * cr - h * sr) * speed);
    
        //角色也要面向镜头前方的位置
        transform.rotation = Quaternion.LookRotation(new Vector3((v * sr + h * cr), 0, (v * cr - h * sr)));
    }
    else{
        rigidbody.velocity = Vector3.zero;
    }
    
  3. 镜头控制,需要写在LateUpdate函数中:
    //更新相机的位置,跟随
    camera.transform.position += this.transform.position - offset;
    offset = this.transform.position;
    
    //鼠标控制相机水平移动
    float mouseX = Input.GetAxis("Mouse X") * rotate_speed;
    camera.transform.RotateAround(this.transform.position, Vector3.up, mouseX);
    

EventPublishier事件发布者

  1. 在基类中,需要写通知函数:
    //通知所有的订阅者
    public void Notify(int e){
        for(int i=0; i<listeners.Count; i++){
            IEventHandler eventHandler = listeners[i];
            eventHandler.Reaction(e);
        }
    }
    
  2. 在子类的Update过程中,需要实时监测是否出界或走到了哪个房间:
    private void Update() {
        //获得当前所在房间号
        int ret = FindPosition(this.transform.position);
    
        //发生变化
        if(ret != player_sign){
            if(ret >= 0 && ret <= 8){
                //玩家还在9个房间中,通知所有巡逻兵,让他们的GuardData变化,从而采取不同操作
                player_sign = ret;
                Notify(player_sign);
            }
            else{
                //玩家出界
                GameOver();
            }
            
        }
    }
    
  3. 当出界或碰撞导致游戏结束时,需要通知三方:
    private void OnCollisionEnter(Collision other) {
        if(other.gameObject.name.Contains("Guard")){//撞到巡逻兵,游戏结束
            GameOver();
        }
    }
    
    private void GameOver(){
        //通知三方做处理:
        //玩家控制器灭活,取消玩家的动画和移动
        PlayerController playerController = GetComponent<PlayerController>();
        playerController.isLive = false;
    
        //场景总控制器
        sceneController.Gameover();
    
        //巡逻兵的GuardData变化,取消巡逻兵的动作(立刻回调)并取消动画
        Notify(-1);
    }
    

GuardData巡逻兵数据和事件处理器

  1. 所需数据:
    public float walk_speed = 1.8f;        //移速
    public float run_speed = 3.5f;
    public int sign;                      //巡逻兵所在区域
    public bool isRun = false;            //当前是否在追逐玩家
    public bool isLive = true;            //当前是否活跃
    public Vector3 start_position;        //巡逻兵初始位置   
    
  2. 响应函数,详见注释:
    /*IEventHandler响应函数*/
    public void Reaction(int pos){
        //游戏结束,灭活,取消巡逻兵的动作(立刻回调)并取消动画
        if(pos == -1){
            isLive = false;
            return;
        }
    
        //若玩家处于巡逻兵一样的位置,开始追逐;否则,结束追逐
        isRun = (pos == sign);
    }
    

GuardFactory工厂

  1. 每次获得随机位置
    //为每一个巡逻兵,在一定范围内随机生成位置。
    int[] pos_x = {-5, 7, 19};
    int[] pos_z = {-5, 7, 19};
    int index = 0;
    for(int i=0;i < 3;i++) {
        for(int j=0;j < 3;j++) {
            pos_x[i] += Random.Range(0,3);
            pos_z[i] += Random.Range(0,3);
            vec[index] = new Vector3(pos_x[i], 0, pos_z[j]);
            index++;
        }
    }
    
  2. 导入资源
    //加载巡逻兵,初始化位置和sign,并放在数组中。
    for(int i = 0; i < 8; i++) {
        guard = Instantiate(Resources.Load<GameObject>("Prefabs/Guard"));
        guard.transform.position = vec[i];
        guard.GetComponent<GuardData>().sign = i;
        guard.GetComponent<GuardData>().start_position = vec[i];
        used.Add(guard);
    }  
    

GuardWalkAction巡逻模式下的动作

  1. FixedUpdate的检测:
    public override void FixedUpdate()
    {
        //观察GuardData中的变量isLive(Reaction会改变这个变量)
        //若被灭活,则取消巡逻兵的动作(立刻回调)并取消动画
        if(!data.isLive){
            anim.SetBool("isLive", false);
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, -1, this.gameobject);
            return;
        }
    
        //若不被灭活,则按规则走动
        Walk();
    
        //观察GuardData中的变量isRun(Reaction会改变这个变量)
        //如果玩家进入该区域则开始追击
        if(data.isRun){
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 0, this.gameobject);
        }
    }
    
  2. Walk函数的智能移动方案:
    //获取目标位置,并转向目标位置
    Vector3 dir = new Vector3(pos_x, gameobject.transform.position.y, pos_z);
    gameobject.transform.LookAt(dir);
    
    //由于有高低差等缺陷,因此采用距离而不是比较直接相等
    float distance = Vector3.Distance(dir, this.transform.position);
    
    if(distance > 0.9){
        //未到达,需要借助RigidBody移动
        Vector3 vec = data.walk_speed * gameobject.transform.forward;
        rigid.MovePosition(gameobject.transform.position + vec * Time.deltaTime);
    }
    else{
        //到达,转向
        Turn();
    }
    
  3. 碰撞墙壁时转向:
    private void OnCollisionEnter(Collision other) {
        if(other.gameObject.name.Contains("Wall")){
            //撞墙,转向
            Turn();
        }
    }
    

GuardRunAction追击模式下的动作

  1. FixedUpdate的检测:
    public override void FixedUpdate() {
        //观察GuardData中的变量isLive(Reaction会改变这个变量)
        //若被灭活,则取消巡逻兵的动作(立刻回调)并取消动画
        if(!data.isLive){
            anim.SetBool("isLive", false);
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, -1, this.gameobject);
            return;
        }
        
        //若不被灭活,则跑向玩家
        Run();
    
        //观察GuardData中的变量isRun(Reaction会改变这个变量)
        //如果玩家脱离该区域则继续巡逻
        if (!data.isRun) {
            this.destroy = true;
            this.callback.SSActionEvent(this, SSActionEventType.Competeted, 1, this.gameobject);
        }
    }
    
  2. Run跑动方案:
    //控制巡逻兵跑向玩家
    Vector3 dir = player.transform.position - player.transform.forward; //玩家偏后一点的位置
    gameobject.transform.LookAt(dir);   //转向
    Vector3 vec = data.run_speed * gameobject.transform.forward;    //设置移动向量
    rigid.MovePosition(gameobject.transform.position + vec * Time.deltaTime);   //借助RigidBody移动
    

GuardActionManager动作管理器

  1. 这里是我一开始出现的问题,因为没有给他一个初始运动方式。因此,需要一个初始函数来让巡逻兵先走动巡逻:
    public void GuardWalk(GameObject guard, GameObject player) {
        //首先需要让巡逻兵走动
        this.player = player;
        GuardWalkAction walkAction = GuardWalkAction.GetSSAction(guard.transform.position);
        this.RunAction(guard, walkAction, this);
    }
    
  2. 动作的切换:
    if (intParam == 0) {
        //巡逻状态返回,开始追逐
        GuardRunAction runAction = GuardRunAction.GetSSAction(player);
        this.RunAction(objectParam, runAction, this);
    } else if(intParam == 1){
        //追逐状态返回,开始巡逻
        GuardWalkAction walkAction = GuardWalkAction.GetSSAction(objectParam.GetComponent<GuardData>().start_position);
        this.RunAction(objectParam, walkAction, this);
        sceneController.Record();
    }
    //对于其他状态,则是动作直接销毁,不再生成新动作
    

实验结果

具体的实验结果请参考展示视频
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值