Posted in: Unity, 프로젝트 When the Spring comes

16. 근접형 기본 몬스터 공격 기능 수정

private void OnCollisionEnter(Collision other)
{
    if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
    {
        if(!playerStat.isUnBeatTime)
        {
            //무적시간 아님
            Attack(playerStat);
        }
        else if(playerStat.isUnBeatTime)
        {
            //무적 중
            return;
        }
    }
}

기존 근접형 기본 몬스터 공격 기능은 플레이어와 몬스터가 충돌하면 OnCollisionEnter에 의해 Attack()가 호출되었다.

그런데 이런 방식은 오류가 있다. 플레이어가 몬스터와 달라붙어 있으면 Attack()이 호출이 안된다. 몬스터 애니메이션을 넣기 전에 수정할 필요가 있었다.

근접형 기본 몬스터의 공격 패턴은 이렇다.

  1. 플레이어가 추적 범위 안에 진입
  2. 플레이어 추적
  3. 플레이어가 공격 범위 안에 진입
  4. 공격 준비 상태 (제자리에 멈춤)
  5. 일정 거리 앞으로 전진하면서 공격
  6. 정해진 시간을 기다렸다가 다시 추적

처음에는 전진하면서 공격을 할 때 플레이어와 충돌하면 공격 기능이 호출되면 되겠다고 생각했다. 하지만 만들고 보니 버그가… ㅠ

지금은 앞으로 전진하면서 공격할 때 Attack() 함수가 계속 호출되고 공격 범위 안에 플레이어가 있다면 플레이어 체력을 깎는 코드로 수정하였다.

플레이어가 추적 범위 안에 집입하여 추적하는 기능은 이전 글에서 소개하였고 다른 스크립트에서 다루고 있다. 그러므로 이번 글에서는 제외하였다.

플레이어가 공격 범위안에 진입


몬스터 자식 오브젝트에 SphereCollider 컴포넌트를 연결해 공격 범위로 사용하였다. 사진을 잘못 찍었는데 BasicAttack도 SphereCollider를 가지고 있다.

private void OnTriggerStay(Collider other)
{
    if(other.gameObejct.layer == LayerMask.NameToLayer("Player"))
    {
        FindVisibleTargets();
    }
}

public void FindVisibleTargets()
{
    Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

    for(int i = 0; i < targets.Length; ++i)
    {
        Vector3 dirToTarget = (targets[i].transform.position - transform.position).normalized;

        if(Vector3.Dot(monsterMove.directionVec.normalized, dirToTarget) > Mathf.Cos((viewAngle / 2) * Mathf.Deg2Rad))
        {
            playerStat = targets[i].GetComponent<PlayerStat>();

            attack = true;
            Debug.DrawLine(transform.position, targets[i].transform.position, Color.red);
        }
    }
    if(targets.Length <= 0)
    {
        attack = false;
    }
}

플레이어가 몬스터에 OnTriggerStay하면 몬스터가 바라보는 방향으로 viewAngle 각도 범위만큼 플레이어가 있는지 체크하는 기능이다. 만약 범위안에 플레이어가 존재한다면 attack bool 변수가 true가 된다.

공격 준비 상태


void Update()
{
    checkTime += Time.deltaTime;
}

void FixedUpdate()
{
    if(attack)
    {
        if(checkTime >= attackDelay)
        {
            transform.GetComponent<MonsterMove>().enabled = false;

            //공격 준비 애니메이션 On
    
            StartCoroutine(BasicAttackCoroutine());

            attack = false;

            checkTime = 0;
        }
    }
}

IEnumerator BasicAttackCoroutine()
{
    yield return new WaitForSeconds(attackReady);

    attacking = true;

    rigidbody.AddForce(monsterMove.directionVec.normalized * attackForce,
                       ForceMode.Impulse);
    StartCoroutine(ReMove());
}

attack bool 변수가 true가 되면 조건을 따지게 된다.

checkTime이 attackDelay보다 크냐는 건데 공격 딜레이보다 흐른 시간이 길면 몬스터 움직임을 멈추고 공격 준비 코루틴에 들어간다.

attackReady 만큼의 시간이 흐르면 AddForce에 의해 앞으로 전진하면서 attacking 변수에 의해 공격한다. 다시 움직이기 까지의 시간은 ReMove 코루틴으로 만들었다.

