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。)
- UI Events
-
keydown
: https://w3c.github.io/uievents/#event-type-keydown -
keyup
: https://w3c.github.io/uievents/#event-type-keyup -
keypress
: https://w3c.github.io/uievents/#event-type-keypress -
mousedown
: https://w3c.github.io/uievents/#event-type-mousedown -
mouseup
: https://w3c.github.io/uievents/#event-type-mouseup -
mousemove
: https://w3c.github.io/uievents/#event-type-mousemove
-
- Touch Events
-
touchstart
: https://www.w3.org/TR/touch-events/#dfn-touchstart -
touchend
: https://www.w3.org/TR/touch-events/#dfn-touchend -
touchmove
: https://www.w3.org/TR/touch-events/#dfn-touchmove -
touchcancel
: https://www.w3.org/TR/touch-events/#dfn-touchcancel
-
- DeviceOrientation Event
-
devicemotion
: https://w3c.github.io/deviceorientation/#devicemotion -
deviceorientation
: https://w3c.github.io/deviceorientation/#deviceorientation
-
- Gamepad
-
gamepadconnected
: https://www.w3.org/TR/gamepad/#the-gamepadconnected-event -
gamepaddisconnected
: https://www.w3.org/TR/gamepad/#event-gamepaddisconnected
-
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のチュートリアルをいくつか試すか。
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色空間だともうちょっと調整しないとダメなのかな。あと、
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呼び出しでアロケーションをしないようにするとか地道な最適化が必要だろう。
ゲームパッドに対応する
適当にやった。。
動的挿抜とか(couch co-op時の)ユーザアサインとか考えること色々あるんだよな。。とりあえずデバッグ用にプレイできれば十分なので。