스카이박스, ForceMode, Raycast, 플레이어 구현

유승아·2024년 5월 26일

내일배움캠프

목록 보기
50/69

어느덧 숙련 주차에 들어서게 되었다...
숙련? 저번 주차가 입문이었지만 난 아직도 입문인걸요...

2D도 다 못 뗐는데 3D를?! 🫠


3D 환경에서 작업을 하게 되면 추가되는 것 중 하나 하늘

하늘을 표현할 때 스카이박스를 다룬다.

1. 스카이박스

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

6개의 텍스처로 구성된 큐브 맵(Cube Map) 또는 하나의 구체로 텍스처가 매핑된 구체형 스카이박스(Sphere Map)로 구성된다.

  • 씬의 배경으로 사용되며, 게임 환경을 확장시키는데 활용된다.
  • 주로 하늘, 구름, 산 등의 자연적인 배경을 표현하는 데 사용된다.
  • 미리 만들어진 스카이박스를 사용하거나 직접 만들어서 적용할 수 있다.
  • 낮과 밤 등의 시간대나 특정 이벤트에 맞게 배경을 변화시킬 수 있다.
  • 성능에 영향을 미치므로 최적화에 주의해야 한다.

2. Rigidbody - ForceMode

Rigidbody 컴포넌트를 사용하여 게임 오브젝트에 물리적인 힘을 가할 때, 이 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);
    • 달리기

3. Raycast

가상의 광선을 발사하여 어떤 물체가 있는지 판단하고 정보를 검출할 수 있다.

Ray

직선의 시작점(origin)과 방향(direction) 정하기

Ray ray = new Ray(transform.position, transform.forward); // 오브젝트
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0)); // 카메라 중심
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 마우스

Raycast

눈에 보이지 않는 광선(Ray)에 맞은 물체가 무엇인지 여부를 판단한 뒤 여러 가지 후처리를 하는 방식
Ray, RaycastHit, MaxDistance, LayerMask 등의 옵션

RaycastHit

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

RaycastHit.point - 레이캐스팅이 감지된 위치
RaycastHit.distance - Ray의 원점에서 충돌 지점까지의 거리
RaycastHit.transform - 충돌 객체의 transform에 대한 참조


4. 플레이어 구현

하기 전에 스카이박스 세팅
에 앞서 맵 깔아주기

강의에서 제공해준 패키지에 있는 맵 세팅하기

1) 스카이박스 세팅하기

Shader - Skybox - Procedural

값이 변하는 것을 실시간으로 확인하기 위해서 Material 적용하기

Ctrl+S

Sky Tint: 하늘 색
Ground: 지평선 색
Exposure: 노출 값

2) 플레이어 만들기

카메라컨테이너 안에 메인카메라 넣기

실제로 물리 작용과 충돌 처리 해주기

Mass: 무게
Constraints - Freeze Rotation 체크: 회전 막기

플레이어 기본 세팅 완료

플레이 조작을 위한 Input 값 받기

Package Manager에서 Input System 설치

게임에 필요한 입력값 세팅 완료

컴포넌트 추가해 주고, Behavior - Invoke Unity Events 설정

레이어 추가 및 적용하기

필요한 스크립트 생성

Player에 스크립트 붙여주기
CharacterManager는 싱글톤으로 만들 것임

CharacterManager.cs

public class CharacterManager : MonoBehaviour
{
    private static CharacterManager _instance;
    public static CharacterManager Instance // 외부에서 접근할 것
    {
        get
        {
            if (_instance == null)
            {
                _instance = new GameObject("CharacterManager").AddComponent<CharacterManager>();
            }
            return _instance;
        }
    }

    public Player _player;
    public Player Player
    {
        get { return _player; }
        set { _player = value; }
    }

    private void Awake() // 싱글톤 처음 세팅
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject); // 씬이 넘어가도 파괴되지 않기
        }
        else
        {
            if(_instance != this)
            {
                Destroy(gameObject);
            }
        }
    }
}

Player.cs

public class Player : MonoBehaviour
{
    public PlayerController controller;

    private void Awake()
    {
        CharacterManager.Instance.Player = this;
        controller = GetComponent<PlayerController>();
    }
}

PlayerController.cs

public class PlayerController : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed;
    private Vector2 curMovementInput; 

    private Rigidbody _rigidbody;

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

    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked; 
    }

    void FixedUpdate()
    {
        Move();
    }

    void Move()
    {                   
        Vector3 dir = transform.forward * curMovementInput.y // 앞뒤, W S 는 curMovemen Vector2에서 y값에 해당
                    + transform.right * curMovementInput.x;  // 좌우
        dir *= moveSpeed; // 힘 곱해주기
        dir.y = _rigidbody.velocity.y; // y값 초기화 -> 점프를 했을 때만 위아래로 움직여야 함. 기존의 값을 유지시켜 주기 위해서 넣어줬음

        _rigidbody.velocity = dir; // velocity에 세팅된 방향 넣어주기
    }

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

context 변수를 통해서 값을 받을 수 있게 세팅
PlayerController - Move Speed 5 적용

여기까지 하면 Player를 조작할 수 있게 된다.

3) 카메라 회전하기 만들기

PlayerController.cs

public class PlayerController : MonoBehaviour
{
    [Header("Look")] // 캐릭터(카메라) 회전에 필요한 변수들
    public Transform cameraContainer; // 카메라컨테이너
    public float minXLook; // 회전 범위 최소값
    public float maxXLook; // 회전 범위 최대값
    private float camCurXRot; // 마우스의 델타값
    public float lookSensitivity; // 회전할 때 민감도
    private Vector2 mouseDelta; // 마우스 델타값
    
    private void LateUpdate()
	{
    	CameraLook();
	}
    
    void CameraLook()
	{
 	   camCurXRot += mouseDelta.y * lookSensitivity; // 돌려줄 값을 마우스 델타에서 y 값 뽑아오기, 민감도 곱하기
	   camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook); // camCurXRot 값이 최소값 최대값 벗어나지 않도록 하기
	   cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0); // 카메라컨테이너에 로컬좌표 돌려주기

	   transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0); // 위아래는 캐릭터 각도 돌려주기
	}
    
    public void OnLook(InputAction.CallbackContext context)
    {
        mouseDelta = context.ReadValue<Vector2>(); // 마우스는 값이 계속 유지되고 있기 때문에 phase가 없음
    }
    
}

4) 점프 기능 만들기

점프 상태일 때 또 점프되는 현상 수정하기

PlayerController.cs

public void OnJump(InputAction.CallbackContext context)
{
    if (context.phase == InputActionPhase.Started && IsGrounded()) // IsGrounded()값이 false면 점프 X
    {
        _rigidbody.AddForce(Vector2.up * jumpPower, ForceMode.Impulse); // 점프키 눌렀을 때 힘을 확! 주기
    }
}

bool IsGrounded()
{
    Ray[] rays = new Ray[4]
    {                                                          // 바로 아래로 쏘면 Ground를 인식하지 못할 수 있으니 살짝 올려주기
        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)
    };

    // Raycast를 통해 부딪힌 물체의 정보 가져오기, 값은 bool 값을 반환
    for(int i = 0; i < rays.Length; i++)
    {                            // Ray 길이, groundLayerMask에 해당되는 애들만 검출
        if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
        {
            return true;
        }
    }

    return false;
}


PlayerController.cs 정리

public class PlayerController : MonoBehaviour
{
    // 인스펙터 창에 구분하기 편하게 헤더 만들기
    [Header("Movement")] // 이동에 필요한 변수들
    public float moveSpeed; // 속도
    public float jumpPower; // 점프력
    private Vector2 curMovementInput; // InputAction에서 받아올 값
    public LayerMask groundLayerMask; // Ray가 플레이어를 감지하지 않도록 레이어 구분하기

