UI가 아닌 오브젝트를 클릭해서 이동하는 것을 구현 하고 싶었다.

 

현재 드래그 이동이 아닌 클릭 후 카드를 움직이는 방식으로 구현하였다.

 

public class TestInputController : MonoBehaviour
{
    [SerializeField] private LayerMask cardLayerMask;
    private GameObject cardObject = null;
    private bool isCardSelect = false;

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            //화면 좌표계에 있기에 월드 좌표로 변환
            Vector2 worldPos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
            RaycastHit2D hit = Physics2D.Raycast(worldPos, Vector2.zero, 0f, cardLayerMask);

            if (!isCardSelect && hit.collider != null)
            {
                Debug.Log(hit.transform.name);
                cardObject = hit.collider.gameObject;
                isCardSelect = true;
            }
            else if (isCardSelect)
            {
                cardObject = null;
                isCardSelect = false;
            }
        }

        if (cardObject != null)
        {
            Vector3 pos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
            pos.z = 0;
            cardObject.transform.position = pos;
        }
    }
}

현재 Update() 문으로 구현을 해서 최적화에 좋지 않을것이다. 

1.마우스 클릭 Class를 생성후, event를 등록시켜서 필요할때만 호출 시키는 형식으로 리팩토링이 필요하다. 

2.InputSystem을 활용하여 event로 연동 시킨다.

 

이 두가지 방법 중 하나를 택해서 진행하게 될것같다. 

 

Card Class 안에 IPointerHandler를 상속시켜 쉽게 진행 할 수도 있었겠지만, 

Card에는 카드가 게임에서 사용되는 효과,연출이 담겨 있어야 되고 

Card를 조작하는 것은 InputController에서 담당을 해야 SRP(단일 책임 원칙)을 지킬 수 있을것 같아

