Creating Tower Defense in Unity: Towers and Shooting Enemies

Original author: Jasper Flick
  • Transfer
  • Tutorial
[ The first and second parts of the tutorial]

  • We place on the tower field.
  • We aim at enemies with the help of physics.
  • We track them while it is possible.
  • We shoot them with a laser beam.

This is the third part of a series of tutorials on creating a simple tower defense genre. It describes the creation of towers, aiming and shooting at enemies.

The tutorial was created in Unity 2018.3.0f2.


Let’s heat the enemies.

Tower creation


Walls only slow down enemies, increasing the length of the path they need to go. But the goal of the game is to destroy the enemies before they reach the end point. This problem is solved by placing towers on the field that will shoot at them.

Tile Content


Towers are another type of tile content, so add an entry to them for GameTileContent.

publicenum GameTileContentType {
	Empty, Destination, Wall, SpawnPoint, Tower€
}

In this tutorial, we will support only one type of tower, which can be implemented by providing GameTileContentFactoryone link to the tower prefab, an instance of which can also be created through Get.

	[SerializeField]
	GameTileContent towerPrefab = default;
	public GameTileContent Get (GameTileContentType type) {
		switch (type) {
			…
			case GameTileContentType.Tower€: return Get(towerPrefab);
		}
		…
	}

But the towers must shoot, so their condition will need to be updated and they need their own code. For this purpose we create a class Towerthat extends the class GameTileContent.

using UnityEngine;
publicclassTower : GameTileContent {}

You can make the tower prefab have its own component by changing the factory field type to Tower. Since the class is still considered GameTileContent, nothing else needs to be changed.

	Tower towerPrefab = default;

Prefab


Create a prefab for the tower. You can start by duplicating the wall prefab and replacing its component GameTileContentwith a component Tower, and then change its type to Tower . To make the tower fit the walls, save the wall cube 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 a turret, this part will aim and shoot at enemies.



Three cubes forming a tower.

The turret will rotate, and since it has a collider, a physical engine will track it. But we do not need to be so precise, because we use tower colliders only to select cells. This can be done approximately. Remove the turret cube collider and change the tower cube collider so that it covers both cubes.



Collider cube tower.

The tower will shoot a laser beam. It can be visualized in many ways, but we just use a translucent cube, which we will stretch to form a beam. Each tower must have its own beam, so add it to the tower prefab. Place it inside the turret so that by default it is hidden, and give it a smaller scale, for example 0.2. Let's make it a child of the prefab root, not the turret cube.

laser beam

hierarchy

Hidden cube of a laser beam.

Create a suitable material for the laser beam. I just used the standard translucent black material and turned off all reflections, and also gave it a red emitted color.

color

no reflections

The material of the laser beam.

Check that the laser beam does not have a collider, and also turn off its cast and shadow.


The laser beam does not interact with shadows.

Having completed the creation of the tower prefab, we will add it to the factory.


Factory with a tower.

Tower placement


We will add and remove towers using another switching method. You can simply duplicate GameBoard.ToggleWallby changing the method name and content type.

publicvoidToggleTower (GameTile tile) {
		if (tile.Content.Type == GameTileContentType.Tower€) {
			tile.Content = contentFactory.Get(GameTileContentType.Empty);
			FindPaths();
		}
		elseif (tile.Content.Type == GameTileContentType.Empty) {
			tile.Content = contentFactory.Get(GameTileContentType.Tower€);
			if (!FindPaths()) {
				tile.Content = contentFactory.Get(GameTileContentType.Empty);
				FindPaths();
			}
		}
	}

When Game.HandleTouchyou hold down the shift key, the towers will switch, not the walls.

voidHandleTouch () {
		GameTile tile = board.GetTile(TouchRay);
		if (tile != null) {
			if (Input.GetKey(KeyCode.LeftShift)) {
				board.ToggleTower(tile);
			}
			else {
				board.ToggleWall(tile);
			}
		}
	}


Towers on the field.

Path blocking


So far, only walls can block the search for a path, so enemies move through towers. Let's add a GameTileContenthelper property to indicate whether the content is blocking the path. The path is blocked if it is a wall or a tower.

publicbool BlocksPath =>
		Type == GameTileContentType.Wall || Type == GameTileContentType.Tower€;

We use this property in GameTile.GrowPathToinstead of checking the type of content.

