MDN ブロックくずしゲームでウェブゲーム開発に入門してみた。その1

Webゲームプログラミングもやってみる。

放置しているのもどうかと思うので半年ぶりに書きます。

ウェブゲーム開発に入門

MDN web docs にある 「そのままのJavaScriptを使ったブロックくずしゲーム」 の教材でウェブゲーム開発に入門してみる。

PureScript+Halogen

この教材は pure JavaScript(そのままのJavaScript)とあるけど, そのままのJavaScript(UIフレームワークなし)は個人的にしんどいので変更します。

OutIn
プログラミング言語JavaScriptPureScript
UIフレームワーク無しHalogen

PureScript ってのは簡単にいうと
ビルドするとJavaScriptを得られる正格評価のHaskell。

開発環境

Microsoft Windows [Version 10.0.22000.613]
(c) Microsoft Corporation. All rights reserved.

C:\Users\aki>wsl -l -v
  NAME            STATE           VERSION
* Ubuntu-20.04    Running         2
$ cat /etc/os-release
PRETTY_NAME="Ubuntu 22.04 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04 (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

このWindows 11 + WSL2 + Ubuntuに
https://github.com/purescript/documentation/blob/master/guides/Getting-Started.md
を済ませた開発環境で始める。

Halogen example

Halogen example の Basicを動かしてみる。

aki: $ git clone https://github.com/purescript-halogen/purescript-halogen.git
Cloning into 'purescript-halogen'...
remote: Enumerating objects: 9892, done.
remote: Counting objects: 100% (385/385), done.
remote: Compressing objects: 100% (208/208), done.
remote: Total 9892 (delta 195), reused 325 (delta 167), pack-reused 9507
Receiving objects: 100% (9892/9892), 3.91 MiB | 5.70 MiB/s, done.
Resolving deltas: 100% (6005/6005), done.
aki:~$ cd purescript-halogen/examples/basic/
aki:~/purescript-halogen/examples/basic$ npm install
aki:~/purescript-halogen/examples/basic$ npm run example-basic

省略...

[info] Build succeeded.
[info] Bundle succeeded and output file to examples/basic/dist/example.js
aki:~/purescript-halogen/examples/basic$ cd dist/
aki:~/purescript-halogen/examples/basic/dist$ wslpath -w .
\\wsl.localhost\Ubuntu-20.04\home\aki\purescript-halogen\examples\basic\dist

Webブラウザで上のパスを開いて index.htmlを開く。ボタンがあってOnOffできることを確認出来たらOK。

自分の書くコードの置き場

aki:~/purescript-halogen/examples/basic/dist$ cd
aki:~$ mkdir breakout
aki:~$ cd breakout
aki:~/breakout$ mkdir lesson01
aki:~/breakout$ cd lesson01/
aki:~/breakout/lesson01$ spago init
aki:~/breakout/lesson01$ spago run

省略...

[info] Build succeeded.
🍝

purescript-halogen/examples/basic/src/ にある Main.purs と Button.purs を src/ にコピーする。

