Stellar Survivor

Project Stats:

Name: Stellar Survivor
Project type: Student project
Production Time: 8 months
Date: 2019
Engine/language: Unreal engine/C++
Platforms: PC
Tools: Unreal Engine, Perforce, Jira
Team members: 10 programming students 9 design/production students and 8 art students

Project Description

Stellar Survivor is a survival/tower defense game made for PC. The game has multiplayer support so you can play with your friends to defend your base.

Assignment

In the third year, we were allowed to pick from a list of project briefs. I picked the survival game project brief. We ended up with a team of 29 people. We had all of year 3 to work on the game. I worked on several components of the game. A lot of my work did not make it into the end product because the game went through many iterations and some components were scraped.

My contributions

I worked on multiple parts of the game. Please click on any of the links below to delve deeper into my contributions.

Terrain generation research and testing

At the start of the project, the game designers worked on the concept of the game. The programmers researched and experimented with tech that might be useful for the game. During this time I researched and experimented with different methods of procedural terrain generation.

For my first attempt, I used 2d noise data to generate a plane where I change the height of the vertex based on the noise value from a 2d noise function. I used a combination of Perlin and Voronoi noise.

I also tried procedurally generating a terrain with marching cubes. this allowed for caves and overhangs. But was way more expensive to generate.

Base building system

The base-building system is a combination of the base-building systems for the games Rust and Fortnite. The base building system I show here is not used in the game we ended up making. But its used as the basis of a object placement system.

Object placement system

Using the base building system as the basis of an object placement system

After receiving feedback on the state of the game we realized that we overscoped. Our team decided to scrap the base building system to have me work on different parts of the game. Fortunately, I was able to use the base building system as a basis for the object placement system. The code needed for the object placement system was similar to the base building system. A object in the base building system is essentially a base with only one part. I left most of the code of the base building intact to possibly support connecting walls or placing traps on walls in the future.

Code from the object placement system

Some of the code still uses names that make sense for a base building system.

BuildingPlacementComponent Is a component attached to each player responsible for allowing the player to place objects in the world.

Code snippets - BuildingPlacementComponent.cpp

