Skip to content

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

@Mosakaas

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

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions