IMC 관리 구조 설계
- 현재는 플레이어의 입력은 PlayerCharacter, UI 관련 입력은 Controller쪽에서 처리 중
- 게임 특성에 의해 캐릭터가 로봇으로 전환되면 입력 키들이 변경될 가능성 있음
- 같은 IMC 내에서 바인딩 되는 함수들만 적절히 변경하여 사용도 가능하지만 관리가 힘들고 헷갈림
- 이미 UI 관련 IMC는 따로 관리할 예정이었기 때문에 로봇과 캐릭터의 IMC도 분리하기로 결정
장점
- 분리된 IMC들의 개별 역할 명확
- 코드를 구성함에 있어 모호한 부분이 없음
- 각 IMC의 수정이 다른 클래스에 영향을 주지 않음
로봇 탑승 및 하차
PlayerCharacter
헤더
class CR4S_API APlayerCharacter : public AAlsCharacter
{
...
virtual void UnPossessed() override;
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "Player Character")
TObjectPtr<UInteractionComponent> Interaction;
...
};
소스
void APlayerCharacter::UnPossessed()
{
if (const APlayerController* PC = Cast<APlayerController>(GetController()))
{
UEnhancedInputLocalPlayerSubsystem* InputSubsystem{ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PC->GetLocalPlayer())};
if (IsValid(InputSubsystem))
{
InputSubsystem->RemoveMappingContext(InputMappingContext);
}
}
Super::UnPossessed();
}
- 빙의 해제시 호출되는 UnPossessed 오버라이드
- 추가되어 있던 IMC 제거
- 상호작용을 하기 위해 필요한 컴포넌트 추가
ModularRobot
헤더
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "ModularRobot.generated.h"
class APlayerCharacter;
class UInteractableComponent;
class UInputAction;
class UInputMappingContext;
class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;
UCLASS()
class CR4S_API AModularRobot : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AModularRobot();
#pragma region ChangePossess
void MountRobot(AController* InController);
UFUNCTION(BlueprintCallable)
void UnMountRobot();
#pragma endregion
#pragma region OverrideFunctions
public:
// Called every frame
virtual void Tick(float DeltaTime) override;
// Called to bind functionality to input
virtual void SetupPlayerInputComponent(UInputComponent* PlayerInputComponent) override;
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
// === Character Override Functions ===
virtual void NotifyControllerChanged() override;
#pragma endregion
#pragma region MoveFunctions
protected:
UFUNCTION()
void Move(const FInputActionValue& Value);
UFUNCTION()
void Look(const FInputActionValue& Value);
UFUNCTION()
void StartJump(const FInputActionValue& Value);
UFUNCTION()
void StopJump(const FInputActionValue& Value);
UFUNCTION()
void StartSprint(const FInputActionValue& Value);
UFUNCTION()
void StopSprint(const FInputActionValue& Value);
#pragma endregion
#pragma region InputActions
protected:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputMappingContext> InputMappingContext;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> LookAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> MoveAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> SprintAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> JumpAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> DashAction;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Settings|Modular Robot", Meta = (DisplayThumbnail = false))
TObjectPtr<UInputAction> AttackAction;
#pragma endregion
#pragma region MountOffset
private:
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Mount", meta = (AllowPrivateAccess = "true"))
FVector UnMountLocation;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components", meta = (AllowPrivateAccess = "true"))
FName MountSocketName;
#pragma endregion
#pragma region Components
private:
// === Components ===
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<USpringArmComponent> CameraBoom;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UCameraComponent> FollowCamera;
UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Components", meta = (AllowPrivateAccess = "true"))
TObjectPtr<UInteractableComponent> InteractComp;
#pragma endregion
#pragma region Cached
TObjectPtr<APlayerCharacter> MountedCharacter;
#pragma endregion
};
소스
#include "ModularRobot.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "PlayerCharacter.h"
#include "CR4S/Character/CharacterController.h"
#include "Camera/CameraComponent.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "Gimmick/Components/InteractableComponent.h"
// Sets default values
AModularRobot::AModularRobot():
UnMountLocation(FVector(-200.f,0.f,0.f)),
MountSocketName("cockpit")
{
//Set Tick
PrimaryActorTick.bCanEverTick = false;
// Set size for collision capsule
GetCapsuleComponent()->InitCapsuleSize(42.f, 96.0f);
//Set Mesh option
if (IsValid(GetMesh()))
{
GetMesh()->SetRelativeLocation_Direct({0.0f, 0.0f, -92.0f});
GetMesh()->SetRelativeRotation_Direct({0.0f, -90.0f, 0.0f});
GetMesh()->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickMontagesWhenNotRendered;
GetMesh()->bEnableUpdateRateOptimizations = false;
}
// Don't rotate when the controller rotates.
bUseControllerRotationPitch = false;
bUseControllerRotationYaw = true;
bUseControllerRotationRoll = false;
// Configure character movement
GetCharacterMovement()->bOrientRotationToMovement = true; // Character moves in the direction of input...
GetCharacterMovement()->RotationRate = FRotator(0.0f, 500.0f, 0.0f); // ...at this rotation rate
// Note: For faster iteration times these variables, and many more, can be tweaked in the Character Blueprint
// instead of recompiling to adjust them
GetCharacterMovement()->JumpZVelocity = 500.f;
GetCharacterMovement()->AirControl = 0.35f;
GetCharacterMovement()->MaxWalkSpeed = 500.f;
GetCharacterMovement()->MinAnalogWalkSpeed = 20.f;
GetCharacterMovement()->BrakingDecelerationWalking = 2000.f;
GetCharacterMovement()->BrakingDecelerationFalling = 1500.0f;
// Create a camera boom (pulls in towards the player if there is a collision)
CameraBoom = CreateDefaultSubobject<USpringArmComponent>(TEXT("CameraBoom"));
CameraBoom->SetupAttachment(RootComponent);
CameraBoom->TargetArmLength = 1200.0f; // The camera follows at this distance behind the character
CameraBoom->SetWorldRotation((FRotator(-15, 0, 0)));
CameraBoom->bUsePawnControlRotation = true; // Rotate the arm based on the controller
// Create a follow camera
FollowCamera = CreateDefaultSubobject<UCameraComponent>(TEXT("FollowCamera"));
FollowCamera->SetupAttachment(CameraBoom, USpringArmComponent::SocketName); // Attach the camera to the end of the boom and let the boom adjust to match the controller orientation
FollowCamera->bUsePawnControlRotation = false; // Camera does not rotate relative to arm
//InteractableComponent
InteractComp=CreateDefaultSubobject<UInteractableComponent>(TEXT("InteractComp"));
}
void AModularRobot::MountRobot(AController* InController)
{
if (!IsValid(InController)) return;
ACharacter* PreviousCharacter=Cast<ACharacter>(InController->GetPawn());
if (IsValid(PreviousCharacter))
{
MountedCharacter=Cast<APlayerCharacter>(PreviousCharacter);
PreviousCharacter->SetActorEnableCollision(false);
PreviousCharacter->SetActorTickEnabled(false);
FAttachmentTransformRules AttachRule(EAttachmentRule::SnapToTarget,true);
PreviousCharacter->AttachToComponent(
GetMesh(),
AttachRule,
MountSocketName
);
}
InController->UnPossess();
InController->Possess(this);
}
void AModularRobot::UnMountRobot()
{
ACharacter* NextCharacter=MountedCharacter.Get();
if (IsValid(NextCharacter))
{
NextCharacter->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
FVector UnMountOffset=GetActorForwardVector()*UnMountLocation;
FVector DropLocation=GetActorLocation()+UnMountOffset;
NextCharacter->TeleportTo(DropLocation,NextCharacter->GetActorRotation(),false,true);
NextCharacter->SetActorEnableCollision(true);
NextCharacter->SetActorTickEnabled(true);
}
if (AController* CurrentController=GetController())
{
CurrentController->UnPossess();
if (IsValid(NextCharacter))
{
CurrentController->Possess(NextCharacter);
}
}
MountedCharacter=nullptr;
}
// Called when the game starts or when spawned
void AModularRobot::BeginPlay()
{
Super::BeginPlay();
if (InteractComp)
{
InteractComp->OnTryInteract.BindUObject(this,&AModularRobot::MountRobot);
}
}
void AModularRobot::NotifyControllerChanged()
{
const auto* PreviousPlayer{Cast<APlayerController>(PreviousController)};
if (IsValid(PreviousPlayer))
{
auto* InputSubsystem{ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PreviousPlayer->GetLocalPlayer())};
if (IsValid(InputSubsystem))
{
InputSubsystem->RemoveMappingContext(InputMappingContext);
}
}
auto* NewPlayer{Cast<APlayerController>(GetController())};
if (IsValid(NewPlayer))
{
NewPlayer->InputYawScale_DEPRECATED = 1.0f;
NewPlayer->InputPitchScale_DEPRECATED = 1.0f;
NewPlayer->InputRollScale_DEPRECATED = 1.0f;
auto* InputSubsystem{ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(NewPlayer->GetLocalPlayer())};
if (IsValid(InputSubsystem))
{
FModifyContextOptions Options;
Options.bNotifyUserSettings = true;
InputSubsystem->AddMappingContext(InputMappingContext, 0, Options);
}
}
Super::NotifyControllerChanged();
}
// Called every frame
void AModularRobot::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
}
void AModularRobot::Move(const FInputActionValue& Value)
{
if (!Controller) return;
//Move Logic
// input is a Vector2D
FVector2D MoveInput = Value.Get<FVector2D>();
if (!FMath::IsNearlyZero(MoveInput.X))
{
AddMovementInput(GetActorForwardVector(),MoveInput.X);
}
if (!FMath::IsNearlyZero(MoveInput.Y))
{
AddMovementInput(GetActorRightVector(),MoveInput.Y);
}
}
void AModularRobot::Look(const FInputActionValue& Value)
{
FVector2D LookInput=Value.Get<FVector2D>();
AddControllerYawInput(LookInput.X);
AddControllerPitchInput(LookInput.Y);
}
void AModularRobot::StartJump(const FInputActionValue& Value)
{
// Jump 함수는 Character가 기본 제공
if (Value.Get<bool>())
{
Jump();
}
}
void AModularRobot::StopJump(const FInputActionValue& Value)
{
// StopJumping 함수도 Character가 기본 제공
if (!Value.Get<bool>())
{
StopJumping();
}
}
void AModularRobot::StartSprint(const FInputActionValue& Value)
{
if (GetCharacterMovement())
{
GetCharacterMovement()->MaxWalkSpeed = 800.f;
}
}
void AModularRobot::StopSprint(const FInputActionValue& Value)
{
if (GetCharacterMovement())
{
GetCharacterMovement()->MaxWalkSpeed = 500.f;
}
}
// Called to bind functionality to input
void AModularRobot::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// Set up action bindings
if (UEnhancedInputComponent* EnhancedInputComponent = Cast<UEnhancedInputComponent>(PlayerInputComponent)) {
if (ACharacterController* MyController=Cast<ACharacterController>(GetController()))
{
// Moving
EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AModularRobot::Move);
//Looking
EnhancedInputComponent->BindAction(LookAction,ETriggerEvent::Triggered, this, &AModularRobot::Look);
//Sprinting
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Triggered, this, &AModularRobot::StartSprint);
EnhancedInputComponent->BindAction(SprintAction, ETriggerEvent::Completed, this, &AModularRobot::StopSprint);
// Jumping
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AModularRobot::Jump);
EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AModularRobot::StopJumping);
}
}
}
- IMC 및 인풋 액션 관련 변수 추가
- 기본 이동 및 화면 회전 관련 함수들 구현 및 바인딩
- 상호작용되어야 하는 액터들에 추가할 예정이었던 InteractableComponent 추가
- BeginPlaye에서 InteractableComponent의 OnTryInteract 델리게이트에 MountRobot 바인딩
ㅡ> 캐릭터가 상호작용하면 바인딩된 함수 호출되는 구조 - MountRobot
- 탑승한 캐릭터 캐싱
- 탑승한 캐릭터의 콜리전 및 Tick 관련 설정 비활성화
- 소켓 "cockpit" 위치에 캐릭터를 SnapToTarget 으로 부착
- UnPossess 호출하여 이전 캐릭터 빙의 해제
- 현재 로봇 클래스로 Possess 호출
- UnMountRobot
- 캐싱해 두었던 캐릭터 사용
- DetachFromActor를 이용해 부착 해제
- 각종 벡터 연산을 통해 로봇 기준 뒤쪽에 내려지도록 위치 계산
- TeleportTo로 캐릭터 위치 이동
- 콜리전, 틱 관련 설정 활성화
- 현재 빙의된 로봇 클래스에 UnPossess 호출
- 탑승 해제하는 캐릭터쪽으로 Possess
- 탑승된 캐릭터를 저장해 뒀던 변수 비우기
에디터 설정
- 캐릭터 BP에 임시 상호작용 키 이벤트 할당
- TryStartInteraction 함수를 호출하면 OnTryInterace 델리게이트가 호출
- 로봇 BP에서 임시 하차 키 이벤트 할당
결과
참고자료
'프로젝트 > CR4S' 카테고리의 다른 글
Core Reboot:Four Seasons - #13 Control Rig을 이용한 애니메이션 시퀀스 제작 (0) | 2025.05.23 |
---|---|
Core Reboot:Four Seasons - #12 ALS 오버레이 모드 추가를 위한 구조 파악 (0) | 2025.05.22 |
Core Reboot:Four Seasons - #10 인풋 버퍼 시스템 구현 및 공격 애니메이션 상체 블렌딩 (0) | 2025.05.20 |
Core Reboot:Four Seasons - #9 무기 위치에 맞는 공격 범위 추적 (0) | 2025.05.19 |
Core Reboot:Four Seasons - #8 스테이터스 UI 제작 및 연동 (0) | 2025.05.16 |