다양한 기록

[ChronoSpace] Black Hole & White Hole 본문

언리얼 엔진/ChronoSpace

[ChronoSpace] Black Hole & White Hole

라구넹 2025. 1. 26. 01:23

https://www.youtube.com/watch?v=_TNFaHFzXfM&t=270s

메테리얼은 이거 보고 만들었는데,

내부 로직은 Chrono Space가 오브젝트가 캐릭터 기반이라 아예 다를 것이라

그냥 내부 안보고 새로 만들었다

 

메테리얼

 

관련 소스

CSGABlackHole

CSAT_BlackHoleSphere

CSTA_BlackHoleSphere

 

CSGAWhiteHole

CSWhiteHole (일반 액터임)

 

화이트홀이 없다 → 블랙홀이 Event Horizon 내부로 들어온 거 제거

화이트홀이 있다 → 블랙홀이 화이트홀 위치로 이동시킴

화이트홀은 CSCharacterPlayer가 보유 → 멀티플레이어 환경에선 각각 로컬 플레이어마다 하나씩 보유 가능

 

블랙홀

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


#include "GA/TA/CSTA_BlackHoleSphere.h"
#include "AbilitySystemComponent.h"
#include "Abilities/GameplayAbility.h"
#include "Components/SphereComponent.h"
#include "Components/StaticMeshComponent.h"
#include "GameFramework/Character.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Character/CSCharacterPlayer.h"
#include "Actor/CSWhiteHall.h"
#include "Physics/CSCollision.h"
#include "Kismet/GameplayStatics.h"
#include "ChronoSpace.h"

ACSTA_BlackHoleSphere::ACSTA_BlackHoleSphere()
{
	PrimaryActorTick.bCanEverTick = true; 

	// GravitySphereTrigger
	GravitySphereTrigger = CreateDefaultSubobject<USphereComponent>(TEXT("GravitySphereTrigger"));
	RootComponent = GravitySphereTrigger;
	GravitySphereTrigger->SetSphereRadius(GravityInfluenceRange, true);
	GravitySphereTrigger->SetRelativeLocation(FVector(600.0f, 0.0f, 200.0f));
	GravitySphereTrigger->SetCollisionProfileName(CPROFILE_CSTRIGGER);

	GravitySphereTrigger->OnComponentBeginOverlap.AddDynamic(this, &ACSTA_BlackHoleSphere::OnTriggerBeginOverlap);
	GravitySphereTrigger->OnComponentEndOverlap.AddDynamic(this, &ACSTA_BlackHoleSphere::OnTriggerEndOverlap);

	// EventHorizonSphereTrigger
	EventHorizonSphereTrigger = CreateDefaultSubobject<USphereComponent>(TEXT("EventHorizonSphereTrigger"));
	EventHorizonSphereTrigger->SetupAttachment(GravitySphereTrigger);
	EventHorizonSphereTrigger->SetSphereRadius(EventHorizonRadius, true);
	EventHorizonSphereTrigger->SetRelativeLocation(FVector(0.0f, 0.0f, 0.0f));
	EventHorizonSphereTrigger->SetCollisionProfileName(CPROFILE_CSTRIGGER);

	EventHorizonSphereTrigger->OnComponentBeginOverlap.AddDynamic(this, &ACSTA_BlackHoleSphere::OnEventHorizonBeginOverlap); 

	// Static Mesh
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComp"));
	StaticMeshComp->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	StaticMeshComp->SetupAttachment(GravitySphereTrigger);

	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/Mesh/StaticMesh/MaterialSphere.MaterialSphere'"));
	if (StaticMeshRef.Object)
	{
		StaticMeshComp->SetStaticMesh(StaticMeshRef.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterialInstance> MaterialRef(TEXT("/Script/Engine.MaterialInstanceConstant'/Game/Material/MI_BlackHole.MI_BlackHole'"));
	if (MaterialRef.Object)
	{
		StaticMeshComp->SetMaterial(0, MaterialRef.Object);
	}
}

void ACSTA_BlackHoleSphere::BeginPlay()
{
	Super::BeginPlay();

	if (!bShowDebug) return;

	if (GravitySphereTrigger)
	{
		FVector SphereLocation = GravitySphereTrigger->GetComponentLocation(); 
		float SphereRadius = GravitySphereTrigger->GetScaledSphereRadius();   

		DrawDebugSphere(
			GetWorld(),
			SphereLocation,
			SphereRadius,
			12,          // 세그먼트 수 (구의 매끄러움)
			FColor::Green,
			false,       // 지속 표시
			10,           // 지속 시간
			0,           // 디버그 선 우선순위
			2.0f         // 선 두께
		);
	}
}

void ACSTA_BlackHoleSphere::Tick(float DeltaSeconds)
{
	Super::Tick(DeltaSeconds);

	FVector BlackHoleLocation = GravitySphereTrigger->GetComponentLocation();
	
	for (auto Char = CharactersInSphereTrigger.CreateIterator(); Char; ++Char)
	{
		if (IsValid(Char.Value()))
		{
			FVector Power(230000.0f, 230000.0f, 230000.0f);
			FVector TargetLocation = Char.Value()->GetActorLocation();
			FVector Distance = BlackHoleLocation - TargetLocation;
			FVector Direction = Distance.GetSafeNormal();
			
			Char.Value()->GetCharacterMovement()->AddForce(Power * Direction);
		}
	}
}

void ACSTA_BlackHoleSphere::StartTargeting(UGameplayAbility* Ability)
{
	Super::StartTargeting(Ability);
	SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}

void ACSTA_BlackHoleSphere::ConfirmTargetingAndContinue()
{
	if (SourceActor)
	{
		OnComplete.Broadcast();
	}
}

void ACSTA_BlackHoleSphere::OnTriggerBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	ACharacter* DetectedCharacter = Cast<ACharacter>(OtherActor);

	if (DetectedCharacter)
	{
		CharactersInSphereTrigger.Emplace(DetectedCharacter->GetFName(), DetectedCharacter);
	}
}

void ACSTA_BlackHoleSphere::OnTriggerEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
	ACharacter* DetectedCharacter = Cast<ACharacter>(OtherActor);

	if (DetectedCharacter)
	{
		CharactersInSphereTrigger.Remove(DetectedCharacter->GetFName());
	}
}

void ACSTA_BlackHoleSphere::OnEventHorizonBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	ACharacter* OverlapedCharacter = Cast<ACharacter>(OtherActor);
	ACSCharacterPlayer* OverlapedCharacterPlayer = Cast<ACSCharacterPlayer>(OtherActor);


	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>( UGameplayStatics::GetPlayerCharacter( GetWorld(), 0 ) );

	if ( ACSWhiteHall* WhiteHall = Player->GetWhiteHall() )
	{
		FVector NewLocation = WhiteHall->GetActorLocation();
		
		OtherActor->SetActorLocation(NewLocation);
		CharactersInSphereTrigger.Remove(OverlapedCharacter->GetFName());
	}
	else if (OverlapedCharacter)
	{
		if (OverlapedCharacterPlayer == nullptr)
		{
			OtherActor->Destroy();
		}
	} 
}

블랙홀이 영향이 끼치는 범위(GravityInfluenceRange)

물체가 사라지는 범위(EventHorizonRadius)

GravityInfluenceRange에 들어오면 Tick에서 AddForce로 끌어당긴다

 

void ACSTA_BlackHoleSphere::OnEventHorizonBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepHitResult)
{
	ACharacter* OverlapedCharacter = Cast<ACharacter>(OtherActor);
	ACSCharacterPlayer* OverlapedCharacterPlayer = Cast<ACSCharacterPlayer>(OtherActor);


	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>( UGameplayStatics::GetPlayerCharacter( GetWorld(), 0 ) );

	if ( ACSWhiteHall* WhiteHall = Player->GetWhiteHall() )
	{
		FVector NewLocation = WhiteHall->GetActorLocation();
		
		OtherActor->SetActorLocation(NewLocation);
		CharactersInSphereTrigger.Remove(OverlapedCharacter->GetFName());
	}
	else if (OverlapedCharacter)
	{
		if (OverlapedCharacterPlayer == nullptr)
		{
			OtherActor->Destroy();
		}
	} 
}

EventHorizonRadius에 들어오면 화이트홀이 있는지 체크

화이트홀이 있으면 화이트홀 위치로 텔레포트, 없으면 제거

 

화이트홀

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


#include "GA/CSGA_WhiteHall.h"
#include "Actor/CSWhiteHall.h"
#include "Character/CSCharacterPlayer.h"
#include "ChronoSpace.h"

UCSGA_WhiteHall::UCSGA_WhiteHall()
{
}

void UCSGA_WhiteHall::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	UE_LOG(LogCS, Display, TEXT("UCSGA_WhiteHall::ActivateAbility"));
	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;

	ACSCharacterPlayer* Player = Cast<ACSCharacterPlayer>(ActorInfo->AvatarActor);
	if ( Player->GetWhiteHall() )
	{
		Player->ClearWhiteHall(); 
		EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled); 
		return;
	}

	const FVector SpawnLocation = Player->GetActorLocation();

	ACSWhiteHall* WhiteHall = GetWorld()->SpawnActor<ACSWhiteHall>(ACSWhiteHall::StaticClass(), SpawnLocation, FRotator::ZeroRotator);
	Player->SetWhiteHall(WhiteHall);

	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

화이트홀은 게임 어빌리티에서 바로 스폰해준다

현재 플레이어 캐릭터의 위치에 스폰

이미 스폰된게 있으면 제거하는 방식으로 작동

스폰 -> 제거 -> 스폰 -> 제거 ..

 

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


#include "Actor/CSWhiteHall.h"
#include "Components/StaticMeshComponent.h"

// Sets default values
ACSWhiteHall::ACSWhiteHall()
{
	StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMeshComp"));
	RootComponent = StaticMeshComp;

	static ConstructorHelpers::FObjectFinder<UStaticMesh> StaticMeshRef(TEXT("/Script/Engine.StaticMesh'/Game/Mesh/StaticMesh/MaterialSphere.MaterialSphere'"));
	if (StaticMeshRef.Object)
	{
		StaticMeshComp->SetStaticMesh(StaticMeshRef.Object);
	}

	static ConstructorHelpers::FObjectFinder<UMaterialInstance> MaterialRef(TEXT("/Script/Engine.MaterialInstanceConstant'/Game/Material/MI_WhiteHole.MI_WhiteHole'"));
	if (MaterialRef.Object)
	{
		StaticMeshComp->SetMaterial(0, MaterialRef.Object);
	}
}

화이트홀은 액터에 스태틱 메쉬만 달렸다

 

 

 

실행 시 이렇게 된다

 

캐릭터 AddForce 관련하여 이해하는게 중요했는데,

이전 게시글인 Character 밀기가 상당히 도움이 되었다.