[SKKU DT] 51일차 -유니티 프로젝트 탱크 게임 -Wheel Collider로 탱크 이동, 카메라가 플레이어를 따라다니기, 적 만들기, 적 이동(waypoints), 총알 생성, 총알 발사, 체력 바 만들기

2024. 1. 12. 18:03SKKU DT

728x90
반응형
 

[SKKU DT] 50일차 -유니티 프로젝트 GitHub 충돌, repository, 게임 수학과 이동(삼각함수/구면좌표), Light

GitHub 충돌 GitHub에서 충돌이 난다면, 충돌 메시지가 뜨게 되고, [Open in Visual Studio Code] 버튼을 선택하면 코드를 볼 수 있다. 변경된 점의 위쪽에 Accept Current Change, Accept Incoming Change, Accept Both Changes,

lightbakery.tistory.com


Light Probe가 잘 적용되었지만 꼼꼼하게 배치하지 않아서 자연스럽지는 않게 구워졌다.

 

 

MyTank 스크립트를 조금 수정해서 테스트한다.

using System.Collections;
using System.Collections.Generic;
using UnityEditor.Rendering;
using UnityEngine;

public class MyTank : MonoBehaviour
{
    [SerializeField] private Transform turret;
    Transform tr;

    private void Start()
    {
        tr = transform;
        StartCoroutine(StartTank()); //코루틴 호출
    }

    IEnumerator Move() //코루틴으로 Move 함수 만들기
    {
        Vector3 targetPos = new Vector3(tr.position.x, tr.position.y, tr.position.z + 10); //탱크 자신의 transform보다 10만큼 더 이동해서 tr.position과 targetPos에 차이를 둔다.

        while (Vector3.Distance(tr.position, targetPos) >= 0.01f) //두 지점의 차이가 0.01보다 크면
        {
            tr.position = Vector3.MoveTowards(tr.position, targetPos, 0.1f); //0.1만큼 움직여라
            yield return null; //한 프레임을 쉬겠다.
        }

        yield return StartCoroutine(TurnTurret(90, 3f)); //3초동안 90도 회전
    }

    IEnumerator TurnTurret(float rotateY, float duration) //터렛 돌리는 코루틴 생성
    {
        float startTime = Time.time;
        float initTime = Time.time - startTime;
        Quaternion turretRot = turret.localRotation;

        while(initTime <= duration)
        {
            turret.localRotation = Quaternion.Slerp(turretRot, Quaternion.Euler(0, rotateY, 0), initTime / duration);
            initTime = Time.time - startTime;
            yield return null;
        }
    }

    IEnumerator StartTank()
    {
        while(true)
        {
            yield return StartCoroutine(Move());
        }
    }
}

 

 

Tank에 스크립트 넣고 Turret도 넣는다.

 

 

그러면 10만큼 갔다가 포탑이 회전하는 것을 볼 수 있다.

PumpJack이 왜 같이 움직이는지는 의문 -> 알고보니 배경에도 스크립트를 잘못 넣었다...

 

 


 

 

Wheel Collider 생성

Tank 밑에 Wheels를 빈 오브젝트로 만든다.

 

 

그 아래에 FrontLeft 빈 오브젝트 만들고 컴포넌트로 Wheel Collider 추가

Mass는 질량, Radius는 바퀴 반지름

 

 

위치와 설정 값을 아래와 같이 세팅한다. 위치는 앞뒤좌우에 각각 아래와 같이 포지션 값을 주었다.

 

 


 

 

Tank는 PlayerTank로 이름 변경, Rigid Body 추가, Box Collider 추가

Rigidbody 컴포넌트를 추가하니 Wheel Collider가 보인다.

 

 

TankRenderers에 Box Collider를 추가한다. 콜라이더의 바닥은 지면에서 살짝 띄운다.

Wheel Collider가 Box Collider 안쪽에 들어가지는 않게 한다.

 

 


 

 

탱크에 그림자를 넣어주기 위해서 PlayerTank를 Unpack Completely 한다.

 

 

