어느덧 숙련 주차에 들어서게 되었다...
숙련? 저번 주차가 입문이었지만 난 아직도 입문인걸요...
2D도 다 못 뗐는데 3D를?! 🫠
3D 환경에서 작업을 하게 되면 추가되는 것 중 하나 하늘
하늘을 표현할 때 스카이박스를 다룬다.
게임 세계의 배경을 둘러싸는 환경 매핑 기술
6개의 텍스처로 구성된 큐브 맵(Cube Map) 또는 하나의 구체로 텍스처가 매핑된 구체형 스카이박스(Sphere Map)로 구성된다.
Rigidbody 컴포넌트를 사용하여 게임 오브젝트에 물리적인 힘을 가할 때, 이 ForceMode를 사용하여 다양한 힘 적용 방식을 설정할 수 있다.
원하는 물리적인 힘 부여 가능
Force: 힘을 지속적으로 적용한다.Rigidbody.AddForce(Vector3 force, ForceMode.Force);Acceleration: 가속도를 적용한다. 이전 힘의 누적에 따라서 점진적으로 더 빠르게 움직이게 된다.Rigidbody.AddForce(Vector3 force, ForceMode.Acceleration);Impulse: 순간적인 힘을 적용한다. 짧은 시간에 갑작스러운 움직임이 발생한다.Rigidbody.AddForce(Vector3 force, ForceMode.Impulse);VelocityChange: 변화하는 속도를 적용한다. 물체의 현재 속도를 변경하면서 움직인다.Rigidbody.AddForce(Vector3 force, ForceMode.VelocityChange);가상의 광선을 발사하여 어떤 물체가 있는지 판단하고 정보를 검출할 수 있다.
직선의 시작점(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); // 마우스
눈에 보이지 않는 광선(Ray)에 맞은 물체가 무엇인지 여부를 판단한 뒤 여러 가지 후처리를 하는 방식
Ray, RaycastHit, MaxDistance, LayerMask 등의 옵션
Raycast에 의해 검출된 객체의 정보가 담겨있다.
RaycastHit.point - 레이캐스팅이 감지된 위치
RaycastHit.distance - Ray의 원점에서 충돌 지점까지의 거리
RaycastHit.transform - 충돌 객체의 transform에 대한 참조
하기 전에 스카이박스 세팅
에 앞서 맵 깔아주기

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


Shader - Skybox - Procedural


Ctrl+S

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



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


Mass: 무게
Constraints - Freeze Rotation 체크: 회전 막기
플레이어 기본 세팅 완료

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를 조작할 수 있게 된다.
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가 없음
}
}





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;
}
}