Meta Crush Saga:编译时游戏

图片

去年,在获得期待已久的Lead C ++ Over-Engineer头衔的过程中,我决定使用现代C ++(C ++ 17)的精髓重写我在工作时间内开发的游戏(Candy Crush Saga)。 因此, Meta Crush Saga诞生了:一种在编译阶段运行游戏 。 马特·伯纳(Matt Birner)的Nibbler游戏给我很大的启发,该游戏在模板上使用纯元编程,以诺基亚3310重现著名的Snake。

它在编译阶段正在运行哪种游戏 ?”,“它看起来如何?”,“在该项目中您使用了C ++ 17的哪些功能?”,“您学到了什么?” -您可能会想到类似的问题。 要回答这些问题,您要么必须阅读整个帖子,要么忍受内心的懒惰,并观看该帖子的视频版本-我在斯德哥尔摩的Meetup活动中的报告:


注意:为了您的心理健康,并且由于人道的错误 ,本文提供了一些替代事实。

一个在编译时运行的游戏?



我认为,为了理解在编译阶段执行游戏的“概念”的含义,您需要将此类游戏的生命周期与普通游戏的生命周期进行比较。

常规游戏的生命周期:


作为具有正常生活的游戏的常规开发者,以正常的心理健康水平从事常规工作,您通常首先以自己喜欢的语言(当然是C ++)编写游戏逻辑 ,然后运行编译器将其转换,就像意大利面条一样可执行文件中的逻辑。 双击可执行文件 (或从控制台开始)后,操作系统会产生一个进程 。 此过程将执行游戏逻辑 ,该逻辑由99.42%的游戏周期组成。 游戏周期根据某些规则和用户输入来 更新游戏状态,一次又一次,一次又一次地渲染新计算出的游戏状态。


在编译过程中运行的游戏的生命周期:


作为创建新的酷炫编译游戏的过度工程师,您仍然可以使用自己喜欢的语言(当然,还是C ++!)来编写游戏逻辑 。 然后,和以前一样, 编译阶段继续进行 ,但是有一个图样折衷:您在编译阶段执行 游戏逻辑 。 您可以将其称为“执行”(编译)。 在这里,C ++非常有用。 它具有诸如模板元编程(TMP)constexpr之类的功能 ,可让您在编译阶段执行计算 。 稍后我们将考虑可用于此目的的功能。 由于在此阶段我们执行游戏的逻辑 ,因此在这一刻,我们还需要插入玩家的输入 。 显然,我们的编译器仍会在输出中创建一个可执行文件 。 它可以用来做什么? 可执行文件将不再包含游戏循环 ,但其任务非常简单:显示新的计算状态 。 让我们将此可执行文件称为 渲染器 ,然后渲染 渲染 的数据 。 在我们的渲染中,不会包含美丽的粒子效果或环境光遮蔽阴影,而是ASCII。 新计算状态的 ASCII 呈现是一种方便的属性,可以很容易地向播放器演示,但是我们还将其复制到文本文件中。 为什么是文本文件? 显然,因为它可以以某种方式与代码结合并重新执行所有前面的步骤,从而获得一个循环

如您所知, 在编译过程执行的游戏包含一个游戏周期 ,其中游戏的每个都是一个编译阶段编译的每个阶段都会计算出游戏的新状态 ,可以将其显示给玩家并插入下一 / 编译阶段

您可以随心所欲地构思一下这个宏伟的图表,直到了解我刚刚写的内容:


在我们详细介绍实现这种循环的细节之前,我确定您想问我唯一的问题...

“为什么要这么做?”



您是否真的认为破坏我的C ++元编程田园诗是一个基本问题? 是的,一辈子都没有!

  • 首先也是最重要的一点是,在编译阶段执行的游戏将具有惊人的执行速度,因为大部分计算是在编译阶段执行的。 运行速度是使用ASCII图形的AAA游戏成功的关键!
  • 您可以减少一些甲壳类动物出现在您的存储库中的可能性,并要求您用Rust重写游戏。 一旦您向他解释编译时就不会存在无效的指针,那么他准备充分的演讲就会崩溃。 Haskell自信的程序员甚至可以在您的代码中确认类型安全性
  • 您将赢得Javascript时髦王国的尊重,只要重新命名的框架带有很酷的名字,任何具有强烈NIH综合症的经过重新设计的框架都可以统治该框架。
  • 我的一个朋友曾经说过,实际上任何行的Perl代码都可以用作非常强大的密码。 我确信他从来没有尝试过用C ++编译时生成密码。