일정 거리 앞으로 전진하면서 공격


void FixedUpdate()
{
    if(attacking && reMove >= checkTime)
    {
        if(!playerStat.isUnBeatTime)
            Attack();
        else if(playerStat.isUnBeatTime)
            Debug.Log("플레이어 무적 시간..");
    }
}

void Attack()
{
    Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

    for(int i = 0; i < targets.Length; ++i)
    {
        Vector3 dirToTarget = (targets[i].transform.position - transform.position).normalized;

        if(Vector3.Dot(monsterMove.directionVec.normalized, dirToTarget) > Mathf.Cos((viewAngle / 2) * Mathf.Deg2Rad))
        {
            playerStat = targets[i].GetComponent<PlayerStat>();

            //체력 깎임
            playerStat.health -= monsterStat.damage;
            Debug.Log(targets[i].name + "플레이어 남은 체력: " + playerStat.health);

        }
    }

    attacking = false;
}

attacking이 true가 되고 checkTime이 reMove 변수보다 작은 동안 공격 기능이 호출된다. 왜나하면 ReMove 코루틴은 reMove 만큼 기다렸다가 다시 움직이는 기능이다. AddForce에 의해 앞으로 전진하면서 다시 움직이기 전까지 공격 기능이 계속 호출되는 것이다.

Attack() 함수는 FindVisibleTargets() 함수와 비슷하다. 공격 범위에 들어온다면 플레이어 체력을 깎는다.

이 코드는 플레이어가 피격 시 무적 판정을 가지지 않거나 다른 조건으로 통제하지 않으면 플레이어가 순식간에 죽어버리는데 무적 판정을 만들어서 플레이어 체력이 한 번만 깎이도록 하였다.

정해진 시간을 기다렸다가 다시 추적


IEnumerator ReMove() //다시 움직일 수 있도록 하자
{
    yield return new WaitForSeconds(reMove);

    attacking = false;
    checkTime = 0;

    transform.GetComponent<MonsterMove>().enabled = true;

    Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

    if(targets.Length > 0)
    {
        Vector3 temp = transform.position - targets[0].transform.position;
        float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

        monsterMove.Direction(direction);

        attack = true;
    }
}

AddForce 하고난 후 곧바로 ReMove() 코루틴이 호출되는데 공격은 reMove 시간만큼 호출된다. reMove 만큼 시간이 지나면 코루틴 함수가 본격적으로 시작되는데 몬스터를 다시 움직이게 해주고 OverlapSphere로 플레이어가 계속 범위 안에 위치해있는지 체크하는 것이 중요하다. 움직이기 시작하자마자 공격이 시작돼야 하는 순간이 있기 때문이다.

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

public class MonsterBasicAttack : MonoBehaviour
{
    public float viewAngle;
    public float distance;
    private float maxDistance;
    public LayerMask targetMask;

    private PlayerStat playerStat;
    private MonsterStat monsterStat;
    private MonsterMove monsterMove;
    private SphereCollider sphereCollider;
    private bool attack;

    public float attackReady = 0.5f;

    public float attackDelay = 3.0f;

    private float checkTime;

    private bool attacking;

    private Rigidbody rigidbody;

    public float attackForce;

    public float reMove;

    private void Start() {
        maxDistance = 10.0f;

        monsterStat = GetComponent<MonsterStat>();
        //sphereCollider = GetComponentInChildren<SphereCollider>();
        sphereCollider = transform.Find("BasicAttack").GetComponent<SphereCollider>();
        monsterMove = GetComponent<MonsterMove>();
        rigidbody = GetComponent<Rigidbody>();

    }

    private void Update() {
        sphereCollider.radius = distance;

        DrawView();

        checkTime += Time.deltaTime;
    }

    private void FixedUpdate() {
        
        if(attack)
        {
            if(checkTime >= attackDelay)
            {
                transform.GetComponent<MonsterMove>().enabled = false;

                //공격 준비 애니메이션 켜고

                StartCoroutine(BasicAttackCoroutine());

                attack = false;

                checkTime = 0;
            }
        }

        if(attacking && reMove >= checkTime)
        {
            if(!playerStat.isUnBeatTime)
                Attack();
            else if(playerStat.isUnBeatTime)
                Debug.Log("플레이어 무적 시간..");
        }
    }

