What is a Buildable Hologram?

Buildable Holograms control the logic of what happens when constructing buildings and where players can place them.

Each FGBuildable needs a hologram specified to be constructed. Most of the time, you can re-use an existing hologram class made by Coffee Stain, since they have a lot of the common use cases covered already. However, you may want to write your own logic for additional functionality, such as when you want to upgrade a building, snap to other buildings, or to snap something in the environment.

Writing your own Hologram logic requires writing some C++ code. This page assumes basic knowledge of C++.

Create your Own Hologram Class

First, you’ll have to create a class that extends FGBuildableHologram, or one of its subclasses, such as FGFactoryBuildingHologram (which implements Zooping) or FGSplineHologram (used for pipes and belts), if you want to re-use some of their functionality.

Most base-game hologram headers can be found in Source/FactoryGame/Public/Hologram/.

This docs page assumes your hologram class is called AMyModHologram.

Build Modes

Build Modes are the different options users can select from while constructing a building. Examples include the Noodle and Vertical build modes for pipes and hypertubes, and the default or zooping modes for walls and foundations.

Holograms define which modes are supported for a building through its implementation of GetSupportedBuildModes.

Here are some C++ headers relevant to Build Modes from FGHologram.h:

/**
* Get the build modes implemented for the hologram
* @param out_buildmodes	 Array with all supported build modes
*/
UFUNCTION( BlueprintNativeEvent, Category = "Hologram" )
void GetSupportedBuildModes( TArray< TSubclassOf<UFGHologramBuildModeDescriptor> >& out_buildmodes ) const;

UFUNCTION( BlueprintPure, Category = "Hologram" )
TSubclassOf<UFGHologramBuildModeDescriptor> GetCurrentBuildMode();

UFUNCTION( BlueprintCallable, Category = "Hologram" )
void SetBuildMode( TSubclassOf<UFGHologramBuildModeDescriptor> mode );

UFUNCTION( BlueprintCallable, Category = "Hologram" )
void CycleBuildMode( int32 deltaIndex );

UFUNCTION( BlueprintPure, Category = "Hologram" )
bool IsCurrentBuildMode( TSubclassOf<UFGHologramBuildModeDescriptor> buildMode ) const;

virtual void OnBuildModeChanged();

In the below example, we have our custom hologram add an additional build mode mNewBuildmode to the supported build modes it inherited from its superclass.

void AMyModHologram::GetSupportedBuildModes_Implementation(TArray<TSubclassOf<UFGHologramBuildModeDescriptor>>& out_buildmodes) const
{
	Super::GetSupportedBuildModes_Implementation(out_buildmodes);

	if(mNewBuildmode)
		out_buildmodes.AddUnique(mNewBuildmode);
}

Don’t add the same BuildMode class twice, and don’t add any invalid BuildModes! That’s why the example includes the null check before adding an item to out_buildmodes, and why AddUnique is used.

BuildModes are usually changed by the user pressing the keybinding when the buildgun is out (default R). However, we can also do this programmatically via SetBuildMode(MyBuildMode) or CycleBuildMode(1) (next) / CycleBuildMode(-1) (previous).

We may want to change our hologram’s behavior depending on which build mode is active. There are a few ways to handle this.

The first is to have the function IsCurrentBuildMode(MyBuildMode), which returns true if the current build mode is MyBuildMode. We can also get the active build mode via GetCurrentBuildMode() and compare it using equality checks or some other logic. In addition, we can also perform actions when the build mode has been changed.

For example, the below snippet uses OnBuildModeChanged() to set MyProperty in response to a changed build mode.

void AMyModHologram::OnBuildModeChanged()
{
    Super::OnBuildModeChanged();

    MyProperty = IsCurrentBuildMode(MyBuildMode);
}

If you want to re-use the build mode of an existing base-game buildable, it’s usually found as a field in that buildable’s hologram. In this case, your hologram should probably inherit from the class that owns that build mode. Otherwise, you can read the reference from the class' CDO at hologram BeginPlay/Construction.

Limit Placement in the World

Holograms may have different constraints, based on physical limits (e.g. "Floor is too step"), Building type (like power cables), or advanced logic (like FicsIt-Network Computer Components, which can only be placed inside a Computer Case).

You may need to customize this logic too for your necessities and use cases, like:

  • Allow building only on Walls

  • Require placement on a resource node

  • Snapping to one or more existing Buildable classes

  • Whatever your creativity demands

The FGBuildableHologram class exposes different methods which can be overridden in your Hologram to achieve the desired constraints.

Checking for a Valid Hit

Whenever the Build Gun points somewhere in the game world, the IsValidHitResult method defined in your Hologram class is invoked. The hitResult argument will contain information about the location and the Actor which is currently being aimed at. Using this data, we can enable placement only when pointing at specific objects.

Suppose you created some specific pillars MyModSpecificPillar which can only be placed over your specific supports UMyModSpecificPillarSupport for aesthetic reasons. To do so, you could override the method as following:

