26.08.10
댓글 0
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
댓글을 작성하려면 로그인이 필요합니다.
아직 댓글이 없습니다. 첫 번째 댓글을 작성해보세요.
팀프로젝트 정리 김민준
프로젝트: Project_Dwarf_Extraction — Lethal Company 스타일 4인 협동 1인칭 호러 엔진: Unreal Engine 5.4.4 담당: Player 파트 (이동·전투·생존·상호작용 전반) 협업: GitHub + Git LFS, 브랜치
feature/Player/develop, 팀원별 파트 분담(아이템·몬스터·UI·세션)
프로젝트를 처음부터 끝까지 진행하며 배운 것들을 주제별로 정리했다. 단순 기능 나열이 아니라 "왜 그렇게 해야 하는가"와 "어디서 막혔고 어떻게 뚫었는가"를 남긴다.
프로젝트의 이동·스탯·스킬을 모두 GAS로 구성했다. GAS는 초반 러닝커브가 크지만, 한 번 구조를 잡으면 확장이 쉽고 네트워크 처리를 상당 부분 자동화해준다는 게 큰 장점이었다.
IAbilitySystemInterface를 구현하고, PossessedBy()(서버)와 OnRep_PlayerState()(클라이언트) 양쪽에서 InitAbilityActorInfo()를 호출한다.
PostGameplayEffectExecute()는 Instant 이펙트에서만 호출된다.
GetGameplayAttributeValueChangeDelegate(Attribute).AddUObject(...)를 쓴다.CharacterMovement->MaxWalkSpeed 동기화는 이 델리게이트로 처리했다.PostGameplayEffectExecute에서 한다.| Duration Policy | 용도 | 실제 사용 예 |
|---|---|---|
| Instant | 즉시 1회 | GE_Damage, GE_JumpCost, GE_Revive |
| Duration | 일정 시간 지속 | 버프/디버프 |
| Infinite | 수동 제거까지 | GE_SprintCost, GE_StaminaRegen |
FGameplayEffectContextHandle Ctx = ASC->MakeEffectContext();
FGameplayEffectSpecHandle Spec = ASC->MakeOutgoingSpec(EffectClass, 1.f, Ctx);
Spec.Data->SetSetByCallerMagnitude(Tag, Value);
ASC->ApplyGameplayEffectSpecToSelf(*Spec.Data.Get());
Set by Caller → Data Tag 지정이 필요하다.State.Downed → Sprint/Interact 차단State.Crouching → Sprint 차단 (웅크린 채 스프린트 시 "비용만 내고 효과 없는" 버그 해결)CancelAbilities(&TagContainer)로 특정 어빌리티만 골라 취소.AddLooseGameplayTag() / RemoveLooseGameplayTag(): 코드에서 임시 태그 부여.
LocalPredicted: 클라이언트 예측 실행ServerOnly: 클라이언트가 입력해도 서버에서 실행 ← 서버 권위 로직에 적합ServerOnly 어빌리티는 활성화는 자동으로 서버에 전달되지만, 취소(CancelAbilities)는 전달되지 않는다.
void APlayerCharacter::InteractReleased(const FInputActionValue& Value)
{
if (HasAuthority()) Server_CancelInteract_Implementation();
else Server_CancelInteract(); // 클라 → 서버 전달
}
리슨 서버 + 4인 구조. 멀티플레이 버그의 대부분은 아래 원칙을 안 지켜서 생겼다.
역할을 명확히 나누면 대부분의 멀티 버그가 예방된다.
| 구분 | 담당 | 실제 예시 |
|---|---|---|
HasAuthority() | 상태 결정 (서버 권위) | 다운/사망 판정, 타이머, 데미지 적용 |
OnRep_ | 클라이언트 화면 연출 | 래그돌, 카메라 부착, 화면 효과 |
IsLocallyControlled() | 내 화면에만 보이는 것 | 1인칭 카메라, 포스트프로세스, 머리 숨김 |
적용 예 — 사망 처리:
서버: BleedOut → HandleDeath (상태 결정 + 타이머)
↓ ApplyDeathEffects (서버 자기 화면)
bIsDead 복제 ↓
클라: OnRep_IsDead → ApplyDeathEffects (클라 화면 연출)
| 종류 | 방향 | 용도 |
|---|---|---|
UFUNCTION(Server, Reliable) | 클라 → 서버 | 입력 전달, 취소 요청 |
UFUNCTION(NetMulticast, Unreliable) | 서버 → 전체 | 몽타주/이펙트 등 시각 효과 |
UFUNCTION(Client, Reliable) | 서버 → 특정 클라 | 개인 알림 |
Unreliable이 적절(한 프레임 놓쳐도 무방, 네트워크 부담 적음)..h에 Multicast_X() 선언 → .cpp에 Multicast_X_Implementation() 구현 → 호출은 Multicast_X()(접미사 없이).
_Implementation 없이 구현하면 "unresolved external" 링커 에러.Multicast_X(접미사 없는 본체)를 직접 정의하면 "already defined" 에러 (엔진이 자동 생성함).// .h
UPROPERTY(ReplicatedUsing = OnRep_IsDowned)
bool bIsDowned = false;
// .cpp — 반드시 등록해야 복제됨
void APlayerCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Out) const
{
Super::GetLifetimeReplicatedProps(Out);
DOREPLIFETIME(APlayerCharacter, bIsDowned);
}
.h에 Replicated를 붙였는데 GetLifetimeReplicatedProps에 등록을 안 하면 복제가 안 된다. 둘 다 필요.NetMulticast RPC로 서버가 전파해야 한다.ServerOnly 어빌리티 안에서 Multicast를 호출하면 자연스럽게 서버 기점이 된다.Controller->GetPlayerViewPoint()를 써야 정확하다. 이 값은 복제된다.Get Base Aim Rotation을 써야 다른 클라이언트에서 보인다.Pawn)은 Visibility 채널을 Ignore하는 경우가 많다.ECC_Pawn 채널을 쓴다.
가장 시간을 많이 쓴 영역. State Machine으로 지속 상태를, Montage로 단발 액션을, Layered Blend로 상하체 분리를 처리하는 조합이 핵심이었다.
다른 스켈레톤(Manny)의 애니메이션을 프로젝트 캐릭터(Astronaut)로 가져오는 표준 절차:
Auto Create Retarget Chains → Retarget Root = pelvisScale Horizontal = 0 → 루트 수평 이동 제거 = In-Place 변환Rotation Alpha 조정 → 좌우 이동 애니메이션이 90도 틀어지는 문제 해결Export Selected Animations (Suffix _Astro 등)| 방식 | 이동 주체 | 용도 |
|---|---|---|
| Root Motion | 애니메이션에 구워진 루트 이동 | 컷신, 정밀 공격 이동 |
| In-Place | CharacterMovement | 일반 이동 (본 프로젝트) |
Root Motion Mode를 조정한다.Additive Settings > Additive Anim Type을 직접 확인한다. 일반 재생용은 No additive로.Direction(-180SpeedCalculate Direction 노드: 속도 벡터가 몸 정면 기준 몇 도인지 반환. 입력은 Get Velocity + Get Actor Rotation.Get Base Aim Rotation의 Pitch가 0~360 범위로 나올 수 있다 (아래를 보면 -30이 아니라 330).Normalize Axis 노드로 -180~180 정규화 후 사용.Normalize Axis부터 의심한다.실전에서 필요했던 전환들:
| 전환 | 조건 | 이유 |
|---|---|---|
| Ground → Jump | bIsFalling && VerticalVelocity > 100 | 점프(위로) |
| Ground → Fall | bIsFalling && VerticalVelocity <= 100 | 낙하 |
| Land → Jump | bIsFalling && VerticalVelocity > 100 | 연속 점프 대응 |
| Land → Ground | Speed > 10 || TimeRemaining < 0.1 | 착지 즉시 이동 반응성 |
New Save cached pose (이름 지정)Use cached pose '이름' 노드를 만들어 연결 (여러 개 가능)1인칭에서 팔 흔들림이 시야를 방해할 때, 아이템 자세를 상체만 덮을 때 사용.
하체(이동 Blend Space) → Base Pose
Layered blend per bone → 출력
상체(아이템 Idle 포즈) → Blend Poses 0
Branch Filters에 Bone Name(예: spine_01)과 Blend Depth 지정.clavicle_r/clavicle_l을 Branch Filter에 직접 추가.Add Blend Pin → 사용할 값 선택.Default Pose를 활용하면 나머지 값을 전부 커버하므로 모든 값에 핀을 만들 필요 없다.EItemCategory(Weapon/Tool/Light), Weapon 안쪽은 EWeaponAnimType(Pistol/Rifle)로 2단 분기해서 무기 종류를 구분했다.| State Machine | Montage | |
|---|---|---|
| 성격 | 조건에 따라 지속/반복 | 특정 순간 1회 재생 |
| 예시 | 걷기, 웅크림, 다운 | 공격, 발사, 채굴, 피격 |
Create AnimMontage.Slot '이름' 노드를 배치해야 재생된다.UpperBody)을 만들고 Layered blend per bone으로 상체에만 적용.에셋 없이 상체 조준, 팔 각도 보정에 사용한 실용 도구.
Rotation Mode: Add to Existing, Rotation Space: Bone Space.+ Key.bUseControllerRotationYaw = true; // 몸통이 카메라 좌우를 즉시 따라감
GetCharacterMovement()->bOrientRotationToMovement = false;
FirstPersonCamera->bUsePawnControlRotation = true; // Pitch는 독립
PC->PlayerCameraManager->ViewPitchMin/Max.| 상황 | 해결 |
|---|---|
| 머리/헬멧이 가림 | HideBoneByName("head") — IsLocallyControlled()일 때만 |
| 사망 시 목 없는 시체 | UnHideBoneByName()으로 복원 |
| 웅크림 시 상체가 가림 | OnStartCrouch/OnEndCrouch에서 카메라 오프셋 앞으로 |
| 다운 시 화면 어두움 | 캡슐 부착 유지 + 오프셋 조정 (본 부착은 축 뒤틀림으로 실패) |
| 상황 | 부착 대상 | 이유 |
|---|---|---|
| 사망(래그돌) | 머리 본 | 물리 시뮬레이션이라 위치가 매번 다름 |
| 다운(애니메이션) | 캡슐 + 오프셋 | 고정 자세라 좌표로 충분, 본 축 뒤틀림 회피 |
// ✗ 생성자 — BP가 덮어쓰기 전의 C++ 기본값만 얻음 (0,0,64)
DefaultCameraLocation = FirstPersonCamera->GetRelativeLocation();
// ✓ BeginPlay — BP에서 설정한 실제 값 (20,0,56)
void APlayerCharacter::BeginPlay()
{
DefaultCameraLocation = FirstPersonCamera->GetRelativeLocation();
}
BeginPlay에서 캐싱한다. 생성자는 BP 오버라이드 이전 시점이다.체력 0 → 다운(EnterDownedState)
├→ 팀원이 E 홀드 → 부활(Revive) → 체력 일부 회복
└→ 출혈 타이머 만료 → 사망(HandleDeath) → 관전
bIsDowned/bIsDead 둘 다 복제 + OnRep_으로 연출. 다운/사망을 대칭 구조로 만들면 읽기 쉽다.IInteractableInterface를 구현 → 다운된 플레이어를 E로 조준하면 기존 상호작용 시스템이 그대로 부활 처리.Started + Completed 양쪽에 바인딩해야 "떼는 시점"을 안다.EndAbility를 거치므로 정리 코드는 거기 한 곳에.OnHealthChanged에서 취소, 출혈 타이머는 PauseTimer/UnPauseTimer(남은 시간 유지).SpectatorPawn + SpringArm(궤도 회전) + Camera 구조.SpectateTarget을 복제 변수로 두면 서버/클라가 같은 대상을 따라가 위치 보정 충돌이 없다.Triggered가 아니라 Started (Triggered는 누르는 동안 매 프레임 발동 → 대상이 왔다갔다 함).TActorIterator 순서는 보장되지 않으므로 Sort로 순서 고정 (안 하면 클릭할 때마다 엉뚱한 대상).ProbeChannel = ECC_Camera: 벽은 막고 플레이어 몸은 통과(대부분 벽은 Camera Block, Pawn은 Ignore).Clear All Mappings 필수).bIsOpen 복제) + 스위치가 EditInstanceOnly로 대상 문을 개별 참조.EditInstanceOnly가 핵심: BP 기본값이 아니라 레벨에 배치된 인스턴스마다 다른 값 지정 가능.bIsOpen만 복제하고 각 머신이 FInterpConstantTo + InterpEaseInOut로 자체 애니메이션(위치를 매 프레임 복제하지 않음). 도착하면 SetActorTickEnabled(false)로 Tick 중지.OffCol)에 갇힌 당사자가 겹쳐 있으면 → 즉시 해제되어 "갇혔는데 덫이 안 보이는" 버그.Contains 가드) ② 배열 추가를 상태 변경보다 먼저 ③ 위치를 물리적으로 분리(가장 확실).Add Socket → Add Preview Asset로 실시간 위치 확인.// ✗ 캡슐(루트)에 붙이면 소켓이 없어 원점(몸통 중앙)에 박힘
Item->AttachToComponent(Pawn->GetRootComponent(), Rules);
// ✓ 스켈레탈 메시 + 소켓 이름
Item->AttachToComponent(Char->GetMesh(), Rules, Def->AttachSocket);
SnapToTargetNotIncludingScale은 소켓 Scale을 무시 → 크기 조정은 아이템 BP의 MeshComp에서.UItemDefinition(DataAsset)에 Category / AttachSocket / Mesh / Weight를 넣고 BP에서 관리.EWeaponAnimType enum을 WeaponItem에 두고 ABP가 읽어 분기 → 무기 종류가 늘어도 데이터로 관리.ItemBase)이 Static MeshComponent 기준으로 콜리전·부착·표시를 관리한다.Make Static Mesh로 변환하는 게 시스템과 일관된다.Make Static Mesh는 원본에 딸린 여분 오브젝트(바닥 Plane, 총알 등 잔여 본)까지 통째로 합친다 → 블렌더 등으로 원본에서 총만 추출해 재임포트하는 게 근본 해결.ItemBase의 콜리전 관리 밖이라 예외 처리가 계속 쌓인다(줍기 시 물리 솟구침, 표시/부착 문제).PrimaryDataAsset으로 정의(부위별 머티리얼 묶음) → 색 추가가 DA 수정만으로.ColorIndex를 복제 + OnRep으로 각 클라이언트가 머티리얼 교체.Forward/Right 벡터의 Dot Product로 앞/뒤/좌/우 판정:
const float ForwardDot = FVector::DotProduct(Forward, ToAttacker);
const float RightDot = FVector::DotProduct(Right, ToAttacker);
if (FMath::Abs(ForwardDot) >= FMath::Abs(RightDot))
return (ForwardDot >= 0) ? Front : Back;
else
return (RightDot >= 0) ? Right : Left;
ReceivePlayerDamage(Damage, DamageCauser)에서 얻는다(OnHealthChanged엔 공격자 정보가 없음).| 코드 | 의미 | 대응 |
|---|---|---|
C#### | 진짜 컴파일러 에러 | 이것만 보면 됨 |
E#### | IntelliSense 가짜 에러 | 무시 (특히 ArchiveProxy.h, FTextureBuildSettings) |
MSB3073 | 빌드 실패 통보 | 원인이 아님. 진짜 에러를 찾아야 함 |
error 검색.#include found after .generated.h → .generated.h는 반드시 마지막 include.C4129 + C1004 → 문자열의 따옴표가 스마트 따옴표로 깨짐 → 영어로 다시 타이핑.UCLASS() + class ... { + GENERATED_BODY() 한 벌만..generated.h include 위에 둔다..cpp에 구현이 없음 (특히 머지 후 함수 누락)..uasset은 LFS 포인터(텍스트)로 저장되는데, 머지 충돌 시 포인터 안에 마커가 박힌다:
version https://git-lfs.github.com/spec/v1
<<<<<<< HEAD
oid sha256:00502451... size 45990
=======
oid sha256:b1d4b54f... size 46208
>>>>>>> d609341...
The summary for the package ... is invalid).진단
findstr /s /m "<<<<<<< HEAD" Content\*.uasset
type "경로\파일.uasset"
복구
git checkout <커밋해시> -- "경로/파일.uasset"
git lfs pull
GetLifetimeReplicatedProps가 한쪽 버전으로 덮어써짐.!bIsDead && !bIsDowned).HasAuthority() 같은 방어 코드는 유지.This repository exceeded its LFS budget → 저장소 소유자가 Data pack 구매 또는 용량 증설해야 함(개인이 못 고침).IInteractableInterface → 아이템/문/플레이어 누가 구현하든 E키 하나로 동작.ReceivePlayerDamage(float, AActor*) → 몬스터 파트가 호출할 진입점.SwingMontage+Multicast_PlaySwing을 구현해둔 걸 발견한 적이 있다.Blend Poses by Enum + Default Pose 덕에 곡괭이 추가 시 ABP를 거의 안 건드렸다.showdebug abilitysystem — 어트리뷰트 값 + 활성 태그 실시간 확인.UFUNCTION(Exec) 치트로 테스트 시나리오 재현 (Cheat_Damage, Cheat_SetColor 등).
Cheat_DamageDelayed 20 3: 3초 뒤 데미지 → 그 사이 홀드).AddOnScreenDebugMessage의 Key를 고정값으로 주면 같은 줄이 갱신됨(-1이면 계속 쌓임).Print String으로 이벤트 호출 여부부터 확인한 사례.이 프로젝트에서 가장 크게 배운 건 "기능을 만드는 것"보다 "구조를 잡는 것"이 중요하다는 점이다.
HasAuthority/OnRep/IsLocallyControlled 3분할만 지켜도 버그의 대부분이 예방됐다.

우리들의 게임 발매 이야기

안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다


안녕하세요. 플밍 4기 입니다. 게임 개발을 배우기 전 네트워크 엔지니어 도메인에서 익히고 배웠던 네트워크 이론에 대한 기초 입니다. 학습에 도움이 되길 바라며 공유 드립니다.
XR을 활용한 게임 개발 3기(유니티) 수강생입니다. 곧 수료 하지만 앞으로 이곳에 가끔 저의 개발 경험이 나 지식 기록할까 합니다. 더 나아가 이 사이트가 제 개인위키의 역할을 할 수 있으면 좋겠습니다. 한국 게임 시장을 흔들겠습니다
