Unity ์ˆ™๋ จ - 2

์ด์ค€ํ˜ธยท2023๋…„ 12์›” 11์ผ
0

๐Ÿ“Œ Unity ๊ฒŒ์ž„ ๊ฐœ๋ฐœ ์ˆ™๋ จ



๐Ÿ“Œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

Aimation & Animator

์ด ๋‘ ์ปดํฌ๋„ŒํŠธ๋Š” ์„œ๋กœ ๋‹ค๋ฅธ ๋ชฉ์ ๊ณผ ์‚ฌ์šฉ ์ผ€์ด์Šค์— ๋”ฐ๋ผ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ œ์–ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค. Animation์€ ๋” ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์— ์‚ฌ์šฉ๋˜๋ฉฐ, Animator๋Š” ๋” ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œํ€€์Šค์™€ ์ƒํƒœ ๊ด€๋ฆฌ์— ์‚ฌ์šฉ๋œ๋‹ค.

Animation :

  • Animation ์ปดํฌ๋„ŒํŠธ๋Š” ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ์— ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

  • ์ด ์ปดํฌ๋„ŒํŠธ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ฆฝ์„ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Animation ์ปดํฌ๋„ŒํŠธ๋Š” ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์— ์ ํ•ฉํ•˜๋ฉฐ, ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ฆฝ์€ Unity์˜ Animation window๋ฅผ ํ†ตํ•ด ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ํŽธ์ง‘ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์˜ค๋ธŒ์ ํŠธ์˜ ํฌ๊ธฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ฑฐ๋‚˜ ์ƒ‰์ƒ์„ ๋ณ€ํ™˜ํ•˜๋Š” ๋“ฑ์˜ ๊ฐ„๋‹จํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

Animator

  • Animator ์ปดํฌ๋„ŒํŠธ๋Š” ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์ƒํƒœ๋ฅผ ์ œ์–ดํ•˜๊ณ  ์ „ํ™˜์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

  • Animator ์ปดํฌ๋„ŒํŠธ๋Š” Animation Controller๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ๋ณต์žกํ•œ ์ƒํƒœ ๊ธฐ๊ณ„๋ฅผ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • Animator ์ปดํฌ๋„ŒํŠธ๋Š” ์—ฌ๋Ÿฌ ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ฆฝ์„ ์กฐ์ ˆํ•˜๊ณ , ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ฐ„์˜ ์ „ํ™˜์„ ์ œ์–ดํ•˜๊ณ , ๋ณต์žกํ•œ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œํ€€์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ์ ํ•ฉํ•˜๋‹ค.

  • ์˜ˆ๋ฅผ ๋“ค์–ด, ์บ๋ฆญํ„ฐ์˜ ๊ฑท๊ธฐ, ๋›ฐ๊ธฐ, ์ ํ”„ ๋“ฑ์˜ ์• ๋‹ˆ๋ฉ”์ด์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋‹ค.

  • Animator ์ปดํฌ๋„ŒํŠธ๋Š” Mecanim ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์Šคํ…œ์˜ ์ผ๋ถ€๋กœ์„œ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋ธ”๋ Œ๋”ฉ, ํŠธ๋ฆฌ, ์ƒํƒœ ๋จธ์‹  ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค.






StringToHash

์œ ๋‹ˆํ‹ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹œ์Šคํ…œ์—์„œ ์ž์ฃผ ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋Šฅ์ด๋‹ค. ๋ฌธ์ž์—ด์„ ํ•ด์‹œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋ฉฐ, ์ด๋กœ์จ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•˜๊ณ  ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ์œ ์ง€ํ•œ๋‹ค.

  • ๋ฌธ์ž์—ด ๋น„๊ต๋Š” ์ƒ๋Œ€์ ์œผ๋กœ ์—ฐ์‚ฐ ๋น„์šฉ์ด ํฐ ์ž‘์—…์ด๋‹ค. "StringToHash"๋Š” ๋ฌธ์ž์—ด์„ ๊ณ ์œ ํ•œ ์ •์ˆ˜ ๊ฐ’์ธ ํ•ด์‹œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ด ์—ฐ์‚ฐ ๋น„์šฉ์„ ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ๋‹ค.

  • ํ•ด์‹œ ๊ฐ’์€ ๊ณ ์œ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋‹ค๋ฅธ ๋ฌธ์ž์—ด์ด ๋™์ผํ•œ ํ•ด์‹œ ๊ฐ’์„ ๊ฐ–๋Š” ํ™•๋ฅ ์ด ๋งค์šฐ ๋‚ฎ๋‹ค. ์ด๋ฅผ ํ™œ์šฉํ•ด ๋ฌธ์ž์—ด์„ ํ•ด์‹œ๋กœ ๋ณ€ํ™˜ํ•˜๋ฉด ํšจ์œจ์ ์ธ ๋ฌธ์ž์—ด์„ ๋น„๊ตํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง€์ •ํ•  ๋•Œ ๋ฌธ์ž์—ด ๋Œ€์‹  ํ•ด์‹œ ๊ฐ’์„ ์‚ฌ์šฉํ•˜๋ฉด CPU์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ณ  ์• ๋‹ˆ๋ฉ”์ด์…˜ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

  • ๊ทธ๋Ÿฌ๋‚˜ ์ฃผ์˜ํ•  ์ ์€ ๋™์ผํ•œ ๋ฌธ์ž์—ด์€ ํ•ญ์ƒ ๋™์ผํ•œ ํ•ด์‹œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋งŒ, ๋ฐ˜๋Œ€๋กœ ๋™์ผํ•œ ํ•ด์‹œ ๊ฐ’์ด ํ•ญ์ƒ ๋™์ผํ•œ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค. ์ด ์ ์œผ๋กœ ์ธํ•ด ํ•ด์‹œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ๋˜ํ•œ, "StringToHash" ํ•จ์ˆ˜๋Š” ๋ฌธ์ž์—ด์„ ํ•ด์‹œ ๊ฐ’์œผ๋กœ ๋ณ€ํ™˜ํ•  ๋•Œ ์ผ๋ฐฉํ–ฅ์œผ๋กœ ์ž‘๋™ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ํ•ด์‹œ ๊ฐ’์„ ๋‹ค์‹œ ์›๋ž˜์˜ ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅํ•˜๋‹ค.












โž” ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ปจํŠธ๋กค ํ•˜๊ธฐ

๋งŒ๋“ค๊ธฐ

TopDownAnimations

public class TopDownAnimations : MonoBehaviour
{
    protected Animator animator;
    protected TopDownCharacterController controller;

    protected virtual void Awake()
    {
        animator = GetComponentInChildren<Animator>();
        controller = GetComponent<TopDownCharacterController>();
    }
}

TopDownAnimationController

public class TopDownAnimationController : TopDownAnimations
{
    // StringToHash : ํŠน์ •ํ•œ ๋ฌธ์ž์—ด์„ ์ผ์ •ํ•œ ๊ณต์‹์— ์˜ํ•ด์„œ ์ˆซ์ž๊ฐ’(Hash๊ฐ’)์œผ๋กœ ๋ณ€ํ™˜์„ ํ•œ๋‹ค.
    // ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ? : Animator์•ˆ์—์„œ ํ‚ค๊ฐ’์„ string์œผ๋กœ ์ œ๊ณต์„ ํ–ˆ์„ ๋•Œ, ๋ฌธ์ž์—ด ์—ฐ์‚ฐ์€ ๋น„์šฉ์ด ๋งค์šฐ ๋†’๋‹ค.
    // ๊ทธ๋ž˜์„œ ๋ฌธ์ž์—ด์„ ๋น„๊ตํ•˜์ง€ ๋ง๊ณ  Hash๊ฐ’(์ˆซ์ž๊ฐ’)์„ ๋น„๊ตํ•˜๋ผ๊ณ  ์ฒ˜๋ฆฌ๋ฅด ํ•ด์ฃผ๋Š” ๊ฒƒ์ด๋‹ค.
    // ์ด๋ฏธ ๊ณ ์œ ํ•œ Hashtable์ด ์กด์žฌํ•˜๊ธฐ ๋–„๋ฌธ์— Hash๊ฐ’์œผ๋กœ ์ค˜๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๊ฐ€ ์ด๋ฃจ์–ด์ง„๋‹ค.
    private static readonly int IsWalking = Animator.StringToHash("IsWalking");
    private static readonly int Attack = Animator.StringToHash("Attack");
    private static readonly int IsHit = Animator.StringToHash("IsHit");

    protected override void Awake()
    {
        base.Awake();
    }

    void Start()
    {
        controller.OnAttackEvent += Attacking;
        controller.OnMoveEvent += Move;
    }

    private void Move(Vector2 obj)
    {
        animator.SetBool(IsWalking, obj.magnitude > .5f);
    }

    private void Attacking(AttackSO obj)
    {
        animator.SetTrigger(Attack);
    }

    private void Hit()
    {
        animator.SetBool(IsHit, true);
    }

    private void OnBecameInvisible()
    {
        animator.SetBool(IsHit, false);
    }
}

player_idle - Animation Clip

  • Player - MainSprte ํด๋ฆญ

  • Ctrl + 6 ๋˜๋Š” Window โž” Animation โž” Animation ํด๋ฆญ

  • Creat ํด๋ฆญ โž” Animations - Player ํด๋” ์ƒ์„ฑ โž” player_idle ์ด๋ฆ„ ์„ค์ • โž” ํ™•์ธ

  • Samples 12 ์„ค์ • ( ์—†์œผ๋ฉด ๋ฐ‘์— ์‚ฌ์ง„์ฒ˜๋Ÿผ ์„ค์ • ๋ณ€๊ฒฝ )

  • knight_f_idle_anim_f0 ~ 3 ์„ ํƒ ํ•˜์—ฌ ๋„ฃ๊ธฐ

  • ๋‘ ์นธ์”ฉ ๋„์–ด ๋ฐฐ์น˜ ( ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์•Œ์•„์„œ ์กฐ์ • )

player_run - Animation Clip

  • Creat New Clip ํด๋ฆญ

  • Animations - Player ํด๋” โž” player_run ์ด๋ฆ„ ์„ค์ • โž” ํ™•์ธ

  • Samples 12 ์„ค์ •

  • knight_f_run_anim_f0 ~ 3 ์„ ํƒ ํ›„ ๋„ฃ๊ธฐ

player_hit - Animation Clip

  • Creat New Clip ํด๋ฆญ

  • Animations - Player ํด๋” โž” player_hit ์ด๋ฆ„ ์„ค์ • โž” ํ™•์ธ

  • Samples 12 ์„ค์ •

  • knight_f_hit-anim_f0 ์„ ํƒ ํ›„ 2ํšŒ ๋„ฃ๊ธฐ

  • Add Property โž” Sprite Renderer Color โž” + ํด๋ฆญ

  • ์‹œ์ž‘๊ณผ ๋ ํ”„๋ ˆ์ž„์„ ํด๋ฆญํ•˜์—ฌ ๊ฐ๊ฐ ์„ค์ •

player_attack - Animation Clip

  • Creat New Clip ํด๋ฆญ

  • Animations - Player ํด๋” โž” player_attack ์ด๋ฆ„ ์„ค์ • โž” ํ™•์ธ

  • Samples 12 ์„ค์ •

  • WeaponSprite์˜ Position๊ณผ Scale ์ถ”๊ฐ€







์ˆ˜์ •

Animatior Controller

  • Window โž” Animation โž” Animator ํด๋ฆญ

  • Animations/Player/MainSprite ํด๋ฆญ

  • idle, hit, run ๋‚จ๊ธฐ๊ณ  ์‚ญ์ œ

  • Entry ์šฐํด๋ฆญ โž” Set StateMachine Default State โž” idle ํด๋ฆญ

  • Parameter ํด๋ฆญ โž” IsWalking(Bool), Attack(Trigger), IsHit(Bool) ์ถ”๊ฐ€

  • idle ์šฐํด๋ฆญ โž” Make Transition โž” run ํด๋ฆญ

  • Transition ์„ค์ • ๋ณ€๊ฒฝ โž” Has Exit Time ์ฒดํฌ ํ•ด์ œ, Condition ์ถ”๊ฐ€

  • run ์šฐํด๋ฆญ โž” Make Transition โž” idle ํด๋ฆญ

  • Transition ์„ค์ • ๋ณ€๊ฒฝ โž” Has Exit Time ์ฒดํฌ ํ•ด์ œ, Condition ์ถ”๊ฐ€

  • hit & idle ๋„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •

Player ์˜ค๋ธŒ์ ํŠธ

  • Player ์˜ค๋ธŒ์ ํŠธ์— TopDownAnimationController.cs ์ถ”๊ฐ€






์ถ”๊ฐ€

Layer

  • Animator โž” Layer โž” +

  • Attack ์ด๋ฆ„ ๋ณ€๊ฒฝ โž” Weight 1 โž” Blending Additive ( Additive : ๊ธฐ์กด์— Layer์— ์ถ”๊ฐ€๋˜๋Š” ํ˜•์‹ )

  • Empty ๋…ธ๋“œ ์ถ”๊ฐ€ ( ๊ณต๊ฒฉ ์ด์™ธ์—๋Š” ํ‰์†Œ์—๋Š” ๋‚˜์˜ค๋ฉด ์•ˆ๋˜๊ธฐ ๋•Œ๋ฌธ )

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ • ํ›„ Transition ์„ค์ • ๋ณ€๊ฒฝ












๐Ÿ“Œ ์  ๊ตฌํ˜„

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

FindGameObjectWithTag

  • "FindGameObjectWithTag"๋Š” ์ง€์ •๋œ ํƒœ๊ทธ์™€ ์ผ์น˜ํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ํ™œ์„ฑ GameObject๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด ๋ฉ”์„œ๋“œ๋Š” ํŠน์ • ํƒœ๊ทธ๋ฅผ ๊ฐ€์ง„ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋น ๋ฅด๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š”๋‹ค.

  • ํƒœ๊ทธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์”ฌ ๋‚ด์—์„œ ํŠน์ • ์œ ํ˜•์˜ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์œผ๋ฉฐ, ์ฝ”๋“œ์—์„œ ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ฐธ์กฐํ•  ๋•Œ ์œ ์šฉํ•˜๋‹ค.

  • ๊ทธ๋Ÿฌ๋‚˜ "FindGameObjectWithTag"๋Š” ๋งค์šฐ ๋น„์‹ผ ์—ฐ์‚ฐ์ด๋‹ค. ์ฆ‰, ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด CPU๋ฅผ ๋งŽ์ด ์‚ฌ์šฉํ•˜๊ฒŒ ๋œ๋‹ค. ๋”ฐ๋ผ์„œ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ๋งค ํ”„๋ ˆ์ž„๋งˆ๋‹ค ํ˜ธ์ถœํ•˜๋ฉด ๊ฒŒ์ž„ ์„ฑ๋Šฅ์— ์‹ฌ๊ฐํ•œ ์˜ํ–ฅ์„ ๋ฏธ์น  ์ˆ˜ ์žˆ๋‹ค.

  • ๋”ฐ๋ผ์„œ ์ผ๋ฐ˜์ ์œผ๋กœ๋Š” ์ด ๋ฉ”์„œ๋“œ๋ฅผ Start๋‚˜ Awake์™€ ๊ฐ™์€ ์ดˆ๊ธฐํ™” ๋ฉ”์„œ๋“œ์—์„œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœํ•˜๋Š” ๊ฒƒ์ด ๊ถŒ์žฅ๋œ๋‹ค. ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ฐพ์€ ํ›„์—๋Š” ์ฐธ์กฐ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋‚˜์ค‘์— ์žฌ์‚ฌ์šฉํ•œ๋‹ค.

  • ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ๊ฐ€ ๋งŽ์€ ํฐ ์”ฌ์—์„œ๋Š” ํƒœ๊ทธ ๋Œ€์‹  ๋ ˆ์ด์–ด๋‚˜ ๋‹ค๋ฅธ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ๋‹ค.

Physics.Raycast ๋˜๋Š” Physics2D.Raycast

  • Raycasting์€ ์ผ๋ จ์˜ ์ฝœ๋ผ์ด๋”์™€ ๊ต์ฐจํ•˜๋Š”์ง€๋ฅผ ๊ฐ์ง€ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ์ˆ ์ด๋‹ค. ์ด๋Š” ๋ ˆ์ด์ € ํฌ์ธํ„ฐ๋‚˜ ์ด์•Œ์„ ์˜๋Š” ํšจ๊ณผ๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜, ํ”Œ๋ ˆ์ด์–ด์˜ ์‹œ์•ผ๋ฅผ ๊ณ„์‚ฐํ•˜๋Š” ๋“ฑ ๋‹ค์–‘ํ•œ ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

  • Unity์—์„œ๋Š” Physics.Raycast ๋˜๋Š” Physics2D.Raycast๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Raycast๋ฅผ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ๋ฉ”์„œ๋“œ๋Š” ์‹œ์ž‘์ , ๋ฐฉํ–ฅ, ์ตœ๋Œ€ ๊ฑฐ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ  ์„ ํƒ์ ์œผ๋กœ ๋ ˆ์ด์–ด ๋งˆ์Šคํฌ๋ฅผ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค.

  • Raycast๋Š” hit์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด ์ •๋ณด์—๋Š” ์ถฉ๋Œํ•œ ๊ฐ์ฒด, ์ถฉ๋Œ ์ง€์ , ์ถฉ๋Œ ์ง€์ ์˜ ์ •๊ทœํ™”๋œ ๋ฒกํ„ฐ ๋“ฑ์ด ํฌํ•จ๋œ๋‹ค.

  • Raycast๋Š” ์ถฉ๋Œ ๊ฒ€์‚ฌ์— ๋น„ํ•ด ๊ณ„์‚ฐ ๋น„์šฉ์ด ๋งŽ์ด ๋“ ๋‹ค. ๋”ฐ๋ผ์„œ ๋ถˆํ•„์š”ํ•œ Raycast๋ฅผ ์ค„์ด๊ณ , ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ๋ ˆ์ด์–ด ๋งˆ์Šคํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒ€์‚ฌ ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•˜๋Š” ๊ฒƒ์ด ์„ฑ๋Šฅ์— ์ข‹๋‹ค.

  • Raycast๋Š” ๋น„์ฃผ์–ผ ๋””๋ฒ„๊น…์„ ์œ„ํ•ด Debug.DrawRay์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด Scene๋ทฐ์—์„œ Raycast์˜ ๊ฒฝ๋กœ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.






โž” ๊ทผ๊ฑฐ๋ฆฌ ์  ๋งŒ๋“ค๊ธฐ

๋งŒ๋“ค๊ธฐ

GameManager

public class GameManager : MonoBehaviour
{
    public static GameManager instance;

    public Transform Player { get; private set; }
    [SerializeField] private string playerTag = "Player";

    private void Awake()
    {
        instance = this;
        Player = GameObject.FindGameObjectWithTag(playerTag).transform;
    }
}
  • GameManager ์˜ค๋ธŒ์ ํŠธ์— ์ถ”๊ฐ€

TopDownEnemyController

public class TopDownEnemyController : TopDownCharacterController
{
    GameManager gameManager;
    protected Transform ClosestTarget { get; private set; }

    protected override void Awake()
    {
        base.Awake();
    }

    protected virtual void Start()
    {
        gameManager = GameManager.instance;
        ClosestTarget = gameManager.Player;
    }

    protected virtual void FixedUpdate()
    {

    }

    // ๊ฐ€๊นŒ์šด ์ ๊ณผ์˜ ๊ฑฐ๋ฆฌ๋ฅผ ๊ตฌํ•˜๋Š” ๋ฉ”์†Œ๋“œ
    protected float DistanceToTarget()
    {
        return Vector3.Distance(transform.position, ClosestTarget.position); // ํ˜„์žฌ ์˜ค๋ธŒ์ ํŠธ์™€ ๊ฐ€๊นŒ์šด ํƒ€๊ฒŸ ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ
    }

    // ํƒ€๊ฒŸ์˜ ๋ฐฉํ–ฅ์„ ๊ตฌํ•˜๋Š” ๋ฉ”์†Œ๋“œ
    protected Vector2 DirectionToTarget()
    {
        // transform.position์—์„œ ClosestTarget.position๋ฅผ ๋ฐ”๋ผ๋ณด๋Š” ๋ฐฉํ–ฅ
        return (ClosestTarget.position - transform.position).normalized; // normalized : ์ •๊ทœํ™” ํ•˜์—ฌ ๋ฐฉํ–ฅ๋งŒ ๋‚จ๊ธด๋‹ค.
    }
}

TopDownContactEnemyController (๊ทผ๊ฑฐ๋ฆฌ ์ )

public class TopDownContactEnemyController : TopDownEnemyController
{
    [SerializeField][Range(0f, 100f)] private float followRange;
    [SerializeField] private string targetTag = "Player";
    private bool _isCollidingWithTarget;

    [SerializeField] private SpriteRenderer characterRendere;

    protected override void Start()
    {
        base.Start();
    }

    protected override void FixedUpdate()
    {
        base.FixedUpdate();

        Vector2 direction = Vector2.zero;
        if (DistanceToTarget() < followRange)
        {
            direction = DirectionToTarget();
        }

        CallMoveEvent(direction);
        Rotate(direction);
    }

    private void Rotate(Vector2 direction)
    {
        float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        characterRendere.flipX = Mathf.Abs(rotZ) > 90f;
    }
}



Goblin

  • Default AttackData ์ƒ์„ฑ โž” Goblin_DefaultAttackData ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ - Goblin ์ด๋ฆ„ ๋ณ€๊ฒฝ
  • ํ•˜์œ„์— ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ - MainSprite ์ด๋ฆ„ ๋ณ€๊ฒฝ โž” Sprite Renderer ์ถ”๊ฐ€ โž” goblin_idle_anim_f0 ์„ค์ •, Order in Layer4

  • Goblin ์˜ค๋ธŒ์ ํŠธ์— ํ•„์š” ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

Animation Clip

  • Player Animation์„ ์ฐธ๊ณ ํ•˜์—ฌ Goblin ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒ์„ฑ






์ˆ˜์ •

Player Object

  • Player โž” Tag โž” Player ์ˆ˜์ •

Animator Controller

  • Animator Controller ์ˆ˜์ • โž” Base Layer, Hit











โž” ์›๊ฑฐ๋ฆฌ ์  ๋งŒ๋“ค๊ธฐ

๋งŒ๋“ค๊ธฐ

TopDownRangedEnemyController (์›๊ฑฐ๋ฆฌ ์ )

public class TopDownRangeEnemyController : TopDownEnemyController
{
    [SerializeField] private float followRange = 15f;
    [SerializeField] private float shootRange = 10f;

    protected override void FixedUpdate()
    {
        base.FixedUpdate();

        float distance = DistanceToTarget();
        Vector2 direction = DirectionToTarget();

        IsAttacking = false;
        if(distance <= followRange)
        {
            if(distance <= shootRange)
            {
                int layerMaskTarget = Stats.CurrentStates.attackSO.target;
                // Enemy์™€ Player ์‚ฌ์ด์— ์ง€ํ˜•(์žฅ์• ๋ฌผ)์ด ์žˆ๋‹ค๋ฉด ๊ณต๊ฒฉ(์›๊ฑฐ๋ฆฌ)ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค. ๋ง‰ํ˜€์žˆ๋Š” ์ง€ํ˜•์ด ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•˜๋Š” ์ฝ”๋“œ
                RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 11f, (1 << LayerMask.NameToLayer("Level")) | layerMaskTarget);
                
                if(hit.collider != null && layerMaskTarget == (layerMaskTarget | (1 << hit.collider.gameObject.layer)))
                {
                    CalllLookEvent(direction);
                    CallMoveEvent(Vector2.zero);
                    IsAttacking = true;
                }
                else
                {
                    CallMoveEvent(direction);
                }
            }
            else
            {
                CallMoveEvent(direction);
            }
        }
        else
        {
            CallMoveEvent(direction);
        }
    }
}

Orc_Shaman

  • Ranged AttackData ์ƒ์„ฑ โž” Orc_Shaman_RangedAttackData ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ - Orc_Shaman ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ํ•˜์œ„์— ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ - mainSprite ์ด๋ฆ„ ๋ณ€๊ฒฝ โž” Sprite Renderer ์ถ”๊ฐ€ โž” orc_shaman_idle_anim_f0 ์„ค์ •, Order in Layer 4

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด WeaponPivot ๋ฐ ํ•˜์œ„ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ์ƒ์„ฑ

  • Orc_Shaman ์˜ค๋ธŒ์ ํŠธ์— ํ•„์š” ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

Animaton Clip

  • Goblin๊ณผ ๋™์ผํ•˜๊ฒŒ ์„ค์ •



์ˆ˜์ •

Animation Controller

  • Goblin๊ณผ ๋™์ผํ•˜๊ฒŒ ์„ค์ •











