- Home /
Programmatically playing a scene for a single frame
Update:
I gave up and changed tack; I now run our .cs files through the mono compiler as outlined in this post (thanks to Lucas for that). This allows us to use NUnit as with normal .NET development. The only difference is that we can't test classes that derive from MonoBehaviour, but that's something we can live with.
Original Post:
Context: I'm trying to create a "run tests" menu item which does the following:
- Creates a new scene
- Creates a new game object & test component
- Run a single frame using the new scene (which will run the tests in the script's Start method via SharpUnit)
- After the frame has completed, discard the generated scene and reload the user's original scene.
Here's a quick stab at the code:
public static class RunUnitTestsMenuItem { [MenuItem("Testing/Run Tests")] public static void RunUnitTests() { //string previousScene = EditorApplication.currentScene; EditorApplication.isPlaying = true; EditorApplication.NewScene(); var gameObject = new GameObject("UnitTestObject"); gameObject.AddComponent<Unity3D_TestRunner>();
// If I leave the script as is, it will run the tests as expected,
// but the editor will be playing until the user clicks stop
// plus it will require the user to manually discard the test
// scene and then reload their original scene ... too much friction.
//EditorApplication.OpenScene(previousScene);
}
}
If I attempt to reload the original scene after the test run (see commented out code), my script won't report any test results -- the script's Start method won't get called (verified via adding a Debug.Log
line -- nothing is printed). If I remove the scene reloading code, the tests are run correctly and I get the result output in the console, but it means the user has to re-select their original scene and choose not to save the temporary test scene. I'm used to running tests via ReSharper & NUnit, so I'd like to minimise as much friction as possible.
Does anyone have any ideas about how to make the editor ensure that a single frame is completed in its entirety prior to reloading the original scene? I've tried setting EditorApplication.isPaused
to true and then using EditorApplication.Step()
, but that approach had the same problem.
Any help gratefully appreciated, cheers!
Answer by Tom 17 · May 25, 2011 at 09:10 AM
Full batch-mode-safe solution to be found here: Answer to Continuous Integration with Unity?
EDIT2: I'm afraid it is next to impossible to give a full solution to the problem. I do manage to get a signal when one frame was processed, but I can neither block on the main thread (in Update() or OnGUI() of the editor window script) nor can I access EditorApplication from a seperate thread.
EDIT: The following works OK, but I get three Update() runs reported befor the game is really stopped
On a sidenode, I use SharpUnit as testframework, it works with MonoBehaviour-derived classes.
This is the code for the signalling script using UnityEngine; using System.Collections; using System.Threading;
public class OneFrameSignaller : MonoBehaviour { public static volatile bool startGame = false; public static volatile bool stopGame = false;
void Start()
{
StartCoroutine(SignalOneFrame());
}
IEnumerator SignalOneFrame()
{
yield return new WaitForEndOfFrame();
stopGame = true;
}
}
This is the code of the editor script I use for triggering the magic and doing the tests.
using UnityEditor; using UnityEngine;
public class InEditorTestsWindow : EditorWindow { [MenuItem("Tests/run tests")] public static void ShowWindow () { EditorWindow.GetWindow (typeof(InEditorTestsWindow)); }
public void OnGUI ()
{
if (GUILayout.Button ("play and step once"))
{
OneFrameSignaller.startGame = true;
}
}
void Update()
{
if (OneFrameSignaller.startGame)
{
OneFrameSignaller.startGame = false;
EditorApplication.NewScene();
GameObject testObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
UnityCSharpScript testScript = testObject.AddComponent<UnityCSharpScript>();
OneFrameSignaller signaller = testObject.AddComponent<OneFrameSignaller>();
EditorApplication.isPlaying = true;
}
if (OneFrameSignaller.stopGame)
{
OneFrameSignaller.stopGame = false;
EditorApplication.isPaused = true;
// do tests/assertions here with the testScript
EditorApplication.isPlaying = false;
EditorApplication.isPaused = false;
}
}
}
Original post:
This is barely an answer as I did not make it work, yet. However this might help to further the effort: I assume the key to allowing exactly n frames to run is to use WaitForEndOfFrame in a coroutine. This only works for MonoBehaviour-derived classes and not within an editor script. Thus what I do is to have one special script that I add to the scene. At the start it fires up a coroutine that does wait for the end of the frame. When this has happened the waiting thread that ran the editor script gets signalled about the end of the frame and it can thus stop the game. The problem is to get the signalling right. I tried to share an AutoResetEvent, but didn't get that to work (yet). I also tried to start a thread within the editor script, but I can't access EditorApplication from that thread. The research goes on.
For some reason I don't get the formatting right for the first code snippet, sorry for that.
Answer by Tom 17 · Jun 03, 2011 at 08:26 AM
I write this answer as an update because I can give you a quite short and simple solution now.
With all my experiementing on using the play mode in batch mode I made the following observation:
starting the play mode resets most variables, therefor you need to serialize the information you need to controll the test simulation and deserialize it after the play mode actuall startet
EditorApplication.update delegate is called after each frame during batch mode, you just need to set it after the play mode actually startet.
I have a test-governing script, which I add to each test scene, that does just that in its Start method.
The only drawback is that you can don't get a delegate invocation on the very first frame. I'll assume you write your games in a way that involves more than one frame, so maybe you can live with it.