이렇게 구현 하였다. 

 

 if (Input.GetMouseButtonDown(0))
        {
            //화면 좌표계에 있기에 월드 좌표로 변환
            Vector2 worldPos = Camera.main.ScreenToWorldPoint(Mouse.current.position.ReadValue());
            RaycastHit2D hit = Physics2D.Raycast(worldPos, Vector2.zero, 0f, cardLayerMask);

이부분은 InputManager 와 InputSystem 둘다 사용 중이기에 하나를 택해서 바꿔서 진행해야 된다. 

 

InputSystem의 현재 마우스 좌표를 반환하는 메서드, 반환값은 Vector2
Layer를 Card로 선언하여 RayCastHit를 이용하여 체크후 이동시킨다.

 

https://school.programmers.co.kr/learn/courses/30/lessons/340212?language=cpp

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr


문제 내용

 

퍼즐의 난이도를 정수로 담은 배열 (diffs[]),

퍼즐 소요 시간이 정수로 있는 배열 (times[])

가 주어질때 , 모든 퍼즐을 풀었을때의 제한시간(limit)안에 풀수있는 숙련도(level)을 반환하기

 

*입력값

diffs 배열 : 퍼즐의 난이도

times 배열 : 퍼즐의 소요시간

limit : 모든 퍼즐을 풀었을때의 제한시간

*출력값

result : 모든 퍼즐을 풀었을때의 최소 숙련도(Level)

 

문제 해결 (코드포함)

더보기

문제에서 계산식을 제공해준다.

(현재 퍼즐 소요시간 + 이전 퍼즐 소요시간) * (현재퍼즐 레벨 - 숙련도) + 현재 퍼즐 소요시간 = 해당 숙련도의 퍼즐 소요시간

해당 계산식에서 limit 범위 안에 최소 <숙련도>를 구하는 문제다 .

//(현재 퍼즐 소요시간 + 이전 퍼즐 소요시간) * (현재퍼즐 레벨 - 미지수 숙련도) + 현재 퍼즐 소요시간 = 해당 숙련도의 퍼즐 소요시간 

long long PuzzleSolve(int level, vector<int> diffs, vector<int> times)
{
    long long solveTime = 0;

    for (long long i = 0; i < diffs.size(); i++)
    {
        if (diffs[i] > level)
        {
            solveTime += (times[i] + times[i - 1]) * (diffs[i] - level) + times[i];
        }
        else
        {
            solveTime += times[i];
        }
    }

    return solveTime;
}

우선 해당 계산 수식을 함수로 구현한다.

(long long을 쓴 이유는 limit의 범위 10^15 때문)

 

첫번째 시도

#include <string>
#include <vector>
#include <iostream>
#include <stack>
using namespace std;

//제한 시간 내에 퍼즐을 모두 해결하기 위한 숙련도의 최솟값을 정수로 return 하도록 solution 함수를 완성해 주세요.

//(현재 퍼즐 소요시간 + 이전 퍼즐 소요시간) * (현재퍼즐 레벨 - 미지수 숙련도) + 현재 퍼즐 소요시간 = 해당 숙련도의 퍼즐 소요시간 

long long PuzzleSolve(int level, vector<int> diffs, vector<int> times, long long limit)
{
    long long solveTime = 0;

    for (int i = 0; i < diffs.size(); i++)
    {
        if (diffs[i] > level)
        {
            solveTime += (times[i] + times[i - 1]) * (diffs[i] - level) + times[i];
        }
        else
        {
            solveTime += times[i];
        }
    }

    return solveTime;
}

int solution(vector<int> diffs, vector<int> times, long long limit)
{
    int level = 0;
    long long answer;

    do 
    {
        level++;
        answer = PuzzleSolve(level, diffs, times, limit);
    } while (limit < answer);


    return level;
}

 

그리디 문제인줄 알고 , 최솟값 Level(숙련도)를 구하는것이기에 1부터 ++ 하면서 값을 탐색했다.

 

결론은 시간초과

 

그렇다, 이 문제가 그리디로 나왔으면 Lv.2로 나오지 않을것이다.

 

그럼 탐색하는 연산을 줄여야된다.

 

두번째 시도

탐색 알고리즘 : 이진(이분) 탐색을 사용하기 

이진 탐색은 start 와 end 를 선언하고 mid 값을 이용하여 원하는 값을 탐색하는 방법이다.

이진탐색시 로직 구상도

이 그림에서는 탐색하는값 37이라는 구체적인 값이 있지만 , 

해당 문제에서는 최소 숙련도를 구하는 문제이기에 mid가 Level값으로 사용함과 동시에 매개변수가 될것이다.

 

int solution(vector<int> diffs, vector<int> times, long long limit)
{
    long long start = 1;
    long long end = limit;
    long long level = (end + start) / 2;
    long long answer; // 걸리는 시간

    do 
    {
        answer = PuzzleSolve(level, diffs, times);

        if (answer > limit )
        {
            start = level + 1;           
        }
        else
        {
            end = level - 1;
        }

        level = (end + start) / 2;
    } while (start <= end);


    return level + 1;
}

 

start = 1 , end = limit ,level = (end + start) / 2 로 초기화를 한다.

answer > limit : 해당 레벨의 소요시간이 limit 보다 큰경우 start의 값을 올린다.

answer <= limit : 해당 레벨의 소요시간이 limit 보다 작은경우 end의 값을 올린다.

이런식으로 start와 end값이 교차될때까지 진행한다.

 

나온 결과값에 +1을 해준다.

(level이 mid 값인데, 원래 이진 탐색시 mid에 low값을 더해주는것을 마지막에 해준것일뿐)

 

해당 방법을 사용하니 시간복잡도를 해결 하였다.

 

이진 탐색의 시간 복잡도는 O(logN) 이다. 

 

 

 

'코딩테스트 > 프로그래머스' 카테고리의 다른 글

LV3.디스크 컨트롤러  (0) 2024.04.08
LV2.가장 큰 수  (0) 2023.11.03
LV1.둘만의 암호  (0) 2023.10.26
LV1.체육복  (0) 2023.10.26
LV2.택배상자  (0) 2023.10.26

https://github.com/lostwaltz/PossibleDefense

 

GitHub - lostwaltz/PossibleDefense

Contribute to lostwaltz/PossibleDefense development by creating an account on GitHub.

github.com

 

해당 프로젝트에서 발생한 트러블 슈팅 중의 하나인

<타워 이동시 기존에 있던 타일 데이터가 초기화 되지 않는 이슈> 가 있었는데 , 발표 자료에만 작성이 되어있어

블로그에 기록 하려고 한다. 

 

발표에서 사용했던 자료

문제원인

1.타워가 원하는 타일로 이동 후 기존 타일의 데이터를 따로 가지고 있지 않아


타일 데이터가 타워가 없는데도 있다고 판정을 함
2.해당 문제로 인하여 타워 소환 및 타워 판매시에 데이터가 꼬여버림
 

문제원인

1.BaseSlimeTower 에 프로퍼티로 설치 가능한 타워의 Index를 저장하고
필요할때마다 불러서 값을 수정해주기로 변경

다음 프로젝트시 적용

1. Tile 코드를 작성하면서 Inventory와 유사한 방식이였던것을 알아서


다음 프로젝트 진행 시 Tile처럼 데이터를 보관할 일이 생기면 event를
활용해야 된다고 생각함

 


타워의 데이터 대신에 Index(int) 자료값을 사용하여 , 메모리를 최적화 하였습니다. 

최종프로젝트가 종료되고 포트폴리오 정리를 하고 있는 도중에 내가 작업한 부분의 트러블 슈팅이 TIL에 기록되있지 않아 

지금이라도 기록을 하려고 한다. 

 

 

구현을 목적으로 작업하는 도중 매니저 클래스들이 많아지며 점점 호출 시점이 꼬이기 시작

해당 문제를 해결하기 위해 GameSceneTrigger 라는 클래스를 만들어 매니저들의 호출시점을 정리해주었습니다.

최종 프로젝트를 진행하면서 느낀 것이 하나 있다.

DB를 관리하면서 너무 불편했던 경험이 있다.

GitHub를 이용해서 데이터 관리를 하니 데이터가 변경 되었을때 해당 작업자가 상위 브런치에 Merge를 해야만

변경된 데이터를 확인할수가 있어서 너무 불편했었다. 

 

해당 프로젝트에서는 메인 기획자라고 할 사람이 없었지만 , 내가 회사에 취직했을때는 기획자가 데이터 값을 변동만하면 바로 적용 하게 해야할 필요를 느꼇다.

 

구글 스프레드 시트를 이용하여 온라인으로 외부 데이터를 읽어오는 방식이다.

현재 방법은 구글 스프레드 시트 API를 이용하지 않는 방법으로 간단한 프로젝트를 구성할때 좋은 방식이다. 

 

 

 

해당 구글 스프레드 시트에 데이터를 기입하자.

 

구글 스프레드시트에 데이터를 기입하고 [공유] 버튼을 눌러 링크를 활성화 하자.

 

공유에 있는 링크를 가져오자

 

이렇게 가져온 링크를 밑에 스크립트 코드에 붙여 넣는다. 

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

//복사해온 URL : URL주소

//1.복사해 온 주소에서 끝에 있는 "edit?usp=sharing" 삭제한다.
//2.삭제 한 주소에 다음을 추가한다 "export?format=tsv&range=A2:E";
//2-1. 설명 : 모드와 포맷 그리고 시트의 범위를 뜻함, =A2:E 를 엑셀 연산식에 적용시 범위를 알수 있음 

//사용할 URL :  URL주소 + export?format=tsv&range=A2:E


public class GoogleSheetDBLoader : MonoBehaviour 
{
    private string sheetData; // URL에서 불러온 데이터를 저장하는 변수
    private readonly string googleSheetURL = "사용할 URL";

    public string SheetData { get => sheetData; set => sheetData = value; }

    IEnumerator Start()
    {
        //UnityWebRequest 인스턴스 리소스 해제를 위한 using
        using(UnityWebRequest www = UnityWebRequest.Get(googleSheetURL))
        {
            yield return www.SendWebRequest();

            if(www.isDone)
            {
                sheetData = www.downloadHandler.text;
            }
        }

        DisplayText();
    }

    public void DisplayText()
    {
        //Split 함수로 데이터 처리하기
        string[] rows = sheetData.Split('\n'); //행 데이터 (카드의 데이터 모음)

        string str = "";
        for (int j = 0; j < rows.Length; j++)
        {
            string[] columns = rows[j].Split('\t'); // 열 데이터 (카드의 데이터 중 하나)

            //첫번째 행의 카드 데이터를 출력한다.
            for (int i = 0; i < columns.Length; i++)
            {
                str += columns[i] + " ";
            }

            str += "\n";
        }

        Debug.Log(str);
    }
}

 

이렇게하면 해당 스크립트에 범위에 표시된 데이터를 다 문자열로 파싱해온다.

 

범위는 추가되는 URL의 ( A2:E ) 이 부분을 수정해서 사용하면된다.

물론 이 코드는 확장성에서 많이 떨어지는 부분이 있기에 나중에 프로젝트가 커지면 API를 사용하는게 더 효율적일것이다. 

URL에 추가할때 해당 빨간상자 친 부분을 수정해주면된다.

 

 

불러온 구글 스프레드 시트 데이터가 출력된다.

 

해당 기능을 이용하여 나중에 게임에 DB를 구성하여 사용 할수 있을것이다. 

프로젝트가 마무리 시점이 되어 , 최적화 작업에 들어갔다.

 

* Dynamic Batching(동적 배칭)

동적배칭Mesh파티클 시스템과 같은 동적으로 생성된 지오메트리에 대해 작은 메쉬(vertex 900개 이하)같은 Material를 사용하는 조건하에 하나의 드로우콜로 처리 하는 것입니다. 해당 프로젝트에서 해당 기술을 사용하여 Batch수를 약 40% 감소 시켰습니다.

player setting - build 에서 동적배칭을 설정해줘야한다.

 

* Static Batching(정적 배칭)

정적배칭같은 Material를 사용하는 움직이지 않은 오브젝트를 하나의 Mesh로 결합하여 최적화 하는 기법입니다. 메모리 사용량이 증가 할 수 있지만 , CPU에서 오브젝트를 배치한 후(하나의 Mesh로 결합) GPU에서 한번만 처리하기에 드로우 콜이 감소하여 최적화를 할 수 있습니다.

 

정적 배칭은 생각밖으로 최적화 효과를 못봤지만 , 동적 배칭에 대해서는 많은 최적화가 진행 되었다 . 

using DG.Tweening

Sequence seq = DOTween.Sequence();
seq.SetUpdate(true);
seq.SetUpdate(false);

 

Sequence의 SetUpdate() 메서드를 사용하면 TimeScale 이 0이여도 DoTween 시퀀스에 저장된 애니메이션들은 동작하게 된다.

 

예제)

  private void MakeSquence()
  {
      Time.timeScale = 0;
      Sequence seq = DOTween.Sequence();
      seq.SetUpdate(true);
      seq.Append(gachaImage.transform.DOMoveY(500, 0));
      seq.Append(gachaText.DOFade(0, 0));
      seq.Append(gachaText.DOFade(1, 1));
      seq.Append(gachaText.DOFade(0, 0.5f));
      seq.Append(gachaImage.DOColor(Color.white, 1));
      seq.Append(gachaImage.transform.DOMoveY(-500, 1));
      seq.Append(gachaImage.DOColor(Color.black, 1));
      seq.OnComplete(() =>
      {
          Time.timeScale = 1;
          this.gameObject.SetActive(false);
      });
  }

