본문 바로가기

Study/Unreal Engine

[Unreal] Socket Attach/Detach

UE은 소켓을 붙일 때, component 단위로 붙인다.

이는 Unreal Engine의 Actor는 좌표를 가지지 않기 때문이다.

 

그럼 우선, Component에 대해 알아야한다.

 

Hierarchy 구성은 더 복잡하지만, 중요한 클래스만 정리해보면 위 그림과 같이 구성할 수 있다.

1. Actor와 SceneComponent

다음 Blueprint Class로 생성한 Actor를 보게 되면, Details에 좌표를 결정하는 Transform이 없는 것을 확인할 수 있다. 

 

하지만 이 액터는 아래와 같이 레벨에 바로 배치가 가능하다.

상식적으로 생각해보면, 좌표 정보를 가지지 않았으므로, 레벨에 올리는 것이 불가능해야한다.

그렇다면 어떻게 배치가 가능한 것일까?

 

바로 다음 그림에서 알 수 있다.

 

블루프린트에서 Actor를 생성하면, 기본적인 루트 컴포넌트가 추가된다. SceneComponent라고 불리는 이 컴포넌트는 Transform, 좌표를 가진다. 이 SceneComponent가 있기 때문에, Actor는 좌표를 가질 수 있고, 레벨에 배치가 가능하다.

Actor.h

액터 클래스의 코드를 보면, SceneComponent를 RootComponent로 설정하여 기본적으로 생성하는 것을 볼 수 있다. 이러한 로직이 있기 때문에, BP Class로 Actor를 생성하면 바로 사용이 가능한 것이다.

그리고 SceneComponent에는 다음과 같이 Transform이 들어있다. (상위 클래스인 ActorComponent에는 Transform이 없다.)

SceneComponent.h

그럼, 캐릭터를 생성한 경우에는 어떨까?

BP Character를 생성하면, 위 그림과 같이 Capsule Component가 생성되어있고, 해당 Capsule Component가 Root Component로 설정되어 있는 것을 볼 수 있다.

 

Character의 C++ Class의 생성자를 보게 되면, CapsuleComponent를 생성하고, Root Component로 지정하는 것을 볼 수있다.

 

그리고, C++ 클래스에서 액터를 생성하고, 별도의 Root Component를 설정하고자 한다면, 다음과 같이 구성할 수 있다.

 

MyClass라는 액터를 생성하고, 해당 생성자에 CapsuleComponent를 생성, 이를 RootComponent로 만든 코드이다.

 

이 코드를 빌드하면 다음과 같이 정상적으로 RootComponent가 생성된 것을 확인할 수 있다.

 

정리해보면 다음과 같다.

  • Actor는 Transform을 가지지 않는다.
  • Actor 혹은 Actor를 상속받는 클래스를 생성할 때, 별도로 RootComponent를 생성하지 않는다면 SceneComponent가 자동으로 생성되어 RootComponent가 된다.

2. SceneComponent / PrimitiveComponent

대표적인 Component를 정리해보면 다음과 같다.

ActorComponent
액터(Actor)에 기능을 추가하는 데 사용되는 가장 기본적인 컴포넌트 클래스.
SceneComponent와 달리 공간 정보(위치, 회전, 스케일)를 직접적으로 다루지 않으며, 주로 로직이나 데이터 처리를 담당.
SceneComponenet 기본적인 트랜스폼(위치, 회전, 스케일)을 제공하는 컴포넌트
다른 많은 컴포넌트 클래스기반 클래스다.
Static Mesh Component
정적 메쉬를 렌더링하는 데 사용
Skeletal Mesh Component 뼈대(스켈레톤)를 사용해 애니메이션이 적용된 3D 모델을 렌더링
Camera Component 액터의 뷰포인트를 제공하는 카메라 역할
Audio Component 소리를 재생하는 데 사용
Box/Sphere/Capsule Collision Component 충돌을 감지하거나 물리 연산을 처리할 수 있도록 돕는 컴포넌트
Spring Arm Component 카메라나 다른 컴포넌트를 특정 거리만큼 떨어뜨려 연결
Light Component 씬에서 빛을 생성
PrimitiveComponent
SceneComponent를 상속받은 클래스 중 하나로, 주로 렌더링, 충돌 감지, 및 물리 연산과 관련된 기능을 제공
3D 공간에서 표시되거나 물리적인 상호작용을 해야 하는 모든 것의 기본 클래스.
SceneComponent, NavRelevantInterface, Interface_AsyncCompilation, PhysicsComponent 클래스들로부터 다중 상속 받는다.

 

SceneComponent의 주요 특징

  1. 공간 정보 포함
    위치(Location), 회전(Rotation), 스케일(Scale)과 같은 트랜스폼 속성을 관리
    부모-자식 관계에 따라 트랜스폼이 계층적으로 전파됩니다.
  2. 계층 구조
    다른 SceneComponent를 부모 또는 자식으로 설정하여 계층 구조(Hierarchy)를 구성
    예: 캐릭터의 손에 무기를 장착하면, 손의 움직임에 따라 무기도 움직인다.
    렌더링 및 물리 연산 없음
    SceneComponent는 렌더링이나 물리 연산 기능이 없다.
    이 기능은 PrimitiveComponent와 같은 하위 클래스에서 제공된다.
  3. 추상적 구성 요소
    주로 다른 컴포넌트나 액터의 위치를 기준으로 참조점을 정의하는 데 사용
    예: 스프링 암(Spring Arm)을 기반으로 카메라를 연결.

PrimitiveComponent의 주요 특징

  1. 렌더링 기능
    화면에 렌더링될 수 있는 기본 구성 요소를 제공
    예: 정적 메쉬, 스켈레탈 메쉬, 입자 시스템 등.
  2. 충돌 및 물리
    충돌 감지와 반응을 위한 기능을 내장
    물리적 상호작용(중력, 충격 등)을 처리 가능
  3. 머티리얼(Material) 적용
    머티리얼을 통해 시각적인 외형을 설정
  4. 트랜스폼 관리
    위치, 회전, 스케일 같은 공간적 속성을 가지고 있으며, 부모-자식 관계에서 동작
  5. LOD(Level of Detail) 지원
    렌더링 성능 최적화를 위해 LOD를 설정

그러면 처음부터 물리적 요소를 넣어서, 아예 움직이지 못한느 액터를 만들거나(Static Mesh), 스켈레탈 메시를 만들어서 사용하려면 Actor 클래스를 생성할 때, RootComponent를 PrimitiveComponent를 생성하여 설정하면 될까?

 

안타깝게도 그렇게 사용할 수 없다.

SceneComponent가 생성되었다.

 

그 이유는 PrimitiveComponent는 추상 클래스이기 때문이다.

 

따라서, 다음 PrimitiveComponent의 하위클래스를 이용하여한다.

 

인스턴스를 생성할 떄 추상클래스인지 꼭 확인하자.

30분 삽질했다.

StaticMeshComponent 정적 메쉬 렌더링
SkeletalMeshComponent 애니메이션 가능한 메쉬 렌더링
ParticleSystemComponent 입자 효과 생성
LightComponent 계열 다양한 유형의 광원 렌더링
DecalComponent 표면에 텍스처 투영
TextRenderComponent 3D 공간에서 텍스트 렌더링
BillboardComponent 카메라를 향하는 2D 이미지 렌더링
ProceduralMeshComponent 런타임 메쉬 생성
InstancedStaticMeshComponent 인스턴싱된 정적 메쉬 렌더링
Shape Components (Box, Sphere) 충돌 및 물리 연산
LandscapeComponent 대규모 지형 렌더링
CableComponent
케이블 렌더링 및 물리 처리
PostProcessComponent 후처리 효과 적용

 

이제 Socket을 이해할 첫 번째 단계가 끝났다.

3. Socket

서론에서 말한 바 있듯, Socket은 Component 단위로 붙는다. 그리고 이러한 Socket 정보들은 SkeletalMesh가 관리한다. Socket을 붙이는 함수는 SceneComponent에서 관리하고, Actor에서 이 함수를 호출하여 사용하고 있다. 즉, 정리해보면 다음과 같이 생각할 수 있다.

SceneComponent와 Actor에서 사용하는 Attach를 정리해보면 다음과 같이 볼 수 있다.

 

