Open
7

WebGL-Native: 入力に対応する

prev: https://zenn.dev/okuoku/scraps/7bf87a81129b7d
next: https://zenn.dev/okuoku/scraps/cb8a6b831f5501

入力 → AssetStoreのゲームテンプレートで遊べそうな奴を探す → オーディオ → ファイルシステム の順で行こうか。。

Unity WebGLがlistenしてくるEventは以下:

Add Event Listender keydown [Function: jsEventHandler] 0
Add Event Listender keyup [Function: jsEventHandler] 0
Add Event Listender keypress [Function: jsEventHandler] 1
Add Event Listender mouseup [Function: jsEventHandler] 0
Add Event Listender mousedown [Function: jsEventHandler] 0
Add Event Listender mousemove [Function: jsEventHandler] 0
Add Event Listender touchstart [Function: jsEventHandler] 0
Add Event Listender touchend [Function: jsEventHandler] 0
Add Event Listender touchmove [Function: jsEventHandler] 0
Add Event Listender touchcancel [Function: jsEventHandler] 0
Add Event Listender devicemotion [Function: jsEventHandler] 0
Add Event Listender deviceorientation [Function: jsEventHandler] 0
Add Event Listender gamepadconnected [Function: jsEventHandler] 0
Add Event Listender gamepaddisconnected [Function: jsEventHandler] 0

(Listenderって何だよ。。Listenerのtypo。)

GamePadさえあればKeyは省略できないかな。。どうかな。。 TouchとDeviceOrientationは操作に必須ということは無いはずなので今回はサボる。

SDLのイベント構造体

... 色々考えたけど良い抽象が思いつかないので一旦tentativeにSDLのイベントをほぼ生でやりとりしよう。。

例えば、キーボードイベントはIMEと干渉するため比較的真面目に設計しないと文字入力のあるゲームを(本来は)サポートできない。ある程度実際のアプリを動かしてから考えた方が良い気がする。

DOMのキーボードイベントはキーの物理位置をコード化している。SDLでは、これはスキャンコード http://wiki.libsdl.org/SDL_Scancode に相当する。つまり、DOMにせよSDLにせよ、実際に入力される文字へのマップはアプリケーション自身が行う必要がある。

SDLにはJoyStickとControllerの2つのゲームコントローラAPIがある。JoyStickが伝統的なAPIで、Controllerはそれにボタンのリマップ機能(= 要するに標準Xboxコントローラにマップする機能)が付いている。

イベントの分析

この辺はUnityはEmscriptenのものをほぼそのまま使っているので、Emscriptenと同じ要素をひっぱって来るのが良さそうと言える。(ちなみにオーディオは別物っぽい)

キーボード

JS上ではこんな感じ:

        HEAP32[idx + 0] = e.location;
        HEAP32[idx + 1] = e.ctrlKey;
        HEAP32[idx + 2] = e.shiftKey;
        HEAP32[idx + 3] = e.altKey;
        HEAP32[idx + 4] = e.metaKey;
        HEAP32[idx + 5] = e.repeat;
        HEAP32[idx + 6] = e.charCode;
        HEAP32[idx + 7] = e.keyCode;
        HEAP32[idx + 8] = e.which;
        stringToUTF8(e.key || '', keyEventData + 36, 32);
        stringToUTF8(e.code || '', keyEventData + 68, 32);
        stringToUTF8(e.char || '', keyEventData + 100, 32);
        stringToUTF8(e.locale || '', keyEventData + 132, 32);

SDLのキーイベントはモディファイヤ(ShiftやCtrl等の状態)を送ってこないので、同時にキャプチャして送る必要がある。

マウス

      HEAP32[idx + 0] = e.screenX;
      HEAP32[idx + 1] = e.screenY;
      HEAP32[idx + 2] = e.clientX;
      HEAP32[idx + 3] = e.clientY;
      HEAP32[idx + 4] = e.ctrlKey;
      HEAP32[idx + 5] = e.shiftKey;
      HEAP32[idx + 6] = e.altKey;
      HEAP32[idx + 7] = e.metaKey;
      HEAP16[idx*2 + 16] = e.button;
      HEAP16[idx*2 + 17] = e.buttons;

      HEAP32[idx + 9] = e["movementX"]
        ;

      HEAP32[idx + 10] = e["movementY"]
        ;

      var rect = __getBoundingClientRect(target);
      HEAP32[idx + 11] = e.clientX - rect.left;
      HEAP32[idx + 12] = e.clientY - rect.top;

Movement〜 はPointerLock(今回は対応しない)用なので無視できる。

ゲームパッド

マウスやキーボードと違って、ゲームパッドは入力のpollingをクライアント側が行う必要がある。

  function __fillGamepadEventData(eventStruct, e) {
      HEAPF64[((eventStruct)>>3)]=e.timestamp;
      for(var i = 0; i < e.axes.length; ++i) {
        HEAPF64[(((eventStruct+i*8)+(16))>>3)]=e.axes[i];
      }
      for(var i = 0; i < e.buttons.length; ++i) {
        if (typeof(e.buttons[i]) === 'object') {
          HEAPF64[(((eventStruct+i*8)+(528))>>3)]=e.buttons[i].value;
        } else {
          HEAPF64[(((eventStruct+i*8)+(528))>>3)]=e.buttons[i];
        }
      }
      for(var i = 0; i < e.buttons.length; ++i) {
        if (typeof(e.buttons[i]) === 'object') {
          HEAP32[(((eventStruct+i*4)+(1040))>>2)]=e.buttons[i].pressed;
        } else {
          // Assigning a boolean to HEAP32, that's ok, but Closure would like to warn about it:
          /** @suppress {checkTypes} */
          HEAP32[(((eventStruct+i*4)+(1040))>>2)]=e.buttons[i] == 1;
        }
      }
      HEAP32[(((eventStruct)+(1296))>>2)]=e.connected;
      HEAP32[(((eventStruct)+(1300))>>2)]=e.index;
      HEAP32[(((eventStruct)+(8))>>2)]=e.axes.length;
      HEAP32[(((eventStruct)+(12))>>2)]=e.buttons.length;
      stringToUTF8(e.id, eventStruct + 1304, 64);
      stringToUTF8(e.mapping, eventStruct + 1368, 64);
    }

