在Unity中创建攻击Slot系统

作者: 归零者 分类: Unity3D,未分类 发布时间: 2018-12-16 21:10
在Unity中创建攻击Slot系统

如果您打算在3D(或自顶向下的)游戏中有多个攻击玩家的敌人,那么您将需要一个攻击槽系统。 在我们的最后一篇科技文章中,我们开始构建一个航点路径系统。 我们继续使用AI教程并构建攻击槽系统! 这里的一个重要细节是,该系统实际上并不适用于基于路基的系统,因此我们将使用Unity内置的navmesh支持跟踪slot效果
 
什么是Attack Slots
攻击槽系统通常相当简单,可以使战斗看起来更好。 基本上,他们的做法是为每个攻击者分配一个攻击位置,以便攻击者不受束缚,位置可以是围绕攻击者,或者正面排列面对玩家。 如果您一次只希望有一个攻击者,那这个系统就不会有任何帮助,但是有两个以上的攻击者这可能是一个非常有用的工具。
没有Attack Slots

带有Attack Slots

应该很清楚的是,在Attack Slots系统中,AI控制的实体看起来更加聪明,行为更有条理。
 
创建场景

首先要做的是与玩家和敌人一起创建一个简单的场景。 我刚刚添加了一个plane和一些立方体和气缸的障碍物。 然后为玩家以及敌人创建了一个胶囊。
确保您已将导航窗口打开并对焦。 然后创建NavMesh,选择plane,立方体和圆柱体(但不是播放器和敌人),并在导航窗口的对象选项卡上选择导航静态:

现在你可以去转到烘焙选项卡,点击烘焙按钮:

你应该看到这样的东西:

下一步是将Nav Mesh Agent组件添加到Enemy和Player中:

不要担心任何设置,如果你不想要的。
 
player控制器
让我们制作一个简单的PlayerController脚本,以便我们可以移动播放器:
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class PlayerController : MonoBehaviour {
        // Use this for initialization
        void Start () {
               
        }
       
        // Update is called once per frame
        void Update () {
                if (Input.GetMouseButtonDown (0))
                {
                        var mpos = Input.mousePosition;
                        mpos.z = 10;
                        var ray = Camera.main.ScreenPointToRay (mpos);
                        RaycastHit hit;
                        if (Physics.Raycast (ray, out hit))
                        {
                                GetComponent<NavMeshAgent> ().destination = hit.point;
                        }
                }
        }
}
在更新中,我们只是检查鼠标左键是否按下此框。 如果是这样,通过屏幕将光线投射到鼠标的位置。 如果它碰到任何东西,指导玩家移动到那一点。 所以现在我们可以点击左键移动player。 将此组件附加到player。
初始敌军控制器
现在来简单的创建一下EnemyController:
 
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class EnemyController : MonoBehaviour {
        GameObject target = null;
        float pathTime = 0f;
        // Use this for initialization
        void Start () {
                target = GameObject.Find ("Player");
        }
       
        // Update is called once per frame
        void Update () {
                pathTime += Time.deltaTime;
                if (pathTime > 0.5f)
                {
                        pathTime = 0f;
                        var tpos = target.transform.position;
                        var offset = (transform.position - tpos).normalized * 1.5f;
                        GetComponent<NavMeshAgent> ().destination = tpos + offset;
                }
        }
}
首先,在开始,我们只是缓存播放器作为我们的目标。 然后在更新中,每0.5秒,我们得到玩家的位置,从我们的方向计算出1.5个单位的偏移量,然后将其设置为路径目的地。 将此组件附加到敌人。 这就是没有攻击槽系统的东西。

这只是一个敌人。 很多敌人看起来不太好:

这通常会使你的敌人看起来不智能,如果你是在根据目标的距离进行攻击,那么背后的敌人可能不会被攻击。 如果他们都可以在玩家身上摆动,这样他们可以更快地杀死他们会更好! 这是我们的AttackSlots引进的原因。
 
创建Slot Manager
我们可以创建一个新的文件 SlotManager并添加一些初始代码:
 
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class SlotManager : MonoBehaviour
{
        private List<GameObject> slots;
        public int count = 6;
        public float distance = 2f;
        void Start()
        {
                slots = new List<GameObject> ();
                for (int index = 0; index < count; ++index)
                {
                        slots.Add (null);
                }
        }
}
SlotManager真的只包含一个用于GameObjects的插槽列表,然后是参数来定义要创建多少个插槽以及它们距离防御器的距离。 在开始,我们只是将这些插槽初始化为null。 该系统的工作方式,如果一个插槽为空,那么它是空的。 当它设置为一个GameObject时,它被认为在使用或者满了。
 
获得Slots的位置
我们需要一个函数来返回Slots的位置。 我们希望这些slot围绕GameObject排列成一个圆圈:

每个线框球代表一个槽,第一个槽是顶部的。 之后插槽顺时针旋转。 以下是GetSlotPosition的代码:
        public Vector3 GetSlotPosition(int index)
        {
                float degreesPerIndex = 360f / count;
                var pos = transform.position;
                var offset = new Vector3 (0f, 0f, distance);
                return pos + (Quaternion.Euler(new Vector3(0f, degreesPerIndex * index, 0f)) * offset);
        }
