[2025_11_19]아이템 효과 플레이어와 연동

2025. 11. 19. 21:57TIL

아이템 효과 시스템 구현 - 버프 관리와 스탯 복구

 

구현한 기능

아이템 시스템 구조

Item (충돌 감지)
    ↓
PlayerController.ApplyItemEffect()
    ↓
ItemEffectManager.ApplyItem()
    ↓
버프 활성화 + 스탯 적용
    ↓
ManagerRoot.Update() → itemEffectManager.Update()
    ↓
만료 시 원본 스탯 복구

 

핵심 구현 사항

1. 버프 중복 처리

 
csharp
private void ApplyBuff(ItemData itemData)
{
    // 같은 타입의 버프가 이미 있는지 확인
    ActiveItem existing = activeItems.Find(x => x.itemData.itemType == itemData.itemType);
    
    if (existing != null)
    {
        // 이미 있으면 시간만 갱신
        existing.remainingTime = itemData.duration;
        existing.itemData = itemData;
    }
    else
    {
        // 새로 추가
        ActiveItem buff = new ActiveItem { };
        activeItems.Add(buff);
    }
}

왜 이렇게?

  • JumpUp 버프 중복 시 → 중첩 X, 시간만 연장
  • 스택처럼 쌓이면 점프력이 너무 높아짐

2. 원본 스탯 저장 & 복구

 
csharp
// 게임 시작 시 원본 저장
public void SaveOriginalStats(float jumpPower, float maxHeight)
{
    originalJumpPower = jumpPower;
    originalMaxHeight = maxHeight;
}

// 버프 적용
private void ApplyJumpStats(ItemData itemData)
{
    float bonus = GetValue(itemData, 0, 0);
    player.SetJumpPower(originalJumpPower + bonus);  // 원본 + 보너스
}

// 버프 만료 시 복구
private void RestoreJumpStats()
{
    player.SetJumpPower(originalJumpPower);  // 원본으로 되돌림
}

중요 포인트:

  • 항상 원본 기준으로 계산 → 원본 + 보너스
  • 만료 시 원본으로 복구 → 정확한 값 보장

3. 시간 기반 버프 관리

 
 
csharp
public void Update()
{
    // 역순으로 순회 (RemoveAt 때문에)
    for (int i = activeItems.Count - 1; i >= 0; i--)
    {
        activeItems[i].remainingTime -= Time.deltaTime;
        
        if (activeItems[i].remainingTime <= 0)
        {
            OnBuffExpired(activeItems[i]);  // 스탯 복구
            activeItems.RemoveAt(i);         // 리스트에서 제거
        }
    }
}

역순 순회 이유:

csharp
// 정순으로 하면 문제 발생
for (int i = 0; i < list.Count; i++)
{
    list.RemoveAt(i);  // i번째 제거
    // 다음 요소들이 앞으로 당겨짐
    // i+1은 이제 원래 i+2 요소
    // 한 요소를 건너뜀!
}

// 역순이면 안전
for (int i = list.Count - 1; i >= 0; i--)
{
    list.RemoveAt(i);  // 뒤에서부터 제거
    // 앞 요소들에 영향 없음
}

 

개선할 점

1. Item.cs - 초기화 순서 문제

현재 코드:

 
 
csharp
private void OnEnable()
{
    ResetItem();  // meshRenderer 사용
}

private void Start()
{
    itemData = ManagerRoot.Instance.dataManager.GetItemData(itemID);  // 늦음
}

문제:

  • OnEnable이 Start보다 먼저 실행됨
  • itemData가 null인 상태로 로직 실행 가능

개선안:

 
 
csharp
private void Awake()
{
    meshRenderer = GetComponent<MeshRenderer>();
    itemCollider = GetComponent<Collider>();
    // Awake에서 itemData 로드
    itemData = ManagerRoot.Instance.dataManager.GetItemData(itemID);
    audioManager = ManagerRoot.Instance.audioManager;
}

private void OnEnable()
{
    ResetItem();
}

 

2. ManagerRoot의 Update 호출 - 현재 구조 분석

현재 코드:

 
 
csharp
public class ManagerRoot : Singleton<ManagerRoot>
{
    public ItemEffectManager itemEffectManager;
    
    protected override void Init()
    {
        itemEffectManager = new ItemEffectManager();  // 일반 클래스
    }
    
    private void Update()
    {
        itemEffectManager?.Update();  // 수동 호출
    }
}

현재 방식의 장단점:

장점:

  • ItemEffectManager가 MonoBehaviour에 의존하지 않음
  • 단위 테스트가 쉬움 (GameObject 없이 테스트 가능)
  • 생명주기를 ManagerRoot가 완전히 제어

단점:

  • ManagerRoot가 여러 매니저의 Update를 호출하면 코드가 늘어남

개선안: 통합 Update 관리

csharp
public class ManagerRoot : Singleton<ManagerRoot>
{
    private List<IUpdatable> updatableManagers = new();
    
    protected override void Init()
    {
        itemEffectManager = new ItemEffectManager();
        updatableManagers.Add(itemEffectManager);
        
        // 다른 매니저도 추가 가능
        // updatableManagers.Add(buffManager);
        // updatableManagers.Add(effectManager);
    }
    
    private void Update()
    {
        foreach (var manager in updatableManagers)
        {
            manager.Update();
        }
    }
}

// 인터페이스 정의
public interface IUpdatable
{
    void Update();
}

// ItemEffectManager에 적용
public class ItemEffectManager : IUpdatable
{
    public void Update()
    {
        
    }
}

 

3. ItemData.value의 의미 불명확

현재:

 
csharp
public int[] value;  // 뭘 의미하는지 알 수 없음!

// 사용할 때도 매직 넘버
float jumpPowerBonus = GetValue(itemData, 0, 0);  // 0번째가 점프력?
float maxHeightBonus = GetValue(itemData, 1, 0);  // 1번째가 높이?

개선안 : Enum으로 인덱스 명확화

 
csharp
public class ItemEffectManager
{
    private enum JumpUpValueIndex
    {
        JumpPower = 0,
        MaxHeight = 1
    }
    
    private enum MagnetValueIndex
    {
        Range = 0
    }
    
    private void ApplyJumpStats(ItemData itemData)
    {
        float jumpPowerBonus = GetValue(itemData, (int)JumpUpValueIndex.JumpPower, 0);
        float maxHeightBonus = GetValue(itemData, (int)JumpUpValueIndex.MaxHeight, 0);
        // 훨씬 명확함!
    }
}

 

4. 매니저 초기화 개선

현재 ManagerRoot:

 
csharp
protected override void Init()
{
    dataManager = new DataManager("Data/Item", "Items");
    itemEffectManager = new ItemEffectManager();
    gameManager = _gameManager;
    
    gameManager?.Init();
    dataManager?.Init();
}

 

개선안:

csharp
protected override void Init()
{
    Debug.Log("ManagerRoot 초기화 시작");
    
    try
    {
        // 1. 매니저 생성
        dataManager = new DataManager("Data/Item", "Items");
        itemEffectManager = new ItemEffectManager();
        gameManager = _gameManager;
        
        // 2. 의존성 순서대로 초기화
        InitializeManager(dataManager, "DataManager");
        InitializeManager(gameManager, "GameManager");
        InitializeManager(scoreManager, "ScoreManager");
        InitializeManager(sceneController, "SceneController");
        InitializeManager(audioManager, "AudioManager");
        
        Debug.Log("ManagerRoot 초기화 완료");
    }
    catch (Exception e)
    {
        Debug.LogError($"ManagerRoot 초기화 실패: {e.Message}");
    }
}

private void InitializeManager<T>(T manager, string managerName) where T : class
{
    if (manager == null)
    {
        Debug.LogWarning($"{managerName}가 null입니다.");
        return;
    }
    
    // IInitializable 인터페이스가 있다면
    if (manager is IInitializable initializable)
    {
        initializable.Init();
        Debug.Log($"{managerName} 초기화 완료");
    }
}

// 인터페이스 정의
public interface IInitializable
{
    void Init();
}

 

잘한 점

1. 버프 중복 처리

  • 같은 버프 여러 개 -> 시간만 갱신
  • 스택되지 않아 밸런스 유지

2. 원본 스탯 복구

  • 버프 만료 시 정확한 값으로 되돌림
  • 다른 버프의 영향 없음

3. 확장 가능한 구조

 
csharp
// 새 아이템 추가가 쉬움
public enum ItemType
{
    Coin,
    JumpUp,
    Magnet,
    Shield,     // 새로 추가
    SpeedBoost  // 새로 추가
}

// switch-case만 추가하면 됨

 

배운 점

1. List 순회 시 역순 주의

 
csharp
// RemoveAt 할 때는 반드시 역순!
for (int i = list.Count - 1; i >= 0; i--)

 

 

고민할 점

Q: 버프가 100개면 Update가 느리지 않을까?

  • A: 아이템은 보통 3~5개 동시 활성화
  • 필요하면 코루틴으로 변경

Q: 원본 스탯을 매번 저장할까, 한 번만 저장할까?

  • 한 번만: 간단, 업그레이드 시 문제
  • 매번: 복잡, 업그레이드 안전→ 버프 최초 적용 시에만 저장하는 방식 추천
    ->원본 스탯 변경 시 그때 다시 저장하는 방식

Q: value를 배열로 관리 vs 구조체?

  • 배열: JSON 연동 쉬움, 의미 불명확
  • 구조체: 명확, JSON 파싱 복잡
  • ->어떻게 해야 할까...?

Q: ManagerRoot에서 Update를 직접 호출 vs 이벤트?

  • 직접 호출: 간단, 명확, 성능 좋음
  • 이벤트: 느슨한 결합, 확장성
  • -> 매니저가 적으면 직접 호출, 많으면 인터페이스/이벤트