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.OverlapCapsule
is that for each call it allocates a new array. This can be avoided by selecting the array once and calling the alternative method OverlapCapsuleNonAlloc
with 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.OverlapCapsuleNonAlloc
returns 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; }
Tower
needs to have a reference to the Transform
turret component . Add a configuration field for this and connect it to the tower prefab. [SerializeField] Transform turret = default;
GameUpdate
there 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.LookAt
with 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); }
Tower
also 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;
GameUpdate
setting its scale to 0. public override void GameUpdate () { if (TrackTarget() || AcquireTarget()) { Shoot(); } else { laserBeam.localScale = Vector3.zero; } }
Enemy
property 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; }
ApplyDamage
that 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; } … }
Tower
the configuration field. Since the laser beam does continuous damage, we express it as damage per second. We Shoot
apply it to the Enemy
target 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