오늘 학습 키워드

유니티 숙련 공부, 스탠다드 반 강의

오늘 학습 한 내용을 나만의 언어로 정리하기

유니티 숙련 공부

TryGetComponent

  • 특정 컴포넌트가 게임 오브젝트에 연결되어있는지 확인을 해 줌
  • 있으면 true, 없으면 false
// 메소드 형식
public bool TryGetComponent<T>(out T component) where T : component;
// 예시
Rigidbody rb;
if (TryGetComponent<Rigidbody>(out rb))
{
	// Rigidbody를 가져오는데 성공했을 때
	rb.AddForce(Vector3.up * 100f);
}
else 
{
	// Rigidbody를 가져오는데 실패했을 때
	Debug.Log("There's No Rigidbody");
}
  • TryGetComponent는 컴포넌트가 없어도 예외를 발생시키지 않음.

카메라 절두체

  • 절두체 : 피라미드 모양의 윗부분을 밑면에 병렬로 잘라낸 입체 형상
  • 머리를 잘라냈다고 해서 절두체이지 않을까 싶음

  • 이 내용을 카메라에 적용한게 카메라(뷰) 절두체.
  • 시야 범위(FOV), Near Clipping Plane, Far Clipping Plane 등의 값을 조절해 절두체의 모양을 바꿀 수 있음
  • 렌더링 할 범위를 지정해줄 수 있음

Coroutine

  • 동기 : 하나의 작업이 끝날 때 까지 다른 작업을 수행하지 못하는 상태

  • 비동기 : 동시 진행이 가능한 상태

  • 작업을 다수 프레임에 분산하는 메소드

  • 중단점에서 다음 프레임을 계속할 수 있는 메소드

  • 스레드와는 다름. 코루틴은 여전히 메인 스레드에서 실행됨.

  • 코루틴은 여전히 동기다!!!

  • 동시 진행처럼 보이지만 왔다갔다 하는거임

  • 유니티는 단일 스레드임.

데미지 만들기

  • 데미지를 받을 수 있는 애들은 IDamagable 인터페이스로 정의
public interface IDamagable
{
    void TakePhysicalDamage(int damage);
}
  • 데미지를 받았을 때 Indicator 깜빡임 표현은 Delegate 사용
// PlayerCondition.cs
public event Action onTakeDamage;
 
public void TakePhysicalDamage(int damage)
{
    health.Substract(damage);
    onTakeDamage?.Invoke();
}
  • 캠프파이어에 다가가면 IDamagable을 상속한 애들을 찾아서 데미지를 주도록 함
// CampFire.cs
public class CampFire : MonoBehaviour
{
    public int damage;
    public float damageRate;
 
    List<IDamagable> things = new();
 
    // Start is called before the first frame update
    void Start()
    {
        InvokeRepeating("DealDamage", 0f, damageRate);
    }
 
    void DealDamage()
    {
        for(int i = 0; i < things.Count; i++)
        {
            things[i].TakePhysicalDamage(damage);
        }
    }
 
    private void OnTriggerEnter(Collider other)
    {
        // IDamagable이 구현된 애인지 찾음
        if(other.TryGetComponent(out IDamagable damagable))
        {
            things.Add(damagable);
        }
    }
 
    private void OnTriggerExit(Collider other)
    {
	    // IDamagable이 구현된 애인지 찾음
        if(other.TryGetComponent(out IDamagable damagable))
        {
            things.Remove(damagable);
        }
    }
}
 
  • DamageIndicator가 서서히 옅어지는 걸 표현하기 위해 코루틴 사용
// DamageIndicator.cs
public void Flash()
{
    if(coroutine != null)
    {
       StopCoroutine(coroutine);
    }
 
    image.enabled = true;
    image.color = new Color(1f, 100f / 255f, 100f / 255f);
    coroutine = StartCoroutine(FadeAway());
}
 
private IEnumerator FadeAway()
{
    float startAlpha = 0.3f;
    float a = startAlpha;
 
    while(a > 0)
    {
        a -= (startAlpha / flashSpeed) * Time.deltaTime;
        image.color = new Color(1f, 100f / 255f, 100f / 255f, a);
        // 다음 프레임 넘어감
        yield return null;
    }
 
    image.enabled = false;
}
  • 그리고 이 Flash 함수를 플레이어가 데미지를 입었을 때 실행할 함수 모음(delegate)에 등록
// DamageIndicator.cs
private Coroutine coroutine;
 
