如何使用PureScript Native和C ++创建3D游戏

当我上次写有关将PureScript绑定到C ++的文章时,我演示了使用SFML对PureScript徽标进行动画处理。 从那时起,PureScript Native(PSN)取代了Pure11,并且使用PSN的详细信息已更改。

为了具体化,我将介绍创建Lambda Lantern的过程,Lambda Lantern是有关功能编程模式的3D游戏。 最初,Lambda Lantern最初是作为GitHub Game Off提交提交的。 由于您只有30天左右的时间才能完成,因此必须做出一些捷径。 我计划解决这些快捷方式,并以将其作为驱动C ++绑定及其周围生态系统的工具来充实游戏。

💡如果您不想制作游戏,但仍想将PureScript绑定到C ++代码,或者您正在寻找将Haskell绑定到C ++的替代方法–不用担心–我试图使本指南保持通用可能。

设置项目

Git克隆了PSN,并像其他任何Haskell Stack项目一样使用堆栈构建它。

  git clone https://github.com/andyarvanitis/purescript-native 
cd purescript-native
堆叠安装
光盘

完成后,您将获得名为pscpp的PureScript C ++编译器。 如果您已堆叠安装,则它应位于本地bin目录中。 无论如何,请确保您的路径环境变量具有pscpp的路径。

  export PATH =“ $ {PATH}:$ {HOME} /。local / bin” 

接下来,您将需要Node.js和NPM。 我喜欢用NVM管理Node

  git clone https://github.com/lettier/lambda-lantern.git 
cd lambda灯笼/
nvm install`cat .nvmrc` && nvm使用

但请随意使用您喜欢的方法。 像这样安装purescriptpsc-package

  npm install -g purescript psc-package-bin-simple 

PSN可以生成一个方便的Makefile来管理构建过程。 如果Makefile还不存在,请继续生成它。

  pscpp-制作文件 

这是一个基本的PSN C ++ FFI文件。

  #include“ purescript.h” FOREIGN_BEGIN(SomeModuleName) 
FOREIGN_END

在外部开始和结束之间是定义从PureScript端调用的导出的位置。

  exports [“ id”] = [](const boxed&param_)-> boxed { 
返回param_;
};

为了遍历参数和返回值,PSN使用boxed类。 此boxed类基于shared_ptr构建。

要使用boxed值,您必须unbox ,并提供其类型。

 自动参数=取消框(参数_); 

⚠️如果您不知道类型,那您就被困住了。 这是隐式类型相等比较的问题,因为boxed值的类型已删除。

💡所有出口都必须接受boxed值并返回boxed值。 可以直接返回某些类型,例如stringchardoubleintlong等。 由于boxed类带有一些方便的构造函数,因此编译器将为您构造boxed值。

  exports [“ echo”] = [](const boxed&s_)-> boxed { 
const auto s = unbox (s_);
返回s;
};

对于其他类型,您必须调用box过程来对您的类型进行装箱。

  exports [“ someFunction”] = [](const boxed&param_)-> boxed { 
auto param =取消框(param_);
// ...
return box (param);
};

您需要使用咖喱函数,或者换句话说,为每个参数返回一个新的lambda函数。 如果您的函数返回一个Effect ,则需要一个不带参数的附加lambda函数。

在PureScript中:

 国外进口add1 ::数字->数字外国进口add1'::数字->效果数字 

在C ++中:

  exports [“ add1”] = [](const boxed&n_)-> boxed { 
const auto n =拆箱(n_);
返回n + 1.0;
};
exports [“ add1'”] = [](const boxed&n_)-> boxed {
const auto n =拆箱(n_);
返回[=]()->装箱的{
返回n + 1.0;
};
};

您可以根据自己的喜好来标记一些导入Effect ,但是通常情况下,如果该功能在其范围之外进行了更改或为同一输入返回了不同的输出,则应使用Effect

对于不熟悉以上语法的用户,PSN导出使用C ++ lambda函数。

  [] //表示不捕获任何外部变量。 
[=] //表示按值捕获外部变量。
[&] //表示通过引用捕获外部变量。

按值表示复制它的副本,以使对该副本的任何编辑都不会影响原始文档;而按引用表示复制一个别名,以使任何编辑都可以影响原始文档。 理想情况下,您将要使用[=]而不是[&]这样就不会更改输入参数。

这是一些典型的C ++ lambda函数模式。

  [&capture,= list](p,a,r,a,m,s)-> ReturnType {body;  } 
[&capture,= list](p,a,r,a,m,s){正文; }
[&capture,= list] {正文; }

我相信最佳做法是在unbox参数时将其unbox并为每个返回的lambda函数使用[=] ,如下所示。

  [](const boxed&param_)-> boxed { 
const auto param =取消框(param_); 返回[=](const boxed&param1_)-> boxed {
const auto param1 =拆箱(param1_); 返回[=](const boxed&param2_)-> boxed {
const auto param2 =取消框(param2_); 返回...
};
};
};

