본문 바로가기
다이어리/내일배움 개발일지

게임개발캠프 - 팀과제(C) 5일차

by E.Clone 2024. 1. 30.

과정명 : 내일배움캠프 Unity 게임개발 3기

전체진행도 : 26일차

부분진행도 : Chapter3.2 - 5일차

작성일자 : 2024.01.30(화)

개발일지 목록 : 클릭


1. 진행중인 과정에 대해

팀 프로젝트 '탕후루를 부탁해'를 마무리 지었지만, 해상도 관련 대응이 되지 않는다는 내용이 있어 관련한 버그를 고치고, 개인 학습 시간을 가졌다.

저녁에는 event와 Action에 대한 특강이 있어 강의와 질의시간을 가졌다.

2. 오늘 학습에 대해

팀 프로젝트 중 이슈

변동 해상도에 대응되지 않음

  • 고해상도나 저해상도의 기기에서 실행할 경우
  • 게임 중 해상도의 변경이 있을 경우

현재 개발을 계속 760x1280 해상도에 맞춰 해왔기 때문에, 더 고해상도가 된다면 UI배치가 전체적으로 화면의 가운데에 몰리고, 저해상도의 경우는 UI가 화면 밖으로 나가버리는 이슈가 있었다.

이러한 경우, 아래와 같이 Canvas 오브젝트에서 UI Scale Moce를 기본값인 Constant Pixel Size 에서 Scale With Screen Size 로 수정해주면 해결할 수 있었다.

설정을 적용할 경우, 어느 해상도에서도 Canvas가 스케일링되어 나오게 된다.

목표 탕후루

그러나 현재 스크립팅 된 내용으로는, 목표 탕후루를 Canvas의 position 기준으로 생성 위치를 선정했기 때문에, 해상도가 달라지면 목표 탕후루의 이미지도 엉뚱한 곳에 위치하는 문제가 있었다.

목표 탕후루의 생성 로직 변경

현재 목표 탕후루의 오브젝트는 Canvas의 하위에 생성되며, position을 설정하기 때문에, 해상도의 변경이 있을 경우, 목표 탕후루의 생성 지점도 유동적으로 움직일 수 없다.

그러한 문제를 해결하기 위해, 목표 탕후루 오브젝트의 생성을 아래의 패널의 하위에 생성되도록 하여 부모의 위치를 기준으로 y값만 조절하여 과일을 배치할 수 있도록 하였다.

부모 패널

// 'TargetTanghulu' GameObject 생성 및 'Image'의 자식으로 설정
GameObject targetTanghuluObject = new GameObject("TargetTanghulu");
Transform imageTransform = GameObject.Find("TanghuluUI").transform.Find("Image");
targetTanghuluObject.transform.SetParent(imageTransform, false);

결과적으로 목표 탕후루 이미지의 생성되는 위치를 정상화 할 수 있었고, 해상도의 문제도 해결되어 아래와 같이 극단적으로 정사각형의 해상도를 가질 경우에도 게임 플레이를 할 수 있게 되었다.

모든 이슈를 해결한 뒤의 스크린샷

Code Solve

코딩테스트 연습 > 연습문제 > 달리기 경주

using System;
using System.Linq;
using System.Collections.Generic;

public class Solution {
    public string[] solution(string[] players, string[] callings) {
        string[] answer = new string[] {};

        // 솔루션 1
        // 1. 선수이름으로 등수를, 등수로부터 선수이름을 아는 사전형 두개를 준비
        // 2. 선수 이름이 불리면
        // 2-1. 해당 선수의 등수 확인 (ex. 5등)
        // 2-2. 5등과 4등의 value값을 서로 바꾸며 이름 확인
        // 2-3. 해당 이름을 가진 두 선수의 등수를 바꾼다
        // 3. 모든 호명이 끝나면, 1등부터 answer에 입력

        // 솔루션 2
        // 다른 솔루션으로 생각해 볼 수 있는 건, ('5등이름','4등이름') 과 같이
        // ('선수','앞선선수')의 key,value값을 가지는 사전형을 사용해 볼 수도 있을 듯.
        // 이렇게 하면, 공간복잡도를 줄일 수 있다.
        // => 파기. 실제 해보니 ('앞선수','선수','뒷선수')의 리스트형이 필요한데, 공간복잡도도 낮지 않고 복잡함.

        // 1. 선수이름으로 등수를, 등수로부터 선수이름을 아는 사전형 두개를 준비
        Dictionary<string,int> playerToNum = new Dictionary<string,int>();
        Dictionary<int,string> numToPlayer = new Dictionary<int,string>();
        for(int i=0;i<players.Length;i++){
            playerToNum.Add(players[i],i);
            numToPlayer.Add(i,players[i]);
        }

        // 2. 선수 이름 호명
        foreach(string call in callings){

            // 2-1. 해당 선수의 등수 확인
            int num = playerToNum[call];

            // 2-2. 해당 등수 선수와 앞등수 선수의 value값을 서로 바꾸며 이름 확인
            string prePlayer = numToPlayer[num-1];
            numToPlayer[num] = prePlayer;
            numToPlayer[num-1] = call;

            // 2-3. 해당 이름을 가진 두 선수의 등수를 바꾼다
            playerToNum[prePlayer] = num;
            playerToNum[call] = num-1;
        }

        // 3. 모든 호명이 끝나면, 1등부터 answer에 입력
        // 검색 : Linq를 이용한 한줄 표현
        answer = numToPlayer.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray();

        return answer;
    }
}
  • 솔루션 2번을 시도하려다가, '뒷 선수'의 정보도 리스트에 필요하다는 것을 알게 되어 포기
  • Linq를 이용하여 한줄 코드
    • answer = numToPlayer.OrderBy(kvp => kvp.Key).Select(kvp => kvp.Value).ToArray();
    • 람다 표현식(kvp => kvp.Key) 사용
    • kvp는 KeyValuePair<int, string> 타입의 각 요소를 나타내며, NumToPlayer 딕셔너리의 각 항목을 순회하며 사용
    • =>는 람다 연산자라고 하며, 이를 기준으로 왼쪽은 입력 파라미터, 오른쪽은 함수 몸체를 나타냄
    • OrderBy(kvp => kvp.Key)를 호출하면, NumToPlayer의 각 KeyValuePair<int, string> 요소(kvp)를 kvp.Key (등수)를 기준으로 오름차순 정렬

event와 Action 특강

delegate, event, Action에 대해

delegate : 함수에 대한 참조 타입. 함수를 변수처럼 저장하거나 매개변수로 전달 할 수 있음.

delegate 반환형 델리게이트이름(매개변수);

event : delegate의 한 종류. 옵저버 패턴으로 활용 할 수 있다.

event 델리게이트이름 변수이름;
// 델리게이트 변수 앞에 event 키워드를 붙인다

Action : C#에서 제공하는 내장 delegate. 아래와 같이 두 줄을 한 줄로 줄여 쓸 수 있게 해 준다.

public delegate void MoveDelegateFunc(Vector2 moveVector);
public delegate void LookDelegateFunc(Vector2 lookVector);
public delegate void ShootDelegateFunc(bool fire);

public event MoveDelegateFunc OnMoveEvent;
public event LookDelegateFunc OnLookEvent;
public event ShootDelegateFunc OnFireEvent;
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public event Action<bool> OnFireEvent;

단, Action은 반환형이 void인 델리게이트에만 사용이 가능하다.

구독 시스템

  1. 이렇게 event로 만든 변수에 여러 함수들을 추가 해 준 뒤
  2. 옵저버 역할을 하는 다른 클래스에서 특정 이벤트를 관측하여 해당 event 변수에 추가했던 메서드들을 모두 실행하도록 하는 패턴.
// Observer.cs
void OnMove(){
    Controller.CallMoveEvent(inputVector2);
}
// Controller.cs
public event Action<Vector2> OnMoveEvent;
public event Action<Vector2> OnLookEvent;
public event Action<bool> OnFireEvent;

public void CallMoveEvent(Vector2 direction)
{ OnMoveEvent?.Invoke(direction); }
// Player.cs
// 플레이어를 움직이는 메서드를, OnMoveEvent에 구독
void Awake(){ Controller.OnMoveEvent += Move(); }
void Move(Vector2 moveDirection){
    // 플레이어를 이동하는 구문들
}

플레이어 메서드에서 Move 메서드를 Controller의 OnMoveEvent에 구독해두고, 옵저버에서 키보드 입력을 관측하여 OnMove 메서드를 통해 CallMoveEvent 메서드를 실행하면 OnMoveEvent에 구독했던 모든 메서드들을 실행한다.

특강 중 질의응답 내용

델리게이트도 +=이 되나요?

  • Yes

event가 존재하는 스크립트를 싱글톤으로 사용해도 되나요?

  • Yes, 오히려 그렇게 하게 될 듯 함.

그래도 키가 눌렸는지 확인하려면 결국 Update는 써야겠죠?

  • Yes, 예) Player Input

만약 등록된 모든 스크립트에 OnMove를 둔다면 다 따로동작하게도 할수 있나요?

  • Yes

event들을 한군데로 모아서 이벤트 매니저를 만들어서 사용하는게 좀 더 효율적일까요?

  • 케바케겠지만 일단 Yes

아까의 GameOver같은 경우는 자주 일어나는 상황이 아닌데, 옵저버로 매순간 관측하는 게 좋은가요?

  • Yes, Update()형태가 아니게 할 수도 있고, 한다고 해도 오버헤드가 나지는 않을 것

델리게이트를 이벤트 처럼 쓸수있는데 반대는 못 하는거네요?

  • Yes, 델리게이트가 이벤트를 포함하는 관계

혹시 이벤트를 너무 많이 사용하면 안 좋은 점도 있나요?

  • 비상식적으로 과한게 아니라면 No

이벤트나 델리게이트를 하는일이 별로 없어도 습관적으로 모든곳에 쓰는게 낫다고 보면 될까요?

  • 개발적으로 권장되는 사항이며, 특히 공부 단계에서는 Yes.
  • 다만 단순한 프로그램에서는 효율이 좋지 않을 수 있음.

이벤트를 호출할때 Invoke로 호출하는 이유가 있을까요?

  • ? (널러블)를 사용하기 위해.
    action?.Invoke() // 이와 같이 action이 null 일 경우에도 대응되도록 하기 위해 사용.
    위 구문은 if(action != null) action(); 과 같음

3. 과제에 대해

  • 내용 정리 후 발표 준비
반응형