3 minute read

유닛 스탯 구현

본격적으로 유닛이 공격하는 시스템을 만들기 위해 기본 스탯들을 설정했다. 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에 추가했다. ResourceStatAttributeStat은 이전 개인 과제에서 만든 클래스를 활용했다.

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);
            }
        }
    }
}

이렇게 적이 범위에 들어올 때랑 나갈 때 이미 대상이 있나 없나, 추적 중인가 아닌가로 나뉘어 처리한다.

느낀점

적을 선택했을 때 추적하여 공격하는 기능도 추가해야 한다.