
Tower class, but first, duplicate its contents for later use in a specific LaserTower class. Then we remove all the laser-related code from Tower . The tower may not keep track of a specific target, so delete the target field and change the AcquireTarget and TrackTarget so that the output parameter is used as the link parameter. Then we will remove the OnDrawGizmosSelected visualization from OnDrawGizmosSelected , but we will leave the aiming range, because it is used for all towers. 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 that extends the Tower and uses the functionality of its base class, getting rid of the duplicate code. 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 () { … } } 
TowerType enumeration similar to GameTileContentType . We will create support for the existing laser tower and mortar tower, which we will create later. public enum TowerType { Laser, Mortar } Tower to indicate its type. This works similarly to the type of behavior of a figure in the Object Management series of tutorials. public abstract TowerType TowerType€ { get; } LaserTower so that it returns the correct type. public override TowerType TowerType€ => TowerType.Laser; GameTileContentFactory so that the factory can produce the tower of the desired type. We implement this with an array of towers and add an alternative public Get method with the TowerType parameter. To verify that the array is configured correctly, we will use assertions. Another public Get method will now only apply to the contents of tiles without towers. [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 method should be Tower . But the private Get method used to instantiate the prefab returns a GameTileContent . Here you can either perform the conversion, or make the private Get method generic. Let's choose the second option. public Tower Get (TowerType type) { … } T Get<T> (T prefab) where T : GameTileContent { T instance = CreateGameObjectInstance(prefab); instance.OriginFactory = this; return instance; } 
GameBoard.ToggleTower so that it requires the TowerType parameter and TowerType it to the factory. 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 should now track the type of switchable tower. We simply denote each type of tower by a number. The laser tower is 1, it will be the default tower, and the mortar tower is 2. By pressing the number keys, we will select the appropriate type of towers. 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 type. Mortars have a frequency of fire, to indicate which you can use the “shots per second” configuration field. In addition, we will need a link to the mortar so that it can aim. 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 to mortar , move it down so that it stands on top of the base, give it a light gray color and attach it. We can leave the mortar collider, in this case, using a separate object, which is a simple collider superimposed on the default mortar orientation. I assigned a mortar range of 3.5 and a frequency of 1 shot per second.




MortarTower method to GameUpdate , which always calls the Launch method. Instead of firing a real projectile, we will visualize mathematical calculations for now. The shot point is the position of the mortar in the world, which is located just above the ground. We place the target point three units from it along the X axis, and zero out the Y component, because we always aim at the ground. Then we will show the points by Debug.DrawLine yellow line between them by calling Debug.DrawLine . The line will be visible in scene mode for one frame, but this is enough, because in each frame we draw a new line. 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); } 
Vector3 launchPoint = mortar.position; Vector3 targetPoint = new Vector3(launchPoint.x + 3f, 0f, launchPoint.z); float x = 3f; float y = -launchPoint.y; 
Launch and will call it with four displacements in XZ: , , and . When the aiming point becomes equal to the point of the shot plus this offset, and then its Y coordinate becomes equal to zero. 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; … } 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 ); 

