[2025/07/08]TIL

오수호·2025년 7월 8일

TIL

목록 보기
36/60

턴제 RPG - ManiMind 적 타겟 로직 수정

  1. ManiMind 게임에서 적들이 사용할 스킬과 타겟을 플레이어가 행동을 하기 전에 알려줘야 할
    필요성이 생겼다. 이전에는 적의 턴이 시작될 때 행동과 타겟선택 로직을 돌았지만, 이제부터는 배틀 턴이 시작되기 전에 해당 로직을 돌아야 할 필요가 생긴 것이다.

  2. 타겟 선택 로직

  • ManiMind의 가중치 선택 로직은 이전에 만들어 두었던 WeightedSelect 클래스를 사용하여 구현하였다.

    가중치 선택로직 링크

  • 가중치 선택 로직의 기본이 되는 값은 PlayerUnit에 있는 Aggro 스텟이다. 적대치가 높을 수록 적 유닛에게 노려질 확률이 증가한다.
  1. 그래서 적 유닛의 행동과 타겟은 언제 처리해주어야 하는가? 처음 BattleManager에서 적들을 세팅해주는 SetEnemies메서드에서 행동과 타겟을 선택하는 메서드인 ChoiceAction을 호출하여 이를 처리하고, 이후에는 전체 한 턴의 종료를 알리는 BattleManager의 EndTurn메서드에서 살아있는 각각의 유닛들에 대하여 ChoiceAction을 호출하여 이를 처리하도록 만들었다.

    EnemyUnitController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using PlayerState;
using System;
using IdleState = EnemyState.IdleState;
using MoveState = EnemyState.MoveState;
using Random = UnityEngine.Random;
using StunState = EnemyState.StunState;

[RequireComponent(typeof(EnemySkillContorller))]
public class EnemyUnitController : BaseController<EnemyUnitController, EnemyUnitState>
{
    [SerializeField] private int id;
    public EnemyUnitSO MonsterSo { get; private set; }
    // Start is called before the first frame update

    private HPBarUI hpBar;
    public override bool IsAtTargetPosition => Agent.remainingDistance < setRemainDistance;
    public float setRemainDistance;

    public override bool IsAnimationDone
    {
        get
        {
            var info = Animator.GetCurrentAnimatorStateInfo(0);
            return info.IsTag("Action") && info.normalizedTime >= 0.9f;
        }
    }

    private float remainDistance;
    public Vector3 StartPostion { get; private set; }
    public WeightedSelector<Unit> mainTargetSelector;
    
    
    protected override void Awake()
    {
        SkillController = GetComponent<EnemySkillContorller>();
        base.Awake();
    }

    protected override void Start()
    {
        hpBar = HealthBarManager.Instance.SpawnHealthBar(this);
        StartPostion = transform.position;
    }

    // Update is called once per frame
    protected override void Update()
    {
        if (IsDead)
            return;

        base.Update();
    }


    public override void ChangeUnitState(Enum newState)
    {
        stateMachine.ChangeState(states[Convert.ToInt32(newState)]);
        CurrentState = (EnemyUnitState)newState;
    }

    public override void Initialize(UnitSO unitSO)
    {
        UnitSo = unitSO;
        if (UnitSo is EnemyUnitSO enemyUnitSo)
        {
            MonsterSo = enemyUnitSo;
        }

        if (MonsterSo == null)
            return;

        //Test
        if (PlayerDeckContainer.Instance.SelectedStage == null)
            StatManager.Initialize(MonsterSo);
        else
        {
            StatManager.Initialize(MonsterSo, this, PlayerDeckContainer.Instance.SelectedStage);
        }

        AnimatorOverrideController = new AnimatorOverrideController(Animator.runtimeAnimatorController);
        AnimationEventListener.Initialize(this);
        ChangeClip(Define.IdleClipName, MonsterSo.IdleAniClip);
        ChangeClip(Define.MoveClipName, MonsterSo.MoveAniClip);
        ChangeClip(Define.AttackClipName, MonsterSo.AttackAniClip);
        ChangeClip(Define.DeadClipName, MonsterSo.DeadAniClip);
        foreach (var skillData in MonsterSo.SkillDatas)
        {
            SkillManager.AddActiveSkill(skillData.skillSO);
        }

        SkillManager.InitializeSkillManager(this);
        EnemySkillContorller sc = SkillController as EnemySkillContorller;
        if (sc != null)
        {
            sc.InitSkillSelector();
        }
        InitTargetSelector();
        ChangeEmotion(MonsterSo.StartEmotion);
    }

    protected override IState<EnemyUnitController, EnemyUnitState> GetState(EnemyUnitState unitState)
    {
        return unitState switch
        {
            EnemyUnitState.Idle   => new IdleState(),
            EnemyUnitState.Move   => new MoveState(),
            EnemyUnitState.Return => new EnemyState.ReturnState(),
            EnemyUnitState.Attack => new EnemyState.AttackState(),
            EnemyUnitState.Skill  => new EnemyState.SkillState(),
            EnemyUnitState.Stun   => new StunState(),
            EnemyUnitState.Die    => new EnemyState.DeadState(),

            _ => null
        };
    }

    public override void Attack()
    {
        IsCompletedAttack = false;
        //어택 타입에 따라서 공격 방식을 다르게 적용
        IDamageable finalTarget = IsCounterAttack ? CounterTarget : Target;

        float hitRate = StatManager.GetValue(StatType.HitRate);
        if (CurrentEmotion is IEmotionOnAttack emotionOnAttack)
            emotionOnAttack.OnBeforeAttack(this, ref finalTarget);

        else if (CurrentEmotion is IEmotionOnHitChance emotionOnHit)
            emotionOnHit.OnCalculateHitChance(this, ref hitRate);

        bool isHit = Random.value < hitRate;
        if (!isHit)
        {
            Debug.Log("빗나갔지롱");
            return;
        }

        //TODO: 크리티컬 구현
        MonsterSo.AttackType.Execute(this, finalTarget);
        IsCompletedAttack = true;
    }

    public override void MoveTo(Vector3 destination)
    {
        Agent.SetDestination(destination);
    }

    public override void UseSkill()
    {
        SkillController.UseSkill();
    }

    public override void TakeDamage(float amount, StatModifierType modifierType = StatModifierType.Base)
    {
        if (IsDead)
            return;

        float finalDam = amount;
        if (modifierType == StatModifierType.Base)
        {
            //방어력 계산.
        }

        var curHp  = StatManager.GetStat<ResourceStat>(StatType.CurHp);
        var shield = StatManager.GetStat<ResourceStat>(StatType.Shield);

        if (shield.CurrentValue > 0)
        {
            float shieldUsed = Mathf.Min(shield.CurrentValue, finalDam);
            StatManager.Consume(StatType.Shield, modifierType, shieldUsed);
            finalDam -= shieldUsed;
        }

        if (finalDam > 0)
            StatManager.Consume(StatType.CurHp, modifierType, finalDam);
        Debug.Log($"공격 받음 {finalDam} 남은 HP : {curHp.Value}");
        if (curHp.Value <= 0)
        {
            Dead();
        }
    }

    public override void Dead()
    {
        IsDead = true;
        ChangeUnitState(EnemyUnitState.Die);
        StatusEffectManager.RemoveAllEffects();
        hpBar.UnLink();


        // gameObject.SetActive(false);
    }

    public bool ShouldUseSkill()
    {
        if (SkillController.CheckAllSkills() && Random.value < MonsterSo.skillActionProbability) return true;
        else return false;
    }
    
    public void InitTargetSelector()
    {
        mainTargetSelector = new WeightedSelector<Unit>();
        var playerUnits = BattleManager.Instance.PartyUnits;
        foreach (var playerUnit in playerUnits)
        {
            mainTargetSelector.Add(
                playerUnit,
                () => playerUnit.StatManager.GetValue(StatType.Aggro),
                ()=> !playerUnit.IsDead
            );
        }
    }

    public void WeightedMainTargetSelector(SelectCampType campType)
    {
        if (campType == SelectCampType.Enemy)
        {
            var allies = BattleManager.Instance.GetAllies(this);
            SetTarget(allies[Random.Range(0, allies.Count)]);
        }
        else if (campType == SelectCampType.Player)
        {
            SetTarget(mainTargetSelector.Select());
        }
    }
    public void SelectMainTarget(ActionType actionType)
    {
        if (actionType == ActionType.SKill)
        {
            EnemySkillContorller sc = SkillController as EnemySkillContorller;
            WeightedMainTargetSelector(sc.CurrentSkillData.skillSo.selectCamp);
        }
        else if (actionType == ActionType.Attack)
        {
            WeightedMainTargetSelector(SelectCampType.Player);
        }
    }

    public void ChoiceAction()
    {
        if (ShouldUseSkill())
        {
            EnemySkillContorller sc = SkillController as EnemySkillContorller;
            if (sc != null)
            {
                sc.WeightedSelectSkill();
                ChangeAction(ActionType.SKill);
                SelectMainTarget(ActionType.SKill);
            }
        }
        else
        {
            ChangeAction(ActionType.Attack);
            SelectMainTarget(ActionType.Attack);
        }
    }



    public override void StartTurn()
    {
        if (IsDead || IsStunned)
        {
            BattleManager.Instance.TurnHandler.OnUnitTurnEnd();
            return;
        }

        ChangeTurnState(TurnStateType.StartTurn);
    }

    public override void EndTurn()
    {
        if (!IsDead)
            CurrentEmotion.AddStack(this);

        ChangeAction(ActionType.None);
        BattleManager.Instance.TurnHandler.OnUnitTurnEnd();
        ChangeUnitState(PlayerUnitState.Idle);
    }
}

BattleManager.cs

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

public class BattleManager : SceneOnlySingleton<BattleManager>
{
    //Test
    public List<Transform> PartyUnitsTrans;
    public List<Transform> EnemyUnitsTrans;
    public List<int> PartyUnitsID;
    public List<int> EnemyUnitsID;
    public List<Unit> PartyUnits;
    public List<Unit> EnemyUnits;
    public TurnHandler TurnHandler { get; private set; }

    private List<Unit> allUnits = new List<Unit>();
    public event Action OnBattleEnd;


    protected override void Awake()
    {
        base.Awake();
    }

    private void Start()
    {
        if (PlayerDeckContainer.Instance.CurrentDeck.deckDatas.Count == 0)
            SetAlliesUnit(PartyUnitsID.Select(id => TableManager.Instance.GetTable<PlayerUnitTable>().GetDataByID(id)).ToList());
        else
        {
            SetAlliesUnit(PlayerDeckContainer.Instance.CurrentDeck);
        }

        if (PlayerDeckContainer.Instance.SelectedStage == null)
            SetEnemiesUnit(EnemyUnitsID.Select(id => TableManager.Instance.GetTable<MonsterTable>().GetDataByID(id)).ToList());
        else
        {
            SetEnemiesUnit(PlayerDeckContainer.Instance.SelectedStage.Monsters);
        }

        TurnHandler = new TurnHandler();
        SetAllUnits(PartyUnits.Concat(EnemyUnits).ToList());
    }

    public void SetAlliesUnit(List<PlayerUnitSO> units)
    {
        int index = 0;
        foreach (PlayerUnitSO playerUnitSo in units)
        {
            GameObject go = Instantiate(playerUnitSo.UnitPrefab, Vector3.zero, Quaternion.identity);
            go.transform.SetParent(PartyUnitsTrans[index].transform);
            go.transform.localPosition = Vector3.zero;
            go.transform.localRotation = Quaternion.identity;
            Unit unit = go.GetComponent<Unit>();
            unit.Initialize(playerUnitSo);
            PartyUnits.Add(unit);
            index++;
        }
    }

    public void SetAlliesUnit(PlayerDeck playerDeck)
    {
        int index = 0;
        foreach (EntryDeckData deckData in playerDeck.deckDatas)
        {
            GameObject go = Instantiate(deckData.CharacterSo.UnitPrefab, Vector3.zero, Quaternion.identity);
            go.transform.SetParent(PartyUnitsTrans[index].transform);
            go.transform.localPosition = Vector3.zero;
            go.transform.localRotation = Quaternion.identity;
            PlayerUnitController unit = go.GetComponent<PlayerUnitController>();
            unit.Initialize(deckData.CharacterSo);
            PartyUnits.Add(unit);
            foreach (EquipmentItem equipment in deckData.equippedItems.Values)
            {
                unit.EquipmentManager.EquipItem(equipment);
            }

            unit.SkillManager.selectedSkill = deckData.skillDatas.ToList();

            index++;
        }
    }

    public void SetEnemiesUnit(List<EnemyUnitSO> units)
    {
        int index = 0;
        foreach (EnemyUnitSO enemyUnitSo in units)
        {
            GameObject go = Instantiate(enemyUnitSo.UnitPrefab, Vector3.zero, Quaternion.identity);
            go.transform.SetParent(EnemyUnitsTrans[index].transform);
            go.transform.localPosition = Vector3.zero;
            go.transform.localRotation = Quaternion.identity;
            EnemyUnitController unit = go.GetComponent<EnemyUnitController>();
            unit.Initialize(enemyUnitSo);
            unit.ChoiceAction();
            EnemyUnits.Add(unit);
            index++;
        }
    }

    public void SetAllUnits(List<Unit> units)
    {
        allUnits = units;
        TurnHandler.Initialize(allUnits);
    }

    public void StartTurn()
    {
        TurnHandler.StartNextTurn();
    }

    public void EndTurn()
    {
        foreach (Unit unit in allUnits)
        {
            if (unit.IsDead)
                continue;
            unit.StatusEffectManager?.OnTurnPassed();
        }


        Debug.Log("배틀 턴이 종료 되었습니다.");
        // allUnits.RemoveAll(u => u.IsDead);
        PartyUnits.ForEach(x => x.ChangeUnitState(PlayerUnitState.Idle));
        if (EnemyUnits.TrueForAll(x => x.IsDead))
        {
            Debug.Log("승리");
            PartyUnits.Where(x => !x.IsDead).ToList().ForEach(x => x.ChangeUnitState(PlayerUnitState.Victory));
        }

        foreach (var enemy in EnemyUnits)
        {
            EnemyUnitController enemyUnit = enemy as EnemyUnitController;
            if (enemyUnit != null && !enemyUnit.IsDead)
            {
                enemyUnit.ChoiceAction();
            }
        }
        CommandPlanner.Instance.Clear();    // 턴 종료되면 전략 플래너도 초기화
        InputManager.Instance.Initialize(); // 턴 종료되면 인풋매니저도 초기화
        OnBattleEnd?.Invoke();
    }

    public List<Unit> GetAllies(Unit unit)
    {
        if (PartyUnits.Contains(unit))
            return PartyUnits.Where(u => !u.IsDead && u != unit).ToList();
        else
            return EnemyUnits.Where(u => !u.IsDead && u != unit).ToList();
    }

    public List<Unit> GetEnemies(Unit unit)
    {
        if (PartyUnits.Contains(unit))
            return EnemyUnits.Where(u => !u.IsDead && u != unit).ToList();
        else
            return PartyUnits.Where(u => !u.IsDead && u != unit).ToList();
    }

    protected override void OnDestroy()
    {
        base.OnDestroy();
    }
}

Rider에디터에서 디버깅 모드를 할 수 없는 문제 해결

  • 라이더에서 중단점이 먹히지 않는 문제가 있었다.
    • 위와 같이 라이더에서 중단점을 사용할 수 없는 버그가 생겼다.

해결 방법

  • 유니티 에디터 좌 하단에 있는 디버그모드 에러를 고치면 문제를 해결할 수 있다.

    • Switch ro release mode를 눌러 해결한다.
  • 해당 버그는 유니티 에디터상에서는 리빌딩 되었는데 라이더IDE에서 리빌딩 되지않아 생기는 문제라고 한다.

profile
게임개발자 취준생입니다

0개의 댓글