他们是怎么做到的? PHPSnake:检测按键

Vector image of an old school joystick

At a recent conference in Bulgaria, there was a hackathon for which Andrew Carter created a PHP console version of the popular “snake” game.

保加利亚最近的一次会议上安德鲁·卡特(Andrew Carter)为此举办了一次黑客马拉松,为流行的“蛇”游戏创建了PHP控制台版本。

I thought it was a really interesting concept, and since Andrew has a history of using PHP for weird things, I figured I’d demystify and explain how it was done.

我认为这是一个非常有趣的概念,并且由于安德鲁有使用PHP处理怪异事物的历史,所以我想我要揭开神秘面纱,并解释一下它是如何完成的。

The original repository is here, but we’ll build a version of it from scratch in this series so no need to clone it.

原始存储库在这里 ,但是在本系列中我们将从头开始构建它的一个版本,因此无需克隆它。

Screenshot of the game

先决条件和规则 (Prerequisites and Rules)

As usual, I’ll be using my Homestead Improved box to get up and running instantly. You can use your own environment, just make sure it has a decent version of PHP runnable from the command line.

和往常一样,我将使用“ 改良的Homestead”盒子立即启动并运行。 您可以使用自己的环境,只需确保它具有可从命令行运行的体面版本PHP。

The snake game we’re replicating has the following features:

我们要复制的蛇游戏具有以下功能:

  • a snake starts as a single on-screen character, and gets longer by one character every time it eats a piece of food.

    蛇以屏幕上的单个字符开始,每次吃食物时都会增加一个字符。
  • food is spawned randomly anywhere on the map.

    食物会在地图上的任何地方随机产生。
  • in single player mode, the snake is controlled by the arrow keys.

    在单人游戏模式下,蛇由箭头键控制。
  • in two player mode, one snake is controlled with the WSAD keys, while the other is controlled with the arrow keys.

    在两种播放器模式下,一条蛇由WSAD键控制,而另一条则由箭头键控制。
  • in single player mode, the walls are obstacles and cause a collision. Running into a wall or into yourself ends the game.

    在单人游戏模式下,墙壁是障碍物,会引起碰撞。 碰壁或撞到自己就结束了游戏。
  • in multi player mode, only your own snake or the enemy’s snake is an obstacle – the walls wrap around the world. Colliding will reset your snake’s length to 0. The player with the longest snake after 100 seconds have elapsed is the winner.

    在多人游戏模式下,只有您自己的蛇或敌人的蛇是障碍物-墙壁环绕着世界。 碰撞会将您的蛇的长度重置为0。经过100秒后,蛇最长的玩家将成为赢家。
  • it’s CLI, so does not run in the browser – it runs in the terminal window

    它是CLI,因此不会在浏览器中运行-它在终端窗口中运行

Note that the game doesn’t work in native Windows – to run it on a Windows platform, use a good VM like Homestead Improved.

请注意,该游戏无法在本机Windows上运行-要在Windows平台上运行该游戏,请使用优质的VM,例如Homestead Improvement

自举 (Bootstrapping)

To launch a CLI (console) game, we need something similar to an index.php file in traditional websites – a “front controller” which reads our command line input, parses it, and then launches the required classes, just like in a traditional web app. We’ll call this file play.php.

要启动CLI(控制台)游戏,我们需要与传统网站中的index.php文件类似的东西–一个“前端控制器”,它读取我们的命令行输入,对其进行解析,然后启动所需的类,就像在传统网站中一样网络应用。 我们将其称为play.php

<?php

$param = ($argc > 1) ? $argv[1] : '';

echo "Hello, you said: " . $param;

Okay, so if we launch this file with php play.php something, we’ll get:

好的,因此,如果我们使用php play.php something启动此文件,我们将获得:

Hello, you said: something

Let us now make a bogus SnakeGame.php in the subfolder classes class which gets invoked by this front controller.

现在让我们在子文件夹classes创建一个假的SnakeGame.php ,该类由该前端控制器调用。

