다크소울 시리즈는 그 독특한 캐릭터 조작 시스템으로 많은 게임 개발자들에게 영감을 주었습니다. 플레이어가 느끼는 캐릭터의 반응성, 타이밍, 스태미나 관리 등은 게임의 몰입도를 결정짓는 핵심 요소로 작용합니다. 이번 글에서는 언리얼 엔진을 사용하여 다크소울 스타일의 캐릭터 조작 시스템을 완벽하게 구현할 수 있는 코드를 제공합니다. 이 글을 통해, 다크소울 같은 액션 RPG에서 요구하는 세밀한 캐릭터 조작을 구현하는 데 필요한 모든 요소를 단계별로 이해하고 적용할 수 있습니다.
목차
다크소울 스타일 캐릭터 조작의 중요성
다크소울 스타일의 게임은 그 특유의 느리고 강력한 전투와 반응성 높은 캐릭터 조작으로 유명합니다. 이를 위해서는 캐릭터의 움직임, 애니메이션, 공격 및 회피 동작, 스태미나 관리 등 모든 요소가 유기적으로 결합되어야 합니다. 이러한 시스템을 제대로 구현하는 것은 게임의 전반적인 재미와 몰입감을 극대화하는 데 필수적입니다.
이제 다크소울 스타일의 캐릭터 조작 시스템을 만들기 위한 중요한 구성 요소와 그에 대한 완성도 높은 코드를 하나씩 살펴보겠습니다.
구현해야 할 주요 기능
- 이동 및 회피
- 앞, 뒤, 좌, 우로 자유롭게 이동
- 회피 (롤링) 동작 추가
- 벽에 부딪히거나 장애물이 있을 때 반응
- 스태미나 시스템
- 캐릭터의 스태미나 관리 (달리기, 공격, 회피 시 소모)
- 스태미나가 부족할 경우 이동 속도 감소 및 공격 불가
- 애니메이션 전환 및 블렌딩
- 걷기, 달리기, 공격, 회피 등의 애니메이션 전환
- 이동 중 애니메이션과 공격, 회피 애니메이션의 블렌딩
- 전투 시스템
- 강력한 타격을 위한 공격, 반격 및 대미지 처리
- 상태 관리 시스템
- 캐릭터가 공격 중일 때 다른 액션을 수행하지 못하도록 제어
- 스태미나, 공격 타이밍, 애니메이션 상태 관리
다크소울 스타일 캐릭터 조작 시스템 완벽 코드
1. 기본 이동 및 회피 (롤링) 시스템
먼저 캐릭터의 이동과 회피 기능을 구현합니다.
// MyCharacter.h
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float WalkSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float SprintSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float RollSpeed;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float Stamina;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float MaxStamina;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement")
float StaminaDrainRate;
bool bIsRolling;
// MoveForward and MoveRight
void MoveForward(float Value);
void MoveRight(float Value);
// Roll (회피)
void Roll();
// Update Movement Speed
void UpdateMovementSpeed();
// Stamina Drain
void DrainStamina(float Amount);
// Stamina Recovery
void RecoverStamina(float Amount);
// MyCharacter.cpp
void AMyCharacter::MoveForward(float Value)
{
if (Controller && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::X);
AddMovementInput(Direction, Value);
}
}
void AMyCharacter::MoveRight(float Value)
{
if (Controller && Value != 0.0f)
{
const FRotator Rotation = Controller->GetControlRotation();
const FRotator YawRotation(0, Rotation.Yaw, 0);
const FVector Direction = FRotationMatrix(YawRotation).GetUnitAxis(EAxis::Y);
AddMovementInput(Direction, Value);
}
}
void AMyCharacter::Roll()
{
if (Stamina >= RollStaminaCost && !bIsRolling)
{
bIsRolling = true;
DrainStamina(RollStaminaCost);
// 회피 애니메이션 실행
PlayRollAnimation();
// Roll Speed로 이동
GetCharacterMovement()->Launch(FVector(RollSpeed, 0, 0), true);
// Roll 종료 후 상태 복귀
FTimerHandle UnusedHandle;
GetWorld()->GetTimerManager().SetTimer(UnusedHandle, this, &AMyCharacter::EndRoll, 0.5f, false);
}
}
void AMyCharacter::EndRoll()
{
bIsRolling = false;
// 이동 속도 복구
UpdateMovementSpeed();
}
void AMyCharacter::UpdateMovementSpeed()
{
if (bIsSprinting)
{
GetCharacterMovement()->MaxWalkSpeed = SprintSpeed;
}
else
{
GetCharacterMovement()->MaxWalkSpeed = WalkSpeed;
}
}
void AMyCharacter::DrainStamina(float Amount)
{
Stamina -= Amount;
if (Stamina < 0)
{
Stamina = 0;
}
}
void AMyCharacter::RecoverStamina(float Amount)
{
Stamina += Amount;
if (Stamina > MaxStamina)
{
Stamina = MaxStamina;
}
}
이 코드는 기본적인 이동, 회피, 스태미나 관리를 구현한 예시입니다. Roll()
함수는 캐릭터가 회피를 할 때 사용되며, DrainStamina()
를 통해 스태미나를 소모하고, 회피 후 EndRoll()
을 호출하여 회피 상태를 종료합니다.
2. 애니메이션 블렌딩 및 전환
다음은 캐릭터의 애니메이션을 전환하는 코드입니다.
// CharacterAnimation.cpp
void AMyCharacter::UpdateAnimation()
{
if (bIsRolling)
{
PlayAnimation(RollAnimation);
}
else if (bIsAttacking)
{
PlayAnimation(AttackAnimation);
}
else if (IsMoving())
{
if (bIsSprinting)
{
PlayAnimation(SprintAnimation);
}
else
{
PlayAnimation(WalkAnimation);
}
}
else
{
PlayAnimation(IdleAnimation);
}
}
void AMyCharacter::PlayAnimation(UAnimationAsset* Animation)
{
if (CharacterMesh)
{
CharacterMesh->PlayAnimation(Animation, false);
}
}
애니메이션 전환은 bIsRolling
, bIsAttacking
등의 상태에 따라 변경됩니다. IsMoving()
함수는 이동 중인지 체크하고, 각 상태에 맞는 애니메이션을 재생합니다.
3. 공격 및 타이밍 시스템
// AttackSystem.cpp
void AMyCharacter::PerformAttack()
{
if (CanAttack() && Stamina >= AttackStaminaCost)
{
PlayAttackAnimation();
DrainStamina(AttackStaminaCost);
// 타격 판정 및 충돌 처리
DetectEnemyCollision();
}
}
bool AMyCharacter::CanAttack() const
{
return !bIsAttacking && !bIsRolling && !bIsDodging;
}
이 코드는 공격 기능을 처리합니다. CanAttack()
함수는 캐릭터가 공격할 수 있는 상태인지 확인하며, 공격이 가능하면 애니메이션을 실행하고 스태미나를 소모합니다.
4. 상태 관리 및 전반적인 시스템 연결
// CharacterState.cpp
void AMyCharacter::UpdateState()
{
if (bIsAttacking || bIsRolling)
{
// 상태 변경이 불가능한 상태에서는 이동 및 공격을 막음
DisableMovement();
}
else
{
EnableMovement();
}
}
void AMyCharacter::DisableMovement()
{
GetCharacterMovement()->SetMovementMode(MOVE_None);
}
void AMyCharacter::EnableMovement()
{
GetCharacterMovement()->SetMovementMode(MOVE_Walking);
}
상태 관리는 캐릭터가 공격 중 또는 회피 중일 때 다른 액션을 할 수 없도록 제한하는 중요한 기능입니다. DisableMovement()
와 EnableMovement()
를 통해 캐릭터의 이동 상태를 제어합니다.
장점과 단점
장점:
- 고유한 타이밍 및 반응성: 다크소울 스타일의 전투와 캐릭터 조작 시스템을 구현하여, 게임의 몰입도를 크게 향상시킵니다.
- 유연한 스태미나 시스템: 스태미나 관리 시스템을 통해 플레이어가 전투 및 탐험을 전략적으로 접근할 수 있게 합니다.
- 애니메이션 블렌딩: 다양한 애니메이션을 부드럽게 전환하여 캐릭터의 움직임을 자연스럽게 만듭니다.
단점:
- 복잡한 상태 관리: 다양한 상태를 관리하고 충돌 처리를 정확하게 구현하는 것은 시간이 많이 소요될 수 있습니다.
- 스태미나 관리의 어려움: 스태미나가 부족할 경우 캐릭터가 제대로 동작하지 않게 되므로, 밸런스를 맞추는 데 신경 써야 합니다.
결론
이제 다크소울 스타일의 캐릭터 조작 시스템을 언리얼 엔진에서 구현할 수 있는 기본적인 코드와 개념을 제공하였습니다. 이 코드를 기반으로 각자 게임에 맞는 세부 조정 및 추가 기능을 더하면, 완성도 높은 액션 RPG 전투 시스템을 구현할 수 있습니다. 다크소울의 특유의 긴장감 넘치는 전투와 반응성을 게임에 잘 녹여내어, 플레이어들에게 더욱 몰입감 있는 경험을 선사할 수 있을 것입니다.