void Start()
{
    CharacterManager.Instance.Player.condition.onTakeDamage += Flash;
}
  • UI도 스크립트로 적용할 수 있도록 하는게 좋음.

조명

  • 라이트 소스 : 게임/3D 렌더링에 광원을 추가하는 데 사용됨. 특정 위치 또는 방향에서 발생하는 빛을 나타냄

  • 종류

    • 점 광원 : 모든 방향으로 균등하게 빛을 발산
    • 방향성 라이트 : 무한히 멀리 위치하여 한 방향으로만 빛을 발산
    • 스포트라이트 : 한 점에서 원뿔 모양으로 빛을 발산
    • 면 광원 : 표면 전체에 걸쳐 균등하게 모든 방향으로 빛을 방출. 사각형의 한쪽 면에서만 빛을 방출
  • 속성 : 위치, 방향, 강도, 색상, 범위, 각도

  • 그림자 : 라이트와 객체 사이 관계에 따라 그림자는 라이트가 부딪히는 객체 뒤에 생성됨

  • 성능 : 라이트는 렌더링 성능에 큰 영향을 미침. (특히 그림자 포함된 경우)

  • Lighting Intensity Multiplier : 실제 환경의 빛을 조절함

  • Reflecting Intensity Multiplier : 실제 오브젝트에 반사되는 정도를 조절함

AnimationCurve

  • 키프레임을 사용해 값을 보간하는데 사용하는 클래스

  • AnimationCurve 구성 요소

    • 키프레임 : 시간에 따른 값을 정의하는 점. 시간 t와 해당 시간에 대응하는 값 value가 있음
    • 보간 방식 : 인접한 키 프레임 사이의 값을 보간하는 방법들을 선택.
      • Cubic Bezier, 선형, 스텝 등등 다양한 방식이 있음
  • 사용 예시

// 예시
public class ExampleScript : MonoBehaviour
{
	private AnimationCurve curve;
 
	private void Start()
	{
		// 새로운 AnimationCurve 생성
		curve = new AnimationCurve();
 
		// 키프레임 추가 (시간 t, 값 value)
		curve.AddKey(0f, 0f);
		curve.AddKey(1f, 1f);
	}
 
	private void Update()
	{
		// 시간에 따라 값을 보간하여 출력
		float time = Time.time;
		float value = curve.Evaluate(time);
		Debug.Log("Time: " + time + ", Value : " + value);
	}
}
  • AddKey : 새로운 키프레임을 추가
  • Evaluate : 특정 시간에 해당하는 값을 보간하여 반환
  • keys : 키프레임의 배열을 반환

낮/밤 만들기

  • 3D 맵 생성하면 기본적으로 있는 Directional Light는 해를 의미함
  • Rotation.x
    • 0 : 동쪽
    • 90 : 정오
    • 180 : 서쪽
    • 270 : 밤
  • Window > Rendering > Lighting > Environment에 Sun Source가 Directional Light로 되어있기 때문에 이런 현상이 나타남

  • Skybox의 반구가 반대쪽으로 넘어갔을 때 Moon Light가 켜지도록 할 예정
void Start()
{
    timeRate = 1.0f / fullDayLength;
    time = startTime;
}
 
void Update()
{
    time = (time + timeRate * Time.deltaTime) % 1.0f;
    UpdateLighting(sun, sunColor, sunIntensity);
    UpdateLighting(moon, moonColor, moonIntensity);
 
	// Window > Rendering > Lighting > Environment 에 있는 부분
    RenderSettings.ambientIntensity = lightingIntensityMultiplier.Evaluate(time);
    RenderSettings.reflectionIntensity = reflectionIntensityMultiplier.Evaluate(time);
}
 
void UpdateLighting(Light lightSource, Gradient gradient, AnimationCurve intensityCurve)
{
    float intensity = intensityCurve.Evaluate(time);
 
	// time = 0.5를 예시로 듬
    // time이 0.5 = 정오 = 해는 90도 회전해 있어야 함
    // 반대로 달은 180도 회전해 있어야 함
    // 360의 0.25는 90, 0.75는 180임
    // 0.5(time) - 0.25(sun) = 0.25
    // 0.25 * 4 = 1
    // 1 * noon = noon
    // 그래서 식이 이렇게 나옴
 
    lightSource.transform.eulerAngles = 
        (time - (lightSource == sun ? 0.25f : 0.75f)) 
        * 4f 
        * noon;
    lightSource.color = gradient.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);
    }
}
 

  • Intensity : 빛의 강도라는 것을 잊지 말자

  • 해 높게 뜰수록 빛 더 세지니까 intensity 그래프를 중앙에 갈수록 세게 만든 것

  • 0.25에서 시작해서 0.75에서 끝남

  • 달은 반대모양 그래프. 00.2 / 0.81 까지 푸른 빛 발산하다가 그 사이(낮)에는 빛 발산 안함

  • 빛의 세기(Other Lighting > Lighting Intensity Multiplier)는 낮에가 세기 때문에 0.4 / 0.6 부분 1이고 0 / 1부분 0

  • 빛에 세기가 쎈 만큼 반사되는 양도 많아지기 때문에, 빛의 세기와 같게 설정

인터페이스

  • 공통적인 동작을 구현하는데 용이
  • 인터페이스를 구현하는 클래스들은 공통 규약을 준수할 수 있음
  • 특징
    • 추상화 : 인터페이스는 실제로 메소드를 구현하는게 아니고 개념만 가지고 있음
    • 메소드 시그니처 : 인터페이스는 구현 클래스가 반드시 구현해야 하는 메소드들의 시그니처를 정의
    • 다중 상속 가능 : 클래스와 다르게 인터페이스는 다중으로 구현할 수 있음
    • 강제적 구현 : 인터페이스를 구현하는 클래스는 무조건 메소드 시그니처를 구현해야 함
    • 인터페이스 간 확장 : 인터페이스는 다른 인터페이스를 확장시킬 수 있음
  • 사용하는 이유
    • 코드는 결합도가 낮아야 함 클래스 간 의존도를 낮춰야 함
    • 구체적으로 구현한 클래스가 아니고 작은 단위의 여러 인터페이스를 사용하는 것이 좋음
  • 협업 관점에서의 장점
    • 개발 기간이 단축됨 : 틀만 작성하면 구현 클래스에서 코드 작성/개발 가능함
    • 표준화가 가능함 : 여러 명이 작업해도 정형화된 작업이 가능함
    • 독립적인 프로그래밍이 가능함 : 선언은 인터페이스에서 하고, 구현은 클래스에서 하는 방식
// 예시
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
{
	Payment payment; // 여기에는 Card, Cash, QR 다 들어갈 수 있음
	payment.Pay();
}
 

아이템과 상호작용

  • 아이템 다섯 개(나무, 도끼, 당근, 바위, 칼)을 제작
  • ScriptableObject로 아이템들을 관리
// ItemData.cs
public enum ItemType
{
    Equipable, // 장비
    Consumable, // 소비 아이템
    Resource // 자원
}
 
public enum ConsumableType
{
    Health,
    Hunger
}
 
[Serializable]
public class ItemDataConsumable
{
    public ConsumableType type;
    public float value; // 효과 값
}
 
[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 canStack;
    public int maxStackAmount;
 
    [Header("Consumable")]
    public ItemDataConsumable consumables;
}
  • ItemObject에서 ItemData를 사용할 수 있게 설정
  • 인터페이스를 만들어서 상호작용 가능하게 함
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()
    {
        CharacterManager.Instance.Player.itemData = data;
        CharacterManager.Instance.Player.addItem?.Invoke();
        Destroy(gameObject);
    }
}
  • 레이를 쏨
// Interaction.cs
// 얼마나 자주 검출할 지
public float checkRate = 0.05f;
private float lastCheckTime;
public float maxCheckDistance;
public LayerMask layerMask;
 
// 캐싱용
public GameObject curInteractGameObject;
private IInteractable curInteractable;
 
public TextMeshProUGUI promptText;
private Camera camera;
 
void Start()
{
    camera = Camera.main;
}
 
void Update()
{
	// 매 Update마다 부르면 비효율적이니까 Time Check함
    if(Time.time - lastCheckTime > checkRate)
    {
        lastCheckTime = Time.time;
        // 스크린 중앙에서 나가는 레이 생성
        Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));
        RaycastHit hit;
 
        if (Physics.Raycast(ray, out hit, maxCheckDistance, layerMask))
        {
        // Interact 가능한 오브젝트에다가 커서 대면 설명 나오도록 설정
            if (hit.collider.gameObject != curInteractGameObject)
            {
                curInteractGameObject = hit.collider.gameObject;
                curInteractable = hit.collider.GetComponent<IInteractable>();
                SetPromptText();
            }
        }
        else
        {
            curInteractGameObject = null;
            curInteractable = null;
            promptText.gameObject.SetActive(false);
        }
    }
}
 
private void SetPromptText()
{
    promptText.gameObject.SetActive(true);
    promptText.text = curInteractable.GetInteractPrompt();
}
 
public void OnInteractionInput(InputAction.CallbackContext context)
{
    if ((context.phase == InputActionPhase.Started && curInteractable != null))
    {
        curInteractable.OnInteract();
        curInteractGameObject = null;
        curInteractable = null;
        promptText.gameObject.SetActive(false);
    }
}

한글 폰트 만들기

  • Window > TextMeshPro > Font Asset Creator
  • Font File에 폰트 에셋으로 만들고 싶은 폰트 파일 추가
  • Alias Resolution을 4096 / 4096 으로 맞춤
  • Custom Range에 아래 숫자 추가
// 영어 범위
32-126
 
// 한글 범위
44032-55203
 
// 한글 글자 따로따로 범위
12593-12643
 
// 특수문자
8200-9900
32-126,44032-55203,12593-12643,8200-9900
  • Render Mode : SDFAA 설정 확인
  • Generate Font Atlas 누르면 나옴

나무랑 돌의 Collider가 이상한 현상

  • 움직이지 않는 물체를 static으로 설정하면, 유니티는 그것들을 하나의 큰 오브젝트로 판단함
  • 지금은 Item_Wood와 Item_Rock은 Static이 아니지만 그 안에 있는 Log와 Rock은 Static이었음
  • 그래서 유니티는 그 두 물체가 움직이지 않는 것이라고 판단하고 콜라이더를 고정시켜버림
  • static을 해제하면 원래대로 작동하는 모습을 볼 수 있음

스탠다드 반 강의 (주제 : Input System)

  • 레거시 방식 : Input.GetKeyDown 등등
  • 다이렉트 방식 : 키보드를 직접 가져와서 씀. 입력 확인 정도로만 씀
Keyboard keyboard;
Mouse mouse;
Gamepad gamepad;
 
private void Start()
{
	keyboard = Keyboard.current;
}
 
void Update()
{
	if(keyboard.aKey.isPressed)
	{
		// 기타등등
	}
}
  • 임베디드 방식 : 업데이트를 안해도 된다는게 큰 장점
    • 장점 : 로컬 멀티를 만들 때 아주 편해짐!
public InputAction moveAction;
// 이러고 인스펙터 창에서 설정하기
public event Action jumpEvent;
 
private void OnEnable() // 여기서 설정하고
{
	moveAction.Enable();
	// started, performed, canceled 세 개의 단계가 있음
	// 미리 빼고
	moveAction.performed -= PlayerMove;
	moveAction.canceled -= PlayerStop;
 
	// 등록하기
	moveAction.performed += PlayerMove;
	moveAction.canceled += PlayerStop;
}
 
private void OnDisable() // 여기서 끄기
{
	moveAction.Disable();
 
	moveAction.performed -= PlayerMove;
	moveAction.canceled -= PlayerStop;
}
 
public void PlayerJump(InputAction.CallbackContext value)
{
	jumpEvent?.Invoke();
}
 
public void PlayerMove(InputAction.CallbackContext value)
{
	// moveAction.started/performed/canceled 안에 들어가려면
	// InputAction.CallbackContext 를 매개변수로 받아야 함
	dir = value.ReadValue<float>();
}
 
public void PlayerStop(InputAction.CallbackContext value)
{
	dir = 0;
}
 
  • 틈새 공부 : 델리게이터는 캡슐화가 안되어있음. 이벤트는 캡슐화가 되어있음.
    • 이벤트는 외부에서 접근할 때 구독(+=) 하거나 해지(-=) 밖에 안됨.
// event 형식의 Action을 만듬
event Action<float> action;
 
// 유니티에서 제공하는 액션을 씀
event UnityAction unityAction;
  • 틈새 공부 2 : region endRegion 을 쓰면 구역을 나눠서 짤 수 있다?

  • 플레이어 인풋 방식 : 캐릭터에 Player Input 컴포넌트를 붙이고 Action Map 해서 만들기

    • Control Scheme : 어떤 입력기기를 쓸 지 정하는 것
    • Action Type
      • Value : 입력 기기가 하나가 세팅되면 그 값만 받음
      • Button : 한 번만 누르면 될 때
      • Pass Through : 입력 기기 하나를 누르다가 다른걸 눌러도 입력이 동시에 됨
    • Control Type : 데이터를 어떻게 전달할 것인지 방법
    • Behaviour
      • Send Messages : On + “액션 이름” 으로 만들어 주어야 함
        • 얘만 매개변수가 InputValue 임
      • Broadcast Message : 쓰지마셈. 부모부터 자식까지 죄다 Send Messages를 보냄.
      • Invoke Unity Events : 버튼 OnClick 하는거처럼 인스펙터에서 붙여주는 방식
      • Invoke C Sharp Events : 직접 구현하는 방법.
    • Interactions : 입력 조건들. 중첩 가능
      • Hold : 누르고 있어야지 입력을 처리해줌.
        • Press Point : 아날로그 입력에 쓰임.
        • Hold Time : N초 이상 입력해야 입력으로 인정해줌
      • Tap : 빠르게 떼야지 입력을 처리해줌
        • Max Tap Duration : N초 보다 짧게 누르고 떼야 입력으로 인정해줌
      • Press : 눌렀을 때만? 뗄 때만? 둘 다?
      • Multi Tap : N번 입력해야 한 번의 입력으로 처리함
        • Max Tap Spacing : 총 몇 초 내로 N번 입력해야하는지
        • Max Tap Durations : 다음 입력까지
      • Slow Tap : N초 이후에 떼어야지 입력으로 처리함
    • Processor : 값을 알아서 정리해줌. 중첩 가능
      • Normalize : 정규화
      • Clamp : 최대/최소값 넘지 않게 해줌
      • Invert : 반대로 바꾸기
      • Scale : 값을 배수로
      • Axis Deadzone : 게임패드에서 씀
// Invoke C Sharp Events 만드는 예제
public PlayerInput playerInput;
 
InputActionMap playerMap;
InputAction moveAction;
InputAction jumpAction;
 
private void Awake()
{
	plyaerInput = GetComponent<PlayerInput>();
	playerMap = playerInput.actions.FindActionMap("Player");
	moveAction = playerInput.actions.FindAction("Move"); // PlayerInput에서 찾는 법
	jumpAction = playerMap.FindAction("Jump"); // InputActionMap 에서 찾는 법
}
 
private void OnEnable()
{
	playerMap.Enable();
	moveAction.Enable();
	jumpAction.Enable();
 
	moveAction.started += OnMoveStop;
	moveAction.performed += OnMove;
	jumpAction.performed += OnJump;
 
}
 
public void OnMove(InputAction.CallbackContext context)
{
	dir = value.ReadValue<float>();
}
 
  • C# 제너레이트 방식 : PlayerInput 컴포넌트가 필요없음.
    • Input Action Map에서 Generate C# Class라는 부분이 있음. 그걸 누르면 알아서 클래스를 만듬
    • 이벤트 시스템에 있는 오류 밑에 버튼 누르면 자동으로 UI 관련으로 생성해줌
    • 거기서 UI 복붙해주면 편함
PlayerInputs input; // Input Action Map이 자동 생성해준 class
 
private void OnEnable()
{
	input = new PlayerInputs();
 
	input.Player.Move.performed += OnMove; // 문자열로 불러올 필요 없음!
	input.Player.Move.canceled += OnMoveStop;
	input.Player.Jump.started += OnJump;
	// input.맵이름.액션이름
 
	input.Changer.Change.Started += Change;
 
	input.Changer.Enable(); // Changer 맵은 항상 켜져있음.
	input.Player.Enable();
}
 
public void Change(InputAction.CallbackContext context)
{
	isPlaying = !isPlaying;
	ui.SetActive(!isPlaying);
	Time.timeScale = isPlaying ? 1f : 0f;
	if(isPlaying)
	{
		input.Player.Enable();
		input.UI.Disable();
	}
	else
	{
		input.Player.Disable();
		input.UI.Enable();
	}
}
 
  • 이 방법들 말고도 InputActionReference, 인터페이스 구현 등등 정말 많이 존재함!

잊지 말자

  • Action Map이 무조건 하나만 활성화 되어야 하는건 아니다!!!

학습하며 겪었던 문제점 & 에러

문제 1

  • 문제&에러에 대한 정의

남은 시간을 표시할 방법을 찾고 있었음

  • 해결 방법
string timeString = $"{minutes:00}:{seconds:00}";
  • 새롭게 알게 된 점

:00은 숫자를 항상 두 자리로 표시하도록 해줌