// classes/Snake.php

<?php

namespace PHPSnake;

class SnakeGame
{
    public function __construct()
    {
        echo "Hello, I am snake!";
    }
}

Let’s also update the front controller to load this class and invoke it:

我们还更新前端控制器以加载此类并调用它:

<?php

use PHPSnake\SnakeGame;

require_once 'classes/SnakeGame.php';

$param = ($argc > 1) ? $argv[1] : '';

$snake = new SnakeGame();

Okay, we should be seeing a snakey greeting now if we re-run php play.php.

好的,如果我们重新运行php play.php ,我们现在应该会收到令人讨厌的问候。

镜框 (Frames)

The way that traditional games work is by re-checking the system’s state every frame, and a higher frame rate means more frequent checking. For example, when the screen is rendered (a single frame), the system can tell us that the letter A is pressed, and that one of the players is in the process of colliding with food so a snake growth needs to happen. All this happens in a single frame, and “FPS” or frames per second means exactly that – how many of these system observations happen per second.

传统游戏的工作方式是通过每帧重新检查系统状态,而更高的帧速率意味着更频繁的检查。 例如,当渲染屏幕(单帧)时,系统可以告诉我们字母A被按下,并且其中一个玩家正在与食物碰撞,因此需要发生蛇的生长。 所有这些都发生在单个帧中,而“ FPS”或每秒帧数恰好意味着–每秒发生多少这些系统观察。

Programming languages intended for this have built-in loops for checking state. PHP… not so much. We can hack our way around this, but let’s go through it all step by step. Let’s modify Snake’s constructor like so:

用于此目的的编程语言具有用于检查状态的内置循环。 PHP…没那么多。 我们可以解决这个问题,但是让我们逐步进行所有操作。 让我们这样修改Snake的构造函数:

public function __construct()
    {
        echo "Hello, I am snake!";

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $key = fgetc($stdin);
            echo $key;
        }
    }

First, we open an “stdin” stream, meaning we’re creating a way for PHP to get “standard input” from the command line, treating it as if it was a file (hence, fopen). The fgetc function is used to get a single character from a file pointer (as opposed to fgets which gets a whole line) and then the key is printed on screen. The while loop is there so PHP keeps waiting for more input, and doesn’t end the script after a single key is pressed.

首先,我们打开一个“ stdin”流,这意味着我们正在为PHP创建一种从命令行获取“标准输入”的方式,将其视为文件(因此,是fopen )。 fgetc函数用于从文件指针获取单个字符(而不是fgets获取整行),然后将键打印在屏幕上。 while循环在那里,因此PHP一直在等待更多输入,并且在按下单个键后不会结束脚本。

If we try running our app, though, we’ll notice that the key is only echoed out after we press enter – so after a new line. What’s more – everything we wrote gets echoed. What we want instead, is for PHP to echo out each key we press, as we press it.

但是,如果尝试运行我们的应用程序,我们会注意到,只有在按Enter键之后才回显键,因此在换行之后也是如此。 更重要的是-我们写的所有东西都得到了回应。 相反,我们想要的是让PHP在按下时回显我们按下的每个键。

Here are two ways to accomplish this.

这是完成此操作的两种方法。

姿势 (stty)

The first way is via a tool called stty which comes native with terminal applications on *nix systems (so no Windows, unless you’re using a VM like Homestead Improved). It’s used to modify and configure terminal input and output through the use of flags – when prefixed with -, these often mean “deactivation” and vice versa.

第一种方法是通过一个名为stty的工具,该工具随* nix系统上的终端应用程序提供(不是Windows,除非您使用的是Homestead Enhanced )。 它用于通过使用标志来修改和配置终端输入和输出–当标志带有- ,它们通常表示“停用”,反之亦然。

What we want is stty’s cbreak flag. As per the docs:

我们想要的是stty的cbreak标志。 根据文档:

Normally, the tty driver buffers typed characters until a newline or carriage return is typed. The cbreak routine disables line buffering and erase/kill character-processing (interrupt and flow control characters are unaffected), making characters typed by the user immediately available to the program.

通常,tty驱动程序将缓冲键入的字符,直到键入换行符或回车符为止。 cbreak例程禁用行缓冲和擦除/杀死字符处理(中断和流控制字符不受影响),使用户键入的字符可立即用于程序。

In layman’s terms, we’re no longer waiting for the enter key to be fired to send the inputs.

用外行的话来说,我们不再等待输入Enter键被发送来发送输入。

public function __construct()
    {
        echo "Hello, I am snake!";

        system('stty cbreak');
        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }

We invoke the system function which basically acts as a shell proxy and forwards the command provided as an argument to the terminal we’re running the PHP app from. After this, running the app with php play.php should let you write characters and echo them immediately after every keypress.

我们调用system功能,该功能基本上充当shell代理,并将作为参数提供的命令转发到我们从中运行PHP应用程序的终端。 此后,使用php play.php运行应用程序应使您编写字符并在每次按键后立即回显它们。

Note that we’re getting the key code because we wrapped the character into ord – this function returns the ASCII code for a given character.

请注意,我们获取键是因为将字符包装到ord -该函数返回给定字符的ASCII码。

stty solution working

readline回调 (readline callback)

The second way is by using the surprisingly mystical and underdocumented readline_callback_handler_install function in combination with stream_select (also *nix only because stream_select calls the system select command, which is not available in Windows).

第二种方法是结合使用stream_select (也** nix,仅是因为stream_select调用系统select命令,在Windows中不可用)结合使用令人惊讶的神秘且文档不足的readline_callback_handler_install函数。

readline_callback_handler_install takes a prompt message as its first argument (so, what to “ask” the user), and a callback as the second. In our case, we leave it as an empty function because we don’t really need it – we’re reading characters by parsing STDIN, a constant which is actually just a shortcut for fopen('php://stdin', 'r');. Our code for this part looks like this:

readline_callback_handler_install将提示消息作为其第一个参数(因此,向用户“询问”的内容),并作为第二个回调。 在我们的例子中,我们将其保留为空函数,因为我们并不是真正需要它-我们通过解析STDIN来读取字符,STDIN实际上是fopen('php://stdin', 'r'); 。 我们这部分的代码如下所示:

public function __construct()
    {
        echo "Hello, I am snake!";

        readline_callback_handler_install('', function() { });
        while (true) {
            $r = array(STDIN);
            $w = NULL;
            $e = NULL;
            $n = stream_select($r, $w, $e, null);
            if ($n) {
                $c = ord(stream_get_contents(STDIN, 1));
                echo "Char read: $c\n";

            }
        }
    }

The stream select accepts several streams, and acts as an event listener for when something changes on any of them. Given that we’re only looking for “read” (i.e. input), we define that as an array format of STDIN. The others are set to NULL, we don’t need them. Since stream_select accepts only values by reference, we cannot pass NULL in directly, they must be defined as variables beforehand.

流选择接受多个流,并充当事件侦听器,以了解其中任何一个发生更改的情况。 鉴于我们只在寻找“读取”(即输入),因此将其定义为STDIN的数组格式。 其他设置为NULL,我们不需要它们。 由于stream_select仅通过引用接受值,因此我们不能直接传递NULL,因此必须事先将它们定义为变量。

The if block checks if $n is positive ($n is the number of updated streams), and if so, it extracts the first character from STDIN, which is our input key press:

if块检查$n是否为正( $n是更新的流数),如果是,则从STDIN中提取第一个字符,这是我们的输入按键:

readline solution working

哪个更好? (Which is better?)

I prefer the stty method because both are *nix only, both invoke system commands, but the latter is arguably more complex, and its effect can vary depending on current terminal settings in a given OS.

我更喜欢使用stty方法,因为它们都仅是* nix,都可以调用系统命令,但是后者可能更复杂,并且其效果会根据给定OS中的当前终端设置而有所不同。

