恐竜本舗

エンジニアをしている恐竜の徒然日記です。

PhaserJSでパンを撃って敵を討つゲームを作った

Summary

  • パンを撃って敵を討つシューティングゲームを作ってみた
  • いつもお世話になっているPdMや社内に向けたネタ作だが、割とちゃんと作った
  • Googleログインでランキング機能を入れている

あそぶ

こちらから遊べます。キーボード操作が必要なため、PCブラウザでのみ遊べます。 推奨環境はChrome

  • URL

pan-shoot.fly.dev

github.com

Tech Stack

モチベーション

Viteの学習も兼ねて、ブラウザゲームを作ってViteでBuild した結果を、サーバから静的配信したい、というモチベーションでネタを探していた。

ちょこちょこ小ネタのゲームを作るので、今回も小ネタとして作ってみた。

感想もろもろ

Fly.io

個人開発の置き場を Heroku からどこに変えるかを悩んでいて、今回は Fly.io に置いてみた。

  • 無料枠内で DBサーバとして PostgreSQL も使える
  • flyctl でアプリの構築〜デプロイ〜管理すべてコマンドラインベースで勝手が良い
  • Dockerfileに記述しておき、Fly.io 上でDockerイメージを立ち上げてくれる

構成管理が toml なのが慣れないともやもやするが、簡単なサービスなら今後はこれで行こうかなと思った。

Stable Diffusion

今回のメインビジュアルは背景を Stable Diffusion で作ってみた。

ローカルやGoogle Colaboratory に置いて試すのが一般的だが、 mage.space というサービスで今回は作成した。

画像生成系モデルのAIについては全然詳しくないのだが、 Example も多いのでそのプロンプトを見つつ作ってみた。

いい感じのアラビアンな画像ができたと思う。

Google ログイン

もともとはViteでブラウザゲームをビルドしたいだけだったので Firebase Hosting あたりに置くつもりだった。

しかし、やっぱり展開するならランキング機能欲しいなという欲が出てきて、 Google ログインを突っ込むことにした。

Auth0や Firebase Authorication も検討したものの、アカウント情報を基本的には保持したくなかったため、IDと名前以外は持たないように内製した。

スコア保持

ゲームはすべてCanvas 上で動いている。

普段の仕事だとSPAのWEBアプリを作っているのだが、Canvas上での画面をまたぐ際の情報保持がイマイチ分からず苦戦した。

結局、CryptoJS を用いて暗号化して、SessionStorageに保持 → 画面遷移後に認証、スコアを複合→DB保存後、破棄 という手順を踏んだ。

https://github.com/daitasu/pan-shoot/blob/main/frontend/src/scenes/mypage.ts#L32-L46

もっといい方法がありそうだが、そんなに大事な情報でもないのでフロント側で管理することにした。

Static Server 配信のための src リプレイス

今回のVite側の静的ファイルのパス解決とGolangサーバ側のパス解決を合わせるのに中々詰まった。

今回のフロントエンドをビルドした結果は下記のようになる。

dist
└ assets
   └ main-xxxxxxxxx.js
└ images
   └ xxx.png # 各種の画像
└ index.html

このとき、

  • Golang側で静的ファイルサーバを / 直下には置かず、 /static 配下にしたかった
  • index.html から js を <script type="module" src="/static/assets/main-xxx.js"></script> のように取得したい
  • 画像群もまた、 href="/static/images/chocopan.ico" のようにして取得したい

という条件があるのだが、Golang側で /static/ を静的ファイルサーバとして配信すると、画像ファイルは正常に上記のパスになる。

一方、Viteでビルドした時点で index.html の script タグは src="/assets/main-xxx.js" となっており、ここにずれがあった。

prefix をつけると画像側のパスが今度はずれてしまうため、ここだけ個別書き換えるようにした。

pan-shoot/buildPlugin.ts at main · daitasu/pan-shoot · GitHub

これを vite.config.ts 上でプラグインとして入れ込むことで解决した。

import updateIndexHtml from "./src/scripts/buildPlugin";

export default defineConfig({
  // ...
  build: {
    // ...
    rollupOptions: {
      input: {
        main: resolve(root, "index.html"),
      },
    },
  },
  plugins: [updateIndexHtml()],
  server: {
    port: 9000,
  },
});

Viteにおける環境変数の持ち方

Viteを使って個人開発してる中で地味に詰まったのでメモ。

Summary

  1. Vite の環境変数vite.config.tsにおける envDir で指定したディレクトリパスにある .env を参照する
  2. .env の情報は import.meta.env 内に保持される
  3. VITE_ prefix がつく環境変数のみを .env ファイルから取得し、それ以外は undefined となる
  4. TS 用の型補完は vite/client.d.ts で型定義を提供しており、自前型定義は ImportMetaEnv を補う形で追加する

ref: Env Variables and Modes | Vite

Contents

① Vite の環境変数vite.config.tsにおける envDir で指定したディレクトリパスにある .env を参照する

ディレクト

- public/
- src/
   └ main.ts
   └ index.html
- .env
- package.json
- yarn.lock
- vite.config.ts

vite.config.ts

import { defineConfig } from "vite";

export default defineConfig({
  root: "src", // src 配下を root としたい
  envDir: "../", // src がroot のため、envDir は1つ戻す
  publicDir: "../public",
  build: {
    outDir: "../dist",
  },
  server: {
    port: 9000,
  },
});

うっかりで地味に気づくのに時間かかった。。

.env の情報は import.meta.env 内に保持される

Vite でビルドした結果において、環境変数import.meta.env に保持される。

標準で下記のような情報を保持している。

  • import.meta.env.BASE_URL ... アプリのベースURL。
  • import.meta.env.DEV ... 開発環境で動作しているかどうか
  • import.meta.env.PROD ... 本番環境で動作しているかどうか
  • import.meta.env.MODE ... アプリのモード。 yarn build --mode {mode} のように文字列指定できる。 yarn devdevelopment モード、 buildproduction モードとして動作する
  • import.meta.env.SSR ... SSR での動作かどうか

VITE_ prefix がつく環境変数のみを .env ファイルから取得し、それ以外は undefined となる

Vite は標準で dotenvを内包している。

現在のモードに合わせて、下記のように読み込む条件が変わる。

- .env                # 全ての場合に読み込まれる
- .env.local          # 全ての場合に読み込まれ、gitには無視される
- .env.[mode]         # 指定されたモードでのみ読み込まれる
- .env.[mode].local   # 指定されたモードでのみ読み込まれ、gitには無視される

少し変わった特徴として、Viteでは環境変数が誤ってクライアントサイドに漏れないように、 VITE_ prefix がつく変数のみをViteで処理されたコードに公開する。

sample

VITE_HOGE=aaa
HOGE=bbb

これに対し、 クライアントサイドで取得を試みると下記のようになる。

  • import.meta.env.VITE_HOGE ... aaa
  • import.meta.env.HOGE ... undefined

デフォルトは VITE_ という指定だが、この prefix は enxPrefixvite.config.ts に指定して変更可能。

Viteのコードを追ってみると、シンプルに .env で読み取った情報の中から prefix が一致するものだけを吸っているっぽい。

github.com

④ TS 用の型補完は vite/client.d.ts で型定義を提供しており、自前型定義は ImportMetaEnv を補う形で追加する

Viteの import.meta.env の型定義は vite/client.d.ts で提供されている。 しかし、自前の環境変数には自動補完が効かないので、下記のような env.d.ts を作成する。

/// <reference types="vite/client" />

