ManiMind 게임에서 적들이 사용할 스킬과 타겟을 플레이어가 행동을 하기 전에 알려줘야 할
필요성이 생겼다. 이전에는 적의 턴이 시작될 때 행동과 타겟선택 로직을 돌았지만, 이제부터는 배틀 턴이 시작되기 전에 해당 로직을 돌아야 할 필요가 생긴 것이다.
타겟 선택 로직
그래서 적 유닛의 행동과 타겟은 언제 처리해주어야 하는가? 처음 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();
}
}

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

해당 버그는 유니티 에디터상에서는 리빌딩 되었는데 라이더IDE에서 리빌딩 되지않아 생기는 문제라고 한다.