01/19 본캠프#19

guno park·2024년 1월 19일
0

본캠프

목록 보기
19/77
post-custom-banner

알고리즘 풀어보기

문자열 나누기

풀이

나누기를 종료하는 조건은 두 가지로
1. 남은 부분이 없다면 종료
2. 두 횟수가 다른 상태에서 더 이상 읽을 글자가 없다면, 읽은 문자열을 분리하고 종료

그래서 두 조건 중 하나를 만족할 때까지 반복하기 위해 while문을 사용하고,
글자를 차례대로 검사하기 위해 for문을 문자열의 길이만큼 반복한다.

매번 for 반복마다 문자열 x의 맨 앞 글자를 선택하고, 차례대로 비교해 카운트를 쌓는다.
그러다 횟수가 같아지면 읽은 문자열을 분리해 뒷부분을 다시 x로 반환하고,
만약 끝까지 같아지지 않는다면 반복을 종료한다. 이 부분 또한 문자열을 분리하여야 하기 때문에 나눈 횟수 answer에 ++을 해준다.

using System;

public class Solution {
    public int solution(string s) {
       int answer = 0;

		char x = 'a';
		int xCnt = 0;
		int otherCnt = 0;
        bool isPossible = true;
		while (isPossible)
		{
    		for (int i = 0; i < s.Length; i++)
    		{
        		x = s[0];
        		if (x == s[i])
            		xCnt++;
        		else otherCnt++;
                
        		if (xCnt == otherCnt)
        		{
            		s = s.Substring(xCnt + otherCnt); //여길 조져야됨.
            		answer++;
            		xCnt = 0;
            		otherCnt = 0;
            		if (s.Length ==0)
               		 isPossible = false;
            		break;
       			 }
        		else if (i == s.Length-1 && xCnt != otherCnt){
            		answer++;
            		isPossible = false;
            		break;
        		}
    		}
		}
		return answer;
    }
}

트러블 슈팅

읽은 부분까지 나누기

예전에 사용했던 Substring 메서드를 사용해 분리했는데, 쓰던 방식대로 사용하려다보니 문제가 발생했다.

Substring(시작인덱스,길이); : 시작 인덱스부터 길이만큼의 문자열을 반환함
Substring(시작인덱스); : 시작 인덱스부터 문자열 끝까지를 반환함.

사용방법이 이렇게 두 가지인데, 이걸 조합하면 특정 문자열을 제외하고 반환시킬수 있다.

string s = "abcedf";
s=s.Substring(0,2)+s.Substring(3); //"abedf" 반환

이 방법을 읽은 문자열 이후를 반환시키는데 사용해버렸더니, 제거되야하는 문자가 계속 붙어나왔다.
천천히 코드를 읽었을 때 발견해서 정정해주었다.

while이 종료되는 조건

처음에는 while(x.Length >0)로 하고 특정 조건이 됬을 때 break로 빠져나오려고 했었다.
그 조건이 2번째 종료조건인데, 2번째 종료조건을 만족할 때 문자열을 분리하는 기능이 없어서 무한루프에 빠져버리고 말았다.
그래서 while의 조건을 bool값으로 주고, 종료 조건 2가지에 부합하면 false로 바꿔서 종료될 수 있도록 해주었다.

유니티 입문 강좌 뜯어먹기

어제에 이어 8강부터 시작

8강 스탯시스템

CharacterStats 클래스

클래스를 데이터 저장용으로 사용할 때는 객채화 시키지 않기 때문에 Monobehaviour에 상속시킬 필요가 없다.
=> 유니티에서 쓸 수 있는 메세지들을 아무것도 쓸 수 없음

[Range(0,100)]public int maxHealth; //인스펙터에서 조정할 수 있게 슬라이드바가 생김

스크립터블 오브젝트 (ScriptableObject)

public class AttackSO : ScriptableObject
스크립터블 오브젝트는 당연하게도 스크립터블 오브젝트를 상속받는다.

여기서는 공격에 관한 데이터 저장소

사용 이유

클래스 방식으로 구현하는 방식도 있겠지만 객체마다 자기 나름의 저장공간을 가지고있다.
똑같은 몬스터가 100마리 1000마리가 됬을 때 공격 데이터도 다 할당을 해줘야되기
때문에 클라이언트가 복잡하고 무거워진다.
그래서 스크립터블 오브젝트를 쓴다.
하나의 데이터 컨테이너를 만들어놓고 모두가 공유해 쓰는 것
훨씬 메모리적으로 간편하고 접근도 용이하며, 인스펙터 상에서
조절 가능하기때문에 훨씬 간편하다.

클래스를 인스펙터창에서 보려면

1-9강 16:20초쯤 스탯핸들러 넣어줬는데 인스펙터에 안보인다. 왜냐면 클래스라서, 보이게 하려면 클래스 위에도 시리얼라이즈블을 걸어줘야된다.

투사체 구현해보기

as 형변환

private void OnShoot(AttackSO attackSO)
   {
       RangedAttackData rangedAttackData = attackSO as RangedAttackData;
       float projectilesAngleSpace = rangedAttackData.multipleProjectilesAngel;
       int numberOfProjectilesPerShot = rangedAttackData.numberofPorjectilesPerShot;
	}

as는 보통 캐스팅 할 때 사용되는 문법이다.
연산의 결과로 캐스팅 성공은 성공한 거고, 실패하면 null을 반환한다.
참조 타입간의 캐스팅만 가능하고 값 형식끼리는 불가능하다.

여기서는 AttackSO 클래스 -> RangedAttackData 클래스로 변환을 하는데,
AttackSO 클래스가 더 상위 클래스로 형변환이 가능하다.
OnShoot, 즉 원거리 캐릭터의 공격데이터가 필요하므로 원거리 데이터로 바꿔주는 것이다. 만약 기존 매개변수에 원거리 데이터가 없다면 형변환을 하면서 기존에는 없는 투사체에 관한 멤버변수들이 추가된다. 이 경우에는 기본값으로 들어올 것이라 생각한다.

싱글턴 패턴

"싱글턴 패턴"은 소프트웨어 디자인 패턴 중 하나로, 특정 클래스의 인스턴스가 하나만 존재하도록 보장하고, 이를 전역적으로 접근할 수 있는 글로벌 접근점을 제공하는 패턴입니다.

  • 싱글턴 패턴은 클래스의 인스턴스가 하나만 존재하도록 보장하는 디자인 패턴입니다.
  • 싱글턴 객체는 전역적으로 접근 가능한 글로벌 접근점을 제공합니다.
  • 싱글턴 패턴은 전역 변수를 사용하지 않고도 객체 간 데이터를 공유하거나, 전역적인 상태를 관리하거나, 특정 서비스를 애플리케이션 전체에서 사용할 수 있게 해줍니다.
  • 싱글턴 패턴은 잘못 사용하면 코드의 결합도를 높이고 테스트와 유지 보수를 어렵게 할 수 있으므로 주의해서 사용해야 합니다.
  • 싱글턴 객체는 프로그램의 수명주기 동안 계속 존재하므로, 메모리 관리에 신경써야 합니다.

쿼터니언과 벡터의 곱셈

  1. 먼저 벡터 v를 쿼터니언으로 변환합니다. 이때 v = (x, y, z)인 벡터는 (0, x, y, z)라는 쿼터니언으로 변환됩니다.
  2. 이렇게 변환된 벡터 쿼터니언을 원래의 쿼터니언 q와 그 켤레 쿼터니언 q와의 사이에 두고 곱셈을 수행합니다. 즉, 결과는 qvq 형태가 됩니다.
  3. 이 결과로 나온 쿼터니언의 x, y, z 요소는 원래 벡터 v가 쿼터니언 q에 의해 회전한 후의 좌표를 나타냅니다.

Unity에서는 Quaternion과 Vector3의 곱셈을 직접 지원하며, 이는 내부적으로 위에서 설명한 과정을 거쳐 수행됩니다.

우리가 일반적으로 쓰는 각도는 Degree라고 한다.
유니티에서는 각도를 쿼터니언(오일러각)으로 계산하기 때문에, 각도를 회전하려면 쿼터니언에 곱해주는 방식으로 해야된다.
또한 유니티에서 각도를 구할 때 각도의 최초 형태는 라디안각으로 이를 쿼터니언에 곱해주기 위해서는 라디안각을 Degree로 변환하고 곱해야 한다.

float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg; 
//라디안각을 디그리로 변경함
 armPivot.rotation = Quaternion.Euler(0,0,rotZ); 
 //유니티에서는 쿼터니언을 사용함.

레이어

레이어란?