// Copyright 2018 Sticks & Stones. All Rights Reserved. #include "BuildingPlacementComponent.h" #include "BuildingNode.h" #include "BuildingEnum.h" #include "Gameplay/Player/SurvivalGamePlayerController.h" #include "Gameplay/Placeable/BasePlaceable.h" #include "Gameplay/PowerCore/PowerCore.h" #include "Gameplay/Player/InventoryComponent.h" #include "Gameplay/Sound/ReplicatedSoundComponent.h" #include "Gameplay/Player/PlayerCharacter.h" #include "UI/BuildingWidget.h" #include "Blueprint/UserWidget.h" #include "Engine/GameEngine.h" #include "Engine/Public/EngineUtils.h" #include "Engine/Classes/Components/StaticMeshComponent.h" #include "Engine/Classes/Materials/MaterialInstanceDynamic.h" #include "Engine/Classes/Sound/SoundCue.h" #include "Runtime/Engine/Public/TimerManager.h" #include "Runtime/Engine/Classes/Kismet/GameplayStatics.h" #include "Runtime/UMG/Public/Blueprint/WidgetBlueprintLibrary.h" #include "Runtime/CoreUObject/Public/UObject/Class.h" #include "Net/UnrealNetwork.h" UBuildingPlacementComponent::UBuildingPlacementComponent() { PrimaryComponentTick.bCanEverTick = true; CurrentSelected = EBuildingType::BUILDING_SQUARE_FOUNDATION; TotalMineCount = 0; TotalSpikeTrapCount = 0; TotalElectricTrapCount = 0; TotalTurretCount = 0; TotalLaserCannonCount = 0; } FBuildingMeshData UBuildingPlacementComponent::GetPreviewPlaceableData() { return PreviewPlaceable; } void UBuildingPlacementComponent::BeginPlay() { Super::BeginPlay(); PlayerCharacter = Cast<APlayerCharacter>(GetOwner()); if (PlayerCharacter == nullptr) { GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Yellow, FString::Printf(TEXT("UBuildingPlacementComponent is not owned by a character"))); } ReplicatedSoundComponent = Cast<UReplicatedSoundComponent>(PlayerCharacter->GetComponentByClass(UReplicatedSoundComponent::StaticClass())); LocallyControlled = PlayerCharacter->IsLocallyControlled(); NewBuildingPreviewNode = GetWorld()->SpawnActor<ABuildingNode>(); NewBuildingPreviewNode->SetActorScale3D(FVector(1.0f, 1.0f, 1.0f)); NewBuildingPreviewNode->SetBuildingSettings(BuildingSettings, CurrentSelected, true); PreviewStaticMeshComp = Cast<UStaticMeshComponent>(NewBuildingPreviewNode->GetComponentByClass(UStaticMeshComponent::StaticClass())); if (!LocallyControlled) { return; } if (BuildingSettings == nullptr) { GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Yellow, FString::Printf(TEXT("Set BuildingPlacmentSettings in the building placement component"))); return; } InitBuildingUI(); } ABuildingNode* UBuildingPlacementComponent::GetPreviewMesh() { if (NewBuildingPreviewNode) { return NewBuildingPreviewNode; } return nullptr; } void UBuildingPlacementComponent::SetPreviewPlaceable(FBuildingMeshData arg_NewPreviewPlaceable) { PreviewPlaceable = arg_NewPreviewPlaceable; bPreviewPlacableSet = true; OnBuildingTypeSelectedChange(PreviewPlaceable.BuildingType); } void UBuildingPlacementComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); DOREPLIFETIME(UBuildingPlacementComponent, PreviewPlaceable); DOREPLIFETIME(UBuildingPlacementComponent, BuildingSettings); } void UBuildingPlacementComponent::TurnBuildUIOn() { if (PlayerCharacter) { PlayerCharacter->EventOnOpenBuildMenu(); bBuildingUION = true; bBuildingModeOn = true; NotifyPlaceablesOnBuildModeToggle(true); if (BuildingWidget) { BuildingWidget->AddToViewport(3); } UpdatePreviewMesh(nullptr); bPreviewPlacableSet = false; APlayerController *playerController = Cast<APlayerController>(PlayerCharacter->GetController()); if (playerController != nullptr) { playerController->bShowMouseCursor = true; FInputModeGameAndUI InputMode; InputMode.SetHideCursorDuringCapture(false); InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock); playerController->SetInputMode(InputMode); } } } void UBuildingPlacementComponent::TurnBuildUIOff() { if (bBuildingUION) { bBuildingUION = false; if (PlayerCharacter) { PlayerCharacter->EventOnCloseBuildMenu(); if (BuildingWidget != nullptr) { BuildingWidget->RemoveFromViewport(); APlayerController *playerController = Cast<APlayerController>(PlayerCharacter->GetController()); if (playerController != nullptr) { playerController->bShowMouseCursor = false; UWidgetBlueprintLibrary::SetInputMode_GameOnly(playerController); } } } } } bool UBuildingPlacementComponent::GetIsBuildingModeOn() { return bBuildingModeOn; } void UBuildingPlacementComponent::TickComponent(float arg_DeltaTime, ELevelTick arg_TickType, FActorComponentTickFunction* arg_ThisTickFunction) { Super::TickComponent(arg_DeltaTime, arg_TickType, arg_ThisTickFunction); if (!bBuildingModeOn || !LocallyControlled) { return; } const FVector PlayerForward = PlayerCharacter->GetControlRotation().Vector(); const FVector CameraLocation = GEngine->GetFirstLocalPlayerController(GetWorld())->PlayerCameraManager->GetCameraLocation(); // Line trace to get possible placement position const FVector LineOrigin = CameraLocation; const float LineTraceDistance = 1000.0f; const FVector LineEnd = LineOrigin + (PlayerForward * LineTraceDistance); const auto Channel = ECC_CanPlacePlaceableOn; const auto BuildingTraceChannel = ECC_Building; FHitResult HitResultWorld; TArray<FHitResult> BuildingHitResults; const auto Params = FCollisionQueryParams(TEXT("Trace"), false, GetOwner()); const bool bHit = GetWorld()->LineTraceSingleByChannel(HitResultWorld, LineOrigin, LineEnd, Channel, Params); bool bHitBuilding = GetWorld()->LineTraceMultiByChannel(BuildingHitResults, LineOrigin, LineEnd, BuildingTraceChannel, Params); //bHitBuilding = false; // check other nodes near if (bHitBuilding) { float Closest = TNumericLimits<float>::Max(); int ClosestIndex = 0; for (int i = 0; i < BuildingHitResults.Num(); i++) { float HitDist = FVector::Dist(BuildingHitResults[0].ImpactPoint, BuildingHitResults[i].GetActor()->GetActorLocation()); if (HitDist < Closest) { Closest = HitDist; ClosestIndex = i; } } UPrimitiveComponent* HitComponentBuilding = BuildingHitResults[ClosestIndex].GetComponent(); if (HitComponentBuilding != nullptr) { ABuildingNode* HitNode = Cast<ABuildingNode>(BuildingHitResults[ClosestIndex].GetActor()); if (bPrintHitOnScreenDebugMessage) { GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Cyan, FString::Printf(TEXT("[UBuildingPlacementComponent] hit building: ")) + HitComponentBuilding->GetName()); } if (HitNode != nullptr) { UpdatePreview(BuildingHitResults[ClosestIndex], LineOrigin, PlayerForward, HitNode); return; } } } if (bHit) // if something is hit that is not a building node { UPrimitiveComponent* HitComponent = HitResultWorld.GetComponent(); if (HitComponent != nullptr) { if (bPrintHitOnScreenDebugMessage) { GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Blue, FString::Printf(TEXT("[UBuildingPlacementComponent] hit: ")) + HitComponent->GetName()); } } else { GEngine->AddOnScreenDebugMessage(-1, 1.0f, FColor::Red, FString::Printf(TEXT("[UBuildingPlacementComponent] HitComponent = null "))); } UpdatePreview(HitResultWorld, LineOrigin, PlayerForward); } else // if nothing is hit { UpdatePreviewCanNotPlace(LineOrigin, PlayerForward); } } void UBuildingPlacementComponent::BuildTogglePressed() { if (bBuildToggleModeEnabled) { if (bBuildingUION) { TurnBuildUIOff(); if (PreviewStaticMeshComp->GetStaticMesh() != nullptr) { if (PlayerCharacter) { PlayerCharacter->GetWeaponComponent()->TurnOff(); } } else { TurnOffBuildingMode(); } } else { TurnBuildUIOn(); if (PlayerCharacter) { PlayerCharacter->GetWeaponComponent()->TurnOff(); } } } else { TurnBuildUIOn(); if (PlayerCharacter) { PlayerCharacter->GetWeaponComponent()->TurnOff(); } } } void UBuildingPlacementComponent::BuildToggleRelease() { if (!bBuildToggleModeEnabled) { TurnBuildUIOff(); if (PreviewStaticMeshComp->GetStaticMesh() == nullptr) { TurnOffBuildingMode(); } } if (PlayerCharacter) { PlayerCharacter->GetWeaponComponent()->TurnOn(); } } void UBuildingPlacementComponent::BuildModeClickDisable() { TurnBuildUIOff(); TurnOffBuildingMode(); UpdatePreviewMesh(nullptr); bPreviewPlacableSet = false; /*if (PlayerCharacter) { UWeaponComponent* WeaponComponent = PlayerCharacter->GetWeaponComponent(); if (WeaponComponent) { WeaponComponent->TurnOff(); } else { UE_LOG(LogTemp, Warning, TEXT("Weapon component on the player not found!")); } }*/ } void UBuildingPlacementComponent::NotifyPlaceablesOnBuildModeToggle(bool arg_IsBuildModeEnabled) { for (TActorIterator<ABasePlaceable> ItPlaceable(GetWorld()); ItPlaceable; ++ItPlaceable) { if (ItPlaceable->IsValidLowLevelFast()) { if (arg_IsBuildModeEnabled) { ItPlaceable->OnBuildingModeEnabled(); } else { ItPlaceable->OnBuildingModeDisabled(); } } } } void UBuildingPlacementComponent::RotatePreview(float arg_rotationChange) { if (!bPreviewPlacableSet) { return; } if (bBuildingModeOn) { PreviewRotation += BuildingSettings->PreviewRotationChangePerInput * arg_rotationChange; } } void UBuildingPlacementComponent::RotatePreviewReset() { if (!bPreviewPlacableSet) { return; } if (bBuildingModeOn) { PreviewRotation = 0.0f; } } void UBuildingPlacementComponent::UpdatePreview(const FHitResult &arg_HitResult, const FVector &arg_LineOrigin, const FVector &arg_Forward, ABuildingNode *arg_HitBuildingNode) { const bool bIsInRange = FVector::Distance(arg_HitResult.ImpactPoint, arg_LineOrigin) < BuildingSettings->BuildingPlacementDistance; const bool bIsFoundationOrPlaceable = NewBuildingPreviewNode->GetBuildingType() == EBuildingType::BUILDING_SQUARE_FOUNDATION || NewBuildingPreviewNode->GetBuildingType() == EBuildingType::BUILDING_TRIANGLE_FOUNDATION || NewBuildingPreviewNode->GetBuildingType() == EBuildingType::BUILDING_PLACEABLE; const bool bHasHitBuilding = arg_HitBuildingNode != nullptr; FVector_NetQuantizeNormal HitNormal = arg_HitResult.ImpactNormal; float TerrainDot = FVector::DotProduct(HitNormal, FVector(0.0f, 0.0f, 1.0f)); bool bCanPlaceAtGroundAngle = 1.0f - ((TerrainDot + 1.0f) / 2.0f) < (BuildingSettings->MaxTrapPlaceAngle / 360.0f); if (bIsInRange && bHasHitBuilding) // if hit building node and is in range { BuildingSocket* ClosestSocketOfHitBuilding = arg_HitBuildingNode->GetClosestSocketToPoint(arg_HitResult.ImpactPoint, CurrentSelected); FVector LocalNodeOffset = ClosestSocketOfHitBuilding->LocalPosition; FVector SockWorldSpacePositionLocalSpaceRotation = arg_HitResult.GetActor()->GetActorRotation().RotateVector(LocalNodeOffset); NewBuildingPreviewNode->SetActorRotation(arg_HitBuildingNode->GetActorRotation()); NewBuildingPreviewNode->SetActorLocation(SockWorldSpacePositionLocalSpaceRotation + arg_HitResult.GetActor()->GetActorLocation()); BuildingSocket *PreviewSocket = NewBuildingPreviewNode->GetClosestSocketToPoint(arg_HitBuildingNode->GetActorLocation(), arg_HitBuildingNode->GetBuildingType()); if (PreviewSocket != nullptr) { FRotator NewRotation = arg_HitBuildingNode->GetActorRotation(); FVector previewDir = NewRotation.RotateVector(PreviewSocket->SocketDirection); FVector hitSocketDir = NewRotation.RotateVector(ClosestSocketOfHitBuilding->SocketDirection); float previewAngle = FMath::RadiansToDegrees(FMath::Atan2(previewDir.X, previewDir.Y)); float hitSocketAngle = FMath::RadiansToDegrees(FMath::Atan2(hitSocketDir.X, hitSocketDir.Y)); float angleDelta = previewAngle - hitSocketAngle + 180.0f; NewRotation.Add(0.0f, angleDelta, 0.0f); // preview sockets rotation NewBuildingPreviewNode->SetActorRotation(NewRotation); NewBuildingPreviewNode->SetActorLocation(NewBuildingPreviewNode->GetActorLocation() - NewBuildingPreviewNode->GetActorRotation().RotateVector(PreviewSocket->LocalPosition)); } bCanSpawn = true; } else if (bIsInRange && bIsFoundationOrPlaceable) // if can place on ground and is a foundation or placeable { // set rotation and location of preview FRotator PlaceableZRotation = FRotator(0.0f, PreviewRotation + FMath::RadiansToDegrees(FMath::Atan2(arg_Forward.Y, arg_Forward.X)), 0.0f); float AngleXZ = FMath::RadiansToDegrees(FMath::Atan2(HitNormal.X, HitNormal.Z)); float AngleYZ = FMath::RadiansToDegrees(FMath::Atan2(HitNormal.Y, HitNormal.Z)); PlaceableZRotation = (FQuat::MakeFromEuler(FVector(AngleYZ, -AngleXZ, 0.0f))* PlaceableZRotation.Quaternion() * PreviewPlaceable.PreviewRotation.Quaternion()).Rotator(); NewBuildingPreviewNode->SetActorRotation(PlaceableZRotation); NewBuildingPreviewNode->SetActorLocation(arg_HitResult.ImpactPoint); // overlap check ABasePlaceable *PlaceableDefaultObject = Cast<ABasePlaceable>(PreviewPlaceable.Placeable.GetDefaultObject()); bool bAreaFilled = true; if (PlaceableDefaultObject != nullptr) { float shpereRadius = PlaceableDefaultObject->GetSphereOverlapTestRadius(); FVector BoxOverlapTestSize = PlaceableDefaultObject->GetBoxOverlapTestSize(); bAreaFilled = GetWorld()->OverlapBlockingTestByChannel(arg_HitResult.ImpactPoint, PlaceableZRotation.Quaternion(), ECC_Building, FCollisionShape::MakeSphere(shpereRadius)) || GetWorld()->OverlapBlockingTestByChannel(arg_HitResult.ImpactPoint, PlaceableZRotation.Quaternion(), ECC_Building, FCollisionShape::MakeBox(.5f*BoxOverlapTestSize)); } // can buy check bool bCanBuy = true; if (PlayerCharacter->InventoryComponent->GetResource(PreviewPlaceable.PlacementCost.ItemType) < PreviewPlaceable.PlacementCost.ItemAmount) { bCanBuy = false; } bCanSpawn = bCanPlaceAtGroundAngle && !bAreaFilled && bCanBuy; } else { UpdatePreviewCanNotPlace(arg_LineOrigin, arg_Forward); } UpdatePreviewPosAndRot(NewBuildingPreviewNode->GetActorLocation(), NewBuildingPreviewNode->GetActorRotation(), bCanSpawn); } void UBuildingPlacementComponent::UpdatePreviewCanNotPlace(const FVector &arg_LineOrigin, const FVector &arg_Forward) { FVector Forward = arg_Forward; Forward.Normalize(); Forward *= BuildingSettings->BuildingPlacementDistance; NewBuildingPreviewNode->SetActorLocation(Forward + arg_LineOrigin); NewBuildingPreviewNode->SetActorRotation(FRotator(0.0f, PreviewRotation + FMath::RadiansToDegrees(FMath::Atan2(Forward.Y, Forward.X)), 0.0f).Quaternion() * PreviewPlaceable.PreviewRotation.Quaternion()); bCanSpawn = false; UpdatePreviewPosAndRot(NewBuildingPreviewNode->GetActorLocation(), NewBuildingPreviewNode->GetActorRotation(), false); } void UBuildingPlacementComponent::UpdatePreviewPosAndRot(FVector arg_NewPosition, FRotator arg_NewRotation, bool arg_CanPlace) { if (!LocallyControlled) { return; } UpdatePreviewMaterial(bCanSpawn); NewBuildingPreviewNode->SetActorLocation(arg_NewPosition); NewBuildingPreviewNode->SetActorRotation(arg_NewRotation); PreviewStaticMeshComp->SetWorldRotation(arg_NewRotation); PreviewStaticMeshComp->SetWorldLocation(arg_NewPosition + arg_NewRotation.RotateVector(PreviewPlaceable.Offset)); ServerUpdatePreview(PreviewStaticMeshComp->GetComponentLocation(), NewBuildingPreviewNode->GetActorLocation(), NewBuildingPreviewNode->GetActorRotation(), arg_CanPlace); } void UBuildingPlacementComponent::PlacePowerCore_Implementation(FVector arg_Location, FRotator arg_Rotation) { TArray<AActor*> FoundPowerCores; UGameplayStatics::GetAllActorsOfClass(GetWorld(), APowerCore::StaticClass(), FoundPowerCores); for (auto Core : FoundPowerCores) { Core->SetActorLocation(arg_Location); Core->SetActorRotation(arg_Rotation); Cast<APowerCore>(Core)->PlacePowerCore(); TurnOffBuildingMode(); APlayerCharacter* Player = Cast<APlayerCharacter>(GetOwner()); if (Player) { Player->GetWeaponComponent()->TurnOn(); Player->GetInventoryComponent()->DropPowerCore(); } } } bool UBuildingPlacementComponent::PlacePowerCore_Validate(FVector arg_Location, FRotator arg_Rotation) { return true; } void UBuildingPlacementComponent::ServerUpdatePreview_Implementation(FVector arg_MeshLocation, FVector arg_ActorLocation, FRotator arg_ActorRotation, bool arg_CanPlace) { MulticastUpdatePreview(arg_MeshLocation, arg_ActorLocation, arg_ActorRotation, arg_CanPlace); } bool UBuildingPlacementComponent::ServerUpdatePreview_Validate(FVector arg_MeshLocation, FVector arg_ActorLocation, FRotator arg_ActorRotation, bool arg_CanPlace) { return true; } void UBuildingPlacementComponent::MulticastUpdatePreview_Implementation(FVector arg_MeshLocation, FVector arg_ActorLocation, FRotator arg_ActorRotation, bool arg_CanPlace) { if (!LocallyControlled) { PreviewStaticMeshComp->SetWorldLocation(arg_MeshLocation); PreviewStaticMeshComp->SetWorldRotation(arg_ActorRotation); NewBuildingPreviewNode->SetActorLocation(arg_ActorLocation); NewBuildingPreviewNode->SetActorRotation(arg_ActorRotation); UpdatePreviewMaterial(arg_CanPlace); } } void UBuildingPlacementComponent::UpdatePreviewMaterial(bool arg_CanPlace) { int32 NumMaterials = PreviewStaticMeshComp->GetNumMaterials(); for (int32 i = 0; i < NumMaterials; i++) { if (arg_CanPlace) { PreviewStaticMeshComp->SetMaterial(i, BuildingSettings->MaterialCanPlace); } else { PreviewStaticMeshComp->SetMaterial(i, BuildingSettings->MaterialCanNotPlace); } } } void UBuildingPlacementComponent::ServerUpdatePreviewMesh_Implementation(UStaticMesh* arg_NewMesh) { MulticastUpdatePreviewMesh(arg_NewMesh); } bool UBuildingPlacementComponent::ServerUpdatePreviewMesh_Validate(UStaticMesh* arg_NewMesh) { return true; } void UBuildingPlacementComponent::MulticastUpdatePreviewMesh_Implementation(UStaticMesh* arg_NewMesh) { if (!LocallyControlled) { PreviewStaticMeshComp->SetStaticMesh(arg_NewMesh); } } void UBuildingPlacementComponent::UpdatePreviewMesh(UStaticMesh* arg_NewMesh) { if (PreviewStaticMeshComp->IsValidLowLevelFast()) { PreviewStaticMeshComp->SetStaticMesh(arg_NewMesh); ServerUpdatePreviewMesh(arg_NewMesh); if (PlayerCharacter && arg_NewMesh != nullptr) { PlayerCharacter->EventOnStartBuildPreview(); } } } bool UBuildingPlacementComponent::InitBuildingUI() { if (BuildingSettings->BuildingWidgetClassReference) { if (BuildingWidget == nullptr) { BuildingWidget = Cast<UBuildingWidget>(CreateWidget(GetWorld(), BuildingSettings->BuildingWidgetClassReference)); BuildingWidget->SetBuildingPlacementComponent(this); if (PlayerCharacter) { APlayerController* PlayerController = Cast<APlayerController>(PlayerCharacter->GetController()); if (PlayerController) { BuildingWidget->SetOwningPlayer(PlayerController); } } } return true; } else { GEngine->AddOnScreenDebugMessage(-1, 10.0f, FColor::Yellow, FString::Printf(TEXT("Set building widget class reference in the building settings"))); return false; } } void UBuildingPlacementComponent::OnFireAction() { if (!bPreviewPlacableSet) { return; } if (bBuildingModeOn && !bBuildingUION) { if (SpawnBuildingNode()) { ReplicatedSoundComponent->PlaySoundAtLocationReplicated(GetOwner(), BuildingSettings->BuildAudioCue, GetOwner()->GetActorLocation()); } else { ReplicatedSoundComponent->PlaySoundAtLocationReplicated(GetOwner(), BuildingSettings->CanNotBuildAudioCue, GetOwner()->GetActorLocation()); } } } void UBuildingPlacementComponent::TurnOffBuildingMode() { NotifyPlaceablesOnBuildModeToggle(false); TurnOffBuildingModeRPC(); } void UBuildingPlacementComponent::TurnOffBuildingModeRPC_Implementation() { if (PlayerCharacter) { PlayerCharacter->EventOnStopBuildPreview(); if (!bBuildingModeOn) { return; } bBuildingModeOn = false; PreviewStaticMeshComp->SetHiddenInGame(true); } } void UBuildingPlacementComponent::OnBuildingTypeSelectedChange(EBuildingType arg_NewType) { if (!bBuildingModeOn) { return; } CurrentSelected = arg_NewType; if (BuildingWidget) { BuildingWidget->OnBuildingTypeSelectedChange(arg_NewType); UpdatePreviewMesh(PreviewPlaceable.Mesh); PreviewStaticMeshComp->SetHiddenInGame(false); NewBuildingPreviewNode->SetBuildingSettings(BuildingSettings, CurrentSelected, true); }; } bool UBuildingPlacementComponent::SpawnBuildingNode() { if (bCanSpawn) { // Remove resource cost PlayerCharacter->InventoryComponent->AddResource(PreviewPlaceable.PlacementCost.ItemType, -PreviewPlaceable.PlacementCost.ItemAmount); // Spawn APlayerCharacter* Owner = Cast<APlayerCharacter>(GetOwner()); if (Owner) { Owner->SpawnRemoteBuildingNodeServer(PreviewPlaceable, CurrentSelected, NewBuildingPreviewNode->GetActorRotation(), NewBuildingPreviewNode->GetActorLocation(), PreviewStaticMeshComp->GetComponentRotation(), PreviewStaticMeshComp->GetComponentLocation()); return true; } } return false; } void UBuildingPlacementComponent::SpawnRemoteBuildingNode(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location) { if (arg_BuildType == EBuildingType::BUILDING_PLACEABLE) { TSubclassOf<APowerCore> PowerCoreClass = arg_PreviewData.Placeable.Get(); if (PowerCoreClass == nullptr) { StartBuilding(arg_PreviewData, arg_BuildType, arg_NodeRotation, arg_NodeLocation, arg_Rotation, arg_Location); } else { PlacePowerCore(arg_Location, arg_Rotation); } } else { StartBuilding(arg_PreviewData, arg_BuildType, arg_NodeRotation, arg_NodeLocation, arg_Rotation, arg_Location); } } void UBuildingPlacementComponent::StartBuilding(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location) { bIsBuilding = true; EndBuilding(arg_PreviewData, arg_BuildType, arg_NodeRotation, arg_NodeLocation, arg_Rotation, arg_Location); PlayerCharacter->EventOnBuildingStart(); } void UBuildingPlacementComponent::EventOnBuildSuccessMulticast_Implementation(FBuildingMeshData arg_PreviewData) { Cast<APlayerCharacter>(GetOwner())->EventOnBuildSuccess(arg_PreviewData); } void UBuildingPlacementComponent::EndBuilding(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location) { ABasePlaceable* NewBuildingNode = Cast<ABasePlaceable>(GetWorld()->SpawnActor(arg_PreviewData.Placeable.Get())); EventOnBuildSuccessMulticast(arg_PreviewData); if (NewBuildingNode != nullptr) { NewBuildingNode->SetActorRotation(arg_NodeRotation); NewBuildingNode->SetActorLocation(arg_NodeLocation); UStaticMeshComponent* BuildingNodeStaticMeshComp = Cast<UStaticMeshComponent>(NewBuildingNode->GetComponentByClass(UStaticMeshComponent::StaticClass())); if (BuildingNodeStaticMeshComp) { BuildingNodeStaticMeshComp->SetCollisionProfileName("Placable"); } } else { GEngine->AddOnScreenDebugMessage(0, 5.0f, FColor::Red, TEXT("Placeable actor is not a child of ABasePlaceable")); } bIsBuilding = false; PlayerCharacter->EventOnBuildingEnd(); } FBuildingMeshData* UBuildingPlacementComponent::GetBuidlingMeshData(EBuildingType arg_Type) { switch (arg_Type) { case EBuildingType::BUILDING_PLACEABLE: if (bPreviewPlacableSet) { return &PreviewPlaceable; } return nullptr; break; default: return nullptr; break; } } void UBuildingPlacementComponent::RefreshPreviewMesh() { UpdatePreviewMesh(PreviewPlaceable.Mesh); }

Code snippets - BuildingPlacementComponent.h

// Copyright 2018 Sticks & Stones. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Components/ActorComponent.h" #include "Engine/Blueprint.h" #include "Blueprint/UserWidget.h"// needed for FTraceDelegate #include "BuildingEnum.h" #include "BuildingPlacementSettings.h" #include "Systems/WeaponSystem/WeaponComponent.h" #include "BuildingPlacementComponent.generated.h" class UBuildingPlacementSettings; class ABuildingNode; class UStaticMeshComponent; class APlayerCharacter; class UBuildingWidget; class USoundCue; class UMaterial; class UReplicatedSoundComponent; #define ECC_CanPlacePlaceableOn ECollisionChannel::ECC_GameTraceChannel3 #define ECC_Building ECollisionChannel::ECC_GameTraceChannel4 DECLARE_DELEGATE_OneParam(FRotationInput, float); UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) class SURVIVALGAME_API UBuildingPlacementComponent : public UActorComponent { GENERATED_BODY() public: /* * @Description Sets default values for this component's properties */ UBuildingPlacementComponent(); /* * @Description Called every frame */ virtual void TickComponent(float arg_DeltaTime, ELevelTick arg_TickType, FActorComponentTickFunction* arg_ThisTickFunction) override; /* * @Brief: Toggles build mode on if BuildToggleMode is enabled */ void BuildTogglePressed(); /* * @Brief: Does nothing if BuildToggleMode is enabled */ void BuildToggleRelease(); UFUNCTION(BlueprintCallable) /* * @Brief: Input Handle event for disabling BuildMode */ void BuildModeClickDisable(); void NotifyPlaceablesOnBuildModeToggle(bool arg_IsBuildModeEnabled); /* * @Brief: Input change preview rotation * @Param[in] arg_rotationChange: rotation change should be -1.0 or 1.0 */ void RotatePreview(float arg_rotationChange); /* * @Brief: Input Reset preview rotation to 0 */ void RotatePreviewReset(); /* * @Description Initializes the Building Widget if class is set correctly and sets playercontroller as owner * @Return: Succes */ bool InitBuildingUI(); UFUNCTION(BlueprintCallable) /* * @Description Turns on the building UI. */ void TurnBuildUIOn(); UFUNCTION(BlueprintCallable) /* * @Description Turns off the building UI. */ void TurnBuildUIOff(); UFUNCTION(BlueprintCallable) FBuildingMeshData GetPreviewPlaceableData(); UFUNCTION(BlueprintCallable) /* * @Description Turns off building mode */ void TurnOffBuildingMode(); UFUNCTION(BlueprintCallable, NetMultiCast, Reliable) /* * @Description Turns off building mode RPC */ void TurnOffBuildingModeRPC(); /* * @Description Called by input fire action */ void OnFireAction(); /* * @Description Returns the mesh data for the currently selected building type */ FBuildingMeshData* GetBuidlingMeshData(EBuildingType arg_Type); /* * @Description Refresh the preview mesh */ void RefreshPreviewMesh(); /* * @Description Spawns currently selected building node * @Param[in] arg_BuildType: type of new building node * @Param[in] arg_NodeRotation: rotation of new building node * @Param[in] arg_NodeLocation: location of new building node * @Param[in] arg_Rotation: rotation of new building node mesh * @Param[in] arg_Location: location of new building node mesh */ void SpawnRemoteBuildingNode(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location); UFUNCTION(NetMulticast, Reliable) void EventOnBuildSuccessMulticast(FBuildingMeshData arg_PreviewData); /* * @Description Starts the building of a building node. This is for effects only. */ UFUNCTION(BlueprintCallable) void StartBuilding(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location); /* * @Description Ends the building of a building node. This actually places the tower */ UFUNCTION(BlueprintCallable) void EndBuilding(FBuildingMeshData arg_PreviewData, EBuildingType arg_BuildType, FRotator arg_NodeRotation, FVector arg_NodeLocation, FRotator arg_Rotation, FVector arg_Location); UFUNCTION(BlueprintCallable) /* * @Description Getter returns true if the building mode is on. * @Return: True if buiding mode is on. */ bool GetIsBuildingModeOn(); UFUNCTION(BlueprintCallable) /* * @Description Getter returns preview mesh * @Return: Returns preview mesh */ ABuildingNode* GetPreviewMesh(); /* * @Description Sets a new preview * @Param[in] arg_NewPreviewPlaceable: new preview data */ void SetPreviewPlaceable(FBuildingMeshData arg_NewPreviewPlaceable); /* * @Brief Returns the properties used for network replication * @Param[out] arg_OutLifetimeProps return things that need to be replicated to unreal */ void GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const; UPROPERTY(EditAnywhere, BlueprintReadWrite) float BuildingPlacementDuration = 1.f; protected: /* * @Description Update new building node preview */ void UpdatePreview(const FHitResult &arg_HitResult, const FVector &arg_LineOrigin, const FVector &arg_Forward, ABuildingNode *arg_HitBuildingNode = nullptr); /* * @Description Update new building node preview * @Param[in] arg_LineOrigin users mouse line origin * @Param[in] arg_Forward users mouse line forward */ void UpdatePreviewCanNotPlace(const FVector &arg_LineOrigin, const FVector &arg_Forward); /* * @Description Update new building node preview mesh component */ void UpdatePreviewPosAndRot(FVector arg_NewPosition, FRotator arg_NewRotation, bool arg_CanPlace); UFUNCTION(Server, Reliable, WithValidation) /* * @Description Call place power core on the server * @Param[in] arg_Location powercores spawn location * @Param[in] arg_Rotation powercores spawn rotation */ void PlacePowerCore(FVector arg_Location, FRotator arg_Rotation); /* * @Description Update placeable preview on the server * @Param[in] arg_MeshLocation preview mesh location * @Param[in] arg_ActorLocation preview actor location * @Param[in] arg_ActorRotation preview actor rotation * @Param[in] arg_CanPlace true if preview can be placed */ UFUNCTION(Server, unreliable, WithValidation) void ServerUpdatePreview(FVector arg_MeshLocation, FVector arg_ActorLocation, FRotator arg_ActorRotation, bool arg_CanPlace); /* * @Description Update placeable preview on all clients from the server * @Param[in] arg_MeshLocation preview mesh location * @Param[in] arg_ActorLocation preview actor location * @Param[in] arg_ActorRotation preview actor rotation * @Param[in] arg_CanPlace true if preview can be placed */ UFUNCTION(NetMulticast, unreliable) void MulticastUpdatePreview(FVector arg_MeshLocation, FVector arg_ActorLocation, FRotator arg_ActorRotation, bool arg_CanPlace); /* * @Description Call update preview mesh on the server * @Param[in] arg_NewMesh New preview mesh */ UFUNCTION(Server, unreliable, WithValidation) void ServerUpdatePreviewMesh(UStaticMesh* arg_NewMesh); /* * @Description Call update preview mesh on all clients form the server * @Param[in] arg_NewMesh New preview mesh */ UFUNCTION(NetMulticast, unreliable) void MulticastUpdatePreviewMesh(UStaticMesh* arg_NewMesh); /* * @Description Update the preview mesh * @Param[in] arg_NewMesh New preview mesh */ void UpdatePreviewMesh(UStaticMesh* arg_NewMesh); /* * @Description Update the preview material * @Param[in] arg_CanPlace true if the material should be changed to the can place material */ void UpdatePreviewMaterial(bool arg_CanPlace); UPROPERTY() TArray<ABuildingNode*> buildingNodes; UPROPERTY(Replicated, EditDefaultsOnly) UBuildingPlacementSettings* BuildingSettings = nullptr; UPROPERTY(EditAnywhere, BlueprintReadWrite) UBuildingWidget* BuildingWidget = nullptr; FTraceDelegate TraceDelegate; UPROPERTY() ABuildingNode *NewBuildingPreviewNode = nullptr; UPROPERTY() UStaticMeshComponent *PreviewStaticMeshComp = nullptr; UPROPERTY(Replicated) FBuildingMeshData PreviewPlaceable; bool bPreviewPlacableSet = false; UPROPERTY() APlayerCharacter* PlayerCharacter = nullptr; /* * @Description Called when the game starts */ virtual void BeginPlay() override; EBuildingType CurrentSelected; FTimerHandle BuilingTimeHandle; bool bCanSpawn = false; bool bBuildingModeOn = false; bool bBuildingUION = false; bool bPrintHitOnScreenDebugMessage = false; float PreviewRotation; UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Settings", meta = (AllowPrivateAccess = "True")) bool bBuildToggleModeEnabled; /* * @Description Called when Selected building type changes */ void OnBuildingTypeSelectedChange(EBuildingType arg_NewType); /* * @Description Spawns currently selected building node * @Return: Succes */ bool SpawnBuildingNode(); UPROPERTY() UReplicatedSoundComponent* ReplicatedSoundComponent; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalMineCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalSpikeTrapCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalElectricTrapCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalTurretCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalLaserCannonCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalFullWallCount; UPROPERTY(EditAnywhere, BlueprintReadWrite) int TotalHalfWallCount; UPROPERTY(BlueprintReadOnly) bool bIsBuilding = false; bool LocallyControlled; };

BuildingPlacementSettings is the class that inherets form UPrimaryDataAsset to make a settings file for the basebuilding system that works inside of the unreal engine editor. I created a separate settings file because the buildingPlacementComponent is attached to the player and because its the biggest prefabe in the game. The player prefabe is a binary unreal blueprint file that we can't murge trough text. We had a big team that needed to edit the player prefab alot. To take some of the load of the player prefabe I seperate some settings into the BuildingPlacementSettings setttings file.

Code snippets - BuildingPlacementSettings.h

// Copyright 2018 Sticks & Stones. All Rights Reserved. #pragma once #include "CoreMinimal.h" #include "Engine/DataAsset.h" #include "Gameplay/Interactables/Gatherables/GatherableData.h" #include "Runtime/Core/Public/Containers/StaticArray.h" #include "Gameplay/Building/BuildingEnum.h" #include "Engine/DataTable.h" #include "BuildingPlacementSettings.generated.h" class UUserWidget; class UStaticMesh; class USoundCue; class ABasePlaceable; class UMaterial; class UMaterialInterface; USTRUCT(BlueprintType) struct FPlacementCost { GENERATED_BODY() UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) EResourceTypes ItemType; UPROPERTY(EditDefaultsOnly, BlueprintReadWrite) int ItemAmount; }; USTRUCT(BlueprintType) struct FBuildingMeshData { GENERATED_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) UStaticMesh* Mesh; UPROPERTY(EditAnywhere, BlueprintReadWrite) FRotator PreviewRotation; UPROPERTY(EditAnywhere, BlueprintReadWrite) TSubclassOf<ABasePlaceable> Placeable; UPROPERTY(EditAnywhere, BlueprintReadWrite) FVector Offset; UPROPERTY(EditAnywhere, BlueprintReadWrite) FPlacementCost PlacementCost; UPROPERTY(EditAnywhere, BlueprintReadWrite) EBuildingType BuildingType; UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Name; UPROPERTY(EditAnywhere, BlueprintReadWrite) FString Description; UPROPERTY(EditAnywhere, BlueprintReadWrite) bool Locked; UPROPERTY(EditAnywhere, BlueprintReadWrite) bool SupportedInLevel; }; UCLASS() class SURVIVALGAME_API UBuildingPlacementSettings : public UPrimaryDataAsset { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) TSubclassOf<UUserWidget> BuildingWidgetClassReference; UPROPERTY(EditDefaultsOnly) UMaterial* MaterialCanPlace; UPROPERTY(EditDefaultsOnly) UMaterial* MaterialCanNotPlace; UPROPERTY(EditDefaultsOnly) UMaterialInterface* MaterialTileActive; UPROPERTY(EditDefaultsOnly) UMaterialInterface* MaterialTileNotActive; UPROPERTY(EditDefaultsOnly) float BuildingPlacementDistance = 400.0f; UPROPERTY(EditDefaultsOnly) float BuildingGridsizeHor = 400.0f; UPROPERTY(EditDefaultsOnly) float BuildingGridsizeVer = 300.0f; UPROPERTY(EditDefaultsOnly) FName CollisionProfile = "BlockAll"; UPROPERTY(EditDefaultsOnly) bool bWallsStackable = true; UPROPERTY(EditDefaultsOnly) UStaticMesh* PreviewWallTileMesh; UPROPERTY(EditDefaultsOnly) bool NoCostMode = false; UPROPERTY(EditDefaultsOnly) USoundCue* BuildAudioCue; UPROPERTY(EditDefaultsOnly) USoundCue* CanNotBuildAudioCue; UPROPERTY(EditDefaultsOnly) float MaxTrapPlaceAngle = 40.0f; UPROPERTY(EditDefaultsOnly) float PreviewRotationChangePerInput = 15.0f; };