bool UMyModSpecificPillarHologram::IsValidHitResult(const FHitResult& hitResult) const {
  AActor* Actor = hitResult.GetActor();

  // Try casting to our specific Buildable class.
  const UMyModSpecificPillarSupport* = Cast<UMyModSpecificPillarSupport>(Actor);
  // If the cast result is valid, it means we're aiming a UMyModSpecificPillarSupport.
  if (IsValid(UMyModSpecificPillarSupport)) {
    // This is the place where we could check more information,
    // such as height, checking fields on the support to see if one was already bound, etc.
    // For the sake of simplicity, we simply allow the placement.
    return true;
  }

  return false;
}

Returning false from this method hides the hologram. In case you just want to display a red hologram with a message explaining why the buildable cannot be placed in the aimed location, see Show Disabled Holograms with Disqualifiers.

Snapping to other Buildables

Sometimes you need your Buildable to snap to one Actor, "locking" it into place. An example of this behavior is the Power Cable, which "snaps" to existing Power Poles / Plugs. The peculiarity in this case is that hologram position will not be updated while snapped, so the SetHologramLocationAndRotation method will not be called. In order to control the snapping, you should implement the AFBuildableHologram::TrySnapToActor method.

As a reference, this behavior is implemented in the base game in the following situations:

  • Snapping to Factory connections like Inputs or Outputs (pipes, belts)

  • Snapping to snap points like Signs do

This behavior is not intended for things like aligning to a grid. In that case you should consider overriding the SetHologramLocationAndRotation method, adjusting the positioning logic there.

The term "snap" should be intended as a "lock in place" where you don’t want to allow moving the buildable, showing a link to the snapped Actor.

For the next example, suppose you want to snap your shiny new Glass Window buildable (MyModGlassWindow) to existing walls. Whenever it snaps, the Glass Window should be locked to the wall without allowing further movement. So we could write:

// In the header file, define a Snapped property to track the currently snapped object.
AFGBuildableWall* Snapped = nullptr;

// In the cpp file, override the snapping method:
bool UMyModGlassWindowHologram::TrySnapToActor(const FHitResult& hitResult) {
  const auto Actor = hitResult.Actor.Get();
  if (!IsValid(Actor)) {
    // We moved away from our previous snapping, so clear our tracker
    Snapped = nullptr;
    return false;
  }

  if (Actor->IsA<AFGBuildableWall>()) {
    Snapped = Cast<AFGBuildableWall>(Actor);
    // Here you can add your custom snapping logic.
    // We are using `SetActorLocationAndRotation` which is better for performance than setting single properties.
    SetActorLocationAndRotation(Actor->GetActorLocation(), Actor->GetActorRotation());
    // We snapped, so return true to disable following updates
    return true;
  }

  Snapped = nullptr;
  return false;
}

If the method returns true, the hologram location and rotation will not be updated automatically. You have to write your own custom snapping logic to move the hologram.

TrySnapToActor will not be called if IsValidHitResult returns false. Furthermore, consider that IsValidHitResult has a default implementation, so if you have issues, try to override it with a return true; statement to let the code reach your TrySnapToActor.

Show Disabled Holograms with Disqualifiers

It is possible to show the red outline and the red hologram while placing the buildable, keeping the hologram visible while still disallowing Build Gun placement. An example of this in the base game is seeing the red hologram with a message like "Floor too step" - damned rail tracks!

To implement something similar for your buildables, you can simply return true from the IsValidHitResult method (allowing your successive methods like TrySnapToActor or CheckValidPlacement to be called). Then, when an invalid placement is found, you can use AddConstructDisqualifier() to signal the player about the error.

We can get the previous MyModGlassWindow example, and add a disqualifier if it’s not snapped. This time, you should still see the Glass Window hologram, highlighted in red.

void UMyModGlassWindowHologram::CheckValidPlacement() {
  if (!IsValid(Snapped) || !Snapped->IsA<AFGBuildableWall>()) {
    AddConstructDisqualifier(UFGCDMustSnapWall::StaticClass());
  }

  Super::CheckValidPlacement();
}

It’s not mandatory to call AddConstructDisqualifier from the CheckValidPlacement method. It works even from TrySnapToActor, for example.

You can usually re-use one of the many preexisting disqualifiers from the base game for your own buildables. For example, UFGCDMustSnapWall will require snapping to a wall. You can find the complete list in the header file Source/FactoryGame/Public/FGConstructDisqualifier.h.

It’s possible to define your custom disqualifiers too, like:

#define LOCTEXT_NAMESPACE "MyModLocNamespace"

UCLASS()
class UMyModCDMustSnapBeautifulWall : public UFGConstructDisqualifier {
  GENERATED_BODY()

  UMyModCDMustSnapBeautifulWall() {
    mDisqfualifyingText = LOCTEXT( "UMyModCDMustSnapBeautifulWall", "Must snap to a beautiful wall!" );
  }
};

#undef LOCTEXT_NAMESPACE

Note that the property is spelled mDisqfualifyingText and not mDisqualifyingText. This is a typo in the game headers that your file must be consistent with.

Configuring a Buildable

Holograms also allow us to supply values or perform changes to buildings as they are built. This allows, for example, changing a mesh depending on the building’s location, rotating a component a bit, or setting references to a snapped building.

There are different phases of the that we can use depending on what we want to do to the buildable, and when we want the changes to take place.

Configure functions are called in the following order, and can thus override each others' steps. This list is adapted from comments in FGBuildableHologram.h

  • PreConfigureActor( buildable );

  • ConfigureActor( buildable );

  • ConfigureBuildEffect( buildable );

  • (Perform the actual spawning of the buildable actor in the world)

  • ConfigureComponents( buildable );

  • (BeginPlay called on the buildable)

Next, we’ll go into each phase in more detail.

PreConfigureActor

/**
 * Function to allow any pre-initialization on the actor before the configuration occurs. This is to allow for
 * final checks and to set properties as once were configuring its all const from there
 */
virtual void PreConfigureActor( class AFGBuildable* inBuildable );

In certain cases it may be necessary to check the properties again before the configuration of the actor starts. We can do that here.

ConfigureActor

/**
* Configure function: Configuring the actor created from the hologram when executed.
* @param inBuildable - The resulting buildable placed in the world that we are to configure before it's finished.
* @note DO NOT TOUCH COMPONENTS HERE as they'll be overwritten! Use ConfigureComponents for that
*/
virtual void ConfigureActor( class AFGBuildable* inBuildable ) const;

Configure Actor should only be used to set properties, not to create components or anything like that.

This is useful, for example, for moving properties from an upgraded actor to the new one if performing an Building Upgrade.

ConfigureBuildEffect

/** Configures the build effect for the constructed actor. */
void ConfigureBuildEffect( class AFGBuildable* inBuildable );

ConfigureComponents

/**
* Configure function: Configuring the actor component created from the hologram when executed.
* @param inBuildable - The resulting buildable placed in the world that we are to configure before it's finished.
* @note This is a good place to initialize snapped connections etc.
*/
virtual void ConfigureComponents( class AFGBuildable* inBuildable ) const;

Configure Components is a good place to, for example, change positions of components, or to replace a pipe connection with an upgraded actor.

Upgrading a Buildable

Holograms also allow implementing the upgrading of existing buildings. This is useful when you have multiple tiers of a building, and you want to upgrade them without having to dismantle the old one each time.

In the base game this is used by belts, for example.

Here are some C++ headers relevant to upgrading from FGHologram.h:

/** Get the target upgraded Actor */
virtual AActor* GetUpgradedActor() const override;

/** Do we allowed to Upgrade? */
virtual bool TryUpgrade(const FHitResult& hitResult) override;

private:
/** target upgraded Actor */
UPROPERTY(Transient)
AActor* mUpgradedActor = nullptr;

Let’s go into each of these in more detail.

mUpgradedActor

UPROPERTY(Transient)
AActor* mUpgradedActor = nullptr;

This field references the actor we are looking at when trying to upgrade. It’s the old building whose information we probably want to move to our new one.

GetUpgradedActor

/** Get the target upgraded Actor */
virtual AActor* GetUpgradedActor() const override;

You should return the Target actor here (in our example, mUpgradedActor).

TryUpgrade

/** Do we allowed to Upgrade? */
virtual bool TryUpgrade(const FHitResult& hitResult) override;

This function is called to check whether we are allowed to upgrade an actor. You should be sure to set mUpgradedActor from the hit result here, otherwise strange things can happen. You should also set the location of the hologram to that of the hit actor. Returning true means the upgrade is allowed.

Example Upgrade Hologram

A very basic example for the C++ part:

The base game logic for Upgrading actors will automatically handle belt, pipe, and power connections as long as the connection points use the same location and the same names.

However, inventories must be manually transferred, as well as fields like the selected recipe in machines.

For inventories, you can use for example NewBuildingInventory→CopyFromOtherComponent(OldBuildingInventory); in the ConfigureComponents step.

AActor* AMyModHologram::GetUpgradedActor() const
{
  // return the target actor to hide them ingame!
  return mUpgradedActor;
}

bool AMyModHologram::TryUpgrade(const FHitResult& hitResult)
{
  if(hitResult.GetActor())
  {
    const TSubclassOf<AActor> ActorClass = GetActorClass();

    // we check here that we don't try to upgrade the same Actor. the class should be different!
    if(hitResult.GetActor()->GetClass() != ActorClass)
    {
      // IMPORTANT we need to set the location from our hologram to the target Actor
      SetActorTransform(hitResult.GetActor()->GetActorTransform());

      // set the UpgradedActor and return true if it is valid (should be only make sure)
      mUpgradedActor = hitResult.GetActor();

      return mUpgradedActor != nullptr;
    }
  }

  // otherwise the UpgradedActor to nullptr
  mUpgradedActor = nullptr;
  return Super::TryUpgrade(hitResult);
}