interface ImportMetaEnv {
  readonly VITE_APP_TITLE: string;
  readonly VITE_HOGE: string;
  readonly VITE_FUGA: string;
  /// etc...
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

robotgo(Go) でカラーピッカーを作る

はじめに

Goでキーボードやマウス操作ができる robotgo というライブラリを知り、定常的に使っているカラーピッカーを自前で作り直したかったので今回挑戦してみました。

完成物

先に、出来上がったものはこちらです。

github.com

robotgo とは

github.com

キーボードやマウス操作、画面キャプチャなどをGoから扱えるライブラリです。 例えば、下記のようにマウス操作やスクリーン操作が行なえます。(サンプルコードより拝借)

package main

import (
  "github.com/go-vgo/robotgo"
)

func main() {
  // マウス操作
  robotgo.ScrollDir(10, "up")
  robotgo.ScrollDir(20, "right")

  robotgo.Scroll(0, -10)
  robotgo.Scroll(100, 0)

  robotgo.MilliSleep(100)
  robotgo.ScrollSmooth(-10, 6)

  robotgo.Move(10, 20)
  robotgo.MoveRelative(0, -10)
  robotgo.DragSmooth(10, 10)

  robotgo.Click("wheelRight")
  robotgo.Click("left", true)
  robotgo.MoveSmooth(100, 200, 1.0, 10.0)

  robotgo.Toggle("left")
  robotgo.Toggle("left", "up")

  // スクリーン操作
  x, y := robotgo.Location()
  fmt.Println("pos: ", x, y)

  color := robotgo.GetPixelColor(100, 200)
  fmt.Println("color---- ", color)

  sx, sy := robotgo.GetScreenSize()
  fmt.Println("get screen size: ", sx, sy)

  bit := robotgo.CaptureScreen(10, 10, 30, 30)
  defer robotgo.FreeBitmap(bit)

  img := robotgo.ToImage(bit)
  imgo.Save("test.png", img)

  num := robotgo.DisplaysNum()
  for i := 0; i < num; i++ {
    robotgo.DisplayID = i
    img1 := robotgo.CaptureImg()
    path1 := "save_" + strconv.Itoa(i)
    robotgo.Save(img1, path1+".png")
    robotgo.SaveJpeg(img1, path1+".jpeg", 50)

    img2 := robotgo.CaptureImg(10, 10, 20, 20)
    robotgo.Save(img2, "test_"+strconv.Itoa(i)+".png")
  }
}

カラーピッカーを作ってみる

robotgoを用いて、各種イベントに対して処理を登録していきます。 robotgo.EventHookというメソッドがあるので、これを用いてクリックイベントとKeyDownイベントを渡して処理の登録をしています。

fmt.Println("Please mouse left click to pick the color of current mouse position.")
fmt.Println("To pick the color of mouse positions, please enter ctrl+Shift+c.")
fmt.Println("To quit checking mouse event, please enter ctrl+Shift+q.")

// Stop event
robotgo.EventHook(hook.KeyDown, []string{"q", "shift", "ctrl"}, func(e hook.Event) {
    robotgo.EventEnd()
})

// Pick the color of event
robotgo.EventHook(hook.KeyDown, []string{"c", "shift", "ctrl"}, func(e hook.Event) {
    posx, posy := robotgo.GetMousePos()
    color := robotgo.GetPixelColor(posx, posy)
    robotgo.WriteAll(color)

    fmt.Println("Pick the color: ", color)
})

// Get color of current mouse position
robotgo.EventHook(hook.MouseDown, []string{}, func(e hook.Event) {
    posx, posy := robotgo.GetMousePos()
    color := robotgo.GetPixelColor(posx, posy)

    fmt.Println("pos-> ", posx, posy)
    fmt.Println("color-> ", color)
})

マウスクリックの度にカラーコードをクリップボードに取得されるのはモヤモヤするので、クリップボードへのコピーは処理を分けてます。 下記のようなメソッドを利用しています。

下記のように動きます。

$ go run main.go
Please mouse left click to pick the color of current mouse position.
To pick the color of mouse positions, please enter ctrl+Shift+c.
To quit checking mouse event, please enter ctrl+Shift+q.

// マウス左クリック時
pos->  968 703
color->  2d313b
// ctrl + shit + c 押下時
Pick the color:  efefef

robotgo を用いることで、さくっとマウスの現在地とそのカラーコードの取得ができました。 CLIだけだとどんな色を取得するか分かりにくく扱いづらいので、次回はもう少し改良していこうと思います。

参考

今回の制作にあたり、下記の記事も参考にさせて頂きました。

Robotgoでマウスポインタを自動で移動させる | かずさプログラマーの雑記帳