Log In  

In a game that I'm working on, I tried to create some 'magic' for my game states by triggering a state change when a global variable is modified:

-- ditto for _update and _init
function _draw()
 cls()
 local action = {
  [0] = title_draw,
  [1] = menu_draw,
  [2] = marathon_draw,
  [3] = gameover_draw,
 }
 action[game_state]()
end

-- later, something changes game_state

What I ran into was what I assume to be scoping issues; variables were present in the game state that shouldn't be, or had stale values, etc. So I'll try to phrase my question in a way that might make sense: How do I cleanly separate game state from the API? I want to avoid massive _init, _update, and _draw functions if possible, so I'm able to focus on one game state at a time.

My apologies if this is a stupid question. I don't have a formal background in programming, so everything I know is self-taught and as a result I don't know the jargon to look for to find my answer.

Solution

The answer is called a "closure". See the below post by @freds72 and a test cart I made to prove it works.

Thanks to everyone who helped me out!

P#65070 2019-06-08 03:42 ( Edited 2019-06-10 01:40)

1

Think functions! A construct I use is something like that:

function make_title_state()
 — say title screen lives for 2s
 local ttl=60
 return {
   update=function(self)
     ttl-=1
     if ttl<0 then
      — example switch to another state
      return make_game_state()
     end
     return self
    end,
    draw=function()
     ...
    end
    }
end

— initial state
local state=make_title_state()
function _update()
  — update (and switch state)
 state=state:update()
end

function _draw()
 state:draw()
end

note how ttl can be used to maintain state (look for closure concept).
You can have many more variables captured this eay.

P#65074 2019-06-08 13:11 ( Edited 2019-06-09 14:51)

@freds72 so you're using multiple return values to create a table on the fly? 'state' later has functions as members... And ttl, is that similar to Time To Live for networking packets? It looks like a closure is good for passing state around. I'm not trying to pass state around though, just switch it cleanly so there are as few stray variables as possible. I'm not sure how I would use a closure to achieve separation.


Perhaps I should better outline the structure I'm aiming for.

There's the standard API functions, which dispatch to specialized versions of the same functions, designed to work only in their given game state:

_update, title_update, menu_update, etc.
_draw, title_draw, menu_draw, etc.
_init, title_init, menu_init, etc.

So the game does _init(), determines that it's in state 0, calls title_init(), then _update() is called, it determines game state is 0, calls title_update(), then the same for _draw(), etc.

I've been storing state in the 'init' functions as regular variables, but last night before going to bed I found an oversight in my usage. I used to switch game states with:

game_state = foo
_init()

Since the structure picks up on game_state changing, I think there was a (very small) period of time where the game's state is weird before trying to call _init (which would then dispatch), creating the side effects.

I decided that the game state should be explicitly changed rather than constructs like 'game_state += 1'. It's more clear what's going on. So going back to the title screen is now:

title_init()

The game_state variable is then updated at the beginning of the specialized init() function, where it belongs.

I'll see what I can do to write a test cart to illustrate what I mean. The game I'm working on is unfinished and not in a state (lol) ready for sharing.

P#65080 2019-06-08 18:31
1

I do something like this:

-- title screen functions

function title_init()
  _update=title_update
  _draw=title_draw
  -- and whatever other initialization
end

function title_update()
  if btnp(4) then
    game_init()
  end
end

function title_draw()
  -- ...
end

-- in-game functions

function game_init()
  _update=game_update
  _draw=game_draw
end

function game_update()
  -- ...
end

function game_draw()
  -- ...
end

-- just set _init, and it'll handle the others

_init=title_init
P#65085 2019-06-08 21:13 ( Edited 2019-06-08 22:54)

@Saffith: That doesn't appear to work for me, on PICO-8 0.1.12c. It runs what I tell it to, but then exits. I think it's not correctly telling PICO-8 where to plug the API. That is, _update and _draw are being set in game_init, and subsequently ran, but then it exits the game_init function and so the default _draw and _update aren't found, which causes it to exit. The call to print() is also showing that 'msg' is undefined. That's how I understand it, anyway. Please correct me if I'm wrong.

Here's what I used to test it:

Cart #yonujasiyo-0 | 2019-06-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

P#65086 2019-06-08 22:43 ( Edited 2019-06-08 22:50)
1

Sorry, I screwed up my example. I've corrected it. No parentheses on the assignments; you don't want to call the functions.

Wrong:

function game_init()
  _update = game_update()
  _draw = game_draw()
  msg = "doot"
end

Right:

function game_init()
  _update = game_update
  _draw = game_draw
  msg = "doot"
end
P#65087 2019-06-08 22:55

@Saffith: Thanks for the correction! It appears to work now. Have you noticed any weird side effects in your games as a result of this structure? Can you jump around game states without much leakage?

Here's another cart I used to test states and scopes. This time, it switches all functions over and seamlessly changes states, but the 'msg' variable is still present when I hit btn 4 to advance to the "menu" phase.

To test it, hit X once to see it switch to the 'menu' phase, which should show "hai", with "doot" under it. If you press X again, it'll switch back to the 'game' phase, except now the "hai" is present, below the "doot". This suggests that simply defining variables puts them in the global scope, even if they're defined in a function. So I guess that means the 'local' keyword should be used practically everywhere, except when you actually need some sort of global state.

Cart #panimabufo-0 | 2019-06-08 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

(also a sidenote to @zep: inline code blocks don't appear to work. They instead highlight the whole line after the opening backtick, even if only a small phrase is wrapped in the backticks.)

P#65088 2019-06-08 23:16
1

Unless you work around it, the update and draw functions will be mismatched on the frame of a state switch. In that example, the last frame of game_update is drawn with menu_draw instead of game_draw and vice versa. As long as the draw function doesn't depend on some state that isn't valid between init and update, that shouldn't be a problem. Well, and you may want to have the init function clean up any stale data that would affect drawing.

P#65090 2019-06-08 23:54 ( Edited 2019-06-09 00:06)

@Zig I am not really using multiple return values, rather returning a table that contains all the necessary functions to hande update and draw, having all the state variables owned by the closure (that ttl example var).
With your code you will still end up with state specific variables declared in the global scope.

P#65098 2019-06-09 07:08

@freds72: My bad for not totally understanding what you were doing. I'll try to wrap my head around it and figure out how to use it. If it prevents global state being thrown around then it sounds like what I'm after.

P#65099 2019-06-09 09:34

Okay, I think I have it mostly figured out now. @freds72's closure idea appears to package everything for a game phase in a single function. Its local members are visible to the defined functions, and the state gets correctly reset when switching states because the API is hooked up to these member functions.

I wrote a test cart that has a movable sprite and some text in one phase, and another piece of text in the other. Press X to cycle between phases, and move the FOE around with the arrow keys. I put the same tests for msg2 and msg to prove that state isn't being kept between swaps. The FOE's position resets when you switch back to it, too, further suggesting that state is cleanly wiped.

Cart #nejakufebu-0 | 2019-06-09 | Code ▽ | Embed ▽ | License: CC4-BY-NC-SA

The only thing I'm not clear on now is how the _update function works by redefining 'state' to the return value of its update() member.

P#65108 2019-06-09 18:17 ( Edited 2019-06-09 18:22)
1

yep- you got it!
state holds the current game state instance - the idea is that the current state can either return itself (eg still active) or a new game state.

P#65109 2019-06-09 19:37 ( Edited 2019-06-09 21:09)

Thanks for the help, guys. I can't wait to try this out on a real game. Maybe next weekend! :)

P#65115 2019-06-10 01:24 ( Edited 2019-06-10 01:24)

bumping this as it’s a common question!

P#105487 2022-01-21 22:34

[Please log in to post a comment]

Follow Lexaloffle:          
Generated 2024-03-28 23:30:08 | 0.041s | Q:37