Notice a crucial difference between the two gifs above – the stty method also echoes out the character being pressed before outputting its keycode. To completely remove all auto-output and have PHP process it all, we need another stty flag: -echo.

注意上面两个gif之间的关键区别– stty方法还在输出其键码之前回显了被按下的字符。 要完全删除所有自动输出并让PHP处理所有这一切,我们需要另一个stty标志: -echo

public function __construct()
    {
        echo "Hello, I am snake!";

        system('stty cbreak -echo');
        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }

    }

As per the docs, -echo disables the output of input characters.

根据docs-echo禁用输入字符的输出。

将蛇映射到方向 (Mapping Snakes to Directions)

It goes without saying we’ll need a way to tell each snake which direction to move in. First, let’s make a new Snake class in Snake.php to represent an instance of a player and hold their state.

不用说,我们将需要一种方法来告诉每条蛇向哪个方向移动。首先,让我们在Snake.php创建一个新的Snake类,以表示玩家的实例并保持其状态。

<?php

namespace PHPSnake;

class Snake
{

    /** @var string */
    private $name;

    /** @var string */
    private $direction;

    /** @var int */
    private $size = 0;

    const DIRECTIONS = ['UP', 'DOWN', 'LEFT', 'RIGHT'];

    public function __construct(string $name = null)
    {
        if ($name === null) {
            $this->name = $this->generateRandomName();
        } else {
            $this->name = $name;
        }
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function setDirection(string $direction) : Snake
    {
        $direction = strtoupper($direction);
        if (!in_array($direction, Snake::DIRECTIONS)) {
            throw new \InvalidArgumentException(
                'Invalid direction. Up, down, left, and right supported!'
            );
        }
        $this->direction = $direction;
        echo $this->name.' is going '.$direction."\n";

        return $this;
    }

    private function generateRandomName(int $length = 6) : string
    {
        $length = ($length > 3) ? $length : 6;
        $name = '';

        $consonants = 'bcdfghklmnpqrstvwxyz';
        $vowels = 'aeiou';

        for ($i = 0; $i < $length; $i++) {
            if ($i % 2 == 0) {
                $name .= $consonants[rand(0, strlen($consonants)-1)];
            } else {
                $name .= $vowels[rand(0, strlen($vowels)-1)];
            }
        }

        return ucfirst($name);
    }
}

Upon instantiation, a snake is given a name. If none is provided, a simple function generates one at random. The direction is null at the time of instantiation, and the directions that can be passed in are limited by the DIRECTIONS constant of the class.

实例化后,会给蛇起一个名字。 如果没有提供,则简单函数会随机生成一个函数。 实例化时,该方向为null,并且可以传递的DIRECTIONS受该类的DIRECTIONS常量限制。

Next, we’ll update our SnakeGame class.

接下来,我们将更新SnakeGame类。

<?php

namespace PHPSnake;

class SnakeGame
{

    /** @var array */
    private $snakes = [];

    public function __construct()
    {

    }

    /**
     * Adds a snake to the game
     * @param Snake $s
     * @return SnakeGame
     */
    public function addSnake(Snake $s) : SnakeGame
    {
        $this->snakes[] = $s;
        return $this;
    }

    /**
     * Runs the game
     */
    public function run() : void
    {
        if (count($this->snakes) < 1) {
            throw new \Exception('Too few players!');
        }
        system('stty cbreak -echo');

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";
        }
    }
}

This moves the keypress watching logic into a run function which we call after adding the necessary snakes to the game via addSnake.

这会将按键观看逻辑移动到运行功能,在通过addSnake将必要的蛇添加到游戏中后,我们将调用该功能。

Finally, we can update the frontcontroller to use these updated classes.

最后,我们可以更新前控制器以使用这些更新的类。

<?php

use PHPSnake\Snake;
use PHPSnake\SnakeGame;

require_once 'classes/Snake.php';
require_once 'classes/SnakeGame.php';

$param = ($argc > 1) ? $argv[1] : '';

