3D 프로젝트 생성하기
이번 강의에서 사용할 내용을 미리 확인해보자!
라이트 소스: 게임 또는 3D 랜더링에 광원을 추가하는 데 사용된다. 이것은 특정 위치 또는 방향에서 발생하는 빛을 나타낸다.
Light의 유형: 라이트는 여러 유형이 있다.
점 광원(Point Light): 특정 지점에서 모든 방향으로 빛을 발산하는 라이트.
방향성 라이트(Directional Light): 특정 방향에서 모든 객체를 비추는 라이트. 이 라이트는 거리에 관계없이 동일한 강도로 모든 객체를 비춘다.
스포트라이트(Spot Light): 특정 방향으로 원뿔형의 빛을 발산하는 라이트.
영역 라이트(Area Light): 특정 영역에서 발생하고 그 주변에 빛을 발산하는 라이트 (ex. 실내 조명).
속성: 각 라이트에는 여러 속성이 있다. 이러한 속성에는 위치, 방향, 강도(intensity), 색상(color), 범위(range), 각도(angle) 등이 포함된다.
그림자: 라이트는 그림자를 생성할 수 있다. 라이트와 객체 사이의 관계에 따라 그림자는 라이트가 부딪히는 객체 뒤에 생성된다.
성능: 라이트는 렌더링 성능에 큰 영향을 미친다. 많은 라이트를 사용하면 특히 동적 그림자가 포함된 경우 렌더링 성능에 부정적인 영향을 미칠 수 있다. 따라서 최적화는 중요한 고려사항항이다.
빛 반사 및 산란: 라이트는 표면에 부딪히고 반사되거나 다른 방향으로 산란되어 재질과 표면의 실제성을 나타낸다. 이러한 효과는 물리 기반 렌더링(PBR)에서 중요한 요소이다.
게임 세계의 배경을 둘러싸는 환경 매핑 기술(3D 버전의 카메라 백그라운드 같은 느낌인듯). 큐브 맵(Cube Map)과 구체형 스카이박스(Sphere Map) 등이 있으며, 주로 다음과 같은 특징을 지닌다.
Rigidbody 컴포넌트를 사용하여 게임 오브젝트에 물리적인 힘을 가할 때, 이 ForceMode
를 사용하여 다양한 힘 적용 방식을 설정할 수 있다.
주요한 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);
이러한 ForceMode
를 적절히 활용하여 게임 오브젝트에 원하는 물리적인 움직임과 효과를 부여할 수 있다.
이번에는 강의에서 준비된 에셋을 사용해서 생성을 해보자.
현재 하이어라키를 확인해보면 Main Camera외에도 Directional Light라는 오브젝트가 있는 것을 확인할 수 있다.
확인을 해보면 Light라는 컴포넌트가 붙어있는 것을 확인할 수 있다.
미리 받아놓은 지형 프리셋을 확인해보자
Assets 밑에 Materials 폴더를 생성하자.
그후, Material을 하나 생성한다.
Material은 오브젝트들의 재질을 나타낸다. 빛을 받아서 반사되는 색들을 어떻게 표현할지에 대한 정의가 담겨 있다.
위에서 만든 Material을 Skybox로 만들어 보자. 이름을 Skybox로 변경한다.
게임에서 맵을 뚫게 되면 아래로 떨어지면서 하늘이 보이는 경우가 발생하는 데, 이 하늘이 Skybox이다.
보통 일반적으로 사용되는 Skybox는 구체형의 Skybox와 큐브형의 Skybox가 주로 사용된다.
이제 Skybox의 Shader를 변경해보자.
우리는 Procedural을 사용해볼 것이다.
생성된 모습!
이제 Skybox의 프로퍼티들을 변경해보자.
프로버티 | 설명 |
---|---|
Sun | Unity가 스카이박스에 태양면을 생성할 때 사용하는 방식 |
Sun Size | 태양면의 크기 수정자. 값이 클수록 태양면이 더 크게 보인다. 값을 0으로 설정하면 태양이 사라짐. |
Sun Size Convergence | 태양 크기 수렴. 값이 작을수록 태양면이 더 크게 보인다. Sun을 High Quality로 설정했을 때만 활성화됨. |
Atmosphere Thickness | 대기의 밀도. 밀도가 높을수록 더 많은 광원을 흡수한다. |
Sky Tint | 하늘에 적용할 컬러. |
Ground | 지면(수평선 아래 영역)의 컬러. |
Exposure | 하늘의 노출을 조정한다. 이를 통해 스카이박스의 색조 값을 변경할 수 있다. 값이 클수록 스카이박스가 더 밝아진다. |
Sun Size를 변경해보자.
Sky Tint와 Ground를 변경해보자.
Sky Tint를 우리 지형과 어울리는 색으로 변경해보자.
Ground의 경우, 바다색을 따라서 변경해주자.
혹은 Unity 우측 하단에 있는 Autio Generate Lighting Off를 눌러도 된다.
혹은 단축키 Ctrl + 9를 누르면 된다.
누르면 아래와 같은 창이 뜨게 된다.
이제 Environment 탭에서 Skybox Material을 변경해주자.
값들을 좀 더 조정하여 색을 하늘색을 이쁘게 변경해보자.
빈 오브젝트를 생성하고 Player로 이름을 변경해주자.
그리고 Player의 아래에 Create Empty → CameraContainer로 이름을 변경해주자.
그리고 CameraContainer는 Player의 눈이 될 것이기 때문에 Y좌표값을 1.7로 변경해준다.
이런 느낌으로~!
그후, MainCamera를 CameraContainer 밑으로 옮겨준다. 카메라를 옮겨주고 Transform을 reset해주면 캐릭터의 시점이 완성된다!
Center 값을 0.85, Radius를 0.25, Height를 1.7로 변경해준다.
Mass(질량)을 20으로 변경해주고, 캐릭터가 회전하지 않게끔 Freeze Rotation을 적용한다.
Scripts 폴더를 생성, 그 밑에 Player 폴더를 생성한다. 그리고 그 밑에 PlayerController.cs를 생성한다.
이후 모두 추가가 완료되면, 플레이어의 Tag와 Layer를 모두 Player로 설정해준다.
Player Layer를 추가해서 플레이어의 Layer로 설정해주자.
완료된 모습!
Window → Package Manager에서 Input System을 Import한다. 이후, Input Actions라는 폴더를 생성하고 그 밑에 Create → Input Actions를 추가한다. 이름을 Player Controls로 변경한다.
Move Action
Attack Action
Jump, Inventory 모두 Action Type은 Button, Binding에서 Path를 각각 Space와 Tab 키로 변경한다.
Look Action의 경우 마우스의 Delta 값이 필요하기 때문에 아래와 같이 받아준다.
Binding은 Mouse에서 Delta를 찾아 설정하면 된다.
Interact의 경우도 위의 Attack이나 Jump, Inventory와 같이 Action Type은 Button, Binding에서 E키로 변경한다.
이후 설정이 모두 끝나면 Player에게 Player Input을 추가해준다. Actions에는 Player Controls를 넣어준다.
마지막으로 Behavior를 Invoke Unity Events로 변경해준다.
변경해주면 밑에 우리가 설정한 액션들에 대한 이벤트들을 설정할 수 있다.
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
public float moveSpeed;
private Vector2 curMovementInput;
public float jumpForce;
public LayerMask groundLayerMask;
[Header("Look")]
public Transform cameraContainer; // Player 밑에 생성한 cameraContainer.
public float minXLook;
public float maxXLook;
private float camCurXRot;
public float lookSensitivity; // 마우스의 민감도
private Vector2 mouseDelta;
// SerializedField와 반대로 public 상태인 것을 Inspector에서 숨기기 위한 용도
[HideInInspector]
public bool canLook = true;
private Rigidbody _rigidbody;
// 이 프로젝트에서는 플레이어가 혼자이기 때문에 편리를 위한 싱글톤화
public static PlayerController instance;
private void Awake()
{
instance = this;
_rigidbody = GetComponent<Rigidbody>();
}
void Start()
{
// FPS 게임처럼 게임 시작 시 마우스 커서 숨기기
Cursor.lockState = CursorLockMode.Locked;
}
// Update is called once per frame
void FixedUpdate()
{
// 물리적인 처리를 하기 위해 사용한다.
Move();
}
private void LateUpdate()
{
// LateUpdate는 모든 처리가 끝난 뒤에 동작하기 때문에 카메라 작업을 하기 위해 사용된다.
if(canLook)
{
CameraLook();
}
}
private void Move()
{
// 플레이어가 움직일 방향 정하기.
// forward와 right를 이용해서 방향을 지정한다.
Vector3 dir = transform.forward * curMovementInput.y + transform.right * curMovementInput.x;
// 플레이어 이동 속도
dir *= moveSpeed;
// Y 값을 없애는 코드. Y 값을 없애고 현재 위치의 Y 값으로 변경한다.
dir.y = _rigidbody.velocity.y;
_rigidbody.velocity = dir;
}
// 카메라 마우스로 움직이기
void CameraLook()
{
// 보통 마우스를 위 아래로 움직였을 때, 고개를 끄덕이는 것처럼 동작한다.
// 이를 구현하기 위해서는 X, Y, Z축 중 X축을 기준으로 움직여야 고개를 끄덕이는 것처럼 움직일 수 있다.
camCurXRot += mouseDelta.y * lookSensitivity;
camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook); // X축의 가동범위 설정하기
// 카메라의 각도를 마우스가 움직인 만큼 변경해주기.
cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0);
// 플레이어의 각도 또한 변경해준다.
transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensitivity, 0);
}
// 이벤트와 연결하여 마우스의 델타값을 받아오기
public void OnLookInput(InputAction.CallbackContext context)
{
mouseDelta = context.ReadValue<Vector2>();
}
// 이벤트와 연결하여 키보드의 입력 받아오기
public void OnMoveInput(InputAction.CallbackContext context)
{
// InputAction을 통해 키보드의 입력 방식에 따라 구분해서 처리하기
// InputActionPhase : Started, Performed, Cancled의 프로버티가 있으며,
// Started는 처음 Input이 들어 왔을 때 한번만 처리를 한다. ex) 키보드 입력이 들어올 때
// Performed는 Input이 들어오는 동안 처리를 한다. ex) 키보드 키를 누르고 있는 동안
// Cancled는 Input이 끝날 때 한번만 처리를 한다. ex) 마우스 클릭에서 손을 땔 때
if(context.phase == InputActionPhase.Performed)
{
// 키보드 입력 시 움직이기
curMovementInput = context.ReadValue<Vector2>();
}
else if(context.phase == InputActionPhase.Canceled)
{
// 키보드에서 손을 땔 때 멈추기
curMovementInput = Vector2.zero;
}
}
}
Header를 추가했기 때문에 Inspector 창에서 값들을 확인할 수 있다.
Player의 Movement에 값을 변경해보자.
Speed : 5, Jump Force : 80, Ground Layer Mask는 Everything을 선택한 뒤 Player를 제거해준다(Player가 Player를 밟고 뛸 수는 없으니까)
다음으로는 Look을 변경해보자. Camera Container에는 이미 만든 CameraContainer를 넣어주고, 나머지는 아래와 같이 변경하면 된다.
이후, Player에 추가한 Player Input의 Event에서 Player Event들을 등록하자.
추가할 때, InputAction.CallbackContext
를 매개변수로 받는 함수들이 제일 위에 뜨는 것을 확인할 수 있다.
게임 화면을 클릭하면 마우스 커서가 사라지는 것 또한 확인할 수 있다.
// PlayerController.cs
...
public void OnJump(InputAction.CallbackContext context)
{
// Space 키를 누를 때 점프.
if(context.phase == InputActionPhase.Started)
{
_rigidbody.AddForce(Vector2.up * jumpForce, ForceMode.Impulse);
}
}
해서 같은 방식으로 이벤트도 등록해주고 실행해보자.
허공답보 하는 것을 막기 위해 코드를 수정하자.
// PlayerController.cs
...
public void OnJump(InputAction.CallbackContext context)
{
// Space 키를 누를 때 점프.
if(context.phase == InputActionPhase.Started)
{
if (IsGrounded())
_rigidbody.AddForce(Vector2.up * jumpForce, ForceMode.Impulse);
}
}
private bool IsGrounded()
{
// 땅을 밟고 있는지 여부를 확인하기 위한 Raycast
// 캐릭터의 앞뒤좌우에서 Ray를 바닥으로 쏴서 바닥과 충돌 여부를 감지한다.
Ray[] rays = new Ray[4]
{
new Ray(transform.position + (transform.forward * 0.2f) + (Vector3.up * 0.01f), Vector3.down),
new Ray(transform.position + (-transform.forward * 0.2f) + (Vector3.up * 0.01f), Vector3.down),
new Ray(transform.position + (transform.right * 0.2f) + (Vector3.up * 0.01f), Vector3.down),
new Ray(transform.position + (-transform.right * 0.2f) + (Vector3.up * 0.01f), Vector3.down)
};
for(int i = 0; i < rays.Length; i++)
{
// rays 중 하나라도 땅에 닿았으면 true
if (Physics.Raycast(rays[i], 0.1f, groundLayerMask)) return true;
}
return false;
}
// Gizmos : 시각적으로 범위를 체크할 수 있게 해주는 그래픽스.
// OnDrawGizmons() : 프로그래머가 임의로 Gizmos를 그리고 싶을 때 사용하는 함수.
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawRay(transform.position + (transform.forward * 0.2f), Vector3.down);
Gizmos.DrawRay(transform.position + (-transform.forward * 0.2f), Vector3.down);
Gizmos.DrawRay(transform.position + (transform.right * 0.2f), Vector3.down);
Gizmos.DrawRay(transform.position + (-transform.right * 0.2f), Vector3.down);
}
해당 코드는 캐릭터한테서 Raycast를 발사하여 바닥과 충돌 여부를 확인하여 return 한다.
Ray 배열은 캐릭터의 앞뒤좌우로 Raycast를 쏘기 위한 배열이다.
// 플레이어의 방향에서 바닥을 향해 Raycast를 쏜다.
new Ray(transform.position + (transform.forward * 0.2f), Vector3.down);
// 위의 코드의 경우 실제 플레이어보다 아래에서 Ray가 나가기 때문에
// Raycast의 발사 지점을 살짝 위로 올리는 코드.
new Ray(transform.position + (transform.forward * 0.2f) + (Vector3.up * 0.01f), Vector3.down);
캐릭터를 위에서 봤을 때, 앞뒤좌우로 Raycast를 바닥을 향해 쏴서 충돌 감지를 한다.
if문에서는 Ray배열 중 하나라도 Raycast의 0.1f만큼의 범위 내에 땅이 있을 경우 땅과 충돌했다 판단, true를 return해준다.
Gizmos는 시각적으로 범위를 체크할 수 있도록 해주는 그래픽스이다.
위와 같이 Scene 뷰에서 표시되는 선들이 Gizmos로, 위의 경우 카메라와 Light에 달려있는 BuiltIn Gizmos다.
OnDrawGizmos()를 통해 프로그래머가 임의로 Gizmos를 만들어 런타임에서 Gizmos가 Scene 뷰에서 출력이 되도록 해준다.
점프를 뛸 때 Scene 뷰에서 빨간 색 선이 나오는 것을 볼 수 있다.
시점 변경, 움직이기, 점프 모두 정상적으로 동작하는 모습!