Hey guys, I had been testing out the new system for 2D lighting on a game with tilemaps. At first obviously this doesn’t work at all for the time being, but with some hacking thanks to this thread I was able to get it working:
And this is what I end up with:
(Gif too large for forum)
I used a version of his script for ShadowCaster2D that allows it to generate the shadows based on the tilemap collision mesh, which is handy! I discovered that with that implementation there was no way for tiles to be dynamically added/removed from the world and having the lighting adjust to match - so I wrote some changes to his other script (I’ve named it “SetTilemapShadows” for lack of a better name) that allow it to recreate the shadows when the user needs to (like after adding new tiles to the tilemap). Here that is:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;
public class SetTilemapShadows : MonoBehaviour
{
public static SetTilemapShadows Instance;
private CompositeCollider2D tilemapCollider;
private GameObject shadowCasterContainer;
private List<GameObject> shadowCasters = new List<GameObject>(), toDelete = new List<GameObject>();
private List<PolygonCollider2D> shadowPolygons = new List<PolygonCollider2D>();
private List<ShadowCaster2D> shadowCasterComponents = new List<ShadowCaster2D>();
private bool doReset = false, doCleanup = false;
public void Start()
{
Instance = this;
tilemapCollider = GetComponent<CompositeCollider2D>();
shadowCasterContainer = GameObject.Find("shadow_casters");
for (int i = 0; i < tilemapCollider.pathCount; i++)
{
Vector2[] pathVertices = new Vector2[tilemapCollider.GetPathPointCount(i)];
tilemapCollider.GetPath(i, pathVertices);
GameObject shadowCaster = new GameObject("shadow_caster_" + i);
shadowCasters.Add(shadowCaster);
PolygonCollider2D shadowPolygon = (PolygonCollider2D)shadowCaster.AddComponent(typeof(PolygonCollider2D));
shadowPolygons.Add(shadowPolygon);
shadowCaster.transform.parent = shadowCasterContainer.transform;
shadowPolygon.points = pathVertices;
shadowPolygon.enabled = false;
//if (shadowCaster.GetComponent<ShadowCaster2D>() != null) // remove existing caster?
// Destroy(shadowCaster.GetComponent<ShadowCaster2D>());
ShadowCaster2D shadowCasterComponent = shadowCaster.AddComponent<ShadowCaster2D>();
shadowCasterComponents.Add(shadowCasterComponent);
shadowCasterComponent.selfShadows = true;
}
}
private void Reset()
{
toDelete = new List<GameObject>(shadowCasters);
shadowCasters.Clear();
shadowPolygons.Clear();
shadowCasterComponents.Clear();
for (int i = 0; i < tilemapCollider.pathCount; i++)
{
Vector2[] pathVertices = new Vector2[tilemapCollider.GetPathPointCount(i)];
tilemapCollider.GetPath(i, pathVertices);
GameObject shadowCaster = new GameObject("shadow_caster_" + i);
shadowCasters.Add(shadowCaster);
PolygonCollider2D shadowPolygon = (PolygonCollider2D)shadowCaster.AddComponent(typeof(PolygonCollider2D));
shadowPolygons.Add(shadowPolygon);
shadowCaster.transform.parent = shadowCasterContainer.transform;
shadowPolygon.points = pathVertices;
shadowPolygon.enabled = false;
//if (shadowCaster.GetComponent<ShadowCaster2D>() != null) // remove existing caster?
// Destroy(shadowCaster.GetComponent<ShadowCaster2D>());
ShadowCaster2D shadowCasterComponent = shadowCaster.AddComponent<ShadowCaster2D>();
shadowCasterComponents.Add(shadowCasterComponent);
shadowCasterComponent.selfShadows = true;
}
doCleanup = true;
}
private void LateUpdate()
{
if(doReset)
{
Reset();
doReset = false;
}
if(doCleanup)
{
StartCoroutine(Cleanup());
doCleanup = false;
}
}
IEnumerator Cleanup()
{
yield return null;
for (int i = 0; i < toDelete.Count; i++)
{
Destroy(toDelete[i]);
}
toDelete.Clear();
}
public void UpdateShadows()
{
doReset = true;
}
}
And while that isn’t exactly “high performance” minded in design - it works well enough to not cause any lag on a tilemap that is 128x128, so good enough for now. You’ll notice I had to use some strange timing and a coroutine to get the shadows to render right between the time the user adds the new tile(s) and the time it actually recreates the shadow casters, but by doing it that way it helps prevent a odd flickering on the shadow map, though still it does make a bit of flicker where the new shadow is added, but it doesn’t seem too annoying to use it that way. There are also some bits left in where I thought I should track existing shadow casters and just change the shape of them rather than create new - but I never got that working properly so feel free to cutout those leftover Lists<> that don’t serve a purpose now.
I just attached that script to something in my scene and when I need to update the shadows, I call the UpdateShadows() method on the static instance.
So now that I’ve shared the results of that here, hopefully others can improve upon and perhaps replace this system with a more performance friendly one that doesn’t delete then recreate all the existing shadows and such - I would love to see more clever solutions but figure it can’t hurt to get the concept I’ve made out into the hands of anybody needing dynamic 2D lights on a tilemap!
Another really annoying thing is that the package manager (or something) automatically “updates” the ShadowCaster2D script in the library files every single time I restart the editor. That means I have to keep a separate copy of the script outside the project and copy/paste it over the “updated” script unity keeps changing back… but regardless it works so I am working with that annoyance for now.
EDIT: And one last thing - UT please please please… build in a function to make the ShadowCaster2D do this with the collision mesh of the tilemap - that’ll save me all this headache