Hands-On Game Development with WebAssembly
上QQ阅读APP看书,第一时间看更新

JavaScript keyboard input

The first thing we will do is learn how to listen for JavaScript keyboard events and make calls into our WebAssembly module based on those events. We will be reusing a lot of the code we wrote for Chapter 2, HTML5 and WebAssembly, so the first thing we should do is grab that code from the Chapter02 folder and copy it into our new Chapter05 folder. Copy the new_shell.html file from inside the Chapter02 directory to the Chapter05 directory, then rename that file jskey_shell.html. Next, copy shell.c from the Chapter02 directory to the Chapter05 directory and rename that file jskey.c. Finally, copy the shell.css file from the Chapter02 directory into the Chapter05 directory, but do not rename it. These three files will give us a starting point for writing the JavaScript keyboard input code.

First, let's take a look at the jskey.c file that we have just created from shell.c. We can get rid of most of the code inside this file right at the beginning. Delete all of the code after the end of the main function. That means you will be deleting all of the following code:

void test() {
printf("button test\n");
}

void int_test( int num ) {
printf("int test=%d\n", num);
}

void float_test( float num ) {
printf("float test=%f\n", num);
}

void string_test( char* str ) {
printf("string test=%s\n", str);
}

Next, we will modify the main function. We no longer want to use EM_ASM inside our main function to call our JavaScript wrapper initialization function, so delete the following two lines of code from the main function:

EM_ASM( InitWrappers() );
printf("Initialization Complete\n");

The only thing left in our main function is a single printf statement. We will change that line to let us know that the main function has run. You can change this code to say anything you like, or remove the printf statement entirely. The following code shows what we have for the main function:

int main() {
printf("main has run\n");
}

Now that we have modified the main function, and removed all of the functions we no longer need, let's put in some functions called when a JavaScript keyboard event is triggered. We will add a function for a keypress event when the user presses one of the arrow keys on the keyboard. The following code will be called by those keypress events:

void press_up() {
printf("PRESS UP\n");
}

void press_down() {
printf("PRESS DOWN\n");
}

void press_left() {
printf("PRESS LEFT\n");
}

void press_right() {
printf("PRESS RIGHT\n");
}

We would also like to know when the user releases a key. So,to do this, we will add four release functions into the C module, as follows:

void release_up() {
printf("RELEASE UP\n");
}

void release_down() {
printf("RELEASE DOWN\n");
}

void release_left() {
printf("RELEASE LEFT\n");
}

void release_right() {
printf("RELEASE RIGHT\n");
}

Now that we have our new C file, we can change our shell file. Open up jskey_shell.html. We do not need to change anything in the head tag, but inside the body, we will want to remove a lot of the HTML elements that we will no longer be using. Go ahead and delete all of the elements except the textarea element. We want to keep our textarea element around so that we can see the output of the printf statements inside our module. We need to delete the following HTML from the jskey_shell.html before our textarea element:

<div class="input_box">&nbsp;</div>
<div class="input_box">
<button id="click_me" class="em_button">Click Me!</button>
</div>

<div class="input_box">
<input type="number" id="int_num" max="9999" min="0" step="1"
value="1" class="em_input">
<button id="int_button" class="em_button">Int Click!</button>
</div>

<div class="input_box">
<input type="number" id="float_num" max="99" min="0" step="0.01"
value="0.0" class="em_input">
<button id="float_button" class="em_button">Float Click!</button>
</div>

<div class="input_box">&nbsp;</div>

Then, after the textarea element, we need to delete the following div and its contents:

<div id="string_box">
<button id="string_button" class="em_button">String Click!</button>
<input id="string_input">
</div>

After that, we have the script tag that contains all of our JavaScript code. We will need to add some global variables into that script tag. First, let's add some Boolean variables that will tell us if the player is pressing any of our arrow keys. Initialize all of these values to false, as per the following example:

var left_key_press = false;
var right_key_press = false;
var up_key_press = false;
var down_key_press = false;

Following our key_press flags, we will have all of the wrapper variables that will be used to hold the wrapper functions that call functions within our WebAssembly module. We will initialize all of these wrappers to null. Later, we will only call these functions if they are not null. The following code shows our wrappers:

var left_press_wrapper = null;
var left_release_wrapper = null;

var right_press_wrapper = null;
var right_release_wrapper = null;

var up_press_wrapper = null;
var up_release_wrapper = null;

var down_press_wrapper = null;
var down_release_wrapper = null;

Now that we have defined all of our global variables, we need to add functions triggered on the key_press and key_release events. The first of these functions is keyPress. The code we have for this function is as follows:

function keyPress() {
event.preventDefault();
if( event.repeat === true ) {
return;
}

// PRESS UP ARROW
if (event.keyCode === 38) {
up_key_press = true;
if( up_press_wrapper != null ) up_press_wrapper();
}

// PRESS LEFT ARROW
if (event.keyCode === 37) {
left_key_press = true;
if( left_press_wrapper != null ) left_press_wrapper();
}

// PRESS RIGHT ARROW
if (event.keyCode === 39) {
right_key_press = true;
if( right_press_wrapper != null ) right_press_wrapper();
}

// PRESS DOWN ARROW
if (event.keyCode === 40) {
down_key_press = true;
if( down_press_wrapper != null ) down_press_wrapper();
}
}

The first line of this function is event.preventDefault();. This line prevents the web browser from doing what it would normally do when the user presses the key in question. For instance, if you are playing a game, and you press the down arrow key to have your spaceship move down, you would not want the web page also to scroll down. Placing this preventDefault call at the beginning of the keyPress function will disable the default behavior for all key presses. In other projects, this may not be what you want. If you only wanted to disable the default behavior when pressing the down arrow key, you would place that call inside of the if block that manages the down arrow key press. The following block of code checks to see if the event is a repeat event:

if( event.repeat === true ) {
return;
}

That would be true if you held down one of the keys. For example, if you held down the up arrow key, you would initially get one up arrow key press event, but, after a delay, you would start getting a repeat event for the up arrow key. You may have noticed that behavior inside a word processor if you have ever held down a single key, like the F key for instance. You would start with a single f that appears inside your word processor, but, after a second or so you would start to get ffffffffffffff, and you would continue to see f repeated into your word processor for as long as you held down the F key. Generally speaking, this behavior may be helpful when you are using a word processor, but is detrimental when you are playing a game. The preceding if block causes us to exit the function when we are receiving repeat key events.

The next several if blocks in our function check the various JavaScript keycodes and make calls to our WebAssembly module based on those keycodes. Let's take a quick look at what happens when the player presses the up arrow key, as follows:

// PRESS UP ARROW
if (event.keyCode === 38) {
up_key_press = true;
if( up_press_wrapper != null ) up_press_wrapper();
}

The if statement is checking the event's keycode against the value 38, which is the keycode value for the up arrow. You can find a list of HTML5 keycodes at: https://www.embed.com/typescript-games/html-keycodes.html. If the triggering event was an up arrow key press, we set the up_key_press variable to true. If our up_press_wrapper is initialized, we call it, which in turn will call the press_up function inside our WebAssembly module. After the if block that checks against the up arrow keycode, we will need more if blocks to check against the other arrow keys, as shown in the following example:

    // PRESS LEFT ARROW
if (event.keyCode === 37) {
left_key_press = true;
if( left_press_wrapper != null ) left_press_wrapper();
}

// PRESS RIGHT ARROW
if (event.keyCode === 39) {
right_key_press = true;
if( right_press_wrapper != null ) right_press_wrapper();
}

// PRESS DOWN ARROW
if (event.keyCode === 40) {
down_key_press = true;
if( down_press_wrapper != null ) down_press_wrapper();
}
}

After the keyUp function, we need to create a very similar function: keyRelease. This function is pretty much the same as keyUpexcept it will be calling the key release functions in the WebAssembly module. The following code shows what the keyRelease() function looks like:

function keyRelease() {
event.preventDefault();

// PRESS UP ARROW
if (event.keyCode === 38) {
up_key_press = false;
if( up_release_wrapper != null ) up_release_wrapper();
}

// PRESS LEFT ARROW
if (event.keyCode === 37) {
left_key_press = false;
if( left_release_wrapper != null ) left_release_wrapper();
}

// PRESS RIGHT ARROW
if (event.keyCode === 39) {
right_key_press = false;
if( right_release_wrapper != null ) right_release_wrapper();
}

// PRESS DOWN ARROW
if (event.keyCode === 40) {
down_key_press = false;
if( down_release_wrapper != null ) down_release_wrapper();
}
}

After we have defined these functions, we need to make them event listeners with the following two lines of JavaScript code:

document.addEventListener('keydown', keyPress);
document.addEventListener('keyup', keyRelease);

The next thing we need to do is modify our InitWrappers function to wrap the functions we created earlier. We do this using the Module.cwrap function. The new version of our InitWrappers function is as follows:

function InitWrappers() {
left_press_wrapper = Module.cwrap('press_left', 'undefined');
right_press_wrapper = Module.cwrap('press_right', 'undefined');
up_press_wrapper = Module.cwrap('press_up', 'undefined');
down_press_wrapper = Module.cwrap('press_down', 'undefined');

left_release_wrapper = Module.cwrap('release_left', 'undefined');
right_release_wrapper = Module.cwrap('release_right', 'undefined');
up_release_wrapper = Module.cwrap('release_up', 'undefined');
down_release_wrapper = Module.cwrap('release_down', 'undefined');
}

We have two functions that are no longer needed that we can remove. These are the runbefore and runafter functions. These functions were used in our shell in chapter 2, HTML5 and WebAssembly, to demonstrate the preRun and postRun module functionality. All they do is log a line out to the console, so please remove the following code from the jskey_shell.html file:

function runbefore() {
console.log("before module load");
}

function runafter() {
console.log("after module load");
}

Now that we have deleted these lines, we can remove the call to these functions from our module's preRun and postRun arrays. Because we had earlier removed the call to EM_ASM( InitWrappers() ); inside our WebAssembly module's main function, we will need to run InitWrappers from the module's postRun array. The following code shows what the beginning of the Module object definition looks like after these changes:

preRun: [],
postRun: [InitWrappers],

Now we should build and test our new JavaScript keyboard handler. Run the following emcc command:

emcc jskey.c -o jskey.html  -s NO_EXIT_RUNTIME=1 --shell-file jskey_shell.html -s EXPORTED_FUNCTIONS="['_main', '_press_up', '_press_down', '_press_left', '_press_right', '_release_up', '_release_down', '_release_left', '_release_right']" -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall']"

You will notice that we have used the -s EXPORT_FUNCTIONS flag to export all of our key press and key release functions. Because we are not using the default shell, we have used the --shell-file jskey_shell.html flag. The -s NO_EXIT_RUNTIME=1 flag prevents the browser from exiting the WebAssembly module if there is no emscripten main loop.  We also exported cwrap and ccall with -s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall']".

The following is a screenshot of the app:

Figure 5.1:  Screenshot of jskey.html

It is important to remember that the app must be run from a web server, or using  emrun . If you do not run the app from a web server, or use emrun, you will receive a variety of errors when the JavaScript glue code attempts to download the WASM and data files. You should also know that IIS requires additional configuration in order to set the proper MIME types for the  .wasm  and  .data  file extensions.

In the next section, we will be using the SDL event handler and the default WebAssembly shell to capture and process keyboard events.