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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// 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.