在Unity中创建塔防:弹道学

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

  • 支持不同类型的塔。
  • 创建砂浆塔。
  • 抛物线轨迹的计算。
  • 发射爆炸弹。

这是有关创建简单塔防游戏的教程的第四部分。 在其中,我们将添加迫击炮塔,在碰撞中发射引爆弹。

该教程是在Unity 2018.4.4f1中创建的。


敌人被炸。

塔的类型


激光并不是唯一可以放置在炮塔上的武器。 在本教程中,我们将添加第二种类型的塔,该塔将在接触时发射爆炸的炮弹,对附近的所有敌人造成伤害。 为此,我们需要各种类型的塔的支持。

抽象塔


目标检测和跟踪是任何塔楼均可使用的功能,因此我们将其放在塔楼的抽象基类中。 为此,我们只使用Tower类,但首先,复制其内容以供以后在特定的LaserTower类中使用。 然后我们从Tower删除所有与激光相关的代码。 塔楼可能无法跟踪特定目标,因此请删除target字段并更改AcquireTargetTrackTarget以便将输出参数用作链接参数。 然后,我们将从OnDrawGizmosSelected删除OnDrawGizmosSelected可视化,但是我们将保留瞄准范围,因为它用于所有塔楼。

 using UnityEngine; public abstract class Tower : GameTileContent { const int enemyLayerMask = 1 << 9; static Collider[] targetsBuffer = new Collider[100]; [SerializeField, Range(1.5f, 10.5f)] protected float targetingRange = 1.5f; protected bool AcquireTarget (out TargetPoint target) { … } protected bool TrackTarget (ref TargetPoint target) { … } void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } } 

让我们更改重复的类,以使其变为LaserTower ,它扩展Tower并使用其基类的功能,从而消除重复的代码。

 using UnityEngine; public class LaserTower : Tower { [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; [SerializeField] Transform turret = default, laserBeam = default; TargetPoint target; Vector3 laserBeamScale; void Awake () { laserBeamScale = laserBeam.localScale; } public override void GameUpdate () { if (TrackTarget(ref target) || AcquireTarget(out target)) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } void Shoot () { … } } 

然后更新激光塔的预制件以使用新组件。


激光塔的组件。

创建特定类型的塔


为了能够选择要放置在现场的塔,我们将添加一个类似于GameTileContentTypeTowerType枚举。 我们将为现有的激光塔和迫击炮塔提供支持,我们将在稍后创建。

 public enum TowerType { Laser, Mortar } 

由于我们将为每种类型的塔创建一个类,因此将向塔添加一个抽象的getter属性以指示其类型。 这与“ 对象管理”系列教程中人物的行为类型相似。

  public abstract TowerType TowerType€ { get; } 

LaserTower重新定义它,使其返回正确的类型。

  public override TowerType TowerType€ => TowerType.Laser; 

接下来,更改GameTileContentFactory以便工厂可以生产所需类型的塔。 我们使用TowerType数组实现此目标,并使用TowerType参数添加替代的公共Get方法。 为了验证阵列配置正确,我们将使用断言。 现在,另一个公共Get方法将仅适用于没有塔的磁贴内容。

  [SerializeField] Tower[] towerPrefabs = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … } Debug.Assert(false, "Unsupported non-tower type: " + type); return null; } public GameTileContent Get (TowerType type) { Debug.Assert((int)type < towerPrefabs.Length, "Unsupported tower type!"); Tower prefab = towerPrefabs[(int)type]; Debug.Assert(type == prefab.TowerType€, "Tower prefab at wrong index!"); return Get(prefab); } 

返回最特定的类型是合乎逻辑的,因此理想情况下,新Get方法的返回类型应为Tower 。 但是用于实例化预制件的私有Get方法返回GameTileContent 。 在这里,您可以执行转换,也可以使私有的Get方法通用。 让我们选择第二个选项。

  public Tower Get (TowerType type) { … } T Get<T> (T prefab) where T : GameTileContent { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } 

虽然我们只有激光塔,但我们将使其成为工厂塔架阵列中的唯一元素。


一系列预制塔。

创建特定塔类型的实例