Minimap

I did not create the minimap from scratch but when I started working on it it was in a pretty early development state.

Minimap was able to:

  • Display icons of objects in the world
  • Follow the player’s movement
  • zoom in and out

Minimap to do:

  • Making the minimap rotate
  • Update map image easily
  • properly scale icons when zooming in and out

Minimap before I worked on it

Making the minimap rotate

I made the minimap rotate by first passing the player’s rotation to the shader. Then I use the player’s position and rotation inside Unreal’s CustomRotator material function to offset the sample locations of the height map texture.

The minimap shape

Designers requested a square minimap with rounded corners. I implemented a mask so they could easily use any shape.

First version rotating minimap using depth texture(with my programmer art)


Programmatically rendering the minimap

Designer request: “A system to easily create textures needed for the minimap. I don’t know how much manual work it is to replace it but due to frequent map updates we might need to be able to replace it a lot.”

The minimap used to be a static top-down screenshot of the level. But because of frequent iteration, it was a lot of work to keep up to date. I decided to programmatically create a minimap texture to make iterating the level easier.

I used a top-down isometric camera to create a depth texture. I used the created texture and a gradient texture as input data for an unreal material(Unreal’s way of creating shaders). I use the material to render a texture that draws a colored gradient based on height data and draws a border where the height difference exceeds a variable value.

Improving minimap icon clarity

The minimap icons were unclear they overlapped and it was hard to distinguish individual objects. I looked at different games for inspiration I ended up simplifying the minimap icons to simple shapes and adding thin black borders and I made them smaller. This made the individual icons more easily distinguishable.

I also changed the endpoint to a circle so it is easier to see on the minimap when enemies have reached the endpoint.