오늘 학습 키워드

유니티 숙련 공부, 객체지향 특강

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

유니티 숙련 공부

인벤토리 만들기

  • ItemSlot (Button, Outline)
    • Icon
    • QuantityText
  • UIInventory
    • Bg
      • Slots (Grid Layout Group)
    • InfoBg
      • ItemName
      • ItemDescription
      • StatName
      • StatValue
      • UseButton
      • EquipButton
      • UnEquipButton
      • DropButton
// UIInventory.cs
// 인벤토리 키는 Toggle 함수를 만들어놓고
// 사용 자체는 PlayerController에서 하도록 위임?함
void Start()
{
    controller = CharacterManager.Instance.Player.controller;
    condition = CharacterManager.Instance.Player.condition;
 
	// PlayerController 안에 있는 inventory라는 델리게이트에
	// 인벤토리 창을 끄고 키는 Toggle 함수를 연결함
    controller.inventory += Toggle;
 
    inventoryWindow.SetActive(false);
    slots = new ItemSlot[slotPanel.childCount];
 
    for(int i = 0; i < slots.Length; i++)
    {
        slots[i] = slotPanel.GetChild(i).GetComponent<ItemSlot>();
        slots[i].index = i;
        slots[i].inventory = this;
    }
 
    ClearSelectedItemWindow();
}
 
public void Toggle()
{
    if(IsOpen())
    {
        inventoryWindow.SetActive(false);
    }
    else
    {
        inventoryWindow.SetActive(true);
    }
}
 
public bool IsOpen()
{
    return inventoryWindow.activeInHierarchy;
}
 
// PlayerController.cs
public Action inventory;
 
public void OnInventory(InputAction.CallbackContext context)
{
    if (context.phase == InputActionPhase.Performed)
    {
        inventory?.Invoke();
        ToggleCursor();
    }
}
 
void ToggleCursor()
{
    bool toggle = Cursor.lockState == CursorLockMode.Locked;
    Cursor.lockState = toggle ? CursorLockMode.None : CursorLockMode.Locked;
    canLook = !toggle;
}

아이템 습득 처리

// 뼈대
void AddItem()
{
    ItemData data = CharacterManager.Instance.Player.itemData;
 
    // 아이템이 중복 가능한지 (Can Stack)
    if(data.canStack)
    {
        ItemSlot slot = GetItemStack(data);
        if(slot != null)
        {
            slot.quantity++;
            UpdateUI();
            CharacterManager.Instance.Player.itemData = null;
            return;
        }
    }
    
    // 비어있는 슬롯 가져옴
    ItemSlot emptySlot = GetEmptySlot();
 
    // 비어있는 슬롯이 있다면 거기에 데이터 넣음
    if(emptySlot != null)
    {
        emptySlot.item = data;
        emptySlot.quantity = 1;
        UpdateUI();
        CharacterManager.Instance.Player.itemData = null;
        return;
    }
 
    // 없다면 아이템 버림
    ThrowItem(data);
    CharacterManager.Instance.Player.itemData = null;
}
 
void UpdateUI()
{
	// UI를 업데이트 해주는 부분
}
 
ItemSlot GetItemStack(ItemData data)
{
	// ItemData이 지금까지 몇 개 쌓여있는지 확인하는 부분
    return null;
}
 
ItemSlot GetEmptySlot()
{
	// 빈 슬롯 반환하는 부분
    return null;
}
 
void ThrowItem(ItemData data)
{
	// 아이템을 버리는 부분
    return;
}

카메라 여러 개 쓰기

  • 새 카메라를 만들기
  • Clear Flags : Depth Only로 하면 뒤에 배경이 까맣게 나옴
  • Culling Mask : 어떤 오브젝트를 표시할 지 정하는 것. 여기서는 장착 아이템만 찍을 것

아이템 장착 처리

  • 기존에 있던 ItemData에 GameObject equipPrefab 을 선언함
// Equip.cs
public class Equip : MonoBehaviour
{
    public virtual void OnAttackInput()
    {
        // 공격 함수
    }
}
// EquipTool.cs
 
 
  • 장착 자체는 플레이어에 Interaction 붙였던 것처럼 Equipment 따로 만들어서 붙여주기
// Equipment.cs
public void EquipNew(ItemData data)
{
    /// 나의 예상
    /*
    ItemData에 equipPrefab이 있으니까
    그걸 일단 GameObject Instnatiate로 만들고
    그거를 equipParent 자식으로 넣은 다음에
    GO 안에 있는 Equip 을 curEquip으로 넣으면 되지 않을까?
    */
 
    Unequip();
    curEquip = Instantiate(data.equipPrefab, equipParent).GetComponent<Equip>(); // 맞았다!
}
 
public void Unequip()
{
    if(curEquip != null)
    {
        Destroy(curEquip.gameObject);
        curEquip = null;
    }
}
  • 그리고 장착/해제를 관리하도록 UIInventory 편집
public void OnEquipButton()
{
    /// 나의 예상
    /*
    일단 지금 선택된 selectedItemIndex 를 curEquipIndex로 넣고
    아까 만들어 둔 EquipNew(slots[curEquipIndex].item) 으로 부르면 될듯?
    */
 
    if (slots[curEquipIndex].equipped)
    {
        // 이미 장착한 아이템이 있으면
        // 그 아이템 장착 해제 해줘야됨
        Unequip(curEquipIndex);
    }
    slots[selectedItemIndex].equipped = true;
    curEquipIndex = selectedItemIndex;
    CharacterManager.Instance.Player.equip.EquipNew(selectedItem); // 얼추 맞음!
    UpdateUI();
 
    SelectItem(selectedItemIndex);
}
 
void Unequip(int index)
{
    /// 나의 예상
    /*
    인덱스 받아서 equipped면 false로 바꾸기?
    */
 
    slots[index].equipped = false; // 정답!
    CharacterManager.Instance.Player.equip.Unequip(); // 이건 생각 못했다 까비
    UpdateUI();
 
    if(selectedItemIndex == index)
    {
        SelectItem(selectedItemIndex); // 장착 해제했어도 설명은 보여야하니까
    }
}
 
public void OnUnequipButton()
{
    Unequip(selectedItemIndex);
}

무기 애니메이션 만들기

// EquipTool.cs
public override void OnAttackInput()
{
    /// 나의 예상
    /*
    attacking이 아닐 때에 애니메이션을 재생하도록 함
    현재 애니메이터는 Attack 이라는 Trigger를 가지고 있음
    그래서 !attacking일 때 SetTrigger("Attack")을 하면 될 것 같음
    */
 
    if(!attacking)
    {
        attacking = true;
        animator.SetTrigger("Attack");
        Invoke("OnCanAttack", attackRate); // 일정 시간 후 공격 가능하도록
    }
}
 
void OnCanAttack()
{
    attacking = false;
}
// Equipment.cs
public void OnAttackInput(InputAction.CallbackContext context)
{
    /// 나의 예상
    /*
     * 입력 딱 하면 curEquip 안에 있는 OnAttackInput을 호출하면 될 것 같음
     */
    if(context.phase == InputActionPhase.Performed && curEquip != null && controller.canLook) // 플레이어가 화면 돌릴 수 있는 상태일 때에만 공격하도록 함
    {
        curEquip.OnAttackInput();
    }
}

자원 채취

  • Quaternion Quaternion.LookRotation(Vector3 forward , Vector3 upwards = Vector3.up)
    • 앞과 위 벡터를 주면 그 방향으로 바라보는 각도를 반환함
    • upwards는 기본적으로 Vector3.up임
// Resources.cs
public void Gather(Vector3 hitPoint, Vector3 hitNormal)
{
    /// 나의 예상
    /*
    예상도 안감...
    */
 
    for(int i = 0; i < quantityPerHit; i++)
    {
        if (capacity <= 0) break;
        capacity--;
        // Quaternion.LookRotation(hitNormal) : hitNormal 방향을 바라보도록 설정
        Instantiate(itemToGive.dropPrefab, hitPoint + Vector3.up, Quaternion.LookRotation(hitNormal));
    }
}
// EquipTool.cs
public void OnHit()
{
    /// 나의 예상
    /*
    attackDistance 만큼 Ray를 쏴서
    그 안에 Resource가 있으면 Gather를 호출
    Damagable이 있으면 Damage를 호출?
    */
 
    Ray ray = camera.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2, 0));
    RaycastHit hit;
    if(Physics.Raycast(ray, out hit, attackDistance))
    {
        if(doesGatherResources && hit.collider.TryGetComponent(out Resource resource))
        {
            // hit.normal이 뭐지?...
            // 법선 벡터!!!
            resource.Gather(hit.point, hit.normal);
        }
    }
}

  • Equip.OnHit() > Resource.Gather() 로 이어지는 과정 도식화
  • 공격 하자마자가 아니고 조금 지나서 OnHit 처리가 되도록 애니메이션 이벤트 생성

스태미나

// PlayerCondition.cs
// 스태미나를 사용할 수 있는 상태인지 아닌지 체크
public bool UseStamina(float amount)
{
    if (stamina.curValue - amount < 0f)
    {
        return false;
    }
    stamina.Substract(amount);
    return true;
}

AI 네비게이션

  • Unity가 알아서 AI 네비게이션 기능을 제공하고 있음
  • Navigation Mesh : 3D 공간을 나눠서 이동 가능한 지역과 장애물이 있는 지역을 구분하는 매쉬
    • 지역들에 대한 비용을 설정할 수 있음.
  • Pathfinding : 캐릭터의 현재 위치에서 목표 지점까지 가장 적절한 경로를 찾는 알고리즘
    • 주로 A* 알고리즘이 사용
  • Sterring Behaviour : 경로를 따라 이동할 때 자연스러운 동작을 구현하는데 사용됨
  • Obstacle Avoidance : 캐릭터가 이동 중에 장애물과 충돌하지 않도록 하는 기술
    • Carving System을 통해 갈 수 있는 길에서 장애물 주변 부분을 잘라내어 장애물이 있어도 다른 길을 생성할 수 있도록 함
  • Local Avoidance : 여러 캐릭터나 NPC가 서로 충돌하지 않도록 하는 기술
    • 캐릭터들 사이의 거리를 유지하거나 회피 동작을 수행해 서로 부딪히지 않도록 함

A* 알고리즘?

  • 다익스트라에서 진화한 형태
f(n) = g(n) + h(n) 
// g : 현재 비용
// h : 추정 비용
  • f(n)이 가장 작은 노드를 다음 노드로 선택해 감.
    • 이 때 우선순위 큐를 사용
  • A* 알고리즘은 휴리스틱 함수에 따라 성능이 크게 바뀜
  • 휴리스틱 함수의 종류
    • 맨해튼 거리 : x 좌표 차이와 y 좌표 차이의 합
    • 유클리드 거리 : 대각선 길이를 구하는 공식 사용
    • 패턴 데이터베이스 : 미리 계산된 패턴을 DB에서 찾아다 사용

참고자료 링크

적 생성

  • Package Manager에서 AI Navigation 패키지 설치
  • Window > AI > Navigation (Obsolete) 클릭

객체지향 강의 : 레거시 코드 리팩토링

  • 구루 디자인 패턴 여기서 추가 공부 가능

  • 이전 코드를 모르면 장점이 보이지 않는다!

  • 수정을 해나가는 과정을 볼것

  • Prototype, Bridge, Facade, Fleyweight, Observer, Strategy, Builder, Singleton, Composite, State, TemplateMethod, Command 정도는 알아두는게 좋음

  • 입문강의에서 Singleton, Observer(InputAction), 숙련강의에서 Strategy(아이템 상호작용), 심화에서 State를 써볼 예정

  • Prototype : Prefab 자체가 Prototype 패턴을 기반으로 만든 기능임

  • Composite : 컴포넌트 기능 자체가 Comosite 패턴을 기반으로 만든 기능임

  • Facade : 숙련주차에서 Player가 Facade 패턴이라고 할 수 있음. 플레이어와 관련된 것들이 다 묶여있으니까

  • 의존도, 결합도를 줄이는게 객체지향의 핵심임을 잊지 말자

  • 디자인 패턴을 몰라도 객체지향적인 코드를 작성할 수는 있다.

  • 분기점을 없앤다!

  • 동시 작업을 했을 때 서로 코드에 영향을 안주는 상태여야 함

브릿지 패턴

  • 두 클래스를 이어준다고 해서 브릿지 패턴임 (이름진짜대충지었다)
  • 마름모 모양 있으면 대부분 인터페이스로 관리되고 있다고 보면 됨

왜 이렇게 힘들게 해야되나요…

  • 동시 작업 했을 때 편함
  • 다양한 몬스터 이동, 공격 패턴 만들 때 유용함
  • 작업량이 많아질 때 편함

중요한 것

  • 추상부를 호출하도록 하자 늘!!!!
  • 그리고 구현부가 부모(추상부) 말을 잘 듣도록 하자!!
  • 불필요한 생성(생성 패턴), 탐색/정렬(구조 패턴), 연산/조건문(행위 패턴)을 줄이는게 중요함!
  • 파생 클래스보단 base클래스, 구현 클래스보단 추상화된 클래스/인터페이스를 캐싱하기!

몬스터 만드는 예시

// Monster.cs
public class Monster : MonoBehaviour
 
{
	private string name;
	private int attack
 
	IMonsterBehaviour behaviour;
	MonsterOffense offense;
 
	private void Awake()
	{
		behaviour = GetComponent<IMonsterBehaviour>();
		offense = GetComponent<MonsterOffense>();		
	}
 
	private void Update()
	{
		if(behaviour != null)
		{
		// behaviour 는 뭐가 되었든 Move()를 가지고 있을 것
			behaviour.Move();
		}
 
		if(offense != null && Input.GetKeyDown(KeyCode.Space))
		{
		// offense 는 뭐가 되었든 Attack()을 가지고 있을 것
			offense.Attack();
		}
	}
}
// MonsterBehaviour.cs
// 인터페이스를 쓰는 예시
public interface IMonsterBehaviour
{
	public void Move();
}
 
public abstract class MonsterBehaviour : MonoBehaviour, IMonsterBehaviour
{
	public abstract void Move();
}
// MonsterOffense.cs
// 추상클래스를 쓰는 예시
public abstract class MonsterOffense : MonoBehaviour
{
	public abstract void Attack();
}

템플릿 메소드 패턴

  • 제일 좋은 기준은 어떻게 하면 else if / switch case를 줄일지 고민하는 것
  • 브릿지랑 묘하게 비슷함.
    • 각 역할을 하는걸 따로 만드는거임
    • 브릿지는 파츠를 섞어서 하나에 넣고 쓰는거고
  • 구체적으로 동작하는 부분을 추상화 하기.

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

  • 문제&에러에 대한 정의

  • 내가 한 시도

  • 해결 방법

  • 새롭게 알게 된 점

  • 이 문제&에러를 다시 만나게 되었다면?

내일 학습 할 것은 무엇인지