要创建特定类型的塔,我们GameBoard.ToggleTower ,使其需要TowerType参数TowerTypeTowerType给工厂。

  public void ToggleTower (GameTile tile, TowerType towerType) { if (tile.Content.Type == GameTileContentType.Tower€) { … } else if (tile.Content.Type == GameTileContentType.Empty) { tile.Content = contentFactory.Get(towerType); … } else if (tile.Content.Type == GameTileContentType.Wall) { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } } 

这创造了一个新的机会:塔的状态在已存在时切换,但是塔的类型多种多样。 到目前为止,切换只是删除了现有的塔,但是将其替换为新类型是合乎逻辑的,因此让我们实现它。 由于磁贴仍然很忙,因此您无需再次搜索路径。

  if (tile.Content.Type == GameTileContentType.Tower€) { updatingContent.Remove(tile.Content); if (((Tower)tile.Content).TowerType€ == towerType) { tile.Content = contentFactory.Get(GameTileContentType.Empty); FindPaths(); } else { tile.Content = contentFactory.Get(towerType); updatingContent.Add(tile.Content); } } 

Game现在应该跟踪可切换塔的类型。 我们仅用数字表示每种类型的塔。 激光塔是1,将是默认塔,灰浆塔是2。按数字键,我们将选择合适的塔类型。

  TowerType selectedTowerType; … void Update () { … if (Input.GetKeyDown(KeyCode.G)) { board.ShowGrid = !board.ShowGrid; } if (Input.GetKeyDown(KeyCode.Alpha1)) { selectedTowerType = TowerType.Laser; } else if (Input.GetKeyDown(KeyCode.Alpha2)) { selectedTowerType = TowerType.Mortar; } … } … void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile, selectedTowerType); } else { board.ToggleWall(tile); } } } 

砂浆塔


由于尚未安装预制砂浆塔,因此尚无法放置。 让我们从创建最小的MortarTower类型开始。 迫击炮的发射频率很高,以指示您可以使用“每秒发射”配置字段。 另外,我们需要一个链接到迫击炮,以便瞄准。

 using UnityEngine; public class MortarTower : Tower { [SerializeField, Range(0.5f, 2f)] float shotsPerSecond = 1f; [SerializeField] Transform mortar = default; public override TowerType TowerType€ => TowerType.Mortar; } 

现在为砂浆塔创建一个预制件。 这可以通过复制激光塔的预制件并更换其塔组件来完成。 然后我们摆脱了塔的物体和激光束。 将turret重命名为mortar ,向下移动,使其立于底座顶部,使其呈浅灰色并连接。 在这种情况下,我们可以使用单独的对象离开迫击炮对撞机,该对象是叠加在迫击炮默认方向上的简单对撞机。 我分配的迫击炮射程为3.5,频率为每秒1发。

场景

等级制

检查员

砂浆塔的预制件。

他们为什么叫迫击炮?
这种武器的第一个变种基本上是铁钵,类似于研钵,其中的成分是使用杵研磨的。

将预制砂浆添加到工厂阵列中,以便可以在现场放置砂浆塔。 但是,他们还没有做任何事情。

检查员

场景

两种类型的塔,其中一种不活动

轨迹计算


莫尔蒂拉以一定角度射击炮弹,使他飞越障碍物并从上方击中目标。 通常,使用的炮弹在与目标碰撞时会爆炸。 为了不使事物复杂化,我们将始终瞄准地面,以使壳体的高度下降到零时会爆炸。

水平瞄准


为了瞄准迫击炮,我们需要将其水平指向目标,然后改变其垂直位置,使弹丸以正确的距离着陆。 我们将从第一步开始。 首先,我们将使用固定的相对点,而不是移动的目标,以确保我们的计算正确。

GameUpdate添加一个MortarTower方法,该方法始终调用Launch方法。 现在,我们将可视化数学计算,而不是发射真实的弹丸。 着火点是砂浆在世界上的位置,它位于地面上方。 我们将目标的点沿X轴放置在距其三个单位的位置,并将Y分量归零,因为我们始终瞄准地面。 然后,我们将通过调用Debug.DrawLine在它们之间Debug.DrawLine黄线来显示这些点。 该线将在场景模式下显示一帧,但这已经足够了,因为在每一帧中我们都绘制了一条新线。

  public override void GameUpdate () { Launch(); } public void Launch () { Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); Debug.DrawLine(launchPoint, targetPoint, Color.yellow); } 


我们的目标是相对于塔固定的点。

使用这条线,我们可以定义一个直角三角形。 他的最高点在迫击炮位置。 关于砂浆,这是 \开bmatrix00\结bmatrix 。 在塔底的下面的点是 \开bmatrix0y\结bmatrix ,目标是 \开bmatrixxy\结bmatrix 在哪里 x 等于3,并且 y 是砂浆的负垂直位置。 我们需要跟踪这两个值。

  Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); float x = 3f; float y = -launchPoint.y; 


三角瞄准。

通常,目标可以在发射塔范围内的任何位置,因此还必须考虑Z。 但是,瞄准三角形仍然保持二维,它仅绕Y轴旋转,为说明这一点,我们将在Launch添加相对位移矢量的参数,并在XZ中将其称为四个位移: \开bmatrix30\结bmatrix\开bmatrix01\结bmatrix\开bmatrix11\结bmatrix\开bmatrix31\结bmatrix 。 当瞄准点等于射击点加上此偏移量时,其Y坐标将变为零。

  public override void GameUpdate () { Launch(new Vector3(3f, 0f, 0f)); Launch(new Vector3(0f, 0f, 1f)); Launch(new Vector3(1f, 0f, 1f)); Launch(new Vector3(3f, 0f, 1f)); } public void Launch (Vector3 offset) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = launchPoint + offset; targetPoint.y = 0f; … } 

现在,瞄准三角形的x等于从塔的底部指向瞄准点的2D矢量的长度。 通过对该向量进行归一化,我们还可以获得XZ方向向量,该向量可用于对齐三角形。 您可以通过将三角形的底部绘制为从方向和x获得的白线来显示它。

  Vector2 dir; dir.x = targetPoint.x - launchPoint.x; dir.y = targetPoint.z - launchPoint.z; float x = dir.magnitude; float y = -launchPoint.y; dir /= x; Debug.DrawLine(launchPoint, targetPoint, Color.yellow); Debug.DrawLine( new Vector3(launchPoint.x, 0.01f, launchPoint.z), new Vector3( launchPoint.x + dir.x * x, 0.01f, launchPoint.z + dir.y * x ), Color.white ); 


对齐的瞄准三角形。

射角


接下来,我们应该找出射弹的角度。 有必要从弹丸轨迹的物理学中得出它。 我们将不考虑阻力,风阻和其他障碍,仅考虑射击速度 v 和重力 g=9.81

偏移量 d 射弹与瞄准三角形成一直线,并且可以由两个部分来描述。 使用水平位移,很简单: dx=vxt 在哪里 t -射击后的时间。 对于垂直分量,一切都相似,然后由于重力而受到负加速度,因此它具有以下形式 dy=vytgt2/2

偏移量计算如何执行?
速度 v 由每秒的距离确定,因此,将速度乘以持续时间 t 我们得到了距离 d=vt 。 当涉及加速时 ,速度是可变的。 加速度是每秒速度的变化,即每秒的距离的平方。 在任何时候,速度都是 v= 。 在我们的情况下,持续不断的加速 a=g ,因此我们可以将其除以一半以获得平均速度,然后乘以时间以找到偏移量 d=at2/2 由重力引起。

我们以相同的速度射击炮弹 s 这不取决于拍摄角度  theta (θ)。 那是 vx=s cos thetavy=s sin theta


射击速度的计算。

执行替换,我们得到 dx=st cos thetady=st sin thetagt2/2

发射子弹,使其飞行时间 t 是实现目标所需的确切价值。 由于使用水平位移更容易,我们可以将时间表示为 t=dx/vx 。 在终点 dx=x 那就是 t=x/s cos theta 。 这意味着 y=x tan thetagx2/2s2 cos2 theta

如何得到方程y?
y=dy=sx/s cos theta sin thetagx/s cos theta2/2=x sin theta/ cos thetagx2/2s2 cos2 theta tan theta= sin theta/ cos theta

使用这个方程我们发现  tan theta=s2+ sqrts4ggx2+2ys2/gx
如何得到方程tanθ?
首先,我们将使用三角恒等式 \秒 theta=1/ cos theta1+ tan2 theta= sec2 thetay=x tan thetagx2/2s21+ tan2 theta=gx2/2s2 tan2 theta+x tan thetagx2/2s2

这是形式的表达 au2+bu+c=0 在哪里 u=\棕 thetaa=gx2/2s2b=xc=

我们可以使用二次方程的根公式来解决 u=b+ sqrtb24ac/2a

替换后,方程将变得混乱,但是您可以通过乘以来简化方程 m=s2/x 所以得到  tan theta=mb+m sqrtr/2ma 在哪里 r=b24ac

在这种情况下,我们获得  tan theta=s2+ sqrtm2r/gx

结果 m2r=s4/x2r=s4+2gs2c=s4g2x22gys2=s4ggx2+2ys2

有两种可能的角度,因为您可以瞄准高或低。 低路径的速度更快,因为它更接近目标线。 但是高轨迹看起来更有趣,因此我们将选择它。 这意味着我们只需要使用最大的解决方案。  tan theta=s2+ sqrts4ggx2+2ys2/gx 。 我们计算它,并且  cos theta\罪 theta ,因为我们需要它们来获取镜头的速度矢量。 为此,您需要转换 \棕 theta 使用Mathf.Atan将弧度调整为弧度。 首先,让我们使用5的恒定拍摄速度。

  float x = dir.magnitude; float y = -launchPoint.y; dir /= x; float g = 9.81f; float s = 5f; float s2 = s * s; float r = s2 * s2 - g * (g * x * x + 2f * y * s2); float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta; 

让我们通过绘制十个蓝色的线段来可视化轨迹,这些线段表示飞行的第一秒。

  float sinTheta = cosTheta * tanTheta; Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { float t = i / 10f; float dx = s * cosTheta * t; float dy = s * sinTheta * t - 0.5f * g * t * t; next = launchPoint + new Vector3(dir.x * dx, dy, dir.y * dx); Debug.DrawLine(prev, next, Color.blue); prev = next; } 


抛物线的飞行路径持续一秒钟。

可以在不到一秒钟的时间内到达两个最远的点,因此我们可以看到它们的全部轨迹,并且这些线段继续在地下延伸一点。 对于其他两个点,需要更大的发射角,因此轨迹变长,飞行持续一秒以上。

射速


如果您想在一秒钟内到达最近的两点,则需要降低拍摄速度。 让我们等于4。

  float s = 4f; 


射击速度降低到4。

现在他们的轨迹已经完成,但其他两个都消失了。 之所以发生这种情况,是因为射击速度现在不足以达到这些点。 在这种情况下, \棕 theta 不,也就是说,我们得到负数的平方根,导致NaN值和线条消失。 我们可以通过检查来识别 消极。

  float r = s2 * s2 - g * (g * x * x + 2f * y * s2); Debug.Assert(r >= 0f, "Launch velocity insufficient for range!"); 

通过设置足够高的注射速度可以避免这种情况。 但是,如果太大,则要击中塔附近的目标将需要很高的轨迹和较长的飞行时间,因此您应将速度保持尽可能低的水平。 射击速度应足以在最大范围内击中目标。

在最大范围 r=0 ,即 \棕 theta 只有一种解决方案,对应的轨迹很短。 这意味着我们知道所需的射击速度。 s= sqrtgy+ sqrtx2+y2

如何得出s的等式?
需要决定 s4ggx2+2ys2=s42gys2g2x2=0s

这是形式的表达 au2+bu+c=0 在哪里 u=s2a=1b=2gyc=g2x2

您可以使用二次方程根的简化公式来求解 u=b+ sqrtb24c/2

替换后我们得到 s2=2gy+ sqrt4g2y2+4g2x2/2=gy+g sqrtx2+y2

我们需要一个积极的解决方案,所以我们来 s2=gy+ sqrtx2+y2

仅在迫击炮唤醒(唤醒)或在“播放”模式下更改其范围时,才需要确定所需的速度。 因此,我们将使用该字段跟踪它并在AwakeOnValidate对其进行计算。

  float launchSpeed; void Awake () { OnValidate(); } void OnValidate () { float x = targetingRange; float y = -mortar.position.y; launchSpeed = Mathf.Sqrt(9.81f * (y + Mathf.Sqrt(x * x + y * y))); } 

但是,由于浮点计算精度的限制,确定非常接近最大范围的目标可能是错误的。 因此,在计算所需速度时,我们会在范围内添加少量。 另外,敌人的对撞机的半径实质上扩大了塔架射程的最大半径。 我们将其设置为等于0.125,但是随着敌人规模的增加,它可以尽可能地增加一倍,因此我们将实际范围增加0.25,例如增加0.25001。

  float x = targetingRange + 0.25001f; 

接下来,对Launch中的射击速度应用导出的方程式。

  float s = launchSpeed; 


将计算出的速度应用于瞄准范围3.5。

射击


正确计算了轨迹,您可以摆脱相对的测试目标。 现在,您需要将Launch点传递给目标。

  public void Launch (TargetPoint target) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = target.Position; targetPoint.y = 0f; … } 

此外,并非每帧都拍摄照片。 我们需要以与创建敌人的过程相同的方式跟踪射击过程,并在GameUpdate射击时间到来时捕获随机目标。 但是目前,可能没有任何目标可用。 在这种情况下,我们将继续进行射击,但不会进一步累积。 为了避免无限循环,您需要使其小于1。

  float launchProgress; … public override void GameUpdate () { launchProgress += shotsPerSecond * Time.deltaTime; while (launchProgress >= 1f) { if (AcquireTarget(out TargetPoint target)) { Launch(target); launchProgress -= 1f; } else { launchProgress = 0.999f; } } } 

我们不会在两次射击之间跟踪目标,但是我们需要在射击过程中正确旋转迫击炮。 您可以使用镜头的水平方向使用Quaternion.LookRotation水平旋转砂浆。 我们还需要 \棕 theta 为方向向量的分量Y应用射击角度。 这将起作用,因为水平方向的长度为1,即  tan theta= sin theta


外观的转弯向量的分解。

  float tanTheta = (s2 + Mathf.Sqrt(r)) / (g * x); float cosTheta = Mathf.Cos(Mathf.Atan(tanTheta)); float sinTheta = cosTheta * tanTheta; mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y)); 

要仍然查看镜头的轨迹,可以在Debug.DrawLine中添加一个参数,使它们可以长时间绘制。

  Vector3 prev = launchPoint, next; for (int i = 1; i <= 10; i++) { … Debug.DrawLine(prev, next, Color.blue, 1f); prev = next; } Debug.DrawLine(launchPoint, targetPoint, Color.yellow, 1f); Debug.DrawLine( … Color.white, 1f ); 


瞄准

炮弹


计算轨迹的意思是我们现在知道如何射击炮弹。 接下来,我们需要创建它们并进行射击。

战争工厂


我们需要一个工厂来实例化外壳对象。 在空中时,炮弹是独立存在的,不再依赖于发射炮弹的迫击炮。 因此,不应使用砂浆塔对其进行处理,并且瓷砖含量工厂也不适合这样做。让我们为与武器相关的所有内容创建一个新工厂,然后将其称为战争工厂。首先,创建一个WarEntity具有属性OriginFactory和方法的摘要Recycle

 using UnityEngine; public abstract class WarEntity : MonoBehaviour { WarFactory originFactory; public WarFactory OriginFactory { get => originFactory; set { Debug.Assert(originFactory == null, "Redefined origin factory!"); originFactory = value; } } public void Recycle () { originFactory.Reclaim(this); } } 

然后Shell为外壳创建一个特定的实体

 using UnityEngine; public class Shell : WarEntity { } 

然后WarFactory使用public getter属性创建一个将创建射弹的射弹。

 using UnityEngine; [CreateAssetMenu] public class WarFactory : GameObjectFactory { [SerializeField] Shell shellPrefab = default; public Shell Shell€ => Get(shellPrefab); T Get<T> (T prefab) where T : WarEntity { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } public void Reclaim (WarEntity entity) { Debug.Assert(entity.OriginFactory == this, "Wrong factory reclaimed!"); Destroy(entity.gameObject); } } 

为弹丸创建一个预制件。我使用了一个具有0.25的比例尺和深色材质的简单多维数据集以及一个组件Shell然后创建工厂资产并为其分配射弹的预制件。


战争工厂。

游戏行为


要移动外壳,需要对其进行更新。您可以使用与Game更新敌人状态相同的方法实际上,我们甚至可以通过创建GameBehavior扩展MonoBehaviour并添加虚拟方法的抽象组件使这种方法通用化GameUpdate

 using UnityEngine; public abstract class GameBehavior : MonoBehaviour { public virtual bool GameUpdate () => true; } 

现在进行重构EnemyCollection,将其转换为GameBehaviorCollection

 public class GameBehaviorCollection { List<GameBehavior> behaviors = new List<GameBehavior>(); public void Add (GameBehavior behavior) { behaviors.Add(behavior); } public void GameUpdate () { for (int i = 0; i < behaviors.Count; i++) { if (!behaviors[i].GameUpdate()) { int lastIndex = behaviors.Count - 1; behaviors[i] = behaviors[lastIndex]; behaviors.RemoveAt(lastIndex); i -= 1; } } } } 

让我们WarEntity扩展GameBehavior,而不是MonoBehavior

 public abstract class WarEntity : GameBehavior { … } 

我们将Enemy为此做同样的事情,这次是重写方法GameUpdate

 public class Enemy : GameBehavior { … public override bool GameUpdate () { … } … } 

从现在开始,Game它将必须跟踪两个集合,一个集合用于敌人,另一个集合用于非敌人。必须在其他所有内容之后更新非敌人。

  GameBehaviorCollection enemies = new GameBehaviorCollection(); GameBehaviorCollection nonEnemies = new GameBehaviorCollection(); … void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); } 

实施Shell升级的最后一步是将它们添加到非敌人的集合中。让我们使用一个功能Game将其作为战争工厂的静态立面来执行此操作,以便可以通过Challenge创建弹丸Game.SpawnShell()为此,您Game必须具有指向war factory的链接并跟踪您自己的实例。

  [SerializeField] WarFactory warFactory = default; … static Game instance; public static Shell SpawnShell () { Shell shell = instance.warFactory.Shell€; instance.nonEnemies.Add(shell); return shell; } void OnEnable () { instance = this; } 


与战争工厂的游戏。

静态外观是一个好的解决方案吗?
, , .

我们拍贝壳


创建射弹实例后,它应该沿着其路径飞行直到到达最终目标。为此,添加到Shell方法中Initialize并使用它来指定拍摄点,目标点和拍摄速度。

  Vector3 launchPoint, targetPoint, launchVelocity; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity ) { this.launchPoint = launchPoint; this.targetPoint = targetPoint; this.launchVelocity = launchVelocity; } 

现在我们可以在其中创建外壳MortarTower.Launch并将其发送到路上。

  mortar.localRotation = Quaternion.LookRotation(new Vector3(dir.x, tanTheta, dir.y)); Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); 

弹丸运动


Shell移动,我们需要跟踪其存在的持续时间,即从射击开始经过的时间。然后我们可以计算他在中的位置GameUpdate我们始终针对其发射点执行此操作,因此,无论刷新率如何,射弹均完美地遵循路径。

  float age; … public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; transform.localPosition = p; return true; } 


脱壳。

为了使壳与它们的轨迹对齐,我们需要使它们沿着导出的向量看,这是它们在相应时间的速度。

  public override bool GameUpdate () { … Vector3 d = launchVelocity; dy -= 9.81f * age; transform.localRotation = Quaternion.LookRotation(d); return true; } 


炮弹在转动。

我们清理游戏


既然可以清楚地看到炮弹正按其应有的飞行状态,则可以从MortarTower.Launch可视化效果中删除轨迹。

  public void Launch (TargetPoint target) { … Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); } 

另外,我们需要确保击中目标后将炮弹销毁。由于我们始终瞄准地面,因此可以通过检查Shell.GameUpdate垂直位置是否低于零来完成。计算它们之后,您可以立即执行此操作,然后再更改位置并旋转弹丸。

  public override bool GameUpdate () { age += Time.deltaTime; Vector3 p = launchPoint + launchVelocity * age; py -= 0.5f * 9.81f * age * age; if (py <= 0f) { OriginFactory.Reclaim(this); return false; } transform.localPosition = p; … } 

爆轰


我们射击炮弹是因为它们含有炸药。当弹丸到达目标时,它必须爆炸并对爆炸区域内的所有敌人造成伤害。爆炸的半径和造成的损害取决于迫击炮发射的炮弹的类型,因此我们将MortarTower为其添加配置选项。

  [SerializeField, Range(0.5f, 3f)] float shellBlastRadius = 1f; [SerializeField, Range(1f, 100f)] float shellDamage = 10f; 


爆炸半径和15枚弹药的1.5伤害。

此配置仅在爆炸期间才重要,因此必须将其添加到Shell及其方法中Initialize

  float age, blastRadius, damage; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity, float blastRadius, float damage ) { … this.blastRadius = blastRadius; this.damage = damage; } 

MortarTower 只能在创建后将数据传输到射弹。

  Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y), shellBlastRadius, shellDamage ); 

要向射程内的敌人射击,子弹必须捕获目标。我们已经有代码,但是在中Tower由于它对需要目标的一切有用,因此请将其功能复制到其中TargetPoint并使其静态可用。添加一个方法来填充缓冲区,一个属性来获取缓冲量,以及一个方法来获取缓冲目标。

  const int enemyLayerMask = 1 << 9; static Collider[] buffer = new Collider[100]; public static int BufferedCount { get; private set; } public static bool FillBuffer (Vector3 position, float range) { Vector3 top = position; top.y += 3f; BufferedCount = Physics.OverlapCapsuleNonAlloc( position, top, range, buffer, enemyLayerMask ); return BufferedCount > 0; } public static TargetPoint GetBuffered (int index) { var target = buffer[index].GetComponent<TargetPoint>(); Debug.Assert(target != null, "Targeted non-enemy!", buffer[0]); return target; } 

现在,我们可以接收最大缓冲区大小范围内的所有目标,并在爆炸时造成伤害Shell

  if (py <= 0f) { TargetPoint.FillBuffer(targetPoint, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy€.ApplyDamage(damage); } OriginFactory.Reclaim(this); return false; } 


爆弹。

您还可以添加到TargetPoint静态属性以从缓冲区中获取随机目标。

  public static TargetPoint RandomBuffered => GetBuffered(Random.Range(0, BufferedCount)); 

这将使我们简化Tower,因为现在您可以用来搜索随机目标TargetPoint

 protected bool AcquireTarget (out TargetPoint target) { if (TargetPoint.FillBuffer(transform.localPosition, targetingRange)) { target = TargetPoint.RandomBuffered; return true; } target = null; return false; } 

爆炸物


一切正常,但看起来仍然不太可信。您可以通过在爆炸壳时添加爆炸的可视化效果来改善图片。这不仅看起来更有趣,而且还可以为播放器提供有用的反馈。为此,我们将像激光束一样创建爆炸的预制件。只有它会是一个更透明的明亮色球。添加Explosion具有自定义持续时间的新实体组件半秒就足够了。向她添加一种Initialize设置爆炸位置和半径的方法设置比例时,由于球体网格的半径为0.5,因此需要将半径加倍。这里也是对范围内所有敌人造成伤害的好地方,因此我们还将添加一个伤害参数。此外,他需要一种方法GameUpdate来检查时间是否用完。

 using UnityEngine; public class Explosion : WarEntity { [SerializeField, Range(0f, 1f)] float duration = 0.5f; float age; public void Initialize (Vector3 position, float blastRadius, float damage) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } transform.localPosition = position; transform.localScale = Vector3.one * (2f * blastRadius); } public override bool GameUpdate () { age += Time.deltaTime; if (age >= duration) { OriginFactory.Reclaim(this); return false; } return true; } } 

向爆炸WarFactory

  [SerializeField] Explosion explosionPrefab = default; [SerializeField] Shell shellPrefab = default; public Explosion Explosion€ => Get(explosionPrefab); public Shell Shell => Get(shellPrefab); 


爆炸的战争工厂。

还添加到GameFacade方法。

  public static Explosion SpawnExplosion () { Explosion explosion = instance.warFactory.Explosion€; instance.nonEnemies.Add(explosion); return explosion; } 

现在,它Shell可以在达到目标时产生并引发爆炸。爆炸本身会造成损坏。

  if (py <= 0f) { Game.SpawnExplosion().Initialize(targetPoint, blastRadius, damage); OriginFactory.Reclaim(this); return false; } 


爆炸的贝壳。

爆炸更顺畅


不可变的球体而不是爆炸看起来并不漂亮。您可以通过设置不透明度和缩放比例来改善它们。您可以为此使用一个简单的公式,但让我们使用更易于设置的动画曲线。为此添加Explosion两个配置字段AnimationCurve我们将使用曲线来调整爆炸生命周期内的值,而时间1将指示爆炸的结束,无论其真实持续时间如何。爆炸的规模和半径也是如此。这将简化其配置。

  [SerializeField] AnimationCurve opacityCurve = default; [SerializeField] AnimationCurve scaleCurve = default; 

不透明度将以零开始和结束,平滑缩放至平均值0.3。比例将从0.7开始,然后迅速增加,然后慢慢接近1。


爆炸曲线。

要设置材料的颜色,我们将使用材料属性块。黑色是不透明度变量。比例尺现在设置为GameUpdate,但是我们需要使用半径字段进行跟踪。Initialize,你可以用加倍的规模。通过Evaluate使用自变量调用曲线来找到曲线的值,该自变量计算为爆炸的当前寿命除以爆炸的持续时间。

  static int colorPropertyID = Shader.PropertyToID("_Color"); static MaterialPropertyBlock propertyBlock; … float scale; MeshRenderer meshRenderer; void Awake () { meshRenderer = GetComponent<MeshRenderer>(); Debug.Assert(meshRenderer != null, "Explosion without renderer!"); } public void Initialize (Vector3 position, float blastRadius, float damage) { … transform.localPosition = position; scale = 2f * blastRadius; } public override bool GameUpdate () { … if (propertyBlock == null) { propertyBlock = new MaterialPropertyBlock(); } float t = age / duration; Color c = Color.clear; ca = opacityCurve.Evaluate(t); propertyBlock.SetColor(colorPropertyID, c); meshRenderer.SetPropertyBlock(propertyBlock); transform.localScale = Vector3.one * (scale * scaleCurve.Evaluate(t)); return true; } 


动画爆炸。

示踪剂壳


由于外壳很小,并且具有相当高的速度,因此很难注意到它们。而且,如果您查看单个帧的屏幕截图,则轨迹是完全无法理解的。您可以通过在外壳上添加跟踪效果使它们更加明显。对于常规外壳,这不是很现实,但是可以说它们是示踪剂。这种弹药是特制的,因此它们会留下鲜明的痕迹,使弹道清晰可见。

创建跟踪有多种方法,但是您将使用一种非常简单的方法。我们重新制作爆炸,以便Shell在每个帧创建一个小的爆炸。这些爆炸不会造成任何损害,因此捕获目标将浪费资源。加到Explosion如果损坏大于零,则将其损坏,然后将损伤参数设为Initialize可选,以支持此用途

  public void Initialize ( Vector3 position, float blastRadius, float damage = 0f ) { if (damage > 0f) { TargetPoint.FillBuffer(position, blastRadius); for (int i = 0; i < TargetPoint.BufferedCount; i++) { TargetPoint.GetBuffered(i).Enemy.ApplyDamage(damage); } } transform.localPosition = position; radius = 2f * blastRadius; } 

我们将在Shell.GameUpdate半径较小的一端(例如0.1)创建爆炸,以将其变成示踪剂壳。应当指出,使用这种方法,将逐帧创建爆炸,也就是说,爆炸取决于帧速率,但是对于这种简单的效果,这是允许的。

  public override bool GameUpdate () { … Game.SpawnExplosion().Initialize(p, 0.1f); return true; } 

图片

弹丸示踪剂。

教程资料库
PDF文章

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


All Articles