I'm developing a small C# Unity application to display stereoscopic images as part of a wider research study.
Since the experiment consists of unique stages (Start, during a trial, between trials, and End), I decided to use the State Design Pattern to make a State Machine for code readability.
The program design is based on a 'Model', with the model having 'Metadata' (about the participant) and a Queue of 'Trials', with each trial having several data fields.
/Experiment/Model.cs
namespace Experiment
{
public struct Metadata
{
public string Id;
public string Age;
public string OriginalImageDirectory;
public override string ToString() => $"# {Id}\n# {Age}\n# {OriginalImageDirectory}";
}
public struct Trial
{
public string ImageName;
public IReadOnlyList<Stereoscopic3DImage> ImageAssets;
public Point Origin;
public bool Flickers;
}
public struct Result
{
public string ImageName;
public Point Origin;
public bool CorrectResponse;
public override string ToString() => $"{ImageName}, {Origin.X}, {Origin.Y}, {CorrectResponse.ToString()}";
}
public class Model
{
public readonly Metadata Metadata;
public readonly Queue<Trial> Trials;
public Model(IReadOnlyList<string> configurationFileRows)
{
// ...
Trials = new Queue<Trial>(Utils.ParseTrialsFrom(Metadata, configurationFileRows.Skip(3)));
}
}
}
/Experiment/StateManager.cs
namespace Experiment
{
public class StateContext
{
public interface IState
{
void OnStateEnter();
void OnStateContinue(StateContext context);
void OnStateExit();
}
private IState currentState;
public IState CurrentState
{
get => currentState;
set
{
if (currentState != null)
{
currentState.OnStateExit();
}
currentState = value;
currentState.OnStateEnter();
}
}
public void Update() => CurrentState.OnStateContinue(this);
}
}
/Experiment/TrialState.cs
(For the sake of brevity, I'll omit the other three *State.cs
files)
namespace Experiment
{
class TrialState : StateContext.IState
{
private float flickerTimer;
private float trialTimer;
private readonly Model model;
private Trial trial;
private readonly VR3DMediaViewer viewer;
private readonly GameObject textField;
private int index;
public TrialState(Model model, GameObject viewer, GameObject textField)
{
this.model = model;
this.viewer = viewer.GetComponent<VR3DMediaViewer>();
this.textField = textField;
}
public void OnStateEnter()
{
viewer.gameObject.SetActive(true);
trial = model.Trials.Dequeue();
viewer.SetNewImage(trial.ImageAssets[index]);
flickerTimer = 0.0f;
trialTimer = 0.0f;
}
public void OnStateContinue(StateContext context)
{
Contract.Requires(viewer.gameObject.activeSelf);
// ...
if (affirmativePressed || rejectivePressed)
{
// ...
if (model.Trials.Count == 0)
{
context.CurrentState = new EndState();
}
else
{
OnStateEnter();
}
return;
}
if (trialTimer >= Controller.TrialDuration)
{
context.CurrentState = new BetweenTrialsState(model, trial, viewer.gameObject, textField);
return;
}
// ...
}
public void OnStateExit()
{
viewer.gameObject.SetActive(false);
}
}
}
/Controller.cs
public class Controller : MonoBehaviour
{
public static StreamWriter StreamWriter;
private StateContext stateContext;
// MARK: GameObjects
public GameObject textField;
public GameObject viewer;
// Start is called before the first frame update
void Start()
{
// ... (initialization)
stateContext.CurrentState = new StartState(model, textField, viewer);
}
// Update is called once per frame
void Update()
{
stateContext.Update();
}
[ContractInvariantMethod]
protected void ObjectInvariant()
{
Contract.Invariant(textField.activeSelf != viewer.activeSelf);
}
}
Any feedback on the software design of my program and/or my implementation of the State Pattern would be appreciated, thanks.
{ get; set; }
accessors inModel.cs
andController.cs
: public fields aren't commonly used, public properties are. If you want your fields to appear in the inspector, useprivate
and[SerializeField]
attribute. Nesting of the interfaceIState
seems a bit strange to me as well: MSDN documentation asvices against it. Apart from that, everything seems fine. \$\endgroup\$