📜 ⬆️ ⬇️

How to create a simple tower defense game on Unity3D, part two

Hello! For a very long time, I dragged out the preparation of the material (life gave me a lot of kicks in the ass), but I managed and was ready to share the continuation of the first article with you.

Part one


Failed physics test
')
In this part we:
- we optimize the code from the previous article;
- create an object “base” and teach it how to fix it from time to time;
- add ammo to guns and reload;
- get rid of the “inconvenient” gv variable;

And at the end of the article you will find a small bonus :)

All interested - welcome under the long-awaited cat!


Optimization, bug fixes, permutation on stage and all that



In this part of the tutorial, we optimize the shit code we wrote earlier, which will give us a margin of performance for the game.

Let's start with the AI ​​script of the gun, the changes in which touched the method of calculating the distance, a clip with cartridges appeared, and reloading that lasts the specified time:

PlasmaTurretAI.cs
using UnityEngine; public class PlasmaTurretAI : MonoBehaviour { public GameObject curTarget; public float towerPrice = 100.0f; public float attackMaximumDistance = 50.0f; //  public float attackMinimumDistance = 5.0f; public float attackDamage = 10.0f; // public float reloadTimer = 2.5f; //  ,   public float reloadCooldown = 2.5f; //  ,  public float rotationSpeed = 1.5f; //    public int FiringOrder = 1; //    (    2) public int upgradeLevel = 0; public int ammoAmount = 64; public int ammoAmountConst = 64; public float ammoReloadTimer = 5.0f; public float ammoReloadConst = 5.0f; public LayerMask turretLayerMask; //  Unity3D             .    Monster.       . public Transform turretHead; //     private void Start() { turretHead = transform.Find("pushka"); //      } //      private void Update() { if (curTarget != null) //      { float squaredDistance = (turretHead.position - curTarget.transform.position).sqrMagnitude; //    if (Mathf.Pow(attackMinimumDistance, 2) < squaredDistance && squaredDistance < Mathf.Pow(attackMaximumDistance, 2)) //          { turretHead.rotation = Quaternion.Lerp(turretHead.rotation, Quaternion.LookRotation(curTarget.transform.position - turretHead.position), rotationSpeed * Time.deltaTime); //     if (reloadTimer > 0) reloadTimer -= Time.deltaTime; //     -   if (reloadTimer <= 0) { if (ammoAmount > 0) //     { MobHP mhp = curTarget.GetComponent<MobHP>(); switch (FiringOrder) //,     { case 1: if (mhp != null) mhp.ChangeHP(-attackDamage); //   FiringOrder++; //  ammoAmount--; //  break; case 2: if (mhp != null) mhp.ChangeHP(-attackDamage); FiringOrder = 1; ammoAmount--; break; } reloadTimer = reloadCooldown; //        "" } else { if (ammoReloadTimer > 0) ammoReloadTimer -= Time.deltaTime; if (ammoReloadTimer <= 0) { ammoAmount = ammoAmountConst; ammoReloadTimer = ammoReloadConst; } } } if (squaredDistance < Mathf.Pow(attackMinimumDistance, 2)) curTarget = null;//    ,      } } else { curTarget = SortTargets(); //     } } //     private GameObject SortTargets() { float closestMobSquaredDistance = 0; //       GameObject nearestmob = null; //    Collider[] mobColliders = Physics.OverlapSphere(transform.position, attackMaximumDistance, turretLayerMask.value); //              foreach (var mobCollider in mobColliders) //     { float distance = (mobCollider.transform.position - turretHead.position).sqrMagnitude; //    ,  closestMobDistance    if (distance < closestMobSquaredDistance && (distance > Mathf.Pow(attackMinimumDistance, 2)) || closestMobSquaredDistance == 0) { closestMobSquaredDistance = distance; //    nearestmob = mobCollider.gameObject;//    } } return nearestmob; //    } private void OnGUI() { Vector3 screenPosition = Camera.main.WorldToScreenPoint(gameObject.transform.position); //       Vector3 cameraRelative = Camera.main.transform.InverseTransformPoint(transform.position); //     if (cameraRelative.z > 0) //     { string ammoString; if (ammoAmount > 0) { ammoString = ammoAmount + "/" + ammoAmountConst; } else { ammoString = "Reloading: " + (int)ammoReloadTimer + " s left"; } GUI.Label(new Rect(screenPosition.x, Screen.height - screenPosition.y, 250f, 20f), ammoString); } } } 


As you can see, here calculation is used through the square of the distance and comparing it with the square of the maximum distance for the gun. It works faster, because Sqrt is not used. Thanks Leopotam for the advice :)

