[教程
的第一部分和
第二部分]
- 我们放在塔场上。
- 我们在物理的帮助下瞄准敌人。
- 我们会尽可能地跟踪它们。
- 我们用激光束射击它们。
这是有关创建简单塔防类型的一系列教程的第三部分。 它描述了塔的创建,瞄准和射击敌人。
该教程是在Unity 2018.3.0f2中创建的。
让我们加热敌人。塔楼创作
城墙只会使敌人减速,增加他们走的路途。 但是游戏的目标是在敌人到达终点之前消灭他们。 通过将塔架放置在可向其射击的场上,可以解决此问题。
平铺内容
塔是平铺内容的另一种类型,因此
GameTileContent
在
GameTileContent
中
GameTileContent
添加一个条目。
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
现在,这条路已被墙壁和高楼阻挡。更换墙壁
玩家很可能会经常用塔代替墙壁。 首先要拆除墙对他来说很不方便,此外,敌人还可以穿透这个暂时出现的缝隙。 您可以通过强制
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.GetTile
向
Physics.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);
为了完成这项工作,现在我们只需更新
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()) {
只是瞄准。我们发射激光
为了定位激光束,班级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; }
随机瞄准。可以使用其他选择目标的标准吗?, , . , , . . .
因此,在我们的塔防游戏中,塔终于出现了。在下一部分中,游戏将进一步发挥其最终形状。