将 Surreal Engine C++ 移植到 WebAssembly

在这篇文章中,我将描述如何将 Surreal Engine 游戏引擎移植到 WebAssembly。

超现实引擎–一个游戏引擎,实现了Unreal Engine 1的大部分功能,著名的游戏都使用这个引擎–虚幻竞技场 99、虚幻、杀出重围、不朽。它指的是主要在单线程执行环境中工作的经典引擎。

最初,我有一个想法,那就是承担一个我无法在任何合理时间内完成的项目,从而向我的 Twitch 粉丝表明,有些项目甚至是我也无法完成。在我的第一次直播中,我突然意识到使用 Emscripten 将 Surreal Engine C++ 移植到 WebAssembly 的任务是可行的。

Surreal Engine Emscripten Demo

一个月后,我可以在 WebAssembly 上演示我的前叉和引擎组装:
https://demensdeum.com/demos/SurrealEngine/

与原始版本一样,控制是使用键盘箭头进行的。接下来,我计划将其调整为移动控制 (tachi),添加正确的光照和 Unreal Tournament 99 渲染的其他图形功能。

从哪里开始?

我想说的第一件事是,任何项目都可以使用 Emscripten 从 C++ 移植到 WebAssembly,唯一的问题是功能有多完整。选择一个库端口已经可用于 Emscripten 的项目;对于 Surreal Engine,你非常幸运,因为该引擎使用 SDL 2、OpenAL 库。它们都被移植到 Emscripten。不过,Vulkan 用作图形 API,目前无法用于 HTML5,实现 WebGPU 的工作正在进行中,但也处于草案阶段,也未知从 Vulkan 到 WebGPU 的进一步移植有多简单,完全标准化后。因此,我必须为 Surreal Engine 编写自己的基本 OpenGL-ES / WebGL 渲染器。

构建项目

在 Surreal Engine 中构建系统 – CMake,这也简化了移植,因为Emscripten 提供其原生构建器“ emcmake,emmake。
Surreal Engine 移植基于我最新的 WebGL/OpenGL ES 和 C++ 游戏代码,称为 Death-Mask,因此开发更加简单,我随身携带了所有必要的构建标志和代码示例。

CMakeLists.txt 中最重要的一点是 Emscripten 的构建标志,下面是项目文件中的示例:


-s MAX_WEBGL_VERSION=2 \

-s EXCEPTION_DEBUG \

-fexceptions \

--preload-file UnrealTournament/ \

--preload-file SurrealEngine.pk3 \

--bind \

--use-preload-plugins \

-Wall \

-Wextra \

-Werror=return-type \

-s USE_SDL=2 \

-s ASSERTIONS=1 \

-w \

-g4 \

-s DISABLE_EXCEPTION_CATCHING=0 \

-O3 \

--no-heap-copy \

-s ALLOW_MEMORY_GROWTH=1 \

-s EXIT_RUNTIME=1")

构建脚本本身:


emmake make -j 16

cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data

cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js

cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm

cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

cp ../buildScripts/Emscripten/background.png /srv/http/SurrealEngine/background.png

接下来我们将准备索引.html ,其中包括项目文件系统预加载器。为了上传到网络,我使用了 Unreal Tournament Demo 版本 338。正如您从 CMake 文件中看到的,解压后的游戏文件夹已添加到构建目录中,并作为 Emscripten 的预加载文件进行链接。

主要代码更改

那就需要改变游戏的游戏循环,不能无限循环,这样会导致浏览器卡住,而是需要使用emscripten_set_main_loop,我在2017年的笔记中写过这个功能< a href="https://demensdeum.com/blog/ru/2017/03/29/porting-sdl-c-game-to-html5-emscripten/" rel="noopener" target="_blank">将 SDL C++ 游戏移植到 HTML5 (Emscripten)”
我们将退出 while 循环的代码更改为 if,然后在全局范围内显示包含游戏循环的游戏引擎的主类,并编写一个全局函数,该函数将从全局对象中调用游戏循环步骤:


#include <emscripten.h>

Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;

void emscripten_game_loop_step() {

	EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();

}

#endif

之后,您需要确保应用程序中没有后台线程;如果有,则准备将其重写为单线程执行,或者使用 Emscripten 中的 phtread 库。
Surreal Engine中的后台线程用于播放音乐,数据来自主引擎线程,有关当前曲目、是否需要播放音乐或是否缺少音乐,然后后台线程通过互斥体接收新状态并开始播放新音乐,或暂停。背景流还用于在播放期间缓冲音乐。
我尝试使用 pthread 为 Emscripten 构建 Surreal Engine,但没有成功,因为 SDL2 和 OpenAL 端口是在没有 pthread 支持的情况下构建的,而且我不想为了音乐而重建它们。因此,我使用循环将背景音乐流的功能转移到单线程执行。通过从 C++ 代码中删除 pthread 调用,我将缓冲和音乐播放移至主线程,这样就不会出现延迟,我将缓冲区增加了几秒钟。

接下来我会描述图形和声音的具体实现。

不支持 Vulkan!

是的,HTML5 不支持 Vulkan,尽管所有营销手册都将跨平台和广泛的平台支持作为 Vulkan 的主要优势。因此,我必须为简化的 OpenGL 类型编写自己的基本图形渲染器 – ES,它用在移动设备上,有时它不包含现代OpenGL的时尚功能,但它很好地移植到WebGL,这正是Emscripten所实现的。写了基本的tile渲染,bsp渲染,最简单的GUI显示,渲染模型+贴图,两周就完成了。这也许是该项目中最困难的部分。实现 Surreal Engine 渲染的全部功能还有很多工作要做,因此欢迎读者以代码和拉取请求的形式提供任何帮助。

支持 OpenAL!

幸运的是,Surreal Engine 使用 OpenAL 进行音频输出。在 OpenAL 中编写了一个简单的 hello world 并使用 Emscripten 在 WebAssembly 中将其组装后,我清楚地意识到一切是多么简单,然后我开始移植声音。
经过几个小时的调试,很明显 Emscripten 的 OpenAL 实现存在几个错误,例如,在初始化读取单通道数时,该方法返回无限数,并且在尝试初始化无限大小的向量后,C++崩溃并出现异常向量::length_error。
我们通过将单声道数量硬编码为 2048 来解决这个问题:


		alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);



#if __EMSCRIPTEN__

		monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?

#endif



有网络吗?

Surreal Engine目前不支持在线游戏,支持与机器人一起玩,但我们需要有人为这些机器人编写AI。理论上,您可以使用Websockets在WebAssembly/Emscripten上实现网络游戏。

结论

总之,我想说,由于使用了 Emscripten 移植的库,以及我过去在 WebAssembly 中使用 C++ 实现游戏的经验,Surreal Engine 的移植非常顺利在 Emscripten 上。以下是有关该主题的知识来源和存储库的链接。
M-M-M-杀死怪物!

此外,如果您想帮助该项目,最好使用 WebGL/OpenGL ES 渲染代码,请在 Telegram 中给我写信:
https://t.me/demenscave

链接

https://demensdeum.com/demos/Sur​​realEngine/
https://github.com/demensdeum/SurrealEngine-Emscripten

https://github.com/dpjudas/SurrealEngine

Flash Forever – Interceptor 2021

Recently, it turned out that Adobe Flash works quite stably under Wine. During a 4-hour stream, I made the game Interceptor 2021, which is a sequel to the game Interceptor 2020, written for the ZX Spectrum.

For those who are not in the know – the Flash technology provided interactivity on the web from 2000 to around 2015. Its shutdown was prompted by an open letter from Steve Jobs, in which he wrote that Flash should be consigned to history because it lagged on the iPhone. Since then, JS has become even more sluggish than Flash, and Flash itself has been wrapped in JS, making it possible to run it on anything thanks to the Ruffle player.

You can play it here:
https://demensdeum.com/demos/Interceptor2021

Video:
https://www.youtube.com/watch?v=-3b5PkBvHQk

Source code:
https://github.com/demensdeum/Interceptor-2021

Masons-DR 演示游戏

Masonry-AR 是一款增强现实游戏,您需要在现实世界中的城市中导航,从书中收集共济会知识,为您的共济会订单获取货币并占领领土。游戏与任何真实组织无关,所有比赛都是随机的。

游戏演示:
https://demensdeum.com/demos/masonry-ar/client

维基:
https://demensdeum.com/masonry-ar-wiki-ru/

源代码:
https://github.com/demensdeum/Masonry-AR

火焰钢发动机转轮

我向您介绍火焰钢发动机转轮&#8211;用于启动基于 Flame Steel Engine 工具包的多媒体应用程序的平台。支持的平台有 Windows、MacOS、Linux、Android、iOS、HTML 5。应用程序代码开发的重点已转向脚本编写目前,已经使用TinyJS添加了对JavaScript的支持;工具包和引擎本身将继续使用接近硬件的语言(C、C++、Rust等)进行开发
Flame Steel Engine Runner Demo
在下面的页面上,您可以旋转立方体,用 JavaScript 编写代码,使用“上传文件”按钮上传模型、声音、音乐和代码,并使用“运行”按钮从 main.js 文件开始。
https://demensdeum.com/demos/FlameSteelEngineRunner/

太空美洲虎动作角色扮演游戏 0.0.4

Web assembly 的 Space Jaguar 动作 RPG 游戏的第一个原型:

加载时间较长(53MB)且没有加载指示。

太空美洲虎动作角色扮演游戏太空海盗的生活模拟器。目前,可以使用最简单的游戏机制:

  • 遨游太空
  • 睡觉
  • 聘请团队
  • 看那不停歇、飞逝的时间流

由于网页版3D场景渲染优化不佳,无法在3D环境中行走。后续版本会增加优化。

引擎、游戏和脚本的源代码可根据 MIT 许可证获得。所有改进建议都得到了非常积极的回应:

https://gitlab.com/demensdeum/space-jaguar -动作RPG

Linux 本机版本的屏幕截图:

使用 C 语言进行 ZX Spectrum 游戏开发

这篇废话帖子专门为老ZX Spectrum电脑用C语言开发一个游戏,来看看帅哥吧:

1982年开始生产,一直生产到1992年。该机的技术特点:8位Z80处理器,16-128kb内存和其他扩展,例如AY-3-8910声音芯片。

作为Yandex Retro Games Battle 2019竞赛的一部分,我为这台机器编写了一个游戏叫做Interceptor 2020。由于我没有时间学习Z80的汇编语言,所以我决定用C来开发它。作为工具链,我选择了一套现成的工具链—— z88dk,其中包含 C 编译器和许多辅助库,可加快 Spectrum 应用程序的执行速度。它还支持许多其他 Z80 机器,例如 MSX、德州仪器计算器。

接下来,我将描述我对计算机体系结构、z88dk 工具链的浅薄了解,并展示我如何设法实现 OOP 方法和使用设计模式。

安装功能

z88dk 的安装应该根据存储库中的手册进行,但是,对于 Ubuntu 用户,我想注意一个功能:如果您已经从 deb 包安装了 Z80 编译器,那么您应该删除它们,因为 z88dk 默认情况下将从 bin 文件夹访问它们;由于工具链编译器版本不兼容,您很可能无法编译任何内容。 /p>

你好世界

编写 Hello World 非常简单:

#include 

void main()
{
    printf("Hello World");
}

编译 Tap 文件更加容易:

zcc +zx -lndos -create-app -o helloworld helloworld.c

要运行,请使用任何支持 Tap 文件的 ZX Spectrum 模拟器,例如在线模拟器:
http://jsspeccy.zxdemo.org/

全屏在图片上绘图

tl;dr 图片以图块形式绘制,图块大小为 8×8 像素,图块本身内置于 Spectrum 字体中,然后将图片打印为索引中的一条线。

精灵和图块输出库 sp1 使用 UDG 输出图块。图片被转换为一组单独的 UDG(图块),然后使用索引在屏幕上组装。应该记住,UDG 用于显示文本,如果您的图片包含非常大的图块集(例如,超过 128 个图块),那么您将不得不超出该集的边界并删除默认的 Spectrum字体。为了解决这个限制,我使用了 128 – 的基数。 255,通过简化图像同时保留原始字体。关于简化下面的图片。

要绘制全屏图像,您需要使用三个实用程序来武装自己:
瘸子
img2spec
png2c-z88dk

真正的 ZX 男人、真正的复古战士有一种方法,那就是使用 Spectrum 调色板打开图形编辑器,了解图像输出的特征,手动准备并使用 png2c-z88dk 或 png2scr 上传。< /p>

更简单的方法–拍摄一张32位图像,在Gimp中将颜色数量切换为3-4,稍微编辑一下,然后将其导入到img2spec中,以免手动处理颜色限制,导出png并使用png2c-将其转换为C数组z88dk。

您应该记住,为了成功导出,每个图块不能包含超过两种颜色。

因此,您将收到一个包含唯一图块数量的 h 文件,如果超过 ~128 个,则在 Gimp 中简化图像(增加重复性)并在新图块上执行导出过程.

导出后,您可以从字面上加载图块中的“字体”,并将图块索引中的“文本”打印到屏幕上。下面是渲染“类”的示例:

// грузим шрифт в память
    unsigned char *pt = fullscreenImage->tiles;

    for (i = 0; i < fullscreenImage->tilesLength; i++, pt += 8) {
            sp1_TileEntry(fullscreenImage->tilesBase + i, pt);
    }

    // ставим курсор в 0,0
    sp1_SetPrintPos(&ps0, 0, 0);

    // печатаем строку
    sp1_PrintString(&ps0, fullscreenImage->ptiles);

在屏幕上绘制精灵

接下来我将描述一种在屏幕上绘制 16×16 像素精灵的方法。我没有开始动画和改变颜色,因为……正如我所假设的,在这个阶段我已经耗尽了内存,这是微不足道的。因此,游戏仅包含透明的单色精灵。

我们在Gimp中绘制一个单色png图像16×16,然后使用png2sp1sprite将其转换为asm汇编文件,在C代码中我们从汇编文件中声明数组,并在汇编阶段添加该文件。< /p>

在声明精灵资源阶段之后,必须将其添加到屏幕上所需的位置,下面是游戏对象“类”的代码示例:

    struct sp1_ss *bubble_sprite = sp1_CreateSpr(SP1_DRAW_MASK2LB, SP1_TYPE_2BYTE, 3, 0, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2,    SP1_TYPE_2BYTE, col2-col1, 0);
    sp1_AddColSpr(bubble_sprite, SP1_DRAW_MASK2RB,  SP1_TYPE_2BYTE, 0, 0);
    sp1_IterateSprChar(bubble_sprite, initialiseColour);

从函数名称就可以大致明白–的含义。为精灵分配内存,添加两个 8×8 列,为精灵添加颜色。

每帧中都会指示精灵的位置:

sp1_MoveSprPix(gameObject->gameObjectSprite, Renderer_fullScreenRect, gameObject->sprite_col, gameObject->x, gameObject->y);

模拟 OOP

C 中没有 OOP 语法,如果你真的想要的话该怎么办?您需要连接您的思想并受到以下想法的启发:不存在 OOP 设备之类的东西;一切最终都属于一种机器架构,其中根本不存在对象的概念以及与之相关的其他抽象。 /p>

这个事实让我在很长一段时间里都无法理解为什么需要 OOP,如果最终一切都变成了机器代码,为什么有必要使用它。

但是,在从事产品开发之后,我发现了这种编程范式的好处,当然主要是开发灵活性、代码保护机制、采用正确的方法、减少熵、简化团队工作。所有上述好处都源于三大支柱——多态、封装、继承。

还值得注意的是解决与应用程序架构相关的问题的简化,因为 80% 的架构问题是在上个世纪由计算机科学家解决的,并在设计模式的文献中进行了描述。接下来,我将描述向 C 添加类似 OOP 的语法的方法。

以C结构体作为存储类实例数据的基础更为方便。当然,您可以使用字节缓冲区,为类、方法创建自己的结构,但为什么要重新发明轮子呢?毕竟,我们已经在重新发明语法。

类数据

游戏对象“类”数据字段示例:

struct GameObjectStruct {
    struct sp1_ss *gameObjectSprite;
    unsigned char *sprite_col;
    unsigned char x;
    unsigned char y;
    unsigned char referenceCount;
    unsigned char beforeHideX;
    unsigned char beforeHideY;
};
typedef struct GameObjectStruct GameObject;

将我们的类保存为“GameObject.h”,在正确的位置执行#include“GameObject.h”并使用它。

类方法

考虑到 Objective-C 语言开发人员的经验,类方法的签名将是全局范围内的函数,第一个参数始终是数据结构,后面是方法参数。下面是“类”GameObject 的“方法”示例:

void GameObject_hide(GameObject *gameObject) {
    gameObject->beforeHideX = gameObject->x;
    gameObject->beforeHideY = gameObject->y;
    gameObject->y = 200;
}

方法调用如下所示:

GameObject_hide(gameObject);

构造函数和析构函数的实现方式相同。可以将构造函数实现为分配器和字段初始值设定项,但我更喜欢单独控制对象分配和初始化。

使用内存

使用 new 和 delete 宏中包含的 malloc 和 free 来手动管理表单内存以匹配 C++:

#define new(X) (X*)malloc(sizeof(X))
#define delete(X) free(X)

对于同时被多个类使用的对象,半手动内存管理是基于引用计数实现的,类似于旧的 Objective-C Runtime ARC 机制:

void GameObject_retain(GameObject *gameObject) {
    gameObject->referenceCount++;
}

void GameObject_release(GameObject *gameObject) {
    gameObject->referenceCount--;

    if (gameObject->referenceCount < 1) { sp1_MoveSprAbs(gameObject->gameObjectSprite, &Renderer_fullScreenRect, NULL, 0, 34, 0, 0);
        sp1_DeleteSpr(gameObject->gameObjectSprite);
        delete(gameObject);
    }
}

因此,每个类必须使用retain声明共享对象的使用,通过release释放所有权。现代版本的 ARC 使用自动保留/释放调用。

声音!

Spectrum 有一个能够再现 1 位音乐的高音扬声器;当时的作曲家能够同时再现多达 4 个声道。

Spectrum 128k包含一个独立的声音芯片AY-3-8910,可以播放跟踪器音乐。

提供了一个用于在 z88dk 中使用高音扬声器的库

还有什么需要学习

我有兴趣熟悉 Spectrum、使用 z88dk 实现游戏并学习很多有趣的东西。我还有很多东西需要学习,例如 Z80 汇编器,因为它使我能够充分利用 Spectrum 的功能、使用内存库以及使用 AY-3-8910 声音芯片。希望明年还能参加比赛!

链接

https://rgb.yandex
https://vk.com/sinc_lair
https://www.z88dk.org/forum/

源代码

https://gitlab.com/demensdeum/ zx-projects/tree/master/interceptor2020

死亡面具狂野测试版

《Death-Mask》游戏进入公测状态(野测)
游戏的主菜单屏幕已重新设计,添加了科技迷宫的蓝色区域的视图,并有令人愉悦的背景音乐。

接下来,我计划重新设计游戏控制器,添加像旧射击游戏一样的流畅移动,盒子、武器、敌人的高质量 3D 模型,不仅通过门户移动到技术迷宫的其他级别的能力(电梯、门、从地板上的洞掉下来、墙上的洞掉下来),我将为生成的迷宫的环境添加一点变化。我还将致力于游戏平衡。
骨骼动画将作为发布前的完善阶段添加。< /p>