Skip to content

Latest commit

 

History

History
857 lines (648 loc) · 34.9 KB

File metadata and controls

857 lines (648 loc) · 34.9 KB

五、键盘输入

现在我们有了精灵和动画,可以在画布上移动这些精灵,我们需要在游戏中增加一些互动。有几种方法可以让我们的游戏获得键盘输入。一种方法是通过 JavaScript,基于该输入调用我们的 WebAssembly 模块中的不同函数。我们代码的第一部分就是这么做的。我们将在 WebAssembly 模块中添加一些函数,以便用 JavaScript 包装器包装。我们还将设置一些 JavaScript 键盘事件处理程序,每当键盘事件被触发时,我们将使用这些程序来调用我们的 WebAssembly 模块。

另一种方法是让 SDL 为我们做所有繁重的工作。这包括将 C 代码添加到我们捕获SDL_KEYDOWNSDL_KEYUP事件的 WebAssembly 模块中。然后,该模块将查看事件键码,以确定是哪个键触发了该事件。使用这两种方法编写代码都有成本和收益。一般来说,让 SDL 管理我们的键盘输入会让我们失去在 JavaScript 中编写键盘输入管理器的一些灵活性,同时,我们还会受益于更直接的代码。

You will need to include several images in your build to make this project work. Make sure you include the /Chapter05/sprites/ folder from the project's GitHub. If you haven't yet downloaded the GitHub project, you can get it online at: https://github.com/PacktPublishing/Hands-On-Game-Development-with-WebAssembly.

在本章中,我们将执行以下操作:

  • 了解如何使用 JavaScript 键盘事件调用我们的 WebAssembly 模块
  • 了解如何使用 SDL 事件从我们的 WebAssembly 模块内部管理键盘输入
  • 通过使用键盘输入在画布上移动飞船精灵来演示我们所学的内容

JavaScript 键盘输入

我们要做的第一件事是学习如何监听 JavaScript 键盘事件,并根据这些事件调用我们的 WebAssembly 模块。我们将重用很多我们为第 2 章HTML5 和 WebAssembly 编写的代码,所以我们应该做的第一件事是从Chapter02文件夹中抓取该代码,并将其复制到我们新的Chapter05文件夹中。将new_shell.html文件从Chapter02目录复制到Chapter05目录,然后重命名该文件jskey_shell.html。接下来,将shell.cChapter02目录复制到Chapter05目录,并重命名该文件jskey.c。最后,将shell.css文件从Chapter02目录复制到Chapter05目录,但不要重命名。这三个文件将为我们编写 JavaScript 键盘输入代码提供一个起点。

首先,我们来看看刚刚从shell.c创建的jskey.c文件。我们可以在一开始就删除这个文件中的大部分代码。删除main功能结束后的所有代码。这意味着您将删除以下所有代码:

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);
}

接下来,我们将修改main功能。我们不再想使用我们的main函数中的EM_ASM来调用我们的 JavaScript 包装器初始化函数,所以从main函数中删除下面两行代码:

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

我们的main函数中唯一剩下的就是一个单一的printf语句。我们将更改该行,让我们知道main功能已经运行。您可以更改该代码以说出您喜欢的任何内容,或者完全删除printf语句。下面的代码显示了main函数的内容:

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

现在我们已经修改了main函数,并删除了所有不再需要的函数,让我们放入一些在 JavaScript keyboard事件被触发时调用的函数。当用户按下键盘上的一个箭头键时,我们将为keypress事件添加一个功能。这些keypress事件将调用以下代码:

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");
}

我们还想知道用户何时释放密钥。为此,我们将在 C 模块中添加四个release函数,如下所示:

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");
}

现在我们有了新的 C 文件,我们可以更改我们的 shell 文件了。打开jskey_shell.html。我们不需要改变head标签中的任何东西,但是在body里面,我们会想要删除很多我们将不再使用的 HTML 元素。继续删除除textarea元素之外的所有元素。我们希望保留我们的textarea元素,这样我们就可以在我们的模块中看到printf语句的输出。我们需要从textarea元素之前的jskey_shell.html中删除以下 HTML:

