在Unity中创建塔防:塔与射击敌人

[教程的第一部分和第二部分]

  • 我们放在塔场上。
  • 我们在物理的帮助下瞄准敌人。
  • 我们会尽可能地跟踪它们。
  • 我们用激光束射击它们。

这是有关创建简单塔防类型的一系列教程的第三部分。 它描述了塔的创建,瞄准和射击敌人。

该教程是在Unity 2018.3.0f2中创建的。


让我们加热敌人。

塔楼创作


城墙只会使敌人减速,增加他们走的路途。 但是游戏的目标是在敌人到达终点之前消灭他们。 通过将塔架放置在可向其射击的场上,可以解决此问题。

平铺内容


塔是平铺内容的另一种类型,因此GameTileContentGameTileContentGameTileContent添加一个条目。

 public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint, Tower€ } 

在本教程中,我们将仅支持一种类型的塔,可以通过向GameTileContentFactory一个到塔预制件的链接来实现,也可以通过Get来创建其实例。

  [SerializeField] GameTileContent towerPrefab = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … case GameTileContentType.Tower€: return Get(towerPrefab); } … } 

但是这些塔必须射击,因此它们的状况需要更新,并且需要自己的代码。 为此目的创建一个Tower类,以扩展GameTileContent类。

 using UnityEngine; public class Tower : GameTileContent {} 

您可以通过将工厂字段类型更改为Tower来使Tower预制件具有其自己的组件。 由于该类仍被视为GameTileContent ,因此无需更改其他任何内容。

  Tower towerPrefab = default; 

预制件


为塔创建一个预制件。 您可以从复制墙壁预制件开始,然后将其GameTileContent组件替换为Tower组件,然后将其类型更改为Tower 。 为了使塔适合墙壁,请将立方体墙保存为塔的基础。 然后将另一个立方体放在其顶部。 我给他0.5分。 在其上放另一个立方体,表明是炮塔,该部分将瞄准并向敌人射击。



形成塔的三个立方体。

炮塔将旋转,并且由于它具有对撞机,因此将由物理引擎跟踪。 但是我们不必如此精确,因为我们仅使用塔对撞机来选择像元。 这可以近似完成。 卸下转塔立方体​​对撞机,然后更换塔式立方体对撞机,使其覆盖两个立方体。



对撞机立方体塔。

塔将发射激光束。 它可以通过多种方式可视化,但是我们仅使用半透明的立方体,我们将其拉伸以形成光束。 每个塔必须有自己的梁,因此将其添加到塔的预制件中。 将其放置在转塔内,以便默认情况下将其隐藏,并为其设置较小的比例,例如0.2。 让我们将其作为预制根的子级,而不是转塔立方体​​。

激光束

等级制

激光束的隐藏立方体。

为激光束创建合适的材料。 我只是使用标准的半透明黑色材料并关闭了所有反射,还给了它红色的发光颜色。

颜色

没有思考

激光束的材料。

检查激光束是否没有对撞机,并关闭其投射和阴影。


激光束不与阴影相互作用。

完成塔式预制件的创建后,我们将其添加到工厂中。


有塔的工厂。

塔的位置


我们将使用另一种切换方法添加和删除塔。 您可以通过更改方法名称和内容类型来简单地复制GameBoard.ToggleWall

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower€) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower€); if (!FindPaths()) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } } 

Game.HandleTouch ,按住Shift键将切换塔楼而不是墙壁。

  void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile); } else { board.ToggleWall(tile); } } } 


在球场上的塔。

路径阻塞


到目前为止,只有墙壁可以阻止寻找路径,因此敌人会穿过塔楼。 让我们向GameTileContent添加GameTileContent辅助属性,该属性指示内容是否阻塞路径。 如果路径是墙或塔,则将其阻塞。

  public bool BlocksPath => Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€; 

GameTile.GrowPathTo使用此属性,而不是检查内容类型。

  GameTile GrowPathTo (GameTile neighbor, Direction direction) { … return //neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; neighbor.Content.BlocksPath ? null : neighbor; } 


现在,这条路已被墙壁和高楼阻挡。

更换墙壁


玩家很可能会经常用塔代替墙壁。 首先要拆除墙对他来说很不方便,此外,敌人还可以穿透这个暂时出现的缝隙。 您可以通过强制GameBoard.ToggleTower检查墙当前是否在瓷砖GameBoard.ToggleTower实现直接替换。 如果是这样,请立即用塔更换它。 在这种情况下,我们不必寻找其他方式,因为图块仍会阻止它们。

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); } } 

我们瞄准敌人


塔只有找到敌人才能完成其任务。 找到敌人后,她必须决定将目标对准哪一部分。

瞄准点


为了检测目标,我们将使用物理引擎。 与塔对撞机一样,我们不需要敌方对撞机一定与其形状重合。 您可以选择最简单的对撞机,即球体。 在检测到敌人之后,我们将使用附着有对撞机的游戏对象的位置作为瞄准点。

我们无法将对撞机附加到敌人的根对象上,因为对撞机并不总是与模型的位置重合,而是会使塔对着地面。 也就是说,您需要将对撞机放置在模型上的某个位置。 物理引擎将为我们提供指向该对象的链接,我们可以使用该链接进行瞄准,但是我们仍然需要访问根对象的“ Enemy组件。 为了简化任务,让我们创建TargetPoint组件。 让我们为它提供一个属性以供私人分配和公开接收Enemy组件,以及另一个属性以获取其在世界上的地位。

 using UnityEngine; public class TargetPoint : MonoBehaviour { public Enemy Enemy€ { get; private set; } public Vector3 Position => transform.position; } 

让我们给它一个Awake方法,该方法建立到其Enemy组件的链接。 使用transform.root直接转到根对象。 如果“ Enemy组件不存在,那么我们在制造敌人时犯了一个错误,因此我们为此添加一个声明。

  void Awake () { Enemy€ = transform.root.GetComponent<Enemy>(); Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); } 

此外,对撞机必须附加到与TargetPoint附加到的同一游戏对象上。

  Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); Debug.Assert( GetComponent<SphereCollider>() != null, "Target point without sphere collider!", this ); 

向敌人的预制立方体中添加组件和对撞机。 这将使塔对准立方体的中心。 我们使用半径为0.25的球形对撞机。 立方体的比例为0.5,因此对撞机的真实半径为0.125。 由于这个原因,敌人将不得不在视觉上越过塔的射程圆,并且只有一段时间后才可以实现真正的目标。 对撞机的大小也会受到敌人随机比例的影响,因此在游戏中它的大小也会略有不同。


检查员

一个具有瞄准点并在立方体上具有对撞机的敌人。

敌人层


塔只在乎敌人,它们不会瞄准其他任何东西,因此我们会将所有敌人放在单独的图层中。 我们将使用第9层。在“ 层和标签”窗口中将其名称更改为Enemy ,可以通过编辑器右上角的“ 层”下拉菜单中的“ 编辑层”选项打开。


第9层将用于敌人。

该层仅用于识别敌人,而无需进行物理交互。 让我们在项目参数的“ 物理”面板中的“ 层碰撞矩阵”中禁用它们来指出。


图层碰撞矩阵。

确保瞄准点的游戏对象在所需的图层上。 敌人的其他预制件可能在其他层上,但是协调所有内容并将整个预制件放置在敌人层会更容易。 如果更改根对象的层,将提示您更改其所有子对象的层。


右边的敌人。

让我们添加一下声明: TargetPoint确实在正确的图层上。

  void Awake () { … Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this); } 

此外,敌人的对撞机必须忽略玩家的行为。 这可以通过在GameBoard.GetTilePhysics.Raycast添加一个图层蒙版参数来实现。 此方法的形式采用到光束和图层蒙版的距离作为附加参数。 默认情况下,我们将为其提供最大距离和图层蒙版,即1。

  public GameTile GetTile (Ray ray) { if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) { … } return null; } 

图层蒙版不应该为0吗?
默认的图层索引为零,但是我们传递了图层蒙版。 如果需要打开图层,则掩码会将整数的各个位更改为1。 在这种情况下,您只需要设置第一位,即最低有效位,即2 0 ,等于1。

更新图块内容


塔只有在状态更新时才能执行其任务。 整个图块的内容也是如此,尽管到目前为止,其余所有内容均不起作用。 因此,将GameTileContent虚拟方法添加到GameUpdate ,默认情况下不执行任何操作。

  public virtual void GameUpdate () {} 

让我们重新定义Tower ,即使现在它只是简单地在控制台中显示它正在寻找目标。

  public override void GameUpdate () { Debug.Log("Searching for target..."); } 

GameBoard处理图块及其内容,因此它还将跟踪需要更新哪些内容。 为此,将列表添加到其中,并添加公共GameUpdate方法,该方法将更新列表中的所有内容。

  List<GameTileContent> updatingContent = new List<GameTileContent>(); … public void GameUpdate () { for (int i = 0; i < updatingContent.Count; i++) { updatingContent[i].GameUpdate(); } } 

在我们的教程中,您只需要更新塔。 更改ToggleTower以便在必要时添加和删除内容。 如果还需要其他内容,我们将需要一种更通用的方法,但是到目前为止,这已经足够了。

  public void ToggleTower (GameTile tile) { if (tile.Content.Type == GameTileContentType.Tower) { updatingContent.Remove(tile.Content); tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(GameTileContentType.Tower); //if (!FindPaths()) { if (FindPaths()) { updatingContent.Add(tile.Content); } else { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(GameTileContentType.Tower); updatingContent.Add(tile.Content); } } 

为了完成这项工作,现在我们只需更新Game.Update的字段就Game.Update 。 我们将在敌人之后更新该领域。 因此,这些塔将能够准确瞄准敌人所在的位置。 如果我们不这样做,这些塔将瞄准敌人在最后一帧中的位置。

  void Update () { … enemies.GameUpdate(); board.GameUpdate(); } 

瞄准范围


塔的瞄准半径有限。 让我们通过向Tower类添加一个字段来使其自定义。 该距离是从塔瓦的中心开始测量的,因此在0.5的范围内,它将仅覆盖自己的瓦。 因此,合理的最小和标准范围应为1.5,覆盖大多数相邻图块。

  [SerializeField, Range(1.5f, 10.5f)] float targetingRange = 1.5f; 


瞄准范围2.5。

让我们用Gizmo可视化范围。 我们不需要经常看到它,因此我们将创建仅对所选对象调用的OnDrawGizmosSelected方法。 我们绘制球体的黄色框,其半径等于距离并相对于塔为中心。 将其稍微放在地面上方,以便始终清晰可见。

  void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } 


Gizmo瞄准范围。

现在我们可以看到哪些敌人是每个塔楼可承受的目标。 但是在场景窗口中选择塔很不方便,因为我们必须选择一个子多维数据集,然后切换到塔的根对象。 其他类型的图块内容也遇到相同的问题。 通过将SelectionBase属性添加到GameTileContent ,可以在场景窗口中强制选择GameTileContent内容的根。

 [SelectionBase] public class GameTileContent : MonoBehaviour { … } 

目标捕获


将一个TargetPoint字段添加到Tower类,以便它可以跟踪其捕获的目标。 然后,我们GameUpdate来调用新的AquireTarget方法,该方法返回有关是否找到目标的信息。 检测到后,它将在控制台中显示一条消息。

  TargetPoint target; public override void GameUpdate () { if (AcquireTarget()) { Debug.Log("Acquired target!"); } } 

AcquireTarget我们通过调用Physics.OverlapSphere以塔的位置和范围作为参数来获取所有可用的目标。 结果将是一个Collider数组,其中包含所有与球体接触的碰撞体。 如果阵列的长度为正,则至少有一个瞄准点,我们只需选择第一个即可。 取其必须始终存在的TargetPoint组件,将其分配给目标字段并报告成功。 否则,我们将清除目标并报告故障。

  bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange ); if (targets.Length > 0) { target = targets[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targets[0]); return true; } target = null; return false; } 

