RUBI Gamestate Stack

Showcase your libraries, tools and other projects that help your fellow love users.
mateusak
Prole
Posts: 10
Joined: Thu May 02, 2019 4:08 am

Re: RUBI Gamestate Stack

Post by mateusak »

pgimeno wrote: Thu Jun 06, 2019 1:19 am I've now taken a look at other similar libraries. Yours is the only one I see that has separate event handlers while running in the background, which seems interesting. However, I wonder if it wouldn't be better calling the same callbacks but letting them know whether they're on the background or not, and passing back whether the event was handled.
Interesting. It's riveting how simple ideas don't come to mind sometimes. It could surely help with code duplication, if something had to be executed regardless of stack position. I do think it's at the expense of a little bit of clarity, but I think it's worth it.
pgimeno wrote: Thu Jun 06, 2019 1:19 am I don't like the idea of having the game screen stacked on top of the menu screen; I don't think the menu has to be preserved as if it was "paused" while you are playing and "resumed" when you finish. Also, if setting a pause state doesn't call the draw method of the previous stack, handling anything but a full-screen pause is going to be more complicated than adding the pause as another state in the current stack.
That was one of my main concerns while writing this. I wanted to be able to have each menu as a gamestate (changed the name BTW :D) and also to be able to pause without hassle. Ultimately I decided update, input and draw had to be controlled separately. At first I was also obliging gamestates to be objects (with a .new function), but later I realized that meant whenever a menu opened a new object had to be created. Instead, now you can save a menu gamestate in a variable and just push it in the stack whenever you want, pretty much without cost.

I'll definitely look into implementing your suggestion, if you have any more of them nice ideas I appreciate it :)
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: RUBI Gamestate Stack

Post by pgimeno »

mateusak wrote: Thu Jun 06, 2019 2:33 am I'll definitely look into implementing your suggestion, if you have any more of them nice ideas I appreciate it :)
It wasn't my idea, it was in s-ol's library actually. I liked the design. It doesn't differentiate between event types; instead, you can decide per event type if you want to call events in order from bottom to top of the stack (adequate for draw events) or from top to bottom (adequate for control events). It tracks that with an internal order[eventtype] variable and a method to change it.

I suggest reading the thread. Someone proposes an isFocused function to differentiate whether it's the top of the stack state or not. s-ol's response is to use the argument passed between events for that purpose, but I don't like that much, as I don't see it working with bottom-to-top events like draw, where it's probably most useful, so the proposal is a good idea.
mateusak
Prole
Posts: 10
Joined: Thu May 02, 2019 4:08 am

Re: RUBI Gamestate Stack

Post by mateusak »

pgimeno wrote: Thu Jun 06, 2019 10:09 am It wasn't my idea, it was in s-ol's library actually. I liked the design.
You're the one who brought it to me. Anyways, I'm having a hard time deciding my approach. A messaging system sounds interesting but I don't see use for it on normal callbacks like update, draw, etc, and there's a special problem with draw since it calls it backwards, so it's impossible to use a messaging system to control focus. I can see use for it in special stack related callbacks (pushed, popped, emerged, submerged): say that a dialog is a gamestate that asks a question that can be answered 'yes' or 'no'. Once chosen, the popped callback can send a message to the emerged callback of the gamestate below with what was chosen.
pgimeno wrote: Thu Jun 06, 2019 10:09 am It doesn't differentiate between event types; instead, you can decide per event type if you want to call events in order from bottom to top of the stack (adequate for draw events) or from top to bottom (adequate for control events). It tracks that with an internal order[eventtype] variable and a method to change it.
That's fine if there's no focus control like mine. If you see my code you'll see that the callbacks are actually calls to stackcall(), but they need extra arguments for focus control that would just complicate things for the end-user. If I replace focus control with a messaging system it's possible but then drawing blocking is inviable. If I use both, then it's just too complex.

As for merging normal and weak callbacks, I tried and the impact in clarity is actually pretty high. The thing is, in 80% of the cases you don't want anything to execute on weak callbacks, which means you just have to add 'if not active then return end' for every callback. In cases you do want weak callbacks, then an ugly if else is needed. There's also the issue of error. It's much more likely for errors to happen with merged callbacks, since all it takes is not checking if it's active.

In the midst of these points, I shall strip away the bad and conquer victory with the steady hand of justice.
User avatar
pgimeno
Party member
Posts: 3544
Joined: Sun Oct 18, 2015 2:58 pm

Re: RUBI Gamestate Stack

Post by pgimeno »

mateusak wrote: Thu Jun 06, 2019 3:10 pm That's fine if there's no focus control like mine. If you see my code you'll see that the callbacks are actually calls to stackcall(), but they need extra arguments for focus control that would just complicate things for the end-user. If I replace focus control with a messaging system it's possible but then drawing blocking is inviable. If I use both, then it's just too complex.
Here's my idea through an example (I may have used too much OOP):

Code: Select all

function lower_level_state:draw()
  if not self:focused() then return end -- don't draw if not the top state of the stack
end

