Level Vs Level

Talk about creating Grimrock 1 levels and mods here. Warning: forum contains spoilers!
User avatar
Mysterious
Posts: 226
Joined: Wed Nov 06, 2013 8:31 am

Level Vs Level

Post by Mysterious »

Just a question. For every level do the amount of objects, fx's, lights, monsters etc.... affect the next level? I mean performance wise.. Say lvl 1 has not many things in it as is lvl2. Then as you progress to next lot of levels and add more objects does this start to slow the game down??

I am just wondering is this true?

Ohh thxs Orrr2 guys for your Source Code, although it's hard to understand (for me that is) it's a great help :)
User avatar
antti
Posts: 688
Joined: Thu Feb 23, 2012 1:43 pm
Location: Espoo, Finland
Contact:

Re: Level Vs Level

Post by antti »

Having a level with A LOT of stuff in it can affect performance, even if you are not in that level since the other levels are running (just the simulation, not the graphics) regardless of if you are in the level or not. But generally speaking, the CPU load of Grimrock 1 was quite light and not the bottleneck (the game was limited by the GPU pretty much always) so the threat feels quite theoretical to me.

To sum it up, I really wouldn't worry about the other levels bogging down the performance.
Steven Seagal of gaming industry
User avatar
msyblade
Posts: 792
Joined: Fri Oct 12, 2012 4:40 am
Location: New Mexico, USA
Contact:

Re: Level Vs Level

Post by msyblade »

Some easily avoidable things that can slowdown performance in a mod:

Multiple FX's/lights visible: Try to limit the torches/FX's visible to the party, to 3 or 4 at any given time.

Hooks: Some hooks require constant checks. The ORRR2 is a good example of this. For instance, if you have an onMove hook (or three!), it has to check every time the party moves a square. Add to this a monster move hook, and it has to check everytime that monster moves also (and possibly other monsters, each time they move), it can start to add up.

Timers: Deactivate timers when they finish their task. Some dungeons have dozens upon dozens of timers doing many different things. To maximize effeciency, always ensure that they deactivate themselves when they're done. Timer based script checks are a terrible bleed on resources also. For example, a script checking for a projectile every 0.1 seconds, throughout an entire dungeon, just to cover 1 square near a particular puzzle would be a bad idea. Start the timer checks when the player gets near, and deactivate when not needed anymore. Or find another way to accomplish the goal.
It's also noteworthy that timers hit every frame on the current level, but only every other frame on the levels above/below, and even less on the levels further away. This can throw multilevel puzzles off balance. It also means low framerate can affect the timers in slight ways.

As Antti says, the real bottleneck comes from the GPU, so the first culprit to tackle is visible lights/FX . The timers and hooks really have to be numerous to cause slowdown, but it doesn't take many Torches before you experience a performance drop.

In conclusion, the number of assets doesn't have much of an effect, unless you are running constant checks on many different things (As is the case in the ORRR2). This is why Diarmuid set it up to load each room on entry, and unload on exit. Once you start to affect framerate, every tick counts.

Hope this helps!
Currently conspiring with many modders on the "Legends of the Northern Realms"project.

"You have been captured by a psychopathic diety who needs a new plaything to torture."
Hotel Hades
User avatar
Mysterious
Posts: 226
Joined: Wed Nov 06, 2013 8:31 am

Re: Level Vs Level

Post by Mysterious »

Hi guys. Sorry for answering so late Computer went down.

Ok thank you for that info, will keep this in mind. I have noticed things are different with Timers. I don't know if I am correct here but they are time wise different on each level. It could just be me but not sure. It feels as though they get slower each level. Aren't they supposed to be the same, I mean a timer is a timer eg: 1 to 60 Seconds so they should be all the same? I think that's what you mentioned Mysblade.

Mysblade thxs for the additional Village objects that go with the Set :) and for your answer Antti :)
User avatar
antti
Posts: 688
Joined: Thu Feb 23, 2012 1:43 pm
Location: Espoo, Finland
Contact:

Re: Level Vs Level

Post by antti »

Mysterious wrote:Hi guys. Sorry for answering so late Computer went down.

Ok thank you for that info, will keep this in mind. I have noticed things are different with Timers. I don't know if I am correct here but they are time wise different on each level. It could just be me but not sure. It feels as though they get slower each level. Aren't they supposed to be the same, I mean a timer is a timer eg: 1 to 60 Seconds so they should be all the same? I think that's what you mentioned Mysblade.

Mysblade thxs for the additional Village objects that go with the Set :) and for your answer Antti :)
The other levels are updated slower so the timers also advance slower there. To avoid unnecessary load on CPU, during each frame only three levels are updated and the logic goes like this:
1. The current level player party is on is updated every frame
2. The level below and above take turns updating. Eg, every even frame, the level above is updated and every odd frame, the level below is updated.
3. All the rest of the levels are updated in round robin fashion, eg. they take turns after each other. Basically this means that if you have 103 levels in your dungeon, it might take 100 frames until a specific level is updated. 8-)

If you have timing critical scripts that need to work across multiple levels, I remember that people here have used timers in every level and I'm sure someone here can provide you with the implementation details :) Also in some cases, using getStatistic("play_time") could be suitable for keeping things in sync.
Steven Seagal of gaming industry
User avatar
msyblade
Posts: 792
Joined: Fri Oct 12, 2012 4:40 am
Location: New Mexico, USA
Contact:

Re: Level Vs Level

Post by msyblade »

The solution for the timer issue by guys way smarter than me: Spawn dynamic timers, as needed, using the play time stat to get reference and set . Or pass a timer from one floor to the next, destroying the previous, creating a new one on current floor, and applying the math to subtract what is needed from the count.

I THINK Komag came up with a more elegant solution, although i never investigated it fully.
Currently conspiring with many modders on the "Legends of the Northern Realms"project.

"You have been captured by a psychopathic diety who needs a new plaything to torture."
Hotel Hades
User avatar
JohnWordsworth
Posts: 1397
Joined: Fri Sep 14, 2012 4:19 pm
Location: Devon, United Kingdom
Contact:

Re: Level Vs Level

Post by JohnWordsworth »

If you need something that's ticking often (but not necessarily every single frame) then a good solution is something like this. I'm just writing this off the top of my head, so it might need some tweaks. But create a script_entity called "global_timers" and use the following script...

Global Timer Script: global_timers

Code: Select all

gGlobalTimers = {};
gTimerToGlobalMap = {};


--
-- Create a global timer that will tick at the same rate regardless of the level the player is on.
-- Note: The timer is running by default but can be stopped (paused) or destroyed later.
--
function createTimer(id, interval, tickFunc) 
  if ( gGlobalTimers[id] ~= nil ) then
    print("A timer with ID <" .. id .. "> already exists, cannot create another.");
  end
 
  local levelCount = getMaxLevels();
  local globalTimerInfo = {
    timerId = id,
    timers = {},
    lastTick = getStatistic("play_time"),
    isRunning = true,
    tickFuncs = {},
    destroyFuncs = {},
    destroyDelay = 0,
  };

  if ( type(tickFunc) == "function" ) then
    table.insert(globalTimerInfo.tickFuncs, tickFunc);
  elseif ( type(tickFunc) == "table" ) then
    globalTimerInfo.tickFuncs = tickFunc;
  end
 
  for lvl = 1, levelCount do
    local timer_id = "gt_" .. id .. "_" .. lvl;
    local timer = spawn("timer", lvl, 1, 1, 1, timer_id);
    timer:setTimerInterval(interval);
    timer:addConnector("activate", "global_timers", "tick");
    timer:activate();

    table.insert(globalTimerInfo["timers"], timer_id);
    gTimerToGlobalMap[timer_id] = id;
  end

  gGlobalTimers[id] = globalTimerInfo;
end


--
-- Add a tick function to a global timer
--
function addTimerTickFunc(id, tickFunc)
  if ( gGlobalTimers[id] == nil ) then
    print("Cannot add tick func to global timer " .. id .. " without creating it first");
    return false;
  end

  if ( type(tickFunc) ~= "function" ) then
    print("Tried to add tick func to timer, but given " .. typeof(tickFunc) .. " instead.");
    return false;
  end

  table.insert(gGlobalTimers[id].tickFuncs, tickFunc);
  return true;
end


--
-- Add a destoy callback function to a global timer
--
function addTimerDestroyFunc(id, destroyFunc)
  if ( gGlobalTimers[id] == nil ) then
    print("Cannot add destroy func to global timer " .. id .. " without creating it first");
    return false;
  end

  if ( type(destroyFunc) ~= "function" ) then
    print("Tried to add tick func to timer, but given " .. typeof(destroyFunc) .. " instead.");
    return false;
  end

  table.insert(gGlobalTimers[id].destroyFuncs, destroyFunc);
  return true;
end


--
-- Start a timer that has previously be paused. 
--
function startTimer(id) 
  if ( gGlobalTimers[id] == nil ) then
    print("Cannot start global timer " .. id .. " without creating it first");
    return false;
  end

  gGlobalTimers[id]["isRunning"] = true;
  return true;
end

--
-- Stop a running global timer but don't destroy it (so it can be resumed)
--
function stopTimer(id) 
  if ( gGlobalTimers[id] == nil ) then
    print("Cannot stop global timer " .. id .. " without creating it first");
    return false;
  end

  gGlobalTimers[id]["isRunning"] = false;
  return true;
end

--
-- Destroy the given global timer, removing it completely from the world
--
function destroyTimer(id, delay)
  if ( gGlobalTimers[id] == nil ) then
    print("Cannot destroy global timer " .. id .. " without creating it first");
    return false;
  end
 
  if ( delay ~= nil and delay > 0 ) then
    gGlobalTimers[id].destroyDelay = delay;
    return false;
  end

  for i,timer_id in ipairs(gGlobalTimers[id]["timers"]) do
    local timer = findEntity(timer_id);
    gTimerToGlobalMap[timer_id] = nil;
    
    if ( timer ~= nil ) then
      timer:destroy();
    end
  end

  gGlobalTimers[id] = nil;

  for i,func in ipairs(globalTimer.destroyFuncs) do
	  func(dt);
  end
end

--
-- Internal 'tick' method for the timers
--
function tick(sender)
  if ( sender.level ~= party.level ) then
    return;
  end
  
  globalTimer = gGlobalTimers[gTimerToGlobalMap[sender.id]];
  
  if ( globalTimer == nil ) then
    return;
  end
  
  local time = getStatistic("play_time");
  local dt = time - globalTimer.lastTick;
  globalTimer.lastTick = time;

  for i,func in ipairs(globalTimer.tickFuncs) do
	  func(dt);
  end

  if ( globalTimer.destroyDelay > 0 ) then
    globalTimer.destroyDelay = globalTimer.destroyDelay - dt;

    if ( globalTimer.destroyDelay <= 0 ) then
      destroyTimer(globalTimer.timerId);
    end
  end
end
Hmm, this grew a bit bigger than I planned while writing it. However, assuming this works (untested at the moment), then you should just be able to do the following elsewhere in your dungeon...

Example Usage

Code: Select all

-- Sample script for using Global Timers
-- Note: This should really be called from a function instead of just being in a script
-- entity, otherwise it will only work if the global_timers script is loaded first. 

function tickA(dt)
   print("Timer 1 Tick A: " .. dt);
end

function tickB(dt)
	print("Timer 1 Tick B: " .. dt);
end

if ( findEntity("global_timers") ~= nil ) then
	-- Start timer
	global_timers.createTimer("my_timer_id", 0.10);
	global_timers.addTimerTickFunc("my_timer_id", tickA);
	global_timers.addTimerTickFunc("my_timer_id", tickB);
	
	global_timers.addTimerDestroyFunc("my_timer_id", function() print("Timer Destroyed!"); end);
	
	-- Destroy in 5 seconds
	global_timers.destroyTimer("my_timer_id", 5);
else 
	print("Global Timers not installed");
end
Let me know how you get on. If it doesn't work and you find the fix, please post it here and I'll update. If it doesn't work and you can't make it work, I'll paste it into a dungeon when I get home and fix it!

Edit: Updated with fixed code. This should just work now :)
Last edited by JohnWordsworth on Thu Apr 17, 2014 3:23 am, edited 1 time in total.
My Grimrock Projects Page with links to the Grimrock Model Toolkit, GrimFBX, Atlas Toolkit, QuickBar, NoteBook and the Oriental Weapons Pack.
User avatar
JKos
Posts: 464
Joined: Wed Sep 12, 2012 10:03 pm
Location: Finland
Contact:

Re: Level Vs Level

Post by JKos »

Just happened to notice this thread and remembered from other thread that you use my framework. It already has a support for constant timers, basically the idea is same as JohnWordsworth's. The problem with that approach is that when party changes dungeon level the timer on the target level is running behind, so there will be gaps on level changes and this gap is growing with the interval. Which means that this approach only works well enough with really small intervals. My solution was that when the interval is over 1 second the timer will internally use 0.1 second interval and count to actual interval (see the code if you like) and also in my solution only the timer on the party level is active, which saves some cpu-cycles.

Usage example

Code: Select all

local t = timers:create('my_constant_timer')
t:setConstant(true) --this makes the timer constant
t:addConnector("activate", "script", "tick");
t:activate()
Timers-script also has a lot of other features, documentation: https://sites.google.com/site/jkosgrimr ... ule-timers

ORRR2 uses a party:onDraw-hook and party:getStatistics('play_time') to calculate the intervals, I think that it is a better approach, but I wrote my script before we had onDraw-hook and it works well enough too for most cases.
- LoG Framework 2http://sites.google.com/site/jkoslog2 Define hooks in runtime by entity.name or entity.id + multiple hooks support.
- cloneObject viewtopic.php?f=22&t=8450
User avatar
Mysterious
Posts: 226
Joined: Wed Nov 06, 2013 8:31 am

Re: Level Vs Level

Post by Mysterious »

Hi guys.

First thank you all for your replies, it is good to have so much support in this community :)

I would appreciate a Demo Mod if possible John thank you, only if you have the time. I do enjoy a challenge but the Coding seems Alien to me :). If I could get a sample Dungeon then I should be able to work it out from there. :)

Jkos I will also give your system a try, I have been reading the Website on most of the Framework System and it's a slow process for me, being so new to lua coding. Well any coding for that fact. :)

Antti it is very good that you help the community out so much. I don't remember any other Software makers doing this :) Well anyway thank you all for your replies again and cant wait to get the Timer running better. Also LOG 2 release. :)

PS: Yes I did look at the ORRR 2 Timer and Komags Timer, but to me it's like reading a different language :) but it wont stop me from my goal.. Hey just learnt how to highlight text in the post lol. Have a nice day guys :D

EDIT: John I tried your code and it comes up with this error: '}' expected (to close '{' at line 16) near '='; I am not sure what this means.

Oh the line of Code is:

local levelCount = getMaxLevels();
local globalTimerInfo = {
"timers" = {}, -- This is where Error is --
"lastTick" = getStatistic("play_time"),
"isRunning" = true,
"tickFunction" = tickFunction
};
User avatar
JohnWordsworth
Posts: 1397
Joined: Fri Sep 14, 2012 4:19 pm
Location: Devon, United Kingdom
Contact:

Re: Level Vs Level

Post by JohnWordsworth »

Hi Mysterious, I've popped my global timer script into a sample dungeon and fixed the bug. Have also made a few changes.

1. You can now add multiple callbacks for a single global timer to save resources. So, if you have a bunch of things that you would like to happen every 1/10th of a second - create one timer with a 0.1 interval and add multiple callbacks. You can also add a function callback for when the timer gets destroyed.

2. You can now also destroy a timer after a given amount of time. Just call 'global_timers.destroyTimer("timer_id", 5)' and it will get destroyed in 5 seconds.

You can grab it from here: https://dl.dropboxusercontent.com/u/610 ... xample.zip

Installation is still almost the same as before - just add the global_timers script entity to your own mod and call functions on it. As mentioned by JKos, don't expect this to always callback at the same interval. Moving up and down levels will result in one call being off. However, your tick function receives a 'dt' parameter - which is the amount of time that has passed since the last call. Use this to keep track of the exact changes in time.

If you really need something to happen every single frame or very precisely at a repeated interval (even when changing floors), you'll have to look at an onGui hook solution. However, this is more effort than just dropping a script entity in as you also need to edit some lua files. I don't particularly like using onGui for anything but rendering or GUI related methods - but that's just me being finicky about putting game logic in the render loop - however, it is the most reliable high-resolution function call we get.

I'll update my previous post with the new script and info.
My Grimrock Projects Page with links to the Grimrock Model Toolkit, GrimFBX, Atlas Toolkit, QuickBar, NoteBook and the Oriental Weapons Pack.
Post Reply