In J.UM.P, I set up levels in different 'world' or theme

What this means, is that I have a bunch of Materials, colors, music to apply to various elements in a level.

While I use prefabs all over the place, it is a tedious task to go each of the prefab instances to change the 'default' material. Too bad Unity3D don't do prefab in prefab, as it should have solve the problem in a quite elegant and maintanable manner.

Unfortunatly, I have to run my own solution to circumvent the problem.

First I have a 'themeable' prefab, which only resides in the asset database, that contains all materials of a given theme, as well as other variable like fog color and music etc..

Here is a quick preview of my Themeable class :
 public class Themeable : MonoBehaviour { 
public AudioClip levelMusic;
public Material floor;
public Material wall;
public Material line;
public Material ceiling;
public Material mediumBump;
public Material highBump;
public Material normal;
public Color32 fog;

public Material[] materials
{
get {
List< Material > mats = new List< Material >();
if( floor != null && !mats.Contains( floor ) )
mats.Add ( floor );
if( wall != null && !mats.Contains( wall ) )
mats.Add ( wall );
if( line != null && !mats.Contains( line ) )
mats.Add ( line );
if( ceiling != null && !mats.Contains( ceiling ) )
mats.Add ( ceiling );
if( mediumBump != null && !mats.Contains( mediumBump ) )
mats.Add ( mediumBump );
if( highBump != null && !mats.Contains( highBump ) )
mats.Add ( highBump );
if( normal != null && !mats.Contains( normal ) )
mats.Add ( normal );
return mats.ToArray();
}
}
}
The materials property is just here to get all materials of the theme in one array

Given that prefab, I need to apply the various theme materials to the current level. As I have no way of knowing that a given object is the scene is 'themeable', I have to resort on a semi automatic solution and iterate some way over all materials in the scene and allow user to change it to one of the Themeable object.

In order to change materials, one can create an Editor Window that list all materials in the scene, and provide a way to assign a choosen material to another one, effectivly replacing the old material by the new

Creating a custom Editor is quite simple and straight forward, you need to derive your class from UnityEditor.EditorWindow and place the file in the 'Editor' folder in the assets database
 using UnityEngine; 
using UnityEditor;
using System.Collections;
using System.Collections.Generic;

public class MyTool : EditorWindow
{
[MenuItem("Window/My tool")]
static void Init()
{
MyTool win = (MyTool)EditorWindow.GetWindow (typeof (MyTool));
win.Show();
}
void OnGUI()
{
}
}
With that setup, you just have a small empty window, when you go under 'Window' and select "My tool" ( this is wired by using the [MenuItem("Window/My tool")] attribute above the Init function

Time to add some functionnalities. 
All the gui stuff will happen as always with Unity3D in the OnGui function of the class.

So first, we need to know what Themeable to use. For that we can use an ObjectField. You can use it either from EditorGUILayout or EditorGUI depending if you want automatic layout or if you want to deal with the layout yourself. The call will return the object dropped, so better store it somewhere for further process.

To add a little prefix in front of it, we can use PrefixLabel to just do that.
As I'm using EditorGUILayout , I will enclose those 2 calls between BeginHorizontal/EndHorizontal for the automatic layout to know I want them side by side
   public class MyTool : EditorWindow 
{
Themeable theme;

....

void OnGUI()
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel( "Theme" );
theme = EditorGUILayout.ObjectField( theme, typeof( Themeable ), true ) as Themeable;
EditorGUILayout.EndHorizontal();
}
}
We can check if we have a dropped object by checking the theme variable. As soon as we have one, we will start listing all materials from the scene.

To collect all materials, we will have to find all Renderable objects, iterate over them and store the sharedMaterials used in them
  void OnGUI() 
{
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel( "Theme" );
theme = EditorGUILayout.ObjectField( theme, typeof( Themeable ), true ) as Themeable;
EditorGUILayout.EndHorizontal();

// make sure we have a theme dropped
if( theme != null )
{
// list of all materials
List< Material > mats = new List();
// get all MeshRenderer
MeshRenderer[] all = FindObjectsOfType< MeshRenderer >();
// iterate over all
for( int i = 0 ; i < all.Length; ++i )
{
// iterate over all sharedMaterials
for( int m = 0; m < all[i].sharedMaterials.Length; ++m )
{
// make sure we have an actual material,
// and we don't have it already in the list
if( all[i].sharedMaterials[m] != null
&& !mats.Contains( all[i].sharedMaterials[m] ) )
{
mats.Add( all[i].sharedMaterials[m] );
}
}
}
}
}

What we want to display is the list of above materials, with a combo from where the user will be able to choose a material from the Themeable object. We just have to store the name of the material. That is why I have a materials property in the Themeable object
 void OnGUI() 
{

......

// make sure we have a theme dropped
if( theme != null )
{

......

// list of name of Materials from the Theme
List< string > matNames = new List< string >();
for( int i = 0 ; i < theme.materials.Length; ++i )
{
matNames.Add ( theme.materials[i].name );
}
}
}
Unfortunalty there is no 'ComboBox' like widget in the EditorGUI, but we can use the Popup widget that will do the job perfectly well
The Popup function can take an array of string and will return you the selected item or -1 if none selected
 void OnGUI() 
{

......

// make sure we have a theme dropped
if( theme != null )
{

......

// display the materials
foreach( Material mat in mats )
{
// we will have the name of the scene material and a popup with materials names from the theme
// side by sie
// one material on each line
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel( mat.name );
int choosen = EditorGUILayout.Popup(-1, matNames.ToArray() );
EditorGUILayout.EndHorizontal();
}
}
}
Once we have a material choosen ( so that choosen will not be -1 ), we can replace all occurence of the current material in the scene by the one from the Themeable. which will be theme.materials[choosen]
 void OnGUI() 
{

......

// make sure we have a theme dropped
if( theme != null )
{

......

// display the materials
foreach( Material mat in mats )
{

.....
int choosen = EditorGUILayout.Popup(-1, matNames.ToArray() );
// a material from the theme has been chosen
if( choosen != -1 )
{
Material replace = theme.materials[ choosen ];
// replace in all renderer that use that material
for( int i = 0 ; i < all.Length; ++i )
{
// need to use a temporary material list as
// MeshRenderer.sharedMaterials item can not be modified
// we need to modified the whole list
List< Material > newMats = new List();
for( int sharedIdx = 0; sharedIdx < all[i].sharedMaterials.Length; ++sharedIdx )
{
if( all[i].sharedMaterials[sharedIdx] == mat )
{
newMats.Add( replace );
}
else
{
newMats.Add ( all[i].sharedMaterials[sharedIdx] );
}
}
all[i].sharedMaterials = newMats.ToArray();
}
}
EditorGUILayout.EndHorizontal();
}
}
}
Everything is now in place and we can easily change a material all over the scene by one click.
Window editor
The Custom themable window and the World Theme in inspector. Changing one material of the scene to one of the selected theme is just a matter of selecting material from a popup
Photo
The original level, within the 'World01' theme
Photo
10 seconds later, it is setup with the bonus theme
Photo
Another option, that will take less space in the editor window, would be to replace the list of all scene materials by using a Popup like the one used in for the Theme materials (as in the picture ).


While it seems a viable and better option, I tend to prefer seeing all my materials and just have to do a one click operation.


Your opinion may vary, and in the end, it is up to you to choose the correct way to display and provide info for the end user to be the most productive.


Here is the code for the above Popup display :

 void OnGUI() 
{

......

// make sure we have a theme dropped
if( theme != null )
{

......

// display the materials
EditorGUILayout.BeginHorizontal();
matFromScene = EditorGUILayout.Popup( Mathf.Min ( matFromScene, matSceneNames.Count-1), matSceneNames.ToArray() );
int choosen = EditorGUILayout.Popup(-1, matNames.ToArray() );
if( choosen != -1 )
{
Material original = mats[ matFromScene ];
Material replace = theme.materials[ choosen ];
for( int i = 0 ; i < all.Length; ++i )
{
List< Material > newMats = new List();
for( int ma = 0; ma < all[i].sharedMaterials.Length; ++ma )
{
if( all[i].sharedMaterials[ma] == original )
{
newMats.Add( replace );
}
else
{
newMats.Add ( all[i].sharedMaterials[ma] );
}
}
all[i].sharedMaterials = newMats.ToArray();
}
}
EditorGUILayout.EndHorizontal();
}
}
You can use the code, modify it as you please

Thanks for reading this far



Leave a Reply.