모든 애니메이션이 동작한 뒤 Time.timeScale을 원복하는 메서드이다.

 

+p.s) DoTween 에셋 기능이 아닌 , Animator의 UpdateMode를 사용하는것도 방법이다.

 

animator.updateMode = AnimatorUpdateMode.UnscaledTime;

 

  • Normal: Time.timeScale에 따라 애니메이션이 재생됩니다.
  • UnscaledTime: Time.timeScale 값에 관계없이 애니메이션이 계속 재생됩니다.
  • AnimatePhysics: 물리 시뮬레이션과 함께 애니메이션이 업데이트됩니다.

3가지를 선언하여 사용하면된다. 

 

1. 도전과제 저장/불러오기 구현 : 도전과제를 클리어했거나 업적 진척도를 Json으로 역직렬화를 하여 저장과불러오기를 구현하였음

 

2.플레이어 사망시 PopUp UI 비활성화 : 플레이어 사망시 동그랗게 마스크처리가 되는데 , 해당 영역에 PopUP창이 계속 유지 되는 상태였음 -> UIManager에 플레이어가 Die시 모든 PopUP UI를 비활성화 처리하는 메서드를 만들어 적용 시킴

 

3.수동조작 상태일때 ,플레이어 사망시 슬라이딩 하는 버그 : 플레이어 사망시에 cuyInputMove라는 vector2 변수 데이터가 남아있어 플레이어 사망 -> 게임오버 로직 처리시에 해당 변수를 0,0으로 초기화 함

 

4.Enemy를 대상으로 플레이어가 와라가리하는 경우 Enemy의 공격 노쿨 버그 : 플레이어가 FSM 공격 상태일때 계속 쿨타임이 초기화 되는 버그가 발생하여 , FSM 생성자에서 한번만 바로 공격할수 있게 구성한 뒤 그 이후에는 해당 공격의 쿨타임을 저장하고 있어 공격상태일때만 공격쿨타임이 줄어들게 설계함, 이렇게되면 와리가리를 해도 Enemy는 벙찌는 현상을 줄일수 있음 

 

5.아이템 획득시, 강화 스택량이 저장할때 불러오는 데이터가 다른 버그 : 아이템 획득시 스택이 1/1 인데 , 게임 종료후 불러오기를 하면 1/3 이 되어있음 , 불러오기시 해당 변수의 데이터를 매직넘버로 넣고있었기에 발생한 버그 , 변수화 하여 데이터를 넣게 리팩토링 함 

+ Recent posts