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.
-
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\$iSR5– iSR52024年06月02日 01:45:24 +00:00Commented 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\$Abdul Ahmad– Abdul Ahmad2024年06月05日 14:37:56 +00:00Commented Jun 5, 2024 at 14:37
2 Answers 2
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
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
-
1\$\begingroup\$ Nice call! I was twisting my brain trying to use CRTP to solve it. \$\endgroup\$Jesse C. Slicer– Jesse C. Slicer2024年05月31日 13:34:29 +00:00Commented 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\$Peter Csala– Peter Csala2024年05月31日 13:43:21 +00:00Commented May 31, 2024 at 13:43