function top_level_state:keypressed(key, scancode, isrepeat)
  if key == "return" then
    -- do stuff
    return true -- "stop processing messages"
  end
end

function lower_level_state:keypressed(key, scancode, isrepeat)
  if key == "space" then
    -- do stuff - this code may execute
  end
  if key == "return" then
    -- this code won't execute if top_level_state runs on top of this
    -- (the whole callback won't be called in this case)
  end
end
Implementation wise, the idea (not necessarily the final code) would be roughly like this:

Code: Select all

function states:focused()
  return self.stack[#self.stack] == self
end

function states:call_event(event_name, ...)
  if self.bottom_up[event_name] then
    for i = 1, #self.stack do
      local state = self.stack[i]
      if state[callback] then
        -- for bottom-up, propagation isn't stopped
        state[callback](state, ...)
      end
    end
  else
    -- Loop from top to bottom
    for i = #self.stack, 1, -1 do
      local state = self.stack[i]
      if state[callback] and state[callback](state, ...) then
        -- for top-down, returning true causes to automatically stop propagation
        break
      end
    end
  end
end
This way, you don't need to clobber the functions with 'if handled then return end'; you just return true when handled, and that stops propagation, which looks clean to me.

I think that in most cases, you'll want the background draw() to be executed, therefore the cases where you need to check self:focused() systematically will not be many. It has the additional advantage that you can do something special in the middle of the event when the state isn't focused, without needing to duplicate the code.

For example, imagine you want to darken the background while paused, but not the foreground. If there's no function to tell you if focused(), you would need to do something like this:

Code: Select all

function game:draw()
  camera:set()
  self:drawBackground()
  self:drawForeground()
  camera:unset()
end

function game:weakDraw()
  camera:set()
  self:drawBackground()
  camera:unset()
  love.graphics.setColor(0, 0, 0, 0.7)
  love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions())
  camera:set()
  self:drawForeground()
  camera:unset()
end
i.e. you need to duplicate the code, to insert the darkening in the middle. This example is very simple, but the idea is that it goes against the principles of DRY.

With a focused() function, no code duplication is necessary:

Code: Select all

function game:draw()
  camera:set()
  self:drawBackground()
  if self:focused() then
    camera:unset()
    love.graphics.setColor(0, 0, 0, 0.7)
    love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions())
    camera:set()
  end
  self:drawForeground()
  camera:unset()
end
mateusak
Prole
Posts: 10
Joined: Thu May 02, 2019 4:08 am

Re: RUBI Gamestate Stack

Post by mateusak »

pgimeno wrote: Thu Jun 06, 2019 11:53 pm This way, you don't need to clobber the functions with 'if handled then return end'; you just return true when handled, and that stops propagation, which looks clean to me.

I think that in most cases, you'll want the background draw() to be executed, therefore the cases where you need to check self:focused() systematically will not be many.
The only issue I have with that is the draw exception. I don't like inconsistencies. I actually wrote the code for what you're saying already. Maybe one day I'll learn to love it, like a long forgotten child.
pgimeno wrote: Thu Jun 06, 2019 11:53 pm For example, imagine you want to darken the background while paused, but not the foreground. If there's no function to tell you if focused(), you would need to do something like this:

Code: Select all

function game:draw()
  camera:set()
  self:drawBackground()
  self:drawForeground()
  camera:unset()
end

function game:weakDraw()
  camera:set()
  self:drawBackground()
  camera:unset()
  love.graphics.setColor(0, 0, 0, 0.7)
  love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions())
  camera:set()
  self:drawForeground()
  camera:unset()
end
This is a very poor example, my man. I mean if you open a menu you'd darken eveything. I'm thinking of how weird it would be if Mario's background just went dark and yet the foreground did not. Anyways, you can still do that if you must:

Code: Select all

function drawEveything(darkenBackground)
  camera:set()
  self:drawBackground()
  
  if darkenBackground then
    love.graphics.setColor(0, 0, 0, 0.7)
    love.graphics.rectangle("fill", 0, 0, love.graphics.getDimensions())
  end
  
  self:drawForeground()
  camera:unset()
end

function game:draw()
  drawEverything(false)
end

function game:weakDraw()
  drawEverything(true)
end
Which IMO is much better as far as maintaining code goes. The darken should actually be part of drawBackground TBH. I know just like this it seems excessive, but in a real life scenario where you wouldn't just have this piece of code, it makes perfect sense.

The thing about DRY is that sometimes it's best to repeat yourself if the things being repeated are fundamentally different and prone to diverge later. Things that are together change together. If DRY is taken without care, then changing a small thing will lead to series of changes and bugs because things that should've been kept separate were DRIed.
User avatar
dusoft
Party member
Posts: 492
Joined: Fri Nov 08, 2013 12:07 am
Location: Europe usually
Contact:

Re: RUBI Gamestate Stack

Post by dusoft »

I am adding mine to the thread:
https://github.com/nekromoff/love-state-switcher

Quite old (and I would solve some things differently these days...), but very simple.
Post Reply

Who is online

Users browsing this forum: No registered users and 42 guests