Beginner looking for help refactoring game event callbacks


#1

Hi,
This is my first Elm program, and I’m writing some code that looks like it’ll be hard for other, even beginner-y beginners to understand. I’m looking for a clearer way to do what I wanna do, which is: process multiple types of key presses in a GraphicSVG gameApp.

The documentation at https://package.elm-lang.org/packages/MacCASOutreach/graphicsvg/2.1.0/GraphicSVG shows this example:

update msg model =
    case msg of
        Tick _ ( keys, _, _ ) ->
            case keys (Key "r") of
                JustDown ->
                    { model
                        | angle = model.angle - model.speed
                        , speed = -model.speed
                    }

                _ ->
                    { model | angle = model.angle + model.speed }

This works for me, but the problem comes when I want to process input on, say, keys ‘a’, ‘s’, ‘d’, and ‘f’.

My first try was this, handling just two keys:

update msg model =
    case msg of
        Tick _ ( keys, _, _ ) ->
            let model2 = case keys (Key "e") of
                             JustDown ->
                                 { model | speed = model.speed * 2}
                             _ -> model
            in
            case keys (Key "r") of
                JustDown ->
                    { model2
                        | angle = model2.angle - model2.speed
                        , speed = -model2.speed
                    }

                _ ->
                    { model2 | angle = model2.angle + model2.speed }

In prose: first a new model is computed for the presence/absence of the ‘e’ key, then that’s used as an input to compute another new model based on the presence/absecnce of the ‘r’ key.

I was able to generalize this using the foldl function (reproduced below), but without going through it line-by-line, but at this point the code is looking quite big and mysterious, and not suited towards other beginners. Is there a simpler approach I’m missing here?

update msg model =
    case msg of
        Tick _ (keys, _, _) ->
            let
                turnyUpdate model = { model | angle = model.angle + model.speed }
                keyUpdater string model =
                    case keys (Key string) of
                        JustDown ->
                            case string of
                                "a" ->
                                    {   model
                                        | angle = model.angle - model.speed
                                        , speed = -model.speed
                                    }
                                "s" ->
                                    {   model
                                        | angle = 0
                                        , speed = 0
                                    }
                                "d" ->
                                    {   model
                                        | speed = model.speed + 1
                                        , angle = model.angle + model.speed
                                    }
                                _ ->
                                    turnyUpdate model

                        _ ->
                            turnyUpdate model
            in
            List.foldl keyUpdater model ["a", "s", "d", "f"]

#2

I think the easiest way to make this code more beginner-friendly, is to split it up in multiple functions.

Besides that: Instead of reading all keys every tick, maybe it is possible to set the key handlers up in some way that they arrive independently?

You can add ‘is the go left key pressed’ to your model, and then check for that every tick to update the location/angle/etc. of the player entity.

If this is not possible, it would still probably be more readable to create handler functions for every key, and then thread the model through these. (So rather than executing a case with all of them for all elements of the list [“a”, “s”, “d”, “f”], you just pass the model to the first one). Something like: (Note: untested; here be typos)


isKeyDown keycode keys =
   case keys (Key keycode) of
     JustDown -> True
     _ -> False

goLeftIfLeftPressed keys model =
  if isKeyDown "a" keys then
     {model | angle = model.angle - model.speed, speed = -model.speed}
  else
    model

goRightIfRightPressed keys model =
    if isKeyDown "d" keys then
        {model | angle = model.angle + model.speed, speed = model.speed + 1}
    else
        model

stopIfDownPressed keys model =
    if isKeyDown "s" keys then
        {model | angle = 0, speed = 0}
    else
        model

update msg model =
    case msg of
        Tick _ (keys, _, _) ->
            updateGameTick keys model
        OtherMsg ->
            doSomethingElse model

updateGameTick keys model
    model
        |> stopIfDownPressed keys
        |> goLeftIfLeftPressed keys
        |> goRightIfRightPressed keys

The if/else structure might be abstracted away by creating a function that works on a condition and a function to apply to the input. Something like:

updateIf condition function input = 
  if condition then function input else input

goRightIfRightPressed keys =
  updateIf (isKeyDown "d" keys) (\model -> {model | angle = model.angle + model.speed, speed = model.speed + 1})

-- and the same for the other functions.

The result: Less boilerplate, but probably more difficult for a beginner to understand.


#3

I’d agree with @Qqwy about breaking it into separate functions. But if you wanted something beginner-friendly in terms of reading, you could do something along the lines of…

incrementSpeed : Model -> Model
incrementSpeed ({speed} as model) =
    { model | speed = speed + 1 }


invertSpeed : Model -> Model
invertSpeed ({speed} as model) =
    { model | speed = (-speed) }


zeroSpeedAndAngle : Model -> Model
zeroSpeedAndAngle model =
    { model | speed = 0, angle = 0 }


evolveAngle : Model -> Model
evolveAngle ({andle, speed} as model) =
    { model | speed = angle + speed }


update : Msg -> Model -> Model
update msg model =
    case msg of
        Tick _ (keys, _, _) ->
            let
                ifPressed key func =
                    if keys (Key key) == JustDown then
                        func
                    else
                        identity
            in
                model
                    |> ifPressed "d" incrementSpeed
                    |> ifPressed "a" invertSpeed
                    |> ifPressed "s" zeroSpeedAndAngle
                    |> evolveAngle

It separates the logic of what key is assigned to an action from what an action does, is easily extensible, and keeps your main update function compact.

But I’m far from experienced myself so I’m sure others could suggest solutions that are more idiomatic.


#4

I like this even more than the wharblegarble that I wrote up just now! :+1:


#5

Thanks to you both!

I merged something in this vein this morning. It is concise and extensible: yay!


#6

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.