Gamepadイベントはボタン状態と id の両方を含んだ大きなデータになっている。

適当なゲームを移植する(1)

とりあえずUnityのチュートリアルをいくつか試すか。

https://assetstore.unity.com/packages/essentials/tutorial-projects/3d-beginner-complete-project-143846

TAAはGLES2では正常にビルドできない

Shader error in 'Hidden/PostProcessing/FinalPass': Input signature parameter  (1-based Entry 3) type must be a scalar uint. at line 44 (on gles)

ので一旦無効化した。が、

やっぱりなんか妙に明いな。。たぶんゲーム自体がLinear前提で、(WebGL1では必須になる)Gamma色空間だともうちょっと調整しないとダメなのかな。あと、

https://issuetracker.unity3d.com/issues/audio-no-longer-plays-in-build

Unity自体のバグ( ! )でWebブラウザでも音が出ない。。修正版はプレリリースされているので後で試す。

また、フレームレートはゲームにならないくらい低い。明かにCPUバウンドなので、ネイティブ呼び出しをもっと真面目に最適化しないとダメそう。 ...もしかしたら単にdrawcallが多いだけかもしれないけど。

プロファイルする

とりあえず↑のサンプルでプロファイリングをとってみた( node --prof )。

 [JavaScript]:
   ticks  total  nonlib   name
    682    1.0%    3.7%  LazyCompile: *proxy C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ffi-napi\lib\_foreign_function.js:32:26
    579    0.9%    3.1%  Function: *__Z8Perlin3DRKN4math8floatNx3ERKf
    547    0.8%    2.9%  LazyCompile: *get C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\debug\src\common.js:123:9
    359    0.6%    1.9%  LazyCompile: *alloc C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:513:32
    270    0.4%    1.4%  LazyCompile: *debug C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\debug\src\common.js:64:17
    233    0.4%    1.2%  Function: __Z21DecodeAlpha3BitLinearPjRK23DXTAlphaBlock3BitLineariji
    192    0.3%    1.0%  LazyCompile: *get C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:444:28
    162    0.2%    0.9%  Function: __Z16DecodeColorBlockPjRK11DXTColBlockiPKj
    156    0.2%    0.8%  LazyCompile: *writePointer C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:747:46
    143    0.2%    0.8%  LazyCompile: *set C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:480:28
    140    0.2%    0.7%  Function: __Z19GetColorBlockColorsPK11DXTColBlockP9Color8888
    116    0.2%    0.6%  Function: _UNITY_LZ4_decompress_safe

_UNITY_LZ4_decompress_safe とかはアセットのロードに使われるものなので今回は無視して良いとして、やっぱりFFI呼び出し関連が大半を占めている。

Bottom up profilerで割ときれいに頻出のOpenGLコマンドが出ている。

   4999   15.4%    LazyCompile: *writePointer C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:747:46
   4902   98.1%      LazyCompile: *set C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:480:28
   4901  100.0%        LazyCompile: *alloc C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ref-napi\lib\ref.js:513:32
   4901  100.0%          LazyCompile: *proxy C:\cygwin64\home\oku\repos\cwgl\jstestapp\node_modules\ffi-napi\lib\_foreign_function.js:32:26
   1978   40.4%            LazyCompile: *_glUniform4fv :13153:23
    884   18.0%            LazyCompile: *_glActiveTexture :11555:26
    392    8.0%            LazyCompile: *_glVertexAttribPointer :13294:32
    355    7.2%            LazyCompile: *_glBindBuffer :11571:23
    265    5.4%            LazyCompile: *_glBindTexture :11597:24
    190    3.9%            LazyCompile: *_glDrawElements :11868:25
    157    3.2%            LazyCompile: *_glDisable :11849:20
    118    2.4%            LazyCompile: *_glEnable :11874:19
    109    2.2%            LazyCompile: *_glScissor :12971:20
     91    1.9%            LazyCompile: ~getActiveUniform C:\cygwin64\home\oku\repos\cwgl\jstestapp\webgl-cwgl.js:774:35

40%〜(選択部分の直上)は呼び出したFFI関数内部の処理 == OpenGLESエミュレータ内部で、その呼び出しコスト(選択部分)が15%〜となっている。ただ、

 [Summary]:
   ticks  total  nonlib   name
   7867   12.1%   42.1%  JavaScript
      0    0.0%    0.0%  C++
   8516   13.1%   45.6%  GC
  46562   71.4%          Shared libraries
  10811   16.6%          Unaccounted

GCも無視できないコストになっている。

たぶん、呼び出しのバッチングやFFI呼び出しでアロケーションをしないようにするとか地道な最適化が必要だろう。

ログインするとコメントできます