Неудачный тест физики
В этой части мы:
— оптимизируем код из предыдущей статьи;
— создадим объект «база» и научим её чиниться время от времени;
— добавим пушкам патроны и перезарядку;
— избавимся от «неудобной» переменной gv;
А в конце статьи вас ожидает маленький бонус 🙂
Всем заинтересовавшимся — добро пожаловать под долгожданный кат!
Оптимизация, багфиксы, перестановка на сцене и всё такое
В этой части туториала мы оптимизируем ранее написанный нами говнокод, что даст нам запас производительности для игры.
Начнём же мы с AI скрипта пушки, изменения в котором коснулись способа расчёта расстояния, появилась обойма с патронами, и перезарядка, длящаяся указанное время:
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); } } }
Как видно, тут используется расчёт через квадрат расстояния и сравнение его с квадратом максимальной дистанции для пушки. Это работает быстрее, т.к. не используется Sqrt. Спасибо Leopotam за совет 🙂
Следующим шагом приведём сцену примерно в следующий вид:
Красными точками я обозначил места спаунпойнтов, по центру у меня находится «база» в виде стандартного максовского чайника 🙂
На базу я повесил тег Base, чтобы можно было её легко найти.
Нам нужно сделать так, чтобы мобы шли прямо к базе, игнорируя пушки. Для этого нужно научить базу понимать урон и чиниться через определённые интервалы.
Что ж, начнём:
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; //и возвращаем нашу переменную в её первоначальное значение } } } }
Вешаем скрипт на наш объект с базой. Она готова, можно приступить к переобучению мобов!
В скрипте AI мобов изменению подлежит только метод Update, потому остальной код приводить не буду:
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; //если она существует - делаем её целью для моба. } }
Всё хорошо, мобы ползут кусать базу, пушки методично отстреливают нахалов. Но камера-то статичная! Непорядок, исправляем:
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); //направо } }
Скрипт, само собой, вешается на камеру. Теперь всё двигается, можно поглядеть вокруг на подходящих к базе мобов, ставить пушки ещё на подходе.
Следующий багфикс состоит в том, что мы может покупать пушки «в кредит». Да, нужна простая проверка денег игрока и стоимости пушки. Правим это дело:
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 }
Далее, уже после написания всего предыдущего кода, я избавился от объекта GlobalVars, сделав его и все его переменные static.
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 //улучшаем пушку } }
Во всех классах, где использовался GlobalVars, удаляем переменные gv, их инициализацию в Awake(). Заменяем все gv на GlobalVars. Удаляем бесполезные проверки GlobalVars на null. Удаляем компонент GlobalVars из одноимённого ГО (можно сам ГО переименовать во что-то информативное, например, cfg).
Я приведу полные листинги классов с изменениями, чтобы вам было, с чем сравнить результат этой операции.
Осторожно, спойлеры к следующей части! 🙂
Graphic.cs
using UnityEngine; public class Graphic : MonoBehaviour { 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 buildRaycastLayers = 1; public LayerMask upgradeRaycastLayer; //слой для апгрейда public LayerMask sellRaycastLayer; //слой для продажи private GameObject ghost; //переменная для призрака устанавливаемой пушки private void Awake() { 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 (GlobalVars.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, buildRaycastLayers)) // бьём этим лучем в заданном выше направлении (т.е. в землю) { Quaternion normana = Quaternion.FromToRotation(Vector3.up, hit.normal); //получаем нормаль от столкновения ghost.transform.position = hit.point; //задаём позицию пzризрака равной позиции точки удара луча по земле ghost.transform.rotation = normana; //тоже самое и с вращением, только не от точки, а от нормали if (Input.GetMouseButtonDown(0)) //при нажатии ЛКМ { GameObject tower = Instantiate(plasmaTower, ghost.transform.position, ghost.transform.rotation) as GameObject; //Спауним башенку на позиции призрака if (tower != null) { GlobalVars.PlayerMoney -= tower.GetComponent<PlasmaTurretAI>().towerPrice; //отнимаем лаве за башню GlobalVars.TurretCount++; } Destroy(ghost); //уничтожаем призрак башни GlobalVars.mau5tate = GlobalVars.ClickState.Default; //меняем глобальное состояние мыши на обычное } } } break; } case GlobalVars.ClickState.Upgrading: { //Ray scrRay = Camera.main.ScreenPointToRay(Input.mousePosition); //создаём луч, бьющий от координат мыши по координатам в игре //if (Physics.Raycast(scrRay, out hit, Mathf.Infinity)) // бьём этим лучем в заданном выше направлении (т.е. в землю) //{ // Collider[] colls = Physics.OverlapSphere(hit.point, 10.0f); // float closestMobSquaredDistance = 0; // GameObject nearestObject = null; // if (Input.GetMouseButtonDown(0) && GlobalVars.PlayerMoney >= 1000.0f) //при нажатии ЛКМ и наличии более 1000$ денег // { // foreach (Collider coll in colls) // { // float distance = (coll.transform.position - hit.point).sqrMagnitude; // if (distance < 100f || closestMobSquaredDistance == 0.0f) // { // closestMobSquaredDistance = distance; // nearestObject = coll.gameObject; // } // } // if (nearestObject != null) // { // switch (nearestObject.tag) // { // case "Turret": // { // GlobalVars.mau5tate = GlobalVars.ClickState.Default; // break; // } // case "Base": // { // nearestObject.GetComponent<BaseHP>().Upgrade(); // GlobalVars.mau5tate = GlobalVars.ClickState.Default; // break; // } // default: // GlobalVars.mau5tate = GlobalVars.ClickState.Default; // break; // } // } // } //} break; } } } private void OnGUI() { GUI.Box(buyMenu, "Buying menu"); //Делаем гуевский бокс на квадрате buyMenu с заголовком, указанным между "" if (GUI.Button(firstTower, "Plasma Tower\n" + (int)TowerPrices.Plasma + "$")) //если идёт нажатие на первую кнопку { if (GlobalVars.PlayerMoney >= (int)TowerPrices.Plasma) //если у игрока достаточно денег GlobalVars.mau5tate = GlobalVars.ClickState.Placing; //меняем глобальное состояние мыши на "Установка пушки" } if (GUI.Button(secondTower, "Pulse Tower\n" + (int)TowerPrices.Pulse + "$")) //с остальными аналогично { //action here } if (GUI.Button(thirdTower, "Beam Tower\n" + (int)TowerPrices.Beam + "$")) { //action here } if (GUI.Button(fourthTower, "Tesla Tower\n" + (int)TowerPrices.Tesla + "$")) { //action here } if (GUI.Button(fifthTower, "Artillery Tower\n" + (int)TowerPrices.Artillery + "$")) { //action here } GUI.Box(playerStats, "Player Stats"); GUI.Label(playerStatsPlayerMoney, "Money: " + GlobalVars.PlayerMoney + "$"); GUI.Box(towerMenu, "Tower menu"); if (GUI.Button(towerMenuSellTower, "Sell")) { //action here } if (GUI.Button(towerMenuUpgradeTower, "Upgrade")) { //if (GlobalVars.mau5tate == GlobalVars.ClickState.Default) //{ // GlobalVars.mau5tate = GlobalVars.ClickState.Upgrading; //} } } //цены на пушки private enum TowerPrices { Plasma = 100, Pulse = 150, Beam = 250, Tesla = 300, Artillery = 350 } }
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; //и целого моба public GameObject explosionPrefab; private void Awake() { GlobalVars.MobList.Add(gameObject); //добавляем себя в общий лист мобов GlobalVars.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) { GlobalVars.PlayerMoney += mai.mobPrice; //если он существует - добавляем денег игроку в размере цены за голову моба } if (explosionPrefab != null) { Instantiate(explosionPrefab, gameObject.transform.position, Quaternion.identity); } Destroy(gameObject); //удаляем себя } } private void OnDestroy() //при удалении { GlobalVars.MobList.Remove(gameObject); //удаляем себя из глобального списка мобов GlobalVars.MobCount--; //уменьшаем глобальный счетчик мобов на 1 } }
Здесь просто вырезана переменная и её инициализация, в коде не использовалась.
MobAI.cs
using UnityEngine; 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; //константа, используется для сброса таймера атаки в начальное значение public float MobCurrentSpeed; //скорость моба, инициализируем позже private Transform mob; //переменная для трансформа моба private void Awake() { mob = transform; //присваиваем трансформ моба в переменную (повышает производительность) MobCurrentSpeed = Random.Range(mobMinSpeed, mobMaxSpeed); //посредством рандома выбираем скорость между минимально и максимально указанной } 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; //если таймер атаки больше 0 - отнимаем его if (attackTimer <= 0) //если же он стал меньше нуля или равен ему { BaseHP bhp = Target.GetComponent<BaseHP>(); //подключаемся к компоненту ХП цели if (bhp != null) bhp.ChangeHP(-damage); //если цель ещё живая, наносим урон (мы можем не одни бить по цели, потому проверка необходима) attackTimer = coolDown; //возвращаем таймер в исходное положение MobHP mhp = GetComponent<MobHP>(); mhp.curHP = 0; } } } else { GameObject baza = GameObject.FindGameObjectWithTag("Base"); //находим наш объект с базой, он всего один if (baza != null) Target = baza; } } }
BaseHP.cs
using UnityEngine; public class BaseHP : MonoBehaviour { public float maxHP = 1000; public float curHP = 1000; public const float regenerationDelayConstant = 2.5f; //константа задержки между регенерацией хп базы public float regenarationDelayVariable = 2.5f; //переменная той же задержки public float regenerationAmount = 10.0f; //количество восстанавливаемого хп при регенерации за раз private void Awake() { 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; //и возвращаем нашу переменную в её первоначальное значение } } } private void OnGUI() { Vector3 screenPosition = Camera.main.WorldToScreenPoint(gameObject.transform.position); //Находим позицию объекта на экране относительно мира Vector3 cameraRelative = Camera.main.transform.InverseTransformPoint(transform.position); //Получаем дальность объекта от камеры if (cameraRelative.z > 0) //если объект находится впереди камеры { //отображаем количество его HP if (curHP > 0) GUI.Label(new Rect(screenPosition.x, Screen.height - screenPosition.y, 200f, 20f), curHP.ToString()); } } }
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 void Awake() { SpawnPoints = GameObject.FindGameObjectsWithTag("Spawnpoint"); //забираем все точки спауна в массив } private void Update() { if (waveDelayTimer > 0) //если таймеh спауна волны больше нуля { if (GlobalVars.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++; //увеличиваем номер волны } } } }
TurretHP.cs
using UnityEngine; public class TurretHP : MonoBehaviour { public float maxHP = 100; //Максимум ХП public float curHP = 100; //Текущее ХП private void Awake() { GlobalVars.TurretList.Add(gameObject); GlobalVars.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() { GlobalVars.TurretList.Remove(gameObject); GlobalVars.TurretCount--; } }
На этом замена завершена. Следующим моментом добавлю маленький бонус, который значительно облегчит жизнь при регулировке дальности атаки как пушкам, так и мобам:
myGizmos.cs
using UnityEngine; public static class myGizmos { public static void DrawCircle(Vector3 pos, Vector3 normal, int segs, float radius, bool showNormal) { float stepAng = 360.0f / Mathf.Max(segs, 1.0f); float curAng = stepAng; Quaternion rot = Quaternion.FromToRotation(Vector3.up, normal); while (curAng <= 360.0f) { Vector3 pPrev = rot * (Quaternion.Euler(Vector3.up * (curAng - stepAng)) * (Vector3.right * radius)); Vector3 p = rot * (Quaternion.Euler(Vector3.up * curAng) * (Vector3.right * radius)); Gizmos.DrawLine(pos + pPrev, pos + p); curAng += stepAng; } Gizmos.DrawWireSphere(pos, radius * 0.1f); if (showNormal) Gizmos.DrawLine(pos, pos + normal * radius); } public static void DrawArrow(Vector3 pos, Vector3 dir, Vector2 size) { Gizmos.DrawWireSphere(pos, size.x); Vector3 endPos = pos + dir * size.y; Gizmos.DrawLine(pos, endPos); Vector3 ax1 = Vector3.Cross(dir, Vector3.right); Vector3 ax2 = Vector3.Cross(dir, Vector3.forward); Vector3 hpB = endPos - (dir * size.y * 0.2f); Vector3 hp1 = hpB + ax1 * size.x; Vector3 hp2 = hpB - ax1 * size.x; Vector3 hp3 = hpB + ax2 * size.x; Vector3 hp4 = hpB - ax2 * size.x; Gizmos.DrawLine(endPos, hp1); Gizmos.DrawLine(endPos, hp2); Gizmos.DrawLine(endPos, hp3); Gizmos.DrawLine(endPos, hp4); Gizmos.DrawLine(hp1, hp4); Gizmos.DrawLine(hp1, hp3); Gizmos.DrawLine(hp2, hp4); Gizmos.DrawLine(hp2, hp3); } }
GizmoOnObject.cs
using UnityEngine; public class GizmoOnObject : MonoBehaviour { public Color GizmoColor = Color.yellow; public float GizmoRadius = 40.0f; public int GizmoSegments = 36; public bool ShowNormal; public void OnDrawGizmosSelected() { Gizmos.color = GizmoColor; myGizmos.DrawCircle(transform.position, Vector3.up, GizmoSegments, GizmoRadius, ShowNormal); } }
Как использовать: надеваете скрипт на объект и в инспекторе регулируете дальность. Вокруг ГО при выделении появится желтый круг, это и есть указанная дальность.
Заключение
В заключении хочется сказать, что несмотря на до сих пор присутствующие косяки в коде, из этого можно создать вполне рабочий прототип игры. Я так и не успел поковыряться с NavMesh, но на первый взгляд — ничего сложного.
Пишите в комментариях, какие ещё моменты вам хочется видеть в третьей части.
Продолжение следует…
ссылка на оригинал статьи http://habrahabr.ru/post/182672/
Добавить комментарий