This guide walks through the complete process of adding a new building to the game, using the Pipe Reader as a reference example.
Adding a new building requires creating files in several locations:
- Simulation Layer - Core game logic
- Building Assets - Unity ScriptableObjects and rendering
- Registration - Connecting everything together
- Content & UI - Research unlocks, toolbar, localization
Assets/
├── Game.Content/AtomicBuildings/[Category]/[BuildingName]/
│ ├── I[BuildingName]Configuration.cs
│ ├── [BuildingName]Simulation.cs
│ └── [BuildingName]SimulationFactory.cs
│
├── Game/Buildings/[Category]/[BuildingName]/
│ ├── [BuildingName]DefaultVariant.asset
│ ├── [BuildingName]DefaultInternalVariant.asset
│ ├── [BuildingName]MetaBuildingDefinition.cs
│ ├── [BuildingName]SimulationRenderer.cs
│ └── [BuildingName]BuildingModuleDataProvider.cs
│
├── Game/Metadata/Resources/Scenarios/SharedData/
│ ├── ContentBundles/CB[Category].json
│ └── Toolbar/Categories/[Category]/[Group].json
│
└── Resources/Translations/en.json
Create I[BuildingName]Configuration.cs in Assets/Game.Content/AtomicBuildings/[Category]/[BuildingName]/:
public interface IPipeReaderConfiguration
{
public IStoringFluidContainerConfiguration ContainerConfig { get; }
}Create [BuildingName]Simulation.cs:
using Game.Content.Features.Fluids;
using Game.Content.Features.Signals;
using Game.Content.Features.Signals.Conductor;
using Game.Content.Features.Signals.Connections;
using Game.Content.Features.Signals.Simulation;
using Game.Content.Features.Signals.Tick;
using Game.Core.Serialization;
using Game.Core.Simulation;
// State class - handles serialization
[SyncableIdentifier("PipeReaderState")]
public class PipeReaderSimulationState : ISimulationState
{
public readonly FluidContainerState ContainerState = new();
public void Sync(ISerializationVisitor visitor)
{
ContainerState.Sync(visitor);
}
}
// Simulation class - implements interfaces based on building type
public class PipeReaderSimulation
: Simulation<PipeReaderSimulationState>, IFluidSimulation, ISignalSimulation, IUpdatableSimulation
{
public readonly StoringFluidContainer Container;
public readonly SignalConductorOutput OutputConductor;
// Define input/output counts
public int NumFluidProviders => 1;
public int NumFluidReceivers => 1;
public int NumSignalProviders => 1;
public int NumSignalReceivers => 0;
public PipeReaderSimulation(PipeReaderSimulationState state, IPipeReaderConfiguration config) : base(state)
{
Container = new StoringFluidContainer(state.ContainerState, config.ContainerConfig);
OutputConductor = new SignalConductorOutput();
}
public IFluidProvider GetFluidProvider(int index) => Container;
public IFluidReceiver GetFluidReceiver(int index) => Container;
public ISignalProvider GetSignalProvider(int index) => OutputConductor;
public void ClearContent()
{
Container.Flush();
}
public void Update(Ticks startTicks, Ticks deltaTicks)
{
Container.Update(deltaTicks);
// Output signal based on current fluid
int amountOfSignals = SignalSimulation.GetAmountOfSignalsThisUpdate(startTicks, deltaTicks);
IFluid fluid = Container.Fluid;
ISignal outputSignal = fluid != null ? FluidSignal.From(fluid) : NullSignal.Instance;
SignalTicks startSignalTick = SignalTicks.FromTicks(startTicks);
for (int i = 0; i < amountOfSignals; i++)
{
var signalTick = new SignalTicks(startSignalTick.NumOfTicks + i);
OutputConductor.PushSignal(outputSignal, startTicks, signalTick);
}
}
}Create [BuildingName]SimulationFactory.cs:
using Core.Factory;
public class PipeReaderSimulationFactory : IFactory<PipeReaderSimulationState, PipeReaderSimulation>
{
private IPipeReaderConfiguration Configuration;
public PipeReaderSimulationFactory(IPipeReaderConfiguration configuration)
{
Configuration = configuration;
}
public PipeReaderSimulation Produce(PipeReaderSimulationState state)
{
return new PipeReaderSimulation(state, Configuration);
}
}Create [BuildingName]MetaBuildingDefinition.cs in Assets/Game/Buildings/[Category]/[BuildingName]/:
using System;
using System.Collections.Generic;
using Game.Core.Rendering.MeshGeneration;
using Sirenix.OdinInspector;
using UnityEngine;
public class PipeReaderMetaBuildingDefinition : MetaBuildingDefinition
{
public override IBuildingCustomDrawData CustomDrawData => Draw;
public override ICustomSimulationConfiguration CustomSimulationConfiguration => Config;
[Space(20)]
[Title("Specific Draw Data")]
[TabGroup("main", "Draw")]
[HideLabel]
public DrawData Draw;
[Title("Additional Simulation Configuration")]
[TabGroup("main", "Simulation")]
[HideLabel]
[Space(20)]
public Configuration Config;
[Serializable]
public class DrawData : IFluidRendererDrawData, IBuildingMirrorableCustomDrawData, ISerializationCallbackReceiver
{
public IReadOnlyList<ILODMesh> FluidFillMeshes { get; protected set; }
[RequiredListLength(1, 99)]
[SerializeField]
protected LODMeshAsset[] _FluidFillMeshes;
[Required]
public LODMeshAsset ActiveIndicatorMesh;
public IBuildingCustomDrawData Mirror(IMeshCache meshCache) => this;
public void OnBeforeSerialize() { }
public void OnAfterDeserialize() { FluidFillMeshes = _FluidFillMeshes; }
}
[Serializable]
public class Configuration : ICustomSimulationConfiguration, IPipeReaderConfiguration
{
public StoringFluidContainerConfiguration ContainerConfig;
IStoringFluidContainerConfiguration IPipeReaderConfiguration.ContainerConfig => ContainerConfig;
}
}Create [BuildingName]SimulationRenderer.cs:
using Game.Content.Features.Fluids;
using Game.Core.Coordinates;
using Game.Core.Map.Simulation;
using JetBrains.Annotations;
using Unity.Mathematics;
[UsedImplicitly]
public class PipeReaderSimulationRenderer
: FluidBuildingRenderer<PipeReaderSimulation, PipeReaderMetaBuildingDefinition.DrawData,
DefaultBuildingCustomSoundData, FluidBuildingRendererStateData<PipeReaderSimulation>>
{
public PipeReaderSimulationRenderer(IMapModel map, IMapSimulator simulator, IBuildingSoundManager soundManager)
: base(map, simulator, soundManager) { }
protected override void OnDrawDynamic(in Entity entity, FrameDrawOptions options)
{
// Rendering logic here - see existing renderers for patterns
}
}Create [BuildingName]BuildingModuleDataProvider.cs:
using System.Collections.Generic;
using Game.Core.Map.Simulation;
public class PipeReaderBuildingModuleDataProvider
: SimulationBasedBuildingModuleDataProvider<ILocalizedSimulation, PipeReaderSimulation,
PipeReaderMetaBuildingDefinition.Configuration>
{
protected override IEnumerable<StructureStat> GetStats(
IBuildingDefinition definition,
PipeReaderMetaBuildingDefinition.Configuration config)
{
yield return new StructureStatBuildingsPerFluidLauncher(1);
yield return new StructureStatFluidThroughput(config.ContainerConfig.ProvidingRate);
}
protected override IEnumerable<IHUDSidePanelModuleData> GetSimulationModules(
BuildingModel building,
ILocalizedSimulation localizedSimulation,
PipeReaderSimulation actualSimulation)
{
yield return new HUDSidePanelModuleWireInfo.Data("Output", actualSimulation.OutputConductor);
}
}Create the .asset files. These are YAML files that reference Unity scripts by GUID.
[BuildingName]DefaultVariant.asset:
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_Script: {fileID: 11500000, guid: 982168674c2b94a8f87734c052f2e6f0, type: 3}
m_Name: PipeReaderDefaultVariant
Id: PipeReaderDefaultVariant
# ... other properties (see existing variants for reference)
InternalVariants:
- {fileID: 11400000, guid: [INTERNAL_VARIANT_GUID], type: 2}[BuildingName]DefaultInternalVariant.asset:
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_Script: {fileID: 11500000, guid: [META_BUILDING_DEFINITION_GUID], type: 3}
m_Name: PipeReaderDefaultInternalVariant
Id: PipeReaderDefaultInternalVariant
# Connector definitions - Direction_L values:
# 0 = East (right), 1 = North (top), 2 = West (left), 3 = South (bottom)
FluidProviderConnectorIOs:
- Direction_L: 0
Position_L: {x: 0, y: 0, z: 0}
_IOType: 2
FluidConsumerConnectorIOs:
- Direction_L: 2
Position_L: {x: 0, y: 0, z: 0}
_IOType: 2
SignalProviderConnectorIOs:
- Direction_L: 1
Position_L: {x: 0, y: 0, z: 0}
_IOType: 2
# ... configuration and draw dataIn Assets/Game/Core/GameMode/GameBuildings.cs, add the building ID:
public readonly BuildingDefinitionGroupId PipeReaderBuildingId = new("PipeReaderDefaultVariant");In Assets/Game.Orchestration/BuiltinSimulationSystems.cs:
- Add to the chain in
CreateSimulationSystems():
.Concat(CreatePipeReaderSystems())- Add the creation method:
private IEnumerable<ISimulationSystem> CreatePipeReaderSystems()
{
IBuildingDefinitionGroup pipeReader = Mode.Buildings.GetVariant(Mode.Buildings.PipeReaderBuildingId);
foreach (IBuildingDefinition internalVariant in pipeReader.Definitions)
{
var config = internalVariant.ConfigAs<IPipeReaderConfiguration>();
var factory = new PipeReaderSimulationFactory(config);
yield return new AtomicStatefulBuildingSimulationSystem<PipeReaderSimulation, PipeReaderSimulationState>(
factory,
internalVariant.Id,
Logger);
}
}In Assets/Game.Orchestration/.../SignalBuildingsPlacersCreator.cs:
- Add enum value:
PipeReaderPlacementInitiator,- Register in
RegisterPlacers():
registry.RegisterInitiator(
GetName(SignalBuildingsPlacerId.PipeReaderPlacementInitiator),
CreateDefaultPlacer(Buildings.PipeReaderBuildingId));In Assets/Game/Metadata/BuildingCollections/RegularModeBuildingsCollection.asset, add the building GUID to AllGameBuildings:
- {fileID: 11400000, guid: [YOUR_VARIANT_ASSET_GUID], type: 2}In Assets/Game.Orchestration/.../GameSessionOrchestrator.cs, add:
AddModules(b.PipeReaderBuildingId, modules, new PipeReaderBuildingModuleDataProvider());In Assets/Game/Metadata/Resources/Scenarios/SharedData/ContentBundles/CBWires_Core.json:
{
"$type": "BuildingReward",
"BuildingDefinitionGroupId": "PipeReaderDefaultVariant"
}Create Assets/Game/Metadata/Resources/Scenarios/SharedData/Toolbar/Categories/SignalBuildings/FlowControlGroup/PipeReader.json:
{
"$type": "BuildingBasedPlacementToolbarElementData",
"BuildingDefinition": "PipeReaderDefaultVariant",
"PlacementInitiatorId": "PipeReaderPlacementInitiator"
}In FlowControlGroup.json, add to Children array:
"#include:Scenarios/SharedData/Toolbar/Categories/SignalBuildings/FlowControlGroup/PipeReader"In Assets/Resources/Translations/en.json:
"building-variant.PipeReaderDefaultVariant.title": "Pipe Reader",
"building-variant.PipeReaderDefaultVariant.description": "Reads the current <gl>Fluid</gl> in a pipe and outputs it as a signal.",| Interface | Purpose |
|---|---|
IItemSimulation |
Belt item input/output |
IFluidSimulation |
Pipe fluid input/output |
ISignalSimulation |
Wire signal input/output |
IUpdatableSimulation |
Called each tick |
| Connector | Direction | Purpose |
|---|---|---|
BeltInputs / BeltOutputs |
Item flow | Conveyor belt connections |
FluidProviderConnectorIOs |
Fluid output | Pipe output |
FluidConsumerConnectorIOs |
Fluid input | Pipe input |
SignalProviderConnectorIOs |
Signal output | Wire output |
SignalConsumerConnectorIOs |
Signal input | Wire input |
0= East (right)1= North (top)2= West (left)3= South (bottom)
- Building appears in toolbar after unlocking content bundle
- Building can be placed
- Building connects to appropriate transport types
- Simulation works correctly
- Side panel shows correct stats/info
- Localization displays correctly
- No errors in console
| Error | Solution |
|---|---|
| "Invalid building group reward" | Add to RegularModeBuildingsCollection.asset |
| "Unknown translation id" | Add to en.json |
| "Could not find module provider" | Register in GameSessionOrchestrator.cs |
| Building not in toolbar | Add to toolbar group JSON's Children array |
| Building not unlockable | Add to content bundle JSON |