ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • FPS 연습 01. Player 이동 및 회전 구현
    연습 프로젝트/FPS 연습

    안녕하세요.

     

    이 카테고리에서는 FPS를 연습해 보고

     

    직접 제작해보며 그 과정을 올려 보려고 합니다.

     

     

    점점 해야 할 것들이 많아지는 느낌적인 느낌...

     

    이지만! 오히려 한 가지만 계속하는 것보다 

     

    다양한 것을 볼 수 있어서 더 재밌지 않나 싶기도 하고 

     

    해야 할 것이 생긴다는 게 뭘 해야 할지 모르는 것보다 훨씬 좋은 것 같습니다.

    자 그럼 시작하시죠!

     

    우선 처음에 총을 쏘는 게임을 어떻게 만들까 잠시 생각을 해 봤는데

     

    처음으로 생각난 부분이 Player와 적이었습니다.

     

    단순히 Player와 적을 구현하여 

     

    player가 총을 쏴서 적을 잡는 모습이

     

     머릿속에 그려졌습니다.

     

     

    수류탄, 패줌 기능, 다양한 총기 등등 이것저것 생각나는데

     

    제일 먼저 가장 기초가 되는 Player를 먼저 만들어 보겠습니다.

     

    PLAYER

    유니티 에셋 스토어를 이용해서 다운로드를 많이 했었지만

     

    이번 MODEL은 유니티 초창기에 발표된 AngryBot 프로젝트를 

    URP(Universal Render Pipeline)와 포스트 프로세싱(Post Processing)을

    적용해 리뉴얼한 것으로

    유니티 사의 깃 저장소에서 내려받을 수 있는 모델입니다.

     

     

    우선 Player에게 생명을 입혀주기 위해 RigidBody를 입힐까 했지만,

    이번엔 Character Controller를 입혀 줬습니다.

     

    둘 의 기능 차이를 궁금해하시는 분들이 계실 텐데

    제가 알아본 결과를 말씀드리겠습니다.

     

    리지드 바디

    - 클래스 내부에 복잡한 물리 연산을 편하게 할 수 있는 수많은 함수가 미리 구현돼 있다.

    - 어려운 물리 작용을 코드 몇 줄로 손쉽게 구현이 가능하다는 장점이 존재함.

     

    - 메모리를 많이 차지하는 문제가 있다.

    - 리지드 바디 컴포넌트를 가진 오브젝트가 많아질수록 불필요하게 처리되는 연산량이 높아져 

    프레임 저하를 일으킬 수 있음.

     

    캐릭터 컨트롤러

    -유니티에서 이러한 점에 착안해, 중력이나 충돌 등의 간단한 물리 연산만을 필요로 하는

    캐릭터 전용 클래스인 캐릭터 컨트롤러를 제공하고 있음.

     

    - 캐릭터 컨트롤러 컴포넌트는 충돌체(Collider)도 포함하고 있음

    (주로 물리적 특성을 사용하지 않는 3인칭 또는 1인칭 플레이어 제어에 주로 사용된다고 함)

     

    우선 제 생각을 정리해 보자면, 물리적 특성? 을 사용하지 않는 경우면

    불필요한 연산량이 높아지는 리지드 바디보다는, 캐릭터 컨트롤러가 좋을 것 같습니다.

    아직 모든 말이 100% 이해가 되는 건 아니지만..

    언젠가 제 스스로 구별해서 컴포넌트를 선택하는 날이 올 것을 굳게 믿습니다.

     

    우선 플레이어의 이동 동작 구현 코드인

    PlayerMove부터 만들어 보겠습니다.

     

    우선 플레이어는 각 w, s, a, d  ||  ↑↓←→ 의 키를 입력받아 이동을 하며

     

    자주 사용한 Horizontal과 Vertical 과도 같이, Input Manager에 Axes로 있는

    "Jump" - 입력키 : Spacebar를 누르면 점프를 실행합니다.

     

    코드 내용

    moveSpeed = 7f : 플레이어의 움직임 스피드입니다.

    jumpPower = 10f : 플레이어의 점프할 파워입니다.

     

    CharacterController / Animator를 받아올 변수 cc / ani입니다.

    Start함수에서 GetComponenet를 사용해 받아 왔습니다.

     

    grativy = -10f : 플레이어에게 중력을 적용해줄 값입니다.

    yVelocity : 플레이어의 y축 값을 제어하기 위해 선언했습니다.

     

    isJumping = false : 동작 구현을 1단 점프만 하기에, 중복 실행을 막아 줬습니다.

    PlayerMove의 Update 함수입니다.

     

    float h / float v를 통해 각 Horizontal과 Vertical을 받아 왔습니다.

    Horizontal : a, d || ←→ 일 시 -1.0f ~ 0 ~ 1.0f의 값을 반환시켜 줍니다.

    Vertical : w, s || ↑↓ 일 시 -1.0f ~ 0 ~ 1.0f의 값을 반환시켜 줍니다.

     

    new vector3로 (Horizontal, 0, Vertical)을 받고, 이 백터를 

    Vector dir로 주어 추가 제어가 가능하게 만들었습니다.

     

    지난 RPG 연습하면서  Directional(방향)을 줄여서 dir로 많이 사용한다고 하는 말을 설명으로 적은 적이 있는데

    하여 풋내기 지만 따라 해 보려고 dir로 만들어 봤습니다


    dir = dir.normalized : 벡터의 정규화입니다.

    벡터의 길이를 1인 벡터를 반환시켜 주는 normalized인데

    정규화되기에 너무 작은 경우 0 벡터가 반환된다고 합니다.

     

    오브젝트의 균일한 이동을 위하여 필요한 벡터의 정규화이며

    모든 방향의 벡터의 길이가 1이어야 방향에 따른 이동속도가 같아집니다.

     

    음 예를 들자면 대각선으로 움직일 때 더 빨라지는 효과를 생각하시면 됩니다.


    여기서 TransformDirection 함수가 나옵니다.

     

    TransformDirection을

    Transform컴포넌트가 붙어 있는 게임 오브젝트를 기준으로 방향벡터로 변환해 주는 TransformDirection()

    함수가 구현이 되어있다고 합니다.

     

    음 조금 풀어서 얘기하자면 로컬 공간에서의 벡터를 월드 공간에서의 벡터로 변경해 주며

     

    현재 쓴 스크립트의 Camera.main.transform.TransformDirection(dir)

    은 메인 카메라를 기준으로 방향을 변환하는 코드로 보시면 됩니다.

     

    그리고 캐릭터 컨트롤러를 받아온 cc의 변수를 이용해

    cc.Move함수를 활용하여 (dir(방향) * speed(스피드) * Time.deltaTime())

    Time.deltaTime : PC의 성능과 무관하게 동등한 조건?

    Time.deltaTime : 이전 프레임에서 현재 프레임까지 걸린 시간?

    Time.deltaTime : 델. 타?

    혹시 개발자 분들 만의 다른 간편한 용어가 있는지 한번 알아오면 수정하겠습니다!


    바로 이어서 점프 부분을 구현하겠습니다.

    점프 구분입니다.


    아 점프 부분은 아마 처음 보여드리는 것 같습니다.

     

    처음엔 Player가 바닥인지 판정을 어떻게 하면 좋을지 생각이 안 나서

     

    코 루틴 함수로 Player의 점프 시간을 재서 소스코드를 짜 본 적도 있고

     

    점프를 실행하면 불 값이 트루였다가, 일정 시간이 지난 후 함수 호출을 할 수 있는 Invoke함수로 

     

    인자 값에 시간을 입력해 false가 되게도 해본 기억들이 있는데요 음...

     

    그냥 갑자기 옛날 생각이 났습니다..ㅎㅎ


    맨 아래 코드인

    yVelocity += grativy * Time.deltaTime
    dir.y = yVelocity

    부터 보겠습니다.

     

    쉽게 말해 Player에게 중력을 지정해 주는 코드라고 보시면 편하실 겁니다.

    만들어 준 yVelocity에 지정한 gravity의 값 * Time.deltaTime를 += 연산자로 누적시켜 줍니다.

    이후 Vector3로 받은 dir.y축 을 이 yVelocity 값으로 지정해 줍니다.

    -Update문입니다-

     

    이렇게 먼저 구현을 해 두고 

     

    두 번째 if문부터 보겠습니다.

     

     if(Input.GetButtonDown("Jump") &&! isJumping)

    조건을 걸어주어, GetButtonDown로 Name : "Jump / 입력키 : Spacebar를 받아왔습니다.

    이 입력 키는 유니티 자체에 있는 Input.Manager의 Axes들 이였죠?

     

    또한 GetButtonDown(누를 시) 말고도 GetButtonUp(뗄 시) GetButton(누르고 있을 때 계속)

    이 있습니다. 제가 기억하기 위해 한번 더 적었습니다!

     

    그리고 isJumping이 false 라면의 조건입니다.

    isJumping == false와 같은 말인데, 코드가 길어지다 보니

    아시는 한 개발자 분이 말씀해 주신 건데 개발자 분들은! isJumping으로 사용하는 분들이 많다고 합니다.

    물론 isJumping == false가 잘못됐다거나 사용하면 안 된다는 말은 아니라고 합니다!

     

     

    처음에 isJumping = false로 지정해 주었으니, Spacebar만 눌리면 맨 처음엔 조건에 부합하여 동작을 할 것입니다.

    yVelocity = JumpPower로 지정된 값을 대입하여 주고

    isJump는 true로 바꿔 주어 중복 방지를 해줬습니다.

     

    이제 위의 if문을 보겠습니다.

    if (cc.collisionFlags == CollisionFlags.Below)

    정말 중요한 코드입니다.

     

    현재 제가 알 수 있는 바닥인지 체크를 할 수 있는 코드는 두 가지입니다.

    1. 캐릭터 컨트롤러에 들어 있는 isGrounded와

    2. 현재 사용한 CollisionFlags 변수입니다.

     

    CollisionFlags.Blow는 캐릭터 컨트롤러의 충돌 영역 중 아래쪽 부분에 충돌했을 때 true의 값이 반환되며

    Above는 위쪽 / Side는 옆쪽에 충돌됐을 때 true의 값이 나타내어집니다.

    바닥에 착지했을 때는 플레이어의 아래쪽이기에 Below를 사용한 모습입니다.

     

    바닥에 착지를 했다면

     

    if(isJumping) : 그리고 isJumping이 true라면

    isJumping = false로 바꿔주고

    yVelocity는 0으로 바꿔주는 모습입니다.

     

    이 부분은 현재 플레이어의 적용되는 중력값이 계속 누적되어 지기에,

    어떤 물체나 각 다른 높이에 올라가 있다가 내려오게 되면

    누적되어 있는 값 이 크면 클수록 떨어지는 속도가 엄청 빨라질 것이기에

    물체나 낙하될 시 이상 현상을 방지하고자 구현을 해 두었습니다.

    마지막으로 PlayerAni 함수입니다.

    float h와 float v는 각각 Horizontal과 Vertical을 받아 올 것이므로, 직관적이진 않지만 

    알 수 있는 변수로 선언했습니다.

     

    전진 후진을 담당하는 v가 0.1f 보다 큰 값이 반환된다면 RunF의 애니메이션을 

    Play 함수로 실행시켜 주는 부분입니다.

     

    나머지 RunB / RunL / RunR /

    마지막으로 모든 값이 0.1보다 낮다면, 이동을 멈춘 상태로 간주하여

    else문이 실행되며 Play 함수로 "Idle"을 실행시켜 줍니다.

     

    단지 이렇게만 해 두면 카메라와 상관없이

     

    player 혼자만 단독으로 움직입니다.

     

    음.. 우선 짧은 영상으로 봐보겠습니다.

     

     

    00 : 10초

    그리 하여서 긴 코드 분량이 아니기에

     

    player의 회전 + 카메라의 회전 / 마지막으로 카메라가 플레이어를 따라다니게 만들겠습니다.


    바로 우선 카메라의 회전부터 보시겠습니다.

     

    카메라의 회전을 담당하는 전체 코드입니다.

    turnSpeed = 200.0f로 맞춰 주었고 인스펙터 창에서 테스트하며 값을 바꾸기 위해 public으로 선언했습니다.

     

    각 각 Mouse X / Mouse Y로 스피드 * Axis값 * Time.deltaTime이 값을 담을 float변수입니다.

     

    Mouse X / Mouse Y 도 Input.Manager에 유니티 자체에 내장되어있는 axes들입니다.

    Input Manager

    각 각 마우스의 좌/우 -1.0f ~ 0 ~ 1.0f는 Mouse X / 마우스의 상/하 -1.0f ~ 0 ~ 1.0f 는 Mouse Y입니다.

     

    값을 제한하기 위해 Mathf.Clamp(my, -18.0f, 45.0f)를 사용하였습니다.

    Clamp함수의 인자는 (float value, float min, float max)로 되어 있습니다.

    my의 값을 최대 -18.0f 도, 45.0f도로 제한하여 최고 내릴 수 있는 각도와 올릴 수 있는 각도를

    직접 계산하여 넣었습니다.

     

    이건 책을 통해 알게 된 사실인데,

    여기서 mx와 my에 미리 값을 누적시키지 않고 실행한다면

    유니티 내부에서는 0도에서 -1도만큼 회전시키면 -1도가 되는 것이 아니라 359도로 반환된다고 합니다.

    그래서 0의 지점에서 살짝만 올려도 자꾸만 아래를 보게 되는 현상이 생기게 되는데요.

    물론 clamp 함수에 막혀 90도 이상 이면 제한이 되지만

    원하는 동작 구현은 아닙니다.

     

    그래서 mx / my에 

    mx += turnSpeed * mouse_X * Time.deltaTime

    my += turnSpeed * mouse_Y * Time.deltaTime

    값을 미리 할당시켜 누적시켜 준 뒤

     

    transform.eulerAngles = new Vector3(-my, mx, 0)로 카메라의 회전을 구현해 보았습니다.

    여기서 중요한 new Vector3에(x, y, z)의 값에

    각 (my, mx, 0)인 부분을 기억해야 합니다.

     

    Mouse Y는 상/하입니다.

    상 하로 움직이는 것은 x축의 회전입니다.

     

    또한 Mouse X는 좌/우입니다.

    좌 우로 움직이는 것은 y축의 회전입니다.

    new Vector3(-my, mx, 0)

    나머지 z값은 0으로 지정해 주었습니다.

     

    또한 my값에 -를 곱하였는데,

     

    오류는 아니고 찾아보기로 해외에서는 상/하 가 저희가 생각하는 거랑 반대로 생각한다고 합니다.

    아무리 생각해도 이게 맞는 것 같은데

    마우스를 올리면 화면은 내려가고

    마우스를 내리면 화면은 올라가기에

    상하 값을 반대로 입력을 해 주었습니다.

    이번엔 Player의 회전 구현입니다.

    카메라가 회전되는 방식과 비슷하지만.

    Player는 상 하를 받지 않을 것이기 때문에 Mouse X 값만 정해주었습니다.

     

    mx의 스피드 * 반환 값 * Time.deltaTime의 값을 누적시켜

     

    트랜스폼 오일러 값을 new vector3로 받아 0, mx, 0으로 적용시켜 주는 모습입니다.

     

     

    마지막으로 캠이 player를 따라오게 만드는 기능의 스크립트입니다.

    플레이어의 뒤쪽 부분에 빈 오브젝트를 붙여줬고

    그 오브젝트를 target으로 지정해 주었습니다.

     

    이후 Update가 아닌 LateUpdate에서 구현을 했습니다.

    Update가 호출된 후 호출되는 LateUpdate함수이기에

     

    Update에 플레이어 이동 구현이 되어 있어서

    이동 구현이 끝난 후 카메라의 Follow 로직 기능이 순서대로 실행되면 상관없지만,

    그렇지 않을 가능성도 존재하는 랜덤적인 도박이기에

    떨림 현상이 발생할 수 있습니다.

     

    그래서 LateUpdate함수에 실행을 해주며

    로직은 간단합니다.

    현재 transform(Camera)의 position을 target의 position으로 이동시켜라입니다.

     

    그 위에 구현되어 있는 부분은

    Cursor.visible = false : 마우스가 단순히 보이지 않게 합니다.

    Cursor.visible = true : 마우스가 보이게 합니다.

     

    Cursor.lockState = CursorLockMode.Locked : 커서가 게임 창 밖으로 이동이 안됩니다.

     

    Cursor.lockState = CursorLockMode.None : 커서를 게임 중앙 좌표에 고정시킵니다 / 마우스 보임

     

    Cursor.lockState = CursorLockMode.Confined: 커서를 게임 중앙 좌표에 고정시킵니다. / 마우스 안보임

     

    중 

    Cursor.lockState = CursorLockMode.Locked 와 Cursor.visible = false를 사용하여 

     

    마우스를 보이지 않게 하고 게임 창 밖으로 이동이 안되게 하여

     

    테스트하는데 불편함을 없앴습니다.

     

    회전 값을 아무리 맞춰도 마우스가 화면 밖으로 이탈되기에...


    뭔가 몇 안 되는 동작 구현인데, 스크립트를 나눠서 관리하여서 

    설명도 나눠서 하게 되니깐 글이 길어진 것 같습니다.

     

    마지막으로 스크립트를 컴포넌트 하는 캡처 이미지와

    캠 포지션의 위치를 보고 짧은 영상으로 오늘은 마무리를 짓겠습니다!!

    CamPosition

     

    00 : 23초

     

    댓글

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