    [Header("Look")] // 캐릭터(카메라) 회전에 필요한 변수들
    public Transform cameraContainer; // 카메라컨테이너
    public float minXLook; // 회전 범위 최소값
    public float maxXLook; // 회전 범위 최대값
    private float camCurXRot; // 마우스의 델타값
    public float lookSensitivity; // 회전할 때 민감도
    private Vector2 mouseDelta; // 마우스 델타값

    private Rigidbody _rigidbody;

    private void Awake()
    {
        _rigidbody = GetComponent<Rigidbody>(); // Rigidbody 가져오기
    }

    void Start()
    {
        // 마우스 모양 숨기는 방법
        Cursor.lockState = CursorLockMode.Locked; // 마우스 상태를 락 시킬 것이냐, 상태 정해주는 것
    }

    void FixedUpdate()
    {
        // 물리 연산을 하는 곳(rigidbody)에는 FixedUpdate에서 호출해 주는 것이 좋음
        Move();
    }

    private void LateUpdate()
    {
        CameraLook();
    }

    // 실제로 캐릭터 이동시켜주기
    void Move()
    {
        // 방향 추출(방향값 정해주기)                          
        Vector3 dir = transform.forward * curMovementInput.y // 앞뒤, W S 는 curMovemen Vector2에서 y값에 해당
                    + transform.right * curMovementInput.x;  // 좌우
        dir *= moveSpeed; // 힘 곱해주기
        dir.y = _rigidbody.velocity.y; // y값 초기화 -> 점프를 했을 때만 위아래로 움직여야 함, 기존의 값을 유지시켜 주기 위해서 넣어줬음

        _rigidbody.velocity = dir; // velocity에 세팅된 방향 넣어주기
    }

    // 실제로 카메라 돌리기
    void CameraLook()
    {
        camCurXRot += mouseDelta.y * lookSensitivity; // 돌려줄 값을 마우스 델타에서 y 값 뽑아오기, 민감도 곱하기
        camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook); // camCurXRot 값이 최소값 최대값 벗어나지 않도록 하기
        cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0); // 카메라컨테이너에 로컬좌표 돌려주기

        transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0); // 위아래는 캐릭터 각도 돌려주기
    }

    // 이벤트 등록해줄 함수
    public void OnMove(InputAction.CallbackContext context) // context 현재 상태
    {
        // Started 눌렸을 때, Performed 눌리고 나서 내부 로직이 실행 완료 됐을 때, Canceled 취소됐을 때 등
        if (context.phase == InputActionPhase.Performed) // 키가 계속 눌린 뒤에도 값을 계속 받기 위해 Performed 사용
        {
            // 값을 받아오기
            curMovementInput = context.ReadValue<Vector2>();
        }
        else if(context.phase == InputActionPhase.Canceled)
        {
            curMovementInput = Vector2.zero; // 값 0, 0
        }
    }

    public void OnLook(InputAction.CallbackContext context)
    {
        mouseDelta = context.ReadValue<Vector2>(); // 마우스는 값이 계속 유지되고 있기 때문에 phase가 없음
    }

    public void OnJump(InputAction.CallbackContext context)
    {
        if (context.phase == InputActionPhase.Started && IsGrounded()) // IsGrounded()값이 false면 점프 X
        {
            _rigidbody.AddForce(Vector2.up * jumpPower, ForceMode.Impulse); // 점프키 눌렀을 때 힘을 확! 주기
        }
    }

    // Ground에 붙어있는지 확인하기
    bool IsGrounded()
    {
        Ray[] rays = new Ray[4]
        {                                                          // 바로 아래로 쏘면 Ground를 인식하지 못할 수 있으니 살짝 올려주기
            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)
        };

        // Raycast를 통해 부딪힌 물체의 정보 가져오기, 값은 bool 값을 반환
        for(int i = 0; i < rays.Length; i++)
        {                            // Ray 길이, groundLayerMask에 해당되는 애들만 검출
            if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
            {
                return true;
            }
        }

        return false;
    }
}

0개의 댓글