📜 ⬆️ ⬇️

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

Hello! I have long wanted to publish this article, but did not have time to allocate time. I would like to warn in advance that the article is intended for a user who does not know Unity3D very well, therefore there will be an abundance of explanations in the text.

Part two

All interested - welcome under the cat!

')

The first steps


Base scene

It's time to create a surface on which we, in fact, will have our guns, mobs, and more. Click Terrain -> Create Terrain. And let's color it at the same time? Select the terrain in the objects, then click in the properties of the button with the image of the brush, just below we press Edit textures -> Add texture.
The add texture window will appear, there is a small circle on the right side of the first line, it is a direct analogue of the Browse button, click on it and select a texture for our surface).


Close the texture selection window, go back to the texture settings window, click the Apply button in it. Voila, our terrain is colored in the texture we have chosen. You can add more textures in the same way and paint it in more detail. Experiment with buttons, create some slides, holes and so on;)

Add a gun

Played with terrain? Great, time for serious toys: swing our only gun (5Mb, link )
Unpack the archive in the Assets folder of the project or in the Project window in the editor, the effect will be the same.
We also need to create another folder: in the same Project there is a Create button at the top. We press it, we select Folder. Name the folder prefabs. Right click on this folder and select Create -> Prefab. And his name will be gun_prefab.

A bit of theory: Prefab is a special kind of object that contains another object (s) with its own settings, and they can be quickly spun on the stage (and this will be useful in the future). Prefabs in the list of scene objects are blue.

Click on the gun file (cannon2) - the properties of the gun import plug-in will be loaded in the property inspector.
Set the values ​​as follows:
Scale Factor set to 0.1
Check Generate Colliders
The rest is not touched, just press the Apply button below.

By this we set the cannon to an adequate size (the initial 0.01 is too small, and the unit is very huge), and generated colliders so that the gun does not collapse under the ground.

In the Project window, drag our cannon (cannon2) onto the newly created prefab (gun_prefab).

Now you can drag our prefab directly onto the stage - the gun will be cloned, but at the same time each new instance will be a separate object. Try it! And when you play enough - remove all the guns from the scene (select them and press Delete).

Create mobs


We have the simplest TD, therefore mobs will be simple balls. Creating them without resorting to 3D modeling is easier than ever.
Click GameObject -> Create Other -> Sphere. An object with the name Sphere will appear on the scene and in the object inspector. Create a prefab with the name monster01 for it in a known way and drag our monster to the prefab. After that, you can remove the monster from the scene, it is no longer needed there, because we'll spawn it right from the code.

Cards, money, two trunks


Drag the gun prefab directly onto the stage from Project, and put it in any convenient place (yes, then the spawn will be implemented differently, but for a start, it will go like this). Time to write AI guns!
Create a folder in the Project scripts, and in it a folder ai. Then right click on the ai folder and select Create -> C # Script. The script is called PlasmaTurretAI.
Open it with a doubleclick, your IDE will load with this script, which will be such a framework for scripting:
PlasmaTurretAI.cs
using UnityEngine; using System.Collections; public class PlasmaTurretAI : MonoBehaviour //       ,    MonoBehaviour    ""    GameObject. { //     void Start () { } //      void Update () { } } 