aki:~/breakout/lesson01$ cp ~/purescript-halogen/examples/basic/src/* src/
aki:~/breakout/lesson01$
aki:~/breakout/lesson01$ ls src
Button.purs  Main.purs

vscodeを起動する

aki:~/breakout/lesson01$ code .

lesson01-1

赤線が出ている。そういえばVSCode拡張機能をいれていた。 lesson01-2

これは halogen が無いというエラーだから, とりあえずターミナルから halogen を入れる。

aki:~/breakout/lesson01$ spago install halogen
aki:~/breakout/lesson01$ spago build

Main.pursを保存すると赤線が消える。

aki:~/breakout/lesson01$ spago bundle-app
[info] Build succeeded.
[info] Bundle succeeded and output file to index.js

これで index.js が出来た。

続いて purescript-halogen/examples/dists/index.html を ./ にコピーする。

aki:~/breakout/lesson01$ ls
index.js  output  packages.dhall  spago.dhall  src  test
aki:~/breakout/lesson01$ cp ~/purescript-halogen/examples/basic/dist/index.html ./
aki:~/breakout/lesson01$ ls
index.html  index.js  output  packages.dhall  spago.dhall  src  test

VSCode上で index.htmlを開いて
<script src="example.js"></script>
と書かれているところを
<script src="index.js"></script>
にして保存する。

Webブラウザでこの index.htmlを開く。
同じくボタンがあってOnOffできることを確認出来たらOK。

lesson01

準備が済んだので Canvasを作ってその上に描画する を始めます。

ゲームのHTML

チュートリアル通りならこのとおりだけど

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Gamedev Canvas Workshop</title>
    <style>
    	* { padding: 0; margin: 0; }
    	canvas { background: #eee; display: block; margin: 0 auto; }
    </style>
</head>
<body>

<canvas id="myCanvas" width="480" height="320"></canvas>

<script>
	// JavaScriptのコードがここに入ります
</script>

</body>
</html>

中身はプログラムの方で用意するので, index.htmlはこのくらいにする。

index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>Gamedev Canvas Workshop</title>
</head>

<body>
  <script src="./index.js"></script>
</body>

</html>

これをindex.htmlに保存する。

lesson01-3

ブロックくずしゲームのUIコンポーネント

今あるButton.purs は不要なので削除して Breakout.purs を新規作成する。

src/Breakout.purs
module Breakout (component) where

import Prelude
import CSS (background, block, display, margin, marginBottom, marginLeft, marginRight, marginTop, padding, px, rgb)
import CSS.Common (auto)
import Data.Maybe (Maybe(..))
import Effect (Effect)
import Effect.Aff.Class (class MonadAff)
import Graphics.Canvas (CanvasElement)
import Graphics.Canvas as Canvas
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.CSS (style)
import Halogen.HTML.Properties as HP
import Math as Math

canvasId :: String
canvasId = "myCanvas"

type State
  = {}

data Action
  = Initialize

component :: forall query input output m. MonadAff m => H.Component query input output m
component =
  H.mkComponent
    { initialState
    , render
    , eval:
        H.mkEval
          $ H.defaultEval
              { handleAction = handleAction
              , initialize = Just Initialize
              }
    }

initialState :: forall i. i -> State
initialState _ = {}

render :: forall m. State -> H.ComponentHTML Action () m
render _ =
  HH.main
    [ style do
        margin (px 0.0) (px 0.0) (px 0.0) (px 0.0)
        padding (px 0.0) (px 0.0) (px 0.0) (px 0.0)
    ]
    [ HH.canvas
        [ style do
            background (rgb 238 238 238)
            display block
            marginTop (px 0.0)
            marginRight auto
            marginBottom (px 0.0)
            marginLeft auto
        , HP.id canvasId
        , HP.width 480
        , HP.height 320
        ]
    ]

handleAction :: forall output m. MonadAff m => Action -> H.HalogenM State Action () output m Unit
handleAction = case _ of
  Initialize -> do
    maybeCanvas <- H.liftEffect $ Canvas.getCanvasElementById canvasId
    H.liftEffect
      $ case maybeCanvas of
          Nothing -> pure unit
          Just canvas -> draw canvas

draw :: CanvasElement -> Effect Unit
draw canvas = do
  ctx <- Canvas.getContext2D canvas
  --
  Canvas.beginPath ctx
  Canvas.rect ctx
    $ { x: 20.0
      , y: 40.0
      , width: 50.0
      , height: 50.0
      }
  Canvas.setFillStyle ctx "#FF0000"
  Canvas.fill ctx
  Canvas.closePath ctx
  --
  Canvas.beginPath ctx
  Canvas.arc ctx
    $ { x: 240.0
      , y: 160.0
      , radius: 20.0
      , start: 0.0
      , end: Math.pi * 2.0
      }
  Canvas.setFillStyle ctx "green"
  Canvas.fill ctx
  Canvas.closePath ctx
  --
  Canvas.beginPath ctx
  Canvas.rect ctx
    $ { x: 160.0
      , y: 10.0
      , width: 100.0
      , height: 40.0
      }
  Canvas.setStrokeStyle ctx "rgba(0, 0, 255, 0.5)"
  Canvas.stroke ctx
  Canvas.closePath ctx

Main.purs をこう書き換える。

src/Main.purs
module Main where

import Prelude
import Effect (Effect)
import Breakout as Breakout
import Halogen.Aff as HA
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main =
  HA.runHalogenAff do
    body <- HA.awaitBody
    runUI Breakout.component unit body

spagoの指示通り CSS Halogen-CSS Canvas Math Aff Maybe を入れる。

aki:~/breakout/lesson01$ spago install css halogen-css canvas math aff maybe

Canvasの基本

チュートリアルで説明されている「Canvasの基本部分」はここ。

draw :: CanvasElement -> Effect Unit
draw canvas = do
  ctx <- Canvas.getContext2D canvas
  --
  Canvas.beginPath ctx
  Canvas.rect ctx
    $ { x: 20.0
      , y: 40.0
      , width: 50.0
      , height: 50.0
      }
  Canvas.setFillStyle ctx "#FF0000"
  Canvas.fill ctx
  Canvas.closePath ctx
  --
  Canvas.beginPath ctx
  Canvas.arc ctx
    $ { x: 240.0
      , y: 160.0
      , radius: 20.0
      , start: 0.0
      , end: Math.pi * 2.0
      }
  Canvas.setFillStyle ctx "green"
  Canvas.fill ctx
  Canvas.closePath ctx
  --
  Canvas.beginPath ctx
  Canvas.rect ctx
    $ { x: 160.0
      , y: 10.0
      , width: 100.0
      , height: 40.0
      }
  Canvas.setStrokeStyle ctx "rgba(0, 0, 255, 0.5)"
  Canvas.stroke ctx
  Canvas.closePath ctx

ブラウザーで確認すると

lesson01-4

GitHubリポジトリ

コードはGitHub上にあります。
https://github.com/ak1211/breakout

つづく

よてい