-
내일배움캠프 28일차 TIL - 3D 게임 기초 개발 1TIL/Unity 2024. 5. 24. 21:28
[한 줄 요약]
유니티 숙련 주차 개인 강의학습을 시작하고, 3D 게임 개발 기초 단계 중 플레이어 준비를 실습해본다.
[학습 내용]
유니티 숙련 주차 강의
Rigidbody ForceMode
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);
Raycast
눈에 보이지 않는 광선(Ray)에 맞은 객체가 무엇인지 판단 후 여러 후처리를 하는 방식이다.
- 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); // 마우스 중심 - RaycastHit : Raycast에 의해 검출된 객체의 정보가 담겨있다.
RaycastHit.point // Raycasting이 감지된 위치
RaycastHit.distance // Ray의 원점에서 충돌지점까지의 거리
RaycastHit.tranform // 충돌한 객체의 tranform에 대한 참조 - 이 외에도 MaxDistance, LayerMask 등의 옵션이 있다.
Input System의 Behaviour
- SendMessage : "On + (ActionName)"의 이름을 가진 메서드를 찾아서 호출하는 방식
- Invoke Event : 인스펙터 상에서 Action에 메서드를 설정하고 키 입력이 들어왔을 때 호출하는 방식
- Invoke C Sharp Events : C# 스크립트에서 Invoke Event 과정을 수행하는 방식
키를 입력받고 실행 전, 키를 입력받고 실행 완료, 키 입력 해제 등의 구체적인 상황에 따라 별도의 메서드를 등록할 수 있다.
지형 준비
Skybox 생성
- Material을 생성하고 이름을 Skybox로 변경
- 인스펙터 창에서 Shader를 Skybox - Proceduaral로 변경
- Window - Rendering - Lighting 창을 열어 Environment탭의 Skybox Material에 만든 Skybox 넣어주기
- Skybox 인스펙터 창에서 설정값 주기
- Sky Tint : 하늘 색
- Ground : 수평선과 비슷한 지면 색
- Exposure : 노출값 - 높을수록 밝아진다.
플레이어 준비
Tool handle이 이상한 곳에 있다면 아래 위치에서 변경할 수 있다.
플레이어 생성
- 빈 오브젝트 Player 생성
- 자식으로 빈 오브젝트 CameraContainer 만들고 메인 카메라 붙여주기
- Capsule Colider 컴포넌트 붙여주고 크기 조절
- Rigidbody 컴포넌트 붙여주고 Constraints에서 모든 축 회전 고정
Input System
- 패키지 매니저에서 Input System 추가
- Input Action으로 PlayerInput 만들고 설정
- Player에 PlayerInput 컴포넌트 붙여주기 - Behavior를 Invoke Unity Events로 변경
플레이어 레이어
Player 레이어를 만들고 Player 오브젝트에 적용시켜준다.
마우스 커서 고정
- Cursor.lockState = CursorLockMode.Locked; // 커서를 화면 가운데에 고정
- Cursor.lockState = CursorLockMode.Confined; // 커서를 게임창 안에 고정
- Cursor.lockState = CursorLockMode.None; // 커서를 고정하지 않음
플레이어 이동 구현
- CharacterManager, Player, PlayerController 스크립트 생성
- 싱글톤 형태인 Character Manager 오브젝트 추가하고 CharacterManager 스크립트 컴포넌트 추가
- Player 오브젝트에 Player, PlayerController 스크립트 컴포넌트 추가
- Player 오브젝트의 PlayerController 스크립트 컴포넌트 변수에 MoveSpeed 값 할당
- Player 오브젝트의 Player Input - Events - Player - Move 이벤트에 PlayerController.OnMove 메서드 등록
카메라 움직임 추가
- PlayerController 스크립트에 카메라 기능 추가 작성
- PlayerController 스크립트 컴포넌트에 변수값 할당
- Player 오브젝트의 Player Input - Events - Player - Look에 PlayerController.OnLook 메서드 등록
카메라 위치 조정
카메라가 플레이어를 기준으로 너무 크게 도는 느낌이 강하므로 메인카메라 위치를 (0, 0, 0)으로 조정한다.
점프 구현
- PlayerController 스크립트에 점프 기능 추가 작성
- PlayerController 스크립트 컴포넌트의 JumpPower 변수값 할당
- Player 오브젝트의 Player Input - Events - Player - Jump에 PlayerController.OnJump 메서드 등록
그라운드 체크
위의 점프만 구현할 경우 무한 점프가 가능하다. 그래서 땅에 닿아있는지 체크해서 닿아있을 때만 점프가 가능하게 설정할 것이다. 그러기 위해서는 Raycast가 필요하다.
- PlayerController 클래스에 그라운드 체크 기능 추가 작성 (Raycast 활용)
- Jump 메서드 조건문에 IsGrounded 메서드 반환값 추가
- GroundLayerMask에 플레이어 제외하고 모두 체크
코드
더보기CharacterManager.cs
using UnityEngine; public class CharacterManager : MonoBehaviour { private static CharacterManager _instance; public static CharacterManager Instance { get { if(_instance == null) { _instance = new GameObject("CharacterManager").AddComponent<CharacterManager>(); } return _instance; } } private 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
using UnityEngine; public class Player : MonoBehaviour { public PlayerController Controller; private void Awake() { CharacterManager.Instance.Player = this; Controller = GetComponent<PlayerController>(); } }
PlayerController.cs
using UnityEngine; using UnityEngine.InputSystem; public class PlayerController : MonoBehaviour { [Header("Movement")] public float moveSpeed; public float jumpPower; private Vector2 curMovementInput; public LayerMask groundLayerMask; [Header("Look")] public Transform cameraContainer; public float minXLook; public float maxXLook; private float camCurXRot; public float lookSensivity; private Vector2 mouseDelta; private Rigidbody _rigidbody; private void Awake() { _rigidbody = GetComponent<Rigidbody>(); } private void Start() { Cursor.lockState = CursorLockMode.Locked; } private void FixedUpdate() { Move(); } private void LateUpdate() { CameraLook(); } private void Move() { Vector3 dir = transform.forward * curMovementInput.y + transform.right * curMovementInput.x; dir *= moveSpeed; dir.y = _rigidbody.velocity.y; // 점프할 때 상하 움직임도 반영 _rigidbody.velocity = dir; } private void CameraLook() { camCurXRot += mouseDelta.y * lookSensivity; camCurXRot = Mathf.Clamp(camCurXRot, minXLook, maxXLook); cameraContainer.localEulerAngles = new Vector3(-camCurXRot, 0, 0); transform.eulerAngles += new Vector3(0, mouseDelta.x * lookSensivity); } public void OnMove(InputAction.CallbackContext context) { // 키가 눌렸을 때 if (context.phase == InputActionPhase.Performed) { curMovementInput = context.ReadValue<Vector2>(); } // 키가 떼어졌을 때 else if (context.phase == InputActionPhase.Canceled) { curMovementInput = Vector2.zero; } } public void OnLook(InputAction.CallbackContext context) { mouseDelta = context.ReadValue<Vector2>(); } public void OnJump(InputAction.CallbackContext context) { if (context.phase == InputActionPhase.Started && IsGrounded()) { _rigidbody.AddForce(Vector2.up * jumpPower, ForceMode.Impulse); } } private bool IsGrounded() { // 다리 4개라고 생각 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; } }
수준별 특강 - 객체지향
SOLID 원칙 적용 예시
단일 책임 원칙
- 상황 : 이동, 공격, 점프 기능이 있는 플레이어 캐릭터를 만들고 싶다.
- 안 좋은 예시 : Player 클래스가 이동, 공격, 점프 모두 직접 실시
- 원칙을 지킨 예시 : Player 클래스가 Movement 클래스에게 이동, Attack 클래스에게 공격, Jump 클래스에게 점프를 맡겨놓는다. Player 클래스는 각각을 조합하는 역할
개방 폐쇄 원칙
- 상황 : 특색이 있는 적 몬스터들을 만들고 싶다. 그런데 몬스터의 종류는 이후에도 계속 추가된다.
- 안 좋은 예시 : Enemy 클래스 안에서 Switch 문으로 일일이 각 유형의 Enemy들의 특성을 코드로 정해준다. 나중에 오브젝트가 늘어나면 기존의 코드를 수정해야 하고, Enemy 클래스 코드가 굉장히 길어진다.
- 원칙을 지킨 예시 : Enemy를 추상클래스로 구현하고 공통된 특성을 정의한다. 그리고 새로운 클래스를 만들어서 Enemy를 상속시키고 필요한 특성을 오버라이드 해준다. 새로운 기능이 생겨도 기존의 코드를 수정하지 않고 새로운 클래스를 만들면 된다.
리스코프 치환 원칙
- 상황 : 모든 새는 점프 포인트에서 비행해야하고, 새의 종류는 계속 추가된다. 그런데 펭귄, 타조 같이 날지 못하는 새가 있다.
- 안 좋은 예시 : 부모클래스인 Bird 클래스에서 Fly 메서드를 구현하고, Penguin 클래스는 Fly 메서드를 오버라이드해서 나는 기능을 지워버린다. 그래서 Fly 메서드는 존재하지만 아무 기능을 하지 않게 둔다.
- 원칙을 지킨 예시 : Bird 클래스를 FlightlessBird와 FlyableBird 클래스로 나눠서 FlyableBird 클래스에만 Fly 메서드를 구현한다. Penguin은 FlyableBird를 상속받는다.
인터페이스를 사용하면 더 편리하다. IFlyable 인터페이스를 만들고 그 안에 Fly 메서드를 구현하여 FlyableBird 클래스에 이 인터페이스를 붙여주면 된다.
인터페이스 분리 원칙
- 요약 : 단일 책임 원칙의 인터페이스 버전
의존선 역전 원칙
- 상황 : 스위치로 문을 여닫게 만들어야 한다. 뿐만 아니라 불을 끄고 켜거나 충전을 진행시키거나 멈추거나 해야 한다.
- 안 좋은 예시 : Door 클래스의 Open, Close 메서드를 실행하기 위해 Switch 클래스에서 ToggleDoor 메서드로 호출한다. 또, Light 클래스에서 On, Off 메서드를 호출하기 위해 ToggleLight 메서드를 다시 구현해서 호출한다. Switch가 Door와 Light에 대해서 할 일을 알고 있어야 한다.
- 원칙을 지킨 예시 : Switch 클래스에서는 ISwitchable 인터페이스를 가진 클래스만 사용하겠다고 선언하고, Toggle 메서드 하나만 구현한다. 이 메서드는 상대의 Active, Deactive 메서드만 호출한다. 그러면 Door, Light, Charger 클래스는 여기에 맞춰서, ISwitchable 인터페이스를 상속하고 Active와 Deactive 안에 자기 기능을 구현해서 Switch 클래스에게 가져다줘야 한다.
[결과물]
[회고]
숙련주차가 시작되고 다시 개인 강의 학습으로 돌아왔다. 이전보다 강의가 더 많아졌는데 마음은 좀 더 편한 거 같다. 오늘 강의를 3강 밖에 듣지 못해서 주말까지 더 힘내야 할 거 같다.
'TIL > Unity' 카테고리의 다른 글
내일배움캠프 30일차 TIL - 3D 게임 기초 개발 3 (0) 2024.05.28 내일배움캠프 29일차 TIL - 3D 게임 기초 개발 2 (0) 2024.05.27 내일배움캠프 27일차 TIL - 유니티 입문 팀 프로젝트 6 (0) 2024.05.23 내일배움캠프 26일차 TIL - 유니티 입문 팀 프로젝트 5 (0) 2024.05.22 내일배움캠프 25일차 TIL - 유니티 입문 팀 프로젝트 4 (0) 2024.05.21 - Force : 힘을 지속적으로 적용한다.