Fork me on GitHub
#clojure-gamedev
<
2020-12-01
>
Noah Bogart14:12:38

Hey all, I’m experimenting with rewriting my game to be “pure”/stateless: pass in the current game state and player action to a function, get the next state back. Are there any resources out there for structuring the code so the number of helper functions doesn’t explode?

Noah Bogart14:12:48

Right now the game is built on passing a state atom around and doing a fair amount of swap!ing inside the functions

paul.​legato18:12:23

It depends a lot on how your game is architected. One technique I’ve seen is to skip atoms entirely, and make a single recursive “run game loop” function that takes the entire state as an argument.

paul.​legato18:12:21

https://prog21.dadgum.com/23.html Article on “purely functional retrogames,” by an 80s console game programmer

Kelsey Sorrels19:12:33

For my roguelike I went passing in gamestate and producing a new gamestate as output. It works fairly well, but I can see how some may not be interested in adding an additional parameter to many functions.

Noah Bogart19:12:06

well, we already pass around the game state atom because it's a web game so there's a map of game-id to game atom, and when a player makes a move, we grab their atom and pass it into the corresponding action function (along with any other relevant stuff), so i don't mind passing around state as the first variable in all functions lol, and because it's a turn-based card game, we don't need the "run game loop"

Noah Bogart19:12:53

i'm more asking about, like, how to handle when a bunch of things are changed in a single place: "draw 2 cards" means we have to update both card objects with the new locations, trigger "player 1 drew 2 cards" event handlers, move the card objects from the deck to the hand, write the message "player 1 drew 2 cards" to the game log, etc

Noah Bogart19:12:01

this is pretty easy when using an atom, because like a classic variable, i can just pass it around and not have to nest let bindings, or even just call swap! manually to change a flag

paul.​legato19:12:28

It sounds like the overhead of an atom is maybe not worth it, if it’s all single threaded and you don’t mind passing it around everywhere?

Noah Bogart19:12:34

@paul.legato i'll read that article, thanks for the link

Noah Bogart19:12:23

yeah, it's all single-threaded, and because it's a web game, rendering is handled by clojurescript/reagent client-side

paul.​legato19:12:37

One way to make it pure functional involves discarding the notion of “objects.” It’s all just one big map, with deeper maps nested in it. Every bit of state is somewhere in that map.

paul.​legato19:12:03

The game loop is (from that article):

repeat forever {
   get user input
   process one frame
   draw everything on the screen
   wait until a frame's worth of time has elapsed
}

paul.​legato19:12:54

get user input returns “player pressed the draw 2 cards button”. process one frame takes that + the current huge state map as its arguments, and returns a new state (with 2 cards now drawn). Draw updates the screen to show two cards drawn, etc.

paul.​legato19:12:49

This should work well with reagent-type reactive UI rendering. It’s a very similar structure.

paul.​legato19:12:30

The map that tells us the state of the cards doesn’t have to know where they are drawn on the screen. That’s the UI renderer’s job. Likewise, the UI renderer doesn’t need to know how two more cards got added to the state map.

paul.​legato19:12:43

Similarly, there are no event handlers in this model. That is all driven reactively, changing the state map by returning a new state map from some “process one frame” function.

paul.​legato19:12:14

e.g. initial state:

{:deck [ {:10 :hearts} {:J :clubs} ... ]
 :player_hand [{:k :hearts} ... ]}
Call (process-frame current-state user-input), get a new state back like:
{:deck [ {:10 :hearts} ... ]
 :player_hand [{:k :hearts} {:J :clubs} ... ]}

Noah Bogart19:12:57

yeah, that sounds about right