GameTile GrowPathTo (GameTile neighbor, Direction direction) {
		…
		return//neighbor.Content.Type != GameTileContentType.Wall ? neighbor : null;
			neighbor.Content.BlocksPath ? null : neighbor;
	}


Now the path is blocked by walls and towers.

Replace the walls


Most likely, the player will often replace the walls with towers. It will be inconvenient for him to remove the wall first, and besides, enemies can penetrate into this temporary gap. You can implement a direct replacement by forcing GameBoard.ToggleTowerto check whether the wall is currently on the tile. If so, then immediately replace it with a tower. In this case, we do not have to look for other ways, because the tile still blocks them.

publicvoidToggleTower (GameTile tile) {
		if (tile.Content.Type == GameTileContentType.Tower) {
			tile.Content = contentFactory.Get(GameTileContentType.Empty);
			FindPaths();
		}
		elseif (tile.Content.Type == GameTileContentType.Empty) {
			…
		}
		elseif (tile.Content.Type == GameTileContentType.Wall) {
			tile.Content = contentFactory.Get(GameTileContentType.Tower);
		}
	}

We aim at enemies


A tower can fulfill its task only when it finds an enemy. After finding the enemy, she must decide which part of it to aim.

Aiming point


To detect targets, we will use the physics engine. As in the case of the tower collider, we do not need the enemy collider to necessarily coincide with its shape. You can choose the simplest collider, that is, a sphere. After detecting the enemy, we will use the position of the game object with the collider attached to it as a point for aiming.

We cannot attach the collider to the enemy’s root object, because it doesn’t always coincide with the model’s position and will make the tower aim at the ground. That is, you need to place the collider somewhere on the model. The physics engine will give us a link to this object, which we can use for aiming, but we still need access to the component of the Enemyroot object. To simplify the task, let's create a componentTargetPoint. Let's give it a property for private assignment and public receipt of the component Enemy, and another property for obtaining its position in the world.

using UnityEngine;
publicclassTargetPoint : MonoBehaviour {
	public Enemy Enemy€ { get; privateset; }
	public Vector3 Position => transform.position;
}

Let's give it a method Awakethat sets a reference to its component Enemy. Go directly to the root object with transform.root. If the component Enemydoes not exist, then we made a mistake when creating the enemy, so let's add a statement for this.

voidAwake () {
		Enemy€ = transform.root.GetComponent<Enemy>();
		Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this);
	}

In addition, the collider must be attached to the same game object to which it is attached TargetPoint.

		Debug.Assert(Enemy€ != null, "Target point without Enemy root!", this);
		Debug.Assert(
			GetComponent<SphereCollider>() != null,
			"Target point without sphere collider!", this
		);

Add a component and a collider to the enemy’s prefab cube. This will make the towers aim at the center of the cube. We use a spherical collider with a radius of 0.25. The cube has a scale of 0.5, so the true radius of the collider will be 0.125. Thanks to this, the enemy will have to visually cross the range circle of the tower, and only after some time the real goal becomes. The collider size is also affected by the random scale of the enemy, so its size in the game will also vary slightly.


inspector

An enemy with an aiming point and a collider on a cube.

Enemy Layer


Towers care only for enemies, and they do not aim at anything else, so we will put all the enemies in a separate layer. We use the layer 9. Change the name on the Enemy in the window Layers & Tags , which can be opened via the option Edit Layers drop-down menu Layers in the top right corner of the editor.


Layer 9 will be used for enemies.

This layer is needed only for recognition of enemies, and not for physical interactions. Let's point it out by disabling them in the Layer Collision Matrix , which is located in the Physics panel of the project parameters.


Matrix of layer collisions.

Make sure that the game object of the aiming point is on the desired layer. The rest of the enemy’s prefab may be on other layers, but it will be easier to coordinate everything and place the entire prefab in the Enemy layer . If you change the layer of the root object, you will be prompted to change the layer for all its child objects.


Enemy on the right layer.

Let's add a statement about what TargetPointreally is on the right layer.

voidAwake () {
		…
		Debug.Assert(gameObject.layer == 9, "Target point on wrong layer!", this);
	}

In addition, player actions must be ignored by enemy colliders. This can be achieved by adding a layer mask argument to Physics.Raycastin GameBoard.GetTile. This method has a form that takes the distance to the ray and the layer mask as additional arguments. Let's give it the maximum distance and layer mask by default, i.e. 1.

