나 개발자 진짜 되냐?

[ Unity 3D 서바이벌 게임 만들기 1 ] 플레이어 만들기 본문

유니티를 공부해봐요!/중급이에요!

[ Unity 3D 서바이벌 게임 만들기 1 ] 플레이어 만들기

Snow Rabbit 2024. 10. 25. 21:35

 

3D 게임을 만들어보려고 한다

 

아니..

나 마크 10분 이상 못하는

멀미보유자

 

과연...

게임을 완성할 수 있을까??

 

ㅋㅋㅋ그래서 강의를 보며 따라 할 때

시연영상 때 눈을 감고 있긴 하다

 

 

 

일단 3D를 만들고 나서

플레이어 이동까지

구현해 보자!!

 

 

3D는 이렇게 생성!

 

 

오... 3D는 뭔가 다르다!

시작 배경이 예쁘다

 

여러 에셋을 추가하고

플레이어를 이동시키는 스크립트를 짜보자

 

 

먼저! 캐릭터 이동 전에

캐릭터를 이동할 만한 배경과 바닥이 있어줘야 한다.

 

그것을 우리는 3D에서

스카이박스

라고 한다.

게임 세계의 배경을 둘러싸는 환경 매핑 기술

 

큐브 맵과 ( 정육면체 ) 구체형 스카이박스

등등이 있고

 

여기서 스카이박스는

육면체 큐브맵

또는

하나의 구체로 텍스터가 매핑된 구체형으로

구성된다.

 

 

매력적인 이 친구

스카이박스를 동적으로 변경하여

낮과 밤 등의 시간대나 특정 이벤트에 맞게

배경을 변화시킬 수 있다.

 

 

우선 우리는

패키지를 import 해주고

그 패키지에서

environments 프리팹을 꺼내서 올려둔다.

 

 

 

다음에 우리는

Asset 밑에

Materials 파일 생성 후

 

새로운 뭬테리얼 생성!

 

그의 이름은 skybox

바로밑에

shader 보면 지금은 standard인데

 Skybox > Procedural로 바꿔주면 된다!

 

밑에 보면 SkyTint라고 하늘색을 바꿔줄 수 있는데

이 부분을 A0C0D1로 바꿔주자!

Ground는 땅인가? 싶겠지만

지평선 부분의 색을 바꿔줄 수 있다.

 

3D7FB6으로 바꿔보자

 

마지막으로 Exposure라고 해서

밝기 조절이 있는데 1.3으로 맞춰준다.

 

 

이다음 우리는 적용시켜줘야 한다.

 

window > Rendering > Lighting

 

로 들어가서

 

두 번째에 Environment 눌러보면

Skybox Material에 디폴트가 적용되어 있는데

이 부분을 우리가 방금 만든 Skybox로 바꿔주면 된다.

 

 

환경세팅은 끝났고 플레이어를 만들어보자!

 

플레이어는 빈오브젝트를 만들어주고

player이라고 하고

그 자식으로 빈오브젝트를 한 개 더 추가해서

CameraContainer를 만들어주고

이 안에다가!

main Camera를 넣어준다.

 

그다음에 우리의 player로 올라가서

capsule Collider을 추가해 준다

2D 안됩니다!!

 

지금은 우리가 카메라시점을 보고 있어서

내가 어딨 는가.. 싶겠지만

충돌과 중력을 넣어줗어서 상호작용이 되게 할 것이다.

 

y 0.85

Radius 0.25

Height 1.7

 

이렇게 해주었는데도 캡슐이 붕 떠있다면

 

스크린 밑에 피봇으로 바꿔주면 된다!

 

중력도 추가한다

mass 20 ( 무게 )

constrints에서 

xyz를 다 체크 해서

회전을 막아준다.

 

 다음에 우리는

이 플레이어를 움직이게 해야 한다

input시스템을 이용해서

키값을 받아

이동시켜 보자!

 

인풋시스템을 쓰려면 설치해줘야 한다.

 

Window > Package Manager 

 

Unity Registry로 해주고 Input System 설치

 

ReStart 뜨면 해주기!

 

다음에 우리는 Asset 밑에

InputAction이라는 파일 생성!

그 안에다가

create 맨 밑에

Input Actions 눌러주고

이름은

PlayerInput이라고 지어주자!

 

오른쪽에 보면

Edit asset 해주면

인풋액션이 뜬다.

 

우리가 필요한 키들은

이동 : wasd 

점프 : space

인벤토리 : tab

