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

11. 근접형 기본 몬스터 공격 기능

  • 적의 공격 범위에 플레이어가 들어온다면 공격 시작
  • 공격 시 명확하게 공격 행위가 표시
  • 손을 들어 공격 준비를 하고 이때는 멈추어있다
  • 손을 내리며 일정 거리 이동하며 공격한다
  • 8방향으로 공격

기획자님이 요구하신 근접형 기본 몬스터의 공격 설정은 위와 같다. 처음에는 간단하다고 생각했다. 플레이어 기본 근접 공격에서 조금만 수정하면 될 줄 알았는데 아니었다.

근접형 기본 몬스터는 현재 두 가지 자식 오브젝트를 가진다. TargetArea는 SphereCollider가 연결되어 있고 추적 범위를 결정 짓는다. BasicAttack도 SphereCollider가 연결되어 있고 공격 범위를 결정 짓는다.

물론 플레이어도 비슷한 방식으로 Weapon_1 자식 오브젝트를 가지고 SphereCollider를 가지고 있다.

private void OnTriggerEnter(Collider other) {
    if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
    {
        if(attack == false)
        {
            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);
        }
        else
        {
            attack = false;
        }
    }
}

몬스터 근접 공격에 대해 순서대로 설명하자면 최우선으로 SphereCollider 트리거가 호출이 되면 target이 범위안에 있는지 판별하는 함수인 FindVisibleTargets() 가 호출된다.

여기서 생기는 문제는 트리거 엔터는 범위안에 있을 때 한 번만 호출된다는 것이다. 만약에 몬스터가 공격 행위를 했는데 플레이어가 계속 범위안에 있을 때 다시 공격을 하지 않는 것이다..! 트리거 엔터는 트리거가 켜졌을 때만 불리기 때문이다.

이 문제는 코루틴으로 적절하게 해결하였지만 좀 찝찝하긴 하다.

아 그리고 참고로 Vector3.Dot은 단위 벡터 두 개를 넣으면 각도를 반환해 주는데 이때 몬스터 이동 방향이 필요하다. 그래서 몬스터 추적 상태일 때 Direction() 함수가 적절하게 작동하도록 수정한 것이다.

private void FixedUpdate() {
    
    if(attack == true)
    {
        BasicAttackMotion();
    }
}

void BasicAttackMotion()
{
    attack = false;

    attacking = true;

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

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

    StartCoroutine(BasicAttackCoroutine());
}

공격 범위안에 플레이어가 들어온다면 attack 변수에 의해 FixedUpdate()에서 BasicAttackMotion()이 호출된다. 당연히 여러번 호출되면 안되니까 호출되자 마자 attack bool 변수는 false가 된다.

몬스터가 공격 준비를 할 때 제자리에서 멈추어야 하므로 MonsterMove 스크립트는 false 상태로 바꾸고 코루틴으로 일정 시간 후에 공격하도록 하는 것이다.

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

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

    StartCoroutine(ReMove());
}

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

    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;
    }

    attacking = false;
}

void Attack(PlayerStat player)
{
    player.health -= monsterStat.damage;
    Debug.Log("몬스터의 공격 성공! 플레이어 체력| " + player.health);
}

private void OnCollisionEnter(Collision other) {
    if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
    {
        Attack(playerStat);
    }            
}

attackReady 만큼의 시간이 지나면 Rigidbody.AddForce로 인해 앞으로 전진하면서 공격을 하게된다. 이때 플레이어가 몬스터와 충돌하면 OnCollisionEnter가 호출되고 플레이어 체력을 깎는 Attack() 함수가 호출된다.

문제는 공격이 끝나고 바로 움직이면 몬스터가 미친듯한 움직임으로 플레이어를 쫓게 되므로 일정 시간 후에 MonsterMove 스크립트를 true 상태로 바꾸는게 중요하다. 그 기능은 다시 코루틴으로 만들면 되고 위 코드에선 reMove 만큼의 시간이 지나면 몬스터가 다시 움직이도록 하였다.

그리고 ReMove 코루틴은 MonsterMove 스크립트를 켜는 것 말고도 중요한 역할을 한다. SphereCollider 범위 만큼의 OverlapSphere를 통해 근처에 플레이어가 있다면 attack 변수에 true를 대입해 다시 공격하도록 하는 것이다. 이 기능을 통해 OnTriggerEnter의 플레이어가 범위안에 계속 있다면 다시 호출되지 않는 문제점을 고쳤다.

하지만 Enter과 Exit는 가끔 호출되지 않는 문제점이 있는데 위 코드도 그렇다. 어쩔때는 몬스터가 멍청하게 플레이어한테 달려가기만 한다. 공격 기능을 켜지 않는 것이다. 이건 일단 나중에 생각하기로 하였다.

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;

    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();
    }

    private void FixedUpdate() {
        
        if(attack == true)
        {
            BasicAttackMotion();
        }
    }

    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);
            }
            else
            {
                attack = false;
            }
        }
    }

    void BasicAttackMotion()
    {
        attack = false;

        attacking = true;

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

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

        StartCoroutine(BasicAttackCoroutine());
    }

    void Attack(PlayerStat player)
    {
        player.health -= monsterStat.damage;
        Debug.Log("몬스터의 공격 성공! 플레이어 체력| " + player.health);
    }

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

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

        StartCoroutine(ReMove());
    }

    IEnumerator ReMove()
    {
        yield return new WaitForSeconds(reMove);

        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;
        }

        attacking = false;
    }

    private void OnCollisionEnter(Collision other) {
        if(other.gameObject.layer == LayerMask.NameToLayer("Player"))
        {
            Attack(playerStat);
        }            
    }

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

}

답글 남기기

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