<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>

然后,在textarea元素之后,我们需要删除下面的div及其内容:

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

之后,我们有了包含所有 JavaScript 代码的script标签。我们需要在script标签中添加一些全局变量。首先,让我们添加一些布尔变量,它会告诉我们玩家是否按下了我们的任何箭头键。将所有这些值初始化为false,如下例所示:

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

在我们的key_press标志之后,我们将拥有所有的wrapper变量,这些变量将用于保存wrapper函数,这些函数调用我们的 WebAssembly 模块中的函数。我们将所有这些包装器初始化为null。稍后,我们将只调用这些函数,如果它们不是null。下面的代码显示了我们的包装器:

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;

既然我们已经定义了所有的全局变量,我们需要添加在key_presskey_release事件上触发的函数。第一个功能是keyPress。这个函数的代码如下:

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();
    }
}

这个功能的第一行是event.preventDefault();。这一行阻止 web 浏览器做它通常在用户按下有问题的键时会做的事情。例如,如果你正在玩游戏,你按下向下箭头键让你的飞船向下移动,你不会希望网页也向下滚动。在keyPress功能开始时调用preventDefault将禁用所有按键的默认行为。在其他项目中,这可能不是您想要的。如果您只想在按下向下箭头键时禁用默认行为,您可以将该调用放在管理向下箭头键按下的if块内。下面的代码块检查该事件是否是重复事件:

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

如果你按住其中一把钥匙,那就是真的。例如,如果您按住向上箭头键,您最初会得到一个向上箭头键按下事件,但经过一段时间后,您会开始得到一个向上箭头键的重复事件。如果你曾经按下一个键,比如说 F 键,你可能已经注意到了文字处理器内部的这种行为。你会从出现在你的文字处理器中的一个 F 开始,但是,大约一秒钟后,你会开始得到 ffffffffffff,只要你按住 F 键,你就会继续看到 F 重复出现在你的文字处理器中。一般来说,当你使用文字处理器时,这种行为可能会有帮助,但当你玩游戏时,这种行为是有害的。前面的if块使我们在接收重复按键事件时退出该功能。

我们函数中接下来的几个if块检查各种 JavaScript 键码,并基于这些键码调用我们的 WebAssembly 模块。让我们快速了解一下当玩家按下向上箭头键时会发生什么,如下所示:

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

if语句正在对照值38检查事件的键码,该值是向上箭头的键码值。你可以在以下网址找到 HTML5 键码列表:https://www.embed.com/typescript-games/html-keycodes.html。如果触发事件是按下向上箭头键,我们将up_key_press变量设置为true。如果我们的up_press_wrapper被初始化,我们就调用它,这又会调用我们的 WebAssembly 模块中的press_up函数。在检查向上箭头键的if块之后,我们将需要更多的if块来检查其他箭头键,如下例所示:

    // 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();
    }
}

keyUp函数之后,我们需要创建一个非常相似的函数:keyRelease。该功能与keyUp基本相同,只是它将调用 WebAssembly 模块中的关键发布功能。以下代码显示了keyRelease()功能的样子:

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();
    }
}

在我们定义了这些函数之后,我们需要用下面两行 JavaScript 代码使它们成为事件侦听器:

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

接下来我们需要做的是修改我们的InitWrappers函数来包装我们之前创建的函数。我们使用Module.cwrap功能来实现。我们新版本的InitWrappers功能如下:

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');
}

我们有两个不再需要的功能可以删除。这些是runbeforerunafter功能。这些功能在第 2 章 HTML5 和的 shell 中被用来演示preRunpostRun模块的功能。他们所做的只是在控制台上记录一行代码,所以请从jskey_shell.html文件中删除以下代码:

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

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

现在我们已经删除了这些行,我们可以从模块的preRunpostRun数组中删除对这些函数的调用。因为我们之前已经移除了对我们的 WebAssembly 模块的main函数中的EM_ASM( InitWrappers() );的调用,我们将需要从模块的postRun数组中运行InitWrappers。以下代码显示了这些更改后Module对象定义的开头是什么样子的:

preRun: [],
postRun: [InitWrappers],

现在我们应该构建并测试我们新的 JavaScript 键盘处理程序。运行以下emcc命令:

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']"

您会注意到我们已经使用了-s EXPORT_FUNCTIONS标志来导出我们所有的按键和按键释放功能。因为我们没有使用默认外壳,所以我们使用了--shell-file jskey_shell.html标志。如果没有脚本主循环,则-s NO_EXIT_RUNTIME=1标志阻止浏览器退出网络组件模块。我们还出口了cwrapccall``-s EXTRA_EXPORTED_RUNTIME_METHODS="['cwrap', 'ccall']"

以下是该应用的截图:

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.

在下一节中,我们将使用 SDL 事件处理程序和默认的 WebAssembly shell 来捕获和处理键盘事件。

向网络组件添加 SDL 键盘输入

SDL 允许我们轮询键盘输入。每当用户按下一个键,对SDL_PollEvent( &event )的呼叫将返回给我们一个SDK_KEYDOWN SDL_Event。当一个键被释放时,它将返回一个SDK_KEYUP事件。在这种情况下,我们可以查看值,找出哪个键被按下或释放。我们可以利用这些信息在我们的游戏中设置旗帜,让我们知道什么时候移动我们的飞船,向什么方向移动。稍后,我们可以添加代码来检测将发射我们飞船武器的空格键按压。

现在,我们将回到使用默认的 Emscripten shell。在本节的剩余部分,我们将能够从 WebAssembly C 代码中完成所有的工作。我将带您从头开始创建一个新的keyboard.c文件,它将处理键盘事件并打印到我们默认外壳中的textarea中。

首先创建一个新的keyboard.c文件,并在文件顶部添加以下#include指令:

#include <SDL2/SDL.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>

之后,我们需要添加我们的全局SDL对象。前两个,SDL_WindowSDL_Renderer,现在应该很熟悉了。第三个SDL_Event,是新的。我们将在后面的代码中使用对SDL_PollEvent的调用来填充这个事件对象:

SDL_Window *window;
SDL_Renderer *renderer;
SDL_Event event;

像这段代码的 JavaScript 版本一样,我们将使用全局变量来跟踪我们当前正在按下的箭头键。这些都是布尔变量,如下面的代码所示:

bool left_key_press = false;
bool right_key_press = false;
bool up_key_press = false;
bool down_key_press = false;

我们要定义的第一个函数是input_loop,但是在定义该函数之前,我们需要声明input_loop将要调用的两个函数,如下所示:

void key_press();
void key_release();

这将允许我们在实际定义input_loop调用这些函数时会发生什么之前定义input_loop函数。input_loop函数将调用SDL_PollEvent来获取事件对象。然后我们可以查看事件的类型,如果是SDL_KEYDOWNSDL_KEYUP事件,我们可以调用适当的函数来处理这些事件,如下所示:

void input_loop() {
    if( SDL_PollEvent( &event ) ){
        if( event.type == SDL_KEYDOWN ){
            key_press();
        }
        else if( event.type == SDL_KEYUP ) {
            key_release();
        }
    }
}

我们将定义的第一个函数是key_press()函数。在这个函数中,我们将查看开关中的键盘事件,并将该值与不同的箭头键 SDLK 事件进行比较。如果该键之前已被按下,它会打印出一条消息,让我们知道用户按下的键。那我们就应该把keypress旗设为true。以下示例完整显示了key_press()功能:

void key_press() {
    switch( event.key.keysym.sym ){
        case SDLK_LEFT:
            if( !left_key_press ) {
                printf("left arrow key press\n");
            }
            left_key_press = true;
            break;

        case SDLK_RIGHT:
            if( !right_key_press ) {
                printf("right arrow key press\n");
            }
            right_key_press = true;
            break;

        case SDLK_UP:
            if( !up_key_press ) {
                printf("up arrow key press\n");
            }
            up_key_press = true;
            break;

        case SDLK_DOWN:
            if( !down_key_press ) {
                printf("down arrow key press\n");
            }
            down_key_press = true;
            break;

        default:
            printf("unknown key press\n");
            break;
    }
}

