8. 공격 타워 구현
IGDC 입문 프로젝트입니다.
몬스터들이 침략하여 타워까지 도달하여 게임오버를 지난 글에서 구현했습니다.
당하고만 있을 순 없습니다! 공격 타워를 세워 몬스터들로부터 방어해봅시다.
우선 다음 에셋을 프리팹으로 만듭니다. (towerSquare_sampleB)
일단은 임의로 배치를 먼저 하도록 합시다.
배치를 한 후, Rigidbody와 Sphere Collider를 붙였습니다. Radisu를 통해 충돌 영역의 반지름을 조절할 수 있습니다.
이 타워도 Rigidbody의 Use Gravity 체크를 해제합니다.
그리고 AttackTower 스크립트를 생성합니다.
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class AttackTower : MonoBehaviour
{
// 공격 타워의 발사체 오브젝트입니다
public GameObject projectile;
// 타워의 공격 간격입니다
public float coolTime;
// 시간을 저장하는 변수입니다
private float currentTime;
// 공격 대상을 담는 큐입니다
private Queue<GameObject> monsters;
void Start() {
// 큐를 생성합니다
monsters = new();
}
void Update() {
// 몬스터가 범위 안에 있을 때만 실행
if (monsters.Count > 0) {
// 누적된 시간이 coolTime을 넘는다면 if문 안을 실행합니다
if (currentTime >= coolTime) {
// 여러 번 실행되지 않도록 0으로 다시 초기화합니다
currentTime = 0f;
// 발사체 생성은 아래 코드에서 합니다. 생성된 발사체는 shoot 변수에 담깁니다
var shoot = Instantiate(projectile, transform.position, Quaternion.identity);
// 발사체의 공격 대상을 monsters 큐에 담긴 첫번째 오브젝트로 지정합니다
// 이렇게 함으로써, 가장 먼저 들어온 몬스터를 공격대상으로 삼습니다
shoot.GetComponent<Projectile>().target = monsters.First();
}
// 공격이 불가하다면, 시간을 누적시킵니다
else {
currentTime += Time.deltaTime;
}
}
}
private void OnTriggerEnter(Collider other) {
// 만약 몬스터가 공격범위에 들어왔을 때, monsters에 담습니다
if (other.gameObject.CompareTag("Enemy")) {
monsters.Enqueue(other.gameObject);
}
}
private void OnTriggerExit(Collider other) {
if (other.gameObject.CompareTag("Enemy")) {
// 몬스터가 공격범위 밖으로 나갈 경우, 큐에서 제거합니다
monsters.Dequeue();
// 만일 큐가 전부 비었다면, 공격 관련 변수를 초기화합니다
if (monsters.Count == 0) {
currentTime = coolTime;
}
}
}
}
코드가 조금 복잡해보입니다. 그렇지만 괜찮습니다!
이 코드들은 다 배운 것들입니다. (Queue를 제외하면 말이죠)
아래와 같이 작성하고 타워 프리팹에 붙여줍시다.
주의할 점은 실행문이 Enemy 태그인지 검사한다는 것입니다. 만일 검사하지 않는다면, 발사체 또한 충돌로 감지해 여러 번 실행되는 문제가 발생할 수 있습니다. (Enemy 태그는 만들어줬습니다)
shoot의 자료형이 var입니다. var은 C++에서의 auto와 같이 자료형을 유추합니다.
간단히 이 코드를 설명하는 GIF를 만들어보았습니다. 조금 빠르니 주의해주세요.
범위 안으로 몬스터가 들어온다면, monsters에 유령을 Enqueue()로 담습니다. First는 공격대상이 됩니다.
만약 범위 밖으로 나간다면 Dequeue()로 First를 제거합니다.
물론 여기에는 문제가 있습니다. 만일 뒤에 따라오던 유령이 첫번째 유령을 앞질러간다면 Queue가 꼬이게 되겠죠.
이는 List를 통해 해결할 수 있습니다.
List를 사용한다면, 아래와 같이 변경해주면 됩니다.
// Queue를 List로 바꿔줍니다
private Queue<GameObject> monsters;
private List<GameObject> monsters;
// Enqueue를 Add로 바꿔줍니다
monsters.Enqueue(other.gameObject);
monsters.Add(other.gameObject);
// Dequeue를 Remove로 변경하고, 매개변수를 지정해줍니다
monsters.Dequeue();
monsters.Remove(other.gameObject);
생성된 발사체도 움직이는 코드가 있어야겠죠.
오브젝트를 생성하고, 프리팹화 시킨 후 아래 스크립트를 생성하고 붙여줍시다.
저의 경우에는 Cube 오브젝트를 하나 만들었습니다. Cube에 자동으로 Box Collider가 붙어있었으며, IsTrigger를 체크했습니다.
using UnityEngine;
public class Projectile : MonoBehaviour
{
public GameObject target;
public float speed;
private void Update() {
float step = speed * Time.deltaTime;
transform.position = Vector3.MoveTowards(transform.position, target.transform.position, step);
}
}
에디터에서 speed를 원하는 값으로 조정해줍시다. 단, 몬스터의 이동속도보다 느리다면 발사체가 쫓아가지 못하겠죠?
그리고 Enemy 스크립트를 다음과 같이 업데이트합니다.
private void OnTriggerEnter(Collider other) {
if (other.gameObject.CompareTag("AllyTower")) {
other.gameObject.GetComponent<AllyTower>().Hp--;
}
// 아래 코드를 추가합니다
else if (other.gameObject.CompareTag("Projectile")) {
// 몬스터(자신)의 체력을 1 감소시킵니다
Hp--;
// 발사체 오브젝트를 없앱니다
Destroy(other.gameObject);
}
// 여기까지가 추가된 내용입니다
}
발사체에 Projectile이라는 태그를 만들어 충돌을 체크합니다.
만일 몬스터에 발사체가 충돌한다면, 몬스터의 Hp를 하나 깎고, 발사체를 Destroy()를 통해 파괴합니다.
실행시켜보면, 문제가 있습니다.
발사체가... 몬스터에 안 부딪히고 아래에 붙어 쫓아다닙니다...
아무래도 에셋에 무슨 문제가 있나봅니다. 프리팹에서 확인해보니, 콜라이더의 y축이 0.43 높고, 오브젝트도 y축 값이 0.48이네요.
제가 지난글에서 귀찮게 만들어놨나 보네요... 자업자득입니다. 이것은 개인마다 다를 수 있습니다. 만약 잘 작동한다면 그냥 놔두셔도 됩니다.
오브젝트가 0.45만큼 높습니다. 프리팹을 수정할 수 있지만 (귀찮으므로) 발사체 코드를 조금 수정하겠습니다.
// 발사체가 향하는 위치를 2번째 코드처럼 벡터를 더해 올려줍니다.
transform.position = Vector3.MoveTowards(transform.position, target.transform.position, step);
transform.position = Vector3.MoveTowards(transform.position, target.transform.position + new Vector3(0, 0.43f, 0), step);
로그를 찍어봐도 잘 부딪힘을 볼 수 있습니다.
몬스터의 체력을 설정합시다.
일전의 타워 Hp 만들 때처럼 만들어줬습니다.
// 변수 선언
private int hp;
// 프로퍼티 선언
public int Hp {
get => hp;
set {
hp = value;
if (hp <= 0) {
Die();
}
}
}
void Start(){
// 아래 코드를 추가해주세요
Hp = 3;
}
void Die() {
Destroy(this.gameObject);
}
Enemy 스크립트의 Start 함수 안에 Hp를 세팅해줍니다. 저는 일단 3으로 세팅했습니다.
그 다음 체력이 다 닳는다면, Die 함수가 호출됩니다. Die 함수 안에서는 간단히 몬스터 오브젝트를 파괴하도록 코드를 작성합니다.
발사체와 3번 부딪히니 유령이 사라지는 것을 볼 수 있습니다.
얼레? 하지만 갑자기 게임이 멈춥니다. 콘솔 창을 열어보니 다음과 같이 무언가 뜹니다.
호오..
아무래도 몬스터가 Destroy되었으나, AttackTower.cs의 monster는 지워지지 않아서 발생하는 문제인가 봅니다!
// 몬스터 오브젝트는 사라졌지만,
// monsters에는 남아있고 계속해서 접근을 시도합니다
shoot.GetComponent<Projectile>().target = monsters.First();
알고 계셨나요? 에러 내용을 잘 읽어보면 "check if it is null or..."이라는 힌트를 던집니다. null 체크를 해서 만약 monsters.First()가 null이라면 제거해주면 되는 일입니다.
// target을 지정하기 전, while문으로 null 체크를 합니다
// target이 null이 아닐 때까지 반복합니다
var target = monsters.First();
while (target == null) {
// List의 0번째 값을 제거합니다
// Queue로 진행할 경우, Dequeue()로 작성하세요
monsters.RemoveAt(0);
target = monsters.First();
}
shoot.GetComponent<Projectile>().target = target;
실행해보면, 에러없이 잘 진행됨을 볼 수 있습니다.
이상으로 공격타워 구현을 마치겠습니다.