📕학습 개요

오늘은 Unity 튜토리얼 시스템과 몬스터 동작 관련 기능들을 집중적으로 개발했다. UI 위치 선정의 함정부터 코루틴 관리, 애니메이터 심화, 2D 뎁스 정렬까지 다양한 문제를 해결하며 많은 것을 배울 수 있었다.


📖학습 내용

1. Unity UI 위치 계산 : transform.position을 믿지 말자

  • 문제: 튜토리얼 화살표를 특정 UI 버튼에 배치할 때, 일부 버튼에서만 위치가 엉뚱한 곳으로 쏠리는 현상 발생.
// 기존 코드
public void Place(RectTransform target, bool placeOnTop)
{
float targetWidth = target.sizeDelta.x;
float targetHeight = target.sizeDelta.y;
float indicatorHeight = m_RectTransform.sizeDelta.y;
float margin = 20;

float indicatordist = targetHeight / 2 + indicatorHeight / 2 + margin;

Vector3 position = target.position;
Quaternion rotation;

if(placeOnTop)
{
position.y += indicatordist;
rotation = Quaternion.Euler(0, 0, 180.0f);
}
else
{
position.y -= indicatordist;
rotation = Quaternion.identity;
}
m_RectTransform.gameObject.SetActive(true);
m_RectTransform.position = position;
m_RectTransform.localRotation = rotation;
}
  • 원인: RectTransform의 .position 값은 피벗(Pivot)의 월드 좌표를 반환한다. 모든 UI 요소의 피벗이 중앙(0.5, 0.5)으로 통일되어 있지 않다면, .position은 시각적 중심이 아닐 수 있다.
//수정된 코드
public void Place(RectTransform target, bool placeOnTop)
{
    float targetHeight = target.sizeDelta.y;
    float indicatorHeight = m_RectTransform.sizeDelta.y;
    float margin = 0f;
    float indicatordist = targetHeight / 2 + indicatorHeight / 2 + margin;

    // GetWorldCorners를 사용해 정확한 중앙 위치 계산
    Vector3[] corners  = new Vector3[4];
    target.GetWorldCorners(corners);
    _targetPosition = (corners[0] + corners[2]) / 2f;  // 좌하단과 우상단의 평균 => 정중앙
    Vector3 position = _targetPosition;
    Quaternion rotation;

    if(placeOnTop)
    {
        position.y += indicatordist;
        rotation = Quaternion.Euler(0, 0, 180.0f);
    }
    else
    {
        position.y -= indicatordist;
        rotation = Quaternion.identity;

    }
    m_RectTransform.gameObject.SetActive(true);
    m_RectTransform.position = position;
    m_RectTransform.localRotation = rotation;
}
  • 해결 및 배운 점:

    • target.GetWorldCorners() 함수를 사용하면 앵커나 피벗 위치에 상관없이 UI의 실제 네 꼭짓점 월드 좌표를 얻을 수 있다.

    • 정확한 시각적 중심점은 (corners[0] + corners[2]) / 2f (좌하단과 우상단의 평균)와 같이 계산해야 한다.

    • 핵심: UI 요소의 위치를 다룰 땐 transform.position을 맹신하지 말고, GetWorldCorners를 활용해 월드 좌표 기준의 계산을 수행하는 것이 훨씬 안정적이다.


Animator 심화 : StateMachineBehaviour 로 랜덤 공격 구현

  • 문제 : 여러 공격 모션을 가진 몬스터가 하나의 “Attack” 트리거로 다양한 공격을 랜덤하게 사용하도록 구현. C# 스크립트의 복잡도를 낮추고 싶었음.
public class SelectAttack : StateMachineBehaviour
{
    public int numberOfAttacks = 2;

    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        // 0부터 (공격 개수 - 1)까지의 정수 중 하나를 랜덤으로 선택
        int attackIndex = Random.Range(0, numberOfAttacks);

        // 애니메이터의 "AttackIndex" 파라미터에 랜덤으로 생성된 값을 설정
        animator.SetInteger("AttackIndex", attackIndex);
    }
}
  • 해결 및 배운 점:

    • 애니메이터 컨트롤러 내에 Sub-State Machine을 만들어 공격 로직을 캡슐화했다.

    • Sub-State Machine 진입 시, 애니메이션이 없는 빈 상태(Selector)를 거치도록 했다.

    • Selector 상태에 StateMachineBehaviour 스크립트를 추가하여, OnStateEnter 함수 내에서 Random.Range()로 랜덤 인덱스를 생성하고 animator.SetInteger(“AttackIndex”, …)를 통해 파라미터 값을 설정했다.

    • Selector 상태에서 각 공격 상태(Attack_01, Attack_02)로의 전환 조건으로 AttackIndex 값을 사용했다.

    • 핵심: StateMachineBehaviour를 활용하면 C# 코드 변경 없이 애니메이션 로직 자체를 애니메이터 컨트롤러 내에서 구현할 수 있어, 코드와 애니메이션의 역할을 명확히 분리하고 재사용성을 높일 수 있다.


2D 뎁스 정렬 : Y-Sort 로 입체감 구현

  • 문제: 몬스터들이 스폰될 때 서로 겹쳐 보이며, 앞뒤 구분이 명확하지 않았음.

  • 해결 및 배운 점:

    • 캐릭터의 Y 좌표가 낮을수록(화면 아래일수록) 더 앞에 보이도록 Renderer의 sortingOrder 값을 동적으로 변경하는 Y-Sort 기법을 적용했다.

    • LateUpdate()에서 sortingOrder를 업데이트하여 위치 계산이 모두 끝난 후 최종적으로 렌더링 순서를 결정하도록 했다.

    • 적용 공식: sortingOrder = baseOrder - (int)(transform.position.y * precision);

    • baseOrder: 기준이 되는 정렬 순서 (예: 0 또는 100).

    • y * precision: y좌표가 소수점이므로 정밀도를 높이기 위해 10 같은 상수를 곱해 정수화.

    • aseOrder - …: y값이 클수록(화면 위) sortingOrder가 작아지게 만들어, 뒤에 그려지도록 한다.

    • 핵심: LateUpdate와 Renderer.sortingOrder를 활용한 Y-Sort는 2D/2.5D 게임에서 깊이감을 표현하는 필수적이고 효과적인 방법이다.

// 랜덤 위치 스폰 여부(보스는 랜덤스폰 X)
if (!data.isBossWave)
{
    float xOffset = _spawnPoint.MonsterSpawnPoint.position.x + Random.Range(-1.5f, 2f);
    float yOffset = _spawnPoint.MonsterSpawnPoint.position.y;
    if (i > 0)
    {
        float magnitude = Mathf.Ceil((float)i / 2.0f) * 0.1f;

        float sign = (i % 2 == 1) ? 1f : -1f;

        yOffset += magnitude * sign;
    }
    monster.transform.position = new Vector2(xOffset, yOffset);

    // **바로 이 부분!**
    if (monster.TryGetComponent<LayerManager>(out var layerManager))
    {
        int sortingOrder = 100 - (int)(monster.transform.position.y * 10f);
        layerManager.SetSortingGroupOrder(sortingOrder);
    }
}

댓글남기기