怎么了 您对我的回答满意吗? 那么也许您的问题应该是:“您甚至如何做到这一点?”

实际上,我真的很想尝试C ++ 17中添加的功能。 它的许多功能旨在提高语言的有效性以及元编程(主要是constexpr)。 我认为,与其编写小的​​代码示例,不如将所有这些都变成游戏,将更加有趣。 宠物项目是学习不需要在工作中经常使用的概念的好方法。 再次在编译时执行基本游戏逻辑的能力再次证明模板和constepxr是C ++语言的图灵完备子集。

元粉碎传奇游戏评论


三消游戏:


Meta Crush Saga是一款类似于BejeweledCandy Crush Saga的 游戏 。 游戏规则的核心是将具有相同图案的三个图块连接起来以获得得分。 快速浏览一下我“转储” 的游戏状态 (以ASCII转储很容易实现):

  R“(
    迷恋传奇      
 ------------------------  
 |  | 
 |  RBGBBYGR | 
 |  | 
 |  | 
 |  YYGRBGBR | 
 |  | 
 |  | 
 |  RBYRGRYG | 
 |  | 
 |  | 
 |  RYBY(R)YGY | 
 |  | 
 |  | 
 |  BGYRYGGR | 
 |  | 
 |  | 
 |  RYBGYBBG | 
 |  | 
 ------------------------  
 >得分:9009
招式:27
 )“ 


这款Match-3游戏本身的玩法并不特别有趣,但是,它们都可以在其上运行的架构又如何呢? 为了让您理解它,我将尝试用代码解释这个编译期游戏生命周期的每个部分。

游戏状态注入:



如果您是一个热情的C ++爱好者或书呆子,您可能已经注意到,以前的游戏状态转储以以下模式开头: R“( 。实际上,这是原始的C ++ 11字符串文字 ,这意味着我不需要转义特殊字符,例如, 翻译字符串 :原始字符串文字存储在名为current_state.txt的文件中。

我们如何将游戏的当前状态注入编译状态? 让我们将其添加到循环输入中!

// loop_inputs.hpp constexpr KeyboardInput keyboard_input = KeyboardInput::KEYBOARD_INPUT; //       constexpr auto get_game_state_string = []() constexpr { auto game_state_string = constexpr_string( //       #include "current_state.txt" ); return game_state_string; }; 

无论是.txt文件还是.h文件,C预处理器中的include指令都将以相同的方式工作:它将文件的内容复制到其位置。 在这里,我将游戏状态的原始字符串文字(以ascii形式)复制到一个名为game_state_string的变量中。

请注意, loop_inputs.hpp文件还将键盘输入扩展到当前帧/编译步骤。 与游戏状态不同,键盘的状态非常小,可以很容易地将其作为预处理程序的定义。

在编译时计算新状态:



现在我们已经收集了足够的数据,我们可以计算新状态。 最后,我们到达了需要编写main.cpp文件的地步:

 // main.cpp #include "loop_inputs.hpp" //   ,   . // :    . constexpr auto current_state = parse_game_state(get_game_state_string); //      . constexpr auto new_state = game_engine(current_state) //    , .update(keyboard_input); //  ,    . constexpr auto array = print_game_state(new_state); //      std::array<char>. // :    . //  :   . for (const char& c : array) { std::cout << c; } 

奇怪,但是考虑到它的作用,此C ++代码看起来并不那么混乱。 大多数代码在编译阶段执行,但是它遵循传统的OOP和过程编程范例。 为了在编译时完全执行计算,只有最后一行(渲染)是一个障碍。 正如我们将在下面看到的,在正确的地方扔一些constexpr,我们可以在C ++ 17中获得相当优雅的元编程。 当在运行时和编译时执行混合执行时,C ++给我们带来了自由,这让我感到很高兴。

您还将注意到,此代码仅执行一帧,没有游戏循环 。 让我们解决这个问题!

我们将所有内容粘合在一起:



如果您讨厌C ++的技巧,那么希望您不要介意我的Bash技能。 实际上,我的游戏循环不过是一个不断编译的bash脚本

 #  !  ,    !!! while; do : #      G++ g++ -o renderer main.cpp -DKEYBOARD_INPUT="$keypressed" keypressed=get_key_pressed() #  . clear #   current_state=$(./renderer) echo $current_state #    #     current_state.txt file       . echo "R\"(" > current_state.txt echo $current_state >> current_state.txt echo ")\"" >> current_state.txt done 

实际上,从控制台获取键盘输入时遇到了一些麻烦。 最初,我想与编译并行进行。 经过多次尝试和错误,我设法从Bashread命令中获得或多或少的帮助。 我从不敢与决斗的巫师Bash战斗-这种语言太险恶了!

因此,我必须承认,为了管理游戏周期,我不得不求助于另一种语言。 尽管从技术上讲,没有什么阻止我用C ++编写这部分代码。 此外,这并不否认我的游戏90%的逻辑是在g ++编译团队内部执行的事实,这真是太了不起了!

一个小游戏,让您休息一下:


既然您已经经历了解释游戏架构的折磨,那么吸引眼球的绘画的时候到了:


这个像素化的gif记录了我如何玩Meta Crush Saga 。 如您所见,游戏运行流畅,可以实时播放。 显然,她并没有那么吸引人,我可以流传她的Twitch并成为新的Pewdiepie,但是她可以工作!

.txt文件中存储游戏状态的有趣方面之一是非常方便地作弊或测试极端情况的能力。

现在,我已经向您简要介绍了体系结构,我们将深入研究该项目中使用的C ++ 17功能。 我不会详细考虑游戏逻辑,因为它专门指的是Match-3,而是我将讨论可用于其他项目的C ++方面。

我有关C ++ 17的教程:



与主要包含次要修复程序的C ++ 14不同,新的C ++ 17标准可以为我们提供很多帮助。 希望最终会出现期待已久的功能(模块,协程,概念……),但是……总的来说……它们并没有出现。 它使我们许多人不高兴。 但是,在消除了哀悼之后,我们发现了许多意外的小型珍宝,但这些珍宝已成为标准品。

我敢说,喜欢元编程的孩子今年太宠坏了! 现在,对该语言进行了单独的细微更改和添加,使您可以编写在编译时以及运行后非常有效的代码。

Constepxr在所有领域:


正如Ben Dean和Jason Turner在其有关C ++ 14报告中所预测的那样,C ++使您可以在编译时使用全能关键字constexpr快速改进值的编译。 通过在正确的位置放置此关键字,可以告诉编译器表达式是常量, 可以在编译时直接对其求值。 在C ++ 11中,我们已经可以编写以下代码:

 constexpr int factorial(int n) //    constexpr       . { return n <= 1? 1 : (n * factorial(n - 1)); } int i = factorial(5); //  constexpr-. //      : // int i = 120; 

尽管constexpr关键字功能非常强大,但是它有很多使用限制,因此很难以这种方式编写表达性代码。

C ++ 14大大减少了constexpr的需求,并且使用起来变得更加自然。 我们以前的阶乘函数可以重写如下:

 constexpr int factorial(int n) { if (n <= 1) { return 1; } return n * factorial(n - 1); } 

C ++ 14摆脱了constexpr函数应仅包含一个return语句的规则,这迫使我们使用三元运算符作为主要构造块。 现在, C ++ 17带来了更多constexpr关键字应用程序,我们可以探索!

在编译时分支:


您是否曾经遇到过根据所使用的模板参数需要采取不同行为的情况? 假设我们需要一个参数serialize函数serialize ,如果对象提供了该函数,它将调用.serialize() ,否则将诉诸于调用to_string 。 如这篇关于SFINAE的文章中更详细地解释的那样,很可能您将不得不编写这样的外来代码:

 template <class T> std::enable_if_t<has_serialize_v<T>, std::string> serialize(const T& obj) { return obj.serialize(); } template <class T> std::enable_if_t<!has_serialize_v<T>, std::string> serialize(const T& obj) { return std::to_string(obj); } 

只有在梦中,您才能将这个从SFINAE技巧C ++ 14的丑陋技巧重写为如此宏伟的代码:

 // has_serialize -  constexpr-,  serialize  . // .    SFINAE,  ,    . template <class T> constexpr bool has_serialize(const T& /*t*/); template <class T> std::string serialize(const T& obj) { //  ,  constexpr    . if (has_serialize(obj)) { return obj.serialize(); } else { return std::to_string(obj); } } 

不幸的是,当您醒来并开始编写真正的C ++ 14代码时 ,编译器发出了有关调用serialize(42);的令人不愉快的消息serialize(42); 。 它解释说obj类型为intobj没有成员函数serialize() 。 不管它如何激怒您,编译器都是正确的! 使用此代码,他将始终尝试编译两个分支return obj.serialize();
return std::to_string(obj); 。 对于int分支, return obj.serialize(); 可能是某种has_serialize(obj)代码,因为has_serialize(obj)将始终返回false ,但是编译器仍必须对其进行编译。

您可能已经猜到了, C ++ 17使我们摆脱了这种令人不快的情况,因为它使在if语句后添加constexpr以在编译时“强制”分支并丢弃未使用的构造成为可能:

 // has_serialize... // ... template <class T> std::string serialize(const T& obj) if constexpr (has_serialize(obj)) { //     constexpr   'if'. return obj.serialize(); //    ,    ,  obj  int. } else { return std::to_string(obj);branch } } 


显然,这是对我们之前必须应用的SFINAE技巧的巨大改进。 此后,我们开始与Ben和Jason一样上瘾-我们开始在所有地方始终使用constexpr 。 las,还有另一个适合constexpr关键字但尚未使用的地方: constexpr parameters

Constexpr参数:


如果小心,在前面的代码示例中可能会注意到一个奇怪的模式。 我说的是循环输入:

 // loop_inputs.hpp constexpr auto get_game_state_string = []() constexpr // ? { auto game_state_string = constexpr_string( //       #include "current_state.txt" ); return game_state_string; }; 

为什么将变量game_state_string封装在constexpr lambda中? 她为什么不让她成为全局变量constexpr

我想将此变量及其内容传递给一些函数。 例如, 需要将其传递给我的parse_board并在某些常量表达式中使用它:

 constexpr int parse_board_size(const char* game_state_string); constexpr auto parse_board(const char* game_state_string) { std::array<GemType, parse_board_size(game_state_string)> board{}; // ^ 'game_state_string' -   - // ... } parse_board(“...something...”); 

如果我们采用这种方式,则脾气暴躁的编译器将抱怨game_state_string参数不是常量表达式。 创建切片数组时,需要直接计算其固定容量(由于需要内存分配,因此无法在编译时使用向量),并将其作为参数传递给std :: array中的值模板。 因此, parse_board_size(game_state_string)表达式必须是一个常量表达式。 尽管parse_board_size被显式标记为constexpr ,但game_state_string不是,也不能是! 在这种情况下,有两个规则会干扰我们:

  • constexpr函数的参数不是constexpr!
  • 而且我们不能在它们前面添加constexpr!

所有这些归结为一个事实,即constexpr函数必须适用于计算运行时和编译时间。 假设存在constexpr参数 ,则不允许在运行时使用它们。


幸运的是,有一种方法可以解决此问题。 除了将值作为函数的常规参数接受之外,我们可以将此值封装为一个类型并将此类型作为模板参数传递:

 template <class GameStringType> constexpr auto parse_board(GameStringType&&) { std::array<CellType, parse_board_size(GameStringType::value())> board{}; // ... } struct GameString { static constexpr auto value() { return "...something..."; } }; parse_board(GameString{}); 

在此代码示例中,我将创建一个具有静态成员函数constexpr value()GameString结构类型,该成员函数返回要传递给parse_board的字符串文字。 在parse_board中,我使用GameStringType模板参数 (使用提取模板参数的规则)获得此类型。 拥有GameStringType ,由于value()是constexpr的事实,即使在需要常量表达式的地方,我也可以在适当的时间简单地调用静态成员函数value()以获取字符串文字。

我们设法封装了文字,以便使用constexpr将其传递给parse_board 。 但是,每次需要发送新的parse_board文字时,都需要定义一个新类型是非常烦人的:“ ... something1 ...”,“ ... something2 ...”。 为了解决C ++ 11中的这个问题,您可以使用匿名联合和lambda应用一些丑陋的宏和间接寻址。 迈克尔•帕克(Michael Park)在一篇帖子中很好地解释了这个话题。

C ++ 17中,情况甚至更好。 如果我们列出了传递字符串文字的要求,则会得到以下信息:

  • 函数生成
  • 那就是constexpr
  • 具有唯一或匿名名称

这些要求应该给您提示。 我们需要的是constexpr lambda ! 在C ++ 17中,他们完全自然地增加了将constexpr关键字用于lambda函数的功能。 我们可以如下重写示例代码:

 template <class LambdaType> constexpr auto parse_board(LambdaType&& get_game_state_string) { std::array<CellType, parse_board_size(get_game_state_string())> board{}; // ^      constexpr-. } parse_board([]() constexpr -> { return “...something...”; }); // ^    constexpr. 

相信我,这已经比以前使用C ++ 11使用宏进行黑客攻击更加方便了。 感谢我参与的C ++ mitap小组的成员Bjorn Fahler ,我发现了这个很棒的技巧。 在他的博客中阅读有关此技巧的更多信息。 同样值得考虑的是,在这种情况下,实际上constexpr关键字是可选的:默认情况下,所有具有成为constexpr能力的lambda都是它们。 显式添加constexpr是一种签名,可以简化我们的故障排除。

现在,您必须了解为什么我被迫使用constexpr lambda传递代表游戏状态的字符串。 查看此lambda函数,您将再次遇到另一个问题。 我还用来包装股票文字的constexpr_string类型是什么?

constexpr_string和constexpr_string_view:

在处理字符串时,您不应以C风格处理它们,您需要忘记所有执行原始迭代并检查零完成的烦人算法! C ++提供的替代方法是万能的std :: stringSTL算法 。 不幸的是, std :: string可能需要在堆上分配内存(即使使用Small String Optimization)也要存储其内容。 回到一两个标准,我们可以使用constexpr new / delete ,也可以将constexpr分配器传递给std :: string ,但是现在我们需要找到另一个解决方案。

我的方法是编写一个具有固定容量的constexpr_string类。 此容量作为参数传递给值模板。 这是我班的简要概述:

 template <std::size_t N> // N -    . class constexpr_string { private: std::array<char, N> data_; //  N char   -. std::size_t size_; //   . public: constexpr constexpr_string(const char(&a)[N]): data_{}, size_(N -1) { //   data_ } // ... constexpr iterator begin() { return data_; } //    . constexpr iterator end() { return data_ + size_; } //     . // ... }; 

我的constexpr_string类试图尽可能地模仿std :: string接口(用于我需要的操作):我们可以请求开始和结束的迭代器 ,获取大小(大小) ,访问数据(数据)删除(擦除)其中的一部分,获取substr使用substr等。 这使得将一段代码从std :: string转换constexpr_string非常容易。 您可能想知道当我们需要使用通常需要在std :: string中突出显示的操作时会发生什么。 在这种情况下,我被迫将它们转换为创建constexpr_string的新实例的不可变操作

让我们看一下append操作:

 template <std::size_t N> // N -    . class constexpr_string { // ... template <std::size_t M> // M -    . constexpr auto append(const constexpr_string<M>& other) { constexpr_string<N + M> output(*this, size() + other.size()); // ^    . ^     output. for (std::size_t i = 0; i < other.size(); ++i) { output[size() + i] = other[i]; ^     output. } return output; } // ... }; 


您不需要菲尔兹奖就可以假设,如果我们有一个大小为N的字符串和一个大小为M的字符串,那么大小为N + M的字符串就足以存储它们的串联。 我们可能会浪费“编译时存储库”的一部分,因为两条代码行可能都没有使用全部容量,但是为方便起见,这是一个相当小的价格。 显然,我还写了一个std :: string_view的副本 ,称为constexpr_string_view

在这两个类中,我准备编写优美的代码来解析我的游戏状态 。 想像这样的事情:

 constexpr auto game_state = constexpr_string(“...something...”); //          : constexpr auto blue_gem = find_if(game_state.begin(), game_state.end(), [](char c) constexpr -> { return c == 'B'; } ); 

在运动场上遍历珠宝非常容易-顺便说一句,您在此代码示例中注意到C ++ 17的另一个重要功能吗?

是的 在构造它时,我不必显式指定constexpr_string的容量。 以前,使用类模板时 ,我们必须明确指出其参数。 为了避免这些麻烦,我们创建了make_xxx函数,因为可以跟踪函数模板的参数。 了解跟踪类模板参数如何更好地改变我们的生活:

 template <int N> struct constexpr_string { constexpr_string(const char(&a)[N]) {} // .. }; // ****  C++17 **** template <int N> constexpr_string<N> make_constexpr_string(const char(&a)[N]) { //      N ^   return constexpr_string<N>(a); // ^    . } auto test2 = make_constexpr_string("blablabla"); // ^      . constexpr_string<7> test("blabla"); // ^      ,    . // ****  C++17 **** constexpr_string test("blabla"); // ^    ,  . 

在某些困难的情况下,您将需要帮助编译器正确地计算参数。 如果遇到此类问题,请阅读用户定义参数计算手册

来自STL的免费食物:


好吧,我们总是可以自己重写所有内容。 但是,也许委员会成员已经在标准库中为我们慷慨地准备了一些东西?

新的助手类型:

C ++ 17中std :: variantstd :: optional被添加到基于constexpr的标准字典类型中。 第一个非常有趣,因为它允许我们表达类型安全的关联,但是使用常量表达式时,使用GCC 7.2libstdc ++库中的实现存在问题。 因此,我放弃了在代码中添加std :: variant的想法,而只使用std :: optional

对于类型T,类型std :: optional允许我们创建一个新的类型std :: optional <T>,它可以包含类型T的值或不包含任何值。这与有意义的类型非常相似,这些有意义的类型允许C#中使用未定义的值。让我们看一下find_in_board函数,该函数返回第一个元素在确认谓词正确的字段上的位置。字段上可能没有这样的元素。要处理这种情况,头寸类型必须是可选的:

 template <class Predicate> constexpr std::optional<std::pair<int, int>> find_in_board(GameBoard&& g, Predicate&& p) { for (auto item : g.items()) { if (p(item)) { return {item.x, item.y}; } //   ,     . } return std::nullopt; //      . } auto item = find_in_board(g, [](const auto& item) { return true; }); if (item) { // ,   optional. do_something(*item); //    optional, ""   *. /* ... */ } 

以前,我们不得不求助于指针语义,或者直接向位置类型添加“空状态”,或者返回布尔值并采用输出参数。诚然,这很尴尬!

一些预先存在的类型也获得了constexpr支持tuplepair。我不会详细解释它们的用法,因为已经有很多关于它们的文章,但是我将分享我的失望之一。该委员会标准中添加了语法糖,以提取元组成对的值。这种新的声明类型称为结构化绑定,使用括号指定在其中存储拆分元组对的变量

 std::pair<int, int> foo() { return {42, 1337}; } auto [x, y] = foo(); // x = 42, y = 1337. 

很聪明!但是可惜的是,委员会成员[无法,不想,没有找到时间,忘记了]使他们对constexpr友好我期望这样的事情:

 constexpr auto [x, y] = foo(); // OR auto [x, y] constexpr = foo(); 

现在我们有了复杂的容器和帮助程序类型,但是如何方便地操作它们呢?

算法:

升级容器以处理constexpr是一项非常单调的任务。与之相比,将constexpr移植修改算法似乎很简单。但是很奇怪,在C ++ 17中我们没有看到这方面的进展,它只会出现在C ++ 20中例如,出色的std :: find算法未接收constexpr签名

但是不要害怕!正如Ben和Jason解释的那样,您只需复制当前实现即可轻松地将算法转换为constexpr(但不要忘记版权)。cppreference很好。女士们,先生们,我请您注意constexpr std ::查找

 template<class InputIt, class T> constexpr InputIt find(InputIt first, InputIt last, const T& value) // ^ !!!    constexpr. { for (; first != last; ++first) { if (*first == value) { return first; } } return last; } //  http://en.cppreference.com/w/cpp/algorithm/find 

我已经从看台上听到优化迷的尖叫声!是的,仅cppreference提供的示例代码之前添加constexpr可能无法在运行时提供理想的速度但是,如果我们必须改进此算法,则在编译时为了提高速度将需要它据我所知,谈到编译速度,简单的解决方案是最好的。

速度和错误:


任何AAA游戏的开发人员都应该投资解决这些问题,对吗?

速度:


当我设法创建一个半工作版本的Meta Crush Saga时,工作进行得更加顺利。实际上,我的旧笔记本电脑将i5超频到1.80 GHz(在这种情况下,频率很重要),我设法达到了3 FPS(每秒帧数)以上。像在任何项目中一样,我很快意识到以前编写的代码令人讨厌,并开始使用constexpr_string和标准算法来重写游戏状态的解析。尽管这使代码的维护更加方便,但是更改严重影响了速度。新的上限为0.5 FPS

尽管关于C ++的说法很老套,但“零头抽象”不适用于编译时计算如果我们将编译器视为某些“编译时间代码”的解释器,则这是非常合乎逻辑的。仍然可以对各种编译器进行改进,但是对于我们(此类代码的作者)来说,也存在增长的机会。这是我发现的观察结果和提示的不完整列表,可能特定于GCC:

  • C数组std :: array更好std :: array是在C样式数组之上的一些现代C ++外观,因此在这种情况下使用它必须付出一定的代价。
  • , ( ) . , , , . : , , , , ( ) , .
  • , . , .
  • . GCC. , «».

:



很多时候,我的编译器喷出了可怕的编译错误,并且我的代码逻辑遭受了损失。但是,如何找到错误隐藏的地方?没有调试器printf,事情将变得更加复杂。如果您的隐喻“程序员的胡须”尚未屈服(我的隐喻和真正的胡须都还远未达到这些期望),那么您可能没有动力使用templight或调试编译器。

我们的第一个朋友是static_assert,它使我们有机会检查编译时间的布尔值。我们的第二个朋友将是一个在可能的情况下启用和禁用constexpr的宏

 #define CONSTEXPR constexpr //      //  #define CONSTEXPR //     

使用此宏,我们可以使逻辑在运行时工作,这意味着我们可以将调试器附加到该宏。

Meta Crush Saga II-在运行时完全追求游戏性:


显然,Meta Crush Saga今年不会赢得游戏大奖。它具有巨大的潜力,但是游戏性在编译时并未完全执行。这可能会使铁杆游戏玩家感到烦恼……除非有人在编译阶段添加键盘输入和不洁的逻辑,否则我无法摆脱bash脚本的困扰(坦白地说,这很疯狂!)。但是我相信有一天我将能够完全放弃渲染器可执行文件,编译时显示游戏状态


疯狂男子与化名saarraz GCC的扩展,添加到语言结构static_print。此构造应采用多个常量表达式或字符串文字,并在编译阶段将其输出。如果将这样的工具添加到标准中,或者至少将其扩展为static_assert以使其接受常量表达式,我将感到高兴

但是,在C ++ 17中,可能有一种方法可以实现此结果。编译器已经输出了两件事- 错误警告!如果我们能够以某种方式管理或更改警告以适应我们的需求,我们将已经收到一个有价值的结论。我尝试了几种解决方案,特别是不推荐使用的属性

 template <char... words> struct useless { [[deprecated]] void call() {} // Will trigger a warning. }; template <char... words> void output_as_warning() { useless<words...>().call(); } output_as_warning<'a', 'b', 'c'>(); // warning: 'void useless<words>::call() [with char ...words = {'a', 'b', 'c'}]' is deprecated // [-Wdeprecated-declarations] 

虽然输出显然存在并且可以解析,但是不幸的是,代码无法播放!如果纯属巧合,如果您是可以在编译期间执行输出的C ++程序员秘密协会的成员,那么我将很乐意邀请​​您加入我们的团队来创建完美的Meta Crush Saga II

结论:


我最终把我的诈骗游戏卖给了你希望您对此文章感到好奇,并在阅读过程中学到一些新东西。如果您发现错误或改进文章的方法,请与我联系。

我要感谢SwedenCpp团队让我在他们的活动之一中进行项目报告。此外,我还要感谢Alexander Gurdeev,他帮助我改善了Meta Crush Saga的重要方面

Source: https://habr.com/ru/post/zh-CN414465/


All Articles