🧠 오늘의 핵심 정리

  • 펫 시스템 추가: 3종 프리팹을 플레이어 AttachPoint에 Instantiate, 같이 달리는 재미 요소 넣기
  • 펫 1회 보호 구현: 충돌/피격 시 펫이 한 번 대신 맞고 스킬 카운트 감소
  • 펫 애니메이션 트리거 안 먹는 문제 원인: 펫이 런타임 Instantiate라 인스펙터 배열이 무의미 → GetComponentsInChildren<Animator>()로 해결
  • 펫/플레이어 피격 이펙트 분리 : ShowEffect() + ShowPetEffect()로 각각 관리

 

 

🐾 본캠프 38일차 : 펫 시스템 추가 + 점프 애니메이션 안정화

러너 게임을 만들다 보니 “기능은 다 만들었다” 느낌이 들었는데… 여기서 끝내면 너무 아쉽더라고요. 그래서 플레이어 옆에서 같이 뛰어주는 펫을 넣어서 소소한 재미를 추가했습니다. 거창한 성장형 펫 이런 건 아니고, 플레이어 머리 왼쪽 위에 고정되어 같이 달리는 정도! 대신 그냥 장식이면 재미가 없으니까, 장애물에 부딪힐 때 펫이 1번 대신 맞아주는 기능까지 넣었습니다.

 

 

🐶 펫 시스템 : 3종 프리팹 + AttachPoint 스폰

펫은 3종류(BlueRobot / YellowRobot / Cat)로 만들었고, 전부 프리팹화한 뒤 게임 시작 시 플레이어 자식의 AttachPoint에 Instantiate 되도록 구성했습니다. GameManager에는 “해금 여부”와 “장착된 펫” 정보를 들고 있게 만들어서, 이후 확장(해금 UI, 상점 등)도 붙일 수 있게 기반을 잡아뒀습니다.

GameManager : 펫 해금/장착 상태

// GameManager.cs
public enum PetType { None, BlueRobot, YellowRobot, Cat }

public Dictionary<PetType, bool> petUnlocks = new Dictionary<PetType, bool>(); // 펫 해금여부
public PetType equippedPet = PetType.None; // 펫 장착

private void Awake()
{
// ...
petUnlocks[PetType.BlueRobot] = true;
petUnlocks[PetType.YellowRobot] = true;
petUnlocks[PetType.Cat] = true;
equippedPet = PetType.Cat;
}

그리고 실제 스폰은 PetSpawner에서 장착된 타입을 보고 프리팹을 선택해서 AttachPoint에 붙입니다. (러너게임이라 “플레이어랑 같이 뛰는” 느낌이 핵심이라서, 부모를 AttachPoint로 잡고 로컬 포지션/회전 초기화로 고정했습니다.)

PetSpawner : 장착된 펫 Instantiate

// PetSpawner.cs (핵심 흐름)
PetType petType = GameManager.Instance.equippedPet;
if (petType == PetType.None) return;
if (!GameManager.Instance.petUnlocks[petType]) return;

GameObject prefabToSpawn = null;
switch (petType)
{
case PetType.BlueRobot: prefabToSpawn = blueRobotPrefab; break;
case PetType.YellowRobot: prefabToSpawn = yellowRobotPrefab; break;
case PetType.Cat: prefabToSpawn = catPrefab; break;
}

if (prefabToSpawn != null)
{
petInstance = Instantiate(prefabToSpawn);
petInstance.transform.SetParent(attachPoint, false);
petInstance.transform.localPosition = Vector3.zero;
petInstance.transform.localRotation = Quaternion.identity;
}

 

 

🛡️ “펫이 한 번 대신 맞아주기” 구현

펫이 단순 장식이면 재미가 없어서, 장애물에 부딪힐 때 펫이 1회 보호막처럼 대신 맞아주는 기능을 넣었습니다. 구조는 간단하게 skillCount로 “펫 보호 횟수”를 들고 있다가, 데미지/즉사 들어오기 전에 먼저 체크해서 펫이 막으면 return하는 흐름입니다.

PlayerCondition : 펫 보호 1회

// PlayerCondition.cs
int skillCount = 0;

private bool PetProtectsPlayer()
{
if (GameManager.Instance.equippedPet != PetType.None && skillCount > 0)
{
animationHandler.ShowPetEffect();
skillCount--;
TriggerPetAnimtion("DoRotate");
return true; // 펫이 대신 맞아줌
}
return false;
}

public void InstantDeath()
{
if (PetProtectsPlayer()) return;
// ...
}

public void Damaged()
{
if (PetProtectsPlayer()) return;
// ...
}

이 방식의 장점은 “피격 루트가 여러 개여도(InstantDeath / Damaged / Slow 등) 펫 보호 로직을 한 곳에서 처리”할 수 있다는 점이었습니다. 그리고 펫이 막아줄 때는 펫 전용 이펙트까지 같이 나오게 해서 ‘대신 맞아줬다’ 느낌을 살렸습니다.

AnimationHandler : 펫 이펙트 분리

// AnimationHandler.cs
public GameObject _gameObject;  // 플레이어 타격 이펙트
public GameObject _gameObject2; // 펫 타격 이펙트

public void ShowPetEffect()
{
StartCoroutine(PetEffectCoroutine());
}

private IEnumerator PetEffectCoroutine()
{
_gameObject2.SetActive(true);
yield return new WaitForSeconds(2);
_gameObject2.SetActive(false);
}

 

 

🧠 펫 애니메이션 트리거가 안 먹던 이유 : Instantiate의 함정

오늘 제일 크게 배운 건 이거였습니다. 저는 처음에 “[SerializeField] Animator[] petAnimators로 인스펙터에 연결하면 되겠지?”라고 생각했는데… 펫은 런타임에 Instantiate로 생성되잖아요?

즉, 인스펙터에 미리 연결한 배열은 생성된 펫의 Animator를 가리킬 수가 없음… 이걸 놓치고 있었던 거죠.

그래서 Start에서 자식 포함 Animator를 전부 긁어와서 펫을 바꾸거나 새로 생성해도 유연하게 동작하게 수정했습니다.

GetComponentsInChildren로 런타임 펫 Animator 연결

// PlayerCondition.cs
[SerializeField] private Animator[] petAnimators;

void Start()
{
// ...
petAnimators = GetComponentsInChildren();
}

추가로, 트리거 이름이 모든 펫에 존재하지 않으면 에러/무반응이 날 수 있어서 파라미터가 있는지 검사한 뒤 트리거를 쏘는 방식도 넣었습니다.

TriggerPetAnimation : 파라미터 존재 여부 체크 후 실행

// PlayerCondition.cs
private bool HasParameter(Animator animator, string paramName)
{
    foreach (var param in animator.parameters)
    {
        if (param.name == paramName) return true;
    }
    return false;
}

private void TriggerPetAnimtion(string triggerName)
{
if (GameManager.Instance.equippedPet == PetType.None) return;

```
foreach (var anim in petAnimators)
{
    if (anim != null && HasParameter(anim, triggerName))
    {
        anim.SetTrigger(triggerName);
    }
}
```

}

이거 진짜 다음에 또 똑같이 실수할 것 같아서… 무조건 기억해둘 포인트로 남겨둡니다. 런타임 Instantiate 오브젝트는 인스펙터 연결이 아니라 “런타임 탐색/바인딩”이 기본…!

 

 

📌 오늘의 회고

오늘은 “제출 하루 전”이라는 사실이 가장 무서웠습니다… 그래서 펫도 원래는 “종류별 능력”까지 넣고 싶었는데, 현실적으로는 펫 1회 보호 정도가 딱 마지노선이었습니다. 그래도 기능적으로는 확실히 “재미 요소”가 추가돼서 만족!

그리고 펫 애니메이션 문제를 통해, Instantiate 오브젝트는 인스펙터 연결로 해결이 안 된다는 걸 제대로 체감했습니다. 다음부터는 런타임에 붙는 오브젝트들은 “탐색해서 바인딩”하는 방식을 습관처럼 쓰게 될 것 같습니다.

 

 

🔜 내일 할 일

  • 펫 “대신 맞기” 연출 다듬기 (회전 트리거 + 이펙트 타이밍 조정)
  • 제출용 버그 체크 : 애니메이션 루프/전이, 펫 스폰 조건, GameState 입력 가드 재확인