Notice
Recent Posts
Recent Comments
Link
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

동구의_C# & Unity_개발일지

2024.02.01 내일배움캠프 29일차 TIL_Unity (숙련, 알고리즘) 본문

Unity

2024.02.01 내일배움캠프 29일차 TIL_Unity (숙련, 알고리즘)

mongle_0l 2024. 2. 1. 18:24

오늘은 미루어 왔던 입문 주차 때 지급되었던 강의를 완강하였다.

UI을 만들었고 물약과 사운드 등을 넣어보았다.

중간중간 어려운 부분이 많았지만 천천히 복습해가며 학습해갈 예정이다!

추후에 사용에 숙달해져 시간이 남으면 추가 구현을 더 해보고 싶다.


알고리즘 코드카타 15일차

정수 제곱근 판

문제 설명
임의의 양의 정수 n에 대해, n이 어떤 양의 정수 x의 제곱인지 아닌지 판단하려 합니다.
n이 양의 정수 x의 제곱이라면 x+1의 제곱을 리턴하고, n이 양의 정수 x의 제곱이 아니라면 -1을 리턴하는 함수를 완성하세요.

 


2D 게임 심화 개발 / 2D 게임 로직 및 상태 관리

사운드 컨트롤
UI 만들기
로직 구현하기
스텟 계산하기
아이템
로직 강화하기

 

 

01. 핵심 내용

1-1) 사운드를 처리하는 주요 컴포넌트

AudioClip:

  • AudioClip은 사운드 파일을 Unity에서 사용할 수 있도록 하는 데이터 타입입니다.
  • .wav, .mp3, .ogg 등 다양한 형식의 오디오 파일을 지원합니다.

AudioSource:

  • AudioSource 컴포넌트는 사운드를 재생하는 데 사용됩니다.
  • AudioSource에 AudioClip을 연결하여 재생할 수 있습니다.
  • AudioSource는 3D 사운드 설정, 볼륨 조절, 사운드 반복 재생 등의 설정을 제공합니다.

AudioListener:

  • AudioListener 컴포넌트는 사운드를 듣는 포인트를 나타냅니다.
  • 일반적으로 주요 카메라에 AudioListener가 위치합니다.
  • 게임에는 하나의 AudioListener만 있어야 합니다.

02. 핵심 내용

  • 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 상호작용을 관리합니다.
    • 마우스 클릭, 드래그, 키보드 입력 등 다양한 입력 이벤트를 처리합니다.

2-1) TextMeshPro

  • TextMeshPro는 원래 독립적인 개발자에 의해 Unity 에셋 스토어에서 판매되었습니다. 그러나 이후 Unity Technologies에 인수되어 Unity의 기본 기능으로 포함되었습니다. 이러한 변화는 2017년에 발생했으며, 그 이후로 TextMeshPro는 Unity 사용자들에게 무료로 제공되고 있습니다.
  • TextMeshPro는 Unity에서 제공하는 고급 텍스트 렌더링 시스템입니다. 기본 텍스트 구성 요소보다 훨씬 더 많은 기능과 정확성을 제공합니다.

03. 핵심 내용

3-1) 코루틴(Coroutine)

  • 코루틴은 비동기적으로 실행되는 함수로, 특정 코드 블럭의 실행을 일시적으로 중지하고 다시 시작할 수 있게 해줍니다.
  • IEnumerator 리턴 타입의 함수에서 yield return을 사용하여 코루틴을 구현할 수 있습니다.
  • StartCoroutine 함수를 통해 코루틴을 시작할 수 있고, StopCoroutine 함수를 통해 코루틴을 중지할 수 있습니다.
  • 코루틴은 프레임 간의 지연, 비동기 작업, 시간에 따른 애니메이션 등의 작업에 주로 사용됩니다.
  • yield return null은 다음 프레임까지 대기를 의미하고, yield return new WaitForSeconds(n)은 n초 동안 대기를 의미합니다.
  • 코루틴은 별도의 스레드에서 실행되지 않습니다. 따라서 Unity의 메인 스레드에서 안전하게 Unity API를 호출할 수 있습니다.
  • 코루틴은 일반 함수와는 다르게, 실행을 일시 중단하고 나중에 다시 시작할 수 있어, 시간 지연, 반복, 조건부 대기 등의 작업을 수행할 때 매우 유용합니다.

