Lua tricks I've learnt so far...

Discussion about Mods for Prison Architect

Moderator: NBJeff

User avatar
aubergine18
level2
level2
Posts: 231
Joined: Sun Jul 05, 2015 3:24 pm

Lua tricks I've learnt so far...

Postby aubergine18 » Sat Jul 11, 2015 4:25 pm

I'm currently developing a health and safety mod; learning Lua and PA at the same time it's been a bit of a struggle so I decided to keep notes of some of the things that caught me out in case they are helpful to others (and also hopefully if I'm doing anything stupid someone will show me better way)...

Quotes

Make sure your operating system isn't putting ``smart quotes´´ in to your text editor. A tell take sign of this is lots of question marks in your mod description shown on the mods screen in game. It will screw up everything, only normal quotes can be used.

Debugging

Getting Game.DebugOut() to work... For some unknown reason the in-game debug window will only appear if there's an error in your script. Here's how I format my script to ensure that this happens in a safe and reliable manner...

Code: Select all

-- define any locals, etc., here

function log(s) Game.DebugOut(tostring(s)) end;

function Create()
  -- object init code goes here
  log("obj create")
end

function Update()
  -- object update code goes here
  log("obj update")
end

-- any other code goes here

print(); -- this will throw error and open debug window


Put print() right down at the end of the script, because it will throw an error and Lua will not run anything past that point in the script.

The next interesting thing about the debug window is that it's scoped to the first script that makes it appear. This means, as far as I can tell, you can only debug one script at a time - whichever script most recently threw an error.

Apparently the debug output should also appear in debug.txt in the main PA folder, but on a Mac at least this does not work. I've searched the disk for other debug.txt and found none, and can't find any other logs that seem related to PA. Things may be different on other operating systems, but on a Mac I seem limited to the in-game debug window for now.

Update() updates a lot

The Update() function is called dozens, if not hundreds, of times a second. The only code you should put in Update() is something to check the amount of time since the last Update() otherwise you're just wasting CPU cycles. Your object doesn't need updating dozens/hundreds of times a second, in fact it probably only needs updating every 5 seconds (5 prison minutes) or so in most cases.

Code: Select all

local time = Game.Time
local delay = 15 -- seconds
local was, ready = time()

function Create()
  -- init your object
end

function Update()
    local now = time()
    if now > ready then
       DoStuff( now - was )
       was = now
       ready = now + delay
    end
end

function DoStuff( elapsedTime )
  -- your update code here
end


We're basically using Update() as a de-spamming function. Note that Game.Time() will always have changed between Create() and the first Update() so the first Update() will still trigger DoStuff().

But wait... Do we even need the elapsedTime any more? It's just going to be 'delay' (except on the first Update() call), so we can shrink the code further to:

Code: Select all

local time  = Game.Time
local delay = 15 -- seconds
local ready = time()

function Create()
  -- init your object
end

function Update()
    local now = time()
    if now > ready then
       DoStuff()
       ready = now + delay
    end
end

function DoStuff()
  -- your update code here
end


Checking if a table is full or empty

For my smoke detector object I use Object.GetNearbyObjects() to detect nearby fires. I don't need any details about the fires, I just need to know if there are any.

Looking through other mods I saw this sort of thing being used very regularly:

Code: Select all

    local nearbyPrisoners = Object.GetNearbyObjects("Workman", 5.0)
    local pris = "None"
    for name, distance in pairs( nearbyPrisoners ) do
        pris = name
    end
    if pris ~= "None" then
        Object.SetProperty("Triggered",2)
    else
        Object.SetProperty("Triggered",0)
    end


I've also seen people incrementing counters in the for loop, just to then check if the counter is >0 at the end of the loop. That's just nuts, it's doing far too much processing.

After some digging I found a better (faster, shorter) way to check if some objects have been returned - Lua's next() function:

Code: Select all

local find = Object.GetNearbyObjects
local withinRange = 10
local found = next

function DoStuff()
   local fires = find("Fire", withinRange)
   if found( fires ) then
     -- sound the alarm!
   else
     -- turn off alarm
   end
end


The next() function (which I access via my local 'find' variable) pulls the first item from the table (in an undefined order); in this scenario I don't care what order things are in, I just want to know if something was found.

Note also that I'm regularly making local references to function chunks, it reduces the amount of time taken for Lua to get a pointer to the chunk (it's not searching through as many metatables, etc).

In my case I didn't need to keep a list of the fires, so I further further simplified the code as follows:

Code: Select all

local found = next
local any = Object.GetNearbyObjects
local nearby = 10

function DoStuff()
   if found( any("Fire", nearby) ) then
     -- sound the alarm!
   else
     -- turn off alarm
   end
end
Mavoc
level1
level1
Posts: 10
Joined: Thu Sep 17, 2015 5:34 pm

Re: Lua tricks I've learnt so far...

Postby Mavoc » Mon Sep 21, 2015 9:40 am

You can further optimize the Update function using random delays.
Lets say we have 6000 objects that are running this script and 10 seconds is a good enough de-spamming delay. Also lets assume we just loaded a saved game with all these objects.

In your example 1 frame every 10 seconds will run DoStuff for all 6000 objects due to them all initializing at the same time with fixed delays. This will most likely create hitches.

Code: Select all

local ready = 0

function Update()
   local now = Game.Time()
   if now > ready then
      ready = 10 + now
      DoStuff()
   end
end



Now in this example the delay is randomized to be between 5 and 15 seconds to force the objects to spread out their updates with an average delay of 10 seconds. Assuming we are running 60 fps, at 1 update a frame, then we are running DoStuff for an average of 10 objects every frame. This evenly distributes the work load between all the frames removing the likelihood of hitches.
Note: math.random(5,15) does integers while math.random() returns a real/float value between 0 and 1

Code: Select all

local ready = 0

function Update()
   local now = Game.Time()
   if now > ready then
      ready = math.random() * 10 + 5 + now
      DoStuff()
   end
end



At least that is all in theory, but as I tested it all I found some issues.

First off is that Game.Time() returns time played in real world seconds which counts at a steady rate regardless of game speed. So after a long pause all your Do Stuff functions will trigger instantly on an un-pause. Conversely, the faster the game speed, the less DoStuff will be triggered per game hour.
Also the value that gets returned is cached and only seems to recache when at least 50ms have pasted which is obviously an optimization of PA that messes with my optimizations.
All in all Game.Time() is not the correct way of going about this.

After further tested I realized that the Update function gets passed a param of game time units since last update. These TU(time units) translate into 2 per game minute. So by keeping a running total and using the optimizations I attempted early we come up with a much cleaner way of doing this. This triggers DoStuff every 5 - 15 game minutes regardless of game speed and spreading the load around to prevent hitches.

Code: Select all

local timer = 0
local delay = 0

function Update(elapsed)
   timer = timer + elapsed
   if timer > delay then
      delay = math.random() * 20 + 10  -- 10 to 30 TU which is 5 to 15 game minutes
      timer = 0
      DoStuff()
   end
end


TL;DR:
Using Game.Time in your Update de-spamming code is like using a hammer on a screw, sure it can work but there is a better tool for the job.
Instead use the Update function's elapsed param in a running count that resets after the delay.
Also randomize your delays to avoid hitches caused by multiple objects triggering their full Update code simultaneously.
Trixi
level2
level2
Posts: 245
Joined: Wed Mar 04, 2015 9:22 am
Location: Ulm, Germany
Contact:

Re: Lua tricks I've learnt so far...

Postby Trixi » Mon Sep 21, 2015 12:21 pm

what about making the delay random at initalisation

or why not using just simple integer values, that also would prevent running all Updates at once after pause mode.

The most disadvantage is like its best advantage, As more objects you have, as less often the code will be run. But im thinking that the incremented int-values should be faster, then calculating and comparing doubles.

Code: Select all

local count    = 0;
local maxcount = math.random() * 500 + 750; -- value of 1000 should be about 40 ingame-minutes,

function doSth()
   -- here the code
end

function Update()
   count = count + 1;
   if count == maxcount then
      count = 0;
      doSth();     
   end
end
elDiablo
level5
level5
Posts: 3111
Joined: Thu Mar 14, 2002 12:23 pm
Location: London, UK

Re: Lua tricks I've learnt so far...

Postby elDiablo » Mon Sep 21, 2015 12:44 pm

Hey, nice guide. I just want to point out that you can bring up the script window for any scripted object by select that object and pressing F3, so you don't need to invoke an error in order to open it. It also means you can open a window for each object if you want.
Mavoc
level1
level1
Posts: 10
Joined: Thu Sep 17, 2015 5:34 pm

Re: Lua tricks I've learnt so far...

Postby Mavoc » Mon Sep 21, 2015 7:30 pm

Trixi wrote:what about making the delay random at initalisation

or why not using just simple integer values, that also would prevent running all Updates at once after pause mode.

The most disadvantage is like its best advantage, As more objects you have, as less often the code will be run. But im thinking that the incremented int-values should be faster, then calculating and comparing doubles.

That can also work but I prefer re-randomizing on each full update to remove the possibility of an inited delay coinciding with a brief downtime in an objects work cycle. Of course this could also be prevent by picking a better random formula. Honestly any bit of delay randomization is loads better then none at all.

As for int values, you increment 1 each update, which means your DoStuff function will run less time ever game hour the faster the game speed is set. Your 40m increments will be 80m at 2x and 200m at 5x game speeds. In my testing the elapsed was ~0.03 at 1x, ~0.06 at 2x, and ~0.15 at 5x. So if the update delays need to correspond with game time it is best to use elapsed, though I can think of several cases which this wouldn't matter and your int counter might be slightly more optimized. Though if you are using an int counter, then math.random(750, 1250) is better then mathing out a non int delay.

Not sure I fully understand your last point, think I am getting lost in the pronouns. As for the int counter, in lua 5.1 there is only 1 number type and that is float(double-precision) http://www.lua.org/pil/2.3.html

Return to “Modding”

Who is online

Users browsing this forum: No registered users and 2 guests