[Unity] 1인칭 FPS 게임 - 3. Living Entity-2

홍예주·2021년 7월 7일
0

1. Ememy

1) Unity Object

  • Animator
    Controller와 Avatar는 SimpleApocalypse Asset에 있는 걸 사용했다.(Asset store에 있음)
  • Skinned Mesh Renderer
    Materials도 마찬가지로 SimpleApocalypse Asset에 있는 걸 사용했다.
  • Enemy Script
  • Nav Mesh Agent
    (Navigation 탭에서 Agent의 크기 조정도 가능)
  • Capsule Collider

2) C# Code

적은 정찰, 쫓기, 공격 3가지 상태를 가진다.
코루틴을 이용해 0.25초마다 상태를 업데이트 한다.

  • 정찰 : 매 주기마다 랜덤 지점을 선택해 해당 위치로 이동한다. 이동 불가능한 구역인 경우 다음 주기까지 대기, 다시 지점을 선택한다.
  • 쫓기 : Sight Range 안에 플레이어가 들어올 시 수행. 플레이어가 다시 시야 범위 밖으로 나가더라도 계속 쫓아온다. (bool SetChase 이용)
  • 공격 : Attack Range 안에 플레이어가 들어올 시 수행. Time Between Attack만큼의 간격을 두고 공격한다.
using UnityEngine.AI;
using UnityEngine;
using System.Collections;


public class EnemyAI : LivingEntity
{
    public NavMeshAgent agent;

    public Transform playerPos;
    public Player player;

    public float damage = 1; //좀비가 주는 데미지

    public LayerMask whatIsGround, whatIsPlayer, whatIsObstacle;

    GameObject gameManager;

    // 모델링 넣기 전 테스트 용으로 넣은 코드
    Material skinMaterial; //적의 메테리얼
    Color OriginColor;
   

    float refreshRate = 0.25f; //path 업데이트 갱신 간격

    //Patroling
    public Vector3 walkPoint; //위치
    bool walkPointSet = false; //해당 위치가 이미 세팅 되어있는지 확인
    public float walkPointRange; //범위
    
    //Chasing
    bool setChase = false;


    //Attacking
    //bool setAttack;
    public float timeBetweenAttacks;
    bool alreadyAttacked;

    //States
    public float sightRange, attackRange;
    public bool playerInSightRange, playerInAttackRange;


    //충돌범위
    float myCollisionRadius; //적의 충돌범위
    float targetCollisionRadius;// 타겟 충돌범위


    private void Awake()
    {
    
    }


   
    protected override void Start()
    {
        base.Start();



        gameManager = GameObject.Find("GameManager");

        dead = false;
        //플레이어 위치 할당
        playerPos = GameObject.Find("Player(Clone)").transform;
        player.dead = false;
        //nav mesh agent 할당
        agent = GetComponent<NavMeshAgent>();

        //적과 플레이어 겹침 방지 위해
        myCollisionRadius = GetComponent<CapsuleCollider>().radius;
        targetCollisionRadius = playerPos.GetComponent<CapsuleCollider>().radius;


        //상태 변화 시 색상 변화 위해
        skinMaterial = GetComponent<Renderer>().material;
        OriginColor = skinMaterial.color;
        
        //Player의 초기 체력 값이 자꾸 스크립트로 리셋이 안되어서 Enemy 쪽에서 강제로 세팅 -> 추후 수정 필요
        player.setHealth(player.startingHealth);
        StartCoroutine(UpdatePath());
    }

    private void Update()
    {
        if (gameManager.GetComponent<GameManager>().isClear)
        {
            this.gameObject.SetActive(false);
        }

    }



    IEnumerator UpdatePath()
    {
        Debug.Log("enemy updatePath");
        Debug.Log(player.getHealth());
        while (playerPos!=null && !player.GetComponent<Player>().dead)
        {
            //Debug.Log(!player.GetComponent<Player>().dead);
            //Debug.Log(!dead);
            if (!dead)

            {
                //Debug.Log("Player exist");
                //시야와 공격범위 확인
                playerInSightRange = Physics.CheckSphere(transform.position, sightRange, whatIsPlayer);
                //Debug.Log(playerInSightRange);
                playerInAttackRange = Physics.CheckSphere(transform.position, attackRange, whatIsPlayer);
                //Debug.Log(playerInAttackRange);

                if (!playerInSightRange && !playerInAttackRange && !setChase)
                    Patroling();

                if ((playerInSightRange && !playerInAttackRange) || setChase)
                    ChasePlayer();

                if ((playerInAttackRange && playerInSightRange))
                {
                    setChase = false;
                    AttackPlayer();
                    
                }

                if (gameManager.GetComponent<GameManager>().isClear)
                {
                    this.gameObject.SetActive(false);
                }
            }

            yield return new WaitForSeconds(refreshRate);

        }
        

    }

    private void Patroling()
    {
        skinMaterial.color = OriginColor;
       //Debug.Log("Patroling");
        if (!walkPointSet)
            SearchWalkPoint();

        if (walkPointSet)
        {
            agent.SetDestination(walkPoint);

            Vector3 distanceToWalkPoint = transform.position - walkPoint;

            //Walkpoint에 도달 (거리 1 미만일 때)
            if (distanceToWalkPoint.magnitude < 0.1f)
                walkPointSet = false;
        }
    }

