프로젝트 소스 코드 복기 01 Character & Components

김민오·2022년 9월 18일
1
post-thumbnail

원래는 다이어그램부터 먼저 그리고 설계를 어느정도 해놓은 다음에 코드 짜는게 맞는데...
처음에 PVP용 슈팅게임 만들려고 시작한 프로젝트가 근접 무기도 넣고 몬스터도 추가하고 스킬도 추가하고 이것저것 덕지덕지 붙이다보니 프로젝트가 겉잡을수도없이 불어나버렸다...

일단 소스코드가 꽤 길어져서 다이어그램에 모두 담기엔 보기 힘들것같아 중요한 부분만 표시했다. 일부 메소드들은 RPC가 적용되어 Server, Mulitcast함수들이 구현되어 있는데 이 경우에는 다이어그램에서 생략을 했다.
또한 BeginPlay(), Tick()과 같이 소스코드 생성 시 에디터에서 기본적으로 제공되는 요소들 또한 생략해서 표현했다.

Combat Character

게임에서 플레이어가 직접 컨트롤하는 캐릭터이다.
키를 입력받아 이동을 하고 장비를 장착하고, 스킬을 사용하고 인벤토리를
통해 아이템을 수집할 수도 있다.

Components

캐릭터의 기능들 (전투 시스템, 인벤토리 등)을 실질적으로 수행한다.
탈/부착이 용이하면 차후에 다른 프로젝트를 만들때에도 재활용하기 쉬울 것 같아 컴포넌트로 구현했다.
캐릭터의 클래스다이어그램을 보면 입력함수만 존재하고(PressFire, PressAbility 등) 기능을 수행하는 함수는 없는데, 앞서 언급했듯이 이렇게 실제로 기능을 수행하는 부분은 전부 컴포넌트들이 처리한다.

그래서 입력부분 함수를 보면

// 무기 공격 입력함수
void ACombatCharacter::PressFire()
{
	// 먼저 현재 공격이 가능한지를 체크
	if (GetIsCCControlled()) return;
	if (gameplay_disable_option) return;
	if (combat_component) // 컴포넌트 유효성 체크
	{
    	// 컴포넌트에서 실질적으로 기능을 수행한다.
		combat_component->PressFireKey(true);
	}
}

void ACombatCharacter::PressAbility1()
{
	if (GetIsCCControlled()) return;
	// 스킬을 사용하는데에 50만큼의 마나 자원을 필요로한다.
    // 이를 체크한다, 만약 부족하다면 스킬을 사용할 수 없다.
	if (ManaCheck(50)) 
	{
		UseMainAbility();
	}
}
void ACombatCharacter::UseMainAbility()
{
	// 어빌리티 컴포넌트에서 실질적으로 기능을 수행
	ability_component->ExecuteMainAbility(selected_character_class);
}

이런 방식으로 구현했다.

Combat Component

무기 장착, 무기 공격 등 무기와 관련된 기능들은 모두 이 컴포넌트가 수행한다.

Weapon Equipment

void UCombatComponent::EquipWeapon(ACombatWeapon* weapon)
{
	if (weapon_owner == nullptr || weapon == nullptr) return;
	
	if (equipped_weapon != nullptr && equipped_sub_weapon == nullptr)
	{
		EquipSubWeapon(weapon);
	}
	else
	{
		EquipPrimaryWeapon(weapon);
	}

	weapon_owner->GetCharacterMovement()->bOrientRotationToMovement = false;
	weapon_owner->bUseControllerRotationYaw = true;

	//weapon_owner->SetWeaponImage();
}

캐릭터 무기 장착 입력 함수에서 컴포넌트를 통해 실행되는 함수이다.
만약 주무기, 보조무기를 모두 장착하지 않았다면 주무기를 먼저 장착하고, 주무기만 장착되어 있는 상태라면 보조무기를 장착한다.

Weapon Attack

void UCombatComponent::PressFireKey(bool is_pressed)
{
	is_pressed_fire_key = is_pressed;

	if (is_pressed_fire_key)
	{
		Fire();
	}
}

캐릭터가 컴포넌트를 통해 호출하는 함수이다.
Fire라는 함수를 호출하는데 이 함수 내부에서 현재 어떤 타입의 무기를 들고 있는지에 따라 공격 방식이 달라지게된다.
원래 Attack 같은 이름을 쓰는게 더 좋은데 약간 어거지로 근접 공격 기능을 넣으려고 하다보니까 이렇게 돼버렸다..

void UCombatComponent::Fire()
{
	// CanCheckFire() 함수는 현재 플레이어가 무기를 들고 있는지를 확인한다.
	if (CanCheckFire())
	{
    	// 무기의 공격방식은 EWeaponStyle 이라는 열거형 타입을 따로 만들어서 사용했다.
		if (equipped_weapon->weapon_style == EWeaponStyle::WST_Ranger)
		{
        // 원거리 무기의 경우
			ServerFire(aim_target);
			if (equipped_weapon) { is_can_fire = false; }
			FireTimerStart();
		}
		else if (equipped_weapon->weapon_style == EWeaponStyle::WST_Melee)
		{
        // 근접 무기의 경우
			ServerMeleeAttack();
		}
	}
}

