4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

CAMPFIREAdvent Calendar 2022

Day 19

Webでうごくレトロなパズルゲームをつくる

Last updated at Posted at 2022-12-18

つなげて行こう!
なぞって動いてコマを消す、シンプルなゲームです。

こんにちは。
会社のアドベントカレンダーでなぜかゲームを作るWebフロントエンジニアです。
今回はパズルゲーム。
指で、マウスで、一筆書きをする。そんなノリのゲームになりました。

そこそこ音が出ます

尚、こちらの記事では知見をしたためて、もうひとつ note ではふり返りの感想。
そんな感じになっています。

技術概要

2年前は HTML や CSS。1年前は Canvas 2D。
と来て。
今年は WebGL です!ついに!

とはいえ。
正直 WebGL 周りについては、ほぼ書けることがありません。
Webフロントエンド開発としての、新たな知見もそんなにありません。

ちなみに今回も Vanilla JS。
ランキング等の Firebase を除けばライブラリなし。メタ言語なし。
ビルドせずそのまま動きます。

※手元で動かすならローカルサーバーくらいは要ります
※デプロイ時はビルドでファイルをまとめています

カラーパレット

画面のカラーテーマ切り替え。それを一度やってみたかった。
今回 WebGL を選んだのもほぼこれが理由です。
もしかするとCanvas 2D でも SVGフィルタ で実現できるかもしれません。
けど、なんとなくゲームには重そうで(調べてません)。

実装ではカラーパレット用の画像を用意しました。
モノクロに任意の色をoverlayブレンドする事も考えましたが、こちらの方が楽ですね。
融通も効きます。

色々省略してますがフラグメントシェーダーの概要です。

const vec3 grayScale = vec3(0.299, 0.587, 0.114);

void main() {
  vec4 textureColor = texture(tex, vTextureCoord);
  float gray = dot(textureColor.rgb, grayScale);
  vec4 paletteColor = texture(paletteTexture, vec2(gray, paletteV));
  outColor = vec4(paletteColor.rgb, textureColor.a);
}

格子模様

ドットマトリクスな格子模様。これもレトロな液晶表現には欲しくなるもの。
CAMPFIREランゲーム (404ページで遊べるミニゲーム)でもやっています。
けどあれ、実はそれっぽい透過PNGを重ねているだけでした。

今回はそれもシェーダーのお仕事。
とりあえず座標の小数をすごい累乗すれば、端以外はだいたい 0 になるだろう。
という思考で線を描いています。ざっくり。

前項と同じく、色々省略されたフラグメントシェーダー概要。

void main() {
  outColor = texture(tex, vTextureCoord);
  float x = mod(vPosition.x, 1.0);
  float y = mod(vPosition.y, 1.0);
  outColor.rgb += pow(max(x, y), 10.0) * 0.5;
}

テクスチャパッキング

正直フロントとは直接関係ない話なのですが。

1枚の画像に、複数の画像をまとめる。ゲーム作りでよく見かけますね。
テクスチャアトラスと言われたりします。あとはCSSスプライトとか?
TexturePacker というツールの名前もよく見るかもしれません。

技術としての正しい呼び方は分かりませんが。
この画像の詰め込み作業を、今回はゲームの実行時に行っています。
理由は長くなりそうなので割愛。

とりあえず手軽な実装なので一例だけ書いておきます。

function insert(node, size) {
  if (node.child1) {
    return insert(node.child1, size) || insert(node.child2, size)
  } else if (node.w >= size.w && node.h >= size.h) {
    const x2 = node.x + size.w
    const y2 = node.y + size.h
    const dw = node.w - size.w
    const dh = node.h - size.h

    if (dw > dh) {
      node.child1 = { x: node.x, y: y2, w: size.w, h: dh }
      node.child2 = { x: x2, y: node.y, w: dw, h: node.h }
    } else {
      node.child1 = { x: x2, y: node.y, w: dw, h: size.h }
      node.child2 = { x: node.x, y: y2, w: node.w, h: dh }
    }

    return { x: node.x, y: node.y }
  }
}

// 例
const root = { x: 0, y: 0, w: 200, h: 200 }
insert(root, { w: 100, h: 100 }) // => { x: 0, y: 0 }
insert(root, { w: 100, h: 100 }) // => { x: 100, y: 0 }
insert(root, { w: 100, h: 100 }) // => { x: 0, y: 100 }

驚くほどお手軽。なので効率もほどほど。
とはいえ、こだわっても効率はそこまで上がらないという話も聞きます。

テクスチャ配列

前項の画像の敷き詰め。その格納先としてテクスチャ配列を使っています。
テクスチャとはざっくり画像のバッファで、それを複数持つのがテクスチャ配列。

