ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 몬스터 상태 구현 (Update)
    연습 프로젝트/2D로그라이크 게임

     

     

    현재 작업 진행중인 영상 입니다.

    몬스터 상태 구현을 업데이트 하였습니다.

     

    또한 기존 애니메이션 전이 조건을 대부분 Trigger로 진행 하였었는데, 한번 꼬이기 시작하먼 겉잡을수 없어진다는 점과, 흐름을 파악하기가 매우 힘들어 지는 현상이 발생하여 모든 파라미터를 Bool 변수로 true || false로 변경하였습니다.

     

    간단한 흐름 상태를 보자면

     

    몬스터는 Idle 상태에서 유니티가 실행 되면 Walk 상태가 됩니다.

     

    Walk 상태에서 Idle로 넘어가는 조건은 플레이어에게 피격을 당했을시 전투 돌입 준비 상태로 들어섭니다.

     

    준비 상태 이후 플레이의 위치를 총 2번 감지하는데, 첫번째 감지는 플레이어를 추적하기 위함이고

     

    두번재 감지는 플레이어가 몬스터의 공격 범위 내에 들어섰는지 감지합니다.

    이후 검을 드는 모션 (공격) 을 취하게 됩니다.

    이 모션은 마법을 쓰는 형태로 메테오나 번개를 생각중에 있습니다.

     

    다음은 코드를 보겠습니다.

     

    public enum MonsterState
    {
        Idle, Walk, AttackReading, AttackIdle, Tracking, Attack
    }
    
    public MonsterState monsterState = MonsterState.Walk;
    
    private void Start()
    {
        startingPosition = monsterRigidbdy2D.position;
    
    }
    
    private void Update()
    {
        if (monster_Hp <= 0)
        {
            Destroy(gameObject);
        }
    
        if (monsterState == MonsterState.Idle && timerStart == true)
        {
            readytimer += Time.deltaTime;
            while (readytimer > readytime)
            {
                monsterState = MonsterState.AttackReading;
                readytimer = 0;
                timerStart = false;
            }
        }
        if (isAttacking == true)
        {
            attackTime += Time.deltaTime;
            while (attackTime > attackTimer)
            {
    
                attackTime = 0;
                attackTimer = UnityEngine.Random.Range(5.0f, 10.0f);
                isAttacking = false;
            }
        }
    
        switch (monsterState)
        {
            case MonsterState.Idle:
                monsterAnimator.SetBool("Walk", false);
                monsterAnimator.SetBool("Idle", true);
                if (player.position.x < transform.position.x)
                {
                    Vector3 localScale = transform.localScale;
                    localScale.x = -1;
                    transform.localScale = localScale;
                }
                else
                {
                    Vector3 localScale = transform.localScale;
                    localScale.x = 1;
                    transform.localScale = localScale;
                }
    
                timerStart = true;
    
                break;
            case MonsterState.Walk:
                monsterAnimator.SetBool("Walk", true);
                monsterAnimator.SetBool("Idle", false);
    
                float targetX = startingPosition.x + distance * direction;
    
                monsterRigidbdy2D.velocity = new Vector2(speed * direction, monsterRigidbdy2D.velocity.y);
    
                if (direction == 1 && monsterRigidbdy2D.position.x >= targetX)
                {
                    if (monsterState == MonsterState.Attack)
                        transform.localScale = new Vector2(-2, 2);
                    else
                        transform.localScale = new Vector2(-1, 1);
                    direction = -1;
                }
                else if (direction == -1 && monsterRigidbdy2D.position.x <= startingPosition.x - distance)
                {
                    if (monsterState == MonsterState.Attack)
                        transform.localScale = new Vector2(2, 2);
                    else
                        transform.localScale = new Vector2(1, 1);
                    direction = 1;
                }
                break;
            case MonsterState.AttackReading:
                elapsedTime += Time.deltaTime;
    
                float t = Mathf.Clamp01(elapsedTime / duration);
                transform.localScale = Vector3.Lerp(Vector3.one, new Vector3(2, 2, 2), t);
                monsterAnimator.SetBool("Idle", false);
                monsterAnimator.SetBool("AttackReading", true);
                if (elapsedTime >= duration)
                {
                    elapsedTime = 0.0f;
                    monsterState = MonsterState.AttackIdle;
                }
                break;
            case MonsterState.AttackIdle:
                monsterAnimator.SetBool("AttackReading", false);
                Collider2D playerInRange = Physics2D.OverlapCircle(transform.position, attackRange, playerLayer);
                if (playerInRange != null)
                {
                    monsterState = MonsterState.Tracking;
                }
                break;
            case MonsterState.Tracking:
                monsterAnimator.SetBool("Walk", true);
                Vector2 directionToPlayer = new Vector2(player.position.x - transform.position.x, 0).normalized;
                monsterRigidbdy2D.velocity = new Vector2(directionToPlayer.x * speed, monsterRigidbdy2D.velocity.y);
    
                if (player.position.x < transform.position.x)
                {
                    transform.localScale = new Vector2(-Mathf.Abs(transform.localScale.x), transform.localScale.y);
                }
                else
                {
                    transform.localScale = new Vector2(Mathf.Abs(transform.localScale.x), transform.localScale.y);
                }
    
                targetX = startingPosition.x + distance * direction;
    
                if (direction == 1 && monsterRigidbdy2D.position.x >= targetX)
                {
                    if (monsterState == MonsterState.AttackIdle || monsterState == MonsterState.Tracking)
                        transform.localScale = new Vector2(-2, 2);
                    else
                        transform.localScale = new Vector2(-1, 1);
                    direction = -1;
                }
                else if (direction == -1 && monsterRigidbdy2D.position.x <= startingPosition.x - distance)
                {
                    if (monsterState == MonsterState.AttackIdle || monsterState == MonsterState.Tracking)
                        transform.localScale = new Vector2(2, 2);
                    else
                        transform.localScale = new Vector2(1, 1);
                    direction = 1;
                }
    
                playerInRange = Physics2D.OverlapCircle(transform.position, 2.0f, playerLayer);
                if (playerInRange != null)
                {
                    monsterAnimator.SetBool("Walk", false);
                    monsterState = MonsterState.Attack;
                }
    
                break;
            case MonsterState.Attack:
                if (isAttacking == false)
                {
                    monsterAnimator.SetBool("Attack", true);
                    monsterState = MonsterState.Tracking;
                    isAttacking = true;
                }
                break;
    
        }
    }
    
    public void BeDamaged(float damage, Vector2 dir, float force)
    {
        if (monsterState == MonsterState.Walk)
        {
            monsterState = MonsterState.Idle;
        }
    
        if (monsterState == MonsterState.Walk || monsterState == MonsterState.Tracking || monsterState == MonsterState.Attack)
        {
            monster_Hp -= damage;
            monsterRigidbdy2D.AddForce(dir * -force * recoilFactor);
        }
    }
    
    private void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(transform.position, attackRange);
        Gizmos.DrawWireSphere(transform.position - new Vector3(-1, 0.5f, 0), 0.5f);
    }

    선언 변수를 제외한 MonsterController 전체 코드입니다

     

    전부 설명을 들어가지 않고, 핵심 부분만 살펴 보겠습니다.

     

    public enum MonsterState
    {
        Idle, Walk, AttackReading, AttackIdle, Tracking, Attack
    }
    
    public MonsterState monsterState = MonsterState.Walk

    몬스터의 상태는 대기, 걷기, 공격 준비중, 공격 대기, 추적, 공격 으로 총 6가지가 있습니다.

     

    초기 상태는 몬스터는 필드 위를 걷고 있습니다.

     

    case MonsterState.Walk:
        monsterAnimator.SetBool("Walk", true);
        monsterAnimator.SetBool("Idle", false);
    
        float targetX = startingPosition.x + distance * direction;
    
        monsterRigidbdy2D.velocity = new Vector2(speed * direction, monsterRigidbdy2D.velocity.y);
    
        if (direction == 1 && monsterRigidbdy2D.position.x >= targetX)
        {
            if (monsterState == MonsterState.Attack)
                transform.localScale = new Vector2(-2, 2);
            else
                transform.localScale = new Vector2(-1, 1);
            direction = -1;
        }
        else if (direction == -1 && monsterRigidbdy2D.position.x <= startingPosition.x - distance)
        {
            if (monsterState == MonsterState.Attack)
                transform.localScale = new Vector2(2, 2);
            else
                transform.localScale = new Vector2(1, 1);
            direction = 1;
        }

    초기 워크 상태에서는 대부분 지난번 설명과 동일하지만, SetTriger의 함수가 SetBool로 바뀐것을 볼 수 있습니다.

    몬스터의 앞 방향을 플레이어를 바라보게 하는 동작 구현 코드이며, 좌 우 회전은 LocalScalex축 으로 조절 하였습니다.

     

    case MonsterState.Idle:
        monsterAnimator.SetBool("Walk", false);
        monsterAnimator.SetBool("Idle", true);
        if (player.position.x < transform.position.x)
        {
            Vector3 localScale = transform.localScale;
            localScale.x = -1;
            transform.localScale = localScale;
        }
        else
        {
            Vector3 localScale = transform.localScale;
            localScale.x = 1;
            transform.localScale = localScale;
        }
    
        timerStart = true;

    이 후 Idle 상태 입니다.

     

    Idle 상태에서는 걷는 모션을 false로 꺼주고 몬스터가 (1, 1, 1) 에서 (2, 2, 2)로 커지는 시간에 대기중에도 몬스터의 앞 방향이 플레이어를 향하게 구현을 해보았습니다.

     

    아래 timerStart = true의 조건이 성립되면 Update문 에서

    if (monsterState == MonsterState.Idle && timerStart == true)
    {
        readytimer += Time.deltaTime;
        while (readytimer > readytime)
        {
            monsterState = MonsterState.AttackReading;
            readytimer = 0;
            timerStart = false;
        }
    }

     

    이와 같은 타이머를 걸어주어 몬스터의 상태를 AttackReading으로 전환 시켜 주었습니다.

    *현재 타이머는 테스트를 편하게 하기 위함으로 10초로 설정되어 있습니다.

     

    case MonsterState.AttackReading:
        elapsedTime += Time.deltaTime;
    
        float t = Mathf.Clamp01(elapsedTime / duration);
        transform.localScale = Vector3.Lerp(Vector3.one, new Vector3(2, 2, 2), t);
        monsterAnimator.SetBool("Idle", false);
        monsterAnimator.SetBool("AttackReading", true);
        if (elapsedTime >= duration)
        {
            elapsedTime = 0.0f;
            monsterState = MonsterState.AttackIdle;
        }
        break;

    AttackReading 상태로 전환 되면 Vector3.Lerp 함수를 이용해 보간된 값으로 스케일이 커지는 모션을 구현하였습니다.

     

    설정된 값 duration(2.0f) 보다 elapsedTime이 같거나 크다면 더이상 몬스터의 스케일은 커지지 않고 AttackIdle 상태로 전환 됩니다.

     

    case MonsterState.AttackIdle:
        monsterAnimator.SetBool("AttackReading", false);
        Collider2D playerInRange = Physics2D.OverlapCircle(transform.position, attackRange, playerLayer);
        if (playerInRange != null)
        {
            monsterState = MonsterState.Tracking;
        }
        break;

    플레이어를 OverlabCircle 함수로 감지합니다.

     

    인자를 보시면 transform.position(몬스터 중심 위치) 에서 attackRange 범위 내에 있는 레이어를 감지를 합니다.

    에디터에서 시각적으로 보기 위함이며 큰 원이 현재 attackRange의 범위가 됩니다.

     

    case MonsterState.Tracking:
        monsterAnimator.SetBool("Walk", true);
        Vector2 directionToPlayer = new Vector2(player.position.x - transform.position.x, 0).normalized;
        monsterRigidbdy2D.velocity = new Vector2(directionToPlayer.x * speed, monsterRigidbdy2D.velocity.y);

    Tracking(추적) 상태가 되면 몬스터는 다시 걷기 애니메이션 상태로 돌입 하며

     

    normalized를 이용하여 방향 벡터를 구해와 Rigidbody2Dvelocity를 이동해 주었습니다.

    x축만 이동을 하며, y축은 몬스터가 갖고 있는 그대로의 값을 넣어 주었습니다.

     

    playerInRange = Physics2D.OverlapCircle(transform.position, 2.0f, playerLayer);
    if (playerInRange != null)
    {
        monsterAnimator.SetBool("Walk", false);
        monsterState = MonsterState.Attack;
    }

    또한 Tracking 상태에서 플레이어 레이어를 가진 이를 한번 더 감지합니다.

     

    이 Circle 안에 Player 레이어를 가진 이가 감지가 된다면 몬스터의 상태는 Attack 상태로 전환 됩니다.

     

    case MonsterState.Attack:
        if (isAttacking == false)
        {
            monsterAnimator.SetBool("Attack", true);
            monsterState = MonsterState.Tracking;
            isAttacking = true;
        }
        break;

    Attack 상태로 전환되면 애니메이션에서 Attack을 실행 합니다.

     

    *아직 마법이나 공격, 플레이어의 피격 등 구현이 되어있지 않은 상태 입니다.

     

    isAttacking의 조건을 걸어준 이유는 반복 공격을 하지 않기 위함 입니다.

     

    if (isAttacking == true)
    {
        attackTime += Time.deltaTime;
        while (attackTime > attackTimer)
        {
    
            attackTime = 0;
            attackTimer = UnityEngine.Random.Range(5.0f, 10.0f);
            isAttacking = false;
        }
    }

    Update문 에서 위와 같은 코드로 조건을 걸어 주었으며 5.0~ 10.0 의 값으로 랜덤으로 설정된 attackTimer 마다 몬스터는 공격을 할 수 있습니다.

     

    public void BeDamaged(float damage, Vector2 dir, float force)
    {
        if (monsterState == MonsterState.Walk)
        {
            monsterState = MonsterState.Idle;
        }
    
        if (monsterState == MonsterState.Walk || monsterState == MonsterState.Tracking || monsterState == MonsterState.Attack)
        {
            monster_Hp -= damage;
            monsterRigidbdy2D.AddForce(dir * -force * recoilFactor);
        }
    }

    마지막으로 몬스터가 피격 받는 함수인 BeDamaged에서 조건을 걸어주어, 몬스터가 걷기, 추적, 공격 중일 시에만 플레이어의 피격을 받도록 조건을 걸어 주었습니다.

     

    공격 감지 시간이나 피격 모션 등 부자연 스러운 부분이 많은것 같습니다.

     

    천천히 하나씩 수정해 가며 진행해 보겠습니다.

     

    감사합니다.

    '연습 프로젝트 > 2D로그라이크 게임' 카테고리의 다른 글

    UI 정보 패널 & 미니맵  (0) 2024.06.08
    UI 기획 (1차)  (0) 2024.06.08
    몬스터 상태 구현  (0) 2024.06.07
    플레이어 공격  (0) 2024.06.07
    플레이어 이동 로직  (0) 2024.06.06

    댓글

김효겸 / Tel. 010-7735-0580 / E-mail. dollzzang2@hanmail.net