The next step is to bring the scene to something like the following:



I marked the points of the spawnpoints with red dots, in the center I have the “base” in the form of a standard Maksov teapot :)



On the base, I hung the Base tag so that you can easily find it.
We need to make the mobs go straight to the base, ignoring the guns. To do this, you need to teach the base to understand damage and repair at regular intervals.
Well, let's start:

BaseHP.cs
 using UnityEngine; public class BaseHP : MonoBehaviour { public float maxHP = 1000; public float curHP = 1000; public float regenerationDelayConstant = 2.5f; //      public float regenarationDelayVariable = 2.5f; //    public float regenerationAmount = 10.0f; //       private GlobalVars gv; private void Awake() { gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); if (maxHP < 1) maxHP = 1; } public void ChangeHP(float adjust) { if ((curHP + adjust) > maxHP) curHP = maxHP; else curHP += adjust; if (curHP > maxHP) curHP = maxHP; //just in case } private void Update() { if (curHP <= 0) { Destroy(gameObject); } else { if (regenarationDelayVariable > 0) regenarationDelayVariable -= Time.deltaTime; //     -       if (regenarationDelayVariable <= 0) //       { ChangeHP(regenerationAmount); //     regenarationDelayVariable = regenerationDelayConstant; //        } } } } 


We hang the script on our object with the base. She is ready, you can start retraining mobs!

In the AI ​​script of mobs, only the Update method is subject to change, because I will not give the rest of the code:

MobAI.cs
 private void Update() { if (Target != null) { mob.rotation = Quaternion.Lerp(mob.rotation, Quaternion.LookRotation(new Vector3(Target.transform.position.x, 0.0f, Target.transform.position.z) - new Vector3(mob.position.x, 0.0f, mob.position.z)), mobRotationSpeed); mob.position += mob.forward * MobCurrentSpeed * Time.deltaTime; float squaredDistance = (Target.transform.position - mob.position).sqrMagnitude; //    Vector3 structDirection = (Target.transform.position - mob.position).normalized; float attackDirection = Vector3.Dot(structDirection, mob.forward); if (squaredDistance < attackDistance * attackDistance && attackDirection > 0) { if (attackTimer > 0) attackTimer -= Time.deltaTime; if (attackTimer <= 0) { BaseHP bhp = Target.GetComponent<BaseHP>(); //   HP  if (bhp != null) bhp.ChangeHP(-damage); //    HP   attackTimer = coolDown; } } } else { GameObject baseGO = GameObject.FindGameObjectWithTag("Base"); //    ,    if (baseGO != null) Target = baseGO; //   -     . } } 


All is well, mobs crawling to bite the base, guns methodically shoot impudent guys. But the camera is static! Disorder, fix:

CameraControl.cs
 using UnityEngine; public class CameraControl : MonoBehaviour { public float CameraSpeed = 100.0f; //   public float CameraSpeedBoostMultiplier = 2.0f; //      Shift //     ,    -    public float DefaultCameraPosX = 888.0f; public float DefaultCameraPosY = 50.0f; public float DefaultCameraPosZ = 1414.0f; private void Awake() { //     ,     transform.position = new Vector3(DefaultCameraPosX, DefaultCameraPosY, DefaultCameraPosZ); } private void Update() { float smoothCamSpeed = CameraSpeed * Time.smoothDeltaTime; //       Time.deltaTime //  -    WASD     ,       (WA      ),  Shift    . if (Input.GetKey(KeyCode.W)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(0.0f, 0.0f, smoothCamSpeed * CameraSpeedBoostMultiplier) : new Vector3(0.0f, 0.0f, smoothCamSpeed); // if (Input.GetKey(KeyCode.A)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(-smoothCamSpeed * CameraSpeedBoostMultiplier, 0.0f, 0.0f) : new Vector3(-smoothCamSpeed, 0.0f, 0.0f); // if (Input.GetKey(KeyCode.S)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(0.0f, 0.0f, -smoothCamSpeed * CameraSpeedBoostMultiplier) : new Vector3(0.0f, 0.0f, -smoothCamSpeed); // if (Input.GetKey(KeyCode.D)) transform.position += Input.GetKey(KeyCode.LeftShift) ? new Vector3(smoothCamSpeed * CameraSpeedBoostMultiplier, 0.0f, 0.0f) : new Vector3(smoothCamSpeed, 0.0f, 0.0f); // } } 


The script, of course, hangs on the camera. Now everything is moving, you can look around at the mobs approaching the base, put the guns on the way.

The next bugfix is ​​that we can buy guns "on credit". Yes, you need a simple check of the player’s money and the value of the gun. Rule this thing:
Graphic.cs
 private void OnGUI() { GUI.Box(buyMenu, "Buying menu"); //     buyMenu  ,   "" if (GUI.Button(firstTower, "Plasma Tower\n" + (int)TowerPrices.Plasma + "$")) //      { if (gv.PlayerMoney >= (int)TowerPrices.Plasma) //     gv.mau5tate = GlobalVars.ClickState.Placing; //     " " } if (GUI.Button(secondTower, "Pulse Tower\n" + (int)TowerPrices.Pulse + "$")) //   { //same action here } if (GUI.Button(thirdTower, "Beam Tower\n" + (int)TowerPrices.Beam + "$")) { //same action here } if (GUI.Button(fourthTower, "Tesla Tower\n" + (int)TowerPrices.Tesla + "$")) { //same action here } if (GUI.Button(fifthTower, "Artillery Tower\n" + (int)TowerPrices.Artillery + "$")) { //same action here } GUI.Box(playerStats, "Player Stats"); GUI.Label(playerStatsPlayerMoney, "Money: " + gv.PlayerMoney + "$"); GUI.Box(towerMenu, "Tower menu"); if (GUI.Button(towerMenuSellTower, "Sell")) { //action here } if (GUI.Button(towerMenuUpgradeTower, "Upgrade")) { //same action here } } //   private enum TowerPrices { Plasma = 100, Pulse = 150, Beam = 250, Tesla = 300, Artillery = 350 } 


Then, after writing all the previous code, I got rid of the GlobalVars object, making it and all its static variables.

GlobalVars.cs
 using System.Collections.Generic; using UnityEngine; public static class GlobalVars { public static List<GameObject> MobList = new List<GameObject>(); //    public static int MobCount = 0; //    public static List<GameObject> TurretList = new List<GameObject>(); //    public static int TurretCount = 0; //    public static float PlayerMoney = 200.0f; //  ,        -   200$,     public static ClickState mau5tate = ClickState.Default; //   public enum ClickState //    { Default, // Placing, //  Selling, //  Upgrading //  } } 


In all classes where GlobalVars was used, we delete the gv variables, their initialization in Awake (). Replace all gv with GlobalVars. Remove useless checks from GlobalVars to null. We delete the GlobalVars component from the GO of the same name (you can rename the GO itself to something informative, for example, cfg).
I will give complete class listings with changes so that you can compare the result of this operation.

Beware, spoilers to the next part! :)

bitbucket.org/andyion/habratd-tutorial/commits/db7c1bc0c10c89f45be187e59e0608a2fbb3083d

This completes the replacement.
The next moment I will add a small bonus, which will greatly facilitate life when adjusting the attack range for both guns and mobs: bitbucket.org/andyion/habratd-tutorial/commits/18ec053f5f5697abbd3598890aa40306e038d472

How to use: put the script on the object and adjust the range in the inspector. A yellow circle will appear around GO when selected, this is the specified distance.

Conclusion

In conclusion, I would like to say that despite the still-present jambs in the code, from this you can create a fully working prototype of the game. I did not have time to dig out with NavMesh, but at first glance - nothing complicated.

Source: https://habr.com/ru/post/182672/


All Articles