[Unity2D] #8 - CardDeck UI-2

qweasfjbv·2024년 3월 30일

Unity2D

목록 보기
8/15
post-thumbnail

개요

저번에 만든 카드UI를 꾸며보겠습니다.
저는 그림에 재능이 없기 때문에, 모든 아트는 에셋으로 대체하도록 하겠습니다.

https://assetstore.unity.com/packages/2d/gui/icons/potions-kit-196598
https://assetstore.unity.com/packages/2d/gui/2d-modular-cards-kit-demo-227623
https://www.dafont.com/korean-calligraphy.font

위 두 에셋과 폰트를 사용하여 UI를 꾸미고 실제로 맵에 효과가 나타나는것 까지 만들겠습니다.

구현

일단 카드 에셋을 사용하려면 문제점이 두 가지 있습니다.

  • UI에서 사용해야 하기 때문에 TransformSpriteRendererRectTransformImage로 바꿔야합니다.
  • 모든 요소들이 자식으로 들어가있어서 부모의 width, height, color로 이미지를 바꿀 수 없습니다.

우선 Transform과 SpriteRenderer부터 바꿔보겠습니다.
이건 어렵다기보다는 귀찮은 문제입니다.
전부 바꾼다음 적절히 width, height, pos를 조정해주기만 하면 아래 사진처럼 깔끔한 UI가 만들어집니다.

하지만 글자가 없으니 아무래도 허전합니다.
위에서 다운받은 폰트부터 적용하겠습니다.

window -> TextMeshRro -> Font Asset Creator 에서 폰트를 만들어줍니다.
그 후에 cost, name, description 을 적어줍니다.

글꼴과 에셋이 생각보다 잘 어울려서 다행입니다.

다음 문제는 카드의 모든 요소들이 자식으로 들어가있다는 것입니다. 제가 전에 작성한 코드는 해당 오브젝트의 width/height만 조절하기 때문에 수정이 필요합니다.
아래는 수정 부분입니다.


public abstract class CardBase : MonoBehaviour
    , IPointerEnterHandler
    , IPointerExitHandler
    , IBeginDragHandler
    , IDragHandler
    , IEndDragHandler
{
	//
    // variables...
    //
    
    public static readonly float CARD_WIDTH = 100 * Settings.xScale;
    public static readonly float CARD_HEIGHT = 150 * Settings.yScale;
    
    //
    // functions...
    //
    
    private void SetColor(Color color)
    {
        foreach(Transform child in transform)
        {
            var img = child.GetComponent<Image>();
            if (img != null) {
                img.color = color;
            }

            var text = child.GetComponent<TextMeshProUGUI>();
            if(text != null)
            {
                text.color = new Color(0,0, 0, color.a);
            }
        }
    }


}

원래 GetComponent<Image>() 로 접근하던 부분을 전부 SetColor로 교체했습니다.
Image는 흰색, TextMeshPro는 검정색을 쓰기 때문에 따로 color를 설정해줍니다.
여기까지 한번 테스트 해보겠습니다.

보기에 나쁘지 않으니 이제 실제 맵에 이펙트를 구현하겠습니다.
저번에 만든 CardBase의 추상함수 ActivateEffect를 사용하겠습니다.


public class PotionCard : CardBase
{
    [SerializeField]
    private GameObject effectPrefab;

    public override void ActivateEffect(Vector3 pos)
    {
        pos.z = 0;
        Instantiate(effectPrefab,pos, Quaternion.identity);
    }
}

간단하게 마우스 위치를 인자로 받아서 그 위치에 Instantiate 하는 함수입니다.
그럼 인스턴스화 할 effectPrefab도 만들어보겠습니다.

public class PurpleEffect : MonoBehaviour
{
    private TextMeshPro textMesh;
    private float duringTime = 5f;

    private void OnEnable()
    {
        foreach (Transform child in transform)
            if (child.GetComponent<TextMeshPro>() != null) 
                textMesh = child.GetComponent<TextMeshPro>();


        StartCoroutine(TimerCoroutine());
    }


    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.GetComponent<EnemyBT>() != null)
            collision.GetComponent<EnemyBT>().MoveDebuff(2);
    }

    private void OnTriggerExit2D(Collider2D collision)
    {
        if (collision.GetComponent<EnemyBT>() != null)
            collision.GetComponent<EnemyBT>().MoveBuff(2);
    }


    public IEnumerator TimerCoroutine()
    {
        float elapsedTime = 0f;
        while (elapsedTime < duringTime)
        {
            elapsedTime += Time.deltaTime;
            textMesh.text = (duringTime - elapsedTime).ToString("0.0");
            yield return null;
        }

        Destroy(this.gameObject);
    }

}

