오늘 학습한 것
1. 코딩 테스트 - 큰 수 만들기 (미완)
2. Challenge 분반 — GA_Dodge 무적 구르기 구현
3. SetByCaller 제대로 알아보기
4. 인강 마지막 챕터 — 언리얼 네트워크 이론 정리
number에서 k개의 문자를 제거했을 때 얻을 수 있는 가장 큰 수를 구하는 문제입니다. 가능한 경우를 전부 탐색해야 하니 DFS나 BFS가 적합해 보였고, 좀 더 간단하게 느껴지는 DFS로 접근했습니다.
처음에는 제거할 인덱스를 하나씩 선택하며 DFS로 모든 경우를 탐색하고 unordered_set에 담는 방식으로 접근했습니다.
void DFS(unordered_set<string>& outSet, string number, int k, int loopCnt, int startIdx)
{
if (k == loopCnt)
{
outSet.insert(number);
return;
}
else if (number.length() <= startIdx)
{
return;
}
for (int i = startIdx; i < number.length(); i++)
{
string newNumber = number.substr(0, i);
newNumber += number.substr(i + 1, number.length() - i - 1);
DFS(outSet, newNumber, k, loopCnt + 1, i);
}
}
string solution(string number, int k) {
string answer = "";
unordered_set<string> answerSet;
DFS(answerSet, number, k, 0, 0);
answer = *(answerSet.begin());
for (const string& num : answerSet)
{
if (answer < num) answer = num;
}
return answer;
}
최대 100만 자짜리 입력을 너무 얕봤네요. 시간 초과가 약 70% 발생했습니다. 이 방식은 완전 탐색이라 number의 길이가 길어질수록 지수적으로 경우의 수가 폭발합니다.
⏰ 시간이 너무 지나버려서 내일 이어서 풀기로 했습니다. 그리디 방식으로 접근해야 할 것 같습니다.
애니메이션은 이전에 사용했던 Mixamo_Converter로 변환했습니다.
완성 결과:

무적 구간을 담당하는 AnimNotifyState의 이름을 무엇으로 할지 고민했습니다.
ANS_IFrames: 격투 게임이나 소울라이크에서 흔히 부르는 "I-Frame(Invincibility Frame)" 표현을 쓴 것ANS_ApplyTag: 좀 더 범용적이고 재사용성이 좋은 이름처음에는 ANS_IFrames로 결정했다가, 이쪽이 범용성과 재사용성이 더 좋다는 판단에 ANS_ApplyTag 로 변경했습니다.
State.Invincible 태그를 가지고 있을 때 데미지를 무시하도록 UExecCalc_Damage::Execute_Implementation에 아래 코드를 추가했습니다.
if (TargetASC->HasMatchingGameplayTag(
FGameplayTag::RequestGameplayTag(FName("State.Invinsible"))))
{
UE_LOG(LogTemp, Log, TEXT("[State.Invinsible] : No Damage!"));
return;
}
이 태그는 Dodge 몽타주에서 ANS_ApplyTag를 통해 부여/제거됩니다. ANS_ComboWindow의 코드 구조를 참고해서 동일하게 AddLooseGameplayTag / RemoveLooseGameplayTag 방식으로 구현했습니다.
void UANS_ComboWindow::NotifyBegin(...)
{
if (UAbilitySystemComponent* ASC = GetASC(MeshComp))
{
if (ComboWindowTag.IsValid())
{
ASC->AddLooseGameplayTag(ComboWindowTag);
}
}
}
void UANS_ComboWindow::NotifyEnd(...)
{
if (UAbilitySystemComponent* ASC = GetASC(MeshComp))
{
if (ComboWindowTag.IsValid())
{
ASC->RemoveLooseGameplayTag(ComboWindowTag);
}
}
}
테스트에 사용된 트랩 액터는 BP로 구현되어 있어 아래와 같이 블루프린트에서 State.Invincible 태그 여부를 체크합니다.

