This commit is contained in:
2026-04-30 19:33:16 +08:00
commit fb2252f688
556 changed files with 69721 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
using GeometryTD.Definition;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class BasicBaseComp : MonoBehaviour
{
[SerializeField] [Min(0.01f)] private float _attackSpeed = 1f;
[SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.Physics;
[SerializeField] private SpriteRenderer _renderer;
private float _cooldownRemaining;
public float AttackSpeed => _attackSpeed;
public AttackPropertyType AttackPropertyType => _attackPropertyType;
public bool CanAttack => _cooldownRemaining <= 0f;
public void OnInit(float attackSpeed, AttackPropertyType attackPropertyType)
{
_attackSpeed = Mathf.Max(0.01f, attackSpeed);
_attackPropertyType = attackPropertyType;
_cooldownRemaining = 0f;
}
public void OnReset()
{
_attackSpeed = 1f;
_attackPropertyType = AttackPropertyType.None;
_cooldownRemaining = 0f;
}
public void SetColor(Color color)
{
if (_renderer != null)
{
_renderer.color = color;
}
}
public void Tick(float deltaTime)
{
if (_cooldownRemaining <= 0f)
{
return;
}
_cooldownRemaining = Mathf.Max(0f, _cooldownRemaining - Mathf.Max(0f, deltaTime));
}
public bool TryAttack(BasicBearingComp bearingComp, ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
{
if (bearingComp == null || shooterMuzzleComp == null || target == null)
{
return false;
}
Tick(deltaTime);
bool isAligned = bearingComp.TrackTarget(target, deltaTime);
if (!isAligned || !CanAttack)
{
return false;
}
bool fired = shooterMuzzleComp.Attack(target, _attackPropertyType);
if (!fired)
{
return false;
}
_cooldownRemaining = 1f / _attackSpeed;
return true;
}
}
}
+120
View File
@@ -0,0 +1,120 @@
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class BasicBearingComp : MonoBehaviour
{
[SerializeField] [Min(1f)] private float _rotateSpeed = 180f;
[SerializeField] [Min(0.1f)] private float _attackRange = 5f;
[SerializeField] [Min(0.1f)] private float _aimToleranceAngle = 2f;
[SerializeField] private Transform _rotateRoot;
[SerializeField] private SpriteRenderer _renderer;
public float RotateSpeed => _rotateSpeed;
public float AttackRange => _attackRange;
public float AimToleranceAngle => _aimToleranceAngle;
public void OnInit(float rotateSpeed, float attackRange = 5f)
{
_rotateSpeed = Mathf.Max(1f, rotateSpeed);
_attackRange = Mathf.Max(0.1f, attackRange);
}
public void OnReset()
{
_rotateSpeed = 1f;
_attackRange = 5f;
}
public void SetColor(Color color)
{
if (_renderer != null)
{
_renderer.color = color;
}
}
public bool IsTargetInRange(Transform target, Transform origin = null)
{
if (target == null)
{
return false;
}
Transform originTransform = origin != null ? origin : transform;
Vector3 delta = target.position - originTransform.position;
return delta.sqrMagnitude <= _attackRange * _attackRange;
}
public bool TrackTarget(Transform target, float deltaTime)
{
if (target == null)
{
return false;
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
float currentZAngle = rotateRoot.eulerAngles.z;
float maxStep = _rotateSpeed * Mathf.Max(0f, deltaTime);
float angleDelta = Mathf.DeltaAngle(currentZAngle, targetZAngle);
float nextZAngle = currentZAngle + Mathf.Clamp(angleDelta, -maxStep, maxStep);
rotateRoot.rotation = Quaternion.Euler(0f, 0f, nextZAngle);
return IsTargetAligned(target);
}
public bool IsTargetAligned(Transform target)
{
if (target == null)
{
return false;
}
Transform rotateRoot = _rotateRoot != null ? _rotateRoot : transform;
Vector2 toTarget = (Vector2)(target.position - rotateRoot.position);
if (toTarget.sqrMagnitude <= Mathf.Epsilon)
{
return true;
}
float targetZAngle = Vector2.SignedAngle(Vector2.up, toTarget);
float currentZAngle = rotateRoot.eulerAngles.z;
float angle = Mathf.Abs(Mathf.DeltaAngle(currentZAngle, targetZAngle));
return angle <= _aimToleranceAngle;
}
public bool TryAttack(ShooterMuzzleComp shooterMuzzleComp, Transform target, float deltaTime)
{
if (shooterMuzzleComp == null || target == null)
{
return false;
}
if (!IsTargetInRange(target))
{
return false;
}
bool isAligned = TrackTarget(target, deltaTime);
if (!isAligned)
{
return false;
}
return shooterMuzzleComp.Attack(target);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using GeometryTD.Definition;
namespace Components
{
public interface IDamageReceiver
{
void TakeDamage(AttackPayload attackPayload);
}
}
+51
View File
@@ -0,0 +1,51 @@
using UnityEngine;
namespace Components
{
public class InputComponent : MonoBehaviour
{
private PlayerInputActions _inputActions;
private bool _isListening = false;
private Vector3 _direction = Vector3.zero;
public Vector3 Direction => _direction;
public void OnInit()
{
_inputActions = new();
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (_isListening && _inputActions != null)
{
ReadInput();
}
}
public void OnReset()
{
_inputActions = null;
_isListening = false;
}
private void ReadInput()
{
Vector2 rawDir = _inputActions.Game.Move.ReadValue<Vector2>();
_direction = new Vector3(rawDir.x, 0, rawDir.y);
}
public void SetListening(bool isListening)
{
_isListening = isListening;
if (_isListening)
{
_inputActions.Enable();
}
else
{
_inputActions.Disable();
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
using UnityEngine;
namespace Components
{
public class MovementComponent : MonoBehaviour
{
private bool _isMoving = false;
private Vector3 _direction = Vector3.forward;
private float _speed = 0;
private float _speedMultiplier = 1f;
private Transform _cachedTransform;
public void OnInit(float speed, Transform transform)
{
_speed = speed;
_speedMultiplier = 1f;
_cachedTransform = transform;
}
public void OnUpdate(float elapseSeconds, float realElapseSeconds)
{
if (_isMoving && _cachedTransform != null)
{
Move(elapseSeconds);
}
}
public void OnReset()
{
_speed = 0;
_speedMultiplier = 1f;
_cachedTransform = null;
_isMoving = false;
}
private void Move(float deltaTime = 0)
{
_cachedTransform.Translate(_direction * (_speed * _speedMultiplier) * deltaTime);
}
public void SetMove(bool isMoving) => _isMoving = isMoving;
public void SetDirection(Vector3 direction) => _direction = direction;
public void SetSpeedMultiplier(float speedMultiplier) => _speedMultiplier = Mathf.Max(0f, speedMultiplier);
}
}
+137
View File
@@ -0,0 +1,137 @@
using UnityEngine;
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
namespace Components
{
[DisallowMultipleComponent]
public class ShooterBullet : MonoBehaviour
{
[SerializeField] [Min(0.1f)] private float _defaultSpeed = 12f;
[SerializeField] [Min(0.02f)] private float _hitDistance = 0.15f;
[SerializeField] [Min(0.1f)] private float _maxLifetime = 3f;
private Transform _target;
private float _speed;
private AttackPayload _attackPayload;
private float _lifetime;
private float _runtimeMaxLifetime;
private bool _isRunning;
private bool _despawnRequested;
public void OnShow(BulletData bulletData)
{
_target = bulletData != null ? bulletData.Target : null;
_attackPayload = bulletData?.AttackPayload?.Clone() ?? new AttackPayload();
_speed = bulletData != null && bulletData.Speed > 0f ? bulletData.Speed : _defaultSpeed;
_runtimeMaxLifetime = bulletData != null && bulletData.MaxLifetime > 0f ? bulletData.MaxLifetime : _maxLifetime;
_lifetime = 0f;
_despawnRequested = false;
_isRunning = _target != null;
if (_isRunning)
{
Vector2 initialDirection = (Vector2)(_target.position - transform.position);
RotateToDirection(initialDirection);
}
if (!_isRunning)
{
_despawnRequested = true;
}
}
public void OnReset()
{
_target = null;
_speed = 0f;
_attackPayload = new AttackPayload();
_lifetime = 0f;
_runtimeMaxLifetime = _maxLifetime;
_isRunning = false;
_despawnRequested = false;
}
public void Tick(float deltaTime)
{
if (!_isRunning)
{
return;
}
_lifetime += Mathf.Max(0f, deltaTime);
if (_lifetime >= _runtimeMaxLifetime || _target == null)
{
_isRunning = false;
_despawnRequested = true;
return;
}
Vector3 currentPosition = transform.position;
Vector2 toTarget = (Vector2)(_target.position - currentPosition);
float hitDistanceSquared = _hitDistance * _hitDistance;
if (toTarget.sqrMagnitude <= hitDistanceSquared)
{
HitTarget();
return;
}
Vector2 direction = toTarget.normalized;
RotateToDirection(direction);
float moveDistance = _speed * Mathf.Max(0f, deltaTime);
if (moveDistance * moveDistance >= toTarget.sqrMagnitude)
{
Vector3 targetPosition = _target.position;
transform.position = new Vector3(targetPosition.x, targetPosition.y, currentPosition.z);
HitTarget();
return;
}
transform.position += (Vector3)(direction * moveDistance);
}
public bool TryConsumeDespawnRequest()
{
if (!_despawnRequested)
{
return false;
}
_despawnRequested = false;
return true;
}
private void HitTarget()
{
if (_target == null)
{
_isRunning = false;
_despawnRequested = true;
return;
}
MonoBehaviour[] components = _target.GetComponentsInParent<MonoBehaviour>();
foreach (var mono in components)
{
if (mono is IDamageReceiver damageReceiver)
{
damageReceiver.TakeDamage(_attackPayload);
break;
}
}
_isRunning = false;
_despawnRequested = true;
}
private void RotateToDirection(Vector2 direction)
{
if (direction.sqrMagnitude <= Mathf.Epsilon)
{
return;
}
float targetZAngle = Vector2.SignedAngle(Vector2.up, direction);
transform.rotation = Quaternion.Euler(0f, 0f, targetZAngle);
}
}
}
+128
View File
@@ -0,0 +1,128 @@
using GeometryTD.Definition;
using GeometryTD.Entity.EntityData;
using GeometryTD;
using GeometryTD.Entity;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class ShooterMuzzleComp : MonoBehaviour
{
[SerializeField] [Min(1f)] private int _attackDamage = 100;
[SerializeField] private AttackMethodType _attackMethodType = AttackMethodType.NormalBullet;
[SerializeField] [Min(1)] private int _bulletTypeId = 501;
[SerializeField] private Transform _muzzlePoint;
[SerializeField] [Min(0.1f)] private float _bulletSpeed = 12f;
[SerializeField] private SpriteRenderer _renderer;
private TagRuntimeData[] _tagRuntimes = System.Array.Empty<TagRuntimeData>();
public int AttackDamage => _attackDamage;
public AttackMethodType AttackMethodType => _attackMethodType;
public void OnInit(
int attackDamage,
AttackMethodType attackMethodType = AttackMethodType.NormalBullet,
int bulletTypeId = 501,
TagRuntimeData[] tagRuntimes = null)
{
_attackDamage = Mathf.Max(1, attackDamage);
_attackMethodType = attackMethodType;
_bulletTypeId = Mathf.Max(1, bulletTypeId);
_tagRuntimes = CloneTagRuntimes(tagRuntimes);
}
public void OnReset()
{
_attackDamage = 1;
_attackMethodType = AttackMethodType.None;
_bulletTypeId = 501;
_tagRuntimes = System.Array.Empty<TagRuntimeData>();
}
public void SetColor(Color color)
{
if (_renderer != null)
{
_renderer.color = color;
}
}
public bool Attack(Transform target)
{
return Attack(target, AttackPropertyType.None);
}
public bool Attack(Transform target, AttackPropertyType attackPropertyType)
{
if (_attackMethodType != AttackMethodType.NormalBullet)
{
return false;
}
if (target == null)
{
return false;
}
Transform spawnPoint = _muzzlePoint != null ? _muzzlePoint : transform;
int bulletEntityId = GameEntry.Entity.GenerateSerialId();
AttackPayload attackPayload = new AttackPayload
{
BaseDamage = _attackDamage,
AttackPropertyType = attackPropertyType,
SourceEntityId = ResolveSourceEntityId(),
ProjectileEntityId = bulletEntityId,
OriginPosition = spawnPoint.position,
TagRuntimes = CloneTagRuntimes(_tagRuntimes)
};
BulletData bulletData = new BulletData(
bulletEntityId,
_bulletTypeId,
spawnPoint.position,
target,
attackPayload,
_bulletSpeed);
GameEntry.Entity.ShowBullet(bulletData);
return true;
}
private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes)
{
if (tagRuntimes == null || tagRuntimes.Length <= 0)
{
return System.Array.Empty<TagRuntimeData>();
}
TagRuntimeData[] cloned = new TagRuntimeData[tagRuntimes.Length];
for (int i = 0; i < tagRuntimes.Length; i++)
{
TagRuntimeData runtime = tagRuntimes[i];
if (runtime == null)
{
continue;
}
cloned[i] = new TagRuntimeData
{
TagType = runtime.TagType,
TotalStack = runtime.TotalStack
};
}
return cloned;
}
private int ResolveSourceEntityId()
{
TowerEntity towerEntity = GetComponentInParent<TowerEntity>();
return towerEntity != null ? towerEntity.Id : 0;
}
}
}
+359
View File
@@ -0,0 +1,359 @@
using GeometryTD.Definition;
using GeometryTD.Entity;
using UnityEngine;
namespace Components
{
[DisallowMultipleComponent]
public class TowerController : MonoBehaviour
{
private const string AttackRangeIndicatorObjectName = "AttackRangeIndicator";
private const int MinTowerLevel = 0;
private const int MaxTowerLevel = 4;
private static Material _attackRangeSharedMaterial;
[SerializeField] private ShooterMuzzleComp _muzzleComp;
[SerializeField] private BasicBearingComp _bearingComp;
[SerializeField] private BasicBaseComp _baseComp;
[SerializeField] private Transform _scanOrigin;
[SerializeField] [Min(0.02f)] private float _retargetInterval = 0.1f;
[SerializeField] private LineRenderer _attackRangeRenderer;
[SerializeField] [Min(12)] private int _attackRangeSegments = 64;
[SerializeField] [Min(0.005f)] private float _attackRangeLineWidth = 0.08f;
[SerializeField] private Color _attackRangeColor = new Color(0.1f, 1f, 0.4f, 0.8f);
[SerializeField] private float _attackRangeZOffset = -0.01f;
private Transform _currentTarget;
private float _retargetTimer;
private float _attackRange;
private int _towerLevel;
private Color _muzzleColor = Color.white;
private Color _bearingColor = Color.white;
private Color _baseColor = Color.white;
private TagRuntimeData[] _tagRuntimes = System.Array.Empty<TagRuntimeData>();
public Transform CurrentTarget => _currentTarget;
private void Awake()
{
ResolveComponents();
}
public void OnInit(TowerStatsData stats)
{
OnInit(stats, MinTowerLevel);
}
public void OnInit(TowerStatsData stats, int towerLevel, Color muzzleColor, Color bearingColor, Color baseColor)
{
SetComponentColors(muzzleColor, bearingColor, baseColor);
OnInit(stats, towerLevel);
}
public void OnInit(TowerStatsData stats, int towerLevel)
{
ResolveComponents();
if (stats == null)
{
return;
}
_towerLevel = Mathf.Clamp(towerLevel, MinTowerLevel, MaxTowerLevel);
int attackDamage = ResolveIntValue(stats.AttackDamage, _towerLevel, 1, 1);
float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f);
float attackRange = ResolveFloatValue(stats.AttackRange, _towerLevel, 5f, 0.1f);
float attackSpeed = ResolveFloatValue(stats.AttackSpeed, _towerLevel, 1f, 0.01f);
_tagRuntimes = ResolveRuntimeTags(stats);
_muzzleComp?.OnInit(attackDamage, stats.AttackMethodType, tagRuntimes: _tagRuntimes);
_bearingComp?.OnInit(rotateSpeed, attackRange);
_baseComp?.OnInit(attackSpeed, stats.AttackPropertyType);
ApplyComponentColors();
SetAttackRange(attackRange);
SetAttackRangeVisible(false);
_currentTarget = null;
_retargetTimer = 0f;
}
public void OnReset()
{
SetAttackRangeVisible(false);
_currentTarget = null;
_retargetTimer = 0f;
_towerLevel = MinTowerLevel;
_tagRuntimes = System.Array.Empty<TagRuntimeData>();
_muzzleComp?.OnReset();
_bearingComp?.OnReset();
_baseComp?.OnReset();
}
public void SetAttackRangeVisible(bool visible)
{
EnsureAttackRangeRenderer();
if (_attackRangeRenderer != null)
{
_attackRangeRenderer.enabled = visible;
}
}
public void SetAttackRange(float range)
{
_attackRange = Mathf.Max(0.05f, range);
EnsureAttackRangeRenderer();
RebuildAttackRangeGeometry();
}
public void SetComponentColors(Color muzzleColor, Color bearingColor, Color baseColor)
{
_muzzleColor = muzzleColor;
_bearingColor = bearingColor;
_baseColor = baseColor;
ApplyComponentColors();
}
public void SetTarget(Transform target)
{
_currentTarget = target;
}
public void ClearTarget()
{
_currentTarget = null;
}
public void OnUpdate(float deltaTime)
{
if (!HasCoreComponents())
{
return;
}
if (!IsTargetValid(_currentTarget) || !_bearingComp.IsTargetInRange(_currentTarget, _scanOrigin))
{
TryRetarget(deltaTime);
}
if (_currentTarget == null)
{
_baseComp.Tick(deltaTime);
return;
}
_baseComp.TryAttack(_bearingComp, _muzzleComp, _currentTarget, deltaTime);
}
private void TryRetarget(float deltaTime)
{
_retargetTimer -= Mathf.Max(0f, deltaTime);
if (_retargetTimer > 0f)
{
return;
}
_retargetTimer = _retargetInterval;
_currentTarget = FindNearestEnemyTarget();
}
private Transform FindNearestEnemyTarget()
{
Vector3 origin = _scanOrigin != null ? _scanOrigin.position : transform.position;
float maxRange = _bearingComp.AttackRange;
float maxRangeSquared = maxRange * maxRange;
EnemyEntity bestEnemy = null;
float bestDistanceSquared = float.MaxValue;
foreach (var enemy in EnemyEntity.ActiveEnemies)
{
if (enemy == null || !enemy.isActiveAndEnabled)
{
continue;
}
Transform enemyTransform = enemy.transform;
Vector3 delta = enemyTransform.position - origin;
float distanceSquared = delta.sqrMagnitude;
if (distanceSquared > maxRangeSquared || distanceSquared >= bestDistanceSquared)
{
continue;
}
bestDistanceSquared = distanceSquared;
bestEnemy = enemy;
}
return bestEnemy != null ? bestEnemy.transform : null;
}
private void ApplyComponentColors()
{
_muzzleComp?.SetColor(_muzzleColor);
_bearingComp?.SetColor(_bearingColor);
_baseComp?.SetColor(_baseColor);
}
private void ResolveComponents()
{
if (_muzzleComp == null)
{
_muzzleComp = GetComponent<ShooterMuzzleComp>();
}
if (_bearingComp == null)
{
_bearingComp = GetComponent<BasicBearingComp>();
}
if (_baseComp == null)
{
_baseComp = GetComponent<BasicBaseComp>();
}
ApplyComponentColors();
EnsureAttackRangeRenderer();
}
private bool HasCoreComponents()
{
return _muzzleComp != null && _bearingComp != null && _baseComp != null;
}
private static bool IsTargetValid(Transform target)
{
return target != null && target.gameObject.activeInHierarchy;
}
private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] source)
{
if (source == null || source.Length <= 0)
{
return System.Array.Empty<TagRuntimeData>();
}
TagRuntimeData[] cloned = new TagRuntimeData[source.Length];
for (int i = 0; i < source.Length; i++)
{
TagRuntimeData runtime = source[i];
if (runtime == null)
{
continue;
}
cloned[i] = new TagRuntimeData
{
TagType = runtime.TagType,
TotalStack = runtime.TotalStack
};
}
return cloned;
}
private static TagRuntimeData[] ResolveRuntimeTags(TowerStatsData stats)
{
if (stats == null)
{
return System.Array.Empty<TagRuntimeData>();
}
if (stats.TagRuntimes != null && stats.TagRuntimes.Length > 0)
{
return CloneTagRuntimes(stats.TagRuntimes);
}
return TowerTagAggregationService.BuildRuntimeTagsFromUniqueTags(stats.Tags);
}
private static int ResolveIntValue(int[] values, int level, int fallback, int minValue)
{
int resolved = Mathf.Max(minValue, fallback);
if (values == null || values.Length <= 0)
{
return resolved;
}
int index = Mathf.Clamp(level, 0, values.Length - 1);
return Mathf.Max(minValue, values[index]);
}
private static float ResolveFloatValue(float[] values, int level, float fallback, float minValue)
{
float resolved = Mathf.Max(minValue, fallback);
if (values == null || values.Length <= 0)
{
return resolved;
}
int index = Mathf.Clamp(level, 0, values.Length - 1);
return Mathf.Max(minValue, values[index]);
}
private void EnsureAttackRangeRenderer()
{
if (_attackRangeRenderer == null)
{
Transform indicatorTransform = transform.Find(AttackRangeIndicatorObjectName);
if (indicatorTransform == null)
{
GameObject indicatorObject = new GameObject(AttackRangeIndicatorObjectName);
indicatorTransform = indicatorObject.transform;
indicatorTransform.SetParent(transform, false);
}
_attackRangeRenderer = indicatorTransform.GetComponent<LineRenderer>();
if (_attackRangeRenderer == null)
{
_attackRangeRenderer = indicatorTransform.gameObject.AddComponent<LineRenderer>();
}
}
_attackRangeRenderer.useWorldSpace = false;
_attackRangeRenderer.loop = true;
_attackRangeRenderer.positionCount = Mathf.Max(12, _attackRangeSegments);
_attackRangeRenderer.widthMultiplier = _attackRangeLineWidth;
_attackRangeRenderer.startColor = _attackRangeColor;
_attackRangeRenderer.endColor = _attackRangeColor;
_attackRangeRenderer.numCapVertices = 4;
_attackRangeRenderer.numCornerVertices = 4;
_attackRangeRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
_attackRangeRenderer.receiveShadows = false;
_attackRangeRenderer.allowOcclusionWhenDynamic = false;
_attackRangeRenderer.enabled = false;
if (_attackRangeSharedMaterial == null)
{
Shader lineShader = Shader.Find("Sprites/Default");
if (lineShader != null)
{
_attackRangeSharedMaterial = new Material(lineShader);
}
}
if (_attackRangeSharedMaterial != null)
{
_attackRangeRenderer.sharedMaterial = _attackRangeSharedMaterial;
}
}
private void RebuildAttackRangeGeometry()
{
if (_attackRangeRenderer == null)
{
return;
}
int segmentCount = Mathf.Max(12, _attackRangeSegments);
_attackRangeRenderer.positionCount = segmentCount;
float stepAngle = Mathf.PI * 2f / segmentCount;
for (int i = 0; i < segmentCount; i++)
{
float angle = stepAngle * i;
float x = Mathf.Cos(angle) * _attackRange;
float y = Mathf.Sin(angle) * _attackRange;
_attackRangeRenderer.SetPosition(i, new Vector3(x, y, _attackRangeZOffset));
}
}
}
}