代码示例:Panaconda - 持久内存版本的《贪吃蛇》游戏

签署人: Kelly Lyon IDZSupport KS

已发布:08/03/2018   最后更新时间:06/13/2018

文件: 下载
许可: 3 条款 BSD 许可
面向...优化  
操作系统: Linux* 内核版本 4.3 或更高版本
硬件: 模拟:了解如何使用动态随机访问内存 (DRAM) 模拟持久内存
软件:
(编程语言、工具、IDE 和框架)
C++ 编译器和持久内存开发工具包 (PMDK) 
前提条件: 熟悉 C++

简介

Panaconda Game

《贪吃蛇》 是一款小时候钟爱的游戏,您使用方向键来控制蛇的移动方向,拾取食物来生长,并避免撞到墙壁或自己的尾巴。Panaconda 是一款 《贪吃蛇》 游戏,旨在展示持久内存池、指针和事务。所有对象都存储在持久内存中,这意味着如果出现断电或应用崩溃,游戏状态将被保留下来,您可以从故障发生前的位置继续玩游戏。在本例中,我们将展示使这款游戏持久化的细节,讨论如何使用类似的方法实现应用持久化,并总结如何玩 Panaconda 游戏。

本文假定您已经具备持久内存的基础知识,并基本掌握了持久内存开发工具包 (PMDK) 库中所用的概念。在我们的《英特尔持久内存编程简介》文章中,我们介绍了持久内存的定义和颠覆性优势。若要设置您的开发环境,请参见我们的 入门指南。Pmem.io 有一个很棒的系列视频介绍如何使用 libpmemobj 库进行持久内存编程。强烈建议至少阅读 第一部分,其中介绍了本文所展示的基本概念。 

游戏设计

在 Panaconda 中,一切都在 main 的while 循环中进行。在贪吃蛇因任何原因停下来之前,它将执行向前移动、吃食和检查碰撞的循环。

while (!snake_game->is_stopped()) {
	…
}

数据结构

data structure flowchart
图 1Panaconda 的数据结构。

游戏有三个主类:gamegame_boardsnake。在上图中,您可以看到其他类以及它们如何交互。游戏类是 root 对象。这个对象用于固定持久内存池中创建的所有其他对象。通过游戏类,可以访问池中的所有其他对象。这在游戏的 init() 函数中进行,如图所示:

persistent_ptr<game_state> r = state.get_root();

游戏

除了作为根对象之外,游戏类还检查指定的游戏文件是否已经退出。在下面的代码段中,池会检查存储在 game_state 中的 LAYOUT_NAME,看看它是否与传入的游戏文件相匹配。此代码将查看池是否已存在。无论池正在创建,还是已经存在并正在打开,它都将分配给池对象指针 (pop) 变量。

if (pool<game_state>::check(params->name, LAYOUT_NAME) == 1)
    pop = pool<game_state>::open(params->name, LAYOUT_NAME);
else
	pop = pool<game_state>::create(params->name, LAYOUT_NAME,
				           PMEMOBJ_MIN_POOL * 10, 0666);

game::init 中,我们看到了我们的第一笔交易。这一事务可包装迷宫设置流程。如果发生断电,数据结构不会被损坏,因为所有更改都会回滚。有关事务及其实施方法的更多详情可在 pmem.io 上的libpmemobj 的 C++ 绑定(第 6 部分) – 事务 中找到。

transaction::exec_tx(state, [&r, &ret, this]() {
	r->init();
	if (params->use_maze)
		ret = parse_conf_create_dynamic_layout();
	else
		ret = r->get_board()->creat_static_layout();

	r->get_board()->create_new_food();
});

如果游戏以"–m” 标签开始,use_maze 将设为 true。如果传入自定义迷宫,游戏将创建动态布局,否则会创建静态的预定义布局。

在实施 《贪吃蛇》 的过程中,game_player 类将存储 scoreplay_state。状态可能为:STATE_NEW, STATE_PLAYSTATE_GAMEOVER

Game_board

Game_board 创建指向 foodlayout (地图)和 snake 的持久指针。如果您要创建自己的地图,也可以在此处更改游戏板大小。

game_board::game_board()
{
	persistent_ptr<element_shape> shape =
		make_persistent<element_shape>(FOOD);
	food = make_persistent<board_element>(0, 0, shape,
					      direction::UNDEFINED);
	layout = make_persistent<list<board_element>>();
	anaconda = make_persistent<snake>();
	size_row = 20;
	size_col = 20;
}

在上面的代码段中,以下对象作为持久对象指针返回:

food—指向形状 FOOD 板元素的持久指针,没有定义点和方向

layout—指向板元素列表的持久指针

anaconda—指向贪吃蛇对象的持久指针,最终是一个板元素列表

所有这些分配都是事务的一部分,因此如果游戏中止,则回滚这些分配,将任何内存分配恢复到其原始状态。阅读 libpmemobj 的 C++ 绑定(第 5 部分) – make_persistent,可以找到关于 make_persistent 函数的更多信息。

记住,在 game_board 析构函数中,使用以下语法删除这些指针:

game_board::~game_board()
{
	layout->clear();
	delete_persistent>(layout);
	delete_persistent(anaconda);
	delete_persistent(food);
}

game_board 类的另一个功能是跟踪冲突。如果蛇的头部撞到食物,蛇会变长,游戏难度将增加。蛇与食物之间、蛇与墙之间、蛇与自身之间会发生碰撞。

bool is_snake_collision(point point);
bool is_wall_collision(point point);
bool is_snake_head_food_hit(void);

Snake

snake 类中,snake_segments 是一个指向 board_element 对象列表的持久指针。最初,蛇有五个部分。当蛇撞到食物时,将增加更多部分。

snake_segments = make_persistent<list<board_element>>();

snake 中的移动函数使用持久指针和 for 循环遍历 snake_segments 的每个元素。循环向后迭代,将先前的 snake_segments 点和位置分配给下一个部分。这样可以看出蛇的移动。当循环到达 snake_segments 的第一个元素时,它会计算下一个位置并根据传递给函数的方向来设置方向。

void snake::move(const direction dir)
{
	int snake_size = snake_segments->size();
	persistent_ptr<point> new_position_point;

	last_seg_position = *(snake_segments->get(snake_size - 1)->get_position().get());
	last_seg_dir = snake_segments->get(snake_size - 1)->get_direction();

	for (int i = (snake_size - 1); i >= 0; --i) {
		if (i == 0) {
			new_position_point =
				snake_segments->get(i)->calc_new_position(dir);
			snake_segments->get(i)->set_direction(dir);
		} else {
			new_position_point =
				snake_segments->get(i)->calc_new_position(
					snake_segments->get(i - 1)->get_direction());
			snake_segments->get(i)->set_direction(
				snake_segments->get(i - 1)->get_direction());
		}
		snake_segments->get(i)->set_position(new_position_point);
	}
}

正如您在下图中所看到的,蛇基本上是一个移动的 snake_segments 阵列。snake_segments 的每个元素都包含它所在的 x、y 点及其前进方向。调用 snake::move(const direction dir) 时,每个元素都会占据其前面的元素的位置。第一个元素根据其传递到函数的方向移动。

Panaconda Game
图 2.Snake_segments 在移动函数之前和之后的图像。

To Play

首先下载并构建 Panaconda。安装帮助(包括依赖关系)可以在 PMDK 自述文件中找到。

启动游戏

$./panaconda /path/game/session/gameFile

gameFile 是存储游戏会话的地方。这是在您第一次玩游戏时创建的,或者您可以打开之前玩过的游戏文件。如果是您第一次玩游戏时创建的,请为您的文件命名并开始游戏,如下所示:

$./panaconda myFirstGameFile

此外,您可以创建自己的游戏迷宫或使用朋友的游戏迷宫。"-m” 指明您想要使用自定义迷宫。

$./panaconda /path/game/session/gameFile –m /path/myMapCfg

panaconda/conf.cfg 包含预定义迷宫的示例。使用位图定义迷宫,其中“1”是墙,“0”是开放空间。目前,地图大小限制为 80 x 40 位,但可以在代码中进行配置。尝试创建您自己的迷宫,看看您是否能走出迷宫;然后与朋友分享您的迷宫!

控制

Panaconda 使用方向键移动。"q” 退出游戏,"n” 创建新游戏。

若要模拟终止游戏,按 "ctrl+c"、"q” 或在另一个终端执行 "kill –p `pgrep panaconda`"。这会使您返回命令行并退出游戏。若要恢复,只需使用您之前指定的游戏文件再次启动游戏。由于游戏具备持久特性,它会从您离开的位置恢复。

目标

游戏的目标是尽可能延长活着的时间,同时让您的蛇越来越长。当蛇吃掉食物块时,它会变得更长。小心避免撞到任何障碍物、墙壁或蛇身体的其他部分。

总结

在此代码示例中,我们看到了事务和持久指针的示例。我们查看了 game,它是在持久内存池中固定所有其他对象的根对象。这只是如何使用持久内存的一个示例。尽管简单有趣,但此示例演示了基本的持久内存编程概念。如果您想要了解更多信息,我建议您深入了解一下 Panaconda 或研究一下我们在 software.intel.com 上或 GitHub* 存储库 中的其他 代码示例

PMDK 可在 GitHub and on the 持久内存编程主页上找到。

关于作者

Kelly Lyon 是英特尔公司的开发大使,拥有三年的软件工程师经验。最近,Kelly 负责维护英特尔开源遥测框架 Snap。Kelly 致力于为用户提供鼎力支持,希望通过研究并提供简单易懂的指南和教程,帮助用户理解复杂的概念。请关注她的 Twitter @a_lyons_tale。


参考

产品和性能信息

1

英特尔的编译器针对非英特尔微处理器的优化程度可能与英特尔微处理器相同(或不同)。这些优化包括 SSE2、SSE3 和 SSSE3 指令集和其他优化。对于在非英特尔制造的微处理器上进行的优化,英特尔不对相应的可用性、功能或有效性提供担保。该产品中依赖于微处理器的优化仅适用于英特尔微处理器。某些非特定于英特尔微架构的优化保留用于英特尔微处理器。关于此通知涵盖的特定指令集的更多信息,请参阅适用产品的用户指南和参考指南。

通知版本 #20110804