게임의 완성 (2) 전투 시스템 설계

유영준·2023년 1월 30일
0

무한하게 실행되는 전투가 메인 컨텐츠인 만큼, 이번에는 전투 시스템을 더욱 더 정교하게 만들고자 한다
추가할 요소는 다음과 같다

  • 캐릭터는 무기를 들 때 더 긴 공격범위를 가진다
  • 무기에는 공격력 증가치가 랜덤으로 부여된다
  • 현재 게임 스코어가 높다면 NPC의 레벨 또한 증가한다

무기에 관한 부분을 하나로 묶어 정리하고, 레벨을 따로 나누도록 하겠다

무기의 범위와 공격력 설정

무기에 공격 범위인 AttackRange, 공격력과 보정치인 AttackDamage 와 AttackModifier 변수를 추가한다

ABWeapon.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "GameFramework/Actor.h"
#include "ABWeapon.generated.h"

UCLASS()
class ARENABATTLE_API AABWeapon : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AABWeapon();

	float GetAttackRange() const;
	float GetAttackDamage() const;
	float GetAttackModifier() const;

	UPROPERTY(VisibleAnywhere, Category = Weapon)
	USkeletalMeshComponent* Weapon;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
	float AttackRange;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
	float AttackDamageMin;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
	float AttackDamageMax;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
	float AttackModifierMin;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = Attack)
	float AttackModifierMax;

	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = Attack)
	float AttackDamage;

	UPROPERTY(Transient, VisibleInstanceOnly, BlueprintReadOnly, Category = Attack)
	float AttackModifier;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

};

ABWeapon.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "ABWeapon.h"

// Sets default values
AABWeapon::AABWeapon()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = false;

	Weapon = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("WEAPON"));
	RootComponent = Weapon;

	static ConstructorHelpers::FObjectFinder<USkeletalMesh> SK_WEAPON
	(TEXT("/Game/InfinityBladeWeapons/Weapons/Blade/Swords/Blade_HeroSword11/SK_Blade_HeroSword11.SK_Blade_HeroSword11"));

	if (SK_WEAPON.Succeeded())
		Weapon->SetSkeletalMesh(SK_WEAPON.Object);

	Weapon->SetCollisionProfileName(TEXT("NoCollision"));

	AttackRange		  = 150.0f;
	AttackDamageMin   = -2.5f;
	AttackDamageMax   = 10.0f;
	AttackModifierMin = 0.85f;
	AttackModifierMax = 1.25f;
}

float AABWeapon::GetAttackRange() const
{
	return AttackRange;
}

float AABWeapon::GetAttackDamage() const
{
	return AttackDamage;
}

float AABWeapon::GetAttackModifier() const
{
	return AttackModifier;
}

// Called when the game starts or when spawned
void AABWeapon::BeginPlay()
{
	Super::BeginPlay();
	
	AttackDamage = FMath::RandRange(AttackDamageMin, AttackDamageMax);
	AttackModifier = FMath::RandRange(AttackModifierMin, AttackModifierMax);

	ABLOG(Warning, TEXT("Weapon Damage : %f, Modifier : %f"), AttackDamage, AttackModifier);
}

설정된 변수들을, 캐릭터가 습득할때 받을 수 있도록 설정해준다

ABCharacter.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "ArenaBattle.h"
#include "GameFramework/Character.h"
#include "ABCharacter.generated.h"

DECLARE_MULTICAST_DELEGATE(FOnAttackEndDelegate);

UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	AABCharacter();
	void SetCharacterState(ECharacterState NewState);
	ECharacterState GetCharacterState() const;
	int32 GetExp() const;
	float GetFinalAttackRange() const;
	float GetFinalAttackDamage() const;
    
    ...
    
}
    

ABCharacter.cpp

...
AABCharacter::AABCharacter()
{

	...

	AttackRange      = 80.0f;
	AttackRadius     = 50.0f;	
    
    ...
    
}

...

float AABCharacter::GetFinalAttackRange() const
{
	return (nullptr != CurrentWeapon) ? CurrentWeapon->GetAttackRange() : AttackRange;
}

float AABCharacter::GetFinalAttackDamage() const
{
	float AttackDamage = (nullptr != CurrentWeapon) ? 
		(CharacterStat->GetAttack() + CurrentWeapon->GetAttackDamage()) : (CharacterStat->GetAttack());

	float AttackModifier = (nullptr != CurrentWeapon) ? CurrentWeapon->GetAttackModifier() : 1.0f;

	return AttackDamage * AttackModifier;
}

...

bool AABCharacter::CanSetWeapon()
{
	return true;
}

...

void AABCharacter::SetWeapon(AABWeapon* NewWeapon)
{

	if (nullptr != CurrentWeapon)
	{
		CurrentWeapon->DetachFromActor(FDetachmentTransformRules::KeepWorldTransform);
		CurrentWeapon->Destroy();
		CurrentWeapon = nullptr;
	}
    
    ...
    
}

...

void AABCharacter::AttackCheck()
{
	FHitResult HitResult;
	FCollisionQueryParams Params(NAME_None, false, this);
	bool bResult = GetWorld()->SweepSingleByChannel
	(
		HitResult,
		GetActorLocation(),
		GetActorLocation() + GetActorForwardVector() * GetFinalAttackRange(),
		FQuat::Identity,
		ECollisionChannel::ECC_GameTraceChannel2,
		FCollisionShape::MakeSphere(AttackRadius),
		Params
	);

#if ENABLE_DRAW_DEBUG
	
	FVector TraceVec    = GetActorForwardVector() * GetFinalAttackRange();
	FVector Center      = GetActorLocation() + TraceVec * 0.5f;
	float HalfHeight    = GetFinalAttackRange() * 0.5f + AttackRadius;
	FQuat CapsuleRot    = FRotationMatrix::MakeFromZ(TraceVec).ToQuat();
	FColor DrawColor    = bResult ? FColor::Green : FColor::Red;
	float DebugLifeTime = 3.0f;

	DrawDebugCapsule
	(
		GetWorld(),
		Center,
		HalfHeight,
		AttackRadius,
		CapsuleRot,
		DrawColor,
		false,
		DebugLifeTime
	);

#endif

	if (bResult)
	{
		if (HitResult.GetActor()->IsValidLowLevel())
		{
			UGameplayStatics::ApplyDamage(HitResult.GetActor(), GetFinalAttackDamage(), GetController(), this, UDamageType::StaticClass());
		}
	}
}

...

다음은 행동 트리에서도 캐릭터의 사거리를 받아 공격이 가능하게끔 설정해주어야 한다

BTDecorator_IsInAttackRange.cpp


...

bool UBTDecorator_IsInAttackRange::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
	bool bResult = Super::CalculateRawConditionValue(OwnerComp, NodeMemory);

	auto ControllingPawn = Cast<AABCharacter>(OwnerComp.GetAIOwner()->GetPawn());
	if (nullptr == ControllingPawn)
		return false;

	auto Target = Cast<AABCharacter>(OwnerComp.GetBlackboardComponent()->GetValueAsObject(AABAIController::TargetKey));
	if (nullptr == Target)
		return false;

	bResult = (Target->GetDistanceTo(ControllingPawn) <= ControllingPawn->GetFinalAttackRange());
	return bResult;
}

무기가 있을때와 없을때의 차이가 생기며

로그창에서도 데미지가 바뀌는것이 제대로 출력된다


NPC 레벨 조정

NPC의 레벨은 현재 스코어를 받아와 로딩 상태일때 스코어를 기반으로 만들어주도록 하겠다

ABGameMode.h

...

public:
	AABGameMode();
	
	virtual void PostInitializeComponents() override;
	virtual void PostLogin(APlayerController* NewPlayer) override;
	void AddScore(class AABPlayerController* ScoredPlayer);
	int32 GetScore() const;
    
    ...
    
}

ABGameMode.cpp

int32 AABGameMode::GetScore() const
{
	return ABGameState->GetTotalGameScore();
}

ABCharacter.cpp

void AABCharacter::SetCharacterState(ECharacterState NewState)
{
	CurrentState = NewState;
	switch (CurrentState)
	{
	case ECharacterState::LOADING:
	{
		if (bIsPlayer)
		{
			DisableInput(ABPlayerController);

			ABPlayerController->GetHUDWidget()->BindCharacterStat(CharacterStat);

			auto ABPlayerState = Cast<AABPlayerState>(GetPlayerState());
			CharacterStat->SetNewLevel(ABPlayerState->GetCharacterLevel());
		}
		else
		{
			auto ABGameMode   = Cast<AABGameMode>(GetWorld()->GetAuthGameMode());
			int32 TargetLevel = FMath::CeilToInt(((float)ABGameMode->GetScore() * 0.8f));
			int32 FinalLevel  = FMath::Clamp<int32>(TargetLevel, 1, 20);
			CharacterStat->SetNewLevel(FinalLevel);
		}
		SetActorHiddenInGame(true);
		HPBarWidget->SetHiddenInGame(true);
		SetCanBeDamaged(false);
		break;
	}
    
    ...
    
}

이때 FMath::CeilToInt 는 float 값을 반올림해 Int 값으로 리턴해주는 함수이다

이제부턴 레벨에 따라 강한 NPC가 나오게 된다

profile
토비폭스가 되고픈 게임 개발자

0개의 댓글