public GameTile GetTile (Ray ray) {
		if (Physics.Raycast(ray, out RaycastHit hit, float.MaxValue, 1)) {
			…
		}
		returnnull;
	}

Shouldn't the layer mask be 0?
Индекс слоя по умолчанию равен нулю, но мы передаём маску слоя. Маска меняет отдельные биты целого числа на 1, если слой нужно включить. В данном случае нужно задать только первый бит, то есть самый младший, а значит, 20, что равняется 1.

Updating Tile Content


Towers can perform their task only when their status is updated. The same applies to the contents of the whole tiles, although the rest of the contents do nothing so far. Therefore, we add to the GameTileContentvirtual method GameUpdate, which by default does nothing.

publicvirtualvoidGameUpdate () {}

We will force Towerit to be redefined, even if for now it simply displays in the console that it is looking for a target.

publicoverridevoidGameUpdate () {
		Debug.Log("Searching for target...");
	}

GameBoarddeals with tiles and their contents, so it will also keep track of what content needs to be updated. To do this, add a list to it and a public method GameUpdatethat updates everything in the list.

	List<GameTileContent> updatingContent = new List<GameTileContent>();
	…
	publicvoidGameUpdate () {
		for (int i = 0; i < updatingContent.Count; i++) {
			updatingContent[i].GameUpdate();
		}
	}

In our tutorial you only need to update the towers. Change it ToggleTowerso that it adds and removes content if necessary. If other content is required, we will need a more general approach, but for now this is enough.

publicvoidToggleTower (GameTile tile) {
		if (tile.Content.Type == GameTileContentType.Tower) {
			updatingContent.Remove(tile.Content);
			tile.Content = contentFactory.Get(GameTileContentType.Empty);
			FindPaths();
		}
		elseif (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();
			}
		}
		elseif (tile.Content.Type == GameTileContentType.Wall) {
			tile.Content = contentFactory.Get(GameTileContentType.Tower);
			updatingContent.Add(tile.Content);
		}
	}

For this to work, now it’s enough for us to simply update the field in 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 where the enemies were in the last frame.

voidUpdate () {
		…
		enemies.GameUpdate();
		board.GameUpdate();
	}

Aiming range


Towers have a limited aiming radius. Let's make it custom by adding a field to the class Tower. The distance is measured from the center of the tower tile, so at a range of 0.5 it will only cover its own tile. Therefore, a reasonable minimum and standard range would be 1.5, covering most neighboring tiles.

	[SerializeField, Range(1.5f, 10.5f)]
	float targetingRange = 1.5f;


Aiming range 2.5.

Let's visualize the range with gizmo. We don’t need to see it constantly, therefore we will create a method OnDrawGizmosSelectedcalled only for the selected objects. We draw the yellow frame of the sphere with a radius equal to the distance and centered relative to the tower. Place it slightly above the ground so that it is always clearly visible.

voidOnDrawGizmosSelected () {
		Gizmos.color = Color.yellow;
		Vector3 position = transform.localPosition;
		position.y += 0.01f;
		Gizmos.DrawWireSphere(position, targetingRange);
	}


Gizmo aiming range.

Now we can see which of the enemies is an affordable target for each of the towers. But choosing towers in the scene window is inconvenient, because we have to select one of the child cubes, and then switch to the root object of the tower. Other types of tile content also suffer from the same problem. We can force the root of the tile content in the scene window by adding it to the GameTileContentattribute SelectionBase.

[SelectionBase]
publicclassGameTileContent : MonoBehaviour { … }

Target capture


Add a Towerfield TargetPointto the class so that it can track its captured target. Then we change GameUpdateit so that it calls a new method AquireTargetthat returns information about whether it found a target. Upon detection, it will display a message in the console.

	TargetPoint target;
	publicoverridevoidGameUpdate () {
		if (AcquireTarget()) {
			Debug.Log("Acquired target!");
		}
	}

We AcquireTargetget all available targets by calling Physics.OverlapSpherewith the position of the tower and range as arguments. The result will be an array Collidercontaining all the colliders in contact with the sphere. If the length of the array is positive, then there is at least one aiming point, and we simply select the first. Take its component TargetPoint, which should always exist, assign it to the target field and report success. Otherwise, we clear the target and report the failure.

boolAcquireTarget () {
		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]);
			returntrue;
		}
		target = null;
		returnfalse;
	}

