ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 내일배움캠프 28일차 TIL - 3D 게임 기초 개발 1
    TIL/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

    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 생성

    1. Material을 생성하고 이름을 Skybox로 변경
    2. 인스펙터 창에서 Shader를 Skybox - Proceduaral로 변경
    3. Window - Rendering - Lighting 창을 열어 Environment탭의 Skybox Material에 만든 Skybox 넣어주기
    4. Skybox 인스펙터 창에서 설정값 주기
      • Sky Tint : 하늘 색
      • Ground : 수평선과 비슷한 지면 색
      • Exposure : 노출값 - 높을수록 밝아진다.

    1. Material 생성
    2. Shader 변경
    3. Lighting 창 열기
    3. Environment 탭에서 Skybox Material에 넣어주기
    Skybox 설정값

     

    플레이어 준비

    Tool handle이 이상한 곳에 있다면 아래 위치에서 변경할 수 있다.

    Center → Pivot 변경

     

    플레이어 생성

    1. 빈 오브젝트 Player 생성
    2. 자식으로 빈 오브젝트 CameraContainer 만들고 메인 카메라 붙여주기
    3. Capsule Colider 컴포넌트 붙여주고 크기 조절
    4. Rigidbody 컴포넌트 붙여주고 Constraints에서 모든 축 회전 고정

     

    Input System

    1. 패키지 매니저에서 Input System 추가
    2. Input Action으로 PlayerInput 만들고 설정
    3. Player에 PlayerInput 컴포넌트 붙여주기 - Behavior를 Invoke Unity Events로 변경

    2. PlayerInput
    3. Behaviour 변경
    3. Invoke Unity Event 방식으로 등록할 이벤트가 생겼다.

     

    플레이어 레이어

    Player 레이어를 만들고 Player 오브젝트에 적용시켜준다.

    Player

     

    마우스 커서 고정

    • Cursor.lockState = CursorLockMode.Locked; // 커서를 화면 가운데에 고정
    • Cursor.lockState = CursorLockMode.Confined; // 커서를 게임창 안에 고정 
    • Cursor.lockState = CursorLockMode.None; // 커서를 고정하지 않음

     

    플레이어 이동 구현

    1. CharacterManager, Player, PlayerController 스크립트 생성
    2. 싱글톤 형태인 Character Manager 오브젝트 추가하고 CharacterManager 스크립트 컴포넌트 추가
    3. Player 오브젝트에 Player, PlayerController 스크립트 컴포넌트 추가
    4. Player 오브젝트의 PlayerController 스크립트 컴포넌트 변수에 MoveSpeed 값 할당
    5. Player 오브젝트의 Player Input - Events - Player - Move 이벤트에 PlayerController.OnMove 메서드 등록

    4. Move Speed 값 할당
    5. 이벤트 등록

     

    카메라 움직임 추가

    1. PlayerController 스크립트에 카메라 기능 추가 작성
    2. PlayerController 스크립트 컴포넌트에 변수값 할당
    3. Player 오브젝트의 Player Input - Events - Player - Look에 PlayerController.OnLook 메서드 등록

    2. PlayerController에 값 할당
    3. 이벤트에 OnLook 메서드 등록

     

    카메라 위치 조정

    카메라가 플레이어를 기준으로 너무 크게 도는 느낌이 강하므로 메인카메라 위치를 (0, 0, 0)으로 조정한다.

    Position을 (0, 0, 0)

     

    점프 구현

    1. PlayerController 스크립트에 점프 기능 추가 작성
    2. PlayerController 스크립트 컴포넌트의 JumpPower 변수값 할당
    3. Player 오브젝트의 Player Input - Events - Player - Jump에 PlayerController.OnJump 메서드 등록

    2. JumpPower 값 할당
    3. 이벤트에 OnJump 메서드 등록

     

    그라운드 체크

    위의 점프만 구현할 경우 무한 점프가 가능하다. 그래서 땅에 닿아있는지 체크해서 닿아있을 때만 점프가 가능하게 설정할 것이다. 그러기 위해서는 Raycast가 필요하다.

    1.  PlayerController 클래스에 그라운드 체크 기능 추가 작성 (Raycast 활용)
    2. Jump 메서드 조건문에 IsGrounded 메서드 반환값 추가
    3. GroundLayerMask에 플레이어 제외하고 모두 체크 

    1. Raycast 방향
    3. Ground Layer Mask 설정

     

     

     

    코드

    더보기

    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강 밖에 듣지 못해서 주말까지 더 힘내야 할 거 같다.