// Actor.h에서 사용되는 Socket Attach/Detach 로직들

/*
* 더 이상 사용되지 않음 - AttachToComponent()를 대신 사용하세요.
*/
UE_DEPRECATED(4.17, "Use AttachToComponent() instead.")
UFUNCTION(BlueprintCallable, meta = (DisplayName = "AttachRootComponentTo (Deprecated)", ScriptNoExport, AttachLocationType = "KeepRelativeOffset"), Category = "Transformation")
ENGINE_API void K2_AttachRootComponentTo(USceneComponent* InParent, FName InSocketName = NAME_None, EAttachLocation::Type AttachLocationType = EAttachLocation::KeepRelativeOffset, bool bWeldSimulatedBodies = true);

/*
* 이 액터의 RootComponent를 지정된 컴포넌트에 부착합니다.
* 등록되지 않은 컴포넌트에서는 유효하지 않습니다.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Attach Actor To Component", ScriptName = "AttachToComponent", bWeldSimulatedBodies = true), Category = "Transformation")
ENGINE_API bool K2_AttachToComponent(USceneComponent* Parent, FName SocketName, EAttachmentRule LocationRule, EAttachmentRule RotationRule, EAttachmentRule ScaleRule, bool bWeldSimulatedBodies);

// 사용 예시:
bool bSuccess = Actor->K2_AttachToComponent(ParentComponent, TEXT("SocketName"), EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, true);

/*
* 이 액터의 RootComponent를 주어진 변환 규칙으로 지정된 컴포넌트에 부착합니다.
*/
ENGINE_API bool AttachToComponent(USceneComponent* Parent, const FAttachmentTransformRules& AttachmentRules, FName SocketName = NAME_None);

// 사용 예시:
bool bSuccess = Actor->AttachToComponent(ParentComponent, FAttachmentTransformRules::KeepRelativeTransform, TEXT("SocketName"));

/*
* 더 이상 사용되지 않음 - AttachToActor()를 대신 사용하세요
*/
UE_DEPRECATED(4.17, "Use AttachToActor() instead.")
UFUNCTION(BlueprintCallable, meta = (DisplayName = "AttachRootComponentToActor (Deprecated)", ScriptNoExport, AttachLocationType = "KeepRelativeOffset"), Category = "Transformation")
ENGINE_API void K2_AttachRootComponentToActor(AActor* InParentActor, FName InSocketName = NAME_None, EAttachLocation::Type AttachLocationType = EAttachLocation::KeepRelativeOffset, bool bWeldSimulatedBodies = true);

/*
* 이 액터의 RootComponent를 지정된 액터의 RootComponent에 부착합니다.
*/
ENGINE_API bool AttachToActor(AActor* ParentActor, const FAttachmentTransformRules& AttachmentRules, FName SocketName = NAME_None);

// 사용 예시:
bool bSuccess = Actor->AttachToActor(ParentActor, FAttachmentTransformRules::KeepRelativeTransform, TEXT("SocketName"));

/*
* 이 액터의 RootComponent를 지정된 액터에 변환 규칙과 함께 부착합니다.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Attach Actor To Actor", ScriptName = "AttachToActor", bWeldSimulatedBodies = true), Category = "Transformation")
ENGINE_API bool K2_AttachToActor(AActor* ParentActor, FName SocketName, EAttachmentRule LocationRule, EAttachmentRule RotationRule, EAttachmentRule ScaleRule, bool bWeldSimulatedBodies);

// 사용 예시:
bool bSuccess = Actor->K2_AttachToActor(ParentActor, TEXT("SocketName"), EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, EAttachmentRule::KeepRelative, true);

/*
* 더 이상 사용되지 않음 - DetachFromActor()를 대신 사용하세요
*/
UE_DEPRECATED(4.17, "Use DetachFromActor() instead")
UFUNCTION(BlueprintCallable, meta = (DisplayName = "DetachActorFromActor (Deprecated)", ScriptNoExport), Category = "Transformation")
ENGINE_API void DetachRootComponentFromParent(bool bMaintainWorldPosition = true);