๐Ÿ“Œ ๋„‰๋ฐฑ ๊ตฌํ˜„

๋„‰๋ฐฑ์„ ๊ฑธ์–ด์ฃผ๋ฉด, ์‹œ๊ฐ„๊ณผ ํž˜์„ ์ฃผ๊ณ  ๊ทธ ๊ฐ’์„ ์ €์žฅํ•ด ๋†จ๋‹ค๊ฐ€ ๋„‰๋ฐฑ์„ ํ•ด์•ผํ•  ๋•Œ, ์ž๋™์ ์œผ๋กœ ๋„‰๋ฐฑ์ด ์ง„ํ–‰๋˜๊ณ , duration์ด ์ค„์–ด๋“ค์–ด 0์ด๋˜๋ฉด ๋”์ด์ƒ ๋„‰๋ฐฑ์— ๋Œ€ํ•ด์„œ๋Š” ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์ง€ ์•Š๋Š”๋‹ค.

์ˆ˜์ •

TopDownMovement

--------------------- ์ƒ๋žต ---------------------
private Vector2 _knockback = Vector2.zero;
private float knockbackDuration = 0.0f;

private void FixedUpdate()
{
    ApplyMovment(_movementDirection);
    if(knockbackDuration > 0.0f)	// ์ถ”๊ฐ€
    {
        knockbackDuration -= Time.fixedDeltaTime;	// ์ถ”๊ฐ€
    }
}
--------------------- ์ƒ๋žต ---------------------
public void ApplyKnockback(Transform other, float power, float duration)	// ์ถ”๊ฐ€
{
    knockbackDuration = duration;	// ์ถ”๊ฐ€
    _knockback = -(other.position - transform.position).normalized * power;	// ์ถ”๊ฐ€
}

 // ๋„‰๋ฐฑ์„ ๊ฑธ์–ด์ฃผ๋ฉด, ์‹œ๊ฐ„๊ณผ ํž˜์„ ์ฃผ๊ณ  ๊ทธ ๊ฐ’์„ ์ €์žฅํ•ด ๋†จ๋‹ค๊ฐ€ ๋„‰๋ฐฑ์„ ํ•ด์•ผํ•  ๋•Œ, ์ž๋™์ ์œผ๋กœ ๋„‰๋ฐฑ์ด ์ง„ํ–‰๋˜๊ณ 
 // duration์ด ์ค„์–ด๋“ค์–ด 0์ด๋˜๋ฉด ๋”์ด์ƒ ๋„‰๋ฐฑ์— ๋Œ€ํ•ด์„œ๋Š” ์ฒ˜๋ฆฌ๋ฅผ ํ•˜์ง€ ์•Š๋Š”๋‹ค.
private void ApplyMovment(Vector2 direction)
{
    direction = direction * _stats.CurrentStates.speed;

    if(knockbackDuration > 0.0f)	// ์ถ”๊ฐ€
    {
        direction += _knockback;	// ์ถ”๊ฐ€
    }
    _rigidbody.velocity = direction;
}











๐Ÿ“Œ ๋ฐ๋ฏธ์ง€ ํ”ผ๊ฒฉ ๊ตฌํ˜„

โž” Health System ๋งŒ๋“ค๊ธฐ

Health System ๋งŒ๋“ค๊ธฐ

public class HealthSystem : MonoBehaviour
{
    [SerializeField] private float healthChangeDelay = .5f;

    private CharacterStatsHandler _statsHandler;
    private float _timeSinceLastChange = float.MaxValue;

    public event Action OnDamage;
    public event Action OnHeal;
    public event Action OnDeath;
    public event Action OnInvincibilityEnd;

    public float CurrentHealth { get; private set; }

    public float MaxHealth => _statsHandler.CurrentStates.maxHealth;

    private void Awake()
    {
        _statsHandler = GetComponent<CharacterStatsHandler>();
    }

    private void Start()
    {
        CurrentHealth = _statsHandler.CurrentStates.maxHealth;
    }

    private void Update()
    {
        if (_timeSinceLastChange < healthChangeDelay)   // ๋”œ๋ ˆ์ด ์ฒดํฌ
        {
            _timeSinceLastChange += Time.deltaTime;
            if (_timeSinceLastChange >= healthChangeDelay )
            {
                OnInvincibilityEnd?.Invoke();	// ๋ฌด์ ์‹œ๊ฐ„ ๋
            }
        }
    }

    // ์ฒด๋ ฅ ํšŒ๋ณต or ๊ฐ์†Œ ๋ฉ”์†Œ๋“œ
    public bool ChangeHealth(float change)
    {
        if (change == 0 || _timeSinceLastChange < healthChangeDelay)
        {
            return false;
        }

        _timeSinceLastChange = 0f;
        CurrentHealth += change;
        CurrentHealth = CurrentHealth > MaxHealth ? MaxHealth : CurrentHealth;  // ์ตœ๋Œ€ ์ฒด๋ ฅ์„ ๋„˜์ง€ ๋ชปํ•˜๋Š” ์ฒ˜๋ฆฌ

        if ( change > 0)    // change๊ฐ’์ด ์–‘์ˆ˜? ์ฒด๋ ฅ ํšŒ๋ณต
        {
            OnHeal?.Invoke();
        }
        else    // change๊ฐ’์ด ์Œ์ˆ˜? ์ฒด๋ ฅ ๊ฐ์†Œ
        {
            OnDamage?.Invoke();
        }

        if (CurrentHealth <= 0f)
        {
            CallDeath();
        }

        return true;
    }

    private void CallDeath()
    {
        OnDeath?.Invoke();
    }
}

์‹œ์Šคํ…œ ์ ์šฉํ•˜๊ธฐ

  • Player - HealthSystem ์ถ”๊ฐ€

  • Goblin, OrcShaman Prefab - HealthSystem ์ถ”๊ฐ€




TopDownAnimationController ์ˆ˜์ •

--------------------- ์ƒ๋žต--------------------- 
private HealthSystem _healthSystem;

protected override void Awake()
{
    base.Awake();
    _healthSystem = GetComponent<HealthSystem>();	// ์ถ”๊ฐ€
}

--------------------- ์ƒ๋žต--------------------- 

void Start()
{
    controller.OnAttackEvent += Attacking;
    controller.OnMoveEvent += Move;

    if(_healthSystem != null) // ์ถ”๊ฐ€
    {
        _healthSystem.OnDamage += Hit; // ์ถ”๊ฐ€
        _healthSystem.OnInvincibilityEnd += InvincibilityEnd; // ์ถ”๊ฐ€
    }
}

RangedAttackController ์ˆ˜์ •

    private void OnTriggerEnter2D(Collider2D collision)
    {
        // ๋น„ํŠธ์—ฐ์‚ฐ 1 << collision.gameObject.layer : collision.gameObject.layer ๋ฅผ ์™ผ์ชฝ์œผ๋กœ ํ•˜๋‚˜ ๋ฐ€๊ณ 
        // levelCollisionLayer.value์™€ | ์—ฐ์‚ฐ ํ•˜๋‚˜๋ผ๋„ 1์ด๋ฉด 1
        if (levelCollisionLayer.value == (levelCollisionLayer.value | (1 << collision.gameObject.layer)))
        {
            // ClosestPoint : ๊ฐ€์žฅ ๊ฐ€๊นŒ์šด position , _direction * .2f : ๋ฒฝ์— ๋ถ€๋”ชํžŒ ๋ฐ์„œ ์กฐ๊ธˆ ์•ˆ์ชฝ์œผ๋กœ ์˜ค๊ฒŒ ํ•˜๋Š” ์—ฐ์‚ฐ
            DestoryProjectile(collision.ClosestPoint(transform.position) - _direction * .2f, fxOnDestory);
        }
        // ๋ฐ‘์— ๋‹ค ์ถ”๊ฐ€
        else if (_attackData.target.value == (_attackData.target.value | (1 << collision.gameObject.layer)))
        {
            HealthSystem healthSystem = collision.GetComponent<HealthSystem>();
            if (healthSystem != null)
            {
                healthSystem.ChangeHealth(-_attackData.power);
                if (_attackData.isOnKnockback)
                {
                    TopDownMovement movement = collision.GetComponent<TopDownMovement>();
                    if (movement != null)
                    {
                        movement.ApplyKnockback(transform, _attackData.knockbackPower, _attackData.knockbackTime);
                    }
                }
            }
            DestoryProjectile(collision.ClosestPoint(transform.position), fxOnDestory);
        }
    }



์  Layer ์„ค์ •ํ•˜๊ธฐ

  • Goblin Prefab - Layer โž” Enemy

  • OrcShaman๋„ ๋™์ผํ•˜๊ฒŒ ์ ์šฉ

AttackData ์ˆ˜์ •ํ•˜๊ธฐ

  • Player_RangedAttackData โž” Target โž” Enemy

  • Goblin_DefaultAttackData, Orc_Shaman_RangedAttackData โž” Target โž” Player




TopDownContactEnemyController ์ˆ˜์ •

private HealthSystem healthSystem;
private HealthSystem _collidingTargetHealthSystem;
private TopDownMovement _collidingMovement;

protected override void Start()
{
    base.Start();
    
    healthSystem  = GetComponent<HealthSystem>();
    healthSystem.OnDamage += OnDamage;
}

// "์ž์‹ "์ด ๋ฐ๋ฏธ์ง€๋ฅผ ๋ฐ›์•˜์„ ๋•Œ, ์ฒ˜๋ฆฌํ•  ํ•จ์ˆ˜
// ์ด๋ ‡๊ฒŒ ๋งŒ๋“ค์–ด๋‘์ง€ ์•Š์œผ๋ฉด ๊ณต๊ฒฉ์„ ๋ฐ›์•„๋„ ๋”ฐ๋ผ๊ฐ€์งˆ ์•Š๊ณ  ๊ฐ€๋งŒํžˆ ์„œ์žˆ๋Š”๋‹ค.
// ๊ทธ๋ž˜์„œ "์ž์‹ "์ด ๋ฐ๋ฏธ์ง€๋ฅผ ๋ฐ›์•˜์„ ๋•Œ, followRange๋ฅผ ํฌ๊ฒŒ ๋Š˜๋ ค์ฃผ์–ด์„œ ๋”ฐ๋ผ์˜ค๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ.
private void OnDamage()
{
    followRange = 100f;
}

