clojure-gamedev

2021-11-08T14:36:55.040800Z

Because it allows you to decouple an idea of what to do with how to do it. Say for example you want to damage your player. You send an event which says [:damage 100 :target :player] or something. Now when you take the damage, it's in the player code that you can check to see if you're currently in invincibility frames, or if the player has a damage-reducing effect, and you can decide whether or not to apply knockback, etc. This has the advantage over doing it manually in every place that you damage the player of reduced code duplication, but also because it keeps code collected in one place the event you want to have different damage handling based on the source (just add a :source kwarg to the event you send)

2021-11-08T14:36:58.041Z

@nbtheduke

2021-11-08T14:41:05.042400Z

hmm interesting. maybe this is my bias of only programming turn-based games with limited reaction timing, but i guess i don’t see how that’s different from just having a damage function that does that directly. by having the layer of indirection, you have to now track when things are happening and if/how they’ll be applied

2021-11-08T14:47:01.044300Z

It's also possible (if not likely) that eventually more things than just your target will want to pay attention to that, and only sometimes. Eventually it'll make your damage function very unweildy. Yes you could just have a damage function, but part of the idea here to passing the state of the world but only returning what you're changing (and limiting what any function is allowed to change) is that it prevents the game state from effectively becoming a large set of mutable global variables.

2021-11-08T14:47:25.044900Z

Because if you pass the state map in to every function and return a new version of it from every function, you just have mutable globals with extra steps.

2021-11-08T14:48:30.045800Z

The idea more or less is that you want to contain your manipulation of the world, and ideally make it traceable, that is you can by stepping through your code always know what part of the state may change at any function.

2021-11-08T14:50:48.047500Z

Depending on how you decide to handle events, they can mostly either end up being the only things that mutate the world state, and if you have a fixed point in the frame where that happens it can make it easier to follow, or you can set up the events so that entities can "listen" to them, which means that nothing ever mutates a portion of the state besides itself.

2021-11-08T14:51:16.048100Z

More or less I see the difference between the two as an optimization problem and depends on how many events you'll have per frame on average and what sorts they will be.

2021-11-08T14:52:16.049600Z

hah yeah, i’ve never worked on a game where i care about frame timing. it’s the “beauty” of web turn-based games, lol

2021-11-08T14:52:21.049800Z

my game engine just supports global events and the "default" way to implement event handlers is to make a multimethod reducing function that takes a game state and an event, "runs" the event on the world, and returns the new world. However it's pretty easy to build a system which will instead just index the events every frame on their targets and allow entities to listen to them with good performance.

2021-11-08T14:54:11.051300Z

Yeah, I am in-progress on a general-purpose game engine that I'll be using for game jams and eventually more. I've put a lot of effort into the frame timing and rendering systems to try and make them extensible and ensure that simulation happens at a fixed timestep etc.

👍 1
2021-11-08T14:54:30.051800Z

hah yeah, coffii is sick and i can’t wait to see your engine

2021-11-08T14:54:39.052200Z

Which actually has given me the first thing I've wanted to use reducers for

2021-11-08T14:57:01.054700Z

My game engine is public already

2021-11-08T14:57:17.055400Z

there's not anything particularly cool written in it yet but there's a lot of machinery in it

2021-11-08T14:57:36.056100Z

I'd be happy to go over my event systems etc. and how I'm working to ensure super consistent and accurate frame times to make butter-smooth rendering.

2021-11-08T14:58:18.057400Z

Which this engine is actually not using coffi (yet). I'll probably wait to port it until after I've figured out how I want to generate gl bindings in coffi.

2021-11-08T15:00:04.059200Z

let me see if i can explain/walk through this to see where the misunderstanding is coming from: the game state is a giant map with a nested map :players of :player-id to the player map (along with all sorts of other stuff). player 1 performs some action that damages player 2. in my scheme, i pass in state and either positional args or a map of args to the damage function, and it performs the necessary work to see if invincibility/etc applies, and if not, it performs the actual change to the state map: (update-in state [:players (-> args :target :player-id) :health] - (:amount args)) (or i’d put all this into local variables, you get the idea). and the calling function would say something like (-> state (get-input) (damage args) (print-messages) (etc)) . and at all times i’m just passing around a hashmap.

2021-11-08T15:02:25.061300Z

