오늘 학습 키워드
유니티 숙련 공부, 객체지향 특강
오늘 학습 한 내용을 나만의 언어로 정리하기
유니티 숙련 공부
인벤토리 만들기
- ItemSlot (Button, Outline)
- Icon
- QuantityText
- UIInventory
- Bg
- Slots (Grid Layout Group)
- InfoBg
- ItemName
- ItemDescription
- StatName
- StatValue
- UseButton
- EquipButton
- UnEquipButton
- DropButton
- Bg
// 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를 줄일지 고민하는 것
- 브릿지랑 묘하게 비슷함.
- 각 역할을 하는걸 따로 만드는거임
- 브릿지는 파츠를 섞어서 하나에 넣고 쓰는거고
- 구체적으로 동작하는 부분을 추상화 하기.
학습하며 겪었던 문제점 & 에러
-
문제&에러에 대한 정의
-
내가 한 시도
-
해결 방법
-
새롭게 알게 된 점
-
이 문제&에러를 다시 만나게 되었다면?