일정표
| 시간 | 할 일 | 비고 |
| 08:00~10:00 | 코드카타, Atanny, 프로그래머스 | |
| 10:00~13:00 | 언리얼 따라하기, 프로그래머스 | |
| 14:00~16:00 | 오늘의 수학, 언리얼 C++ | |
| 16:00~18:00 | 라이라 스타터 게임 파먹기 | |
| 19:00~21:00 | 팀프로젝트 회의 | |
| 21:00~22:00 | 운동 | |
| 22:00~23:30 | 개인 작업 |
*오늘의 코드카타*
문제. 삼총사
한국중학교에 다니는 학생들은 각자 정수 번호를 갖고 있습니다. 이 학교 학생 3명의 정수 번호를 더했을 때 0이 되면 3명의 학생은 삼총사라고 합니다. 예를 들어, 5명의 학생이 있고, 각각의 정수 번호가 순서대로 -2, 3, 0, 2, -5일 때, 첫 번째, 세 번째, 네 번째 학생의 정수 번호를 더하면 0이므로 세 학생은 삼총사입니다. 또한, 두 번째, 네 번째, 다섯 번째 학생의 정수 번호를 더해도 0이므로 세 학생도 삼총사입니다. 따라서 이 경우 한국중학교에서는 두 가지 방법으로 삼총사를 만들 수 있습니다
한국중학교 학생들의 번호를 나타내는 정수 배열 number가 매개변수로 주어질 때, 학생들 중 삼총사를 만들 수 있는 방법의 수를 return 하도록 solution 함수를 완성하세요.
#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 += 1;
}
}
}
}
return answer;
}
(1) for문을 세번 겹쳐서 푼 방법.
#include <vector>
#include <algorithm>
using namespace std;
int solution(vector<int> number) {
int answer = 0;
int n = number.size();
vector<int> mask(n, 0);
fill(mask.begin(), mask.begin() + 3, 1);
do {
int sum = 0;
for (int i = 0; i < n; ++i) {
if (mask[i] == 1) {
sum += number[i];
}
}
if (sum == 0) {
answer++;
}
} while (prev_permutation(mask.begin(), mask.end()));
return answer;
}
(2) C++ STL을 활용한 풀이
알고리즘 헤더의 std::prev_permutation을 사용하여 조합 개념을 사용한 풀이를 할 수 있다. 사실 n개의 원소 중 3개를 뽑는 건 nC3이렇게 표현할 수 있다는 건 이미 알고 있기 때문에, 이를 활용해보는 것이다.
prev_permutation은 배열의 원소들을 사전 순서상 바로 이전 형태(더 작은 형태)로 뒤섞어주는 함수이다.
예를 들어, [1, 1, 1, 0, 0] 배열에 prev_permutation을 실행하면 [1, 1, 0, 1, 0] 이 된다. 이렇게 prev_permutation을 계속 실행하면 숫자가 점점 작아지는 방향으로 1의 위치가 이동하게 된다. whille문과 함께 이를 계속 실행하면 더이상 작은 형태가 나오지 않을 때 까지 반복하므로, nC3과 똑같은 경우의 수를 얻을 수가 있게 된다.
먼저 n개의 원소 중 3개를 뽑기 위해 마스크 배열을 생성한다. vector<int> mask(n, 0). 3개가 뽑히면 1이 3개가 들어서게 된다.
3개를 뽑는 과정을 fill()로 해결한다. fill(mask.begin(), mask.end() + 3, 1);
1은 이 자리의 학생을 뽑겠다는 의미이고, 0은 뽑지 않는다를 의미하게 된다.
do-while : 순열을 이용해 모든 조합을 탐색하는 과정이다.
for문으로 학생 수만큼 반복문을 돌면서, 현재 mask[i]가 1인 학생 번호만 sum에 더한다. sum이 0이면 answer를 1 증가시킨다.
while 조건문에 있는 prev_permutation이 실행되면서 mask배열의 1 위치를 다음 작은 형태로 이동시킨다.
*Atanny*
1. Enhanced Input System의 구조와 차이점 분석
핵심 개념
Enhanced Input System은 Input Mapping Context를 통해 런타임에 동적으로 입력을 관리할 수 있다.
기존 시스템은 프로젝트 설정에서만 입력을 관리했기 때문에 상대적으로 단순하다. EIS는 더 복잡하지만 그만큼 유연성과 확장성이 뛰어나다.
IMC 덕분에 게임 상태에 따라 입력 설정을 다르게 할 수도 있고, 플레이어 선호도에 맞게 조작키를 재설정하거나, 게임 플레이 중에도 입력 반응성을 조정할 수 있다.
2. PlayerController와 GameInstance의 역할 이해
핵심 개념
PlayerController : 플레이어의 입력을 받아서 게임 속 캐릭터를 조종하는 관리자. 마우스, 키보드, 게임 패드 같은 입력을 감지하고 그것을 Pawn에게 명령을 전달하는 역할을 한다. 카메라 시점 제어도 PlayerController의 책임이다. 레벨이 바뀌면 PlayerController는 자동으로 소멸되고, 새 레벨에서 새로운 PlayerController가 생성된다.
GameInstance : 게임 전체 수명 동안 살아있는 데이터 보관함. 게임이 시작되면 생기고, 게임이 종료될 때까지 죽지 않는다.
3. 충돌 이벤트 처리와 충돌 타입 구분
핵심 개념
충돌 이벤트 처리의 흐름 : 충돌 감지 - 콜리전 채널 확인 - 충돌 응답 결정 - 이벤트 발생
4. 인터페이스의 다형성 이해
핵심 개념
인터페이스 : "이 객체가 어떤 기능을 제공한다"는 약속을 정의하는 것. 여러 다른 클래스들이 같은 기능을 제공하도록 강제하면서도, 각자 다르게 구현할 수 있게 해주는 도구이다. 언리얼 엔진에서는 UInterface와 IInterface 두 개의 클래스로 구성되며, U 접두사 클래스는 리플렉션용, I 접두사 클래스는 실제 구현용이다. UInterface는 직접 구현하지는 않지만 엔진이 인터페이스를 추적할 수 있게 해주고, IInterface 클래스는 실제로 메서드를 정의하고 구현하여 다른 클래스들이 상속받아서 실제 기능을 구현하는 클래스이다.
다형성 : 인터페이스를 만족하는 여러 객체들을 같은 방식으로 다룰 수 있다는 뜻. 즉, 실제 구현은 각각 다르지만 사용하는 입장에서는 통일된 방식으로 접근할 수 있다는 뜻이다.
*프로그래머스*
1. [기초] 조건에 맞게 수열 변환하기 1
정수 배열 arr가 주어집니다. arr의 각 원소에 대해 값이 50보다 크거나 같은 짝수라면 2로 나누고, 50보다 작은 홀수라면 2를 곱합니다. 그 결과인 정수 배열을 return 하는 solution 함수를 완성해 주세요
#include <vector>
using namespace std;
vector<int> solution(vector<int> arr) {
vector<int> answer;
answer.reserve(arr.size());
for (int num : arr)
{
if (num >= 50 && num % 2 == 0)
{
num /= 2;
answer.emplace_back(num);
}
else if (num < 50 && num % 2 != 0)
{
num *= 2;
answer.emplace_back(num);
}
else
{
answer.emplace_back(num);
}
}
return answer;
}
(1) 기초적인 방법으로 풀었으나, 테스트 과정에서 시간이 너무 오래 걸리는 케이스가 발생했다.
#include <vector>
using namespace std;
vector<int> solution(vector<int> arr) {
for (int& num : arr)
{
if (num >= 50 && num % 2 == 0)
{
num /= 2;
}
else if (num < 50 && num % 2 != 0)
{
num *= 2;
}
}
return arr;
}
(2) 문제의 조건에 따라 새로운 배열을 만들 필요 없이 넘겨받은 배열 자체를 바로 수정해 버리면 메모리 낭비와 재할당 시간을 아낄 수 있다.
따라서 참조자& 을 int 뒤에 붙여서, 배열 내부의 해당 위치를 직접 가리켜서 곧바로 값을 덮어쓰게 만들면 된다.
#include <vector>
#include <algorithm>
using namespace std;
vector<int> solution(vector<int> arr) {
transform(arr.begin(), arr.end(), arr.begin(),
[](int num) {
if (num >= 50 && num % 2 == 0) return num / 2;
if (num < 50 && num % 2 != 0) return num * 2;
return num;
});
return arr;
}
//극한의 극한까지 [] 조건문을 삼항 연산자로도 풀어낼 수 있다.
//
transform(arr.begin(), arr.end(), arr.begin(), [](int num) {
return (num >= 50 && num % 2 == 0) ? (num / 2) :
((num < 50 && num % 2 != 0) ? (num * 2) : num);
});
return arr;
(3) C++을 다룰 줄 아는 사람이라면 데이터를 변환하고 싶을 때 transform과 [] 람다 함수식을 사용할 수 있어야 한다.
2. [기초] 조건에 맞게 수열 변환하기 2
정수 배열 arr가 주어집니다. arr의 각 원소에 대해 값이 50보다 크거나 같은 짝수라면 2로 나누고, 50보다 작은 홀수라면 2를 곱하고 다시 1을 더합니다
이러한 작업을 x번 반복한 결과인 배열을 arr(x)라고 표현했을 때, arr(x) = arr(x + 1)인 x가 항상 존재합니다. 이러한 x 중 가장 작은 값을 return 하는 solution 함수를 완성해 주세요
단, 두 배열에 대한 "="는 두 배열의 크기가 서로 같으며, 같은 인덱스의 원소가 각각 서로 같음을 의미합니다
#include <vector>
using namespace std;
int solution(vector<int> arr) {
int x = 0;
while (true) {
bool bIs_Changed = false;
for (int& num : arr) {
if (num >= 50 && num % 2 ==0)
{
num /= 2;
bIs_Changed = true;
}
else if (num < 50 && num % 2 != 0)
{
num = num * 2 + 1;
bIs_Changed = true;
}
}
if (!bIs_Changed)
{
break;
}
x++;
}
return x;
}
(1) 배열을 통째로 비교하게 되면 매번 배열 전체를 새로 메모리에 할당하고 통째로 비교해야 하므로, 배열의 길이가 길어질수록 불리해진다. 따라서 이런 경우엔는 "배열 안의 숫자 중 단 하나라도 변한 적이 있는지?"를 확인하는 bIs_Changed를 활용하는 것이 좋다.
3. [기초] 1로 만들기
정수가 있을 때, 짝수라면 반으로 나누고, 홀수라면 1을 뺀 뒤 반으로 나누면, 마지막엔 1이 됩니다. 정수들이 담긴 리스트 num_list가 주어질 때, num_list의 모든 원소를 1로 만들기 위해서 필요한 나누기 연산의 횟수를 return하도록 solution 함수를 완성해주세요
#include <vector>
using namespace std;
int solution(vector<int> num_list) {
int answer = 0;
for (int num : num_list)
{
while (num > 1)
{
num /= 2;
answer++;
}
}
return answer;
}
(1) C++ 에서는 홀수를 2로 나누면 소수점 .5가 사라진다.(int 형일 경우). 따라서 if-else문을 굳이 사용할 필요 없이 num /= 2 만으로도 조건식을 작성할 수 있다.
4. [기초] 길이에 따른 연산
정수가 담긴 리스트 num_list가 주어질 때, 리스트의 길이가 11 이상이면 리스트에 있는 모든 원소의 합을 10 이하이면 모든 원소의 곱을 return하도록 solution 함수를 완성해주세요
#include <vector>
using namespace std;
int solution(vector<int> num_list) {
int answer = 0;
int mul = 1;
if (num_list.size() > 10)
{
for (int num : num_list)
{
answer += num;
}
}
else
{
for (int num : num_list)
{
mul *= num;
}
answer = mul;
}
return answer;
}
(1) 기초적인 풀이.
#include <bits/stdc++.h>
using namespace std;
int solution(vector<int> l) {
return l.size() >= 11 ? accumulate(l.begin(), l.end(), 0) : accumulate(l.begin(), l.end(), 1, multiplies<int>());
}
(2) C++ 코딩테스트 다운 풀이. 웬만한 헤더를 #include <bits/stdc++.h> 로 인클루드 하고, accumulate를 사용하고, multiplies를 사용할 수 있다.
multiplies<int>는 <functional> 헤더에 정의된 기능으로, "들어오는 두 값을 서로 곱해라"는 명령을 내린다. 결과적으로 배열의 첫 원소부터 끝까지 계속해서 곱해나가는 결과를 만든다.
5. [기초] 원하는 문자열 찾기
알파벳으로 이루어진 문자열 myString과 pat이 주어집니다. myString의 연속된 부분 문자열 중 pat이 존재하면 1을 그렇지 않으면 0을 return 하는 solution 함수를 완성해 주세요 단, 알파벳 대문자와 소문자는 구분하지 않습니다
#include <string>
#include <cctype>
using namespace std;
int solution(string myString, string pat) {
for (char& c : myString)
{
c = tolower(c);
}
for (char& c : pat)
{
c = tolower(c);
}
if (myString.find(pat) != string::npos)
{
return 1;
}
else
{
return 0;
}
}
(1) find를 사용한 풀이. cctype의 tolower는 char 자료형에만 사용가능하므로 for문을 사용해서 처음부터 순회하면서 소문자로 바꿔주어야 한다.
#include <string>
#include <cctype>
#include <algorithm>
using namespace std;
int solution(string myString, string pat) {
transform(myString.begin(), myString.end(), myString.begin(), ::tolower);
transform(pat.begin(), pat.end(), pat.begin(), ::tolower);
return myString.find(pat) != string::npos ? 1 : 0;
}
(2) 지금까지 배운 방법으로 개선한 코드.
그런데 이렇게 단어를 검색할 때마다 매번 tolower를 쓰든, transform을 쓰든 공간 복잡도나 시간 복잡도나 엄청 많이 소요될 것으로 예상된다. 그래서 실제 상용 코드들은 어떻게 구성되어있을지 깊게는 살펴보지 못하겠지만, 가볍게 알아보았다.
#include <string>
#include <algorithm>
#include <cctype>
using namespace std;
int solution(string myString, string pat) {
auto it = search(
myString.begin(), myString.end(),
pat.begin(), pat.end(),
[](char ch1, char ch2) {
return tolower(ch1) == tolower(ch2);
}
);
return it != myString.end() ? 1 : 0;
}
(3) std::serach와 람다 함수의 조합
문자열을 미리 바꾸는 게 아니라, 두 글자를 비교하는 바로 그 순간에만 대소문자를 무시하고 비교하는 방법이다. 결국, [] 함수를 잘 이용해서 커스텀 비교 규칙을 만드는 것이다.
#include <boost/algorithm/string.hpp>
// boost::icontains
if (boost::icontains(myString, pat)) {
return 1;
}
(4) Boost 라이브러리
C++로 개발된 실제 상용 서버나 프로그램에서는 문자열 처리에 특화된 Boost 라이브러리를 굉장히 많이 사용한다. 이미 대소문자를 무시하는 최적화된 검색 함수가 내장되어있기 때문이다.
icontains 에서 i 는 ignore case를 의미한다. 즉, 대소문자를 무시하라는 의미이다. 내부적으로 Boyer-Moore Searcher(보이어-무어 알고리즘)을 사용하고 있어서 시간복잡도도 매우 줄일 수 있다.
*참고 : 보이어-무어 알고리즘 원리 >> #include <functional>
1) 일반적인 검색은 텍스트와 패턴을 왼쪽에서 오른쪽으로 한 글자씩 비교하지만, 보이어-무어는 패턴의 맨 오른쪽 끝 글자부터 거꾸로 비교한다.
2) Bad Character Rule을 사용해서 스킵이 가능하다. 패턴에 아예 없는 글자라면(패턴 안에 아예 존재하지 않는 글자라면) 패턴 길이만큼 통째로 skip한다. 어차피 겹칠 일이 없기 때문이다. 틀리긴 했지만 그 글자가 패턴 어딘가에 존재한다면, 패턴을 오른쪽으로 밀어서 그 글자가 서로 일치하도록 위치를 맞춘다.
핵심은 이 원리로 검색에 걸리는 복잡도를 획기적으로 줄일 수 있다는 것이다.
*오늘의 총평*
언리얼 블루프린트를 활용한 맵 디자인, 지형 그리기, 아이템(액터) 만들기, 액터 회전시키고 효과음(Sound Cue), 파티클(Cascade) 등을 삽입하고 플레이어에게 상태 이상을 부여하는 로직까지 구현해보았다. 이에 더해 Fab 에셋을 활용해서 키보드 버튼을 누르면 해당 위치에 액터가 생성되게 하거나, 또는 파괴할 수 있게 만들었다. 추가로 Billboard를 사용한 배경 빛, 배경음악등의 조절을 연습했고, Instance Component를 활용해서 조건을 부여하고 액터가 규칙에 따라서 생성될 수 있도록 연습했다.