And now, actually, the AI ​​code itself in the comments:
PlasmaTurretAI.cs
 using System.Collections.Generic; using System.Linq; using UnityEngine; //       ,    MonoBehaviour    ""    GameObject (     ). public class PlasmaTurretAI : MonoBehaviour { public GameObject[] targets; //   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 const float reloadCooldown = 2.5f; //  ,  public float rotationSpeed = 1.5f; //    public int FiringOrder = 1; //    (    2) public Transform turretHead; public RaycastHit Hit; //     private void Start() { turretHead = transform.Find("pushka"); //      } //      private void Update() { if (curTarget != null) //      { float distance = Vector3.Distance(turretHead.position, curTarget.transform.position); //    if (attackMinimumDistance < distance && distance < attackMaximumDistance) //          { turretHead.rotation = Quaternion.Slerp(turretHead.rotation, Quaternion.LookRotation(curTarget.transform.position - turretHead.position), rotationSpeed * Time.deltaTime); //     if (reloadTimer > 0) reloadTimer -= Time.deltaTime; //     -   if (reloadTimer < 0) reloadTimer = 0; //     -     if (reloadTimer == 0) //  { MobHP mhp = curTarget.GetComponent<MobHP>(); switch (FiringOrder) //,     { case 1: Debug.Log("  "); //   FiringOrder++; // FiringOrder  1 break; case 2: Debug.Log("  "); //   FiringOrder = 1; // FiringOrder    break; } reloadTimer = reloadCooldown; //        } } } else // { curTarget = SortTargets(); //     } } //    ,    ! public GameObject SortTargets() { float closestMobDistance = 0; //       GameObject nearestmob = null; //    List<GameObject> sortingMobs = GameObject.FindGameObjectsWithTag("Monster").ToList(); //     Monster      foreach (var everyTarget in sortingMobs) //     { //    ,  closestMobDistance    if ((Vector3.Distance(everyTarget.transform.position, turretHead.position) < closestMobDistance) || closestMobDistance == 0) { closestMobDistance = Vector3.Distance(everyTarget.transform.position, turretHead.position); //     ,     nearestmob = everyTarget;//    } } return nearestmob; //   } } 


Comments, I think, quite clearly describe the code. The only incomprehensible may seem such a monster as a quaternion. Do not hesitate, google, read, this topic is not easy for everyone. And here you can read about quaternions in Unity3D on their own website.

Save the changes and switch back to Unity3D.

To "pull" our newly written script on the gun, you need to drag the script file directly to its prefab. After that, if you click on the gun prefab, a section with our script will appear in the property inspector, where you can customize all the public fields in the code!


Next, to test our code, we need to attach a Monster tag to our monster. Click on it in the Project, then look at the Inspector of the object: in its upper part there is a drop-down field Tag, now there is the value Untagged. Click on this list and at the bottom of it click Add tag.


Expand the Tags list, and in the Element 0 field we write “Monster” (without quotes, as in the screenshot).


Again we click on our monster, again we expand the list of possible tags - Monster will be among them. Choose it.

Novice Mob School


Until now, our mobs were just objects, but now we will teach them to crawl to the cannon and inflict joy, happiness and, above all, damage on it.
In a well-known way, we create new C # scripts: MobAI, GlobalVars, MobHP, TurretHP, SpawnerAI. Let's start in order:

MobAI.cs
 using UnityEngine; using System.Collections.Generic; public class MobAI : MonoBehaviour { public GameObject Target; //  public float mobPrice = 5.0f; //    public float mobMinSpeed = 0.5f; //   public float mobMaxSpeed = 2.0f; //   public float mobRotationSpeed = 2.5f; //   public float attackDistance = 5.0f; //  public float damage = 5; //,   public float attackTimer = 0.0f; //     public const float coolDown = 2.0f; //,         private float MobCurrentSpeed; // ,   private Transform mob; //    private GlobalVars gv; //     private void Awake() { gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //  mob = transform; //     ( ) MobCurrentSpeed = Random.Range(mobMinSpeed, mobMaxSpeed); //         } private void Update() { if (Target == null) //    { Target = SortTargets(); //      } else //     { 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 distance = Vector3.Distance(Target.transform.position, mob.position); //    Vector3 structDirection = (Target.transform.position - mob.position).normalized; //   float attackDirection = Vector3.Dot(structDirection, mob.forward); //   if (distance < attackDistance && attackDirection > 0) //         { if (attackTimer > 0) attackTimer -= Time.deltaTime; //    0 -   if (attackTimer <= 0) //         { TurretHP thp = Target.GetComponent<TurretHP>(); //     if (thp != null) thp.ChangeHP(-damage); //   ,   (      ,   ) attackTimer = coolDown; //     } } } } //    ,    ! private GameObject SortTargets() { float closestTurretDistance = 0; //       GameObject nearestTurret = null; //    List<GameObject> sortingTurrets = gv.TurretList; //    foreach (var turret in sortingTurrets) //     { //    ,  closestTurretDistance    if ((Vector3.Distance(mob.position, turret.transform.position) < closestTurretDistance) || closestTurretDistance == 0) { closestTurretDistance = Vector3.Distance(mob.position, turret.transform.position); //     ,     nearestTurret = turret;//    } } return nearestTurret; //   } } 


GlobalVars.cs - class of global variables
 using System.Collections.Generic; using UnityEngine; public class GlobalVars : MonoBehaviour { public List<GameObject> MobList = new List<GameObject>(); //    public int MobCount = 0; //    public List<GameObject> TurretList = new List<GameObject>(); //    public int TurretCount = 0; //    public float PlayerMoney = 200.0f; //  } 


MobHP.cs
 using UnityEngine; public class MobHP : MonoBehaviour { public float maxHP = 100; //  public float curHP = 100; //  public Color MaxDamageColor = Color.red; //   public Color MinDamageColor = Color.blue; //   private GlobalVars gv; //     private void Awake() { gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //  if (gv != null) { gv.MobList.Add(gameObject); //      gv.MobCount++; //   } if (maxHP < 1) maxHP = 1; //      -   } public void ChangeHP(float adjust) //    { if ((curHP + adjust) > maxHP) curHP = maxHP;//     adjust   ,    -      else curHP += adjust; //   adjust } private void Update() { gameObject.renderer.material.color = Color.Lerp(MaxDamageColor, MinDamageColor, curHP / maxHP); //       .  :  -    ,  - . if (curHP <= 0) //       { MobAI mai = gameObject.GetComponent<MobAI>(); //   AI  if (mai != null && gv != null) gv.PlayerMoney += mai.mobPrice; //   -          Destroy(gameObject); //  } } private void OnDestroy() //  { if (gv != null) { gv.MobList.Remove(gameObject); //      gv.MobCount--; //     1 } } } 


And I did not comment on the next class, it is almost a complete copy of MobHP, with some differences (for example, he doesn’t have to use his color).

TurretHP.cs
 using UnityEngine; public class TurretHP : MonoBehaviour { public float maxHP = 100; //  public float curHP = 100; //  private GlobalVars gv; //     private void Awake() { gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //  if (gv != null) { gv.TurretList.Add(gameObject); gv.TurretCount++; } if (maxHP < 1) maxHP = 1; } public void ChangeHP(float adjust) { if ((curHP + adjust) > maxHP) curHP = maxHP; else curHP += adjust; if (curHP > maxHP) curHP = maxHP; } private void Update() { if (curHP <= 0) { Destroy(gameObject); } } private void OnDestroy() { if (gv != null) { gv.TurretList.Remove(gameObject); gv.TurretCount--; } } } 


SpawnerAI.cs
 using UnityEngine; public class SpawnerAI : MonoBehaviour { public int waveAmount = 5; //   1      public int waveNumber = 0; //   public float waveDelayTimer = 30.0F; //    public float waveCooldown = 20.0F; // (  !)    ,     public int maximumWaves = 500; //     public Transform Mob; //     Unity public GameObject[] SpawnPoints; //   private GlobalVars gv; //     private void Awake() { SpawnPoints = GameObject.FindGameObjectsWithTag("Spawnpoint"); //      gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //  } private void Update() { if (waveDelayTimer > 0) // h     { if (gv != null) { if (gv.MobCount == 0) waveDelayTimer = 0; //     -     else waveDelayTimer -= Time.deltaTime; //   } } if (waveDelayTimer <= 0) //      { if (SpawnPoints != null && waveNumber < maximumWaves) //           { foreach (GameObject spawnPoint in SpawnPoints) //    { for (int i = 0; i < waveAmount; i++) // i    ,          { Instantiate(Mob, new Vector3(spawnPoint.transform.position.x, spawnPoint.transform.position.y, spawnPoint.transform.position.z + i * 10), Quaternion.identity); //  } if (waveCooldown > 5.0f) //    5  { waveCooldown -= 0.1f; //  0.1  waveDelayTimer = waveCooldown; //   } else // { waveCooldown = 5.0f; //     5  waveDelayTimer = waveCooldown; } if (waveNumber >= 50) // 50  { waveAmount = 10; //   10     } } waveNumber++; //   } } } } 


And now you need to fix the AI ​​code of the gun. Find the switch (FiringOrder) there and replace the entire block completely with this:

  switch (FiringOrder) //,     { case 1: if (mhp != null) mhp.ChangeHP(-attackDamage); //   FiringOrder++; // FiringOrder  1 break; case 2: if (mhp != null) mhp.ChangeHP(-attackDamage); //   FiringOrder = 1; // FiringOrder    break; } 


It is also necessary to replace the line at the very end of the same class.

 return nearestmob; 


on such

 return closestMobDistance > attackMaximumDistance ? null : nearestmob; 


This is called the "ternary operator." If the condition is before the "?" true, it will return null, otherwise the nearestmob will return. The meaning of the expression is that the gun will not grasp the target to which it cannot reach.

In general, the code is ready. Now you need to prepare game objects. Create a MobSpawner object, its location does not matter, if only it does not interfere further. Hang the SpawnerAI script on it and set the desired variable values. On the value of the variable Mob we overtighten our prefab mob.

Spawn more do not touch.

Create an object with the name GlobalVars and drag the script with the same name onto it, specify the starting amount of money from the player.
Next, create the required number of objects (for convenience, call it in the spirit of "name_orderNumber") for spawn points and place them in the desired locations of the spawn mobs. Assign a Spawnpoint tag to them, and at the same time create a Turret tag and assign it to the gun prefab.

Hang our 2 MobAI and MobHP scripts on your mobs, and TurretHP on your guns. Do not forget to play around with the values ​​of variables.
You do not need to drag the prefab of the gun to the target value in MobAI, AI itself searches for targets. Very primitive, slow, but looking.

Add the Rigidbody component to the monster prefab (Component -> Physics -> Rigidbody).



Getting started?


To create a GUI, we need a new C # script called Graphic:

Graphic.cs
 using UnityEngine; public class Graphic : MonoBehaviour { private GlobalVars gv; //     public Rect buyMenu; //   public Rect firstTower; //     public Rect secondTower; //     public Rect thirdTower; //     public Rect fourthTower; //     public Rect fifthTower; //     public Rect towerMenu; //    (/) public Rect towerMenuSellTower; //    public Rect towerMenuUpgradeTower; //    public Rect playerStats; //   public Rect playerStatsPlayerMoney; //     public GameObject plasmaTower; //  ,     public GameObject plasmaTowerGhost; //  ,     private RaycastHit hit; //   public LayerMask raycastLayers = 1; //    / - ,    private GameObject ghost; //     private void Awake() { gv = GameObject.Find("GlobalVars").GetComponent<GlobalVars>(); //  if (gv == null) Debug.LogWarning("gv variable is not initialized correctly in " + this); //  ,  gv  buyMenu = new Rect(Screen.width - 185.0f, 10.0f, 175.0f, Screen.height - 100.0f); //  ,   X, Y, , . X  Y       firstTower = new Rect(buyMenu.x + 12.5f, buyMenu.y + 30.0f, 150.0f, 50.0f); secondTower = new Rect(firstTower.x, buyMenu.y + 90.0f, 150.0f, 50.0f); thirdTower = new Rect(firstTower.x, buyMenu.y + 150.0f, 150.0f, 50.0f); fourthTower = new Rect(firstTower.x, buyMenu.y + 210.0f, 150.0f, 50.0f); fifthTower = new Rect(firstTower.x, buyMenu.y + 270.0f, 150.0f, 50.0f); playerStats = new Rect(10.0f, 10.0f, 150.0f, 100.0f); playerStatsPlayerMoney = new Rect(playerStats.x + 12.5f, playerStats.y + 30.0f, 125.0f, 25.0f); towerMenu = new Rect(10.0f, Screen.height - 60.0f, 400.0f, 50.0f); towerMenuSellTower = new Rect(towerMenu.x + 12.5f, towerMenu.y + 20.0f, 75.0f, 25.0f); towerMenuUpgradeTower = new Rect(towerMenuSellTower.x + 5.0f + towerMenuSellTower.width, towerMenuSellTower.y, 75.0f, 25.0f); } private void Update() { switch (gv.mau5tate) //    { case GlobalVars.ClickState.Placing: //      { if (ghost == null) ghost = Instantiate(plasmaTowerGhost) as GameObject; //    -       else // { Ray scrRay = Camera.main.ScreenPointToRay(Input.mousePosition); // ,         if (Physics.Raycast(scrRay, out hit, Mathf.Infinity, raycastLayers)) //        (..  ) { Quaternion normana = Quaternion.FromToRotation(Vector3.up, hit.normal); //    ghost.transform.position = hit.point; //          ghost.transform.rotation = normana; //    ,    ,    if (Input.GetMouseButtonDown(0)) //   { GameObject tower = Instantiate(plasmaTower, ghost.transform.position, ghost.transform.rotation) as GameObject; //     if (tower != null) gv.PlayerMoney -= tower.GetComponent<PlasmaTurretAI>().towerPrice; //    Destroy(ghost); //   gv.mau5tate = GlobalVars.ClickState.Default; //      } } } break; } } } private void OnGUI() { GUI.Box(buyMenu, "Buying menu"); //     buyMenu  ,   "" if (GUI.Button(firstTower, "Plasma Tower\n100$")) //      { gv.mau5tate = GlobalVars.ClickState.Placing; //    } if (GUI.Button(secondTower, "Pulse Tower\n155$")) //   { //action here } if (GUI.Button(thirdTower, "Beam Tower\n250$")) { //action here } if (GUI.Button(fourthTower, "Tesla Tower\n375$")) { //action here } if (GUI.Button(fifthTower, "Artillery Tower\n500$")) { //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")) { //action here } } } 


Oh yeah, now we still need a good change to the GlobalVars script:

GlobalVars.cs
 using System.Collections.Generic; using UnityEngine; public class GlobalVars : MonoBehaviour { public List<GameObject> MobList = new List<GameObject>(); //    public int MobCount = 0; //    public List<GameObject> TurretList = new List<GameObject>(); //    public int TurretCount = 0; //    public float PlayerMoney; //  public ClickState mau5tate = ClickState.Default; //   public enum ClickState //    { Default, Placing, Selling, Upgrading } public void Awake() { PlayerMoney = PlayerPrefs.GetFloat("Player Money", 200.0f); //  ,        -   200$,     } public void OnApplicationQuit() { PlayerPrefs.SetFloat("Player Money", PlayerMoney); //     PlayerPrefs.Save(); } } 


Next, we need to create a ghost cannon, and this is done quite easily: we duplicate the cannon, throw some empty GO into it, delete it and untie the gun from its prefab, the main thing is not to save it! Then we go through the entire hierarchy of objects inside the gun and change the shaders to Transparent Diffuse. By the way, to see absolutely the whole structure of the gun - it is necessary to put it on the scene and reveal the hierarchy already there. If there are problems with the creation of a ghost - lay out already ready. Some people still have a link to the package download here (3.7 mb).

Theoretically should work. Of course, there are jambs and the most obvious of them is the need to write the code of the mechanism for spawn guns for each state - this is corrected by creating your own method with overloads in the form of states and parsing them within the method, but this is already code optimization that needs to be dealt with immediately because the thoughtful code makes it easy to rule yourself under sudden ideas.

And although this part of the lesson is not very long, but the most important in the implementation and understanding of the basic mechanisms of the game.

And if everything worked out right for you, it would look something like this:



To be continued!

07/26/2012 : I fixed the errors in the code
05/14/2018 : I fixed the broken links

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


All Articles