<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>park94128 님의 블로그</title>
    <link>https://park94128.tistory.com/</link>
    <description>park94128 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 20:19:40 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>park94128</managingEditor>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (7)</title>
      <link>https://park94128.tistory.com/77</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 버튼이 100개면 함수를 100개 만들 것인가?&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 이전에도 언급했지만 마음에 들지 않는 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1775062097764&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private:
	//클래스 선택 버튼1
	UFUNCTION()
	void OnClickSelectClassButton1();
	
	//클래스 선택 버튼2
	UFUNCTION()
	void OnClickSelectClassButton2();
	
	//클래스 선택 버튼3
	UFUNCTION()
	void OnClickSelectClassButton3();
	
	//클래스 선택
	void SelectClass(int32 ClassNum);
	
	//클래스 버튼1
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton1;
	
	//클래스 버튼2
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton2;
	
	//클래스 버튼3
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton3;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스 선택 화면의 위젯 코드는 마음에 들지 않는 구조로 짜여있다. 왜냐하면 버튼이 3개라고 함수를 3개 만들어서 바인딩 했기 때문이다. 이 3개의 버튼의 목적이 완전히 다르다면 모르겠는데 이들은 선택에 따라 값 1개만 달라질 뿐 나머지는 동일하다. 그런 상황에서 함수를 각각 따로 만든 것은 비효율적인 코드다. 이 구조를 따를경우, 버튼이 100개라면 함수를 100개 만들어야 한다. 설마 진짜로 그러지는 않겠지...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 생각하거나 시도했지만 정답이 아닌 경우&lt;/h4&gt;
&lt;pre id=&quot;code_1775062618144&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SelectClassWidget::NativeConstruct()
{
	for (int32 iNum = 0; iNum &amp;lt; 3; iNum++)
	{
		//에러가 발생함
		ClassButtons[iNum]-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::SelectClass);
	}
}

void UT3SelectClassWidget::SelectClass(int32 ClassNum)
{
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 매개변수 1개를 받는 함수를 for문으로 돌려서 버튼에 바인딩할까 생각했지만 버튼의 OnClicked는 매개변수가 없는 함수만 받을 수 있는 델리게이트라서 그럴 수는 없다. 위 코드는 에러가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775063021325&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SelectClassWidget::NativeConstruct()
{
	for (int32 iNum = 0; iNum &amp;lt; 3; iNum++)
	{
		//이런게 실제로 존재할까?
		ClassButtons[iNum]-&amp;gt;OnClicked.AddDynamic(this, () =&amp;gt;
		{
			SelectClass(iNum)
		});
	}
}

void UT3SelectClassWidget::SelectClass(int32 ClassNum)
{
	
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OnClicked에 람다 함수 같은걸 만들어서 할당할 수는 없을까? 아쉽게도 언리언 엔진에서 사용하는 코드는 C++라서 C#과 같은 람다식은 존재하지 않는다. 그래서 위 코드와 같은 구조는 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;혹시나 언리얼 엔진에서 구현한 람다 생성 함수(ex : TimerManager의 SetTimer함수를 사용할 경우) 중에서 위젯 버튼에 사용가능한 것이 존재할까 해서 정보를 찾아봤다. 그리고 나는 답을 찾았다. 과정은 다소 복잡하니 제작한 코드에 대한 개념을 순서대로 밟아본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. User위젯 내부에는 Slate위젯이 포함된다.&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cy3Zq0/dJMcaarf4ng/9MMpmtCbvBc0ZUj03Wm8r1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cy3Zq0/dJMcaarf4ng/9MMpmtCbvBc0ZUj03Wm8r1/img.png&quot; data-alt=&quot;버튼인 UButton 클래스 내부에 존재하는 SButton&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cy3Zq0/dJMcaarf4ng/9MMpmtCbvBc0ZUj03Wm8r1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcy3Zq0%2FdJMcaarf4ng%2F9MMpmtCbvBc0ZUj03Wm8r1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;520&quot; height=&quot;114&quot; data-origin-width=&quot;520&quot; data-origin-height=&quot;114&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;버튼인 UButton 클래스 내부에 존재하는 SButton&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 엔진에서 UI를 만들때 사용하는 버튼, 텍스트 등의 위젯은 내부에 Slate 위젯이 있다. 언리얼 엔진에서 제공되는 User 위젯의 기능은 내장된 Slate 위젯을 빌려서 구현된 경우가 많다. 당장 지금 다루는 중인 버튼도 마찬가지다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 Slate위젯이 왜 언급됐을까? 바로 &lt;b&gt;Slate위젯용 람다 생성 함수가 존재&lt;/b&gt;하기 때문이다.&lt;/p&gt;
&lt;pre id=&quot;code_1775063561641&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SlateButton-&amp;gt;SetOnClicked(FOnClicked::CreateLambda([this, TempClassValue]()
{
	OnClickSelectClassButton(TempClassValue);
	return FReply::Handled(); 
}));&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼을 예로 들자면, SButton에 있는 SetOnClicked 함수로 클릭시 실행할 기능을 부여할 수 있다. 이 함수의 매개변수로 람다 함수를 넣을 수 있는데 FOnClicked::CreateLambda함수를 사용하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점으로 이 람다 함수는 FReply::Handled()가 반환 값인 함수로 만들어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1775064616660&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;TSharedPtr&amp;lt;SButton&amp;gt; SlateButton = StaticCastSharedPtr&amp;lt;SButton&amp;gt;(MyUserButton-&amp;gt;GetCachedWidget());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이 Slate위젯은 public으로 선언된 상태가 아니라서 직접 참조할 수 없다. 대신 여기에 접근할 수 있는 함수인 GetChachedWidget()이 존재한다. 이 함수의 반환값은 C스타일 포인터라서 TSharedPtr로 사용하려면 변환 함수인 StaticCastCharedPtr()를 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 선언된 버튼 변수를 제거하고 그 부모인 변수를 선언한다.&lt;/h4&gt;
&lt;pre id=&quot;code_1775063723074&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3SelectClassWidget.h

private:
	//캐릭터 선택 버튼이 있는 패널
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UHorizontalBox&amp;gt; SelectClassBox;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ozqyh/dJMcabqbdJY/ucRshi854AOCzjHR4HdYfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ozqyh/dJMcabqbdJY/ucRshi854AOCzjHR4HdYfk/img.png&quot; data-origin-width=&quot;919&quot; data-origin-height=&quot;395&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.4945%; margin-right: 10px;&quot; data-widthpercent=&quot;49.07&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ozqyh/dJMcabqbdJY/ucRshi854AOCzjHR4HdYfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOzqyh%2FdJMcabqbdJY%2FucRshi854AOCzjHR4HdYfk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;919&quot; height=&quot;395&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qMcxg/dJMcahqnOxn/JjooxUvv3mKYKBlwZtZzW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qMcxg/dJMcahqnOxn/JjooxUvv3mKYKBlwZtZzW1/img.png&quot; data-origin-width=&quot;285&quot; data-origin-height=&quot;118&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.3427%;&quot; data-widthpercent=&quot;50.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qMcxg/dJMcahqnOxn/JjooxUvv3mKYKBlwZtZzW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqMcxg%2FdJMcahqnOxn%2FJjooxUvv3mKYKBlwZtZzW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;285&quot; height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하는 데에는 이유가 있다. 원래는 그 버튼으로 구성된 TArray를 선언하려고 했다. 하지만 생각해보니 이러면 버튼을 생성할 때마다 일일이 그 TArray에 추가해줘야 하니 불편할 것이다. 그래서 생각한 것은 부모인 가로 박스(Horizontal Box)에서 그 자식인 위젯을 가져오게 하는 것이다. 이러면 코드에 직접 추가하지 않아도, 디테일 패널에 일일이 추가하지 않아도 자식을 가져오는 함수 하나만으로 가져올 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 그래서 완성된 코드&lt;/h4&gt;
&lt;pre id=&quot;code_1775063850286&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//각 클래스 버튼은 SButton에 Lambda로 바인딩한다.
int32 TempClassValue = 0;
for (TObjectPtr&amp;lt;UWidget&amp;gt; ChildWidget : SelectClassBox-&amp;gt;GetAllChildren())
{
	TObjectPtr&amp;lt;UButton&amp;gt; ChildButton = Cast&amp;lt;UButton&amp;gt;(ChildWidget);
	if (!ChildButton)
	{
		UE_LOG(LogTemp, Warning, TEXT(&quot;%s : 버튼이 아님&quot;), *ChildWidget.GetName());
		continue;
	}
	TSharedPtr&amp;lt;SButton&amp;gt; SlateButton = StaticCastSharedPtr&amp;lt;SButton&amp;gt;(ChildButton-&amp;gt;GetCachedWidget());
	SlateButton-&amp;gt;SetOnClicked(FOnClicked::CreateLambda([this, TempClassValue]()
	{
		SelectClass(TempClassValue);
		return FReply::Handled(); 
	}));
		
	++TempClassValue;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 버튼의 부모인 SelectClassBox에서 GetAllChildren() 함수를 사용하면 모든 자식으로 구성된 위젯 배열이 반환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이 위젯 배열을 for문을 통해 순회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 만약을 위해 자식 위젯마다 버튼 위젯인지 검증하는 구간이 필요하다. GetAllChildren()는 자식이 어떤 종류인지는 가르쳐 주지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 버튼 위젯이면 여기서 Slate위젯을 추출한다. 그리고 OnClicked에서 람다를 생성하여 여기에 매개변수가 있는 함수를 집어넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 통해 아무리 버튼이 많이 추가되어도 동일한 함수 1개를 사용할 수 있게 됐다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 확인 좀 하겠습니다&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 기획상 어디서 확인을 요구하는가&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dslMgm/dJMcabcBsgy/WbWMelEBoiYteUrzx3NDmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dslMgm/dJMcabcBsgy/WbWMelEBoiYteUrzx3NDmK/img.png&quot; style=&quot;width: 33.3898%; margin-right: 10px;&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;613&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;34.18&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dslMgm/dJMcabcBsgy/WbWMelEBoiYteUrzx3NDmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdslMgm%2FdJMcabcBsgy%2FWbWMelEBoiYteUrzx3NDmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1114&quot; height=&quot;613&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edEbTS/dJMcahqogEc/Qfd1XZbhRttRNEd2zpmJ61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edEbTS/dJMcahqogEc/Qfd1XZbhRttRNEd2zpmJ61/img.png&quot; style=&quot;width: 33.0977%; margin-right: 10px;&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;574&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;33.89&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edEbTS/dJMcahqogEc/Qfd1XZbhRttRNEd2zpmJ61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedEbTS%2FdJMcahqogEc%2FQfd1XZbhRttRNEd2zpmJ61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1034&quot; height=&quot;574&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qk5h7/dJMcahjEa5b/GIFLf1qAMj5h4tDpVSuJ00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qk5h7/dJMcahjEa5b/GIFLf1qAMj5h4tDpVSuJ00/img.png&quot; data-origin-width=&quot;976&quot; data-origin-height=&quot;575&quot; data-is-animation=&quot;false&quot; style=&quot;width: 31.1869%;&quot; data-widthpercent=&quot;31.93&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qk5h7/dJMcahjEa5b/GIFLf1qAMj5h4tDpVSuJ00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fqk5h7%2FdJMcahjEa5b%2FGIFLf1qAMj5h4tDpVSuJ00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;976&quot; height=&quot;575&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 내가 담당한 범위 중 확인을 요구하는 것은 타이틀 화면에만 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 게임을 새로 시작할 때(이미 저장된 게임이 있다면)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 저장된 게임을 이어할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 종료할 때.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 지금 여기에는 확인을 거치는 과정이 없으며 무조건 그 기능을 실행한다. 이제 그 기능을 구현해야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/77</guid>
      <comments>https://park94128.tistory.com/77#entry77comment</comments>
      <pubDate>Wed, 1 Apr 2026 23:23:46 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (6)</title>
      <link>https://park94128.tistory.com/76</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 너의 이름은&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 구현한 클래스 선택 레벨에서 당장 추가할 것을 말하자면 현재 이름 입력 기능이 없다. 기획에 따르면 클래스를 선택하면 이름을 입력한 뒤에 튜토리얼을 시작해야 하지만 지금은 그런거 없이 바로 튜토리얼을 시작한다. 그래서 이름 입력 기능을 구현해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 코드 구성&lt;/h4&gt;
&lt;pre id=&quot;code_1774457098694&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class DESECRATION_API UT3InputNamePanel : public UUserWidget
{
	GENERATED_BODY()
	
	friend UT3SelectClassWidget;

protected:
	virtual void NativeOnInitialized() override;
	
private:	
	//확인 버튼
	UFUNCTION()
	void OnClickConfirmButton();
	
	//취소 버튼
	UFUNCTION()
	void OnClickCancelButton();
	
	//이름 입력 칸
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UEditableTextBox&amp;gt; InputNameBox;
	
	//확인 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; ConfirmButton;
	
	//취소 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; CancelButton;
	
	//클래스 선택 위젯
	UPROPERTY()
	TWeakObjectPtr&amp;lt;UT3SelectClassWidget&amp;gt; SelectClassWidget;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 입력 패널인 유저 위젯 클래스다. 여기서 몇 가지 설명할 것이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- friend 키워드로 특정 클래스를 지정하면 그 클래스는 이 클래스의 protected 및 private를 public인 것처럼 접근할 수 있다. (객체 지향 프로그래밍 관점에서는 은닉성을 망치는 기능이니 적당히 사용하자) 여기서는 클래스 선택 위젯이 이 패널 클래스 내 모든 것을 접근할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 새로 알게 된 언리얼 엔진 함수 : NativeOnInitialized는 객체 자체가 파괴되지 않는 이상 단 1번만 실행되는 함수라고 한다. 나는 NativeConstruct 함수가 1번만 실행되는 함수인 줄 알았는데 그게 아니었다. 이 프로젝트에서는 보여주고 숨길 때 SetVisibility를 사용하여 보여주고 숨기기 때문에 다시 실행될 일은 없지만, 만약 패널을 닫기 위해 뷰포트에서 뺀 다음 다시 뷰포트에 추가하면 NativeConstruct 함수가 실행된다고 한다. NativeConstruct 함수는 뷰 포트에 추가되는 시점에서 실행하고 NativeOnInitialized함수는 CreateWidget 시점에서 실행되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 이 패널은 이름 입력 칸인 EditableTextBox와 확인, 취소 버튼이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 클래스 선택 위젯 내에 배치되는 위젯으로 만들 생각이다. 그래서 클래스 선택 위젯을 캐싱할 변수를 선언한다. (클래스 선택 위젯 클래스에 friend 키워드를 사용한 이유) 다만 TObjectPtr로 선언하면 순환 참조 문제가 발생할 염려가 있어 약한 참조인 TWeakObjectPtr로 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 확인, 취소 버튼에 대응되는 함수를 선언한 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774458467706&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3InputNamePanel::NativeOnInitialized()
{
	Super::NativeOnInitialized();
	
	//버튼 바인딩
	ConfirmButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickConfirmButton);
	CancelButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickCancelButton);
	
	//이름 입력 칸 초기화
	InputNameBox-&amp;gt;SetText(FText::GetEmpty());
}

void UT3InputNamePanel::OnClickConfirmButton()
{
	//이름 미입력시 무시
	const FString InputName = InputNameBox-&amp;gt;GetText().ToString();
	if (InputName.IsEmpty())
	{
		return;
	}
	
	//튜토리얼 시작
	if (const TObjectPtr&amp;lt;UT3SelectClassWidget&amp;gt; WidgetInstance = SelectClassWidget.Get())
	{
		WidgetInstance-&amp;gt;TutorialStart(InputName);
	}
}

void UT3InputNamePanel::OnClickCancelButton()
{
	SetVisibility(ESlateVisibility::Collapsed);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NativeOnInitialized함수에서는 함수 바인딩과 같은 기본적인 초기화를 진행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인 버튼을 누르면 클래스 선택 위젯의 튜토리얼 시작 함수를 실행한다. 이때 이름 입력칸에 입력한 이름을 가져와서 매개변수로 넘겨준다. 만약 가져온 이름이 빈 값이면 실행하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;취소 버튼의 함수에서는 이 패널을 닫도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 이게 무슨 버그야&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 함수 중 확인 버튼의 함수를 처음 만들었을 때는 아래와 같은 모습이었다.&lt;/p&gt;
&lt;pre id=&quot;code_1774459240275&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3InputNamePanel::OnClickConfirmButton()
{
	//이름 미입력시 무시
	const FText InputName = InputNameBox-&amp;gt;GetText();
	if (InputName.IsEmpty())
	{
		return;
	}
    
    UE_LOG(LogTemp, Warning, TEXT(&quot;이름 입력함&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 입력칸을 FText로 가져온 다음 변환하려는 생각이었고 일단 잘 가져오는지 부터 확인하려고 했다. 그런데 여기서 문제가 발생했다. 저 &lt;b&gt;FText 변수를 디버그로 확인해보면 빈 값으로 나온다&lt;/b&gt;. 그래서 매우 당황했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774459509603&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = true))
FText InputNameBoxText;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yaNjT/dJMcahw3X6V/KuykMCXV9wIF8RulFZleZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yaNjT/dJMcahw3X6V/KuykMCXV9wIF8RulFZleZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yaNjT/dJMcahw3X6V/KuykMCXV9wIF8RulFZleZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyaNjT%2FdJMcahw3X6V%2FKuykMCXV9wIF8RulFZleZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;649&quot; height=&quot;180&quot; data-origin-width=&quot;649&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;심지어는 FText를 블루프린트에 노출시키고 블루프린트에서 입력을 하는 경우에도 코드에서는 빈 값으로 나온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774459594012&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPROPERTY(BlueprintReadWrite, meta = (AllowPrivateAccess = true))
FString InputNameBoxString;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;176&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Fltf8/dJMcab4E58R/DYZJHt1FX5PHb2GFS9zXyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Fltf8/dJMcab4E58R/DYZJHt1FX5PHb2GFS9zXyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Fltf8/dJMcab4E58R/DYZJHt1FX5PHb2GFS9zXyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFltf8%2FdJMcab4E58R%2FDYZJHt1FX5PHb2GFS9zXyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;763&quot; height=&quot;176&quot; data-origin-width=&quot;763&quot; data-origin-height=&quot;176&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 여기서 이상한 현상을 발견했다. FText 대신 FString을 사용하면 정상적으로 값을 받아온다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774459663715&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;FString InputName = InputNameBox-&amp;gt;GetText().ToString();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 현상을 발견한 직후 혹시나 하는 마음에 C++ 코드에서도 FText를 받자마자 바로 FString으로 변환하니 값이 정상적으로 확인 가능했다. 분명 FText일때는 빈 값이던 것이 FString으로 변환하니 문자열이 되살아났다. 이게 무슨 어처구니 없는 상황인가...&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상황을 해결하고자 &lt;a href=&quot;https://forums.unrealengine.com/t/ue5-2-1-bug-report-editabletextbox-broke-further-gettext-not-returning-set-value/1213966&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;찾아본 자료&lt;/a&gt;에 따르면 언리얼 엔진의 버그로 추측된다. 나만 겪은 현상이 아니었다. 황당한 문제였지만 어쨌든 FString으로 사용하면 문제가 없을 것이고 그 결과로 위에 적어둔 확인 버튼의 함수 형태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 위젯 블루프린트 제작 및 클래스 선택 위젯에 배치하기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;120&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDI0Bf/dJMcah4TzFf/lkSVGNz3SBOvt85tskl33k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDI0Bf/dJMcah4TzFf/lkSVGNz3SBOvt85tskl33k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDI0Bf/dJMcah4TzFf/lkSVGNz3SBOvt85tskl33k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDI0Bf%2FdJMcah4TzFf%2FlkSVGNz3SBOvt85tskl33k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;400&quot; height=&quot;120&quot; data-origin-width=&quot;400&quot; data-origin-height=&quot;120&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 입력 패널은 화면에 가득 찬 모습이 아니라서 작은 크기로 제작된다. 커스텀(Custom) 크기를 사용하는데 너비를 600, 높이를 400으로 정했다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;280&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6ae7E/dJMcahKBfm2/vKppXHL2czgcfYXpkmaSa0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6ae7E/dJMcahKBfm2/vKppXHL2czgcfYXpkmaSa0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6ae7E/dJMcahKBfm2/vKppXHL2czgcfYXpkmaSa0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6ae7E%2FdJMcahKBfm2%2FvKppXHL2czgcfYXpkmaSa0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;377&quot; height=&quot;280&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;280&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;78&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwPL3b/dJMcadVFPit/oi1vjkerLJ6YkYxTqSzONk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwPL3b/dJMcadVFPit/oi1vjkerLJ6YkYxTqSzONk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwPL3b/dJMcadVFPit/oi1vjkerLJ6YkYxTqSzONk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwPL3b%2FdJMcadVFPit%2Foi1vjkerLJ6YkYxTqSzONk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;78&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;78&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기획서에 있는 내용대로 위젯을 배치한다. 그리고 코드의 변수와 연동이 잘되는지도 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pxw2j/dJMcaiWY5k1/Qzic882vH9bxeWkwSPlkK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pxw2j/dJMcaiWY5k1/Qzic882vH9bxeWkwSPlkK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pxw2j/dJMcaiWY5k1/Qzic882vH9bxeWkwSPlkK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpxw2j%2FdJMcaiWY5k1%2FQzic882vH9bxeWkwSPlkK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;793&quot; height=&quot;484&quot; data-origin-width=&quot;793&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배치해봤는데 여기서 문제점이 하나 예상된다. 이름 입력 패널이 띄워지면 패널 뒤에 있는 버튼은 눌려지면 안되는데 그걸 막아낼 것이 없다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;125&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brdmBG/dJMcagEVXLQ/r8gtMDQwMykSgUaSmYn0P0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brdmBG/dJMcagEVXLQ/r8gtMDQwMykSgUaSmYn0P0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brdmBG/dJMcagEVXLQ/r8gtMDQwMykSgUaSmYn0P0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrdmBG%2FdJMcagEVXLQ%2Fr8gtMDQwMykSgUaSmYn0P0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;272&quot; height=&quot;125&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;125&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;85&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1L674/dJMcai3JDJs/4JNppvPqcdrbDKGGVFv751/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1L674/dJMcai3JDJs/4JNppvPqcdrbDKGGVFv751/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1L674/dJMcai3JDJs/4JNppvPqcdrbDKGGVFv751/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1L674%2FdJMcai3JDJs%2F4JNppvPqcdrbDKGGVFv751%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;224&quot; height=&quot;85&quot; data-origin-width=&quot;224&quot; data-origin-height=&quot;85&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 내가 선택한 것은 패널을 바로 배치하는 것이 아니라 보더를 먼저 배치한 다음 보더 안에 패널을 배치하는 방법이다. 이때 패널 크기를 조절해야 하니 캔버스 패널까지 포함해서 2중으로 감싼 형태가 됐다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;467&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bMy2TH/dJMcagx7qvH/fkOpSpxF0lZKrSYeslaKi1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bMy2TH/dJMcagx7qvH/fkOpSpxF0lZKrSYeslaKi1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bMy2TH/dJMcagx7qvH/fkOpSpxF0lZKrSYeslaKi1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbMy2TH%2FdJMcagx7qvH%2FfkOpSpxF0lZKrSYeslaKi1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;801&quot; height=&quot;467&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;467&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보더를 화면 전체로 늘려서 뒤쪽 버튼을 누르지 못하게 만든다. 이때 보더의 색상은 반투명한 검은색으로 만든다. 그리고 보더의 자식인 캔버스 패널의 크기는 보더 전체에 채우도록 만든다. 이제 패널은 캔버스 패널의 자식으로 배치하면 된다. 이르면 패널이 띄워진 동안 클래스 선택 위젯의 버튼은 건드릴 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 이렇게 만든 다음 이름 입력 패널의 취소 버튼을 누르면 이 보더는 사라지지 않고 패널만 사라진다. 그래서 코드를 수정할 겸 클래스 선택 위젯 C++ 클래스에서도 이 패널을 추가한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774458086157&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3SelectClassWidget.h

public:
	//이름 입력 패널 활성화 여부
	void SetActiveInputNamePanel(bool bActive);

private:
	//이름 입력 패널이 있는 부모 위젯
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UWidget&amp;gt; InputNamePanelParent;
	
	//이름 입력 패널
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UT3InputNamePanel&amp;gt; InputNamePanel;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보더 와 이름 입력 패널을 클래스 선택 위젯에 추가한다. 보더는 비지빌리티만 건드리니 변수 타입을 UWidget으로 해도 상관없다. 그리고 이름 입력 패널을 보여주고 숨기는 것을 이 보더의 비지빌리티로 구현할 것인데 그 내용을 담을 함수도 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774462138610&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SelectClassWidget::NativeConstruct()
{
	Super::NativeConstruct();
	
	...
    
	//이름 입력 패널 초기화
	SetActiveInputNamePanel(false);
	InputNamePanel-&amp;gt;SelectClassWidget = this;
    
	//그외 나머지 초기화
	SelectedPlayerClass = -1;
}

void UT3SelectClassWidget::SetActiveInputNamePanel(bool bActive)
{
	InputNamePanelParent-&amp;gt;SetVisibility(bActive ? ESlateVisibility::Visible : ESlateVisibility::Collapsed);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 입력 패널을 초기화 한다. 해당 패널 클래스에서 이 위젯 클래스를 friend로 선언했으니 마음대로 변수에 접근할 수 있다. 이름 입력 패널은 시작시 숨긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774462280268&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3InputNamePanel.cpp


void UT3InputNamePanel::OnClickCancelButton()
{
	if (const TObjectPtr&amp;lt;UT3SelectClassWidget&amp;gt; WidgetInstance = SelectClassWidget.Get())
	{
		WidgetInstance-&amp;gt;SetActiveInputNamePanel(false);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이름 입력 패널의 취소 버튼의 내용은 자기 자신을 직접 숨기는 것이 아니라 클래스 선택 위젯을 통해 숨기도록 한다.&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/76</guid>
      <comments>https://park94128.tistory.com/76#entry76comment</comments>
      <pubDate>Wed, 25 Mar 2026 21:56:06 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (5)</title>
      <link>https://park94128.tistory.com/75</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 블루프린트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;C++ 클래스를 만들었으니 이를 기반으로 하는 블루프린트를 만들 차례다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 게임 모드&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;106&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GLjkt/dJMcagZa20t/Vzp7pnjwAClFdyRc8Fu801/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GLjkt/dJMcagZa20t/Vzp7pnjwAClFdyRc8Fu801/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GLjkt/dJMcagZa20t/Vzp7pnjwAClFdyRc8Fu801/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGLjkt%2FdJMcagZa20t%2FVzp7pnjwAClFdyRc8Fu801%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;106&quot; height=&quot;177&quot; data-origin-width=&quot;106&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;238&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/myYAM/dJMcabDADR8/7Rftv3YXktnW2PoAFSbgG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/myYAM/dJMcabDADR8/7Rftv3YXktnW2PoAFSbgG1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/myYAM/dJMcabDADR8/7Rftv3YXktnW2PoAFSbgG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmyYAM%2FdJMcabDADR8%2F7Rftv3YXktnW2PoAFSbgG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;462&quot; height=&quot;238&quot; data-origin-width=&quot;462&quot; data-origin-height=&quot;238&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면의 게임 모드처럼 얘도 블루프린트에서 딱히 추가로 해줄 것은 없다. 역시 동일하게 월드 세팅에서 이 블루프린트를 현재 레벨의 게임 모드로 지정해준다. 이후 플레이어 컨트롤러 블루프린트를 만들면 여기서 지정해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. UI&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;125&quot; data-origin-height=&quot;208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ym8UW/dJMcaiWXZnt/tPKkmAyGD1OCVzluxGlVt0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ym8UW/dJMcaiWXZnt/tPKkmAyGD1OCVzluxGlVt0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ym8UW/dJMcaiWXZnt/tPKkmAyGD1OCVzluxGlVt0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fym8UW%2FdJMcaiWXZnt%2FtPKkmAyGD1OCVzluxGlVt0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;125&quot; height=&quot;208&quot; data-origin-width=&quot;125&quot; data-origin-height=&quot;208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 컨트롤러 블루프린트를 만들기 전에 이 위젯 블루프린트를 먼저 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;590&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oUzrf/dJMcabXS6ip/YkZtJ8ABtkbn4oBfuXAJZ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oUzrf/dJMcabXS6ip/YkZtJ8ABtkbn4oBfuXAJZ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oUzrf/dJMcabXS6ip/YkZtJ8ABtkbn4oBfuXAJZ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoUzrf%2FdJMcabXS6ip%2FYkZtJ8ABtkbn4oBfuXAJZ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1011&quot; height=&quot;590&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;590&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가로 박스(HorizontalBox)에 3개의 버튼을 넣음으로 클래스 선택 버튼을 만들었다. 그 외 나머지 선택 버튼과 돌아가기 버튼은 기획서대로 밑에 배치한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bT1p5r/dJMcad2pGcj/xk2wYA8S9tp9Kxwk5BkLZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bT1p5r/dJMcad2pGcj/xk2wYA8S9tp9Kxwk5BkLZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bT1p5r/dJMcad2pGcj/xk2wYA8S9tp9Kxwk5BkLZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbT1p5r%2FdJMcad2pGcj%2Fxk2wYA8S9tp9Kxwk5BkLZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;283&quot; height=&quot;126&quot; data-origin-width=&quot;283&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 버튼 위젯은 C++코드에서 선언한 변수명과 동일한 이름으로 명명하여 잘 연결됐는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 플레이어 컨트롤러&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;117&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Iz0fh/dJMcaaSerel/gC5aLGvAR2kW709vVbBKk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Iz0fh/dJMcaaSerel/gC5aLGvAR2kW709vVbBKk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Iz0fh/dJMcaaSerel/gC5aLGvAR2kW709vVbBKk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIz0fh%2FdJMcaaSerel%2FgC5aLGvAR2kW709vVbBKk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;117&quot; height=&quot;177&quot; data-origin-width=&quot;117&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;73&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/n8lX5/dJMcaaEHd71/60CpOVkAQgfYPcnUD1Qh4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/n8lX5/dJMcaaEHd71/60CpOVkAQgfYPcnUD1Qh4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/n8lX5/dJMcaaEHd71/60CpOVkAQgfYPcnUD1Qh4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn8lX5%2FdJMcaaEHd71%2F60CpOVkAQgfYPcnUD1Qh4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;563&quot; height=&quot;73&quot; data-origin-width=&quot;563&quot; data-origin-height=&quot;73&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 컨트롤러에서 해줄 것은 딱하나, 위젯 블루프린트 작업을 했으니 여기서 지정해준다. 그리고 월드 세팅에서 게임 모드의 플레이어 컨트롤러를 이것으로 지정해준다.&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/75</guid>
      <comments>https://park94128.tistory.com/75#entry75comment</comments>
      <pubDate>Tue, 24 Mar 2026 22:25:30 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (4)</title>
      <link>https://park94128.tistory.com/74</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 클래스 선택 레벨&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;185&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cNmyWs/dJMcafeVOLS/Adnuiwy5KAAPbLkWLB7Rr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cNmyWs/dJMcafeVOLS/Adnuiwy5KAAPbLkWLB7Rr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cNmyWs/dJMcafeVOLS/Adnuiwy5KAAPbLkWLB7Rr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcNmyWs%2FdJMcafeVOLS%2FAdnuiwy5KAAPbLkWLB7Rr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;208&quot; height=&quot;185&quot; data-origin-width=&quot;208&quot; data-origin-height=&quot;185&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면 레벨은 이정도로 하고 클래스 선택 레벨에서 기능을 구현할 차례다. 레벨 자체는 타이틀 화면의 기능 구현을 위해 이미 생성해뒀지만 구현한 것은 아무것도 없으니 그냥 빈 깡통 수준이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccRwYl/dJMcaf61iWc/951Uutojzj9KJrydHExKmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccRwYl/dJMcaf61iWc/951Uutojzj9KJrydHExKmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccRwYl/dJMcaf61iWc/951Uutojzj9KJrydHExKmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccRwYl%2FdJMcaf61iWc%2F951Uutojzj9KJrydHExKmk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1051&quot; height=&quot;645&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;플레이어 클래스를 선택하는 기능이라서 들어가는 기능은 다르겠지만&lt;/span&gt; 여기도도 UI 외에는 출력할 것이 없기 때문에 대략적인 C++ 클래스 구조는 타이틀 레벨과 다르지 않다. 게임 인스턴스는 이미 만든 상태라서 이를 제외한 나머지를 만들면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 모드&lt;/h2&gt;
&lt;pre id=&quot;code_1774278982801&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API AT3SelectClassGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//튜토리얼 시작
	void TutorialStart();
	
	//타이틀 레벨로 돌아가기
	void ReturnToTitleLevel();
	
private:
	//게임 인스턴스
	UPROPERTY()
	TObjectPtr&amp;lt;UT3GameInstance&amp;gt; T3GameInstance;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금 게임 모드에서는 튜토리얼 시작 기능과 타이틀로 돌아가는 기능만 있으면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774283166483&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3SelectClassGameMode::BeginPlay()
{
	Super::BeginPlay();
	
	//게임 인스턴스
	T3GameInstance = Cast&amp;lt;UT3GameInstance&amp;gt;(GetGameInstance());
	if (!T3GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : T3GameInstance is NULL&quot;), *GetNameSafe(this));
		return;
	}
}

void AT3SelectClassGameMode::TutorialStart()
{
	UGameplayStatics::OpenLevel(CurrentWorld, TEXT(&quot;T3TutorialLevel&quot;));
}

void AT3SelectClassGameMode::ReturnToTitleLevel()
{
	UGameplayStatics::OpenLevel(CurrentWorld, TEXT(&quot;T3TitleLevel&quot;));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면의 게임모드 처럼 시작시 게임 인스턴스를 캐싱한다. 튜토리얼 시작은 튜토리얼 레벨을 열고, 타이틀로 돌아가기는 타이틀 레벨을 연다. 튜토리얼 레벨은 다른 분이 만들고 있으니 임시로 예상하여 이름을 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 플레이어 컨트롤러&lt;/h2&gt;
&lt;pre id=&quot;code_1774283136129&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API AT3SelectClassPlayerController : public APlayerController
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//튜토리얼 시작
	void TutorialStart(const FString&amp;amp; PlayerName, const int32 ClassNum);
	
	//타이틀 화면으로
	void ReturnToTitleLevel();
	
private:	
	//게임 모드
	UPROPERTY()
	TObjectPtr&amp;lt;AT3SelectClassGameMode&amp;gt; SelectClassGameMode;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면의 플레이어 컨트롤러처럼 여기서도 위젯과 게임모드를 중계하는 역할이다. 게임 모드에서 선언한 함수에 대응되도록 함수를 만든다. 다만 기획상 선택한 클래스와 이름이 필요하기에 튜토리얼 시작 함수에 매개변수를 넣었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774283664232&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3SelectClassPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	//마우스 사용
	bShowMouseCursor = true;
	const FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	//게임 모드
	SelectClassGameMode = Cast&amp;lt;AT3SelectClassGameMode&amp;gt;(GetWorld()-&amp;gt;GetAuthGameMode());
	if (!SelectClassGameMode)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : TitleGameMode is NULL&quot;), *GetNameSafe(this));
		return;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UI만 존재하는 레벨이니 여기서도 마우스 커서를 보이게 하고 입력 모드를 UI 전용으로 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 이미 타이틀 화면에서 입력 모드를 UI 전용으로 했으니 여기서는 할 필요가 없는데, 테스트를 위해 이 레벨에서 바로 시작할 때 마우스가 간혹 사라지는 현상이 있어서 이렇게 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774283936534&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3SelectClassPlayerController::TutorialStart(const FString&amp;amp; PlayerName, const int32 ClassNum)
{	
	UE_LOG(LogTemp, Warning, TEXT(&quot;Name : %s, Class : %d&quot;), *PlayerName, ClassNum);
	SelectClassGameMode-&amp;gt;TutorialStart();
}

void AT3SelectClassPlayerController::ReturnToTitleLevel()
{
	SelectClassGameMode-&amp;gt;ReturnToTitleLevel();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 이름이나 선택 클래스를 처리할 방법이 없으니 로그로 띄우는 용도로만 사용한다. 그 외에는 각 함수는 게임 모드에 대응되는 함수를 호출하도록 만든다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● UI&lt;/h2&gt;
&lt;pre id=&quot;code_1774284372246&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API UT3SelectClassWidget : public UUserWidget
{
	GENERATED_BODY()
	
protected:
	virtual void NativeConstruct() override;
	
private:
	//클래스 선택 버튼1
	UFUNCTION()
	void OnClickSelectClassButton1();
	
	//클래스 선택 버튼2
	UFUNCTION()
	void OnClickSelectClassButton2();
	
	//클래스 선택 버튼3
	UFUNCTION()
	void OnClickSelectClassButton3();
	
	//클래스 선택
	void SelectClass(int32 ClassNum);
	
	//선택 버튼
	UFUNCTION()
	void OnClickSelectButton();
	
	//타이틀로 돌아가기
	UFUNCTION()
	void ReturnToTitle();
	
	//클래스 버튼1
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton1;
	
	//클래스 버튼2
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton2;
	
	//클래스 버튼3
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectClassButton3;
    
	//선택 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SelectButton;
    
	//돌아가기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; ReturnButton;
	
	//플레이어 컨트롤러
	UPROPERTY()
	TObjectPtr&amp;lt;AT3SelectClassPlayerController&amp;gt; SelectClassPlayerController;
	
	//마지막으로 선택한 클래스
	int32 SelectedPlayerClass;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;미리 캐싱할 플레이어 컨트롤러와 각 버튼마다 연결할 변수 및 대응되는 함수를 선언하느라 헤더가 다소 길어졌다. 내가 짠 코드지만 영 마음에 들지 않는다. 이걸 최적화할 방법을 생각해볼 필요가 있다. 하지만 당장 떠오르는 해결책이 없으니 일단 넘어가야겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774284621633&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SelectClassWidget::NativeConstruct()
{
	Super::NativeConstruct();
	
	//플레이어 컨트롤러
	SelectClassPlayerController = Cast&amp;lt;AT3SelectClassPlayerController&amp;gt;(GetOwningPlayer());
	if (!SelectClassPlayerController)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : SelectClassPlayerController가 NULL&quot;), *GetNameSafe(this));
		return;
	}
	
	//선택 버튼은 비활성화 상태로 시작
	SelectButton-&amp;gt;SetIsEnabled(false);
	
	//버튼 바인딩
	SelectClassButton1-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickSelectClassButton1);
	SelectClassButton2-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickSelectClassButton2);
	SelectClassButton3-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickSelectClassButton3);
	SelectButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickSelectButton);
	ReturnButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::ReturnToTitle);
    
	//그외 나머지 초기화
	SelectedPlayerClass = -1;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NativeConstruct에서는 플레이어 컨트롤러 캐싱 및 버튼 바인딩 작업을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774285264158&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3TitleLevelWidget::OnClickSelectClassButton1()
{
	SelectClass(1);
}

void UT3TitleLevelWidget::OnClickSelectClassButton2()
{
	SelectClass(2);
}

void UT3TitleLevelWidget::OnClickSelectClassButton3()
{
	SelectClass(3);
}

void UT3TitleLevelWidget::SelectClass(int32 ClassNum)
{
	//이전에 누른 버튼과 동일하면 선택 버튼을 누른 것으로 간주
	if (ClassNum == SelectedPlayerClass)
	{
		OnClickSelectButton();
		return;
	}
	
	//선택 버튼 활성화
	SelectButton-&amp;gt;SetIsEnabled(true);
	
	//누른 버튼 기억
	SelectedPlayerClass = ClassNum;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 클래스 버튼의 함수는 하나의 클래스 선택 함수로 이어진다. 이 함수에서는 이전에 누른 버튼의 번호를 기억한 다음 다시 버튼을 누를 때 그게 동일한 버튼이면 선택 버튼의 함수를 호출하도록 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 클래스 버튼 중 아무거나 눌렀다면 선택 버튼을 활성화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774285674954&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SelectClassWidget::OnClickSelectButton()
{
	SelectClassPlayerController-&amp;gt;TutorialStart(TEXT(&quot;&quot;), SelectedPlayerClass);
}
	

void UT3SelectClassWidget::ReturnToTitle()
{
	SelectClassPlayerController-&amp;gt;ReturnToTitleLevel()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선택 버튼과 타이틀로 돌아가기 버튼의 함수는 플레리어 컨트롤러에서 대응되는 함수를 호출한다. 이름 입력은 아직 구현하지 않았으니 튜토리얼 시작 함수의 첫 번째 매개변수는 임시로 빈 문자열을 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774285814219&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3SelectClassPlayerController.h

private:
	//클래스 선택 화면의 위젯
	UPROPERTY(EditDefaultsOnly, Category = &quot;Widget&quot;, meta = (AllowPrivateAccess = true))
	TSubclassOf&amp;lt;UT3SelectClassWidget&amp;gt; SelectClassWidgetClass;
	
	UPROPERTY()
	TObjectPtr&amp;lt;UT3SelectClassWidget&amp;gt; SelectClassWidgetInstance;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1774285847790&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3SelectClassPlayerController.cpp

void AT3SelectClassPlayerController::BeginPlay()
{
	Super::BeginPlay();
	
    
	...
    
	
	//위젯 생성
	SelectClassWidgetInstance = CreateWidget&amp;lt;UT3SelectClassWidget&amp;gt;(this, SelectClassWidgetClass);
	if (!SelectClassWidgetInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : Failed to create TitleLevelWidget&quot;), *GetNameSafe(this));
		return;
	}
	SelectClassWidgetInstance-&amp;gt;AddToViewport();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위젯 클래스를 만들었으니 플레이어 컨트롤러에서는 해당 위젯을 화면에 출력하도록 한다.&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/74</guid>
      <comments>https://park94128.tistory.com/74#entry74comment</comments>
      <pubDate>Mon, 23 Mar 2026 23:47:36 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (3)</title>
      <link>https://park94128.tistory.com/73</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 지금 타이틀 레벨에서 추가로 구현할 수 있는 것&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 새 게임을 누르면 다음 레벨로 이동하기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;108&quot; data-origin-height=&quot;179&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1ScdK/dJMcaadzqaC/1gIZWNjaKqFWuyiBpRdIM0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1ScdK/dJMcaadzqaC/1gIZWNjaKqFWuyiBpRdIM0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1ScdK/dJMcaadzqaC/1gIZWNjaKqFWuyiBpRdIM0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1ScdK%2FdJMcaadzqaC%2F1gIZWNjaKqFWuyiBpRdIM0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;108&quot; height=&quot;179&quot; data-origin-width=&quot;108&quot; data-origin-height=&quot;179&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드로는 미리 UGameplayStatics의 OpenLevel함수를 사용했지만 정작 그 대상인 클래스 선택 레벨은 없는 상태다. 그래서 그 상태에서는 눌러도 아무 일도 생기지 않는다. 그래서 클래스 선택 레벨을 만들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 작동해보니 아무것도 없는 빈 레벨인 클래스 선택 레벨이 잘 열리는 것을 확인했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 설정 패널을 누르면 타이틀 버튼을 잠시 숨기기&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;579&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brmnhU/dJMcach88bg/z2z3T7pmrDAkK1ZPJJNpY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brmnhU/dJMcach88bg/z2z3T7pmrDAkK1ZPJJNpY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brmnhU/dJMcach88bg/z2z3T7pmrDAkK1ZPJJNpY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrmnhU%2FdJMcach88bg%2Fz2z3T7pmrDAkK1ZPJJNpY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;996&quot; height=&quot;579&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 만들었던 블루프린트 편집화면대로, 설정 화면을 열면 타이틀 화면 위에 그대로 설정 패널이 출력된다. 패널이 열린 상태지만 뒤에 덮힌 버튼들은 여전히 누를 수 있는 상태다. 어제 잠깐 언급된 내용 중 패널이 열린 동안 버튼이 잠시 사라진다고 했지만 정작 그 기능은 없는 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774021374442&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private:
    //타이틀 버튼이 있는 세로 박스
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UWidget&amp;gt; TitleButtonsBox;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tiO0N/dJMb99Msl51/Kt6TXcLeA6Ept0ey6Yh2g1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tiO0N/dJMb99Msl51/Kt6TXcLeA6Ept0ey6Yh2g1/img.png&quot; data-origin-width=&quot;279&quot; data-origin-height=&quot;143&quot; data-is-animation=&quot;false&quot; style=&quot;width: 66.6353%; margin-right: 10px;&quot; data-widthpercent=&quot;67.42&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tiO0N/dJMb99Msl51/Kt6TXcLeA6Ept0ey6Yh2g1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtiO0N%2FdJMb99Msl51%2FKt6TXcLeA6Ept0ey6Yh2g1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;279&quot; height=&quot;143&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwzCRG/dJMcahRg5co/aZiYkNn7ZJAj2w5zFkjKlk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwzCRG/dJMcahRg5co/aZiYkNn7ZJAj2w5zFkjKlk/img.png&quot; data-origin-width=&quot;297&quot; data-origin-height=&quot;315&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;32.58&quot; style=&quot;width: 32.2019%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwzCRG/dJMcahRg5co/aZiYkNn7ZJAj2w5zFkjKlk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwzCRG%2FdJMcahRg5co%2FaZiYkNn7ZJAj2w5zFkjKlk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;297&quot; height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 버튼마다 일일이 숨길 수도 있지만 그것보다는 그 버튼을 자식으로 둔 세로 박스를 숨기는 쪽이 더 편하다. 그래서 세로 박스를 코드에서 다룰 수 있도록 코드의 헤더에서 변수를 선언한다. 그리고 위젯과 잘 연동하는지도 확인한다. 숨기는 것 외에는 다룰 것이 없으니 변수 타입은 세로 박스(UVerticalBox)가 아닌 그냥 위젯(UWidget)으로 선언했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774021640643&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public:
	//설정 패널을 닫을 때 버튼 복구
	void RestoreTitleButtons();&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 패널을 출력하는 함수는 이미 설정 버튼으로서 만들었기 때문에 버튼을 복구하는 함수만 추가로 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774021701839&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3TitleLevelWidget::OnClickSettingsButton()
{
	SettingsPanel-&amp;gt;OpenSettingsPanel();
	TitleButtonsBox-&amp;gt;SetVisibility(ESlateVisibility::Collapsed);
}

void UT3TitleLevelWidget::RestoreTitleButtons()
{
	TitleButtonsBox-&amp;gt;SetVisibility(ESlateVisibility::Visible);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 패널을 여는 함수에서는 설정 패널에 있는 패널 출력 함수를 실행하면서 버튼이 있는 세로 박스를 숨긴다. 그리고 버튼을 복구하는 함수에서는 세로 박스를 다시 보여준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 설정 패널은 게임 내에서도 열 수 있다.&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래라면 타이틀 화면 위젯의 버튼 복구 함수를 설정 패널을 닫는 확인 버튼 함수에서 직접 호출하려고 했다. 하지만 여기서 하나 걸리는 점을 발견했다. &lt;b&gt;설정 패널은 게임 내에서도 열 수 있어야 한다&lt;/b&gt;는 기획이 있기 때문이다. 그러면 이 블루프린트 위젯 그대로 가져다가 사용할텐데, 게임 내에서는 타이틀 화면 위젯이 존재하지 않는다. 따라서 게임 내에서 그대로 쓸거면 &lt;b&gt;타이틀 화면 위젯에 직접 접근하는 코드를 사용하면 안된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제에 대한 해법은 다음과 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1774022531996&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DECLARE_DELEGATE(FOnClosePanel);

UCLASS()
class DESECRATION_API UT3SettingsPanel : public UUserWidget
{
	GENERATED_BODY()
    
    
	...
   
public:
	//설정 패널 닫을 때 실행할 내용
	FOnClosePanel OnClosePanel;
    
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;델리게이트를 이용하면 된다. 그러면 타이틀 위젯이든, 게임내 다른 위젯이든 이 설정 패널을 가지고 있다면 설정 패널의 델리게이트에 설정 패널을 닫을 때 실행할 내용을 등록하면 된다. 타이틀 위젯에서는 버튼을 복구하는 함수를 등록하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774022716835&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SettingsPanel::OnClickConfirmButton()
{
	//패널 닫기
	SetVisibility(ESlateVisibility::Collapsed);
    
	//패널을 닫을 때 해야할 일
	OnClosePanel.ExecuteIfBound();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 패널을 닫을 때 이 델리게이트를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774022790022&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3TitleLevelWidget::NativeConstruct()
{

	...
    
	
	//설정 패널을 닫으면 타이틀 버튼을 복구
	SettingsPanel-&amp;gt;OnClosePanel.BindUObject(this, &amp;amp;ThisClass::RestoreTitleButtons);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 말한대로 타이틀 화면 위젯에서는 설정 패널의 이 델리게이트에 버튼을 복구하는 함수를 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/73</guid>
      <comments>https://park94128.tistory.com/73#entry73comment</comments>
      <pubDate>Fri, 20 Mar 2026 23:21:41 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (2)</title>
      <link>https://park94128.tistory.com/72</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 레벨 생성&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;121&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JTzXW/dJMb99Td46k/t4UB6Oa4MhNCm6eSX4WD4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JTzXW/dJMb99Td46k/t4UB6Oa4MhNCm6eSX4WD4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JTzXW/dJMb99Td46k/t4UB6Oa4MhNCm6eSX4WD4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJTzXW%2FdJMb99Td46k%2Ft4UB6Oa4MhNCm6eSX4WD4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;121&quot; height=&quot;193&quot; data-origin-width=&quot;121&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클래스를 만들었다면 그 클래스를 적용할 곳이 필요하다. 바로 레벨(맵)이다. 이 레벨은 타이틀 화면을 보여주는 곳이라서 이 게임의 얼굴을 담당한다고 볼 수 있다. 그래서 여기에서의 기능은 게임 진행하는것 만틈이나 중요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;169&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dj2wm/dJMcab4zTzj/JpPQOEEiAfVcawhnr2dzo0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dj2wm/dJMcab4zTzj/JpPQOEEiAfVcawhnr2dzo0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dj2wm/dJMcab4zTzj/JpPQOEEiAfVcawhnr2dzo0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDj2wm%2FdJMcab4zTzj%2FJpPQOEEiAfVcawhnr2dzo0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;596&quot; height=&quot;169&quot; data-origin-width=&quot;596&quot; data-origin-height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면을 제일 먼저 보여줄거면 프로젝트 세팅에서 게임 기본 맵으로 지정하는 것을 잊지 말자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;● 블루프린트 생성&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;옛날이면 모든 기능을 코드로 구현해야 했지만 요즘은 블루프린트로 충분히 구현할 수 있다. 하지만 모든 것을 블루프린트로 만드는 것은 코딩이라고 할 수 없다. 나같은 경우, 핵심 기능은 코드로 구현하고 변수 값 할당, 간단한 키 입력 등은 블루프린트에서 해결하는 하이브리드 방식을 지향한다. 그리고 블루프린트로 구현하는 것을 선호하는 팀원과의 협업을 하려면 블루프린트를 통해서 연결하야 하니 블루프린트 생성은 반드시 해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 게임 인스턴스&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;107&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UEW5z/dJMcabp0u2N/KDfqI7fNq2V1sQk7kX1PKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UEW5z/dJMcabp0u2N/KDfqI7fNq2V1sQk7kX1PKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UEW5z/dJMcabp0u2N/KDfqI7fNq2V1sQk7kX1PKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUEW5z%2FdJMcabp0u2N%2FKDfqI7fNq2V1sQk7kX1PKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;107&quot; height=&quot;177&quot; data-origin-width=&quot;107&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;75&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cfwJq2/dJMcagx2hcA/Ha1MM6mPizRyveTRoayf50/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cfwJq2/dJMcagx2hcA/Ha1MM6mPizRyveTRoayf50/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cfwJq2/dJMcagx2hcA/Ha1MM6mPizRyveTRoayf50/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcfwJq2%2FdJMcagx2hcA%2FHa1MM6mPizRyveTRoayf50%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;589&quot; height=&quot;75&quot; data-origin-width=&quot;589&quot; data-origin-height=&quot;75&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 인스턴스 클래스를 기반으로 하는 블루프린트다. 프로젝트 세팅에서 게임 인스턴스 지정시 이 블루프린트를 지정한다. 현 시점에서는 블루프린트에서 따로 구현할 내용은 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 게임 모드&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;105&quot; data-origin-height=&quot;177&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bh6TpD/dJMcafFVmm6/DkhMyjbKDjRjdRe4IxKEhk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bh6TpD/dJMcafFVmm6/DkhMyjbKDjRjdRe4IxKEhk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bh6TpD/dJMcafFVmm6/DkhMyjbKDjRjdRe4IxKEhk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbh6TpD%2FdJMcafFVmm6%2FDkhMyjbKDjRjdRe4IxKEhk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;105&quot; height=&quot;177&quot; data-origin-width=&quot;105&quot; data-origin-height=&quot;177&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0lydx/dJMcacJdknQ/UETAKcyVHa26EmVsrIOGx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0lydx/dJMcacJdknQ/UETAKcyVHa26EmVsrIOGx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0lydx/dJMcacJdknQ/UETAKcyVHa26EmVsrIOGx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0lydx%2FdJMcacJdknQ%2FUETAKcyVHa26EmVsrIOGx1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;510&quot; height=&quot;232&quot; data-origin-width=&quot;510&quot; data-origin-height=&quot;232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 모드 클래스를 기반으로 하는 블루프린트다. 이 블루프린트도 추가로 구현할 내용은 없지만 월드 세팅에서 오버라이드할 게임 모드를 이 블루프린트로 지정해야 한다. 위 사진에는 아래의 플레이어 컨트롤러 블루프린트가 지정된 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;3. 위젯 블루프린트&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;A) 설정 패널&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nGG7f/dJMcaiikAib/3iQZHQO5O1c65ko7wkjOX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nGG7f/dJMcaiikAib/3iQZHQO5O1c65ko7wkjOX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nGG7f/dJMcaiikAib/3iQZHQO5O1c65ko7wkjOX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnGG7f%2FdJMcaiikAib%2F3iQZHQO5O1c65ko7wkjOX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;675&quot; height=&quot;392&quot; data-origin-width=&quot;675&quot; data-origin-height=&quot;392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 패널은 타이틀 화면 안에 들어갈 것이니 크기를 작게 잡았다. 전체 화면을 1920x1080으로 잡았으니 임시로 1280x720으로 만들었다. 정확한 크기는 차후 아트 담당자 분이 그리는 대로 결정하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드에서는 닫기 버튼, 확인 버튼에 대해서만 선언했지만 기획상 사용할 기능은 많기 때문에 위젯을 미리 구성해놓는다. 지금 이걸 구현하기엔 시간이 부족하니 MVP를 완성한 다음 구현할 생각이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HPA7o/dJMcah4OlZe/bmPNc604DMVpNZ7OhflD71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HPA7o/dJMcah4OlZe/bmPNc604DMVpNZ7OhflD71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HPA7o/dJMcah4OlZe/bmPNc604DMVpNZ7OhflD71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHPA7o%2FdJMcah4OlZe%2FbmPNc604DMVpNZ7OhflD71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;126&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;닫기 버튼과 확인 버튼은 코드의 변수와 동일한 이름으로 명명하여 코드와 잘 연동됐는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;B ) 타이틀 화면&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/czGHiN/dJMcaf6YuN8/mfduuE4gnXJb4d0W5bBWv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/czGHiN/dJMcaf6YuN8/mfduuE4gnXJb4d0W5bBWv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/czGHiN/dJMcaf6YuN8/mfduuE4gnXJb4d0W5bBWv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FczGHiN%2FdJMcaf6YuN8%2FmfduuE4gnXJb4d0W5bBWv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;775&quot; height=&quot;458&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 제목이 들어갈 자리에 이미지를 배치했다. 일부러 눈에 잘 띄라고 마젠타 색상으로 칠했다. 그래야 나중에 안까먹고 텍스쳐를 넣을테니까&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면에 사용할 버튼 4개를 배치했다. 각 버튼마다 텍스트를 넣었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ciNyGb/dJMcaadylQz/B0S1kDGCeM4lQgAbDomca0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ciNyGb/dJMcaadylQz/B0S1kDGCeM4lQgAbDomca0/img.png&quot; data-origin-width=&quot;1020&quot; data-origin-height=&quot;597&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.2505%; margin-right: 10px;&quot; data-widthpercent=&quot;49.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ciNyGb/dJMcaadylQz/B0S1kDGCeM4lQgAbDomca0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FciNyGb%2FdJMcaadylQz%2FB0S1kDGCeM4lQgAbDomca0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1020&quot; height=&quot;597&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6dRjR/dJMcai3EvIx/NRIGwR6104qe4SXIP4fh51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6dRjR/dJMcai3EvIx/NRIGwR6104qe4SXIP4fh51/img.png&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;579&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.5867%;&quot; data-widthpercent=&quot;50.17&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6dRjR/dJMcai3EvIx/NRIGwR6104qe4SXIP4fh51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6dRjR%2FdJMcai3EvIx%2FNRIGwR6104qe4SXIP4fh51%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;996&quot; height=&quot;579&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 설정 패널을 집어넣었는데... 패널에 배경이 없어서 타이틀의 위젯과 구분이 안된다. 설정 패널을 여는 동안 타이틀의 버튼은 잠시 사라지지만 그래도 패널에 배경이 없으니 패널이라는 느낌이 없다. 그래서 회색 배경이라도 넣어줬다. 그러니 패널이라는 느낌이 든다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;281&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xI6FM/dJMcahDH67Q/DbX1bEGIkISFdid3j3E14K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xI6FM/dJMcahDH67Q/DbX1bEGIkISFdid3j3E14K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xI6FM/dJMcahDH67Q/DbX1bEGIkISFdid3j3E14K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxI6FM%2FdJMcahDH67Q%2FDbX1bEGIkISFdid3j3E14K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;281&quot; height=&quot;110&quot; data-origin-width=&quot;281&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면의 버튼과 설정 패널 모두 이름을 코드의 변수와 동일한 이름으로 명명한다. 그리고 코드와 연동됐는지도 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 플레이어 컨트롤러&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;106&quot; data-origin-height=&quot;183&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k2gHa/dJMcaaEDadA/R3qJIe25OZKtR2ZcOOoUm0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k2gHa/dJMcaaEDadA/R3qJIe25OZKtR2ZcOOoUm0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k2gHa/dJMcaaEDadA/R3qJIe25OZKtR2ZcOOoUm0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk2gHa%2FdJMcaaEDadA%2FR3qJIe25OZKtR2ZcOOoUm0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;106&quot; height=&quot;183&quot; data-origin-width=&quot;106&quot; data-origin-height=&quot;183&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0S4Xy/dJMcaduvcAo/plKqkARoskokGLcMwejd0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0S4Xy/dJMcaduvcAo/plKqkARoskokGLcMwejd0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0S4Xy/dJMcaduvcAo/plKqkARoskokGLcMwejd0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0S4Xy%2FdJMcaduvcAo%2FplKqkARoskokGLcMwejd0k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;463&quot; height=&quot;112&quot; data-origin-width=&quot;463&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 컨트롤러 클래스 기반인 블루프린트다. 여기서 내가 생성한 타이틀 화면 위젯 블루프린트를 위젯 클래스로 지정한다.&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/72</guid>
      <comments>https://park94128.tistory.com/72#entry72comment</comments>
      <pubDate>Thu, 19 Mar 2026 23:58:39 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 시작부터 MVP까지 (1)</title>
      <link>https://park94128.tistory.com/71</link>
      <description>&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;● MVP란 무엇인가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스포츠에서 말하는 Most Valuable Player도 아니다. 백화점 같은 곳에서 중요 고객을 칭하는 VIP의 일종 같은것도 아니다. 당연히 &lt;a href=&quot;https://namu.wiki/w/%EB%A9%94%EC%9D%B4%ED%94%8C%EC%8A%A4%ED%86%A0%EB%A6%AC/MVP&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;메이플스토리의 MVP 시스템&lt;/a&gt;도 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;여기서 말하는 MVP는 Minimum Viable Product의 약자로, '최소한의 기능만을 갖춘 상품'이라는 의미다. 어떻게 생각해보면 프로토타입과 비슷하다고 볼 수 있다. 게임이라고 불릴 수 있도록 핵심적인 기능만 구현해둔 상태를 말할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이 MVP라는 것은 완성을 위한 초석이라고 할 수 있는데, 만드려고 하는 게임을 MVP를 거치지 않고 한꺼번에 모든 것을 구현하려고 하면 막대한 분량, 막연한 방향에 의해 진행이 막혀버리고 말 것이다. 하지만 MVP를 먼저 완성하면 이후 추가적인 기능은 MVP에서 붙이거나 수정하는 식으로 구현하면 되기 때문에 만들고자 하는 방향이 명확해지고 많은 분량을 정리할 수 있어서 제작 분량을 수시로 가늠할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;이러한 이유로 나와 팀원은 당장에 만들 것, 지금은 만들지 않아도 되는 것을 구분하여 각자 파트에서 필수 기능만을 만들기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 내가 담당할 범위&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bDQug1/dJMcabpZsNb/knnalEeY6C8sbJ5pEumOsK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bDQug1/dJMcabpZsNb/knnalEeY6C8sbJ5pEumOsK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bDQug1/dJMcabpZsNb/knnalEeY6C8sbJ5pEumOsK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbDQug1%2FdJMcabpZsNb%2FknnalEeY6C8sbJ5pEumOsK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;981&quot; height=&quot;766&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;팀원끼리 설계를 했을때 내가 하기로 한 것은 게임시스템이다. 본 게임을 시작하기전 일어나는 일(새 게임 시작, 이어하기, 종료 등)과 화면에 직접 보이지 않는 기능(진행 상황 저장, 게임 설정 등)을 담당하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 타이틀 화면을 위한 클래스&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmoYRf/dJMb996K1Op/ZAzswDT29WCCib1RizxlSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmoYRf/dJMb996K1Op/ZAzswDT29WCCib1RizxlSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmoYRf/dJMb996K1Op/ZAzswDT29WCCib1RizxlSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmoYRf%2FdJMb996K1Op%2FZAzswDT29WCCib1RizxlSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1051&quot; height=&quot;645&quot; data-origin-width=&quot;1051&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 생각한 구조를 그려보자면 위와 같다. 레벨내 클래스 구조는 그냥 기본적인 언리얼 엔진의 레벨 구성에 폰(캐릭터)만 빠졌다. UI만 필요할 것이니 폰은 쓸 일이 없고 레벨에 따로 배치될 액터도 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 인스턴스는 타이틀 레벨 뿐만이 아니라 전역으로 사용할 것이다. 다만 게임 인스턴스의 Init함수를 통한 초기화 외에도 타이틀 레벨에서 진행할 초기화가 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 레벨에 필요한 기능들은 게임 모드에서 주로 구현할 것이며 일부는 게임 인스턴스를 이용할 생각이다. 플레이어 컨트롤러는 UI 생성 목적으로 사용할 생각이며 UI를 통해 타이틀 화면의 기능을 유저에게 제공하도록 만들 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 이렇게 생각하며 만들고 추가로 필요한 클래스가 생긴다면 그때가서 만들어 본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● SaveGame&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가로 필요한 클래스가 바로 생겼다. 나는 게임 데이터를 게임 인스턴스에서 보관하기로 했다. 이 게임 데이터는 게임 구성을 담고 있으며 이 데이터를 저장하도록 만들 것이다. 그리고 게임 불러오기를 하면 이 데이터를 생성하도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해 필요한 것이 SaveGame이라는 클래스다. 언리얼 엔진은 생각보다 싱글 게임을 위한 기능이 많이 있는데 게임 저장 및 불러오기에 필요한 기능을 이 SaveGame 클래스를 통해 제공하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;216&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/svFEx/dJMb996K3D8/angMbU4PJgK0EyItS5CDf1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/svFEx/dJMb996K3D8/angMbU4PJgK0EyItS5CDf1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/svFEx/dJMb996K3D8/angMbU4PJgK0EyItS5CDf1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsvFEx%2FdJMb996K3D8%2FangMbU4PJgK0EyItS5CDf1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;670&quot; height=&quot;216&quot; data-origin-width=&quot;670&quot; data-origin-height=&quot;216&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;뭔가 설명은 긴데 지금 미리 말하자면 저 설명대로 하기엔 다소 귀찮으니 간단하게 처리할 수 있는 함수를 사용할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773848820068&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API UT3SaveGame : public USaveGame
{
	GENERATED_BODY()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금은 게임 데이터의 구성을 생각하지 않았으니 일단 클래스를 만들기만 하고 다음에 내용을 채운다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 인스턴스&lt;/h2&gt;
&lt;pre id=&quot;code_1773849195786&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API UT3GameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	virtual void Init() override;

	/**
	 * 게임 저장하기
	 * @return true : 저장 성공
	 */
	bool SaveGame();
	
	/**
	 * 저장된 게임 불러오기
	 * @return true : 불러오기 성공
	 */
	bool LoadGame();
	
	//저장된 게임
	FORCEINLINE TObjectPtr&amp;lt;UT3SaveGame&amp;gt; GetSavedGameData() { return SavedGameData; }
	
private:
	//현재 설정
	UPROPERTY()
	TObjectPtr&amp;lt;UT3SaveUserSettings&amp;gt; CurrentSettings;
	
	//저장, 불러오기에 사용할 슬롯 이름
	const FString SAVE_GAME_NAME = TEXT(&quot;SaveSlot1&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말한대로, 게임 데이터를 게임 인스턴스에 보관한다. 그리고 게임 인스턴스에서 게임 데이터를 가지고 있으니 게임 불러오기 및 저장도 게임 인스턴스에서 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773849227238&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3GameInstance::Init()
{
	Super::Init();
	
	//게임 데이터
	LoadGame();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 인스턴스의 시작시 자동으로 실행하는 Init 함수에서 곧바로 저장되 게임 데이터를 가져오는 함수를 실행한다 타이틀 화면에서 불러온 데이터가 있으면 이어하기를 할 수 있고 아니면 할 수 없게 만들도록 구현할 생각이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773849345233&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;bool UT3GameInstance::SaveGame()
{
	return UGameplayStatics::SaveGameToSlot(SavedGameData, SAVE_GAME_NAME, 0);
}

bool UT3GameInstance::LoadGame()
{
	TObjectPtr&amp;lt;UT3SaveGame&amp;gt; SavedData = Cast&amp;lt;UT3SaveGame&amp;gt;(UGameplayStatics::LoadGameFromSlot(SAVE_GAME_NAME, 0));
	if (!SavedData)
	{
		return false;
	}
	
	SavedGameData = SavedData;
	SavedGameData-&amp;gt;bSetLocation = true;
	return true;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 저장하는 함수와 불러오는 함수의 내용이다. 저장과 불러오기 기능은 UGameplayStatics에 있는 함수로 간단하게 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- SaveGameToSlot함수를 통해 데이터를 저장할 수 있다. 매게변수는 SaveGame클래스(게임 데이터), FString(이름), int(슬롯 번호)다. 지정한 이름과 슬롯에 게임 데이터를 저장하는 기능이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- LoadGameFromSlot함수는 반대로 저장된 데이터를 불러온다. 지정한 이름과 슬롯 번호에 해당되는 데이터를 가져오는데 함수의 결과는 원형인 USaveGame이니 캐스팅할 필요가 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;184&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uWuBn/dJMcabwICA7/VZ5EMiYBxMzxMcmjV7daD0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uWuBn/dJMcabwICA7/VZ5EMiYBxMzxMcmjV7daD0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uWuBn/dJMcabwICA7/VZ5EMiYBxMzxMcmjV7daD0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FuWuBn%2FdJMcabwICA7%2FVZ5EMiYBxMzxMcmjV7daD0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;567&quot; height=&quot;184&quot; data-origin-width=&quot;567&quot; data-origin-height=&quot;184&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임을 저장했다면 [프로젝트 폴더]/Saved/SaveGames에 저장된다. 위 두 함수는 이 경로에 있는 파일과 관련 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 모드&lt;/h2&gt;
&lt;pre id=&quot;code_1773849816886&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API AT3TitleGameMode : public AGameModeBase
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//클래스 선택 화면으로
	void MoveToSelectClassLevel();
	
	//마지막 저장 장소에서 계속하기 (저장된 게임 데이터 적용)
	void MoveToLastSavedLevel();
	
	//게임 종료
	void QuitGame();
	
private:
	//게임 인스턴스
	UPROPERTY()
	TObjectPtr&amp;lt;UT3GameInstance&amp;gt; T3GameInstance;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;타이틀 화면에서 구현할 기능은 게임 모드에서 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773849876339&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3TitleGameMode::BeginPlay()
{
	Super::BeginPlay();
	
	//게임 인스턴스
	T3GameInstance = Cast&amp;lt;UT3GameInstance&amp;gt;(GetGameInstance());
	if (!T3GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : T3GameInstance is NULL&quot;), *GetNameSafe(this));
		return;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게임 모드 시작시 게임 인스턴스를 미리 캐싱한다. 저장된 게임과 관련된 기능이 있기 때문에 게임 인스턴스를 자주 이용할 것이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773850043098&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3TitleGameMode::MoveToSelectClassLevel()
{
	UGameplayStatics::OpenLevel(GetWorld(), TEXT(&quot;T3SelectClassLevel&quot;);
}

void AT3TitleGameMode::MoveToLastSavedLevel()
{
	//TODO : 게임 불러오기
}

void AT3TitleGameMode::QuitGame()
{
	UKismetSystemLibrary::QuitGame(GetWorld(), GetWorld()-&amp;gt;GetFirstPlayerController(), EQuitPreference::Quit, false);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 현 시점에서는 아직 게임 불러오기를 구현하지 않는다. 아직 SaveGame클래스의 내용을 정해놓지 않았기 때문에 보류한다. 그래서 새 게임 시작시 클래스 선택 화면으로 넘어가는 것과 게임을 종료하는 기능만 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 플레이어 컨트롤러&lt;/h2&gt;
&lt;pre id=&quot;code_1773850260640&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API AT3TitlePlayerController : public APlayerController
{
	GENERATED_BODY()
	
protected:
	virtual void BeginPlay() override;
	
public:
	//클래스 선택 화면으로
	void MoveToSelectClassLevel();
	
	//마지막 저장 장소에서 계속하기 (저장된 게임 데이터 적용)
	void MoveToLastSavedLevel();
	
	//저장된 게임 데이터 존재 여부
	bool DoesSavedDataExist();
	
	//게임 종료
	void QuitGame();
	
private:
	//타이틀 화면의 위젯 클래스
	UPROPERTY(EditDefaultsOnly, Category = &quot;Widget&quot;, meta = (AllowPrivateAccess = true))
	TSubclassOf&amp;lt;UT3TitleLevelWidget&amp;gt; TitleLevelWidgetClass;
	
	//타이틀 화면의 위젯
	UPROPERTY()
	TObjectPtr&amp;lt;UT3TitleLevelWidget&amp;gt; TitleLevelWidgetInstance;
	
	//게임 모드
	UPROPERTY()
	TObjectPtr&amp;lt;AT3TitleGameMode&amp;gt; TitleGameMode;
	
	//게임 인스턴스
	UPROPERTY()
	TObjectPtr&amp;lt;UT3GameInstance&amp;gt; T3GameInstance;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;플레이어 컨트롤러는 UI를 생성하고 UI와 기능을 중계하는 역할을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773850384362&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3TitlePlayerController::BeginPlay()
{
	Super::BeginPlay();
	
	//마우스 사용
	bShowMouseCursor = true;
	const FInputModeUIOnly InputModeUIOnly;
	SetInputMode(InputModeUIOnly);
	
	//게임 모드
	TitleGameMode = Cast&amp;lt;AT3TitleGameMode&amp;gt;(GetWorld()-&amp;gt;GetAuthGameMode());
	if (!TitleGameMode)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : TitleGameMode is NULL&quot;), *GetNameSafe(this));
		return;
	}
	
	//게임 인스턴스
	T3GameInstance = Cast&amp;lt;UT3GameInstance&amp;gt;(GetGameInstance());
	if (!T3GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : T3GameInstance is NULL&quot;), *GetNameSafe(this));
		return;
	}
	
	//위젯 생성
	TitleLevelWidgetInstance = CreateWidget&amp;lt;UT3TitleLevelWidget&amp;gt;(this, TitleLevelWidgetClass);
	if (!TitleLevelWidgetInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : Failed to create TitleLevelWidget&quot;), *GetNameSafe(this));
		return;
	}
	TitleLevelWidgetInstance-&amp;gt;AddToViewport();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BeginPlay에서는 게임 인스턴스와 게임 모드를 캐싱한다. 그 다음 UI를 화면에 출력한다. 타이틀 화면은 직접 움직일 폰은 없고 UI만 있는 레벨이니 UI전용 입력으로 설정하고 마우스를 보이게 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773850441764&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AT3TitlePlayerController::MoveToSelectClassLevel()
{
	TitleGameMode-&amp;gt;MoveToSelectClassLevel();
}

void AT3TitlePlayerController::MoveToLastSavedLevel()
{	
	TitleGameMode-&amp;gt;MoveToLastSavedLevel();
}

bool AT3TitlePlayerController::DoesSavedDataExist()
{
	return T3GameInstance-&amp;gt;GetSavedGameData() != nullptr;
}

void AT3TitlePlayerController::QuitGame()
{
	TitleGameMode-&amp;gt;QuitGame();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 함수는 위젯에서 사용할 것이며 모두 게임 인스턴스 및 게임 모드를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● UI&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알다시피 UI를 구현하려면 유저 위젯(UserWidget) 클래스가 필요하다. 이 클래스를 기반으로 위젯 블루프린트를 만들기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 타이틀 화면&lt;/h4&gt;
&lt;pre id=&quot;code_1773850825760&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API UT3TitleLevelWidget : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeConstruct() override;
	
private:
	//새로하기
	UFUNCTION()
	void OnClickNewGameButton();
	
	//이어하기
	UFUNCTION()
	void OnClickLoadButton();
	
	//설정
	UFUNCTION()
	void OnClickSettingsButton();
	
	//나가기
	UFUNCTION()
	void OnClickQuitButton();
	
	//새로하기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; NewGameButton;
	
	//이어하기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; LoadButton;
	
	//설정 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; SettingsButton;
	
	//나가기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; QuitButton;
    
    //풀레이어 컨트롤러
	UPROPERTY()
	TObjectPtr&amp;lt;AT3TitlePlayerController&amp;gt; TitlePlayerController;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위젯 블루프린트에 4개의 버튼이 있을 것을 생각해서 4개의 버튼 위젯 변수를 선언한다. 그리고 이 버튼에 사용할 함수도 각각 선언한다. 그 외에 플레이어 컨트롤러를 자주 이용하니 캐싱용으로 해당 변수도 선언한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773851016848&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3TitleLevelWidget::NativeConstruct()
{
	Super::NativeConstruct();

	//플레이어 컨트롤러
	TitlePlayerController = Cast&amp;lt;AT3TitlePlayerController&amp;gt;(GetOwningPlayer());
	if (!TitlePlayerController)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : TitlePlayerController is NULL&quot;), *GetNameSafe(this));
		return;
	}
	
	//버튼 바인딩
	NewGameButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickNewGameButton);
	SettingsButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickSettingsButton);
	QuitButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickQuitButton);
	//불러오기는 저장된 게임이 있을 때만 사용
	if (TitlePlayerController-&amp;gt;DoesSavedDataExist())
	{
		LoadButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickLoadButton);
	}
	else
	{
		LoadButton-&amp;gt;SetIsEnabled(false);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 사용할 플레이어 컨트롤러를 캐싱한다. 그 다음 각 버튼에 알맞는 함수를 바인딩한다. 이때 불러오기 버튼에 대한 바인딩은 저장된 게임 데이터가 존재할 때만 바인딩하도록 하고 아니면 버튼을 사용하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저장된 게임 데이터 존재 여부는 플레이어 컨트롤러에서 구현한 함수를 사용하면 알 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773851303230&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3TitleLevelWidget::OnClickNewGameButton()
{
	//TODO : 저장된 게임이 있는 경우 물어보기
	TitlePlayerController-&amp;gt;MoveToSelectClassLevel();
}

void UT3TitleLevelWidget::OnClickLoadButton()
{
	//저장된 게임이 없다면 무시
	if (!TitlePlayerController-&amp;gt;DoesSavedDataExist())
	{
		return;
	}
	
	//TODO : 저장된 게임을 이어할지 여부를 물어보기
	TitlePlayerController-&amp;gt;MoveToLastSavedLevel();
}

void UT3TitleLevelWidget::OnClickSettingsButton()
{
	//TODO : 설정 패널 열기
}

void UT3TitleLevelWidget::OnClickQuitButton()
{
	//TODO : 게임 종료 여부 물어보기
	TitlePlayerController-&amp;gt;QuitGame();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼에 바인딩된 각 함수는 플레이어 컨트롤러의 함수를 사용한다. TODO에 적은 내용을 보면 알겠지만, 지금은 기능을 즉시 실행하지만 차후 유저에게 물어보는 기능을 구현할 예정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 설정 패널&lt;/h4&gt;
&lt;pre id=&quot;code_1773851623062&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UCLASS()
class DESECRATION_API UT3SettingsPanel : public UUserWidget
{
	GENERATED_BODY()
	
public:
	virtual void NativeConstruct() override;
	
private:
	//확인 버튼
	UFUNCTION()
	void OnClickConfirmButton();
	
public:
	//설정 패널 열기
	void OpenSettingsPanel();

private:
	//닫기 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; CloseButton;
	
	//확인 버튼
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UButton&amp;gt; ConfirmButton;
	
	//게임 인스턴스
	UPROPERTY()
	TObjectPtr&amp;lt;UT3GameInstance&amp;gt; T3GameInstance;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 기획에 있는 각 내용은 나중에 구현하더라도 일단 열고 닫는 것 까지는 필요해 보인다. 그래서 설정 패널에서는 확인, 닫기 버튼과 바인딩할 함수 1개를 만든다. (확인 버튼과 닫기 버튼은 같은 동작을 실행하니 바인딩할 함수가 2개일 필요는 없다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773851789145&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SettingsPanel::NativeConstruct()
{
	Super::NativeConstruct();
	
	//게임 인스턴스
	T3GameInstance = Cast&amp;lt;UT3GameInstance&amp;gt;(GetGameInstance());
	if (!T3GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT(&quot;%s : T3GameInstance가 NULL&quot;), *GetNameSafe(this));
		return;
	}
	
	//버튼 바인딩
	CloseButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickConfirmButton);
	ConfirmButton-&amp;gt;OnClicked.AddDynamic(this, &amp;amp;ThisClass::OnClickConfirmButton);
	
	//숨김 상태로 시작
	SetVisibility(ESlateVisibility::Collapsed);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시작시 캐싱 및 바인딩은 타이틀 화면 위젯과 비슷하지만 숨김 상태로 시작한다는 차이점이 있다. 이때 Hidden대신 Collapsed를 쓰는 것이 좋은데 Hidden은 눈에 보이지만 않을 뿐 Tick이나 크기 계산등 기능이 계속 작동하는 상태지만 Collapsed는 완전히 사용하지 않는 상태이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773852082106&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UT3SettingsPanel::OnClickConfirmButton()
{
	//패널 닫기
	SetVisibility(ESlateVisibility::Collapsed);
}

void UT3SettingsPanel::OpenSettingsPanel()
{
	//패널 활성화
	SetVisibility(ESlateVisibility::Visible);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버튼에 사용할 함수는 1개 뿐이고 그 내용은 패널을 숨기는 기능을 실행한다. 다른 하나는 반대로 패널을 출력하는 기능을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1773852270853&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3TitleLevelWidget.h

private:
	//설정 패널 위젯
	UPROPERTY(meta = (AllowPrivateAccess = true, BindWidget))
	TObjectPtr&amp;lt;UT3SettingsPanel&amp;gt; SettingsPanel;&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1773852191565&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//T3TitleLevelWidget.cpp

void UT3TitleLevelWidget::OnClickSettingsButton()
{
	SettingsPanel-&amp;gt;OpenSettingsPanel();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정 패널 클래스의 기초 완성됐으니 타이틀 화면 위젯에서 사용하도록 한다.&lt;/p&gt;</description>
      <category>언리얼엔진 학습 및 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/71</guid>
      <comments>https://park94128.tistory.com/71#entry71comment</comments>
      <pubDate>Wed, 18 Mar 2026 23:54:47 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 설계 (3)</title>
      <link>https://park94128.tistory.com/70</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● UI 기획 시작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;758&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AK4QQ/dJMcaaRJ6Qi/VNbj3wKh3nrpQISZ5nTPk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AK4QQ/dJMcaaRJ6Qi/VNbj3wKh3nrpQISZ5nTPk1/img.png&quot; data-alt=&quot;다른 사람이 그린 러프 디자인&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AK4QQ/dJMcaaRJ6Qi/VNbj3wKh3nrpQISZ5nTPk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAK4QQ%2FdJMcaaRJ6Qi%2FVNbj3wKh3nrpQISZ5nTPk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;641&quot; height=&quot;758&quot; data-origin-width=&quot;641&quot; data-origin-height=&quot;758&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;다른 사람이 그린 러프 디자인&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 약속한 내용대로 UI 기획을 했다. 여기 부터 UI 나 혼자서 기획하고 남은 내용은 팀장, 부팀장이 기획한다. 다만 나 혼자 완전 독단적으로 만들지는 않고 다른 사람의 의견을 수렴하면서 기획할 것이고 1차로 다 만든 다음에는 팀장의 컨펌을 받을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 기획할 내용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음부터 완성본을 만들 수는 없다. 괜히 뱁새가 황제 따라가다가 다리 찢어진다는 말이 나온게 아니다. MVP(&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;Minimum Viable Produc)를 먼저 만들고 이를 기반으로 완성 하는 것이 올바른 방법이다. 이 MVP에 사용할 컨텐츠에 대한 UI를 기획하는 것이 나의 목표다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bNxPLS/dJMcabQBYJM/5fpnnUpXSSMvwSLJhNIEN1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bNxPLS/dJMcabQBYJM/5fpnnUpXSSMvwSLJhNIEN1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bNxPLS/dJMcabQBYJM/5fpnnUpXSSMvwSLJhNIEN1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbNxPLS%2FdJMcabQBYJM%2F5fpnnUpXSSMvwSLJhNIEN1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;562&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 간단히 기획했던 내용을 확인해보니 타이틀화면(+ 옵션), 클래스 선택, 기본 UI, 튜토리얼, 인벤토리를 기획하면 되겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● UI 기획을 하는 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 나는 미술 관련 전공자가 아니라서 무언가를 그릴 실력은 없다. 하지만 기획자는 그리는 것은 중요하지 않고 정보 전달이 중요하다. 어차피 아트 담당자는 따로 있으니까 그 분이 쉽게 이해할 수 있도록 UI 기획을 하는 것이 중요하다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c0wnXZ/dJMcabwjfg5/yUwu4ZoqW0K6U39pEcRkd1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c0wnXZ/dJMcabwjfg5/yUwu4ZoqW0K6U39pEcRkd1/img.png&quot; data-origin-width=&quot;1831&quot; data-origin-height=&quot;1008&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.1869%; margin-right: 10px;&quot; data-widthpercent=&quot;49.77&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c0wnXZ/dJMcabwjfg5/yUwu4ZoqW0K6U39pEcRkd1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc0wnXZ%2FdJMcabwjfg5%2FyUwu4ZoqW0K6U39pEcRkd1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1831&quot; height=&quot;1008&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ngVEW/dJMcabiOaFo/yeuQPijTk9m6hsd4BDh8y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ngVEW/dJMcabiOaFo/yeuQPijTk9m6hsd4BDh8y0/img.png&quot; data-origin-width=&quot;1234&quot; data-origin-height=&quot;673&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6503%;&quot; data-widthpercent=&quot;50.23&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ngVEW/dJMcabiOaFo/yeuQPijTk9m6hsd4BDh8y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FngVEW%2FdJMcabiOaFo%2FyeuQPijTk9m6hsd4BDh8y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1234&quot; height=&quot;673&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cVfvKE/dJMcagYGPZK/skpYsrevouoIxNqgZJc9U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cVfvKE/dJMcagYGPZK/skpYsrevouoIxNqgZJc9U1/img.png&quot; data-origin-width=&quot;1215&quot; data-origin-height=&quot;680&quot; data-is-animation=&quot;false&quot; style=&quot;width: 48.5879%; margin-right: 10px;&quot; data-widthpercent=&quot;49.16&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cVfvKE/dJMcagYGPZK/skpYsrevouoIxNqgZJc9U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcVfvKE%2FdJMcagYGPZK%2FskpYsrevouoIxNqgZJc9U1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1215&quot; height=&quot;680&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dBLPQo/dJMcajgLAtC/0nIsFuGPnslLjiv3QOwocK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dBLPQo/dJMcajgLAtC/0nIsFuGPnslLjiv3QOwocK/img.png&quot; data-origin-width=&quot;1251&quot; data-origin-height=&quot;677&quot; data-is-animation=&quot;false&quot; style=&quot;width: 50.2493%;&quot; data-widthpercent=&quot;50.84&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dBLPQo/dJMcajgLAtC/0nIsFuGPnslLjiv3QOwocK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdBLPQo%2FdJMcajgLAtC%2F0nIsFuGPnslLjiv3QOwocK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1251&quot; height=&quot;677&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 UI 별로 어떤 위치에 어떤 그림이 필요한지를 표현하기 위해 단순한 도형을 사용하여 단순한 그림을 그렸다. 그림만으로 설명이 부족할 경우 그 그림에 화살표를 쳐서 부가 설명을 적었다. 이때 아트담당자 뿐만이 아니라 프로그래머도 이해를 하도록 어떤 기능을 사용할지, 값은 얼마나 되는지 등 다양한 내용을 적었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lxh34/dJMcaf6yvkv/sN9uLSLY7ewKnQzvTa4Tu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lxh34/dJMcaf6yvkv/sN9uLSLY7ewKnQzvTa4Tu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lxh34/dJMcaf6yvkv/sN9uLSLY7ewKnQzvTa4Tu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flxh34%2FdJMcaf6yvkv%2FsN9uLSLY7ewKnQzvTa4Tu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1600&quot; height=&quot;900&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;900&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;튜토리얼에 사용하는 안내 패널에 대한 기획은 &lt;a href=&quot;https://namu.wiki/w/%EC%9E%90%20%EC%9D%B4%EA%B2%8C%20%ED%81%B4%EB%A6%AD%EC%9D%B4%EC%95%BC&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;무한도전 밈&lt;/a&gt;을 사용했다. 일부러 기억을 잘 하라고 이렇게 만들었다. 이런 식으로 몇 군데는 재밌으라고 넣은 내용이 있는데, 게임 제작에 필요한 내용만 적으면 내용이 심심하고 지루해지니 이런 식의 분위기 띄우기도 좋다고 생각한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PxmYa/dJMcabXnQsQ/DMrFzjyQ35YaUL5sCORz61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PxmYa/dJMcabXnQsQ/DMrFzjyQ35YaUL5sCORz61/img.png&quot; data-origin-width=&quot;689&quot; data-origin-height=&quot;400&quot; data-is-animation=&quot;false&quot; style=&quot;width: 52.3553%; margin-right: 10px;&quot; data-widthpercent=&quot;52.97&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PxmYa/dJMcabXnQsQ/DMrFzjyQ35YaUL5sCORz61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPxmYa%2FdJMcabXnQsQ%2FDMrFzjyQ35YaUL5sCORz61%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ddbUlW/dJMcafZL23p/8ZmEK4YFstXeK3wUREExZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ddbUlW/dJMcafZL23p/8ZmEK4YFstXeK3wUREExZk/img.png&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;393&quot; data-is-animation=&quot;false&quot; style=&quot;width: 46.4819%;&quot; data-widthpercent=&quot;47.03&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ddbUlW/dJMcafZL23p/8ZmEK4YFstXeK3wUREExZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FddbUlW%2FdJMcafZL23p%2F8ZmEK4YFstXeK3wUREExZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;601&quot; height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌 : MS 파워포인트, 우 : 구글 슬라이드&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MS 파워포인트로 내용을 만들다보니 약간 사소한 문제가 생겼다. 기획서를 구글 드라이브를 통해 공유를 하니 구글 슬라이드를 통한 미리보기가 제공되는데, 구글 슬라이드와 MS 파워포인트는 서로 pptx파일을 표현하는 방식에 차이가 있었다. 텍스트 상자의 크기가 조금씩 다른 문제는 예삿일이고, 네온 효과 등 구글 슬라이드에서는 출력되지 않는 표현도 있었다. 다만 다수의 내용은 온전하게 출력되니 큰 문제로 생각하지는 않는다. 그래도 정확하게 보기 위해 다운받아서 MS 파워포인트로 보는 것을 권장한다고 적어두긴 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N1Po1/dJMcadOnut3/KVGNxzkRwgBktosD76IES1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N1Po1/dJMcadOnut3/KVGNxzkRwgBktosD76IES1/img.png&quot; data-origin-width=&quot;1266&quot; data-origin-height=&quot;608&quot; data-is-animation=&quot;false&quot; style=&quot;width: 49.6146%; margin-right: 10px;&quot; data-widthpercent=&quot;50.2&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N1Po1/dJMcadOnut3/KVGNxzkRwgBktosD76IES1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN1Po1%2FdJMcadOnut3%2FKVGNxzkRwgBktosD76IES1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1266&quot; height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l1UCz/dJMcai3d6v3/8MrwwMyXvoEey201folMk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l1UCz/dJMcai3d6v3/8MrwwMyXvoEey201folMk1/img.png&quot; data-origin-width=&quot;1256&quot; data-origin-height=&quot;608&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;49.8&quot; style=&quot;width: 49.2227%;&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l1UCz/dJMcai3d6v3/8MrwwMyXvoEey201folMk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl1UCz%2FdJMcai3d6v3%2F8MrwwMyXvoEey201folMk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1256&quot; height=&quot;608&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
  &lt;figcaption&gt;좌 : 수정 전, 우 : 수정 후&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1차로 완성하고 팀장, 부팀장에게 보여주니 몇가지 수정 요청을 받았다. 기획을 잘못 이해하여 잘못 만든 것도 있고 막상 디자인해보니 이상해 보이는 것도 있다. 요청받은 대로 내용을 수정하고 다시 컨펌을 받았다. 그리고 괜찮다고 해서 팀원 모두에게 공개했다. 제일 중요한 것은 아트 담당자분이 기획서대로 그려주는 것이지만 나머지 인원들도 이 기획서를 통해 자신이 만들 게임이 어떤 모습일지 잘 알아야 한다고 생각한다.&lt;/p&gt;</description>
      <category>기타 학습, 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/70</guid>
      <comments>https://park94128.tistory.com/70#entry70comment</comments>
      <pubDate>Fri, 6 Feb 2026 23:48:37 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 설계 (2)</title>
      <link>https://park94128.tistory.com/69</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 기획 담당자로 지원하다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 팀에게 1명의 기획 담당자를 지명하라는 지시를 받았다. 내가 있는 팀도 마찬가지다. 그래서 팀장인 분이 기획자로 지원할 사람을 구하고 있었다. 이에 내가 지원했다. 비록 기획에 대해선 잘 알지 못하지만 일을 하면서 기획자와 소통을 많이 해보고 기획자가 만든 기획서를 많이 보고 그 기획에 따라 프로그래밍을 해본 적이 많아 내가 해도 되지 않을가 하는 마음이었다. 그리고 가능한 다양한 경험을 쌓고 싶었기 때문이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 나 혼자서 나 포험 10명이 만들 내용을 다 만드는 것은 아니고 팀장과 부팀장과 함께 총 3명이서 기획하기로 했다. 애초에 기획자로 지명된 사람 뿐만이 아니라 팀장과 부팀장도 같이 기획 특강 시간을 가졌는데 3명이서 기획을 하라는 교육기관의 의도로 보인다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 시스템 기획&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;766&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVKDZE/dJMcaiCaaHk/0hrQ4cjTHDjI5NKC5hEDe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVKDZE/dJMcaiCaaHk/0hrQ4cjTHDjI5NKC5hEDe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVKDZE/dJMcaiCaaHk/0hrQ4cjTHDjI5NKC5hEDe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVKDZE%2FdJMcaiCaaHk%2F0hrQ4cjTHDjI5NKC5hEDe1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;981&quot; height=&quot;766&quot; data-origin-width=&quot;981&quot; data-origin-height=&quot;766&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 게임 시스템에 대한 기획은 간단했다. 주로 게임 시작 전 설정이나 진행한 게임의 저장 및 불러오기에 대한 내용이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cMP2DD/dJMcahDgbIt/bkc1660IEYYum7KYy5BJn0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cMP2DD/dJMcahDgbIt/bkc1660IEYYum7KYy5BJn0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cMP2DD/dJMcahDgbIt/bkc1660IEYYum7KYy5BJn0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcMP2DD%2FdJMcahDgbIt%2Fbkc1660IEYYum7KYy5BJn0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;509&quot; height=&quot;562&quot; data-origin-width=&quot;509&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에는 UI에 대한 기획을 하기로 했는데 이는 내가 자세하게 기획하기로 했다. 우선 당장에 어디어 어떤 내용이 들어가야 할 지만 정해놓고 세부적인 디자인 기획은 내가 할 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 플레이어 능력치&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;741&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Y2mF0/dJMcagxAEYJ/mXzwZss04W9v16hWxVotyK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Y2mF0/dJMcagxAEYJ/mXzwZss04W9v16hWxVotyK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Y2mF0/dJMcagxAEYJ/mXzwZss04W9v16hWxVotyK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FY2mF0%2FdJMcagxAEYJ%2FmXzwZss04W9v16hWxVotyK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1209&quot; height=&quot;741&quot; data-origin-width=&quot;1209&quot; data-origin-height=&quot;741&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 단순하게 공격력에서 방어력을 뺀 값을 대미지로 하는 단순 계산으로는 후반 스테이지에서 밸런스 맞추기 어렵다는 것을 발견했다. 이에 대하여 여러가지 의논이 이어졌다. 방어력을 아예 없애고 공격력과 체력만 있는 시스템, 정직한 값이 아닌 최대 체력 비례 대미지를 주는 방식 등 여러가지 기획안이 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이에 대하여 나온 결론은 공격력과 체력은 특정 값을 가지되 스테이지나 플레이어의 상태에 따라 대미지가 가감되도록 만들었다. 그리고 이 플레이어의 상태 중 하나인 플레이어가 받는 피해를 일정 수치만큼 감소하는 것을 방어력이라고 정의했다. 나중에 몬스터 기획에서 자세하게 다룰 것이지만 몬스터의 능력치가 스테이지에 따라 퍼센트 비율로 강해지는 것도 이 쯤에서 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 스테이지를 진행할 수록 그에 대한 보상을 받으면서 강해져야 하는데 이 강해지는 요소에 강화 시스템을 넣어서 공격력 및 방어력이 상승하는 것도 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;694&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cg5bQu/dJMcacPwGjz/9WOsercVFU4d2RFiurQc91/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cg5bQu/dJMcacPwGjz/9WOsercVFU4d2RFiurQc91/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cg5bQu/dJMcacPwGjz/9WOsercVFU4d2RFiurQc91/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcg5bQu%2FdJMcacPwGjz%2F9WOsercVFU4d2RFiurQc91%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;504&quot; height=&quot;694&quot; data-origin-width=&quot;504&quot; data-origin-height=&quot;694&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하게 공격을 하고 받는 것만 있다면 소울라이크라고 할 수 없다. 기본적으로 적을 공격하는 방법에 다양성을 주어 일반적인 공격 뿐만이 아니라 스킬에 의한 강력한 공격도 기획했으며 적을 공격을 피하기 위해 막거나 회피하는 것도 기획했다. 다만 좋은 능력에는 대가도 필요한 법, 스테미나 및 마나라는 것이 존재하기 때문에 빠르게 또는 무한하게 사용할 수는 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것 외에도 록온, 상호작용 등 실제 사람이 할 법한 행동 중 게임에 어울리는 행동 다수를 기획했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 캐릭터 클래스&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;383&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zBsqh/dJMcac9LoHX/Sq8PLEqen32OcuiaYnjcy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zBsqh/dJMcac9LoHX/Sq8PLEqen32OcuiaYnjcy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zBsqh/dJMcac9LoHX/Sq8PLEqen32OcuiaYnjcy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzBsqh%2FdJMcac9LoHX%2FSq8PLEqen32OcuiaYnjcy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;672&quot; height=&quot;383&quot; data-origin-width=&quot;672&quot; data-origin-height=&quot;383&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 게임에서 플레이어의 캐릭터는 3개의 클래스가 있는데, 다른 게임에서 쉽게 떠올릴 수 있는 검사는 당연히 존재한다. 검과 방패를 가지고 묵직한 공격과 방어를 중시한 성기사 캐릭터로 기획했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 외에 만들기로 확적지은 도사라는 동양풍 캐릭터도 기획했다. 전체적으로 퓨전 판타지를 만들기로 했기 때문에 서양적인 배경에 동양적인 요소를 섞자는 것에서 출발한 기획이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나머지 하나는 일단 마법사로 기획했는데, 기획할 때는 확정 지었지만 추후 팀원에서 다른 의견이 나와 보류된 상태다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 몬스터&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;686&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wYQrp/dJMcah4nu2J/9sU156SPY7LskSk2oYfbFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wYQrp/dJMcah4nu2J/9sU156SPY7LskSk2oYfbFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wYQrp/dJMcah4nu2J/9sU156SPY7LskSk2oYfbFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FwYQrp%2FdJMcah4nu2J%2F9sU156SPY7LskSk2oYfbFk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;727&quot; height=&quot;686&quot; data-origin-width=&quot;727&quot; data-origin-height=&quot;686&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;흔히 등장하는 잡몹부터 최종 목표가 될 보스까지 다양하게 기획했다. 던전 하나마다 수 많은 잡몹, 소수의 엘리트 몬스터, 하나의 보스 몬스터를 배치할 예정이고 각각 능력치를 세부 조절했다. 잡몹은 3번 공격에 죽는다던지, 엘리트 몬스터는 추가 패턴을 만든다던지 등 게임의 재미를 위해 다양한 의견을 주고 받았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 플레이어 능력치 및 클래스를 고려하다보니 몬스터의 능력치를 정하는 것에 어려움이 뒤따랐다. 그래서 각자 계산기를 두들겨 보거나 MS오피스의 엑셀을 사용하는 등 최선을 다했다.&lt;/p&gt;</description>
      <category>기타 학습, 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/69</guid>
      <comments>https://park94128.tistory.com/69#entry69comment</comments>
      <pubDate>Tue, 3 Feb 2026 23:55:06 +0900</pubDate>
    </item>
    <item>
      <title>소울라이크 팀 프로젝트 Desecration : 설계 (1)</title>
      <link>https://park94128.tistory.com/68</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;최종 팀 프로젝트 제작 단계가 시작됐다. 이 프로젝트는 대형 프로젝트로 내가 지금까지 배운 모든 지식을 쏟아부어 완성을 해야 한다. 마지막 프로젝트를 함께 만들 팀을 찾다가 총원 10명인 조에 합류하여 프로젝트를 시작했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 역할 나누기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 10명이라서 각자 파트를 어떻게 나눌지가 관건이었다. 이전에 팀 프로젝트를 할 때는 팀원 별로 어떤 역할을 맡을지 정하는 데에 항상 어려움이 뒤따랐는데 다행이도 이번 팀장은 역할 분배를 잘 해줬다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;295&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3244n/dJMcaaKU68Z/VkPAIzQNIVnoeEcVVNIc00/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3244n/dJMcaaKU68Z/VkPAIzQNIVnoeEcVVNIc00/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3244n/dJMcaaKU68Z/VkPAIzQNIVnoeEcVVNIc00/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3244n%2FdJMcaaKU68Z%2FVkPAIzQNIVnoeEcVVNIc00%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1635&quot; height=&quot;295&quot; data-origin-width=&quot;1635&quot; data-origin-height=&quot;295&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 역할 분배안이다. 이후에 좀 내용이 변경되고 컨텐츠가 추가되긴 했지만 제법 깔끔하게 나누고 정리하는 모습이 좋았다. &lt;a href=&quot;https://miro.com/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Miro&lt;/a&gt;라는 사이트를 이때 처음 알았는데 팀장이 이 사이트를 이용하는 것이 인상깊었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;626&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CBqEC/dJMcaiIUCgO/qqMgxz4FCkuCSD1kXzKP7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CBqEC/dJMcaiIUCgO/qqMgxz4FCkuCSD1kXzKP7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CBqEC/dJMcaiIUCgO/qqMgxz4FCkuCSD1kXzKP7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCBqEC%2FdJMcaiIUCgO%2FqqMgxz4FCkuCSD1kXzKP7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1225&quot; height=&quot;626&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;626&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 중에서 내가 처음에 맡기로 한 것은 게임 시스템이었다. 초안에 적은 내용은 게임 데이터 저장 및 로드, 게임 설정, 게임 진행 관련 내용을 구현하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐릭터나 맵 디자인 등 다른 사람이 하는 것이 더 멋져보이지만 그냥 이 역할을 하는 것에 동의했다. 이것은 언리얼 엔진, 다양한 기능을 알고 있다면 물론 좋지만 자신이 맡은 분야에 최선을 다하여 협력하는 것이 중요하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 내용 정하기&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;724&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7YU4j/dJMcahiZNGx/JpLUd7WulYq1L6GUaqlk5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7YU4j/dJMcahiZNGx/JpLUd7WulYq1L6GUaqlk5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7YU4j/dJMcahiZNGx/JpLUd7WulYq1L6GUaqlk5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7YU4j%2FdJMcahiZNGx%2FJpLUd7WulYq1L6GUaqlk5K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1181&quot; height=&quot;724&quot; data-origin-width=&quot;1181&quot; data-origin-height=&quot;724&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장르는 무엇으로 할 것인가, 스토리는 어떻게 설정할 것인가를 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 팀원 모두가 생각나는대로 아무거나 적어봤다. 지금 나는 &lt;a href=&quot;https://www.monsterhunter.com/rise/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;몬스터헌터 라이즈&lt;/a&gt;를 즐기고 있기에 액션, 보스 사냥등의 의견을 냈다. 여러가지 의견이 난립하는 가운데 괜찮겠다 싶은 부분을 정리한 후 팀원의 투표로 결정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 내용은 소울라이크 게임을 만드는 것이고 스토리상 내용은 악마의 침공에 맞서는 주인공으로 결정됐다. &lt;a href=&quot;https://www.fromsoftware.jp/ww/?lng=ko&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;프롬 소프트웨어&lt;/a&gt;의 다크 소울, 엘든링과 비슷하게 만들어 보자는 취지의 의견이 강했으며 나를 포함한 모두가 이를 따르기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;572&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/J68SI/dJMcafZKBeC/qpRrv7SuhjDuArC8irQmY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/J68SI/dJMcafZKBeC/qpRrv7SuhjDuArC8irQmY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/J68SI/dJMcafZKBeC/qpRrv7SuhjDuArC8irQmY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJ68SI%2FdJMcafZKBeC%2FqpRrv7SuhjDuArC8irQmY0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;572&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;572&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;게임 내용도 정했다면 제목도 정해야겠지? 이번에도 각자 생각나는대로 어울리겠다 싶은 단어를 적었다. 그리고 동일하게 적당한 제목을 정해놓고 투표를 했다. 그 결과 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Desecration&lt;/b&gt;&lt;/span&gt;이라는 제목으로 정했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;● 게임 구조 설계&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;754&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Dze9A/dJMcaf6wViW/AscIzbqwKwDf24Als4ZAE1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Dze9A/dJMcaf6wViW/AscIzbqwKwDf24Als4ZAE1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Dze9A/dJMcaf6wViW/AscIzbqwKwDf24Als4ZAE1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDze9A%2FdJMcaf6wViW%2FAscIzbqwKwDf24Als4ZAE1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;754&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;754&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아직 초안 단계지만 게임을 어떤 구조로 만들지도 정했다. 큰 단계로 나누면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 게임 시작을 위해 타이틀 화면에 진입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 클래스와 이름을 정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 튜토리얼 진행후 마을 맵으로 진입한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 던전 클리어, 마을 귀환을 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 엔딩&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;막무가내로 아무거나 만들기 보다는 명확한 방향을 가지고 만드는 것이 중요하다. 그래서 위 내용은 추후 변경되겠지만 대단히 중요한 과정이다.&lt;/p&gt;</description>
      <category>기타 학습, 과제</category>
      <author>park94128</author>
      <guid isPermaLink="true">https://park94128.tistory.com/68</guid>
      <comments>https://park94128.tistory.com/68#entry68comment</comments>
      <pubDate>Mon, 2 Feb 2026 23:44:37 +0900</pubDate>
    </item>
  </channel>
</rss>