#zone event listeners question (Eunomiac)

1 messages ยท Page 1 of 1 (latest)

vocal dagger
#

@stable orbit @visual estuary Certainly! I'll put it in a thread as it's more than just a snippet. I hope it's not as simple as me trying to do too much, as I've tried to be as efficient as possible (I think I said earlier, I get nervous around event listeners that get called a lot) --- gating everything behind simple boolean checks to avoid unnecessary steps, etc.

But regardless of what's actually in the event listeners, why should assigning them to a variable (the ActivateZones() function I posted) cause a delay? (I thought I was quite clever coming up with that idea for cleanly enabling/disabling zones all by myself ๐Ÿ˜‰ )

#

The opening, with imports, some relevant local variables, and a zone throttling function I put in when I thought the delay might be caused by a bunch of objects all calling the zone event listener at once:

local U = require("lib.utilities") -- a utilities library of mine
local S = require("core.state") -- handles data that persists through saves
local G = require("lib.guids") -- an index of object guids

local isCheckingSuspicion = false
local isZoneThrottled = {}

local function throttleZone(zone, func)
    local throttle = 0.5 -- seconds
    isZoneThrottled[zone.guid] = true
    Wait.time(function()
        func()
        isZoneThrottled[zone.guid] = nil
    end, throttle)
end
#

onObjectEnterZone, with a bunch of calls to other functions; I figured posting them all would be a bit much. Besides, they're all gated behind if checks and shouldn't run unless a relevant object is entering a relevant zone:

function Z.onObjectEnterZone(zone, object)

  if S.getStateVal("allZonesLocked") == true then return end
  if isZoneThrottled[zone.guid] == true then return end

  if Z.isPlayerCardZone(zone) then
    if S.getStateVal("playerZonesLocked") == true then return end
    if S.isInPhase({"AFTERMATH", "END"}) then return end

    throttleZone(zone, function()

      U.timeSequence({ -- runs functions with 0.5s between
        function() Z.alignCard(zone) end,
        function() Z.refreshUI(zone) end
      })

      if not S.isInPhase("PLAY") then return end

      if object.type == "Card" then
        getObjectFromGUID(G.charQueue).call("Populate_Queue")
      end

      if isCheckingSuspicion
        and (object.hasTag("ExhaustTorpor")
             or object.hasTag("Infamy")) then
        Wait.time(function()
          if Z.isTorpored(zone) or object.hasTag("Infamy") then
            Z.hideSuspicion()
            getObjectFromGUID(G.battlegrounds.d).call("EndTrial")
          end
        end, 1)
      end

    end)

    return
  end

  if Z.isTokenClearZone(zone) then
    if object.hasTag("Power")
      or object.hasTag("Infamy")
      or object.hasTag("ExhaustTorpor") then
      object.destruct()
    end
    return
  end

  if not S.isInPhase("PLAY") then return end

  if Z.isBattlegroundZone(zone) then
    if G.highlighters.bgIndicators[object.guid] ~= nil then return end
    local bg = Z.getBattlegroundOfZone(zone)
    if bg == nil then return end
    bg.call("DimBattleground")
    bg.call("OnObjectEnterZone", { zone = zone, object = object })
  end

end
#

Annd Z.onObjectLeaveZone()

function Z.onObjectLeaveZone(zone, object)

  if S.getStateVal("allZonesLocked") == true then return end

  if Z.isPlayerCardZone(zone) then
    if object.hasTag("Character") or object.hasTag("ClanLeader") then
      object.removeTag("TopChar")
      object.removeTag("Monstrosity_1")
      object.removeTag("Monstrosity_2")
      object.removeTag("Monstrosity_3")
      object.removeTag("Monstrosity_4")
      object.sticky = true
    end
    if object.hasTag("ClanScheme") then object.setScale({ 1, 1, 1 }) end

    U.timeSequence({
      function() Z.alignCard(zone) end,
      function() Z.refreshUI(zone) end
    })
    
    return
  end

  if not S.isInPhase("PLAY") then return end

  if Z.isMainQueueZone(zone) then
    if object.type == "Card" then
      object.setRotation(U.fixRotation(object.getRotation(), 180))
    end
    return
  end

  if Z.isBattlegroundZone(zone) then
    if G.highlighters.bgIndicators[object.guid] ~= nil then return end
    local bg = Z.getBattlegroundOfZone(zone)
    if bg == nil then return end
    bg.call("DimBattleground")
    bg.call("OnObjectLeaveZone", { zone = zone, object = object })
  end

end
idle hound
#

How is your S variable storing it's data

#

And what is U.timeSequence

#

Actually it's really hard to tell you what might cause hierop with all this refractoref out of what you show here

vocal dagger
# idle hound How is your `S` variable storing it's data

In "core.state":

function S.getStateVal(key, subKey)
  local state = Global.getTable("gameState")
  if not state[key] then return state[key] end
  if subKey then return state[key][subKey] end
  return state[key]
end

function S.setStateVal(value, key, subKey)
  local state = Global.getTable("gameState")
  if (subKey) then
    if (state[key] == nil) then state[key] = {} end
    state[key][subKey] = value
  else
    state[key] = value
  end
  Global.setTable("gameState", state)
end

In the global script:

local gameState

function onLoad(script_state)
  local newState = JSON.decode(script_state)
  if newState then
    gameState = newState
  end
end

function onSave() return JSON.encode(gameState) end
vocal dagger
#

The zone listeners work perfectly well during play, no performance issues there. It's only when I activate the zones by setting their global event listeners to the functions that there's a delay of a second or two

vocal dagger
# idle hound And what is `U.timeSequence`

... and here's U.timeSequence() just because it's short ๐Ÿ™‚

function U.timeSequence(funcs)
  local stepSize = 0.5 -- default step of 0.5s
  local waitTime = 0
  for _, func in ipairs(funcs) do
    if type(func) == "function" then
      Wait.time(func, waitTime)
      waitTime = waitTime + stepSize
    elseif type(func) == "number" then
      -- can include a number, which becomes a delay in seconds
      waitTime = waitTime + func
    end
  end
end
glossy obsidian
#

When do you call the activate code that enables the events again? You said disable them, then call a bunch of stuff, then enable them again. Is this all in the same frame? Could the activation after doing the bunch of stuff still trigger the events afterwards, basically nullifying your attempt to disable them?

#

E.g., I'd assume something like this would still fire the zone event (didn't test though)

onObjectEnterZone = nil
spawnObjectData (...)
onObjectEnterZone = function()
end
stable orbit
#

At a first glance I can also not make out what causes your lag. You could add some log(os.time()) calls at various points to narrow down which part is taking long.

idle hound
#

bg.call("OnObjectEnterZone", { zone = zone, object = object })
I think this might be causeing a problem.

