Unreal - 스킬창 & 연동

2026. 1. 1. 02:13·Unreal 프로젝트 다이어리/두번째 프로젝트

미리보기

구현내용

플레이어의 어빌리티 포인트로 배운 각 마지막 트리의 노드를 배우게되면

플레이어의 스킬창에 사용할수있는 액티브 스킬이 추가되며

해당 스킬은 플레이어의 퀵슬롯에 등록하여 사용할수 있습니다

해당 스킬들은 일정시간동안 플레이어의 스탯을 대폭 증가시켜줍니다

 

세키로의 캔디 버프류를 생각하며 구현하였습니다

 

사용한 클래스

사용한 클래스 사용 목적
MainCharacter(캐릭터) 스킬 사용의 연출 + 상태 제어 담당
스킬의 사용 함수의 존재가 있는곳
MainCharacterController(캐릭터컨트롤러) 입력의 시작점 (퀵슬롯 클릭)
입력이 아이템인지 스킬인지 분기
SkillManager(액터컴포넌트) 스킬의 두뇌 (로직 담당)
스킬의 트리 관리, 스킬 해금 여부
액티브 스킬 실행, 버프 생성 및 타이머관리 무기 연출 제어
BasicSword(액터) 캐릭터에 ChildActor로 붙어있는 무기
(연출 전용 클래스)
SkillSlotWidget(위젯) 스킬 하나 를 정의하는 UI
SkillWidget(위젯) 스킬 창 전체 관리자 패널

 

구현방식

우선 어빌리티 포인트를 사용하여 스킬을 배우는 큐브 라는 객체에게서 스킬을 배우면

각 마지막 트리를 배우게되면 스킬창에 습득되는 방식입니다

2025.12.10 - [Unreal 프로젝트 다이어리/두번째 프로젝트] - Unreal - 스킬 트리

 

Unreal - 스킬 트리

구현내용게임중 레벨업으로 얻는 어빌리티 포인트를 사용하여플레이어가 나뉘어진스킬 트리에서 포인트를 투자하고 스킬을 언락하거나영구적으로 스탯을 증가시키는 브랜치로 선택할 수 있

lucodev.tistory.com

 

어빌리티 강화의 그리드트리는 마지막 노드인 리프노드들은 사용이 가능한

액티브 형태의 액티브 스킬입니다

각각 강력한 버프 혹은 스탯뻥튀기의 버프를 사용 할 수 있습니다.

 

스킬의 로직을 담당하는 SkillManager를 제작하여 플레이어의 스킬로직을 담당하는 컴포넌트를 제작하고

해당 컴포넌트내에서 스킬을 컨트롤하는 형식으로 제작하였습니다

 

C++ 구현

  • Skill Manager

스킬 트리 글에서 구현한 SkillManager에서 로직을 추가해주었습니다

더보기
void USkillManager::MakeAbilityGraph()
{
	//node add
	AddSkillNode(1, FSkillNode());
	AddSkillNode(11, FSkillNode());
	AddSkillNode(111, FSkillNode());

	AddSkillNode(2, FSkillNode());
	AddSkillNode(21, FSkillNode());
	AddSkillNode(211, FSkillNode());
	AddSkillNode(22, FSkillNode());
	AddSkillNode(221, FSkillNode());
	AddSkillNode(222, FSkillNode());

	AddSkillNode(3, FSkillNode());
	AddSkillNode(31, FSkillNode());
	AddSkillNode(311, FSkillNode());
	AddSkillNode(32, FSkillNode());

	//부모조건
	skillNodes[11].prerequisites.Add(1);
	skillNodes[111].prerequisites.Add(11);

	skillNodes[21].prerequisites.Add(2);
	skillNodes[211].prerequisites.Add(21);

	skillNodes[22].prerequisites.Add(2);
	skillNodes[221].prerequisites.Add(22);
	skillNodes[222].prerequisites.Add(22);

	skillNodes[31].prerequisites.Add(3);
	skillNodes[311].prerequisites.Add(31);

	skillNodes[32].prerequisites.Add(3);

	//edge
	AddSkillEdge(1, 11);
	AddSkillEdge(11, 111);

	AddSkillEdge(2, 21);
	AddSkillEdge(21, 211);

	AddSkillEdge(2, 22);
	AddSkillEdge(22, 221);
	AddSkillEdge(22, 222);

	AddSkillEdge(3, 31);
	AddSkillEdge(31, 311);
	AddSkillEdge(3, 32);

	//cost 가중치
	skillNodes[1].cost = 1;
	skillNodes[11].cost = 2;
	skillNodes[111].cost = 5;

	skillNodes[2].cost = 1;
	skillNodes[21].cost = 2;
	skillNodes[211].cost = 4;

	skillNodes[22].cost = 2;
	skillNodes[221].cost = 4;
	skillNodes[222].cost = 4;

	skillNodes[3].cost = 1;
	skillNodes[31].cost = 2;
	skillNodes[311].cost = 5;
	skillNodes[32].cost = 2;

	//액티브처리
	// 111 : 체력 3분 20% 증가
	{
		FSkillUsing use;
		use.usingType = ESkillUsingType::UnlockActiveSkill;
		use.usingName = TEXT("HP Buff 20%");
		use.activeSkillID = TEXT("Buff_HP_3M20P");

		skillNodes[111].skill.Add(use);
	}

	// 211 : 공격력 3분 20%
	{
		FSkillUsing use;
		use.usingType = ESkillUsingType::UnlockActiveSkill;
		use.usingName = TEXT("ATK Buff 20%");
		use.activeSkillID = TEXT("Buff_ATK_3M20P");

		skillNodes[211].skill.Add(use);
	}

	// 221 : 공격력 50% + 공속 20% (1분)
	{
		FSkillUsing use;
		use.usingType = ESkillUsingType::UnlockActiveSkill;
		use.usingName = TEXT("ATK50 AS20");
		use.activeSkillID = TEXT("Buff_ATKAS_1M50P20P");

		skillNodes[221].skill.Add(use);
	}

	// 222 : 공격속도 5분 20%
	{
		FSkillUsing use;
		use.usingType = ESkillUsingType::UnlockActiveSkill;
		use.usingName = TEXT("AS Buff 20%");
		use.activeSkillID = TEXT("Buff_AS_5M20P");

		skillNodes[222].skill.Add(use);
	}

	// 311 : 크리 확률 5분 30%
	{
		FSkillUsing use;
		use.usingType = ESkillUsingType::UnlockActiveSkill;
		use.usingName = TEXT("CRIT Buff 30%");
		use.activeSkillID = TEXT("Buff_CRIT_5M30P");

		skillNodes[311].skill.Add(use);
	}
}
void USkillManager::ExecuteActiveSkill(const FSkillUsing& usingData)
{
	if (!statComp)
		return;

	if (usingData.usingType != ESkillUsingType::UnlockActiveSkill)
		return;

	int32 skillNodeID = 0;
	for (auto& nodeIdElem : skillNodes)
	{
		for (auto& skillUse : nodeIdElem.Value.skill)
		{
			if (skillUse.activeSkillID == usingData.activeSkillID)
			{
				skillNodeID = nodeIdElem.Key;
				break;
			}
		}
		if (skillNodeID != 0)
			break;
	}

	FSkillData* skillData = GetSkillDataByID(skillNodeID);

	const FName skillID = usingData.activeSkillID;
	FTempBuff activeBuff;
	activeBuff.buffIcon = skillData->skillIcon;
	bool bValid = true;

	//== HP20% 3분 ==
	if (skillID == "Buff_HP_3M20P")
	{
		activeBuff.statType = EBaseStatType::Hp;
		activeBuff.bonusValue = 0.f;
		activeBuff.bonusRate = 0.2f;
		activeBuff.duration = 180.f;
		UE_LOG(LogTemp, Warning, TEXT("WWDD"));
		
	}
	//== ATK 20% 3분 ==
	else if (skillID == "Buff_ATK_3M20P")
	{
		activeBuff.statType = EBaseStatType::AttackPower;
		activeBuff.bonusValue = 0.f;
		activeBuff.bonusRate = 0.2f;
		activeBuff.duration = 180.f;
	}
	//== ATK 50% + AS 20% 1분 ==
	else if (skillID == "Buff_ATKAS_1M50P20P")
	{
		//공격력
		FTempBuff atkBuff;
		atkBuff.statType = EBaseStatType::AttackPower;
		atkBuff.bonusRate = 0.5f;
		atkBuff.duration = 60.f;
		atkBuff.buffIcon = skillData->skillIcon;
		statComp->ApplyTempBuff(atkBuff, skillID);

		//공속
		FTempBuff asBuff;
		asBuff.statType = EBaseStatType::AttackSpeed;
		asBuff.bonusRate = 0.2f;
		asBuff.duration = 60.f;
		atkBuff.buffIcon = skillData->skillIcon;
		statComp->ApplyTempBuff(asBuff, skillID);

		sword->ActiveSkill(true);
		float skillDuration = activeBuff.duration;

		GetWorld()->GetTimerManager().ClearTimer(th_ActiveSkillTimerHandle);
		GetWorld()->GetTimerManager().SetTimer(th_ActiveSkillTimerHandle, this,
			&USkillManager::OnActiveSkillEnd, 60, false);
		return;
	}
	//==AS 20% 5분==
	else if (skillID == "Buff_AS_5M20P")
	{
		activeBuff.statType = EBaseStatType::AttackSpeed;
		activeBuff.bonusRate = 0.2f;
		activeBuff.duration = 300.f;
	}
	//==CRIT 30% 5분==
	else if (skillID == "Buff_CRIT_5M30P")
	{
		activeBuff.statType = EBaseStatType::CriticalRate;
		activeBuff.bonusRate = 0.3f;
		activeBuff.duration = 300.f;
	}
	else
	{
		bValid = false;
	}
	if (bValid)
	{
		statComp->ApplyTempBuff(activeBuff, skillID);

		sword->ActiveSkill(true);
		float skillDuration = activeBuff.duration;

		GetWorld()->GetTimerManager().ClearTimer(th_ActiveSkillTimerHandle);
		GetWorld()->GetTimerManager().SetTimer(th_ActiveSkillTimerHandle,this,
			&USkillManager::OnActiveSkillEnd,skillDuration,false);

	}
}
void USkillManager::ApplyActiveSkill(int32 skillID)
{
	FSkillData* skillData = GetSkillDataByID(skillID);
	if (!skillData)
		return;

	if (GetSkillState(skillID) != ESkillState::learned)
		return;

	if (!IsLeafSkill(skillID))
		return;

	const FSkillNode* node = skillNodes.Find(skillID);
	if (!node)
		return;

	for (const FSkillUsing& usingData : node->skill)
		ExecuteActiveSkill(usingData);
	
}
bool USkillManager::CanUseActiveSkill(int32 SkillNodeID) const
{
	if (!statComp)
		return false;

	const FSkillNode* node = skillNodes.Find(SkillNodeID);
	if (!node)
		return false;

	for (const FSkillUsing& usingData : node->skill)
	{
		if (usingData.usingType != ESkillUsingType::UnlockActiveSkill)
			continue;

		if (statComp->HasActiveSkillBuff(usingData.activeSkillID))
			return false;
	}

	return true;
}

 

  • SkillWidget && SkillSlotWidget

어빌리티 스탯을 사용하여 스킬을 찍으면 트리의 배움 정보를 바탕으로

스킬창에 잠김 / 열림을 표시하며

스킬의 한칸을 정의하는 SkillSlotWidget으로 구성되어있는 전체 스킬창

SkillWidget입니다

리프 노드만슬롯으로 생성하여 UI슬롯에 추가합니다.

 

스킬창을 미리 다 만들어 두지 않고 나의 스킬노드 와 리프 노드가 뭐가 있냐에 따라

슬롯이 변동되는 방식으로 하드코딩을 하지않을려고 노력하며 구현하였습니다

 

위젯은 이렇게 생겼습니다

드래그 기능이 들어있으며 DragAndDropOper을 사용하여 스킬드롭또한 구현하였습니다

 

더보기
FReply USkillSlotWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && skillID >= 0)
		return FReply::Handled().DetectDrag(TakeWidget(), EKeys::LeftMouseButton);

	return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent);
	
}

void USkillSlotWidget::NativeOnDragDetected(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent, UDragDropOperation*& OutOperation)
{
	ESkillState state = skillManager->GetSkillState(skillID);
	if (state != ESkillState::learned)
		return;


	USkillDragDropOper* dragOper = NewObject<USkillDragDropOper>();
	if (!dragOper)
		return;

	dragOper->skillID = skillID;
	dragOper->startSlotWidget = this;

	UUserWidget* dragWidget = CreateWidget<UUserWidget>(GetWorld(), dragVisualClass); 
	if (dragWidget)
	{
		UImage* dragImage = Cast<UImage>(dragWidget->GetWidgetFromName(TEXT("Image_ReplicateImage")));
		if (dragImage)
		{
			dragImage->SetBrush(Image_Skill->Brush);
		}
		dragOper->DefaultDragVisual = dragWidget;
		dragOper->Pivot = EDragPivot::CenterCenter;
	}

	OutOperation = dragOper;


}


bool USkillSlotWidget::NativeOnDrop(const FGeometry& InGeometry, const FDragDropEvent& InDragDropEvent, UDragDropOperation* InOperation)
{
	USkillDragDropOper* dragOper = Cast<USkillDragDropOper>(InOperation);
	if (!dragOper)
		return false; 

	int32 draggedSkillID = dragOper->skillID;
	if (slotIdx >= 0)
	{
		//mainCon->mainWidget->buffwidget
	}
	if (dragOper->startSlotWidget && dragOper->startSlotWidget != this)
		dragOper->startSlotWidget->InitSlot(-1, skillManager);

	return true;
}

void USkillSlotWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent)
{
	Super::NativeOnMouseEnter(InGeometry, InMouseEvent);

	if (!skillManager) return;
	FSkillData* skillData = skillManager->GetSkillDataByID(skillID);
	if (!skillData) return;

	APlayerController* pc = GetOwningPlayer();
	if (!pc || !mainCon || !mainCon->toolTipWidget) return;

	mainCon->toolTipWidget->SetSkillInformation(*skillData);
	mainCon->toolTipWidget->SetRenderScale(FVector2D(0.8f, 0.8f));

	FVector2D mousePos = UWidgetLayoutLibrary::GetMousePositionOnViewport(pc);

	FVector2D viewportSize;
	GEngine->GameViewport->GetViewportSize(viewportSize);

	FVector2D tooltipSize = mainCon->toolTipWidget->GetDesiredSize();
	FVector2D adjustedPos = mousePos + FVector2D(12.f, 12.f);

	if (adjustedPos.X + tooltipSize.X > viewportSize.X)
		adjustedPos.X = mousePos.X - tooltipSize.X - 12.f;
	if (adjustedPos.Y + tooltipSize.Y > viewportSize.Y)
		adjustedPos.Y = mousePos.Y - tooltipSize.Y - 12.f;

	adjustedPos.X = FMath::Max(0.f, adjustedPos.X);
	adjustedPos.Y = FMath::Max(0.f, adjustedPos.Y);

	if (mainCon->toolTipWidget->WidgetSwitcher_Main && mainCon->toolTipWidget->Overlay_Ability)
		mainCon->toolTipWidget->WidgetSwitcher_Main->SetActiveWidget(mainCon->toolTipWidget->Overlay_Ability);

	mainCon->toolTipWidget->SetPositionInViewport(adjustedPos, false);
	mainCon->toolTipWidget->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
}

void USkillSlotWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent)
{
	Super::NativeOnMouseLeave(InMouseEvent);

	if (mainCon && mainCon->toolTipWidget)
		mainCon->toolTipWidget->SetVisibility(ESlateVisibility::Collapsed);
}



void USkillSlotWidget::InitSlot(int32 inSkillID, USkillManager* inManager)
{
	skillID = inSkillID;
	skillManager = inManager;
	Refresh();
}

void USkillSlotWidget::Refresh()
{
	if (!skillManager)
		return;

	ESkillState state = skillManager->GetSkillState(skillID);
	FSkillData* skillData = skillManager->GetSkillDataByID(skillID);
	if (!skillData)
		return;

	TextBlock_SkillName->SetText(FText::FromString(skillData->skillName));
	Image_Skill->SetBrushFromTexture(skillData->skillIcon);

	switch (state)
	{
	case ESkillState::locked:
		Overlay_Lock->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
		break;
	case ESkillState::Available:
		Overlay_Lock->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
		break;
	case ESkillState::learned:
		Overlay_Lock->SetVisibility(ESlateVisibility::Collapsed);
		break;
	default:
		break;
	}
	
}

 

void USkillWidget::NativeConstruct()
{
	Super::NativeConstruct();

    mainCon = Cast<AMainCharacterController>(GetOwningPlayer());
    skillManager = mainCon->GetPawn()->FindComponentByClass<USkillManager>();

    skillManager->onSkillManagerReady.RemoveAll(this);
    skillManager->onSkillManagerReady.AddDynamic(this, &USkillWidget::InitSkillSlot);

	if (Button_Exit)
		Button_Exit->OnClicked.AddDynamic(this, &USkillWidget::ClickExitButton);

}

FReply USkillWidget::NativeOnMouseButtonDown(const FGeometry& inGeometry, const FPointerEvent& inMouseEvent)
{
    if (!DragBorder || !OutterBox)
        return Super::NativeOnMouseButtonDown(inGeometry, inMouseEvent);

    FVector2D mousePos = inMouseEvent.GetScreenSpacePosition();

    if (DragBorder->GetCachedGeometry().IsUnderLocation(mousePos) && inMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
    {
        bIsDragging = true;

        if (UCanvasPanelSlot* canvasSlot = Cast<UCanvasPanelSlot>(OutterBox->Slot))
            dragOffset = inGeometry.AbsoluteToLocal(mousePos) - canvasSlot->GetPosition();

        return FReply::Handled().CaptureMouse(TakeWidget()).UseHighPrecisionMouseMovement(TakeWidget());
    }

    return Super::NativeOnMouseButtonDown(inGeometry, inMouseEvent);
}

FReply USkillWidget::NativeOnMouseMove(const FGeometry& inGeometry, const FPointerEvent& inMouseEvent)
{
	if (!bIsDragging || !OutterBox)
		return Super::NativeOnMouseMove(inGeometry, inMouseEvent);

	if (UCanvasPanelSlot* canvasSlot = Cast<UCanvasPanelSlot>(OutterBox->Slot))
	{
		FVector2D localMousePos =inGeometry.AbsoluteToLocal(inMouseEvent.GetScreenSpacePosition());

		canvasSlot->SetPosition(localMousePos - dragOffset);
	}

	return FReply::Handled();
}

FReply USkillWidget::NativeOnMouseButtonUp(const FGeometry& inGeometry, const FPointerEvent& inMouseEvent)
{
    if (bIsDragging && inMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton)
    {
        bIsDragging = false;
        return FReply::Handled().ReleaseMouseCapture();
    }

    return Super::NativeOnMouseButtonUp(inGeometry, inMouseEvent);
}

void USkillWidget::ClickExitButton()
{
    if (!mainCon)
        return;
    mainCon->ToggleSkillWindow();
}

void USkillWidget::InitSkillSlot()
{
    WrapBox_Skill->ClearChildren();
    skillSlots.Empty();

    for (auto& slotElem : skillManager->skillNodes)
    {
        int32 skillID = slotElem.Key;

        if (!skillManager->IsLeafSkill(skillID))
            continue;

        USkillSlotWidget* slot = CreateWidget<USkillSlotWidget>(this, skillSlotClass);
        slot->InitSlot(skillID, skillManager);

        slot->InitSlot(skillID, skillManager);
        slot->mainCon = mainCon;
        slot->slotIdx = -1;

        WrapBox_Skill->AddChild(slot);
        skillSlots.Add(slot);
    }
    skillManager->onSkillStateChanged.RemoveAll(this);
    skillManager->onSkillStateChanged.AddDynamic(this, &USkillWidget::RefreshAllSlots);
}

void USkillWidget::RefreshAllSlots()
{
    for (auto* skillSlot : skillSlots)
        skillSlot->Refresh();
}

  • MainCharacter

캐릭터에서는 스킬매니저에 스킬을 사용한다 라는개념을 요청합니다

데이터테이블에 있는 애니메이션을 재생하고 노티파이로 호출하는방식을 채택하였습니다

더보기
void AMainCharacter::ExcuteSkill()
{
    if (bUsingSkill)
        return;
    if (!skillManager->CanUseActiveSkill(pendingSkillID))
        return;

    FCharacterAnimDataTable* row = buffRows[0];
    UAnimInstance* animIst = GetMesh()->GetAnimInstance();

    float playAnimTime = PlayAnimMontage(row->usingAnimation);

    bUsingSkill = true;
    FOnMontageEnded endDel;
    endDel.BindUObject(this, &AMainCharacter::OnSkillMontageEnded);
    animIst->Montage_SetEndDelegate(endDel, row->usingAnimation);
}

void AMainCharacter::OnSkillMontageEnded(UAnimMontage* montage, bool bInterrupted)
{
    bUsingSkill = false;
}



//노티파이 호출

void UMainCharacterAnimInstance::AnimNotify_UsingSkill()
{
if (!mainCharacter)
return;

if (mainCharacter->pendingSkillID < 0)
return;

mainCharacter->skillManager->ApplyActiveSkill(mainCharacter->pendingSkillID);
}



//노티파이에서 호출되어 실행되는 함수

void USkillManager::ApplyActiveSkill(int32 skillID)
{
FSkillData* skillData = GetSkillDataByID(skillID);
if (!skillData)
return;

if (GetSkillState(skillID) != ESkillState::learned)
return;

if (!IsLeafSkill(skillID))
return;

const FSkillNode* node = skillNodes.Find(skillID);
if (!node)
return;

for (const FSkillUsing& usingData : node->skill)
ExecuteActiveSkill(usingData);

}

//ExecuteActiveSkill의 함수는 SkillManager에서 소개

  • MainCharacterController

캐릭터가 사용하는 컨트롤러이자 퀵슬롯에 등록되어있는

ID로 스킬인지 아이템인지 분기

아이템은 퀵슬롯에 ID가 0번 ~ 100번

스킬은 퀵슬롯에 ID가 111번 ~ 으로 구성되어있습니다

더보기
void AMainCharacterController::HandleQuickSlotInput(int32 slotIdx)
{
	FItemSlot slot = quickSlotComp->GetQuickSlot(slotIdx);
	if (slot.itemID < 0)
		return;

	// 스킬의 경우 111 ~ 
	if (slot.itemID >= 111)
	{
		mainCharacter->pendingSkillID = slot.itemID;
		mainCharacter->ExcuteSkill();
		return;
	}
	mainCharacter->usingConsumSlotIdx = slotIdx;
	mainCharacter->ExcutePendingItems();
}

 

void AMainCharacter::ExcutePendingItems()
{
    if (bUsingItem)
        return;

    if (healRows.Num() == 0)
        return;

    FItemSlot slot = quickSlotComp->GetQuickSlot(usingConsumSlotIdx);
    if (slot.itemID < 0)
        return;

    if (slot.itemID >= 111)   //스킬일때
       return;

    cachedUsingItemID = slot.itemID;
    usingPotion->SetActorHiddenInGame(false);

    FCharacterAnimDataTable* row = healRows[0];
    UAnimInstance* animIst = GetMesh()->GetAnimInstance();
    bUsingItem = true;

    float playAnimTime = PlayAnimMontage(row->usingAnimation);
    if (playAnimTime <= 0.f)
    {
        bUsingItem = false;
        return;
    }

    FOnMontageEnded endDel;
    endDel.BindUObject(this, &AMainCharacter::OnUseItemMontageEnded);
    animIst->Montage_SetEndDelegate(endDel, row->usingAnimation);
}

 

 


  • ABasicSword

마지막으로 연출으로 사용되는 실제로 캐릭터가 사용하는 검 액터입니다

캐릭터에 ChildActor로 달려서 사용되며

버프를 사용하면 플레이어의 칼은 오버레이 메테리얼이 설정되며 케스케이드 혹은 나이아가라설정 함수가 호출됩니다

연출을 추가해주었습니다

오버레이 메테리얼 을 사용하였습니다

더보기
	UPROPERTY(EditAnywhere, Category="Particle")
	UParticleSystemComponent* swordParticle;

	UFUNCTION()
	void ActiveSkill(bool bActive);

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Overlay")
	UMaterialInterface* overlayMaterial;

	UPROPERTY()
	UMaterialInstanceDynamic* overlayMID;
    
    //생성자에 추가
    swordParticle = CreateDefaultSubobject<UParticleSystemComponent>(TEXT("SwordParticle"));
	swordParticle->SetupAttachment(RootComponent); 
    
    //begin play에 추가
    if (overlayMaterial)
		overlayMID = UMaterialInstanceDynamic::Create(overlayMaterial, this);
        
      
    //연출함수
    void ABasicSword::ActiveSkill(bool bActive)
{
	if (bActive)
	{
		swordParticle->SetVisibility(true);
		swordParticle->Activate(true);
		swordMesh->SetOverlayMaterial(overlayMID);
	}
	else
	{
		swordParticle->SetVisibility(false);
		swordParticle->Deactivate();
		swordMesh->SetOverlayMaterial(nullptr);
	}
}

결과

  • 플레이어는 실제 스탯에 적용되는 스킬을 사용할 수 있습니다
  • 플레이어는 어빌리티 포인트를 사용하여 스킬을 배우면 스킬창의 스킬창이 언락됩니다
  • 언락된 스킬은 드래그앤드롭하여 퀵슬롯에 등록 할 수 있습니다
  • 퀵슬롯에 등록된 스킬을 사용하면 데이터테이블에 등록된 애니메이션이 나오며 노티파이로 오버레이 연출이 나옵니다

플레이어가 사용 가능한 액티브 스킬 (리프노드) 들은 스킬창에 등록되며 스킬이 "배움" 상태가 아니면 잠겨져있게됩니다

 

어빌리티 포인트를 사용하여 스킬을 배우고 배운 스킬은 언락되며 퀵슬롯에 등록 할 수 있습니다

 

 

스킬창에서 스킬창으로 스킬을 등록 할수 있습니다

 

스킬을 사용하면 캐릭터의 검의 오버레이가 변경되며 주위의 케스케이드가 연출됩니다

 

풀영상

 

드디어 전투 부분으로 들어갈수 있겠군요 정말 멀고도 멀었습니다..

저작자표시 비영리 변경금지 (새창열림)

'Unreal 프로젝트 다이어리 > 두번째 프로젝트' 카테고리의 다른 글

Unreal - 처형( Execution )  (0) 2026.01.05
Unreal - 회피(Dodge)  (0) 2026.01.03
Unreal - 버프창  (0) 2025.12.27
Unreal - 퀵슬롯  (0) 2025.12.27
Unreal - 퀘스트 시스템2  (0) 2025.12.22
'Unreal 프로젝트 다이어리/두번째 프로젝트' 카테고리의 다른 글
  • Unreal - 처형( Execution )
  • Unreal - 회피(Dodge)
  • Unreal - 버프창
  • Unreal - 퀵슬롯
lucodev
lucodev
언리얼 포폴개발 일기
  • lucodev
    루코 개발테이블
    lucodev
  • 전체
    오늘
    어제
    • 분류 전체보기 (236)
      • Unreal 프로젝트 다이어리 (132)
        • 첫번째 프로젝트 (73)
        • 두번째 프로젝트 (59)
      • Unreal 팁 (8)
      • Unreal 디버깅 (8)
      • C++ 프로그래머스 (52)
        • Stack,Queue (7)
        • Hash (4)
        • Heap (2)
        • Sort (5)
        • Exhaustive search (5)
        • Greedy (2)
        • BFS , DFS (7)
        • Graph (2)
        • Dynamic Programming (1)
        • C++ Math (2)
        • 기타 문제 (14)
      • C++ 백준 (5)
      • C++ 팁 (1)
      • 개인 코테 & 스타디 <비공개> (29)
        • 코드 개인보관함 (9)
        • 코딩테스트+@ (11)
        • 알고리즘 스타디 (6)
        • 알고리즘 스타디 과제 (3)
        • 비공개 (0)
  • 인기 글

  • 최근 글

  • 최근 댓글

  • 링크

  • 공지사항

  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 태그

    언리얼 ui
    unreal
    언리얼 behaviortree
    언리얼 시퀀스
    언리얼 세키로
    unreal 파쿠르
    언리얼 behavior tree
    unreal 상호작용
    언리얼 비헤이비어트리
    언리얼 파쿠르
    언리얼 컷씬
    unreal npc
    언리얼 인터렉션
    unreal 세키로
    언리얼 인벤토리
    언리얼
    언리얼 상호작용
    언리얼 parkour
    unreal inventory
    unreal 인벤토리
  • hELLO· Designed By정상우.v4.10.3
lucodev
Unreal - 스킬창 & 연동
상단으로

티스토리툴바