Laravel上でebitengineのゲームを遊べるようにする

## はじめに
ebitengineでゲームを作ったら、是非公開したくなります。ebitengineではマルチプラットフォームに対応しているので好きなプラットフォームで公開できるわけですが、実際に公開するにはプラットフォームごとに異なる大きなハードルを超えないといけません。

そこで、今回はWebブラウザでebitengineで作ったゲームを遊べるようにするまでの方法を公開します。今回ご紹介する方法を実践後に、後はAWSやherokuなどにデプロイすればすぐに全世界に公開できます。

さらに、ログインユーザーごとに異なる値をebitengine側に渡すという実装も含まれているので、ebitengine本体に備わっていない「ゲームデータを保存する」ということもできます。今回はその取っ掛かりの部分を実装しているので、是非拡張してみてください。

## ざっくり全体の説明
1. ebitengineで作ったアプリをWebAssemblyで書き出す
1. 書き出したアプリをlaravel上に設置する
1. ログインユーザー名をebitengineで表示できるようにする

## Laravel部の作成
### 今回作成する環境
dockerを使用します。未インストールの方はインストールをお願いします。
- Laravel バージョン：v8.83.23
- PHP バージョン：7.4.30

### 1. dockerfileなどの作成

#### ディレクトリ構造
~~~
┣ nginx
┃   ┣ default.conf
┃   ┗ mime.conf
┃
┣ php-fpm
┃   ┗ Dockerfile
┃
┣ src (一旦ディレクトリのみ作ってください）
┃
┗ docker-compose.yml
~~~

#### 各ファイルの中身

~~~nginx/default.conf
server {
    listen       80;
    server_name  localhost;

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html/public;
        index  index.html index.htm index.php;

        try_files $uri $uri/ /index.php$is_args$args;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    
    location ~ \.php$ {
        root           /var/www/html/public;
        fastcgi_pass   php-fpm:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}
~~~

~~~ conf:nginx/mime.conf
types{
    application/wasm wasm;
}
~~~


~~~ Dockerfile:php-fpm/Dockerfile
FROM php:7.4-fpm

#pdoインストール
RUN docker-php-ext-install pdo_mysql

#composerインストール
RUN curl -sS https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer

#いろんなものをインストール
RUN apt-get update
RUN apt-get install -y git vim zip unzip nodejs npm
~~~

~~~ yml:docker-compose.yml
version: '3'
services:
        nginx:
                image: nginx:1.15
                ports:
                        - 80:80
                volumes:
                        - ./src:/usr/share/nginx/html
                        - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
                        - ./nginx/mime.conf:/etc/nginx/conf.d/mime.conf
        php-fpm:
                build: ./php-fpm
                volumes:
                        - ./src:/var/www/html
        mysql:
                image: mysql:5.7
                environment:
                        MYSQL_DATABASE: ebiten 
                        MYSQL_ROOT_PASSWORD: password
                        MYSQL_USER: user
                        MYSQL_PASSWORD: password
                        TZ: Asia/Tokyo
                ports:
                        - 3306:3306
                volumes:
                        - ./mysql/data:/var/lib/mysql
~~~

### 2. Laravelプロジェクトの作成
$マークはコンテナの外、>マークはコンテナ内でのコマンドを表します
1.  ルートディレクトリにてコンテナ立ち上げ
    > $ docker-compose up -d
2. php-fpmコンテナへ入る
    > $ docker exec -it ebiten-web_php-fpm_1 /bin/bash
3. プロジェクトを作成
    > \> composer create-project laravel/laravel ebiten
4. src/ebiten 以下のファイルをすべて src 配下へ移動させて、ebitenフォルダを削除
4. .envを開き、データベース関連の値を以下のように修正
    > ~~~ 
    > DB_CONNECTION=mysql
    > DB_HOST=mysql
    > DB_PORT=3306
    > DB_DATABASE=ebiten
    > DB_USERNAME=user
    > DB_PASSWORD=password
5. コンテナ内でマイグレーション実施
    > \> php artisan migrate
6. 8. laravel ui （ログイン周り）の導入
    > \> composer require laravel/ui
    \> php artisan ui bootstrap --auth
    \> npm install && npm run dev
    エラーがここで出るので、 下記を実行
    \> npm install resolve-url-loader@^5.0.0 --save-dev --legacy-peer-deps
    \> npm run dev
7. http://localhost/ にアクセスして新規登録できることを確認する


## ebitengine部の作成
### go言語のバージョン
- 1.19

### サンプルの作成

1. 適当なディレクトリ（ebitengineのプロジェクト名）を作成
1. １で作ったディレクトリ内でプロジェクトの作成
    > $ go mod init github.com/yourname/yourgame
    
    yourenameやyourgameの部分は各自入力してください。
1. 2で作られた`go.mod`と同じディレクトリに`main.go`を作成して、以下のように記述
    > ~~~go:main.go
    > package main
    > 
    > import (
    >	"log"
    >
    >	"github.com/hajimehoshi/ebiten/v2"
    >	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
    > )
    >
    >type Game struct{}
    >
    >func (g *Game) Update() error {
    >	return nil
    >}
    >
    >func (g *Game) Draw(screen *ebiten.Image) {
    >	ebitenutil.DebugPrint(screen, "Hello World !!")
    >}
    >
    >func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    >	return 320, 240
    >}
    >
    >func main() {
    >	ebiten.SetWindowSize(640, 480)
    >	ebiten.SetWindowTitle("Hello, World!")
    >
    >	if err := ebiten.RunGame(&Game{}); err != nil {
    >		log.Fatal(err)
    >	}
    >}
4. 一度 `main.go`を実行
    > $ go run main.go
     
    このとき、go get ~~~ というエラーが出たら一通り実行して、Hello Worldが表示されるのを確認する

5. WebAssemblyへ書き出す
    > $ GOOS=js GOARCH=wasm go build -o main.wasm main.go

6. WebAssemblyを動かすためのjsファイルを取得
    > $ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

    なんか斜体になっていますが、`$ cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .`です

## Laravelにebitengineのアプリを移植
1. ebitengine部の5番目の手順で`main.wasm`ファイルが生成されているので、このファイルをlaravel側の`src/public/wasm/main.wasm` となるように配置する。（wasmフォルダがないので作ってください）
1. ebitengine部の6番目の手順で`wasm_exec.js`ファイルが生成されているので、このファイルをlaravel側の`src/public/js/wasm_exec.js`となるように配置する。
ここまででこのようなディレクリ構成になっているはずです。
![Monosnap home.blade.php — ebiten-web 2022-08-24 19-23-26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489761/15ab6d2e-8890-b901-6640-8e3ebbc6aff5.png)

1. ログイン後のhomeのページにebitengineのアプリを埋め込みます。iframeの中の記述がebitengineのアプリの実行部分になります。
    > ~~~php:src/resources/views/home.blade.php
    > @extends('layouts.app')
    > 
    > @section('content')
    > <div class="container">
    >     <div class="row justify-content-center">
    >         <div class="col-md-8">
    >             <div class="card">
    >                 <div class="card-header">{{ __('Dashboard') }}</div>
    > 
    >                 <div class="card-body d-flex justify-content-center">
    >                     @if (session('status'))
    >                         <div class="alert alert-success" role="alert">
    >                             {{ session('status') }}
    >                         </div>
    >                     @endif
    > 
    >                 <iframe src="" allow = "autoplay" width="640" height="480" srcdoc="
    >                     <!DOCTYPE html>
    >                         <script src='js/wasm_exec.js'></script>
    >                         <script>
    >                             // Polyfill
    >                             if (!WebAssembly.instantiateStreaming) {
    >                                 WebAssembly.instantiateStreaming = async (resp, importObject) => {
    >                                     const source = await (await resp).arrayBuffer();
    >                                     return await WebAssembly.instantiate(source, importObject);
    >                                 };
    >                             }
    > 
    >                             const go = new Go();
    >                                 WebAssembly.instantiateStreaming(fetch('wasm/main.wasm'), > go.importObject).then(result => {
    >                                 go.run(result.instance);
    >                             });
    >                     </script>">
    >                 </iframe>
    >                 </div>
    >             </div>
    >         </div>
    >     </div>
    > </div>
    > 
    > @endsection
4. http://localhost/ にアクセスして、ページ内でebitengineのアプリが実行されていることを確認する
    > ![Monosnap Laravel 2022-08-24 22-46-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489761/d04a2fa6-e145-74fc-adb7-79c953da0da5.png)


## ebitengineにLaravelで取得したデータを受け渡す
ここまででebitengineで作ったアプリをブラウザで遊ぶことができますが、`プレイデータの保存や読み出し`ができないため、いつも同じところからゲームがスタートしてしまいます。

そこで、Laravel側でデータベースからプレイデータを取得してをebitengine側に渡したり、ebitengine側からLaravelのデータセーブAPIを叩くことでプレイデータの保存や読み出しが可能になります。

今回は、`プレイデータの読み出し`の例として、ログインユーザー名をeibtengineに渡して表示させてみましょう。


1. ebitengine側にログインユーザー名を保存する変数とセットする関数の準備と、ユーザー名を画面に表示させるロジックを追加する
    >~~~diff_go:main.go
    >package main
    >
    >import (
    >	"log"
    >	"syscall/js"
    >
    >	"github.com/hajimehoshi/ebiten/v2"
    >	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
    >)
    >
    >type Game struct{}
    >
    >+ var (
    >+ 	userName string = ""
    >+ )
    >
    >func (g *Game) Update() error {
    >	return nil
    >}
    >
    >func (g *Game) Draw(screen *ebiten.Image) {
    >+ 	ebitenutil.DebugPrint(screen, "Hello "+userName+"!!")
    >}
    >
    >func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth,screenHeight int) {
    >	return 320, 240
    >}
    >
    >func main() {
    >	ebiten.SetWindowSize(640, 480)
    >	ebiten.SetWindowTitle("Hello, World!")
    >
    >+	js.Global().Set("setUserName", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
    >+		userName = args[0].String()
    >+		// set return value
    >+		args[1].Invoke(userName)
    >+		return nil
    >+	}))
    >
    >	if err := ebiten.RunGame(&Game{}); err != nil {
    >		log.Fatal(err)
    >	}
    >}
    >~~~
2. WebAssemblyで書き出して、Laravelプロジェクトのwasmファイルを入れ替える
    > $ GOOS=js GOARCH=wasm go build -o main.wasm main.go
3. Laravelのコントローラーからviewにユーザーデータを渡す
    > ~~~diff_php:src/app/Http/Controllers/HomeController.php
    >/**
    > * Show the application dashboard.
    > *
    > * @return \Illuminate\Contracts\Support\Renderable
    > */
    >public function index()
    >{
    >+    $user = Auth::user();
    >+    return view('home', compact('user'));
    >}
4. Laravel側でログインユーザー名をセットする関数を実行する
    > ~~~diff_php:src/resources/views/home.blade.php
    > @extends('layouts.app')
    > 
    > @section('content')
    > <div class="container">
    >     <div class="row justify-content-center">
    >         <div class="col-md-8">
    >             <div class="card">
    >                 <div class="card-header">{{ __('Dashboard') }}</div>
    > 
    >                 <div class="card-body d-flex justify-content-center">
    >                     @if (session('status'))
    >                         <div class="alert alert-success" role="alert">
    >                             {{ session('status') }}
    >                         </div>
    >                     @endif
    > 
    >                 <iframe src="" allow = "autoplay" width="640" height="480" srcdoc="
    >                     <!DOCTYPE html>
    >                         <script src='js/wasm_exec.js'></script>
    >                         <script>
    >                             // Polyfill
    >                             if (!WebAssembly.instantiateStreaming) {
    >                                 WebAssembly.instantiateStreaming = async (resp, importObject) => {
    >                                     const source = await (await resp).arrayBuffer();
    >                                     return await WebAssembly.instantiate(source, importObject);
    >                                 };
    >                             }
    > 
    >                             const go = new Go();
    >                                 WebAssembly.instantiateStreaming(fetch('wasm/main.wasm'), > go.importObject).then(result => {
    >                                 go.run(result.instance);
    >+                               setUserName('{{ $user->name  }}', function(userName){
    >+                                  console.log('ebitengine set userName :', userName)
    >+                               });
    >                             });
    >                     </script>">
    >                 </iframe>
    >                 </div>
    >             </div>
    >         </div>
    >     </div>
    > </div>
    > 
    > @endsection

5. http://localhost/ にアクセスして、ページ内でebitengineのアプリでユーザー名の表示ができている確認する
    > ![Monosnap Laravel 2022-08-24 23-32-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489761/d34af439-ecdc-591e-5e4d-f93b339099f7.png)

### ざっくり解説
1. LaravelのHomeControllerでユーザーデータを$userに格納して、viewに渡す
1. Laravelのviewでebitengineのユーザーネームを設定する関数に、Cotrollerから渡されたユーザーデータからユーザーネームを渡す
1. ebitengine側のユーザーネームを設定する関数で、Laravelから渡されてきたユーザーネームを変数に格納
1. ebitengine側で変数に格納されたユーザーネームを画面に表示する

ユーザーネームの移動としては
DB→php→javascript→go
という感じでバケツリレーのようにユーザーネームを渡しています。

## 最後に
これでデータベースにある値をebitengine側に渡すことができたので、あとはユーザートークンやユーザーIDをLaravelからebitengineに渡してあげて、ebitengineは渡された値を元にLaravelのデータ取得や更新APIを叩くことでプレイデータの保存や更新ができるようになります。
（もしかしたらAPI叩くときにもいろんな壁があるかも）

みなさんも素敵なゲーム作成ライフを！