One of the first problems that I’ve tried to tackle in each game engine that I play with is drawing lines. This is not much use for game development (which uses predominately 3D objects), but it is very useful for displaying geographic data. I came across a forum post and a code snippet on Github, but then thought that this would be a perfect test case for seeing if ChatGPT could be helpful. And to my surprise, it was, generating code that worked right out of the gate. It did produce some warnings that were quite easily resolved.
Here’s the header:
// Fill out your copyright notice in the Description page of Project Settings. #pragma once #include "CoreMinimal.h" #include "GameFramework/Actor.h" #include "MultiSegmentLineActor.generated.h" UCLASS() class HAIVIZ_API AMultiSegmentLineActor : public AActor { GENERATED_BODY() public: // Sets default values for this actor's properties AMultiSegmentLineActor(); // Sets the points for the spline and draws the line UFUNCTION(BlueprintCallable, Category = "Line") void SetPoints(const TArray<FVector>& Points); UFUNCTION() void OnSelected(AActor* Target, FKey ButtonPressed); protected: virtual void BeginPlay() override; private: UPROPERTY(VisibleAnywhere, Category = "Components") class USplineComponent* SplineComp; UPROPERTY(EditDefaultsOnly, Category = "Line") UStaticMesh* SegmentMesh; UPROPERTY(EditDefaultsOnly, Category = "Line") UMaterialInterface* SegmentMaterial; // Helper to clear old spline meshes void ClearSplineMeshes(); // Keep track of created mesh components TArray<class USplineMeshComponent*> SplineMeshes; };
And here’s the implementation:
#include "MultiSegmentLineActor.h" #include "Components/SplineComponent.h" #include "Components/SplineMeshComponent.h" #include "UObject/ConstructorHelpers.h" AMultiSegmentLineActor::AMultiSegmentLineActor() { PrimaryActorTick.bCanEverTick = false; SplineComp = CreateDefaultSubobject<USplineComponent>(TEXT("SplineComponent")); RootComponent = SplineComp; // Load built-in cylinder mesh for segments ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Engine/BasicShapes/Cylinder.Cylinder")); if (MeshAsset.Succeeded()) { SegmentMesh = MeshAsset.Object; } // Load default material ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial")); if (MaterialAsset.Succeeded()) { SegmentMaterial = MaterialAsset.Object; } } void AMultiSegmentLineActor::BeginPlay() { OnClicked.AddUniqueDynamic(this, &AMultiSegmentLineActor::OnSelected); } void AMultiSegmentLineActor::ClearSplineMeshes() { for (USplineMeshComponent* MeshComp : SplineMeshes) { if (MeshComp) { MeshComp->DestroyComponent(); } } SplineMeshes.Empty(); } void AMultiSegmentLineActor::SetPoints(const TArray<FVector>& Points) { if (Points.Num() < 2) return; ClearSplineMeshes(); SplineComp->ClearSplinePoints(false); // Add spline points for (int32 i = 0; i < Points.Num(); i++) { SplineComp->AddSplinePoint(Points[i], ESplineCoordinateSpace::World, false); } SplineComp->SetMobility(EComponentMobility::Static); SplineComp->UpdateSpline(); // Create mesh segments between each pair of points for (int32 i = 0; i < Points.Num() - 1; i++) { FVector StartPos, StartTangent, EndPos, EndTangent; StartPos = SplineComp->GetLocationAtSplinePoint(i, ESplineCoordinateSpace::World); StartTangent = SplineComp->GetTangentAtSplinePoint(i, ESplineCoordinateSpace::World); EndPos = SplineComp->GetLocationAtSplinePoint(i + 1, ESplineCoordinateSpace::World); EndTangent = SplineComp->GetTangentAtSplinePoint(i + 1, ESplineCoordinateSpace::World); USplineMeshComponent* SplineMesh = NewObject<USplineMeshComponent>(this, USplineMeshComponent::StaticClass()); SplineMesh->RegisterComponent(); SplineMesh->AttachToComponent(SplineComp, FAttachmentTransformRules::KeepWorldTransform); UMaterialInstanceDynamic* DynMat = UMaterialInstanceDynamic::Create(SegmentMaterial, this); DynMat->SetVectorParameterValue(FName("Color"), FLinearColor::Green); SplineMesh->SetMobility(EComponentMobility::Movable); SplineMesh->SetStaticMesh(SegmentMesh); SplineMesh->SetMaterial(0, DynMat); // Stretch and orient the mesh along spline segment SplineMesh->SetStartAndEnd(StartPos, StartTangent, EndPos, EndTangent); // Adjust thickness — default cylinder radius is scaled via X/Y scale SplineMesh->SetStartScale(FVector2D(0.05f, 0.05f)); SplineMesh->SetEndScale(FVector2D(0.05f, 0.05f)); //SplineMesh->SetMobility(EComponentMobility::Static); SplineMesh->SetCollisionEnabled(ECollisionEnabled::QueryOnly); SplineMesh->SetCollisionResponseToAllChannels(ECR_Block); SplineMesh->SetGenerateOverlapEvents(true); SplineMeshes.Add(SplineMesh); } } void AMultiSegmentLineActor::OnSelected(AActor* Target, FKey ButtonPressed) { GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Cyan, FString("Line clicked")); }
ChatGPT even told me how to change the line color after I asked politely. This class also registers when it is clicked.
Finally, some code for spawning a line. This code also assumes that you’re using the GeoReferencing plugin for conversion between world and engine coordinates, and moves the spawning actor’s position to the first vertex of the line.
UWorld* World = GetWorld(); if (!World) return; GEngine->AddOnScreenDebugMessage(-1, 2.f, FColor::Cyan, FString("Spawning line")); // Spawn parameters FActorSpawnParameters SpawnParams; SpawnParams.Owner = this; SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; // Spawn location & rotation (doesn't matter much — points will define final shape) FVector SpawnLocation = FVector::ZeroVector; FRotator SpawnRotation = FRotator::ZeroRotator; // Spawn the actor AMultiSegmentLineActor* LineActor = World->SpawnActor<AMultiSegmentLineActor>( AMultiSegmentLineActor::StaticClass(), SpawnLocation, SpawnRotation, SpawnParams ); if (LineActor) { // Define the points for the line TArray<FVector> Points; Points.Add(FVector(213000.f, 585000.f, 0.f)); Points.Add(FVector(213300.f, 585000.f, 100.f)); Points.Add(FVector(213600.f, 585200.f, 150.f)); World2Engine(Points); // Apply points LineActor->SetPoints(Points); SetActorLocation(Points[0]); }