[教程
的第一 ,
第二和
第三部分]
- 支持不同类型的塔。
- 创建砂浆塔。
- 抛物线轨迹的计算。
- 发射爆炸弹。
这是有关创建简单
塔防游戏的教程的第四部分。 在其中,我们将添加迫击炮塔,在碰撞中发射引爆弹。
该教程是在Unity 2018.4.4f1中创建的。
敌人被炸。塔的类型
激光并不是唯一可以放置在炮塔上的武器。 在本教程中,我们将添加第二种类型的塔,该塔将在接触时发射爆炸的炮弹,对附近的所有敌人造成伤害。 为此,我们需要各种类型的塔的支持。
抽象塔
目标检测和跟踪是任何塔楼均可使用的功能,因此我们将其放在塔楼的抽象基类中。 为此,我们只使用
Tower
类,但首先,复制其内容以供以后在特定的
LaserTower
类中使用。 然后我们从
Tower
删除所有与激光相关的代码。 塔楼可能无法跟踪特定目标,因此请删除
target
字段并更改
AcquireTarget
和
TrackTarget
以便将输出参数用作链接参数。 然后,我们将从
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 () { … } }
然后更新激光塔的预制件以使用新组件。
激光塔的组件。创建特定类型的塔
为了能够选择要放置在现场的塔,我们将添加一个类似于
GameTileContentType
的
TowerType
枚举。 我们将为现有的激光塔和迫击炮塔提供支持,我们将在稍后创建。
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
参数
TowerType
其
TowerType
给工厂。
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=vyt−(gt2)/2 。
偏移量计算如何执行?速度 v 由每秒的距离确定,因此,将速度乘以持续时间 t 我们得到了距离 d=vt 。 当涉及加速时 一 ,速度是可变的。 加速度是每秒速度的变化,即每秒的距离的平方。 在任何时候,速度都是 v=在 。 在我们的情况下,持续不断的加速 a=−g ,因此我们可以将其除以一半以获得平均速度,然后乘以时间以找到偏移量 d=(at2)/2 由重力引起。
我们以相同的速度射击炮弹
s 这不取决于拍摄角度
theta (θ)。 那是
vx=s cos theta 和
vy=s sin theta 。
射击速度的计算。执行替换,我们得到
dx=st cos theta 和
dy=st sin theta−(gt2)/2 。
发射子弹,使其飞行时间
t 是实现目标所需的确切价值。 由于使用水平位移更容易,我们可以将时间表示为
t=dx/vx 。 在终点
dx=x 那就是
t=x/(s cos theta) 。 这意味着
y=x tan theta−(gx2)/(2s2 cos2 theta) 。
如何得到方程y?y=dy=s(x/(s cos theta)) sin theta−(g(x/(s cos theta))2)/2=x sin theta/ cos theta−(gx2)/(2s2 cos2 theta) 和 tan theta= sin theta/ cos theta 。
使用这个方程我们发现
tan theta=(s2+− sqrt(s4−g(gx2+2ys2)))/(gx) 。
如何得到方程tanθ?首先,我们将使用三角恒等式 \秒 theta=1/ cos theta 和 1+ tan2 theta= sec2 theta 来 y=x tan theta−(gx2)/(2s2)(1+ tan2 theta)=−(gx2)/(2s2) tan2 theta+x tan theta−(gx2)/(2s2) 。
这是形式的表达 au2+bu+c=0 在哪里 u=\棕褐色 theta , a=−(gx2)/(2s2) , b=x 和 c=一年 。
我们可以使用二次方程的根公式来解决 u=(−b+− sqrt(b2−4ac))/(2a) 。
替换后,方程将变得混乱,但是您可以通过乘以来简化方程 m=s2/x 所以得到 tan theta=(−mb+−m sqrtr)/(2ma) 在哪里 r=b2−4ac 。
在这种情况下,我们获得 tan theta=(s2+− sqrt(m2r))/(gx) 。
结果 m2r=(s4/x2)r=s4+2gs2c=s4−g2x2−2gys2=s4−g(gx2+2ys2) 。
有两种可能的角度,因为您可以瞄准高或低。 低路径的速度更快,因为它更接近目标线。 但是高轨迹看起来更有趣,因此我们将选择它。 这意味着我们只需要使用最大的解决方案。
tan theta=(s2+ sqrt(s4−g(gx2+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= sqrt(g(y+ sqrt(x2+y2))) 。
如何得出s的等式?需要决定 s4−g(gx2+2ys2)=s4−2gys2−g2x2=0 为 s 。
这是形式的表达 au2+bu+c=0 在哪里 u=s2 , a=1 , b=−2gy 和 c=−g2x2 。
您可以使用二次方程根的简化公式来求解 u=(−b+− sqrt(b2−4c))/2 。
替换后我们得到 s2=(2gy+− sqrt(4g2y2+4g2x2))/2=gy+−g sqrt(x2+y2) 。
我们需要一个积极的解决方案,所以我们来 s2=g(y+ sqrt(x2+y2)) 。
仅在迫击炮唤醒(唤醒)或在“播放”模式下更改其范围时,才需要确定所需的速度。 因此,我们将使用该字段跟踪它并在
Awake
和
OnValidate
对其进行计算。
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);
爆炸的战争工厂。还添加到Game
Facade方法。 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文章