<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>gbleem 님의 블로그</title>
    <link>https://gbleem.tistory.com/</link>
    <description>gbleem 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Wed, 29 Jul 2026 05:04:30 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>gbleem</managingEditor>
    <item>
      <title>시간복잡도 줄이기 2) 하나 고정하기</title>
      <link>https://gbleem.tistory.com/189</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 하나 고정하고 나머지 값을 통해 찾기&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열에서 세가지 값의 합을 구하는 등의 작업을 할 때, 하나의 값을 고정시켜두고 나머지 값들만 투포인터를 통해서 계산하기&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 문제&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/2473&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/2473&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 문제는&quot;중간에서 만나기&quot;를 쓰면 추가 공간이 필요하여 메모리가 터진다.&lt;/li&gt;
&lt;li&gt;n = 5000이기 때문에 n^2은 25,000,000이 되고, 여기서 long long값과 인덱스 2개를 int로 저장하면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;25,000,000 * (8 + 4 + 4)&amp;nbsp; = 400,000,000&amp;nbsp;&lt;/li&gt;
&lt;li&gt;400MB 메모리 필요하므로 메모리 초과가 될 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1759218594793&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
#include &amp;lt;climits&amp;gt;
using namespace std;

int n;
vector&amp;lt;long long&amp;gt; vec;
long long minValue = LLONG_MAX;
vector&amp;lt;long long&amp;gt; answer;

void Print()
{
	sort(answer.begin(), answer.end());

	for (const auto&amp;amp; a : answer)
	{
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot; &quot;;
	}
}

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; n;

	for (int i = 0; i &amp;lt; n; ++i)
	{
		int num;
		cin &amp;gt;&amp;gt; num;
		vec.push_back(num);
	}

	sort(vec.begin(), vec.end());

	for (int i = 0; i &amp;lt; n - 2; ++i)
	{
		int l = i + 1;
		int r = n - 1;

		while (l &amp;lt; r)
		{
			long long temp = vec[i] + vec[l] + vec[r];

			if (abs(temp) &amp;lt; minValue)
			{
				minValue = abs(temp);

				answer.clear();
				answer.push_back(vec[i]);
				answer.push_back(vec[l]);
				answer.push_back(vec[r]);
			}

			if (temp &amp;lt; 0)
				l++;
			else if (temp &amp;gt; 0)
				r--;
			else
			{
				Print();
				return 0;
			}
		}
	}
	Print();
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;</description>
      <category>알고리즘(코딩테스트)</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/189</guid>
      <comments>https://gbleem.tistory.com/189#entry189comment</comments>
      <pubDate>Tue, 30 Sep 2025 16:49:59 +0900</pubDate>
    </item>
    <item>
      <title>시간복잡도 줄이기 1) 중간에서 만나기 (+이분탐색)</title>
      <link>https://gbleem.tistory.com/188</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 중간에서 만나기&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;알고리즘의 전체 탐색 범위를 반으로 나누어, 양쪽에서 동시에 탐색을 시작해 중간에서 만나는 방식으로 시간 복잡도를 줄이는 기법&lt;/li&gt;
&lt;li&gt;예시
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;00000000~99999999 비밀번호 풀기&lt;br /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;앞의 4자리와 뒤의 4자리를 따로 계산한 결과 A와 B를 합쳐서 정답이 되는지 확인하기&lt;/li&gt;
&lt;li&gt;10^8의 연산을 10^4 * 2 의 연산으로 줄여준다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A+B+C+D = 0 찾기
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;A+B = -(C+D) 로 계산하기&lt;/li&gt;
&lt;li&gt;모든 조합의 합을 계산 : n^2&lt;/li&gt;
&lt;li&gt;C+D를 정렬한 후 A+B를 순환하면서 이분탐색을 하기 : O(N^2logN)
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;N^2사이즈의 A+B 배열을 돌면서 O(N^2)&lt;/li&gt;
&lt;li&gt;N^2사이즈의 배열을 이진탐색 O(logN^2) 하기 -&amp;gt; O(N^2logN^2)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;!중요!
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;메모리 제한을 항상 생각하고 쓰기&lt;/li&gt;
&lt;li&gt;이 방식은 메모리를 많이 사용하게 된다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 문제&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://www.acmicpc.net/problem/7453&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://www.acmicpc.net/problem/7453&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;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1758641388039&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;algorithm&amp;gt;
using namespace std;

int n;
vector&amp;lt;int&amp;gt; board[4];
vector&amp;lt;int&amp;gt; sumA;
vector&amp;lt;int&amp;gt; sumB;
long long answer = 0;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; n;

	for (int i = 0; i &amp;lt; n; ++i)
	{
		int a, b, c, d;
		cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c &amp;gt;&amp;gt; d;

		board[0].push_back(a);
		board[1].push_back(b);
		board[2].push_back(c);
		board[3].push_back(d);
	}

	for (int i = 0; i &amp;lt; n; ++i)
	{
		for (int j = 0; j &amp;lt; n; ++j)
		{
			sumA.push_back(board[0][i] + board[1][j]);
			sumB.push_back(board[2][i] + board[3][j]);
		}
	}

	sort(sumB.begin(), sumB.end());

	for (const auto&amp;amp; a : sumA)
	{
		int target = -a;

		int st = lower_bound(sumB.begin(), sumB.end(), target) - sumB.begin();
		int en = upper_bound(sumB.begin(), sumB.end(), target) - sumB.begin();

		answer += (long long)(en - st);
	}

	cout &amp;lt;&amp;lt; answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;</description>
      <category>알고리즘(코딩테스트)</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/188</guid>
      <comments>https://gbleem.tistory.com/188#entry188comment</comments>
      <pubDate>Wed, 24 Sep 2025 00:29:53 +0900</pubDate>
    </item>
    <item>
      <title>트라이</title>
      <link>https://gbleem.tistory.com/187</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개념&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;문자열을 효율적으로 처리하기 위한 트리 자료구조&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;삽입 삭제 탐색에 있어서 O(|S|) 시간이 소요&lt;/li&gt;
&lt;li&gt;그러나 메모리는 최대 4*글자의 종류 만큼 차지하므로 메모리 효율은 좋지 않다.&lt;/li&gt;
&lt;li&gt;이론적인 시간 복잡도와 달리 실제로는 트라이가 해시나 이진 검색 트리에 비해 느리다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그러나 이 특징을 사용해야하는 문제가 있다.&lt;/li&gt;
&lt;li&gt;문자열의 접두사 검색 등과 같은 느낌&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;310&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw3sVb/btsQpxUacU3/1eTHDPyDmCVA8cVK4DmCmk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw3sVb/btsQpxUacU3/1eTHDPyDmCVA8cVK4DmCmk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw3sVb/btsQpxUacU3/1eTHDPyDmCVA8cVK4DmCmk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw3sVb%2FbtsQpxUacU3%2F1eTHDPyDmCVA8cVK4DmCmk%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;591&quot; height=&quot;310&quot; data-origin-width=&quot;591&quot; data-origin-height=&quot;310&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 구현&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1757324267360&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

const int ROOT = 1;
int unused = 2; //새로 추가할 정점
const int MX = 10000 * 500 + 5; //최대 등장 가능한 글자 수 -&amp;gt; 최대 500인 문자열이 10000개 들어온 경우
bool chk[MX];
int nxt[MX][26]; // 각 정점에서 자식 정점의 번호 (가능한 문자가 소문자인 경우)

int c2i(char c)
{
	return c - 'A';
}

void insert(string&amp;amp; s)
{
	int cur = ROOT;
	for (auto c : s)
	{
		if (nxt[cur][c2i(c)] == -1)
			nxt[cur][c2i(c)] = unused++;
		cur = nxt[cur][c2i(c)];
	}
	chk[cur] = true;
}

bool find(string&amp;amp; s)
{
	int cur = ROOT;
	for (auto c : s)
	{
		if (nxt[cur][c2i(c)] == -1)
			return false;
		cur = nxt[cur][c2i(c)];
	}
	return chk[cur];
}

void erase(string&amp;amp; s)
{
	int cur = ROOT;
	for (auto c : s)
	{
		if (nxt[cur][c2i(c)] == -1)
			return;
		cur = nxt[cur][c2i(c)];
	}
	chk[cur] = false;
}

int main()
{
	for (int i = 0; i &amp;lt; MX; ++i)
		fill(nxt[i], nxt[i] + 26, -1);

	///
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 문제&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boj 14426&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&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://blog.encrypted.gg/1059&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.encrypted.gg/1059&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757337654749&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실전 알고리즘] 0x1F강 - 트라이&quot; data-og-description=&quot;안녕하세요, 드디어 마지막 강이라니 가슴이 웅장해집니다. 마지막인만큼 난이도도 끝판왕일 수 있지만 개인적으로는 꽤 좋아하는 알고리즘 중 하나여서 같이 즐거운 마음으로 배워봅시다. 아&quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1059&quot; data-og-url=&quot;https://blog.encrypted.gg/1059&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/diKOuG/hyZGavYX2k/HDLkSZFOJPvklssdQg4vA1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bhVzCN/hyZGgiEAkf/pBhQpB7f01QIJdSDcOeJN1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/kt3Ac/hyZGi1P2nA/kJKatG92u6gyRRuKKwqLkk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1059&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1059&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/diKOuG/hyZGavYX2k/HDLkSZFOJPvklssdQg4vA1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bhVzCN/hyZGgiEAkf/pBhQpB7f01QIJdSDcOeJN1/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/kt3Ac/hyZGi1P2nA/kJKatG92u6gyRRuKKwqLkk/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[실전 알고리즘] 0x1F강 - 트라이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 드디어 마지막 강이라니 가슴이 웅장해집니다. 마지막인만큼 난이도도 끝판왕일 수 있지만 개인적으로는 꽤 좋아하는 알고리즘 중 하나여서 같이 즐거운 마음으로 배워봅시다. 아&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘(코딩테스트)</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/187</guid>
      <comments>https://gbleem.tistory.com/187#entry187comment</comments>
      <pubDate>Mon, 8 Sep 2025 18:41:51 +0900</pubDate>
    </item>
    <item>
      <title>위상정렬</title>
      <link>https://gbleem.tistory.com/186</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 개념&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;방향 그래프에서 간선으로 주어진 정점 간 선후관계를 위배하지 않도록 나열하는 정렬을 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어, 교과 이수 제도가 있다.&lt;/li&gt;
&lt;li&gt;특정 과목을 듣기 위해서 선수과목이 있는 그런 형태를 말한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주의할 점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;같은 그래프에서 여러 가지의 위상 정렬이 있을 수는 있다.&lt;/li&gt;
&lt;li&gt;단, 그래프의 순환이 있는 경우는 위상정렬이 될 수 없다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;위상 정렬은 사이클이 없는 방향 그래프(DAG) 에서만 정의된다.&lt;/li&gt;
&lt;/ul&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 구현&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;참고&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;indegree : 자신에게 들어오는 간선의 수&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;동작 방식&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 간선을 순환하여 indegree 테이블 채우기&lt;/li&gt;
&lt;li&gt;indegree가 0인 정점들을 queue에 넣기&lt;/li&gt;
&lt;li&gt;queue에서 pop을 통해 위상 정렬 결과에 추가&lt;/li&gt;
&lt;li&gt;해당 정점으로부터 연결된 모든 정점의 indegree값을 1감소, 만약 이 때 indegree가 0이라면 정점을 queue에 추가&lt;/li&gt;
&lt;li&gt;queue가 빌 때 까지 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&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;605&quot; data-origin-height=&quot;688&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/uugow/btsQoqHcGVG/uRqajE12aI5azR3fkNeJw1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/uugow/btsQoqHcGVG/uRqajE12aI5azR3fkNeJw1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/uugow/btsQoqHcGVG/uRqajE12aI5azR3fkNeJw1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fuugow%2FbtsQoqHcGVG%2FuRqajE12aI5azR3fkNeJw1%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;605&quot; height=&quot;688&quot; data-origin-width=&quot;605&quot; data-origin-height=&quot;688&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;코드&lt;/p&gt;
&lt;pre id=&quot;code_1757173547308&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

vector&amp;lt;int&amp;gt; adj[10];
int indegree[10];
int n;

int main()
{
	queue&amp;lt;int&amp;gt; q;
	vector&amp;lt;int&amp;gt; result;

	for (int i = 1; i &amp;lt;= n; ++i)
	{
		if (indegree[i] == 0)
			q.push(i);
	}
	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		result.push_back(cur);
		
		for (int nxt : adj[cur])
		{
			indegree[nxt]--;
			if (indegree[nxt] == 0)
				q.push(nxt);
		}
	}
	if (result.size() != n)
		cout &amp;lt;&amp;lt; &quot;cycle&quot;;
	else
	{
		for (const auto&amp;amp; r : result)
		{
			cout &amp;lt;&amp;lt; r &amp;lt;&amp;lt; &quot; &quot;;
		}
	}
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 문제&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boj 2252 : 기본 위상정렬&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1757173985335&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
using namespace std;

int n, m;
vector&amp;lt;int&amp;gt; adj[32'002];
int deg[32'002];
vector&amp;lt;int&amp;gt; answer;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; m;


	while (m--)
	{
		int a, b;
		cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b;
		adj[a].push_back(b); //a가 b앞
		deg[b]++; //b는 a가 필요해
	}
	queue&amp;lt;int&amp;gt;q;
	for (int i = 1; i &amp;lt;= n; ++i)
	{
		if (deg[i] == 0)
			q.push(i);
	}

	while (!q.empty())
	{
		int cur = q.front();
		q.pop();
		answer.push_back(cur);

		for (int nxt : adj[cur])
		{
			deg[nxt]--;
			if (deg[nxt] == 0)
				q.push(nxt);
		}
	}

	for (const auto&amp;amp; a : answer)
	{
		cout &amp;lt;&amp;lt; a &amp;lt;&amp;lt; &quot; &quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;boj 1005 : 위상정렬 + DP&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1757177506750&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;set&amp;gt;
using namespace std;

int t, n, k, w;
vector&amp;lt;int&amp;gt; adj[1002];
int deg[1002];
int D[1002];
int dp[1002];
set&amp;lt;int&amp;gt; ans;
int answer = 0;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; t;

	while (t--)
	{
		for (int i = 0; i &amp;lt; 1002; ++i)
			adj[i].clear();
		fill(D, D + 1002, 0);
		fill(deg, deg + 1002, 0);
		fill(dp, dp + 1002, 0);
		answer = 0;

		cin &amp;gt;&amp;gt; n &amp;gt;&amp;gt; k;
		
		for (int i = 1; i &amp;lt;= n; ++i)
		{
			cin &amp;gt;&amp;gt; D[i];
		}

		for (int i = 0; i &amp;lt; k; ++i)
		{
			int x, y;
			cin &amp;gt;&amp;gt; x &amp;gt;&amp;gt; y;
			adj[x].push_back(y);
			deg[y]++;
		}
		cin &amp;gt;&amp;gt; w;

		queue&amp;lt;int&amp;gt; q;

		for (int i = 1; i &amp;lt;= n; ++i)
		{
			if (deg[i] == 0)
			{
				q.push(i);
				dp[i] = D[i];
			}
		}

		while (!q.empty())
		{
			int cur = q.front();
			q.pop();

			for (int nxt : adj[cur])
			{
				dp[nxt] = max(dp[nxt], dp[cur] + D[nxt]);
				deg[nxt]--;
				if (deg[nxt] == 0)
					q.push(nxt);
			}
		}

		cout &amp;lt;&amp;lt; dp[w] &amp;lt;&amp;lt; &quot;\n&quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&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://blog.encrypted.gg/1020&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://blog.encrypted.gg/1020&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1757174210097&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[실전 알고리즘] 0x1A강 - 위상 정렬&quot; data-og-description=&quot;안녕하세요, 이번 시간에는 위상 정렬을 다뤄보도록 하겠습니다. 위상 정렬이 무엇인지도 소개해드릴거고 구현과 연습 문제도 다룰 예정입니다. 위상 정렬의 본격적인 정의를 배워보기 전에 실&quot; data-og-host=&quot;blog.encrypted.gg&quot; data-og-source-url=&quot;https://blog.encrypted.gg/1020&quot; data-og-url=&quot;https://blog.encrypted.gg/1020&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/TIZRo/hyZIUELzgU/ncXe7K6fvjNsKq15oHrq7k/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bpyTKh/hyZGXo4poe/a7UWllpAGv1RGoBZTHaiy0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/dTbE6V/hyZF8dBgc3/ARNbXtCXnWrtB96rqybai0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080&quot;&gt;&lt;a href=&quot;https://blog.encrypted.gg/1020&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://blog.encrypted.gg/1020&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/TIZRo/hyZIUELzgU/ncXe7K6fvjNsKq15oHrq7k/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/bpyTKh/hyZGXo4poe/a7UWllpAGv1RGoBZTHaiy0/img.png?width=800&amp;amp;height=450&amp;amp;face=0_0_800_450,https://scrap.kakaocdn.net/dn/dTbE6V/hyZF8dBgc3/ARNbXtCXnWrtB96rqybai0/img.png?width=1920&amp;amp;height=1080&amp;amp;face=0_0_1920_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[실전 알고리즘] 0x1A강 - 위상 정렬&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;안녕하세요, 이번 시간에는 위상 정렬을 다뤄보도록 하겠습니다. 위상 정렬이 무엇인지도 소개해드릴거고 구현과 연습 문제도 다룰 예정입니다. 위상 정렬의 본격적인 정의를 배워보기 전에 실&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;blog.encrypted.gg&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&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>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/186</guid>
      <comments>https://gbleem.tistory.com/186#entry186comment</comments>
      <pubDate>Sun, 7 Sep 2025 01:52:06 +0900</pubDate>
    </item>
    <item>
      <title>플로이드</title>
      <link>https://gbleem.tistory.com/185</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 플로이드 알고리즘&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 정점 쌍 사이의 최단 거리를 구하는 알고리즘&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;방향 그래프나 무방향 그래프 모두 상관없음&lt;/li&gt;
&lt;li&gt;간선이 음수여도 동작함, 단 음수 사이클은 없어야 함&lt;/li&gt;
&lt;li&gt;시간 복잡도는 O(V^3)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 쓸까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예를 들어 정점이 100개 정도일 때 쓸만함 -&amp;gt; 코드가 쉬우니까&lt;/li&gt;
&lt;li&gt;모든 정점에 대한 거리를 알고 싶을 때&amp;nbsp;&lt;/li&gt;
&lt;li&gt;경유지를 고려해야 할 때 (다익스트라 같은 경우는 A-&amp;gt;Z 까지의 최단 거리만 구하기 때문)&lt;/li&gt;
&lt;li&gt;또한 음수 가중치가 있는 경우..&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 동작 방식&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 테이블을 만들어 두고, 각 정점에서 인접한 정점까지의 거리만 업데이트하여 최단 거리 테이블을 만든다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bTNtMA/btsPMBWYI8j/PonrkNG56rWPtXJH8mzXL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bTNtMA/btsPMBWYI8j/PonrkNG56rWPtXJH8mzXL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bTNtMA/btsPMBWYI8j/PonrkNG56rWPtXJH8mzXL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbTNtMA%2FbtsPMBWYI8j%2FPonrkNG56rWPtXJH8mzXL1%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;529&quot; height=&quot;281&quot; data-origin-width=&quot;806&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;지금까지 적어둔 값은 어떠한 정점을 거치지 않았을 때의 모습이다. 이제부터는 특정 정점을 거쳐가면서 더 짧은 경로가 있다면 업데이트 해주면 된다.&lt;/li&gt;
&lt;li&gt;아래 모습은 정점 1은 거쳐가는 경우 업데이트된 최단 거리 테이블의 모습이다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;373&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2ePdH/btsPNIHMr5l/UVAi949XgHWcuaZUQEdJBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2ePdH/btsPNIHMr5l/UVAi949XgHWcuaZUQEdJBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2ePdH/btsPNIHMr5l/UVAi949XgHWcuaZUQEdJBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2ePdH%2FbtsPNIHMr5l%2FUVAi949XgHWcuaZUQEdJBK%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;569&quot; height=&quot;271&quot; data-origin-width=&quot;782&quot; data-origin-height=&quot;373&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위에서 한 방식을 모든 정점을 다 돌면서 수행하면 된다.&lt;/li&gt;
&lt;li&gt;코드로 생각해보면 업데이트 하는 부분에 있어서는 dp 방식과 유사한 비교를 하게 되는데, s에서 t로 가는동안 n을 지난다고 했을때 아래와 같은 코드를 통해 업데이트 하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1754637143842&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;DP[s][t] = min(DP[s][t], DP[s][n] + DP[n][t]);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 정점에 대해서 V 단계에 걸쳐서 갱신이 이루어지고, 각 단계마다 V^2개의 DP 값을 비교하기 때문에 시간 복잡도는 O(V^3) 이 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 코드&lt;/h3&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;두 거리 덧셈에 있어서 int overflow 방지를 위해서 INF값 설정하기&lt;/li&gt;
&lt;li&gt;플로이드는 대충 정점 1000개까지는 해볼만 하다...&lt;/li&gt;
&lt;li&gt;비교하는 부분에서 min 함수를 쓰기보다 if문으로 체크하는 방식으로 바꾸면 좀 더 효율적이다
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이유는 대입을 매번 하는 것을 막을 수 있기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;boj 11404 풀이&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1754638346905&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
#include &amp;lt;climits&amp;gt;
using namespace std;

int n, m;
int arr[102][102];
const int INF = 0x3f3f3f3f;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; n;
	cin &amp;gt;&amp;gt; m;
	
	for (int i = 1; i &amp;lt;= n; ++i)
		fill(arr[i], arr[i] + 1 + n, INF);

	for (int i = 1; i &amp;lt;= n; ++i)
	{
		arr[i][i] = 0;
	}

	for (int i = 0; i &amp;lt; m; ++i)
	{
		int a, b, c;
		cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
		arr[a][b] = min(arr[a][b], c);
	}

	for (int i = 1; i &amp;lt;= n; ++i)
	{
		for (int j = 1; j &amp;lt;= n; ++j)
		{
			for (int k = 1; k &amp;lt;= n; ++k)
			{
				arr[j][k] = min(arr[j][k], arr[j][i] + arr[i][k]);
			}
		}
	}

	for (int i = 1; i &amp;lt;= n; ++i)
	{
		for (int j = 1; j &amp;lt;= n; ++j)
		{
			if (arr[i][j] == INF)
				cout &amp;lt;&amp;lt; &quot;0 &quot;;
			else
				cout &amp;lt;&amp;lt; arr[i][j] &amp;lt;&amp;lt; &quot; &quot;;
		}
		cout &amp;lt;&amp;lt; &quot;\n&quot;;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가) 사이클 구하기 (boj 1956)&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1757401352701&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

int board[402][402];
const int INF = 0x3f3f3f3f;
int v, e;
int answer = INF;

int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);

	cin &amp;gt;&amp;gt; v &amp;gt;&amp;gt; e;
	for (int i = 1; i &amp;lt;= v; ++i)
		fill(board[i] + 1, board[i] + 1 + v, INF);

	for (int i = 1; i &amp;lt;= v; ++i)
		board[i][i] = 0;

	for (int i = 0; i &amp;lt; e; ++i)
	{
		int a, b, c;
		cin &amp;gt;&amp;gt; a &amp;gt;&amp;gt; b &amp;gt;&amp;gt; c;
		board[a][b] = c;
	}

	for (int i = 1; i &amp;lt;= v; ++i)
	{
		for (int j = 1; j &amp;lt;= v; ++j)
		{
			for (int k = 1; k &amp;lt;= v; ++k)
			{
				board[j][k] = min(board[j][k], board[j][i] + board[i][k]);
			}
		}
	}

	//사이클 계산
	for (int i = 1; i &amp;lt;= v; ++i)
	{
		for (int j = i + 1; j &amp;lt;= v; ++j)
		{
			answer = min(answer, board[i][j] + board[j][i]);
		}
	}

	if (answer == INF)
		cout &amp;lt;&amp;lt; -1;
	else
		cout &amp;lt;&amp;lt; answer;
}&lt;/code&gt;&lt;/pre&gt;</description>
      <category>알고리즘(코딩테스트)</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/185</guid>
      <comments>https://gbleem.tistory.com/185#entry185comment</comments>
      <pubDate>Fri, 8 Aug 2025 16:34:14 +0900</pubDate>
    </item>
    <item>
      <title>UE5 Issues - 파괴되는 Mesh</title>
      <link>https://gbleem.tistory.com/184</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;보스 레벨에서 플레이어들이 사용하는 다리 Mesh가 있는데, 이것을 파괴하는 시스템을 개발하게 되었다.&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;1. 고민&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 해당 기획을 듣고 여러 방식을 떠올리게 되었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Chaos&lt;/li&gt;
&lt;li&gt;Physics Constraint&lt;/li&gt;
&lt;li&gt;etc...&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발하기 전 가장 중요하게 생각했던 부분은 &lt;b&gt;&quot;부서진 이후의 결과물&quot;&lt;/b&gt;에 대한 고민이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;파괴된 이후의 Mesh가 사라지도록 하여 게임에 영향을 미치지 않도록 할 것인가&lt;/li&gt;
&lt;li&gt;파괴된 이후의 Mesh가 이후 게임에 영향을 미치도록 할 것인가&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재 팀에는 배경 모델링을 담당해주시는 분이 있었기 때문에 카오스로 부수기보다는 부서져 있는 에셋 자체를 쓰는 것이 더 좋다는 생각이 들었다.&lt;/li&gt;
&lt;li&gt;다리 메시가 뼈모양의 다리였고, 이 경우 뼈 마디마디가 부서지는 것이 자연스럽다는 생각이 들었기 때문이다.&lt;/li&gt;
&lt;li&gt;돌이나 기둥 같은 에셋이었다면, 매번 다르게 부서지는 것이 더 그럴듯 해 보이겠지만...&lt;/li&gt;
&lt;/ul&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;1021&quot; data-origin-height=&quot;407&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB1a5L/btsOHZd0qb5/lifxe8xhlL8dz61jEH3sJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB1a5L/btsOHZd0qb5/lifxe8xhlL8dz61jEH3sJ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB1a5L/btsOHZd0qb5/lifxe8xhlL8dz61jEH3sJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB1a5L%2FbtsOHZd0qb5%2Flifxe8xhlL8dz61jEH3sJ0%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;1021&quot; height=&quot;407&quot; data-origin-width=&quot;1021&quot; data-origin-height=&quot;407&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;1057&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/47gAK/btsOH6xeRgH/KjL1cAQkZ9l82xekonkJk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/47gAK/btsOH6xeRgH/KjL1cAQkZ9l82xekonkJk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/47gAK/btsOH6xeRgH/KjL1cAQkZ9l82xekonkJk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F47gAK%2FbtsOH6xeRgH%2FKjL1cAQkZ9l82xekonkJk0%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;1057&quot; height=&quot;140&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 여러 시도들&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다리 메시 에셋을 받기 전, 카오스 시스템에 대한 테스트를 해보았다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;가장 큰 문제점은 부서진 이후의 위치가 동기화 되지 않는다는 문제가 있었다.&lt;/li&gt;
&lt;li&gt;이걸 해결하는 방법을 찾아보긴 했지만, Chaos 시스템에 익숙하지 않은 상태에서 너무 오랜 시간을 쏟아버리는 것은 좋지 않다고 판단했다.&lt;/li&gt;
&lt;li&gt;특히 위치 동기화가 제대로 되지 않은 경우, 부서진 이후 게임 내에서 사라지지 않도록 구현하게 된다면 문제가 되기 때문에 다른 방법을 선택하게 되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;598&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cv3GCM/btsOJH4ezuR/HPqaQdeAkiq3gQ0ohoG2K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cv3GCM/btsOJH4ezuR/HPqaQdeAkiq3gQ0ohoG2K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cv3GCM/btsOJH4ezuR/HPqaQdeAkiq3gQ0ohoG2K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcv3GCM%2FbtsOJH4ezuR%2FHPqaQdeAkiq3gQ0ohoG2K0%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;1632&quot; height=&quot;598&quot; data-origin-width=&quot;1632&quot; data-origin-height=&quot;598&quot;/&gt;&lt;/span&gt;&lt;/figure&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;새롭게 시도한 방식이 Physics Constraint를 사용한 내용이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언리얼이 가지고 있는 기능으로 로프 시스템이나 흔들다리와 같은 시스템을 개발할 때 주로 사용하는 컴포넌트이다&lt;/li&gt;
&lt;li&gt;아래 사진처럼 각 mesh를 연결하여 두고, 특정 힘을 가했을 때 해당 연결을 끊어주는 식으로 구현하려고 했다.&lt;/li&gt;
&lt;li&gt;그러나 모든 제한조건을 잠궈두어도, 약간 용수철처럼 통통거리는 느낌이 나서 뼈 모양의 다리와는 어울리지 않는다고 느껴졌고,&lt;/li&gt;
&lt;li&gt;이 방식 역시 새롭게 사용하는 방식이다 보니 시간내에 원하는 퀄리티의 결과물을 내기 어려울 것으로 판단되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;238&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AV3zL/btsOHDWCstP/IwnV1S8K2Y9xW99sqnLOs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AV3zL/btsOHDWCstP/IwnV1S8K2Y9xW99sqnLOs0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AV3zL/btsOHDWCstP/IwnV1S8K2Y9xW99sqnLOs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAV3zL%2FbtsOHDWCstP%2FIwnV1S8K2Y9xW99sqnLOs0%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;238&quot; height=&quot;29&quot; data-origin-width=&quot;238&quot; data-origin-height=&quot;29&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;788&quot; data-origin-height=&quot;388&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b75T9y/btsOG8CCBLh/PTgFvHcalFmNtmh9OzGsl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b75T9y/btsOG8CCBLh/PTgFvHcalFmNtmh9OzGsl1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b75T9y/btsOG8CCBLh/PTgFvHcalFmNtmh9OzGsl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb75T9y%2FbtsOG8CCBLh%2FPTgFvHcalFmNtmh9OzGsl1%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;788&quot; height=&quot;388&quot; data-origin-width=&quot;788&quot; data-origin-height=&quot;388&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다리를 구성하는 메시 한 조각을 각각 액터로 만든 후, 조각 액터를 모두 가지는 부모 액터로 다리를 구성하는 방식이다.&lt;/li&gt;
&lt;li&gt;처음에는 조각 액터들을 simulate physics를 false로 만들어두고, 이후에 true로 변경하여 부서지도록 구현을 하게 되었다.&lt;/li&gt;
&lt;li&gt;이 방식으로 구현하면, Actor의 리플리케이션을 통해 모든 클라이언트들에게 같은 모습으로 보여줄 수 있기 때문에, 처음한 고민에 있어서도 문제가 없기에 이 방식을 택하게 되었다.&lt;/li&gt;
&lt;/ul&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;h2 data-ke-size=&quot;size26&quot;&gt;3. 개발&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 각 조각 단위의 액터를 만들었다.&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 주의할 점은 각 메시가 너무 가볍게 날라가는 느낌이 아니라 묵직하게 무너져 내리는 느낌을 내고 싶었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 느낌을 내기 위해서 메시의 무게를 무겁게 만드는 방법을 사용하였다.&lt;/li&gt;
&lt;li&gt;또한 데미지를 가한 방향을 무시하여, 무조건 아래쪽으로만 내려가도록 구현하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750318031351&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;AGS_BridgePiece::AGS_BridgePiece()
{
	...
    	MeshComponent-&amp;gt;SetSimulatePhysics(false);
	MeshComponent-&amp;gt;SetMassOverrideInKg(NAME_None, 100000.0f, true);
}

//데미지 입히기 및 파괴되면 시뮬레이션 시작하는 함수
void AGS_BridgePiece::BrokeBridge(float InDamage)
{
	if (HasAuthority() &amp;amp;&amp;amp; !bIsDestroyed)
	{
		CurrentHealth -= InDamage;
		
		if (CurrentHealth &amp;lt;= KINDA_SMALL_NUMBER)
		{
			MeshComponent-&amp;gt;SetSimulatePhysics(true);
			MeshComponent-&amp;gt;bApplyImpulseOnDamage = false;
			MeshComponent-&amp;gt;SetCollisionEnabled(ECollisionEnabled::PhysicsOnly);
			
			...
		}
	}
}

//처음에 StaticMesh를 세팅하는 함수
void AGS_BridgePiece::SetBridgeMesh(UStaticMesh* InMesh, float InValue)
{
	if (MeshComponent &amp;amp;&amp;amp; InMesh)
	{
		MeshComponent-&amp;gt;SetStaticMesh(InMesh);
		MaxHealth = InValue;
	}
}&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;이후로 Piece를 포함하는 Bridge 액터를 만들었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;또한 각 조각마다 체력을 가지고 있어, 일정 데미지 이상 맞은 경우 파괴되는 시스템으로 구현하고 싶었다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 가운데 부분에 있어서는 체력을 낮게 설정하여, 쉽게 부서지고 가장자리로 갈수록 체력이 높아서 잘 부서지지 않도록 구현하였다.&lt;/li&gt;
&lt;li&gt;공중에 가운데 쪽 메시만 떠잇는 모습은 플레이어들에게 이상하게 느껴질 수 있기 때문&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Bridge 액터에서는 StaticMesh의 갯수만큼 BridgePiece를 만들어서 ChildActor로 붙여주는 식으로 구성하였다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가적으로 양 끝에 존재하는 다리 조각은 부서지지 않고 남아있도록 구현하였다.&lt;/li&gt;
&lt;li&gt;다리가 다 부서져서 사라지는것 보다 &quot;원래 이곳이 다리가 있었던 자리다&quot; 라는 것을 알려주기 위해 부서지지 않게 하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750318065517&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//GS_Bridge.h
//부서지지 않는 액터
UPROPERTY(EditAnywhere)
TObjectPtr&amp;lt;UStaticMeshComponent&amp;gt; NotBrokenPieces1;
...

//GS_Bridge.cpp
void AGS_Bridge::SetUpBridge()
{
	if (HasAuthority())
	{
		HalfBridgeMeshSize = BridgeMeshAssets.Num() / 2;
		
		for (int32 i = 0; i &amp;lt; BridgeMeshAssets.Num(); ++i)
		{
			UStaticMesh* CurrentMesh = BridgeMeshAssets[i];
			if (!CurrentMesh)
			{
				continue;
			}
			
			UChildActorComponent* NewPieceComponent = NewObject&amp;lt;UChildActorComponent&amp;gt;(this);
			if (NewPieceComponent)
			{
				NewPieceComponent-&amp;gt;SetChildActorClass(AGS_BridgePiece::StaticClass());
				NewPieceComponent-&amp;gt;AttachToComponent(RootComponent,FAttachmentTransformRules::KeepRelativeTransform);
				NewPieceComponent-&amp;gt;RegisterComponent();

				AGS_BridgePiece* PieceActor = Cast&amp;lt;AGS_BridgePiece&amp;gt;(NewPieceComponent-&amp;gt;GetChildActor());
				if (PieceActor)
				{
                			//가운데 인덱스의 가중치를 크게 만들기
					float weight = 1.f - (abs(i - HalfBridgeMeshSize) / (float)HalfBridgeMeshSize);
					
					FTransform Transform = FTransform::Identity;
					PieceActor-&amp;gt;SetActorRelativeTransform(Transform);
					PieceActor-&amp;gt;SetBridgeMesh(CurrentMesh, weight * 100.f + 100.f);
				}
			}
		}
	}
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 트러블 슈팅&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리플리케이션 관련 문제가 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GS_BridgePiece의 생성자에서 replicated 설정만 해주었더니 아래와 같이 메시가 보이지 않는 문제가 발생했다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5j6oL/btsOJpwkz41/kcUwSvOPONSCabzVMUSwRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5j6oL/btsOJpwkz41/kcUwSvOPONSCabzVMUSwRK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5j6oL/btsOJpwkz41/kcUwSvOPONSCabzVMUSwRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5j6oL%2FbtsOJpwkz41%2FkcUwSvOPONSCabzVMUSwRK%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;924&quot; height=&quot;558&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원인을 찾아보니 액터 자체는 리플리케이트 되지만, 메시 컴포넌트의 변경사항은 클라이언트에게 전송하지 않기 때문이었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언리얼 엔진은 성능상의 이슈로 모든 컴포넌트를 복제하지 않기 때문에 필요한 부분에 있어서는 아래와 같이 직접 리플리케이트 세팅을 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750322993139&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;MeshComponent-&amp;gt;SetIsReplicated(true);&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;참고) GS_Bridge는 리플리케이트 하지 않음&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&amp;lt; 패키징 시 오류 발생 &amp;gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에디터로 테스트할 때에는 문제가 없었지만, 패키징 시 아래와 같은 에러와 함께 패키징에 실패하였다.&lt;/li&gt;
&lt;li&gt;해당 원인 분석 결과 &quot;생성자에서 물리 관련된 속성을 바꾸려고 할 때 생기는 문제&quot; 였다.&lt;/li&gt;
&lt;li&gt;CDO라는 것은 에디터에서 속성 패널등을 표시하기 위한 용도로 사용되는 것인데, CDO가 생성될 때는 게임 월드나 GEngine이 아직 로드되지 않은 상황이기 때문에 컴포넌트 자체가 시스템보다 먼저 초기화되면서 생기는 문제였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750341423940&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LogPhysics: Error: FBodyInstance::GetSimplePhysicalMaterial : GEngine not initialized! Cannot call this during native CDO construction, wrap with if(!HasAnyFlags(RF_ClassDefaultObject)) or move out of constructor, material parameters will not be correct.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결 방안으로 생성자에서 진행한 물리세팅 로직을 BeginPlay로 옮겨서 해결하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1750341688636&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AGS_BridgePiece::BeginPlay()
{
	Super::BeginPlay();
	
	...
	MeshComponent-&amp;gt;SetSimulatePhysics(false);
	MeshComponent-&amp;gt;SetMassOverrideInKg(NAME_None, 100000.0f, true);
}&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;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&amp;lt; 텍스쳐 적용 오류 &amp;gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;텍스쳐를 적용하기 위해 아래와 같이 SetBridgeMesh 함수에 매개변수를 추가하여 해결하려고하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751129853091&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AGS_BridgePiece::SetBridgeMesh(UStaticMesh* InMesh, UMaterialInterface* InMaterial, float InValue)
{
	if (MeshComponent &amp;amp;&amp;amp; InMesh)
	{
		MeshComponent-&amp;gt;SetStaticMesh(InMesh);
        	//텍스쳐 세팅
		MeshComponent-&amp;gt;SetMaterial(0, BridgeMaterial);
		MaxHealth = InValue;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;그러나 이 경우 아래와 같이 텍스쳐 적용이 되지 않는 문제가 있었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;485&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAgAsh/btsOV9AFE7z/aKF6o7qcmFGkWGNML9trY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAgAsh/btsOV9AFE7z/aKF6o7qcmFGkWGNML9trY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAgAsh/btsOV9AFE7z/aKF6o7qcmFGkWGNML9trY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAgAsh%2FbtsOV9AFE7z%2FaKF6o7qcmFGkWGNML9trY1%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;751&quot; height=&quot;485&quot; data-origin-width=&quot;751&quot; data-origin-height=&quot;485&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이유는 SetMaterial 함수가 리플리케이티드 되지 않기 때문이었다.&lt;/li&gt;
&lt;li&gt;이것을 해결하기 위해서 Material 자체를 OnRep함수와 연동하여 클라이언트 쪽에서 SetMaterail 함수를 호출하도록 구현하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1751130318330&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//헤더
UFUNCTION()
void OnRep_BridgeMaterial();

UPROPERTY(ReplicatedUsing=OnRep_BridgeMaterial)
UMaterialInterface* BridgeMaterial;

//cpp
void AGS_BridgePiece::SetBridgeMesh(UStaticMesh* InMesh, UMaterialInterface* InMaterial, float InValue)
{
	if (MeshComponent &amp;amp;&amp;amp; InMesh)
	{
		MeshComponent-&amp;gt;SetStaticMesh(InMesh);

		if (InMaterial)
		{
			BridgeMaterial = InMaterial;
		}
		MaxHealth = InValue;
	}
}

void AGS_BridgePiece::OnRep_BridgeMaterial()
{
	if (MeshComponent &amp;amp;&amp;amp; BridgeMaterial)
	{
		MeshComponent-&amp;gt;SetMaterial(0, BridgeMaterial);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해결된 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;969&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/V7cV2/btsOW5R0DVv/P2HaxX4HLKK4ERtJO0o6GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/V7cV2/btsOW5R0DVv/P2HaxX4HLKK4ERtJO0o6GK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/V7cV2/btsOW5R0DVv/P2HaxX4HLKK4ERtJO0o6GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FV7cV2%2FbtsOW5R0DVv%2FP2HaxX4HLKK4ERtJO0o6GK%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;969&quot; height=&quot;562&quot; data-origin-width=&quot;969&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;&amp;nbsp;&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;5. 결과 및 추후 개발할 것&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현재까지 결정된 내용으로는 일정 시간 시뮬레이션 후, 사라지는 것으로 기획이 결정되었다.&lt;/li&gt;
&lt;li&gt;사라질때 디졸브 효과로 사라지게 하는 것을 추가할 예정이며,&lt;/li&gt;
&lt;li&gt;사라지는 것으로 기획이 픽스된다면, 카오스를 통해 갈비뼈와 같이 약한 부분을 부수는 시스템을 추가하여 좀 더 실감나는 파괴 시뮬레이션을 구현해 볼 예정이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>Unreal Engine</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/184</guid>
      <comments>https://gbleem.tistory.com/184#entry184comment</comments>
      <pubDate>Wed, 18 Jun 2025 20:09:29 +0900</pubDate>
    </item>
    <item>
      <title>UnrealEngine - HitStop</title>
      <link>https://gbleem.tistory.com/183</link>
      <description>&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;여러가지 정보를 찾다가 아래 영상을 통해 Hit Stop이라는 것을 접하게 되었고 게임에 적용해 보게 되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://youtu.be/IL9j4NchTvA?si=MM6F7kJEw7RysZRI&amp;amp;t=1951&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://youtu.be/IL9j4NchTvA?si=MM6F7kJEw7RysZRI&amp;amp;t=1951&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 기능 구현&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SetGlobalTimeDilation을 통해 World의 시간을 지연시켜서 느려지도록 하는 방식이다.&lt;/li&gt;
&lt;li&gt;단순히 싱글플레이 게임에 있어서는 아래와 같은 방식을 사용해도 문제가 없을것이지만, 현재 게임은 멀티플레이 게임이기 때문에 멀티캐스트를 통해서 시간 지연하는 함수를 사용하기 때문에 월드에 존재하는 &lt;b&gt;모든 플레이어들의 시간이 느려지는 문제&lt;/b&gt;가 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750158640029&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AGS_Guardian::MulticastRPCApplyHitStop_Implementation()
{
	if (GetWorld() &amp;amp;&amp;amp; GetWorld()-&amp;gt;GetNetMode() == NM_DedicatedServer)
	{
		return;	
	}

	UGameplayStatics::SetGlobalTimeDilation(GetWorld(), HitStopTimeDilation);
	GetWorld()-&amp;gt;GetTimerManager().SetTimer(HitStopTimerHandle, this, &amp;amp;AGS_Guardian::EndHitStop, HitStopDurtaion*HitStopTimeDilation, false);
}

void AGS_Guardian::EndHitStop()
{
	UGameplayStatics::SetGlobalTimeDilation(GetWorld(), 1.f);
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 트러블슈팅&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;위에서 언급한 문제를 해결하기 위해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;CustomTimeDelation&lt;/b&gt;을 사용하였다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/GameFramework/AActor/CustomTimeDilation&quot;&gt;https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/GameFramework/AActor/CustomTimeDilation&lt;/a&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;해당 기능은 각 액터가 가진 기능으로&lt;/li&gt;
&lt;li&gt;&lt;b&gt;액터 각각의 시간&lt;/b&gt;을 지연시키는 기능이기 때문에 멀티플레이에 있어서 해당 기능을 사용하는 것이 더 좋은 방식이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 구현된 코드는 아래와 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;데미지를 입은 캐릭터를 매개변수로 넣어준 후 멀티캐스트를 통해 HitStop을 적용해준다.&lt;/li&gt;
&lt;li&gt;이후에 &quot;타이머 델리게이트&quot;를 통해서 EndHitStop 함수에 데미지를 입은 캐릭터를 매개변수로 넣어주었다.&lt;/li&gt;
&lt;li&gt;추가적으로 CameraShake 기능을 추가하였다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750162184179&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void AGS_Guardian::ApplyDamageToDetectedPlayer(const TSet&amp;lt;AGS_Character*&amp;gt;&amp;amp; DamagedCharacters, float PlusDamge)
{
	for (auto const&amp;amp; DamagedCharacter : DamagedCharacters)
	{
    	      ...
	      MulticastRPCApplyHitStop(DamagedCharacter);			
        
	}
}
void AGS_Guardian::MulticastRPCApplyHitStop_Implementation(AGS_Character* InDamagedCharacter)
{
	if (HasAuthority())
	{
		if (CameraShakeComponent)
		{
			CameraShakeComponent-&amp;gt;PlayCameraShake(HitStopShakeInfo);
		}
	}
	if (!HasAuthority())
	{
		CustomTimeDilation = 0.1f;
		InDamagedCharacter-&amp;gt;CustomTimeDilation = 0.1f;

		FTimerDelegate HitStopTimerDelegate;
		FTimerHandle HitStopTimerHandle;
        
		HitStopTimerDelegate.BindUFunction(this, FName(&quot;EndHitStop&quot;), InDamagedCharacter);
		GetWorld()-&amp;gt;GetTimerManager().SetTimer(HitStopTimerHandle, HitStopTimerDelegate, HitStopDurtaion, false);
	}
}

void AGS_Guardian::MulticastRPCEndHitStop_Implementation(AGS_Character* InDamagedCharacter)
{
	if (!HasAuthority())
	{
		CustomTimeDilation = 1.f;
		InDamagedCharacter-&amp;gt;CustomTimeDilation = 1.f;
	}
}&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;결과 영상&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 두 화면 (공격자와 피격자) 의 캐릭터만 화면 흔들림과 시간 지연이 발생한다&lt;/li&gt;
&lt;li&gt;오른쪽 위 화면의 캐릭터는 시간지연 발생 x&lt;/li&gt;
&lt;/ul&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unreal Engine</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/183</guid>
      <comments>https://gbleem.tistory.com/183#entry183comment</comments>
      <pubDate>Tue, 17 Jun 2025 15:27:09 +0900</pubDate>
    </item>
    <item>
      <title>UE5 Issues : 데디케이트 서버 UI에 관해</title>
      <link>https://gbleem.tistory.com/181</link>
      <description>&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;Owner설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 Owner&lt;/li&gt;
&lt;li&gt;UI에 띄울 정보를 가질 Owner&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보이는 곳&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Owner에게만 보여줄 것인가&lt;/li&gt;
&lt;li&gt;다른 유저들에게도 보여줄 것인가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위젯에서 표시할 값이 어디에서 변하는가&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 변경&lt;/li&gt;
&lt;li&gt;클라이언트에서 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Owner 설정과 보여지는 곳&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, 우리가 자신의 체력 바를 만든다고 했을 때를 생각해 보자&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 Owner는 나 자신이고, 띄울 정보의 Owner도 나 자신이다.&lt;/li&gt;
&lt;li&gt;보여지는 곳은 나 자신이 될 것이다.&lt;/li&gt;
&lt;li&gt;이 경우는 크게 생각할 문제가 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 우리가 다른 플레이어의 체력 바를 또한 띄우고 싶다면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;UI의 Owner는 나 자신이지만, 띄울 정보는 다른 플레이어의 정보이다.&lt;/li&gt;
&lt;li&gt;보여지는 곳은 여전히 나 자신이 된다.&lt;/li&gt;
&lt;li&gt;이 경우 해당 Widget을 초기화 함에 있어서 다른 플레이어의 정보를 통해서 초기화 해주어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래의 코드를 보면&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위젯 이름 설명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HP_BoardWidget은 우리 팀원의 체력을 붙여서 띄워줄 VertexBox를 가지고 있는 클래스이다.&lt;/li&gt;
&lt;li&gt;HP_Widget의 경우는 각 팀원들의 체력을 나타내는 클래스이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;코드 설명
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;각각의 HP_Widget의 &lt;b&gt;정보의 Owner&lt;/b&gt;는 나를 제외한 다른 플레이어이다.&lt;/li&gt;
&lt;li&gt;그렇게 때문에 GameState의 PlayerArray를 통해 모든 플레이어의 정보 순환하면서 Owner를 각 플레이어로 지정해주고 부모 Widget인 HP_BoardWidget에 붙여준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749697898022&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UGS_HPBoardWidget::InitBoardWidget()
{
	if (!IsValid(HPWidgetClass) || !IsValid(HPWidgetList))
	{
		return;
	}
	
	HPWidgetList-&amp;gt;ClearChildren();

	AGameStateBase* GS = UGameplayStatics::GetGameState(this);
	if (IsValid(GS))
	{
		TArray&amp;lt;APlayerState*&amp;gt; PSA = GS-&amp;gt;PlayerArray; 
		for (APlayerState* PS : PSA)
		{
			AGS_PlayerState* GSPS = Cast&amp;lt;AGS_PlayerState&amp;gt;(PS);
			if (IsValid(GSPS))
			{
				UGS_HPWidget* HPWidget = CreateWidget&amp;lt;UGS_HPWidget&amp;gt;(this, HPWidgetClass);
				if (IsValid(HPWidget))
				{
					AGS_Character* Character = Cast&amp;lt;AGS_Character&amp;gt;(GSPS-&amp;gt;GetPawn());

					//자기 자신 제외
					if (IsValid(Character) &amp;amp;&amp;amp; Character != OwningCharacter)
					{
						//가디언 제외
						if (GSPS-&amp;gt;CurrentPlayerRole == EPlayerRole::PR_Guardian)
						{
							continue;
						}
						
						HPWidget-&amp;gt;SetOwningActor(Character);
						HPWidget-&amp;gt;InitializeHPWidget(Character-&amp;gt;GetStatComp());
						HPWidgetList-&amp;gt;AddChildToVerticalBox(HPWidget);
					}
				}
			}
		}
	}	
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;부모 Widget인 HP_BoardWidget의 Owner는 나 자신이기 때문에 아래와 같이 생성자에서 OwningActor를 GetOwningPlayer()를 통해 지정해준다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1749698664760&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void UGS_HPBoardWidget::NativeConstruct()
{
	Super::NativeConstruct();

	if (IsValid(GetOwningPlayer()-&amp;gt;GetPawn()))
	{
		OwningCharacter = Cast&amp;lt;AGS_Character&amp;gt;(GetOwningPlayer()-&amp;gt;GetPawn());
	}
    ...
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 어디에서 값이 변경되는가&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두가지 경우가 있을 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;값이 클라이언트에서 변경&lt;/li&gt;
&lt;li&gt;혹은 서버에서 변경&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 클라이언트에서 변하고, 위젯 Owner만 값을 보는 경우는 크게 생각할 것이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나, 값이 서버에서 변경되었을 때 모든 클라이언트가 해당 값을 보고 싶은 경우가 많다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&amp;nbsp;보스 몬스터의 체력과 피버 게이지&lt;/li&gt;
&lt;li&gt;다른 플레이어의 체력 등...&lt;/li&gt;
&lt;/ul&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;902&quot; data-origin-height=&quot;526&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bF4ykS/btsOEcDbdQy/1eD1F3boBsmdsM08XWPcKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bF4ykS/btsOEcDbdQy/1eD1F3boBsmdsM08XWPcKk/img.png&quot; data-alt=&quot;빨간색 화살표가 체력, 파란색 화살표가 피버 게이지&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bF4ykS/btsOEcDbdQy/1eD1F3boBsmdsM08XWPcKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbF4ykS%2FbtsOEcDbdQy%2F1eD1F3boBsmdsM08XWPcKk%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;902&quot; height=&quot;526&quot; data-origin-width=&quot;902&quot; data-origin-height=&quot;526&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;2 - 1. 보스 몬스터의 체력&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;플레이어의 체력은 변수 자체가 replicated된 상태이다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;주의할 점은 체력의 변경은 주요 로직이므로 &quot;서버&quot;에서 진행된다.&lt;/li&gt;
&lt;li&gt;그렇기 때문에, 변경된 순간 클라이언트 쪽에서도 값을 업데이트 해줘야한다.&lt;/li&gt;
&lt;li&gt;이 경우 ReplicatedUsing 키워드를 통해서 클라이언트로 값을 알려주었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;또한 해당 체력을 가지고 있는 Stat 컴포넌트가 변수는 delegate를 통해 연동되어서 값이 바뀔 때마다 위젯의 함수를 호출하여 체력 게이지의 값을 변경해준다.&lt;/li&gt;
&lt;li&gt;로직
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;StatComp와 HPWidget을 Delegate 바인딩 하기&lt;/li&gt;
&lt;li&gt;StatComp에 존재하는 CurrentHealth 변수가 SetCurrentHealth 를 통해 변경되면 BroadCast를 통해 위젯의 업데이트 함수가 call된다.&lt;/li&gt;
&lt;li&gt;이때 OnRep 함수를 통해 클라이언트에서도 BroadCast하게 되면서 모든 클라이언트 위젯이 업데이트 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;이때 주의할 점은 OnRep을 통해서 클라이언트에 변경된 값을 알려주지 않는다면, 위젯이 업데이트되지 않는다&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750053696895&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//GS_StatComp.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCurrentHPChangedDelegate, UGS_StatComp*);

UPROPERTY(ReplicatedUsing = OnRep_CurrentHealth)
float CurrentHealth;

//GS_StatComp.cpp
void UGS_StatComp::OnRep_CurrentHealth()
{
	OnCurrentHPChanged.Broadcast(this);
}

void UGS_StatComp::SetCurrentHealth(float InHealth, bool bIsHealing)
{
	...
        CurrentHealth = InHealth;
	OnCurrentHPChanged.Broadcast(this);
    
    ...
}

//GS_Character.cpp
void AGS_Character::SetHPBarWidget(UGS_HPWidget* InHPBarWidget)
{
	UGS_HPWidget* HPBarWidget = Cast&amp;lt;UGS_HPWidget&amp;gt;(InHPBarWidget);
	if (IsValid(HPBarWidget))
	{
		HPBarWidget-&amp;gt;InitializeHPWidget(GetStatComp());
		StatComp-&amp;gt;OnCurrentHPChanged.AddUObject(HPBarWidget, &amp;amp;UGS_HPWidget::OnCurrentHPBarChanged);
	}
}

//GS_HPWidget.cpp
void UGS_HPWidget::NativeConstruct()
{
	Super::NativeConstruct();

	if (!IsValid(OwningCharacter))
	{
		OwningCharacter = Cast&amp;lt;AGS_Character&amp;gt;(GetOwningPlayer()-&amp;gt;GetPawn());
	}
	
	if (IsValid(OwningCharacter))
	{
		OwningCharacter-&amp;gt;SetHPBarWidget(this);
	}
}
void UGS_HPWidget::OnCurrentHPBarChanged(UGS_StatComp* InStatComp)
{
 	HPBarWidget-&amp;gt;SetPercent(InStatComp-&amp;gt;GetCurrentHealth()/InStatComp-&amp;gt;GetMaxHealth());
}&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;size18&quot;&gt;2 - 2. 보스 몬스터의 피버 게이지&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;피버 게이지의 경우 변수를 리플리케이트하지 않았다.&lt;/li&gt;
&lt;li&gt;그렇기 때문에, 이 값들을 다른 클라이언트에게 모두 알려주기 위해서 Multicast를 사용하였다.&lt;/li&gt;
&lt;li&gt;체력과 거의 유사한 구조로 구성되었지만, 값을 변경해주는 함수를 Multicast로 만든 것이 차이점이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750055613872&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//GS_Drakhar.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCurrentFeverGageChangedDelegate, float);

float CurrentFeverGage;

//GS_Drakhar.cpp
void AGS_Drakhar::SetFeverGageWidget(UGS_DrakharFeverGauge* InDrakharFeverGageWidget)
{
	UGS_DrakharFeverGauge* DrakharFeverGaugeWidget = Cast&amp;lt;UGS_DrakharFeverGauge&amp;gt;(InDrakharFeverGageWidget);
	if (IsValid(DrakharFeverGaugeWidget))
	{
		//client
		DrakharFeverGaugeWidget-&amp;gt;InitializeGage(GetCurrentFeverGage());
		OnCurrentFeverGageChanged.AddUObject(DrakharFeverGaugeWidget, &amp;amp;UGS_DrakharFeverGauge::OnCurrentFeverGageChanged);
	}
}

void AGS_Drakhar::MulticastRPCSetFeverGauge_Implementation(float InValue)
{	
	CurrentFeverGage += InValue;
	...
	
	OnCurrentFeverGageChanged.Broadcast(CurrentFeverGage);
}

void AGS_Guardian::ApplyDamageToDetectedPlayer(const TSet&amp;lt;AGS_Character*&amp;gt;&amp;amp; DamagedCharacters, float PlusDamge)
{
	for (auto const&amp;amp; DamagedCharacter : DamagedCharacters)
	{
		...
        
			AGS_Drakhar* Drakhar = Cast&amp;lt;AGS_Drakhar&amp;gt;(this);
			if (!Drakhar-&amp;gt;GetIsFeverMode())
			{
				Drakhar-&amp;gt;MulticastRPCSetFeverGauge(10.f);
			}
		}
	}
}&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;size18&quot;&gt;2 - 3. 어떤 방식이 더 좋은가?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티캐스트 하는 방식에는 가장 큰 문제점이 존재한다.&lt;/li&gt;
&lt;li&gt;만약 다른 플레이어가 게임 도중에 들어온 경우, 피버 게이지의 값이 동기화 되지 않는 문제점이 있다.&lt;/li&gt;
&lt;li&gt;아래 사진과 같은 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c5Xn8C/btsOCLNOq4n/KyY9RWOCVbphfNMoOH7Y7k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c5Xn8C/btsOCLNOq4n/KyY9RWOCVbphfNMoOH7Y7k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c5Xn8C/btsOCLNOq4n/KyY9RWOCVbphfNMoOH7Y7k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc5Xn8C%2FbtsOCLNOq4n%2FKyY9RWOCVbphfNMoOH7Y7k%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;1546&quot; height=&quot;582&quot; data-origin-width=&quot;1546&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;체력과 마찬가지로 OnRep을 통해 클라이언트에게 알려주도록 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1750068814717&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//Drakhar.h
UPROPERTY(ReplicatedUsing=OnRep_FeverGauge)
float CurrentFeverGauge;

UFUNCTION()
void OnRep_FeverGauge();

//Drakhar.cpp
void AGS_Drakhar::OnRep_FeverGauge()
{
	OnCurrentFeverGageChanged.Broadcast(CurrentFeverGauge);
}

void AGS_Drakhar::GetLifetimeReplicatedProps(TArray&amp;lt;class FLifetimeProperty&amp;gt;&amp;amp; OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);

	...
	DOREPLIFETIME(ThisClass, CurrentFeverGauge);
}&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;결과 모습&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unreal Engine</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/181</guid>
      <comments>https://gbleem.tistory.com/181#entry181comment</comments>
      <pubDate>Thu, 12 Jun 2025 12:31:01 +0900</pubDate>
    </item>
    <item>
      <title>Unreal Engine - 데디케이티드 서버 11 (콤보 공격 최적화)</title>
      <link>https://gbleem.tistory.com/180</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 언리얼 인사이트&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;언리얼 프로파일링 툴인 Unreal Insights를 통해서 최적화 테스트를 해 볼 생각이다.&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 - 1. 설정&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;언리얼 엔진을 다운로드한 경로로 가서 아래와 같이 UnrealInsights.exe 파일을 찾는다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적으로 아래와 같은 경로로 저장된다.&amp;nbsp;&lt;/li&gt;
&lt;li&gt;C:\Program&amp;nbsp;Files\Epic&amp;nbsp;Games\UE_5.5\Engine\Binaries\Win64&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;643&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dahjOG/btsOnXtZwvg/wHrmeCgnLk1y9HSsPY6nS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dahjOG/btsOnXtZwvg/wHrmeCgnLk1y9HSsPY6nS1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dahjOG/btsOnXtZwvg/wHrmeCgnLk1y9HSsPY6nS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdahjOG%2FbtsOnXtZwvg%2FwHrmeCgnLk1y9HSsPY6nS1%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;929&quot; height=&quot;643&quot; data-origin-width=&quot;929&quot; data-origin-height=&quot;643&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;쉽게 사용하기 위해 환경변수에 등록을 한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/FtegN/btsOnaAnD0R/mFpOTjefOm8fpceAXln3K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/FtegN/btsOnaAnD0R/mFpOTjefOm8fpceAXln3K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/FtegN/btsOnaAnD0R/mFpOTjefOm8fpceAXln3K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FFtegN%2FbtsOnaAnD0R%2FmFpOTjefOm8fpceAXln3K0%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;521&quot; height=&quot;497&quot; data-origin-width=&quot;521&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이후 프로파일링을 하고자 하는 프로젝트가 있는 폴더로 들어가서 bat 파일을 만들어준다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;653&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SFyxV/btsOodQXHls/cOzN7zEVvoxlHNWeqoNWx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SFyxV/btsOodQXHls/cOzN7zEVvoxlHNWeqoNWx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SFyxV/btsOodQXHls/cOzN7zEVvoxlHNWeqoNWx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSFyxV%2FbtsOodQXHls%2FcOzN7zEVvoxlHNWeqoNWx1%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;924&quot; height=&quot;653&quot; data-origin-width=&quot;924&quot; data-origin-height=&quot;653&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내용은 아래와 같이 환경변수를 사용하여 만들면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748953977134&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UnrealEditor.exe %cd%\SCC_DedicatedX.uproject -NetTrace=1 -Trace=Net&lt;/code&gt;&lt;/pre&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 - 2. 사용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bat 파일을 더블클릭하면, 에디터를 실행시킬 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 후 &quot;언리얼 인사이트&quot;를 실행&lt;/li&gt;
&lt;li&gt;아래 사진 처럼 Connection을 들어간 후 Connect를 눌러서 연결한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kOwlJ/btsOnYsK4YM/k0dkFUf1ganMWvvk2M2dO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kOwlJ/btsOnYsK4YM/k0dkFUf1ganMWvvk2M2dO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kOwlJ/btsOnYsK4YM/k0dkFUf1ganMWvvk2M2dO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkOwlJ%2FbtsOnYsK4YM%2Fk0dkFUf1ganMWvvk2M2dO0%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;1280&quot; height=&quot;740&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Connect가 되면 아래와 같이 LIVE 표시가 뜬다. LIVE 표시가 뜬 것을 선택 후 Open Trace를 눌러서 실행시킨다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;755&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/riudS/btsOob6GyWV/fm6NtQcL6zjcPzKzkhGfa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/riudS/btsOob6GyWV/fm6NtQcL6zjcPzKzkhGfa1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/riudS/btsOob6GyWV/fm6NtQcL6zjcPzKzkhGfa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FriudS%2FbtsOob6GyWV%2Ffm6NtQcL6zjcPzKzkhGfa1%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;1293&quot; height=&quot;755&quot; data-origin-width=&quot;1293&quot; data-origin-height=&quot;755&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 프로파일링 화면이 뜨는 것을 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;746&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IL4dZ/btsOnZ6r0Rl/UJwr0yKLTKUY3ZMRVKUxg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IL4dZ/btsOnZ6r0Rl/UJwr0yKLTKUY3ZMRVKUxg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IL4dZ/btsOnZ6r0Rl/UJwr0yKLTKUY3ZMRVKUxg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIL4dZ%2FbtsOnZ6r0Rl%2FUJwr0yKLTKUY3ZMRVKUxg1%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;1285&quot; height=&quot;746&quot; data-origin-width=&quot;1285&quot; data-origin-height=&quot;746&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서버 과부화&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;</description>
      <category>Unreal Engine</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/180</guid>
      <comments>https://gbleem.tistory.com/180#entry180comment</comments>
      <pubDate>Mon, 2 Jun 2025 02:23:44 +0900</pubDate>
    </item>
    <item>
      <title>Unreal Engine - 데디케이티드 서버 10 (콤보 공격)</title>
      <link>https://gbleem.tistory.com/179</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 플레이어 쪽 함수 세팅&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 인풋 액션인 IA_ComboAttack을 추가한 후 Input Binding 진행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Section을 사용해야 하기 때문에, SectionName에 사용할 변수를 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748537852976&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//헤더
FName ComboAttackSectionName;
FName DefaultComboAttackSectionName;

//cpp
void ADXPlayerCharacter::HandleComboAttackInput(const FInputActionValue&amp;amp; InValue)
{
	if (!HasAuthority() &amp;amp;&amp;amp; IsLocallyControlled())
	{
		PlayAnimMontage(ComboAttackMontage, 1.f, ComboAttackSectionName);
		//ServerRPCComboAttack();
	}
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;2. 애니메이션 노티파이 스테이트&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-style=&quot;style6&quot; data-ke-type=&quot;horizontalRule&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션이 진행되는 동안 애니메이션 노티파이 스테이트를 통해 로직 수행&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;다음 section을 나타내는 변수 존재&lt;/li&gt;
&lt;li&gt;해당 변수를 통해 다음 섹션으로 넘어갈 수 있도록 Player의 함수와 연동하기&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748538321246&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//헤더
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = &quot;Animation&quot;)
FName SectionName;

//cpp
void UDXANSMeleeCombo::NotifyBegin(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, float TotalDuration)
{
	Super::NotifyBegin(MeshComp, Animation, TotalDuration);

	ADXPlayerCharacter* PlayerCharacter = Cast&amp;lt;ADXPlayerCharacter&amp;gt;(MeshComp-&amp;gt;GetOwner());
	if (IsValid(PlayerCharacter))
	{
		PlayerCharacter-&amp;gt;SetNextComboAttack(SectionName);
	}
}

void UDXANSMeleeCombo::NotifyEnd(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
	Super::NotifyEnd(MeshComp, Animation);
	
	ADXPlayerCharacter* PlayerCharacter = Cast&amp;lt;ADXPlayerCharacter&amp;gt;(MeshComp-&amp;gt;GetOwner());
	if (IsValid(PlayerCharacter))
	{
		PlayerCharacter-&amp;gt;ResetComboAttack();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1748585597887&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::SetNextComboAttack(FName InSectionName)
{
	ComboAttackSectionName = InSectionName;
}

void ADXPlayerCharacter::ResetComboAttack()
{
	ComboAttackSectionName = DefaultComboAttackSectionName;
}&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 애니메이션 몽타주&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애니메이션 몽타주에서 Section을 이용하여 콤보 공격을 구현할 것&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;먼저 애니메이션 몽타주를 만든 후 아래와 같이 사용하고자 하는 애니메이션을 넣기&lt;/li&gt;
&lt;li&gt;각 애니메이션이 시작하는 곳에 section을 만든 다음 사용하고자 하는 이름으로 변경&lt;/li&gt;
&lt;li&gt;전에 만들어둔 &quot;애님 노티파이 스테이트&quot;를 추가 (ANS는 다음 섹션으로 넘어가도 되는 부분부터 시작하도록)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;332&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pgwqu/btsOj5ScSTI/TBZ1Qk9Frfy1HvosxUZ8EK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pgwqu/btsOj5ScSTI/TBZ1Qk9Frfy1HvosxUZ8EK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pgwqu/btsOj5ScSTI/TBZ1Qk9Frfy1HvosxUZ8EK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fpgwqu%2FbtsOj5ScSTI%2FTBZ1Qk9Frfy1HvosxUZ8EK%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;1153&quot; height=&quot;332&quot; data-origin-width=&quot;1153&quot; data-origin-height=&quot;332&quot;/&gt;&lt;/span&gt;&lt;/figure&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++로 만들어 둔 변수에 아래와 같이 다음으로 재생할 section의 이름을 넣어준다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래 사진은 Combo1 섹션에 존재하는 ANS의 변수 세팅 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Wzy9Z/btsOiM69tiD/9aCEo01nYdSEVD5FfgipSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Wzy9Z/btsOiM69tiD/9aCEo01nYdSEVD5FfgipSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Wzy9Z/btsOiM69tiD/9aCEo01nYdSEVD5FfgipSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWzy9Z%2FbtsOiM69tiD%2F9aCEo01nYdSEVD5FfgipSk%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;384&quot; height=&quot;225&quot; data-origin-width=&quot;384&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&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;다음으로 각 section이 연결된 링크를 제거하여 모두 따로 분리해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;389&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bL6mUR/btsOiM69lz4/iPDHW8FfWdlOHGSkdIccVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bL6mUR/btsOiM69lz4/iPDHW8FfWdlOHGSkdIccVk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bL6mUR/btsOiM69lz4/iPDHW8FfWdlOHGSkdIccVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbL6mUR%2FbtsOiM69lz4%2FiPDHW8FfWdlOHGSkdIccVk%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;389&quot; height=&quot;112&quot; data-origin-width=&quot;389&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;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lo9SP/btsOiDQdxnJ/L0iUb1z69of4I573mgNIr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lo9SP/btsOiDQdxnJ/L0iUb1z69of4I573mgNIr0/img.png&quot; data-alt=&quot;모두 분리된 섹션&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lo9SP/btsOiDQdxnJ/L0iUb1z69of4I573mgNIr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Flo9SP%2FbtsOiDQdxnJ%2FL0iUb1z69of4I573mgNIr0%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;398&quot; height=&quot;193&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;193&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 실행 결과&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아직 서버 관련 로직을 넣지 않았기 때문에 아래와 같이 자신의 화면에서만 보인다&lt;/li&gt;
&lt;li&gt;combo 공격은 잘 이루어지는 모습&lt;/li&gt;
&lt;/ul&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 트러블 슈팅 (연속 공격 방지)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 문제는 연속된 input (연속 클릭) 시 첫 몽타주가 계속해서 플레이되는 문제가 존재한다.&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&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;또한 NotifyBegin 함수와 바인딩을 통해 해당 노티파이에서 실행할 함수를 만들어 주었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bCanCombo 변수를 추가&lt;/li&gt;
&lt;li&gt;input 바인딩 된 함수에서 실행하면서 bCanCombo를 false로 변경&lt;/li&gt;
&lt;li&gt;OnMontageNotifyBegin이라는 함수를 OnPlayMontageNotifyBegin에 바인딩
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 애님 몽타주 사진에서 본 것처럼 &quot;None&quot; 몽타주 노티파이에서 바인딩 된 함수를 실행&lt;/li&gt;
&lt;li&gt;바인딩 된 함수에서는 다시 bCanCombo를 true로 만들어주기&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748593580882&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::BeginPlay()
{
	Super::BeginPlay();

    ...

	UDXAnimInstanceBase* Anim = Cast&amp;lt;UDXAnimInstanceBase&amp;gt;(GetMesh()-&amp;gt;GetAnimInstance());
	if (IsValid(Anim))
	{
		Anim-&amp;gt;OnPlayMontageNotifyBegin.AddDynamic(this, &amp;amp;ThisClass::OnMontageNotifyBegin);
	}
}

void ADXPlayerCharacter::HandleComboAttackInput(const FInputActionValue&amp;amp; InValue)
{
	if (!HasAuthority() &amp;amp;&amp;amp; IsLocallyControlled())
	{
		if (bCanCombo)
		{
			PlayAnimMontage(ComboAttackMontage, 1.f, ComboAttackSectionName);
			bCanCombo = false;
		}
	}
}

void ADXPlayerCharacter::OnMontageNotifyBegin(FName NotifyName, const FBranchingPointNotifyPayload&amp;amp; PayLoad)
{
	if (NotifyName == &quot;None&quot;)
	{
		bCanCombo = true;
	}
}&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;결과 모습&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;5. 서버 로직 추가 (몽타주 재생)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;pre id=&quot;code_1748795013109&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::PlayComboAttackMontage()
{
	PlayAnimMontage(ComboAttackMontage, 1.f, ComboAttackSectionName);
}&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;아래와 같이 인풋 바인딩 시 실행되는 함수를 수정해 주었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몽타주 함수 실행&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;ServerRPC 함수 추가&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748795055875&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::HandleComboAttackInput(const FInputActionValue&amp;amp; InValue)
{
	if (!HasAuthority() &amp;amp;&amp;amp; IsLocallyControlled())
	{
		if (bCanCombo)
		{
			PlayComboAttackMontage();
			bCanCombo = false;
			ServerRPCComboAttack();
		}
	}
}&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;ServerRPC 함수에서는 아래와 같이 서버와 자기 자신을 제외한 플레이어에게 Multicast 함수를 통해 몽타주를 재생하도록 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1748795192725&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//헤더
UFUNCTION(Server, Reliable)
void ServerRPCComboAttack();
    
UFUNCTION(NetMulticast, Unreliable)
void MulticastRPCComboAttack();
    
//cpp
void ADXPlayerCharacter::ServerRPCComboAttack_Implementation()
{
	MulticastRPCComboAttack();
}

void ADXPlayerCharacter::MulticastRPCComboAttack_Implementation()
{
	//except server and owner player
	if (!HasAuthority() &amp;amp;&amp;amp; !IsLocallyControlled())
	{
		PlayComboAttackMontage();
	}
}&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;결과&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몽타주가 다른 클라이언트에게도 보이는 것을 확인할 수 있다.&lt;/li&gt;
&lt;/ul&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&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;h2 data-ke-size=&quot;size26&quot;&gt;6. 서버 로직 추가 (데미지 처리)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&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;pre id=&quot;code_1748797122063&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::CheckMeleeAttackHit()
{
	//server
	if (HasAuthority())
	{
		TArray&amp;lt;FHitResult&amp;gt; OutHitResults;
		TSet&amp;lt;ACharacter*&amp;gt; DamagedCharacters;
		FCollisionQueryParams Params(NAME_None, false, this);

		const float MeleeAttackRange = 50.f;
		const float MeleeAttackRadius = 50.f;
		const float MeleeAttackDamage = 10.f;

		const FVector Forward = GetActorForwardVector();
		const FVector Start = GetActorLocation() + Forward * GetCapsuleComponent()-&amp;gt;GetScaledCapsuleRadius();
		const FVector End = Start + GetActorForwardVector() * MeleeAttackRange;

		bool bIsHitDetected = GetWorld()-&amp;gt;SweepMultiByChannel(OutHitResults, Start, End, FQuat::Identity, ECC_Camera, FCollisionShape::MakeSphere(MeleeAttackRadius), Params);
		if (bIsHitDetected)
		{
			for (auto const&amp;amp; OutHitResult : OutHitResults)
			{
				ACharacter* DamagedCharacter = Cast&amp;lt;ACharacter&amp;gt;(OutHitResult.GetActor());
				if (IsValid(DamagedCharacter))
				{
					DamagedCharacters.Add(DamagedCharacter);
				}
			}

			FDamageEvent DamageEvent;
			for (auto const&amp;amp; DamagedCharacter : DamagedCharacters)
			{				
				DamagedCharacter-&amp;gt;TakeDamage(MeleeAttackDamage, DamageEvent, GetController(), this);
			}
		}
		FColor DrawColor = bIsHitDetected ? FColor::Green : FColor::Red;
		MulticastDrawDebugMeleeAttack(DrawColor, Start, End, Forward);
	}
}&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;참고) TakeDamage 함수는 기존의&amp;nbsp; TakeDamage 함수를 override한 함수이다.&lt;/p&gt;
&lt;pre id=&quot;code_1748797507680&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;virtual float TakeDamage(float DamageAmount, struct FDamageEvent const&amp;amp; DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;&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;위에서 만든 로직은 애니메이션 몽타주에서 재생할 수 있도록 애님 인스턴스 클래스에 Notify 함수를 만들어서 실행시켰다.&lt;/p&gt;
&lt;pre id=&quot;code_1748797249423&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//헤더
UFUNCTION()
void AnimNotify_CheckMeleeAttackHit();

//cpp
void UDXAnimInstanceBase::AnimNotify_CheckMeleeAttackHit()
{
	ADXPlayerCharacter* OwnerPlayerCharacter = Cast&amp;lt;ADXPlayerCharacter&amp;gt;(OwnerCharacter);
	if (IsValid(OwnerPlayerCharacter))
	{
		OwnerPlayerCharacter-&amp;gt;CheckMeleeAttackHit();
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;새로 만든 노티파이라면,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;노티파이 칸 우클릭 -&amp;gt; 노티파이 추가 -&amp;gt; 새 노티파이 -&amp;gt; 이름 추가(위에서 만든 함수에서 언더바 이후의 이름 사용)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이전에 만들었던 노티파이라면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;570&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCa6Jg/btsOkT0r4D1/WXId1xZAr5FwpZePP0eEO0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCa6Jg/btsOkT0r4D1/WXId1xZAr5FwpZePP0eEO0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCa6Jg/btsOkT0r4D1/WXId1xZAr5FwpZePP0eEO0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCa6Jg%2FbtsOkT0r4D1%2FWXId1xZAr5FwpZePP0eEO0%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;529&quot; height=&quot;570&quot; data-origin-width=&quot;529&quot; data-origin-height=&quot;570&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;추가) 몽타주 틱 타입을 Branching Point로 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bKpZLU/btsOns0GHWK/7HlkWFTw4aRLn0atQoDRM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bKpZLU/btsOns0GHWK/7HlkWFTw4aRLn0atQoDRM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bKpZLU/btsOns0GHWK/7HlkWFTw4aRLn0atQoDRM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbKpZLU%2FbtsOns0GHWK%2F7HlkWFTw4aRLn0atQoDRM1%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;454&quot; height=&quot;126&quot; data-origin-width=&quot;454&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;완성된 모습&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cm7JW6/btsOm8nNgmV/RZT5Q50dYvOibel5QSpLH0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cm7JW6/btsOm8nNgmV/RZT5Q50dYvOibel5QSpLH0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cm7JW6/btsOm8nNgmV/RZT5Q50dYvOibel5QSpLH0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcm7JW6%2FbtsOm8nNgmV%2FRZT5Q50dYvOibel5QSpLH0%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;1078&quot; height=&quot;334&quot; data-origin-width=&quot;1078&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 트러블 슈팅 (데미지 처리)&lt;/h2&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러나 위의 설정만 진행한 다음 실행하면, 공격 처리가 안된다. (아래 영상 참고)&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로컬 플레이어만 인풋 바인딩 함수 실행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;몽타주 실행&lt;/li&gt;
&lt;li&gt;서버 RPC 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;로컬 플레이어의 몽타주 실행의 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버에서 실행되는 로직이 없으므로 CheckMeleeAttackHit 함수가 실행될 수 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;서버 RPC를 실행한 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;멀티캐스트를 하지만, 멀티캐스트 함수에서 !HasAuthority() 일때 실행되기 때문에&lt;/li&gt;
&lt;li&gt;마찬가지로 서버의 로직이 없다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CheckMeleeAttackHit 함수를 ServerRPC 함수로 만들거나&lt;/li&gt;
&lt;li&gt;멀티캐스트를 서버에서도 진행하게 해주는 방법이 있다.&lt;/li&gt;
&lt;/ul&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;아래와 같이 멀티캐스트 함수를 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1748798250234&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;void ADXPlayerCharacter::MulticastRPCComboAttack_Implementation()
{
	if (!IsLocallyControlled())
	{
		PlayComboAttackMontage();
	}
}&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;결과 모습&lt;/p&gt;

            &lt;figure class=&quot;unsupported component-kakaotv&quot; contenteditable=&quot;false&quot; style=&quot;background:#000;margin:16px 0;min-height:72px;padding:10px 16px;display:flex;align-items:center;justify-content:center;text-align:center;box-sizing:border-box;width:100%;max-width:100%;&quot;&gt;
                &lt;p contenteditable=&quot;false&quot; style=&quot;margin:0;color:#8a8a8a;font-size:13px;line-height:1.6;user-select:none;pointer-events:none;&quot;&gt;동영상 서비스가 종료되어 해당 콘텐츠를 재생할 수 없습니다.&lt;/p&gt;
            &lt;/figure&gt;
        
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Unreal Engine</category>
      <author>gbleem</author>
      <guid isPermaLink="true">https://gbleem.tistory.com/179</guid>
      <comments>https://gbleem.tistory.com/179#entry179comment</comments>
      <pubDate>Fri, 30 May 2025 02:40:23 +0900</pubDate>
    </item>
  </channel>
</rss>