공격 : 마우스 왼쪽

캐릭터 이동에 맞춰 카메라도 이동해야 한다 : 마우스 회전값

아이템 줍기 : e

 

Action Maps + 추가해서

Player이라고 써준다.

 

그다음에 Move라고 지어주고

 

움직이는 것이니 벡터 2를 받아야 해서

벨류에 벡터 투를 쓴다.

 

다음에 Move에서 + 버튼 눌러주고

 

해주면 4칸이 주르륵 나온다.

 

NO라고 적혀있는 부분은 지우고 

우리는

바인딩 밑에 path를 wasd를 넣어주면 된다!

 

 

자 다음

Look

 

룩의 경우 마우스의 움직임을 받아야 해서

 

델타로 설정해 준다.

 

다음에 뜨는 NoBinding을 눌러서

바인딩 밑에 path를 마우스 > 델타를 눌러준다.

 

다음!

JUMP!

스페이스를 써줄 것이다.

스페이스의 경우 꾹 누르는 게 아니라 점프! 만해주면 되는 거기 때문에

액션 타입을 버튼으로 설정한다.

 

 

자, 다음 인벤토리

이 친구도 눌렀다가 끝이기 때문에 같은 버튼!

( 위에랑 같으니 사진 생략 )

 

비슷한 느낌의

아이템 줍기 즉 파밍은

interaction

얘도 줍기라는 한번 딸깍이라

버튼!으로 해주고 e키로 설정

 

마지막으로 공격

Attack

얘도 마우스 딸각으로 공격! 공격! 할 테니

버튼으로 해주고

해주면 끝!

 

 

 

Player Input을 다 만들었으니

플레이어에 넣어주자!

 

추가해 주고 중요한 것은

Behavior이 sendmessage이었을 것이다.

이 부분을 Invoke Unity Events로 해주자.

 

이렇게 해주면!

우리는 아까 만들었던 input친구들을

이렇게 예전에 버튼을 넣어주었던 거처럼 하나하나 넣어주면 된다!

 

이 부분에 대해 잠깐 더 설명하면

 

 [SendMessage와 Invoke Event의 차이 ]

 

SendMessage

    On + "Action name"인 함수를 찾아서 호출하는 방식

   

우리가 아까 액션 네임을

move look 이렇게 설정했었는데

 

 send message의 경우

스크립트에다가 Onmove라든가

Onlook이라고 써줘서

그 함수이름이 같으면 실행해 주는 방식이다!  

Invoke Event

Inspector 상에서 Action에 함수를 설정하고 키 입력이 들어왔을 때 호출하는 방식

 

버튼에다가 함수 드래그드롭하는 방식이다.

이번에는 이렇게 활용해 볼 예정이다.

 

< 추가 >

Invoke C sharp Events


    C# 스크립트에서 Invoke Event 과정을 수행
  키 입력받고 실행 전, 키 입력 받고 실행 완료, 키 입력 해제 등의

구체적인 상황에 따라 별도의 함수를 등록할 수 있다.

 

자! 여기까지 따라왔다면

스크립트 전에 마지막으로

player에서 layer를 추가해 주자

add 해줘서

6번에다가 Player을 적어준다.

 

그리고 6번으로 설정해 주면 된다!

밑에 상속받는 친구들까지 해주면! 끝!

 

 

스크립트 작성은 몇 번 해보았으니 뭐

스크립트 파일 생성하고 하기!!

 

 

 

3개 만들고

미리 스크립트를 드래그드롭 해보자!

 

Player 가서

 

Player과 PlayerController을 넣어준다.

 

우리의 계획은!!

 

CharacterManager을

싱글톤으로 만들어 주고

 

거기에 player스크립트를 넣어줄 것이다.

그리고

이 스크립트는

컨트롤러나, 컨디션, 인터렉션

등등 부가적인 기능을 player에 넣어둘 것이다.

 

이 기능들을

직접적으로 접근하는 게 아니라

player라는 스크립트 내에서 캐싱해서

캐릭터 매니저를 이용해서 적용할 것이다.

 

별이 다섯 개!!

 

스크립트 짜러 가보자!

 

먼저 Character Manager

 

 

싱글톤으로 만들어 준다.

 

외부에서 대문자 Instance로 들어오게 되면

get을 보고 소문자를 반환을 한다.

 

null일 경우도 있기 때문에

예외처리를 해준다.

날일경우

게임오브잭터를 생성해서

그 안에다가 add 추가한다

