Is instancing materials via a script in Unity Edit Mode bad practice?

This question pertains to working in the Unity editor.

I’m working on a game where I want about 500 light-up picture icons. During gameplay, these icons can switch from being lit up or dark (“on” / “off”) by changing a boolean.

I have a script that I manually run from the editor (in Edit Mode) that programatically instantitates the GameObjects for each icon, sets their material, then switches that materials light-up effect to “off”.

The editor throws an error message claiming that I am leaking materials into the scene by editing them during Edit Mode. I don’t think their advice to use the SharedMaterial property is good advice, because before the game has started playing, I want a mix of “on” and “off” lights.

I have considered the following:

  • Am I using materials in a way unintended? Even though it appears to work, does Unity not want materials being instanced and changing? Found this post before posting, where the answer is that Unity should have made it a warning instead of an exception / error. Link
  • Am I misunderstanding SharedMaterials? I’m pretty sure unless I want all similar lights with the same picture to light up in sync, then instancing is the way to go.
  • Is Unity just trying to warn novices that may be writing scripts and intending to change Assets and not change instances? I can find dozens of posts of people complaining about the error message but their use-cases are all slightly different to mine.
  • Should I make separate “on” and “off” materials and switch between them instead of using properties of the material? (Very annoying, did this for anothe older project because I was unable to determine the best way to work there too.) Found similar advice as answer here: Link
  • I’m using URP (Universal Render Pipeline) and taking advantage of some batching process I just read about that says specifically that changing material properties directly instead of using MaterialPropertyBlocks is the intended way to work.
  • All my materials are custom made Shader Graph things that are compatible with the batching described above.

My game doesn’t run slowly, but for some reason the instantiating process runs somewhat slowly (maybe 10 minutes to instantiate 500 icons, which is unacceptable) but I’m still unpacking / optimising; and not sure if the material instantiation is to blame.

My code:

    private void ChangeMaterialProperty(bool enabled)
    {
        Material[] materials = iconMeshRenderer.materials;
        for (int i = 0; i < materials.Length; i++)
        {
            if (enabled)
                materials[i].SetFloat(_Unpowered, 0);
            else
                materials[i].SetFloat(_Unpowered, 1);
        }
        iconMeshRenderer.materials = materials;
    }

The fact that the editor specifically throws a warning for it should hint – it is bad practice!

You are creating instances of materials in edit mode, but those materials are nowhere actually serialized and stored as assets, they only exist c#-wise. The editor complains that these are leaking because Material instances usually only get deleted if you actively Destroy them or unload the scene/object instances they belong to.

Even worse: As those material instances only exist “virtually” c#-wise but not as actual assets, the next time you open the scene/project those material instances will be gone!

Should I make separate “on” and “off” materials and switch between them instead of using properties of the material? (Very annoying, did this for anothe older project because I was unable to determine the best way to work there too.)

Yes! If you only need two instances of that material anyway then simply create the two and assign them to sharedMaterials.

This will be

  • faster than going through shader specific calls like SetFloat
  • saver and more flexible than going through SetFloat which requires the “hardcoded” property name
  • saving draw calls as multiple objects using the same material instance can be batched into a single draw call

You could simply provide them both

public Material onMaterial;
public Material offMaterial;

private void ChangeMaterialProperty(bool enabled)
{
    var materials = iconMeshRenderer.sharedMaterials;
    for(var i = 0; i < materials.Length; i++)
    {
        materials[i] = enabled ? onMaterial : offMaterial;
    }
    iconMeshRenderer.materials = materials;
}

Thanks to derHugo’s advice, I have written the following solution for my use case. What it does, is programatically create and store variations of my materials as Assets.

The reason to do this programatically instead of manually is due to the volume of different materials of my scenes needing to switch between “on” and “off”. This happy compromise seems performant; and logistically lets me maintain masters of my materials that can have variants generated as needed; instead of trying to manually maintain each version (and fearing them become out-of-sync).

The materials in my case, are stored within a ScriptableObject referenced throughout the GameObjects in my scene. As needed, they can look up materials from this resource using Linq and switch material.

Relatedly:

  1. For flashing effects I’d have better performance switching to a flash material instead of changing properties on the material at runtime; and similarly should avoid editing materials.
  2. For fade-outs or other gradual effects achieved via shader maniuplation; seems like making a shader somehow fade-out using HLSL based on some attribute of the object instead of by changing some custom float on the shader from 1 to 0 is more performant as no instancing would have to be involved; at a complexity cost.
    public void SetupDisabledMaterials()
    {
        int _Unpowered = Shader.PropertyToID(nameof(_Unpowered));
        foreach (Properties property in propertiesList)
        {
            if (property.iconMaterialDisabled == null)
            {
                Material disabledMaterial = new Material(property.iconMaterial);
                disabledMaterial.SetFloat(_Unpowered, 1);
                AssetDatabase.CreateAsset(disabledMaterial, generatedMaterialsPath + disabledMaterial.name + "Disabled.mat");
                property.iconMaterialDisabled = disabledMaterial;
            }
        }
    }

Leave a Comment