We are guaranteed to get the correct aiming points, if we take into account colliders only on the layer of enemies. This is layer 9, so we’ll pass the corresponding layer mask.

constint enemyLayerMask = 1 << 9;
	…
	boolAcquireTarget () {
		Collider[] targets = Physics.OverlapSphere(
			transform.localPosition, targetingRange, enemyLayerMask
		);
		…
	}

How does this bitmask work?
Так как слой врагов имеет индекс 9, десятый бит битовой маски должен иметь значение 1. Этому соответствует целое число 29, то есть 512. Но такая запись битовой маски неинтуитивна. Мы можем также записать двоичный литерал, например 0b10_0000_0000, но тогда нам придётся считать нули. В данном случае наиболее удобной записью будет использование оператора сдвига влево <<, сдвигающего биты влево. что соответствует числу в степени двойки.

You can visualize the captured target by drawing a gizmo line between the positions of the tower and the target.

voidOnDrawGizmosSelected () {
		…
		if (target != null) {
			Gizmos.DrawLine(position, target.Position);
		}
	}


Visualization of goals.

Why not use methods like OnTriggerEnter?
Преимущество ручной проверки пересекающих сферу целей заключается в том, что мы можем делать это только при необходимости. Нет причин проверять наличие целей, если у башни она уже есть. Кроме того, благодаря получению всех потенциальных целей за раз нам не придётся обрабатывать список потенциальных целей для каждой башни, который постоянно меняется.

Target Lock


The target chosen to capture depends on the order in which they are represented by the physical engine, that is, in fact, it is arbitrary. Therefore, it will appear that the captured target is changing for no reason. After the tower receives the target, it is more logical for her to track her one, and not switch to another. Add a method TrackTargetthat implements such tracking and returns information about whether it was successful. First, we’ll just let you know if the target is captured.

boolTrackTarget () {
		if (target == null) {
			returnfalse;
		}
		returntrue;
	}

We will call this method in GameUpdateand only when returning false we will call AcquireTarget. If the method returned true, then we have a goal. This can be done by placing both method calls in a check ifwith the OR operator, because if the first operand returns true, the second will not be checked, and the call will be missed. The AND operator acts in a similar way.

publicoverridevoidGameUpdate () {
		if (TrackTarget() || AcquireTarget()) {
			Debug.Log("Locked on target!");
		}
	}


Tracking goals.

As a result, the towers are fixed on the target until it reaches the end point and is destroyed. If you use enemies repeatedly, then instead you need to check the correctness of the link, as is done with links to figures processed in a series of Object Management tutorials .

To track targets only when they are within range, TrackTargetmust track the distance between the tower and the target. If it exceeds the range value, then the target must be reset and return false. You can use the method for this check Vector3.Distance.

boolTrackTarget () {
		if (target == null) {
			returnfalse;
		}
		Vector3 a = transform.localPosition;
		Vector3 b = target.Position;
		if (Vector3.Distance(a, b) > targetingRange) {
			target = null;
			returnfalse;
		}
		returntrue;
	}

However, this code does not take into account the radius of the collider. Therefore, as a result, the tower may lose the target, then capture it again, only to stop tracking it in the next frame, and so on. We can avoid this by adding a collider radius to the range.

if (Vector3.Distance(a, b) > targetingRange + 0.125f) { … }

This gives us the correct results, but only if the scale of the enemy is not changed. Since we give each enemy a random scale, we must take it into account when changing the range. To do this, we must memorize the scale given Enemyand open it using the getter property.

publicfloat Scale { get; privateset; }
	…
	publicvoidInitialize (float scale, float speed, float pathOffset) {
		Scale = scale;
		…
	}

Now we can check in the Tower.TrackTargetcorrect range.

if (Vector3.Distance(a, b) > targetingRange + 0.125f * target.Enemy€.Scale) { … }

We synchronize physics


Everything seems to be working well, but towers that can aim at the center of the field are capable of capturing targets that should be out of range. They will not be able to track these goals, so they are fixed on them only for one frame.


Incorrect aiming.

This happens because the state of the physical engine is imperfectly synchronized with the state of the game. Instances of all enemies are created at the origin of the world, which coincides with the center of the field. Then we move them to the point of creation, but the physics engine does not know about it right away.

You can enable instant synchronization, which is performed when changing object transformations by assigning a Physics.autoSyncTransformsvalue 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 updating the state of the towers. We can execute it, causing Physics.SyncTransformsbetween updates of enemies and fields in Game.Update.

