How can I programatically set readonly properties of 2D Lights?

Hi all,

The game I’m working on has a 2D environments with 3D character models. To manage both lighting sets, I’m trying to setup a CombinedLight class, that syncs both a Light2D and a Light (3D one) to match one another in color, intensity, shape, etc. So far the version I have seems to be working fairly well; however, there’s a few quality settings I want to always have set on my 2D lights and am hitting a wall.

The following fields of Light2D are apparently marked as read-only. The Light2D component is able to configure these in the Inspector, but I cannot seem to influence this from my CombinedLight class:

  • useNormalMaps
  • pointLightQualiy
  • pointLightDistance
  • falloffIntensity

I want to be able to create a fresh GameObject, add a CombinedLight class, and have the default Light2D have these fields configured. I don’t necessarily need to be able to change them at runtime if that is not possible. I can workaround this by using Prefabs, but ideally I want this to have the same level of configuration as a typical 2D light has. CombinedLight has a custom Editor, can I possibly leverage that to update these fields?

EDIT: I’ve also attempted to do this via reflection, but GetProperty returns an object without a Set method defined.

var flags = BindingFlags.Instance | BindingFlags.Public;
var normalMapField = typeof(Light2D).GetProperty("useNormalMap", flags);
normalMapField.SetValue(light2D, true);

EDIT2: SOLVED! After playing around with reflection more, I found that the readonly properties have internal fields that match with them (all of which appears to have a similar m_ prefix to them). So to change these fields - for example with normal maps, I just had to do:

var flags = BindingFlags.Instance | BindingFlags.NonPublic;
var normalMapField = typeof(Light2D).GetField("m_UseNormalMap", flags);
normalMapField.SetValue(light2D, true);

The forms weren’t letting me add this to the original post so just going to post my code as a new comment.

[RequireComponent(typeof(Light2D))]
[RequireComponent(typeof(Light))]
public class CombinedLight : MonoBehaviour
{
    public enum LightType
    {

        POINT, SPOT, GLOBAL
    }

    public LightType type = LightType.POINT;
    public Color color = Color.white;
    public float intensity = 1;
    public bool syncInEditor = true;

    public float radius = 1;
    public float opacity = 0;
    public bool useNormalMaps = true;

    public float innerAngle, outerAngle = 180;

    private Light2D light2D;
    private Light light3D;

    void Start()
    {
        CheckForLights();
        InstantiateLights();
        InitialLightConfiguration();
        SyncLights();
    }

    void Update()
    {
        SyncLights();
    }

    void OnDestroy()
    {
        if (this == currentGlobalLight)
            currentGlobalLight = null;
        Destroy(this.gameObject);
    }

    public void SetupAndSyncExistingLights()
    {
        CheckForLights();
        InitialLightConfiguration();
        SyncLights();
    }

    public void CheckForLights()
    {
        if (light2D == null)
            light2D = GetComponent<Light2D>();
        if (light3D == null)
            light3D = GetComponent<Light>();
    }

    private void InstantiateLights()
    {
        if (light2D == null)
            light2D = gameObject.AddComponent<Light2D>();
        if (light3D == null)
            light3D = gameObject.AddComponent<Light>();
    }

    private void InitialLightConfiguration()
    {
        light3D.bounceIntensity = 0;
        light3D.lightmapBakeType = LightmapBakeType.Realtime;
        light3D.shadows = LightShadows.None;
    }

    private void SyncLights()
    {
        SyncCommonAttributes();
        switch(type)
        {
            case LightType.POINT:
                SyncPointLights();
                break;
            case LightType.SPOT:
                SyncSpotLights();
                break;
            case LightType.GLOBAL:
                SyncGlobalLights();
                break;
        }
    }

    private void SyncCommonAttributes()
    {
        light2D.color = color;
        light3D.color = color;
        light2D.intensity = intensity;
        light3D.intensity = intensity;
        //light2D.useNormalMap = useNormalMaps;
    }

    private void SyncPointLights()
    {
        light2D.lightType = Light2D.LightType.Point;
        light3D.type = UnityEngine.LightType.Point;
        light2D.pointLightInnerRadius = 0;
        light2D.pointLightOuterRadius = radius;
        light2D.pointLightInnerAngle = 360;
        light2D.pointLightOuterAngle = 360;
        light3D.range = radius;
    }

    private void SyncSpotLights()
    {
        light2D.lightType = Light2D.LightType.Point;
        light3D.type = UnityEngine.LightType.Spot;
        light2D.pointLightInnerRadius = 0;
        light2D.pointLightOuterRadius = radius;
        light2D.pointLightInnerAngle = innerAngle;
        light2D.pointLightOuterAngle = outerAngle;
        light3D.innerSpotAngle = innerAngle;
        light3D.spotAngle      = outerAngle;
    }

    private void SyncGlobalLights()
    {
        light2D.lightType = Light2D.LightType.Global;
        light3D.type = UnityEngine.LightType.Directional;
    }
}

Editor Script

[CustomEditor(typeof(CombinedLight))]
public class CombinedLightEditor : Editor
{
    override public void OnInspectorGUI()
    {
        var light = (CombinedLight)target;
        EditorGUI.BeginChangeCheck();
        light.type = (CombinedLight.LightType)EditorGUILayout.EnumPopup("Type", light.type);
        light.color = EditorGUILayout.ColorField("Color", light.color);
        light.intensity = EditorGUILayout.FloatField("Intensity", light.intensity);
        if (light.type == CombinedLight.LightType.POINT)
        {
            RadiusControls(light);
            OpacityControls(light);
            NormalMapControls(light);
        }
        else if (light.type == CombinedLight.LightType.SPOT)
        {
            RadiusControls(light);
            OpacityControls(light);
            SpotAngleControls(light);
            NormalMapControls(light);
        }
        else if (light.type == CombinedLight.LightType.GLOBAL)
        {

        }
        // Light syncing within the Editor
        light.syncInEditor = EditorGUILayout.Toggle("Sync Lights in Editor?", light.syncInEditor);
        if (EditorGUI.EndChangeCheck() && light.syncInEditor)
            light.SetupAndSyncExistingLights();
    }

    private void RadiusControls(CombinedLight light)
    {
        light.radius = EditorGUILayout.FloatField("Radius", light.radius);
    }

    private void OpacityControls(CombinedLight light)
    {
        light.opacity = EditorGUILayout.Slider("Opacity", light.opacity, 0f, 1f);
    }

    private void NormalMapControls(CombinedLight light)
    {
        light.useNormalMaps = EditorGUILayout.Toggle("Use Normal Maps?", light.useNormalMaps);
    }

    private void SpotAngleControls(CombinedLight light)
    {
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Inner/Outer Angle");
        light.innerAngle = EditorGUILayout.FloatField(light.innerAngle);
        EditorGUILayout.MinMaxSlider(ref light.innerAngle, ref light.outerAngle, 0, 360);
        light.outerAngle = EditorGUILayout.FloatField(light.outerAngle);
        EditorGUILayout.EndHorizontal();
    }
}

Hey there! Newbie here, I’m trying to effect the falloff strength of my 2D lights. Are you saying this is possible via scripting? Right now I’m running into the read-only issue.

What version are you on? Falloff Intensity has a setter, 2021.3+ and possibly earlier.
GetComponent<Light2D>().falloffIntensity = Mathf.PingPong(Time.time, 1f);

For other properties, if you go to your Light2D component and on the 3 little dots in the top right click it and then select Edit Script you should be able to see the backing fields to access via reflection.