안녕하세요.😎
오늘은 Enemy에 관련된 작업 중 포탑(Turret)의 공격과 Enemy의 Hp가 줄어드는 작업을 할 예정입니다.
바로 코드부터 보겠습니다.
TurretAttackObject Script
이 스크립트는 포탑(Turret)가 발사하는 오브젝트를 제어하는 역할입니다.
using UnityEngine;
public class TurretAttackObject : MonoBehaviour
{
[SerializeField] private TurrentStats turrentStats;
[SerializeField] private float moveSpeed = 10.0f;
private Transform targetTransform;
public void Initialize(Transform target)
{
targetTransform = target;
}
private void Update()
{
if (targetTransform == null)
{
Destroy(gameObject);
return;
}
Vector3 direction = targetTransform.position - transform.position;
float step = moveSpeed * Time.deltaTime;
if (direction.magnitude <= step)
{
HitTarget(targetTransform.gameObject);
return;
}
transform.Translate(direction.normalized * step, Space.World);
}
private void HitTarget(GameObject target)
{
print(target.name);
target.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
}
private void OnTriggerEnter(Collider other)
{
if (other.transform == targetTransform)
{
other.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
gameObject.SetActive(false);
}
}
}
주 목적은 다음과 같습니다.
1) 타겟을 향해 직선으로 이동
2) 타겟과 충돌 시 대미지 적용
3) 타겟이 사라지면 발사체 정리
1. 변수 선언부
[SerializeField] private TurrentStats turretStats;
[SerializeField] private float moveSpeed = 10.0f;
private Transform targetTransform;
turretStats : 타워의 공격력 정보를 가져오기 위해 사용
moveSpeed : 발사체 속도 사용
targetTransform : 발사체가 따라갈 타깃
2. 타깃 초기화
public void Initialize(Transform target)
{
targetTransform = target;
}
발사체를 생성할 때 어떤 적을 목표로 삼을지 설정
3. 이동 로직
private void Update()
{
if (targetTransform == null)
{
Destroy(gameObject);
return;
}
Vector3 direction = targetTransform.position - transform.position;
float step = moveSpeed * Time.deltaTime;
if (direction.magnitude <= step)
{
HitTarget(targetTransform.gameObject);
return;
}
transform.Translate(direction.normalized * step, Space.World);
}
1) 타깃이 없으면 발사체 제거 (Destory)
2) 타깃까지의 방향벡터 계산 (direction)
3) 한 프레임 이동 거리 계산 (step)
4) 타깃과 너무 가까우면 바로 타격 적용 (HitTarget)
5) 아니면 transform.Translate로 타겟 방향으로 이동
4. 타격 처리
private void HitTarget(GameObject target)
{
print(target.name);
target.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
}
1) 타깃에 데미지 적용
2) 로그를 찍어 디버깅 가능
3) 타겟 자체는 여기서 비활성화/삭제하지 않고 충돌에서 처리
5. 충돌 처리
private void OnTriggerEnter(Collider other)
{
if (other.transform == targetTransform)
{
other.GetComponent<EnumyStats>().TakeDamage(turrentStats.damage);
gameObject.SetActive(false);
}
}
1) Collider 기반 충돌 처리
2) 타깃과 충돌 시 타깃에게 대미지 적용
TurretController Scirpt
이 스크립트는 포탑(Turret) 자체를 제어합니다.
using UnityEngine;
public class TurretController : MonoBehaviour
{
[SerializeField] private TurretAttackObject turretAttackObject;
[Header("Attack Settings")]
[SerializeField] private float attackRange = 5.0f;
[SerializeField] private float attackInterval = 2.0f;
[SerializeField] private GameObject projectile;
[SerializeField] private Transform firePoint;
[Header("Visuals")]
[SerializeField] private LineRenderer targetingLine;
private float nextAttackTime = 0f;
private GameObject currentTarget;
private void Update()
{
if (currentTarget == null || Vector3.Distance(transform.position, currentTarget.transform.position) > attackRange)
{
AcquireTarget();
}
UpdateTargetingLine();
if (currentTarget != null && Time.time >= nextAttackTime)
{
FireAtTarget();
nextAttackTime = Time.time + attackInterval;
}
}
private void AcquireTarget()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enumy");
float closestDistance = float.MaxValue;
GameObject nearestTarget = null;
foreach (var enemy in enemies)
{
float distance = Vector3.Distance(transform.position, enemy.transform.position);
if (distance <= attackRange && distance < closestDistance)
{
closestDistance = distance;
nearestTarget = enemy;
}
}
currentTarget = nearestTarget;
}
private void UpdateTargetingLine()
{
if (currentTarget != null)
{
targetingLine.enabled = true;
targetingLine.SetPosition(0, firePoint.position);
targetingLine.SetPosition(1, currentTarget.transform.position);
}
else
{
targetingLine.enabled = false;
}
}
private void FireAtTarget()
{
projectile.transform.position = firePoint.position;
projectile.transform.rotation = Quaternion.identity;
projectile.gameObject.SetActive(true);
if (projectile != null)
{
turretAttackObject.Initialize(currentTarget.transform);
}
}
}
주목적은 다음과 같습니다.
1) 적을 탐지하고 타깃 지정
2) 타깃을 향해 발사체 발사
3) 시각적으로 공격 범위를 표현
1. 변수 선언부
[SerializeField] private TurretAttackObject turretAttackObject;
[Header("Attack Settings")]
[SerializeField] private float attackRange = 5.0f;
[SerializeField] private float attackInterval = 2.0f;
[SerializeField] private GameObject projectile;
[SerializeField] private Transform firePoint;
[Header("Visuals")]
[SerializeField] private LineRenderer targetingLine;
private float nextAttackTime = 0f;
private GameObject currentTarget;
turretAttackObject : 발사체 스크립트 참조
attackRange : 타겟 감지 거리
attackInterval : 공격 간격
projectile : 발사체 오브젝트
firePoint : 발사 위치
targetingLine : 발사체가 타겟을 조준하는 시각 선
nextAttackTime : 공격 쿨타운 체크
currentTarget : 현재 공격할 적
2. 타겟 관리 및 공격
private void Update()
{
if (currentTarget == null || Vector3.Distance(transform.position, currentTarget.transform.position) > attackRange)
{
AcquireTarget();
}
UpdateTargetingLine();
if (currentTarget != null && Time.time >= nextAttackTime)
{
FireAtTarget();
nextAttackTime = Time.time + attackInterval;
}
}
1) 타깃이 없거나 공격 범위를 벗어나면 새 타겟 탐색 (AcquireTarget())
2) 시각적으로 타겟 표시 (UpdateTargetingLine())
3) 쿨다운 체크 후 발사 (FireAtTarget())
3. 타겟 획득
private void AcquireTarget()
{
GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enumy");
float closestDistance = float.MaxValue;
GameObject nearestTarget = null;
foreach (var enemy in enemies)
{
float distance = Vector3.Distance(transform.position, enemy.transform.position);
if (distance <= attackRange && distance < closestDistance)
{
closestDistance = distance;
nearestTarget = enemy;
}
}
currentTarget = nearestTarget;
}
1) 모든 적(Enumy 태그) 탐색
2) 타워 기준 가장 가까운 적을 선택
3) 공격 범위를 넘어가면 타겟 재선정
4. 타겟 라인 표시
private void UpdateTargetingLine()
{
if (currentTarget != null)
{
targetingLine.enabled = true;
targetingLine.SetPosition(0, firePoint.position);
targetingLine.SetPosition(1, currentTarget.transform.position);
}
else
{
targetingLine.enabled = false;
}
}
1) 타겟이 있으면 LineRenderer로 조준선 표시
2) 타겟이 없으면 라인 비활성화
5. 발사 처리
private void FireAtTarget()
{
projectile.transform.position = firePoint.position;
projectile.transform.rotation = Quaternion.identity;
projectile.gameObject.SetActive(true);
if (projectile != null)
{
turretAttackObject.Initialize(currentTarget.transform);
}
}
1) 발사체 위치를 발사 지점으로 이동
2) 발사체 활성화
3) TurretAttackObject를 통해 타겟 방향으로 이동하도록 초기화
전체 흐름
타겟이 없거나 범위 밖 → AcquireTarget()
타겟이 있으면 → UpdateTargetingLine()으로 시각화
쿨다운 체크 → FireAtTarget()으로 발사
발사체가 TurretAttackObject에서 이동 및 충돌 처리
마지막으로..
여러 가지 문제들이 많았는데, 그중 가장 기억에 남는 게
플레이어가 기본공격을 할 때 포탑에 의해 Enemy가 사라지면
플레이어의 공격 오브젝트가 멈춰있는 문제가 있었습니다.
오브젝트 재사용 문제
우선 결론적으로 플레이어의 기본공격 오브젝트의 움직임과 충돌처리를 담당하는
기존에 작성했던 PlayerTargetObject입니다.
using UnityEngine;
public class PlayerTargetObject : MonoBehaviour
{
[Header("Components")]
[SerializeField] private Transform targetEnumyTr;
[SerializeField] private Rigidbody basicAttackRb;
[SerializeField] private PlayerStats playerStats;
[Header("Setting Value")]
[SerializeField] private float basicAttackSpeed = 0f;
void FixedUpdate()
{
if (targetEnumyTr != null)
{
Vector3 direction = targetEnumyTr.position - transform.position;
basicAttackRb.velocity = direction.normalized * basicAttackSpeed;
}
else
{
gameObject.SetActive(false);
}
}
public void SetTarget(Transform newTarget)
{
targetEnumyTr = newTarget;
}
void OnTriggerEnter(Collider other)
{
if (targetEnumyTr != null && ReferenceEquals(other.gameObject, targetEnumyTr.gameObject))
{
EnumyStats enumyStats = targetEnumyTr.GetComponent<EnumyStats>();
enumyStats?.TakeDamage(playerStats.playerDamage);
targetEnumyTr = null;
this.gameObject.SetActive(false);
}
}
}
공격 모션을 플레이어가 취하고 오브젝트가 생성되었을 때, 포탑이나 혹은 다른 요소로 인하여 Enemy가 사라진다면
비활성화를 되게 로직을 구현하였습니다.
정말 단순한 문제였지만, 접근을 잘못하여서 OnEnable() 함수를 활용하여
오브젝트가 활성화될 때 먼저 모든 오브젝트를 비활성화하는 로직부터 시작해서..
사용자가 마우스 오른쪽(1) 클릭을 할 때 Enemy의 Transform을 받아오는 로직까지
수정을 하려고 했었습니다..
수정 도중 잘 해결이 되지 않아서
잠시 기분 전환을 하고 다시 스크립트를 살펴봤는데
문득 '다 필요 없고 이거 한 줄이면 되는 거 아닌가?'라는 생각에
시도를 해봤는데 해결되었습니다.😂
감사합니다.😁