
GameTileContent . public enum GameTileContentType { Empty, Destination, Wall, SpawnPoint, Tower€ } GameTileContentFactory one link to the tower prefab, an instance of which can also be created via Get .  [SerializeField] GameTileContent towerPrefab = default; public GameTileContent Get (GameTileContentType type) { switch (type) { … case GameTileContentType.Tower€: return Get(towerPrefab); } … } Tower class, which extends the GameTileContent class. using UnityEngine; public class Tower : GameTileContent {} Tower . Since the class is still considered GameTileContent , nothing else needs to be changed.  Tower towerPrefab = default; GameTileContent component with a Tower component, and then changing its type to Tower . To make the tower fit the walls, save the cube of the wall as the base of the tower. Then place another cube on top of it. I gave him a scale of 0.5. Put another cube on it, indicating the turret, this part will aim and shoot at enemies.









GameBoard.ToggleWall by changing the method name and content type.  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 if you hold down the shift key, the walls will not switch, but the towers.  void HandleTouch () { GameTile tile = board.GetTile(TouchRay); if (tile != null) { if (Input.GetKey(KeyCode.LeftShift)) { board.ToggleTower(tile); } else { board.ToggleWall(tile); } } } 
GameTileContent indicate whether the content is blocking the path. The path is blocked if it is a wall or a tower.  public bool BlocksPath => Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€; GameTile.GrowPathTo instead of checking the content type.  GameTile GrowPathTo (GameTile neighbor, Direction direction) { … return //neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null; neighbor.Content.BlocksPath ? null : neighbor; } 
GameBoard.ToggleTower to check if the wall is currently on the tile. If yes, then you need to immediately replace it with a tower. In this case, we will not have to look for other ways, because the tile still blocks them.  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 component of the root object. To simplify the task, let's create a TargetPoint component. We give it a property for a private task and a public receiving component Enemy , and another property for getting its position in the world. using UnityEngine; public class TargetPoint : MonoBehaviour { public Enemy Enemy€ { get; private set; } public Vector3 Position => transform.position; } Awake method, which sets a link to its Enemy component. You can go directly to the root object using transform.root . If the Enemy component does not exist, then we made a mistake in creating the enemy, so let's add a statement for this.  void Awake () { Enemy€ = transform.root.GetComponent<Enemy>(); Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); } TargetPoint attached.  Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this); Debug.Assert( GetComponent<SphereCollider>() != null, "Target point without sphere collider!", this ); 




TargetPoint really is on the right layer.  void Awake () { … Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this); } Physics.Raycast in the GameBoard.GetTile . This method has a form that takes as an additional argument the distance to the beam and the layer mask. Give it the maximum distance and the default layer mask, that is, 1.  public GameTile GetTile (Ray ray) { if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) { … } return null; } GameTileContent add a virtual GameTileContent method to GameUpdate , which by default does nothing.  public virtual void GameUpdate () {} Tower redefine it, even if for now it just displays to the console what the target is looking for.  public override void GameUpdate () { Debug.Log("Searching for target..."); } GameBoard deals with tiles and their contents, so it will also keep track of which content needs to be updated. To do this, add a list to it and a public GameUpdate method that updates everything in the list.  List<GameTileContent> updatingContent = new List<GameTileContent>(); … public void GameUpdate () { for (int i = 0; i < updatingContent.Count; i++) { updatingContent[i].GameUpdate(); } } ToggleTower so that it adds and deletes content if necessary. If the update is required and other content, then we need a more general approach, but for now this is enough.  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 . We will update the field after the enemies. Thanks to this, the towers will be able to aim exactly where the enemies are. If we did otherwise, the towers would aim at where the enemies in the last frame were.  void Update () { … enemies.GameUpdate(); board.GameUpdate(); } Tower class. The distance is measured from the center of the tower tile, so at a distance of 0.5 it will cover only its own tile. Therefore, a reasonable minimum and standard range will be 1.5, covering most of the adjacent tiles.  [SerializeField, Range(1.5f, 10.5f)] float targetingRange = 1.5f; 
OnDrawGizmosSelected method, which is called only for selected objects. Draw the yellow frame of the sphere with a radius equal to the distance and centered relative to the tower. Place it a little above the ground so that it can always be clearly seen.  void OnDrawGizmosSelected () { Gizmos.color = Color.yellow; Vector3 position = transform.localPosition; position.y += 0.01f; Gizmos.DrawWireSphere(position, targetingRange); } 
GameTileContent content in the scene window by adding the SelectionBase attribute to the GameTileContent . [SelectionBase] public class GameTileContent : MonoBehaviour { … } TargetPoint field to the Tower class so that it can track its captured target. Then we change GameUpdate to call the new AquireTarget method, which returns information about whether it found the target. Upon detection, it will display a message in the console.  TargetPoint target; public override void GameUpdate () { if (AcquireTarget()) { Debug.Log("Acquired target!"); } } AcquireTarget we get all available targets by calling Physics.OverlapSphere with the position of the tower and the range as arguments. The result is a Collider array containing all the colliders that are in contact with the sphere. If the array length is positive, then there is at least one aiming point, and we simply choose the first one. Take its TargetPoint component, which should always exist, assign it to the target field and report success. Otherwise, clear the target and report the failure.  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; }   const int enemyLayerMask = 1 << 9; … bool AcquireTarget () { Collider[] targets = Physics.OverlapSphere( transform.localPosition, targetingRange, enemyLayerMask ); … } 0b10_0000_0000 , but then we have to count to zero. In this case, the most convenient entry would be to use the left shift << operator, shifting the bits to the left. which corresponds to a number in the power of two.  void OnDrawGizmosSelected () { … if (target != null) { Gizmos.DrawLine(position, target.Position); } } 
TrackTarget method that implements such tracking and returns information on whether it was successful. At first, let's just say if the target is captured.  bool TrackTarget () { if (target == null) { return false; } return true; } GameUpdate and only when returning false will we call AcquireTarget . If the method returns true, then we have a goal. This can be done by placing both method calls in an if check with the OR operator, because if the first operand returns true , then the second will not be checked and the call will be skipped. The AND operator acts in a similar way.  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Debug.Log("Locked on target!"); } } 
TrackTarget must track the distance between the tower and the target. If it exceeds the range value, the target needs to be reset and return false. For this check, you can use the Vector3.Distance method.  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 and open it with the getter property.  public float Scale { get; private set; } … public void Initialize (float scale, float speed, float pathOffset) { Scale = scale; … } Tower.TrackTarget correct range.  if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … } 
Physics.autoSyncTransforms value to true . But by default it is disabled, because it is much more efficient to synchronize everything together and, if necessary. In our case, synchronization is required only when the state of the towers is updated. We can perform it by calling Physics.SyncTransforms between updates of enemies and the field in Game.Update .  void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); } Tower so that when aiming and tracking it takes into account only the X and Z coordinates. The physics engine works in 3D space, but in fact we can perform AcquireTarget verification in 2D: stretch the sphere upward so that it covers all colliders, regardless from their vertical position. This can be done by using a capsule instead of a sphere, the second point of which will be in several units above the ground (say, in three).  bool AcquireTarget () { Vector3 a = transform.localPosition; Vector3 b = a; by += 3f; Collider[] targets = Physics.OverlapCapsule( a, b, targetingRange, enemyLayerMask ); … } TrackTarget. Of course, we can use 2D vectors and Vector2.Distance, but let's do the calculations ourselves and instead we will compare the squares of distances, this will be enough. So we will get rid of the square root operation.  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; } Physics.OverlapCapsuleis that for each call it allocates a new array. This can be avoided by selecting the array once and calling the alternative method OverlapCapsuleNonAllocwith the array as an additional argument. The length of the transmitted array determines the number of results obtained. All potential targets outside the array are discarded. We will still use only the first element, so we have enough of an array of length 1.OverlapCapsuleNonAllocreturns the number of collisions, up to the maximum permissible, and we will check this number instead of the array length.  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; } Towerneeds to have a reference to the Transformturret component . Add a configuration field for this and connect it to the tower prefab.  [SerializeField] Transform turret = default; 
GameUpdatethere is a real goal, then we must shoot it. Put the firing code in a separate method. Let's make him rotate the turret towards the target, calling his method Transform.LookAtwith an aiming point as an argument.  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { //Debug.Log("Locked on target!"); Shoot(); } } void Shoot () { Vector3 point = target.Position; turret.LookAt(point); } 
Toweralso needs a reference to it.  [SerializeField] Transform turret = default, laserBeam = default; 
  void Shoot () { Vector3 point = target.Position; turret.LookAt(point); laserBeam.localRotation = turret.localRotation; }   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; 
GameUpdatesetting its scale to 0.  public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } } 
Enemyproperty of health. As health, you can choose any value, so let's take 100. But it will be more logical for large enemies to have more health, so we will introduce a factor for this.  float Health { get; set; } … public void Initialize (float scale, float speed, float pathOffset) { … Health = 100f * scale; } ApplyDamagethat subtracts its parameter from health. We will assume that the damage is non-negative, so we add a statement about it.  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; } … } Towerthe configuration field. Since the laser beam does continuous damage, we express it as damage per second. We Shootapply it to the Enemytarget component multiplied by the time delta.  [SerializeField, Range(1f, 100f)] float damagePerSecond = 10f; … void Shoot () { … target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime); } 

  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/459070/
All Articles