캐릭터 매니저라는 스크립트를!!

 

 

_instance = new GameObject("CharacerManager").AddComponent<CharacterManager>();

 

<> 꺾새 안에 있는 건 cs 스크립트고

() 괄호 안에 있는 건 오브젝트이다.

 

 

다음에 플레이어도 만들어 준다.

이게 뭘까? 싶을 텐데

우리가 아까 player 스크립트를 만들어주었었다.

즉 class player를 가져온 것!

 

자 awake가 실행되었다는 건

위에 싱글톤이 만들어졌기 때문에 다음으로 이동한 것!

 

그래서

나를 집어넣고, 

삭제도 못하게 한다.

 

else로

기존에 있던 값지금 값이 다르다면!

 

파괴!

 

자 다음에는

Player 스크립트로 가보자

 

외부에서 플레이어의 정보를 접근하고 싶어 할 텐데

그런 경우 여기 플레이어스크립트를 통해 진행될 것이다.

 

플레이어 컨트롤러를 get 해서 자동으로 추가하고,

매니저에 싱글톤 되어있는 플레이어 그거 나야!라고 해준다.

 

즉,

player 스크립트 자체를 ( this ) 를

캐릭터 매니저에

싱글톤 플레이어로 넣어놨다.

 

Player Controller는

get component로 가져왔기 때문에

 

넣지 않아도 자동으로 들어가게 된다.

과연 뭐가 들어갈까?

 

 

실행하게 되면

캐릭터매니저라는 싱글톤이 생겼고,

아까 none였던 곳에

player이 들어간 모습이다.

 

 

자 정리정리!

 

Player.cs

안에

 CharacterManager.Instance.Player = this;

 

이 식으로 대문자 Instance에 접근!

 

 

진입 후,

우리는 이 씬에 없었기 때문에

조건문 안으로 들어가서

오브젝트 하나 만들고, get으로 갖다 붙이고

 

그래서 지금 이 스크립트가 붙은 상황!

 

PlayerController을 이제 보자!

 

