본문 바로가기
개발일지/게임개발

Project DT - 적 공격 구현

by 라이티아 2025. 9. 10.

현재 구조이다

 

여기에 적이 플레이어를 공격하는 구조를 추가한다

 

매니저를 추가로 넣어어 전투와 관련된 부분을 처리하도록 하고, 플레이어와 적을 하나의 부모를 가지도록 변경한다

ApplyDamage에 객체가 2개가 있으면 전투 기록을 하기에 좋을 것 같지만, 아직 너무 이른 기능 같아서 제거후, 간단히 데미지를 주도록 한다

 

using System.Collections.Generic;
using UnityEngine;

public class BattleManager : MonoSingleton<BattleManager>
{
    private Character _player;
    private List<Character> _enemys = new List<Character>();

    public void ApplyDamage(Character Target, int Damage)
    {
        Target.SetHP(Target.GetHP() - Damage);
    }
}
using UnityEngine;

public class Character : MonoBehaviour
{
    protected int _hp = 10;

    public int GetHP()
    {
        return _hp;
    }

    public void SetHP(int HP)
    {
        _hp = HP;
    }
}

 

이렇게 구현을 했더니, 문제가 이렇게 할 시 플레이어 인스턴스나 적 인스턴스를 각자 가지고 있어야 하고, 귀찮게 싱글톤을 돌아서 호출하는 슬픈 현상이 생길 것 같다

using System.Collections.Generic;
using UnityEngine;

public class BattleManager : MonoSingleton<BattleManager>
{
    private Character _player;
    private List<Character> _enemys = new List<Character>();

    public void ApplyDamageToPlayer(int Damage)
    {
        _player.SetHP(_player.GetHP() - Damage);
    }
    public void ApplyDamageToEnemy(int Damage, int num)
    {
        _enemys[num].SetHP(_enemys[num].GetHP() - Damage);
    }
}

약간 처리를 분리해서 이렇게 하는 방향을 생각했다

 

GPT의 조언

문제점

  1. SetHP 직접 호출
    HP 감소 로직이 BattleManager 안에 박혀 있어서, 캐릭터별 방어력·버프·사망 처리 같은 후속 로직을 넣기 어렵습니다.
    → Character 내부에서 TakeDamage(int amount)로 처리하는 게 안전합니다.
  2. 인덱스로 적 지정
    ApplyDamageToEnemy(int Damage, int num)처럼 리스트 인덱스로만 접근하면, 적이 죽어서 리스트가 변할 때 꼬일 수 있습니다.
    → Character 참조 자체를 파라미터로 받는 게 유연합니다.
  3. 플레이어/적 분리
    지금은 ApplyDamageToPlayer와 ApplyDamageToEnemy가 따로 있지만, 사실 둘 다 “타겟에게 데미지를 적용”이라는 같은 행위입니다.
    → 메서드 시그니처는 통합하는 편이 낫습니다.

해당 조언에 맞게 코드를 수정한다

 

using System.Collections.Generic;
using UnityEngine;

public class BattleManager : MonoSingleton<BattleManager>
{
    private Character _player;
    private List<Character> _enemys = new List<Character>();

    public void ApplyDamage(Character target, int damage)
    {
        // 필요하면 여기서 방어력, 크리티컬 계산
        int finalDamage = damage; 
        target.TakeDamage(finalDamage);
    }
}
using UnityEngine;

public class Character : MonoBehaviour
{
    protected int _hp = 10;

    public int GetHP()
    {
        return _hp;
    }

    public void SetHP(int HP)
    {
        _hp = HP;
    }

    public void TakeDamage(int amount)
    {
        _hp -= amount;
        if (_hp <= 0)
        {
            _hp = 0;
            Die();
        }
    }
    private void Die()
    {
        Debug.Log($"{name} is dead");
        // 사망 처리 로직
    }
}

Character쪽에 좀 더 함수를 추가하여 처리한다

 

이제 데미지를 넣을 수 있는 처리는 끝났고, 적 입장에서 어떻게 패턴을 가지며 공격할지 구현한다

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : Character
{
    private bool _acting = false;
    [SerializeField]
    private List<Movement> _movements = new List<Movement>();
    [SerializeField]
    private int _moveIndex = 0;
    void Start()
    {
        if (TurnManager.Instance != null)
            TurnManager.Instance.OnTurnChanged += TurnChanged;
    }

    private void TurnChanged(TurnManager.TurnOwner owner)
    {
        if (owner == TurnManager.TurnOwner.Enemy)
        {
            Debug.Log("Enemy's Turn Start");
            if (_acting) return;
            _acting = true;
            if (_movements[_moveIndex] != null)
            {
                _movements[_moveIndex].gameObject.SetActive(true);
                _movements[_moveIndex].Action();
                StartCoroutine(EnemyTurnEnd());
            }
            }
            else
            {
                Debug.Log("Enemy's Turn End");
                _acting = false;
            }
    }

    public IEnumerator EnemyTurnEnd()
    {
        yield return new WaitForSeconds(5f);
        if (TurnManager.Instance != null)
        {
            _moveIndex = (_moveIndex > _movements.Count) ? 0 : _moveIndex + 1;
            TurnManager.Instance.NextTurn();
        }
    }
}
using UnityEngine;

public class Movement : MonoBehaviour
{
    private enum ActionState
    {
        attack,
        defence,
        effect
    }
    private Enemy _enemy;
    [SerializeField]
    private ActionState _actionState = new ActionState();

    [SerializeField]
    private int _actionValue = 3;
    void Awake()
    {
        _enemy = transform.parent.GetComponent<Enemy>();
    }
    public void Action()
    {
        switch (_actionState)
        {
            case ActionState.attack:
                BattleManager.Instance.ApplyDamage(BattleManager.Instance.GetPlayer(), _actionValue);
                break;
            case ActionState.defence:
                break;
            case ActionState.effect:
                break;
            default:
                Debug.LogError($"{gameObject.name} = Movement : use undefined ActionState");
                break;
        }
        gameObject.SetActive(false);
    }
}

 


적의 ai는 노드를 활용한 전처리 방식을 사용하려 한다

 

플레이어의 턴에서는 상태를 변경할 수 있는 상태가 되어 여려 변경 사항을 검사하며, 변경 사항에 따라 행동을 정한 뒤 자신의 턴이 오면 그 행동을 하는 형태로 설계 한다

각 노드들은 턴 시작시 action함수를 enemy에서 호출받아 실행하며, 이게 해당 턴에 적이 행동할 주체가 된다

이제 유니티에서 이렇게 세팅 후

void Awake()
{
_movements = GetComponentsInChildren<Movement>(true).ToList();
}

awake에서 linq로 자식중 movement가 있는걸 list로 가져온다

 

자동으로 객체가 담기는것을 볼 수 있다

 

 

차례대로 잘 작동하는 것을 확인할 수 있다

 

_moveIndex = (_moveIndex >= _movements.Count) ? 0 : _moveIndex + 1;

다만 진행중 끝 노드에 도착시 다시 원점 복귀를 못하고 있다

 

_moveIndex = (_moveIndex + 1) % _movements.Count;

조건식을 해당 방식으로 변경하여 해결한다