4
\$\begingroup\$

I'm working on a game with unity, and I've created a state manager for the in-game state of certain organisms. Each state implements a ISimpleStateManager interface, but some have unique behavior and need to do different things when their state is updated:

interface IOrganismStateManager {
 void SetIdle();
 void SetRunning(float dir);
 void SetCarryingItem(Item item);
}
enum OrganismtState {
 idle,
 running,
 carryingItem,
}
class OrganismStateManager : IOrganismStateManager {
 Dictionary<OrganismtState, ISimpleStateManager> _states = new Dictionary<OrganismtState, ISimpleStateManager>() {
 { OrganismtState.idle, new SimpleStateManager() },
 { OrganismtState.running, new RunningStateManager() },
 { OrganismtState.carryingItem, new CarryingItemStateManager() },
 }
 public void SetIdle() {
 _states[OrganismtState.idle].SetActive(true);
 }
 public void SetRunning(float dir) {
 var stateManager = _states[OrganismtState.running];
 stateManager.SetActive(true);
 var runningManager = (RunningStateManager) stateManager;
 runningManager.SetDirection(dir);
 }
 public void SetCarryingItem(Item item) {
 var stateManager = _states[OrganismtState.carryingItem];
 stateManager.SetActive(true);
 var runningManager = (CarryingItemStateManager) stateManager;
 runningManager.SetCarriedItem(item);
 }
}
interface ISimpleStateManager {
 void SetActive(bool active);
}
class SimpleStateManager : ISimpleStateManager {
 protected bool _active = false;
 public virtual void SetActive(bool active) {
 _active = active;
 }
}
class RunningStateManager : SimpleStateManager, ISimpleStateManager {
 float _direction;
 public void SetDirection(float dir) {
 _direction = dir;
 }
}
class CarryingItemStateManager : SimpleStateManager, ISimpleStateManager {
 Item _carriedItem;
 public void SetCarriedItem(Item item) {
 _carriedItem = item;
 }
}

The solution I've landed on is to cast the interface type to the concrete class type for a given state manager, and then call a method on it directly.

I can think of a few other solutions, but I wanted to know if there's a best-practice approach to this kind of situation.

Peter Csala
10.7k1 gold badge16 silver badges36 bronze badges
asked May 30, 2024 at 17:13
\$\endgroup\$
2
  • 1
    \$\begingroup\$ is it mandatory to have each state in a class ? as I don't see a logic that required to be encapsulated into a separate class. not sure what drove you to use encapsulation while it can be much simpler with one class. \$\endgroup\$ Commented Jun 2, 2024 at 1:45
  • \$\begingroup\$ @iSR5 its not mandatory, but these things tend to get real complex real fast and I've found if I dont do basic encapsulation early on, the code turns messy \$\endgroup\$ Commented Jun 5, 2024 at 14:37

2 Answers 2

5
\$\begingroup\$

If you have to cast to concrete type then that is a code smell. Why have the state managers in a dictionary?

Instead of a dictionary you can create another class to hold the state objects

public class OrganismStates
{
 public SimpleStateManager Idle { get; } = new SimpleStateManager();
 public RunningStateManager Running { get; } = new RunningStateManager();
 public CarryingItemStateManager CarryingItem { get; } = new CarryingItemStateManager();
}

Then remove the dictionary and use the new class.

public class OrganismStateManager
{
 private OrganismStates _states = new OrganismStates();
 
 public void SetIdle()
 {
 _states.Idle.SetActive(true);
 }
 public void SetRunning(float dir)
 {
 var stateManager = _states.Running;
 stateManager.SetActive(true);
 stateManager.SetDirection(dir);
 }
 public void SetCarryingItem(Item item)
 {
 var stateManager = _states.CarryingItem;
 stateManager.SetActive(true);
 stateManager.SetCarriedItem(item);
 }
}

No boxing, no casting. Still have the classes implement the interface so they can be passed generic methods that take the interface when not needing concert methods.

Unit test will be a bit more complex but no more than the current implementation

answered Jun 4, 2024 at 2:57
\$\endgroup\$
5
\$\begingroup\$

With pattern matching you can avoid explicit casting:

class OrganismStateManager : IOrganismStateManager {
 Dictionary<OrganismState, ISimpleStateManager> _states = new() {
 { OrganismState.idle, new SimpleStateManager() },
 { OrganismState.running, new RunningStateManager() },
 { OrganismState.carryingItem, new CarryingItemStateManager() },
 };
 public void SetIdle() 
 => SetState(OrganismState.idle, string.Empty);
 public void SetRunning(float dir) 
 => SetState(OrganismState.running, dir);
 public void SetCarryingItem(Item item) 
 => SetState(OrganismState.carryingItem, item);
 private void SetState(OrganismState organismState, object customState) {
 ISimpleStateManager stateManager = _states[organismState];
 stateManager.SetActive(true);
 Action update = stateManager switch
 {
 RunningStateManager running when customState is float dir => () => running.SetDirection(dir),
 CarryingItemStateManager carrying when customState is Item item => () => carrying.SetCarriedItem(item),
 SimpleStateManager simple => () => {},
 _ => () => { throw new NotSupportedException(); }
 };
 update();
 }
}

Please bear in mind that there are trade-off as well

  • Possible boxing and unboxing of the parameter (for example in case of float dir)
  • Possibility to pass (unintentionally) incompatible customState parameter >> NotSupportedException
  • Possibility to forgot to update SetState after extending the _states collection >> NotSupportedException
answered May 31, 2024 at 8:50
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Nice call! I was twisting my brain trying to use CRTP to solve it. \$\endgroup\$ Commented May 31, 2024 at 13:34
  • 1
    \$\begingroup\$ @JesseC.Slicer Thanks :). My first thought was to use Visitor pattern but it would add some complexity to this otherwise simple use case. \$\endgroup\$ Commented May 31, 2024 at 13:43

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.