I made a small game that does the following:
- Displays a popup with the instructions of the game
- Turns on the GUI
- Start a list of trials for the player
- For each trial:
- It shows a symbol
- Waits for a set amount of time
- Show another symbol
- Waits for the user to give an answer or for a timer to expire
- Indicates the feedback for the answer for a set amount of time
- Waits for a while before beginning the next trial
- Turns off the GUI
I am going to show some Unity3d's C# code. If you don't know Unity3D's API, please stick with me, I will make it as generic as possible, and I will explain Unity3D's specificities.
The following was my original code:
public class Game: Component {
...
public IEnumerator<float> PlayGame(
IGameDefinition gameDefinition,
ICourutineManager cm
)
{
CountDown answerCountdown = new CountDown(gameDefinition.answerTimemout);
Trial[] trials = GenerateTrials(gameDefinition);
PlayerAnswer playerAnswer;
yield return cm.WaitForCoroutine(_OpenInstructionsAndWait());
TurnOnGUI();
yield return cm.WaitForSeconds(gameDefinition.waitBeforeBegin);
foreach (Trial trial in trials)
{
FirstCue firstCue = GenerateFirstCue(trial, gameDefinition);
ShowCue(firstCue);
yield return cm.WaitForSeconds(gameDefinition.waitToShutOfCue1);
HideCue(firstCue);
SecondCue secondCue = GenerateSecondCue(trial, gameDefinition);
ShowCue(secondCue);
ActivateInput(ref playerAnswer);
playerAnswer = PlayerAnswer.Unanswered;
answerCountdown.Start();
yield return cm.WaitWhile(() =>
playerAnswer == PlayerAnswer.Unanswered
&&
!answerCountdown.HasFinished()
);
Result result;
if(answerCountdown.HasFinished()) {
result = Result.Late;
} else {
result = CheckAnswer(playerAnswer);
}
HideCue(secondCue);
DisplayFeedback(result);
yield return cm.WaitForSeconds(gameDefinition.interTrialWait);
}
TurnOffGUI();
}
...
}
The ICourutineManager
is meant to decouple with Unity3d Coroutine's engine. What it does is to defer the execution of the current procedure until a certain condition is meet.
For example cm.WaitForSeconds(float seconds)
will defer the execution for seconds
seconds. cm.WaitForCoroutine
will defer the execution until another "Coroutine" finishes, this Coroutine can contain several yield statements. Coroutines are procedures that return an IEnumerator
.
This code was rather long, in my opinion, ill-suited for Unit testing. Most of the functions that it calls are private or protected, so I could not test them. Most of the state is local because I wanted to make pure functions whenever it was possible, so I could not track the state of the coroutine after each yield.
After a lot of research, I decided to move all those functions and local state to a separate class. Now all those functions and glue logic is grouped in public procedures that I can test and the local state variables (like firstCue
and playerAnswer
) are members of the new class, some of them are public.
But I am very disappointed with the result, before, it was a very readable code, but now, it is hard to read, no other class will use the new procedures, and I have a lot of side effects on methods that will run in a particular sequence. Not to mention that I don't think that I have made any progress to make this code more SOLID.
My new code looks like this:
public class Game: Component {
...
public IEnumerator<float> PlayGame(
IGameDefinition gameDefinition,
IGameController gameController,
ICourutineManager cm
)
{
gameController.InitializeGame(gameDefinition);
yield return cm.WaitForCoroutine(_OpenInstructionsAndWait());
gameController.TurnOnGUI();
yield return cm.WaitForSeconds(gameDefinition.waitBeforeBegin);
foreach (Trial trial in gameController.trials)
{
gameController.ShowFirstCue(trial);
yield return cm.WaitForSeconds(gameDefinition.waitToShutOfCue1);
gameController.ShowSecondCue(trial);
gameController.ActivateUserInput();
yield return cm.WaitWhile(() =>
!gameController.HasPlayerAnswer()
&&
!gameController.AnswerCountdownHasFinished()
);
gameController.CheckAnswer(trial);
gameController.ShowFeedback();
gameController.DeactivateUserInput();
yield return cm.WaitForSeconds(gameDefinition.interTrialWait);
}
gameController.FinishGame();
}
...
}
I made a really bad job making this code testable. Can you help me to make it more SOLID, readable and testable?
gameController
, and now I can unit test it. I am ok with only doing interaction tests forGame: Component
. Now my problem is that the code ingameController
is really hard to read, it reminds me of the code of the first Graphics drivers. \$\endgroup\$