因此,首先,我们计算每个索引的度数,以确定每个插槽有多远(角度方向)。 然后,我们旋转一个向量指向z方向的新向量,该插槽的度数,并将其添加到我们的位置以获得插槽位置。
 
预留槽位
现在要做一个方法来为攻击者预留一个插槽:
        public int Reserve(GameObject attacker)
        {
                var bestPosition = transform.position;
                var offset = (attacker.transform.position – bestPosition).normalized * distance;
                bestPosition += offset;
                int bestSlot = -1;
                float bestDist = 99999f;
                for (int index = 0; index < slots.Count; ++index)
                {
                        if (slots [index] != null)
                                continue;
                        var dist = (GetSlotPosition (index) – bestPosition).sqrMagnitude;
                        if (dist < bestDist)
                        {
                                bestSlot = index;
                                bestDist = dist;
                        }
                }
                if (bestSlot != -1)
                        slots [bestSlot] = attacker;
                return bestSlot;
        }
这会变得更复杂一些。 您可能会从EnemyController的初始代码中识别前3行:
                // EnemyController.cs
                var tpos = target.transform.position;
                var offset = (transform.position – tpos).normalized * 1.5f;
                GetComponent<NavMeshAgent> ().destination = tpos + offset;
                // SlotManager.cs
                var bestPosition = transform.position;
                var offset = (attacker.transform.position – bestPosition).normalized * distance;
                bestPosition += offset;
如前所述,我们发现一个靠近防守者的位置在攻击者的方向。 这将是我们想要的槽的最佳位置,因为它理想地意味着不必走到通常看起来不好的防守者的另一边。 接下来,我们通过所有插槽,找到当前没有使用的最接近的插槽。 如果存在的话,我们将它与攻击者进行填充,所以没有人可以接受攻击。就是这样!
 
释放Slot
[C#] 纯文本查看 复制代码
1
2
3
public void Release(int slot)
{
        slots [slot] = null;
}
如果你紧跟我的步骤,下面要做的并不奇怪。 所有需要做的是将哪个信号保留为空的插槽设置为空,以供下一个攻击者使用。
 
添加一个调试演示
还有一件事我们可以做,如果需要,这是添加一些Gizmos像在上面的图像。 为此,我们可以定义OnDrawGizmosSelected:
 
      

[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
void OnDrawGizmosSelected()
       {
               for (int index = 0; index < count; ++index)
               {
                       if (slots == null || slots.Count <= index || slots [index] == null)
                               Gizmos.color = Color.black;
                       else
                               Gizmos.color = Color.red;
                       Gizmos.DrawWireSphere (GetSlotPosition (index), 0.5f);
               }
       }
 
基本上,在编辑器中这样做是为了显示每个插槽。 在播放模式下,如果它们已被保留,它会将颜色显示为红色。
 
更新敌方控制器
最后一件事是向EnemyController添加一些将使用这个新系统的代码。 我们需要添加一个新的私有变量来保存当前保留的插槽,然后更改更新功能:
 
[C#] 纯文本查看 复制代码
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EnemyController : MonoBehaviour {
        GameObject target = null;
        float pathTime = 0f;
        int slot = -1;
        // Use this for initialization
        void Start () {
                target = GameObject.Find ("Player");
        }
       
        // Update is called once per frame
        void Update () {
                pathTime += Time.deltaTime;
                if (pathTime > 0.5f)
                {
                        pathTime = 0f;
                        var slotManager = target.GetComponent<SlotManager> ();
                        if (slotManager != null)
                        {
                                if (slot == -1)
                                        slot = slotManager.Reserve (gameObject);
                                if (slot == -1)
                                        return;
                                var agent = GetComponent<NavMeshAgent> ();
                                if (agent == null)
                                        return;
                                agent.destination = slotManager.GetSlotPosition (slot);
                        }
                }
        }
}
现在,Update只是每0.5秒执行一次,但现在它在目标上得到了SlotManager。然后,如果没有分配槽,它会尝试预留一个槽。如果失败了,我们还没有别的办法,所以我们回来了。如果它有一个插槽,那么它只是使用GetSlotPosition将导航目标引导到插槽的位置。这就是它的一切!
 
打包和未来的改进
在此之前,您必须将SlotManager组件附加到player,但是一旦这样做,这里就是几个敌人的样子:
需要注意的一件事是使用一对攻击对手的攻击插槽。如果他们都有攻击槽,他们可能会尝试永远相互围绕。相反,您需要检查这种情况,并且让任何想要攻击的人首先获得攻击槽。另一个参与者应该瞄准自己的位置,使攻击者的位置与攻击者的位置匹配。
 
我们还可以添加一些改进,例如禁用在navmesh或墙壁另一侧的插槽,自动增加插槽数以适应攻击者的数量,或者我们可以根据不同的攻击范围添加多个攻击槽环。现在,我会把这些留给你来实现!
 

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!

说点什么

avatar
  Subscribe  
提醒
跳至工具栏