Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add space invaders JS tutorial blog post #80

Closed
wants to merge 2 commits into from

Conversation

JoshMeredith
Copy link
Contributor

No description provided.

blog/2024-02-08-space-invaders-tutorial.md Outdated Show resolved Hide resolved

SpaceInvaders on GitHub is a demo game written by user ivanperez-keera to demonstrate Haskell's Yampa library. It was recently ported by AntanasKal to work in the browser using the WASM backend.

In this post, we will port SpaceInvaders to also work on the JavaScript backend as a worked example of using the JavaScript backend with the browser. Comparisons with Wasm will also be made, though comparing backends isn't the main focus of this post.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd frame it like this: "The WebAssembly port motivated us to try to port it to JavaScript using the JavaScript backend we added to GHC. Porting it to JavaScript was straightforward, see the diff at LINK. This is a good example of using the JavaScript backend in the browser and in this post we explain how it works."

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made some changes to this effect, but I'm not sure on the new version either.

Copy link
Contributor

@doyougnu doyougnu Feb 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little too formulaic, don't be afraid to have a voice, this one should be fun after all. I can imagine something like:

|N| weeks ago |person| posted a demo of wasm space invaders on |site| to great fanfare. Truly 2024 is a year of spoils in the haskell community. But this got us (the IOG team and JS backend maintainers) thinking "Well if they can do space invaders, what can we do?" So in the spirit of friendly competition, I (Josh) poured a tall cup of coffee, sat down on a 49 C Australian summer afternoon and ....

blog/2024-02-08-space-invaders-tutorial.md Outdated Show resolved Hide resolved

In this post, we will port SpaceInvaders to also work on the JavaScript backend as a worked example of using the JavaScript backend with the browser. Comparisons with Wasm will also be made, though comparing backends isn't the main focus of this post.

## Background Knowledge
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The text dives in too fast. Give a high-level overview:

  • web page only contains an HTML canvas that the Haskell code draws on and get events from using some FFI.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to integrate this with the other comment but I'm not sure if it sounds awkward now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Sylvain, I think there should be a paragraph like:

Suggested change
## Background Knowledge
Here's the plan, the WASM version uses: X to ...something..., Y to ...another thing..., and Z to ...another another thing.... So to make this work with the JavaScript backend we need to be utilize X, Y, and Z, which turned out to be pretty easy. Here is how we did it:

This also sets up the rest of the blog post with a nice structure of:

## how we got X to work
In WASM-land X is .... but for the JS backend we can ... and then all we need is some glue:
...

## how we got Y to work

Copy link
Contributor

@doyougnu doyougnu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job, you have all the raw materials but I think there needs to be more guidance on "why" you need this or that. Without adding that context I think the audience will get lost.


In this post, we will port SpaceInvaders to also work on the JavaScript backend as a worked example of using the JavaScript backend with the browser. Comparisons with Wasm will also be made, though comparing backends isn't the main focus of this post.

## Background Knowledge
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Sylvain, I think there should be a paragraph like:

Suggested change
## Background Knowledge
Here's the plan, the WASM version uses: X to ...something..., Y to ...another thing..., and Z to ...another another thing.... So to make this work with the JavaScript backend we need to be utilize X, Y, and Z, which turned out to be pretty easy. Here is how we did it:

This also sets up the rest of the blog post with a nice structure of:

## how we got X to work
In WASM-land X is .... but for the JS backend we can ... and then all we need is some glue:
...

## how we got Y to work


## Background Knowledge

The Yampa library in Haskell is based on Functional Reactive Programming (FRP) for modelling programs as events and time-varying values. FRP is useful in the domain of UIs and games because the encoding of events helps with handling user inputs and outputs.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we suddenly talking about Yampa? There is not enough context. Does the WASM version compile Yampa?

I usually try to keep the state of my audience's knowledge in my head when I write. Then I update that state when I give knowledge with the needed context to understand the meaning of the knowledge. For example. the state of your audience at this point is: there was a space invaders made with WASM, we're going to make it with JS backend. To update that state you need to write something like: "The WASM version used Yampa to program its game logic." Now that updates the audience's knowledge state to: There was a space invader made with WASM, we're going to make it with the JS backend, the WASM version used Yampa. Now you can say why we're talking about Yampa: "So if we're going to write a true port then we need to compile Yampa with the JS backend just like the WASM version".


The Yampa library in Haskell is based on Functional Reactive Programming (FRP) for modelling programs as events and time-varying values. FRP is useful in the domain of UIs and games because the encoding of events helps with handling user inputs and outputs.

For this tutorial, we'll assume that the main game logic in Yampa is the same across backends, and that our main concern is in feeding values in and out of the edges of this logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how the WASM version did it? If so, then instead of assuming just show its cabal file and say "it used Yampa"


For this tutorial, we'll assume that the main game logic in Yampa is the same across backends, and that our main concern is in feeding values in and out of the edges of this logic.

To accept values out of the game logic so that we can use them in our JavaScript rendering functions, we'll use the `reactInit` function from Yampa. It has the following type:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Start with the problem that has to be solved and how it ties into the bigger picture:

Suggested change
To accept values out of the game logic so that we can use them in our JavaScript rendering functions, we'll use the `reactInit` function from Yampa. It has the following type:
First, we need some data to feed into the JavaScript rendering functions so we can draw to the screen. To do that, we'll call the `reactInit` function in Yampa:

See how I led with the big picture? This is also a lot less ambiguous than accept values out of the game logic (what does accept mean here?)

reactInit :: IO a -> (ReactHandle a b -> Bool -> b -> IO Bool) -> SF a b -> IO (ReactHandle a b)
```

The first value to `reactInit` initialises the input state, `a`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The first value to `reactInit` initialises the input state, `a`.
`reactInit` takes three arguments and is polymorphic over the game state: The first argument initializes the input state, `a`.


## Exporting `runGameStep` to JavaScript

To input values for the mouse location and button state from JavaScript, we wrap a call to `react` with the `gameReactHandle` that `reactInit` returns. In this case, `gameReactHandle` is a top level binding because the call to `reactInit` uses `unsafePerformIO`. This is done to share code with the Wasm implementation, but in principle it's possible to define `runGameStep` as a local binding in `main` to avoid using `unsafePerformIO`. For a program only targetting the JavaScript backend, the local binding method would be preferred, and an example of how to do this will be provided later.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

state what you achieved in the last section:

Suggested change
To input values for the mouse location and button state from JavaScript, we wrap a call to `react` with the `gameReactHandle` that `reactInit` returns. In this case, `gameReactHandle` is a top level binding because the call to `reactInit` uses `unsafePerformIO`. This is done to share code with the Wasm implementation, but in principle it's possible to define `runGameStep` as a local binding in `main` to avoid using `unsafePerformIO`. For a program only targetting the JavaScript backend, the local binding method would be preferred, and an example of how to do this will be provided later.
Now we can render graphics and write them to the screen. To input values for the mouse location and button state from JavaScript, we wrap a call to `react` with the `gameReactHandle` that `reactInit` returns. In this case, `gameReactHandle` is a top level binding because the call to `reactInit` uses `unsafePerformIO`. This is done to share code with the Wasm implementation, but in principle it's possible to define `runGameStep` as a local binding in `main` to avoid using `unsafePerformIO`. For a program only targetting the JavaScript backend, the local binding method would be preferred, and an example of how to do this will be provided later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After you introduce gameReactHandle show its code, then explain its code. Doesn't have to be much, just a single sentence that says it spawns a random generator and initializes the game state.

return ()
```

With the function we want to export defined, we need to prepare some JavaScript code to receive the exported Haskell function. We declare a global `var runGameStep_` to store the exported function, and a JavaScript function `setRunGameStep(fn)` that sets the global.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function is runGameStep? And I think you mean this is a foreign export right? Not a module export.


```

We'll also need a JavaScript function to call the callback, which we'll also name `runGameStep`. In this function, we package the arguments into a single `JSVal` to be given to our exported Haskell function. `JSVal` is just a Haskell type used to refer to untyped JavaScript data that isn't managed by GHC's runtime. We have to package the arguments instead of passing multiple `JSVal`s because `base` (currently) only defines `Callback`s up to 3 arguments. The exact format of the package isn't too important - we just need to match it later when we unpack the data.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

State the big picture. The JS runtime still doesn't have everything it needs to iterate the game state because the state is in the Haskell Heap right? And this is why you need both the foreign export and the callback?

:: JSVal -> (# Double, Double, Bool, Double #)
```

Next, we can write the actual wrapper function that will get exported. Here we use the unboxed tuple syntax to deconstruct the tuple returned by `unpackGameStepArgs` and pass them into `runGameStep`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha so this is the export!?


## Feeding in JavaScript Inputs

We now have everything set up to trigger input events in JavaScript that will feed into the game logic implemented in Yampa. Next, we'll need to set up browser event listeners on various mouse actions.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good! This is the big picture update that is missing from most of the post.

@hsyl20 hsyl20 closed this Apr 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants