Loading...

Unity3D: exporting scene platforms to a wireframe image file

In this post we illustrate a C# script which creates a wireframe with all the platforms in the scene, with the correct platforms position and size. The script creates a post script file which can be opened in Acrobat, Preview, or other software. We coded this script to help our digital artists draft background artwork and obstacles in the correct size and proportions.

When we drafted level 1 for Funky Bob, we wanted to provide our artists with a wireframe depicting the platforms location and shape, to help them paint backgrounds and obstacles around it. We thought this could be easily done by taking screenshots of the scene in the Unity editor, and then collate all the images into one single file.

This approach worked OK. However, adding the art to the scene ended up requiring more time than we expected. We found out we had to do a substantial re-processing of the artworks, to apply minor corrections in the platforms position, size, and other calibration with the physics of the jumping characters. Also, we realized we had forgotten some objects from the scene all together. It didn’t help that at that time, we were wearing way to may hats (…well let say all the hats), coding, composing music, calibrating the scene, drawing animations, etc.

For the subsequent levels, we decided to make our life (and our artists’ life) easier by generating the wireframes programmatically in Unity. What we need for our wireframe are the vertices of the BoxCollider.

The script below, search all the objects in the scene with a name containing the string “platform” or “obstacle”. If the object also contains a Boxcollider2D. It calculates the vertices of these rectangles, and write them to a post-script file. Simply add the script to any game object in the scene and run the game in the editor to have the file saved.

Platforms size in screen-space coordinates:

An important note about calculating platforms’ coordinates in the screen-space. Obtaining the X,Y coordinates of the Boxcollider2D vertices turned out to be trickier than we thought. Each sprite object in Unity has several parameters that affect the position and size of the sprite, and the attached Boxcollider2D.

The Transform Position, Rotation and Scale. The BoxCollider Offset and Size.

The Transform coordinates and scales affect the size and position of the Boxcollider in the scene. The BoxCollider is shifted from the center of the Transform by the Offset.

Now, it turns out that the Scale factors of the Transform affect the BoxCollider Offsets. This need to be multiplied by the scale. The Boxcollider Size, on the other end, already accounts for the scale, and do not need to be multiplied by it (at least until Unity 2017.1.1). The correct way to adjust the Boxcollider by the scale is as follow:

float left_edge = obstacle1.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x - obstacleCollider.bounds.size.x / 2;
float right_edge = obstacle1.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x + obstacleCollider.bounds.size.x / 2;
float bottom_edge = obstacle1.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y - obstacleCollider.bounds.size.y / 2;
float top_edge = obstacle1.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y + obstacleCollider.bounds.size.y / 2;

Finally, the script to export the post-script file is as follow. The "multiplier" is adjusted to make sure the wireframe fits into the page. The platforms proportions will be respected and the image should scale with no distortion. We add the code in the start function:

float minX = 1000.0f; // Some very large numbers to include the entire scene
float minY = 1000.0f;
float maxX = -1000.0f;
float maxY = -1000.0f;
string minXobject = "";
string minYobject = "";
string maxXobject = "";
string maxYobject = "";
float multiplier = 3.0f;
									
// Loop through every object in the scene and find the extreme edges. This can be used to fit the wireframe in the page
object[] obj = GameObject.FindObjectsOfType(typeof (GameObject));
foreach (GameObject obstacle1 in obj) {
									
    if ((obstacle1.name.Contains ("obstacle") || obstacle1.name.Contains ("platform")) && obstacle1.GetComponent<BoxCollider2D>() != null) {
	Collider2D obstacleCollider = obstacle1.GetComponent<BoxCollider2D> ();
									
        // NOTE: obstacleCollider.bounds.size already account for the scale. No need to multiply it!
	// obstacleCollider.offset on the other hand does not include the scale and need to be multiplied by the local scale
	float left_edge = obstacle1.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x - obstacleCollider.bounds.size.x / 2;
	float right_edge = obstacle1.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x + obstacleCollider.bounds.size.x / 2;
	float bottom_edge = obstacle1.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y - obstacleCollider.bounds.size.y / 2;
	float top_edge = obstacle1.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y + obstacleCollider.bounds.size.y / 2;
									
	if (left_edge < minX) {
		minX = left_edge;
		minXobject = obstacle1.name;
	}
	if (bottom_edge < minY) {
		minY = bottom_edge;
		minYobject = obstacle1.name;
	}
	if (right_edge > maxX) {
		maxX = right_edge;
		maxXobject = obstacle1.name;
	}
	if (top_edge > maxY) {
		maxY = top_edge;
		maxYobject = obstacle1.name;
	}
   }
}
									
// Loop through every object
object[] obj2 = GameObject.FindObjectsOfType(typeof (GameObject));
foreach (GameObject obstacle in obj2) {
									            
    if ((obstacle.name.Contains ("obstacle") || obstacle.name.Contains ("platform")) && obstacle.GetComponent<BoxCollider2D>() != null) {
									
       string path = "level_2.ps";
									
       Collider2D obstacleCollider = obstacle.GetComponent<BoxCollider2D> ();
									
	float left_edge = multiplier * (Mathf.Abs (minX) + obstacle.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x - obstacleCollider.bounds.size.x / 2);
	float right_edge = multiplier * (Mathf.Abs (minX) + obstacle.transform.position.x + obstacleCollider.offset.x * obstacleCollider.transform.transform.localScale.x + obstacleCollider.bounds.size.x / 2);
	float bottom_edge = multiplier * (Mathf.Abs (minY) + obstacle.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y - obstacleCollider.bounds.size.y / 2);
	float top_edge = multiplier * (Mathf.Abs (minY) + obstacle.transform.position.y + obstacleCollider.offset.y * obstacleCollider.transform.transform.localScale.y + obstacleCollider.bounds.size.y / 2);
									
	// Write the obstacle coordinates to the post script file as rectangles
	StreamWriter writer = new StreamWriter (path, true);
	writer.WriteLine ("newpath");
									
	writer.WriteLine (left_edge + " " + bottom_edge + " moveto");
	writer.WriteLine (left_edge + " " + top_edge + " lineto");
	writer.WriteLine (right_edge + " " + top_edge + " lineto");
	writer.WriteLine (right_edge + " " + bottom_edge + " lineto");
	writer.WriteLine ("closepath");
									
	writer.WriteLine ("gsave");
	writer.WriteLine ("0.5 setgray");
	writer.WriteLine ("fill");
									
	writer.WriteLine ("grestore");
	writer.WriteLine ("0.05 setlinewidth");
	writer.WriteLine ("0.0 setgray");
	writer.WriteLine ("stroke");
									
	writer.Close ();
   }
}