From 2291f81110d6602e0170d1d7c242f6416eb62082 Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Tue, 26 May 2026 00:24:53 +0300 Subject: [PATCH 1/8] HW Unit Shooting Mechanics --- .../UnitBrains/Player/SecondUnitBrain.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index c2c80e989..21beaa82f 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -16,12 +16,34 @@ public class SecondUnitBrain : DefaultPlayerUnitBrain protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) { float overheatTemperature = OverheatTemperature; - /////////////////////////////////////// + // Homework 1.3 (1st block, 3rd module) - /////////////////////////////////////// - var projectile = CreateProjectile(forTarget); - AddProjectileToList(projectile, intoList); - /////////////////////////////////////// + + //проверяем, не перегрето ли оружие + if (GetTemperature() >= OverheatTemperature) + { + return; //оружие перегрелось, прерываем + } + + // Определяем количество снарядов (минимум 1) + int projectileCount = Mathf.Max(1, Mathf.FloorToInt(GetTemperature() + 1)); + + //Создание нужного количества снарядов + for (int i = 0; i < projectileCount; i++) + { + var projectile = CreateProjectile(forTarget); + AddProjectileToList(projectile, intoList); + } + + // Увеличиваем температуру + IncreaseTemperature(); + + // Проверяем перегрев после выстрела + if (GetTemperature() >= OverheatTemperature) + { + _overheated = true; + _cooldownTime = Time.time + OverheatCooldown; + } } public override Vector2Int GetNextStep() From 2bfce4ff264c01bb5e1abdbbe3f9173520293575 Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Tue, 26 May 2026 21:11:22 +0300 Subject: [PATCH 2/8] HW Select Closest Targets --- .../UnitBrains/Player/SecondUnitBrain.cs | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index 21beaa82f..661025e6d 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using Model.Runtime; using Model.Runtime.Projectiles; +using System.Collections.Generic; using UnityEngine; +using static UnityEngine.GraphicsBuffer; namespace UnitBrains.Player { @@ -53,16 +55,44 @@ public override Vector2Int GetNextStep() protected override List SelectTargets() { - /////////////////////////////////////// + // Homework 1.4 (1st block, 4rd module) - /////////////////////////////////////// + + // Получаем список всех целей, до которых может достать юнит List result = GetReachableTargets(); + + // Пока в списке больше 1 цели - удаляем последнюю while (result.Count > 1) { - result.RemoveAt(result.Count - 1); + result.RemoveAt(result.Count - 1); // Удаляем элемент с последним индексом } + + // Возвращаем список (в котором остался максимум 1 элемент) return result; - /////////////////////////////////////// + + // Находим ближайшую к нашей базе цель + Vector2Int closestTarget = result[0]; + float closestDistance = DistanceToOwnBase(closestTarget); + + // Перебираем всех остальных врагов + for (int i = 1; i < result.Count; i++) + { + Vector2Int target = result[i]; + float distance = DistanceToOwnBase(target); + + if (distance < closestDistance) + { + closestDistance = distance; + closestTarget = target; + } + } + + // Очищаем список и добавляем только ближайшую цель + result.Clear(); + result.Add(closestTarget); + + return result; + } public override void Update(float deltaTime, float time) From 0927f1b679de33bfc6f242798e051d7d4b7b4b40 Mon Sep 17 00:00:00 2001 From: Aleksdzen Date: Wed, 1 Jul 2026 23:05:46 +0300 Subject: [PATCH 3/8] HW Most Dangerous Target --- .../UnitBrains/Player/SecondUnitBrain.cs | 198 ++++++++++++------ 1 file changed, 133 insertions(+), 65 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index 661025e6d..49fa4a254 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -1,108 +1,162 @@ -using Model.Runtime; -using Model.Runtime.Projectiles; -using System.Collections.Generic; -using UnityEngine; -using static UnityEngine.GraphicsBuffer; - -namespace UnitBrains.Player +using Model; +using Model.Runtime; //импортирует пространство имен для работы с моделью игры +using Model.Runtime.Projectiles; //импортирует классы для работы со снарядами +using System; +using System.Collections.Generic; //импортирует коллекции (List, Dictionary и т.д.) +using UnityEngine; //импортирует основной функционал Unity +using Utilities; +using static UnityEngine.GraphicsBuffer; //импортирует статические методы GraphicsBuffer + +namespace UnitBrains.Player //определяет пространство имен для логики юнитов игрока { - public class SecondUnitBrain : DefaultPlayerUnitBrain + public class SecondUnitBrain : DefaultPlayerUnitBrain //объявление публичного класса для второго юнита : наследование от базового класса для всех юнитов игрока { - public override string TargetUnitName => "Cobra Commando"; - private const float OverheatTemperature = 3f; - private const float OverheatCooldown = 2f; - private float _temperature = 0f; - private float _cooldownTime = 0f; - private bool _overheated; - - protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) + public override string TargetUnitName => "Cobra Commando"; //Переопределение свойства TargetUnitName - возвращает имя цели для этого юнита ("Cobra Commando") + private const float OverheatTemperature = 3f; //константа температуры перегрева (при достижении 3 оружие перегревается) + private const float OverheatCooldown = 2f; //константа времени остывания в секундах + private float _temperature = 0f; //текущая температура оружия (начинается с 0) + private float _cooldownTime = 0f; //время остывания (используется для таймера) + private bool _overheated; //флаг перегрева (true = оружие перегрето) + + // Новое поле для хранения целей, к которым нужно идти + List _targetsToChase = new List(); + + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) //Переопределение метода - вызывается для создания снарядов по цели(forTarget - координаты цели, intoList - список для добавления созданных снарядов) { - float overheatTemperature = OverheatTemperature; - + /*float overheatTemperature = OverheatTemperature;*/ //Создает локальную переменную с температурой перегрева (дублирование константы) + // Homework 1.3 (1st block, 3rd module) - + //проверяем, не перегрето ли оружие if (GetTemperature() >= OverheatTemperature) { - return; //оружие перегрелось, прерываем + return; //оружие перегрелось, прерываем, если температура >= 3, метод завершается (ничего не стреляет) } // Определяем количество снарядов (минимум 1) int projectileCount = Mathf.Max(1, Mathf.FloorToInt(GetTemperature() + 1)); - + //Mathf.FloorToInt - округляет вниз до целого числа + //GetTemperature() + 1 - при температуре 0 дает 1, при 1 дает 2, при 2 дает 3 + //Mathf.Max(1, ...) - гарантирует минимум 1 снаряд + //Примеры: temp = 0 → count = 1, temp = 1 → count = 2, temp = 2 → count = 3 + + //Создание нужного количества снарядов for (int i = 0; i < projectileCount; i++) { - var projectile = CreateProjectile(forTarget); - AddProjectileToList(projectile, intoList); + var projectile = CreateProjectile(forTarget); //CreateProjectile - создает снаряд для цели + AddProjectileToList(projectile, intoList); //добавляет снаряд в список (активирует его в игре) } - // Увеличиваем температуру + // Увеличиваем температуру на 1, вызов метода IncreaseTemperature(); // Проверяем перегрев после выстрела if (GetTemperature() >= OverheatTemperature) { - _overheated = true; - _cooldownTime = Time.time + OverheatCooldown; + _overheated = true; //Если температура >= 3, устанавливает флаг перегрева + _cooldownTime = Time.time + OverheatCooldown;//Запоминает время, когда оружие остынет (текущее время + 2 секунды) } } - public override Vector2Int GetNextStep() + public override Vector2Int GetNextStep()//Переопределение метода перемещения { - return base.GetNextStep(); + // Проверяем, есть ли цели для преследования + if (_targetsToChase.Count > 0) + { + Vector2Int target = _targetsToChase[0]; + + // Проверяем, находится ли цель в зоне досягаемости + if (!IsTargetInRange(target)) + { + // Если цель вне зоны досягаемости - двигаемся к ней + return unit.Pos.CalcNextStepTowards(target); + } + else + { + // Если цель в зоне досягаемости - остаемся на месте + return unit.Pos; + } + } + + // Если целей для преследования нет - остаемся на месте + return unit.Pos; + } protected override List SelectTargets() { - - // Homework 1.4 (1st block, 4rd module) - - // Получаем список всех целей, до которых может достать юнит - List result = GetReachableTargets(); + // A. Получаем все доступные цели + List allTargets = (List)GetAllTargets(); + List result = new List(); - // Пока в списке больше 1 цели - удаляем последнюю - while (result.Count > 1) - { - result.RemoveAt(result.Count - 1); // Удаляем элемент с последним индексом - } + // Очищаем список целей для преследования + _targetsToChase.Clear(); - // Возвращаем список (в котором остался максимум 1 элемент) - return result; + // B. Если есть цели + if (allTargets.Count > 0) + { + // Находим ближайшую к нашей базе цель (самая опасная) + Vector2Int closestTarget = allTargets[0]; + float closestDistance = DistanceToOwnBase(closestTarget); - // Находим ближайшую к нашей базе цель - Vector2Int closestTarget = result[0]; - float closestDistance = DistanceToOwnBase(closestTarget); + for (int i = 1; i < allTargets.Count; i++) + { + Vector2Int target = allTargets[i]; + float distance = DistanceToOwnBase(target); + + if (distance < closestDistance) + { + closestDistance = distance; + closestTarget = target; + } + } - // Перебираем всех остальных врагов - for (int i = 1; i < result.Count; i++) + // Проверяем, находится ли самая опасная цель в зоне досягаемости + if (IsTargetInRange(closestTarget)) + { + // Если цель в зоне досягаемости - добавляем в result + result.Add(closestTarget); + } + else + { + // Если цель вне зоны досягаемости - добавляем в список для преследования + _targetsToChase.Add(closestTarget); + } + } + else { - Vector2Int target = result[i]; - float distance = DistanceToOwnBase(target); + // C. Если целей нет - добавляем базу противника + int enemyBaseId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBase = runtimeModel.RoMap.Bases[enemyBaseId]; - if (distance < closestDistance) + // Проверяем, находится ли база противника в зоне досягаемости + if (IsTargetInRange(enemyBase)) { - closestDistance = distance; - closestTarget = target; + result.Add(enemyBase); + } + else + { + // Если база вне зоны досягаемости - добавляем в список для преследования + _targetsToChase.Add(enemyBase); } } - // Очищаем список и добавляем только ближайшую цель - result.Clear(); - result.Add(closestTarget); - return result; - } - public override void Update(float deltaTime, float time) + public override void Update(float deltaTime, float time) //Метод вызывается каждый кадр, deltaTime - время между кадрами, + //time - текущее время игры, Проверяет, перегрето ли оружие { if (_overheated) { - _cooldownTime += Time.deltaTime; - float t = _cooldownTime / (OverheatCooldown/10); - _temperature = Mathf.Lerp(OverheatTemperature, 0, t); - if (t >= 1) + _cooldownTime += deltaTime; //Увеличивает время остывания на время кадра + + float t = _cooldownTime / OverheatCooldown; //Вычисляет прогресс остывания, t будет 2 секунды + + _temperature = Mathf.Lerp(OverheatTemperature, 0, t); //Mathf.Lerp - линейная интерполяция, Плавно уменьшает температуру от 3 до 0 за время t + + if (t >= 1) //Когда прогресс достиг 1 (прошло 2 секунды), Сбрасывает таймер и флаг перегрева { _cooldownTime = 0; _overheated = false; @@ -110,16 +164,30 @@ public override void Update(float deltaTime, float time) } } - private int GetTemperature() + private int GetTemperature() //Возвращает температуру как целое число { - if(_overheated) return (int) OverheatTemperature; - else return (int)_temperature; + if (_overheated) + { + return (int)OverheatTemperature; //Если перегрет → возвращает 3 + } + + else + { + return (int)_temperature; //Иначе возвращает текущую температуру (0, 1, 2 или 3) + } + } private void IncreaseTemperature() { - _temperature += 1f; - if (_temperature >= OverheatTemperature) _overheated = true; + _temperature += 1f; //Увеличивает температуру на 1 + /*if (_temperature >= OverheatTemperature) _overheated = true;*/ //Если достигла 3, устанавливает флаг перегрева + } + + // Вспомогательный метод для проверки, находится ли цель в зоне досягаемости + private new bool IsTargetInRange(Vector2Int target) + { + return unit.IsTargetInAttackRange(target); } } } \ No newline at end of file From e904427005c01f7c888a65de6d10e6c22e3e9649 Mon Sep 17 00:00:00 2001 From: alekseydzen Date: Fri, 3 Jul 2026 13:07:05 +0300 Subject: [PATCH 4/8] HW Most Dangerous Target_new --- .../UnitBrains/Player/SecondUnitBrain.cs | 235 ++++++++---------- 1 file changed, 105 insertions(+), 130 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index 49fa4a254..0f22f26ce 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -1,162 +1,144 @@ using Model; -using Model.Runtime; //импортирует пространство имен для работы с моделью игры -using Model.Runtime.Projectiles; //импортирует классы для работы со снарядами -using System; -using System.Collections.Generic; //импортирует коллекции (List, Dictionary и т.д.) -using UnityEngine; //импортирует основной функционал Unity +using Model.Runtime.Projectiles; +using System.Collections.Generic; +using UnityEngine; using Utilities; -using static UnityEngine.GraphicsBuffer; //импортирует статические методы GraphicsBuffer -namespace UnitBrains.Player //определяет пространство имен для логики юнитов игрока +namespace UnitBrains.Player { - public class SecondUnitBrain : DefaultPlayerUnitBrain //объявление публичного класса для второго юнита : наследование от базового класса для всех юнитов игрока + public class SecondUnitBrain : DefaultPlayerUnitBrain { - public override string TargetUnitName => "Cobra Commando"; //Переопределение свойства TargetUnitName - возвращает имя цели для этого юнита ("Cobra Commando") - private const float OverheatTemperature = 3f; //константа температуры перегрева (при достижении 3 оружие перегревается) - private const float OverheatCooldown = 2f; //константа времени остывания в секундах - private float _temperature = 0f; //текущая температура оружия (начинается с 0) - private float _cooldownTime = 0f; //время остывания (используется для таймера) - private bool _overheated; //флаг перегрева (true = оружие перегрето) - - // Новое поле для хранения целей, к которым нужно идти - List _targetsToChase = new List(); - - protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) //Переопределение метода - вызывается для создания снарядов по цели(forTarget - координаты цели, intoList - список для добавления созданных снарядов) + public override string TargetUnitName => "Cobra Commando"; + private const float OverheatTemperature = 3f; //температура перегрева + private const float OverheatCooldown = 2f; //время остывания + private float _temperature = 0f; //текущая температур оружия + private float _cooldownTime = 0f; //время с начала остывания + private bool _overheated; //флаг перегрева (true - перегрето) + + + // B.Создаем новое поле для хранения целей, к которым нужно идти, но которые вне зоны досягаемости + + List _outOfRangeTargets = new List(); + + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) //метод генерации снарядов (forTarget - цель для выстрела, список куда добавляются созданные снаряды) { - /*float overheatTemperature = OverheatTemperature;*/ //Создает локальную переменную с температурой перегрева (дублирование константы) - - // Homework 1.3 (1st block, 3rd module) - - //проверяем, не перегрето ли оружие - if (GetTemperature() >= OverheatTemperature) + float overheatTemperature = OverheatTemperature; //локальная переменная для температуры перегрева + /////////////////////////////////////// + // Homework 1.3 (1st block, 3rd module) + /////////////////////////////////////// + if (GetTemperature() >= overheatTemperature) { - return; //оружие перегрелось, прерываем, если температура >= 3, метод завершается (ничего не стреляет) + return; } - // Определяем количество снарядов (минимум 1) - int projectileCount = Mathf.Max(1, Mathf.FloorToInt(GetTemperature() + 1)); - //Mathf.FloorToInt - округляет вниз до целого числа - //GetTemperature() + 1 - при температуре 0 дает 1, при 1 дает 2, при 2 дает 3 - //Mathf.Max(1, ...) - гарантирует минимум 1 снаряд - //Примеры: temp = 0 → count = 1, temp = 1 → count = 2, temp = 2 → count = 3 - - - //Создание нужного количества снарядов + int currentTemp = GetTemperature(); + int projectileCount = currentTemp + 1; + for (int i = 0; i < projectileCount; i++) { - var projectile = CreateProjectile(forTarget); //CreateProjectile - создает снаряд для цели - AddProjectileToList(projectile, intoList); //добавляет снаряд в список (активирует его в игре) + var projectile = CreateProjectile(forTarget); //создание снаряда направленного в указанную цель + AddProjectileToList(projectile, intoList); //добавление созданного снаряда в список } - // Увеличиваем температуру на 1, вызов метода - IncreaseTemperature(); - - // Проверяем перегрев после выстрела - if (GetTemperature() >= OverheatTemperature) - { - _overheated = true; //Если температура >= 3, устанавливает флаг перегрева - _cooldownTime = Time.time + OverheatCooldown;//Запоминает время, когда оружие остынет (текущее время + 2 секунды) - } + IncreaseTemperature(); //увеличиваем температуру после выстрела } - public override Vector2Int GetNextStep()//Переопределение метода перемещения + public override Vector2Int GetNextStep() { - // Проверяем, есть ли цели для преследования - if (_targetsToChase.Count > 0) + // Если есть цели вне зоны досягаемости, идем к первой из них + if (_outOfRangeTargets.Count > 0) { - Vector2Int target = _targetsToChase[0]; - - // Проверяем, находится ли цель в зоне досягаемости - if (!IsTargetInRange(target)) - { - // Если цель вне зоны досягаемости - двигаемся к ней - return unit.Pos.CalcNextStepTowards(target); - } - else - { - // Если цель в зоне досягаемости - остаемся на месте - return unit.Pos; - } + Vector2Int target = _outOfRangeTargets[0]; + Vector2Int currentPos = unit.Pos; + return currentPos.CalcNextStepTowards(target); } - // Если целей для преследования нет - остаемся на месте + //Vector2Int position = Vector2Int.zero; + //Vector2Int nextPosition = Vector2Int.right; + //position = position.CalcNextStepTowards(nextPosition); + // Если целей вне зоны досягаемости нет, возвращаем текущую позицию юнита return unit.Pos; - } - protected override List SelectTargets() + protected override List SelectTargets() //получает список всех достижимых целей { - // A. Получаем все доступные цели - List allTargets = (List)GetAllTargets(); - List result = new List(); + /////////////////////////////////////// + // Homework 1.4 (1st block, 4rd module) + /////////////////////////////////////// - // Очищаем список целей для преследования - _targetsToChase.Clear(); + // Очищаем список целей вне зоны досягаемости + _outOfRangeTargets.Clear(); - // B. Если есть цели - if (allTargets.Count > 0) - { - // Находим ближайшую к нашей базе цель (самая опасная) - Vector2Int closestTarget = allTargets[0]; - float closestDistance = DistanceToOwnBase(closestTarget); + List result = new List(GetAllTargets()); //список всех достижимых целей - for (int i = 1; i < allTargets.Count; i++) - { - Vector2Int target = allTargets[i]; - float distance = DistanceToOwnBase(target); - - if (distance < closestDistance) - { - closestDistance = distance; - closestTarget = target; - } - } - // Проверяем, находится ли самая опасная цель в зоне досягаемости - if (IsTargetInRange(closestTarget)) + //если целей нет добавляем базу противника + if (result.Count == 0) + { + // Определяем ID противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + // Получаем позицию базы противника + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + // Проверяем, находится ли база в зоне досягаемости + if (IsTargetInRange(enemyBasePos)) { - // Если цель в зоне досягаемости - добавляем в result - result.Add(closestTarget); + // Если в зоне досягаемости - возвращаем как цель + return new List { enemyBasePos }; } else { - // Если цель вне зоны досягаемости - добавляем в список для преследования - _targetsToChase.Add(closestTarget); + // Если вне зоны досягаемости - добавляем в список для движения + _outOfRangeTargets.Add(enemyBasePos); + return new List(); } + } - else + + // Находим ближайшую к нашей базе цель (самая опасная) + Vector2Int closestTarget = result[0]; + float closestDistance = DistanceToOwnBase(closestTarget); + + // Перебираем всех остальных врагов + for (int i = 1; i < result.Count; i++) { - // C. Если целей нет - добавляем базу противника - int enemyBaseId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; - Vector2Int enemyBase = runtimeModel.RoMap.Bases[enemyBaseId]; + Vector2Int target = result[i]; + float distance = DistanceToOwnBase(target); - // Проверяем, находится ли база противника в зоне досягаемости - if (IsTargetInRange(enemyBase)) - { - result.Add(enemyBase); - } - else + if (distance < closestDistance) { - // Если база вне зоны досягаемости - добавляем в список для преследования - _targetsToChase.Add(enemyBase); + closestDistance = distance; + closestTarget = target; } } + + if (IsTargetInRange(closestTarget)) + { + // Если цель в зоне досягаемости, добавляем в результат + result.Add(closestTarget); + } + else + { + // Если цель вне зоны досягаемости, записываем в коллекцию для движения + _outOfRangeTargets.Add(closestTarget); + } + + // Очищаем список и добавляем только ближайшую цель + result.Clear(); + result.Add(closestTarget); + return result; + } - public override void Update(float deltaTime, float time) //Метод вызывается каждый кадр, deltaTime - время между кадрами, - //time - текущее время игры, Проверяет, перегрето ли оружие + public override void Update(float deltaTime, float time) //обновляет состояние юнита каждый кадр { if (_overheated) - { - _cooldownTime += deltaTime; //Увеличивает время остывания на время кадра - - float t = _cooldownTime / OverheatCooldown; //Вычисляет прогресс остывания, t будет 2 секунды - - _temperature = Mathf.Lerp(OverheatTemperature, 0, t); //Mathf.Lerp - линейная интерполяция, Плавно уменьшает температуру от 3 до 0 за время t - - if (t >= 1) //Когда прогресс достиг 1 (прошло 2 секунды), Сбрасывает таймер и флаг перегрева + { + _cooldownTime += Time.deltaTime; //если оружия перегрето, увелиичивает время остываения + float t = _cooldownTime / (OverheatCooldown / 10); //вычисляет процесс остывания 0,2 сек + _temperature = Mathf.Lerp(OverheatTemperature, 0, t); //плавное уменьшение температуры от 3 до 0 в зависимотси от прогресса остывания + if (t >= 1) //после завершения остывания сброс таймера и флага перегрева { _cooldownTime = 0; _overheated = false; @@ -164,30 +146,23 @@ public override void Update(float deltaTime, float time) //Метод вызыв } } - private int GetTemperature() //Возвращает температуру как целое число + private int GetTemperature() //возвращает текущую температуру как целое число { - if (_overheated) + if (_overheated) // если перегрето { - return (int)OverheatTemperature; //Если перегрет → возвращает 3 + return (int)OverheatTemperature; //возврат 3 } - - else + else //иначе { - return (int)_temperature; //Иначе возвращает текущую температуру (0, 1, 2 или 3) + return (int)_temperature; // текущая температа } } - private void IncreaseTemperature() - { - _temperature += 1f; //Увеличивает температуру на 1 - /*if (_temperature >= OverheatTemperature) _overheated = true;*/ //Если достигла 3, устанавливает флаг перегрева - } - - // Вспомогательный метод для проверки, находится ли цель в зоне досягаемости - private new bool IsTargetInRange(Vector2Int target) + private void IncreaseTemperature() //метод увеличения температуры { - return unit.IsTargetInAttackRange(target); + _temperature += 1f; //увеличивает температуру на 1 + if (_temperature >= OverheatTemperature) _overheated = true; //если текущая температура = 3, то перегрев } } } \ No newline at end of file From 62bcaead23dec989533581d75cbfe2dbce63ad2a Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Fri, 31 Jul 2026 20:43:32 +0300 Subject: [PATCH 5/8] SortByDistanceToOwnBase Homework#5 --- .../UnitBrains/Player/SecondUnitBrain.cs | 91 ++++++++++++------- 1 file changed, 59 insertions(+), 32 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index 0f22f26ce..fd1d88a87 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -15,11 +15,23 @@ public class SecondUnitBrain : DefaultPlayerUnitBrain private float _cooldownTime = 0f; //время с начала остывания private bool _overheated; //флаг перегрева (true - перегрето) - // B.Создаем новое поле для хранения целей, к которым нужно идти, но которые вне зоны досягаемости - List _outOfRangeTargets = new List(); + // A. Создаем статическое поле-счетчик для выдачи номеров юнитам + private static int _unitCounter = 0; + // A. Создаем поле с номером юнита + private int _unitNumber; + // A. Создаем константу для максимального количества целей для умного выбора + private const int MaxTargetsForSmartSelection = 3; + + // Конструктор для присвоения номера юниту при создании + public SecondUnitBrain() + { + _unitNumber = _unitCounter; + _unitCounter++; + } + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) //метод генерации снарядов (forTarget - цель для выстрела, список куда добавляются созданные снаряды) { float overheatTemperature = OverheatTemperature; //локальная переменная для температуры перегрева @@ -53,9 +65,6 @@ public override Vector2Int GetNextStep() return currentPos.CalcNextStepTowards(target); } - //Vector2Int position = Vector2Int.zero; - //Vector2Int nextPosition = Vector2Int.right; - //position = position.CalcNextStepTowards(nextPosition); // Если целей вне зоны досягаемости нет, возвращаем текущую позицию юнита return unit.Pos; } @@ -69,66 +78,84 @@ protected override List SelectTargets() //получает спис // Очищаем список целей вне зоны досягаемости _outOfRangeTargets.Clear(); - List result = new List(GetAllTargets()); //список всех достижимых целей - + // B. Получаем все цели и очищаем список + List allTargets = new List(GetAllTargets()); + List result = new List(); - //если целей нет добавляем базу противника - if (result.Count == 0) + // Если в списке целей никого нет, добавляем базу противника + if (allTargets.Count == 0) { // Определяем ID противника int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; // Получаем позицию базы противника Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + // Проверяем, находится ли база в зоне досягаемости if (IsTargetInRange(enemyBasePos)) { // Если в зоне досягаемости - возвращаем как цель - return new List { enemyBasePos }; + result.Add(enemyBasePos); + return result; } else { // Если вне зоны досягаемости - добавляем в список для движения _outOfRangeTargets.Add(enemyBasePos); - return new List(); + return result; } - } - // Находим ближайшую к нашей базе цель (самая опасная) - Vector2Int closestTarget = result[0]; - float closestDistance = DistanceToOwnBase(closestTarget); + // B. Сортируем цели по дистанции до нашей базы + SortByDistanceToOwnBase(allTargets); - // Перебираем всех остальных врагов - for (int i = 1; i < result.Count; i++) + // B. Рассчитываем, какую цель по счету должен атаковать текущий юнит + // Если целей меньше, чем MaxTargetsForSmartSelection, берем остаток от деления номера юнита на количество целей + int targetIndex; + if (allTargets.Count <= MaxTargetsForSmartSelection) { - Vector2Int target = result[i]; - float distance = DistanceToOwnBase(target); - - if (distance < closestDistance) + targetIndex = _unitNumber % allTargets.Count; + } + else + { + // Если целей больше, чем MaxTargetsForSmartSelection, берем остаток от деления на MaxTargetsForSmartSelection + targetIndex = _unitNumber % MaxTargetsForSmartSelection; + // Но если индекс выходит за пределы списка, корректируем + if (targetIndex >= allTargets.Count) { - closestDistance = distance; - closestTarget = target; + targetIndex = allTargets.Count - 1; } } - - if (IsTargetInRange(closestTarget)) + // Получаем выбранную цель по индексу + Vector2Int selectedTarget = allTargets[targetIndex]; + + // Проверяем, что цель в радиусе атаки + if (IsTargetInRange(selectedTarget)) { // Если цель в зоне досягаемости, добавляем в результат - result.Add(closestTarget); + result.Add(selectedTarget); } else { // Если цель вне зоны досягаемости, записываем в коллекцию для движения - _outOfRangeTargets.Add(closestTarget); + _outOfRangeTargets.Add(selectedTarget); + // Ищем первую цель в списке, которая находится в зоне досягаемости + foreach (Vector2Int target in allTargets) + { + if (IsTargetInRange(target)) + { + result.Add(target); + break; + } + } + // Если ни одна цель не в зоне досягаемости, добавляем все цели в список для движения + if (result.Count == 0) + { + _outOfRangeTargets.AddRange(allTargets); + } } - // Очищаем список и добавляем только ближайшую цель - result.Clear(); - result.Add(closestTarget); - return result; - } public override void Update(float deltaTime, float time) //обновляет состояние юнита каждый кадр From 3cf9406e046bcb87623751b3b8063d3c83bb28e5 Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Sun, 2 Aug 2026 15:31:07 +0300 Subject: [PATCH 6/8] HW New Unit/@alekseydzen83 --- .../UnitBrains/Player/ThirdUnitBrain.cs | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs new file mode 100644 index 000000000..bbcc903ad --- /dev/null +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs @@ -0,0 +1,230 @@ +using Model; // Подключение пространства имен Model, содержащего основные классы модели игры +using Model.Runtime.Projectiles; // Подключение пространства имен для работы со снарядами +using System.Collections.Generic; // Подключение для использования обобщенных коллекций (List, Dictionary и т.д.) +using UnityEngine; // Подключение Unity-библиотеки для работы с Vector2Int и Time.deltaTime +using Utilities; // Подключение вспомогательных утилит (в т.ч. расширения CalcNextStepTowards) + +namespace UnitBrains.Player // Объявление пространства имен для юнит-мозгов игрока +{ + public class ThirdUnitBrain : DefaultPlayerUnitBrain // Класс мозга третьего юнита, наследующий базовый класс игрового юнита + { + public override string TargetUnitName => "Ironclad Behemoth"; // Переопределение свойства - возвращает имя цели для этого юнита + + private const float OverheatTemperature = 3f; // Константа: температура перегрева оружия (3 градуса) + private const float OverheatCooldown = 2f; // Константа: время полного остывания оружия (2 секунды) + private float _temperature = 0f; // Текущая температура оружия (начинается с 0) + private float _cooldownTime = 0f; // Время, прошедшее с начала процесса остывания + private bool _overheated; // Флаг перегрева: true - оружие перегрето, false - работает нормально + + //поля для задержки атаки + private float _attackDelay = 1f; // Задержка перед атакой после остановки движения (1 секунда) + private float _timeSinceLastMovement = 0f; // Время, прошедшее с момента последнего движения + private bool _isMoving = false; // Флаг: движется ли юнит в данный момент + + // Список целей, которые находятся вне зоны досягаемости - к ним юнит будет двигаться + List _outOfRangeTargets = new List(); + + // Статический счетчик для присвоения уникальных номеров каждому юниту этого типа + private static int _unitCounter = 0; + // Уникальный номер конкретного экземпляра юнита + private int _unitNumber; + // Константа: максимальное количество целей для "умного" распределения между юнитами + private const int MaxTargetsForSmartSelection = 3; + + // Конструктор класса - вызывается при создании каждого экземпляра юнита + public ThirdUnitBrain() + { + _unitNumber = _unitCounter; // Присваиваем текущий номер из счетчика + _unitCounter++; // Увеличиваем счетчик для следующего юнита + } + + // Метод генерации снарядов при атаке + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) + { + float overheatTemperature = OverheatTemperature; // Копируем значение температуры перегрева в локальную переменную + + if (GetTemperature() >= overheatTemperature) // Если текущая температура >= температуры перегрева (3) + { + return; // Выходим из метода - оружие перегрето, стрелять нельзя + } + + int currentTemp = GetTemperature(); // Получаем текущую температуру (целое число) + int projectileCount = currentTemp + 1; // Количество снарядов = температура + 1 (при 0 -> 1 снаряд, при 1 -> 2, при 2 -> 3) + + for (int i = 0; i < projectileCount; i++) // Цикл для создания указанного количества снарядов + { + var projectile = CreateProjectile(forTarget); // Создаем один снаряд, направленный в цель + AddProjectileToList(projectile, intoList); // Добавляем созданный снаряд в общий список + } + + IncreaseTemperature(); // Увеличиваем температуру оружия после выстрела на 1 + } + + // Метод определения следующего шага движения юнита + public override Vector2Int GetNextStep() + { + // Проверяем, есть ли цели вне зоны досягаемости + if (_outOfRangeTargets.Count > 0) + { + _isMoving = true; // Устанавливаем флаг движения + Vector2Int target = _outOfRangeTargets[0]; // Берем первую цель из списка + Vector2Int currentPos = unit.Pos; // Получаем текущую позицию юнита + return currentPos.CalcNextStepTowards(target); // Вычисляем следующий шаг к цели + } + + // Если целей вне зоны досягаемости нет, возвращаем текущую позицию (стоим на месте) + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Возвращаем текущую позицию (юнит стоит на месте) + } + + // Метод выбора целей для атаки + protected override List SelectTargets() + { + // Очищаем список целей вне зоны досягаемости + _outOfRangeTargets.Clear(); + + // Получаем все доступные цели + List allTargets = new List(GetAllTargets()); + List result = new List(); // Список целей для атаки (результат) + + // БЛОКИРОВКА АТАКИ + // Проверяем: движется ли юнит или прошло меньше 1 секунды после остановки + if (_isMoving || _timeSinceLastMovement < _attackDelay) + { + // Если есть цели - добавляем их в список для движения + if (allTargets.Count > 0) + { + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели в список для движения + } + else + { + // Если нет целей - двигаемся к базе противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; // Определяем ID врага + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; // Получаем позицию базы врага + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу врага в список для движения + } + + // Возвращаем пустой список - АТАКА ЗАБЛОКИРОВАНА + return result; + } + // === КОНЕЦ БЛОКИРОВКИ === + + // Если список целей пуст (никого нет на поле) + if (allTargets.Count == 0) + { + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; // Определяем ID врага + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; // Получаем позицию базы врага + + if (IsTargetInRange(enemyBasePos)) // Если база в зоне досягаемости + { + result.Add(enemyBasePos); // Добавляем базу в список целей для атаки + return result; // Возвращаем результат + } + else // Если база вне зоны досягаемости + { + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу в список для движения + return result; // Возвращаем пустой результат (атака невозможна) + } + } + + // Сортируем все цели по расстоянию до нашей собственной базы (приоритет - ближние) + SortByDistanceToOwnBase(allTargets); + + // Выбираем индекс цели для атаки, распределяя цели между юнитами + int targetIndex; + if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей меньше или равно 3 + { + targetIndex = _unitNumber % allTargets.Count; // Берем остаток от деления номера юнита на количество целей + } + else // Если целей больше 3 + { + targetIndex = _unitNumber % MaxTargetsForSmartSelection; // Берем остаток от деления на 3 + // Если индекс выходит за пределы списка + if (targetIndex >= allTargets.Count) + { + targetIndex = allTargets.Count - 1; // Корректируем на последний валидный индекс + } + } + + // Получаем выбранную цель по вычисленному индексу + Vector2Int selectedTarget = allTargets[targetIndex]; + + // Проверяем, находится ли выбранная цель в радиусе атаки + if (IsTargetInRange(selectedTarget)) + { + // Если цель в зоне досягаемости - добавляем в результат + result.Add(selectedTarget); + } + else + { + // Если цель вне зоны досягаемости - добавляем в список для движения + _outOfRangeTargets.Add(selectedTarget); + // Ищем первую цель в списке, которая находится в зоне досягаемости + foreach (Vector2Int target in allTargets) + { + if (IsTargetInRange(target)) // Если цель в зоне досягаемости + { + result.Add(target); // Добавляем в результат + break; // Выходим из цикла, так как нашли цель + } + } + // Если ни одна цель не в зоне досягаемости + if (result.Count == 0) + { + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели в список для движения + } + } + + return result; // Возвращаем список целей для атаки + } + + // Метод обновления состояния юнита (вызывается каждый кадр) + public override void Update(float deltaTime, float time) + { + base.Update(deltaTime, time); // Вызов метода базового класса для стандартной логики обновления + + // Обновляем таймер без движения + if (!_isMoving) // Если юнит не движется + { + _timeSinceLastMovement += Time.deltaTime; // Увеличиваем время с момента последнего движения + } + else // Если юнит движется + { + _timeSinceLastMovement = 0f; // Сбрасываем таймер + } + + if (_overheated) // Если оружие перегрето + { + _cooldownTime += Time.deltaTime; // Увеличиваем время остывания (используем реальное время игры) + float t = _cooldownTime / (OverheatCooldown / 10); // Вычисляем прогресс остывания (делим на 0.2 сек за шаг) + _temperature = Mathf.Lerp(OverheatTemperature, 0, t); // Плавно уменьшаем температуру от 3 до 0 + if (t >= 1) // Если прогресс достиг 1 (полное остывание) + { + _cooldownTime = 0; // Сбрасываем время остывания + _overheated = false; // Снимаем флаг перегрева + } + } + } + + // Метод получения текущей температуры (возвращает целое число) + private int GetTemperature() + { + if (_overheated) // Если оружие перегрето + { + return (int)OverheatTemperature; // Возвращаем 3 (температура перегрева) + } + else // Иначе + { + return (int)_temperature; // Возвращаем текущую температуру (0, 1 или 2) + } + } + + // Метод увеличения температуры после выстрела + private void IncreaseTemperature() + { + _temperature += 1f; // Увеличиваем температуру на 1 + if (_temperature >= OverheatTemperature) // Если температура достигла или превысила порог перегрева (3) + _overheated = true; // Устанавливаем флаг перегрева + } + } +} \ No newline at end of file From 775e77630464560414235510fb03620024c0f3d6 Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Fri, 7 Aug 2026 19:48:33 +0300 Subject: [PATCH 7/8] HW New Unit/@alekseydzen83 --- .../UnitBrains/Player/ThirdUnitBrain.cs | 70 +++---------------- 1 file changed, 11 insertions(+), 59 deletions(-) diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs index bbcc903ad..632615095 100644 --- a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs @@ -1,21 +1,15 @@ -using Model; // Подключение пространства имен Model, содержащего основные классы модели игры -using Model.Runtime.Projectiles; // Подключение пространства имен для работы со снарядами -using System.Collections.Generic; // Подключение для использования обобщенных коллекций (List, Dictionary и т.д.) -using UnityEngine; // Подключение Unity-библиотеки для работы с Vector2Int и Time.deltaTime -using Utilities; // Подключение вспомогательных утилит (в т.ч. расширения CalcNextStepTowards) +using Model; +using Model.Runtime.Projectiles; +using System.Collections.Generic; +using UnityEngine; +using Utilities; -namespace UnitBrains.Player // Объявление пространства имен для юнит-мозгов игрока +namespace UnitBrains.Player { - public class ThirdUnitBrain : DefaultPlayerUnitBrain // Класс мозга третьего юнита, наследующий базовый класс игрового юнита + public class ThirdUnitBrain : DefaultPlayerUnitBrain { public override string TargetUnitName => "Ironclad Behemoth"; // Переопределение свойства - возвращает имя цели для этого юнита - private const float OverheatTemperature = 3f; // Константа: температура перегрева оружия (3 градуса) - private const float OverheatCooldown = 2f; // Константа: время полного остывания оружия (2 секунды) - private float _temperature = 0f; // Текущая температура оружия (начинается с 0) - private float _cooldownTime = 0f; // Время, прошедшее с начала процесса остывания - private bool _overheated; // Флаг перегрева: true - оружие перегрето, false - работает нормально - //поля для задержки атаки private float _attackDelay = 1f; // Задержка перед атакой после остановки движения (1 секунда) private float _timeSinceLastMovement = 0f; // Время, прошедшее с момента последнего движения @@ -28,7 +22,7 @@ public class ThirdUnitBrain : DefaultPlayerUnitBrain // Класс мозга т private static int _unitCounter = 0; // Уникальный номер конкретного экземпляра юнита private int _unitNumber; - // Константа: максимальное количество целей для "умного" распределения между юнитами + // Константа: максимальное количество целей для распределения между юнитами private const int MaxTargetsForSmartSelection = 3; // Конструктор класса - вызывается при создании каждого экземпляра юнита @@ -41,23 +35,14 @@ public ThirdUnitBrain() // Метод генерации снарядов при атаке protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) { - float overheatTemperature = OverheatTemperature; // Копируем значение температуры перегрева в локальную переменную - - if (GetTemperature() >= overheatTemperature) // Если текущая температура >= температуры перегрева (3) - { - return; // Выходим из метода - оружие перегрето, стрелять нельзя - } - - int currentTemp = GetTemperature(); // Получаем текущую температуру (целое число) - int projectileCount = currentTemp + 1; // Количество снарядов = температура + 1 (при 0 -> 1 снаряд, при 1 -> 2, при 2 -> 3) + // Создаем всегда 3 снаряда (максимальное количество из предыдущей логики) + int projectileCount = 3; for (int i = 0; i < projectileCount; i++) // Цикл для создания указанного количества снарядов { var projectile = CreateProjectile(forTarget); // Создаем один снаряд, направленный в цель AddProjectileToList(projectile, intoList); // Добавляем созданный снаряд в общий список } - - IncreaseTemperature(); // Увеличиваем температуру оружия после выстрела на 1 } // Метод определения следующего шага движения юнита @@ -178,7 +163,7 @@ protected override List SelectTargets() return result; // Возвращаем список целей для атаки } - // Метод обновления состояния юнита (вызывается каждый кадр) + // Метод обновления состояния юнита public override void Update(float deltaTime, float time) { base.Update(deltaTime, time); // Вызов метода базового класса для стандартной логики обновления @@ -192,39 +177,6 @@ public override void Update(float deltaTime, float time) { _timeSinceLastMovement = 0f; // Сбрасываем таймер } - - if (_overheated) // Если оружие перегрето - { - _cooldownTime += Time.deltaTime; // Увеличиваем время остывания (используем реальное время игры) - float t = _cooldownTime / (OverheatCooldown / 10); // Вычисляем прогресс остывания (делим на 0.2 сек за шаг) - _temperature = Mathf.Lerp(OverheatTemperature, 0, t); // Плавно уменьшаем температуру от 3 до 0 - if (t >= 1) // Если прогресс достиг 1 (полное остывание) - { - _cooldownTime = 0; // Сбрасываем время остывания - _overheated = false; // Снимаем флаг перегрева - } - } - } - - // Метод получения текущей температуры (возвращает целое число) - private int GetTemperature() - { - if (_overheated) // Если оружие перегрето - { - return (int)OverheatTemperature; // Возвращаем 3 (температура перегрева) - } - else // Иначе - { - return (int)_temperature; // Возвращаем текущую температуру (0, 1 или 2) - } - } - - // Метод увеличения температуры после выстрела - private void IncreaseTemperature() - { - _temperature += 1f; // Увеличиваем температуру на 1 - if (_temperature >= OverheatTemperature) // Если температура достигла или превысила порог перегрева (3) - _overheated = true; // Устанавливаем флаг перегрева } } } \ No newline at end of file From f41f835b09c400a921d57c40f31ea2f3d1af4953 Mon Sep 17 00:00:00 2001 From: aleksdzen Date: Tue, 11 Aug 2026 12:13:06 +0300 Subject: [PATCH 8/8] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=B5=D0=BA=D1=82=D0=BD?= =?UTF-8?q?=D0=B0=D1=8F=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=E2=84=96?= =?UTF-8?q?1/@alekseydzen83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Scripts/UnitBrains/BaseUnitBrain.cs | 239 +++++++++--- .../UnitBrains/Pathfinding/AStarUnitPath.cs | 354 +++++++++++++++++ .../UnitBrains/Pathfinding/BaseUnitPath.cs | 108 ++++- .../UnitBrains/Pathfinding/DebugPathOutput.cs | 369 +++++++++++++++++- .../UnitBrains/Player/SecondUnitBrain.cs | 254 +++++++----- .../UnitBrains/Player/ThirdUnitBrain.cs | 214 ++++++---- 6 files changed, 1285 insertions(+), 253 deletions(-) create mode 100644 Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs diff --git a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs index 513532dd0..03fb981f6 100644 --- a/Assets/Scripts/UnitBrains/BaseUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/BaseUnitBrain.cs @@ -12,156 +12,273 @@ namespace UnitBrains { public abstract class BaseUnitBrain { - public virtual string TargetUnitName => string.Empty; - public virtual bool IsPlayerUnitBrain => true; - public virtual BaseUnitPath ActivePath => _activePath; - - protected Unit unit { get; private set; } - protected IReadOnlyRuntimeModel runtimeModel => ServiceLocator.Get(); - private BaseUnitPath _activePath = null; - + public virtual string TargetUnitName => string.Empty; // Имя целевого юнита для идентификации + public virtual bool IsPlayerUnitBrain => true; // Флаг, указывающий принадлежность к игроку + + // Свойство ActivePath с публичным геттером и защищенным сеттером + // Позволяет наследникам устанавливать активный путь + public virtual BaseUnitPath ActivePath + { + get => _activePath; // Публичный геттер для чтения пути + protected set => _activePath = value; // Защищенный сеттер для установки из наследников + } + + protected Unit unit { get; private set; } // Ссылка на управляемый юнит + protected IReadOnlyRuntimeModel runtimeModel => ServiceLocator.Get(); // Доступ к модели игры + private BaseUnitPath _activePath = null; // Приватное поле для хранения активного пути + + // Массив смещений для визуального разброса снарядов при атаке private readonly Vector2[] _projectileShifts = new Vector2[] { - new (0f, 0f), - new (0.15f, 0f), - new (0f, 0.15f), - new (0.15f, 0.15f), - new (0.15f, -0.15f), - new (-0.15f, 0.15f), - new (-0.15f, -0.15f), + new (0f, 0f), // Центральный снаряд + new (0.15f, 0f), // Смещение вправо + new (0f, 0.15f), // Смещение вверх + new (0.15f, 0.15f), // Смещение вправо-вверх + new (0.15f, -0.15f), // Смещение вправо-вниз + new (-0.15f, 0.15f), // Смещение влево-вверх + new (-0.15f, -0.15f), // Смещение влево-вниз }; + // Виртуальный метод получения следующего шага движения public virtual Vector2Int GetNextStep() { + // Если есть цели в зоне атаки, стоим на месте и атакуем if (HasTargetsInRange()) - return unit.Pos; + return unit.Pos; // Возвращаем текущую позицию (не двигаемся) + // Получаем позицию вражеской базы как цель по умолчанию var target = runtimeModel.RoMap.Bases[ IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]; - _activePath = new DummyUnitPath(runtimeModel, unit.Pos, target); - return _activePath.GetNextStepFrom(unit.Pos); + // Создаем A* путь от текущей позиции до базы врага + _activePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = _activePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (не больше 1 клетки в любом направлении) + Vector2Int delta = nextStep - unit.Pos; + + // Если шаг невалидный (слишком большой), используем прямое движение к цели + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogWarning($"A* returned invalid step {nextStep} from {unit.Pos}, using fallback movement"); + nextStep = unit.Pos.CalcNextStepTowards(target); // Вычисляем прямой шаг к цели + } + + // Дополнительная проверка: можно ли пройти в выбранную клетку + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"Cannot walk to {nextStep} from {unit.Pos}, finding alternative"); + nextStep = FindNearbyWalkableCell(unit.Pos); // Ищем альтернативную проходимую клетку + } + + // Финальная проверка валидности шага + delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogError($"Unable to find valid step from {unit.Pos}, staying in place"); + return unit.Pos; // Остаемся на месте если ничего не помогло + } + + return nextStep; // Возвращаем валидный следующий шаг } + // Метод получения снарядов для атаки public List GetProjectiles() { - List result = new (); + List result = new(); // Создаем список для снарядов + + // Для каждой выбранной цели генерируем снаряды foreach (var target in SelectTargets()) { - GenerateProjectiles(target, result); + GenerateProjectiles(target, result); // Генерируем снаряды для цели } + // Добавляем визуальные смещения снарядам для реалистичности стрельбы for (int i = 0; i < result.Count; i++) { - var proj = result[i]; - proj.AddStartShift(_projectileShifts[i % _projectileShifts.Length]); + var proj = result[i]; // Получаем снаряд + proj.AddStartShift(_projectileShifts[i % _projectileShifts.Length]); // Добавляем смещение } - return result; + return result; // Возвращаем список готовых снарядов } + // Метод установки ссылки на управляемый юнит public void SetUnit(Unit unit) { - this.unit = unit; + this.unit = unit; // Сохраняем ссылку на юнит } + // Виртуальный метод обновления (вызывается каждый кадр) public virtual void Update(float deltaTime, float time) { + // Базовая реализация пустая, переопределяется в наследниках при необходимости } + // Виртуальный метод генерации снарядов для атаки protected virtual void GenerateProjectiles(Vector2Int forTarget, List intoList) { + // Создаем один снаряд и добавляем его в список AddProjectileToList(CreateProjectile(forTarget), intoList); } + // Виртуальный метод выбора целей для атаки protected virtual List SelectTargets() { - var result = GetReachableTargets(); + var result = GetReachableTargets(); // Получаем все цели в зоне досягаемости + + // Оставляем только одну цель (первую в списке) по умолчанию while (result.Count > 1) - result.RemoveAt(result.Count - 1); - return result; + result.RemoveAt(result.Count - 1); // Удаляем лишние цели + + return result; // Возвращаем список с одной целью } - + + // Метод создания снаряда указанного типа protected BaseProjectile CreateProjectile(Vector2Int target) => BaseProjectile.Create(unit.Config.ProjectileType, unit, unit.Pos, target, unit.Config.Damage); - + + // Метод добавления снаряда в список protected void AddProjectileToList(BaseProjectile projectile, List list) => - list.Add(projectile); + list.Add(projectile); // Добавляем снаряд в переданный список + // Метод получения юнита на указанной позиции protected IReadOnlyUnit GetUnitAt(Vector2Int pos) => - runtimeModel.RoUnits.FirstOrDefault(u => u.Pos == pos); + runtimeModel.RoUnits.FirstOrDefault(u => u.Pos == pos); // Ищем юнита по позиции + // Метод получения юнитов в указанном радиусе protected List GetUnitsInRadius(float radius, bool enemies) { - var units = new List(); - var pos = unit.Pos; - var distanceSqr = radius * radius; - + var units = new List(); // Создаем пустой список для результатов + var pos = unit.Pos; // Текущая позиция нашего юнита + var distanceSqr = radius * radius; // Квадрат радиуса для оптимизации вычислений + + // Проверяем всех юнитов в игре foreach (var otherUnit in runtimeModel.RoUnits) { - if (otherUnit == unit) + if (otherUnit == unit) // Пропускаем самого себя continue; + // Проверяем соответствие фильтру (враги или союзники) if (enemies != (otherUnit.Config.IsPlayerUnit == unit.Config.IsPlayerUnit)) continue; - var otherPos = otherUnit.Pos; - var diff = otherPos - pos; - var distance = diff.sqrMagnitude; - if (distance <= distanceSqr) - units.Add(otherUnit); + var otherPos = otherUnit.Pos; // Позиция проверяемого юнита + var diff = otherPos - pos; // Разница позиций + var distance = diff.sqrMagnitude; // Квадрат расстояния между юнитами + + if (distance <= distanceSqr) // Если юнит в заданном радиусе + units.Add(otherUnit); // Добавляем его в список } - return units; + return units; // Возвращаем список найденных юнитов } + // Метод проверки наличия целей в зоне атаки protected bool HasTargetsInRange() { - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; + var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; // Квадрат радиуса атаки + + // Проверяем все возможные цели foreach (var possibleTarget in GetAllTargets()) { - var diff = possibleTarget - unit.Pos; - if (diff.sqrMagnitude < attackRangeSqr) - return true; + var diff = possibleTarget - unit.Pos; // Разница позиций с целью + if (diff.sqrMagnitude < attackRangeSqr) // Если цель в радиусе атаки + return true; // Цель найдена в зоне досягаемости } - return false; + return false; // Целей в зоне атаки нет } + // Метод получения всех вражеских юнитов protected IEnumerable GetAllEnemyUnits() { return runtimeModel.RoUnits - .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain); + .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain); // Фильтруем юниты по принадлежности } + // Метод получения всех возможных целей (вражеские юниты + база) protected IEnumerable GetAllTargets() { return runtimeModel.RoUnits - .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain) - .Select(u => u.Pos) - .Append(runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]); + .Where(u => u.Config.IsPlayerUnit != IsPlayerUnitBrain) // Все вражеские юниты + .Select(u => u.Pos) // Получаем их позиции + .Append(runtimeModel.RoMap.Bases[IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId]); // Добавляем вражескую базу } + // Метод проверки, находится ли цель в зоне атаки protected bool IsTargetInRange(Vector2Int targetPos) { - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; - var diff = targetPos - unit.Pos; - return diff.sqrMagnitude <= attackRangeSqr; + var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; // Квадрат радиуса атаки + var diff = targetPos - unit.Pos; // Разница позиций + return diff.sqrMagnitude <= attackRangeSqr; // Возвращаем true если цель в радиусе } + // Метод получения всех целей в зоне досягаемости атаки protected List GetReachableTargets() { - var result = new List(); - var attackRangeSqr = unit.Config.AttackRange * unit.Config.AttackRange; + var result = new List(); // Создаем пустой список результатов + + // Проверяем все возможные цели foreach (var possibleTarget in GetAllTargets()) { - if (!IsTargetInRange(possibleTarget)) - continue; - - result.Add(possibleTarget); + if (!IsTargetInRange(possibleTarget)) // Если цель не в зоне атаки + continue; // Пропускаем её + + result.Add(possibleTarget); // Добавляем доступную цель в список + } + + return result; // Возвращаем список доступных целей + } + + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) + { + // Проверяем, что клетка находится в пределах игровой карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; // Клетка за пределами карты + + // Проверяем, что клетка не занята препятствием (стеной) + if (runtimeModel.RoMap[pos]) + return false; // Клетка заблокирована + + // Проверяем, что на клетке нет другого юнита + if (runtimeModel.RoUnits.Any(u => u.Pos == pos)) + return false; // Клетка занята юнитом + + return true; // Клетка проходима + } + + // Поиск ближайшей проходимой клетки рядом с текущей позицией + private Vector2Int FindNearbyWalkableCell(Vector2Int currentPos) + { + // Определяем приоритетные направления для поиска + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх + Vector2Int.right, // Вправо + Vector2Int.down, // Вниз + Vector2Int.left // Влево + }; + + // Проверяем каждое направление + foreach (Vector2Int direction in directions) + { + Vector2Int neighborPos = currentPos + direction; // Вычисляем позицию соседа + + // Если соседняя клетка проходима, возвращаем её + if (IsCellWalkable(neighborPos)) + { + return neighborPos; // Найдена проходимая клетка + } } - return result; + // Если все соседние клетки заняты, остаемся на месте + Debug.LogWarning($"No walkable cells near {currentPos}, staying in place"); + return currentPos; // Возвращаем текущую позицию } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs new file mode 100644 index 000000000..4b58f7e83 --- /dev/null +++ b/Assets/Scripts/UnitBrains/Pathfinding/AStarUnitPath.cs @@ -0,0 +1,354 @@ +using System.Collections.Generic; +using System.Linq; +using Model; +using UnityEngine; + +namespace UnitBrains.Pathfinding +{ + public class AStarUnitPath : BaseUnitPath + { + // Внутренний класс для хранения информации об узле в алгоритме A* + private class PathNode + { + public Vector2Int Position { get; set; } // Позиция узла на карте + public float GCost { get; set; } // Стоимость пути от начала до этого узла + public float HCost { get; set; } // Эвристическая оценка расстояния до цели + public float FCost => GCost + HCost; // Общая стоимость узла (F = G + H) + public PathNode Parent { get; set; } // Родительский узел для восстановления пути + + // Конструктор узла - инициализирует начальные значения + public PathNode(Vector2Int position) + { + Position = position; // Устанавливаем позицию узла + GCost = float.MaxValue; // Начальная стоимость пути - максимальная + HCost = 0; // Начальная эвристическая оценка - 0 + Parent = null; // Изначально родитель отсутствует + } + } + + // Конструктор класса - вызывает конструктор базового класса BaseUnitPath + public AStarUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) + : base(runtimeModel, startPoint, endPoint) + { + // Все параметры передаются в базовый конструктор + } + + // Основной метод расчета пути с использованием алгоритма A* + protected override void Calculate() + { + // Открытый список - содержит узлы, которые предстоит проверить + List openList = new List(); + // Закрытый список - содержит уже проверенные узлы (хэш-сет для быстрого поиска) + HashSet closedList = new HashSet(); + + // Создаем начальный узел (позиция юнита) + PathNode startNode = new PathNode(startPoint); + startNode.GCost = 0; // Стоимость пути от начала до начала равна 0 + startNode.HCost = CalculateHeuristic(startPoint, endPoint); // Вычисляем эвристику до цели + + // Добавляем начальный узел в открытый список для обработки + openList.Add(startNode); + + // Основной цикл алгоритма A* - продолжается пока есть узлы для проверки + while (openList.Count > 0) + { + // Находим узел с наименьшей общей стоимостью F = G + H + PathNode currentNode = openList.OrderBy(node => node.FCost).First(); + + // Проверяем, достигли ли мы конечной точки + if (currentNode.Position == endPoint) + { + // Восстанавливаем полный путь от конечного узла к начальному + path = ReconstructPath(currentNode); + return; // Завершаем вычисление пути + } + + // Перемещаем текущий узел из открытого списка в закрытый + openList.Remove(currentNode); + closedList.Add(currentNode.Position); + + // Проверяем всех соседей текущего узла (4 направления) + foreach (Vector2Int neighborPos in GetNeighbors(currentNode.Position)) + { + // Пропускаем узлы, которые уже были проверены + if (closedList.Contains(neighborPos)) + continue; + + // Пропускаем непроходимые клетки (стены или занятые юнитами) + if (!IsWalkable(neighborPos)) + continue; + + // Вычисляем предварительную стоимость пути до соседа + // Текущая стоимость + 1 (стоимость одного шага) + float tentativeGCost = currentNode.GCost + 1; + + // Ищем соседний узел в открытом списке + PathNode neighborNode = openList.FirstOrDefault(node => node.Position == neighborPos); + + if (neighborNode == null) // Если сосед еще не в открытом списке + { + // Создаем новый узел для соседа + neighborNode = new PathNode(neighborPos); + neighborNode.GCost = tentativeGCost; // Устанавливаем стоимость пути + neighborNode.HCost = CalculateHeuristic(neighborPos, endPoint); // Вычисляем эвристику + neighborNode.Parent = currentNode; // Устанавливаем родителя для восстановления пути + openList.Add(neighborNode); // Добавляем соседа в открытый список + } + else if (tentativeGCost < neighborNode.GCost) // Если нашли более короткий путь к соседу + { + // Обновляем стоимость и родителя (более оптимальный маршрут) + neighborNode.GCost = tentativeGCost; + neighborNode.Parent = currentNode; + } + } + } + + // Если цикл завершился, а путь не найден - выводим предупреждение + Debug.LogWarning($"A* path not found from {startPoint} to {endPoint}, using direct path as fallback"); + // Создаем прямой путь как запасной вариант (от начала к концу) + path = new Vector2Int[] { startPoint, endPoint }; + } + + // Переопределенный метод получения следующего шага с проверкой валидности + public new Vector2Int GetNextStepFrom(Vector2Int unitPos) + { + // Получаем все точки вычисленного пути в виде списка + List pathPoints = GetPath().ToList(); + + // Если путь пустой, возвращаем текущую позицию (некуда идти) + if (pathPoints.Count == 0) + { + Debug.LogWarning($"A* path is empty for unit at {unitPos}"); + return unitPos; + } + + // Если путь состоит из одной точки, значит мы уже на месте + if (pathPoints.Count == 1) + { + return pathPoints[0]; + } + + // Ищем индекс текущей позиции юнита в вычисленном пути + int currentIndex = pathPoints.IndexOf(unitPos); + + // Если текущая позиция не найдена в пути (юнит отклонился от маршрута) + if (currentIndex == -1) + { + // Ищем ближайшую точку пути к текущей позиции юнита + int closestIndex = 0; // Индекс ближайшей найденной точки + float closestDistance = float.MaxValue; // Минимальное найденное расстояние + + // Проходим по всем точкам пути для поиска ближайшей + for (int i = 0; i < pathPoints.Count; i++) + { + // Вычисляем евклидово расстояние от текущей позиции до точки пути + float distance = Vector2.Distance( + new Vector2(unitPos.x, unitPos.y), + new Vector2(pathPoints[i].x, pathPoints[i].y) + ); + + // Если нашли более близкую точку, обновляем минимум + if (distance < closestDistance) + { + closestDistance = distance; // Сохраняем новое минимальное расстояние + closestIndex = i; // Запоминаем индекс ближайшей точки + } + } + + // Если мы не в конце пути, возвращаем следующую точку после ближайшей + if (closestIndex < pathPoints.Count - 1) + { + Vector2Int nextPoint = pathPoints[closestIndex + 1]; + // Проверяем и возвращаем валидный шаг к следующей точке + return ValidateAndGetStep(unitPos, nextPoint); + } + else + { + // Если мы в конце пути, возвращаем последнюю точку (финальная позиция) + return ValidateAndGetStep(unitPos, pathPoints[pathPoints.Count - 1]); + } + } + + // Если мы уже в конце пути, возвращаем конечную точку + if (currentIndex >= pathPoints.Count - 1) + { + return pathPoints[pathPoints.Count - 1]; + } + + // Получаем следующую точку пути после текущей позиции + Vector2Int nextPathPoint = pathPoints[currentIndex + 1]; + + // Проверяем валидность шага и возвращаем его + return ValidateAndGetStep(unitPos, nextPathPoint); + } + + // Вспомогательный метод для проверки и получения валидного шага + private Vector2Int ValidateAndGetStep(Vector2Int currentPos, Vector2Int targetPos) + { + // Вычисляем разницу между текущей позицией и желаемой целевой позицией + Vector2Int delta = targetPos - currentPos; + + // Проверяем, что шаг не больше 1 клетки по любой оси + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + Debug.LogWarning($"A* next step {targetPos} is too far from {currentPos}, calculating step towards"); + + // Вычисляем направление к цели с ограничением в 1 шаг + Vector2Int step = new Vector2Int( + Mathf.Clamp(delta.x, -1, 1), // Ограничиваем движение по X одним шагом + Mathf.Clamp(delta.y, -1, 1) // Ограничиваем движение по Y одним шагом + ); + + // Вычисляем следующую позицию как текущая + ограниченный шаг + Vector2Int nextStep = currentPos + step; + + // Проверяем, что вычисленная позиция проходима + if (IsWalkable(nextStep)) + { + return nextStep; // Возвращаем валидный шаг + } + else + { + // Если прямой путь заблокирован, ищем альтернативный обходной шаг + return FindAlternativeStep(currentPos, targetPos); + } + } + + // Проверяем, что целевая позиция проходима (нет стены или юнита) + if (!IsWalkable(targetPos)) + { + Debug.LogWarning($"A* target step {targetPos} is not walkable, finding alternative"); + // Ищем альтернативный путь + return FindAlternativeStep(currentPos, targetPos); + } + + // Возвращаем валидный шаг к целевой позиции + return targetPos; + } + + // Поиск альтернативного шага, если прямой путь заблокирован + private Vector2Int FindAlternativeStep(Vector2Int currentPos, Vector2Int targetPos) + { + // Определяем все возможные направления движения (включая диагональные) + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх + Vector2Int.right, // Вправо + Vector2Int.down, // Вниз + Vector2Int.left, // Влево + new Vector2Int(1, 1), // Диагональ право-вверх + new Vector2Int(-1, 1), // Диагональ лево-вверх + new Vector2Int(1, -1), // Диагональ право-вниз + new Vector2Int(-1, -1) // Диагональ лево-вниз + }; + + // Сортируем направления по близости к целевой точке направление, которое максимально приближает к цели + var sortedDirections = directions + .OrderBy(d => Vector2.Distance( + new Vector2(currentPos.x + d.x, currentPos.y + d.y), + new Vector2(targetPos.x, targetPos.y) + )) + .ToArray(); + + // Проверяем каждое направление в порядке приоритета + foreach (Vector2Int direction in sortedDirections) + { + Vector2Int alternativeStep = currentPos + direction; + + // Проверяем, что клетка находится в пределах игрового поля + if (alternativeStep.x >= 0 && alternativeStep.x < runtimeModel.RoMap.Width && + alternativeStep.y >= 0 && alternativeStep.y < runtimeModel.RoMap.Height) + { + // Проверяем, что клетка проходима (нет стены и юнита) + if (IsWalkable(alternativeStep)) + { + Debug.Log($"Found alternative step: {alternativeStep} from {currentPos}"); + return alternativeStep; // Возвращаем найденный альтернативный шаг + } + } + } + + // Если ни одно направление не подходит, остаемся на текущей позиции + Debug.LogWarning($"No alternative step found from {currentPos}, staying in place"); + return currentPos; + } + + // Получение списка соседних клеток для указанной позиции + private List GetNeighbors(Vector2Int position) + { + // Создаем пустой список для хранения соседей + List neighbors = new List(); + + // Определяем 4 основных направления движения (без диагоналей для простоты) + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Вверх (0, 1) + Vector2Int.down, // Вниз (0, -1) + Vector2Int.left, // Влево (-1, 0) + Vector2Int.right // Вправо (1, 0) + }; + + // Проверяем каждое направление + foreach (Vector2Int direction in directions) + { + Vector2Int neighborPos = position + direction; // Вычисляем позицию соседа + + // Проверяем, что сосед находится в пределах игровой карты + if (neighborPos.x >= 0 && neighborPos.x < runtimeModel.RoMap.Width && + neighborPos.y >= 0 && neighborPos.y < runtimeModel.RoMap.Height) + { + neighbors.Add(neighborPos); // Добавляем валидного соседа в список + } + } + + return neighbors; // Возвращаем список соседних клеток + } + + // Проверка, можно ли пройти через указанную клетку + private bool IsWalkable(Vector2Int position) + { + // Проверяем, что клетка не занята препятствием (стеной) + if (runtimeModel.RoMap[position]) + return false; // Клетка заблокирована стеной + + // Проверяем, что на клетке нет другого юнита + // Исключаем начальную и конечную точки пути + if (position != endPoint && position != startPoint) + { + // Проверяем всех юнитов на карте + if (runtimeModel.RoUnits.Any(u => u.Pos == position)) + return false; // Клетка занята другим юнитом + } + + return true; // Клетка проходима + } + + // Эвристическая функция для оценки расстояния до цели + private float CalculateHeuristic(Vector2Int from, Vector2Int to) + { + + return Mathf.Abs(from.x - to.x) + Mathf.Abs(from.y - to.y); + } + + // Восстановление полного пути от конечного узла к начальному + private Vector2Int[] ReconstructPath(PathNode endNode) + { + // Создаем список для хранения точек пути + List pathList = new List(); + PathNode currentNode = endNode; // Начинаем с конечного узла + + // Движемся от конечного узла к начальному по родительским ссылкам + while (currentNode != null) + { + pathList.Add(currentNode.Position); // Добавляем позицию текущего узла + currentNode = currentNode.Parent; // Переходим к родительскому узлу + } + + // Переворачиваем список, чтобы путь шел от начала к концу + pathList.Reverse(); + + // Преобразуем список в массив и возвращаем + return pathList.ToArray(); + } + } +} \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs b/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs index 80b18be07..d01c69893 100644 --- a/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs +++ b/Assets/Scripts/UnitBrains/Pathfinding/BaseUnitPath.cs @@ -6,44 +6,130 @@ namespace UnitBrains.Pathfinding { public abstract class BaseUnitPath { + // Свойство для получения начальной точки пути (только для чтения) public Vector2Int StartPoint => startPoint; + // Свойство для получения конечной точки пути (только для чтения) public Vector2Int EndPoint => endPoint; - - protected readonly IReadOnlyRuntimeModel runtimeModel; - protected readonly Vector2Int startPoint; - protected readonly Vector2Int endPoint; - protected Vector2Int[] path = null; + // Защищенные поля для хранения данных + protected readonly IReadOnlyRuntimeModel runtimeModel; // Модель времени выполнения для доступа к карте и юнитам + protected readonly Vector2Int startPoint; // Начальная точка пути + protected readonly Vector2Int endPoint; // Конечная точка пути + protected Vector2Int[] path = null; // Массив точек пути (null, если путь еще не вычислен) + + // Абстрактный метод для вычисления пути + // Должен быть реализован в наследниках (AStarUnitPath, DummyUnitPath и т.д.) protected abstract void Calculate(); - + + // Метод для получения всех точек пути + // Если путь еще не вычислен, вызывает Calculate() public IEnumerable GetPath() { + // Проверяем, был ли уже вычислен путь if (path == null) + { + // Если нет, вычисляем его Calculate(); - + } + + // Возвращаем вычисленный путь return path; } + // Метод для получения следующего шага от указанной позиции юнита + // Используется для пошагового движения по пути public Vector2Int GetNextStepFrom(Vector2Int unitPos) { - var found = false; - foreach (var cell in GetPath()) + // Флаг для отслеживания, нашли ли мы текущую позицию юнита в пути + bool found = false; + + // Проходим по всем точкам пути + foreach (Vector2Int cell in GetPath()) { + // Если мы уже нашли текущую позицию, возвращаем следующую точку if (found) + { return cell; + } - found = cell == unitPos; + // Проверяем, является ли текущая точка пути позицией юнита + if (cell == unitPos) + { + found = true; + } } - Debug.LogError($"Unit {unitPos} is not on the path"); + // Если позиция юнита не найдена в пути (ошибка) + Debug.LogError($"Unit at position {unitPos} is not on the calculated path from {startPoint} to {endPoint}"); + + // Возвращаем текущую позицию как запасной вариант return unitPos; } + // Конструктор базового класса + // Принимает модель времени выполнения, начальную и конечную точки protected BaseUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) { + // Проверяем, что модель не null + if (runtimeModel == null) + { + Debug.LogError("RuntimeModel cannot be null in BaseUnitPath constructor"); + } + + // Сохраняем переданные параметры this.runtimeModel = runtimeModel; this.startPoint = startPoint; this.endPoint = endPoint; + + // Путь изначально не вычислен + this.path = null; + } + + // Вспомогательный метод для проверки, вычислен ли уже путь + protected bool IsPathCalculated() + { + return path != null; + } + + // Вспомогательный метод для получения длины пути + public int GetPathLength() + { + // Получаем путь (вычисляем, если нужно) + Vector2Int[] calculatedPath = GetPath() as Vector2Int[]; + + // Возвращаем длину пути или 0, если путь не существует + return calculatedPath?.Length ?? 0; + } + + // Вспомогательный метод для проверки, содержит ли путь указанную точку + public bool ContainsPoint(Vector2Int point) + { + // Получаем путь + IEnumerable pathPoints = GetPath(); + + // Проверяем каждую точку пути + foreach (Vector2Int pathPoint in pathPoints) + { + if (pathPoint == point) + { + return true; + } + } + + return false; + } + + // Переопределение ToString() для удобной отладки + public override string ToString() + { + if (path == null) + { + return $"Path from {startPoint} to {endPoint} [NOT CALCULATED]"; + } + else + { + return $"Path from {startPoint} to {endPoint} [{path.Length} steps]"; + } } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs index 3ffc5123b..db096bbcc 100644 --- a/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs +++ b/Assets/Scripts/UnitBrains/Pathfinding/DebugPathOutput.cs @@ -7,47 +7,374 @@ namespace UnitBrains.Pathfinding { public class DebugPathOutput : MonoBehaviour { - [SerializeField] private GameObject cellHighlightPrefab; - [SerializeField] private int maxHighlights = 5; + [SerializeField] private GameObject cellHighlightPrefab; // Префаб для подсветки клетки + [SerializeField] private int maxHighlights = 5; // Максимальное количество одновременно подсвеченных клеток + [SerializeField] private float highlightDelay = 0.1f; // Задержка между подсветкой клеток (в секундах) + [SerializeField] private float fadeOutDelay = 1f; // Задержка перед удалением всех подсветок (в секундах) + [SerializeField] private Color startColor = Color.green; // Цвет начальной точки пути + [SerializeField] private Color endColor = Color.red; // Цвет конечной точки пути + [SerializeField] private Color pathColor = Color.yellow; // Цвет промежуточных точек пути - public BaseUnitPath Path { get; private set; } - private readonly List allHighlights = new(); - private Coroutine highlightCoroutine; + public BaseUnitPath Path { get; private set; } // Текущий отображаемый путь + private readonly List allHighlights = new List(); // Список всех активных подсветок + private Coroutine highlightCoroutine; // Ссылка на текущую корутину подсветки + private bool isShuttingDown = false; // Флаг завершения работы для предотвращения ошибок + // Метод для запуска подсветки пути public void HighlightPath(BaseUnitPath path) { - Path = path; - while (allHighlights.Count > 0) + // Проверяем, что объект активен и не завершает работу + if (!this.isActiveAndEnabled || isShuttingDown) + return; + + Path = path; // Сохраняем ссылку на путь + + // Очищаем все предыдущие подсветки перед созданием новых + StopAllCoroutines(); // Останавливаем все текущие корутины + ClearAllHighlightsImmediate(); // Мгновенно очищаем все подсветки + + // Запускаем новую корутину для визуализации пути + if (path != null && cellHighlightPrefab != null) + { + highlightCoroutine = StartCoroutine(HighlightCoroutine(path)); + } + } + + // Корутина для пошаговой подсветки пути + private IEnumerator HighlightCoroutine(BaseUnitPath path) + { + // Проверяем, что объект все еще активен + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Получаем все точки пути в виде списка для удобной работы + List pathPoints = new List(path.GetPath()); + + // Проверяем, что путь содержит хотя бы одну точку + if (pathPoints.Count == 0) + { + Debug.LogWarning("DebugPathOutput: Path is empty, nothing to highlight"); + yield break; // Выходим из корутины + } + + int totalPoints = pathPoints.Count; // Общее количество точек в пути + int highlightedCount = 0; // Счетчик подсвеченных клеток + + // Проходим по каждой точке пути последовательно + for (int i = 0; i < totalPoints; i++) + { + // Проверяем, что объект все еще активен + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + Vector2Int currentPoint = pathPoints[i]; // Текущая точка пути + + // Проверяем, не превышен ли лимит одновременно подсвеченных клеток + if (highlightedCount >= maxHighlights) + { + // Удаляем самую старую подсветку (первую в списке) + if (allHighlights.Count > 0 && allHighlights[0] != null) + { + // Запускаем корутину плавного исчезновения для старой подсветки + StartCoroutine(FadeOutHighlight(allHighlights[0], 0.3f)); + + // Ждем небольшую задержку + yield return new WaitForSeconds(0.05f); + + // Удаляем объект подсветки если он еще существует + if (allHighlights.Count > 0) + { + DestroyHighlightSafe(0); + } + } + } + + // Проверяем снова перед созданием новой подсветки + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Создаем новую подсветку для текущей точки + GameObject highlight = CreateHighlight(currentPoint); + + if (highlight != null) + { + // Устанавливаем цвет в зависимости от позиции в пути + SetHighlightColor(highlight, i, totalPoints); + + // Добавляем эффект появления (плавное увеличение) + StartCoroutine(ScaleUpHighlight(highlight, 0.3f)); + } + + highlightedCount++; // Увеличиваем счетчик подсвеченных клеток + + // Ждем указанную задержку перед подсветкой следующей клетки + yield return new WaitForSeconds(highlightDelay); + } + + // Проверяем перед финальной задержкой + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // После подсветки всего пути, ждем дополнительное время + yield return new WaitForSeconds(fadeOutDelay); + + // Проверяем перед удалением + if (!this.isActiveAndEnabled || isShuttingDown) + yield break; + + // Плавно убираем все оставшиеся подсветки + yield return StartCoroutine(FadeOutAllHighlights(1f)); + + // Очищаем все подсветки после завершения + ClearAllHighlightsImmediate(); + + highlightCoroutine = null; // Очищаем ссылку на корутину + } + + // Создание подсветки для указанной клетки + private GameObject CreateHighlight(Vector2Int atCell) + { + // Проверяем, что объект активен и префаб существует + if (!this.isActiveAndEnabled || isShuttingDown || cellHighlightPrefab == null) + return null; + + try + { + // Преобразуем координаты сетки в мировые координаты + Vector3 pos = Gameplay3dView.ToWorldPosition(atCell, 1f); + + // Создаем объект подсветки из префаба + GameObject highlight = Instantiate(cellHighlightPrefab, pos, Quaternion.identity); + + if (highlight != null) + { + // Устанавливаем родительский объект для организации иерархии + highlight.transform.SetParent(transform); + + // Добавляем созданную подсветку в список для отслеживания + allHighlights.Add(highlight); + } + + return highlight; // Возвращаем созданный объект + } + catch (System.Exception e) + { + Debug.LogError($"Error creating highlight at {atCell}: {e.Message}"); + return null; + } + } + + // Установка цвета подсветки в зависимости от позиции в пути + private void SetHighlightColor(GameObject highlight, int index, int totalPoints) + { + // Проверяем существование объекта + if (highlight == null) + return; + + // Получаем компонент Renderer для изменения цвета + Renderer renderer = highlight.GetComponent(); + + if (renderer != null && renderer.material != null) + { + Color targetColor; // Цвет для текущей точки + + if (index == 0) // Если это начальная точка + { + targetColor = startColor; // Зеленый цвет + } + else if (index == totalPoints - 1) // Если это конечная точка + { + targetColor = endColor; // Красный цвет + } + else // Промежуточные точки + { + // Градиент от зеленого к красному + float t = (float)index / (totalPoints - 1); // Прогресс от 0 до 1 + targetColor = Color.Lerp(startColor, endColor, t); // Интерполяция цвета + } + + // Устанавливаем цвет материала + renderer.material.color = targetColor; + } + } + + // Корутина для плавного появления подсветки (увеличение масштаба) + private IEnumerator ScaleUpHighlight(GameObject highlight, float duration) + { + // Проверяем существование объекта в самом начале + if (highlight == null) + yield break; + + // Сохраняем исходный масштаб + Vector3 originalScale = highlight.transform.localScale; + + // Устанавливаем начальный нулевой размер + highlight.transform.localScale = Vector3.zero; + + float elapsed = 0f; // Прошедшее время + + // Плавно увеличиваем масштаб от 0 до исходного + while (elapsed < duration) + { + // ИСПРАВЛЕНИЕ: Проверяем объект на null в каждой итерации + if (highlight == null || !highlight.activeInHierarchy) + yield break; // Объект уничтожен или деактивирован + + elapsed += Time.deltaTime; // Увеличиваем прошедшее время + float t = elapsed / duration; // Прогресс анимации (0-1) + + // Используем easing функцию для более красивого эффекта + float smoothT = Mathf.SmoothStep(0f, 1f, t); + + // Устанавливаем промежуточный масштаб + highlight.transform.localScale = Vector3.Lerp(Vector3.zero, originalScale, smoothT); + + yield return null; // Ждем следующий кадр + } + + // Финальная проверка и установка точного масштаба + if (highlight != null && highlight.activeInHierarchy) + { + highlight.transform.localScale = originalScale; + } + } + + // Корутина для плавного исчезновения одной подсветки + private IEnumerator FadeOutHighlight(GameObject highlight, float duration) + { + // Проверяем существование объекта + if (highlight == null) + yield break; + + Renderer renderer = highlight.GetComponent(); + + // Проверяем наличие рендерера и материала + if (renderer == null || renderer.material == null) + yield break; + + Color originalColor = renderer.material.color; // Сохраняем исходный цвет + float elapsed = 0f; // Прошедшее время + + // Плавно уменьшаем прозрачность + while (elapsed < duration) + { + // ИСПРАВЛЕНИЕ: Проверяем объект и рендерер в каждой итерации + if (highlight == null || !highlight.activeInHierarchy || renderer == null || renderer.material == null) + yield break; // Объект или компоненты уничтожены + + elapsed += Time.deltaTime; // Увеличиваем время + float t = elapsed / duration; // Прогресс (0-1) + + // Уменьшаем альфа-канал для эффекта исчезновения + Color fadedColor = originalColor; + fadedColor.a = Mathf.Lerp(1f, 0f, t); // Плавное уменьшение прозрачности + renderer.material.color = fadedColor; + + yield return null; // Ждем следующий кадр + } + } + + // Корутина для плавного исчезновения всех подсветок + private IEnumerator FadeOutAllHighlights(float duration) + { + // Проверяем, есть ли подсветки + if (allHighlights.Count == 0) + yield break; + + // Создаем копию списка для безопасной итерации + List highlightsToFade = new List(); + foreach (GameObject highlight in allHighlights) + { + if (highlight != null) + { + highlightsToFade.Add(highlight); + } + } + + // Запускаем исчезновение для всех подсветок одновременно + foreach (GameObject highlight in highlightsToFade) { - DestroyHighlight(0); + if (highlight != null && this.isActiveAndEnabled) + { + StartCoroutine(FadeOutHighlight(highlight, duration)); + } } - + + // Ждем указанную длительность + yield return new WaitForSeconds(duration); + } + + // Безопасное удаление подсветки по индексу + private void DestroyHighlightSafe(int index) + { + // Проверяем, что индекс в допустимых пределах + if (index >= 0 && index < allHighlights.Count) + { + GameObject highlight = allHighlights[index]; // Получаем объект + + if (highlight != null) + { + // Проверяем, что объект все еще существует перед уничтожением + if (highlight.activeInHierarchy || highlight != null) + { + Destroy(highlight); // Уничтожаем игровой объект + } + } + + allHighlights.RemoveAt(index); // Удаляем из списка в любом случае + } + } + + // Мгновенная очистка всех подсветок без анимации + private void ClearAllHighlightsImmediate() + { + // Удаляем все подсветки с конца списка для безопасности + for (int i = allHighlights.Count - 1; i >= 0; i--) + { + if (allHighlights[i] != null) + { + Destroy(allHighlights[i]); // Уничтожаем объект + } + } + + allHighlights.Clear(); // Очищаем список + } + + // Метод для принудительной очистки всех подсветок + public void ClearAllHighlights() + { + // Останавливаем текущую корутину, если она запущена if (highlightCoroutine != null) { StopCoroutine(highlightCoroutine); + highlightCoroutine = null; } - highlightCoroutine = StartCoroutine(HighlightCoroutine(path)); + // Останавливаем все остальные корутины + StopAllCoroutines(); + + // Очищаем подсветки + ClearAllHighlightsImmediate(); } - private IEnumerator HighlightCoroutine(BaseUnitPath path) + // Вызывается при деактивации объекта + private void OnDisable() { - // TODO Implement me - yield break; + isShuttingDown = true; // Устанавливаем флаг завершения + ClearAllHighlights(); // Очищаем все подсветки } - private void CreateHighlight(Vector2Int atCell) + // Вызывается при уничтожении объекта + private void OnDestroy() { - var pos = Gameplay3dView.ToWorldPosition(atCell, 1f); - var highlight = Instantiate(cellHighlightPrefab, pos, Quaternion.identity); - highlight.transform.SetParent(transform); - allHighlights.Add(highlight); + isShuttingDown = true; // Устанавливаем флаг завершения + ClearAllHighlights(); // Очищаем все подсветки } - private void DestroyHighlight(int index) + // Вызывается когда объект становится неактивным + private void OnApplicationQuit() { - Destroy(allHighlights[index]); - allHighlights.RemoveAt(index); + isShuttingDown = true; // Устанавливаем флаг при выходе из приложения } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs index fd1d88a87..bb350b7cd 100644 --- a/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/SecondUnitBrain.cs @@ -3,193 +3,267 @@ using System.Collections.Generic; using UnityEngine; using Utilities; +using UnitBrains.Pathfinding; // Импорт для использования AStarUnitPath namespace UnitBrains.Player { public class SecondUnitBrain : DefaultPlayerUnitBrain { - public override string TargetUnitName => "Cobra Commando"; - private const float OverheatTemperature = 3f; //температура перегрева - private const float OverheatCooldown = 2f; //время остывания - private float _temperature = 0f; //текущая температур оружия - private float _cooldownTime = 0f; //время с начала остывания - private bool _overheated; //флаг перегрева (true - перегрето) - - // B.Создаем новое поле для хранения целей, к которым нужно идти, но которые вне зоны досягаемости + public override string TargetUnitName => "Cobra Commando"; // Имя юнита для идентификации + private const float OverheatTemperature = 3f; // Пороговая температура перегрева оружия + private const float OverheatCooldown = 2f; // Время остывания после перегрева (в секундах) + private float _temperature = 0f; // Текущая температура оружия + private float _cooldownTime = 0f; // Время, прошедшее с начала остывания + private bool _overheated; // Флаг, указывающий на перегрев оружия + + // Список целей, находящихся вне зоны досягаемости атаки + // Юнит будет двигаться к этим целям, используя A* pathfinding List _outOfRangeTargets = new List(); - // A. Создаем статическое поле-счетчик для выдачи номеров юнитам + // Статический счетчик для присвоения уникальных номеров юнитам этого типа private static int _unitCounter = 0; - // A. Создаем поле с номером юнита + // Уникальный номер текущего экземпляра юнита private int _unitNumber; - // A. Создаем константу для максимального количества целей для умного выбора + // Максимальное количество целей для "умного" распределения между юнитами private const int MaxTargetsForSmartSelection = 3; - // Конструктор для присвоения номера юниту при создании + // Конструктор класса - вызывается при создании каждого нового юнита public SecondUnitBrain() { - _unitNumber = _unitCounter; - _unitCounter++; + _unitNumber = _unitCounter; // Присваиваем текущий номер из статического счетчика + _unitCounter++; // Увеличиваем счетчик для следующего юнита } - protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) //метод генерации снарядов (forTarget - цель для выстрела, список куда добавляются созданные снаряды) + // Метод генерации снарядов при атаке + // forTarget - цель для выстрела + // intoList - список, куда добавляются созданные снаряды + protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) { - float overheatTemperature = OverheatTemperature; //локальная переменная для температуры перегрева - /////////////////////////////////////// - // Homework 1.3 (1st block, 3rd module) - /////////////////////////////////////// + float overheatTemperature = OverheatTemperature; // Локальная переменная порога перегрева + + // Если оружие перегрето, не создаем снаряды if (GetTemperature() >= overheatTemperature) { - return; + return; // Выходим из метода без создания снарядов } - int currentTemp = GetTemperature(); - int projectileCount = currentTemp + 1; + // Количество снарядов зависит от текущей температуры: температура + 1 + int currentTemp = GetTemperature(); // Получаем текущую температуру как целое число + int projectileCount = currentTemp + 1; // Вычисляем количество снарядов для выстрела + // Создаем указанное количество снарядов for (int i = 0; i < projectileCount; i++) { - var projectile = CreateProjectile(forTarget); //создание снаряда направленного в указанную цель - AddProjectileToList(projectile, intoList); //добавление созданного снаряда в список + var projectile = CreateProjectile(forTarget); // Создаем снаряд, направленный в цель + AddProjectileToList(projectile, intoList); // Добавляем снаряд в выходной список } - IncreaseTemperature(); //увеличиваем температуру после выстрела + IncreaseTemperature(); // Увеличиваем температуру оружия после выстрела } + // Метод определения следующего шага движения юнита с использованием A* public override Vector2Int GetNextStep() { - // Если есть цели вне зоны досягаемости, идем к первой из них + // Если юнит может атаковать цель в зоне досягаемости, остаемся на месте + if (HasTargetsInRange()) + return unit.Pos; + + Vector2Int target; // Цель для движения + + // Определяем цель для движения if (_outOfRangeTargets.Count > 0) { - Vector2Int target = _outOfRangeTargets[0]; - Vector2Int currentPos = unit.Pos; - return currentPos.CalcNextStepTowards(target); + target = _outOfRangeTargets[0]; // Берем первую цель из списка целей вне зоны атаки + } + else + { + // Если нет конкретных целей, идем к базе противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + target = runtimeModel.RoMap.Bases[enemyId]; // Позиция вражеской базы + } + + // Если мы уже на цели, остаемся на месте + if (unit.Pos == target) + return unit.Pos; + + // Создаем A* путь от текущей позиции до цели + ActivePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = ActivePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (разница не должна превышать 1 клетку) + Vector2Int delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + // Если шаг невалидный, используем прямое движение к цели + Debug.LogWarning($"SecondUnitBrain: invalid A* step from {unit.Pos} to {nextStep}, using fallback"); + nextStep = unit.Pos.CalcNextStepTowards(target); + } + + // Проверяем проходимость выбранной клетки + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"SecondUnitBrain: cell {nextStep} is not walkable, finding alternative"); + + // Ищем альтернативный шаг среди соседних клеток + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Проверяем клетку сверху + Vector2Int.right, // Проверяем клетку справа + Vector2Int.down, // Проверяем клетку снизу + Vector2Int.left // Проверяем клетку слева + }; + + bool foundAlternative = false; // Флаг нахождения альтернативы + foreach (Vector2Int direction in directions) + { + Vector2Int alternativeStep = unit.Pos + direction; + if (IsCellWalkable(alternativeStep)) // Если клетка проходима + { + nextStep = alternativeStep; // Используем альтернативный шаг + foundAlternative = true; // Устанавливаем флаг + break; // Выходим из цикла + } + } + + if (!foundAlternative) // Если альтернатива не найдена + { + Debug.LogWarning($"SecondUnitBrain: no alternative step from {unit.Pos}, staying in place"); + return unit.Pos; // Остаемся на месте + } } - // Если целей вне зоны досягаемости нет, возвращаем текущую позицию юнита - return unit.Pos; + return nextStep; // Возвращаем валидный следующий шаг } - protected override List SelectTargets() //получает список всех достижимых целей + // Метод выбора целей для атаки + protected override List SelectTargets() { - /////////////////////////////////////// - // Homework 1.4 (1st block, 4rd module) - /////////////////////////////////////// - - // Очищаем список целей вне зоны досягаемости + // Очищаем список целей для движения перед новым выбором _outOfRangeTargets.Clear(); - // B. Получаем все цели и очищаем список + // Получаем все возможные цели (юниты противника + база) List allTargets = new List(GetAllTargets()); - List result = new List(); + List result = new List(); // Результирующий список целей для атаки - // Если в списке целей никого нет, добавляем базу противника + // Если целей на поле нет, нацеливаемся на базу противника if (allTargets.Count == 0) { - // Определяем ID противника int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; - // Получаем позицию базы противника Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; - // Проверяем, находится ли база в зоне досягаемости + // Проверяем, находится ли база в зоне досягаемости атаки if (IsTargetInRange(enemyBasePos)) { - // Если в зоне досягаемости - возвращаем как цель - result.Add(enemyBasePos); + result.Add(enemyBasePos); // Добавляем базу как цель для атаки return result; } else { - // Если вне зоны досягаемости - добавляем в список для движения - _outOfRangeTargets.Add(enemyBasePos); + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу в список для движения return result; } } - // B. Сортируем цели по дистанции до нашей базы + // Сортируем цели по расстоянию до нашей базы (приоритет - ближайшие) SortByDistanceToOwnBase(allTargets); - // B. Рассчитываем, какую цель по счету должен атаковать текущий юнит - // Если целей меньше, чем MaxTargetsForSmartSelection, берем остаток от деления номера юнита на количество целей + // Распределяем цели между юнитами для избежания скучивания на одной цели int targetIndex; - if (allTargets.Count <= MaxTargetsForSmartSelection) + if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей мало (≤ 3) { - targetIndex = _unitNumber % allTargets.Count; + targetIndex = _unitNumber % allTargets.Count; // Распределяем по номеру юнита } - else + else // Если целей много (> 3) { - // Если целей больше, чем MaxTargetsForSmartSelection, берем остаток от деления на MaxTargetsForSmartSelection - targetIndex = _unitNumber % MaxTargetsForSmartSelection; - // Но если индекс выходит за пределы списка, корректируем - if (targetIndex >= allTargets.Count) + targetIndex = _unitNumber % MaxTargetsForSmartSelection; // Берем остаток от деления на 3 + if (targetIndex >= allTargets.Count) // Если индекс выходит за границы { - targetIndex = allTargets.Count - 1; + targetIndex = allTargets.Count - 1; // Корректируем на последний валидный индекс } } - // Получаем выбранную цель по индексу + // Получаем выбранную цель по вычисленному индексу Vector2Int selectedTarget = allTargets[targetIndex]; - // Проверяем, что цель в радиусе атаки + // Проверяем, находится ли выбранная цель в радиусе атаки if (IsTargetInRange(selectedTarget)) { - // Если цель в зоне досягаемости, добавляем в результат - result.Add(selectedTarget); + result.Add(selectedTarget); // Добавляем цель для атаки } else { - // Если цель вне зоны досягаемости, записываем в коллекцию для движения - _outOfRangeTargets.Add(selectedTarget); + _outOfRangeTargets.Add(selectedTarget); // Добавляем цель для движения к ней + // Ищем первую цель в списке, которая находится в зоне досягаемости foreach (Vector2Int target in allTargets) { - if (IsTargetInRange(target)) + if (IsTargetInRange(target)) // Если цель доступна для атаки { - result.Add(target); - break; + result.Add(target); // Добавляем как цель для атаки + break; // Выходим из цикла } } - // Если ни одна цель не в зоне досягаемости, добавляем все цели в список для движения + + // Если ни одна цель не доступна для атаки if (result.Count == 0) { - _outOfRangeTargets.AddRange(allTargets); + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения } } - return result; + return result; // Возвращаем список целей для атаки } - public override void Update(float deltaTime, float time) //обновляет состояние юнита каждый кадр + // Метод обновления состояния юнита (вызывается каждый кадр) + public override void Update(float deltaTime, float time) { - if (_overheated) + if (_overheated) // Если оружие перегрето { - _cooldownTime += Time.deltaTime; //если оружия перегрето, увелиичивает время остываения - float t = _cooldownTime / (OverheatCooldown / 10); //вычисляет процесс остывания 0,2 сек - _temperature = Mathf.Lerp(OverheatTemperature, 0, t); //плавное уменьшение температуры от 3 до 0 в зависимотси от прогресса остывания - if (t >= 1) //после завершения остывания сброс таймера и флага перегрева + _cooldownTime += Time.deltaTime; // Увеличиваем время остывания + float t = _cooldownTime / (OverheatCooldown / 10); // Вычисляем прогресс остывания (от 0 до 1) + _temperature = Mathf.Lerp(OverheatTemperature, 0, t); // Плавно уменьшаем температуру + + if (t >= 1) // Если остывание завершено { - _cooldownTime = 0; - _overheated = false; + _cooldownTime = 0; // Сбрасываем таймер остывания + _overheated = false; // Снимаем флаг перегрева } } } - private int GetTemperature() //возвращает текущую температуру как целое число + // Вспомогательный метод получения текущей температуры как целого числа + private int GetTemperature() { - if (_overheated) // если перегрето - { - return (int)OverheatTemperature; //возврат 3 - } - else //иначе - { - return (int)_temperature; // текущая температа - } + if (_overheated) // Если оружие перегрето + return (int)OverheatTemperature; // Возвращаем максимальную температуру (3) + else // Если не перегрето + return (int)_temperature; // Возвращаем текущую температуру + } + // Вспомогательный метод увеличения температуры после выстрела + private void IncreaseTemperature() + { + _temperature += 1f; // Увеличиваем температуру на 1 градус + if (_temperature >= OverheatTemperature) // Если достигнут порог перегрева + _overheated = true; // Устанавливаем флаг перегрева } - private void IncreaseTemperature() //метод увеличения температуры + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) { - _temperature += 1f; //увеличивает температуру на 1 - if (_temperature >= OverheatTemperature) _overheated = true; //если текущая температура = 3, то перегрев + // Проверяем, что клетка в пределах карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; + + // Проверяем, что клетка не занята стеной + if (runtimeModel.RoMap[pos]) + return false; + + // Клетка проходима (не проверяем занятость юнитами для конечной точки) + return true; } } } \ No newline at end of file diff --git a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs index 632615095..aa9507cab 100644 --- a/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs +++ b/Assets/Scripts/UnitBrains/Player/ThirdUnitBrain.cs @@ -1,31 +1,32 @@ using Model; -using Model.Runtime.Projectiles; -using System.Collections.Generic; -using UnityEngine; -using Utilities; +using Model.Runtime.Projectiles; +using System.Collections.Generic; +using UnityEngine; +using Utilities; +using UnitBrains.Pathfinding; // Импорт для использования AStarUnitPath namespace UnitBrains.Player { - public class ThirdUnitBrain : DefaultPlayerUnitBrain + public class ThirdUnitBrain : DefaultPlayerUnitBrain { - public override string TargetUnitName => "Ironclad Behemoth"; // Переопределение свойства - возвращает имя цели для этого юнита + public override string TargetUnitName => "Ironclad Behemoth"; // Имя юнита для идентификации - //поля для задержки атаки - private float _attackDelay = 1f; // Задержка перед атакой после остановки движения (1 секунда) + // Параметры для реализации задержки атаки после движения + private float _attackDelay = 1f; // Задержка перед атакой после остановки (1 секунда) private float _timeSinceLastMovement = 0f; // Время, прошедшее с момента последнего движения - private bool _isMoving = false; // Флаг: движется ли юнит в данный момент + private bool _isMoving = false; // Флаг, указывающий движется ли юнит в данный момент - // Список целей, которые находятся вне зоны досягаемости - к ним юнит будет двигаться + // Список целей вне зоны досягаемости - юнит будет двигаться к ним используя A* List _outOfRangeTargets = new List(); - // Статический счетчик для присвоения уникальных номеров каждому юниту этого типа + // Статический счетчик для присвоения уникальных номеров юнитам этого типа private static int _unitCounter = 0; - // Уникальный номер конкретного экземпляра юнита + // Уникальный номер текущего экземпляра юнита private int _unitNumber; - // Константа: максимальное количество целей для распределения между юнитами + // Максимальное количество целей для "умного" распределения между юнитами private const int MaxTargetsForSmartSelection = 3; - // Конструктор класса - вызывается при создании каждого экземпляра юнита + // Конструктор класса - вызывается при создании каждого юнита public ThirdUnitBrain() { _unitNumber = _unitCounter; // Присваиваем текущий номер из счетчика @@ -33,150 +34,223 @@ public ThirdUnitBrain() } // Метод генерации снарядов при атаке + // forTarget - цель для выстрела + // intoList - список для добавления созданных снарядов protected override void GenerateProjectiles(Vector2Int forTarget, List intoList) { - // Создаем всегда 3 снаряда (максимальное количество из предыдущей логики) + // Всегда создаем 3 снаряда (максимальная огневая мощь Behemoth) int projectileCount = 3; - for (int i = 0; i < projectileCount; i++) // Цикл для создания указанного количества снарядов + // Цикл создания указанного количества снарядов + for (int i = 0; i < projectileCount; i++) { - var projectile = CreateProjectile(forTarget); // Создаем один снаряд, направленный в цель - AddProjectileToList(projectile, intoList); // Добавляем созданный снаряд в общий список + var projectile = CreateProjectile(forTarget); // Создаем снаряд, направленный в цель + AddProjectileToList(projectile, intoList); // Добавляем снаряд в выходной список } } - // Метод определения следующего шага движения юнита + // Метод определения следующего шага движения юнита с использованием A* public override Vector2Int GetNextStep() { - // Проверяем, есть ли цели вне зоны досягаемости + // Проверяем, есть ли цели для движения (вне зоны атаки) if (_outOfRangeTargets.Count > 0) { _isMoving = true; // Устанавливаем флаг движения Vector2Int target = _outOfRangeTargets[0]; // Берем первую цель из списка - Vector2Int currentPos = unit.Pos; // Получаем текущую позицию юнита - return currentPos.CalcNextStepTowards(target); // Вычисляем следующий шаг к цели + + // Если мы уже на цели, останавливаемся + if (unit.Pos == target) + { + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Остаемся на месте + } + + // Создаем A* путь от текущей позиции до цели + ActivePath = new AStarUnitPath(runtimeModel, unit.Pos, target); + + // Получаем следующий шаг по вычисленному пути + Vector2Int nextStep = ActivePath.GetNextStepFrom(unit.Pos); + + // Проверяем валидность шага (не больше 1 клетки) + Vector2Int delta = nextStep - unit.Pos; + if (Mathf.Abs(delta.x) > 1 || Mathf.Abs(delta.y) > 1) + { + // Если шаг невалидный, используем прямое движение + Debug.LogWarning($"ThirdUnitBrain: invalid A* step from {unit.Pos} to {nextStep}, using fallback"); + nextStep = unit.Pos.CalcNextStepTowards(target); + } + + // Проверяем проходимость выбранной клетки + if (!IsCellWalkable(nextStep)) + { + Debug.LogWarning($"ThirdUnitBrain: cell {nextStep} not walkable, finding alternative"); + + // Ищем альтернативный шаг среди соседних клеток + Vector2Int[] directions = new Vector2Int[] + { + Vector2Int.up, // Проверяем вверх + Vector2Int.right, // Проверяем вправо + Vector2Int.down, // Проверяем вниз + Vector2Int.left // Проверяем влево + }; + + bool foundAlternative = false; // Флаг нахождения альтернативы + foreach (Vector2Int direction in directions) + { + Vector2Int alternativeStep = unit.Pos + direction; + if (IsCellWalkable(alternativeStep)) // Если клетка проходима + { + nextStep = alternativeStep; // Используем альтернативу + foundAlternative = true; // Устанавливаем флаг + break; // Выходим из цикла + } + } + + if (!foundAlternative) // Если альтернатива не найдена + { + Debug.LogWarning($"ThirdUnitBrain: no alternative step from {unit.Pos}, staying in place"); + _isMoving = false; // Сбрасываем флаг движения + return unit.Pos; // Остаемся на месте + } + } + + return nextStep; // Возвращаем валидный следующий шаг } - // Если целей вне зоны досягаемости нет, возвращаем текущую позицию (стоим на месте) + // Если целей для движения нет, стоим на месте _isMoving = false; // Сбрасываем флаг движения - return unit.Pos; // Возвращаем текущую позицию (юнит стоит на месте) + return unit.Pos; // Возвращаем текущую позицию (юнит не двигается) } - // Метод выбора целей для атаки + // Метод выбора целей для атаки с учетом задержки после движения protected override List SelectTargets() { - // Очищаем список целей вне зоны досягаемости + // Очищаем список целей для движения _outOfRangeTargets.Clear(); // Получаем все доступные цели List allTargets = new List(GetAllTargets()); - List result = new List(); // Список целей для атаки (результат) + List result = new List(); // Результирующий список целей для атаки - // БЛОКИРОВКА АТАКИ - // Проверяем: движется ли юнит или прошло меньше 1 секунды после остановки + // БЛОКИРОВКА АТАКИ ВО ВРЕМЯ ДВИЖЕНИЯ + // Если юнит движется или прошло меньше секунды после остановки if (_isMoving || _timeSinceLastMovement < _attackDelay) { - // Если есть цели - добавляем их в список для движения + // Если есть цели на поле if (allTargets.Count > 0) { - _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели в список для движения + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения к ним } else { - // Если нет целей - двигаемся к базе противника - int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; // Определяем ID врага - Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; // Получаем позицию базы врага - _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу врага в список для движения + // Если целей нет, движемся к базе противника + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу как цель для движения } - // Возвращаем пустой список - АТАКА ЗАБЛОКИРОВАНА - return result; + return result; // Возвращаем пустой список - атака заблокирована } - // === КОНЕЦ БЛОКИРОВКИ === + // === КОНЕЦ БЛОКИРОВКИ АТАКИ === - // Если список целей пуст (никого нет на поле) + // Если целей на поле нет if (allTargets.Count == 0) { - int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; // Определяем ID врага - Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; // Получаем позицию базы врага + int enemyId = IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId; + Vector2Int enemyBasePos = runtimeModel.RoMap.Bases[enemyId]; if (IsTargetInRange(enemyBasePos)) // Если база в зоне досягаемости { - result.Add(enemyBasePos); // Добавляем базу в список целей для атаки - return result; // Возвращаем результат + result.Add(enemyBasePos); // Добавляем базу как цель для атаки + return result; } else // Если база вне зоны досягаемости { - _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу в список для движения - return result; // Возвращаем пустой результат (атака невозможна) + _outOfRangeTargets.Add(enemyBasePos); // Добавляем базу для движения к ней + return result; } } - // Сортируем все цели по расстоянию до нашей собственной базы (приоритет - ближние) + // Сортируем цели по расстоянию до нашей базы (приоритет - ближайшие) SortByDistanceToOwnBase(allTargets); - // Выбираем индекс цели для атаки, распределяя цели между юнитами + // Распределяем цели между юнитами для равномерной атаки int targetIndex; - if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей меньше или равно 3 + if (allTargets.Count <= MaxTargetsForSmartSelection) // Если целей мало (≤ 3) { - targetIndex = _unitNumber % allTargets.Count; // Берем остаток от деления номера юнита на количество целей + targetIndex = _unitNumber % allTargets.Count; // Распределяем по номеру юнита } - else // Если целей больше 3 + else // Если целей много (> 3) { targetIndex = _unitNumber % MaxTargetsForSmartSelection; // Берем остаток от деления на 3 - // Если индекс выходит за пределы списка - if (targetIndex >= allTargets.Count) + if (targetIndex >= allTargets.Count) // Если индекс за пределами списка { - targetIndex = allTargets.Count - 1; // Корректируем на последний валидный индекс + targetIndex = allTargets.Count - 1; // Корректируем на последний индекс } } - // Получаем выбранную цель по вычисленному индексу + // Получаем выбранную цель по индексу Vector2Int selectedTarget = allTargets[targetIndex]; - // Проверяем, находится ли выбранная цель в радиусе атаки + // Проверяем, находится ли цель в зоне досягаемости if (IsTargetInRange(selectedTarget)) { - // Если цель в зоне досягаемости - добавляем в результат - result.Add(selectedTarget); + result.Add(selectedTarget); // Добавляем для атаки } else { - // Если цель вне зоны досягаемости - добавляем в список для движения - _outOfRangeTargets.Add(selectedTarget); - // Ищем первую цель в списке, которая находится в зоне досягаемости + _outOfRangeTargets.Add(selectedTarget); // Добавляем для движения + + // Ищем первую цель в зоне досягаемости из всего списка foreach (Vector2Int target in allTargets) { - if (IsTargetInRange(target)) // Если цель в зоне досягаемости + if (IsTargetInRange(target)) // Если цель доступна { - result.Add(target); // Добавляем в результат - break; // Выходим из цикла, так как нашли цель + result.Add(target); // Добавляем для атаки + break; // Выходим из цикла } } - // Если ни одна цель не в зоне досягаемости + + // Если ни одна цель не доступна для атаки if (result.Count == 0) { - _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели в список для движения + _outOfRangeTargets.AddRange(allTargets); // Добавляем все цели для движения } } return result; // Возвращаем список целей для атаки } - // Метод обновления состояния юнита + // Метод обновления состояния юнита (вызывается каждый кадр) public override void Update(float deltaTime, float time) { - base.Update(deltaTime, time); // Вызов метода базового класса для стандартной логики обновления + base.Update(deltaTime, time); // Вызываем базовый метод обновления - // Обновляем таймер без движения + // Обновляем таймер после движения if (!_isMoving) // Если юнит не движется { - _timeSinceLastMovement += Time.deltaTime; // Увеличиваем время с момента последнего движения + _timeSinceLastMovement += Time.deltaTime; // Увеличиваем время с момента остановки } else // Если юнит движется { - _timeSinceLastMovement = 0f; // Сбрасываем таймер + _timeSinceLastMovement = 0f; // Сбрасываем таймер при движении } } + + // Вспомогательный метод проверки проходимости клетки + private bool IsCellWalkable(Vector2Int pos) + { + // Проверяем, что клетка в пределах карты + if (pos.x < 0 || pos.x >= runtimeModel.RoMap.Width || + pos.y < 0 || pos.y >= runtimeModel.RoMap.Height) + return false; + + // Проверяем, что клетка не занята стеной + if (runtimeModel.RoMap[pos]) + return false; + + // Клетка проходима + return true; + } } } \ No newline at end of file