    Vector3 DirFromAngle(float angleInDegrees)
    {
        if(monsterMove.directionVec == new Vector3(-1.14f, 0, 1.14f))//UpLeft
            angleInDegrees += 315.0f;
        if(monsterMove.directionVec == new Vector3(0, 0, 2.0f))//Up
            angleInDegrees += 0.0f;
        if(monsterMove.directionVec == new Vector3(1.14f, 0, 1.14f))//UpRight
            angleInDegrees += 45.0f;
        if(monsterMove.directionVec == new Vector3(-2.0f, 0, 0))//Left
            angleInDegrees += 270.0f;
        if(monsterMove.directionVec == new Vector3(2.0f, 0, 0))//Right
            angleInDegrees += 90.0f;
        if(monsterMove.directionVec == new Vector3(-1.14f, 0, -1.14f))//DownLeft
            angleInDegrees += 225.0f;
        if(monsterMove.directionVec == new Vector3(0, 0, -2.0f))//Down
            angleInDegrees += 180.0f;
        if(monsterMove.directionVec == new Vector3(1.14f, 0, -1.14f))//DownRight
            angleInDegrees += 135.0f;

        return new Vector3(Mathf.Sin(angleInDegrees * Mathf.Deg2Rad), 0, Mathf.Cos(angleInDegrees * Mathf.Deg2Rad));
    }

    public void DrawView()
    {
        Vector3 leftBoundary = DirFromAngle(-viewAngle / 2);
        Vector3 rightBoundary = DirFromAngle(viewAngle / 2);

        Debug.DrawLine(transform.position, transform.position + leftBoundary * distance, Color.green);
        Debug.DrawLine(transform.position, transform.position + rightBoundary * distance, Color.green);
        Debug.DrawLine(transform.position + leftBoundary * distance, transform.position + rightBoundary * distance, Color.green);
    }

    public void FindVisibleTargets()
    {
        Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

        for(int i = 0; i < targets.Length; ++i)
        {
            Vector3 dirToTarget = (targets[i].transform.position - transform.position).normalized;

            if(Vector3.Dot(monsterMove.directionVec.normalized, dirToTarget) > Mathf.Cos((viewAngle / 2) * Mathf.Deg2Rad))
            {
                playerStat = targets[i].GetComponent<PlayerStat>();

                attack = true;
                Debug.DrawLine(transform.position, targets[i].transform.position, Color.red);
            }
        }
        
        if(targets.Length <= 0)
        {
            attack = false;
        }

    }

    void Attack()
    {
        Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

        for(int i = 0; i < targets.Length; ++i)
        {
            Vector3 dirToTarget = (targets[i].transform.position - transform.position).normalized;

            if(Vector3.Dot(monsterMove.directionVec.normalized, dirToTarget) > Mathf.Cos((viewAngle / 2) * Mathf.Deg2Rad))
            {
                playerStat = targets[i].GetComponent<PlayerStat>();

                //체력 깎임
                playerStat.health -= monsterStat.damage;
                Debug.Log(targets[i].name + "플레이어 남은 체력: " + playerStat.health);

            }
        }
        attacking = false;
    }

    IEnumerator BasicAttackCoroutine()
    {
        yield return new WaitForSeconds(attackReady);

        attacking = true;

        rigidbody.AddForce(monsterMove.directionVec.normalized * attackForce, ForceMode.Impulse);

        StartCoroutine(ReMove());
    }

    IEnumerator ReMove() //다시 움직일 수 있도록 하자
    {
        yield return new WaitForSeconds(reMove);

        attacking = false;
        checkTime = 0;

        transform.GetComponent<MonsterMove>().enabled = true;

        Collider[] targets = Physics.OverlapSphere(transform.position, distance, targetMask);

        if(targets.Length > 0)
        {
            //플레이어 각도 구하기
            Vector3 temp = transform.position - targets[0].transform.position;
            float direction = Mathf.Atan2(temp.z, temp.x) * Mathf.Rad2Deg;

            monsterMove.Direction(direction);

            attack = true;
        }
    }

    private void OnTriggerStay(Collider other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            FindVisibleTargets();
        }
    }
}

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다