Skip to content

Timberborn architecture

mwierzchos edited this page Oct 29, 2025 · 1 revision

Dependency injection

Timberborn uses Bindito, our internal dependency injection framework. The game automatically finds and installs all implementations of IConfigurator annotated with the Context attribute, which specifies the scene in which the configurator should be installed. Valid parameters are: "MainMenu", "Game", "MapEditor", and "Bootstrapper". The last one is used for configurators installed in a global context.

Example:

using Bindito.Core;

[Context("MainMenu")]
public class ConfiguratorTest : Configurator {

  protected override void Configure() {
    Bind<Foo>().AsSingleton();
    Bind<Bar>().AsSingleton();
    Bind<Baz>().AsTransient();
  }

}

There are two main binding types: AsSingleton() and AsTransient(). Singletons are instantiated once and persist for the duration of the scene; they are typically used for service classes. Transients are instantiated on demand - every object that depends on a transient type receives its own instance. They are often used for components that define entity behavior.

When a bound object is instantiated, it automatically receives (is injected with) all dependencies defined in its constructor. For example, the Foo implementation below depends on Bar:

internal class Foo {

  private Bar _bar;

  public Foo(Bar bar) {
    _bar = bar;
  }

}

When the Main Menu scene loads, the game creates an instance of the Foo class because it's bound as a singleton. Before that, it must first create Bar (and any dependencies that Bar itself requires, and so on).


Besides binding and reusing single objects, you can also bind collections of types, as shown below:

protected override void Configure() {
  MultiBind<IBlockObjectValidator>().To<BottomMatchingTemplateValidator>().AsSingleton();
}

In this example, a list of block object validators is defined, one of them being BottomMatchingTemplateValidator. Multibound types are injected as an IEnumerable collection that contains instances of all bound types.

public class BlockObjectValidationService {

  private readonly ImmutableArray<IBlockObjectValidator> _blockObjectValidators;

  public BlockObjectValidationService(IEnumerable<IBlockObjectValidator> blockObjectValidators) {
    _blockObjectValidators = blockObjectValidators.ToImmutableArray();
  }

  [...]

}

Entities & components

Objects spawned in the game world - representing things like beavers, buildings, or plants - are called entities. Each entity is made up of multiple components that define its behavior. Components are either a Spec from a Blueprint or a class inheriting from BaseComponent.

For example, BotBuildingLighting is a simple component that changes the lighting of a building to the variant used by bots. This component is currently used by the Iron Teeth’s Numbercruncher and Control Tower buildings.

namespace Timberborn.Bots {
  public class BotBuildingLighting : BaseComponent,
                                     IFinishedStateListener {

    private readonly MaterialColorer _materialColorer;
    private readonly BotColors _botColors;

    public BotBuildingLighting(MaterialColorer materialColorer,
                               BotColors botColors) {
      _materialColorer = materialColorer;
      _botColors = botColors;
    }

    public void OnEnterFinishedState() {
      UpdateLighting();
    }

    public void OnExitFinishedState() {
    }

    public void UpdateLighting() {
      _materialColorer.SetLightingHueOffset(GameObject, _botColors.BotLightHueOffset);
    }

  }
}

Many BaseComponent classes implement common interfaces that allow them to react to specific lifecycle events. The most commonly used ones include:

  • IAwakableComponent - provides the Awake method, called when the entity or component is created.
  • IInitializableEntity - provides the Initialize method, called when the entity is placed on the map.
  • IFinishedStateListener - provides OnEnterFinishedState (called when construction is completed or a map editor object is placed on the map) and OnExitFinishedState (called when it is destroyed).
  • IDeletableEntity - provides the Delete method, called when the entity is deleted.
  • IPersistentEntity - provides Save and Load methods, called when saving or loading a game.

Singletons

Singletons can also implement lifecycle interfaces that control when their logic runs. The two main ones are:

  • ILoadableSingleton - provides a Load method called when a scene is loaded. It respects dependency order, meaning it is only called after all dependencies have been loaded.
  • IUpdatableSingleton - provides an UpdateSingleton method that is called every frame.

Decoration

Entities are created from Blueprint templates that define them using Specs. These Specs can cause other components to be added during entity creation. Those components may in turn spawn others, and so on - this process is called decoration.

namespace Timberborn.Fields {
  [Context("Game")]
  internal class FieldsConfigurator : Configurator {

    protected override void Configure() {
      MultiBind<TemplateModule>().ToProvider(ProvideTemplateModule).AsSingleton();
    }

    private static TemplateModule ProvideTemplateModule() {
      var builder = new TemplateModule.Builder();
      builder.AddDecorator<FarmHouseSpec, FarmHouse>();
      builder.AddDecorator<FarmHouse, HaulCandidate>();
      return builder.Build();
    }
    
  }
}

In the example above, FarmHouseSpec is decorated by the FarmHouse component, which handles the farmhouse’s planting behavior. The FarmHouse component is further decorated by HaulCandidate, which makes the building operable by haulers.


Specs

Blueprints define most of the entities and configurable aspects of the game. This is achieved through Specs - data storage types that, together with the Decoration system described above, allow the game to create complex entities with diverse and reusable behaviors.

If you want to write your own Spec, there are a few rules to follow:

  • It must be a record.
  • It must inherit from ComponentSpec.
  • It should be immutable and stateless - this is more of a convention than a strict rule, but we strongly recommend it, as Spec instances are shared between entities.
  • It can define any number of properties, but those you want to serialize (read from JSON) must be marked with the [Serialize] attribute and have { get; init; } accessors.
    • To store collections, use ImmutableArray.
    • You can define non-primitive properties, but they must also follow the same rules (except inheriting from ComponentSpec).

Example:

internal record SleeperSpec : ComponentSpec {

  [Serialize]
  public ImmutableArray<ContinuousEffectSpec> SleepOutsideEffects { get; init; }

  [Serialize]
  public float MaxOffsetInHours { get; init; }

}

public record ContinuousEffectSpec {

  [Serialize]
  public string NeedId { get; init; }

  [Serialize]
  public float PointsPerHour { get; init; }

  [Serialize]
  public bool SatisfyToMaxValue { get; init; }

}

Clone this wiki locally