공간은 5 x 5이고, 맨허튼 거리가 2보다 크게 배치되어야 한다.
맨허튼 거리
두 테이블 T1, T2가 행렬 (r1, c1), (r2, c2)에 각각 위치하고 있다면, T1, T2 사이의 맨해튼 거리는 |r1 - r2| + |c1 - c2| 입니다.
쉽게 말해 각 축의 좌표 값 차의 합이 맨허튼 거리가 되네요.
크기도 작도 한 번 이라도 맨허튼 거리가 <= 2이면 0을 집어넣고 반복을 중단하면 된.. 그냥 반복문 돌려도 될 것 같은 문제로 보이네요.
실패 코드
#include <string>
#include <vector>
using namespace std;
vector<int> solution(vector<vector<string>> places) {
vector<int> answer;
for(const vector<string>& place : places)
{
vector<pair<int, int>> points;
for(int i = 0; i < place.size(); i++)
{
for(int j = 0; j < place[i].size(); j++)
{
if(place[i][j] == 'P')
{
points.push_back({i, j});
}
}
}
bool isSafe = true;
for(int i = 0; i < points.size(); i++)
{
for(int j = i + 1; j < points.size(); j++)
{
int dist = abs(points[i].first - points[j].first)
+ abs(points[i].second - points[j].second);
if(dist <= 2)
{
isSafe = false;
break;
}
}
}
if(isSafe)
{
answer.push_back(1);
}
else
{
answer.push_back(0);
}
}
return answer;
}
아 이거 두 P 사이에 파티션이 존재하면 분리된 것을 취급 되나 보네요.
그냥 P 하나마다 맨허튼 거리가 2가 되는 영역을 검색해보는 게 낮겠네요.
실패 2
#include <string>
#include <vector>
using namespace std;
bool CheckPInRange(const vector<string>& map, pair<int, int> pos)
{
vector<pair<int, int>> dir = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for(int i = 0; i < 4; i++)
{
pair<int, int> curPos = pos;
for(int j = 0; j < 2; j++)
{
curPos.first += dir[i].first;
curPos.second += dir[i].second;
if(curPos.first < 0 || curPos.second < 0
|| curPos.first >= 5 || curPos.second >= 5)
{
break;
}
else if(map[curPos.first][curPos.second] == 'X')
{
break;
}
else if(map[curPos.first][curPos.second] == 'P')
{
return true;
}
}
}
vector<pair<int, int>> dir2 = {{-1, 1}, {-1, -1}, {1, -1}, {1, 1}};
for(int i = 0; i < 4; i++)
{
pair<int, int> curPos = pos;
curPos.first += dir2[i].first;
curPos.second += dir2[i].second;
if(curPos.first < 0 || curPos.second < 0
|| curPos.first >= 5 || curPos.second >= 5)
{
continue;
}
else if(map[curPos.first][curPos.second] == 'P')
{
return true;
}
}
return false;
}
vector<int> solution(vector<vector<string>> places) {
vector<int> answer;
for(const vector<string>& place : places)
{
vector<pair<int, int>> points;
bool bIsSafe = true;
for(int i = 0; i < place.size(); i++)
{
for(int j = 0; j < place[i].size(); j++)
{
if(place[i][j] == 'P')
{
if(CheckPInRange(place, make_pair(i, j)))
{
bIsSafe = false;
break;
}
}
}
if(!bIsSafe)
{
break;
}
}
answer.push_back(bIsSafe ? 1 : 0);
}
return answer;
}
잘못 생각했네요. 대각선으로 바로 가는 게 아니라 전후좌우로 한칸 나간 뒤 더 가든 위로 가득 해서 뻗어가야 하는데 그냥 대각선으로 가버리면 파티션으로 막혀있는데도 통과해 버리는 군요.
DFS나 BFS로 이전 위치를 제외한 전후좌우로 이동시켜야 겠네요.
이번엔 어디서 실패한 걸까요?
실패 3
#include <string>
#include <vector>
#include <queue>
using namespace std;
struct Node
{
pair<int, int> prevPos;
pair<int, int> curPos;
};
bool CheckPInRange(const vector<string>& map, pair<int, int> pos)
{
vector<pair<int,int>> dir = {{0,1}, {0, -1}, {-1, 0}, {1, 0}};
queue<Node> nodes;
Node node;
node.prevPos = pos;
node.curPos = pos;
while(!nodes.empty())
{
Node curNode = nodes.front();
nodes.pop();
pair<int, int> prev = curNode.prevPos;
pair<int, int> cur = curNode.curPos;
for(int i = 0; i < 4; i++)
{
pair<int, int> newPos = cur;
newPos.first += cur.first + dir[i].first;
newPos.second += cur.second + dir[i].second;
if(newPos.first < 0 || newPos.first >= 5
|| newPos.second < 0 || newPos.second >= 5)
{
continue;
}
else if(map[newPos.first][newPos.second] == 'X')
{
continue;
}
else if(newPos == prev)
{
continue;
}
else if(map[newPos.first][newPos.second] == 'P')
{
return true;
}
Node newNode;
newNode.prevPos = cur;
newNode.curPos = newPos;
nodes.push(newNode);
}
}
return false;
}
vector<int> solution(vector<vector<string>> places) {
vector<int> answer;
for(const vector<string>& place : places)
{
vector<pair<int, int>> points;
bool bIsSafe = true;
for(int i = 0; i < place.size(); i++)
{
for(int j = 0; j < place[i].size(); j++)
{
if(place[i][j] == 'P')
{
if(CheckPInRange(place, make_pair(i, j)))
{
bIsSafe = false;
break;
}
}
}
if(!bIsSafe)
{
break;
}
}
answer.push_back(bIsSafe ? 1 : 0);
}
return answer;
}
| 항목 | 값 |
|---|---|
| 발견한 버그 수 | 11 |
| 수정 완료한 버그 수 | 10 |
| 수정 시도했지만 실패한 버그 수 | 1 |
| 작성한 테스트 케이스 수 | 싱글플레이 (S-): 42개 |
| 멀티플레이 (M-): 52개 | |
| 사용한 LLM 프롬프트 수 | 15 + @ |
| 발견 못한 버그 수 | 5 |
| 발견 못했던 버그 중 수정 완료한 버그 수 | 1 |
자잘한 프롬포트(되묻기, 수정하기 등)을 제외한 프롬포트의 수는 대략 15로 나왔습니다.
세션 전체 기준으로 집계를 요청한 결과 총 449개가 나왔습니다.
| 구분 | 수 |
|---|---|
| 실제 입력 프롬프트 | 443개 |
| 중간 중단 (Request interrupted) | 6개 |
| 세션 요약 자동 메시지 | 3개 |
| 총합 | 449개 |
작성 팁 : "무엇을" 잘 했는지보다 "왜 잘 됐는지"까지 쓰면 더 좋습니다.
예) "LLM 프롬프트를 3번 반복 개선했더니 테스트 케이스 품질이 올라갔다. 처음부터 완성도를 요구하지 않고 반복 개선하는 방식이 효과적이었다."
작성 팁 : 단순한 불평이 아니라 "왜" 문제가 됐는지를 분석해주세요.
예) "멀티플레이 환경 구성에 시간이 너무 많이 걸렸다. 사전에 테스트 환경을 셋업해두지 않아서 플레이테스트 시작이 늦어졌다."
작성 팁 : Problem에서 도출된 개선책이어야 합니다. 막연한 다짐보다 구체적인 행동으로 작성해주세요.
예) "다음 프로젝트에서는 Day 1에 멀티 테스트 환경을 먼저 셋업한 뒤 테스트를 시작한다."
공개된 전체 버그 리스트를 바탕으로, 내가 발견하지 못한 버그를 확인하고 아래를 작성해보세요.
| 버그 ID | 버그 제목 | 못 찾은 이유 (추정) |
|---|---|---|
| AI-002 | AI-002 | 테스트는 했지만 버그를 버그로 인식하지 못했다 |
| 플레이어의 발소리와 주변 환경음에만 집중하고 AI angent 들의 소리는 생각을 하지 못했습니다. | ||
| INTER-001 | INTER-001 | 멀티플레이 환경에서만 발생해서 싱글 테스트로는 발견 불가였다 |
| MC-02 | MC-02 | 멀티플레이 환경에서만 발생해서 싱글 테스트로는 발견 불가였다 |
| CAN-001 | CAN-001 | 멀티플레이 환경에서만 발생해서 싱글 테스트로는 발견 불가였다 |
| REP-01 | REP-01 | 멀티플레이 환경에서만 발생해서 싱글 테스트로는 발견 불가였다 |
아래 중 해당하는 항목에 체크하고, 어떤 상황이었는지 구체적으로 적어주세요.
| 항목 | 내용 |
|---|---|
| 가장 도움이 됐던 활용 | 1. 테스트 케이스의 작성 요청 시 빠르고 다양하게 테스트 케이스를 작성해 주었습니다. |
| 2. 프로젝트 폴더 구조나 코드 이해도가 부족하여 버그 원인 유추 및 코드 분석에 큰 도움을 받았습니다. | |
| LLM이 틀려서 당황했던 순간 | 1. 실제하지 않는 기능에 대한 테스트 케이스가 몇 개 생성되었습니다. |
| 2. 종종 실존하지 않는 코드나 파일 경로를 제시할 때가 있었습니다. | |
| 다음에 더 잘 활용할 방법 | 1. 테스트 케이스 생성 요청 전에 플레이 영상을 좀 더 꼼꼼히 살펴 보며 미리 어느 정도 특징을 뽑아서 LLM에 전달하기 |
이번 QA 경험을 바탕으로, 최종 프로젝트에서 직접 내 코드에 QA를 적용할 때 달리 할 것을 3가지 이상 작성해주세요.
회고를 작성하며 제일 처음 든 생각은 ‘아 내가 AI에게 잡아먹혔구나’입니다.
버그 원인 유추도 코드 분석도 거의 다 Claude에 맞겼고, 깃허브에 커밋 및 푸시하는 것도 초반에는 Claude에게 요청했었습니다.
커밋 내역을 보고 누락된는 것이 많아 직접하는 것으로 변경했지만요.
QA 프로젝트 초반 시작을 소스 코드 폴더 구조와 각 클래스의 역할 분석으로 시작했었야 했는데 테스트 케이스를 만들고 들 뜬 나머지 무작정 테스트를 시작해 버렸네요.
1단계에서 분석이 있었는데 영상을 위주로 분석을 하고, 코드는 가볍게 흩어보고 끝냈습니다. 이게 제일 멍청한 실수였던 것 같습니다. 하다 못해 LLM으로 분석을 더 쉽게 할 수도 있었을 텐데..
문서 순서도 제대로 지키지 못하고 2단계에서 버그 수정까지 같이 진행하고 있었습니다.
초반에 버그 분석 관련 프롬포트를 작성할 때 [내 QA 역량을 키우기 위한 프로제트이니 짐작되는 원인들(가짜를 섞어서) 3가지를 제시할 것] 같은 문구를 추가하기라도 했다면, 지금보다 더 역량이 올랐을 것 같습니다.
아예 아무런 성장이 없는 것은 아닙니다. LLM을 활용하긴 했지만 버그의 원인이나 코드 분석을 직접 한 버그도 분명 존재하고, LLM의 할로우네이션을 바로 눈치채고 되묻기를 반복해서 정정시키기, 제시된 코드보다 더 나은 해결첵 제시해서 해결하기 등등 분명 더 나아진 것을 체감하게 되는 경험은 몇번 있었습니다.
매 회고 마다 적는 것이긴 하지만 더 잘할 수 있었는데 라는 생각이 드네요.
#회고를 바탕으로 생성한 에이전트
당신은 Unreal Engine 멀티플레이 게임 프로젝트의 QA 보조 에이전트입니다.
목표는 사용자가 스스로 생각하고 성장하는 것입니다.
답을 대신 내어주는 것이 아니라, 사용자가 스스로 답에 도달할 수 있도록 질문하고 안내하는 역할에 집중합니다.
"원인으로 추정되는 가설이 3가지 있습니다.
A.ReadyForActivation()누락으로 태스크 미시작
B.OnCompleted델리게이트 바인딩 오류
C. GE의 Duration 설정 문제
각각 어떻게 확인할 수 있을지 먼저 생각해보시겠어요?"
"이 부분은 먼저 직접 해당 파일을 열어보고, 어디까지 파악했는지 공유해 주시면 거기서부터 같이 이어가겠습니다."
| 금지 행동 | 이유 |
|---|---|
| 코드를 직접 수정하고 커밋 | 사용자의 학습 기회 차단, 의도치 않은 변경 위험 |
| 버그 원인을 즉시 단정 지어 알려주기 | 분석 역량 성장 저해 |
| 테스트 케이스를 코드 확인 없이 작성 | 구현되지 않은 기능 포함 위험 |
| 존재하지 않는 파일 경로 제시 | 혼란 및 시간 낭비 |
| 사용자 확인 없이 문서 내용을 채워넣기 | 사용자 주도권 침해 |
| 멀티플레이 전용 버그를 싱글 테스트로 검증 가능하다고 안내 | 잘못된 테스트 결과 유도 |
// 버그 / // 수정 주석과 함께 나란히 제시이 에이전트는 QA 실습 과정에서의 회고를 바탕으로 설계되었습니다.
"LLM에게 분석을 너무 의지한 나머지 코드에 대한 이해도가 떨어졌습니다."
"버그 예상 지점 및 코드 분석을 요구하는 것이 아닌, 프로젝트의 폴더 구조와 클래스들의 역할 분석 및 요약 정리를 요청하고 각 테스트 케이스와 연결지은 문서를 작성하도록 요청해야 할 것 같습니다."
LLM은 QA를 대신 해주는 도구가 아니라 QA 역량을 키우는 과정의 동반자여야 합니다.
#Lyra 프로젝트 뜯어보기
라이라의 캐릭터 애니메이션은 애니메이션 블루프린트 시스템을 사용해서 거의 전적으로 블루 프린트에서 만들어졌다고 합니다.
프로젝트 내에 AnimBP_Mannequin_Base이 존재하는 데, 라이라 샘플 무기와 게임 플레이 어빌리티 전반에 걸쳐 사용되는 일반적인 기술을 지원하도록 구성되어 있다고 합니다.
라이라에서는 최적의 퍼포먼스를 위해 이벤트 그래프 대신 멀티 스래드 애니메이션을 사용하여 애니메이션 값을 계산합니다.
멀티 스레드 애니메이션 업데이트
보다 많은 애니메이션 작업을 워커 스레드에서 돌릴 수 있도록 해주는 프로젝트 세팅 이며, 기본으로 켜져있다고 합니다.
설정 방법
프로젝트 세팅 에서 General Settings (일반 세팅) > Anim Blueprints (애님 블루프린트 아래 Allow Multi Threaded Animation Update 에서 설정할 수 있습니다.
기존 방식(전통적인 ABP)은 다음과 같이 동작합니다.
[게임 스레드]
└─ Character가 매 프레임 위치/속도 갱신
└─ ABP가 Tick마다 캐스팅으로 Character 참조
└─ Character->GetVelocity() 호출
└─ 결과를 ABP 변수에 저장
└─ AnimGraph에서 사용
이 방식의 문제점은 세 가지입니다.
문제 1: 케스팅 비용 ABP에서 Cast to MyCharacter를 매번 하면 성능 낭비입니다.
언리얼은 성능을 위해 표준 RTTI를 끄고 자체적인 UClass 타입 시스템을 사용합니다. Cast는 이 시스템을 통해 타입을 확인하므로 일반적인 dynamic_cast보다 빠르지만, 여전히 함수 호출과 타입 체크 부하가 있습니다
문제 2: 스레드 안전성 언리얼 5는 애니메이션 업데이트를 워커 스레드(Worker Thread)에서 병렬로 처리하려고 합니다.
게임 스레드가 동시에 Character 데이터를 수정하고 있으면 데이터 레이스(Race condition)가 발생할 수 있다고 합니다.
문제 3: 타이밍 문제 Character의 위치가 갱신 되기 전에 ABP가 읽으면 한 프레임 늦은 데이터를 쓰게 됩니다.
-> 이 부분도 잘못된 내용이라고 하는 군요.
정확하게는 Property Access가 물리적인 "한 프레임 지연(Latent)"을 직접 해결해주지는 않습니다. 캐릭터의 Tick이 애니메이션보다 늦게 돌면 여전히 지연은 발생합니다.
Property Access의 진짜 역할: 데이터를 읽어오는 도중 게임 스레드에서 값이 변해버려 발생하는 데이터 깨짐(Tearing)과 비일관성을 해결하는 것입니다. 즉, "업데이트 시작 시점의 데이터 스냅샷"을 보장하는 것이 핵심입니다.
--- 지우기 ---
Property Access는 이 문제를 한번에 해결할 수 있다고 합니다.
-> Property Access의 핵심 가치는
- 1. 워커 스레드 안정성 보장(문제 2의 영역)
- 2. 데이터 흐름의 선언적 표현(코드 대신 메타데이터)
캐스팅 캐싱은 보너스에 가깝습니다. 진짜 이유는 멀티스레드입니다.
--- 지우기 ---
요즘 CPU의 멀티 코어를 활용하기 위해 언리얼이 작업을 분산하는 방식입니다.
언리얼 엔진은 전통적으로 게임 스레드(Game Thread)라는 단일 스레드에서 거의 모든 게임 로직을 실행했습니다.
[Game Thread] - 1개의 스레드가 모든 일을 함
├─ Tick 처리
├─ Input 처리
├─ AI 업데이트
├─ Animation 업데이트 ← 여기!
├─ Physics 동기화
└─ Rendering 명령 제출
캐릭터가 100마리 있으면, 100마리 모두의 애니메이션 업데이트가 순차적으로 게임 스레드에서 처리되는 것입니다.
한 캐릭터당 0.1ms씩만 잡아먹어도 100마리면 10ms입니다. 16ms(60FPS) 프레임 예산의 절반 이상을 애니메이션이 먹는 셈입니다.
요즘 CPU는 8코어, 16코어가 흔합니다. 그런데 게임 스레드 하나만 쓰면 나머지 코어가 놀고 있는 거죠. 이걸 활용하기 위해 만든 것이 Worker Thread(워커 스레드) 입니다.
Worker Thread는 "백그라운드에서 작업을 병렬 처리하는 보조 스레드"입니다. 언리얼은 시작 시 CPU 코어 수에 맞춰 여러 Worker Thread를 만들어두고, 게임 스레드가 "이 작업 좀 처리해줘"라고 던지면 놀고 있는 Worker Thread가 받아서 실행합니다.
언리얼 내부적으로 Worker Thread는 TaskGraph 시스템으로 관리된다고 합니다. 애니메이션, 파티클, 물리 등 병렬화 가능한 작업들이 여기에 분배됩니다.
**전통적 방식(모두 게임 스레드에서 처리)
시간 ────────────────────────────────────────►
Core 0: [Tick][AI][Anim1][Anim2][Anim3][Anim4][Anim5][Render]
Core 1: ........................놀고 있음...................
Core 2: ........................놀고 있음...................
Core 3: ........................놀고 있음...................
↑ 16ms 안에 끝나야 함
**병렬 애니메이션(Worker Thread활용)
시간 ────────────────────────────►
Core 0: [Tick][AI][데이터복사][결과수집][Render]
Core 1: ............[Anim1][Anim3]........... ← Worker
Core 2: ............[Anim2][Anim4]........... ← Worker
Core 3: ............[Anim5].................. ← Worker
↑ 훨씬 빨리 끝남
애니메이션들이 병렬로 처리되니 게임 스레드 시간이 대폭 줄어듭니다.
여기가 **데이터 레이스의 시작입니다.
시나리오
Game Thread: Worker Thread:
Character->Velocity = Velocity = Character->Velocity
FVector(100, 0, 0); ← 어떤 값이 읽힐까?
두 스레드가 같은 메모리를 동시에 만지면 결과가 예측 불가능해 집니다.
절반만 쓰여진 값을 읽거나, 메모리 깨짐이 발생하거나, 최악의 경우 크래시까지 발생할 수 있습니다.
이를 막을 방법은 두 가지가 있습니다.
[게임 스레드의 안전한 시점]
└─ Property Access가 자동 실행
├─ Character->Velocity 읽기 (안전: 게임 스레드)
└─ ABP의 Velocity 변수에 복사
[Worker Thread]
└─ ABP의 ThreadSafe Update 실행
└─ ABP의 Velocity 변수만 읽음 (안전: 복사본)
Worker Thread는 절대로 Character를 직접 만지지 않습니다. 게임 스레드가 미리 복사해둔 ABP 멤버 변수만 읽습니다. 그래서 안전한 겁니다.
Property Access는 "이 변수를 저 객체의 속성에 묶어둬"**라고 선언하는 방식입니다.
예를 들어 ABP에 Velcoity라는 변수가 있다고 합시다.
전통적 방식
Event Blueprint Update Animation
→ Cast to LyraCharacter
→ Get Velocity
→ Set Velocity (ABP 변수)
이걸 매 프레임 직접 호출해야 합니다.
Property Access 방식이라면
ABP의 Velocity 변수를 우클릭
→ Property Access 바인딩 생성
→ "TryGetPawnOwner → GetVelocity()" 경로 지정
ABP가 알아서, 가장 적절한 타이밍에, 스레드 안전하게 값을 가져온다고 합니다. 캐스팅도 캐시되어 매번 발생하지 않습니다.
Property Access의 진짜 강력함은 실행 시점을 제어할 수 있다는 것입니다. 세가지 옵션이 있습니다.
명칭 옵션을 공식 명칭으로 수정합니다.
|수정 전 명칭|공식 에디터 명칭 (UE 5.x)|비고|
|-|-|-|
|Before Update Animation|Unthreaded|게임 스레드에서 복사 (Event Graph 실행 전)|
|Worker Thread Update Animation|Thread-safe|워커 스레드에서 복사 (병렬 처리 가능)|
|On Initialize|None / Manual|별도 옵션보다는 초기화 시 1회 바인딩을 의미|
대부분의 데이터(속도, 방향 등)는 Worker Thread 옵션으로 설정해서 멀티스레드 성능을 극대화합니다.
고집쟁이 개발자의 팁: Lyra에서는 성능을 위해 대부분 Thread-safe 옵션을 사용합니다. 만약 연결하려는 C++ 함수에 BlueprintThreadSafe 키워드가 없다면 엔진이 자동으로 Unthreaded로 내리거나 경고를 발생시킵니다.
┌─────────────────────────────────────────────────────────┐
│ 게임 스레드 (Game Thread) │
│ │
│ Character Tick │
│ ├─ 위치/속도 갱신 │
│ └─ MovementComponent 업데이트 │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Property Access Layer (자동 생성된 코드) │
│ │
│ Before Update: 게임 스레드에서 안전하게 데이터 복사 │
│ Velocity ← Character->GetVelocity() │
│ Acceleration ← MovementComp->GetCurrentAcceleration()│
│ bIsCrouched ← Character->bIsCrouched │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ 워커 스레드 (Worker Thread) │
│ │
│ ABP의 ThreadSafe Update Animation │
│ ├─ 복사된 Velocity 읽기 (안전!) │
│ ├─ 방향 계산 │
│ └─ State Machine 평가 │
└─────────────────────────────────────────────────────────┘
핵심은 *데이터를 한 번 복사한 뒤, 워커 스레드는 그 복사본만 읽는다**는 점이라고 합니다.
Q, Property Access가 일종의 바인딩의 역할을 한다고 했는데 그게 어떤 식으로 이루어지는 지가 잘 인해가 안 됩니다. 멀티 플레이의 OnRep 비슷한 건가요?
비슷한 점도 있고 다른 점도 있습니다.
| 항목 | OnRep |
|---|---|
| 목적 | 서버 → 클라이언트 동기화 |
| 경계 | 네트워크 (다른 머신) |
| 트리거 | 서버 변수 변경 시 |
| 방향성 | 단방향 (서버→클라) |
| 데이터 흐름 | 직렬화 후 네트워크 전송 |
둘 다 **"한쪽에서 다른 쪽으로 데이터를 안전하게 전달하는 매커니즘"입니다. 직접 접근이 위험하거나 비효율적이니, 시스템이 알아서 적절한 시점에 데이털르 옮겨줍니다.
OnRep은 네트워크 경계를 넘는 데이터 전달입니다. 서버와 클라이언트는 물리적으로 다른 머신이니 네트워크 패킷으로 전송해야 합니다.
Property Access는 스레드 경계를 넘는 데이터 전달입니다. 같은 메모리 공간 안에서, 다만 다른 스레드가 안전하게 접근할 수 있도록 복사본을 만들어줍니다.
더 정확한 비유는 "폴링(Polling) 방식의 데이터 페치" 입니다.
// 의사 코드로 본 Property Access의 실체
void UAnimInstance::PreUpdateAnimation(float DeltaSeconds)
{
// Property Access가 자동 생성한 코드가 여기서 실행됨
if (CachedPawn)
{
this->Velocity = CachedPawn->GetVelocity(); // 자동 복사
this->Acceleration = CachedPawn->GetAcceleration(); // 자동 복사
this->bIsCrouched = CachedPawn->bIsCrouched; // 자동 복사
}
// 이제 워커 스레드에서 ThreadSafe Update가 실행됨
}
개발자가 일일이 Set Velocity 노드를 그리지 않아도, 시스템이 자동으로 이런 복사 코드를 생성해줍니다. 이게 Property Access의 본질이라 합니다.
OnRep이 "변수가 변경되면 자동으로 호출되는 콜백"이라면, Property Access는 "변수가 자동으로 채워지도록 미리 등록된 바인딩"입니다. OnRep은 이벤트 기반(push), Property Access는 풀링 기반(pull)의 데이터 복사 방식입니다..
실제 시스템이 생성하는 최적화된 복사 코드를 묘사하면 다음과 같다고 합니다.
// 1. [게임 스레드 구간]
void UAnimInstance::UpdatePropertyAccess()
{
// Unthreaded 옵션인 경우 여기서 안전하게 미리 복사
this->Internal_Velocity = MyCharacter->GetVelocity();
}
// 2. [워커 스레드 구간]
void UAnimInstance::ParallelUpdateAnimation()
{
// Thread-safe 옵션인 경우 여기서 직접 복사하거나
// 위에서 복사된 'Internal_Velocity' 멤버 변수만 참조하여 연산 수행
float CurrentSpeed = Internal_Velocity.Size();
}
결론적으로 Property Access는 단순히 편리함을 넘어 멀티스레드 환경의 안전벨트 역할을 합니다.
Lyra의 ABP_Mannequin_Base를 열어보면 변수 패널에 Property Access로 바인딩된 변수들을 확인할 수 있다고 합니다.
변수 옆에 작은 체인 아이콘 또는 녹색 점이 표시된다고 하는데 없는데?