onObjectEnterZone (and onObjectLeaveZone are triggered for every object on every zone in every script
How many objects are these scripts on? And how many zones are there?

idle hound
vocal dagger
vocal dagger
# idle hound Can you elaborate this? What do you mean?

bg.call("OnObjectEnterZone", { zone = zone, object = object })
I think this might be causeing a problem.

onObjectEnterZone (and onObjectLeaveZone are triggered for every object on every zone in every script

I realized this, which is why I was so careful to gate everything behind if/then checks (that are themselves as efficient as possible --- i.e. a simple boolean flag set by something else, or a check for a tag, rather than a function that has to iterate through a bunch of objects to determine true or false).

In the case of bg.call("onObjectEnterZone"), that should only ever trigger if Z.isBattlegroundZone(zone) returns true, and only if the phase of the game (held in gameState) is "PLAY" (thanks to the if not S.isInPhase("PLAY") then return end gate earlier in the function) --- there are only three battleground zones, and Z.isBattlegroundZone(zone) is just a wrapper around a simple check to see whether zone.guid is contained in the G.zones.battleground table.

The zone listeners work perfectly well during play, no performance issues there. It's only when I activate the zones by setting their global event listeners to the functions that there's a delay of a second or two

Can you elaborate this? What do you mean?

Certainly. During play, when onObjectEnter/LeaveZone is triggered through normal gameplay (e.g. by dropping a card or token into the zone), there are no performance issues. The lag spike occurs when I trigger my ActivateZones() function, which is the partner to DeactivateZones():

-- in Global.ttslua
function DeactivateZones() -- Sets event listeners to nil
    onObjectEnterZone = nil
    onObjectLeaveZone = nil
end

function ActivateZones() -- Restores event listeners from Zones module
    onObjectEnterZone = Z.onObjectEnterZone
    onObjectLeaveZone = Z.onObjectLeaveZone
end```
I use these functions to toggle the zone event listeners off temporarily when I'm running through a sequence of automated functions that would otherwise cause the zone events to fire multiple times unnecessarily. (E.g. when setting up the game, I move a bunch of objects around programmatically, and found I'd run into performance issues there because those movements would often trigger a whole bunch of zone enter/leave events (cards being dealt from a central deck would  "sweep through" a bunch of zones on their way to a hand zone, causing each zone passed through to fire both `onObjectEnterZone` and `onObjectLeaveZone`). Or, when cleaning up the play space at the end of the game, simply deleting all of the tokens on the board would cause every zone's `onObjectLeaveZone` event to fire multiple times, once for each token --- so I `DeactivateZones()`, then delete the tokens, then `ActivateZones()`) 

What I can't understand is why simply setting the `onObjectEnterZone`/`onObjectLeaveZone` variables would "do" anything that would cause a lag spike.  (They don't trigger any events when I fire `ActivateZones()`, that was one thing I was able to test.)
#

As I said earlier, I was quite pleased with myself for coming up with what I thought was a super elegant method to toggle all of my zones on or off, in what I thought would be a super efficient way to do it. I'm perplexed as to why I was apparently wrong ๐Ÿ˜›

#

I should add that I do have a lot of zones in my game --- again why I was so careful gating those zone events, and why activating/deactivating the zones before I move a bunch of objects through them is necessary --- but they don't appear to cause performance issues during normal play. Only when I set the event listeners via ActivateZones() is there a lag spike

#

My current zones setup: The "battleground zones" are the three at top center; the "player zones" are all the small rectangles embedded inside the three big rectangles in the middle.

#

(You can see how dealing cards from the decks located on the red part of the playspace to the player hand zones would sweep through a bunch of player zones --- despite being only a half-inch or so off the surface of the table --- and would cause a bunch of events to fire, hence my idea to deactivate them first)

idle hound
# vocal dagger > `bg.call("OnObjectEnterZone", { zone = zone, object = object })` > I think thi...

right.
I have no idea if t's possible that tts queues the events when you set it to nil.
two things to try:

function DeactivateZones() -- Sets event listeners to nil
    onObjectEnterZone = function(...) end
    onObjectLeaveZone = function(...) end
end

This keeps it a callable function (which TTS expects to be present) but simply does nothing.
Or use a toggle boolean (like you already have many) instead of removing the function

vocal dagger
#

Just as a general question: Is my general approach to handling these global event listeners correct, or can you see any ways it could be improved? I.e. gating everything as much as possible behind simple boolean checks, to mitigate the performance hit that one should naturally expect from having so many zones?

#

(I realize "reduce the number of zones you're using" is an obvious way to improve things, but everything else works so well that, if I can figure out this single wrinkle, I shouldn't have to)

#

Same question re: my approach to activating/deactivating the zones --- is there another "best practices" way to do this or, again if I can solve this one issue, is my strategy a good one?

#

And thank you again for your time helping me; I realize I just dumped a lot of code on you to analyze, and I really appreciate you taking the time to help

idle hound
#

It's a lot more involved then I like for TTS (IMO players should play the game, not the game should play the game).
I think just preventing the function to run on a boolean flag is better then rewriting the function. But no data to back that up, it's preference.
I don't know why you have that many zone or what it is they are all tracking, so hard to answer the rest.

vocal dagger
# idle hound It's a lot more involved then I like for TTS (IMO players should play the game, ...

I agree with you in principle --- in fact I've had to curb my love of automation to prevent myself from automating things that are better left to the players, to preserve that tactile feel of playing a board game.

The many player zones are meant for individual card placements (during play, you recruit characters and place them in a hierarchy in front of you), and their functionality is largely for fun visual spice, like applying UI overlays to represent various effects. Here's an example of what I mean -- the character is being targeted with a card that declares them an 'Enemy', and remains stuck to that character; the zones let me do this:

#

(I originally had this sort of thing working with collisions, but found that they really impacted performance when a player dropped several items onto a card at once --- i.e. with a box selection of tokens or the like; zones work much more smoothly during play. That throttleZone function I added is probably unnecessary (it's a holdover from when I was using collisions, to try to fix the "multiple items at once" issue), but it works and I figure if it's not broke don't fix it ๐Ÿ˜‰ )

idle hound
#

I almost never use zones, I prefer casts, but I get the point.

vocal dagger
#

How do you trigger the casts? Collisions?

idle hound
#

Physics.cast()

vocal dagger
#

Sorry, no, I know how to do them actively; I was asking how you get them to trigger on something happening in the game space -- as an event listener. If you don't use zones, do you use collisions or is there another way I'm not aware of?

#

Or, backing up in case I'm asking the wrong question: You said you preferred casts over zones, so I assumed you meant that there was a way to do what I'm doing with casts, instead of zones

idle hound