key_press函数内部的第一行是 switch 语句,switch(event.key.keysym.sym)。这些是结构中的结构。在input_loop函数中,我们调用SDL_PollEvent,传递对SDL_Event结构的引用。这个结构包含可能返回给我们的任何可能事件的事件数据,以及告诉我们这是什么类型的事件的类型。如果类型为SDL_KEYDOWNSDL_KEYUP,则表示内部key结构被填充,该结构是类型为SDL_KeyboardEvent的结构。如果你想了解SDL_Event结构的完整定义,你可以在 SDL 网站上找到,网址是:https://wiki.libsdl.org/SDL_Event。查看SDL_Event内部的关键变量,会发现是一个SDL_KeyboardEvent类型的结构。这个结构中有很多我们还不会用到的数据。它包括时间戳、该键是否是重复按下,或者该键是否正在被按下或释放等信息;但是我们在开关中看到的是它们keysym变量,这是一个SDL_Keysym类型的结构。关于SDL_KeyboardEvent的更多信息,可以在 SDL 网站上找到它的定义,网址为:https://wiki.libsdl.org/SDL_KeyboardEventSDL_KeyboardEvent结构中的keysym变量就是你会在sym变量中找到SDL_Keycode的地方。这个键码是我们必须看的,以确定玩家按了哪个键。这就是我们围绕switch( event.key.keysym.sym )构建开关语句的原因。SDL 键码所有可能值的链接位于:https://wiki.libsdl.org/SDL_Keycode

我们的开关中的所有 case 语句看起来都非常相似:如果按下了给定的 SDLK 键码,我们会检查该键在前一个周期中是否被按下,如果没有,我们只打印出该值。然后我们将keypress标志设置为true。以下示例显示了我们检测到按下左箭头键的代码:

case SDLK_LEFT:
    if( !left_key_press ) {
        printf("left arrow key press\n");
    }
    left_key_press = true;
    break;

当事件类型为SDL_KEYUP时,我们的应用调用key_release函数。这与key_down功能非常相似。主要区别在于,它会查看用户是否按下了键,并且仅在状态变为未按下时才打印出消息。以下示例显示了该函数的全部内容:

void key_release() {
    switch( event.key.keysym.sym ){

        case SDLK_LEFT:
            if( left_key_press ) {
                printf("left arrow key release\n");
            }
            left_key_press = false;
            break;

        case SDLK_RIGHT:
            if( right_key_press ) {
                printf("right arrow key release\n");
            }
            right_key_press = false;
            break;

        case SDLK_UP:
            if( up_key_press ) {
                printf("up arrow key release\n");
            }
            up_key_press = false;
            break;

        case SDLK_DOWN:
            if( down_key_press ) {
                printf("down arrow key release\n");
            }
            down_key_press = false;
            break;

        default:
            printf("unknown key release\n");
            break;
    }
}

我们的最后一个函数是新版本的main函数,在我们的Module加载时调用。我们仍然需要使用emscripten_set_main_loop来防止我们的代码捆绑 JavaScript 引擎。我们已经创建了一个input_loop,这是我们之前定义的。它使用 SDL 来调查键盘事件。但是,在此之前,我们仍然需要进行 SDL 初始化。我们使用的是 Emscripten 默认外壳,因此对SDL_CreateWindowAndRenderer的调用将设置我们的canvas元素的宽度和高度。我们不会渲染到我们的input_loop中的canvas元素,但是我们仍然希望在这里对它进行初始化,因为在下一节中,我们将修改这段代码,将飞船图像渲染到画布上,并通过按键来移动它。下面的代码显示了我们新版本的main功能是什么样子的:

int main() {
    SDL_Init( SDL_INIT_VIDEO );

    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );
    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );

    SDL_RenderClear( renderer );
    SDL_RenderPresent( renderer );

    emscripten_set_main_loop(input_loop, 0, 0);
    return 1;
}

现在我们已经有了keyboard.c文件中的所有代码,我们可以用下面的emcc命令编译我们的keyboard.c文件:

emcc keyboard.c -o keyboard.html -s USE_SDL=2

当您在浏览器中运行keyboard.html时,您会注意到按下箭头键会导致一条消息被打印到 Emscripten 默认 shell 的文本区域。

考虑以下截图:

Figure 5.2: Screenshot of keyboard.html

在下一节中,我们将学习如何使用这个键盘输入在画布上移动精灵。

使用键盘输入移动精灵

既然我们知道了如何获得键盘输入并在我们的 WebAssembly 模块中使用它,那么让我们弄清楚如何获得键盘输入并使用它在 HTML 画布上移动我们的宇宙飞船精灵。让我们从将Chapter04目录复制到Chapter05目录开始。这将为我们提供一个良好的起点。现在我们可以开始修改代码了。我们需要在我们的.c文件的开头添加一个单独的#include。因为需要布尔变量,所以必须加上#include <stdbool.h>。我们的.c文件的新开始将如下所示:

#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <emscripten.h>
#include <stdio.h>
#include <stdbool.h>

此后,所有#define指令将保持不变,与它们在sprite_move.c文件中的状态相同,如以下代码所示:

#define SPRITE_FILE "sprites/Franchise1.png"
#define ANIM_FILE "sprites/Franchise%d.png"
#define FRAME_COUNT 4

sprite_move.c文件有几个全局变量,我们将在keyboard_move.c中继续使用。不要删除任何这些变量;我们只会增加:

int current_frame = 0;

Uint32 last_time;
Uint32 current_time;
Uint32 ms_per_frame = 100; // animate at 10 fps

SDL_Window *window;
SDL_Renderer *renderer;
SDL_Rect dest = {.x = 160, .y = 100, .w = 0, .h = 0 };

SDL_Texture *sprite_texture;
SDL_Texture *temp_texture;
SDL_Texture* anim[FRAME_COUNT];

现在我们需要从上一节使用的keyboard.c文件中引入一些变量。我们需要SDL_Event全局变量,这样我们就有东西可以传递到我们对SDL_PollEvent的调用中,并且我们需要我们的布尔按键标志,如下所示:

SDL_Event event;

bool left_key_press = false;
bool right_key_press = false;
bool up_key_press = false;
bool down_key_press = false;

然后我们有函数声明,允许我们在定义了input_loop函数之后定义key_presskey_release函数,如下例所示:

void key_press();
void key_release();

接下来,我们将从keyboard.c文件中引入input_loop功能。这是我们用来调用SDL_PollEvent的函数,根据返回的事件类型,调用key_presskey_release。该功能与我们在keyboard.c中的版本保持不变,如下例所示:

void input_loop() {
    if( SDL_PollEvent( &event ) ){
        if( event.type == SDL_KEYDOWN ){
            key_press();
        }
        else if( event.type == SDL_KEYUP ) {
            key_release();
        }
    }
}

key_presskey_release功能沿用input_loop功能,与keyboard.c版本保持不变。这些功能的主要目的是设置按键标志。printf声明现在没有必要了,但我们将把它们留在那里。这对于性能来说并不是一件好事,因为在每次按键和释放时继续为我们的textarea添加线条最终会降低我们的游戏速度,但是,在这一点上,我觉得最好将这些语句留在中,以供演示:

void key_press() {
    switch( event.key.keysym.sym ){

        case SDLK_LEFT:
            if( !left_key_press ) {
                printf("left arrow key press\n");
            }
            left_key_press = true;
            break;

        case SDLK_RIGHT:
            if( !right_key_press ) {
                printf("right arrow key press\n");
            }
            right_key_press = true;
            break;

        case SDLK_UP:
            if( !up_key_press ) {
                printf("up arrow key press\n");
            }
            up_key_press = true;
            break;

        case SDLK_DOWN:
            if( !down_key_press ) {
                printf("down arrow key press\n");
            }
            down_key_press = true;
            break;

        default:
            printf("unknown key press\n");
            break;
    }
}

void key_release() {
    switch( event.key.keysym.sym ){

        case SDLK_LEFT:
            if( left_key_press ) {
                printf("left arrow key release\n");
            }
            left_key_press = false;
            break;

        case SDLK_RIGHT:
            if( right_key_press ) {
                printf("right arrow key release\n");
            }
            right_key_press = false;
            break;

        case SDLK_UP:
            if( up_key_press ) {
                printf("up arrow key release\n");
            }
            up_key_press = false;
            break;

        case SDLK_DOWN:
            if( down_key_press ) {
                printf("down arrow key release\n");
            }
            down_key_press = false;
            break;

        default:
            printf("unknown key release\n");
            break;
    }
}

keyboard_move.c文件中的下一个功能将是show_animation。该功能将需要从出现在sprite_move.c的版本中进行重大改变,以允许玩家控制飞船并在画布上移动它。下面的例子向您展示了新函数的全部内容,然后我们一次遍历一部分:

void show_animation() {
    input_loop();

    current_time = SDL_GetTicks();
    int ms = current_time - last_time;

    if( ms >= ms_per_frame) {
        ++ current_frame;
        last_time = current_time;
    }

    if( current_frame >= FRAME_COUNT ) {
        current_frame = 0;
    }

    SDL_RenderClear( renderer );
    temp_texture = anim[current_frame];

    if( up_key_press ) {
        dest.y--;

        if( dest.y < -16 ) {
            dest.y = 200;
        }
    }

    if( down_key_press ) {
        dest.y++ ;

        if( dest.y > 200 ) {
            dest.y = -16;
        }
    }

    if( left_key_press ) {
        dest.x--;

        if( dest.x < -16 ) {
            dest.x = 320;
        }
    }

    if( right_key_press ) {
        dest.x++ ;

        if( dest.x > 320 ) {
            dest.x = -16;
        }
    }

    SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
    SDL_RenderPresent( renderer );
}

我们在这个新版本的函数中添加了第一行show_animation。对input_loop的调用用于设置每帧的按键标志。调用input_loop后,有一大块代码我们没有从sprite_move.c文件中更改,如下例所示:

current_time = SDL_GetTicks();
int ms = current_time - last_time;

if( ms >= ms_per_frame) {
    ++ current_frame;
    last_time = current_time;
}

if( current_frame >= FRAME_COUNT ) {
    current_frame = 0;
}

SDL_RenderClear( renderer );
temp_texture = anim[current_frame];

这段代码调用SDL_GetTicks()获取当前时间,然后从当前帧最后一次改变的时间中减去当前时间,得到我们上次帧改变后的毫秒数。如果自最后一帧改变以来的毫秒数大于我们希望停留在任何给定帧上的毫秒数,我们需要提前当前帧。一旦我们弄清楚是否推进了当前帧,我们需要确保当前帧不超过我们的帧数。如果是,我们需要将其重置为0。之后,我们需要清除我们的渲染器,并将我们正在使用的纹理设置为与当前帧相对应的动画数组中的纹理。

sprite_move.c中,我们用下面几行代码将飞船的y坐标每帧上移一个像素:

dest.y--;

if( dest.y < -16 ) {
    dest.y = 200;
}

在新的键盘 app 中,我们只想在玩家按下向上箭头键时改变我们的y坐标。为此,我们必须将更改y坐标的代码放在检查up_key_press标志的if块中。下面是该代码的新版本:

if( up_key_press ) {
    dest.y--;

    if( dest.y < -16 ) {
        dest.y = 200;
    }
}

我们还需要添加当玩家按下其他箭头键时移动飞船的代码。以下代码根据玩家当前按下的键向下、向左或向右移动飞船:

if( down_key_press ) {
    dest.y++ ;

    if( dest.y > 200 ) {
        dest.y = -16;
    }
}

if( left_key_press ) {
    dest.x--;

    if( dest.x < -16 ) {
        dest.x = 320;
    }
}

