프로젝트 소스 코드 복기 02 Weapons Base & Projectile Weapon

김민오·2022년 9월 20일
0

멤버와 메소드는 생략해서 표현했다.

모든 무기는 기본적으로 AActor을 부모로 하는 CombatWeapon을 기반으로 만들어져있으며 무기의 종류는 아래와 같다.

  • MeleeWeaponBase : 근접 무기

    • GreatSword
    • Axe
    • Sword
  • HitscanTypeWeapon : 히트 스캔 발사형식 원거리 무기

    • MachineGun
    • Sniper Rifle
    • ShotGun
  • ProjectileTypeWeapon : 발사체 형식 원거리 무기

    • Assault Rifle
    • Missile Launcher
    • Grenade Launcher

Combat Weapon

일부 요소들은 생략하고 중요하다고 생각되는 멤버와 메소드만 표시했다.

우선 열거형 타입으로 EweaponStatus과 EWeaponStyle이 존재한다.
EwaponStatus는 현재 무기가 장착되어 있는 상태인지(주무기 혹은 보조무기로) 아니면 떨어져있는지 등 무기의 상태를 나타낸다.
EweaponStyle은 이 무기가 원거리 무기인지 근접 무기인지를 나타낸다.

코드상으로는 아래와 같이 나타나있다.

헤더 파일
UENUM(BlueprintType)
enum class EWeaponStatus : uint8
{
	EWS_Initial UMETA(DisplayName = "Initial State"),
	EWS_Equipped UMETA(DisplayName = "Equipped"),
	EWS_EquippedSub UMETA(DisplayName = "EquippedSub"),
	EWS_Dropped UMETA(DisplayName = "Dropped"),
	EWS_MAX UMETA(DisplayName = "DefaultMAX")
};

UENUM(BlueprintType)
enum class EWeaponStyle : uint8
{
	WST_Ranger UMETA(DisplayName = "Ranger Weapon"),
	WST_Melee UMETA(DisplayName = "Melee Weapon")
};

Combat Weapon은 기본적으로 무기의 스켈레탈 메시, 그리고 플레이어가 무기를 장착하기 위해 접근을 허용하는 에어리어, 원거리 무기를 위한 탄약 정보 그리고 근접 무기를 위한 정보로는 최대 몇번까지 콤보로 때릴 수 있는지 등을 가지고 있다. 데미지에 대한 정보 같은 경우 Combat Weapon을 상속받는 MeleeWeaponBase, HitscanType, ProjectileType 무기 클래스들이 각각 가지고 있다.

Fire(), ComboProcess() 함수는 각각 원거리 무기와 근거리 무기의 공격이 실질적으로 실행되는 부분이다. CombatWeapon의 자손 클래스들이 확장해서 쓸 수 있도록 virtual로 선언되어 있다.

헤더
class SCIFICOMBAT_API ACombatWeapon : public AActor
{
	GENERATED_BODY()
public:	
. . . . . . . . .	
	virtual void Fire(const FVector& hit_target);
	virtual void ComboProcess();
. . . . . . . . .
}

구현 부분
void ACombatWeapon::Fire(const FVector& hit_target)
{
	// 애니메이션 실행과 탄약 갱신 부분만 구현되어 있다.
    // 발사체, 히트스캔 발사와 같은 이벤트들은 자손 클래스부터 구현된다.
	if (fire_animation)
	{
		weapon_mesh->PlayAnimation(fire_animation, false);
	}
	SpendRound();
    
}

void ACombatWeapon::ComboProcess()
{
	// 각 무기들마다 콤보 프로세스가 다르기 때문에 비워두었다.
}

무기들은 주무기/보조무기로 장착되거나 드랍될때마다 상태가 변경되며 이를 담당하는 함수들은 아래와 같이 구현되어 있다.

void ACombatWeapon::SetWeaponStatus(EWeaponStatus status)
{
	weapon_status = status;
	OnWeaponStateSet();
}

void ACombatWeapon::OnWeaponStateSet()
{
	switch (weapon_status)
	{
	case EWeaponStatus::EWS_Equipped:
		OnEquipped();
		break;
	case EWeaponStatus::EWS_Dropped:
		OnDropped();
		break;
	case EWeaponStatus::EWS_EquippedSub:
		OnEquippedSub();
		break;
	}
}

void ACombatWeapon::OnEquipped()
{
	ShowPickUpWeaponWidget(false);
	weapon_area->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	weapon_mesh->SetSimulatePhysics(false);
	weapon_mesh->SetEnableGravity(false);
	weapon_mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	EnableCustomDepth(false);
}
void ACombatWeapon::OnDropped()
{
	if (HasAuthority())
	{
		weapon_area->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
	}
	weapon_mesh->SetSimulatePhysics(true);
	weapon_mesh->SetEnableGravity(true);
	weapon_mesh->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
	weapon_mesh->SetCustomDepthStencilValue(outline_depth_color);
	weapon_mesh->MarkRenderStateDirty();
	EnableCustomDepth(true);
}

void ACombatWeapon::OnEquippedSub()
{
	ShowPickUpWeaponWidget(false);
	weapon_area->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	weapon_mesh->SetSimulatePhysics(false);
	weapon_mesh->SetEnableGravity(false);
	weapon_mesh->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	EnableCustomDepth(false);
}

사용 예시 - 무기가 드랍되었을 경우
void ACombatWeapon::DropWeapon()
{
	SetWeaponStatus(EWeaponStatus::EWS_Dropped);
	FDetachmentTransformRules detach_rule(EDetachmentRule::KeepWorld, true);
	weapon_mesh->DetachFromComponent(detach_rule);
	SetOwner(nullptr);
	weapon_owner_character = nullptr;
	weapon_owner_controller = nullptr;
}

사용 예시 - Combat Component에서 보조무기 장착 시 상태 변경
void UCombatComponent::OnRep_EquippedSubWeapon()
{
	if (equipped_sub_weapon && weapon_owner)
	{
		equipped_sub_weapon->SetWeaponStatus(EWeaponStatus::EWS_EquippedSub);
		AttachSubWeaponToSocket(equipped_sub_weapon);
	}
}

Projectile Weapon


/**
 * 
 */
UCLASS()
class SCIFICOMBAT_API AProjectileTypeWeapon : public ACombatWeapon
{
	GENERATED_BODY()

public:
	virtual void Fire(const FVector& hit_target) override;
private:
	UPROPERTY(EditAnywhere)
	TSubclassOf<class AProjectileBase> projectile_class;
	
};

Combat Weapon을 상속받아 구현되는 Projectile Weapon은 Fire함수를 override하며 무기에서 발사될 Projectile 클래스를 멤버로 갖는다.

Projectile Weapon의 Fire 함수는 아래와 같이 구현되어있다.
Fire 함수는 FVector 타입의 hit_target을 파라미터로 받는데, Combat Component가 Camera Lintrace를 Tick() 이벤트를 사용하여 빠르게 hit_target을 갱신하기 때문에 이 정보를 가져와서 사용한다.


Combat Component 구현부

void UCombatComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);

	if (weapon_owner && weapon_owner->IsLocallyControlled())
	{
		FHitResult hit_result;
      	// Aim Target을 계속 업데이트함
		TracePlayerAim(hit_result);
		aim_target = hit_result.ImpactPoint;
		. . . . .
	}
}

void UCombatComponent::Fire()
{
	if (CanCheckFire())
	{
		if (equipped_weapon->weapon_style == EWeaponStyle::WST_Ranger)
		{
        	// 업데이트되는 aim_target을 보내줌
			ServerFire(aim_target);
			. . . . . . . .
        }
}

void UCombatComponent::ServerFire_Implementation(const FVector_NetQuantize& hit_target)
{
// 서버에서 멀티캐스트 이벤트 호출
	MultiCastFire(hit_target);
}

void UCombatComponent::MultiCastFire_Implementation(const FVector_NetQuantize& hit_target)
{
	if (equipped_weapon == nullptr)
	{
		return;
	}
	if (weapon_owner && combat_state == ECombatState::ECS_Unoccupied)
	{
    // 플레이어의 애니메이션 몽타주 실행
    // 무기의 Fire()함수에서 실행되는 애니메이션은 무기 자체의 애니메이션임
		weapon_owner->PlayAnimMontageFire();
		equipped_weapon->Fire(hit_target);
	}
}


// Projectile Type Weapon 구현부

void AProjectileTypeWeapon::Fire(const FVector& hit_target)
{
	Super::Fire(hit_target);

	if (!HasAuthority()) return;

	APawn* instigator = Cast<APawn>(GetOwner());
    
  
	const USkeletalMeshSocket* muzzle_socket = GetWeaponMesh()->GetSocketByName(FName("MuzzleFlash"));

	if (muzzle_socket)
	{
    // 총구 소켓으로부터 발사 시작 위치를 구하고
    // hit_target으로부터 발사체가 도달해야할 위치를 구한다
    // 두 정보를 사용하여 총알이 발사되어야할 회전을 구한다.
		FTransform socket_transform = muzzle_socket->GetSocketTransform(GetWeaponMesh());
		FVector target_vec = hit_target - socket_transform.GetLocation();
		FRotator target_rot = target_vec.Rotation();
		if (projectile_class && instigator)
		{
			FActorSpawnParameters spawn_params;
			spawn_params.Owner = GetOwner();
			spawn_params.Instigator = instigator;
			UWorld* world = GetWorld();
			if (world)
			{
            // 위에서부터 구한 정보들을 바탕으로 Projectile을 스폰
            // 스폰되는 클래스는 기본적으로 Projectile Component가 붙어있어 스폰과 즉시 발사됨
				world->SpawnActor<AProjectileBase>(
					projectile_class,
					socket_transform.GetLocation(),
					target_rot,
					spawn_params
					);
			}
		}
	}
}
profile
https://github.com/gimhema, https://gimhema.tistory.com/

0개의 댓글