Skip to content

Proposal: LogicBlock InitState method to allow for type safety. #217

Description

@Mosakaas

This is a proposal: I would love to implement this, but before I do I would like to know if it'd be warranted, and if there'd be any issues with my approach.


I would like to reduce the amount of setup and increase type safety of LogicBlocks and their state. Currently, Get and Set play a big role in its setup, which offer no compile-time warnings.

public class Player
{
  public void Setup()
  {
    Settings = new PlayerLogic.Settings(...);
    PlayerLogic = new PlayerLogic();

    // Set PlayerLogic data (via blackboard)
    PlayerLogic.Set(this as IPlayer);
    PlayerLogic.Set(Settings);
    PlayerLogic.Set(AppRepo);
    PlayerLogic.Set(GameRepo);
    PlayerLogic.Save(() => new PlayerLogic.Data());
  }
}

public partial class PlayerLogic
{
  public partial record State
  {
    public abstract partial record Alive : State
    {
      public virtual Transition On(in Input.PhysicsTick input)
      {
        // Get PlayerLogic data (via blackboard)
        // Nothing ensures these objects exist.
        var player = Get<IPlayer>();
        var settings = Get<Settings>();
        var gameRepo = Get<IGameRepo>();
        var data = Get<Data>();
      }
    }
  }
}

With an overridable method in LogicBlock, we could specify a factory method for State in which we could provide our objects:

[Meta, Id("player_logic")]
[LogicBlock(typeof(State), Diagram = true)]
public partial class PlayerLogic(
  IPlayer player, 
  PlayerLogic.Settings settings, 
  IAppRepo appRepo, 
  IGameRepo gameRepo) 
  : LogicBlock<PlayerLogic.State>, IPlayerLogic
{
  public override Transition GetInitialState() => To<State.Disabled>();

  protected override TStateType CreateState<TStateType>() 
    where TStateType : PlayerLogic.State, new()
    => new TStateType()
    {
      Player = player,
      Settings = settings,
      AppRepo = appRepo,
      GameRepo = gameRepo
    };

  [Meta]
  public abstract partial record State : StateLogic<State>
  {
    public required IPlayer Player { get; }
    public required PlayerLogic.Settings Settings { get; }
    public required IAppRepo AppRepo { get; }
    public required IGameRepo GameRepo { get; }
  }
}

And in practice, do this:

public class Player
{
  public void Setup()
  {
    Settings = new PlayerLogic.Settings(...);
    PlayerLogic = new PlayerLogic(this, Settings, AppRepo, GameRepo);

    // Save PlayerLogic data (would love to tackle this one as well someday)
    PlayerLogic.Save(() => new PlayerLogic.Data());
  }
}

public partial class PlayerLogic
{
  public partial record State
  {
    public abstract partial record Alive : State
    {
      public virtual Transition On(in Input.PhysicsTick input)
      {
        // Get objects as they have been initialized.
        var player = this.Player;
        var settings = this.Settings;
        var gameRepo = this.GameRepo;
        var data = Get<Data>(); // My arch-nemesis...
      }
    }
  }
}

Of course, you can always do PlayerLogic.GameRepo { get; set; } if you'd like to set them during runtime.

Hidden benefit: using type-checking you can be even more expressive of how you want your state to be created:

  protected override TStateType CreateState<TStateType>() 
    where TStateType : PlayerLogic.State, new()
    => typeof(TStateType) == typeof(PlayerLogic.State.Swimming) ? new() { ... } :
    typeof(TStateType) == typeof(PlayerLogic.State.Running) ? new() { ... } :
    new() { ... };

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions