원신처럼 여러 명의 캐릭터를 실시간 교대하는 시스템을 연구했다.
플레이어 컨트롤러에서 캐릭터를 여러 명 스폰 후, 조작할 캐릭터에 빙의(AController::Possess)하고 위치를 바꾸는 방식으로 구현했다.
싱글플레이에서 문제 없이 작동하는 플레이어 컨트롤러 코드를 공유해본다.
플레이어 컨트롤러가 여러 명의 캐릭터를 스폰한다.
처음 스폰한 캐릭터에 빙의한다.
여러 캐릭터를 빙의하기 때문에 Default Pawn을 AI가 플레이어 캐릭터 위치를 찾는 용도로 사용했다. (TargetingPawn)
public: // Target for Enemy AI attack (Because player character can be swapped at runtime) UPROPERTY(BlueprintReadOnly) APawn* TargetingPawn; protected: virtual void BeginPlay() override; private: void SetupSquad(); /** Character Class */ UPROPERTY(EditAnywhere, Category = "Squad") TSubclassOf<class APholderonCharacter> SquadCharClass0; UPROPERTY(EditAnywhere, Category = "Squad") TSubclassOf<APholderonCharacter> SquadCharClass1; UPROPERTY(EditAnywhere, Category = "Squad") TSubclassOf<APholderonCharacter> SquadCharClass2; /** Spawned Characters */ TStaticArray<APholderonCharacter*, 3> SquadCharacters; UPROPERTY() APholderonCharacter* CurrentCharacter; // Player characters that is waiting for battle locate here. UPROPERTY(EditDefaultsOnly) FTransform SquadInitTransform { FRotator::ZeroRotator, FVector(9999.f), FVector::OneVector };
void APholderonPlayerController::BeginPlay() { Super::BeginPlay(); if (UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(GetLocalPlayer())) { Subsystem->AddMappingContext(ControllerMappingContext, 0); } TargetingPawn = GetPawn(); SetupSquad(); } void APholderonPlayerController::SetupSquad() { UWorld* World = GetWorld(); if (World == nullptr || TargetingPawn == nullptr) return; // Spawn 3 squad characters. if (SquadCharClass0 && SquadCharClass1 && SquadCharClass2) { FActorSpawnParameters SpawnParams; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; SpawnParams.Owner = this; SquadCharacters[0] = World->SpawnActor<APholderonCharacter>(SquadCharClass0, TargetingPawn->GetActorTransform(), SpawnParams); SquadCharacters[1] = World->SpawnActor<APholderonCharacter>(SquadCharClass1, SquadInitTransform, SpawnParams); SquadCharacters[2] = World->SpawnActor<APholderonCharacter>(SquadCharClass2, SquadInitTransform, SpawnParams); } if (SquadCharacters[0]) { Possess(SquadCharacters[0]); } }
캐릭터에 빙의(AController::Possess)하면 CurrentCharacter로 지정된다.
Targeting Pawn을 빙의한 캐릭터에 붙인다.
protected: virtual void OnPossess(APawn* aPawn) override;
void APholderonPlayerController::OnPossess(APawn* aPawn) { Super::OnPossess(aPawn); CurrentCharacter = Cast<APholderonCharacter>(GetPawn()); if (TargetingPawn) { TargetingPawn->AttachToActor(Parent, FAttachmentTransformRules::SnapToTargetIncludingScale, FName("pelvis")); } }
protected: virtual void SetupInputComponent() override; private: /** Input */ UPROPERTY(EditAnywhere, Category = "Input", meta = (AllowPrivateAccess = "true")) class UInputMappingContext* ControllerMappingContext; UPROPERTY(EditAnywhere, Category = "Input", meta = (AllowPrivateAccess = "true")) UInputAction* MoveAction; UPROPERTY(EditAnywhere, Category = "Input", meta = (AllowPrivateAccess = "true")) UInputAction* LookAction; UPROPERTY(EditAnywhere, Category = "Input", meta = (AllowPrivateAccess = "true")) class UInputAction* JumpAction; UPROPERTY(EditAnywhere, Category = "Input", meta = (AllowPrivateAccess = "true")) UInputAction* SwapAction; void MoveTriggered(const FInputActionValue& Value); void LookTriggered(const FInputActionValue& Value); void JumpTriggered(); void JumpReleased(); void SwapPressed(const FInputActionValue& Value);
void APholderonPlayerController::SetupInputComponent() { Super::SetupInputComponent(); if (UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent)) { EnhancedInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &APholderonPlayerController::MoveTriggered); EnhancedInputComponent->BindAction(LookAction, ETriggerEvent::Triggered, this, &APholderonPlayerController::LookTriggered); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &APholderonPlayerController::JumpTriggered); EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &APholderonPlayerController::JumpReleased); EnhancedInputComponent->BindAction(SwapAction, ETriggerEvent::Started, this, &APholderonPlayerController::SwapPressed); } } void APholderonPlayerController::MoveTriggered(const FInputActionValue& Value) { if (CurrentCharacter) { CurrentCharacter->Move(Value); } } void APholderonPlayerController::LookTriggered(const FInputActionValue& Value) { if (CurrentCharacter) { CurrentCharacter->Look(Value); } } void APholderonPlayerController::JumpTriggered() { if (CurrentCharacter) { CurrentCharacter->Jump(); } } void APholderonPlayerController::JumpReleased() { if (CurrentCharacter) { CurrentCharacter->StopJumping(); } } void APholderonPlayerController::SwapPressed(const FInputActionValue& Value) { if (bCanSwap == false || CurrentCharacter == nullptr) return; if (Value.GetMagnitude() == 1.f && CurrentCharacter != SquadCharacters[0] && SquadCharacters[0]->GetCombatState() != ECombatState::ECS_Dead) { SwapCharacter(SquadCharacters[0]); SwapCharTimer_Start(); } else if (Value.GetMagnitude() == 2.f && CurrentCharacter != SquadCharacters[1] && SquadCharacters[1]->GetCombatState() != ECombatState::ECS_Dead) { SwapCharacter(SquadCharacters[1]); SwapCharTimer_Start(); } else if (Value.GetMagnitude() == 3.f && CurrentCharacter != SquadCharacters[2] && SquadCharacters[2]->GetCombatState() != ECombatState::ECS_Dead) { SwapCharacter(SquadCharacters[2]); SwapCharTimer_Start(); } }
기존 캐릭터와 교대할 캐릭터의 Transform을 서로 바꿔준다.
기존 캐릭터의 카메라 FOV와 Control Rotation을 유지해준다.
private: void SwapCharacter(APholderonCharacter* NewChar);
void APholderonPlayerController::SwapCharacter(APholderonCharacter* NewCharacter) { if (NewCharacter == nullptr || CurrentCharacter == nullptr) return; // Swap transform const FTransform CurrentTransform = CurrentCharacter->GetActorTransform(); CurrentCharacter->SetActorTransform(SquadInitTransform); NewCharacter->SetActorTransform(CurrentTransform); // Keep camera FOV const float CurrentFOV = CurrentCharacter->GetCameraFOV(); NewCharacter->SetCameraFOV(CurrentFOV); // Keep control rotation and possess const FRotator CurrentControlRotation = GetControlRotation(); Possess(NewCharacter); SetControlRotation(CurrentControlRotation); }
조작하지 않는 캐릭터는 Tick, Visibility 등을 비활성화하여 퍼포먼스 최적화를 하는 것이 좋다.
캐릭터가 빙의 되었을 때/풀렸을 때 호출하는 함수도 함께 쓰면 좋다.