3. 타워 배치, 타워 공격
[Unity 2D Game] Tower Defense #02 - 타워 배치, 타워의 공격 (youtube.com)
유튜브를 참고하여 작성하였습니다.
TileWall생성

빈 오브젝트에 BoxCollider 컴포넌트와 Tile스크립트를 연결 후 프리팹으로 생성한다.
Tag는 TileWall을 생성하고 연결하도록 한다.
이때 2D 가 아닌 3D BoxCollider 로 생성해야 충돌체에 반응 한다.
TileMap Brush로 등록하여 타워 설치가 가능한 곳에 생성한다.


타워 프리팹 생성하기
사용할 타워 이미지를 생성하고 프리팹으로 설정한다.

Acher_Blue에는 아래와 같이 애니메이터를 생성하고

파라미터는 Vertical, Horizontal, Attack 을 각각 float, float, trigger 타입으로 생성한다.

ArrowMove 의 블랜드트리는 2D Freeform Directional 로 생성하고 Archer_Blue에 들어가있는 각각의 애니메이션을 넣어준다.
애니메이션은 모두 오른쪽을 보고 있으므로 코드를 통해서 왼쪽방향에 있는 적에게 공격할 때 Filp할 수 있게 구현할 예정이다.
단순하게 앞만 쏘는 애니메이션을 사용하여 구현할 수 있지만 디테일을 위해 쏘고있는 적의 위치를 알아내어 해당 방향의 공격 애니메이션을 구현할 수 있도록 한다.
타워 설치 구현
Tile.cs
using UnityEngine;
public class Tile : MonoBehaviour
{
// 타일에 타워가 건설되어 있는지 검사하는 변수
public bool IsBuildTower { set; get; }
private void Awake()
{
IsBuildTower = false;
}
}
TowerSpawner.cs
using UnityEngine;
/// <summary>
/// 타워 생성 제어
/// </summary>
public class TowerSpawner : MonoBehaviour
{
[SerializeField]
private GameObject towerPrefab;
[SerializeField]
private EnemySpawner enemySpawner; // 현재 맵에 존재하는 적 리스트 정보를 얻기 위해
/// <summary>
/// 매개변수의 위치에 타워 생성
/// </summary>
/// <param name="tileTransform"></param>
public void SpawnTower(Transform tileTransform)
{
Tile tile = tileTransform.GetComponent<Tile>();
// 타워 건설 가능 여부 확인
// 1. 현재 타일의 위치에 이미 타워가 건설되어 있으면 타워건설 x
if (tile.IsBuildTower == true)
{
return;
}
// 타워가 건설되어 있음으로 설정
tile.IsBuildTower = true;
// 선택한 타일의 위치에 타워 건설
GameObject clone = Instantiate(towerPrefab, tileTransform.position, Quaternion.identity);
// 타워 무기에 enemySpawner 정보 전달
clone.GetComponent<TowerWeapon>().Setup(enemySpawner);
}
}
빈 오브젝트인 TowerSpawner를 생성하고 코드를 연결한다.
ObjectDetector.cs
using UnityEngine;
public class ObjectDetector : MonoBehaviour
{
[SerializeField]
private TowerSpawner towerSpawner;
private Camera mainCam;
private Ray ray;
private RaycastHit hit;
private void Awake()
{
mainCam = Camera.main;
}
private void Update()
{
//마우스 왼쪽 버튼을 눌렀을 때
if(Input.GetMouseButtonDown(0))
{
// 카메라 위치에서 화면의 마우스 위치를 관통하는 광선 생성
// ray.origin : 광선의 시작위치
// ray.direction : 광선의 진행방향
ray = mainCam.ScreenPointToRay(Input.mousePosition);
// 2D 모니터를 통해 3D 월드의 오브젝트를 마우스로 선택하는 방법
// 광선에 부딪히는 오브젝트를 검출해서 hit에 저장
// Mathf.Infinity : 이 변수가 가질 수 있는 최대 값ㄴ
if(Physics.Raycast(ray, out hit,Mathf.Infinity))
{
// 광선에 부딪힌 오브젝트의 태그가 TileWall이면
if(hit.transform.CompareTag("TileWall"))
{
// 타워를 생성하는 SpawnTower 호출
towerSpawner.SpawnTower(hit.transform);
}
}
}
}
}
ObjectDetector 빈 오브젝트 생성 후 위의 스크립트를 연결해준다
발사체 생성

화살은 위를 보도록 설정하고 해당하는 컴포넌트를 연결하고 설정한 후 프리팹으로 생성한다.
적 관리
EnemySpawner.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField]
private GameObject enemyPrefab;
[SerializeField]
private float spawnTime;
[SerializeField]
private Transform[] myPoints;
private List<EnemyCtrl> enemyList;
// 적의 생성과 삭제는 EnemySpawner에서 하기 때문에 Set은 필요 없음
public List<EnemyCtrl> EnemyList => enemyList;
private void Awake()
{
// 적 리스트 메모리 할당
enemyList = new List<EnemyCtrl>();
// 적 생성 코루틴 함수 호출
StartCoroutine("SpawnEnemy");
}
/// <summary>
/// 적 스폰 코루틴 함수
/// </summary>
/// <returns></returns>
private IEnumerator SpawnEnemy()
{
while(true)
{
GameObject clone = Instantiate(enemyPrefab);
EnemyCtrl enemy = clone.GetComponent<EnemyCtrl>();
enemy.Setup(this,myPoints);
enemyList.Add(enemy); // 리스트에 방금 생성된 적 정보 저장
yield return new WaitForSeconds(spawnTime);
}
}
public void DestoryEnemy(EnemyCtrl enemy)
{
// 리스트에서 사망하는 적 정보 삭제
enemyList.Remove(enemy);
// 적 오브젝트 삭제
Destroy(enemy.gameObject);
}
}

Enemy.cs(저번의 EnemyCtrl.cs 이름변경)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
private int wayPointCount; // 이동 경로 개수
private Transform[] wayPoints; // 이동 경로 정보
private int currentIndex = 0; // 현재 목표지점 인덱스
private Movement2D movement2D; // 오브젝트 이동 제어
private EnemySpawner enemySpawner; // 적의 삭제를 본인이 하지 않고 EnemySpawner에 알려서 삭제
public void Setup(EnemySpawner enemySpawner, Transform[] wayPoints)
{
movement2D = GetComponent<Movement2D>();
this.enemySpawner = enemySpawner;
// 적 이동 경로 wayPoint 정보 저장
wayPointCount = wayPoints.Length;
this.wayPoints = new Transform[wayPointCount];
this.wayPoints = wayPoints;
// 적의 위치를 첫번째 wayPoint 위치로 설정
transform.position = wayPoints[currentIndex].position;
StartCoroutine(OnMove());
}
/// <summary>
/// 적 이동/목표지점 설정 코루틴
/// </summary>
/// <returns></returns>
private IEnumerator OnMove()
{
NextMoveTo();
while(true)
{
// 적의 현재위치와 목표위치의 거리가 MoveSpeed 보다 적을 때 if 조건문 실행
// Tip. MoveSpeed 를 곱해주는 이유는 속도가 빠르면 전 프레임에 0.02보다 크게 움직이기 때문에
// if 조건문에 걸리지 않고 경로를 탈주하는 오브젝트가 발생할 수 있음
if(Vector3.Distance(transform.position,wayPoints[currentIndex].position)<0.02f * movement2D.MoveSpeed)
{
NextMoveTo();
}
yield return null;
}
}
/// <summary>
/// 다음 이동 방향 설정
/// </summary>
private void NextMoveTo()
{
// 아직 이동할 wayPoints 가 남아있다면
if(currentIndex < wayPointCount - 1)
{
// 적의 위치를 정확하게 목표 위치로 설정
transform.position = wayPoints[currentIndex].position;
// 이동 방향 설정 => 다음 목표지점(wayPoints)
currentIndex++;
Vector3 direction = (wayPoints[currentIndex].position - transform.position).normalized;
movement2D.MoveTo(direction);
}
// 현재 위치가 마지막 wayPoints 라면
else
{
// 적 오브젝트 삭제
OnDie();
}
}
public void OnDie()
{
// EnemySpawner에서 리스트로 적 정보를 관리하기 때문에 Destory()를 직접하지 않고
// EnemySpawner에게 본인이 삭제될 때 필요한 처리를 하도록 DestoryEnemy()함수 호출
enemySpawner.DestoryEnemy(this);
}
}
TowerWeapon.cs
using System.Collections;
using UnityEngine;
// 공격 대상을 탐색하는 SearchTarget, 대상을 공격하는 AttackToTarget
public enum WeaponState { SearchTarget = 0, AttackToTarget }
public class TowerWeapon : MonoBehaviour
{
[SerializeField]
private GameObject projectTilePrefab; // 발사체 프리팹
[SerializeField]
private Transform spawnPoint; // 발사체 생성 위치
[SerializeField]
private float attackRate = .5f; // 공격 속도
[SerializeField]
private float attackRange = 2.0f; // 공격 범위
private WeaponState weaponState = WeaponState.SearchTarget; // 타워 무기의 상태
private Transform attackTarget = null; // 공격 대상
private EnemySpawner enemySpawner; // 게임에 존재하는 적 정보 획득용
[SerializeField]
private Animator archerAnimator; // 타워에 있는 아처 애니메이션
private Quaternion spawnArrowRot;
public void Setup(EnemySpawner enemySpawner)
{
this.enemySpawner = enemySpawner;
// 최초 상태를 WeaponState.SearchTarget 으로 설정
ChangeState(WeaponState.SearchTarget);
}
public void ChangeState(WeaponState newState)
{
// 이전에 재생중이던 상태 종료
StopCoroutine(weaponState.ToString());
// 상태 변경
weaponState = newState;
// 새로운 상태 재생
StartCoroutine(weaponState.ToString());
}
private void Update()
{
if(attackTarget != null)
{
RotateToTarget();
}
}
/// <summary>
/// 대상을 바라보게 하는 회전 함수
/// </summary>
private void RotateToTarget()
{
// 원점으로부터의 거리와 수평축으로부터의 각도를 이요해 위치를 구하는 극 좌표게 이용
// 각도 = Arctan(y/x)
// x, y 변위값 구하기
float dx = attackTarget.position.x - transform.position.x;
float dy = attackTarget.position.y - transform.position.y;
// Archer가 왼쪽을 바라볼 수 있게 SpriteRenderer의 Filp.x 설정
if(dx < 0){
archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = true;
archerAnimator.SetFloat("Horizontal", -1);
}
else{
archerAnimator.gameObject.GetComponent<SpriteRenderer>().flipX = false;
archerAnimator.SetFloat("Horizontal", 1);
}
if (dy < 0)
archerAnimator.SetFloat("Vertical", -1);
else
archerAnimator.SetFloat("Vertical", 1);
// x, y 변위값을 바탕으로 각도 구하기
// 각도가 radian 단위이기 때문에 Mathf.Rad2Deg를 곱해도 단위를 구함
float degree = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg;
spawnArrowRot = Quaternion.Euler(0, 0, degree);
}
/// <summary>
/// 현재 타워의 사정거리 안에 있으면서 가장 가까운 적의 거리를 저장
/// </summary>
/// <returns></returns>
private IEnumerator SearchTarget()
{
while(true)
{
// 제일 가까이 있는 적을 찾기 위해 최초 거리를 최대한 크게 설정
float closesDisSqr = Mathf.Infinity;
// EnemySpawner의 EnemyList에 있는 현재 맵에 존재하는 적 검사
for(int i =0; i<enemySpawner.EnemyList.Count; i++)
{
float distance = Vector3.Distance(enemySpawner.EnemyList[i].transform.position, transform.position);
// 현재 감시중인 적과의 거리가 공격범위 내에 있고, 현재까지 감시한 적보다 거리가 가까우면
if(distance <= attackRange && distance <= closesDisSqr)
{
closesDisSqr = distance;
attackTarget = enemySpawner.EnemyList[i].transform;
}
}
if(attackTarget != null)
{
ChangeState(WeaponState.AttackToTarget);
}
yield return null;
}
}
/// <summary>
/// 설정된 적을 공격하는 코루틴함수
/// </summary>
/// <returns></returns>
private IEnumerator AttackToTarget()
{
while(true)
{
// 1. target이 있는지 검사(다른 발사제에 의해 제거, Goal 지점까지 이동해 삭제 등)
if(attackTarget == null)
{
ChangeState(WeaponState.SearchTarget);
break;
}
// 2. target이 공격 범위 안에 있는지 검사(공격 범위를 벗어나면 새로운 적 탐색)
float distance = Vector3.Distance(attackTarget.position, transform.position);
if(distance > attackRange)
{
attackTarget = null;
ChangeState(WeaponState.SearchTarget);
break;
}
// 3. attackRate 시간만큼 대기
yield return new WaitForSeconds(attackRate);
// 4. 공격 (발사체 생성)
archerAnimator.SetTrigger("Attack");
SpawnProjectTile();
}
}
private void SpawnProjectTile()
{
GameObject clone = Instantiate(projectTilePrefab, spawnPoint.position, spawnArrowRot);
// 생성된 발사체에게 공격대상(attackTarget) 정보 제공
clone.GetComponent<ProjectTile>().Setup(attackTarget);
}
}
Arrow.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 발사체의 이동 및 프로젝트 타일 충돌
/// </summary>
public class Arrow : MonoBehaviour
{
private Movement2D movement2D;
private Transform target;
public void Setup(Transform target)
{
movement2D = GetComponent<Movement2D>();
this.target = target;
}
private void Update()
{
if(target != null)
{
// 발사체를 target의 위치로 이동
Vector3 direction = (target.position - transform.position).normalized;
transform.rotation = Quaternion.FromToRotation(Vector3.up, direction);
movement2D.MoveTo(direction);
}
else
{
// 발사체 오브젝트 삭제
Destroy(gameObject);
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (!collision.CompareTag("Enemy")) return; // 적이 아닌 대상과 부딪히면
if (collision.transform != target) return; // 현재 target인 적이 아닐 때
collision.GetComponent<Enemy>().OnDie(); // 적 사망 함수 호출
Destroy(gameObject); // 발사체 오브젝트 삭제
}
}