내일배움캠프 55일차 7/03 TIL + 유닛 공격 구현
유닛 스탯 구현
본격적으로 유닛이 공격하는 시스템을 만들기 위해 기본 스탯들을 설정했다. UnitStat
을 상속한 UserUnitStat
을 만들어 필요한 스탯만 추려내는 과정을 거쳤다.
public class UserUnitStat : UnitStat
{
#region Private Field
private ResourceStat mp;
private AttributeStat attack;
private AttributeStat attackSpeed;
private AttributeStat attackRange;
private AttributeStat cost;
private NavMeshAgent agent;
UnitStat
은 이름, 설명, 레벨, 이동 속도만 가지고 있다. 공격이나 생산 비용 등은 UserUnitStat
에 추가했다. ResourceStat
과 AttributeStat
은 이전 개인 과제에서 만든 클래스를 활용했다.
public class ResourceStat
{
string statName;
protected int maxValue;
protected int currentValue;
public Image Image;
public TextMeshProUGUI Text;
public ResourceStat(string name)
{
statName = name;
}
public int MaxValue
{
get { return maxValue; }
set
{
if (maxValue <= 0)
{
currentValue = value;
}
else
{
currentValue = (currentValue * value) / maxValue;
}
maxValue = value <= 0 ? 1 : value;
UpdateUI();
}
}
public int CurrentValue
{
get { return currentValue; }
set
{
currentValue = value < 0 ? 0 : value;
if (currentValue > maxValue)
{
currentValue = maxValue;
}
UpdateUI();
}
}
private void UpdateUI()
{
if (Image != null)
{
Image.fillAmount = (float)currentValue / maxValue;
}
if (Text != null)
{
Text.text = $"{statName}: {currentValue}/{maxValue}";
}
}
}
ResourceStat
클래스는 최대값과 현재값을 관리하며, 값이 변경될 때마다 UI도 업데이트된다. AttributeStat
도 비슷하지만 공격력 같은 값을 표현할 때 기본값과 추가값으로 이루어져 있다.
유닛 공격 구현
공격 구현의 계획은 A
를 누르면 마우스에 공격 준비 마커가 표시되고, 이 상태에서 좌클릭을 하면 해당 지점으로 공격을 실행한다. 적을 선택했을 경우 적을 공격하는 기능도 포함된다.
public void OnMouseLeftClick(InputAction.CallbackContext context)
{
MouseStartPos = mouseDrag.ReadValue<Vector2>();
Vector2 worldMousePos = mainCamera.ScreenToWorldPoint(MouseStartPos);
RaycastHit2D hit = Physics2D.Raycast(worldMousePos, Vector2.zero);
if (isReadyToAttack)
{
HandleAttackClick(hit, worldMousePos);
}
else
{
HandleSelectionClick(hit);
}
isReadyToAttack = false;
}
좌클릭시 공격 준비 여부에 따라 기능을 분리했다.
private void HandleAttackClick(RaycastHit2D hit, Vector2 worldMousePos)
{
attackPositionMarker.gameObject.SetActive(false);
if (hit.collider != null)
{
ISelectable selectable = hit.collider.GetComponent<ISelectable>();
if (selectable != null && selectable.SelectType() == SelectableType.Enemy)
{
foreach (var unit in selectedUnits)
{
// 해당 유닛을 찾아가서 공격 구현해야함
}
}
}
else
{
MoveUnitsToAttackPosition(worldMousePos);
}
}
private void MoveUnitsToAttackPosition(Vector2 worldMousePos)
{
if (selectedUnits.Count > 0)
{
targetPositionMarker.gameObject.SetActive(true);
targetPositionMarker.SetPosition(worldMousePos);
float offset = 0;
foreach (var unit in selectedUnits)
{
((IAttackable)unit)?.AttackWhileMoving(worldMousePos, offset);
offset += 0.7f;
}
}
}
공격 준비 상태에서 적이 있을 경우 적을 추적하고, 적이 없을 경우 땅을 찍어 해당 지점으로 이동하면서 공격한다.
public void AttackWhileMoving(Vector2 targetPosition, float offset)
{
action.IsAlert = true;
action.IsTracking = false;
action.IsOnTheMove = true;
action.MoveToTarget(targetPosition, offset);
}
이동하며 공격하는 기능이다. IsAlert
를 켜서 주변 적을 감지하고, IsTracking
은 꺼서 적이 범위에서 벗어나도 추적하지 않게 하며, IsOnTheMove
는 이동 중인 상태를 기억하게 한다.
public void MoveToTarget(Vector2 target, float offset)
{
targetPosition = target;
positionOffset = offset;
isHold = false;
agent.stoppingDistance = offset;
agent.SetDestination(target);
}
목표지점과 offset값을 저장해 이후에 다시 이동할 때 사용할 수 있게 했다.
private void Update()
{
if (isAlert && targetEnemy != null && Time.time - lastAttackTime > unitStat.AttackSpeed.TotalValule)
{
Attack();
}
}
public void Attack()
{
if (targetEnemy != null && Time.time - lastAttackTime > unitStat.AttackSpeed.TotalValule && (Vector3.Distance(targetEnemy.transform.position, transform.position) <= unitStat.AttackRange.TotalValule))
{
Stop();
((IDamagable)targetEnemy).TakeDamage((int)unitStat.Atk.TotalValule);
lastAttackTime = Time.time;
}
}
공격 속도를 기준으로 주기적으로 공격 함수를 실행해 타겟이 정해져 있을 때 적을 공격한다.
private void OnTriggerEnter2D(Collider2D collision)
{
Enemy enemy;
if ((enemy = collision.GetComponent<Enemy>()) != null)
{
action.OnEnemyInRange(enemy);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
Enemy enemy;
if ((enemy = collision.GetComponent<Enemy>()) != null)
{
action.OnEnemyExitRange(enemy);
}
}
적을 감지하는 기능을 담당한다. 자식 오브젝트를 추가하고 Trigger 콜라이더를 달아주었다.
/// <summary>
/// 적이 범위안에 들어왔을 때 처리
/// 타겟이 없으면 이 적으로 타겟을 바꾸고
/// 타겟을 추적중이었다면 무시
/// </summary>
/// <param name="enemy"></param>
public void OnEnemyInRange(Enemy enemy)
{
if (isAlert && targetEnemy == null)
{
targetEnemy = enemy;
}
}
/// <summary>
/// 적이 범위에서 나갔을 경우 처리
/// 적을 추적중이면 쫓아가고
/// 아니면 타겟을 null로 변경하고 새 타겟이 들어오길 기다림
/// </summary>
/// <param name="enemy"></param>
public void OnEnemyExitRange(Enemy enemy)
{
if (targetEnemy != null && targetEnemy == enemy)
{
if (isTracking)
{
MoveToTarget(targetEnemy.transform.position, userUnit.Stat.AttackRange.TotalValule / 2);
}
else
{
targetEnemy = null;
if (IsOnTheMove)
{
MoveToTarget(targetPosition, positionOffset);
}
}
}
}
이렇게 적이 범위에 들어올 때랑 나갈 때 이미 대상이 있나 없나, 추적 중인가 아닌가로 나뉘어 처리한다.
느낀점
적을 선택했을 때 추적하여 공격하는 기능도 추가해야 한다.