Skip to content

Instantly share code, notes, and snippets.

@saikyun
Last active April 19, 2021 17:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save saikyun/b83cddafd59e674ab103f01e10181940 to your computer and use it in GitHub Desktop.
Save saikyun/b83cddafd59e674ab103f01e10181940 to your computer and use it in GitHub Desktop.
Different ways to handle interaction and rendering

Different ways to handle interaction and rendering

A list of different ways to deal with interactions (mouse clicks, keyboard input, network events) and rendering of graphics (gui elements, games).

All example code is written in janet.

FRP (old elm style)

Description

Inspired by how elm used to do FRP (https://youtu.be/Ju4ICobPNfw) Makes it possible to act on events and state in a functional way, allowing the user to use map/filter/reduce on values over time. State is a single value, e.g. a table (janet’s hashmap). Rendering is a function taking the state as argument. No mutation is done during rendering. Handling of interaction (e.g. mouse clicks) is done using s which can be resumed in order to check for new values. Then one can declare chains of function that will mutate state depending on which s have changed since last time.

Functions

  • frp/reduce
    • acts like reduce but on an <unknown>
  • frp/map
    • acts like map but on an <unknown>, i.e. transforming values using a function
  • - a collection over time - values are produced inside it, e.g. mouse clicks - you can “pull” values from it
  • resume (on janet fibers)
    • Pulls on a <unknown>, causing modification of the state
    • This means no functions are called until they are needed

Pros

  • separation of state and rendering (this is already done in the def state part)
  • as long as one has the same start state and events, it’s possible to replay the progression of the program
  • efficient, it is “lazy” in the sense that it will only run following functions if any values have been produced by <unknown>
    • specifically, easy to only rerender when something changed

Cons

  • a bit hard to understand
  • functions acting on <unknown> vs regular functions are kind of similar, possibly easy to mix up
  • having to think about <unknown> as a lazy list over time is possibly a bit mind boggling

Example

# the state of the program
(def state {:buttons [{:hitbox @[810 30 100 100]
                       :render |(draw-rectangle-rec ($ :hitbox) :red)
                       :f |(print "red!")}
                      {:hitbox @[860 0 100 100]
                       :render |(draw-rectangle-rec ($ :hitbox) :blue)
                       :f |(print "blue!")}]})

# first run frp/reduce (which is similar to reduce)
# so for each click in `mouse-click` (an <unknown> which will yield mouse clicks)
# filter out the buttons that that were hit. `pos` is mouse position from `mouse-click`
# $ is an element in `(state :buttons)`
(->> (frp/reduce (fn [buttons pos]
                   (filter |(hit-me? pos ($ :hitbox)) buttons))
                     (state :buttons)
                     mouse-click) # mouse-click is an <unknown>

     # after the above step, we end up with an array of
     # all buttons that were clicked.
     # so we select the last one (same as the one rendered last)
     (frp/map last)

     # then, if there was a hit, we run the function :f
     (frp/map |(when-let [f (get $ :f)] (f)))

     # resume just "gets the ball rolling" / pulls on the above
     # i.e. run all of the above, check for clicks, if no clicks happened
     # nothing will happen. it won't run `filter` or `last` or anything after it
     resume))

Backwards rendering

Description

Calls a list of functions, with each function rendering to its own render texture. Each function can contain both rendering and interaction logic.

This means that the first function will be first to react to e.g. mouse clicks, as to disallow for later functions to use the same input. The most obvious case would be when two gui elements overlap, and you only want to trigger the one on top. In this situation, you would have a list of functions:

(def fns [button-above
          button-below])

(def render-textures @{button-above ...
                       button-below ...})

(loop [f :in fns]
  (begin-texture-mode (render-textures f))
  (f) # checks for inputs & renders
  (end-texture-mode))

If just rendering normally, this would mean that button-below renders on top of button-above. To avoid this, they render to a texture. Then afterwards, all render textures are rendered in reverse order:

(map render-texture (map render-textures (reverse fns)))

Pros

  • the functions can contain arbitrary code, very few restrictions
  • functions look very similar to “naive” raylib code, i.e. easy to just start creating components

Cons

  • performance costs of render textures (gpu memory, 2x the rendering)
  • need to declare size of render textures ahead of time
  • no built in way to deal with caching / avoiding rerendering / recalculations
  • consuming mouse clicks is up to the function, i.e. easy to accidentally eat input / forget to eat input

Array of tables / oop-style

Description

Put a bunch of tables into a list, each table containing an update function and a render function (similar structure as the state in the FRP example). Loop through all the update-functions from bottom to top, then loop through all rendering functions from top to bottom.

(def objs [@{:id :below
             :render (fn [self] ...)
             :update (fn [self] ...)}

           @{:id :on-top
             :render (fn [self] ...)
             :update (fn [self] ...)}])

Alternatively, put a :z-value on each table, in order to avoid being dependent on the order of the array.

(def objs [@{:id :on-top
             :z 1                      # despite being before, rendering will be sorted by :z
             :render (fn [self] ...)   # and :on-top will be rendered above :below
             :update (fn [self] ...)}

           @{:id :below
             :z 0
             :render (fn [self] ...)
             :update (fn [self] ...)}]

Pros

  • easy to understand, especially for people having experience with OOP
  • pretty standard, e.g. unity works in a similar way
  • no ceremony around mutation, just modify the “object” tables

Cons

  • no automatic caching / avoidance of rerendering
  • a lot of structure, but not much value from it
  • consuming mouse clicks is up to the function, i.e. easy to accidentally eat input / forget to eat input

Example

(def callbacks @{}) 

(defn push-cb
  [k f]
  (update callbacks k |(array/push (or @[] $) f)))

(def objs [@{:hitbox @[810 30 100 100]
             :render |(draw-rectangle-rec ($ :hitbox) :red)
             :update |(when (and (mouse-button-down? 0)
                                 (in-rec? (get-mouse-position)
                                          ($ :hitbox)))
                        (push-cb :click ($ :f)))
             :f |(print "red!")}
           @{:hitbox @[860 0 100 100]
             :render |(draw-rectangle-rec ($ :hitbox) :blue)
             :update |(when (and (mouse-button-down? 0)
                                 (in-rec? (get-mouse-position)
                                          ($ :hitbox)))
                        (push-cb :click ($ :f)))
             :f |(print "blue!")}])

(varfn draw-frame
  [dt]
  (loop [b :in objs]
    (:update b))

  (loop [[_ cbs] :pairs callbacks
         :when (not (empty? cbs))]
    # when multiple callbacks with same key triggered
    # we only call the last one
    ((last cbs))
    (array/clear cbs))

  (loop [b :in objs]
    (:render b)))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment