First thing's first, we need to fix some problems with his library. His library forgets to do two things: what happens when lights (or their owners) die? and we should update the location of a mobile light when its owner moves.
light
Del()
off()
apply()
lighting.lights -= src
..()
atom
Del()
if(light)
del(light)
..()
Now we make sure lights stay with their owners
atom/movable
Move()
. = ..()
if(.)
if(light)
light.loc = src.loc
light.should_wake = 1
Next, we tackle the efficiency problem. F_A's library constantly updates lights, even lights that don't need to be updated. He did shave off the extra calculations that won't be necessary (so if a light hasn't changed, it doesn't recalculate it's effect) but what he failed to do was to leave out unnecessary updates. If a light hasn't changed, we don't need to check it or even try it. Instead, we should keep track of which lights do need updates, and only update those. We will handle this with a variable: should_wake. should_wake will tell us if a light should be considered for processing during this frame.
light
var/tmp/should_wake = 1
proc/loop() //overwriting the original light.loop()
if(!owner) del(src) //we handle the possibility of stranded lights
if(mobile)
var/opx = owner.x
var/opy = owner.y
if(opx != __x || opy != __y)
__x = opx
__y = opy
changed = 1
if(changed) //changed, process effect
apply()
else //no change
should_wake = 0 //stop waking up
Ok, the next thing we need to accomplish is sorting through the lights and only updating the ones who have should_wake equal to 1. We'll do this by looking at what clients are logged in, and calculating which light each client can see, take the lights that are within that view, and check only those for should_wake. This way, only lights that clients can see are capable of doing anything. We don't need to process or even try to process lights that no one will ever see!
var/list/players = list() //this will keep track of players logged in
mob/player
Login()
..()
players += src
src.light = new(src, 3)
Logout()
players -= src //always do this, even if switching mobs
if(key) //disconnecting
del(src)
//set LIGHT_RANGE to how far away from clients you want lights to "become active"
//for a square view size, set it to half your width
//for a rectangle view size with client.perspective = EDGE_PERSPECTIVE,
// set it to your height (ie: 15x11, set it to 11)
//and feel free to experiment!
//results will vary with each different client.perspective setting
var/const/LIGHT_RANGE = 8
Lighting
//lets rewrite Lighting.loop()
proc/loop() //this completely replaces the Lighting.loop() code in the library
while(src)
sleep(world.tick_lag)
if(players.len <= 0) continue //no players logged in, bail!
if(!states)
if(!icon)
del(src)
CRASH("The global var lighting.icon must be set")
var/list/l = icon_states(icon)
states = l.len ** 0.25
if(__ambient != ambient)
for(var/light/l in lights)
l.ambient(ambient)
__ambient = ambient
//in the following lines, you're going to see the use of the colon operator
//we do this because a big world is going to have a lot of objects and we know that
//within the lists that we're searching, there will only ever be the object we expect
//so we can bypass type checking by omitting the type paths and using colon
//operators instead of the period operator
for(var/p in players)
for(var/light/l in range(p, LIGHT_RANGE))
if(l.should_wake)
l.loop()
l.should_wake = 0
for(var/s in changed)
s:icon_state = "[s:c1:lum][s:c2:lum][s:c3:lum][s:lum]"
s:changed = 0
changed.Cut()
If you want to make a change to a light, you need to make sure that you set should_wake to 1 so that it will be processed.
Lights that only change the area around it once (like a static light on the wall) are only ever processed ONCE.
If a client is logged in and standing still, it will only ever process the lights it can see, once.
I've tested this on my machine up to about 50,000 moving light sources (spread out over two 64x64 maps) with a view size of 15x11, and the CPU doesn't go over 50%. That is some serious performance (and a lot of moving things). Try doing that with the library out of the box! The secret is in processing only what we need to process, and only processing lights that clients can see!
Bonus:
Let's make the light engine also support opacity! We'll implement our own, but we'll call it opaque.
atom
var
// we don't use the opacity var because we don't
// want to use BYOND's built-in opacity system,
// we want to create our own effects.
opaque = 0
turf
var
canshade = 1 //by default, all turfs can shade, but if you want a void that ignores shading, set to 0
//here we speed up shading init a little
var
shading/null_shading = new(null, null, 0)
shading
proc
init()
// get references to its neighbors
c1 = locate(/shading) in get_step(src, SOUTH) //these get_step calls are new!
c2 = locate(/shading) in get_step(src, SOUTHWEST)
c3 = locate(/shading) in get_step(src, WEST)
u1 = locate(/shading) in get_step(src, EAST)
u2 = locate(/shading) in get_step(src, NORTHEAST)
u3 = locate(/shading) in get_step(src, NORTH)
// some of these vars will be null around the edge of the
// map, so in that case we set them to the global null_shading
// instance so we don't constantly have to check if these
// vars are null before referencing them.
if(!c1) c1 = null_shading
if(!c2) c2 = null_shading
if(!c3) c3 = null_shading
if(!u1) u1 = null_shading
if(!u2) u2 = null_shading
if(!u3) u3 = null_shading
Now add this to the first line of shading.lum()
shading
proc
lum(l)
if(!loc:canshade) return //can't shade this, so don't!
//rest of code here...
And finally, the code that does all the work... light.effect()
This was taken directly from the F_A's demo!
light
proc
effect()
var/list/L = list()
for(var/shading/s in range(radius, src))
// if we already have a value for it, skip it
if(!isnull(L[s])) continue
// if its the center we handle this case specially to
// avoid some division by zero errors later. also, we
// know we can always see the center so we don't need
// to check.
if(s.x == x && s.y == y)
var/lum = lum(s)
if(lum > 0)
L[s] = lum
continue
// determine if s can be seen from src...
// find the normalized vector pointing from src to s
var/dx = (s.x - x)
var/dy = (s.y - y)
var/d = sqrt(dx * dx + dy * dy)
if(d > 0)
dx /= d
dy /= d
// we'll use this vector to trace a line from src to s
// and check each tile along the way for opaque objects
var/tx = x + dx + 0.5
var/ty = y + dy + 0.5
// we use a limited number of iterations
for(var/i = 1 to radius)
var/turf/t = locate(round(tx), round(ty), z)
// in case we went off the edge of the map
if(!t) break
// if we don't have an illumination value, compute one
if(!L[t.shading])
var/lum = lum(t.shading)
if(lum > 0)
L[t.shading] = lum
// if it's opaque, stop tracing the line
if(t.opaque) break
//if(t.opaque(owner))
// break
// if we've reached s, stop tracing the line
if(t.shading == s) break
// continue tracing the line
tx += dx
ty += dy
return L
Now I realize this is a lot of changes to the library, and I know its probably hard to follow this article and make all of the changes successfully. So if there is a big enough demand for it, I will consider posting a PASTE BIN of the library in full so its plug and play.
Cheers, and happy lighting.