Wednesday, February 22, 2012

JIT LIGHTING - Skyrim CK Tutorial - Lights on Timers

Go back to main tutorial page

In version 1.1 I introduced a new feature... timed lighting.  It's now possible to using LightingBalls to control when a light, or group of lights, turns on/off.  Ok, the video itself isn't all that great (I'll replace it eventually).

The above video is an example of a 2-stage day/night cycle.  A 4-stage would be: dawn/day/dusk/night -- certainly, that would be even more impressive.

There is no limit to the number of stages you can have with the Just In Time Lighting scripts.

Futhermore, it can all work off of a single onUpdate() call... and you get to control the frequency at which it does the updates.

It gets even better... You can combine them with special triggers, so it only updates when the player crosses certain thresholds.  In that case, it works without registering for OnUpdate calls at all.

Or, you can do a mixture of both... where it will auto-update when the player is nearby (registering its OnUpdate event), but then stop when the player crosses a boundary (deregistering).  Yeah, this is more complicated.

I'm going to go through 4 examples, in increasing order of complexity...

Example 1:  As simple as it gets.

A is a LightingBall, and 1 is light we want to put on a timer -- which can optionally be tied to as many lights as you want via the "Enable Parent" tab.

So, here we have LightingBall A (in blue), and Light #1.  Once we have the light rigged to the ball, all we need to do is go into the ball's script properties window, and:

a) Set OnTimer to TRUE

b) Set OnTimerUpdate to a value larger than 0.  This will be the interval at which it checks the current time.  We'll use 17, since I like prime numbers.

c) Set OnTimerBeginTime and OnTimerEndTime to be the range of time you'd want the light to be on (military time, kinda).  We'll use 8 and 20, respectively.

That's it, you're done.  The light will come on at 8 AM, and turn off at 8 PM.  While the player is in the cell, it will check to see if it needs to turn itself on/off once every 17 seconds of real-time.  If we wanted it to come on at 8:30 AM, we would use 8.5 for the start time (yes, 8.5, not 8.3).  Midnight would be 0 (not 24!!!).

While you can only have 4 lights to one LightingBall, you can work around this by setting other lights' "Enable Parent" to the light that is controlled by a LightingBall, or by using more balls -- whatever works for you.

Advanced:  Nothing's stopping you from using these scripts to put any object on a timer... it doesn't have to be a light source.

Example 2:  Two lights, two different times, one OnUpdate() event.  No problem.

A, B, and C are LightingBalls, while 1 and 2 are lights (or light groups).

A, B, and C are all LightingBalls...  Of course, A is special because it's red.  In this example, only A is calling onUpdate(), but B and C will both turn on their respective lights at totally different times (as opposed to merely opposite times, which could be accomplished by using the layout from example 1, "Enable Parent", along with the "opposite" setting).

Just as in Example #1, without a controller you're letting the engine handle if these flicker or cause other lights to flicker.

Assuming they're all rigged together as shown, here's what we need to do (starting with ball A):

a) Set OnTimer to TRUE.
b) Set OnTimerUpdate to a value larger than 0. Again,  we'll use 17.

Now moving on to B:

a) Set OnTimer to TRUE.
b) Set OnTimerBeginTime and OnTimerEndTime to the range of time you want.  We'll use 8 and 20, like last time.

Finally, we'll modify C's script properties:

a) Set OnTimer to TRUE.
b) Set OnTimerBeginTime and OnTimerEndTime to 19 and 9, respectively.

That's it, you're done.  Now, light 1 will come on at 8 AM, and shut off at 8 PM... and light 2 will come on at 7 PM, and shut off at 9 AM.  So, there's 2 hours of overlap each day when both lights are on.  I don't know why you'd want to do that, but you can.  I know... I hate unrealistic examples, too.

While it's true that we could have done it using a separate onUpdate() timer for each light, sometimes it's necessary to have everything on the same timer.  Granted, I don't think that's true in this particular example, but whatever. ;) Again, you can think of light 1 and 2 more like light groups, rather than just individual lights.

Example 3:   Example 2, except also with a controller.

While the setup in examples 1 & 2 is useful in the general sense that it does what it's supposed to (have lights on timers), it does not really help help you add more dynamic lights to a cell... because one (or both) of the lights is or can be on without consideration to player's position in the cell.

So, here we have a controller (F, with its balls d & e) which turns both lights off when the player crosses its lower boundary, and lets them turn on when s/he crosses the upper boundary.  We're fortunate that in this example there's a wall that completely breaks the player's line of sight of all shadow casting lights and any non-shadow casting lights which may be Parent Enabled by them.  Such is not always the case (as in the next example).

Assuming they're all rigged together as shown, here's what we need to do:

A, B, and C are configured just as in example #2.

That's it, you're done!  Well, let's talk about the controller F, and it's balls d and e.

Nothing special is going on with F, d, or e.  Ball A is rigged to Ball d  just as if A was another light rigged to d.  Of course it's not a light, it's a LightingBall, but the script is built to take this into consideration.  Ball e probably has some other lights rigged to it, but if it doesn't then both the specialCaseNoEnables and specialCaseNoDisables flags must be set to TRUE.  Otherwise, my debugging code will assume you forgot to rig the lights on ball e, and it will complain at you when the cell loads (it's a feature).

So, if the controller turns everything on/off, and balls C & D have the time information... why do we need Ball A at all?

Because we want the lights to be able to update even if the player just stands in the room all day.

Example 4:  Example ESP Helgen Keep's Main Room Day/Night cycle setup demystified.

Congratulations on making it this far!  This is a challenging problem due to there not being a wall handy to break line of sight of the entire room.  So, we have to fake it as best we can.

Lights 1 & 2 are non-shadow casting lights, while 3 & 4 do cast shadows.  1 & 3 are both on at the same timer, as well as 2 & 4.  Both timer pairs are on at opposite times.  There's 5 dynamic lights at the other end of the long hallway (not shown), so the controller is needed.

The non-shadow casting lights, when combined with the long view distance, help to create the illusion that the other lights are on -- even when they are not.  The trick is to let the engine handle the disabling of the non-shadow casting lights while still having them receive enable commands from the controller.  The reason the trick works is because the engine is surprisingly good at fading non-shadow casting lights from view.

Generally, you always want to let the engine handle non-shadow casting lights.

And that's the purpose of ball B.  It blocks any disable commands received through the chain from the X controller's ball y, because I set its specialCaseNoDisables flag to TRUE.   E & F contain the timer information (they are mirrors of C & D in that respect).

However, we do want the enable commands from ball y to propagate through all the balls to lights 1 & 2, because if the player starts down the hall at a time just before the lights switch, then it will switch when the player crosses the left boundary of controller X... rather than when s/he is closer or already in the room.  Granted, this not a major issue, but why half-ass something when you don't have to? :D

This setup (and anything remotely like it, including examples 2 & 3) works because whenever a ball  receives a command from another ball to turn its lights on, it first checks its timer information against the current time.  If its lights shouldn't be enabled because the current time is not between the begin and endtime set, then it ignores the enable command and disables its lights instead.  Even though each Ball A in examples 2, 3, and 4 actually only sends enableNoWait() commands on its own, the attached balls still know what to do with their lights when the time comes.

Here's the actual code that handles this (important parts bolded, and modified slightly to be more readable):

FUNCTION enableNoWait(bool notUsed = false)
allLightsOn = enableLights()

IF (allLightsOn == False)
allLightsOff = True

IF ((OnTimerAutoUpdate > 0) && (OnTimer))

The only time enableLights() could ever return False is when the ball is on a timer AND the current time is not between the begin and endtime (or specialCaseNoEnables is set to True, of course).

Pretty slick, right?  Yeah, I thought you'd think so.  ;)  The usability of this entire feature hinges on those few lines.

Well, I think that pretty much does it.  If there's any questions, I'm sure you'll let me know.

Go back to main tutorial page


  1. This is excellent. Thank you again for taking the time to explain this to those of us who lack scripting skills.

    I'm a visual and hands on kind of learner so this tutorial really helped clear things up for me. Have a perfect use for this in an indoor garden area I want lit at night by lights, but using ambient light and beams during the day.


    1. Glad to hear it! Let me know if you have any problems.