使いたい画像があればテクスチャに敷き詰め、溢れるようなら次のテクスチャへ。
正直テクスチャ配列じゃなくても実現はできます。
ただ、使うとちょっとお手軽感が増します。

今回何気に WebGL 2.0 なんですが、 2.0 な理由はそれくらいかも?
おかげで今回、動作環境がやや新しめに限られます。

Import Maps

シンプルに import の読み先を定義できるのが Import Maps。
去年も書きましたが、一通りのモダンブラウザが対応 するようなので改めて。

<script type="importmap">
  {
    "imports": {
      "@/": "./",
      "@env": "./env.local.js",
      "firebase/app": "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js"
    }
  }
</script>

ちなみに型やビルド設定にも @env 定義はありますけど、そちらの設定は ".env.js"。
一方でこちらはブラウザ動作用、つまりは開発環境用の定義ですね。

Chrome はドットフォントがボケる

ドット表現に使えるフォントが、世の中にはたくさん公開されています。ありがたい。
今回利用したのは大変コンパクトな 美咲フォント
いずれ M+ BITMAP FONTSマルモニカ なども活用してみたいですね。

ところで。
ドットが特徴のフォントですから、もちろんそれを活かしたい所ですが。
ゲームで使うべく Canvas 描画してみると……どうやっても Chrome でボケます。

Firefox

Chrome
M+ BITMAP FONTS 10r を 10px で描画

これブラウザでゲーム開発している世の皆さん、どうされてるんですかね。
やはりフォントは画像化して扱っているのか。
あるいは大きく描画すれば輪郭がボケる程度だから OK なのか。

一応シャープネス的な処理も試作してみましたが、不慣れで限度もあり……。
仕方なく、今回は諦めて画像化しました。

すると。ファイルサイズが 1MB から 70KB に収まってむしろハッピー。
あれ、フォントファイルのまま使うメリットってむしろ何だろう?

逆行列

数学に疎いもので知りませんでした。便利ですね。逆行列。
ローカル座標からワールド座標への変換によく行列を使っています。
それとは逆にワールド座標からローカル座標に出来るのが逆行列。

例えばマウスの位置がローカル座標から見てどこにあるのか、というような。
今までボタンの位置サイズを都度ワールド座標にして判定してましたが。
逆にマウス位置をローカル座標化して判定しても良かったんですね。
もし回転もあるならこちらの方が良さそう。

データ圧縮

リプレイのデータは主に操作ログなんですが、これが地味にかさばる。

基本は 60fps x 60秒 x ログの単位データ。
マウス座標を持つなら X/Y それぞれ 2 バイトくらい欲しいので 14.4KB。
実際には JSON なので単純に数字配列を文字化したら 30 ~ 40KB くらい?
1000 エントリーでも 50MB は行かない計算。

あれ。計算してみると小さいような。別にかさばらない。
よし。この話やめ!

でも良いんですが、一応データは小さいに越したこと無いですよね。
ということで実装コストのかからない圧縮について模索していました。
LZ77LZ78 を試作したり。

結局は基礎中の基礎である ランレングス に落ち着きました。
情報もマウスでなく矢印(ゲームの仕様です)のログにしたり。
形式も base64 的なバイナリにしたり。
最終的にはざっくり 500B くらいに。

GitHub Actions

GitHub Pages デプロイの選択肢として正式にワークフローが出たそうで。
今回初めて使いました GitHub Actions。
試行錯誤もなく、驚くほどサクッと動きますね。
おかげで紹介くらいしか書けることがありません。

0xFF の分割

何気に一番の知見だったかもしれません。

今回のゲームは、表示の基本がモノクロ 4 階調。
なので黒~白までを 4 等分する必要があります。
感覚的には 1 バイト ÷ 4 だしそりゃ割れるでしょ。と思うかもしれません。

しかし実際には 0xFF を 3 で割ります。それが 4 等分です。
そして 0xFF は 3 で割れます。割れるのか!
分かる人には当たり前すぎて恐縮ですが、自分としては衝撃的。

ちなみに具体的には #000 #555 #aaa #fff の4階調。
キレイに割れるんですね。
むしろ白と黒の中央の方がズレていて微妙に思えたり。#7f7f7f とか。

おわりに

今年はかなり余裕を持った着手だったハズなのに、なんやかんやのギリギリ進行。
むしろ完全に予定オーバーという結果でした。
(あと 2 週間早く出したかった。その辺りは note で)

内容も、年々Webフロント的な技術紹介が出来なくなるばかりですが。
ブラウザという共通プラットフォームの気軽さ、その魅力は常に推していきたくて。
たぶん来年も、懲りずに何かを作ります。

Webフロントはたのしいですね!
Webフロントはたのしいですよ!

補足

※ FIRE LINER リポジトリ

※ 2021 年の記事

※ 2020 年の記事

4
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?