使用PHP创建游戏第4部分:侧面滚动射击游戏
作为我在上一篇文章中创建的蛇游戏的又一个步骤,我决定尝试创建侧面滚动射击游戏。就像我的其他帖子一样,这是在命令行上玩的基于ASCII的游戏。
侧面滚动射击游戏(如果您还不知道的话)会将场景从右到左移动到整个屏幕上,敌人随场景一起朝着玩家的飞船移动,该玩家的飞船位于场景的左侧。玩家可以向敌人发射子弹,以便将它们从场景中移出。
为了创建侧面滚动射击游戏,我们需要定义一些元素。
玩家的飞船。
玩家需要避免的敌人。
玩家飞船发射的子弹。
所有这些项目都将使用x,y坐标定位在场景中,以便简化操作,可以创建Entity的基类。这将封装场景中每个实体的位置。
class Entity { public $positionX = 0; public $positionY = 0; public function __construct($x, $y) { $this->positionX = $x; $this->positionY = $y; } }
通过扩展此基本Entity类,我们可以创建游戏所需的组件。太空飞船需要更多属性才能使其在场景中移动并向敌人开火。
class Spaceship extends Entity { public $movementX = 0; public $movementY = 0; public $fire = FALSE; } class Enemy extends Entity {} class Bullet extends Entity {}
主游戏通过单个Scene类进行控制。此类存储游戏所需的所有组件,并允许游戏由玩家控制。该类的构造函数存储游戏场景的尺寸,然后创建玩家的飞船对象。该船位于场景的中间高度,距左侧2个正方形。
class Scene { public $height; public $width; public $ship; public $enemies = []; public $bullets = []; public $score = 0; public function __construct($width, $height) { $this->width = $width; $this->height = $height; $this->ship = new Spaceship(2, round($this->height / 2)); } }
为了绕船移动,moveShip()使用了一种方法。这将采用当前运动x,y并将其应用于船的位置属性。动作中采用了一些逻辑,以防止玩家越过场景的边界并在飞船到达中途点时停止飞船。
public function moveShip() { $this->ship->positionX += $this->ship->movementX; $this->ship->positionY += $this->ship->movementY; $this->ship->movementX = 0; $this->ship->movementY = 0; if ($this->ship->positionX < 0) { $this->ship->positionX = 0; } if ($this->ship->positionX >= $this->height) { $this->ship->positionX = $this->height - 1; } if ($this->ship->positionY < 0) { $this->ship->positionY = 0; } if ($this->ship->positionY > $this->width / 4) { $this->ship->positionY = $this->width / 4; } }
除了移动飞船,我们还希望飞船发射子弹。当舰船处于“射击”模式时,将使用舰船所在的位置作为子弹位置的基础,创建一个新的子弹对象。发射子弹后,我们将关闭船舶的着火状态,这意味着我们一次只能发射一枚子弹。
public function shoot() { if ($this->ship->fire == TRUE) { $this->bullets[] = new Bullet($this->ship->positionX, $this->ship->positionY + 1); $this->ship->fire = FALSE; } }
使用适当的飞船移动和射击方法后,我们需要听取玩家的输入并将这些动作应用于飞船。它使用了与以前的文章相同的按键检测逻辑,包括用于发射子弹的按钮和用于退出游戏的Esc按钮。
public function action($stdin) { //聆听被按下的按钮。 $key = fgets($stdin); if ($key) { $key = $this->translateKeypress($key); switch ($key) { case "UP": $this->ship->movementX = -1; $this->ship->movementY = 0; break; case "DOWN": $this->ship->movementX = 1; $this->ship->movementY = 0; break; case "RIGHT": $this->ship->movementX = 0; $this->ship->movementY = 1; break; case "LEFT": $this->ship->movementX = 0; $this->ship->movementY = -1; break; case "ENTER": case "SPACE": $this->ship->fire = TRUE; break; case "ESC": die(); } } } private function translateKeypress($string) { switch ($string) { case "\033[A": return "UP"; case "\033[B": return "DOWN"; case "\033[C": return "RIGHT"; case "\033[D": return "LEFT"; case "\n": return "ENTER"; case " ": return "SPACE"; case "\e": return "ESC"; } return $string; }
在场景中移动项目符号非常简单,我们只需增加场景中每个项目符号的y位置即可。这将子弹水平移动到场景的右侧。
public function moveBullets() { foreach ($this->bullets as $bullet) { $bullet->positionY++; } }
我们需要让玩家射击的东西,因此让我们创建一种产生敌人的方法。此功能将确保任何时候至少有15个敌人出现在屏幕上。每次移动场景时,我们都会调用此方法,因此,如果玩家射击了一个敌人,则将产生另一个敌人。敌人的产生将在游戏“屏幕”右侧的任意位置进行,以便他们在移入场景之前要摆放得体。
public function spawnEnemies() { if (count($this->enemies) < 15) { $y = rand($this->width, $this->width * 2); $x = rand(0, $this->height - 1); $this->enemies[] = new Enemy($x, $y); } }
在场景中移动敌人有些微动作,因为我们还需要检测是否需要移除敌人。如果和敌人超出了场景的左侧,则可以将其从场景中移出而不会发生任何事件。如果敌人与子弹在同一地点,则将两者都移除,并且玩家的得分将获得+1。这只需要一点点位置匹配,以确保在移走敌人之前将其放在正确的位置。
public function moveEnemies() { foreach ($this->enemies as $enemyId => $enemy) { $enemy->positionY--; if ($enemy->positionY == 0) { //如果敌人超出了场景的左侧,则将其移开。 unset($this->enemies[$enemyId]); continue; } foreach ($this->bullets as $bulletId => $bullet) { if ($bullet->positionX == $enemy->positionX && ($bullet->positionY == $enemy->positionY || $bullet->positionY == $enemy->positionY - 1)) { unset($this->enemies[$enemyId]); unset($this->bullets[$bulletId]); $this->score++; } } } }
这里的最后一步是添加一个“游戏结束”场景。如果敌人到达玩家飞船的相同x,y位置,就会发生这种情况。我们只是在这里杀死整个程序,然后将游戏通过消息打印到输出中。
public function gameOver() { foreach ($this->enemies as $enemy) { if ($this->ship->positionX == $enemy->positionX && $this->ship->positionY == $enemy->positionY) { die('dead :('); } } }
将所有这些元素放置在适当的位置,现在就可以渲染游戏场景。这种渲染方法遍历场景的高度和宽度,并打印出场景中的每个元素。我将这里的每个x,y坐标都视为网格中的一个单元,以使事情更容易理解。船以大于号表示,敌人以字母x表示,子弹以破折号表示。场景上的其他所有内容都是空白。
public function renderGame() { $output = ''; for ($i = 0; $i < $this->height; $i++) { for ($j = 0; $j < $this->width; $j++) { if ($this->ship->positionX == $i && $this->ship->positionY == $j) { $cell = '>'; } else { $cell = ' '; } foreach ($this->enemies as $enemy) { if ($enemy->positionX == $i && $enemy->positionY == $j) { $cell = 'X'; } } foreach ($this->bullets as $bullet) { if ($bullet->positionX == $i && $bullet->positionY == $j) { $cell = '-'; } } $output .= $cell; } $output .= PHP_EOL; } $output .= PHP_EOL; $output .= 'Score:' . $this->score . PHP_EOL; return $output; }
要运行游戏,我们只需要使用给定的高度和宽度实例化Scene对象,设置流监听系统以检测用户的输入,然后运行游戏循环。游戏循环是一个无限循环,将反复调用我上面概述的方法(至少直到游戏结束为止)。方法的顺序在这里很重要,因为我们需要在移动和射击飞船之前先听取用户的动作。此后,我们调用在场景中移动子弹和敌人的方法。
$scene = new Scene(50, 10); system('stty cbreak -echo'); $stdin = fopen('php://stdin', 'r'); stream_set_blocking($stdin, 0); while (1) { system('clear'); $scene->action($stdin); $scene->moveShip(); $scene->shoot(); $scene->moveEnemies(); $scene->moveBullets(); echo $scene->renderGame(); $scene->gameOver(); $scene->spawnEnemies(); usleep(100000); }
运行此代码将显示场景,其中包含玩家的飞船。几秒钟后,敌人开始出现,我们可以使用空格键向他们射击。如果子弹到达敌人,则该敌人将被移走,我们的得分将为+1。
- X X X > - X X X X X X X X X Score:2
我还创建了该游戏的gif图像。
通过GIPHY
除此之外,还可以添加一个调用srand() 以为创建敌人的随机值设置种子。通过为随机性添加种子,我们可以在每次游戏时将敌人的产生设置为更可预测的。这并不是完全可以预见的,因为玩家从场景中移除敌人会改变生成更多敌人的方式,但是游戏的初始条件始终是相同的。如果您想了解更多信息,我已经写过有关在PHP中使用随机种子的信息。该功能只需要放在脚本的开头即可。
srand(1);
这里有很多代码,但是效果很好。实际上,考虑到它是用PHP编写且仅在命令行上运行的,我实际上感到惊讶。它不是R-Type或Silkworm,但是对于一个简单的脚本,我感到非常高兴。上面的gif中的场景只有50宽和10高,但我在150宽和30高时能很好地工作。甚至可以通过增加在任何给定时刻在场景中生成的敌人数量来设置难度级别。如果您想看一下,我已经将整个侧面滚动射击PHP脚本添加到了要点中。随时尝试一下,让我知道您的高分!