04. 핵심 내용

4-1) switch문 & 패턴 매칭

  • Switch문: 프로그래밍에서 조건에 따라 다른 동작을 수행하도록 하는 제어 구조. 다양한 경우의 수를 가진 조건을 처리할 때 효율적.
  • 패턴 매칭: C# 7.0부터 추가된 기능으로, 일치하는 패턴에 따라 코드를 실행. switch문을 이용하여 객체의 타입이나 값에 따라 처리를 다르게 할 수 있다.
  • 패턴 매칭 in switch문 : 코드의 가독성을 높이고 유지 보수를 쉽게 하는데 도움을 준다. 객체의 타입을 확인하거나 특정 조건을 충족하는지 여부를 확인하면서 동시에 변수에 값을 할당할 수 있다.
  • 예제:
    • case RangedAttackData _:: CurrentStats.attackSO가 RangedAttackData 타입일 경우에 실행.
    • case MeleeAttackConfig _:: CurrentStats.attackSO가 MeleeAttackConfig 타입일 경우에 실행.
  • 이런 방식을 이용하면, 여러 타입의 객체를 적절하게 처리할 수 있어 확장성과 유연성이 향상된다.

중요한 기능들을 정리해서 알아보자!
스텟 구현 코드
더보기
-------------------------- 생략 -------------------------- 
using System.Linq;

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 = .1f;

    private const float MinSpeed = 0.8f;

    private const int MinMaxHealth = 5;


-------------------------- 생략 -------------------------- 

    public void AddStatModifier(CharacterStats statModifier)
    {
        statsModifiers.Add(statModifier);
        UpdateCharacterStats();
    }

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


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

        CurrentStats = new CharacterStats { attackSO = attackSO };
				// TODO
        CurrentStats.statsChangeType = baseStats.statsChangeType;
        CurrentStats.maxHealth = baseStats.maxHealth;
        CurrentStats.speed = baseStats.speed;
        UpdateStats((a, b) => b, baseStats);
        if (CurrentStats.attackSO != null)
        {
            CurrentStats.attackSO.target = baseStats.attackSO.target;
        }

        foreach (CharacterStats modifier in statsModifiers.OrderBy(o => o.statsChangeType))
        {
            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();
    }

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

        if (CurrentStats.attackSO== null || newModifier.attackSO == null)
           return;

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

        if (CurrentStats.attackSO.GetType() != newModifier.attackSO.GetType())
        {
            return;
        }

        switch (CurrentStats.attackSO)
        {
            case RangedAttackData _:
                ApplyRangedStats(operation, newModifier);
                break;
        }
    }

    private void UpdateAttackStats(Func<float, float, float> operation, AttackSO currentAttack, AttackSO newAttack)
    {
        if (currentAttack == null || newAttack == null)
        {
            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);
    }

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

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

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

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

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

}
캐릭터의 스텟을 계산할 수 있게 하는 코드이다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class PickupItem : MonoBehaviour
{
    [SerializeField] private bool destroyOnPickup = true;
    [SerializeField] private LayerMask canBePickupBy;
    [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);
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PickupStatModifiers : PickupItem
{
    [SerializeField] private List<CharacterStats> statsModifier;
    protected override void OnPickedUp(GameObject receiver)
    {
        CharacterStatsHandler statsHandler = receiver.GetComponent<CharacterStatsHandler>();
        foreach (CharacterStats stat in statsModifier)
        {
            statsHandler.AddStatModifier(stat);
        }
    }

}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

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

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

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

--------------- 생략  ---------------

void CreateReward()
{
    int idx = Random.Range(0, rewards.Count);
    int posIdx = Random.Range(0, spawnPostions.Count);

    GameObject obj = rewards[idx];
    Instantiate(obj, spawnPostions[posIdx].position, Quaternion.identity);
}
아이템을 생성하고 먹을 수 있는 코드이다.
피를 채울 수 있고 화살이 강화되며 이동속도가 증가하는 아이템 물약을 추가할 수 있는 코드이다.

게임 개발 입문 과정

TopDownShooting 완성!