但是,对于某些导出,我只需要在最后一个lambda函数中unbox对参数的unbox 。 例如,像这样。 在第一个lambda中取消装箱,然后像这样通过引用进行复制会导致段错误。 而且我无法在第一个lambda中解包,然后按这样的值进行复制,因为编译器会抱怨我试图用set_scale更改常量NodePath 。 可以使用像这样的mutable关键字,但是取消最后一个lambda函数中的所有参数的装箱效果很好。

另一种方法是动态分配节点路径-

  // ... 
返回[&]()->装箱{
// ...
const auto nodePathPtr =
std :: make_shared (
nodePath.find(查询)
);
返回nodePathPtr;
};
// ...

返回并接受指向它们的指针-但这会增加不必要的开销。 不过,有了指针,您可以保持它们不变,在收到它们后立即将它们拆箱,并使用[=]按值复制它们。

通常,我一直与Panda3D API保持联系。 理想情况下,您希望导出的内容很小且基本-将任何额外的逻辑放在PureScript方面以提供额外的保护。 但是,在某些情况下,我偏离了常规且重复的程序。

绑定到某些Panda3D API并不是唯一需要的FFI。 还需要为常见功能(如now ,以JS为中心的功能(如setIntervalrequestAnimationFrame等)创建FFI导出,并查找系统环境变量-至少对于JavaScript而言(至少在浏览器中)。

💡大多数PureScript生态系统都采用了可以理解的JavaScript后端。 希望PSN的使用量会增加—随着C ++ FFI覆盖率的增加。 仅使用PureScript就能轻松定位Web和本机平台绝对是理想的选择。

一个特殊的FFI调用是针对浏览器window ,该window不适用,因此我将其设为空操作。

  exports [“ window”] = []()->装箱{return boxed();  }; 

Note️请注意,如果PSN运行时找不到某些FFI调用的FFI导出,则会引发错误。 如果发生这种情况,您将看到类似以下的内容。

 抛出的实例后终止调用 
'std :: runtime_error'what():找不到字典键“ setInterval”

显然,如果这是在编译时发现的,那就太好了,但这可能不可行。

我知道我想使用函数式反应式编程(FRP),所以我需要purescript-behaviorspurescript-event 。 前者使用requestAnimationFrame而后者使用clearIntervalsetInterval

倒计时到比赛结束的日子,我决定使用一个简单的多线程模型来模拟C ++中的这些以JS为中心的功能。 处理多个线程与JavaScript的单线程性质相比,我的FRP事件有一些细微的差别,而不是那么细微的差别。

在JavaScript中

  setInterval 
(function(){while(true){console.log(1);}}
,1000
);
setInterval
(function(){while(true){console.log(2);}}
,1000
);

只会一遍又一遍地打印1 ,永远不会给第二个间隔一个机会。 但是在C ++中(使用我的FFI),像这样

  setInterval 
([] {while(true){std :: cout << 1 <<“ \ n”;}}
,1000
);
setInterval
([] {while(true){std :: cout << 2 <<“ \ n”;}}
,1000
);

会一遍又一遍地同时打印12

我打算返回多线程模型,并将其转换为单线程,非抢先优先级队列调度模型,使其更类似于其JavaScript对应模型。

创建PureScript FFI导入

FFI防护网的PureScript方面并不是那么重要。 如果有更多时间,我会创建类型类来模拟在Panda3D中找到的类层次结构。 现在游戏结束了,我可以浏览并整理界面以抽象出通用性。

为C ++创建PureScript FFI调用与为JavaScript创建相同。

这是创建环境光的示例FFI调用。

 国外进口数据PandaNode ::类型-...外国进口createAmbientLight 
::字符串
->编号
->编号
->编号
->效果PandaNode

此导入将称为其C ++ FFI导出副本。

查看由pscpp生成的C ++,您将看到如何最终将它们钩在一起。

 自动createAmbientLight()-> const boxed&{ 
静态const盒装_ =
foreign()。at(“ createAmbientLight”);
返回_;
}; // ...盒装v27 =
Panda3d :: createAmbientLight
()
(“环境光”)
(0.125)
(0.122)
(0.184)
();

建设项目

💡PSN使用purs编译器(v0.12.0),因此C ++和JavaScript目标之间在PureScript方面没有任何区别。

这是构建过程的一般概述。

  • 通过调用purs并输出称为AST(抽象语法树)表示形式的“ CoreFn”来编译PureScript代码。
  • 通过调用pscpp并输出C ++来编译CoreFn。
  • 通过调用g++类的g++并输出最终的可执行文件来编译和链接C ++。

使用生成的Makefile使构建过程超级容易。 如果需要,它允许您传递编译器和链接器标志。 这是我用来构建Lambda Lantern的make命令

 使 
CXXFLAGS =“ \
-fmax-errors = 1 \
-I / usr / include / python \
-I / usr / include / panda3d \
-I / usr / include / freetype2“ \
LDFLAGS =“ \
-L / usr / lib / panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-pthread \
-lpthread”

大多数标志将包含Panda3D标头并链接到Panda3D库。

/usr/include/python/usr/include/python/usr/include/panda3d/usr/lib/panda3d等可能不是您系统的确切路径。 您可能还需要上面未列出的其他标志。 例如,在Ubuntu上,我必须包含-I/usr/include/eigen3

生成项目后,可执行文件将位于./output/bin/

分发项目

PSN是Linux,macOS和Windows之间的跨平台。 在本节中,我将介绍为Linux分发Lambda Lantern所做的工作。

创建了捕捉单元,flatpaks,AUR软件包和AppImages之后,我发现AppImage是分发服务器和用户的最佳体验。

首先,您将要找出二进制文件所依赖的库。

  objdump -p输出/ bin / main 

在输出中查找“动态部分”。

 动态部分: 
需要libp3framework.so.1.10
需要libpanda.so.1.10
...

You️您可能需要其他未列出的库。 即使您设法包括每个库,您的AppImage可能也不会在较早的发行版上运行,具体取决于您的构建环境的最新程度(通常是由于libc)。 我建议使用Ubuntu 14.04或16.04作为构建环境,然后在尽可能多的全新安装上测试AppImage。 如果错过了任何库,则会收到一个错误-告诉您找不到哪个库-如下所示。

 加载共享库时出错:   libpanda.so.1.10   :无法打开共享对象文件:没有这样的文件或目录 

AppImage的好处是您可以根据需要包含任意数量的库。 理想情况下,您应该包括一般系统上通常找不到的任何库。 这将给您的AppImage开箱即用的最大机会。

这是Lambda Lantern AppImage的目录树。

  / 
等等/
Confauto.prc
配置文件
的usr /
lib /
熊猫3d /
分享/
应用/
com.lettier.lambda-lantern.desktop
图标/ hicolor / 256x256 /
com.lettier.lambda-lantern.png
拉姆达灯笼/
资产/
蛋/
字体/
音乐/
声音/
执照/
元信息/
com.lettier.lambda-lantern.appdata.xml
AppRun

Config.prcConfauto.prc文件是Panda3D运行所必需的。 在AppRun文件中,我提供了prc文件和Lambda Lantern的资产目录的路径。 您还记得吗,我必须创建一个FFI导出/导入来查找环境变量。 在程序开始时,Lambda Lantern查找一个环境变量以查找其资产。

 导出LAMBDA_LANTERN_ASSETS_PATH = \ 
“ $ {HERE} / usr / share / lambda-lantern / assets”
导出PANDA_PRC_DIR =“ $ {HERE} / etc”

放置所有文件并填写AppRun文件后,您将像这样创建AppImage。

  appimagetool-x86_64.AppImage lambda-lantern.AppDir 

AppImage工具将执行一些验证,然后在一切检查完毕后生成AppImage。

如果您使用的是GitHub,则可以将AppImage上传到一个发行版中,以供用户下载。 下载后,用户可以将AppImage标记为可执行文件并开始播放! 🎉

最后的想法

将Haskell绑定到C相当容易,但是使用C ++并非如此。 这是不幸的,因为一些广泛用于游戏开发的库通常使用C ++。 一段时间以来,我尝试了各种不同的方法将Haskell绑定到C ++,但是它从来没有像使用PSN将PureScript绑定到C ++那样简单。 如果您找到了一种将Haskell绑定到复杂的C ++项目的好方法,请告诉我。 但是,如果您一直试图将Haskell绑定到C ++,而没有找到任何不错的选择,那么请强烈考虑使用PSN。

将PureScript绑定到C ++的可能性很大,因为那里有许多基于C ++的出色项目。 从最先进的游戏引擎到强大的机器学习库。 选择其中任何一个并在其上粘贴PureScript接口,将为您带来一些明显的优势。

我相信,选择Panda3D是正确的前进之路。 它不会尝试控制工作流程-从概念到可执行文件-使其对我来说非常完美,因为我喜欢走平凡的路,看到像使用Gifcurry和Movie Monad项目所做的那样。 最初,我想使用Godot,但我需要它成为我要插入的东西,而不是您从内部扩展的环境。 如果您知道一个控制主游戏并且仅使用C ++使用Godot的游戏,请告诉我。 无论如何,Panda3D是一个真正的宝石-深入研究您会发现一些使用它的出色项目,例如下面的项目。

希望其他人会越来越多地使用PSN。 我很高兴将Lambda Lantern背后的概念转变为功能齐全的游戏,但我对使用它来宣传并在PSN周围建立生态系统感到更加兴奋。 我认为,仅使用PureScript就能轻松地针对Web(JavaScript)和桌面(C ++)定位是梦想成真。 👍