GA_Dodge::ActivateAbility에서 몽타주 재생 전에 마지막 입력 방향을 바라보도록 구현했습니다.
TWeakObjectPtr<AActor> TargetActor = ActorInfo->AvatarActor;
if (TargetActor.IsValid() == true)
{
if (UCharacterMovementComponent* MoveComp =
Cast<UCharacterMovementComponent>(
TargetActor->GetComponentByClass(UCharacterMovementComponent::StaticClass())))
{
FVector InputVector = MoveComp->GetLastInputVector();
TargetActor->SetActorRotation(InputVector.Rotation());
}
}
GetLastInputVector()는 마지막으로 입력받은 이동 방향을 반환합니다. 뒤늦게 눈치챈 것이 입력이 없으면 제로 벡터가 반환되니, 추후에 제로 벡터 체크를 추가해야 겠습니다.
스테미나 소모는 두 곳에서 처리했습니다.
ActivateAbility — 소모 적용
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(StaminaCostEffectClass, 1.0f);
if (SpecHandle.IsValid())
{
SpecHandle.Data->SetSetByCallerMagnitude(
FGameplayTag::RequestGameplayTag(FName("Data.Stamina.Cost")),
-DodgeStaminaCost // 음수로 소모
);
ApplyGameplayEffectSpecToOwner(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, SpecHandle);
}
CanActivateAbility — 스테미나 체크
bool UGA_Dodge::CanActivateAbility(...)
{
if (!Super::CanActivateAbility(...)) return false;
UAbilitySystemComponent* ASC = ActorInfo->AbilitySystemComponent.Get();
if (!ASC) return false;
const UMyAttributeSet* AttributeSet = ASC->GetSet<UMyAttributeSet>();
if (!AttributeSet) return false;
float CurrentStamina = AttributeSet->GetStamina();
if (CurrentStamina < DodgeStaminaCost)
{
UE_LOG(LogTemp, Warning, TEXT("[Dodge] Not enough Stamina! (%.1f / %.1f)"),
CurrentStamina, DodgeStaminaCost);
return false;
}
return true;
}
PoisonTrap의 데미지를 SetByCaller로 바꾸려다 예상보다 잘 되지 않아 제대로 파고들어 봤습니다. 그 과정에서 Gemini 할루시네이션으로 시간을 많이 날렸어요... 이미지와 로그를 건네주면서 방법을 알려달라니까 엉뚱한 말만 반복하다가, 나중에 다시 물어보니 처음부터 불가하다고 말을 바꾸네요.
SetByCaller의 기본값은 SpecHandle 단위로 적용된다.
MakeOutgoingSpec()으로 Spec을 생성할 때마다 SetSetByCallerMagnitude()로 값을 지정해야 합니다. 게임 전체에 적용되는 기본값 같은 건 없습니다.
SetByCaller의 설계 의도는 "Effect를 적용하는 시점에 호출자(Caller)가 값을 직접 지정한다" 는 것입니다. Effect는 그냥 "이 태그에 해당하는 값을 써라"라는 틀만 가지고 있고, 실제 값은 SpecHandle 생성 후 외부에서 채워 넣는 구조입니다.



💡 고정값을 사용할 거면
ScalableFloat를 쓰는 것이 훨씬 적합합니다. 오기가 도져서 계속 매달렸지만 결국ScalableFloat로 되돌렸습니다.
C++로 구현했던 ChatX 프로젝트를 블루프린트로 구현하는 내용이었습니다. 이론은 이전에 설명했던 내용이어서 복습하는 마음으로 수강했네요.
| 종류 | 설명 | 예시 |
|---|---|---|
| P2P (Peer to Peer) | 각각의 컴퓨터가 클라이언트이자 서버인 구조 | 다크소울 |
| Listen Server | 클라이언트이자 서버인 방장(Host)이 있고, 나머지는 클라이언트만 담당. P2P의 일종 | 마인크래프트, 어몽어스 |
| Dedicated Server | 온전히 서버 역할만 하는 서버. 클라이언트 로직을 수행하지 않음 | MMO, FPS |
DedicatedServer.exe를 실행하거나 PIE에서 Run as Client를 선택OpenLevel 명령어로 특정 레벨을 띄움Listen 명령어가 실행되어 Socket이 생성되고 다른 PC가 접속 가능해짐Listen 명령어가 없다면 싱글 플레이가 됨GameMode 액터 생성
클라이언트 접속 흐름
1. 클라이언트가 Open (서버IP) 명령어로 접속 요청
2. 서버는 자신이 오픈한 Level을 알려주고, 클라이언트도 동일한 Level을 오픈
3. 클라이언트가 Level을 열었다는 응답을 보내면, 서버가 해당 클라이언트 전용 PlayerController를 생성
4. 해당 PlayerController를 클라이언트에 복제하고 패킷으로 전달
5. PlayerController가 빙의할 Pawn도 서버에서 생성되어 복제됨
중요한 특징
PlayerController는 서버 PC와 Owning PC에만 존재하고, 다른 클라이언트 PC에는 없습니다.Pawn은 모든 PC에 존재합니다.Editor → Editor Preferences → Play에 있는 주요 설정들입니다.
| 설정 | 설명 |
|---|---|
| Run Under One Process | 모든 클라이언트를 하나의 프로세스에서 실행. 에셋 공유가 발생하므로 멀티플레이 테스트 시 반드시 체크 해제 |
| Launch Separate Server | 서버를 따로 띄워줌. 안 하면 에디터 화면이 클라이언트이자 서버 |
| Allow Late Joining | 플레이 시작 후에도 클라이언트 추가 가능 |
| Always on Top | 새 클라이언트 화면이 항상 위에 뜸 |
NetMode
해당 게임 프로세스가 네트워크 상에서 어떤 역할을 하는지를 나타냅니다.
| NetMode | 설명 |
|---|---|
NM_Standalone | 싱글 플레이 |
NM_ListenServer / NM_DedicatedServer | 서버 |
NM_Client | 클라이언트 |
클라이언트는 해킹에 매우 취약하기 때문에 중요 로직을 서버에서만 처리해야 합니다. 지금 프로세스가 서버인지 클라이언트인지 구분하기 위해 NetMode가 필요합니다.
NetDriver
언리얼 네트워크 통신에서 로우레벨 동작을 관리하는 클래스입니다. 싱글 플레이에서는 생성되지 않고, 멀티플레이에서 UWorld::Listen() 함수를 통해 생성됩니다. 내부에서 NetConnection 객체들을 관리하니다.
NetConnection
다른 PC와 연결이 발생할 때 생성되는 UNetConnection 객체입니다.
ClientConnection을 관리ServerConnection 하나만 관리NetMode는 월드 단위의 정보지만, 액터 단위로 "이 액터가 어느 PC에서 스폰되었는지" 를 파악하기 위해 Role이 필요합니다.
Authority와 Proxy
HasAuthority())액터 Role 종류
| Role | 설명 |
|---|---|
None | 액터가 존재하지 않음 |
Authority | 신뢰할 수 있는 룰. 게임 로직을 수행 |
Autonomous Proxy | Authority의 복제. 서버에도 통신 가능 (입력 전송 등) |
Simulated Proxy | Authority의 복제. 서버로부터 데이터를 수신만 함 |
Autonomous vs Simulated
Autonomous Proxy: 클라이언트의 입력 정보를 서버에 보내는 송신 로직 수행 가능. 플레이어 컨트롤러와 빙의된 폰이 해당.Simulated Proxy: 일방적으로 서버로부터 데이터를 수신만 함.Local Rule과 Remote Rule로 나누는 이유
LocalRole만으로는 액터가 서버와 클라이언트 중 어디에서 스폰되었는지 알 수 없습니다. 예를 들어 서버에 접속한 플레이어 A와 서버에서 스폰된 액터 B가 있다면, LocalRole은 둘 다 Authority입니다. RemoteRole에서 차이가 납니다:
Autonomous ProxyNoneNetMode에 따른 액터 존재 위치
| 존재 위치 | 액터 종류 |
|---|---|
| 서버에만 존재 | GameMode |
| 서버 + 모든 클라이언트 | 배경 액터, Pawn |
| 서버 + 소유하는 클라이언트에만 | PlayerController |
| 클라이언트에만 존재 | 애니메이션 블루프린트, HUD |
액터 특성별 주의사항
HasAuthority() 호출 불필요주요 함수
AActor::HasAuthority(): 현재 Role이 Authority인지 반환AController::IsLocalController(): 현재 컨트롤러가 클라이언트에 의해 조종되고 있는지 반환RPC는 "함수를 호출하는 PC가 아닌 다른 PC에서 실행되게끔 해주는 통신 기법" 입니다. 주로 사운드, 파티클 등 코스메틱 효과에 사용되니다다. 게임에 큰 영향을 끼치는 것들은 Property Replication을 사용해야 합니다.
💡 Call vs Invoke
- Call: 컴파일 타임에 어떤 함수인지 결정. 일반적인 함수 호출.
- Invoke: 런타임에 어떤 함수인지 결정. 함수 포인터, 동적 바인딩, RPC. "RPC를 Invoke한다"고 표현.
언리얼에서의 Own
언리얼에서 "Own"은 NetConnection에 의해 소유되고 있음을 뜻합니다.
ClientConnection이 소유하는 PlayerControllerbReplicates가 true인 액터RPC 키워드
| 키워드 | 설명 |
|---|---|
NetMulticast | 서버를 포함한 모든 클라이언트에서 실행. 서버에서 호출해야 함. 부하가 크므로 남용 금지 |
Server | 서버에서 실행. 클라이언트에서 호출해야 함. _Validate() 함수로 실행 여부 결정 가능 |
Client | 해당 클라이언트에서 실행. 서버에서 호출해야 함 |
서버 PC에서 RPC 호출 시 동작

