Skip to content

Instantly share code, notes, and snippets.

@tobspr
Created February 2, 2026 20:54
Show Gist options
  • Select an option

  • Save tobspr/a361349e8da02663c9dafd740619e7ce to your computer and use it in GitHub Desktop.

Select an option

Save tobspr/a361349e8da02663c9dafd740619e7ce to your computer and use it in GitHub Desktop.

Adding New Buildings to Shapez 2

This guide walks through the complete process of adding a new building to the game, using the Pipe Reader as a reference example.

Overview

Adding a new building requires creating files in several locations:

  1. Simulation Layer - Core game logic
  2. Building Assets - Unity ScriptableObjects and rendering
  3. Registration - Connecting everything together
  4. Content & UI - Research unlocks, toolbar, localization

Directory Structure

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

Step 1: Create the Simulation

1.1 Configuration Interface

Create I[BuildingName]Configuration.cs in Assets/Game.Content/AtomicBuildings/[Category]/[BuildingName]/:

public interface IPipeReaderConfiguration
{
    public IStoringFluidContainerConfiguration ContainerConfig { get; }
}

1.2 Simulation State and Class

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);
        }
    }
}

1.3 Simulation Factory

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);
    }
}

Step 2: Create Building Assets

2.1 Meta Building Definition

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;
    }
}

2.2 Simulation Renderer

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
    }
}

2.3 Module Data Provider

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);
    }
}

2.4 Unity Asset Files

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 data

Step 3: Registration

3.1 Add Building ID

In Assets/Game/Core/GameMode/GameBuildings.cs, add the building ID:

public readonly BuildingDefinitionGroupId PipeReaderBuildingId = new("PipeReaderDefaultVariant");

3.2 Register Simulation System

In Assets/Game.Orchestration/BuiltinSimulationSystems.cs:

  1. Add to the chain in CreateSimulationSystems():
.Concat(CreatePipeReaderSystems())
  1. 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);
    }
}

3.3 Register Placement Initiator

In Assets/Game.Orchestration/.../SignalBuildingsPlacersCreator.cs:

  1. Add enum value:
PipeReaderPlacementInitiator,
  1. Register in RegisterPlacers():
registry.RegisterInitiator(
    GetName(SignalBuildingsPlacerId.PipeReaderPlacementInitiator),
    CreateDefaultPlacer(Buildings.PipeReaderBuildingId));

3.4 Add to Building Collection

In Assets/Game/Metadata/BuildingCollections/RegularModeBuildingsCollection.asset, add the building GUID to AllGameBuildings:

- {fileID: 11400000, guid: [YOUR_VARIANT_ASSET_GUID], type: 2}

3.5 Register Module Provider

In Assets/Game.Orchestration/.../GameSessionOrchestrator.cs, add:

AddModules(b.PipeReaderBuildingId, modules, new PipeReaderBuildingModuleDataProvider());

Step 4: Content & UI

4.1 Add to Content Bundle

In Assets/Game/Metadata/Resources/Scenarios/SharedData/ContentBundles/CBWires_Core.json:

{
  "$type": "BuildingReward",
  "BuildingDefinitionGroupId": "PipeReaderDefaultVariant"
}

4.2 Add Toolbar Entry

Create Assets/Game/Metadata/Resources/Scenarios/SharedData/Toolbar/Categories/SignalBuildings/FlowControlGroup/PipeReader.json:

{
  "$type": "BuildingBasedPlacementToolbarElementData",
  "BuildingDefinition": "PipeReaderDefaultVariant",
  "PlacementInitiatorId": "PipeReaderPlacementInitiator"
}

4.3 Add to Toolbar Group

In FlowControlGroup.json, add to Children array:

"#include:Scenarios/SharedData/Toolbar/Categories/SignalBuildings/FlowControlGroup/PipeReader"

4.4 Add Localization

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.",

Common Interfaces

Building Types

Interface Purpose
IItemSimulation Belt item input/output
IFluidSimulation Pipe fluid input/output
ISignalSimulation Wire signal input/output
IUpdatableSimulation Called each tick

Connector Types

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

Direction Values

  • 0 = East (right)
  • 1 = North (top)
  • 2 = West (left)
  • 3 = South (bottom)

Testing Checklist

  • 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

Troubleshooting

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment