Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Logs
Assembly-CSharp.csproj
Assembly-CSharp-Editor.csproj
CsharpMainProject.sln
.vs
5 changes: 2 additions & 3 deletions Assets/Resources/PlayerUnits/PlayerUnit3.prefab
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
_healthBar: {fileID: 178258570018206233}
_debugPathOutput: {fileID: 0}
--- !u!114 &3664015912038799981
MonoBehaviour:
m_ObjectHideFlags: 0
Expand All @@ -67,10 +68,8 @@ MonoBehaviour:
_maxHealth: 400
_brainUpdateDelay: 0.25
_moveDelay: 0.25
_attackDelay: 0.75
_attackDelay: 0.15
_attackRange: 3.5
_shotsPerTarget: 1
_targetsInVolley: 1
_projectileType: 0
_damage: 15
--- !u!1 &526913648782850647
Expand Down
2 changes: 1 addition & 1 deletion Assets/Scripts/EnterPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public class EnterPoint : MonoBehaviour
{
[SerializeField] private Settings _settings;
[SerializeField] private Canvas _targetCanvas;
private float _timeScale = 1;
private float _timeScale = 1f;

void Start()
{
Expand Down
10 changes: 8 additions & 2 deletions Assets/Scripts/UnitBrains/BaseUnitBrain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@ 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<IReadOnlyRuntimeModel>();
private BaseUnitPath _activePath = null;


/// <summary>
protected bool _actionSwitched = false;
/// </summary>

private readonly Vector2[] _projectileShifts = new Vector2[]
{
new (0f, 0f),
Expand All @@ -36,10 +41,11 @@ public virtual Vector2Int GetNextStep()
if (HasTargetsInRange())
return unit.Pos;

_actionSwitched = true;
var target = runtimeModel.RoMap.Bases[
IsPlayerUnitBrain ? RuntimeModel.BotPlayerId : RuntimeModel.PlayerId];

_activePath = new DummyUnitPath(runtimeModel, unit.Pos, target);
_activePath = new Pathfinding.AdvancedUnitPath(runtimeModel, unit.Pos, target);
return _activePath.GetNextStepFrom(unit.Pos);
}

Expand Down
126 changes: 126 additions & 0 deletions Assets/Scripts/UnitBrains/Coordinator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Model;
using Model.Runtime.ReadOnly;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Utilities;

namespace Assets.Scripts.UnitBrains
{
public class Coordinator
{
private static Coordinator _instance;

private IReadOnlyRuntimeModel _runtimeModel;
private TimeUtil _timeUtil;

private Vector2Int _basePos;
private IEnumerable<IReadOnlyUnit> _enemyUnits;

// Приватный конструктор синглтона
private Coordinator()
{
// 1. Получаем ссылки
_runtimeModel = ServiceLocator.Get<IReadOnlyRuntimeModel>();
_timeUtil = ServiceLocator.Get<TimeUtil>();

// 2. БЕЗОПАСНО инициализируем позицию базы, так как _runtimeModel уже существует
_basePos = _runtimeModel.RoMap.Bases[RuntimeModel.PlayerId];

// 3. Подписываемся на обновления
_timeUtil.AddFixedUpdateAction(OnFixedUpdate);

// 4. Первоначальный сбор врагов
UpdateEnemyList();
}

public static Coordinator GetInstance()
{
if (_instance == null)
_instance = new Coordinator();
return _instance;
}

// Обновляем список врагов каждый кадр (как в правильном коде)
private void OnFixedUpdate(float fixedDeltaTime)
{
UpdateEnemyList();
}

private void UpdateEnemyList()
{
// Берем всех юнитов на карте и фильтруем только вражеских (ботов)
_enemyUnits = _runtimeModel.RoUnits.Where(u => u.Config.IsPlayerUnit == false);
}

private bool HasTargetsNearBase() // враги на твоей половине
{
if (!_enemyUnits.Any()) return false;

return NearestEnemy().x < _runtimeModel.RoMap.Width / 2;
}

private Vector2Int MostDamagedTarget() // меньше всего хп
{
// Если врагов нет, идем к базе врага
if (!_enemyUnits.Any())
return _runtimeModel.RoMap.Bases[RuntimeModel.BotPlayerId];

int lowestHealth = int.MaxValue;
Vector2Int mostDamagedTarget = Vector2Int.zero;

foreach (var unit in _enemyUnits)
{
if (unit.Health < lowestHealth)
{
lowestHealth = unit.Health;
mostDamagedTarget = unit.Pos;
}
}
return mostDamagedTarget;
}

private Vector2Int NearestEnemy() // ближайший враг
{
if (!_enemyUnits.Any())
return _runtimeModel.RoMap.Bases[RuntimeModel.BotPlayerId];

int minDistance = int.MaxValue;
Vector2Int nearestTarget = Vector2Int.zero;

foreach (var enemy in _enemyUnits)
{
var diff = enemy.Pos - _basePos;
if (diff.sqrMagnitude < minDistance)
{
minDistance = diff.sqrMagnitude;
nearestTarget = enemy.Pos;
}
}
return nearestTarget;
}

public Vector2Int GetRecomendedTarget()
{
if (HasTargetsNearBase())
return NearestEnemy();
else
return MostDamagedTarget();
}

// ВАЖНО: передаем дальность атаки в метод, так как глобальный Координатор не знает, какой юнит его вызывает
public Vector2Int GetRecomendedPos(float attackRange)
{
if (HasTargetsNearBase())
{
return new Vector2Int(_basePos.x + 1, _basePos.y);
}
else
{
Vector2Int enemyPos = NearestEnemy();
return new Vector2Int(enemyPos.x - (int)Math.Ceiling(attackRange), enemyPos.y);
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/UnitBrains/Coordinator.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

163 changes: 163 additions & 0 deletions Assets/Scripts/UnitBrains/Pathfinding/AdvancedUnitPath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using Model;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace UnitBrains.Pathfinding
{
public class NewUnitPath : BaseUnitPath
{
protected Vector2Int[] dx = {
Vector2Int.down,
Vector2Int.up,
Vector2Int.left,
Vector2Int.right
};
protected const int MaxLength = 150;

public NewUnitPath(IReadOnlyRuntimeModel runtimeModel, Vector2Int startPoint, Vector2Int endPoint) : base(runtimeModel, startPoint, endPoint)
{

}

protected override void Calculate()
{
var listPath = FindPath();
// Проверка на случай, если ничего не было записано в найденный путь
if (FindPath().Count < 1)
{
path = null;
}
else
{
path = listPath.ToArray();
}
}

protected List<Vector2Int> FindPath()
{
// Проверка на случай, если старт и цель совпадают или находятся в 1 клетке друг от друга
bool isCardinalDirection = dx.Contains(endPoint - startPoint);
if (startPoint.Equals(endPoint) || isCardinalDirection == true)
return new List<Vector2Int> { startPoint };

Node startNode = new Node(startPoint);
Node targetNode = new Node(endPoint);

List<Node> openNode = new List<Node> { startNode };
HashSet<Node> closedList = new HashSet<Node>();
int counter = 0;
Node bestNodeSoFar = startNode; // Хранит лучший найденный узел (ближайший к цели)
float bestDistanceSoFar = float.MaxValue; // Расстояние лучшего узла до цели

// Инициализируем стартовый узел
startNode.CalculateEstimate(targetNode.Pos);
startNode.CalculateValue();

while (openNode.Count > 0 && counter < MaxLength)
{
int minIndex = 0;
Node currentNode = openNode[0];

for (int i = 1; i < openNode.Count; i++)
{
if (openNode[i].Value < currentNode.Value)
{
currentNode = openNode[i];
minIndex = i;
}
}

// Удаляем по индексу
openNode.RemoveAt(minIndex);

// Пропускаем, если узел уже обработан
if (closedList.Contains(currentNode))
continue;

closedList.Add(currentNode);

// Обновляем лучший узел, если текущий ближе к цели
float currentDistance = CalculateDistance(currentNode.Pos, endPoint);
if (currentDistance < bestDistanceSoFar)
{
bestDistanceSoFar = currentDistance;
bestNodeSoFar = currentNode;
}

// Проверяем, достигли ли цели
if (endPoint.Equals(currentNode.Pos))
return buildPath(currentNode);

// Исследуем соседей
foreach (var direction in dx)
{
var nextStep = direction + currentNode.Pos;

// Проверяем проходимость
if (!runtimeModel.IsTileWalkable(nextStep) && !nextStep.Equals(endPoint))
continue;

Node neighbor = new Node(nextStep);

// Пропускаем, если узел уже закрыт
if (closedList.Contains(neighbor))
continue;

// Проверяем, есть ли сосед уже в openNode
bool alreadyInOpen = false;
for (int i = 0; i < openNode.Count; i++)
{
if (openNode[i].Equals(neighbor))
{
// Если нашли узел с той же позицией, обновляем его параметры,
// если новый путь лучше (меньшее Value)
int newCost = currentNode.Cost + neighbor.Cost;
if (newCost < openNode[i].Cost)
{
openNode[i].Parent = currentNode;
openNode[i].Cost = newCost;
openNode[i].CalculateValue();
}
alreadyInOpen = true;
break;
}
}

if (!alreadyInOpen)
{
neighbor.Parent = currentNode;
neighbor.Cost = currentNode.Cost + 10;
neighbor.CalculateEstimate(targetNode.Pos);
neighbor.CalculateValue();
openNode.Add(neighbor);
}
}

counter++;
}

// Если достигли MaxLength, но не нашли путь — возвращаем путь к ближайшему найденному узлу
return buildPath(bestNodeSoFar);
}

private float CalculateDistance(Vector2Int a, Vector2Int b)
{
var diff = a - b;
return Mathf.Sqrt(diff.x * diff.x + diff.y * diff.y);
}

protected List<Vector2Int> buildPath(Node node)
{
List<Vector2Int> result = new();
while (node != null)
{
result.Add(node.Pos);
node = node.Parent;
}
result.Reverse();
return result;
}
}
}
11 changes: 11 additions & 0 deletions Assets/Scripts/UnitBrains/Pathfinding/AdvancedUnitPath.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading