
4. C++ 액터 컴포넌트 만들기
- date
- 2월 20, 2026
- status
- Public
- tags
- unrealRL
1. 소개#
강화학습을 진행하기 위해서는 매 프레임 마다
정책 모델의 Action → 특정 관절 목표 각도 → 물리 컨스트레인트에 적용 → 다리 움직임가 필요함. 지금 그래서 축소 버전으로
C++ 테스트 코드 → FrontLeg2_R 관절 목표 각도 변경 → 다리 움직임 확인을 만들어 보도록 하자
Unreal의
USkeletalMeshComponent는 물리 에셋 안의 컨스트레인트 인스턴스를 이름으로 찾을 수 있고,FConstraintInstance는 각도 드라이브 모드와 목표 회전을 설정하는 API를 제공한다(USkeletalMeshComponent | Unreal Engine 5.7 Documentation | Epic Developer Community)
2. 작업#
2.1. C++ Actor Component 만들기#
우선 C++ 클래스를 만들어보자. 해당 클래스의 목표는
- 블루프린트 모델에 붙어서
- SpiderMesh를 찾아서
- 앞다리 컨스트레인트를 찾아서
- 일정 시간 마다 목표 각도를바꿔서 관절을 흔드는
관절 제어 작업을 수행하는 것이다
툴 → 새 C++ 클래스 → 액터 컴포넌트 선택(이름 : SpiderJointControllerComponent)
(툴 변경시,
- 편집 → 에디터 개인 설정 들어가서 검색창에 : source code
- 일반 → 소스 코드 → 소스 코드 에디터 항목에서 Rider Uproject 선택)
2.1.1. 코드 작성#
SpiderJointControllerComponent.h#
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "SpiderJointControllerComponent.generated.h" class USkeletalMeshComponent; struct FConstraintInstance; UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class SPIDERRL_API USpiderJointControllerComponent : public UActorComponent { GENERATED_BODY() public: USpiderJointControllerComponent(); protected: virtual void BeginPlay() override; public: virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override; private: /** * 런타임에 제어할 스켈레탈 메시. * 비워두면 현재 액터에서 첫 번째 SkeletalMeshComponent를 자동으로 찾는다. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control") TObjectPtr<USkeletalMeshComponent> TargetSkeletalMesh = nullptr; /** * 제어할 Physics Constraint의 이름. * 현재 테스트용 기본값은 FrontLeg2_R. * 이후 다른 관절명으로 블루프린트에서 자유롭게 변경 가능하다. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control") FName TargetConstraintName = TEXT("FrontLeg2_R"); /** * 기준 목표 회전값. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control") FRotator BaseTargetOrientation = FRotator::ZeroRotator; /** * 회전 진폭. * 예: Pitch 0, Yaw 45, Roll 0 이면 해당 축을 ±45도 흔든다. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control") FRotator OscillationAmplitude = FRotator(0.0f, 45.0f, 0.0f); /** * 초당 진동 횟수. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control", meta = (ClampMin = "0.0")) float OscillationFrequencyHz = 0.5f; /** * 목표 각도로 끌고 가는 힘. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control", meta = (ClampMin = "0.0")) float PositionStrength = 500.0f; /** * 흔들림을 억제하는 감쇠값. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control", meta = (ClampMin = "0.0")) float VelocityStrength = 50.0f; /** * 모터가 사용할 수 있는 최대 힘. * 0은 엔진에서 무제한으로 처리되는 일반적인 설정이다. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control", meta = (ClampMin = "0.0")) float ForceLimit = 0.0f; /** * 테스트 진동 활성화 여부. */ UPROPERTY(EditAnywhere, Category = "Spider Joint Control") bool bEnableOscillation = true; private: void InitializeTargetConstraint(); USkeletalMeshComponent* ResolvedSkeletalMesh = nullptr; FConstraintInstance* TargetConstraint = nullptr; float ElapsedTime = 0.0f; };SpiderJointControllerComponent.cpp#
// Fill out your copyright notice in the Description page of Project Settings. #include "SpiderJointControllerComponent.h" #include "Components/SkeletalMeshComponent.h" #include "PhysicsEngine/ConstraintInstance.h" // Sets default values for this component's properties USpiderJointControllerComponent::USpiderJointControllerComponent() { // Tick에서 목표 각도를 계속 갱신해야 하므로 활성화 PrimaryComponentTick.bCanEverTick = true; } // Called when the game starts void USpiderJointControllerComponent::BeginPlay() { Super::BeginPlay(); InitializeTargetConstraint(); } // Called every frame void USpiderJointControllerComponent::TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); if (!bEnableOscillation || TargetConstraint == nullptr) { return; } ElapsedTime += DeltaTime; const float AngularFrequency = 2.0f * PI * OscillationFrequencyHz; const float OscillationValue = FMath::Sin(ElapsedTime * AngularFrequency); FRotator TargetOrientation = BaseTargetOrientation; TargetOrientation.Pitch += OscillationAmplitude.Pitch * OscillationValue; TargetOrientation.Yaw += OscillationAmplitude.Yaw * OscillationValue; TargetOrientation.Roll += OscillationAmplitude.Roll * OscillationValue; TargetConstraint->SetAngularOrientationTarget(TargetOrientation.Quaternion()); } void USpiderJointControllerComponent::InitializeTargetConstraint() { // 1. 제어할 SkeletalMeshComponent 결정 if (TargetSkeletalMesh != nullptr) { ResolvedSkeletalMesh = TargetSkeletalMesh; } else if (GetOwner() != nullptr) { ResolvedSkeletalMesh = GetOwner()->FindComponentByClass<USkeletalMeshComponent>(); } if (ResolvedSkeletalMesh == nullptr) { UE_LOG( LogTemp, Warning, TEXT("[SpiderJointController] SkeletalMeshComponent를 찾지 못했습니다.") ); return; } // 2. Physics Asset 안의 특정 Constraint 찾기 TargetConstraint = ResolvedSkeletalMesh->FindConstraintInstance(TargetConstraintName); if (TargetConstraint == nullptr) { UE_LOG( LogTemp, Warning, TEXT("[SpiderJointController] Constraint '%s'를 찾지 못했습니다."), *TargetConstraintName.ToString() ); return; } // 3. 각 모터 초기 설정 // 현재 테스트는 SLERP 기반 목표 자세 추종 방식으로 진행 TargetConstraint->SetAngularDriveMode(EAngularDriveMode::SLERP); TargetConstraint->SetOrientationDriveSLERP(true); TargetConstraint->SetAngularDriveParams( PositionStrength, VelocityStrength, ForceLimit ); // 4. 초기 목표 회전값 적용 TargetConstraint->SetAngularOrientationTarget( BaseTargetOrientation.Quaternion() ); UE_LOG( LogTemp, Log, TEXT("[SpiderJointController] Constraint '%s' 제어 초기화 완료."), *TargetConstraintName.ToString() ); }- 빌드(Ctrl+Shift+B)를 수행하면 해당 오류가 뜬다

0> Running Internal UnrealHeaderTool C:\WorkSpace\Project\UnrealProject\SpiderRL\SpiderRL\SpiderRL.uproject C:\WorkSpace\Project\UnrealProject\SpiderRL\SpiderRL\Intermediate\Build\Win64\SpiderRLEditor\Development\SpiderRLEditor.uhtmanifest -WarningsAsErrors -installed 0>UHT processed SpiderRLEditor in 1.059168 seconds (3 files written) 0>Unable to build while Live Coding is active. Exit the editor and game, or press Ctrl+Alt+F11 if iterating on code in the editor or game 0> 0>Result: Failed (OtherCompilationError) 0>Total execution time: 3.63 seconds이는 Live Coding이 켜진 상태에서 Rider가 전체 빌드를 시도했을 때 나는 에러 코드다. 즉, 지금 상황은:
- Unreal Editor가 켜져 있음
- Live Coding도 활성화되어 있음
- Rider에서 일반 빌드를 눌렀음
이러면 언리얼이:
“지금은 내가 에디터 안에서 라이브 코딩 방식으로 빌드할 준비 중인데, 바깥 Rider가 동시에 일반 빌드를 하려 한다”
고 보고 막아버린 것이다
- 언리얼에서 에디터가 켜진 상태에서 핫 리로드와 비슷한 방식으로 빌드를 수행하려면 라이브 코딩 빌드(Ctrl+Shift+F11)를 수행해야 한다
- 하지만 지금 사항은
- 새 C++ 컴포넌트 클래스를 만들었고
.h파일에UPROPERTY들을 많이 추가했고.cpp구현도 넣었기 때문에,
이건 에디터 켠 채 Live Coding으로 반영하기보다, 언리얼 에디터를 닫고 Rider에서 정식 빌드하는 게 맞는 작업임. 에디터를 끄고 빌드를 수행 후, 다시 언리얼 에디터를 다시 열어보자
- 블루프린트 모델 더블 클릭해서 블루프린트 이벤트 그래프로 이동

