使用 PHP 创建游戏第 3 部分:Snake
到目前为止,在本系列文章中,我们已经了解了检测用户的按键操作并使用PHP和命令行创建井字游戏。下一步是创建具有图形和实时用户输入的游戏。当我们使用命令行时,我们没有太多空间可以使用,因此我们创建的图形不会很详细。我能想到的最简单的动作游戏是蛇。它有一些简单的规则,可以有非常基本的图形,并且不涉及任何会影响整个游戏的物理或其他机制。事实上,游戏蛇的历史可以追溯到1976年的游戏Blockade,该游戏仅使用文本字符串创建。
与井字游戏不同,这里还有一些变量需要跟踪。与其使用传递引用变量,不如将它们封装到一个对象中。使用对象作为蛇允许我们通过引用将整个事物传递给函数,这意味着我们可以将某些功能封装在对象本身中。这绝不是优秀设计的顶峰,而是更多的概念证明,看看这是否可行。
我们只需要跟踪蛇、苹果和蛇的运动,而不是将游戏板存储为一个大数组。这是Snake类,其中包含一些默认参数,我将在本文后面详细介绍。
class Snake { public $width = 0; public $height = 0; public $positionX = 0; public $positionY = 0; public $appleX = 15; public $appleY = 15; public $movementX = 0; public $movementY = 0; public $trail = []; public $tail = 5; public $speed = 100000; public function __construct($width, $height) { $this->width = $width; $this->height = $height; $this->positionX = rand(0, $width - 1); $this->positionY = rand(0, $height - 1); $appleX = rand(0, $width - 1); $appleY = rand(0, $height - 1); while (array_search([$appleX, $appleY], $this->trail) !== FALSE) { $appleX = rand(0, $width - 1); $appleY = rand(0, $height - 1); } $this->appleX = $appleX; $this->appleY = $appleY; } }
要创建Snake对象的新实例,我们只需传入游戏板的宽度和高度。
$width = 20; $height = 30; $snake = new Snake($width, $height);
为了渲染蛇游戏,我们将蛇对象传递给渲染函数。这个函数的工作是渲染游戏网格,蛇将在其中与蛇本身(由字母X表示)和苹果(由数字0表示)一起移动。此函数的性能并不完全,因为它一遍又一遍地循环遍历蛇形轨迹,以查看其中一个坐标是否位于正在打印的当前单元格内。然而,因为我们以线性模式打印东西,所以我们不能真正绘制背景然后绘制轨迹,我们需要同时绘制所有东西。背景在这里至关重要,因为它可以使所有内容正确对齐。如果没有这个,一切都会向左证明。
function renderGame($snake) { $output = ''; for ($i = 0; $i < $snake->width; $i++) { for ($j = 0; $j < $snake->height; $j++) { if ($snake->appleX == $i && $snake->appleY == $j) { $cell = '0'; } else { $cell = '.'; } foreach ($snake->trail as $trail) { if ($trail[0] == $i && $trail[1] == $j) { $cell = 'X'; } } $output .= $cell; } $output .= PHP_EOL; } $output .= PHP_EOL; return $output; }
如果我们渲染一个新创建的蛇对象,我们会看到以下输出。蛇只有一个部分,所以它在游戏板上打印为一个X。这个想法是,当玩家移动时,我们会生成分段,直到达到最小级别5。
.............................. .............................. .............................. .............................. .............................. .............................. .............................. .............................. ...X.......................... .............................. .............................. .............................. .............................. .............................. .............................. ......................0....... .............................. .............................. .............................. ..............................
这不会做很多事情,所以我们需要为蛇添加一些运动机制。我们再次使用translateKeypress()本系列文章第一部分中的函数。我们在这里要做的就是检测被按下的箭头键并更新蛇对象的移动方向。
function direction($stdin, $snake) { //聆听按下的按钮。 $key = fgets($stdin); if ($key) { $key = translateKeypress($key); switch ($key) { case "UP": $snake->movementX = -1; $snake->movementY = 0; break; case "DOWN": $snake->movementX = 1; $snake->movementY = 0; break; case "RIGHT": $snake->movementX = 0; $snake->movementY = 1; break; case "LEFT": $snake->movementX = 0; $snake->movementY = -1; break; } } }
移动方向用于更新蛇的位置,使其看起来在网格上移动。例如,按向右箭头会将movementY设置为1,这意味着snake的positionY值可以在程序的每'tick'中更新1。
以下move()函数用于使用上述逻辑在游戏板上移动蛇,然后提供一些其他检查。我们检查以确保蛇没有越过游戏板的边界,如果有则进行一些更正。通过将蛇头的位置添加到轨迹中,然后从轨迹数组中移除最后一个元素,我们创造了一条蛇在网格中移动的错觉。我们还需要检查蛇的“头”是否在苹果上方,如果是,则增加蛇的长度并将苹果移动到新位置。我还添加了一个小的while循环,以确保苹果的新位置不会位于蛇的身体内部。
function move($snake) { //移动蛇。 $snake->positionX += $snake->movementX; $snake->positionY += $snake->movementY; //将蛇缠绕在棋盘的边界上。 if ($snake->positionX < 0) { $snake->positionX = $snake->width - 1; } if ($snake->positionX > $snake->width - 1) { $snake->positionX = 0; } if ($snake->positionY < 0) { $snake->positionY = $snake->height - 1; } if ($snake->positionY > $snake->height - 1) { $snake->positionY = 0; } //添加到前面的蛇道。 array_unshift($snake->trail, [$snake->positionX, $snake->positionY]); //从蛇的末端移除一个块(但保持正确的长度)。 if (count($snake->trail) > $snake->tail) { array_pop($snake->trail); } if ($snake->appleX == $snake->positionX && $snake->appleY == $snake->positionY) { //蛇吃了一个苹果。 $snake->tail++; if ($snake->speed > 2000) { //将游戏速度提高到一定限度。 $snake->speed = $snake->speed - ($snake->tail * ($snake->width / $snake->height + 10)); } //为苹果找出一个不同的地方。 $appleX = rand(0, $snake->width - 1); $appleY = rand(0, $snake->height - 1); while (array_search([$appleX, $appleY], $snake->trail) !== FALSE) { $appleX = rand(0, $snake->width - 1); $appleY = rand(0, $snake->height - 1); } $snake->appleX = $appleX; $snake->appleY = $appleY; } }
最后要检查的是最后一个动作是否导致蛇死亡。如果蛇的头接触到它的尾巴,就会发生这种情况,并且可以通过观察蛇的整个轨迹与蛇的头部进行比较来计算出。我们不会在游戏开始时检查这种情况,因为这会立即造成游戏结束的情况。
如果发现头部与尾部相交,那么我们结束游戏并打印出游戏结束消息。
function gameOver($snake) { if ($snake->tail > 5) { //如果轨迹大于5,则检查结束条件。 for ($i = 1; $i < count($snake->trail); $i++) { if ($snake->trail[$i][0] == $snake->positionX && $snake->trail[$i][1] == $snake->positionY) { die('dead :('); } } } }
编写所有这些函数后,我们可以将它们组合到控制游戏的无限循环中。这与我在创建井字游戏时引入的无限循环相同。我们通过清除命令行输出开始循环,然后在移动蛇并打印出游戏板之前更新蛇的方向。最后,我们然后检测是否达到游戏结束场景。
while (1) { system('clear'); echo 'Level: ' . $snake->tail . PHP_EOL; direction($stdin, $snake); move($snake); echo renderGame($snake); gameOver($snake); usleep($snake->speed); }
这个循环与我为tictactoe游戏创建的循环之间的主要区别在于我使用usleep().这会强制游戏在循环的每次迭代中暂停100000微秒(0.1秒),目的是降低游戏模拟速度,使其每秒运行10帧左右。如果没有这个,蛇会跑得非常快,不可能真正玩耍。你可能已经在move()函数中看到,每次吃一个苹果我们都会稍微降低这个值。这是为了复制随着蛇变长而增加的难度。
我们现在有一个正常运行的蛇游戏,当我们在屏幕上玩游戏时看起来像这样。
Level: 64 .............................. .............................. .............0................ .............................. .............................. .............................. XXXX.....................XXXXX ...X.....................X.... ...X.....................X.... ...X.....................X.... ...X......X..............X.... ...X......XXXXXXXXXXXXXXXX.... ...X................X......... ...X................X......... ...X................X......... ...X................X......... ...X.......XXXXXXXXXX......... ...XXXXXXXXX.................. .............................. .............................. dead :(
这个脚本运行良好,实际上是蛇的一个有趣版本。包含的高度和宽度设置允许蛇游戏板成为您想要的任何尺寸,因此您可以在非常大的板上制作非常长的蛇。
可以添加的一些改进可能是防止在您的路径上向后倒退(即,当您向右走时按左键)导致游戏结束状态。其次,有些版本的snake在游戏结束前允许有一点回旋余地,如果你接触到线索,我的版本会立即结束游戏。这可以通过在生成游戏结束情况之前添加几帧来完成,但我将把它留给读者作为练习。
如果您想完整地查看此代码,那么我创建了一个GitHub要点,您可以下载并运行它。