    private void ChasePlayer()
    {

        setChase = true;
        if (setChase)
        {
        //chasing할 때 색 바뀌도록 -> 모델링 입히기 전 테스트 위해
            skinMaterial.color = Color.yellow;

             //원래는 Collider의 Is Trigger를 체크하고 콜라이더 범위만큼 빼고 그 앞까지 오는 걸로 했었다
             //그런데 계속 오류나서 Is Trigger를 해제하고 플레이어 위치로 찾아오도록 변경
            //Vector3 dirToTarget = (playerPos.position - transform.position).normalized;
            //Vector3 targetPosition = playerPos.position - dirToTarget * (myCollisionRadius + targetCollisionRadius);

            walkPoint = new Vector3(playerPos.position.x, transform.position.y, playerPos.position.z);
            if (agent.SetDestination(walkPoint))
            {
                Debug.Log("Follow Player");
            }
        }


    }

    private void AttackPlayer()
    {
        skinMaterial.color = Color.red;
        Debug.Log("Attack");
        setChase = false;


        //플레이어 계속 추적
        //agent.SetDestination(playerPos.position);
        Vector3 dirToTarget = (playerPos.position - transform.position).normalized;
        Vector3 targetPosition = playerPos.position - dirToTarget * (myCollisionRadius + targetCollisionRadius);
        agent.SetDestination(targetPosition);

        transform.LookAt(playerPos);


        //Attack
        if (!alreadyAttacked) {

            Debug.Log("Attacking....");
            //Attack Code 여기에

            //Attack();

            //IDamageble damagebleObject = player.GetComponent<IDamageble>();

               

            if (player.dead != true)
            {
                //Attack();
                //Debug.Log("Attacking....");
                Debug.Log(player.getHealth());
                player.TakeHit(damage);
                Debug.Log(player.getHealth());

            }


            alreadyAttacked = true;

            Invoke(nameof(ResetAttack), timeBetweenAttacks);
        }

    }

   

    //랜덤으로 순찰할 위치 선정
    private void SearchWalkPoint()
    {
        float randomZ = Random.Range(-walkPointRange, walkPointRange);
        float randomX = Random.Range(-walkPointRange, walkPointRange);


        
        //랜덤하게 패트롤할 위지 범위 안에서 선청
        walkPoint = new Vector3(transform.position.x + randomX, transform.position.y, transform.position.z + randomZ);
    
        //맵밖으로 안나갔는지 확인
        if (Physics.Raycast(walkPoint, -transform.up, 2f, whatIsGround))
        {
            Vector3 tmpPoint = new Vector3(walkPoint.x, 20, walkPoint.z);
            if (!Physics.Raycast(tmpPoint, -transform.up, Mathf.Infinity, whatIsObstacle))
            {
                walkPointSet = true;
            //    return;
            }
          
        }
        else
        {
            Debug.Log("Can't find");
            //SearchWalkPoint();
        }

       
        

    }

    protected override void Die()
    {
        base.Die();
        gameManager.GetComponent<GameManager>().IncreaseKillCount();
        Destroy(this.gameObject);
    }


    private void ResetAttack()
    {
        alreadyAttacked = false;
    }


//Sight Range, AttackRange를 EDITOR에서 확인하기 위해
#if UNITY_EDITOR
    private void OnDrawGizmosSelected()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
        Gizmos.color = Color.yellow;
        Gizmos.DrawWireSphere(transform.position, sightRange);

    }
#endif
}

2. 발생했던 문제점

1) Enemy Animator 부착

이미 만들어진 Asset을 어떻게 부착해야할 지 몰라서 조금 헤멨다.
처음에는 미리 만들어둔 Enemy Prefab에 Asset에 있는 zombie prefab 요소를 복사해 붙여넣었는데, 애니메이션 컨트롤러랑 매핑이 안된건지(추측) 애니메이션이 동작하지 않았다.
그래서 Asset에 있는 zombie prefab을 그대로 가져와 거기에 Enemy 스크립트와 Nav Mesh Agent를 붙였다.
그리고 그냥 Mesh render로는 Material이 입혀지지 않아서 Skinned Mesh Renderer를 이용했다.

  • Skinned Mesh Renderer

    Unity는 메시의 모양이 미리 정의된 애니메이션 시퀀스에 따라 변형되는 Bone 애니메이션의 렌더링을 위해 Skinned Mesh Renderer 컴포넌트를 사용합니다. 캐릭터 뿐만 아니라 (조인트가 힌지처럼 기능하는 기계와 달리) 조인트가 구부러지는 다른 오브젝트에도 유용한 기법입니다.

2) Enemy 정찰 & 체이싱 문제

= 적이 정찰 및 체이싱을 하지 않는 문제
이건 콜라이더 및 nav mesh agent의 문제였다.
적의 콜라이더와 바닥 콜라이더가 충돌해 적이 바닥에 붙어있지 않아 nav mesh agent가 해당 루트로 나아가지 못했다.
맵에 붙어있는 콜라이더가 초기화 될 때 0.5의 높이를 가지게 스크립트를 짠 것이 원인으로 예상된다.
또한 콜라이더의 Is Trigger를 체크한 상태에서 플레이어와 적의 지름을 뺀 만큼 적을 플레이어 위치로 이동시켜줬는데, 그것도 계속 오류가 나서 Is Trigger를 해제하고 플레이어 위치로 적이 이동하도록 수정했다.

3) Player가 Attack 시 바로 죽음

사실 이건 해결하고 보니 Enemy쪽 문제가 아니라 Player 스크립트 문제였지만, Enemy에서 임시변통으로 수정했기 때문에 여기에 쓴다.
Attack 함수를 호출 했을 때 플레이어가 맞고 바로 죽어버려서, 로그를 찍어봤더니 플레이어가 죽고 다시 생성됐을 때 초기 체력값으로 초기화가 안된 것이 원인이었다.
하지만 플레이어와 적을 prefab으로 만들어서 그런지 계속 동기화가 안되어서 임시방편으로 Enemy가 생성될 때 강제로 플레이어 체력을 초기화해주었다.

profile
기록용.

0개의 댓글