voidUpdate () {
		…
		enemies.GameUpdate();
		Physics.SyncTransforms();
		board.GameUpdate();
	}

Ignore the height


In fact, our gameplay takes place in 2D. Therefore, let's change it Towerso that when aiming and tracking it takes into account only the X and Z coordinates. The physical engine works in 3D space, but in essence we can perform a AcquireTarget2D check : stretch the sphere up so that it covers all the colliders, regardless of their vertical position. This can be done by using a capsule instead of a sphere, the second point of which will be several units above the ground (for example, three).

boolAcquireTarget () {
		Vector3 a = transform.localPosition;
		Vector3 b = a;
		b.y += 3f;
		Collider[] targets = Physics.OverlapCapsule(
			a, b, targetingRange, enemyLayerMask
		);
		…
	}

Isn't it possible to use a physical 2D engine?
Проблема в том, что наша игра проходит в плоскости XZ, а физический 2D-движок работает в плоскости XY. Мы можем заставить его работать, или изменив ориентацию всей игры, или создав отдельное 2D-представление только для физики. Но легче просто использовать 3D-физику.

It is also necessary to change 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 get rid of the operation of calculating the square root.

boolTrackTarget () {
		if (target == null) {
			returnfalse;
		}
		Vector3 a = transform.localPosition;
		Vector3 b = target.Position;
		float x = a.x - b.x;
		float z = a.z - b.z;
		float r = targetingRange + 0.125f * target.Enemy€.Scale;
		if (x * x + z * z > r * r) {
			target = null;
			returnfalse;
		}
		returntrue;
	}

How do these math calculations work?
В них для вычисления 2D-расстояния используется теорема Пифагора, но без расчёта квадратного корня. Вместо этого вычисляется квадрат радиуса, поэтому в результате мы сравниваем квадраты длин. Этого достаточно, потому что нам нужно проверять только относительную длину, а не точную разность.

Avoid memory allocation


The disadvantage of using it Physics.OverlapCapsuleis that for each call it allocates a new array. This can be avoided by allocating the array once and calling an alternative method OverlapCapsuleNonAllocwith the array as an additional argument. The length of the transmitted array determines the number of results. All potential targets outside the array are discarded. Anyway, we will use only the first element, so an array of length 1 is enough for us.

Instead of an array, it OverlapCapsuleNonAllocreturns the number of collisions that have occurred, up to the maximum allowed, and this is the number we will check instead of the length of the array.

static Collider[] targetsBuffer = new Collider[1];
	…
	boolAcquireTarget () {
		Vector3 a = transform.localPosition;
		Vector3 b = a;
		b.y += 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]);
			returntrue;
		}
		target = null;
		returnfalse;
	}

We shoot at enemies


Now that we have a real goal, it is time to shoot it. Shooting includes aiming, a laser shot and dealing damage.

Aim turret


To direct the turret to the target, the class Towerneeds to have a link to the Transformturret component . Add a configuration field for this and connect it to the tower prefab.

	[SerializeField]
	Transform turret = default;


The attached turret.

If GameUpdatethere is a real target, then we must shoot it. Put the shooting code in a separate method. Make him rotate the turret toward the target, calling his method Transform.LookAtwith the aiming point as an argument.

publicoverridevoidGameUpdate () {
		if (TrackTarget() || AcquireTarget()) {
			//Debug.Log("Locked on target!");
			Shoot();
		}
	}
	voidShoot () {
		Vector3 point = target.Position;
		turret.LookAt(point);
	}


Just aiming.

We shoot a laser


To position the laser beam, the class Toweralso needs a link to it.

	[SerializeField]
	Transform turret = default, laserBeam = default;


We connected a laser beam.

To turn a cube into a real laser beam, you need to take three steps. Firstly, its orientation should correspond to the orientation of the turret. This can be done by copying its rotation.

voidShoot () {
		Vector3 point = target.Position;
		turret.LookAt(point);
		laserBeam.localRotation = turret.localRotation;
	}

