타워 디펜스 게임 (1) - 맵, 적, 이동 경로
1. 맵 제작
1) 녹색 육면체 : 적이 필드로 들어오는 입구
2) 적색 육면체 : 적이 필드에서 나가는 출구
3) 흰 타일
타워 배치가 가능한 타일
타워가 올라갈 수 있는 1칸을 기준으로 나눠서 제작
4) 검은 타일
적이 지나가는 통로
길 형태를 갖추도록 콜라이더를 최소한으로 사용
2. 적이 지나가는 경로 만들기. 웨이포인트
적이 검은 타일을 따라 지나다니게 하려면
각 구간마다 목표가 되는 위치 좌표가 있어야 하며
길의 코너마다 위치하게 하는 게 좋겠지요.
위의 스크린샷에서 코너마다 있는 빨간 점이 그 포인트입니다.
적의 이동 목표인 빨간 육면체 내에도 하나 더 존재합니다.
using UnityEngine;
public class WayPoints : MonoBehaviour
{
public static Transform[] points;
private void Awake() //private는 생략이 가능
{
//부모 오브젝트를 통해 자식 오브젝트들의 transform 정보를 받아온다.
points = new Transform[transform.childCount];
for (int i = 0; i < points.Length; i++)
{
points[i] = transform.GetChild(i);
}
}
}
웨이포인트를 담는 부모 오브젝트에
자식인 웨이포인트들의 트랜스폼 >> 위치에 접근할 수 있는 스크립트를 붙여
적들이 이를 참조하여 이동 경로를 만들 수 있게 합니다.
3. 적
에셋을 특별히 쓰지 않고 심볼로 구형의 적 생성
>> HP바를 표시할 자식 오브젝트를 만들어 넣기
>> 프리팹 생성
>> 원본 삭제
공격 받을 때 판정을 위해 적의 형태와 같은 구형 콜라이더를 부착했습니다.
적의 역할에 따라 2가지 스크립트를 부착했습니다.
using UnityEngine;
using TMPro;
public class EnemyMove : MonoBehaviour
{
//이동할 목표지점의 오브젝트
private Transform target;
//목표 WayPoint 인덱스
public int wayPointIndex = 0;
Enemy Enemy;
// Start is called before the first frame update
void Start()
{
Enemy = GetComponent<Enemy>();
//target 설정
wayPointIndex = 0;
target = WayPoints.points[wayPointIndex];
}
// Update is called once per frame
void Update()
{
//타켓까지 이동하기
Vector3 dir = target.position - this.transform.position;
transform.Translate(dir.normalized * Time.deltaTime * Enemy.GetEnemySpeed(), Space.World);
float distance = Vector3.Distance(target.position, transform.position);
if (distance <= 0.5f)
{
GetNextPoint();
}
}
//다음 WayPoint 지점을 타겟으로 설정
private void GetNextPoint()
{
//마지막 지점에 도착하면
if (wayPointIndex == WayPoints.points.Length - 1) //마지막 인덱스 번호는 배열의 길이 - 1
{
// 플레이어 라이프가 0 초과인 경우 하나 차감하고
PlayerStatus.SetLife(PlayerStatus.GetLife() != 0 ? PlayerStatus.GetLife() - 1 : 0);
// 자신을 파괴
Destroy(gameObject);
return;
}
//마지막 지점이 아니라면 다음 목표로 설정
wayPointIndex++;
target = WayPoints.points[wayPointIndex];
}
}
1) 길을 따라서 목표 지점까지 이동 > 목표에 도착했다면 플레이어 라이프 차감 및 스스로를 파괴
플레이어 라이프는 다음 포스팅에서 다루도록 하겠습니다.
2) 공격을 받으면 체력이 깎이다 처치되면 보상 드랍
체력바는 전체 체력을 나타내는 녹색 막대(Hpbar)와 잃은 체력을 나타내는 빨간 막대(Hploss)로 나뉘어집니다.
체력을 잃을 때 (잃은 체력 / 최대 체력) 비율에 맞게 x축 스케일을 늘리고 x축 위치를 이동하여 깎인 부분이 표시되게끔 합니다.
using UnityEngine;
public class Enemy : MonoBehaviour
{
// 적의 스테이터스
// 빔에 공격받고 있는지 여부
bool isAttackedByBeam = false;
// 체력에 변화가 있었는지 여부
bool isHPChaged = false;
// 현재 속도
float speed;
// 원래 속도
float originSpeed;
// 빔에 공격받고 있을 때 속도
float beamSlow;
// 현재 체력
float hp;
// 최대 체력
float MaxHP;
// 적을 잡았을 경우 얻는 골드 -> 여기서 쓴다면 다른 유형의 적이 생성될 때 확장이 용이함
int gainGold = 50;
// 체력바의 초록 부분
Transform HPbar;
// 체력바의 빨간 부분
Transform HPloss;
// Start is called before the first frame update
void Start()
{
// 리스폰되면 최대 체력으로
hp = MaxHP;
// Enemy 프리팹의 자식 오브젝트들을 가져옴
HPloss = transform.Find("HPloss");
HPbar = transform.Find("HPbar");
}
// Update is called once per frame
void Update()
{
// 게임 오버가 된다면 적 모두 파괴
if (!GameManager.isPlayerAlive)
{
// 적 파괴 이펙트에 들어갈 색상 보내주기
EffectManager.SetKillEffectColor(GetComponent<Renderer>().material.color);
// 적 파괴 이펙트를 호출하고 적 자신을 파괴
EffectManager.NeedEffect((int)EffectManager.effectIndex.KillEffect, transform.position);
Destroy(gameObject);
}
// 빔에 의해 공격을 받는 도중에는 이동 속도를 느리게, 공격에서 벗어나면 원래 속도로
if (isAttackedByBeam)
{
speed = beamSlow;
isAttackedByBeam = false; // 빔에 의한 공격이 끊길 경우 원래 속도로 돌아가기 위함
}
else
speed = originSpeed;
// 다음에는 이미지로 표현해보기
// 체력의 변화가 있었다면, HP바 표시 변경
if(isHPChaged)
{
// 체력이 100%인 경우 적 HP 바 비활성화, 100% 미만일 경우 활성화
if (!HPbar.gameObject.activeSelf)
{
HPbar.gameObject.SetActive(true);
HPloss.gameObject.SetActive(true);
}
// 잃은 체력% 만큼 x축 방향 스케일을 키우고 그 절반만큼 이동
float lostHpRatio = (MaxHP - hp) / MaxHP;
float initial_PosX = 0.5f;
HPloss.transform.localScale = new Vector3 (lostHpRatio, HPloss.transform.localScale.y, HPloss.transform.localScale.z);
HPloss.transform.localPosition = new Vector3(initial_PosX - lostHpRatio / 2, HPloss.transform.localPosition.y, HPloss.transform.localPosition.z);
// 다음 hp 변경 대기
isHPChaged = false;
}
}
private void OnDestroy()
{
// 체력이 0 이하로 내려가 파괴되었을 경우에만
if (hp <= 0)
{
// 적 파괴 이펙트에 들어갈 색상 보내주고
EffectManager.SetKillEffectColor(GetComponent<Renderer>().material.color);
// 적 파괴 이펙트 호출
EffectManager.NeedEffect((int)EffectManager.effectIndex.KillEffect, transform.position);
// 파괴되었다면 골드 가산
PlayerStatus.SetGold(PlayerStatus.GetGold() + gainGold);
}
}
// 적이 공격을 받았을 때 호출 -> 대미지 계산 후 죽었는지 살았는지 판정
public bool IsThisEnemyDied(float damage)
{
// 현재 체력 - 대미지
hp -= damage;
// 체력의 변화가 있음을 알리고
isHPChaged = true;
// true : 쥬금, false : 살았음
return hp <= 0;
}
// 람다식으로 메서드를 줄여서 쓸 수 있음
public void SetisAttackedByBeam() => isAttackedByBeam = true;
public float GetEnemySpeed() => speed;
// 적의 스테이터스를 설정하는 메서드
public void SetEnemyStatus(EnemyBlueprint enemyName)
{
GetComponent<Renderer>().material.color = enemyName.color;
MaxHP = enemyName.Health;
originSpeed = enemyName.Speed;
beamSlow = originSpeed * 0.6f;
gainGold = enemyName.Reward;
}
}
4. 적 생성기(스포너)
빈 오브젝트 생성 >> 이름을 SpawnManager 로 변경 >> 스크립트 생성 후 아래처럼 작성하여 부착
using System;
using UnityEngine;
[Serializable]
public class EnemyBlueprint
{
// 적의 색상, 속도, 체력, 처치했을 시 얻는 골드
public Color color;
public float Speed;
public float Health;
public int Reward;
}
[Serializable]
public class Wave
{
public EnemyBlueprint enemySort;
public int numOfEnemy;
public float spawnDelayTime;
}
Serializable 클래스를 이용하여
반복적으로 필요한 데이터들의 공통 정보를 담을 수 있도록 하며
이를 인스펙터에 나타날 수 있도록 하여 인스펙터에서도 할당할 수 있게끔 하였습니다.
using System.Collections;
using UnityEngine;
using TMPro; //텍스트 사용
public class WaveSpawner : MonoBehaviour
{
//적 오브젝트 프리팹
public GameObject enemyPrefab;
// 3종류의 적에 대한 각각의 정보
public EnemyBlueprint simple;
public EnemyBlueprint tough;
public EnemyBlueprint fast;
// 웨이브 정보
public Wave[] waves;
// 현재 웨이브
int currentWave;
// 최대 웨이브
int WaveMax = 4;
//시작 지점
public Transform start;
//스폰 타이머 - 5초 간격
float spawnTimer = 5f;
// 첫 웨이브가 나올 때까지 2초
private float countdown = 2f;
//Text UI
public TextMeshProUGUI countdownText;
private void Start()
{
// 시작은 0번부터
currentWave = 0;
// 웨이브 별로 나올 적 종류
waves[0].enemySort = simple;
waves[1].enemySort = simple;
waves[2].enemySort = simple;
waves[3].enemySort = tough;
waves[4].enemySort = fast;
// 레벨에 따른 적 수 조정
waves[0].numOfEnemy = PlayerStatus.curretStage;
waves[1].numOfEnemy = PlayerStatus.curretStage*2;
waves[2].numOfEnemy = PlayerStatus.curretStage*5;
waves[3].numOfEnemy = PlayerStatus.curretStage*3;
waves[4].numOfEnemy = PlayerStatus.curretStage*10;
}
// Update is called once per frame
void Update()
{
// 게임 오버, 현재 웨이브의 적이 남아있는 상태, 레벨 클리어 상태라면 적이 더 이상 리젠되지 않도록
if (!GameManager.isPlayerAlive || GameManager.isLevelCleared || GameObject.FindGameObjectsWithTag("Enemy").Length != 0 )
return;
// 마지막 웨이브이고, 적을 다 잡았다면 알려줌
if (currentWave > WaveMax)
{
GameManager.isLevelCleared = true;
return;
}
//타이머
if (countdown <= 0)
{
StartCoroutine(SpawnWave());
//실행문
countdown = spawnTimer;
}
countdown -= Time.deltaTime;
//text UI 연결
//countdownText.text = countdown.ToString(); //countdown이 숫자형이라서 문자형으로 변환하기 위해 ToString
//[1]소수점 두자리까지 끊어서 출력
//[1-1]
//countdownText.text = countdown.ToString("F2"); //원하는 소수점 자리수 맞춰서 Fn
//[1-2]
//countdownText.text = string.Format("{0:0.0}", countdown);
//[2]소수점을 보이지 않게 출력 (int형으로 출력)
countdownText.text = Mathf.Round(countdown).ToString();
}
//5초마다 wave 발생
IEnumerator SpawnWave()
{
// 라운드 정보를 넘겨줘서 표시
PlayerStatus.SetRound(currentWave+1);
// 현재 웨이브 적 수에 맞게
for (int i = 0; i < waves[currentWave].numOfEnemy; i++)
{
// 적을 생성하고 그 적의 스테이터스 할당
SpawnEnemy();
// 해당 웨이브의 발생지연 시간만큼 기다리고 다음 적 생성
yield return new WaitForSeconds(waves[currentWave].spawnDelayTime);
}
// 다음 웨이브로 변경
++currentWave;
}
//시작점 위치에 enemy 1개를 생성
private void SpawnEnemy()
{
GameObject thisEnemy = Instantiate(enemyPrefab, start.position, Quaternion.identity);
thisEnemy.GetComponent<Enemy>().SetEnemyStatus(waves[currentWave].enemySort);
}
}
+ 스포너가 이런 게임에서 보통 내부적으로 게임 플레이 시간을 사용하기에
UI에 다음 웨이브까지 남은 시간 표시까지 해줍니다.
이렇게 맵, 적, 적 생성, 이동까지 되었습니다.