psd 파일 자체를 사용할 수 있다. 포토샵의 Layer 정보가 사라지지 않음. 대신 이미지 자체의 용량이 크다. 빌드가 무거워지지는 않음. 프로젝트 크기가 커질 뿐.

 

 

Tank 밑에 [3D Object] - [Quad] 생성, 탱크 아래에서 크기조정.

Material 하나 만든 뒤 Base Map에 스프라이트 넣기

 

 

Unlit 셰이더로 변경, Opaque에서 Transparent로 바꾼다. 그리고 Shadow의 Mesh Collider는 지운다.

먼가 기엽

 

 


 

 

PlayerController 스크립트 생성

Wheel Collider를 굴려서 움직이게 만들 것이다.

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        float v = Input.GetAxis("Vertical1");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }
    }
}

 

 

방향 전환 코드 추가

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        float v = Input.GetAxis("Vertical1");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }

        if(v == 0 )
        {
            foreach (var wheelCollider in wheelColliders)
            {
                wheelCollider.brakeTorque = 1000f; //아무것도 누르지 않을 때 브레이크가 걸리게 한다.
            }
        }

        //방향 바꾸기. 앞쪽 바퀴만 방향 전환
        float h = Input.GetAxis("Horizontal1");

        for(int i = 0; i < 2; i++) //0, 1인덱스가 둘 다 front wheel 이어야 한다.
        {
            wheelColliders[i].steerAngle = h * 45f;
        }
    }
}

 

 


 

 

카메라가 탱크를 따라다니게 하기 - SmoothFollow 스크립트 생성

유니티 에셋 스토에서도 standard asset 이름으로 만들어져있는 기능이 있다. 하지만 용량이 크므로 소스만 가져다가 쓸 것이다.

using UnityEngine;

#pragma warning disable 649
namespace UnityStandardAssets.Utility
{
	public class SmoothFollow : MonoBehaviour
	{

		// The target we are following
		[SerializeField]
		private Transform target;
		// The distance in the x-z plane to the target
		[SerializeField]
		private float distance = 10.0f;
		// the height we want the camera to be above the target
		[SerializeField]
		private float height = 5.0f;

		[SerializeField]
		private float rotationDamping;
		[SerializeField]
		private float heightDamping;

		// Use this for initialization
		void Start() { }

		// Update is called once per frame
		void LateUpdate()
		{
			// Early out if we don't have a target
			if (!target)
				return;

			// Calculate the current rotation angles
			var wantedRotationAngle = target.eulerAngles.y;
			var wantedHeight = target.position.y + height;

			var currentRotationAngle = transform.eulerAngles.y;
			var currentHeight = transform.position.y;

			// Damp the rotation around the y-axis
			currentRotationAngle = Mathf.LerpAngle(currentRotationAngle, wantedRotationAngle, rotationDamping * Time.deltaTime);

			// Damp the height
			currentHeight = Mathf.Lerp(currentHeight, wantedHeight, heightDamping * Time.deltaTime);

			// Convert the angle into a rotation
			var currentRotation = Quaternion.Euler(0, currentRotationAngle, 0);

			// Set the position of the camera on the x-z plane to:
			// distance meters behind the target
			transform.position = target.position;
			transform.position -= currentRotation * Vector3.forward * distance;

			// Set the height of the camera
			transform.position = new Vector3(transform.position.x ,currentHeight , transform.position.z);

			// Always look at the target
			transform.LookAt(target);
		}
	}
}

 

 

MainCamera에 넣는다. 설정 값도 조금 준다.

잘 된다 굿~

 

 


 

 

EnemyTank 만들기

Proejct 창에서 탱크 한 대 더 가져오기

 

 

Unpack Completely

 

 

기존에 들어있는 TankColour 복사해서 우리가 만든 Materials 폴더에 복사. 색을 바꿔서 EnemyTank에 적용한다. PlayerTank에 쓰인 Shadow도 그대로 가져온다.

 

 


 

 

EnemyController 스크립트 생성

플레이어와 거리를 계산해서 일정 거리 안으로 들어오면 적이 플레이어를 공격하도록 만든다.

먼저, 적이 돌아다닐 WayPoint들을 맵에 빈 오브젝트로 설정한다.

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;

    GameObject player;
    int waypointIndex;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    
}

 

 

스크립트를 EnemyTank에 넣고 WayPoints를 넣는다.

 

 

EnemyController 코드 추가

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] float patrolSpeed;

    GameObject player;
    int waypointIndex = 0;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Update()
    {
        if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
        {
            Move(waypoints[waypointIndex].transform.position, patrolSpeed);
        }
        else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
        {
            waypointIndex++;
            if(waypointIndex > waypoints.Length - 1) //다 돌았다면
            {
                waypointIndex = 0; //처음부터 다시 돌도록 만들기
            }
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

Patrol Speed를 10정도 주니 잘 간다.

 

 


 

 

플레이어, 적 탱크에 먼지 날리는 파티클 넣기.

 

 


 

 

EnemyController에 코드 추가

private void Start()
{
    player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
}

 

 

"Player" 태그 추가해서 적용한다.

 

 

switch문을 이용해서 상태를 변화시켰다.

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    GameObject player;
    int waypointIndex = 0;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 10 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                break;
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

적 탱크가 플레이어를 인식하자 빠르게 쫓아온다.

 

 


 

 

State.Chase 상태 코드 추가

case State.Chase:
    if(Vector3.Distance(player.transform.position, transform.position) < 5)
    {
        state = State.Attack;
        break;
    }
    else if(Vector3.Distance(player.transform.position, transform.position) >= 20)
    {
        state = State.Patrol;
        break;
    }
    Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
    break;

 

 

State.Attack 상태 코드 추가

총알 프리팹, 발사 위치 받아오기

[SerializeField] GameObject bullet;
[SerializeField] Transform bulletSpawnPoint;

 

 

발사 쿨타임 멤버 변수

float bulletCoolTime = 2f;
float previousTime = -999f;

 

 

지금까지의 EnemyController 전체 코드

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;

    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    GameObject player;
    int waypointIndex = 0;

    float bulletCoolTime = 2f;
    float previousTime = -999f;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 10 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                if(Vector3.Distance(player.transform.position, transform.position) < 5)
                {
                    state = State.Attack;
                    break;
                }
                else if(Vector3.Distance(player.transform.position, transform.position) >= 20)
                {
                    state = State.Patrol;
                    break;
                }
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                if(Vector3.Distance(player.transform.position, transform.position) >= 5)
                {
                    state = State.Chase;
                    break;
                }
                //공격

                break;
        }
    }

    void Attack(Vector3 targetPosition)
    {

    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

 

 

 


 

 

Shell 모델을 프리팹화 하기, Bullet 스크립트 생성해서 넣기

 

 

EnemyTank의 TankTurret에 SpawnPoint 설정하기

 

 

EnemyController 스크립트의 State.Attack 상태와 Attack 함수 마저 수정

case State.Attack:
    if(Vector3.Distance(player.transform.position, transform.position) >= 5)
    {
        state = State.Chase;
        break;
    }
    //공격
    Attack(player.transform.position);
    break;
void Attack(Vector3 targetPosition)
{
    Vector3 relativePosition = targetPosition - turret.transform.position;
    float angle = Mathf.Atan2(relativePosition.x, relativePosition.z) * Mathf.Rad2Deg; //아크탄젠트 값을 이용해서 터렛이 돌아갈 각도 얻어내기

    turret.transform.rotation = Quaternion.RotateTowards(turret.transform.rotation, Quaternion.Euler(0, angle, 0), 1f);

    //총알 발사
    if(previousTime + bulletCoolTime < Time.time)
    {
        Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
        previousTime = Time.time;
    }
}

 

EnemyController 전체 코드

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;

    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    GameObject player;
    int waypointIndex = 0;

    float bulletCoolTime = 2f;
    float previousTime = -999f;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 10 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                if(Vector3.Distance(player.transform.position, transform.position) < 5)
                {
                    state = State.Attack;
                    break;
                }
                else if(Vector3.Distance(player.transform.position, transform.position) >= 20)
                {
                    state = State.Patrol;
                    break;
                }
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                if(Vector3.Distance(player.transform.position, transform.position) >= 5)
                {
                    state = State.Chase;
                    break;
                }
                //공격
                Attack(player.transform.position);
                break;
        }
    }

    void Attack(Vector3 targetPosition)
    {
        Vector3 relativePosition = targetPosition - turret.transform.position;
        float angle = Mathf.Atan2(relativePosition.x, relativePosition.z) * Mathf.Rad2Deg; //아크탄젠트 값을 이용해서 터렛이 돌아갈 각도 얻어내기

        turret.transform.rotation = Quaternion.RotateTowards(turret.transform.rotation, Quaternion.Euler(0, angle, 0), 1f);

        //총알 발사
        if(previousTime + bulletCoolTime < Time.time)
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
            previousTime = Time.time;
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

총알은 생성되지만 아직 발사는 안된다.

 

 


 

 

총알에 들어있는 Bullet 스크립트 작성

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

[RequireComponent(typeof(Rigidbody))] //Rigidbody가 있어야한다.
public class Bullet : MonoBehaviour
{
    [SerializeField] float force = 500f;
    private void Start()
    {
        GetComponent<Rigidbody>().AddForce(transform.forward * force); //transform.forward는 방향벡터
    }
}

총알이 바닥을 뚫고 내려가므로 총알에도 Capsule Collider를 추가한다.

 

 


 

 

총알이 다른 콜라이더에 닿으면 폭발하도록 설정

Bullet 함수 수정

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

[RequireComponent(typeof(Rigidbody))] //Rigidbody가 있어야한다.
public class Bullet : MonoBehaviour
{
    [SerializeField] float force = 500f;
    [SerializeField] GameObject explosion;
    private void Start()
    {
        GetComponent<Rigidbody>().AddForce(transform.forward * force); //transform.forward는 방향벡터
    }

    private void OnCollisionEnter(Collision collision)
    {
        GameObject effect = Instantiate(explosion, transform.position, Quaternion.identity); //폭발 프리팹 가져오기
        Destroy(gameObject);
        Destroy(effect, 2f); //2초 뒤에 없어진다.
    }
}

파티클 시스템 설정에서 Play On Awake 체크

총알 발사가 잘 된다.

 

 


 

 

플레이어가 총알을 발사하도록 만들기

PlayerController 스크립트 수정

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;
    [SerializeField] GameObject turret;

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        //총알 발사
        if (Input.GetMouseButtonDown(0))
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
        }

        float v = Input.GetAxis("Vertical");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }

        if(v == 0 )
        {
            foreach (var wheelCollider in wheelColliders)
            {
                wheelCollider.brakeTorque = 1000f; //아무것도 누르지 않을 때 브레이크가 걸리게 한다.
            }
        }

        //방향 바꾸기. 앞쪽 바퀴만 방향 전환
        float h = Input.GetAxis("Horizontal");

        for(int i = 0; i < 2; i++) //0, 1인덱스가 둘 다 front wheel 이어야 한다.
        {
            wheelColliders[i].steerAngle = h * 45f;
        }
    }
}

 

 

플레이어의 터렛 회전 시키기

//turret 회전
if (Input.GetKey(KeyCode.Q))
{
    turret.transform.Rotate(-Vector3.up * 20 * Time.deltaTime);
}
if (Input.GetKey(KeyCode.E))
{
    turret.transform.Rotate(Vector3.up * 20 * Time.deltaTime);
}

 

전체 코드

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;
    [SerializeField] GameObject turret;

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        //총알 발사
        if (Input.GetMouseButtonDown(0))
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
        }

        //turret 회전
        if (Input.GetKey(KeyCode.Q))
        {
            turret.transform.Rotate(-Vector3.up * 20 * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.E))
        {
            turret.transform.Rotate(Vector3.up * 20 * Time.deltaTime);
        }

        //이동
        float v = Input.GetAxis("Vertical");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }

        if(v == 0 )
        {
            foreach (var wheelCollider in wheelColliders)
            {
                wheelCollider.brakeTorque = 1000f; //아무것도 누르지 않을 때 브레이크가 걸리게 한다.
            }
        }

        //방향 바꾸기. 앞쪽 바퀴만 방향 전환
        float h = Input.GetAxis("Horizontal");

        for(int i = 0; i < 2; i++) //0, 1인덱스가 둘 다 front wheel 이어야 한다.
        {
            wheelColliders[i].steerAngle = h * 45f;
        }
    }
}

 

 

카메라도 플레이어의 터렛을 따라가려면 카메라 타겟을 플레이어의 TankTurret으로 주면 된다.

 

 


 

 

Aim 만들기

캔버스 하나 만들고 이미지 2개 만들고, 이미지 하나는 Y 스케일을 0.2, 다른 이미지는 X 스케일을 0.2로 하면 화면 가운데에 에임이 생긴다. Aim의 Pos Y 값을 650 으로 키우면 탄이 떨이지는 곳과 비슷하게 잡힌다.

 

 


 

 

데미지를 부여해서 체력이 깎이게 만들기

EnemyController 스크립트에 다음 내용 추가

private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.tag == "bullet")
    {
        hp -= 0.1f;
        Debug.Log("Enemy HP: " + hp);
    }

    if (hp <= 0)
    {
        Destroy(gameObject);
    }
}

 

EnemyController 스크립트 전체 코드

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;

    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    float hp = 1f;

    GameObject player;
    int waypointIndex = 0;

    float bulletCoolTime = 2f;
    float previousTime = -999f;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 50 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                if(Vector3.Distance(player.transform.position, transform.position) < 30)
                {
                    state = State.Attack;
                    break;
                }
                else if(Vector3.Distance(player.transform.position, transform.position) >= 50)
                {
                    state = State.Patrol;
                    break;
                }
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                if(Vector3.Distance(player.transform.position, transform.position) >= 30)
                {
                    state = State.Chase;
                    break;
                }
                //공격
                Attack(player.transform.position);
                break;
        }
    }

    void Attack(Vector3 targetPosition)
    {
        Vector3 relativePosition = targetPosition - turret.transform.position;
        float angle = Mathf.Atan2(relativePosition.x, relativePosition.z) * Mathf.Rad2Deg; //아크탄젠트 값을 이용해서 터렛이 돌아갈 각도 얻어내기

        turret.transform.rotation = Quaternion.RotateTowards(turret.transform.rotation, Quaternion.Euler(0, angle, 0), 1f);

        //총알 발사
        if(previousTime + bulletCoolTime < Time.time) //직전 Time.time에서 쿨타임 더한 값이 현재 Time.time보다 작아지면, 즉 2초가 지나면
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
            previousTime = Time.time;
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "bullet")
        {
            hp -= 0.1f;
            Debug.Log("Enemy HP: " + hp);
        }

        if (hp <= 0)
        {
            Destroy(gameObject);
        }
    }
}

 

총알에 bullet 태그를 생성해서 달아준다.

적의 체력이 잘 깎이고 적이 죽는다!

 

 


 

 

플레이어에 체력 추가

PlayerController 스크립트에 추가

private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.tag == "bullet")
    {
        hp -= 0.1f;
        Debug.Log("Player HP: " + hp);
    }

    if(hp <= 0)
    {
        Destroy(gameObject);
    }
}

 

전체코드

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;
    [SerializeField] GameObject turret;

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    float hp = 1;

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        //총알 발사
        if (Input.GetMouseButtonDown(0))
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
        }

        //turret 회전
        if (Input.GetKey(KeyCode.Q))
        {
            turret.transform.Rotate(-Vector3.up * 20 * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.E))
        {
            turret.transform.Rotate(Vector3.up * 20 * Time.deltaTime);
        }

        //이동
        float v = Input.GetAxis("Vertical");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }

        if(v == 0 )
        {
            foreach (var wheelCollider in wheelColliders)
            {
                wheelCollider.brakeTorque = 1000f; //아무것도 누르지 않을 때 브레이크가 걸리게 한다.
            }
        }

        //방향 바꾸기. 앞쪽 바퀴만 방향 전환
        float h = Input.GetAxis("Horizontal");

        for(int i = 0; i < 2; i++) //0, 1인덱스가 둘 다 front wheel 이어야 한다.
        {
            wheelColliders[i].steerAngle = h * 45f;
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "bullet")
        {
            hp -= 0.1f;
            Debug.Log("Player HP: " + hp);
        }

        if(hp <= 0)
        {
            Destroy(gameObject);
        }
    }
}

 

 


 

 

탱크 죽을 때 폭발 추가

적과 플레이어 스크립트에 각각 수정한다.

[SerializeField] GameObject explosionEffect;
private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.tag == "bullet")
    {
        hp -= 0.1f;
        Debug.Log("Enemy HP: " + hp);
    }

    if (hp <= 0)
    {
        GameObject effect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
        Destroy(gameObject);
        Destroy(effect, 2f);
    }
}

 

 

EnemyController 전체 스크립트

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;

    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    [SerializeField] GameObject explosionEffect;

    float hp = 1f;

    GameObject player;
    int waypointIndex = 0;

    float bulletCoolTime = 2f;
    float previousTime = -999f;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 50 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                if(Vector3.Distance(player.transform.position, transform.position) < 30)
                {
                    state = State.Attack;
                    break;
                }
                else if(Vector3.Distance(player.transform.position, transform.position) >= 50)
                {
                    state = State.Patrol;
                    break;
                }
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                if(Vector3.Distance(player.transform.position, transform.position) >= 30)
                {
                    state = State.Chase;
                    break;
                }
                //공격
                Attack(player.transform.position);
                break;
        }
    }

    void Attack(Vector3 targetPosition)
    {
        Vector3 relativePosition = targetPosition - turret.transform.position;
        float angle = Mathf.Atan2(relativePosition.x, relativePosition.z) * Mathf.Rad2Deg; //아크탄젠트 값을 이용해서 터렛이 돌아갈 각도 얻어내기

        turret.transform.rotation = Quaternion.RotateTowards(turret.transform.rotation, Quaternion.Euler(0, angle, 0), 1f);

        //총알 발사
        if(previousTime + bulletCoolTime < Time.time) //직전 Time.time에서 쿨타임 더한 값이 현재 Time.time보다 작아지면, 즉 2초가 지나면
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
            previousTime = Time.time;
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "bullet")
        {
            hp -= 0.1f;
            Debug.Log("Enemy HP: " + hp);
        }

        if (hp <= 0)
        {
            GameObject effect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
            Destroy(gameObject);
            Destroy(effect, 2f);
        }
    }
}

 

 

PlayerController 전체 스크립트

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

public class PlayerController : MonoBehaviour
{
    [SerializeField] WheelCollider[] wheelColliders; //바퀴가 4개니까 배열로 가져온다.
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;
    [SerializeField] GameObject turret;

    Rigidbody rigidBody; //매번 GetComponent로 가져오지 않게 하기 위해서 미리 선언.

    [SerializeField] GameObject explosionEffect;

    float hp = 1;

    private void Start()
    {
        rigidBody = GetComponent<Rigidbody>(); //Rigidbody 가져오기
    }

    private void Update() //Fixed 업데이트는 일정한 간격으로 호출되는 메서드 물리와 같은 처리를 함.
    {
        //총알 발사
        if (Input.GetMouseButtonDown(0))
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
        }

        //turret 회전
        if (Input.GetKey(KeyCode.Q))
        {
            turret.transform.Rotate(-Vector3.up * 20 * Time.deltaTime);
        }
        if (Input.GetKey(KeyCode.E))
        {
            turret.transform.Rotate(Vector3.up * 20 * Time.deltaTime);
        }

        //이동
        float v = Input.GetAxis("Vertical");

        foreach (var wheelCollider in wheelColliders)
        {
            //v에는 앞으로 가면 1이, 뒤로 가면 -1이 나온다.
            wheelCollider.brakeTorque = 0f; //브레이크는 안쓴다.
            wheelCollider.motorTorque = v * 500f; //모터에 힘을 준다.
        }

        if(v == 0 )
        {
            foreach (var wheelCollider in wheelColliders)
            {
                wheelCollider.brakeTorque = 1000f; //아무것도 누르지 않을 때 브레이크가 걸리게 한다.
            }
        }

        //방향 바꾸기. 앞쪽 바퀴만 방향 전환
        float h = Input.GetAxis("Horizontal");

        for(int i = 0; i < 2; i++) //0, 1인덱스가 둘 다 front wheel 이어야 한다.
        {
            wheelColliders[i].steerAngle = h * 45f;
        }
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "bullet")
        {
            hp -= 0.1f;
            Debug.Log("Player HP: " + hp);
        }

        if(hp <= 0)
        {
            GameObject effect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
            Destroy(gameObject);
            Destroy(effect, 2f);
        }
    }
}

 

**적과 플레이어에 중복되는 표현이 많아서 부모 클래스를 만들어서 설정했어도 좋았을 것 같다.

 

 

탱크 폭발 파티클에서 Play On Awake 체크

 

 


 

 

적 탱크 체력바 UI 표시하기

캔버스 하나 새로 만들기

 

 

Render Mode는 World Space

 

Event Camera는 Main Camera를 끌어놓는다.

 

 

HPCanvas를 잡고 EnemyTank의 Children으로 끌어놓는다.

 

 

HPCanvas Transform 수정

 

 

작아진 HPCanvas를 잡고 위로 올린다.

 

 

자식으로 Image 하나 만들어서 Stretch한다.

 

 

모든 Transform은 0으로 하면 캔버스에 꽉 차게 나온다. 해당 이미지 이름은 "Background"로 바꾼다.

 

 

Background 이미지 아래에 "Bar" 이미지 추가

 

 

색과 Transform 설정하기

 

 

체력 게이지가 한쪽으로 줄어들게 하기 위해서 Alt + Shift + 왼쪽으로 설정

 

 

HPBar 스크립트 생성

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

public class HPBar : MonoBehaviour
{
    [SerializeField] Image bar;

    RectTransform rectTransform;
    Transform mainCamera; //카메라 변수 선언

    private void Start()
    {
        rectTransform = bar.GetComponent<RectTransform>();
        mainCamera = Camera.main.transform;
    }

    public void SetValue(float value)
    {
        rectTransform.localScale = new Vector3(value, 1, 1);
    }

    private void LateUpdate() //다른 Update가 끝나고 나면, 호출. 카메라의 이동이 끝나면 UI가 움직이게 된다.
    {
        transform.LookAt(transform.position + mainCamera.rotation * Vector3.forward, mainCamera.rotation * Vector3.up); //메인 카메라를 바라본다.
    }
}

 

 

EnemyController 스크립트에서는 HPbar를 가져와야한다. SetValue에 hp 값을 준다.

[SerializeField] HPBar hpBar;
private void OnCollisionEnter(Collision collision)
{
    if (collision.gameObject.tag == "bullet")
    {
        hp -= 0.1f;
        hpBar.SetValue(hp);
    }

    if (hp <= 0)
    {
        GameObject effect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
        Destroy(gameObject);
        Destroy(effect, 2f);
    }
}

 

 

EnemyController 전체 스크립트

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

public class EnemyController : MonoBehaviour
{
    [SerializeField] Transform[] waypoints;
    [SerializeField] GameObject turret;
    [SerializeField] GameObject bullet;
    [SerializeField] Transform bulletSpawnPoint;

    [SerializeField] float patrolSpeed = 3f;
    [SerializeField] float chaseSpeed = 7f;

    [SerializeField] GameObject explosionEffect;

    [SerializeField] HPBar hpBar;

    float hp = 1f;

    GameObject player;
    int waypointIndex = 0;

    float bulletCoolTime = 2f;
    float previousTime = -999f;

    public enum State { Patrol, Chase, Attack } //상태 설정
    public State state; //열거형을 담을 변수

    private void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player"); //FindGameObjectWithTag 웬만하면 쓰지 말고 써도 start 함수에서 한번만 쓰자.
        state = State.Patrol; //처음 상태는 State.Patrol 상태
    }

    private void Update()
    {
        //플레이어와 거리가 가까워지면 상태변화
        switch (state)
        {
            case State.Patrol:
                //플레이어와의 거리 확인
                if(Vector3.Distance(transform.position, player.transform.position) < 50 )
                {
                    state = State.Chase;
                    break; //상태를 바꿨으니까 break;로 빠져나온다. 안쓰면 아래 if문도 실행된다. Update함수가 끝난다. 그 다음 프레임에서 Update가 실행되고 Chase로 다시 간다.
                }

                if (Vector3.Distance(waypoints[waypointIndex].transform.position, transform.position) > 0.1f) //두 거리를 비교한 뒤에 거리의 차이가 있다면 해당 waypoint로 이동
                {
                    Move(waypoints[waypointIndex].transform.position, patrolSpeed);
                }
                else //현재 waypoint에 도달한 상태라면(두 거리의 차가 0.1 이하라면)
                {
                    waypointIndex++;
                    if (waypointIndex > waypoints.Length - 1) //다 돌았다면
                    {
                        waypointIndex = 0; //처음부터 다시 돌도록 만들기
                    }
                }
                break;
            case State.Chase:
                if(Vector3.Distance(player.transform.position, transform.position) < 30)
                {
                    state = State.Attack;
                    break;
                }
                else if(Vector3.Distance(player.transform.position, transform.position) >= 50)
                {
                    state = State.Patrol;
                    break;
                }
                Move(player.transform.position, chaseSpeed); //플레이어를 향해서 Move. patrolSpeed 보다 더 빠르게
                break;
            case State.Attack:
                if(Vector3.Distance(player.transform.position, transform.position) >= 30)
                {
                    state = State.Chase;
                    break;
                }
                //공격
                Attack(player.transform.position);
                break;
        }
    }

    void Attack(Vector3 targetPosition)
    {
        Vector3 relativePosition = targetPosition - turret.transform.position;
        float angle = Mathf.Atan2(relativePosition.x, relativePosition.z) * Mathf.Rad2Deg; //아크탄젠트 값을 이용해서 터렛이 돌아갈 각도 얻어내기

        turret.transform.rotation = Quaternion.RotateTowards(turret.transform.rotation, Quaternion.Euler(0, angle, 0), 1f);

        //총알 발사
        if(previousTime + bulletCoolTime < Time.time) //직전 Time.time에서 쿨타임 더한 값이 현재 Time.time보다 작아지면, 즉 2초가 지나면
        {
            Instantiate(bullet, bulletSpawnPoint.position, bulletSpawnPoint.rotation);
            previousTime = Time.time;
        }
    }

    void Move(Vector3 targetPosition, float speed)
    {
        Vector3 relativePosition = targetPosition - transform.position;
        relativePosition.Normalize();

        transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation(relativePosition), 1); //두 지점 차이의 방향으로 normal, 그 각도 사이를 RotateTowards로 돌리기. 한 프레임에 1도씩
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }

    private void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.tag == "bullet")
        {
            hp -= 0.1f;
            hpBar.SetValue(hp);
        }

        if (hp <= 0)
        {
            GameObject effect = Instantiate(explosionEffect, transform.position, Quaternion.identity);
            Destroy(gameObject);
            Destroy(effect, 2f);
        }
    }
}

 

 

HPCanvas에 스크립트 넣고 Bar를 넣는다.

 

 

Enemy Tank에는 스크립트가 들어가있는 HPCanvas를 넣는다.

잘 보면 적의 HP바가 점점 닳는 것을 볼 수 있다. (맞추기가 힘들다)

 

 


 

 

책의 14장까지는 이해해야한다. 할만하면 19장까지 충실하게 보기

728x90
반응형