One of the problems I encountered with Mind Hero towards the end was the sudden rush of assets and what needed to be done with them to get them all working. More specifically- the character models. For something so core to the game, they brought along problems with them as well as bringing more of a technical challenge on how to assemble them.
One of the problems that came around having implemented them was the realization that the scenes already created weren't using prefabs for the characters. For those unfamiliar with Unity, they may not necessarily seem related but them having to go back and replace every character whom the player interacts with is a bit of a nuisance. Each of them are set up with their own unique grid of blocks to put on the bar as well as messages and placement. Having to redo them for as many possible followers as there are per level is a bit crappy, but it isn't really technical issue so I digress.
We wanted to go about handling the follower and player characters using the least amount of assets but still making them look different. To do this, we created 2 character models per level setting (Egyptian, Western, Village, and Medieval) and gave different sections of them a separate material ID. It's the same sort of thing that you're able to do in something like an RPG where you want to reuse assets but give them a different color/effect.
I went about doing this in a very simple way as to give the artists/designers choice or what colours should and would be selected but maintain what colour ends up being selected randomly through code. I created a small script to run at the start of the scene to change the colours. It doesn't matter where I put it really as long as it's in the same prefab as the skinned mesh renderer (SMR) of the character I'm trying to change (this is necessary as the two objects need to remain together to function- breaking them up will have to reference the SMR everytime you place either)
using UnityEngine;
public class CharacterMaterialRandomizer : MonoBehaviour
{
public SkinnedMeshRenderer meshRenderer;
public ColorChoices[] choices = new ColorChoices[3];
void Awake ()
{
for (var i = 0; i < meshRenderer.sharedMaterials.Length; i++)
{
if (i >= choices.Length)
break;
meshRenderer.materials[i].color =
choices[i].colors[Random.Range( 0, choices[i].colors.Length )];
}
}
}
[System.Serializable]
public class ColorChoices
{
public Color[] colors = new[]
{
Color.white
};
}
As you can see, the MonoBehaviour is very simple in what it does. When the scene is first started, it checks how many materials the model needs and then sets the color. The 'if' statement in the middle isn't actually needed really, it's just in case something goes wrong if (when) I edit something later. The key part of this is the <ColorChoices> class I've made underneath. As it's not a behaviour or anything directly tied to a GameObject, I have to manually give it an attribute to make the engine be able to serialize it so that the <CharacterMaterialRandomizer> is able to hold all the data when the game is built.So at this point, you may be asking "Hey Ben, yo' crazy. Why did this cause you trouble?", and I'll tell you 'homes'. The Inspector window where the artist can put all his pretty colours is looks rubbish to work with:
It'll look something like that anyway. Having to go through all those drop through menus to get the colour and seeing all that useless stuff looks bad and there's a few things we can do to clean it up. The only thing anyone else really needs are the colour choices so by creating a custom editor for this type of monobehaviour we can just skip everything else being shown.
As the amount of materials changes from model to model, there isn't really a way of me being able to say on the editor which colour choice affects which part of the model so I just had to use their index and have the artist create a palette for each choice to make it somewhat clear what each does.
To kick things off on what each section does, I'll show the code in pieces.
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(CharacterMaterialRandomizer))]
public class CharacterMaterialEditor : Editor
{
private SerializedProperty _materialsColors;
private SkinnedMeshRenderer _meshRenderer;
void OnEnable()
{
// I need to get how many materials the SMR uses.
_meshRenderer = ( serializedObject.targetObject as CharacterMaterialRandomizer ).meshRenderer;
_materialsColors = serializedObject.FindProperty( "choices" );
}
}
public override void OnInspectorGUI()
{
EditorGUILayout.LabelField("Materials Count: " + _meshRenderer.sharedMaterials.Length);
for (var i = 0; i < _meshRenderer.sharedMaterials.Length; i++)
{
// Missing this out for now.
}
}
This one one of the first problems I ran into. One of the first things I did in this code and it had already somewhat broke although it wasn't clear immediately. Originally I was using the SMR's materials member like:EditorGUILayout.LabelField("Materials Count: " + _meshRenderer.materials.Length);
It worked the whole time I was creating the editor so I didn't notice a problem until I'd finished. What I'd forgotten was that simply accessing the materials causes Unity to make a new instance of it. I only realised I had a problem sometime later when Unity was becoming slow. I checked my task manager and Unity was using around ~1.3GB of memory which is a bit higher than it's normal ~200MB and then saw my leak when I noticed all the models having their materials named similar to "<MaterialName> (Instance)(Instance)(Instance)(Instance)(Instance)(Instance)(Instance)(Instance)(Instance)(Instance)(Instance)..." etc. Changing 'materials' to 'sharedMaterials' fixed this no problem. I wasn't trying to make any changes to the materials themselves through the editor so I didn't need to call it anyway. It was just an oversight.
(I'll skip some checks I do in the code and just talk about the useful parts.) To do the main part of the code required me to do some unpretty things with the code. To access the colours in '_choices' I had to use
var colors = _materialsColors.GetArrayElementAtIndex(i).FindPropertyRelative( "colors" );I have to do this for every material that the object can use (which is the unpretty part; it reaches 102 characters from the side damn it). This gets me the data from 'choices' in the CharacterMaterialRandomizer as an array which I can use. If I don't do this, it gives me a SerializedProperty type 'General'/'General Mono' which is useless. There isn't really another way to get all the data (as far as I know) but it's fine after that as I can then just use 'colors' as much as I need.
The next thing I do is create some easy to use buttons and a title:
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Material: " + (i + 1), EditorStyles.miniBoldLabel);
if (GUILayout.Button("Add", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
{
colors.InsertArrayElementAtIndex(colors.arraySize);
}
if (GUILayout.Button("Remove", EditorStyles.miniButtonRight, GUILayout.Width(50)))
{
if (colors.arraySize > 1)
colors.DeleteArrayElementAtIndex(colors.arraySize - 1);
}
EditorGUILayout.EndHorizontal();
I add the title and buttons to a horizontal 'group' so that I can use them as a separator from any other material colours above and below, and so that it takes up as little space as possible. I do this using the EditorGUILayout.BeginHorizontal() and EditorGUILayout.EndHorizontal(). I do the buttons before I change the colours just so that the array is up to date if any of the colour values get changed. I also add a quick check on the arraysize in remove so that the possible colours for that specific material can never go below 1.
In the last bit of the 'for' loop, I can finally get my colours and show them on screen.
for (var j = 0; j < colors.arraySize; j++)
{
var color = colors.GetArrayElementAtIndex( j );
color.colorValue = EditorGUILayout.ColorField( "", color.colorValue );
}
To finish it off, outside of the loop I call 'serializedObject.ApplyModifiedProperties();' and I can end the function. Full code for the inspector:
public override void OnInspectorGUI()
{
EditorGUILayout.LabelField("Materials Count: " + _meshRenderer.sharedMaterials.Length);
for (var i = 0; i < _meshRenderer.sharedMaterials.Length; i++)
{
if (_materialsColors.arraySize < _meshRenderer.sharedMaterials.Length)
{
_materialsColors.InsertArrayElementAtIndex(_materialsColors.arraySize);
}
var colors = _materialsColors.GetArrayElementAtIndex(i).FindPropertyRelative( "colors" );
EditorGUILayout.BeginHorizontal();
EditorGUILayout.LabelField("Material: " + (i + 1), EditorStyles.miniBoldLabel);
if (GUILayout.Button("Add", EditorStyles.miniButtonLeft, GUILayout.Width(50)))
{
colors.InsertArrayElementAtIndex(colors.arraySize);
}
if (GUILayout.Button("Remove", EditorStyles.miniButtonRight, GUILayout.Width(50)))
{
if (colors.arraySize > 1)
colors.DeleteArrayElementAtIndex(colors.arraySize - 1);
}
EditorGUILayout.EndHorizontal();
for (var j = 0; j < colors.arraySize; j++)
{
var color = colors.GetArrayElementAtIndex( j );
color.colorValue = EditorGUILayout.ColorField( "", color.colorValue );
}
}
serializedObject.ApplyModifiedProperties();
}
This gives me the much prettier


No comments:
Post a Comment