using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class PlayerController : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed;
    private Vector2 curMovementInput; // 현재 위치값 넣는 변수
    public float jumptForce; // 점프
    public LayerMask groundLayerMask;

    [Header("Look")]
    public Transform cameraContainer;
    public float minXLook;
    public float maxXLook;
    private float camCurXRot;
    public float lookSensitivity;

    private Vector2 mouseDelta;

    [HideInInspector]
    public bool canLook = true;

    private Rigidbody rigidbody;

    private void Awake()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    void Start()
    {
        //마우스 위치안보이게 하기
        Cursor.lockState = CursorLockMode.Locked;
    }

    private void FixedUpdate()
    {
        Move();
    }

    private void LateUpdate()
    {
        if (canLook)
        {
            CameraLook();
        }
    }

    public void OnLookInput(InputAction.CallbackContext context)
    {
        mouseDelta = context.ReadValue<Vector2>();
    }

    public void OnMoveInput(InputAction.CallbackContext context)
    {
        if (context.phase == InputActionPhase.Performed)
        {
            curMovementInput = context.ReadValue<Vector2>();
        }
        else if (context.phase == InputActionPhase.Canceled)
        {
            curMovementInput = Vector2.zero;
        }
    }

    public void OnJumpInput(InputAction.CallbackContext context)
    {
        if (context.phase == InputActionPhase.Started && IsGrounded())
        {
            rigidbody.AddForce(Vector2.up * jumptForce, ForceMode.Impulse);
        }
    }

    private void Move()
    {
        Vector3 dir = transform.forward * curMovementInput.y + transform.right * curMovementInput.x;
        dir *= moveSpeed;
        dir.y = rigidbody.velocity.y;

        rigidbody.velocity = dir;
    }

    void CameraLook()
    {
        camCurXRot += mouseDelta.y * lookSensitivity;
        camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook);
        cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0);

        transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0);
    }

    bool IsGrounded()
    {
        Ray[] rays = new Ray[4]
        {
            new Ray(transform.position + (transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (-transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (transform.right * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (-transform.right * 0.2f) +(transform.up * 0.01f), Vector3.down)
        };

        for (int i = 0; i < rays.Length; i++)
        {
            if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
            {
                return true;
            }
        }

        return false;
    }

    public void ToggleCursor(bool toggle)
    {
        Cursor.lockState = toggle ? CursorLockMode.None : CursorLockMode.Locked;
        canLook = !toggle;
    }
}

 

마우스 위치 안 보이게 하기

 

먼저 움직이게 해 보자

 

변수 입력!

 

다음 OnMoveInput를 보자

 

InputAction 변수를 가져와야 하는데

이때!

 

using UnityEngine.InputSystem;

using문 가져와야 한다!

CallbackContext.phase 속성은

Input Actions에 정의한 액션이

어떤 상태로 호출되었는지 나타낸다.

 

phase는 분기점 느낌으로

if문은

인풋액션의 분기점이. 퍼폼이라면

으로 해석할 수 있다.

 

그 이외에도 자주 쓰이는 친구들로는

Waiting

Canceled

Started

Disabled

가 있다.

 

Disabled 동작이 비활성화되어 입력을 받을 수 없다.
Waiting 동작이 활성화되어 입력을 기다리고 있다.
Started 입력 시스템이 동작과 상호 작용을 시작하는 입력을 받았다. ( 키가 눌렸다. )
Performed 동작과의 상호 작용이 완료되었다. ( 키를 누를때 행동을 끝낸다. )
Canceled 동작과의 상호 작용이 취소되었다. ( 키를 뗐다. )

 

즉,

Input System에 등록해 둔 키값의 정보가 context에 저장되고,

ReadValue로 불러올 수 있다.

 

또한

phase로 키값이 눌러졌을 때,

뗐을 때 등등의 상태에 따라

어떤 작업을 수행할지 정할 수 있다.

 

자!!

지금까지 한건 

값을 받아오는 역할을 했다.

 

진짜 움직이는 함수는 적지 않았다는 것!

 

진짜 움직이는 함수를 만들어보자

여기서 forward는 w와 s값이다.

앞으로 나아가는 방향이기 때문에!

 

어? 왜 앞뒤로 가는데 y값인가요?

 

여기서 y값을 가져오는 이유는

x의 위치를 바꿔주려면

y축이 계속 회전해주어야 한다.

 

가만히 있는 y축을 기준으로 한 바퀴 돌아야 x값이 바뀌기 때문에

x값을 만져주려면 y값을 만져줘야 하고

y 또한 x를 만져줘야

실질적으로 우리가 원하는 값을 

바꿀 수 있게 된다. 

 

 

다음에 좌우인 a와 d는 right로 가져오자!

 

다음에 y값은 한번 초기화해준다.

이 이유는 조금 뒤에 나오겠지만

 

점프를 하게 될 경우

y값만 움직여야 한다!

좌우로 점프하는 건.. 좀..ㅎㅋ

 

그렇기 때문에

그 값을 유지하기 위해서 적어둔다.

 

Velocity는

쉽게 말해서

일정한 속도로 이동할 수 있게

물리적 질량 및 관성을 다 배제시키는 친구이다

 

꼭 외워두자!!!!!

 

우리는 한번 움직이고 말 것이 아니기 때문에

이 함수를 update 문에 넣어주면 된다!

하지만 우리는 움직이는, 즉 물리적인 힘이 있기 때문에

FixedUpdate에 넣어주는 것이 좋다!

 

move 해주고 나서,

유니티로 돌아가서 event를 설정해 준다.

player를 넣고

PlayerController에

OnMoveInput

 

자 move가 끝났으니!

 

look도 해보자!

 

일단 변수들!

 

 

아까처럼 Look함수를 만들어보자

 

받아오는 것은 동일하다.

 

마우스 가져오는 것은 쭉 가져올 수 있기 때문에

조건문을 써줄 필요 없다.

 

카메라 변수도 써주고

 

 

 

여기서

y 값 받아오는 이유 또한

x를 움직이게 하기 위해서이다.

 

-camCurXPot가 음수인 이유는

마우스를 위로 올리면 음수가 된다

즉 

하늘을 쳐다보려고 하면 음수고

바닥을 쳐다보려고 하면 양수이다.

 

그래서!

음수를 넣어줘서

Mathf.Clamp는

최솟값보다 작아지면 최솟값을

최댓값보다 커지면 최댓값을 반환해 주는 친구이다.

 

 

다음에 LateUpdate에 넣어준다.

 

이 전에!

메인 카메라의 위치를

0,0,0으로 설정해 주자!

 

자, 다음에

Jump를 해보자

 

점프함수에

AddForce가 있는데 이 친구가 무엇이냐!

 

Rigidbody - ForceMode

 

Rigidbody 컴포넌트를 사용하여

게임 오브젝트에 물리적인 힘을 가할 때,

Add = 추가하다

Force = 힘을

AddForce를 이용하는데

 

이 ForceMode를 사용하여

다양한 힘 적용 방식을 설정할 수 있다.

 

종류에는

 

1. Force

힘을 지속적으로 적용

 

 Rigidbody.AddForce(Vector3 force, ForceMode.Force);

 

 

2. Acceleration

가속도를 적용

이전힘에 누적되어서 점진적으로 빠르게 움직인다.

엑셀느낌

 

Rigidbody.AddForce(Vector3 force, ForceMode.Acceleration);

 

 

3. Impulse

순간적인 힘을 적용

급 부스터 느낌으로 슠슠!!

그래서 점프에 많이 쓴다

 

Rigidbody.AddForce(Vector3 force, ForceMode.Impulse);

 

 

4. VelocityChange

변화하는 속도를 적용

물체의 현재 속도를 변경하면서 움직인다.

 

Rigidbody.AddForce(Vector3 force, ForceMode.VelocityChange);

 

설명이 조금 길었네.. 호호

 

다시 돌아와서!

점프함수를 적고 보니

점프가 자꾸 여러 번 되는 문제점 발생!!

 

그래서 우리는 바닥에 붙어있는가 없는가를 확인하고,

바닥에 없다면 더 이상 점프를 하지 못하게 해야 한다.

 

이때 사용하는 친구가

Ray라는 친구이다.

 

 

보통 우리는 Collider를 통해

내 주변에 뭐가 있다! 를 알 수 있었다.

 

Ray 가상의 광선이라고 생각해 주면 좋다.

직선 광선을 쏴서

이 광선에 오브젝트가 맞는다면

그 오브젝트가 무엇인지 가져올 수 있다.

 

Ray ray = new Ray(transform.position, transform.forward);

new Ray를 통해

시작점과, 방향을 뽑아준다.

여기서 forward는 

앞이기 때문에

z 축 방향으로 나아간다.

Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));

