Представьте, что вы разрабатываете игру, где есть много врагов, например, survival.io. И есть юнит, который должен атаковать ближайшего юнита ближнего боя.
Как бы выглядел поиск этого юнита в классической реализации
private Enemy GetEnemyClassic()
{
var size = Physics2D.OverlapCircleNonAlloc(center.position, radius, results, layerMask);
for (var i = 0; i < size; i++)
{
var enemy = results[i].GetComponent<Enemy>();
if (enemy.IsDead)
continue;
if (enemy.EnemyType != EnemyType.Melee)
continue;
return enemy;
}
return default;
}
⚠️ Минусы:
* Использование коллайдеров для Physics2D.OverlapCircleNonAlloc.
* Необходимость добавлять специальный слой на врагов (и не забывать добавлять его новым врагам).
* Дорогой вызов GetComponent<Enemy>.
* Сложность восприятия метода при чтении кода.
⚙️ Альтернативный вариант: коллекция + Builder
Я часто использую подход через коллекцию и паттерн Builder.
private Enemy GetEnemyViaBuilder() =>
enemyCollection
.InRadius(center.position, radius)
.IsAlive()
.Melee()
.FirstOrDefault();
✅ Преимущества:
* Упрощение кода и повышение читаемости.
* Отсутствие необходимости работать с коллайдерами и слоями.
* Избавление от вызова GetComponent.
⚠️ Минусы:
* Увеличение времени разработки: требуется написать систему фильтрации и поддерживать коллекцию в актуальном состоянии.
✍️ Пример реализации коллекции
public class EnemyCollection
{
private readonly FilterData filter = new();
private IEnumerable<Enemy> enemies;
public EnemyCollection(IEnumerable<Enemy> enemies)
{
this.enemies = enemies;
}
public EnemyCollection IsDead()
{
filter.IsDead = true;
return this;
}
public EnemyCollection IsAlive()
{
filter.IsDead = false;
return this;
}
public EnemyCollection Melee()
{
filter.EnemyType = EnemyType.Melee;
return this;
}
public EnemyCollection InRadius(Vector3 center, float radius)
{
filter.InFilterByRadius = true;
filter.Center = center;
filter.Radius = radius;
return this;
}
public Enemy FirstOrDefault()
{
return Build().FirstOrDefault();
}
public IEnumerable<Enemy> Build()
{
if (filter.IsDead.HasValue)
{
enemies = enemies.Where(e => e.IsDead == filter.IsDead);
}
if (filter.EnemyType.HasValue)
{
enemies = enemies.Where(e => e.EnemyType == filter.EnemyType);
}
if (filter.InFilterByRadius)
{
enemies = enemies.Where(e => Vector3.Distance(filter.Center, e.transform.position);
}
filter.Clear();
return enemies;
}
private class FilterData
{
public bool? IsDead;
public EnemyType? EnemyType;
public bool InFilterByRadius;
public float Radius;
public Vector3 Center;
public void Clear()
{
IsDead = null;
EnemyType = null;
InFilterByRadius = false;
Radius = 0;
Center = new Vector3();
}
}
}
Код, приведеный выше служит лишь примером, что бы передать главнную мысль об использовании кастомных коллекций в связке с Builder. В реальных проектах такие коллекции могут иметь альтернативные реализации, содержать различные кеши для ускорения поиска или фильтрации и многие другие "удобные".
Напишите в комментариях ваше мнение о таком решении! 🚀