클라이언트 PC에서 RPC 호출 시 동작

WithValidation
서버 실행 RPC에 사용하는 키워드입니다. _Implementation() 함수와 _Validate() 함수로 나뉘며, _Validate()는 해당 RPC가 실제로 실행될지 말지 결정하는 위변조 방어막 역할을 합니다.
Unreliable vs Reliable
언리얼 Dedicate 내부는 UDP 기반으로 통신한다고 합니다. RPC는 기본적으로 Unreliable이니다.
| 키워드 | 특징 | 사용 예 |
|---|---|---|
Unreliable | 원격 PC에서 실행 보장 없음 | 코스메틱(이펙트, 사운드 등) |
Reliable | 원격 PC에서 반드시 실행 보장 | 충돌, 데미지, 스폰 등 게임에 중요한 로직 |
액터 속성 값이 변경될 때 해당 값을 클라이언트에 복제하는 기법입니다. 모든 속성을 복제하는 것은 비효율적이므로 원하는 속성만 선택적으로 복제합니다. 항상 Authority에서 Proxy로의 복제만 가능하다.
C++ 설정 방법
Replicates 속성을 true로 설정Replicated 키워드 추가GetLifetimeReplicatedProps()에 복제할 속성 등록 (DOREPLIFETIME 매크로 사용)// 헤더
UPROPERTY(Replicated)
int32 Health;
// 소스
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyActor, Health);
}
OnRep_ vs RepNotify
속성 값이 변경되어 클라이언트에 복제될 때 콜백 함수를 바인딩하는 기능입니다. C++에서는 OnRep_, 블루프린트에서는 RepNotify라고 합니다.
| 비교 | C++ OnRep_ | Blueprint RepNotify |
|---|---|---|
| 호출 시점 | 클라이언트에서만 호출 | 서버와 클라이언트 모두에서 호출 |
| 명시적 호출 | 가능 | 불가능 |
| 호출 조건 | 속성 값이 변경될 때만 | 서버는 항시 호출, 클라이언트는 값 변경 시 |