ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • RPG 액션 연습 07. DRAGON! 02 / 02
    연습 프로젝트/RPG 연습

     

    현재 제작 중인 드래건은 초기 활성화 시

     

    하늘에서 비행모션을 하며 땅으로 내려오면서

     

    운석을 떨어트리는 스킬을 씁니다.

     

    이후 드래곤 밑바닥이 Ground와 닿았다고 판정 시 

     

    DragonBoss스크립트 컴포넌트는 비활성화가 되며

     

    DragonBoss2 의 스크립트에서의 동작이 실행됩니다.

     

     

    DragonBoss 스크립트

    마지막 동작 부분인 카로틴 함수 Enable에서 dragonBoss.enabled = false로

     

    현재 사용하는 dragonBoss 스크립트를 비활성화를 시키며

     

    DragonBoss2.instance.state = DragonBoss2.State.Idle로 상태를 Idle로 변환시켰습니다.
         

    DragonBoss2 스크립트를 보겠습니다.

    DragonBoss2 스크립트

    이 스크립트에선 enum으로 상수 열거형 클래스를 사용하였습니다.

     

    Roding : 초기 상태

    Idle : dragonBoss -> dragonBoss2 넘어오는 상태

    Move : 움직임 상태

    Attack : 공격 상태

    Return : 복귀 상태

    Damaged : 대미지를 입은 상태

    Die : 죽음 상태

     

    로 총 7가지의 상태 값이 있습니다.


     이 상태 값 들은 Update함수에서 제어를 하는데

     

    먼저 Start함수부터 보시겠습니다.

    Start 함수

     

    초기의 드래건의 상태는 Roding 상태입니다.

     

    Roding상태는 아무 동작을 하지 않는 상태로 할 것이기 때문에

     

    dragonBoss 스크립트에서 Idle로 상태를 변환하지 않는 이상

     

    이 스크립트는 단순히 Roding 상태일 뿐 동작은 하지 않습니다.

     

    이후 anim / cc / nav / audi의 변수에게 

    Animator / CharacterController / NavMeshAgent / AudioSource 

    컴포넌트를 할당받습니다.

     

    GameObject.Find의 Find 함수는

    이름으로 대상을 찾습니다.

    (이름이 같을 경우 먼저 검색된 오브젝트를 반환합니다)

     

    오브젝트를 찾는 방법은 여러 가지가 있지만.

     

    Find와 비슷하게

    FindWithTag(태그로 대상을 찾음) 

    (이 FindWithTag도 이름이 같을 경우 먼저 검색된 오브젝트를 반환합니다)

     

    FindObjectsWithTag(같은 태그를 가진 대상들을 찾음)

    (GameObject []의 배열 형태로 반환합니다)

     

    조금 더 검색을 해본 결과 FindWithTag를 이용해서 태그를 사용하는 편이 속도가 더 빠르다고 합니다.

    Find를 쓰는 것보단 FindWithTag를 사용하는 것이 더 효율적인 것 같습니다.

     

    뒤에?. 연산자를 붙여 null 인지를 판단하였습니다.

    c#책을 공부하면서 배웠던 널 연산자인데

    한번 사용을 해봤습니다..ㅎㅎ

     

    ?. 은 null이 아닐 시 뒤에 transform이 실행됩니다.

     

    즉 player = GameObject.Find("Player"). transform를 풀어 보자면

    GameObject를 찾아서 "Player"가 있다면 트랜스폼을, 없다면 null을 반환하게 됩니다.

     

    현재 Player가 있다는 조건이기에

    player = GameObject.Find("Player"). transform과 같이 쓰셔도 똑같은 기능을 하는 코드입니다.

     

    orizinPos : 초기 위치
    orizinRot = 초기 회전 값 

     

    orizinPos = new Vector3(transform.position.x, 0, transform.position.z)

    드래건의 초기 위치는 하늘에 떠 있는 상태이기 때문에

    y축을 0으로 지정한 new Vector3로 받아왔습니다.


            orizinRot = transform.rotation : 초기 회전 값은 자기 자신의 회전 값입니다.

     

    Update 함수

    드래건의 상태 값은 Swicth문으로 각 상태 값에 맞는 함수 실행을 시켜줬습니다.

     

    우선 첫 번째 조건문인 

     

    if (isAnim == false && state!= State.Die)  RotateDragon()

    를 보겠습니다.

     

    isAnim : 애니메이션이 실행 중인지 체크할 불 변수입니다.

    공격 애니메이션에 Event를 추가시켜 

    처음과 끝 부분에 isAnim = true / isAnim = false의 제한을 걸어둘 예정입니다.

     

    즉 공격을 하고 있을 때는 RotateDragon()의 함수가 실행이 안됩니다.

     

    또한 state!= State.Die로 죽지 않았을 때를 조건으로 걸어 주었습니다.

     

    RotateDragon 함수는 Player를 향해 트랜스폼이 회전하는 동작이기에

     

    죽었을 때도 플레이어를 향해 회전한다면 이상한 상황이 연출되는 것을 방지했습니다.

     

    즉! 애니메이션이 실행 중이지 않을 때 (그리고) 죽지 않았을 때

     

    RotateDragon() 함수가 실행됩니다.

    Vector3로 방향을 지정해 주었습니다.

    방향은 player의 위치에서 드래건의 위치를 뺀 값입니다.

     

    transform.localRotationr과 transform.rotation 은 오브젝트의 부모가 없으면 동일합니다.

    다만 transform.localRotation는 부모가 있으면 부모의 기준으로 상대적인 회전 각도를 나타내어 줍니다.

     

    euler이나 eulerAngles 등 트랜스폼을 회전시키는 방법은 많습니다.

    다만 그중에서 Quaternuon의 Slerp이라는 보관법을 사용하여 좀 더 부드럽게 이등이 가능하게 했고

    인자로는 (A, B, time)

    으로 A와 B의 사이를 0.0f - 1.0f 퍼센트로 보간해

    결과를 Quaternion으로 리턴해 주는 함수입니다.

     

    hpSlider.value = (float) hp / (float) maxHp로

    현재 hp / 최대 hp를 나누어 0.0f - 1.0f 값으로 맞춰 주었습니다.

     

    우선 hpSlider는 Slider를 받아왔으며

    이 Slider의 value 값은 0.0f(0%) - 1.0f(100%)의 값을 갖고 있습니다.

    Slider의 Value


         드래건의 상태가 Roding / Idle 일시 실행되는

    Roding함수 / Idle함수입니다.

     

    Roindg 함수 에선 Print를 이용해 콘솔 창에 기록을 확인하여 테스트를 했습니다.

    Console

    상태가 Idle일시 실행되는 Idle함수입니다.

    Distance함수를 이용해 transform(드래건)의 위치와 player의 위치를 계산하여, 지정한 findDistance 값 보다 작다면

    거리를 계산하는 magnitude와 sqrMagnitude 도 있었습니다

     

    Distance 함수와 magnitude는 과정 및 계산 방식은 같지만 편의를 위해 Distance 함수를 제공하는 것이고

    sqrMagnitude는 제곱 값을 리턴해 루트를 하지 않아 연산 속도가 빠르지만

    단순히 크거나 작은지 판단할 경우에 쓰인다고 했었죠.

     

    Distance가 나올 때마다 쓰다 보니 외우지 않으려고 해도 자동적으로 외워졌습니다.

    역시 반복학습의 힘이란... 위대한..

     

    그 범위 안에 들어온 것으로 판단해

    state를 Move로 바꿔줍니다.

    이해 돕기용 드래곤 Animator


    Delete

    Idel 상태에서 드래건의 추적범위 안에 플레이어가 들어왔다면 상태 값이 Move로 변환되어

     

    실행될 Move 함수입니다.

     

    원래 연기서 if(Vector3.Distance(transform.position, orizinPos) > findDistance)의 조건으로

     

    추적 범위 밖이라면 상태 값을 Return으로 바뀌게 구현을 했었는데,

     

    이렇게 움직임에 제한을 걸어주면, 공격 상태 일시 자유로운 이동 구현이 안되게 되어버려서

     

    우선 삭제를 해놨습니다.

     

    추 후 다른 방법으로 일정 거리에 Collider를 붙여서 감지가 되면 초기 위치로 돌아가게 만들어볼까 생각 중입니다.

     

    또한 이 이동 구현을 하는 Move함수도 삭제를 해 주었는데요

     

     

    드래건의 이동 구현을 해 봤는데, 이동을 하는 것보다 Attack함수에서 (근접) / (원거리)를 나뉘게 하는 것이 더 좋다고 판단했습니다.

    Idle 함수
    Idle함수에서공격 범위 안에 플레이어가 들어왔다면, Idle에서 Move함수를 안 거치고 Attack으로 전환이 됩니다.

     

    Attack함수의 원거리 공격 모션입니다.

     

    nav.enabled = true로 꺼져있던 NavMeshAgent를 다시 켜줍니다.

     

    이후 if문으로 조건을 걸어 줍니다.

    (Vector3.Distance(transform.position, player.position) < atkDistance_Big &&

    드래건의 atkDistance_Big의 범위 안에 플레이어가 들어와 있고
                Vector3.Distance(transform.position, player.position) > atkDistance_Small)

    드래곤의 atkDistance_Small의 범위 안에 플레이어가 안 들어와 있다면

     

    즉, 큰 범위 안에는 들어왔지만 작은 범위 안에는 들어와 있는 상태가 아니라면?

    원거리 공격을 실행합니다.

     

    이후 audi.pitch = 0.5f를 이용해 오디오 재생 음 들이 너무 빠른 것 같아 재생되는 속도를 반절로 낮췄습니다.

    AudioSource

    currentTime += Time.deltaTime

    float 변수인 currentTime에 +=연산자로 Time.deltaTime을 누적시켜 1초씩 증가시킵니다.

     

    값이 계속 증가하다가

    if(currentTime >= attackDelay_)의 조건문인

    지정된 attackDelay_의 값 이상이라면?


    이해 돕기용 Dragon Boss2 인스펙터 창


    Attack을 실행해 줍니다.

    int형 변수 atk_bigStep : 공격 스탭을 담당해줄 변수입니다.

     

    atk_bigSetp에 맞춰 각 맞는 애니메이션을 실행하며, 

    atk_bigStep == 0 : atk_bigStep = 1

    atk_bigStep == 1 : atk_bigStep = 0

    이 되는 조건입니다.

     

    이후 공격을 했으니 currentTime은 0으로 초기화시켜줍니다.

    else if문입니다.

    위의 if문의 조건인 범위 안에는 들어왔지만 작은 범위 안에는 들어와 있는 상태가 아니라면? 이 아니고

     

    (Vector3.Distance(transform.position, player.position) < atkDistance_Big &&

    드래건의 atkDistance_Big 범위 안에 플레이어가 들어왔고
                Vector3.Distance(transform.position, player.position) < atkDistance_Small)

    드래건의 atkDistance_Small 범위 안에도 플레이어가 들어와 있다면?

     

    int형 변수 currentTime에 +=연산자를 이용해 Time.deltTime : 1초씩 증가시켜 줍니다.

     

    이후 원거리 공격 때 (attackDelay_)와 다른

    attackDelay의 값 이상이라면?

     

    근접 공격을 실시합니다.

     

    이번에도 int형 변수로 Attack 스탭을 관리하는데

    atk_smallStep_ (원거리 스탭) 이 아닌 atk_smallStep(근접 스탭)을 이용하여

    Swicth문에서 atk_smallStep의 값을 case로 이용해 실행을 합니다.

     

    각 atk_smallStep에 맞게 케이지에서 애니메이션을 실행시켜 주며

     

    atk_smallStep == 0 : atk_smallStep = 1

    atk_smallStep == 1 : atk_smallStep = 2

    atk_smallStep == 2 : atk_smallStep = 0

     

    의 조건이 완성됩니다.

     

    이후 마찬가지로 공격을 실시했으니 currentTime은 0으로 초기화를 해줍니다.

     

    드래건의 Damaged 상태와 Die의 상태가 남았습니다.

    이 상태는 드래건이 맞는 동작을 구현하는 함수 HitDragon에서 나눌 것입니다.

    int hitPower를 받도록 하였고 현재 구현에서는 player의 공격력을 넣어줄 것입니다.

     

    맨 처음으로 드래건의 상태가 Die인 상태에서는 return으로 함수를 종료하게 만들었습니다.

    Die상태에서 동작을 멈추는 구현을 하지만, 예기치 못한 상황이 발생할 수 있으므로 조건을 먼저 걸어줬습니다.

     

    드래건의 hp는 -= 연산자를 통해 hitPower(플레이어 공격력) 만큼 체력을 깎아주고

    isStopped를 true로 움직임을 멈춰줍니다.

    또한 ResetPath()로 목적지를 초기화시켜줬는데

     

    드래건이 hit가 될 시 hit모션을 하면서 미끄러짐을 방지해 주었습니다.


    아래 if-else문에서는

    드래건의 hp가 0보다 크다면 Damaged함수를 실행하고

    hp가 0보다 작다면 죽었으므로 간주해 Die 함수를 실행시킵니다.

     

    Damaged 함수에서는 StartCoroutine로 코 루틴 함수 Damaged_Corutine를 실행시켜줬으며

    hit 대미지를 받았을 시 yield return new WaitForSeconds(0.2f)로 0.2초의 대기시간을 주었습니다.

    이후 상태 값을 Idle로 변경하여 원활한 사이클이 가능하게 만들어 줬습니다.

     

    Die함수에서는 드래건이 죽었을 시 실행되는 함수로

    StopAllCoroutines를 사용해 모든 코 루틴 함수를 스톱시켜줍니다.

     

     StartCoroutine를 이용해 코 루틴 함수 Die_Corutine 함수를 실행시켜줍니다.

    코 루틴 함수 Die_Corutine에서는

    CharactorController를 enabled = false를 이용해 비활성화시켜주고

    NavMeshAgent 도 enabled = flase를 이용해 비활성화시켜줍니다.

     

    이후 yield return new WaitForSeconds(5.0f)를 이용해 5초의 대기시간을 주며

    Destroy로 파괴시켜줍니다.


    마지막으로 각 스킬의 Event 추가 함수를 보며 영상으로 마무리를 하겠습니다.

    Reset(초기화) 해주는 기능입니다.

    nav의 속도를 원래 상태로 만들어 주며

    현재 애니메이션을 실행 중인지 체크할 불 변수 isAnim은 false로 만들어 줍니다.

     

    이후 각 콜라이더를 담은 빈 오브젝트를 SetActive(false)로 비활성화해주는 모습입니다.

     

    이 Reset 기능은 

     

    각 필요 애니메이션의 마지막 부분에 추가를 해줍니다.

    애니메이션

     

    근접공격

    근접 공격 시 각 스탭에 맞게 실행될 애니메이션에 추가시킬 함수들입니다.

    사운드 재생이 필요한 부분은 PlayOneShot으로 사운드를 재생해 주었고

    콜라이더를 계속 키고 있는 것이 아닌 평소엔 비활성화 상태였다가

    공격 시 콜라이더를 켜준다음 Reset으로 꺼주는 기능을 만들었습니다.

     

    atkL_S는 원거리 공격 때는 true였다가 근접 공격때는 false로 만들어 원거리인지 근접인지 체크할 불 변수입니다.

     

    (atkL_S == false) 이 조건이 맞다면 (근접 공격이라면)을 추가시켜 주었습니다.

    nav의 속도는 20f로 빠르게 수정을 해 주었고

    nav의 destination(목적지)는 

    Position

    Position으로 이동시켜 줍니다.

     

    목적지를 Player로 테스트를 했었는데 바로 앞에서 멈추는 어색한 상황이 연출되어

    일정한 거리를 두어 일종의 패턴을 만들었습니다.

     

    원거리 공격 때 각 스탭에 맞춰서 실행될 애니메이션에 붙을 함수들입니다.

     

    첫 번째 공격인 Atk01_ 함수에서는

    운석을 떨어트리는 동작을 연출할 코 루틴 함수 Atk01_Coroutine함수를 실행시켜 줍니다.

     

    Atk01_Corutine함수를 보면

    Instantite를 이용해 프리 팹을 생성시켜 줍니다.

     

    인자로는

    (gameObject(Lava),

    위치 : 플레이어의 위치 + y축 0.01f,

    회전 : Quternion.identity입니다.

     

    플레이어의 위치 + y축을 0.01f만큼 올린 이유는

    Lava의 Base가 땅에 가려져 y축을 0.01f만큼 올린 것이며

     

    회전 값에 Quterniom.identity를 사용한 이유는

    API 문서에서 "회전 없음"을 나타내기에 기본 값을 나타내는 identity랄 사용하였습니다.

     

    yield return new WaitForSeconds(1f)로 1초의 대기시간을 갖으며 생성시켜 주는 코드입니다.

     

    마지막으로 드래건이 활성화되면서 내려온 뒤 Ground에서 동작되는 구현의 영상을 살펴보겠습니다.

     

     

    01 : 00 초

    상당히 글 내용이 길어졌는데.. 작성하다 보니 중간에 멈추기도 애매하고

    또 나누자니 애매하고, 계속 애매해하면서 작성하다 보니 길어졌습니다.

     

    이렇게 되다 보니 영상도 다 담으려고 하다 보니 길이도 길어지고..

     

    좀 더 동작 구현 부분들을 정리하여 여러 번 걸치더라도 나눠서 정리를 하는 게 보기에 효과적일 것 같습니다.

    댓글

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