프로젝트/CCFF

미친 선인장과 분노의 버섯 - #15 버퍼를 이용한 선입력(예약) & 키 조합을 통한 액션 및 상태에 따른 동작 다변화

2h1824 2025. 4. 15. 20:05

버퍼를 이용한 선입력 기능

버퍼 구조체 

USTRUCT(BlueprintType)
struct FBufferedInput
{
	GENERATED_BODY()
public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Buffer")
	ECharacterState InputState;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Buffer")
	EAttackType InputAttack;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Buffer")
	float BufferedTime;
	FBufferedInput(): InputState(ECharacterState::Normal),InputAttack(EAttackType::None), BufferedTime(0.f) {}
	FBufferedInput(ECharacterState InState, EAttackType InAction, const float InTime): InputState(InState), InputAttack(InAction), BufferedTime(InTime) {}
};
  • 캐릭터 상태, 입력된 액션, 입력 시간으로 구성된 버퍼 선언
  • 비어 있는 버퍼용 생성자
  • 저장된 버퍼용 생성자

버퍼 로직 추가

//BaseCharacter.h
#pragma region Buffer
	UPROPERTY()
	FBufferedInput InputBuffer;
	UPROPERTY()
	float BufferThreshold;
#pragma endregion

//BaseCharacter.cpp
ABaseCharacter::ABaseCharacter():
	CurrentActivatedCollision(-1),
	BufferThreshold(0.5f),
    ...
{
	...
}

void ABaseCharacter::ExecuteBufferedAction()
{
	const float CurrentTime=GetWorld()->GetTimeSeconds();
	const int32 InputAction=static_cast<int32>(InputBuffer.InputAttack);
	//Execute Buffered Input Action
	if (InputAction>=0&&InputAction<15&&(CurrentTime-InputBuffer.BufferedTime<=BufferThreshold))
	{
		ExecuteActionByIndex(InputBuffer.InputState,InputAction);
		//UE_LOG(LogTemp,Warning,TEXT("Execute Buffered Input(Index: %d)"),InputAction);
	}
	// Clear Buffer
	InputBuffer=FBufferedInput();
}

void ABaseCharacter::ExecuteActionByIndex(ECharacterState InState, const int32 Index)
{
	if ((CurrentCharacterState==ECharacterState::Normal&&GetCharacterMovement()->IsFalling()==false)||
		(CurrentCharacterState==ECharacterState::Normal&&GetCharacterMovement()->IsFalling()&&Index>7&&Index<14)||
		(CurrentCharacterState==ECharacterState::Hitted&&Index==15)||
		(InState==ECharacterState::Normal))
	{
		if (Index==15&&StatusComponent->GetBurstMeter()>=2500)
		{
			StatusComponent->AddBurstMeter(-2500);
		}
		//UE_LOG(LogTemp,Warning,TEXT("Attack1 Called !!"));
		ServerRPCAction(InState,GetWorld()->GetGameState()->GetServerWorldTimeSeconds(), Index);
		// Play Montage in Owning Client
		if (HasAuthority()==false&&IsLocallyControlled()==true)
		{
			CurrentCharacterState=InState;
			// bCanAttack=false;
			OnRep_CurrentCharacterState();
			PlayActionMontage(InState,Index);
		}
	}
	else
	{
		//UE_LOG(LogTemp,Warning,TEXT("Buffered Action Type: %d, Buffered Input Index: %d"),InState,Index);
		InputBuffer=FBufferedInput(InState, static_cast<EAttackType>(Index),GetWorld()->GetTimeSeconds());
	}
}

void ABaseCharacter::OnRep_CurrentCharacterState()
{
	switch (CurrentCharacterState)
	{
	case ECharacterState::Normal:
		GetCharacterMovement()->SetMovementMode(MOVE_Walking);
		ExecuteBufferedAction();
		break;
	case ECharacterState::Hitted:
		// GetCharacterMovement()->SetMovementMode(MOVE_None);
		PlayActionMontage(ECharacterState::Hitted,0);
		break;
	case ECharacterState::Dead:
		// GetCharacterMovement()->SetMovementMode(MOVE_None);
		PlayActionMontage(ECharacterState::Dead,0);
		BattleComponent->ResetCombo();
	default:
		// GetCharacterMovement()->SetMovementMode(MOVE_None);
		break;
	}
}
  • 버퍼 변수 및 입력 제한시간(Threshold) 변수 추가
  • 액션 실행 함수(ExeucteActionByIndex)에서 현재 입력 액션을 수행할 수 없는 상황이라면 버퍼에 저장하도록 로직 추가
  • 버퍼에 저장된 액션 실행하는 함수 추가 (ExecuteBufferedAction())
  • 서버에서 클라이언트로 캐릭터 상태가 복제(Replication) ㅡ> 액션 실행 가능(Normal) ㅡ> 버퍼 우선적으로 검사 
    ㅡ> 버퍼에 저장된 입력이 있다면 ExecuteBufferedAction 호출 ㅡ> 함수에서 제한 시간 검사 및 실행

키 조합을 통한 액션

  • IMC에서 키 조합 관련 설정
  • 이동+특정 공격(Attack8)
    • 특정 키 지정: E
    • Triggres Chorded Action 설정 
    • Chord Action에 원하는 액션 지정: 이동 액션(IA_Move)
  • 특정 액션 + 특정 액션(IA_Burst)
    • 우선 특정 키 지정: A
    • Triggres Chorded Action 설정 
    • Chord Action에 원하는 액션 지정: 특정 액션(IA_Dodge)

캐릭터 상태에 따른 동작 다변화

void ABaseCharacter::ExecuteActionByIndex(ECharacterState InState, const int32 Index)
{
	if ((CurrentCharacterState==ECharacterState::Normal&&GetCharacterMovement()->IsFalling()==false)||
		(CurrentCharacterState==ECharacterState::Normal&&GetCharacterMovement()->IsFalling()&&Index>7&&Index<14)||
		(CurrentCharacterState==ECharacterState::Hitted&&Index==15)||
		(InState==ECharacterState::Normal))
	{
		...
	}
	else
	{
		...
	}
}

void ABaseCharacter::Attack2(const FInputActionValue& Value)
{
	if (GetCharacterMovement()->IsFalling())
	{
		ExecuteActionByIndex(ECharacterState::Attack,9);
	}
	else if (GetCharacterMovement()->MaxWalkSpeed==BalanceStats.MaxRunSpeed)
	{
		ExecuteActionByIndex(ECharacterState::Attack,2);
	}
	else
	{
		ExecuteActionByIndex(ECharacterState::Attack,1);
	}
}
  • 같은 키로 실행되는 액션 중에서 공중, 지상, 달리기 등의 상태에 따라 다른 액션이 나가도록 로직 구성
  • 공중에 있는 상태: CharacterMovement에서 IsFalling() 함수로 공중인지 체크 후 알맞은 액션 실행
    ㅡ> 추가로 공중일 때는 해당하는 특정 인덱스 범위의 액션만 실행 (지상 액션은 공중에서 실행 불가능해야 함)
  • 달리기 상태: 현재 MaxWalkSpeed를 확인하여 달리기에 해당하면 알맞은 액션 실행, 아니라면 기본 액션 실행

참고자료