[34회차 수업내용] 250705
1. 인벤토리기능
2. 플랫포머 게임 마무리
3. 2D 리깅
1. 인벤토리 기능
1.1 인벤토리 UI 만들기
- Canvas 만들기
- 배경만들기
- 스크롤뷰 만들기
- Scroll View- 위치 크기 조절
- Content - grid layout group 컴포넌트추가
- Content 의 자식으로 Button 추가
- Scrollbar Vertical 만 남겨두고, Handle이랑 Bar 투명화

**주의...
이것때문에 시간을 꽤 보냈다.... Button 프리팹을 만들어 slot 스크립트 넣어줘야한다.
프리팹 만들때에는 아이템 스프라이트가 들어갈 Image 컴포넌트를 가진 자식이 필요하다
그 이미지는 투명화하면 안된다.
그래서 나는 배경 색깔 코드 복사해서 가져와서 겹쳐지게 해놓았다.



1.2 코드 수정
-MonsterCore.cs에 추가 // 아이템 랜덤 생성
int itemCount = Random.Range(0, 3); // 0, 1, 2
if (itemCount > 0) // 혹시나 0이 나오면 에러가 발생하기 때문에 예외처리
{
for (int i = 0; i < itemCount; i++) // itemCount 값으로 반복문 실행
{
itemManager.DropItem(transform.position); // 드롭 아이템 생성
}
* 참고
public void DropItem(Vector3 dropPos)
{
// 아이템을 dropPos 위치에 생성
GameObject item = Instantiate(items[randomIndex], dropPos, Quaternion.identity);
...
}
IItemObject
using UnityEngine;
/// 아이템 인터페이스
/// 모든 인벤토리 아이템은 이 구조를 따라야 함
/// → 아이템 이름, 아이콘, 획득/사용 기능 포함
public interface IItemObject
{
ItemManager Inventory { get; set; } // 소속된 인벤토리
GameObject Obj { get; set; } // 실제 아이템 오브젝트
string ItemName { get; set; } // 아이템 이름
Sprite Icon { get; set; } // UI용 아이콘
void Get(); // 획득 시 호출
void Use(); // 사용 시 호출
}
*리마인드
- get → 값을 가져올 수 있다
- set → 값을 설정할 수 있다
-아이템매니저.cs
using UnityEngine;
using UnityEngine.UI;
public class ItemManager : MonoBehaviour
{
public GameObject inventoryUI; // 인벤토리 UI 오브젝트
public Button inventoryButton; // 인벤토리 열기/닫기 버튼
[SerializeField] private GameObject[] items; // 드롭 가능한 아이템 프리팹들
[SerializeField] private Transform slotGroup; // 슬롯들을 묶는 부모 오브젝트
public Slot[] slots; // 슬롯 배열
void Start()
{
// slotGroup 자신과 자식 중에서 Slot Component가 있는 대상을 모두 가져오는 기능
slots = slotGroup.GetComponentsInChildren<Slot>(true);
// 버튼 클릭 시 OnInventory 함수 실행
inventoryButton.onClick.AddListener(OnInventory);
}
public void OnInventory()
{
// 현재 활성 상태의 반대로 전환 (토글)
inventoryUI.SetActive(!inventoryUI.activeSelf);
}
public void DropItem(Vector3 dropPos)
{
// 랜덤 아이템 선택
var randomIndex = Random.Range(0, items.Length);
// 아이템 생성
GameObject item = Instantiate(items[randomIndex], dropPos, Quaternion.identity);
Rigidbody2D itemRb = item.GetComponent<Rigidbody2D>();
itemRb.AddForceX(Random.Range(-2f, 2f), ForceMode2D.Impulse);
itemRb.AddForceY(3f, ForceMode2D.Impulse);
float ranPower = Random.Range(-1.5f, 1.5f);
itemRb.AddTorque(ranPower, ForceMode2D.Impulse);
}
public void GetItem(IItemObject item)
{
// 비어 있는 슬롯에 아이템 추가
foreach (var slot in slots)
{
if (slot.isEmpty)
{
slot.AddItem(item);
break;
}
}
}
}
*
| .GetComponentsInChildren<Slot>() | 자식 오브젝트 중 Slot 컴포넌트가 붙은 것들을 모두 가져옴 |
| (true) | 비활성화된 자식 오브젝트까지 포함해서 가져옴 |
-slot 코드
using UnityEngine;
using UnityEngine.UI;
public class Slot : MonoBehaviour
{
private IItemObject item; // 슬롯에 들어 있는 아이템
[SerializeField] private Image itemImage; // 아이템 아이콘 이미지
[SerializeField] private Button slotButton; // 아이템 사용 버튼
public bool isEmpty = true; // 슬롯이 비어 있는지 여부
void Awake()
{
// 버튼 클릭 시 아이템 사용
slotButton.onClick.AddListener(UseItem);
}
void OnEnable()
{
// 슬롯 상태에 따라 UI 활성/비활성 처리
slotButton.interactable = !isEmpty;
itemImage.gameObject.SetActive(!isEmpty);
}
public void AddItem(IItemObject newItem)
{
item = newItem;
isEmpty = false;
// 아이콘 설정
itemImage.sprite = newItem.Icon;
itemImage.SetNativeSize();
// UI 활성화
slotButton.interactable = true;
itemImage.gameObject.SetActive(true);
}
public void UseItem()
{
if (item != null)
{
item.Use(); // 아이템 효과 실행
ClearSlot(); // 사용 후 슬롯 비우기
}
}
public void ClearSlot()
{
item = null;
isEmpty = true;
// UI 비활성화
slotButton.interactable = false;
itemImage.gameObject.SetActive(false);
}
}
* interactable vs SetActive
slotButton.interactable = true;
| 비교항목 | SetActive() | interactable |
| 대상 | GameObject 전체 | Button, Toggle 등 UI 컴포넌트만 |
| 기능 | 오브젝트 자체의 활성/비활성 상태 변경 | UI 상호작용 가능 여부만 설정 |
| 비활성화 시 | 아예 사라짐 (화면에도 안 보임) | 화면엔 보이지만 클릭 불가 |
| 함수 | gameObject.SetActive(true / false) | button.interactable = true / false |
| Inspector | 오브젝트 체크박스 (활성화) | Button 컴포넌트의 Interactable 체크박스 |
1.3 아이템의 인터페이스 구현
-HpPotion
using UnityEngine;
// IItemObject 인터페이스를 구현한 체력 회복 아이템
public class HpPotion : MonoBehaviour, IItemObject
{
// 어디에 등록할지 알기 위해 필요
public ItemManager Inventory { get; set; }
// 아이템 정보들 (인터페이스에서 요구됨)
public GameObject Obj { get; set; } // 이 아이템 GameObject 자체
public string ItemName { get; set; } // 이름
public Sprite Icon { get; set; } // 아이콘 이미지
void Start()
{
// 인벤토리 객체를 씬에서 검색해서 연결
Inventory = FindFirstObjectByType<ItemManager>();
Obj = this.gameObject;
ItemName = this.gameObject.name;
Icon = this.GetComponent<SpriteRenderer>().sprite;
}
// 플레이어가 아이템을 획득했을 때 실행
public void Get()
{
gameObject.SetActive(false); // 월드에서 안 보이게
Inventory.GetItem(this); // 인벤토리에 자신을 넘겨 등록
}
// 아이템 사용 시 실행 (ex. 체력 회복)
public void Use()
{
Debug.Log("아이템 사용");
}
// 플레이어가 닿았을 때 아이템 획득 처리
void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.CompareTag("Player"))
Get(); // 플레이어가 닿으면 획득
}
}
- 코인은 .. 인벤토리에 넣기위해 수정이 필요한데.. 내가 수업을 놓쳤는데 Notion에도 수정 코드가 없는것같아서 HP potion 참고해서 수정했다.
Coin
using System;
using UnityEngine;
public class Coin : MonoBehaviour, IItemObject
{
public ItemManager Inventory {get; set;}
public GameObject Obj { get; set; }
public string ItemName { get; set; }
public Sprite Icon { get; set; }
public enum CoinType { Gold, Green, Blue }
public CoinType coinType;
public float price;
void Start()
{
Inventory = FindFirstObjectByType<ItemManager>();
Obj = gameObject;
ItemName = this.gameObject.name;
Icon = this.gameObject.GetComponent<SpriteRenderer>().sprite;
}
public void Get()
{
Debug.Log($"{this.name}을 획득했습니다.");
Inventory.GetItem(this);
gameObject.SetActive(false);
}
public void Use()
{ Debug.Log($"{this.price}원을 사용했습니다.");
}
void OnCollisionEnter2D(Collision2D other)
{
if (other.gameObject.CompareTag("Player")) // Tag == Player 인지 확인하는 조건문
Get();
}
}
가격 정보를 이용한 디버그 로그도 추가해줘었다.
public void Use()
{ Debug.Log($"{this.price}원을 사용했습니다.");
}
(타입과 가격은 인스펙터에서 연결, 작성함.)
ItemManager에도 연결 잘 해주어야한다.

결과


인벤토리에 들어가고, 사용도 잘 된다.
버튼 연결법을 정확히 몰라서 좀 오래걸렸다 ㅜㅜ
1.4 [정리] 아이템 획득부터 슬롯 등록, 아이템 사용까지 전체 흐름
1.4.1. 플레이어가 아이템 충돌 감지
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
// 아이템 인터페이스 가져오기
IItemObject item = this.GetComponent<IItemObject>();
// 인벤토리 매니저 찾아서 아이템 획득 함수 호출
FindObjectOfType<ItemManager>().GetItem(item);
// 월드에서 아이템 제거
Destroy(gameObject);
}
}
- 플레이어가 아이템 오브젝트에 닿으면
- 아이템 스크립트(예: HealPotion)가 IItemObject를 구현하므로 GetComponent<IItemObject>()로 참조
- ItemManager의 GetItem(item)을 호출하며 아이템을 넘김
- 아이템 오브젝트는 씬에서 삭제
1.4.2.ItemManager에서 빈 슬롯 찾아 아이템 등록
public void GetItem(IItemObject item)
{
foreach (var slot in slots)
{
if (slot.isEmpty)
{
slot.AddItem(item); // 아이템을 슬롯에 넘겨서 등록
break;
}
}
}
- 아이템 매니저가 슬롯 배열을 순회하며
- 비어있는 첫 슬롯(slot.isEmpty == true)에
- AddItem(item) 호출로 아이템을 전달 (여기서 newItem으로 넘어감)
1.4.3. Slot에서 아이템 저장 및 UI 업데이트
public void AddItem(IItemObject newItem)
{
item = newItem; // 슬롯 내부에 아이템 데이터 저장
isEmpty = false; // 슬롯 비어있지 않음 표시
itemImage.sprite = newItem.Icon; // 아이콘 변경
itemImage.SetNativeSize(); // 아이콘 크기 맞춤
slotButton.interactable = true; // 버튼 활성화 (클릭 가능)
itemImage.gameObject.SetActive(true); // 아이콘 표시
}
- 슬롯에 아이템 정보를 저장하고
- UI 아이콘과 버튼 상태를 바꿔 보여줌
2. 다시 마을로 돌아가기 (씬 전환)
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;
public class PortalController : MonoBehaviour
{
// 씬 종류를 나타내는 열거형 (타운 or 모험 씬)
public enum SceneType { TOWN, ADVENTURE }
public SceneType sceneType = SceneType.TOWN; // 기본값은 TOWN
// 페이드 연출을 위한 클래스 (외부에서 연결 필요)
public FadeRoutine fade;
// 포탈 효과 오브젝트 (이펙트 표시용)
public GameObject portalEffect;
// 로딩 이미지 UI (씬 전환 시 보여짐)
public GameObject loadingImage;
// 로딩 진행 바 (fillAmount 조절로 표현)
public Image progressBar;
// 플레이어가 포탈에 닿았을 때 자동 호출되는 트리거 이벤트
void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player")) // 플레이어일 경우에만
{
StartCoroutine(PortalRoutine()); // 씬 전환 루틴 실행
}
}
// 씬 전환 + 페이드 + 로딩 바 루틴
IEnumerator PortalRoutine()
{
// 1. 포탈 효과 실행
portalEffect.SetActive(true);
// 2. 화면을 서서히 흰색으로 덮기 (페이드 인)
yield return StartCoroutine(fade.Fade(3f, Color.white, true));
// 3. 로딩 이미지 표시
loadingImage.SetActive(true);
// 4. 다시 화면이 서서히 보이게 하기 (페이드 아웃)
yield return StartCoroutine(fade.Fade(3f, Color.white, false));
// 5. 로딩 바 진행
while (progressBar.fillAmount < 1f)
{
progressBar.fillAmount += Time.deltaTime * 0.3f; // 느리게 증가
yield return null; // 다음 프레임까지 대기
}
// 6. 조건에 따라 씬 이동
if (sceneType == SceneType.TOWN)
{
SceneManager.LoadScene(1); // 씬 번호 1 (TOWN)으로 이동
}
else
{
SceneManager.LoadScene(0); // 씬 번호 0 (ADVENTURE)으로 이동
}
}
}
+ 인트로 추가하기 (start버튼 연결) 등 knight 프로젝트 마무리
3. 2D Animation 리깅
:2D 캐릭터나 오브젝트에 가상의 뼈대(Bone)를 부여해서, 각 부위를 자연스럽게 움직이고 변형할 수 있게 만드는 작업
3.1 스프라이트 준비 및 설정
- 캐릭터를 여러 파츠(팔, 다리, 몸통, 머리 등)로 나누거나, 스프라이트 시트를 준비
- 각 파츠별 스프라이트는 독립적으로 리깅 가능
- Sprite Mode가 Multiple로 되어 있어야 함 (Single이면 Create Bone 비활성화됨)
3.2 Skinning Editor 열기
- Sprite Editor에서 Skinning Editor 탭 열기
- 여기서 뼈대 생성, 점(vertex) 조절, 가중치 할당 작업 수행
3.3 뼈대 생성 및 연결
- Create Bone으로 뼈대를 만들고, 팔꿈치, 무릎 등 관절 위치에 배치
- 뼈대는 계층 구조(부모-자식 관계)로 연결
3.4 Auto Geometry 생성
- 선택한 스프라이트에 대해 Auto Geometry → Generate For Selected 실행
- 스프라이트를 뼈대에 맞게 자동 분할
Geometry 수정하고 싶을때 _ 버텍스 수정
- Create Vertex로 점 추가 가능
- Shift를 누른 상태에서 선 나누기 가능
- 점을 움직여 스프라이트 변형 조절
3.5 자동 및 수동 가중치 설정
- Auto Weights로 각 뼈대가 영향 미치는 스프라이트 부위 자동 지정
- Brush 도구로 필요시 Weight Brush로 가중치 직접 보정 가능
3.6 작업 저장 및 씬에 배치
- 수정 완료 후 Apply 버튼 클릭
- 모델 오브젝트를 씬 뷰로 드래그하여 배치
3.7 애니메이션 제작
- Animator와 Animation Window에서 뼈대의 회전/이동을 키프레임으로 지정
- 다양한 동작(걷기, 점프, 공격 등)을 부드럽게 구현
3.8 IK (Inverse Kinematics, 역운동학) Manager 2D 추가 (옵션)
:관절을 자연스럽게 제어하는 IK 기능 제공, 손발 등 끝 부분 움직임 편리
- 보다 자연스러운 움직임 위해 IK Manager 2D 컴포넌트 추가
- 손과 발 등 4개 뼈대에 IK 적용
- IK를 통해 끝 관절을 직접 조작해 동작 제어
*참고
- Limb IK
- 시작 → 중간 → 끝 뼈대 구조가 확실할 때 추천
- 캐릭터의 팔을 앞으로 뻗거나, 점프할 때 다리 모양 만들기 등에 유용
- Chain IK (기본형)
- 여러 뼈대로 구성된 복잡한 선형 구조를 부드럽게 따라가도록 사용
- 각 관절 회전에 제한을 둘 수 있어 현실적인 움직임 가능
- CCD IK (Cyclic Coordinate Descent)
- 반복적으로 각 관절을 조금씩 회전시켜 목표에 도달하는 알고리즘
- 정확히 목표점에 닿게 하고 싶을 때 적합
| 구분 | Limb Solver (2D Limb IK) | Chain Solver (2D Chain IK) – 기본형 | Chain Solver (2D CCD IK) – CCD 방식 |
| 설명 | 팔/다리처럼 2~3개의 관절에 최적화된 IK | 여러 개의 관절로 구성된 체인형 구조 | CCD 방식으로 전체 체인이 목표점에 도달하도록 반복 계산 |
| 사용 뼈대 수 | 보통 2~3개 | 여러 개 (3개 이상 가능) | 여러 개 (제한 없음) |
| 예시 부위 | 팔, 다리 | 꼬리, 줄, 촉수 | 뱀, 덩굴, 로봇팔 등 |
| 자연스러움 | 아주 자연스러움 (사람형 관절에 적합) | 설정에 따라 유연 | 약간 기계적일 수 있음 |
| 회전 제어 | 자동으로 관절 회전 계산 | 회전 각도 제한 설정 가능 | 수동 제약 어려움 |
| 설정 간편성 | 가장 쉬움 | 중간 | 다소 복잡 |
| 타겟 반응 속도 | 빠름 | 빠름 | 비교적 느림 (반복 계산 때문) |
| 특징 | 빠르게 자연스러운 팔·다리 구현 | 유연한 체인 구조 대응 | 끝이 정확히 도달하도록 반복 계산 |
-knight 프로젝트 후기
background collider가 튀거나, 고블린 hp바가 늦게 회전하고 죽어도 안없어지는것, knight가 죽는 애니메이션을 할때 바닥을 뚫고 들어가거나, 고블린 Trace가 조금 부자연스럽거나.. 고치고 싶은게 정말 많지만 내가 의도한대로 캐릭터가 움직이고 인벤토리가 열리고 그런 일련의 과정을 해보니 전체적인 흐름이 조금은 느껴지는 것 같았다. 특히 인벤토리 구성이 아이템 드롭부터 감지, 활성화 비활성화와 UI와 연결 등등 신경쓸게 많았고, 지금까지 배운 아주 많은걸 종합해서 연결해야하는 느낌이라 좀 복잡했다. 그래도 그만큼 배운게 많은 것 같다. 원리를 자주 보며 더 눈에 익혀야겠다.
멘토님과 커피챗을 했는데 확실히 내용 복습보다는 이걸 내가 쓸수 있는 도구로 만들고, 그러기 위해선 어떤 문제를 해결하기 위해 이런 기능들 코드들이 개발되었는지 한번 더 생각해보고 조사해보는 과정이 필요할 것 같다.
'프로그래밍 > 유니티 부트캠프' 카테고리의 다른 글
| 유니티(자료구조2, 형상관리 깃브랜치, 배열 예제_3D_폭탄) _ 멋쟁이사자처럼 유니티 부트캠프 후기 36회차 (0) | 2025.07.08 |
|---|---|
| 유니티(자료구조1) _ 멋쟁이사자처럼 유니티 부트캠프 후기 35회차 (2) | 2025.07.07 |
| 유니티(공격 판정 / 체력바 / 아이템 드롭) _ 멋쟁이사자처럼 유니티 부트캠프 후기 33회차 (1) | 2025.07.05 |
| 유니티(몬스터 행동패턴 (Finite State Machine, 유한 상태 머신) 만들기 ) _ 멋쟁이사자처럼 유니티 부트캠프 후기 32회차 (1) | 2025.07.05 |
| 유니티(플랫포머 맵 만들기 ) _ 멋쟁이사자처럼 유니티 부트캠프 후기 31회차 (5) | 2025.07.03 |