- 왼쪽 컴포넌트 패널에서 +추가 → 아까 생성한 컴포넌트(Spider Joint Controller Component) 선택해서 추가

- 디테일 패널에서 C++ 코드로 추가했던 설정들이 있는 지 확인

Target Skeletal Mesh = None→ 코드에서 비어 있으면 현재 액터의SpiderMesh를 자동으로 찾기Target Constraint Name = FrontLeg2_R→ 움직여보려는 오른쪽 앞다리 중간 관절Oscillation Amplitude = 마지막 축 45°→ 이 축으로 ±45도 왕복 회전을 시도Oscillation Frequency Hz = 0.5→ 2초에 한 번 왕복하는 느린 움직임Position Strength = 500,Velocity Strength = 50→ 모터가 관절을 움직이려는 힘과 흔들림 억제값Enable Oscillation체크 → 자동 움직임 활성화 상태- 컴파일 → 저장 → 플레이
한 개 관절을 런타임 코드로 찾아서 반응시키는 것이 되었으니, 이제부터 본격적으로 강화학습 환경으로 연결을 시켜보자
정보#
Rider vs Rider UProject#
| 옵션 | 여는 방식 |
|---|---|
| Rider | Visual Studio 솔루션 파일인 .sln 기준으로 열기 |
| Rider UProject | 언리얼 프로젝트 파일인 .uproject를 직접 열기 |
라이브코딩이란?#
- 출처 : Using Live Coding to recompile Unreal Engine Applications at Runtime | Unreal Engine 5.7 Documentation | Epic Developer Community
- 언리얼 엔진이 실행 중인 동안 애플리케이션의 C++ 코드를 리빌드하고 바이너리를 패치를 할 수 있는 시스템
- 언리얼 에디터에서 애플리케이션을 편집하는 경우
- 에디터에서 플레이(PIE)로 애플리케이션을 실행하는 경우
- 디버깅 용도로 에디터에 어태치된 애플리케이션의 패키지 데스크톱 빌드를 실행하는 경우
- 즉, 플레이 테스트 세션이나 에디터 작업에 방해받지 않고 C++ 클래스를 변경하고, 컴파일하고, 변경한 내용을 즉시 확인할 수 있다
언제 에디터를 껐다가 전체 빌드해야 하냐?#
아래처럼 구조가 크게 바뀌는 작업은 Live Coding보다 에디터 종료 후 Rider 빌드가 더 안전하다.
- 새 C++ 클래스 추가 직후
.h파일의UCLASS,UPROPERTY,UFUNCTION변경- 컴포넌트 구조 변경
- 생성자 구조가 크게 바뀜
- 블루프린트에 노출되는 멤버 추가/삭제
우리가 방금 한 작업은
- 새 컴포넌트 클래스 생성
.h에UPROPERTY여러 개 추가
라서 Live Coding으로 될 수도 있지만, 처음엔 에디터를 닫고 Rider에서 전체 빌드하는 편이 더 안전하다