2025.03.05 (수)

윤혜진·2025년 3월 5일

📍오늘의 학습 키워드

  • unity 게임 개발 숙련 1주차 (1-7 ~ 1-10)

    • Light
    • 인터페이스를 사용하는 이유
    • ScriptableObject
    • 폰트 파일을 폰트 에셋으로 만드는 방법

📍학습 내용

  • Light

    • Light란?
      • 게임 또는 3D 랜더링에 광원을 추가하는 데 사용된다.
      • 이것은 특정 위치 또는 방향에서 발생하는 빛을 나타낸다.
      • 참고하면 좋은 Unity Documentation 👉광원 - Unity 매뉴얼
    • Light의 유형
      • 점 광원(Point Light) : 모든 방향으로 균등하게 빛을 발산하는 광원
      • 방향성 라이트(Directional Light) : 무한히 멀리 위치하여 한 방향으로만 빛을 발산하는 광원
      • 스포트라이트(Spot Light) : 씬의 한 점에 위치하여 원뿔 모양으로 빛을 발산하는 광원
      • 면 광원(Area Light) : 표면 영역 전체에 걸쳐 균등하게 모든 방향으로 빛을 방출, 사각형의 한쪽 면에서만 빛을 방출하는 광원
    • Light의 속성
      • 위치, 방향, 강도(intensity), 색상(color), 범위(range), 각도(angle) 등이 있다.
    • 그림자
      • 라이트와 객체 사이의 관계에 따라 그림자는 라이트가 부딪히는 객체 뒤에 생성.
    • 성능
      • 라이트는 렌더링 성능에 큰 영향을 미친다. 특히 그림자가 포함된 경우 렌더링 성능에 부정적인 영향을 미칠 수 있어 최적화에 신경 써야 함.
    • Lighting Intensity Multiplier : 실제 환경의 빛을 조절한다.
    • Reflecting Intensity Multiplier : 실제 오브젝트에 반사되는 정도를 조절한다.
  • 인터페이스를 사용하는 이유

    • 인터페이스를 사용하지 않았다면 Card , Cash , QR 등 상대방의 결제 수단에 맞는 클래스를 받아와야 하고, 또 새로운 결제 수단이 생기면 그 클래스까지 추가해주어야 함. (=수정이 오래 걸림)
    • 그러나 인터페이스를 써서 이 모든 결제수단을 Payment 로 묶는다면 훨씬 간편하게 결제 메서드를 구현할 수 있다.
      public interface Payment
      {
      	public void Pay();
      }
      
      public class Card : Payment
      {
      	public void Pay(){}
      }
      
      public class Cash : Payment
      {
      	public void Pay(){}
      }
      
      public class QR : Payment
      {
      	public void Pay(){}
      }
      
      //인터페이스를 사용하지 않았을 경우
      public class Store
      {
      	Card card;
      	Cash cash;
      	QR qr;
      }
      
      //인터페이스를 사용했을 경우
      public class Store
      {
      	Payment payment;
      	payment.Pay();
      }
  • ScriptableObject

    • ScriptableObject란?

      • ScriptableObject 는 데이터를 저장하고 관리하는 데 특화된 Unity의 클래스
      • 게임 데이터(아이템, 캐릭터 정보, 설정 값 등)를 저장하는 데 사용
      • 씬(Scene)과 독립적으로 존재하며, MonoBehaviour와 다르게, 씬(Scene)에 존재하지 않아도 된다.
    • ScriptableObject를 만드는 방법
      - ScriptableObject 을 상속받는 데이터 클래스를 만든다. (ItemData.cs)

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      public enum ItemType
      {
         Equipable,  //장착 아이템
         Consumable, //소비 아이템
         Resource    //자원 아이템  
      }
      
      public enum ConsumableType
      {
         Health, //체력 회복 아이템
         Hunger  //배고픔 회복 아이템
      }
      
      public class ItemDataConsumable
      {
         public ConsumableType type;     //무엇을 회복시키는 소비 아이템인지
         public float value;             //얼마만큼 회복시켜 주는지
      }
      
      //ScriptableObject를 만들 때 빠르게 만들 수 있도록 에셋 생성 메뉴창에 추가
      [CreateAssetMenu(fileName = "Item", menuName = "New Item")] 
      
      public class ItemData : ScriptableObject
      {
         [Header("Info")]
         public string displayName;      //아이템 이름
         public string description;      //아이템 정보
         public ItemType type;           //아이템 타입 (장착/소비/자원)
         public Sprite icon;             //아이템 이미지
         public GameObject dropPrefab;   //아이템 프리팹
      
         [Header("Stacking")]
         public bool stackable;                //여러개 가지고 있을 수 있는 아이템인지 체크하는 변수
         public int maxStackAmount;            //얼마나 많이 한꺼번에 가지고 있을 수 있는지
      
         [Header("Consumable")]
         public ItemDataConsumable[] consumables;
      }
    • Project > 마우스 오른쪽 버튼 클릭 > Create > New Item으로 ScriptableObject 생성 (지금은 New Item이지만, 스크립트에서 menuName 을 무엇으로 정해주었는지에 따라 이름이 바뀐다.)

    • 생성한 ScriptableObject 에 데이터를 넣어준다.

    • 생성한 ScriptableObject 는 다음과 같은 방식으로 활용할 수 있다.

    • ItemObject.cs :
      - 상호작용을 위한 ray 와 아이템 오브젝트가 만나면 실행시켜줄 메서드들이 들어가있다.

      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;
      
      public interface IInteractable  //상호작용 인터페이스
      {
         public string GetInteractPrompt();  //화면에 띄워줄 프롬포트 함수를 작성
         public void OnInteract();           //인터렉트 했을 때 어떤 효과를 발생 시킬 것인지
      }
      
      public class ItemObject : MonoBehaviour, IInteractable
      {
         public ItemData data;
      
         public string GetInteractPrompt()
         {
             string str = $"{data.displayName}\n{data.description}";
             return str;
         }
      
         public void OnInteract()
         {
             //Player 스크립트 먼저 수정
             CharacterManager.Instance.Player.itemData = data;
             CharacterManager.Instance.Player.addItem?.Invoke();
      
             //E키를 누르면 인벤토리로 이동시킬 것이므로 맵에 있는 Object는 삭제한다. 
             Destroy(gameObject);
         }
  • 폰트 파일을 폰트 에셋으로 만드는 방법

    • Legacy Text와 달리 TextMeshPro Text는 일반 폰트 파일을 넣어줄 수 없다.

    • 가지고 있는 폰트 파일을 TextMeshPro Text에 적용하려면 다음과 같은 과정을 거쳐야 함:

      1. Window > TextMeshPro > Font Asset Creator 선택

      2. 준비한 폰트를 Source Font File에 끌어넣는다.

      3. Atlas ResolutionCharacter Set을 알맞게 설정해주었는지 체크한다.

        • Atlas Resolution :
        • 텍스트 폰트의 글리프(Glyph, 문자 이미지)를 저장하는 텍스처(Atlas)의 크기를 설정하는 옵션
        • 값이 클수록 더 많은 글자를 포함할 수 있지만, 메모리 사용량이 증가함.
        • 만약 한글, 중국어 같은 많은 글자를 포함해야 한다면 4096 x 4096 이상의 해상도가 필요하다!
        • Character Set:
        • 폰트 에셋에 포함할 문자(Character) 범위를 설정하는 옵션.
        • 어떤 문자들을 폰트 파일에 포함할지 선택할 수 있다.
        • Character Set 옵션 :
        옵션설명
        ASCII영어 알파벳 + 숫자 + 특수문자 (기본 128자)
        Extended ASCIIASCII + 유럽어 문자 포함 (256자)
        Unicode모든 유니코드 문자 포함 (메모리 사용 증가)
        Custom Range사용자가 직접 원하는 문자만 포함
      4. Character Sequence32-126,44032-55203,12593-12643,8200-9900 를 입력해준다.

        • 숫자의 의미 : 우리가 이 font를 사용할 때 어떤 언어들을 사용할 것인지, 즉 어떤 언어들을 Font Asset에 저장할 건지 숫자로 범위를 지정해주는 것.
          • 32-126 : 영어 범위 (알파벳)
          • 44032-55203 : 한글 범위
          • 12593-12643 : 자음 모음
          • 8200-9900 : 특수 문자
      5. 다 입력했다면 Generate Font Atlas 버튼을 누르고 변환이 완료될 때 까지 기다린다. (꽤 오래걸린다.)

      6. 끝나면 Save를 누르고, 저장한 Font AssetTextMeshPro Text에 적용해준다!

📍겪은 어려움

  • UpdateLighting 메서드에 대한 이해

    void UpdateLighting(Light lightSource, Gradient colorGradiant, AnimationCurve intensityCurve)
    {
       float intensity = intensityCurve.Evaluate(time);
    
       lightSource.transform.eulerAngles = (time - (lightSource == sun ? 0.25f : 0.75f)) * noon * 4.0f;
       lightSource.color = colorGradiant.Evaluate(time);
       lightSource.intensity = intensity;
    
       GameObject go = lightSource.gameObject;
       if (lightSource.intensity == 0 && go.activeInHierarchy)
            go.SetActive(false);
       else if (lightSource.intensity > 0 && !go.activeInHierarchy)
            go.SetActive(true);
    }
    • 처음 위 코드를 접했을 때, 왜 lightSource 가 sun일 때는 0.25f를 빼주어야 하는지, moon일 때는 왜 0.75f를 빼주어야 하는지, 마지막에는 왜 4.0f를 곱해야 하는지 이해하지 못함.

    • Q1. 왜 하필 0.25f0.75f 인가?

      • 먼저 이걸 이해하려면 time 이 어떤 변수인지 이해해야 함.

      • time 값은 0~1 범위를 반복하며 하루의 흐름을 표현하는데, 대략적으로 표현하자면 다음과 같다.

        • time = 0.0 → 새벽
        • time = 0.25 → 아침
        • time = 0.5 → 정오
        • time = 0.75 → 저녁
        • time = 1.0 → 자정 (0.0과 같음)
      • 해가 뜨기 시작하는 것은 0.25초인 아침이고, 달이 뜨기 시작하는 것은 0.75초인 저녁임.

      • 때문에 0.25초(아침)에는 해가, 0.75초(저녁)에는 달이 떠오를 수 있도록 보정값을 넣어주는 것.

    • Q2. 왜 +가 아니라 -인가?

      • 여기서 많이 해맸는데, 이건 직접 식을 계산해보면 답이 나온다.

      • 아래 표는 해의 위치를 위해 -0.25의 보정을 넣어준 결과이다:

        time계산 결과결과 (eulerAngles)태양 위치
        0.00 (00:00)(-0.25) * 90 * 4(-90, 0, 0)태양이 지평선 아래
        0.25 (06:00)(0.00) * 90 * 4(0, 0, 0)태양이 떠오르기 시작
        0.50 (12:00)(0.25) * 90 * 4(90, 0, 0)태양이 하늘 꼭대기
        0.75 (18:00)(0.50) * 90 * 4(180, 0, 0)태양이 지평선으로 내려감
        1.00 (00:00)(0.75) * 90 * 4(270, 0, 0)태양이 반대쪽 지평선 아래
      • 만약 0.25를 빼주지 않거나, 0.25를 빼는 대신 더해버린다면 해는 자정부터 떠버리거나, 저녁무렵부터 떠오르는 등 해가 너무 일찍 떠오르게 된다.

      • 0.5초에 태양이 하늘 한가운데(90, 0, 0)에 있으려면 무조건 -0.25를 해주어야 하는 것.

      • 달의 경우도 마찬가지로 자정에 하늘 한가운데(90, 0, 0)에 위치할 수 있도록 -0.75의 보정값을 넣어주는 것.

        time계산 결과결과 (eulerAngles)달 위치
        0.00 (00:00)(-0.75) * 90 * 4(-270, 0, 0)달이 지평선 아래
        0.25 (06:00)(-0.50) * 90 * 4(-180, 0, 0)달이 지평선 근처
        0.50 (12:00)(-0.50) * 90 * 4(-90, 0, 0)달이 떠오르기 시작
        0.75 (18:00)(-0.25) * 90 * 4(0, 0, 0)달이 떠오름
        1.00 (00:00)(0.00) * 90 * 4(90, 0, 0)달이 정점
    • Q3. 왜 4.0f를 곱해주어야 하는가?

      • 우리가 계산식에서 4f를 제외하고 0.25 * 90 부분만 계산한다면, 결과 값은 -90이 아니라 -22.5가 나온다.
      • 보정을 넣지 않고, time1.0이 되더라도 우리가 (90, 0, 0)noon을 곱해주는 한 90도 이상의 각도는 나올 수 없다.
      • 때문에 나온 값에 4.0f를 곱해주어 360도로 회전할 수 있도록 보정값을 넣어주는 것!!

📍회고 및 반성

  • 사실 UpdateLighting 메서드를 이해한 것 처럼 적어두긴 했지만 아직 완벽하게 이해한 것 같지는 않다. 어디까지나 아, 이게 이런 뜻이구나! 정도로 납득한 느낌...
  • UpdateLighting 부분은 여유가 된다면 내일 튜터님께 여쭤볼 예정!

0개의 댓글