Range Weapon Fire

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)
	{
		weapon_owner->PlayAnimMontageFire();
		equipped_weapon->Fire(hit_target);
	}
}

ServerFire(), MultiCastFire()는 각각 Server RPC, MultiCast RPC로 구현되어 있다.
먼저 ServerFire() 함수를 통해 서버에게 총좀 쏘겠습니다~ 이렇게 요청하고 그 뒤에 서버에서 호출되는 MultiCast RPC를 통해 지금 클라이언트 누구누구가 총쏜다고합니다~ 이렇게.. 접속중인 모든 클라이언트에게 호출 결과를 보여준다.

Melee Weapon Attack

void UCombatComponent::ServerMeleeAttack_Implementation()
{
	MultiCastServerMeleeAttack();
}

void UCombatComponent::MultiCastServerMeleeAttack_Implementation()
{
	if (equipped_weapon == nullptr){ return; }
	if (weapon_owner)
	{
		equipped_weapon->ComboProcess();
	}
}

원거리 무기와 유사하다 ComboProcess라는 함수를 통해 근접 무기 콤보공격을 진행한다.

Ability Component

메인 스킬, 서브 스킬을 사용하는 기능들은 이 컴포넌트가 수행한다.

메인스킬의 경우 플레이어가 시작화면에서 선택한 클래스에 따라서 switch문으로 다르게 발동되게끔 구현을 했고, 서브 스킬의 경우 함수 델리게이트 맵을 따로 만들어서 구현했다. 한 가지 방식으로 통일하는게 좋긴한데 두 가지 방식중 어떤게 더 쓰기 편할까 시험해보고싶기도 했고 델리게이트를 한번 사용해보고 싶기도해서 이렇게 투트랙으로 구현했다.


메인 스킬 멀티캐스트 함수 구현 부분
void USciFiAbilityComponent::MulticastExecuteMainAbility_Implementation(ECombatCharacterClass selected_character_class)
{
	// 캐릭터의 클래스에 따라서 사용할 스킬이 정해진다
	switch (selected_character_class) 
	{
	case ECombatCharacterClass::ECC_CombatMaster:
		SpawnTurret();
		//Teleport();
		break;
	case ECombatCharacterClass::ECC_Magician:
		SpawnWall();
		break;
	case ECombatCharacterClass::ECC_Guardian:
		DamageImmune();
		break;
	}
}
서브스킬의 경우 BeginPlay()함수 내에서 델리게이트맵을 한번 초기화해준다.
void USciFiAbilityComponent::InitializeAbilityDelegates()
{
	ability_delegates_map.Add("Impulse", CreateAbilityDelegate("Impulse"));
	ability_delegates_map.Add("BlackHole", CreateAbilityDelegate("BlackHole"));
	ability_delegates_map.Add("IncreaseJumpUp", CreateAbilityDelegate("IncreaseJumpUp"));
	ability_delegates_map.Add("SpeedBooster", CreateAbilityDelegate("SpeedBooster"));
	ability_delegates_map.Add("SpawnHealObject", CreateAbilityDelegate("SpawnHealObject"));
}

서브스킬 델리게이트를 생성하는 함수
FAbilityDelagate USciFiAbilityComponent::CreateAbilityDelegate(FString skill_name)
{
	FAbilityDelagate skill_delegate;
    // skill_name에 해당하는 함수를 델리게이트에 바인드한다.
	skill_delegate.BindUFunction(this, FName(skill_name));
	return skill_delegate;
}

서브 스킬 멀티캐스트 함수 구현 부분
void USciFiAbilityComponent::MulticastExecuteSubAbility_Implementation(const FString& selected_sub_skill_name)
{
	// 이름에 맞는 델리게이트가 존재하는지 유효성 검사
	if (ability_delegates_map[selected_sub_skill_name].IsBound()) 
	{ 
    // 델리게이트를 통해 함수를 호출
		ability_delegates_map[selected_sub_skill_name].Execute();
	}
}

Crowd Control Component

만약 몬스터로부터 Crowd Control(에어본, 기절 등)을 유발하는 특정 공격을 받았을 경우
이 컴포넌트가 기능을 수행한다.

Inventory Component

인벤토리 기능을 수행한다.

이제와서 다시보는데 멤버 상당수가 public으로 지정되어있다, 보통 private으로 지정하고 여기에 접근용 메소드를 따로 만드는게 좋은데 급하게 테스트하고 디버그하고 하다보니 그냥 public으로 두는게 편해서 이렇게 한건데 음..

profile
https://github.com/gimhema, https://gimhema.tistory.com/

0개의 댓글