[내일배움캠프 본 캠프]코드카타, C++ 라이브 강의, 라이라 프로젝트(1)(2025/06/11)

김세훈·2025년 6월 11일
0

1) 코드카타

문제(이상한 문자 만들기)
https://school.programmers.co.kr/learn/courses/30/lessons/12930

제출한 답안

#include <string>
#include <vector>
#include <sstream>
#include <cctype>
#include <iostream>

using namespace std;

string solution(string s) {
    string answer = "";
    int count = 0;
    
    for (int i = 0; i < s.size(); i++)
    {
        if (s[i] == ' ')
        {
            count = 0;
        }
        else
        {
            if (count % 2 == 0)
            {
                s[i] = toupper(s[i]);
                count++;
            }
            else
            {
                s[i] = tolower(s[i]);
                count++;
            }
        }
    }
    answer = s;
    return answer;
}

sstream에 있는 stringstream을 사용해서 코드를 만들고 실행하니 테스트 케이스는 전부 맞았는데 제출에서 실패가 나왔다. 아마 코드가 시간이 너무 오래걸려서 실패처리가 된 것 같다. 그래서 검색해보면서 위 코드로 다시 만들었다.
count를 마치 단어 하나의 인덱스처럼 사용하는 것이 핵심 같다. 공백 기준으로 단어를 나눈다고 문제에서 말하지만 나눌 필요없이 count로 단어의 인덱스처럼 사용하면 단어마다의 짝수번째와 홀수번째 문자를 알 수 있다.(stringstream이 어떤 기준으로 문자열을 나누는 함수 - 검색하면 나온다.)
cctype에 toupper와 tolower함수는 각각 대문자와 소문자로 만들어주는 함수이다.(딱 한 문자만 바꾼다.) 이 외에도 대문자, 소문자로 바꿔주는 방법이 있어서 검색하면 다 나온다.

문제(삼총사)
https://school.programmers.co.kr/learn/courses/30/lessons/131705

제출한 답안

#include <string>
#include <vector>

using namespace std;

int solution(vector<int> number) {
    int answer = 0;
    
    for (int i = 0; i < number.size(); i++)
    {
        for (int j = i + 1; j < number.size(); j++)
        {
            for (int k = j + 1; k < number.size(); k++)
            {
                if (number[i] + number[j] + number[k] == 0)
                {
                    answer++;
                }
            }
        }
    }
    return answer;
}

삼중 반복문을 사용한다는게 거부감이 들어서 찾아보니 오히려 이게 가장 편하고 빠른 듯하다;;
그래서 그냥 삼중 반복문을 사용했다. 재귀함수로 푸는 방법도 있는 것 같다.


2) C++ 라이브 강의

배열, 링크드리스트, 스택, 큐

스택(Stack)
STL에서 기본적으로 제공하는 자료구조

끝부터 넣고 앞에서부터 꺼내는 방식

역순의 성질을 이용해야 할 때 유용하다.(모바일 앱 뒤로 가기 버튼, Undo/Redo 등)

벡터를 이용해 간단하게 구현 가능

#include <iostream>
#include <vector>
#include <stdexcept>

class Stack {
private:
    std::vector<int> stack; // 벡터를 이용해 관리
public:
    Stack() = default; // 기본 생성자 자동 생성

    // 스택의 맨 위 데이터를 반환
    int top() {
        if (stack.empty()) {
            throw std::out_of_range("스택이 비어있어요!");
        }
        return stack.back();
    }

    // 스택에 데이터를 추가
    void push(int value) {
        stack.push_back(value);
    }

    // 스택의 맨 위 데이터를 제거
    void pop() {
        if (stack.empty()) {
            throw std::underflow_error("스택이 비어서 꺼낼게 없어요!");
        }
        stack.pop_back();
    }
};

int main() {
    Stack s;

    try {
        s.push(10);
        s.push(20);
        s.push(30);

        std::cout << "현재 톱 원소: " << s.top() << std::endl; // 30

        s.pop();
        std::cout << "현재 톱 원소: " << s.top() << std::endl; // 30 -> 20으로 변경됨

        s.pop();
        s.pop();
        s.pop(); // 예외 발생: 스택이 비어 있음
    } catch (const std::exception& e) {
        std::cerr << e.what() << std::endl;
    }
}

큐(Queue)
STL에서 기본적으로 제공하는 자료구조

처음부터 넣고 처음부터 꺼내는 방식

들어온 순서대로 작동해야할 때 유용하다.(서버 접속 대기 번호, 공용프린터 인쇄 등)

큐도 벡터나 배열로 쉽게 구현 가능하다.

#include <iostream>
#include <vector>
#include <stdexcept>

class Queue {
private:
    std::vector<int> queue;

public:
    Queue() = default;

    // 큐의 첫 번째 원소 반환
    int front() const {
        if (queue.empty()) {
            throw std::out_of_range("큐가 비어있어요!");
        }
        return queue.front();
    }

    // 큐의 마지막 원소 반환
    int back() const {
        if (queue.empty()) {
            throw std::out_of_range("큐가 비어있어요!");
        }
        return queue.back();
    }

    // 큐에 원소 추가
    void push(int value) {
        queue.push_back(value);
    }

    // 큐의 첫 번째 원소 제거
    void pop() {
        if (queue.empty()) {
            throw std::underflow_error("큐가 비어서 꺼낼게 없어요!");
        }
	        queue.erase(queue.begin());
    }
};

int main() {
    Queue q;

    q.push(10);
    q.push(20);
    q.push(30);

    std::cout << "현재 프론트 원소: " << q.front() << std::endl; // 10
    std::cout << "현재 백 원소: " << q.back() << std::endl; // 30

    q.pop();
    std::cout << "현재 프론트 원소: " << q.front() << std::endl; // 10 -> 20으로 변경

    q.pop();
    q.pop();
}

3) 라이라 프로젝트 분석

라이라 프로젝트를 다운받고 generated files를 한다.(처음에 없기 때문에 한다.)

빌드를 하는데 어떤 언리얼 에디터도 켜져있으면 안된다.

에디터가 켜진 상태로 빌드를 하면 라이브 코딩이 활성화 되어 있어서 오류가 뜰 수 있다고 하는데 진짜로 뜬다. 단순히 에디터의 라이브 코딩 기능을 끄는게 아니라 에디터 자체가 켜져있으면 안된다. 다른 프로젝트의 에디터도 켜져있으면 안된다. 라이라 프로젝트뿐만 아니라 다른 프로젝트들도 비슷할 것 같다.

모듈
언리얼엔진의 3대 요소 : 프로젝트, 모듈, 플러그인

  • 모듈 : cpp파일과 헤더파일이 포함된 최소 단위가 모듈이다. 반대로 말하면 cpp와 헤더 단일만으로는 만들 수 없다는 뜻이다.(source폴더 밑의 폴더들)
  • 플러그인 : 플러그인은 모듈들의 모음집 같은 것이다. 당연히 그 모듈들의 안에 cpp와 헤더가 들어 있다. 실험적인 용도나 외부 확장에 사용 한다.(plugins폴더 밑의 폴더들)

    uproject파일에 모듈과 플러그인이 들어 있어서 끄고 킬 수 있다.

  • 프로젝트 : 모듈과 플러그인을 전부 가지고 있는 가장 큰 범위이다. 게다가 컨텐츠 폴더도 가지고 있어서 여러 애셋들도 포함 한다.

모듈과 플러그인의 차이점
아직 실험적인 기능들은 플러그인에 넣어서 개발하고 유저들의 반응도 좋고 안정성도 더 높아졌다면 그 때 기능을 모듈로 옮긴다.

플러그인이 모듈보다 큰 범주라고 생각하면 안된다. 프로젝트가 모듈과 플러그인보다 큰 범주인 건 맞지만, 플러그인은 모듈과 동등한 관계지만 모듈들을 묶어둘 수 있다는 느낌이다.

모든 프로젝트에 기본적으로 하나의 모듈은 들어 있다.

커스텀 모듈을 만들어 보고 그 모듈을 사용하라고 엔진에 알려준다.
(이렇게 한 후에 디버깅을 해보면 중단점에서 멈추는 타이밍이 에디터가 켜지기도 전이라는 걸 알 수 있다.)

주의할 점!
IMPLEMENT_PRIMARY_MODULE은 가장 중요한 모듈로 꼭 하나만 존재해야 한다. 다른 모듈을 만들 때는 IMPLEMENT_MODULE을 각 모듈마다 사용해야 한다.(IMPLEMENT_PRIMARY_MODULE을 또 사용하면 에러가 생기는데 전혀 알아볼 수 없는 에러가 생겨서 고치기 매우 힘들다.)
또한, 다른 모듈을 만들었을 때 IMPLEMENT_MODULE를 꼭 써줘야 한다. IMPLEMENT_MODULE을 안 썼을 때도 에러가 생긴다.

로그
지금은 새 프로젝트라서 괜찮지만 나중에 프로젝트가 커지면 단순히 로그를 찍기만 해서는 어디서 문제가 생겼는지 알아보기 어려운 상황이 온다.

언리얼에서 None으로 새로운 c++클래스 만들고 구현

이렇게 하고 실행을 해보면 언리얼의 출력 로그에서 카테고리가 LogCloneLyra인 디버깅 메세지가 나온다. 나중에 이렇게 카테고리를 여러가지 만들고 이름을 다르게 해서 사용하면 디버깅할 때 유용하다.

애셋 매니저
애셋 매니저는 애셋들을 관리하는 매니저인데 여러 개일 수 없다. 그래서 싱글톤으로 구현 한다.

설명이 필요한 함수(이론적으로 알기는 어려워 예를 들어 설명)
아래 함수는 예를 들면 창이 1000개가 있을 때 1000개를 전부 로드해놓지 말고 현재 필요한 것만 로드하는 함수이다. 거기에 추가로 어떤 함수의 어떤 부분에서 굉장히 느려지는 부분을 발견했는데 시간이 얼마나 걸렸는지 표시를 해준다. 어떤 애셋을 로드하는데 시간이 오래 걸린다는 것을 로그로 확인할 수 있고 그 때는 동기 로딩을 하면 안된다는 것을 알고 뺄 수 있다.

UObject* UCloneLyraAssetManager::SynchronousLoadAsset(const FSoftObjectPath& AssetPath)
{
	if (AssetPath.IsValid())
	{
		TUniquePtr<FScopeLogTime> LogTimePtr;
		if (ShouldLogAssetLoads())
		{
			LogTimePtr = MakeUnique<FScopeLogTime>(*FString::Printf(TEXT("synchronous loaded assets [%s]"), *AssetPath.ToString()), nullptr, FScopeLogTime::ScopeLog_Seconds);
		}

		if (UAssetManager::IsValid())
		{
			return UAssetManager::GetStreamableManager().LoadSynchronous(AssetPath);
		}

		return AssetPath.TryLoad();
	}
	return nullptr;

인수에 FSoftObjectPath가 있는데 이거는 컨텐츠 폴더에 있는 애셋의 메모리를 그대로 다 들고 있을 수는 없으니 그 애셋의 경로만 가지고 있는 것이다. 그리고 로드를 할 때 이 경로에 있는 애셋을 로드하는 것이다.

profile
내일배움캠프 언리얼 과정 열심히 듣기 화이팅!

0개의 댓글