in your scheme, i pass in state and the map of args to a damage-event function, which would do something like (update state :events conj damage-event) ? and then the calling function would say something like (-> state (get-input) (damage-event args) (process-events) (print-messages))?

2021-11-08T15:04:26.064600Z

and process-events would pop off one of the events, and then have a multimethod that handles each type of event, and that would be the implementation of the damage function: (defmethod event-handler :damage [state args] ...)

2021-11-08T15:08:11.070200Z

So yes, damage-event would just add in a new event to the state. There are two ways to handle processing events though. There's the way you just described (where a key feature is that process-events is performed as one of the top-level steps in your game loop and is one of a very small number of functions which can modify the whole game state), but there is also the way where your 'handling of the event' is just in your player entity update code, it looks at the list of events and does what code it wants to update itself based on the events in the queue.

2021-11-08T15:08:57.071200Z

Usually doing it the second way will require having your events be a frame out of date though.

2021-11-08T15:14:33.072200Z

interesting. i feel like i’d want to see this in action cuz at the moment it doesn’t feel helpful, but i also don’t have almost any experience in this field so it can be hard to see the benefit from contrived examples

2021-11-08T15:15:23.072400Z

thanks for the explanation

2021-11-08T15:22:05.072700Z

Like I said, it's basically just a way to avoid shared mutable state.

2021-11-08T15:22:20.073100Z

And it's basically shared mutable state because every function can modify it at any time, it just happens to not use an assignment operator.

2021-11-08T15:23:29.074300Z

In small games it's basically pointless to split things up this way, but it's the same idea as e.g. re-frame. You want to separate what to do from how to do it.

2021-11-08T15:26:57.074900Z

For anyone making a game where framerate matters, these two articles are very important if you want your game to feel smooth: https://gafferongames.com/post/fix_your_timestep/ https://frankforce.com/frame-rate-delta-buffering/

1
2021-11-08T15:27:53.076Z

The first one is a way for you to decouple your rendering and simulation so that you can have stable, deterministic simulations with rendering that goes as fast as your machine allows.

2021-11-08T15:28:53.077100Z

In my engine I applied this by actually separating out my simulation and rendering to two different threads, and the simulation produces simulation frames to a queue and the rendering thread consumes those simulation frames and produces rendered ones.

2021-11-08T15:29:31.077900Z

The simulation steps at a fixed timestep, and the render thread consumes the latest simulation and (if it's provided) interpolates between two game states to render the appropriate thing for a given frame.

2021-11-08T15:31:16.079600Z

Then the second article goes into how even this isn't quite enough to have buttery smooth motion because even if you consistently hit your frame time window, you're probably actually rendering the wrong part of the frame, that is you're rendering "now" in between frames, which is variable because it doesn't always take the same amount of time to render a frame, where instead you should be rendering what the world will be at the next vblank.

2021-11-08T15:31:59.080300Z

Which only really applies when you have vsync enabled, but is critical to getting the game to feel buttery smooth and have no stutters.

2021-11-08T15:33:05.081Z

This later one is more important at lower framerates, but since this is clojure any more complex game is probably going to target a lower framerate like 30-60 fps.

2021-11-08T18:38:58.084800Z

This decoupling of render and simulation state has caused some things that I have had to build additional infrastructure around though. For example since I don't know if a particular simulation frame has been rendered yet, I have no way to tell the renderer to unload a model once I'm done with it and be sure that it won't be used in the next couple of rendered frames that haven't caught up to the current simulation. So I had to build an entire set of systems that work together to guarantee that render events are associated with a particular simulation frame and it's guaranteed that every event will be completed before their current simulation frame or any later frame is rendered, since for example there's no guarantee that each simulation step will get rendered (imagine a simulation step rate of 100 steps per second, but a monitor with a 60hz refresh rate).

2021-11-08T18:39:56.085300Z

All this to allow me to have the simulation send an even that says "unload model x" and have it act correctly.

2021-11-08T18:48:05.085600Z

that sounds complicated as heck

2021-11-08T18:50:20.086Z

it is, but it's needed to have buttery smooth frames, which I quite like.

2021-11-08T18:50:51.086500Z

and I also like have my simulation and rendering on different threads so that I can benefit from the fact that I have immutable game states

2021-11-08T19:05:27.087400Z

(although I imagine eventually I'll want to make a synchronization primitive for mutable things I want to keep in my game state to ensure I don't render a version that's currently being mutated)