如果仅考虑敌人层上的对撞机,则可以确保获得正确的瞄准点。 这是第9层,因此我们将传递相应的图层蒙版。

  const int enemyLayerMask = 1 << 9; … bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange, enemyLayerMask ); … } 

该位掩码如何工作?
由于敌方层的索引为9,因此位掩码的第十位应具有值1。这对应于整数2 9 ,即512。但是这样的位掩码记录是不直观的。 我们还可以编写二进制文字,例如0b10_0000_0000 ,但随后我们必须计算零。 在这种情况下,最方便的输入将是使用左移位运算符<< ,该运算符会将这些位向左移位。 对应于2的幂的数字。

您可以通过在塔筒和目标位置之间绘制一条Gizmo线来可视化捕获的目标。

  void OnDrawGizmosSelected () { … if (target != null) { Gizmos.DrawLine(position, target.Position); } } 


目标的可视化。

为什么不使用OnTriggerEnter之类的方法?
手动检查跨领域目标的优势在于我们只能在必要时执行此操作。 如果塔楼已经有目标,则没有理由检查目标。 此外,通过一次获取所有潜在目标,我们不必处理不断变化的每座塔的潜在目标清单。

目标锁


选择捕获的目标取决于物理引擎表示它们的顺序,也就是说,实际上是任意的。 因此,似乎捕获的目标无缘无故地发生了变化。 塔楼接收到目标后,让她追踪她的目标而不是切换到另一个目标更为合乎逻辑。 添加一个TrackTarget方法,该方法实现这种跟踪并返回有关是否成功的信息。 首先,我们只会告诉您目标是否被捕获。

  bool TrackTarget () { if (target == null) { return false; } return true; } 

我们将在GameUpdate调用此方法,只有返回false时,才调用AcquireTarget 。 如果该方法返回true,则我们有一个目标。 可以通过使用OR运算符将两个方法调用放入if检查中来完成,因为如果第一个操作数返回true ,则第二个操作数将不被检查,并且该调用将丢失。 AND运算符的行为类似。

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Debug.Log("Locked on target!"); } } 


跟踪目标。

结果,塔被固定在目标上,直到到达终点并被破坏。 如果您反复使用敌人,则需要检查链接的正确性,就像在一系列“ 对象管理”教程中处理的图形的链接一样。

要仅在目标在范围内时跟踪目标, TrackTarget必须跟踪塔架与目标之间的距离。 如果超出范围值,则必须重置目标并返回false。 您可以使用Vector3.Distance方法进行此检查。

  bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; if (Vector3.Distance(a, b) > targetingRange) { target = null; return false; } return true; } 

但是,此代码未考虑对撞机的半径。 因此,塔可能会失去目标,然后再次捕获它,只是在下一帧中停止跟踪它,依此类推。 我们可以通过在范围内添加对撞机半径来避免这种情况。

  if (Vector3.Distance(a, b) > targetingRange + 0.125f) { … } 

这给了我们正确的结果,但前提是不改变敌人的规模。 由于我们给每个敌人一个随机的比例尺,因此在改变射程时必须将其考虑在内。 为此,我们需要记住Enemy给定的比例,并使用getter属性将其打开。

  public float Scale { get; private set; } … public void Initialize (float scale, float speed, float pathOffset) { Scale = scale; … } 

现在,我们可以在Tower.TrackTarget检查正确的范围。

  if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … } 

我们同步物理学


一切似乎都运行良好,但是可以对准视野中心的发射塔能够捕获应该超出范围的目标。 他们将无法跟踪这些目标,因此只能将它们固定在一帧上。


瞄准不正确。

发生这种情况是因为物理引擎的状态与游戏状态不完全同步。 所有敌人的实例都是在世界原点创建的,与世界的中心重合。 然后,我们将它们移至创建点,但是物理引擎并不立即知道这一点。

您可以通过将Physics.autoSyncTransforms设置为true来启用在更改对象转换时发生的瞬时同步。 但是默认情况下,它是禁用的,因为在必要时将所有内容同步到一起效率更高。 在我们的情况下,仅在更新塔的状态时才需要同步。 我们可以通过在Game.Update敌人和野外更新之间调用Physics.SyncTransforms来执行它。

  void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); } 

忽略身高


实际上,我们的游戏玩法是以2D进行的。 因此,让我们更改Tower以便在瞄准和跟踪时仅考虑X和Z坐标物理引擎在3D空间中工作,但从本质上讲,我们可以在2D中执行AcquireTarget :将球体向上拉伸,使其覆盖所有对撞机,无论从垂直位置开始。 这可以通过使用胶囊而不是球体来完成,该胶囊的第二点将在地面上方几个单位(例如三个)。

  bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 3f; Collider[] targets = Physics.OverlapCapsule( a, b, targetingRange, enemyLayerMask ); … } 

不可能使用物理2D引擎吗?
, XZ, 2D- XY. , , 2D- . 3D-.

也有必要改变TrackTarget当然,我们可以使用2D向量和Vector2.Distance,但是让我们自己进行计算,而是比较距离的平方,这就足够了。因此,我们摆脱了计算平方根的操作。

  bool TrackTarget () { if (target == null) { return false; } Vector3 a = transform.localPosition; Vector3 b = target.Position; float x = ax - bx; float z = az - bz; float r = targetingRange + 0.125f * target.Enemy€.Scale; if (x * x + z * z > r * r) { target = null; return false; } return true; } 

这些数学计算如何工作?
2D- , . , . , , .

避免分配内存


使用它的缺点Physics.OverlapCapsule是它为每个调用分配一个新的数组。可以通过分配一次数组并调用OverlapCapsuleNonAlloc将数组作为附加参数的替代方法来避免这种情况传输数组的长度决定结果的数量。阵列之外的所有潜在目标都将被丢弃。相同的是,我们将只使用第一个元素,因此长度为1的数组足以满足我们的需要,

而不是数组,它OverlapCapsuleNonAlloc返回已发生的冲突数量,直到允许的最大数量,这是我们将检查的数量而不是数组的长度。

  static Collider[] targetsBuffer = new Collider[1]; … bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 2f; int hits = Physics.OverlapCapsuleNonAlloc( a, b, targetingRange, targetsBuffer, enemyLayerMask ); if (hits > 0) { target = targetsBuffer[0].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", targetsBuffer[0]); return true; } target = null; return false; } 

我们向敌人射击


现在我们有了一个真正的目标,是时候去实现它了。射击包括瞄准,激光射击和造成伤害。

瞄准塔


要将炮塔指向目标,该类Tower需要链接到Transform炮塔组件为此添加一个配置字段,并将其连接到塔式预制件。

  [SerializeField] Transform turret = default; 


附属的炮塔。

如果GameUpdate有真实的目标,那么我们必须射击它。将拍摄代码放在单独的方法中。让他将炮塔向目标旋转Transform.LookAt,以瞄准点作为参数调用他的方法

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { //Debug.Log("Locked on target!"); Shoot(); } } void Shoot () { Vector3 point = target.Position; turret.LookAt(point); } 


只是瞄准。

我们发射激光


为了定位激光束,班级Tower还需要一个链接。

  [SerializeField] Transform turret = default, laserBeam = default; 


我们连接了激光束。

要将立方体变成真实的激光束,您需要执行三个步骤。首先,其方位应与转塔的方位相对应。可以通过复制其旋转方式来完成。

  void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; } 

其次,我们对激光束进行缩放,以使其长度等于转塔原点与瞄准点之间的距离。我们沿着Z轴(即指向目标的局部轴)缩放比例。为了保留原始的XY标度,我们在唤醒Awake炮塔时记下原始标度。

  Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } … void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; float d = Vector3.Distance(turret.position, point); laserBeamScale.z = d; laserBeam.localScale = laserBeamScale; } 

第三,我们将激光束放置在炮塔和瞄准点之间的中间位置。

  laserBeam.localScale = laserBeamScale; laserBeam.localPosition = turret.localPosition + 0.5f * d * laserBeam.forward; 


激光射击。

不可能使激光束成为炮塔的孩子吗?
, , forward. , . .

当转塔固定在目标上时,此方法有效。但是,当没有目标时,激光保持激活状态。我们可以通过GameUpdate激光显示比例尺设置0 来关闭激光显示

  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } 


空闲的塔不会着火。

敌人健康


到目前为止,我们的激光束仅接触敌人,不再影响他们。必须确保激光确实会伤害敌人。我们不想立即消灭敌人,所以我们将给予Enemy健康。您可以选择任何值作为生命值,因此取100。但是,对于大型敌人来说,拥有更多生命值会更加合乎逻辑,因此我们将为此引入一个系数。

  float Health { get; set; } … public void Initialize (float scale, float speed, float pathOffset) { … Health = 100f * scale; } 

要增加对造成损害的支持,请添加一个ApplyDamage从健康中减去其参数的公共方法我们将假定损害为非负性,因此我们对此进行声明。

  public void ApplyDamage (float damage) { Debug.Assert(damage >= 0f, "Negative damage applied."); Health -= damage; } 

敌人的生命值达到零时,我们不会立即摆脱它。开始时将检查身体是否精疲力竭以及敌人是否被摧毁GameUpdate

  public bool GameUpdate () { if (Health <= 0f) { OriginFactory.Reclaim(this); return false; } … } 

因此,所有的塔实际上都会同时射击,而不是依次射击,如果前一个塔摧毁了他们也瞄准的敌人,这将允许它们切换到其他目标。

每秒伤害


现在我们需要确定激光会造成多大的损害。为此,请添加到Tower配置字段。由于激光束会造成持续的损坏,因此我们将其表示为每秒的损坏。我们Shoot将其Enemy乘以增量时间应用于目标组件

  [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); } 

检查员


每座塔的伤害为每秒20个单位。

随机瞄准


由于我们总是选择第一个可用的目标,因此瞄准行为取决于物理引擎检查相交对撞机的顺序。这种依赖性不是很好,因为我们不知道细节,我们无法控制它,而且它看起来很奇怪而且前后不一致。通常,这种行为会导致火灾集中,但并非总是如此。

让我们增加一些随机性,而不是完全依赖于物理引擎。这可以通过增加对撞机接收到的相交点的数量(例如最多100个)来完成。也许这不足以使一个敌人密集地包围着所有可能的目标,但这足以改善瞄准。

  static Collider[] targetsBuffer = new Collider[100]; 

现在,我们将从数组中选择一个随机元素,而不是选择第一个潜在目标。

  bool AcquireTarget () { … if (hits > 0) { target = targetsBuffer[Random.Range(0, hits)].GetComponent<TargetPoint>(); … } target = null; return false; } 


随机瞄准。

可以使用其他选择目标的标准吗?
, , . , , . . .

因此,在我们的塔防游戏中,塔终于出现了。在下一部分中,游戏将进一步发挥其最终形状。

Source: https://habr.com/ru/post/zh-CN459070/


All Articles