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가 전체 빌드를 시도했을 때 나는 에러 코드다. 즉, 지금 상황은:

    1. Unreal Editor가 켜져 있음
    2. Live Coding도 활성화되어 있음
    3. 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.52초에 한 번 왕복하는 느린 움직임
    • Position Strength = 500, Velocity Strength = 50 → 모터가 관절을 움직이려는 힘과 흔들림 억제값
    • Enable Oscillation 체크 → 자동 움직임 활성화 상태
    • 컴파일 → 저장 → 플레이
  • 한 개 관절을 런타임 코드로 찾아서 반응시키는 것이 되었으니, 이제부터 본격적으로 강화학습 환경으로 연결을 시켜보자

정보#

Rider vs Rider UProject#

옵션여는 방식
RiderVisual 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 변경
  • 컴포넌트 구조 변경
  • 생성자 구조가 크게 바뀜
  • 블루프린트에 노출되는 멤버 추가/삭제

우리가 방금 한 작업은

  • 새 컴포넌트 클래스 생성
  • .hUPROPERTY 여러 개 추가

라서 Live Coding으로 될 수도 있지만, 처음엔 에디터를 닫고 Rider에서 전체 빌드하는 편이 더 안전하다

Be Your Own Lamp