protected override void FixedUpdate()
{
    base.FixedUpdate();

    if(_isCollidingWithTarget)
    {
        ApplyHealthChange();
    }

-------------------------- ์ƒ๋žต -------------------------- 

private void OnTriggerEnter2D(Collider2D collision)
{
    GameObject receiver = collision.gameObject;

    if(!receiver.CompareTag(targetTag))
    {
        return;
    }

    _collidingTargetHealthSystem = receiver.GetComponent<HealthSystem>();
    if( _collidingTargetHealthSystem != null )
    {
        _isCollidingWithTarget = true;
    }

    _collidingMovement = receiver.GetComponent<TopDownMovement>();
}

private void OnTriggerExit2D(Collider2D collision)
{
    if (!collision.CompareTag(targetTag))
    {
        return;
    }

    _isCollidingWithTarget = false;
}

private void ApplyHealthChange()
{
    AttackSO attackSO = Stats.CurrentStats.attackSO;
    bool hasBeenChanged = _collidingTargetHealthSystem.ChangeHealth(-attackSO.power);
    if(attackSO.isOnKnockback && _collidingMovement != null )
    {
        _collidingMovement.ApplyKnockback(transform, attackSO.knockbackPower, attackSO.knockbackTime);
    }
}

Goblin ์˜ค๋ธŒ์ ํŠธ ์ˆ˜์ •

  • Circle Collider 2D ์ถ”๊ฐ€

  • isTrigger ์ฒดํฌ






โž” ์  ์ฃฝ์ด๊ธฐ

DisappearOnDeath ๋งŒ๋“ค๊ธฐ

public class DisappearOnDeath : MonoBehaviour
{
    private HealthSystem _healthSystem;
    private Rigidbody2D _rigidbody;

    private void Start()
    {
        _healthSystem = GetComponent<HealthSystem>();
        _rigidbody = GetComponent<Rigidbody2D>();
        _healthSystem.OnDeath += OnDeath;
    }

    void OnDeath()
    {
        // ์ฃฝ์—ˆ์„ ๋•Œ, ์›€์ง์ด๋ฉด ์ด์ƒํ•˜๋‹ค. ๊ทธ๋ž˜์„œ ๋ชป ์›€์ง์ด๊ฒŒ ๊ณ ์ •.
        _rigidbody.velocity = Vector3.zero;

        // "๋‚˜"๋ฅผ ํฌํ•จํ•ด์„œ ๊ทธ ํ•˜์œ„์— ์žˆ๋Š” ๋ชจ๋“  SpriteRenderer๋ฅผ ์ฐพ์•„์™€์„œ ์ปฌ๋Ÿฌ๋ฅผ ์กฐ์ • ( ํˆฌ๋ช…ํ•˜๊ฒŒ ์กฐ์ • )( ์—ฐ์ถœ )
        foreach (SpriteRenderer renderer in transform.GetComponentsInChildren<SpriteRenderer>())
        {
            Color color = renderer.color;
            color.a = 0.3f;
            renderer.color = color;
        }

        // Behaviour : MonoBehaviour ์œ„์— ์žˆ๋Š”(๋ถ€๋ชจ) ํด๋ž˜์Šค (์ปจํŠธ๋กค ํด๋ฆญ์œผ๋กœ ํƒ€๊ณ  ๋“ค์–ด๊ฐ€์„œ ํ™•์ธ ๊ฐ€๋Šฅ)
        // Behaviour์ด Component์˜ ํ‚ค๊ณ  ๋„๋Š” ๊ธฐ๋Šฅ์„ ๊ด€๋ฆฌํ•˜๊ธฐ์— ๋™์ž‘ํ•˜๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋ฅผ ๊บผ์ค€๋‹ค. (์ฃฝ์—ˆ์œผ๋‹ˆ๊นŒ)
        // Behaviour : Component , Behaviour์ด Component๋ฅผ ์ƒ์†๋ฐ›์Œ
        foreach (Behaviour component in transform.GetComponentsInChildren<Behaviour>())
        {
            component.enabled = false;
        }

        Destroy(gameObject, 2f);
    }
}

์ถ”๊ฐ€ํ•˜๊ธฐ

  • Goblin, Orc_Shaman โž” DisappearOnDeath.cs ์ถ”๊ฐ€











๐Ÿ“Œ ํŒŒํ‹ฐํด ์ƒ์„ฑ

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ(Particle System)

  • ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์€ ์ˆ˜ ์ฒœ๊ฐœ์˜ ์ž‘์€ 2D ๋˜๋Š” 3D ์˜ค๋ธŒ์ ํŠธ๋“ค์„ ๊ด€๋ฆฌํ•˜๊ณ , ๊ทธ๋“ค์˜ ๋™์ž‘๊ณผ ์ƒ์• ๋ฅผ ์ œ์–ดํ•œ๋‹ค. ๊ฐ๊ฐ์˜ ์ž‘์€ ์˜ค๋ธŒ์ ํŠธ๋ฅผ 'ํŒŒํ‹ฐํด'์ด๋ผ๊ณ  ๋ถ€๋ฅธ๋‹ค.

  • ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์˜ ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ๋Š” 'emitter'(๋ฐœ์‚ฌ์ฒด), 'particles'(ํŒŒํ‹ฐํด), 'animator'(์• ๋‹ˆ๋ฉ”์ดํ„ฐ), 'renderer'(๋ Œ๋”๋Ÿฌ)๋“ฑ์œผ๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ๋‹ค.

  • Unity์˜ ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์€ ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ํŒŒํ‹ฐํด์˜ ํ–‰๋™์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ํ•˜๋ฉฐ, ์ด๋ฅผ ์œ„ํ•ด ๊ฐ ํŒŒํ‹ฐํด์— ๋Œ€ํ•œ ์œ„์น˜, ์†๋„, ์ˆ˜๋ช…, ์ƒ‰์ƒ, ํฌ๊ธฐ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ์ €์žฅํ•œ๋‹ค.

  • ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์œ„ํ•ด ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•œ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์‹œ์Šคํ…œ์˜ ์ตœ๋Œ€ ํŒŒํ‹ฐํด ์ˆ˜๋ฅผ ์ œํ•œํ•˜๊ฑฐ๋‚˜, ํŒŒํ‹ฐํด์˜ ์ ์šฉ๋ฒ”์œ„๋ฅผ ์ œํ•œํ•˜๋Š” ๋“ฑ์˜ ๊ธฐ๋Šฅ์ด ์žˆ๋‹ค.

์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ(Animation Events)

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ์ง„ํ–‰๋˜๋Š” ๋™์•ˆ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด, ์บ๋ฆญํ„ฐ๊ฐ€ ํŠน์ • ๋™์ž‘์„ ํ•  ๋•Œ ์‚ฌ์šด๋“œ๋ฅผ ์žฌ์ƒํ•˜๊ฑฐ๋‚˜, ํŠน์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ํ”„๋ ˆ์ž„์—์„œ ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์„ ๋ฐœ์‚ฌํ•˜๋Š” ๋“ฑ์˜ ์ž‘์—…์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ๋Š” Unity ์• ๋‹ˆ๋ฉ”์ด์…˜ ํŽธ์ง‘๊ธฐ์—์„œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค. ํŽธ์ง‘๊ธฐ๋ฅผ ํ†ตํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜ ํƒ€์ž„๋ผ์ธ์— ์ด๋ฒคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ํ•ด๋‹น ์ด๋ฒคํŠธ๊ฐ€ ํ˜ธ์ถœํ•  ํ•จ์ˆ˜๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ๋Š” ํ•ด๋‹น ์• ๋‹ˆ๋ฉ”์ด์…˜ ํด๋ฆฝ์ด ์žฌ์ƒ๋˜๋Š” ๊ฒŒ์ž„ ์˜ค๋ธŒ์ ํŠธ์— ์—ฐ๊ฒฐ๋œ ๋ชจ๋“  ์Šคํฌ๋ฆฝํŠธ์—์„œ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋Š” ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ด๋ฒคํŠธ๋Š” ํŠน์ • ํ”„๋ ˆ์ž„์—์„œ๋งŒ ์‹คํ–‰๋˜๋ฉฐ, ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ์—๋„ ์ž๋™์œผ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์• ๋‹ˆ๋ฉ”์ด์…˜๊ณผ ์ฝ”๋“œ์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ๋”์šฑ ์œ ์—ฐํ•˜๊ฒŒ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ์• ๋‹ˆ๋ฉ”์ด์…˜์˜ ์‹œ๊ฐ์  ํšจ๊ณผ์™€ ์‚ฌ์šด๋“œ ํšจ๊ณผ ๋“ฑ์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ์  ์š”์†Œ๋ฅผ ์กฐํ™”๋กญ๊ฒŒ ํ†ตํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ๋ฅผ ํ†ตํ•ด ํ˜ธ์ถœ๋˜๋Š” ํ•จ์ˆ˜๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๊ณต์šฉ ํ•จ์ˆ˜(public function)์ด์–ด์•ผ ํ•˜๋ฉฐ, ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ตœ๋Œ€ ํ•˜๋‚˜์˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.






โž” ๊ฑธ์Œ ํšจ๊ณผ ๋งŒ๋“ค๊ธฐ

DustParticles ์ถ”๊ฐ€

  • Player ์šฐํด๋ฆญ โž” Particle System ์ถ”๊ฐ€, DustParticles ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ์ตœํ•˜๋‹จ Renderer - Order in Layer 4

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝ

DustParticleControl ๋งŒ๋“ค๊ธฐ

public class DustParticleControl : MonoBehaviour
{
    [SerializeField] private bool createDustOnWalk = true;
    [SerializeField] private ParticleSystem dustParticleSystem;

    public void CreateDustParticles()
    {
        if (createDustOnWalk)
        {
            // bool๊ฐ’์ด ๋“ค์–ด์˜ค๋ฉด ํŒŒํ‹ฐํด์„ ๋ฉˆ์ถ”๊ณ  ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ฒŒ ํ•œ๋‹ค.
            dustParticleSystem.Stop();
            dustParticleSystem.Play();
        }
    }
}

์ ์šฉํ•˜๊ธฐ

  • Player - MainSprite โž” DustParticleControl ์ถ”๊ฐ€

  • Ctrl + 6 ๋˜๋Š” Window - Animation - Animation ํด๋ฆญ

  • player_run 1, 3 ํ”„๋ ˆ์ž„ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ด๋ฒคํŠธ ์ถ”๊ฐ€






โž” ํˆฌ์‚ฌ์ฒด ์†Œ๋ฉธ ํšจ๊ณผ ๋งŒ๋“ค๊ธฐ

ImpactParticleSystem ์ถ”๊ฐ€ํ•˜๊ธฐ

  • ProjectileManager์šฐํด๋ฆญ โž” Particle System ์ถ”๊ฐ€, ImpactParticleSystem ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ์ตœํ•˜๋‹จ Renderer - Order in Layer 4

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ณ€๊ฒฝ

์ ์šฉํ•˜๊ธฐ

  • Projectile Manager - Impact Particle System โž” Impact Particle System ์ถ”๊ฐ€




ProjectileManager ์ˆ˜์ • ( ์ถ”๊ฐ€ )

    public void CreateImpactParticlesAtPosition(Vector3 position, RangedAttackData attackData)
    {
        _impactParticleSystem.transform.position = position;    // ํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์„ ํ•ด๋‹น ์œ„์น˜๋กœ ๋ณด๋‚ธ๋‹ค.
        ParticleSystem.EmissionModule em = _impactParticleSystem.emission;  // EmissionModule์„ ๊ฐ€์ ธ์˜จ๋‹ค EmissionModule ?: ์ƒ์„ฑํ•˜๋Š” ๋Š๋‚Œ
        em.SetBurst(0, new ParticleSystem.Burst(0, Mathf.Ceil(attackData.size * 5)));   // ์‚ฌ์ด์ฆˆ๋ณ„๋กœ ํฌ๊ธฐ๋ฅผ ๋‹ค๋ฅด๊ฒŒ, ์ถฉ๊ฒฉํŒŒ๊ฐ€ ์ปค์ง€๊ฒŒ
        ParticleSystem.MainModule mainModule = _impactParticleSystem.main;  // mainํŒŒํ‹ฐํด ์‹œ์Šคํ…œ์„ ๊ฐ€์ ธ์˜จ๋‹ค.
        mainModule.startSpeedMultiplier = attackData.size * 10f;    // ์ฒ˜์Œ์— ์†๋„๋ฅผ ๊ณฑํ•ด์ฃผ๋Š” ๊ฐ’์„ ๋งŒ๋“ค์–ด์ค€๋‹ค. ( ์‹ค์ œ ์‚ฌ์ด์ฆˆ์— ๋น„๋ก€ํ•˜๊ฒŒ ์ปค์ง€๊ฒŒ ๋งŒ๋“ค์–ด์ค€๋‹ค. )
        _impactParticleSystem.Play();
        // ์šฐ๋ฆฌ๊ฐ€ ๋งŒ๋“ ๊ฑธ ๊ทธ ์œ„์น˜๋กœ ๊ฐ€์ ธ๊ฐ€์„œ ์ƒ๋Œ€๊ฐ€ ๊ฐ€์ง€๊ณ ์žˆ๋Š” EmissionModule์ด๋ž‘ MainModule์„ ๊ฐ€์ง€๊ณ ์„œ
        // ํ•œ๋ฒˆ Play๋ฅผ ๋˜์ง€๋Š” ๊ฒƒ์ด๋‹ค. ๊ทธ๋Ÿฌ๊ณ  ๋˜ ๋‹ค๋ฅธ๊ณณ์— ๊ฐ€์„œ ๋˜ ๋˜์ง€๊ณ 
    }

RangedAttackController ์ˆ˜์ • ( ์ถ”๊ฐ€ )

    void DestoryProjectile(Vector3 position, bool createFx)
    {
        if(createFx)
        {
            _projectileManager.CreateImpactParticlesAtPosition(position, _attackData);
        }
        gameObject.SetActive(false);
    }











๐Ÿ“Œ ์‚ฌ์šด๋“œ ์ปจํŠธ๋กค

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

์‚ฌ์šด๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฃผ์š” ์ปดํฌ๋„ŒํŠธ

AudioClip :

  • AudioClip์€ ์‚ฌ์šด๋“œ ํŒŒ์ผ์„ ์œ ๋‹ˆํ‹ฐ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋ฐ์ดํ„ฐ ํƒ€์ž…์ด๋‹ค.

  • .wav, .mp3, .ogg ๋“ฑ ๋‹ค์–‘ํ•œ ํ˜•์‹์˜ ์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ง€์›ํ•œ๋‹ค.

AudioSource :

  • AudioSource ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šด๋“œ๋ฅผ ์žฌ์ƒํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

  • AudioSource์— AudioClip์„ ์—ฐ๊ฒฐํ•˜์—ฌ ์žฌ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.

  • AudioSource๋Š” 3D์‚ฌ์šด๋“œ ์„ค์ •, ๋ณผ๋ฅจ ์กฐ์ ˆ, ์‚ฌ์šด๋“œ ๋ฐ˜๋ณต ์žฌ์ƒ ๋“ค์˜ ์„ค์ •์„ ์ œ๊ณตํ•œ๋‹ค.

AudioListener :

  • AudioListener ์ปดํฌ๋„ŒํŠธ๋Š” ์‚ฌ์šด๋“œ๋ฅผ ๋“ฃ๋Š” ํฌ์ธํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.

  • ์ผ๋ฐ˜์ ์œผ๋กœ ์ฃผ์š” ์นด๋ฉ”๋ผ์— AudioListener๊ฐ€ ์œ„์น˜ํ•œ๋‹ค.

  • ๊ฒŒ์ž„์—๋Š” ํ•˜๋‚˜์˜ AudioListener๋งŒ ์žˆ์–ด์•ผ ํ•œ๋‹ค.






โž” ์‚ฌ์šด๋“œ ๋งค๋‹ˆ์ € ๋งŒ๋“ค๊ธฐ

SoundManager ๋งŒ๋“ค๊ธฐ

public class SoundManager : MonoBehaviour
{
    public static SoundManager instance;

    [SerializeField][Range(0f, 1f)] private float soundEffectVolume;
    [SerializeField][Range(0f, 1f)] private float soundEffectPitchVariance;
    [SerializeField][Range(0f, 1f)] private float musicVolume;
    private ObjectPool objectPool;

    // AudioSource : ์‹ค์ œ๋กœ ์‚ฌ์šด๋“œ๋ฅผ ์ถœ๋ ฅํ•  ์–˜
    // Scene ์–ด๋”˜๊ฐ€์— AudioListener๊ฐ€ ์†Œ๋ฆฌ๋ฅผ ๋“ค์–ด์ฃผ๋Š” ์—ญํ•  ๋ณดํ†ต Camera(์นด๋ฉ”๋ผ)์— ๋‹ฌ๋ ค์žˆ๋‹ค.
    // ์ด ๊ฒŒ์ž„์€ 2D์ด๊ธฐ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ 3D ์‚ฌ์šด๋“œ๋ฅผ ์„ค์ •ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ทธ๋ž˜์„œ ์–ด๋””์žˆ๋“  ์†Œ๋ฆฌ๊ฐ€ ๋“ค๋ฆฐ๋‹ค.
    // 3D ๊ฒŒ์ž„์—์„œ๋Š” ๊ฑฐ๋ฆฌ์— ๋”ฐ๋ผ์„œ ์˜ค๋””์˜ค์˜ ์ฐจ์ด๊ฐ€ ์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ทธ๋Ÿฐ ์ฒ˜๋ฆฌ๋“ค๋„ ์ด๋ฃจ์–ด์งˆ ์ˆ˜ ์žˆ๋‹ค.
    private AudioSource musicAudioSource;
    public AudioClip musicClip; // AudioClip : ์‹ค์ œ ์Œ์›

    private void Awake()
    {
        instance = this;
        musicAudioSource = GetComponent<AudioSource>();
        musicAudioSource.volume = musicVolume;
        musicAudioSource.loop = true;

        objectPool = GetComponent<ObjectPool>();
    }

    private void Start()
    {
        ChangeBackGroundMusic(musicClip);
    }

    // static ๋ฉ”์„œ๋“œ๋“ค์€ static๋ณ€์ˆ˜๋“ค๋งŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. (์ •์ ์ธ ์• ๋“ค๋ผ๋ฆฌ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅ)
    // ๊ทธ๋ ‡๊ธฐ์— ์ •์ ์ธ ๋ณ€์ˆ˜ instance๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๊ทธ ๊ฐ์ฒด์˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๊ณ  ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    public static void ChangeBackGroundMusic(AudioClip music)
    {
        instance.musicAudioSource.Stop();
        instance.musicAudioSource.clip = music;
        instance.musicAudioSource.Play();
    }

    public static void PlayClip(AudioClip clip)
    {
        GameObject obj = instance.objectPool.SpawnFromPool("SoundSource");
        obj.SetActive(true);
        SoundSource soundSource = obj.GetComponent<SoundSource>();
        soundSource.Play(clip, instance.soundEffectVolume, instance.soundEffectPitchVariance);
    }
}

SoundSource ๋งŒ๋“ค๊ธฐ

// Sound Clip์„ ์ปจํŠธ๋กค ํ•˜๊ธฐ ์œ„ํ•œ ํด๋ž˜์Šค
public class SoundSource : MonoBehaviour
{
    private AudioSource _audioSource;

    public void Play(AudioClip clip, float soundEffectVolume, float soundEffectPitchVariance)
    {
        if (_audioSource == null)
            _audioSource = GetComponent<AudioSource>();

        CancelInvoke(); // ์ด์ „์˜ Invoke๊ฐ€ ๋‚จ์•„์žˆ๋Š” ์ƒํƒœ์—ฌ์„œ ์‚ฌ์šด๋“œ๋ฅผ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋Š”๋ฐ ๊บผ์ ธ๋ฒ„๋ฆฌ๋Š”๊ฑธ ๋Œ€๋น„ Invoke์บ”์Šฌ ( ์ทจ์†Œ )
        // ๋‹ค์‹œ ๋“ฑ๋ก
        _audioSource.clip = clip;
        _audioSource.volume = soundEffectVolume;
        _audioSource.Play();
        _audioSource.pitch = 1f + Random.Range(-soundEffectPitchVariance, soundEffectPitchVariance);

        Invoke("Disable", clip.length + 2); // clip.length + 2 ์‹œ๊ฐ„ ์ดํ›„์— Disable(); ์‹คํ–‰
    }

    // ๋„๋Š” ์ž‘์—… ๋ฉ”์„œ๋“œ
    public void Disable()
    {
        _audioSource.Stop();
        gameObject.SetActive(false);    // ์žฌ์‚ฌ์šฉ ๋Œ€๋น„ ํ™œ์„ฑํ™” / ๋น„ํ™œ์„ฑํ™”
    }
}

SoundManager ์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ

  • GameManager ํ•˜์œ„์— SoundManager ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ

  • SoundManager ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€

  • ObjectPool ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€

SoundSource ์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ

  • ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ โž” SoundSource ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • AudioSource ์ปดํฌ๋„ŒํŠธ ์ถ”๊ฐ€

  • SoundSource ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€

  • ํ”„๋ฆฌํŒน์œผ๋กœ ๋งŒ๋“ค๊ธฐ

ObjectPool ์„ค์ •

  • SoundManager์™€ ๊ฐ™์ด ์žˆ๋Š” ObjectPool ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •






โž” ์†Œ๋ฆฌ ์žฌ์ƒํ•˜๊ธฐ

TopDownShooting ์ˆ˜์ • (์ถ”๊ฐ€)

public AudioClip shootingClip;

----------------- ์ƒ๋žต ----------------- 
    private void CreatProjectile(RangedAttackData rangedAttackData, float angle)
    {
        // ( ๋ฐœ์‚ฌ ์œ„์น˜, ํšŒ์ „๊ฐ, ๊ณต๊ฒฉ์ •๋ณด ) ๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค.
        _projectileManager.ShootBullet(projectileSpawnPosition.position, RotateVector2(_aimDirection, angle), rangedAttackData);
        
        if (shootingClip)
            SoundManager.PlayClip(shootingClip);
    }

HealthSystem ์ˆ˜์ • (์ถ”๊ฐ€)

public AudioClip damageClip;
----------------- ์ƒ๋žต ----------------- 
public bool ChangeHealth(float change)
{
----------------- ์ƒ๋žต ----------------- 
    if ( change > 0)    // change๊ฐ’์ด ์–‘์ˆ˜? ์ฒด๋ ฅ ํšŒ๋ณต
    {
        OnHeal?.Invoke();
    }
   else    // change๊ฐ’์ด ์Œ์ˆ˜? ์ฒด๋ ฅ ๊ฐ์†Œ
   {
         OnDamage?.Invoke();

         if (damageClip)
            SoundManager.PlayClip(damageClip);
    }
----------------- ์ƒ๋žต ----------------- 
}

Player์˜ ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •

  • TopDownShooting - DM-CGS-20 ์ถ”๊ฐ€

  • HealthSystem - DM-CGS-44 ์ถ”๊ฐ€












๐Ÿ“Œ UI ๋งŒ๋“ค๊ธฐ

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

UGUI (Unity's User Interface)

Unity์˜ ๊ธฐ๋ณธ UI์‹œ์Šคํ…œ์œผ๋กœ ๊ฒŒ์ž„ ๋‚ด์˜ ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋œ๋‹ค.

Canvas :

  • UGUI์—์„œ ๋ชจ๋“  UI์š”์†Œ๋Š” Canvas๋ผ๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด์— ๋ฐฐ์น˜๋œ๋‹ค.

  • Canvas๋Š” ์Šคํฌ๋ฆฐ ๊ณต๊ฐ„, ์›”๋“œ ๊ณต๊ฐ„, ์นด๋ฉ”๋ผ ๊ณต๊ฐ„์˜ 3๊ฐ€์ง€ ๋ Œ๋” ๋ชจ๋“œ๋ฅผ ์ง€์›ํ•œ๋‹ค.

Rect Transform :

  • Unity์˜ ๊ธฐ๋ณธ Transform ๋Œ€์‹  UI์š”์†Œ์—๋Š” Rect Transform์ด ์‚ฌ์šฉ๋œ๋‹ค.

  • ์œ„์น˜, ํฌ๊ธฐ, ํšŒ์ „, ์Šค์ผ€์ผ์„ ์ง€์ •ํ•˜๋Š”๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, ์•ต์ปค ๋ฐ ํ”ผ๋ฒ—์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ถ€๋ชจ์™€์˜ ์ƒ๋Œ€์ ์ธ ์œ„์น˜๋ฅผ ์ง€์ •ํ•œ๋‹ค.

UI Components :

  • UGUI๋Š” ๋‹ค์–‘ํ•œ UI์š”์†Œ๋“ค์„ ์ œ๊ณตํ•œ๋‹ค. : ๋ฒ„ํŠผ, ์ด๋ฏธ์ง€, ํ…์ŠคํŠธ, ์Šฌ๋ผ์ด๋”, ์Šคํฌ๋กค ๋ฐ” ๋“ฑ

Event System

  • UGUI์˜ ์ด๋ฒคํŠธ ์‹œ์Šคํ…œ์€ UI์ƒํ˜ธ์ž‘์šฉ์„ ๊ด€๋ฆฌํ•œ๋‹ค.

  • ๋งˆ์šฐ์Šค ํด๋ฆญ, ๋“œ๋ž˜๊ทธ, ํ‚ค๋ณด๋“œ ์ž…๋ ฅ ๋“ฑ ๋‹ค์–‘ํ•œ ์ž…๋ ฅ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•œ๋‹ค.

TextMeshPro

  • TextMeshPro๋Š” ์›๋ž˜ ๋…๋ฆฝ์ ์ธ ๊ฐœ๋ฐœ์ž์— ์˜ํ•ด Unity ์—์…‹ ์Šคํ† ์–ด์—์„œ ํŒ๋งค๋˜์—ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ์ดํ›„ Unity Technologies์— ์ธ์ˆ˜๋˜์–ด Unity์˜ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ์œผ๋กœ ํฌํ•จ๋˜์—ˆ๋‹ค. ์ด๋Ÿฌํ•œ ๋ณ€ํ™”๋Š” 2017๋…„์— ๋ฐœ์ƒํ–ˆ์œผ๋ฉฐ, ๊ทธ ๋ฆฌํ›„๋กœ TextMeshPro๋Š” Unity ์‚ฌ์šฉ์ž๋“ค์—๊ฒŒ ๋ฌด๋ฃŒ๋กœ ์ œ๊ณต๋˜๊ณ  ์žˆ๋‹ค.

  • TextMeshPro๋Š” Unity์—์„œ ์ œ๊ณตํ•˜๋Š” ๊ณ ๊ธ‰ ํ…์ŠคํŠธ ๋ Œ๋”๋ง ์‹œ์Šคํ…œ์ด๋‹ค. ๊ธฐ๋ณธ ํ…์ŠคํŠธ ๊ตฌ์„ฑ ์š”์†Œ๋ณด๋‹ค ํ›จ์”ฌ ๋” ๋งŽ์€ ๊ธฐ๋Šฅ๊ณผ ์ •ํ™•์„ฑ์„ ์ œ๊ณตํ•œ๋‹ค.






โž” ๋ฐฐ๊ฒฝ์ƒ‰ ๋ฐ”๊พธ๊ธฐ

Background Color ๋ณ€๊ฒฝ

  • Main Camera - Background - ์ƒ‰ ๋ฐ”๊พธ๊ธฐ ( ์•„๋ฌด ์ƒ‰์ด๋‚˜ ์ƒ๊ด€์—†๋‹ค. )






โž” UI ๋งŒ๋“ค๊ธฐ

๋งŒ๋“ค๊ธฐ

Canvas

  • Creat โž” UI โž” Canvas

  • Canvas โž” Canvas Scaler ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •

Wave

  • Wave ๋นˆ ์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ โž” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •

  • ํ•˜์œ„์— UI - Image ์ถ”๊ฐ€

  • Alt ๋ˆ„๋ฅธ ์ƒํƒœ๋กœ Stretch ํด๋ฆญ

  • Image์˜ Color Alpha ๊ฐ’ ๊ฐ์†Œ

  • TextMeshPro 2๊ฐœ ์ถ”๊ฐ€ ํ›„ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •

HPBar

  • Wave ๋นˆ ์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ โž” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •

  • ํ•˜์œ„์— UI - Slider ์ถ”๊ฐ€

  • Handle Slide Area ์‚ญ์ œ

  • Background Color โž” Wave Image ๊ฐ™์€ Color

  • Fill Area - Fill Color ๋ถ‰์€ ๊ณ„์—ด๋กœ ๋ณ€๊ฒฝ

  • Wave ์ฐธ๊ณ  ํ•˜์—ฌ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ˆ˜์ •

GameOver

  • GameOver ๋นˆ ์˜ค๋ธŒ์ ํŠธ ๋งŒ๋“ค๊ธฐ โž” ์ „์ฒด ํฌ๊ธฐ๋กœ ๋Š˜๋ฆฌ๊ธฐ

  • Image, Text, Button * 2 ์ถ”๊ฐ€

  • Image ์ „์ฒด ์‚ฌ์ด์ฆˆ๋กœ ๋Š˜๋ฆฌ๊ธฐ, ์•ŒํŒŒ๊ฐ’ ์ค„์ด๊ธฐ

  • Text โž” GameOver Text ์„ค์ • โž” ํฌ๊ธฐ์™€ ์œ„์น˜๋ฅผ ํ™”๋ฉด์„ ๋ณด๊ณ  ๋งž์ถ”๊ธฐ

  • Button - Color ๊ฐ๊ฐ ์„ค์ •

  • ๋‹ค์Œ์˜ ํ™”๋ฉด๊ณผ ์œ ์‚ฌํ•˜๊ฒŒ ๋ฐฐ์น˜






โž” ์Šคํฌ๋ฆฝํŠธ ์ ์šฉํ•˜๊ธฐ

GameManager ์ˆ˜์ •

GameManager๊ฐ€ ์•„๋‹Œ UIManager๋ฅผ ๋”ฐ๋กœ ํ™œ์šฉํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ๋‚˜์ค‘์„ ์ƒ๊ฐํ•˜๋ฉด ๋” ์ข‹์„๋“ฏ ํ•˜๋‹ค.

public class GameManager : MonoBehaviour
{
    public static GameManager instance;

    public Transform Player { get; private set; }
    [SerializeField] private string playerTag = "Player";
    private HealthSystem playerhealthSystem;

    [SerializeField] private TextMeshProUGUI waveText;
    [SerializeField] private Slider hpGaugeSlider;
    [SerializeField] private GameObject gameOverUI;

    private void Awake()
    {
        instance = this;
        Player = GameObject.FindGameObjectWithTag(playerTag).transform;
        
        playerhealthSystem = Player.GetComponent<HealthSystem>();
        playerhealthSystem.OnDamage += UpdateHealthUI;
        playerhealthSystem.OnHeal += UpdateHealthUI;
        playerhealthSystem.OnDeath += GameOver;

        gameOverUI.SetActive(false);
    }

    private void UpdateHealthUI()
    {
        hpGaugeSlider.value = playerhealthSystem.CurrentHealth / playerhealthSystem.MaxHealth;
    }

    private void GameOver()
    {
        gameOverUI.SetActive(true);
    }

    private void UpdateWaveUI()
    {
        // waveText.text =
    }

    public void RestartGame()
    {
        // GetActiveScene().buildIndex : ์ง€๊ธˆ ์ผœ์ ธ์žˆ๋Š” Scene์˜ ๋ฒˆํ˜ธ๋ฅผ ๊ฐ€์ ธ์™€์„œ ๋‹ค์‹œ Load๋ฅผ ์‹œ์ผœ๋ผ.
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);   
    }

    public void ExitGame()
    {
        Application.Quit();
    }
}

Button - OnClick (Unity Event) ์„ค์ •

  • GameManager์„ ์„ค์ •

  • ๋ฉ”์„œ๋“œ๋ฅผ ์„ ํƒ

GameManager ์ปดํฌ๋„ŒํŠธ ์„ค์ •

  • ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์„ค์ •












๐Ÿ“Œ ๋กœ์ง ๊ตฌํ˜„ํ•˜๊ธฐ

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

์ฝ”๋ฃจํ‹ด(Coroutin)

  • ์ฝ”๋ฃจํ‹ด์€ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰๋˜๋Š” ํ•จ์ˆ˜๋กœ, ํŠน์ • ์ฝ”๋“œ ๋ธ”๋Ÿญ์˜ ์‹คํ–‰์„ ์ผ์‹œ์ ์œผ๋กœ ์ค‘์ง€ํ•˜๊ณ  ๋‹ค์‹œ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.

  • IEnumerator ๋ฆฌํ„ด ํƒ€์ž…์˜ ํ•จ์ˆ˜์—์„œ yield return์„ ์‚ฌ์šฉํ•˜์—ฌ ์ฝ”๋ฃจํ‹ด์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • StartCoroutineํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฝ”๋ฃจํ‹ด์„ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ณ , StopCoroutineํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ์ฝ”๋ฃจํ‹ด์„ ์ค‘์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ฝ”๋ฃจํ‹ด์€ ํ”„๋ ˆ์ž„ ๊ฐ„์˜ ์ง€์—ฐ, ๋น„๋™๊ธฐ ์ž‘์—…, ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ์• ๋‹ˆ๋ฉ”์ด์…˜ ๋“ฑ์˜ ์ž‘์—…์— ์ฃผ๋กœ ์‚ฌ์šฉ๋œ๋‹ค.

  • yield return null์€ ๋‹ค์Œ ํ”„๋ ˆ์ž„๊นŒ์ง€ ๋Œ€๊ธฐ๋ฅผ ์˜๋ฏธํ•˜๊ณ , yield return new WaitForSeconds(n)์€ n์ดˆ ๋™์•ˆ ๋Œ€๊ธฐ๋ฅผ ์˜๋ฏธํ•œ๋‹ค.

  • ์ฝ”๋ฃจํ‹ด์€ ๋ณ„๋„์˜ ์Šค๋ ˆ๋“œ์—์„œ ์‹คํ–‰๋˜์ง€ ์•Š๋Š”๋‹ค. ๋”ฐ๋ผ์„œ Unity์˜ ๋ฉ”์ธ ์Šค๋ ˆ๋“œ์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ Unity API๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์ฝ”๋ฃจํ‹ด์€ ์ผ๋ฐ˜ ํ•จ์ˆ˜์™€๋Š” ๋‹ค๋ฅด๊ฒŒ ์‹คํ–‰์„ ์ผ์‹œ ์ค‘๋‹จํ•˜๊ณ  ๋‚˜์ค‘์— ๋‹ค์‹œ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ์–ด, ์‹œ๊ฐ„ ์ง€์—ฐ, ๋ฐ˜๋ณต, ์กฐ๊ฑด๋ถ€ ๋Œ€๊ธฐ ๋“ฑ์˜ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉํ•˜๋‹ค.






โž” ๊ฒŒ์ž„ ๋กœ์ง ์ถ”๊ฐ€ํ•˜๊ธฐ

๋กœ์ง ์ถ”๊ฐ€

GameManager

----------------------- ์ƒ๋žต ----------------------- 
    [SerializeField] private int currentWaveIndex = 0;
    private int currentSpawnCount = 0;
    private int waveSpawnCount = 0;
    private int waveSpawnPosCount = 0;

    public float spawnInterval = .5f;
    public List<GameObject> enemyPrefabs = new List<GameObject>();  // ๋”•์…”๋„ˆ๋ฆฌ๋กœ ๋‹ค์–‘ํ•˜๊ฒŒ ๊ทธ๋ฃน์„ ๋งŒ๋“ค์–ด์„œ ์˜คํฌ๋งŒ ๋‚˜์˜ฌ๋•Œ๋Š” ์˜คํฌ ๊ณ ๋ธ”๋ฆฐ์ด ๋‚˜์˜ฌ๋• ๊ณ ๋ธ”๋ฆฐ ๊ทธ๋ ‡๊ฒŒ๋„ ๊ฐ€๋Šฅ

    [SerializeField] private Transform spawnPositionsRoot;
    private List<Transform> spawnPositions = new List<Transform>();
----------------------- ์ƒ๋žต ----------------------- 
    private void Awake()
    {
        instance = this;
        Player = GameObject.FindGameObjectWithTag(playerTag).transform;
        
        playerhealthSystem = Player.GetComponent<HealthSystem>();
        playerhealthSystem.OnDamage += UpdateHealthUI;
        playerhealthSystem.OnHeal += UpdateHealthUI;
        playerhealthSystem.OnDeath += GameOver;

        gameOverUI.SetActive(false);

        // spawnPosition์˜ ์œ„์น˜๋“ค์„ ๋‹ค ๊ฐ€์ ธ์™€์„œ ์ €์žฅ์‹œํ‚จ๋‹ค.
        for (int i = 0; i < spawnPositionsRoot.childCount; i++)
        {
            spawnPositions.Add(spawnPositionsRoot.GetChild(i)); // GetChild ๊ฐ€ transform์„ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, .tranform์€ ์•ˆํ•ด๋„ ๋œ๋‹ค. 
        }
    }
----------------------- ์ƒ๋žต ----------------------- 
    private void Start()
    {
        StartCoroutine("StartNextWave");
    }

    // ์ฝ”๋ฃจํ‹ด : IEnumerator returnํƒ€์ž…์„ ๊ฐ€์ ธ์„œ ๋น„๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.
    // ์ฝ”๋“œ๋ฅผ ์ผ์ •๋ถ€๋ถ„์—์„œ ์ผ์‹œ์ •์ง€ ํ•˜๊ฑฐ๋‚˜ ๋ฉˆ์ถ”๊ฑฐ๋‚˜ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ฒŒ ํ•˜๋Š” ์ฝ”๋“œ๋ฅผ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ค.
    // Thread์—์„œ ๋™์ž‘ํ•˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ๊ณ  Unity Main Thread์•ˆ์—์„œ ๋™์ž‘ํ•˜๋Š” ๊ตฌ๋ถ„์ ์„ ๊ฐ€์ง€๊ณ ์„œ ์ฝ”๋“œ๋ฅผ ๋™์ž‘ํ•˜๊ฒŒ ํ•œ๋‹ค.
    // ์ผ๋ฐ˜์ ์ธ 'return'๋ณด๋‹ค๋Š”  'yield return'์ด๋ผ๋Š” ๊ฒƒ์„ ์“ด๋‹ค. 
    // 'yield return'์œผ๋กœ ์ด ์ฝ”๋“œ์˜ ์‹คํ–‰์„ ๋ฐ˜ํ™˜ํ•ด ๋†จ๋‹ค๊ฐ€ ๋‹ค์‹œ ๊ทธ ๋ถ€๋ถ„์— ๋Œ์•„์˜จ๋‹ค.
    // 'yield return'์„ ๋งŒ๋‚˜๋ฉด ์‹คํ–‰์— ๋Œ€ํ•œ ์ˆœ์„œ๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ๋‹ค๊ฐ€ ๋‹ค์‹œ ๋Œ์•„์™€์„œ ๋™์ž‘ํ•œ๋‹ค.
    IEnumerator StartNextWave()
    {
        while(true)
        {
            if (currentSpawnCount == 0) // currentSpawnCount : ์†Œํ™˜๋˜์–ด ์žˆ๋Š” ๋ชน์˜ ๊ฐฏ์ˆ˜ (0 ? ์ฒ˜์Œ or ๋‹ค ์žก์Œ)
            {
                UpdateWaveUI(); // Wave ์ตœ์‹ ํ™”
                yield return new WaitForSeconds(2f);    // new WaitForSeconds ๋„ ๋ณ€์ˆ˜๋กœ ์บ์‹ฑํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.
            
                if (currentWaveIndex % 10 == 0) // currentWaveIndex ๊ฐ€ 10์˜ ๋ฐฐ์ˆ˜๋‹ค? ( 10 ๋‹จ์œ„๋กœ ๋‚œ์ด๋„ ์ฆ๊ฐ€ )
                {
                    // ์ƒ์„ฑํ•˜๋Š” ํฌ์ง€์…˜ ์ฆ๊ฐ€
                    waveSpawnPosCount = waveSpawnPosCount + 1 > spawnPositions.Count ? waveSpawnPosCount : waveSpawnPosCount + 1;
                }

                if (currentWaveIndex % 5 == 0)
                {

                }

                if (currentWaveIndex % 3 == 0)
                {
                    waveSpawnCount += 1;    // ํ•œ๋ฒˆ์— ๋งŒ๋“ค์–ด์ง€๋Š” ๋ชฌ์Šคํ„ฐ ์ฆ๊ฐ€
                }

                // ๋ช‡ ๊ตฐ๋ฐ์— ์–ผ๋งˆ์”ฉ ์ƒ์„ฑ?
                for(int i = 0; i  < waveSpawnPosCount; i++) // ์œ„์น˜ ๊ฐฏ์ˆ˜
                {
                    int posIdx = Random.Range(0, spawnPositions.Count); // ๋žœ๋ค ์œ„์น˜์—
                    for(int j = 0; j < waveSpawnCount; j++) // ๋งŒ๋“œ๋Š” ๊ฐฏ์ˆ˜
                    {
                        int prefabIdx = Random.Range(0, enemyPrefabs.Count); // ๋žœ๋ค ๋ชฌ์Šคํ„ฐ
                        GameObject enemy = Instantiate(enemyPrefabs[prefabIdx], spawnPositions[posIdx].position, Quaternion.identity);
                        enemy.GetComponent<HealthSystem>().OnDeath += OnEnemyDeath;
                        // enemy.GetComponent<CharacterStatsHandler>
                        currentSpawnCount++;
                        yield return new WaitForSeconds(spawnInterval);
                    }
                }

                currentWaveIndex++;
            }
            yield return null;
        }
    }

    private void OnEnemyDeath()
    {
        currentSpawnCount--;
    }

    private void UpdateHealthUI()
    {
        hpGaugeSlider.value = playerhealthSystem.CurrentHealth / playerhealthSystem.MaxHealth;
    }

    private void GameOver()
    {
        gameOverUI.SetActive(true);
        StopAllCoroutines();    // Coroutin์ด๋ผ๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ์˜ค๋ธŒ์ ํŠธ์— ๊ท€์†๋˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ์— ์ด ์˜ค๋ธŒ์ ํŠธ์—์„œ ๋Œ๊ณ ์žˆ๋Š” ์ฝ”๋ฃจํ‹ด์„ ๋‹ค ๋ฉˆ์ถฐ๋ผ.
    }

    private void UpdateWaveUI()
    {
        waveText.text = (currentWaveIndex + 1).ToString();  // 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ธฐ์— + 1
    }

SpawnPositions ์ถ”๊ฐ€

  • Level ํ•˜์œ„ ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ โž” SpawnPositions ์ด๋ฆ„ ๋ณ€๊ฒฝ

  • ํ•˜์œ„ ๋นˆ ์˜ค๋ธŒ์ ํŠธ ์ƒ์„ฑ

  • ์ธ์ŠคํŽ™ํ„ฐ์—์„œ ๊ธฐ์ฆˆ๋ชจ ์„ค์ •

  • ๋ชฌ์Šคํ„ฐ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์‹ถ์€ ์œ„์น˜๋งŒํผ ์ถ”๊ฐ€

GameManager ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •

  • Enemy Prefabs ์ถ”๊ฐ€. Spawn Position Root ์ถ”๊ฐ€












๐Ÿ“Œ ์Šคํ…Ÿ ๊ณ„์‚ฐํ•˜๊ธฐ (๋ณต์žกํ•จ)

โž” ๐Ÿ”ฅ ํ•ต์‹ฌ ๋‚ด์šฉ

switch๋ฌธ & ํŒจํ„ด ๋งค์นญ

  • Switch๋ฌธ : ํ”„๋กœ๊ทธ๋ž˜๋ฐ์—์„œ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ํ•˜๋Š” ์ œ์–ด ๊ตฌ์กฐ. ๋‹ค์–‘ํ•œ ๊ฒฝ์šฐ์˜ ์ˆ˜๋ฅผ ๊ฐ€์ง„ ์กฐ๊ฑด์„ ์ฒ˜๋ฆฌํ•  ๋•Œ ํšจ์œจ์ .

  • ํŒจํ„ด ๋งค์นญ : C# 7.0๋ถ€ํ„ฐ ์ถ”๊ฐ€๋œ ๊ธฐ๋Šฅ์œผ๋กœ, ์ผ์น˜ํ•˜๋Š” ํŒจํ„ด์— ๋”ฐ๋ผ ์ฝ”๋“œ๋ฅผ ์‹คํ–‰. switch๋ฌธ์„ ์ด์šฉํ•˜์—ฌ ๊ฐ์ฒด์˜ ํƒ€์ž…์ด๋‚˜ ๊ฐ’์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ค๋ฅด๊ฒŒ ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํŒจํ„ด ๋งค์นญ in switch๋ฌธ : ์ฝ”๋“œ์˜ ๊ฐ€๋…์„ฑ์„ ๋†’์ด๊ณ  ์œ ์ง€ ๋ณด์ˆ˜๋ฅผ ์‰ฝ๊ฒŒ ํ•˜๋Š”๋ฐ ๋„์›€์„ ์ค€๋‹ค. ๊ฐ์ฒด์˜ ํƒ€์ž…์„ ํ™•์ธํ•˜๊ฑฐ๋‚˜ ํŠน์ • ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋Š”์ง€ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋ฉด์„œ ๋™์‹œ์— ๋ณ€์ˆ˜์— ๊ฐ’์„ ํ• ๋‹น ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ์˜ˆ์ œ :

    	- **`case RangedAttackData _:`** **`CurrentStats.attackSO`**๊ฐ€ **`RangedAttackData`**ํƒ€์ž…์ผ ๊ฒฝ์šฐ์— ์‹คํ–‰.
    • case MeleeAttackConfig _: CurrentStats.attackSO๊ฐ€ MeleeAttackConfigํƒ€์ž…์ผ ๊ฒฝ์šฐ์— ์‹คํ–‰
  • ์ด๋Ÿฐ ๋ฐฉ์‹์„ ์ด์šฉํ•˜๋ฉด, ์—ฌ๋Ÿฌ ํƒ€์ž…์˜ ๊ฐ์ฒด๋ฅผ ์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์–ด ํ™•์žฅ์„ฑ๊ณผ ์œ ์—ฐ์„ฑ์ด ํ–ฅ์ƒ๋œ๋‹ค.






โž” ์ถ”๊ฐ€ ์Šคํ…Ÿ์„ ์œ„ํ•œ ๊ณ„์‚ฐ ๊ณต์‹

CharacterStatsHandler ์ˆ˜์ •

public class CharacterStatsHandler : MonoBehaviour
{
    private const float MinAttackDelay = 0.03f;
    private const float MinAttackPower = 0.5f;
    private const float MinAttackSize = 0.4f;
    private const float MinAttackSpeed = 0.1f;

    private const float MinSpeed = 0.8f;

    private const int MinMaxHealth = 5;

    [SerializeField] private CharacterStats baseStats;
    public CharacterStats CurrentStats { get; private set; }
    public List<CharacterStats> statsModifiers = new List<CharacterStats>();

    private void Awake()
    {
        UpdateCharacterStats();
    }

    #region Stats Add || Remove
    // AddStatModifier();, RemoveStatModifier() : ์ƒˆ๋กœ์šด ์บ๋ฆญํ„ฐ ์Šคํƒฏ์„ ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ ๋บ„์ˆ˜์žˆ๊ฒŒ
    // ์˜ˆ) ์•„์ดํ…œ์„ ์žฅ์ฐฉํ–ˆ๋‹ค ? ์•„์ดํ…œ์— ๋Œ€ํ•œ ์Šคํƒฏ์ด ํฌํ•จ์ด ๋˜๊ณ  ์›๋ž˜ ์Šคํƒฏ์—๋‹ค ์ด ์Šคํƒฏ์„ ์ถ”๊ฐ€ํ•œ๋‹ค.
    public void AddStatModifier(CharacterStats statModifier)
    {
        statsModifiers.Add(statModifier);
        UpdateCharacterStats();
    }

    public void RemoveStatModifier(CharacterStats statModifier)
    {
        statsModifiers.Remove(statModifier);
        UpdateCharacterStats();
    }
    #endregion

    #region UpdateStats
    private void UpdateCharacterStats()
    {
        AttackSO attackSO = null;
        if (baseStats.attackSO != null)
        {
            attackSO = Instantiate(baseStats.attackSO);
        }

        CurrentStats = new CharacterStats { attackSO = attackSO };  // CurrentStats ์ƒ์„ฑ, baseStats์„ (CurrentStats์—๋‹ค๊ฐ€)๋„˜๊ฒจ ๋ฐ›๋Š”๋‹ค.
        UpdateStats((a, b) => b, baseStats);    // ( (a, b) => b  : a, b๋ฅผ ๋ฐ›์•„์„œ b๋ฅผ ์“ฐ๊ฒ ๋‹ค. ) , (baseStats(CurrnetStats)์„ ๊ฐ€์ ธ๊ฐ€์„œ UpdateStats์— ์“ฐ๊ฒ ๋‹ค.)
        if (CurrentStats.attackSO != null)  // attackSO๊ฐ€ ์žˆ๋Š”์ง€ ์ฒดํฌ
        {
            CurrentStats.attackSO.target = baseStats.attackSO.target;   // ํƒ€๊ฒŸ ์„ค์ •
        }

        foreach (CharacterStats modifier in statsModifiers.OrderBy(o => o.statsChangeType)) // statsChangeType์— ๋งž์ถฐ์„œ OrderBy(์˜ค๋ฆ„์ฐจ์ˆœ)์ •๋ ฌ
        {
            if (modifier.statsChangeType == StatsChangeType.Override)   // ๋ฎ์–ด์”Œ์šฐ๊ธฐ
            {
                UpdateStats((o, o1) => o1, modifier);
            }
            else if (modifier.statsChangeType == StatsChangeType.Add)   // ๋”ํ•˜๊ธฐ
            {
                UpdateStats((o, o1) => o + o1, modifier);
            }
            else if (modifier.statsChangeType == StatsChangeType.Multiple)  // ๊ณฑํ•˜๊ธฐ
            {
                UpdateStats((o, o1) => o * o1, modifier);
            }
        }

        LimitAllStats();    // ์ตœ์†Œ ์Šคํƒฏ ๋ฆฌ๋ฐ‹ ์ฒดํฌ
    }

    // Func : ๋งค๊ฐœ๋ณ€์ˆ˜ ์•ž์— 2๊ฐœ๋ฅผ ๋ฐ›์•„์„œ ๋ฐ˜ํ™˜(3๋ฒˆ์งธ) ๋ฐ‘์˜ ๊ฒฝ์šฐ float์„ 2๊ฐœ ๋ฐ›์•„์„œ float์„ ๋ฐ˜ํ™˜
    private void UpdateStats(Func<float, float, float> operation, CharacterStats newModifier)
    {
        CurrentStats.maxHealth = (int)operation(CurrentStats.maxHealth, newModifier.maxHealth);
        CurrentStats.speed = operation(CurrentStats.speed, newModifier.speed);

        UpdateAttackStats(operation, CurrentStats.attackSO, newModifier.attackSO);

        // ๋‚ด๊ฐ€๊ฐ€์ง„ ํƒ€์ž…(CurrentStats)๊ณผ ๋„˜๊ฒจ๋ฐ›์€ ํƒ€์ž…(newModifier)์ด ๋‹ค๋ฅด๋ฉด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š๋Š”๋‹ค.
        // ์˜ˆ) Ranged Type์ธ๋ฐ ๋‹ค๋ฅธ ํƒ€์ž…์ด ๋“ค์–ด์˜ค๋ฉด ? ์ฒ˜๋ฆฌ x
        if (CurrentStats.attackSO.GetType() != newModifier.attackSO.GetType())
        {
            return;
        }

        // ์ƒˆ๋กœ์šด ํƒ€์ž…์˜ ์ฒ˜๋ฆฌ
        switch (CurrentStats.attackSO)
        {
            case RangedAttackData _:
                ApplyRangedStats(operation, newModifier);
                break;
        }
    }

    // AttackStats
    private void UpdateAttackStats(Func<float, float, float> operation, AttackSO currentAttack, AttackSO newAttack)
    {
        if (currentAttack == null || newAttack == null || currentAttack.GetType() != newAttack.GetType())
        {
            return;
        }

        currentAttack.delay = operation(currentAttack.delay, newAttack.delay);
        currentAttack.power = operation(currentAttack.power, newAttack.power);
        currentAttack.size = operation(currentAttack.size, newAttack.size);
        currentAttack.speed = operation(currentAttack.speed, newAttack.speed);
    }

    // RangedStats
    private void ApplyRangedStats(Func<float, float, float> operation, CharacterStats newModifier)
    {
        RangedAttackData currentRangedAttacks = (RangedAttackData)CurrentStats.attackSO;

        if (!(newModifier.attackSO is RangedAttackData))
        {
            return;
        }

        RangedAttackData rangedAttackModifier = (RangedAttackData)newModifier.attackSO;
        currentRangedAttacks.multipleProjectilesAngel = operation(currentRangedAttacks.multipleProjectilesAngel, rangedAttackModifier.multipleProjectilesAngel);
        currentRangedAttacks.spread = operation(currentRangedAttacks.spread, rangedAttackModifier.spread);
        currentRangedAttacks.duration = operation(currentRangedAttacks.duration, rangedAttackModifier.duration);
        currentRangedAttacks.numberofProjectilesPerShot = Mathf.CeilToInt(operation(currentRangedAttacks.numberofProjectilesPerShot, rangedAttackModifier.numberofProjectilesPerShot));
        currentRangedAttacks.projectileColor = UpdateColor(operation, currentRangedAttacks.projectileColor, rangedAttackModifier.projectileColor);
    }

    // Color(Ranged)
    private Color UpdateColor(Func<float, float, float> operation, Color currentColor, Color newColor)
    {
        return new Color(operation(currentColor.r, newColor.r),
                         operation(currentColor.g, newColor.g),
                         operation(currentColor.b, newColor.b),
                         operation(currentColor.a, newColor.a));
    }
    #endregion

    #region LimitStats
    private void LimitStats(ref float stat, float minVal)
    {
        stat = Mathf.Max(stat, minVal);
    }

    private void LimitAllStats()
    {
        if (CurrentStats == null || CurrentStats.attackSO == null)
        {
            return;
        }

        LimitStats(ref CurrentStats.attackSO.delay, MinAttackDelay);
        LimitStats(ref CurrentStats.attackSO.power, MinAttackPower);
        LimitStats(ref CurrentStats.attackSO.size, MinAttackSize);
        LimitStats(ref CurrentStats.attackSO.speed, MinAttackSpeed);
        LimitStats(ref CurrentStats.speed, MinSpeed);
        CurrentStats.maxHealth = Mathf.Max(CurrentStats.maxHealth, MinMaxHealth);
    }
    #endregion
}











๐Ÿ“Œ ์•„์ดํ…œ

โž” ์•„์ดํ…œ ์ถ”๊ฐ€ํ•˜๊ธฐ

PickupItem ๋งŒ๋“ค๊ธฐ

public abstract class PickupItem : MonoBehaviour
{
    [SerializeField] private bool destroyOnPickup = true;   // Pickupํ–ˆ์„ ๋•Œ, ์‚ญ์ œํ•  ๊ฒƒ์ธ๊ฐ€?
    [SerializeField] private LayerMask canBePickupBy;   // ๋‚ด๊ฐ€ ๋จน์„ ์ˆ˜ ์žˆ๋Š” ๊ฑด์ง€ Layer๋กœ ์ฒดํฌ
    [SerializeField] private AudioClip pickupSound;

    private void OnTriggerEnter2D(Collider2D other)
    {
        // ๋‚ด๊ฐ€ ๋จน์„ ์ˆ˜ ์žˆ๋Š” ๋…€์„์ธ์ง€ ๊ฒ€์‚ฌ
        if (canBePickupBy.value == (canBePickupBy.value | (1 << other.gameObject.layer)))
        {
            OnPickedUp(other.gameObject);   // ํ”ฝ์—…
            if (pickupSound)
                SoundManager.PlayClip(pickupSound); // ์‚ฌ์šด๋“œ

            if (destroyOnPickup)
            {
                Destroy(gameObject);    // ์‚ญ์ œ
            }
        }
    }

    // ์ถ”์ƒ ๋ฉ”์„œ๋“œ (์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›๋Š” ํด๋ž˜์Šค๋“ค์€ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ๋ฌด์กฐ๊ฑด ๊ตฌํ˜„ ํ•ด์•ผํ•œ๋‹ค.)
    protected abstract void OnPickedUp(GameObject receiver); 
}

PickupStatMofifiers ๋งŒ๋“ค๊ธฐ

public class PickupStatModifiers : PickupItem
{
    [SerializeField] private List<CharacterStats> statsModifier;

    // ์–ด๋– ํ•œ ์Šคํ…Ÿ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๊ฐ€ CharacterStatsHandler์—๋‹ค ์ถ”๊ฐ€ํ•ด์ค€๋‹ค.
    protected override void OnPickedUp(GameObject receiver)
    {
        CharacterStatsHandler statsHandler = receiver.GetComponent<CharacterStatsHandler>();
        foreach(CharacterStats stat in statsModifier)
        {
            statsHandler.AddStatModifier(stat);
        }
    }
}

PickupHeal ๋งŒ๋“ค๊ธฐ

public class PickupHeal : PickupItem
{
    [SerializeField] int healValue = 10;
    private HealthSystem _healthSystem;

    protected override void OnPickedUp(GameObject receiver)
    {
        _healthSystem = receiver.GetComponent<HealthSystem>();
        _healthSystem.ChangeHealth(healValue);
    }
}





GameManager ์ˆ˜์ •

if(currentWaveIndex % 5 ==0)
{
    CreateReward();
}

--------------- ์ƒ๋žต  ---------------

void CreateReward()
{
    // spawnPositions ๋žœ๋ค์ค‘์— ํ•˜๋‚˜๋ฅผ ๋ฐ›์•„์„œ ๊ทธ ์œ„์น˜์— ์ƒ์„ฑํ•˜๊ฒŒ ํ•œ๋‹ค.
    int idx = Random.Range(0, rewards.Count);
    int posIdx = Random.Range(0, spawnPositions.Count);

    GameObject obj = rewards[idx];  // rewards์ค‘ ํ•˜๋‚˜ ๋žœ๋ค ํ• ๋‹น
    Instantiate(obj, spawnPositions[posIdx].position, Quaternion.identity);
}

Item Object ๋งŒ๋“ค๊ธฐ

  • ์•„์ดํ…œ์— ์‚ฌ์šฉ๋  ์Šคํ”„๋ผ์ดํŠธ ๊บผ๋‚ด๊ธฐ

  • red โž” Pickup Heal ์ถ”๊ฐ€

  • green โž” Pickup Stat Modifiers ์ถ”๊ฐ€

  • yellow โž” Pickup Stat Modifiers ์ถ”๊ฐ€

  • Ranged Attack Data ์ƒ์„ฑ โž” Yellow_RangedAttackData ์ด๋ฆ„ ๋ณ€๊ฒฝ






GameManager ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •

  • ์‹ ์— ๋ฐฐ์น˜๋œ ์•„์ดํ…œ ํ”„๋ฆฌํŒนํ™” โž” ์‚ญ์ œ

  • ์›ํ•˜๋Š” ๋งŒํผ ๋ณด์ƒ์„ ์ถ”๊ฐ€












๐Ÿ“Œ ๋กœ์ง ๊ฐ•ํ™”ํ•˜๊ธฐ

โž” ๋กœ์ง ๊ฐ•ํ™”

์ฝ”๋“œ ์ถ”๊ฐ€

public class GameManager : MonoBehaviour
{
 ... ์ƒ๋žต ...

    [SerializeField] private CharacterStats defaultStats;
    [SerializeField] private CharacterStats rangedStats;

 ... ์ƒ๋žต ...

    private void Start()
    {
        UpgradeStatInit();
        StartCoroutine("StartNextWave");
    }

    IEnumerator StartNextWave()
    {
        while(true)
        {
            if(currentSpawnCount == 0)
            {
                UpdateWaveUI();
                yield return new WaitForSeconds(2f);

                if(currentWaveIndex % 20 == 0)
                {
                    RandomUpgrade();
                }

                ... ์ƒ๋žต ...


                for(int i =  0; i < waveSpawnPosCount;i++)
                {
                    int posIdx = Random.Range(0, spawnPostions.Count);
                    for(int j = 0; j <waveSpawnCount;j++)
                    {
                        int prefabIdx = Random.Range(0,enemyPrefebs.Count);
                        GameObject enemy = Instantiate(enemyPrefebs[prefabIdx], spawnPostions[posIdx].position, Quaternion.identity);
                        enemy.GetComponent<HealthSystem>().OnDeath += OnEnemyDeath;
                        // enemy๊ฐ€ ์ƒˆ๋กœ ์ฐ์–ด์งˆ ๋•Œ ๋งˆ๋‹ค GetComponent๋กœ ๊ฐ€์ ธ์˜ค๋ฏ€๋กœ ์›จ์ด๋ธŒ์— ๋”ฐ๋ผ ๋žœ๋ค์œผ๋กœ ์Šคํ…Ÿ์„ ์˜ฌ๋ ค๋‘” default, ranged ์Šคํ…Ÿ์„ ์ถ”๊ฐ€(๋”ํ•˜๊ธฐ)ํ•˜์—ฌ ๊ฐ•ํ•˜๊ฒŒ ๋งŒ๋“ ๋‹ค.
                        enemy.GetComponent<CharacterStatsHandler>().AddStatModifier(defaultStats);
                        enemy.GetComponent<CharacterStatsHandler>().AddStatModifier(rangedStats);
                        currentSpawnCount++;
                        yield return new WaitForSeconds(spawnInterval);
                    }
                }

                currentWaveIndex++;
            }

            yield return null;
        }
    }

 ... ์ƒ๋žต ...

    void UpgradeStatInit()
    {
    	// ์บ๋ฆญํ„ฐ ์˜ค๋ธŒ์ ํŠธ๋ฅผ ๋ฐ”๋กœ ์ˆ˜์ •ํ•ด๋ฒ„๋ฆฌ๋ฉด ๋‚จ์•„๋ฒ„๋ฆฌ๊ธฐ ๋•Œ๋ฌธ์—, ๋ฏธ๋ฆฌ ๋ณต์‚ฌ๋ฅผ ํ•œ๋‹ค. (Instantiate)
        defaultStats.statsChangeType = StatsChangeType.Add;
        defaultStats.attackSO = Instantiate(defaultStats.attackSO);

        rangedStats.statsChangeType = StatsChangeType.Add;
        rangedStats.attackSO = Instantiate(rangedStats.attackSO);
    }

    void RandomUpgrade()
    {
        switch (Random.Range(0,6))
        {
            case 0:
                defaultStats.maxHealth += 2;
                break;

            case 1:
                defaultStats.attackSO.power += 1;
                break;

            case 2:
                defaultStats.speed += 0.1f;
                break;

            case 3:
                defaultStats.attackSO.isOnKnockback = true;
                defaultStats.attackSO.knockbackPower += 1;
                defaultStats.attackSO.knockbackTime = 0.1f;
                break;

            case 4:
                defaultStats.attackSO.delay -= 0.05f;
                break;

            case 5:
                RangedAttackData rangedAttackData = rangedStats.attackSO as RangedAttackData;
                rangedAttackData.numberofProjectilesPerShot += 1;
                break;

            default:
                break;
        }
    }
}

GameManager ์ปดํฌ๋„ŒํŠธ ์ˆ˜์ •

  • RangedAttackData, DefaultAttackData ์ƒ์„ฑ

  • Default Stats์™€ Ranged Stats๋ฅผ ์ˆ˜์ •

0๊ฐœ์˜ ๋Œ“๊ธ€