레이저 포탑에게 공격받는 적은 일정 시간마다 데미지를 받도록 구현한다. 초당 입힐 데미지를 변수로 선언한 후 Laser에 데미지 입히는 로직을 구현한다.
public int damageOverTime = 30;
void UpdateTarget() //가장 가까운 적을 찾아 목표로 업데이트
{
//매 프레임마다 모든 적을 확인하면서 업데이트하면 성능 시간 낭비
//"1초에 2번"과 같이 검색 횟수를 제한, 타겟을 가지고 있지 않은 경우에만 탐색하는 등의 방법이 가능
//0.5초에 한번 실행 되도록 start에 InvokeRepeating을 실행
GameObject[] enenmies = GameObject.FindGameObjectsWithTag(enemyTag); //태그에 enemyTag인 오브젝트를 모두 탐색
float shortestDistance = Mathf.Infinity; //최소거리를 구하기 위한 초기값을 Infinity로 설정
GameObject nearestEnemy = null;
foreach(GameObject enemy in enenmies)
{
//적과 내 거리를 구함
float distanceToEnemy = Vector3.Distance(transform.position, enemy.transform.position);
if(distanceToEnemy < shortestDistance) // 더 가까운 적을 찾았다면 nearestEnemy를 최단 거리 업데이트 후 해당 오브젝트로 설정
{
shortestDistance = distanceToEnemy;
nearestEnemy = enemy;
}
}
if (nearestEnemy != null && shortestDistance <= range) //적을 찾았고 + 사거리 안에 들어왔다면
{
target = nearestEnemy.transform; //이제 목표 오브젝트를 미리 찾아놓은 적으로 설정
targetEnemy = nearestEnemy.GetComponent<Enemy>();
}
else
{
target = null; //만족하지 않으면 target을 null로 초기화
}
}
void Laser() //레이저 그리기
{
//---- Damage ----
targetEnemy.TakeDamage(damageOverTime * Time.deltaTime);
~~~(생략)~~~
}
둔화 효과 구현에 앞서 기존 Enemy 코드 내에서 움직임을 담당하는 부분을 EnemyMovement라는 스크립트를 생성하고 해당 부분으로 가져와 구현해서 모듈화를 해준다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(Enemy))]
public class EnemyMovement : MonoBehaviour
{
private Transform target; //목표 방향
private int wavepointIndex = 0;//현재 목표로하는 웨이포인트 인덱스
private Enemy enemy;
private void Start()
{
enemy = GetComponent<Enemy>();
//WayPoints의 points를 static으로 선언해놨기 때문에 바로 불러올 수 있다.
//points를 싱글톤 디자인 패턴으로 사용하는 모습
target = WayPoints.points[0];
}
void Update()
{
//이동해야 할 방향 ( 목표 위치 - 내 위치 )
Vector3 dir = target.position - transform.position;
//방향 벡터로 스피드 만큼 이동
//방향을 단위벡터로 바꾸기 위해 normalized 진행 후 speed를 곱한만큼 진행 (프레임-시간 보정으로 deltaTime을 사용)
transform.Translate(dir.normalized * enemy.speed * Time.deltaTime, Space.World); //World Space에서 이동
//웨이포인트 도착 시 다음 웨이포인트로 변경
if (Vector3.Distance(transform.position, target.position) <= 0.4f) // 웨이 포인트와 에너미의 거리가 0.4 이하면 다음 웨이 포인트로
{
GetNextWayPoint(); //다음 웨이포인트를 타겟으로 변경
}
}
void GetNextWayPoint()
{
if (wavepointIndex >= WayPoints.points.Length - 1) //만약 가지고 있는 모든 웨이포인트를 방문 > 도착 지점 도달
{
EndPath();
return; //아래의 다음 웨이포인트 가져오는 것을 하지 않고 바로 종료
}
wavepointIndex++; //다음 웨이포인트 인덱스
target = WayPoints.points[wavepointIndex]; //다음 인덱스의 웨이포인트 오브젝트를 받아온다.
}
void EndPath() //경로의 끝(목표)에 도달
{
Destroy(gameObject); //도착지점 도착 시 오브젝트 파괴
PlayerStats.Lives--; // lives를 1 감소
}
}
[RequireComponent(typeof(Enemy))] 를 통해 EnemyMovement 스크립트가 정상적으로 작동하기 위해서는 Enemy 컴포넌트가 반드시 있어야한다고 선언해주는 것이다. 이를 이용해 Enemy의 여러 변수를 읽어올 때 Enemy 컴포넌트가 없어서 생기는 오류를 방지할 수 있다..
위와 같이 EnemyMovement로 기존 Enemy의 움직임을 모두 모듈화 시켰다면 Turret 스크립트는 다음과 같다.
Turret.cs
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class Turret : MonoBehaviour
{
public Transform target; // 공격할목표 오브젝트
private Enemy targetEnemy;
[Header("General")]
public float range = 15f; // 사거리는 15로 설정
[Header("Use Bullets (default)")]
public GameObject bulletPrefab; //총알 프리팹
public float fireRate = 1f; //초당 발사하는 탄의 개수 (공격 속도)
private float fireCountdown = 0f; //fireRate에 맞게 공격하도록 fireCountdown을 설정한 후 해당 주기마다 공격
[Header("Use Laser (default)")]
public bool useLaser = false; //레이저를 사용하는 포탑인가? (기본값은 False)
public int damageOverTime = 30;
public LineRenderer lineRenderer; //레이저를 사용하면 라인 렌더러가 필요함
public ParticleSystem impactEffect; //레이저 이펙트
public Light impactLight; // 조명 이펙트
[Header("Unity Setup Fields")]
public string enemyTag = "Enemy";
public Transform partToRotate; //실제로 base를 제외하고 회전될 오브젝트의 트랜스폼
public float turnSpeed = 10f;
public Transform firePoint; //총알이 복사되어 생성될 위치
// Start is called before the first frame update
void Start()
{
InvokeRepeating("UpdateTarget", 0f, 0.5f); //0f에 시작해서 0.5f마다 반복호출
}
void UpdateTarget() //가장 가까운 적을 찾아 목표로 업데이트
{
//매 프레임마다 모든 적을 확인하면서 업데이트하면 성능 시간 낭비
//"1초에 2번"과 같이 검색 횟수를 제한, 타겟을 가지고 있지 않은 경우에만 탐색하는 등의 방법이 가능
//0.5초에 한번 실행 되도록 start에 InvokeRepeating을 실행
GameObject[] enenmies = GameObject.FindGameObjectsWithTag(enemyTag); //태그에 enemyTag인 오브젝트를 모두 탐색
float shortestDistance = Mathf.Infinity; //최소거리를 구하기 위한 초기값을 Infinity로 설정
GameObject nearestEnemy = null;
foreach(GameObject enemy in enenmies)
{
//적과 내 거리를 구함
float distanceToEnemy = Vector3.Distance(transform.position, enemy.transform.position);
if(distanceToEnemy < shortestDistance) // 더 가까운 적을 찾았다면 nearestEnemy를 최단 거리 업데이트 후 해당 오브젝트로 설정
{
shortestDistance = distanceToEnemy;
nearestEnemy = enemy;
}
}
if (nearestEnemy != null && shortestDistance <= range) //적을 찾았고 + 사거리 안에 들어왔다면
{
target = nearestEnemy.transform; //이제 목표 오브젝트를 미리 찾아놓은 적으로 설정
targetEnemy = nearestEnemy.GetComponent<Enemy>();
}
else
{
target = null; //만족하지 않으면 target을 null로 초기화
}
}
// Update is called once per frame
void Update()
{
if (target == null) {//타겟이 없으면
if(useLaser) //레이저 포탑은 레이저를 꺼줘야 함 ( 라인 렌더러를 지워줘야 함)
{
if (lineRenderer.enabled)
{
lineRenderer.enabled = false; //라인 렌더러 컴포넌트를 비활성화
impactEffect.Stop(); //이펙트 종료
impactLight.enabled = false; //Light 비활성화
}
}
return;
}
//--- 만약 target이 있다면 ---
//target Lock On
LockOnTarget();
if(useLaser) //레이저를 사용하는 포탑의 경우
{
Laser(); //레이저 그리기
}
else //일반 총알을 사용하는 경우
{
if (fireCountdown <= 0f)
{ //카운트 다운이 0이 되면 shoot 발사
Shoot();
fireCountdown = 1f / fireRate; // 1초에 fireRate 만큼 발사되도록 Countdown 설정
}
fireCountdown -= Time.deltaTime; //카운트 다운을 계속 줄여서 shoot이 반복되도로 ㄱ설정
}
}
void LockOnTarget()
{
//target Lock On
Vector3 dir = target.position - transform.position; //목표 방향 = 타겟 위치 - 내 위치
Quaternion lookRotation = Quaternion.LookRotation(dir); //dir 방향을 보도록 회전하는 정도
//유니티는 x, y, z를 오일러 각도를 기준으로 사용하고 있다.
//Vector3 rotation = lookRotation.eulerAngles; //따라서 우리가 원하는회전을 오일러 각도로 변환해준다.
//윗줄 코드를 아래의 부드럽게 회전하는 코드로 수정
Vector3 rotation = Quaternion.Lerp(partToRotate.rotation, lookRotation, Time.deltaTime * turnSpeed).eulerAngles;
//partToRate의 회전에서 lookRotation의 회전까지 turnSpeed 단위로 변경되면서 회전을 내보내면 해당 회전을 오일러 각도로 변환해서 rotation Vector에 저장함.
//y축을 중심으로만 회전하기를 원하기 때문에 y회전 정도만 불러와서 사용한다.
partToRotate.rotation = Quaternion.Euler(0f, rotation.y, 0f);
}
void Laser() //레이저 그리기
{
//---- Damage ----
targetEnemy.TakeDamage(damageOverTime * Time.deltaTime);
//----- Lase Graphic -----
if (!lineRenderer.enabled)
{
lineRenderer.enabled = true; //레이저(라인 렌더러)가 꺼져있으면 키고 나서 위치 설정
impactEffect.Play(); //파티클 시스템 재생
impactLight.enabled = true; //Light 활성화
}
lineRenderer.SetPosition(0, firePoint.position); //시작점을 Fire Point로
lineRenderer.SetPosition(1, target.position); //끝점을 Fire Point로
Vector3 dir = firePoint.position - target.position; //총구로부터 타겟으로의 방향 벡터
impactEffect.transform.position = target.position + dir.normalized * 1f; //파티클 위치를 타겟 위치에서 살짝 터렛 방향으로 이동 시킨 위치로 생성
//레이저 이팩트가 적의 표면에 위치하도록 (중심에 위치X)
impactEffect.transform.rotation = Quaternion.LookRotation(dir); //이펙트의 각도를 해당 방향 벡터로 설정
}
void Shoot()
{
GameObject bulletGO = (GameObject)Instantiate(bulletPrefab, firePoint.position, firePoint.rotation);
Bullet bullet = bulletGO.GetComponent<Bullet>(); //복사한 bullet 오브젝트에서 bullet 컴포넌트를 가져옴
if (bullet != null)
{
bullet.Seek(target); //bullet이 존재한다면 target 오브젝트를 넘겨줌
}
}
private void OnDrawGizmosSelected() //기즈모를 그려주는 유니티 함수
{
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(transform.position, range); //내 위치를 기준으로 range를 반지름을 구를 그려줌
}
}
이제 EnemyMovement를 Enemy 프리팹의 컴포넌트에 추가해준 후 잘 작동하는지 확인한다.
레이저 포탑에게 공격받은 Enemy 오브젝트의 이동속도가 감소하도록 로직을 구현해보자.
void Laser() //레이저 그리기
{
//---- Damage ----
targetEnemy.TakeDamage(damageOverTime * Time.deltaTime); //초당 데미지 입력
targetEnemy.Slow(slowPct); //둔화 효과 실행
~~(생략)~~
}
public void Slow(float pct) { //둔화 효과
speed = startSpeed * (1f - pct); // (1 - 둔화율) * 현재 이동속도 = 이동속도
}
void Update()
{
//이동해야 할 방향 ( 목표 위치 - 내 위치 )
Vector3 dir = target.position - transform.position;
//방향 벡터로 스피드 만큼 이동
//방향을 단위벡터로 바꾸기 위해 normalized 진행 후 speed를 곱한만큼 진행 (프레임-시간 보정으로 deltaTime을 사용)
transform.Translate(dir.normalized * enemy.speed * Time.deltaTime, Space.World); //World Space에서 이동
//웨이포인트 도착 시 다음 웨이포인트로 변경
if (Vector3.Distance(transform.position, target.position) <= 0.4f) // 웨이 포인트와 에너미의 거리가 0.4 이하면 다음 웨이 포인트로
{
GetNextWayPoint(); //다음 웨이포인트를 타겟으로 변경
}
enemy.speed = enemy.startSpeed; //둔화 효과를 초기화하기 위한 속도 리셋
}
[HideInInspector] 인스펙터 창으로 변수가 노출되지 않도록 한다. ( public이면 다른 스크립트에서 여전히 접근 가능하다. )
위와 같은 로직의 기본 원리는 Turret에서 Target오브젝트, 즉 Enemy 오브젝트에게 slow라는 함수를 호출하도록 보내면, Enemy는 Slow를 실행한다. 그럼 Speed가 감소되게 되고, 이후에 EnemyMovement에서 감소된 Speed로 Update문이 돌아가면서 느린 속도로 프레임 단위 이동 후에 다시 원래 속도로 돌아온다. 이 때 중요한 점은 "그렇다면 EnemyMovement의 Update문이 먼저 돌고 Enemy의 Slow문이 돌아서 속도가 느려지기 전에 움직이면 둔화 효과가 없어지나요?"라고 할 수 있는데 실제로는 그렇지 않다. 여러 Update문이 동시에 돌아가는 것이 아니라 Unity에서 지정한 계층 구조의 순서대로 순차적으로 돌아가기 때문에 실제로는 문제가 없다. 만약 EnemyMovement가 먼저 실행되어 움직인 후에 속도가 느려진다고 하더라도 그 다음 프레임 연산에서 느려진 속도로 움직인 후에 속도 초기화, 다시 Slow가 반복되기 때문에 실제로는 영향이 없다.
만약에 위의 문제를 완전히 해결하고 싶다면, Edit > Project Settings에서 Scipt Excution Order에서 스크립트의 실행 순서를 미리 지정해줄 수 있다.
위와 같이 Enemy, EnemyMovement 간의 순서도 설정해줄 수 있다.