PHP CLI - get user input while still doing things

2019-02-27 20:41发布

问题:

I'm working on a game, written in PHP and that runs in a console. Think back to old MUDs and other text-based games, even some ASCII art!

Anyway, what I'm trying to do is have things happening while also accepting user input.

For instance, let's say it's a two player game and Player 1 is waiting for Player 2 to make a move. This is easily done by just listening for a message.

But what if Player 1 wants to change some options? What if they want to view details on aspects of the game state? What about conceding the game? There are many things a Player may want to do while waiting for their opponent to make a move.

Unfortunately the best I have right now is the fact that Ctrl+C completely kills the program. The other player is then left hanging, until the connection is dropped. Oh, and the game is completely lost.

I get user input with fgets(STDIN). But this blocks execution until input has been received (which is usually a good thing).

Is it even possible for a console program like this to handle input and output simultaneously? Or should I just look at some other interface?

回答1:

In short PHP is not built for this, but you might get some help from one of these extensions. I'm not sure how thorough they are, but you really probably want to use a text UI library. (And really you probably do not want to use PHP for this.)

All that said, you need to get non blocking input from STDIN character by character. Unfortunately most terminals are buffered from PHP's point of view, so you won't get anything until enter is pressed.

If you run stty -icanon (or your OS's equivalent) on your terminal to disable buffering, then the following short program basically works:

<?php

stream_set_blocking(STDIN, false);

$line = '';

$time = microtime(true);

$prompt = '> ';

echo $prompt;

while (true)
{
  if (microtime(true) - $time > 5)
  {
    echo "\nTick...\n$prompt$line";
    $time = microtime(true);
  }

  $c = fgetc(STDIN);
  if ($c !== false)
  {
    if ($c != "\n")
      $line .= $c;
    else
    {
      if ($line == 'exit' || $line == 'quit')
        break;
      else if ($line == 'help')
        echo "Type exit\n";
      else
        echo "Unrecognized command.\n";

      echo $prompt;
      $line = '';
    }
  }
}

(It relies on local echo being enabled to print the characters as they are typed.)

As you see, we are just looping around forever. If a character exists, add it to the $line. If enter is pressed, process $line. Meanwhile, we are ticking every five seconds just to show that we could be doing something else while we wait for input. (This will consume maximum CPU; you'd have to issue a sleep() to get around that.)

This isn't meant to be a practical example, per se, but perhaps will get you thinking in the proper direction.



回答2:

It is possible to build a game like you describe using ncurses (non-blocking mode) and libevent. That way, you get close to no CPU consumption. Handling individual keys is sometimes awkward (implement Backspace yourself, it's not fun at all - and did you know various OSes send different keycodes on Backspace press?), and gets really tricky if you want to support UTF-8 properly. Still, completely viable.

In particular, it is beneficial to make extensive use of libevent, by reading both the network and keyboard (stdin) input with it. This function enables you to listen for individual keys: http://www.php.net/manual/en/function.ncurses-cbreak.php which you can later read using libevent API. The key to keep in mind is that you will sometimes end up reading more than 1 key at a time, and it has to be handled (so loop over everything that you have read). Otherwise, the user will be annoyed to see that not all key presses are "reaching" the application and some are lost.



回答3:

Sorry Matthew, I'm going to have to un-accept your answer, because I have found it myself:

Use the following code to receive user input while still doing something else:

while(/* some condition that the code running is waiting on */) {
    // perform one step or iteration of that code
    exec("choice /N /C ___ /D _ /T _",$out,$ret);
    // /C is a list of letters that do something
    // /D is the default action that will be used as a no-op
    // /T is the amount of time to wait, probably best set to one second
    switch($ret) {
        // handle cases - the "default" case should be "continue 2"
    }
}

This can then be used to interrupt the loop and enter an options menu, or trigger some other event, or could even be used to type out a command if used right.