나 개발자 진짜 되냐?

[ Unity 3D 서바이벌 게임 만들기 10 ] 적 생성과 로직 본문

유니티를 공부해봐요!/중급이에요!

[ Unity 3D 서바이벌 게임 만들기 10 ] 적 생성과 로직

Snow Rabbit 2024. 11. 2. 20:18

 

곰이 나를 위협한다..

곰을 잡아보자!!


 

곰의 경우

자연스레 움직이게 해야 하는데

그 부분을 우리는 ai로 구현할 예정이다.

 

패키지매니저에서

이 친구 설치!

설치하게 되면 AI가 생기는데

두 번째 Obsolete를 눌러주자

 

이것저것 많은데

여기서 일단

레이어 느낌이랑 비슷하게

선택할 수 있는 곳이 있다.

 

이렇게 되면

 

보통 오브젝트에서

만지작해주는데

못 걷는 곳

 

걷는 곳 이렇게 레이어를 설정해 줄 수 있다.

 

이런 레이어들을 설정해 주고

 

Bake로 가서 구워주어야

한다.

 

 Bake 해주면

 

이렇게 경계선이 생기고

걷지 못하는 곳과 걷는 곳이 생기게 된다.

 

근데 잘 보면 문제가 있다.

나무나 돌의 경우

 

이렇게 있으면

돌이나 나무는 지나 가지면 안 되는데

전체적으로 다 갈 수 있게

경계선이 없는 것 같다.

 

 

Resource_Tree 인스펙터에 들어와서

컴포넌트를 하나 추가해 주자

 

Move Threshoid : 세팅된 값 이상의 포지션을 움직이게 되면

피해 가는 장애물의 영역을 개선

 

 

Time To stationary : 장애물이 정지되었다고 생각하게 되는 시간

 

Carve Only Stationary : 장애물이 정지되었을 때 이렇게 구멍을 뚫어서

못가는 구역으로 만들어 주는 친구이다.

 

이렇게..!

 

 

자, 이제 NPC를 만들어보자

빈 오브젝트를 하나 만들고 NPC라고 저장한 다음에

 

Bear을 검색해서 나오는 프리팹을( 색상이 있는 친구 ) 끌어다가 넣어준다.

 

npc와 bear 위치를 싹 초기화해 주고

 

위치를 w키로 조절해서 두고 싶은 곳에 둔다.

 

다음에 

npc 오브젝트 안에

 

Animator 컴포넌트 다운로드 후

 

 

애니메이션에

곰 모션 추가해 준다.

 

npc파일 생성!

애니메이션 컨트롤러도 생성!

 

생성해 주었으면

미리

인스펙터창에 넣어주고

 

애니메이터 창에서

bool 값으로 Moving을 만들고

Tigger로 Attack를 만들어준다.

 

 

 

여기서

idle

attack

그리고

workforward

 

세 개를 끌어다가 온다.

 

 

이렇게 설정!

 

다음에 주황주황 블록을 눌러서

 

Make Transition

해서 선 그어주고

또 가만히 있다가 때리는 게 아니니까

WalkForward 에다가

Make Transition

해서 attack 할 수 있도록

공격 후 다시 제자리로 돌아와야 하니

idel로 Make Transition

 

 

역시 말보단 사진이 최고야..

 

현재 파란색 줄을 눌러보면

 

 

 컨디션에

무빙을 해줘서 움직일 수 있게 해 주고

 

has exit time을 해제해서

바로 시작할 수 있게끔 해준다.

 

반대 화살표는

moving false 해준다

 

 

공격을 하게 되면

 

공격기능이 활성화되어야겠죠?!

 

밑에 attack 해주고

반대화살표는 해줄 거 없다!

 

WalkForward > attack 에도

attack로 해줘야 한다.

 

< 정리 >

 

Idle → Attack : HasExitTime ( ) , Attack

Idle → WalkForward : HasExitTime ( ) , Moving (true)

 

WalkForward → Idle : HasExitTime ( ) , Moving (false)

WalkForward → Attack : HasExitTime ( ) , Attack

 

Attack → Idle : HasExitTime ( V ) , Moving (false)

Attack → WalkForward : HasExitTime ( V ) , Moving (true)

 

자 이제

NPC 안에다가

컴포넌트를 3개 만들자

 

1. 스크립트

스크립트 폴더에 가서 NPC라는 친구 만들고

 

그 안에다가 넣기

 

2. box Collider 넣어서 충돌감지!

사이즈도 지정

 

3. Nav Mesh Agent

여기는 Agent를 넣어서 여기 값을

코드를 통해 수정할 계획!

 

4. 움직이게 하려면 Animator까지!

 

자 NPC 스크립트를 짜보자!

 

< NPC.cs >

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

public enum AIState
{
    Idle,
    Wandering, // 임의로 목표 찍어서 움직이게 하기
    Attacking, // 공격
    Fleeing // 도망
}


public class NPC : MonoBehaviour, IDamagable
{
    [Header("Stats")]
    public int health; // NPC체력
    public float walkSpeed; // 걷는 속도
    public float runSpeed; // 뛰는 속도
    public ItemData[] dropOnDeath; // 죽었을때 떨어뜨리는 데이터

    [Header("AI")]
    public float detectDistance; // 목표지점까지 거리
    public float safeDistance;
    private AIState aiState; //열거형 친구들

    [Header("Wandering")] // 자동으로 목표를 찍고 이동할때
    public float minWanderDistance; // 최솟값
    public float maxWanderDistance; // 최댓값
    public float minWanderWaitTime; // 새로운 목표지점을 찍을 때 기다리는 시간을
    public float maxWanderWaitTime; // 최댓값 최솟값을 받아서 랜덤으로 시간 추출

    [Header("Combat")] // NPC 공격
    public int damage; // 데미지
    public float attackRate; // 얼마나 공격간의 텀을 줄지
    private float lastAttackTime; // 마지막 공격한 시각
    public float attackDistance; // 공격 가능한 거리

    private float playerDistance; // 플레이어와의 거리

    public float fieldOfView = 120f; // 몬스터를 기준으로 시야각 지정

    private NavMeshAgent agent; // NPC, 즉 곰
    private Animator animator; // 애니메이터
    private SkinnedMeshRenderer[] meshRenderers; // 몬스터들의 각종 매쉬들의 정보 리스트

    private void Awake()
    {
        agent = GetComponent<NavMeshAgent>();
        animator = GetComponentInChildren<Animator>();
        //리스트기도하고 되게 다양할 수 있어서 Children 으로 !
        meshRenderers = GetComponentsInChildren<SkinnedMeshRenderer>();
    }

    private void Start()
    {   //시작할때 스테이트를 정해줘야한다. 
        SetState(AIState.Wandering);
    }

    private void Update()
    {   // 플레이어의 거리에 따라 상태가 바뀔테니, 플레이어 위치를 잘 가져와야한다.
        playerDistance = Vector3.Distance(transform.position, CharacterManager.Instance.Player.transform.position);
        // 애니메이션도 상태를 계속 호출 할 것이다. 가만히 있는지 아닌지
        animator.SetBool("Moving", aiState != AIState.Idle);

        switch (aiState) // 열거형에 맞게 어떤게 나오면 호출할지 적어주어야한다.
        {
            case AIState.Idle:
                PassiveUpdate();
                break;
            case AIState.Wandering:
                PassiveUpdate();
                break;
            case AIState.Attacking:
                AttackingUpdate();
                break;
            case AIState.Fleeing:
                FleeingUpdate();
                break;
        }
    }

    private void SetState(AIState state)
    {
        aiState = state; // 열거형 친구들

        switch (aiState)
        {
            case AIState.Idle: // 가만히 서있을 때
                agent.speed = walkSpeed;
                agent.isStopped = true; // 정지해있다.
                break;
            case AIState.Wandering: // 목표지점을 찍고 이동
                agent.speed = walkSpeed; // 걷는다
                agent.isStopped = false; // 안멈춰있다. = 움직인다.
                break;
            case AIState.Attacking: // 공격범위에 들어오면 뛰어올것
                agent.speed = runSpeed; // 뛰어온다
                agent.isStopped = false; // 안멈춘다.
                break;
            case AIState.Fleeing: // 때린다
                agent.speed = runSpeed; //
                agent.isStopped = false;
                break;
        }
        // 이렇게 나누어주면 runspeed일때 비례해서 값이 늘어나게 된다.
        animator.speed = agent.speed / walkSpeed;
                                                  
    }
    void PassiveUpdate()
    {   //상태가 원더링이거나, 목표지점을 찍고 남은거리가 0.1보다 작으면
        if (aiState == AIState.Wandering && agent.remainingDistance < 0.1f)
        { 
            SetState(AIState.Idle); // 잠시 멈춘다.
            Invoke("WanderToNewLocation", Random.Range(minWanderWaitTime, maxWanderWaitTime));
                // "" 안에 있는 함수 호출, 랜덤한 시간대에!
        }

        if (playerDistance < detectDistance) // 만약에 거리가 가까워지면 
        {
            SetState(AIState.Attacking); //공격함수를 탄다.
        }
    }

    void WanderToNewLocation() //반복적으로 다음 목표지점을 호출하는 함수
    {
        // 방어코드 작성
        if (aiState != AIState.Idle)
        {
            return;
        }
        SetState(AIState.Wandering); //상태를 원더링으로 바꿈
        agent.SetDestination(GetWanderLocation()); // 목표지점을 정하자.
    }

    Vector3 GetWanderLocation() // 목표지점
    {
        NavMeshHit hit; // 변수
        // 현재위치 + 랜덤영역 + out hit + 최대거리 + laymask
        NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);

        int i = 0;
        // 목표지점이 너무 가까우면 곤란하니
        while (GetDestinationAngle(hit.position) > 90 || playerDistance < safeDistance)
        {
            // 반복해서 수행한다.
            NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
            i++; // 그래도 거리가 멀수도 있으니 한번 더해봐라! 하는 의미로 i값을 추가
            if (i == 30) // 한 30번 해보자!
                break;
        }
        return hit.position; //포지션 가져오기
    }

    void AttackingUpdate()
    {   //플레이어 위치가 공격할 수 있는거리까지 오지 않거나, 또는 시야각에 내가 안보이면 진입!
        if (playerDistance > attackDistance || !IsPlayerInFieldOfView())
        {
            agent.isStopped = false; // 안멈춘다. 쫒아온다. 
            NavMeshPath path = new NavMeshPath(); // 경로를 가져온다.
            if (agent.CalculatePath(CharacterManager.Instance.Player.transform.position, path)) // 경로를 계산해주는 친구 t/f로 나오는 값
            {  
                agent.SetDestination(CharacterManager.Instance.Player.transform.position); // 목표지점으로 간다.
            }
            else // 강으로 들어갔다면 다시 임의의 지점으로 이동해야한다.
            {
                SetState(AIState.Fleeing);
            }
        }
        else
        {
            agent.isStopped = true; // 잠시 멈추고
            if (Time.time - lastAttackTime > attackRate) // 마지막 공격한 시간을 현재시간에서 빼주고
            {   // 그값이 공격텀시간보다 커지면 즉, 재공격이 가능해지면
                lastAttackTime = Time.time; // 현재시간 넣고 ( 시간 초기화 )
                //IDamagble 컴포넌트를 가져와서 값을 넣어주며, 플레이어의 체력이 깎일것이다.
                CharacterManager.Instance.Player.controller.GetComponent<IDamagable>().TakePhysicalDamage(damage);
                animator.speed = 1; // 멈춰있으니 속도는 0
                animator.SetTrigger("Attack"); // 그리고 attack로 해서 움직이게!
            }
        }
    }

    bool IsPlayerInFieldOfView() // 시야각
    {
        Vector3 directionToPlayer = CharacterManager.Instance.Player.transform.position - transform.position;
        float angle = Vector3.Angle(transform.forward, directionToPlayer); // 내가 정면으로 바라보는 위치의 각도
        return angle < fieldOfView * 0.5f; //120도를 반으로 나누어서 오른쪽으로 가는지 왼쪽으로 가는지 알게한다.
    }

    public void TakePhysicalDamage(int damageAmount)
    {
        health -= damageAmount;
        if (health <= 0) // 체력이 다 닳았으니 죽는다.
            Die();

        StartCoroutine(DamageFlash()); // 데미지 효과가 나야한다.
    }

    void FleeingUpdate()
    {
        if (agent.remainingDistance < 0.1f) //거리가 가까워지면
        {
            agent.SetDestination(GetFleeLocation()); //받아온 값으로 도망친다.
        }
        else
        {
            SetState(AIState.Wandering);
        }
    }

    Vector3 GetFleeLocation()
    {
        NavMeshHit hit;

        NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);

        int i = 0;
        while (GetDestinationAngle(hit.position) > 90 || playerDistance < safeDistance)
        {

            NavMesh.SamplePosition(transform.position + (Random.onUnitSphere * safeDistance), out hit, maxWanderDistance, NavMesh.AllAreas);
            i++;
            if (i == 30)
                break;
        }
        
        return hit.position;
    }


    float GetDestinationAngle(Vector3 targetPos)
    {   
        return Vector3.Angle(transform.position - CharacterManager.Instance.Player.transform.position, transform.position + targetPos);
    }


    private void Die()
    {
        for (int x = 0; x < dropOnDeath.Length; x++) // 모든 데이터 아이템들을
        {   // 생성한다. 위치는 내주변에서 2만큼 떨어진 곳에, 회전은 안되도록
            Instantiate(dropOnDeath[x].dropPrefab, transform.position + Vector3.up * 2, Quaternion.identity);
        }
        // 몬스터는 사라진다.
        Destroy(gameObject);
    }

    IEnumerator DamageFlash() // 코루틴
    {
        for (int x = 0; x < meshRenderers.Length; x++)
            meshRenderers[x].material.color = new Color(1.0f, 0.6f, 0.6f); //색을 다 바꿔준다.

        yield return new WaitForSeconds(0.1f); //코루틴 리턴, 0.1초 기다렸다가 밑에 식 실행
        for (int x = 0; x < meshRenderers.Length; x++)
            meshRenderers[x].material.color = Color.white; // 다시 색을 돌려놓는다.
    }
}

 

일단 변수를 쭈욱 써보자

 

열거형도 하나 만든다.

그리고 내려가서

필요한 부분을 get해온다.

 

여기서 mesh를 리스트로 받아서 이렇게 get 한 이유는

공격받았을 때는 약간 색을 다르게 해 주려고 계획하고 있기 때문!

 

해주고 start를 통해서 우리는 이 친구가

어떤 동작을 할지 알려주어야 한다.

 

그러니 일단 그 동작을 하는 함수를 만들어주자

 

 

 

함수에는

각각 열거형의 기능들에 대해 적혀있다.

 

다음에 이 값을 start에 넣어주자.

 

 

자 다음에는

Update문으로 지속적으로 반복해야 하는 값을 넣어주어야 한다.

 

Passive Update

Attacking Update

상태에 따라

함수를 계속 돌려줄 예정이다.

 

animator.SetBool("Moving", aiState!= AIState.Idle);

 

여기 해석해 보면

idle로 잡고

만약에 idle이 아니면

뒷 식은 참이 되는 거니까

moving true로 계속 걸을 것이다.

 

근데 만약에 idle가 맞으면 뒤이 조건식이 틀려서 안 걷게 될 것이다.

 

시작을 우리는 Wandering로 했기 때문에

idle는 false라서 걷는 거부터 시작할 것이다.

 

passive부터 보자!

이렇게 해주고

WanderToNewLocation을 만들어주자

이렇게 해주면!

반복적으로 목표지점을 호출 가능하다

 

밑에 또 새로운 함수가 생겼다.

GetWanderLocation

목표지점에 대한 정보 메서드이다.

 

여기 코드 중간에

SamplePosition이 있는데

 

자세히 들어가 보면

여기 매개변수로

out NavMeshHit이라는 변수가 들어가게 된다

그래서 우리는 위에

NavMeshHit hit;를 넣어준 것!

 

sourcePosition에다가 우리가 지정할 영역을 설정해 주고

NavMeshHit으로 그 포지션 안에서 이동경로 한에서 최단 경로를 반환해 준다.

그리고 최고거리, 그리고 LayMask도 받아 필터링을 걸 수 있다.

 

중간쯤

Random.onUnitSphere가 있는데

이 부분은 반지름이 1인 구이다.

가상의 구를 만들어서 영역을 정하게 된다.

 

 

아!

while문 안에 식은

원래 위치에서 내 위치를 뺀 값이다.

 

자 이제 두 번째 Attacking

이제 거리가 가까워지면 공격을 하는데

공격을 하려면

곰의 시야에 내가 들어와야 한다.

 

그래서 시야각을 설정해주어야 한다.

시야각은 bool값으로 정한다.

 

만들어주고 

 

 

공격을 작성해 주면 된다.

 

이제 곰이 공격했으니

이제 내 차례다!

 

공격은 우리

IDamageable이라고 인터페이스로 만들어주었었다.

 

그 친구를 가져오는 법은 간단하다.

 

 

1. 클래스에 가서

 

이렇게 해주고

빨간 줄에서

ctrl.

 

인터페이스이기 때문에

상속받은 이상

무조건 인터페이스가 정의한 메서드를 선언해주어야 한다.

그래서 빨간 것!

 

인터페이스 구현해 주면

 

 

맨 밑줄에 이 친구가 뜬다

 

 

작성

새로운 함수 두 개가 보인다.

Die

DamageFlash

 

DamageFlash의 경우 코루틴으로 만들어보자

 

 

 

 

마지막으로

 Fleeing 도망

 

Get도 만들어보자!

 

이 식은 아까 GetWander과 똑같다.

 

그래서 주석 패스!

 

자 이제 유니티 와서 값 설정해 주자!

 

 

애니메이터에는

이렇게 넣어주면 되는데

컨트롤러에는 우리 아까 블록블록 넣어주고

밑에 아바타는

우리 곰 프리팹을 눌러보면

애니메이터가 잘 붙어있다.

 

저 아바타를 눌러주면 그게 어디 있는지 위치가 왼쪽에 나오는데

npc 컴포넌트로 올라가서

왼쪽 아바타를 고대로 데려오면 된다!

 

다음에

곰에 있던 애니메이터 삭제하고

 

프리팹 언팩해주면 끝!

 

곰이 엄청 무섭게 뛰어온다..

 

적 구현까지 완성!

 

진짜 마지막으로 오디오 넣는 방법으로

3D 게임은 여기서 마무리지으려 한다!