이 식은

카메를 기준으로 뽑아 낼 때 쓰는 식이다.

메인카메라를 기준으로 뽑아주며

 

카메라에서

x 0.5 y 0.5 해서

카메라 정가운데에서 뽑아낼 수 있게

벡터로 위치를 적어준다.

 

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);

 

이것은

메인카메라의 위치이긴 한데

마우스의 위치를 받아서

마우스를 클릭했을 때

그 위치에서 광선이 나오는 방식이다.

 

자 이제 광선을 발사하는 코드는 봤고

이제 광선에 물체가 맞았을 때

그 물체가 무엇인지 가져오기 위한 함수가 바로

 

Raycast이다.

 

맞은 게 있다면 true, 없다면 false

Ray, RaycastHit, MaxDistance, LayerMask 등의 옵션이 필요하다.

 

Ray => 광선

RaycastHit => 변수,

즉 Raycast로 인해 검출된 오브젝트를 담는 변수

MaxDistance => 얼마나 멀리 있는지, 최대거리 설정

LayerMask  => 필터링 느낌

광선은 길게 하면

다양한 물체가 맞을 수 있다.

Layer을 정해주면

그 Layer만 검출된다.

 

< 추가 >

RaycastHit 

Raycast에 의해 검출된 객체의 정보가 담겨있다.


RaycastHit.point => 레이캐스팅이 감지된 위치 

( 오브젝트 위치 )

RaycastHit.distance => Ray의 원점에서 충돌 지점까지의 거리

( 오브젝트 거리 )

RaycastHit.transform => 충돌 객체의 transform에 대한 참조

( 오브젝트 자체를 가져올 수 있다. 컴포넌트 접근 가능 )

 

 

자, 이제 우리는

 

Ray를 통해서

내가 하늘에 떠있나?? 를 확인해 볼 것이다.

광선에서 나, player는 걸러줘야 하기 때문에

변수를 만들어준다

 

 

이렇게 만들어주고 이따가 player을 빼주는 형식으로 진행할 것이다.

 

ray 작성은

위에서도 말했지만,

시작점과, 방향을 적어주어야 한다.

 

방향은 밑으로!

 

 

광선 개수만큼 for문을 돌리고

ray배열의 i번째에서

0.1f 길이만큼 쏠 것이고 ( 굉장히 짧음 )

LayerMask인 애들만 검출!

 

 

자 이제 유니티로 가서

 

LayerMask는 

EveryThing을 눌러주고

player만 빼주면 된다.

 

이렇게 해주면 끝!