Secondly, we scale the laser beam so that its length is equal to the distance between the local point of origin of the turret's coordinates and the aiming point. We scale it along the Z axis, that is, the local axis directed towards the target. To preserve the original scale in XY, we write down the original scale when awakening (Awake) the turret.

	Vector3 laserBeamScale;
	voidAwake () {
		laserBeamScale = laserBeam.localScale;
	}
	…
	voidShoot () {
		Vector3 point = target.Position;
		turret.LookAt(point);
		laserBeam.localRotation = turret.localRotation;
		float d = Vector3.Distance(turret.position, point);
		laserBeamScale.z = d;
		laserBeam.localScale = laserBeamScale;
	}

Thirdly, we place the laser beam in the middle between the turret and the aiming point.

		laserBeam.localScale = laserBeamScale;
		laserBeam.localPosition =
			turret.localPosition + 0.5f * d * laserBeam.forward;


Laser shooting.

Is it not possible to make a laser beam a child of a turret?
Если бы мы сделали это, то нам не пришлось бы поворачивать лазерный луч отдельно, и не понадобился бы его вектор forward. Однако на него бы влиял масштаб турели, поэтому его пришлось бы компенсировать. Проще хранить их по отдельности.

This works while the turret is fixed on target. But when there is no target, the laser remains active. We can turn off the laser display by GameUpdatesetting its scale to 0.

publicoverridevoidGameUpdate () {
		if (TrackTarget() || AcquireTarget()) {
			Shoot();
		}
		else {
			laserBeam.localScale = Vector3.zero;
		}
	}


Idle towers do not fire.

Enemy Health


So far, our laser beams just touch the enemies and no longer affect them. It is necessary to make sure that the laser does damage to enemies. We do not want to destroy enemies instantly, so we will give the Enemyproperty of health. You can choose any value as health, so let's take 100. But it will be more logical for large enemies to have more health, so we’ll introduce a coefficient for this.

float Health { get; set; }
	…
	publicvoidInitialize (float scale, float speed, float pathOffset) {
		…
		Health = 100f * scale;
	}

To add support for dealing damage, add a public method ApplyDamagethat subtracts its parameter from health. We will assume that the damage is non-negative, so we add a statement about this.

publicvoidApplyDamage (float damage) {
		Debug.Assert(damage >= 0f, "Negative damage applied.");
		Health -= damage;
	}

We will not instantly get rid of the enemy as soon as his health reaches zero. Checking for exhaustion of health and destruction of the enemy will be performed at the beginning GameUpdate.

publicboolGameUpdate () {
		if (Health <= 0f) {
			OriginFactory.Reclaim(this);
			returnfalse;
		}
		…
	}

Thanks to this, all towers will essentially shoot simultaneously, and not in turn, which will allow them to switch to other targets if the previous tower destroyed the enemy, which they also aimed at.

Damage per second


Now we need to determine how much damage the laser will do. To do this, add to Towerthe configuration field. Since the laser beam deals continuous damage, we will express it as damage per second. We Shootapply it to the Enemytarget component with multiplication by the delta time.

	[SerializeField, Range(1f, 100f)]
	float damagePerSecond = 10f;
	…
	voidShoot () {
		…
		target.Enemy.ApplyDamage(damagePerSecond * Time.deltaTime);
	}

inspector


The damage of each tower is 20 units per second.

Random aiming


Since we always choose the first available target, the aiming behavior depends on the order in which the physics engine checks intersecting colliders. This dependence is not very good, because we do not know the details, we can’t control it, moreover, it will look strange and inconsistent. Often this behavior leads to concentrated fire, but this is not always the case.

Instead of relying entirely on the physics engine, let's add some randomness. This can be done by increasing the number of intersections received by colliders, for example, up to 100. Perhaps this will not be enough to get all possible targets in a field densely filled with enemies, but this will be enough to improve aiming.

static Collider[] targetsBuffer = new Collider[100];

Now, instead of choosing the first potential target, we will select a random element from the array.

boolAcquireTarget () {
		…
		if (hits > 0) {
			target =
				targetsBuffer[Random.Range(0, hits)].GetComponent<TargetPoint>();
			…
		}
		target = null;
		returnfalse;
	}


Random aiming.

Can other criteria for choosing goals be used?
Да, например, можно выбирать цель с наибольшим или наименьшим здоровьем. Или отслеживать, сколько башен целится в каждого врага, чтобы сконцентрировать или рассредоточить огонь. Или скомбинировать несколько критериев. Однако сложно найти хороший критерий прицеливания при случайном выборе цели для каждой башни.

So, in our tower defense game, towers have finally appeared. In the next part, the game will take its final shape even more.

Also popular now: