美文网首页征服Unity3dunity3D技术分享
《土豆荣耀》重构笔记(六)实现怪物的AI

《土豆荣耀》重构笔记(六)实现怪物的AI

作者: RainbowCyan | 来源:发表于2019-01-09 14:35 被阅读1次

    前言

      在游戏里面,为了提高游戏的难度,增加游戏的趣味性,往往会根据游戏的需要实现怪物AI。一般来说,一个最基本的怪物AI需要包括自动巡逻看到玩家攻击玩家玩家离开恢复自动巡逻等功能。对于一些状态比较复杂的怪物AI,还需要使用行为树来辅助实现。

      在本篇文章中,我们要实现的怪物AI逻辑十分简单,怪物只需要在场景中以恒定速度移动,当遇到障碍物时转弯朝反方向继续行走即可。因此,我们在实现怪物AI的逻辑时没有用到行为树或者状态机

      此外,因为本篇文章是《土豆荣耀》重构笔记系列文章中第一篇涉及脚本编写的文章,所以在开始阅读本篇文章之间,可以先看一下如何使用VS Code编写Unity脚本。此外,本篇文章默认读者已经知道Unity脚本是如何工作的,熟悉获取组件的引用以及使用Unity提供的api等基本操作,对C#的基本语法也有一定的了解。


    为场景添加Collider

      为了能让怪物在场景上行走,我们需要为怪物和场景添加Collider。在Hierarchy窗口中选中需要添加碰撞体的GameObject,然后点击右侧Inspector窗口中的Add Componnet按钮,选择Physics 2D之后,我们就可以选择Collider类型并添加。

    添加Collider

      了解了如何添加Collider之后,我们先为场景添加Collider。场景中能和角色、怪物产生交互的物体都存放在Foreground下,它们添加的Collider属性如下所示:

    Foreground下的物体添加的Collider:

    • env_TowerFull:
      • Collider: Box Collider 2D
      • Offset: (0, 0)
      • Size: (7.3, 27)
    • env_TowerFull (1):
      • Collider: Box Collider 2D
      • Offset: (0, 0)
      • Size: (7.3, 27)
    • env_PlatformBridge:
      • Collider: Box Collider 2D
      • Offset: (0.8, 0.8)
      • Size: (15.5, 1.6)
    • env_PlatformBridge (1):
      • Collider: Box Collider 2D
      • Offset: (0.8, 0.8)
      • Size: (15.5, 1.6)
    • env_PlatformTop:
      • Collider: Box Collider 2D
      • Offset: (0, 0.12)
      • Size: (9.6, 2.6)
    • env_PlatformTop (1):
      • Collider: Box Collider 2D
      • Offset: (0, 0.12)
      • Size: (9.6, 2.6)
    • env_PlatformUfo:
      • Collider: Polygon Collider 2D

    为怪物添加Collider和Rigidbody

      接着,我们在Hierarchy选中AlienSlugAlienShip为它们添加Collider。

    AlienSlugAlienShip添加的Collider信息如下:

    • AlienSlug:
      • Collider: Capsule Collider 2D
      • Offset: (0, 0)
      • Size: (1.14, 1.74)
    • AlienShip:
      • Collider: Circle Collider 2D
      • Offset: (0.1, 0)
      • Radius: 0.9

      点击运行游戏,我们发现怪物悬浮在空中,这是因为我们没有给它们添加刚体组件它们没有物理属性。在2D项目中,如果我们想让一个物体具有重力、速度等物理属性,我们需要给这个物体添加Rigidbody2D组件。Rigidbody2D组件也位于Add Component\Physics 2D目录下。接下来,我们为AlienSlugAlienShip添加Rigidbody2D组件。

      添加完成后,再次运行游戏,可以看到怪物受重力影响掉落下来,且发生了翻滚。我们想让怪物一直保持直立,因此我们需要在Rigidbody2DConstraints属性里设置勾选Freeze Rotation Z,不让物体在进行物理模拟时,绕Z轴进行旋转。

    限制旋转

      设置完成之后,我们再次运行游戏,可以看到怪物会受到重力影响,且一直保持直立,不会翻滚。最后,我们需要将我们所做的修改应用到它们的Prefab上。在Hierarchy分别选中AlienSlugAlienShip,然后在Inspector窗口中点击Apply按钮即可。

    应用修改到Prefab上

    让怪物动起来

      接下来,我们开始编写脚本来实现让怪物在场景中移动的功能。我们在Assets下创建一个名为Scripts的文件夹,然后在Scripts文件夹下创建一个名为Enemy的文件夹用于保存和怪物相关的脚本。创建完毕后,我们在Enemy文件夹下创建脚本Wander.cs,然后双击打开。

      既然想要让怪物在场景中移动,那么我们就需要先知道怪物移动的速度以及方向。首先在Wander.cs加入以下代码:

    [Tooltip("是否朝向右边")]
    [SerializeField]
    private bool FacingRight = true;
    
    [Tooltip("怪物移动的速度")]
    [SerializeField]
    private float MoveSpeed = 2f;
    

    代码说明:

    • Tooltip
      • Unity提供的一个Attribute,参数为string;
      • 我们可以使用Tooltip这一Attribute来设置提示的内容,当我们将鼠标悬停在Inspector窗口显示的参数上时,我们可以看到提示的内容
    • SerializeField
      • Unity提供的一个Attribute,没有参数;
      • Unity只会在Inspector窗口中显示可见性为public的字段,通过使用SerializeField这一Attribute,可以强制在Inspector窗口中显示可见性为privateprotected的字段

      接下来,我们要让怪物动起来,需要给怪物的Rigidbody2D组件设置速度,在Wander.cs加入以下代码:

    //用于设置怪物对象的物理属性
    private Rigidbody2D m_Rigidbody;
    // 用于保存当前的水平移动速度
    private float m_CurrentMoveSpeed;
    
    // 获取组件引用
    private void Awake() {
        m_Rigidbody = GetComponent<Rigidbody2D>();
    }
    
    // 设置字段的初始值
    private void Start() {
        if(FacingRight) {
            m_CurrentMoveSpeed = MoveSpeed;
        } else {
            m_CurrentMoveSpeed = -MoveSpeed;
        }
    }
    
    // 执行和物理相关的代码
    private void FixedUpdate() {
        m_Rigidbody.velocity = new Vector2(m_CurrentMoveSpeed, m_Rigidbody.velocity.y);
    }
    

    代码说明:
      上面代码涉及到的AwakeStartFixedUpdate都是Unity提供的生命周期函数,感兴趣的读者可以查看Unity各生命周期函数的执行顺序来了解它们的执行顺序和作用。

      接着,我们将Wander.cs添加到AlienSlugAlienShip上并运行游戏,可以看到场景中的两个怪物已经动了起来,但是出现了重叠的现象。我们不希望场景中的怪物会产生碰撞等物理交互,所以,我们还需要做一些额外的工作。


    一些额外的工作

      Unity为了方便我们决定Sprite的渲染顺序,提供了Sorting Layer。类似地,为了让我们更方便地管理场景中物体的渲染和物理模拟,Unity也提供了Layer。首先,我们新建一个名为Enemy的Layer,然后将AlienSlugAlienShip的Layer都设置为Enemy。切换Layer时,我们选择Yes, Change the children将子物体的Layer都设置为Enemy

    创建Layer

      接着,我们在Unity的顶部菜单栏选择Edit->Project Settings->Physics 2D打开2D项目的物理设置窗口,然后在Layer Collision Matrix中取消Enemy-Enemy那一项的勾选,告诉Unity,不对都处于Enemy这一Layer的两个物体进行任何物理碰撞模拟。再次运行游戏,可以看到两个怪物已经不会重叠了。

    Layer Collision Matrix

    实现怪物遇到障碍物转向的功能

      目前我们的怪物还只有移动的功能,当它们遇到障碍物的时候,会被卡住,我们需要让它们在遇到障碍物时自动转向。我们可以使用Physics2D.OverlapPointAll来获取场景里某个点上所有的Collider,但我们如何辨别这些Collider是障碍物,还是其他物体呢?答案是,通过LayerLayerMask。所谓的LayerMask,其实就是一个用二进制来表示的int类型变量哪个位上的值为1,就代表对以该位为下标的Layer执行相应的操作。

    例如,我们之前创建的Enemy的Layer下标为8,那么当LayerMask的值为128(二进制的10000000)时,代表我们会对所有Layer为Enemy的物体进行操作

      新建一个名为Obstacle的Layer,然后将所有障碍物的Layer都设置为Obstacle。设置完毕之后,我们在Assets\Scripts\Enemy下新建脚本Enemy.cs,然后在Enemy.cs中加入以下代码:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    [RequireComponent(typeof(Wander))]
    public class Enemy : MonoBehaviour {
        [Tooltip("障碍物检测点")]
        [SerializeField]
        private Transform FrontCheck;
    
        private Wander m_Wander;
        private LayerMask m_LayerMask;
    
        private void Awake() {
            m_Wander = GetComponent<Wander>();  
        }
    
        private void Start() {
            m_LayerMask = LayerMask.GetMask("Obstacle");
        }
    
        private void Update () {
            Collider2D[] frontHits = Physics2D.OverlapPointAll(FrontCheck.position, m_LayerMask);
    
            if(frontHits.Length > 0) {
                m_Wander.Flip();
            }
        }
    }
    

    代码说明:

    • RequireComponent
      • Unity提供的一个Attribute,参数为Type
      • RequireComponent[(typeof(Wander))]表示在添加前,必须给该物体添加定义了Wander这个类的脚本,不然会报错
    • LayerMask.GetMask("Obstacle")表示直接获得Obstacle这个Layer对应的LayerMask

      接着,我们在Wander.cs脚本里添加Flip函数,Wander.cs完成代码如下:

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class Wander : MonoBehaviour {
        [Tooltip("是否朝向右边")]
        [SerializeField]
        private bool FacingRight = true;
    
        [Tooltip("怪物水平移动的速度")]
        [SerializeField]
        private float MoveSpeed = 2f;
    
    
        //用于设置怪物对象的物理属性
        private Rigidbody2D m_Rigidbody;
        // 用于保存当前的水平移动速度
        private float m_CurrentMoveSpeed;
    
        // 获取组件引用
        private void Awake() {
            m_Rigidbody = GetComponent<Rigidbody2D>();
        }
    
        // 设置字段的初始值
        private void Start() {
            if(FacingRight) {
                m_CurrentMoveSpeed = MoveSpeed;
            } else {
                m_CurrentMoveSpeed = -MoveSpeed;
            }
        }
    
        // 执行和物理相关的代码
        private void FixedUpdate() {
            m_Rigidbody.velocity = new Vector2(m_CurrentMoveSpeed, m_Rigidbody.velocity.y);
        }
    
        // 转向函数
        public void Flip() {
            m_CurrentMoveSpeed *= -1;
            
            this.transform.localScale = Vector3.Scale(new Vector3(-1, 1, 1), this.transform.localScale);
        }
    }
    

      最后,我们将Enemy.cs添加到AlienSlugAlienShip中,并在AlienSlugAlienShip下新建一个名为FrontCheckEmpty GameObject。设置FrontCheckPosition(1, 0, 0),接着拖拽FrontCheck,将其复制给Enemy.cs脚本上的FrontCheck属性。运行游戏,可以看到怪物已经能正常转向了。


    修改Sorting Layer

      运行游戏的时候,我们发现AlienSlug的尾巴被UFO遮住了,我们需要调整一下Sorting Layer的渲染顺序。在Hierarchy窗口下点击AlienShip的子物体char_enemy_alienShip,然后点击Add Sorting LayerSorting Layer的顺序调整为下图所示的顺序:

    修改Sorting Layer

      至此,我们所有的修改就都完成了,点击AlienSlugAlienShipInspector窗口中的Apply按钮将我们所做的修改应用到Prefab中,再保存场景产生的修改即可。


    后言

      AlienSlugAlienShipWander脚本的MoveSpeed的值我默认给的是2,大家可以根据自己的喜好进行调整。最后,本篇文章所做的修改,可以在PotatoGloryTutorial这个仓库的essay4分支下看到,读者可以clone这个仓库到本地进行查看。


    参考链接

    1. 使用VS Code编写Unity脚本
    2. Execution Order of Event Functions
    3. Editing a Prefab via its instances
    4. Layer和LayerMask

    相关文章

      网友评论

        本文标题:《土豆荣耀》重构笔记(六)实现怪物的AI

        本文链接:https://www.haomeiwen.com/subject/qefgrqtx.html