CircleCollider2D 를 추가하고 OnTriggerEnter/Exit 로 적의 속도를 느려지게 만들었습니다.
또, 이펙트 중앙에 타이머가 있으면 보기 좋을것 같아서 Text도 하나 넣었습니다.
이 부분을 만들면서 EnemyBT 도 살짝 수정했습니다.


    public class EnemyStat
    {
        private float moveSpeed;
        private float attack;
        private float hp;

        public float MoveSpeed { get => moveSpeed; set => moveSpeed = value; }
        public float Hp { get => hp; set => hp = value; }

        public EnemyStat(float moveSpeed, float attack, float hp)
        {
            this.moveSpeed = moveSpeed;
            this.attack = attack;
            this.hp = hp;
        }
    }
    public class EnemyBT : BTree
    {
        [SerializeField] private int searchRange;
        [SerializeField] private int attackRange;

        private EnemyStat enemyStat = new EnemyStat(0.05f, 3, 10);



        private Vector2Int destination;

        public void SetValues(Vector2Int dest)
        {
            destination = dest;
        }
        public void OnDamaged(float damage)
        {
            enemyStat.Hp -= damage;
        }

        public override Node SetupRoot()
        {
            Node root = new Selector(new List<Node> {
                    new Sequence(new List<Node>
                    {
                        new Search(transform, searchRange),
                        new Move(transform, destination, enemyStat)
                    }),
                    new Sequence(new List<Node>
                    {
                        new IsAttacking(transform),
                        new Track(transform, attackRange, enemyStat),
                        new Attack(transform)
                    })
                });


            return root;
        }

        public void MoveDebuff(float w)
        {
            enemyStat.MoveSpeed /= w;
        }
        public void MoveBuff(float w)
        {
            enemyStat.MoveSpeed *= w;
        }
    }

변할 수 있는 체력, 공격력, 이동속도 같은 요소들은 클래스에 넣었습니다.
C#에서는 포인터를 지원하지 않기 때문에 Call by Reference를 구현하기 위해 클래스로 넘겼습니다.
이렇게 하면 인자로 넘기고 난 뒤에 EnemyBT에서 값을 수정하더라도 각 Action Node에 영향을 끼칠 수 있습니다.
추가로 SetValues 는 디버깅용 함수입니다. 신경쓰지 않으셔도 괜찮습니다.

여기까지 게임씬에서 어떻게 보이는지 확인하겠습니다.

게임을 많이 하시는 분들이라면 뭔가 허전함을 느낄 수 있습니다.

  • 포션을 던지기 전에 사정범위가 옅게 보였으면 좋겠습니다.
  • 바로 생기고 삭제되니까 어색한 것 같습니다. 생성될 때 효과가 있으면 보기 좋을 것 같습니다.

일단 사정범위부터 만들겠습니다.
    public abstract void ActivateEffect(Vector3 pos);
    public abstract void PreviewEffect(Vector3 pos);
    public abstract void UnPreviewEffect();

CardBase에 추상함수를 추가해줍니다.
Preview/UnPreviewEffect 함수는 각각 preview를 보이게/안보이게 합니다.
PreviewEffectOnDrag 함수에, UnPreviewEffectOnEndDrag 에 추가해줍니다.

public class PotionCard : CardBase
{
    [SerializeField]
    private GameObject effectPrefab;

    private GameObject effect = null;

    public override void ActivateEffect(Vector3 pos)
    {
        effect.GetComponent<PotionEffect>().StartEffect(5f);
    }

    public override void PreviewEffect(Vector3 pos)
    {
        pos.z = 0;

        effect.transform.position = pos;
        effect.GetComponent<PotionEffect>().PreviewizeEffect();

        if (!effect.activeSelf)
            effect.SetActive(true);
    }


    public override void OnEnable()
    {
        base.OnEnable();
        if (effect == null)
        {
            effect = Instantiate(effectPrefab);
            effect.SetActive(false);
        }
        
    }

    public override void UnPreviewEffect()
    {
        effect.SetActive(false);
    }
}

PotionCard 클래스입니다.
각자 하나의 effect 를 Instantiate 해두고 숨겼다가, Preview 함수가 호출되면 켜고 Unpreview 함수가 호출되면 끕니다.
저기서 나오는 PotionEffect는 각 Effect가 상속받는 부모 클래스입니다.

public abstract class PotionEffect : MonoBehaviour
{
    protected TextMeshPro textMesh;
    protected const float THROWTIME = 0.5f;

    public virtual void OnEnable()
    {
        foreach (Transform child in transform)
            if (child.GetComponent<TextMeshPro>() != null)
                textMesh = child.GetComponent<TextMeshPro>();

    }

 
    protected void ShowEffect()
    {

        this.GetComponent<CircleCollider2D>().enabled = true;
        foreach (Transform child in transform)
        {
            if (child.GetComponent<SpriteRenderer>() == null) child.gameObject.SetActive(true);
            else
            {
                child.gameObject.SetActive(true);
                var tmpColor = child.GetComponent<SpriteRenderer>().color;
                tmpColor.a = 1f;
                child.GetComponent<SpriteRenderer>().color = tmpColor;
            }
        }
    }

    public void PreviewizeEffect()
    {
        this.GetComponent<CircleCollider2D>().enabled = false;
        foreach (Transform child in transform)
        {
            if (child.GetComponent<SpriteRenderer>() == null) child.gameObject.SetActive(false);
            else
            {
                var tmpColor = child.GetComponent<SpriteRenderer>().color;
                tmpColor.a = 0.4f;
                child.GetComponent<SpriteRenderer>().color = tmpColor;
            }
        }
    }


    public abstract void StartEffect(float duringTime);

}

에셋으로 받은 Effect를 확인해보니, 딱 범위크기 만큼 SpriteRenderer 로 되어있고, 나머지는 ParticleSystem 으로 되어있어서 간단하게 반복문으로 구현했습니다.
preview를 보여주는 동안 효과를 내지 않기 위해서 collider를 잠깐 꺼뒀습니다.
맨 밑의 StartEffect 추상함수는 타이머 코루틴을 밖에서 호출하기 편하게 한 번 감싼 것입니다.
PotionEffectPurpleEffect에서 상속받아줍니다.

코루틴같은 경우에는 아래와 같이 작성하면 됩니다.

    public IEnumerator TimerCoroutine(float duringTime)
    {
        yield return new WaitForSeconds(THROWTIME);

        ShowEffect();


        float elapsedTime = 0f;
        while (elapsedTime < duringTime)
        {
            elapsedTime += Time.deltaTime;
            textMesh.text = (duringTime - elapsedTime).ToString("0.0");
            yield return null;
        }

        Destroy(this.gameObject);
    }

던지는 효과를 구현하기 위해서 잠깐 기다렸다가 효과 타이머를 시작합니다.

이제 던지는 효과만 구현하면 됩니다.
나중에 여러 효과를 구현할 예정이니 아예 싱글톤 객체를 하나 만들겠습니다.


public class EffectGenerator : MonoBehaviour
{
    [SerializeField] private GameObject imagePrefab;

    [SerializeField] private Sprite potionSprite;

	//
    // Singleton Codes...
    //

    public void ThrowPotion(Vector3 dest, float throwTime)
    {
        var tmpPos = Camera.main.ViewportToWorldPoint(new Vector3(0.5f, 0, 0));
        var go = Instantiate(imagePrefab, tmpPos, Quaternion.identity);
        go.transform.localScale = go.transform.localScale * (5);

        Debug.Log(go.transform.localScale);
        go.GetComponent<SpriteRenderer>().sprite = potionSprite;
        go.AddComponent<ThrowEffect>();
        go.GetComponent<ThrowEffect>().Throw(tmpPos, dest, throwTime);
    }
    
}

여기서는 potionSprite를 SerializeField 로 두는데, 기능 구현을 다 하고 나면 ResourceManager 에서 id 값으로 가져올 예정이니 걱정하지 않으셔도 됩니다.
ThrowEffect 클래스는 오브젝트를 tmpPos에서 dest까지 throwTime동안 움직이도록 만들었습니다.
throwTime은 ThrowPotion을 호출하는 각 포션의 부모인 PotionEffect 함수가 가지고 있습니다.

이제 실행해보겠습니다.

마무리

자꾸 Instantiate/Destroy 를 해서 코드가 약간 지저분해졌는데, 이 부분은 쉬는날에 한 번에 오브젝트 풀링 구현하면서 정리하겠습니다.
약간 아쉬운점은 포션을 던질때 포물선으로 던지면 더 예쁠 것 같습니다.
시간나면 베지에 곡선을 사용해서 경로를 포물선으로 수정해보겠습니다.

다음에는 적에게 피해를 입히는 포션을 만들고 EnemyBT를 수정해서 피격과 죽는 애니메이션을 추가해보도록 하겠습니다.

참고자료

https://assetstore.unity.com/packages/2d/gui/icons/potions-kit-196598
https://assetstore.unity.com/packages/2d/gui/2d-modular-cards-kit-demo-227623
https://www.dafont.com/korean-calligraphy.font

0개의 댓글