if( right_key_press ) {
    dest.x++ ;

    if( dest.x > 320 ) {
        dest.x = -16;
    }
}

最后,我们必须渲染纹理并呈现它,如下所示:

SDL_RenderCopy( renderer, temp_texture, NULL, &dest );
SDL_RenderPresent( renderer );

main功能不会从sprite_move.c内部的版本改变,因为初始化没有改变。以下代码显示了出现在keyboard_move.c中的main功能:

int main() {
    char explosion_file_string[40];

    SDL_Init( SDL_INIT_VIDEO );
    SDL_CreateWindowAndRenderer( 320, 200, 0, &window, &renderer );
    SDL_SetRenderDrawColor( renderer, 0, 0, 0, 255 );
    SDL_RenderClear( renderer );

    SDL_Surface *temp_surface = IMG_Load( SPRITE_FILE );

    if( !temp_surface ) {
        printf("failed to load image: %s\n", IMG_GetError() );
        return 0;
    }

    sprite_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );

    SDL_FreeSurface( temp_surface );

    for( int i = 1; i <= FRAME_COUNT; i++ ) {
        sprintf( explosion_file_string, ANIM_FILE, i );
        SDL_Surface *temp_surface = IMG_Load( explosion_file_string );

        if( !temp_surface ) {
            printf("failed to load image: %s\n", IMG_GetError() );
            return 0;
        }

        temp_texture = SDL_CreateTextureFromSurface( renderer, temp_surface );
        anim[i-1] = temp_texture;
        SDL_FreeSurface( temp_surface );
    }

    SDL_QueryTexture( sprite_texture,
                        NULL, NULL,
                        &dest.w, &dest.h ); // query the width and height

    dest.x -= dest.w / 2;
    dest.y -= dest.h / 2;

    SDL_RenderCopy( renderer, sprite_texture, NULL, &dest );
    SDL_RenderPresent( renderer );

    last_time = SDL_GetTicks();
    emscripten_set_main_loop(show_animation, 0, 0);
    return 1;
}

正如我之前所说的,这段代码是我们在第 4 章中用 SDL 在 WebAssembly 中编写的最后一个应用和我们在部分中编写的代码的组合,在部分中,我们将 SDL 键盘输入添加到 WebAssembly* 中,我们从键盘获取输入,并用printf语句记录我们的按键。我们保留了我们的input_loop函数,并从我们的show_animation函数开始添加了对它的调用。在show_animation中,我们不再每帧向上移动一个像素,而是只在按下向上箭头键时向上移动船只。同样,当用户按下左箭头键时,我们向左移动船只,当按下右箭头键时,我们向右移动船只,当用户按下下箭头键时,我们向下移动船只。*

现在我们有了新的keyboard_move.c文件,让我们编译它,并尝试我们新的移动飞船。运行以下emcc命令编译代码:

emcc keyboard_move.c -o keyboard_move.html --preload-file sprites -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"]

我们需要添加--preload-file sprites标志来表明我们想要一个包含 sprites 文件夹的虚拟文件系统。我们还需要添加-s USE_SDL=2-s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS=["png"]标志,以允许我们从虚拟文件系统加载.png文件。一旦你编译好了keyboard_move.html,把它加载到浏览器中,用箭头键在画布上移动飞船。请看下面的截图:

Figure 5.3: Screenshot of keyboard_move.html

摘要

在这一章中,我们学习了如何获取键盘输入以供 WebAssembly 使用。主要有两种方法。我们可以在 JavaScript 端接受键盘输入,并通过用Module.cwrap制作的包装器与 WebAssembly 通信,或者直接用Module.ccall调用 WebAssembly 函数。在 WebAssembly 中接受键盘输入的另一种方法是使用 SDL 键盘输入事件。当我们使用这个方法时,我们可以使用默认的 Emscripten shell。第二种方法,使用 SDL 事件,将是我们在本书其余部分的首选方法。

在下一章中,我们将了解更多关于游戏循环的知识,以及我们将如何在游戏中使用它,以及一般的游戏。