/*
* 이 액터의 RootComponent를 모든 SceneComponent에서 분리합니다.
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Detach From Actor", ScriptName = "DetachFromActor"), Category = "Transformation")
ENGINE_API void K2_DetachFromActor(EDetachmentRule LocationRule = EDetachmentRule::KeepRelative, EDetachmentRule RotationRule = EDetachmentRule::KeepRelative, EDetachmentRule ScaleRule = EDetachmentRule::KeepRelative);

// 사용 예시:
Actor->K2_DetachFromActor(EDetachmentRule::KeepWorld, EDetachmentRule::KeepWorld, EDetachmentRule::KeepWorld);

/*
* 주어진 규칙을 사용하여 이 액터의 RootComponent를 모든 SceneComponent에서 분리합니다.
*/
ENGINE_API void DetachFromActor(const FDetachmentTransformRules& DetachmentRules);

// 사용 예시:
Actor->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);

/*
* 이 액터의 모든 SceneComponent를 지정된 부모 SceneComponent로부터 분리합니다.
*/
ENGINE_API void DetachAllSceneComponents(USceneComponent* InParentComponent, const FDetachmentTransformRules& DetachmentRules);

// 사용 예시:
Actor->DetachAllSceneComponents(ParentComponent, FDetachmentTransformRules::KeepWorldTransform);

 

// SceneComponent.h에서 사용되는 Socket Attach/Detach 로직들.

/*
* 이 컴포넌트가 등록될 때 연결할 부모와 소켓 이름을 초기화합니다.
* 일반적으로 소유한 액터의 생성자에서 호출되며, 등록되지 않은 컴포넌트에서는 AttachToComponent보다 선호됩니다.
* @param  InParent				부착할 부모
* @param  InSocketName			부모의 선택적 소켓 이름
*/
ENGINE_API void SetupAttachment(USceneComponent* InParent, FName InSocketName = NAME_None);

/*
* 이전 방식의 EAttachLocation을 새로운 EAttachmentRules로 변환합니다. (하위 호환성 지원)
*/
static ENGINE_API void ConvertAttachLocation(EAttachLocation::Type InAttachLocation, EAttachmentRule& InOutLocationRule, EAttachmentRule& InOutRotationRule, EAttachmentRule& InOutScaleRule);

/*
* 더 이상 사용되지 않음 - AttachToComponent()를 대신 사용하세요
*/
UE_DEPRECATED(4.17, "This function is deprecated, please use AttachToComponent() instead.")
UFUNCTION(BlueprintCallable, Category = "Transformation", meta = (DeprecationMessage = "OVERRIDE BAD MESSAGE", DisplayName = "AttachTo (Deprecated)", AttachType = "KeepRelativeOffset"))
ENGINE_API bool K2_AttachTo(USceneComponent* InParent, FName InSocketName = NAME_None, EAttachLocation::Type AttachType = EAttachLocation::KeepRelativeOffset, bool bWeldSimulatedBodies = true);

/*
* 이 컴포넌트를 다른 씬 컴포넌트에 연결합니다.
* 등록 여부에 상관없이 호출 가능하지만, 생성자에서 또는 등록되지 않은 상태에서는 SetupAttachment를 사용하는 것이 좋습니다.
* @param  Parent				부착할 부모
* @param  AttachmentRules		부착 시 변환 및 용접 처리 방법
* @param  SocketName			부착할 부모의 선택적 소켓 이름
* @return 부착이 성공했거나 이미 원하는 부모/소켓에 부착된 경우 true, 부착이 거부되었고 AttachParent가 변경되지 않은 경우 false
*/
ENGINE_API virtual bool AttachToComponent(USceneComponent* InParent, const FAttachmentTransformRules& AttachmentRules, FName InSocketName = NAME_None);

/*
* 이 컴포넌트를 다른 씬 컴포넌트에 연결합니다.
* @param  Parent					부착할 부모
* @param  SocketName				부착할 부모의 선택적 소켓 이름
* @param  LocationRule				위치 규칙
* @param  RotationRule				회전 규칙
* @param  ScaleRule					크기 규칙
* @param  bWeldSimulatedBodies		물리적 시뮬레이션 바디를 용접할지 여부
* @return 부착 성공 여부
*/
UFUNCTION(BlueprintCallable, Category = "Transformation", meta = (DisplayName = "Attach Component To Component", ScriptName = "AttachToComponent", bWeldSimulatedBodies=true))
ENGINE_API bool K2_AttachToComponent(USceneComponent* Parent, FName SocketName, EAttachmentRule LocationRule, EAttachmentRule RotationRule, EAttachmentRule ScaleRule, bool bWeldSimulatedBodies);

/*
* 더 이상 사용되지 않음 - DetachFromComponent()를 대신 사용하세요
*/
UE_DEPRECATED(4.12, "This function is deprecated, please use DetachFromComponent() instead.")
UFUNCTION(BlueprintCallable, Category = "Transformation", meta = (DisplayName = "DetachFromParent (Deprecated)"))
ENGINE_API virtual void DetachFromParent(bool bMaintainWorldPosition = false, bool bCallModify = true);

/*
* 현재 연결된 모든 요소에서 이 컴포넌트를 분리합니다.
* 용접된 컴포넌트도 자동으로 분리됩니다.
* @param LocationRule				분리 시 위치 규칙
* @param RotationRule				분리 시 회전 규칙
* @param ScaleRule					분리 시 크기 규칙
* @param bCallModify				수정 호출 여부
*/
UFUNCTION(BlueprintCallable, meta = (DisplayName = "Detach From Component", ScriptName = "DetachFromComponent"), Category = "Transformation")
ENGINE_API void K2_DetachFromComponent(EDetachmentRule LocationRule = EDetachmentRule::KeepRelative, EDetachmentRule RotationRule = EDetachmentRule::KeepRelative, EDetachmentRule ScaleRule = EDetachmentRule::KeepRelative, bool bCallModify = true);

/*
* DetachmentTransformRules를 사용하여 현재 연결된 모든 요소에서 이 컴포넌트를 분리합니다.
*/
ENGINE_API virtual void DetachFromComponent(const FDetachmentTransformRules& DetachmentRules);

/*
* 이 컴포넌트의 모든 소켓 이름을 반환합니다.
* @return 컴포넌트의 모든 소켓 이름 배열
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone"))
ENGINE_API TArray<FName> GetAllSocketNames() const;

/*
* 월드 좌표계에서 소켓의 변환 정보를 가져옵니다.
* @param InSocketName 변환 정보를 가져올 소켓 또는 본의 이름
* @return 소켓이 존재하면 월드 좌표계의 소켓 변환 정보, 없으면 컴포넌트의 월드 좌표계 변환 정보
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone"))
ENGINE_API virtual FTransform GetSocketTransform(FName InSocketName, ERelativeTransformSpace TransformSpace = RTS_World) const;

/*
* 월드 좌표계에서 소켓의 위치를 가져옵니다.
* @param InSocketName 위치를 가져올 소켓 또는 본의 이름
* @return 소켓이 존재하면 월드 좌표계의 위치, 없으면 컴포넌트의 월드 좌표계 위치
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone"))
ENGINE_API virtual FVector GetSocketLocation(FName InSocketName) const;

/*
* 월드 좌표계에서 소켓의 회전(FRotator)을 가져옵니다.
* @param InSocketName 회전을 가져올 소켓 또는 본의 이름
* @return 소켓이 존재하면 월드 좌표계의 회전, 없으면 컴포넌트의 월드 좌표계 회전
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone"))
ENGINE_API virtual FRotator GetSocketRotation(FName InSocketName) const;

/*
* 월드 좌표계에서 소켓의 회전(FQuat)을 가져옵니다. (더 이상 사용되지 않음)
* @param InSocketName 회전을 가져올 소켓 또는 본의 이름
* @return 소켓이 존재하면 월드 좌표계의 회전, 없으면 컴포넌트의 월드 좌표계 회전
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone", DeprecatedFunction, DeprecationMessage="Use GetSocketRotation instead, Quat is not fully supported in blueprints."))
ENGINE_API virtual FQuat GetSocketQuaternion(FName InSocketName) const;

/*
* 주어진 이름의 소켓이 존재하는지 확인합니다.
* @param InSocketName 소켓 또는 본의 이름
* @return 소켓 존재 여부
*/
UFUNCTION(BlueprintCallable, Category="Transformation", meta=(Keywords="Bone"))
ENGINE_API virtual bool DoesSocketExist(FName InSocketName) const;

/*
* 이 컴포넌트에 소켓이 존재하는지 확인합니다.
* @return 소켓 존재 여부
*/
ENGINE_API virtual bool HasAnySockets() const;

/*
* 이 컴포넌트가 포함하는 소켓 목록을 가져옵니다.
*/
ENGINE_API virtual void QuerySupportedSockets(TArray<FComponentSocketDescription>& OutSockets) const;

 

Actor에 나온 함수들도 결국에는 SceneComponet에 있는 함수들을 사용한다. 다만, Actor에서 사용하기 좀 더 쉽게 Wrapping 한 것 뿐이다. 또한 AttachToActor도, 내부적으로 살펴보면 결국 AttachTo(AttachToComponent)를 사용한다.

 

Attach를 할 때에는, AttachTranfromRule이 반드시 필요한데, 이는 enum으로 저장된 Rule로 다음과 같이 정의되어 있다.

UENUM()
enum class EAttachmentRule : uint8
{
    KeepRelative, // 현재 상대 변환을 유지 (기존 부모와의 상대적 위치를 새로운 부모와의 상대적 위치로 사용)

    KeepWorld, // 월드 좌표를 유지 (현재 월드 위치를 기준으로 새로운 부모와의 상대 위치를 계산)

    SnapToTarget, // 부모의 부착 지점(소켓 위치/회전/스케일)에 맞게 변환을 조정
};

 

Component의 AttachTo는 SetupAttachment가 핵심으로, 이를 잘 보고 사용해야 한다. 또한 Detach의 경우 자동으로 이루어진다.

 

OnAttachmentChanged는 Attachment에 변화가 있을 때 처리하는 함수로, 이를 확장하여 오버로딩하여, 붙었을 때, 특정 로딩이 자동으로 처리되게 할 수 있다.

 

가령, 캐릭터의 손바닥에 소켓을 추가하고, 무기를 부착한 뒤, 해당 무기를 바꿔가며 확인하는 로직이 있다고 하면, 다음과 같이 작성하고 사용할 수 있다.

// 헤더 파일 (.h)에 함수 선언
UFUNCTION(BlueprintCallable, Category = "Character|Weapon")
void EquipWeapon(AActor* NewWeapon);

// CPP 파일 (.cpp)에 함수 구현
void AMyCharacter::EquipWeapon(AActor* NewWeapon)
{
    // 이전 무기를 제거
    if (CurrentWeapon)
    {
        // 기존 무기를 분리
        CurrentWeapon->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);

        // 기존 무기 제거 처리 (필요에 따라 삭제하거나 숨김 처리)
        CurrentWeapon->Destroy(); // 또는 CurrentWeapon->SetActorHiddenInGame(true);
    }

    // 새로운 무기를 설정
    if (NewWeapon)
    {
        // Weapon을 palmSocket에 부착
        USceneComponent* CharacterMesh = GetMesh(); // 캐릭터의 Mesh를 가져옴
        if (CharacterMesh)
        {
            FName SocketName = FName(TEXT("palmSocket"));
            NewWeapon->AttachToComponent(CharacterMesh, FAttachmentTransformRules::SnapToTargetIncludingScale, SocketName);

            // 현재 무기로 설정
            CurrentWeapon = NewWeapon;
        }
    }
}

 

// 새로운 무기를 생성하고 장착
AActor* NewWeapon = GetWorld()->SpawnActor<AWeapon>(WeaponClass);
MyCharacter->EquipWeapon(NewWeapon);

 

이제 이를 바탕으로, Socket을 사용할 수 있다.

'Study > Unreal Engine' 카테고리의 다른 글

[Unreal] AssetBundle  (0) 2025.01.19
[Unreal] Gameplay Tag  (0) 2025.01.19
[Unreal] PDA, Bundle  (0) 2025.01.14
[Unreal] 언리얼 엔진과 네트워크  (0) 2024.11.28
[Tip] 파일의 이름 길이  (0) 2024.10.20