$game = new SnakeGame();
$game->addSnake(new Snake());
$game->run();

Now things are getting a bit more structured! Finally, let’s map the directions to the snakes.

现在事情变得更有条理了! 最后,让我们将方向映射到蛇。

We’ll have custom key mappings for each player, and depending on how many players we include, that’s how many mappings we’ll load. That way is much simpler than a long switch block. Let’s add the $mappings property to our SnakeGame class:

我们将为每个玩家提供自定义键映射,并且根据我们包括的玩家数量,我们将加载多少映射。 这样比长的开关块要简单得多。 让我们将$mappings属性添加到我们的SnakeGame类中:

/**
     * Key mappings
     * @var array
     */
    private $mappings = [
        [
            65 => 'up',
            66 => 'down',
            68 => 'left',
            67 => 'right',
            56 => 'up',
            50 => 'down',
            52 => 'left',
            54 => 'right',
        ],
        [
            119 => 'up',
            115 => 'down',
            97 => 'left',
            100 => 'right',
        ],

    ];

Each array corresponds to a single player/snake. The main player can be controlled either via the cursor keys, or the corresponding numpad numbers. Player two can only be controlled via the WSAD keys. You can see how easy this makes adding new mappings for additional players.

每个阵列对应一个播放器/蛇。 可以通过光标键或相应的数字键盘编号来控制主播放器。 玩家二只能通过WSAD键进行控制。 您会看到这使添加更多玩家的新映射变得多么容易。

Then, let’s update the run method:

然后,让我们更新run方法:

/**
     * Runs the game
     */
    public function run() : void
    {
        if (count($this->snakes) < 1) {
            throw new \Exception('Too few players!');
        }

        $mappings = [];
        foreach ($this->snakes as $i => $snake) {
            foreach ($this->mappings[$i] as $key => $dir) {
                $mappings[$key] = [$dir, $i];
            }
        }

        system('stty cbreak -echo');

        $stdin = fopen('php://stdin', 'r');

        while (1) {
            $c = ord(fgetc($stdin));
            echo "Char read: $c\n";

            if (isset($mappings[$c])) {
                $mapping = $mappings[$c];
                $this->snakes[$mapping[1]]->setDirection($mapping[0]);
            }

        }
    }

The run method now loads as many mappings as there are snakes, and reorders them so that the keycode is the key of the mapping array, and the direction and snake index are a child array – this makes it very easy to later on just single-line the direction changes. If we run our game now (I updated play.php to add two snakes), we’ll notice that pressing random keys just produces key codes, while pressing WSAD or the cursor keys outputs the name of the snake and the direction it’ll be moving in after the keypress:

现在,run方法会加载与蛇一样多的映射,并对其进行重新排序,以使键码成为映射数组的键,而direction和snake索引为子数组–这使得以后仅使用单行代码非常容易线的方向变化。 如果现在运行我们的游戏(我更新了play.php以添加两条蛇),我们会注意到按随机键只会生成键代码,而按WSAD或光标键会输出蛇的名称及其方向在按键后进入:

Snakes yelling out directions

We now have a rather nicely structured game base for monitoring key presses and responding to them.

现在,我们有一个结构良好的游戏库,用于监视按键并对其进行响应。

结论 (Conclusion)

In this tutorial, we looked at keypress input game loops in PHP CLI applications. This unconventional use of PHP is about to get even weirder in part two, where we’ll deal with rendering players, movement, borders, and collisions.

在本教程中,我们研究了PHP CLI应用程序中的按键输入游戏循环。 PHP的这种非常规用法将在第二部分变得更加奇怪,在第二部分中,我们将处理渲染播放器,移动,边界和碰撞。

In the meanwhile, do you have any ideas on how to further improve fetching the keypress? Let us know in the comments!

同时,您对如何进一步改善按键的获取有任何想法吗? 让我们在评论中知道!

翻译自: https://www.sitepoint.com/howd-they-do-it-phpsnake-detecting-keypresses/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值