레이어는 주로 게임 오브젝트들을 그룹화하기 위해 사용된다.
레이어 또한 해쉬 값을 가지고 있고, 그것을 사용할 수 있는 메서드를 유니티에서 제공하기 때문에 우리는 그 점을 이용하여 렌더링, 충돌감지, 레이어 마스크를 통한 타겟 선별 등 다양한 작업에 이용할 수 있다.
특히 레이어 마스크와 레이캐스트를 통해 피격당한 투사체를 감지하고, 상호작용을 할 수 있게 해준다.

레이어 비트 연산 (중요)

  • 사용 이유
    레이어의 값은 이진으로 되어 있다.
    태그 비교나 레이어 검사를 해줄 수 도 있는데, 비트 검사하는게 빠르다.
    자주 사용하는 방법으로 기억해두면 좋다.

  • 비트 연산 하는 방법

 RaycastHit2D hit = Physics2D.Raycast(transform.position, direction, 11f,
                    (1 << LayerMask.NameToLayer("Level")) | layerMaskTarget);

라는 코드가 있을 때,

(1 << LayerMask.NameToLayer("Level")) | layerMaskTarget)

이 부분은 레이캐스트에서 검출하려고 하는 레이어들이 동시에 체크되는 레이어 마스크를 만드는 과정이다.
예시로 설명하자면, LayerMaskTarget = "Player"로 8번, "Level"은 7번이다.
(1<<7 | 8) => (10000000 | 1000) => 10001000 이 된다.
챗GPT가 자꾸 10010000이라고 우겨서 싸웠는데 내가 이겼다.
여튼 그래서 저 hit는 10001000를 검사하는 레이어 마스크를 가진다.

layerMaskTarget == (layerMaskTarget | (1 << hit.collider.gameObject.layer))

그리고 이 충돌한 레이캐스트는 반환값으로 콜라이더를 가져오는데, 이 콜라이더의 레이어를 비교하여 지정한 대상에게만 동작하도록 조건을 만들 수 있다.
위처럼 layerMaskTarget ="Player"이고, 8번이 반환되었다고 해보자.

layerMask는 32비트의 int형이고, 비트 플래그로 각각의 레이어들을 상징하기 때문에 32개까지만 만들 수 있다. 출처
(8 | (1<< 8))이라고 할 때, 앞의 8 또한 비트 플래그이므로 1<<8과 같다. 두 개가 모두 같기 때문에 똑같은 똑같은 값이 나오면, 앞의 논리연산을 시작할 수 있다.

정리

쭉 풀어서 써봤는데, 그냥 외워라.

오브젝트 풀 구현하기

오브젝트 풀이란?

오브젝트를 미리 생성한 후 삭제하지 않고 재사용하는 것
오브젝트 생성/파괴에는 큰 비용을 담당하기 때문에 그런 비용을 최소화하는 패턴 중 하나.

코드 뜯어보기

인스펙터에서 볼 수 있어야하기 때문에 Serializable을 넣어주고 시작한다.

이 오브젝트 풀의 이름, 관리할 오브젝트의 프리팹, 크기를 구조체로 작성해준다.
그리고 관리하기 편하게 리스트로 직렬화 해준다.

 public struct Pool
    {
        public string tag;
        public GameObject prefab;
        public int size;
    }
public List<Pool> pools;

키값으로 오브젝트를 풀링하기 위해서 딕셔너리도 만들고,
오브젝트 풀 Awake 시 오브젝트 풀을 세팅해준다.

public Dictionary<string, Queue<GameObject>> PoolDictionary;

    private void Awake()
    {
        PoolDictionary = new Dictionary<string, Queue<GameObject>>();
        foreach (var pool in pools)
        {
            Queue<GameObject> objectPool = new Queue<GameObject>();
            for (int i = 0; i < pool.size; i++)
            {
                GameObject obj = Instantiate(pool.prefab);
                obj.SetActive(false);
                objectPool.Enqueue(obj);
            }
            PoolDictionary.Add(pool.tag,objectPool);
        }
    }

세팅 할 때는 pools에 있는 목록들을 순환하며 만드는데, 오브젝트들을 size만큼 복제해서 큐에다가 집어넣고, 키값과 함께 큐를 딕셔너리에 추가한다.

public GameObject SpawnFromPool(string tag)
    {
        if (!PoolDictionary.ContainsKey(tag))
            return null;

        GameObject obj = PoolDictionary[tag].Dequeue(); 
        PoolDictionary[tag].Enqueue(obj);
        
        //제한 개수를 넘어서면 제일 마지막에 썼던 걸 또 씀.

        return obj;
    }

풀에서 사용할 때는 태그를 매개변수로 받아 사용하는데, 만약 태그를 key로 가지는 딕셔너리가 없다면 돌아가고, obj를 받기위해 Dequeue 먼저 하고, 다시 큐에 넣어 준 후, 메서드 끝에 반환한다. 이 반환된 오브젝트가 현재 날아가는 투사체다.

애니메이션 적용하기

TopDownAnimation.cs

애니메이션 관련 최상위 클래스로, 애니메이터와 캐릭터 컨트롤러를 멤버로 가지고 있으며, Awake 할 때 컴포넌트를 가지고 온다.

ublic class TopDownAnimation : MonoBehaviour
{
   protected Animator animator;
   protected TopDownCharacterController controller;

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

TopDownAnimationController.cs

private static readonly int IsWalking = Animator.StringToHash("IsWalking"); 
private static readonly int Attack = Animator.StringToHash("Attack");  
private static readonly int IsHit = Animator.StringToHash("IsHit");

특정 문자열을 공식을 통해 숫자값으로 변환 (.StringToHash("애니메이션 파라미터 이름")
애니메이션 안에서 키값을 문자열로 제공했을 때,비교할 때 비용이 높기때문에
처리할 때 해쉬값으로 처리해라. 자주사용 되는 코드.

여기서 애니메이션들의 파라미터를 바꿔주는 메서드들을 만들고, 각 이벤트들에 구독해준다.

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

        if (_healthSystem != null)
        {
            _healthSystem.OnDamage += Hit;
            _healthSystem.OnInvincibilityEnd += InvinciblilityEnd;
        }
    }

    // Update is called once per frame
    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 InvinciblilityEnd()
    {
        animator.SetBool(IsHit,false);
    }

레이어

여러 애니메이션 혼합할 때 사용 (별개로 도는 레이어)
동시에 움직여야될 때 사용함
ex) 움직이면서(1레이어) 화살쏘기 (2레이어)

적만들기

고블린

가까이서 공격하고, 몸이 닿아있는 동안에는 공격하는 몬스터

근접에서 공격할 경우, ontriggerenter와 exit를 사용해 내가 지금 타겟을 공격할 수 있는 거리에 있는 지 판별하는 bool값을 만들어 주는 방식으로 작성되었다.
이를 위해 서클 콜라이더를 만들고 istrigger 체크해주었다.

오크 주술사

원거리 공격을 하는데, 공격 사정거리 이내에 들어왔을 때, 레이캐스트를 쏘았을 때 플레이어가 아니면 더 이동하고, 플레이어가 맞을 시 발사하는 메커니즘이다.

파티클 연결하기

동작하는 코드만 작성하고 에니메이션에서 샘플 우측에 길쭉한거 누르면 이벤트 추가하기 있음

애니메이션 이벤트라는건데 꼭 같이 설정되어 있는 곳에 애니메이션이 있어야 호출할 수 있음.

파티클 시스템

  • Limit Velocity over Lifetime 안에 Dampen = 저항정도, 파티클이 멀리 못나감.
  public void CreateImpactParticlesAtPosition(Vector3 position, RangedAttackData attackData)
    {
        _impactParticleSystem.transform.position = position;
        ParticleSystem.EmissionModule em = _impactParticleSystem.emission;
        em.SetBurst(0,new ParticleSystem.Burst(0,Mathf.Ceil(attackData.size*5)));
        ParticleSystem.MainModule mainModule = _impactParticleSystem.main;
        mainModule.startSpeedMultiplier = attackData.size * 10f;
        _impactParticleSystem.Play();
    }

파티클을 코드로 이동시키고 크기를 키우거나 하는 동작도 수행할 수 있다.
알고만 있자.

스탯 관리

스탯이 변화하는 아이템을 만들고, 관리하는 코드를 작성해보는 강의였는데, 어렵다.
스탯의 최대값을 제한하기도 하고, 체력의 변화량을 판별해 heal 이나 damage에 call하기도 하는 등 다양하다.
그 중 눈에 띄는 몇개를 적는다.

패턴 매칭

 switch (CurrentStates.attackSO) 
 //패턴 매칭, 패턴을 제공해주면 케이스에서 패턴을 쓸 수 있음
        {
            case RangedAttackData _:
                ApplyRangedStats(operation, newModifier); 
                //여기서는 스탯변화가 플레이어만 일어나기때문에 원거리밖에 없음
                break;
        }

Func

여기서도 펑션을 사용하는데 짚고 넘어가자면 매개변수 두개를 받으면 하나를 반환한다.

private void UpdateStats(Func<float, float, float> operation, CharacterStats newModifier) 

이렇게 코드로 사용되었는데, 여기만 보면 이해하기 어렵다.

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

enum값으로 정렬한 스탯들을 모두 돌아가며 타입에 따라 적용해준다.
여기서 펑션을 람다식으로 무명메서드를 작성 해준 것으로 보인다.

게임 강화 로직

게임이 계속 똑같은 난이도로 흘러가면 단조롭고 심심해진다.
그래서 게임의 진행에 따라 난이도를 올리기도 하고, 시나리오를 작성하기도 한다.
강의에서는 웨이브에 따라 몬스터의 개체수를 늘리면서 몬스터의 스텟도 증가하게 만들었다.

   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.isInKnockBack = true;
            defaultStats.attackSO.knockbackPower += 1;
            defaultStats.attackSO.knockbackTime = 0.1f;
            break;
         
         case 4:
            defaultStats.attackSO.delay -= 0.05f;
            break;
         
         case 5:
            RangedAttackData rangedAttackData = rangeStats.attackSO as RangedAttackData;
            rangedAttackData.numberofPorjectilesPerShot += 1;
            break;
      }
   }

간단하게 몬스터들의 스탯을 선택된 값에 따라 강화시키는데 20라운드마다 발생한다.

코루틴

IEnumerator로 시작, yield return을 통해 다양한 동작을 할 수 있는 데
시간을 정하고 그 뒤에 다음 동작이 돌아가게하는 invoke도 가능
만약 코루틴안에
while(true)하고 yield return null; 하면 업데이트 돌리는거랑 똑같아서 deltatime도 사용 가능함.

코루틴은 실행에 대한 순서를 반환했다가 다시 돌아와서 동작하기에 너무 남발하면 성능에 안 좋음.

코루틴 시작

StartCoroutine("StartNextWave");     
  1. 루틴을 제공해서 코루틴을 반환받는 방법
  2. 메서드 네임을 제공하고 코루틴을 반환하는 방법
    둘 중 뭘로 했느냐에 따라 멈추는 방법이 다름.
    메서드네임으로 실행하면 이름으로 멈추거나 코루틴에서 멈추거나

코딩 팁

[Header("목록")] 이거 쓰면 인스펙터에 표시됨/ 가독성 UP
  • 리지드바디는 움직이는 애한테 단다고 생각하면 됨.
GetComponentInChildren<컴포넌트 이름>(); //나를 포함해 자식까지 검사
transform.right = direction

-> 게임오브젝트의 오른쪽 (빨간색 + 방향)을 디렉션으로 향하도록 설정하는 것
오른쪽 방향을 강제로 설정하는 속성으로 2차원 평면의 경우 x,y축의 회전은 할 수 없기때문에 z축 회전을 담당한다.
사용하기에 편해보이니 기억해두자.

  • 이벤트 연결해줄때는 실행하는게 아니라서 메서드 이름만 넣어주면 됨. 괄호 필요없음

  • 게임 만들 때 너무 크게 만들다 엔딩까지 못 만드는 걸 더 좋지 않게 보기 때문에 엔딩까지 다 구성하고 살을 붙이는게 좋다.

  • 애니메이션 창 켜기 : Ctrl + 6

  • 애니메이션 만들 때는 항상 주체가 누군지 잡아줘야됨.

문제 발생 - 따라했는데 투사체가 안날아감

문제 내용 : 마우스 왼쪽 눌렀을 때만 발사하게 해놨는데 계속 날아감

해결 : 기존에는 input system action 타입 - Button이 버튼 이였는데, 이 때 .ispressed가 눌려있는 걸로 판정 될 수 있다.
그래서 action type을 Value로 바꾸고 컨트롤 타입을 any로 두면 눌렀을 때만 나간다.
why ? value는 실시간으로 값을 판정하기 때문에.

내일 할 일

  1. 알고리즘 딱 한개
  2. 개인 과제 만들기
post-custom-banner

0개의 댓글