1
\$\begingroup\$

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.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I'd suggest to add { get; set; } accessors in Model.cs and Controller.cs: public fields aren't commonly used, public properties are. If you want your fields to appear in the inspector, use private and [SerializeField] attribute. Nesting of the interface IState seems a bit strange to me as well: MSDN documentation asvices against it. Apart from that, everything seems fine. \$\endgroup\$
    – Xamtos
    Feb 21, 2020 at 8:34

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.