

팀원이 만들어 둔 아이템 베이스 코드를 상속 받아 범위에 들어가는 대상에게 이벤트를 줄 수 있는 아이템을 만들었다.

그리고 자리바꾸기 아이템 포탈!
아이템 베이스
#pragma once
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "Data/DDItemDataTypes.h"
#include "GameplayTagContainer.h"
#include "GA_ItemBase.generated.h"
class ADDBoardGameCharacter;
class UAbilitySystemComponent;
UCLASS(Abstract)
class DOODOONG_API UGA_ItemBase : public UGameplayAbility
{
GENERATED_BODY()
public:
UGA_ItemBase();
virtual void ActivateAbility(
FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData) override;
virtual void EndAbility(
FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled
) override;
protected:
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Item|Cue")
FGameplayTag ConfirmCueTag;
ADDBoardGameCharacter* GetBoardGameCharacter() const;
/** 보드게임 캐릭터가 사용하는 ASC Getter */
UAbilitySystemComponent* GetBoardGameAbilitySystemComponent() const;
UPROPERTY(EditDefaultsOnly, Category = "UX")
AActor* ItemActor;
UPROPERTY()
FItemTableRow CachedItemData;
bool ExecuteItemCue(const FGameplayTag& CueTag) const;
bool ExecuteItemCueOnTarget(const FGameplayTag& CueTag, AActor* CueActor) const;
};
#include "BoardGame/Abilities/ItemAbilities/ItemBase/GA_ItemBase.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "AbilitySystemComponent.h"
#include "BoardGame/Character/DDBoardGameCharacter.h"
#include "Data/DDItemDataTypes.h"
#include "Data/ItemPayloadObject.h"
#include "System/DDGameplayTags.h"
UGA_ItemBase::UGA_ItemBase()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerInitiated;
FAbilityTriggerData TriggerData;
TriggerData.TriggerTag = DDGameplayTags::Event_Item_Activate;
TriggerData.TriggerSource = EGameplayAbilityTriggerSource::GameplayEvent;
AbilityTriggers.Add(TriggerData);
ActivationRequiredTags.AddTag(DDGameplayTags::State_BoardGame_TurnActive);
ActivationRequiredTags.AddTag(DDGameplayTags::State_TurnPhase_BeforeDice);
ActivationBlockedTags.AddTag(DDGameplayTags::State_TurnPhase_Moving);
ActivationBlockedTags.AddTag(DDGameplayTags::State_BoardGame_TurnWaiting);
ActivationBlockedTags.AddTag(DDGameplayTags::State_Character_Death);
}
void UGA_ItemBase::ActivateAbility(FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
// 손에 아이템 액터 붙이기
if (HasAuthority(&ActivationInfo))
{
const UItemPayloadObject* PayloadObj = nullptr;
if (TriggerEventData && TriggerEventData->OptionalObject)
{
PayloadObj = Cast<UItemPayloadObject>(TriggerEventData->OptionalObject);
}
if (PayloadObj)
{
CachedItemData = PayloadObj->ItemRow; // ← 반드시 캐싱
}
ItemActor = GetWorld()->SpawnActor<AActor>(CachedItemData.ItemActorClass);
// 클라 복제
ItemActor->SetReplicates(true);
ItemActor->AttachToComponent(
GetBoardGameCharacter()->GetMesh(),
FAttachmentTransformRules::SnapToTargetNotIncludingScale,
TEXT("RightHand")
);
}
}
void UGA_ItemBase::EndAbility(FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
// 액터 파괴
if (HasAuthority(&CurrentActivationInfo))
{
if (IsValid(ItemActor))
{
ItemActor->Destroy();
}
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
ADDBoardGameCharacter* UGA_ItemBase::GetBoardGameCharacter() const
{
return Cast<ADDBoardGameCharacter>(GetAvatarActorFromActorInfo());
}
UAbilitySystemComponent* UGA_ItemBase::GetBoardGameAbilitySystemComponent() const
{
const ADDBoardGameCharacter* BoardGameCharacter = GetBoardGameCharacter();
return BoardGameCharacter ? BoardGameCharacter->GetAbilitySystemComponent() : nullptr;
}
bool UGA_ItemBase::ExecuteItemCue(const FGameplayTag& CueTag) const
{
if (!CueTag.IsValid())
{
return false;
}
UAbilitySystemComponent* AbilitySystemComponent = GetBoardGameAbilitySystemComponent();
AActor* AvatarActor = GetAvatarActorFromActorInfo();
if (!AbilitySystemComponent || !AvatarActor)
{
return false;
}
FGameplayCueParameters CueParameters;
CueParameters.Instigator = AvatarActor;
CueParameters.EffectCauser = AvatarActor;
CueParameters.Location = AvatarActor->GetActorLocation();
AbilitySystemComponent->ExecuteGameplayCue(CueTag, CueParameters);
return true;
}
bool UGA_ItemBase::ExecuteItemCueOnTarget(const FGameplayTag& CueTag, AActor* CueActor) const
{
if (!CueTag.IsValid() || !IsValid(CueActor))
{
return false;
}
UAbilitySystemComponent* TargetAbilitySystemComponent =
UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(CueActor);
if (!TargetAbilitySystemComponent)
{
return false;
}
AActor* AvatarActor = GetAvatarActorFromActorInfo();
FGameplayCueParameters CueParameters;
CueParameters.Instigator = CueActor;
CueParameters.EffectCauser = AvatarActor ? AvatarActor : CueActor;
CueParameters.Location = CueActor->GetActorLocation();
TargetAbilitySystemComponent->ExecuteGameplayCue(CueTag, CueParameters);
return true;
}
아이템은 게임플레이 어빌리티를 기본으로 하고 있고 선택하면 어빌리티가 실행되고 게임플레이 큐를 통해 연출을 진행한다.
1. 범위 아이템 베이스
#include "BoardGame/Abilities/ItemAbilities/ItemBase/GA_RangeItemBase.h"
#include "Abilities/Tasks/AbilityTask_WaitGameplayEvent.h"
#include "AbilitySystem/Attributes/DDPointSet.h"
#include "Base/Player/DDBasePlayerState.h"
#include "BoardGame/Character/DDBoardGameCharacter.h"
#include "Common/DDLogManager.h"
#include "System/DDGameplayTags.h"
UGA_RangeItemBase::UGA_RangeItemBase()
{
}
void UGA_RangeItemBase::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
LOG_CYS(Warning, TEXT("RangeItem CommitAbility FAILED"));
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
LOG_CYS(Warning,TEXT("범위 아이템 어빌리티 실행"));
TotalDrainedAmount = 0;
if (HasAuthority(&CurrentActivationInfo))
{
ADDBoardGameCharacter* MyChar = GetBoardGameCharacter();
if (MyChar)
{
MyChar->bShowRangeIndicator = true;
MyChar->OnRep_RangeIndicator(); // 서버에서도 즉시 반영
}
}
// // 디버그 라인 그리기
// GetWorld()->GetTimerManager().SetTimer(
// UpdateTimer,
// this,
// &UGA_RangeItemBase::UpdateDebug,
// 0.1f,
// true
// );
UAbilityTask_WaitGameplayEvent* ConfirmTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Confirm
);
if (ConfirmTask)
{
ConfirmTask->EventReceived.AddDynamic(this, &ThisClass::OnConfirm);
ConfirmTask->ReadyForActivation();
}
UAbilityTask_WaitGameplayEvent* CancelTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Cancel
);
if (CancelTask)
{
CancelTask->EventReceived.AddDynamic(this, &ThisClass::OnCancel);
CancelTask->ReadyForActivation();
}
}
void UGA_RangeItemBase::EndAbility(
const FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo,
const FGameplayAbilityActivationInfo ActivationInfo,
bool bReplicateEndAbility,
bool bWasCancelled)
{
GetWorld()->GetTimerManager().ClearTimer(UpdateTimer);
if (HasAuthority(&CurrentActivationInfo))
{
ADDBoardGameCharacter* MyChar =GetBoardGameCharacter();
if (MyChar)
{
MyChar->bShowRangeIndicator = false;
MyChar->OnRep_RangeIndicator();
}
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
void UGA_RangeItemBase::UpdateDebug()
{
ADDBoardGameCharacter* MyChar = Cast<ADDBoardGameCharacter>(GetAvatarActorFromActorInfo());
if (!MyChar) return;
FVector Origin = MyChar->GetActorLocation();
FVector Forward = MyChar->GetActorForwardVector();
DrawDebugCone(
GetWorld(),
Origin,
Forward,
Radius,
FMath::DegreesToRadians(AngleDeg * 0.5f),
FMath::DegreesToRadians(AngleDeg * 0.5f),
20,
FColor::Green,
false,
0.1f
);
}
void UGA_RangeItemBase::FindTargets()
{
ADDBoardGameCharacter* MyChar = Cast<ADDBoardGameCharacter>(GetAvatarActorFromActorInfo());
if (!MyChar) return;
FVector Origin = MyChar->GetActorLocation();
FVector Forward = MyChar->GetActorForwardVector();
CachedTargets.Empty();
for (FConstPlayerControllerIterator It = GetWorld()->GetPlayerControllerIterator(); It; ++It)
{
APawn* Pawn = It->Get()->GetPawn();
if (!Pawn) continue;
ADDBoardGameCharacter* TargetChar = Cast<ADDBoardGameCharacter>(Pawn);
if (!TargetChar || TargetChar == MyChar) continue;
if (IsInFanRange(Origin, Forward, TargetChar->GetActorLocation()))
{
CachedTargets.Add(TargetChar);
DrawDebugSphere(GetWorld(), TargetChar->GetActorLocation(), 30.f, 8, FColor::Red, false, 1.0f);
}
}
}
bool UGA_RangeItemBase::IsInFanRange(const FVector& Origin, const FVector& Forward, const FVector& TargetLoc)
{
FVector ToTarget = TargetLoc - Origin;
float Distance = ToTarget.Size();
if (Distance > Radius) return false;
ToTarget.Normalize();
float Dot = FVector::DotProduct(Forward, ToTarget);
float Angle = FMath::RadiansToDegrees(FMath::Acos(Dot));
return Angle <= AngleDeg * 0.5f;
}
void UGA_RangeItemBase::OnConfirm(FGameplayEventData Payload)
{
if (!HasAuthority(&CurrentActivationInfo))
{
return;
}
LOG_CYS(Warning,TEXT("범위 어빌리티 컨펌 입력 들어옴"));
ExecuteItemCue(ConfirmCueTag);
// 타겟 확정 시에만 탐색
FindTargets();
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo();
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(EffectClass, 1.f);
if (!SpecHandle.IsValid()) return;
SpecHandle.Data->SetSetByCallerMagnitude(EffectDataTag, EffectValue);
// 타겟에게 GE 적용
for (auto Target : CachedTargets)
{
ExecuteItemCueOnTarget(TargetCueTag, Target);
ADDBasePlayerState* PS = Target->GetPlayerState<ADDBasePlayerState>();
if (!PS) continue;
UAbilitySystemComponent* TargetASC = PS->GetAbilitySystemComponent();
if (!TargetASC) continue;
// 적용
SourceASC->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data, TargetASC);
// 코인일 때만 저장
if (EffectDataTag == FGameplayTag::RequestGameplayTag("Data.Point.Coin"))
{
const UDDPointSet* TargetAttributeSet = TargetASC->GetSet<UDDPointSet>();
if (!TargetAttributeSet) continue;
TotalDrainedAmount += TargetAttributeSet->LastCoinLose;
}
}
LOG_CYS(Warning, TEXT("TotalDrainedAmount: %f"), TotalDrainedAmount);
}
void UGA_RangeItemBase::OnCancel(FGameplayEventData Payload)
{
LOG_CYS(Warning,TEXT("범위 어빌리티 캔슬 입력 들어옴"));
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
- 구조
ActivateAbility
├─ CommitAbility 실패 → EndAbility (즉시 종료)
├─ 범위 인디케이터 ON (서버)
├─ WaitGameplayEvent: Confirm ──→ OnConfirm()
└─ WaitGameplayEvent: Cancel ──→ OnCancel()
OnConfirm (서버 전용)
├─ ConfirmCue 실행
├─ FindTargets() → CachedTargets 갱신
└─ CachedTargets 순회
├─ TargetCue 실행
├─ GE 적용 (SetByCaller)
└─ Coin 태그일 경우 TotalDrainedAmount 누적
OnCancel → EndAbility
EndAbility
├─ 타이머 클리어
└─ 범위 인디케이터 OFF (서버)
- 애니메이션
void ADDBoardGameCharacter::OnRep_RangeIndicator()
{
if (RangeIndicator)
{
RangeIndicator->SetVisibility(bShowRangeIndicator, true);
}
// 포즈도 같이
bIsAiming = bShowRangeIndicator;
}
캐릭터의 범위 인디케이터를 켜면서 포즈도 같이 바뀌도록 했다.
ABP에서 캐릭터를 캐스팅해서 bIsAiming 값을 가져오고


애니메이션이 재생되도록 했다.
- 범위 판정
bool UGA_RangeItemBase::IsInFanRange(const FVector& Origin, const FVector& Forward, const FVector& TargetLoc)
{
FVector ToTarget = TargetLoc - Origin;
float Distance = ToTarget.Size();
if (Distance > Radius) return false;
ToTarget.Normalize();
float Dot = FVector::DotProduct(Forward, ToTarget);
float Angle = FMath::RadiansToDegrees(FMath::Acos(Dot));
return Angle <= AngleDeg * 0.5f;
}
1. 타겟까지의 거리가 Radius 이내인지 확인
2. 전방 벡터와 타겟 방향 벡터의 Dot Product로 각도 계산
3. 계산된 각도가 AngleDeg / 2 이하이면 범위 내 판정
2. 자석 아이템
단순하게 타겟에게 -20 나에게 +20 이런게 아닌,
타겟에게 코인을 뺏고 실제로 뺏은 만큼 코인을 얻어야했다.
20개를 뺏었을 때 타겟의 코인이 15개 뿐이면 15 만큼만 얻어야 하는 것이다.
그래서 어트리뷰트셋에서 virtual void PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data) override;
를 통해 마지막으로 잃은 코인 수를 저장하도록 했다.
void UDDPointSet::PostGameplayEffectExecute(const struct FGameplayEffectModCallbackData& Data)
{
Super::PostGameplayEffectExecute(Data);
if (Data.EvaluatedData.Attribute == GetCoinAttribute())
{
float Delta = Data.EvaluatedData.Magnitude;
if (Delta < 0.f) // 코인 감소
{
LastCoinLose = -Delta; // 양수로 저장
}
}
}
PostGameplayEffectExecute 함수는 속성(Attribute) 값이 변경될 때 호출되는 콜백 함수
// 코인일 때만 저장
if (EffectDataTag == FGameplayTag::RequestGameplayTag("Data.Point.Coin"))
{
const UDDPointSet* TargetAttributeSet = TargetASC->GetSet<UDDPointSet>();
if (!TargetAttributeSet) continue;
TotalDrainedAmount += TargetAttributeSet->LastCoinLose;
}
그리고 총 뺏은 양을 저장해둔다.
#include "BoardGame/Abilities/ItemAbilities/GA_Magnet.h"
#include "AbilitySystemComponent.h"
#include "Common/DDLogManager.h"
void UGA_Magnet::OnConfirm(FGameplayEventData Payload)
{
Super::OnConfirm(Payload);
UAbilitySystemComponent* SourceASC = GetAbilitySystemComponentFromActorInfo();
if (!SourceASC) return;
FGameplayEffectSpecHandle SpecHandle = MakeOutgoingGameplayEffectSpec(EffectClass, 1.f);
if (!SpecHandle.IsValid()) return;
SpecHandle.Data->SetSetByCallerMagnitude(EffectDataTag, TotalDrainedAmount);
// 코인 증가
SourceASC->ApplyGameplayEffectSpecToSelf(*SpecHandle.Data);
LOG_CYS(Warning, TEXT("UGA_Magnet: Successfully applied Magnet"));
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
그리고 자석 아이템 어빌리티에서는 뺏은 만큼 코인을 획득하는 로직을 추가해줬다.
3. 장풍 아이템
베이스 코드를 거의 재사용했기에 EndAbility만 해줬다.
void UGA_MeleeDamage::OnConfirm(FGameplayEventData Payload)
{
Super::OnConfirm(Payload);
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, false);
}
4. 포탈 아이템
팀원이 작성한 타겟팅 아이템 베이스
#include "BoardGame/Abilities/ItemAbilities/ItemBase/GA_TargetingItemBase.h"
#include "Abilities/Tasks/AbilityTask_WaitGameplayEvent.h"
#include "EngineUtils.h"
#include "BoardGame/Character/DDBoardGameCharacter.h"
#include "BoardGame/Game/DDBoardGameMode.h"
#include "System/DDGameplayTags.h"
UGA_TargetingItemBase::UGA_TargetingItemBase()
{
}
void UGA_TargetingItemBase::ActivateAbility(FGameplayAbilitySpecHandle Handle,
const FGameplayAbilityActorInfo* ActorInfo, FGameplayAbilityActivationInfo ActivationInfo,
const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
AActor* InitialTarget = TriggerEventData ? const_cast<AActor*>(TriggerEventData->Target.Get()) : nullptr;
if (IsValid(InitialTarget))
{
bSelectingTarget = false;
if (!HasAuthority(&ActivationInfo))
{
return;
}
if (!CommitAbility(Handle, ActorInfo, ActivationInfo))
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
ExecuteItemCue(ConfirmCueTag);
const bool bExecuted = ExecuteTargetingItem(InitialTarget);
EndAbility(Handle, ActorInfo, ActivationInfo, true, !bExecuted);
return;
}
if (!HasAuthority(&ActivationInfo))
{
return;
}
bSelectingTarget = true;
SelectedTargetIndex = INDEX_NONE;
BuildTargetCandidates();
if (CandidateTargets.IsEmpty())
{
EndAbility(Handle, ActorInfo, ActivationInfo, true, true);
return;
}
SelectedTargetIndex = 0;
FocusSelectedTarget();
UAbilityTask_WaitGameplayEvent* NextTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Next,
nullptr,
false
);
if (NextTask)
{
NextTask->EventReceived.AddDynamic(this, &ThisClass::OnTargetNext);
NextTask->ReadyForActivation();
}
UAbilityTask_WaitGameplayEvent* PreviousTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Previous,
nullptr,
false
);
if (PreviousTask)
{
PreviousTask->EventReceived.AddDynamic(this, &ThisClass::OnTargetPrevious);
PreviousTask->ReadyForActivation();
}
UAbilityTask_WaitGameplayEvent* ConfirmTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Confirm
);
if (ConfirmTask)
{
ConfirmTask->EventReceived.AddDynamic(this, &ThisClass::OnTargetConfirm);
ConfirmTask->ReadyForActivation();
}
UAbilityTask_WaitGameplayEvent* CancelTask = UAbilityTask_WaitGameplayEvent::WaitGameplayEvent(
this,
DDGameplayTags::Event_Item_Target_Cancel
);
if (CancelTask)
{
CancelTask->EventReceived.AddDynamic(this, &ThisClass::OnTargetCancel);
CancelTask->ReadyForActivation();
}
}
void UGA_TargetingItemBase::EndAbility(FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
if (bSelectingTarget)
{
FocusCameraOnTarget(GetAvatarActorFromActorInfo());
}
CandidateTargets.Reset();
SelectedTargetIndex = INDEX_NONE;
bSelectingTarget = false;
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
void UGA_TargetingItemBase::OnTargetNext(FGameplayEventData Payload)
{
ChangeTarget(1);
}
void UGA_TargetingItemBase::OnTargetPrevious(FGameplayEventData Payload)
{
ChangeTarget(-1);
}
void UGA_TargetingItemBase::OnTargetConfirm(FGameplayEventData Payload)
{
AActor* TargetActor = GetSelectedTarget();
if (!IsValid(TargetActor))
{
OnTargetCancel(Payload);
return;
}
if (bSelectingTarget)
{
const bool bCommitted = CommitAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo);
if (!bCommitted)
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
return;
}
}
ExecuteItemCue(ConfirmCueTag);
const bool bExecuted = ExecuteTargetingItem(TargetActor);
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, !bExecuted);
}
void UGA_TargetingItemBase::OnTargetCancel(FGameplayEventData Payload)
{
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, true, true);
}
bool UGA_TargetingItemBase::ExecuteTargetingItem(AActor* TargetActor)
{
return false;
}
void UGA_TargetingItemBase::BuildTargetCandidates()
{
CandidateTargets.Reset();
UWorld* World = GetWorld();
if (!World)
{
return;
}
AActor* OwnerActor = GetAvatarActorFromActorInfo();
if (!OwnerActor)
{
return;
}
for (TActorIterator<ADDBoardGameCharacter> It(World); It; ++It)
{
ADDBoardGameCharacter* CandidateCharacter = *It;
if (!IsValid(CandidateCharacter) || CandidateCharacter == OwnerActor)
{
continue;
}
CandidateTargets.Add(CandidateCharacter);
}
}
void UGA_TargetingItemBase::ChangeTarget(int32 Offset)
{
if (CandidateTargets.IsEmpty())
{
return;
}
SelectedTargetIndex = (SelectedTargetIndex + Offset + CandidateTargets.Num()) % CandidateTargets.Num();
FocusSelectedTarget();
}
void UGA_TargetingItemBase::FocusSelectedTarget()
{
FocusCameraOnTarget(GetSelectedTarget());
}
void UGA_TargetingItemBase::FocusCameraOnTarget(AActor* TargetActor)
{
if (!IsValid(TargetActor))
{
return;
}
ADDBoardGameMode* BoardGameMode = GetWorld() ? GetWorld()->GetAuthGameMode<ADDBoardGameMode>() : nullptr;
if (!BoardGameMode)
{
return;
}
BoardGameMode->FocusAllCamerasOnTarget(TargetActor);
}
AActor* UGA_TargetingItemBase::GetSelectedTarget() const
{
return CandidateTargets.IsValidIndex(SelectedTargetIndex)
? CandidateTargets[SelectedTargetIndex].Get()
: nullptr;
}
팀원이 타겟을 지정하는 아이템 베이스를 짜뒀기에 이제 타겟을 받아서 해당 타겟과 위치를 교환하는 아이템을 빠르게 만들었다.
#include "BoardGame/Abilities/ItemAbilities/GA_Portal.h"
#include "Base/Player/DDBasePlayerState.h"
#include "BoardGame/DDTile.h"
#include "BoardGame/Character/DDBoardGameCharacter.h"
#include "BoardGame/Game/DDBoardGameMode.h"
#include "Common/DDLogManager.h"
bool UGA_Portal::ExecuteTargetingItem(AActor* TargetActor)
{
ADDBoardGameCharacter* CasterCharacter = GetBoardGameCharacter();
if (!IsValid(CasterCharacter)) return false;
ADDBoardGameCharacter* TargetCharacter = Cast<ADDBoardGameCharacter>(TargetActor);
if (!IsValid(TargetCharacter)) return false;
// 캐스터 플레이어 state
ADDBasePlayerState* CasterState = CasterCharacter->GetPlayerState<ADDBasePlayerState>();
// 타겟 플레이어 state
ADDBasePlayerState* TargetState = TargetCharacter->GetPlayerState<ADDBasePlayerState>();
if (!CasterState)
{
LOG_CYS(Warning, TEXT("캐스터 PS 없음"));
return false;
}
if (!TargetState)
{
LOG_CYS(Warning, TEXT("타겟 PS 없음"));
return false;
}
// CurrentTile 값 swap
if (!CasterState->CurrentTile) return false;
if (!TargetState->CurrentTile) return false;
auto TempTile = CasterState->CurrentTile;
CasterState->CurrentTile = TargetState->CurrentTile;
TargetState->CurrentTile = TempTile;
// 위치 이동
CasterCharacter->SetActorLocation(CasterState->CurrentTile->GetStandLocation(CasterCharacter));
TargetCharacter->SetActorLocation(TargetState->CurrentTile->GetStandLocation(TargetCharacter));
LOG_CYS(Warning,TEXT("포탈 아이템: 자리 교환 완료"));
return true;
}
void UGA_Portal::EndAbility(FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
{
// 주사위 생략하기
if (ADDBoardGameMode* GM = Cast<ADDBoardGameMode>(GetWorld()->GetAuthGameMode()))
{
GM->NotifyDiceRolled();
GM->NotifyMovementFinished();
}
Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
}
타겟 엑터와 서로 CurrentTile 값을 교환하고 해당 위치로 이동만 시켜주면 된다.
이제 포탈 후에는 주사위를 못굴리도록 해야해서 EndAbility에서 GM를 직접 접근해서 로직을 처리했는데..
사실 이부분은 좋지 않은 것 같다. 강제로 주사위를 굴렸으니 턴을 끝내도록 처리하는 부분이 의미상 맞지 않으나
시간이 부족해서 이런식으로 처리했다.ㅠㅠ
포탈 아이템에서 가장 허겁지겁 한 부분은 아무래도 스태틱 메쉬이다.
적절한 메쉬를 구할 수 가 없어서 할 수 없이 AI에게 이미지를 뽑아서 그걸 임시방편으로 머터리얼로 씌워뒀는데...

하하 적절한 메쉬를 찾기 어려웠다.
아무래도 자리 바꾸는데 아이템 형체가 필요한건 아니라 생각을 안하고 있다가 아이템을 획득할 때 보여야하는 물체가 필요해서 급하게 AI의 도움을 받았다. 시간이 좀 더 있었으면 간단하게라도 3D 작업을 했으면 좋았을텐데 아쉬움이 있다.
5. RESULT

'팀프로젝트 > DooDoong' 카테고리의 다른 글
| [트러블슈팅] 오브젝트 풀링 활성화 시 Overlap Event 누락 문제 해결 (0) | 2026.05.08 |
|---|---|
| [멀티플레이 게임] 'DooDoong' 프로젝트 회고 (2) | 2026.04.27 |
| [TIL] 2026-04-15 | 레벨 시퀀서 재생, 어빌리티 몽타주 재생 (0) | 2026.04.15 |
| [TIL] 2026-04-14 | 레벨 타일 배치, 인트로 시퀀서, 캐릭터 타일 위치 백업 및 복귀, 데미지 타일, 로비 맵 (0) | 2026.04.14 |
| [TIL] 2026-04-13 | [멀티플레이 게임] 오브젝트 풀링 (0) | 2026.04.13 |