Mathf.Atan . First, let's use a constant shot speed of 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; } 
float s = 4f; 
float r = s2 * s2 - g * (g * x * x + 2f * y * s2); Debug.Assert(r >= 0f, "Launch velocity insufficient for range!"); Awake and 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))); } float x = targetingRange + 0.25001f; Launch . float s = launchSpeed; 
Launch point to the target. public void Launch (TargetPoint target) { Vector3 launchPoint = mortar.position; Vector3 targetPoint = target.Position; targetPoint.y = 0f; … } GameUpdate . But at this point, there may not be any goals available. In this case, we continue the process of firing, but without further accumulation. To avoid an infinite loop, you need to make it a little less than 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 . We also need with apply shot angle for component Y of the direction vector. This will work because the horizontal direction has a length of 1, i.e. .
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 that allows them to be drawn for a long time. 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 ); 
WarEntitywith a property OriginFactoryand method 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); } } Shellfor the shells. using UnityEngine; public class Shell : WarEntity { } WarFactoryone that will create the shell using the public getter property. 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); } } Shell. Then create the factory asset and assign it the prefab of the projectile.
Gameto update the status of enemies. In fact, we can even make this approach generalized by creating an abstract component GameBehaviorthat extends MonoBehaviourand adds a virtual method GameUpdate. using UnityEngine; public abstract class GameBehavior : MonoBehaviour { public virtual bool GameUpdate () => true; } EnemyCollection, turning it into 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; } } } } WarEntityexpand GameBehavior, not MonoBehavior. public abstract class WarEntity : GameBehavior { … } Enemy, this time overriding the method GameUpdate. public class Enemy : GameBehavior { … public override bool GameUpdate () { … } … } Gameit will have to track two collections, one for enemies, the other for non-enemies. Non-enemies must be updated after everything else. GameBehaviorCollection enemies = new GameBehaviorCollection(); GameBehaviorCollection nonEnemies = new GameBehaviorCollection(); … void Update () { … enemies.GameUpdate(); Physics.SyncTransforms(); board.GameUpdate(); nonEnemies.GameUpdate(); } Gamethat will be a static facade for a war factory so that projectiles can be created by a challenge Game.SpawnShell(). For this to work, you Gamemust have a link to war factory and keep track of your own instance. [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; } 
Shellmethod Initializeand use it to set the shot point, target point and shot speed. Vector3 launchPoint, targetPoint, launchVelocity; public void Initialize ( Vector3 launchPoint, Vector3 targetPoint, Vector3 launchVelocity ) { this.launchPoint = launchPoint; this.targetPoint = targetPoint; this.launchVelocity = launchVelocity; } MortarTower.Launchand send it on the road. 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) ); Shellmove, we need to track the duration of its existence, that is, the time elapsed since the shot. Then we can calculate his position in GameUpdate. We always do this with respect to its firing point, so that the projectile perfectly follows the path regardless of the refresh rate. 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.Launchtrajectories from the visualization. public void Launch (TargetPoint target) { … Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y) ); } Shell.GameUpdatesee if the vertical position is below zero. You can do this immediately after calculating them, before changing the position and turning the projectile. 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; … } MortarTowerconfiguration options for them. [SerializeField, Range(0.5f, 3f)] float shellBlastRadius = 1f; [SerializeField, Range(1f, 100f)] float shellDamage = 10f; 
Shelland its method 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 should only transmit data to the projectile after its creation. Game.SpawnShell().Initialize( launchPoint, targetPoint, new Vector3(s * cosTheta * dir.x, s * sinTheta, s * cosTheta * dir.y), shellBlastRadius, shellDamage ); Tower. Since it is useful for everything that needs a goal, copy its functionality into TargetPointand make it statically available. Add a method to fill the buffer, a property to get the buffered amount, and a method to get the buffered target. 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; } 
TargetPointstatic property to get a random target from the buffer. public static TargetPoint RandomBuffered => GetBuffered(Random.Range(0, BufferedCount)); Tower, because now you can use to search for a random target TargetPoint. protected bool AcquireTarget (out TargetPoint target) { if (TargetPoint.FillBuffer(transform.localPosition, targetingRange)) { target = TargetPoint.RandomBuffered; return true; } target = null; return false; } Explosionwith a custom duration. Half a second will be enough. Add her a method Initializethat sets the position and radius of the explosion. When setting the scale, you need to double the radius, because the radius of the sphere mesh is 0.5. It is also a good place to deal damage to all enemies within range, so we’ll also add a damage parameter. In addition, he needs a method GameUpdateto check if time is running out. 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); 
Gamefacade method. public static Explosion SpawnExplosion () { Explosion explosion = instance.warFactory.Explosion€; instance.nonEnemies.Add(explosion); return explosion; } Shellcan generate and initiate an explosion upon reaching the target. The explosion itself will cause damage. if (py <= 0f) { Game.SpawnExplosion().Initialize(targetPoint, blastRadius, damage); OriginFactory.Reclaim(this); return false; } 
Explosiontwo configuration fields to this AnimationCurve. We will use the curves to adjust the values over the life of the explosion, and time 1 will indicate the end of the explosion, regardless of its true duration. The same applies to the scale and radius of the explosion. This will simplify their configuration. [SerializeField] AnimationCurve opacityCurve = default; [SerializeField] AnimationCurve scaleCurve = default; 
GameUpdate, but we need to track using the radius field. You Initializecan use the doubling scale. The values of the curves are found by calling them Evaluatewith an argument, calculated as the current lifetime of the explosion, divided by the duration of the explosion. 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; } 
Shellcreates a small explosion in each frame. These explosions will not cause any damage, so capturing targets will be a waste of resources. Add toExplosionsupport for this use by making the damage be done if it is greater than zero, and then make the damage parameter Initializeoptional. 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.GameUpdatewith a small radius, for example, 0.1, in order to turn them into tracer shells. It should be noted that with this approach, explosions will be created frame by frame, that is, they depend on the frame rate, but for such a simple effect this is permissible. public override bool GameUpdate () { … Game.SpawnExplosion().Initialize(p, 0.1f); return true; } 
Source: https://habr.com/ru/post/461605/
All Articles