[Unity][3D-Game] Tower Defense Game (2)

suhan0304·2023년 11월 26일
0
post-thumbnail

강의영상 (3)(4)


개발

웨이브 스포너

WaveSpawner.cs

public class WaveSpawner : MonoBehaviour
{
    public Transform enemyPrefab;

    public Transform spawnPoint; //몬스터 스폰 위치

    public float timeBetweenWaves = 5f; //웨이브 사이 대기 시간
    private float countdown = 2f;

    private int waveIndex = 0;// 웨이브 번호

    private void Update()
    {
        if (countdown <= 0f) //카운트다운이 0 보다 작아지먄 Spawn Wave 실행
        {
            SpawnWave();
            countdown = timeBetweenWaves;//카운트 다운을 중간 시간으로 초기화
        }

        //deltaTime//마지막 프레임을 그린 후 경과한 시간
        countdown -= Time.deltaTime; //시간을 계속 줄인다.
    }

    void SpawnWave()
    {
        waveIndex++; //웨이브가 올때마다 레벨업
        
        for (int i = 0; i < waveIndex; i++) //웨이브 레벨만큼 몬스터 소한
        {
            SpawnEnemy();
        }
    }

    void SpawnEnemy()
    {
        //미리 지정해둔 스폰 포인트에서 몬스터를 복사해서 소환
        Instantiate(enemyPrefab, spawnPoint.position, spawnPoint.rotation);
    }
}

위 코드가 잘 작동하도록 시작 노드를 SpawnPoint로 지정해준다. 이제 실행해보면 에너미 생성을 잘 되고 움직임 또한 잘 작동하지만 여러 마리의 몬스터가 거의 동시에 생성되면서 한마리처럼 겹쳐서 움직이고 있다.

따라서 일종의 카운트 다운을 재설정하거나 코루틴을 사용해 해당 문제를 해결해줄 수 있다.

IEnumerator SpawnWave() //코루틴
{
    for (int i = 0; i < waveIndex; i++) //웨이브 레벨만큼 몬스터 소한
    {
        SpawnEnemy();
        yield return new WaitForSeconds(0.5f);
    }
    waveIndex++; //웨이브가 올때마다 레벨업
}

적이 시간차를 두고 잘 생성되는 것을 확인할 수 있다.

타이머 UI 제작

UI > Text를 하나 추가해준 후 상단에 앵커 설정을 해준다.

  • 이름을 WaveCountdownTimer로 설정한다.
  • 글꼴 크기를 70, 색을 흰색으로 설정한다.
  • 그림자 컴포넌트를 추가해준다.
  • 폰트 스타일을 Bold로 설정한다.

터렛 모델

제공되는 터렛 모델을 임포트해보자. Turrent.fbx를 프로젝트창에 드래그드랍해서 임포트 시킬 수 있다. 임포트 시킨 후 속성을 아래와 같이 설정한다.

  • Model

    • Scale Factor = 3으로 설정한다.
    • Normals을 Calculate로 설정한다.
    • Smoothing Angle = 38로 설정한다.
  • Rig

    • Animation Type = None으로 설정한다.

import 설정은 이 문서를 참고하자.

이제 터렛을 씬창에 드래그해서 트랜스폼을 설정한다.

  • Pivot과 Local로 설정한 후에 트랜스폼을 수정한다.

  • y = 0.5로 설정한다.

  • 자식 오브젝트를 각각 Head와 Base로 수정한다.

터렛 Material 설정

임포트 된 재질들을 복사해서 새 재질로 만들어준 후 해당 터렛에 드래그해서 설정해준다.

새 재질을 만들어서 해당 하는 부분에 맞게 재질을 넣어준 후 색을 조정해도 괜찮다.

이제 각 재질의 속성을 만지면서 터렛을 원하는 타입의 색과 재질로 설정한다.

위처럼 터렛의 모델 세팅이 끝났다면 자식 오브젝트로 Part To Rotate 오브젝트를 하나 생성해준다.

  • Head가 적을 향해 회전할 때 기준이 되는 축 역할을 한다.
  • 해당 오브젝트가 베이스와 헤드 사이의 가운데에 위치하도록 설정한다.

현재 Head를 회전 시키면 기준축이 이상하게 설정되어 있어 아래와 같이 회전한다

Shading Mode를 Wireframe으로 설정하면 위치를 정확하게 볼 수 있다.

이제 Head를 PartToRotate의 자식 오브젝트로 설정한다.

이제 PartToRotate를 회전 시켜보면 머리가 축을 기준으로 잘 회전되는 것을 확인할 수 있다.

이제 다 만들어진 터렛을 프리팹화 시킨다. 이제 터렛을 적절한 위치에 임의로 배치시킨 후 터렛 스크립트를 작성해보자.

터렛 스크립트

주석으로 설명을 상세하게 달아놓았으므로 설명은 주석을 참고하자.

  • 아래와 같이 코딩했는데 터렛의 Head가 회전은 하는데 총구가 향하지 않는다면 Head를 PartToRoate의 자식 오브젝트에서 꺼낸 후 PartToRotate의 초기 각도를 설정해 준후 다시 Head를 자식 오브젝트로 넣어주면서 터렛의 총구가 적을 향하도록 수정할 수 있다.

아래 그림과 같이 총구가 파랑색 화살표를 바라보고 있어야만 정상적으로 적을 향할 수 있다.

Turret.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Turret : MonoBehaviour
{
    public Transform target; // 공격할목표 오브젝트
    public float range = 15f; // 사거리는 15로 설정

    public string enemyTag = "Enemy";

    public Transform partToRotate; //실제로 base를 제외하고 회전될 오브젝트의 트랜스폼
    public float turnSpeed = 10f;

    // 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;    //이제 목표 오브젝트를 미리 찾아놓은 적으로 설정
        }
        else
        {
            target = null; //만족하지 않으면 target을 null로 초기화
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (target == null) //타겟이 없으면 리턴 (아무런 행동도 하지 않음)
            return;

        //--- 만약 target이 있다면 ---
        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); 
        
    }

    private void OnDrawGizmosSelected() //기즈모를 그려주는 유니티 함수
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, range); //내 위치를 기준으로 range를 반지름을 구를 그려줌
    }
}

OnDrawGizmosSelected 함수를 작성하면 씬 창에 임의로 기즈모를 그릴 수 있어서 사거리를 확인하는 용도로 아래와 같이 사용할 수 있다.

private void OnDrawGizmosSelected() //기즈모를 그려주는 유니티 함수
{
    Gizmos.color = Color.red;
    Gizmos.DrawWireSphere(transform.position, range); //내 위치를 기준으로 range를 반지름을 구를 그려줌
}

타겟을 향해 회전하는 부분은 구현 방법도 많고 실제 코드도 간단하지만 유니티에서 사용하는 오일러 각도에 대해 이해하고 코딩하는 것이 좋다.


결과물

profile
Be Honest, Be Harder, Be Stronger

0개의 댓글