我目前正在构建(使用Java进行练习)基本的命令行多人回合制游戏。在这个游戏中,每个玩家有5秒的动作时间。当他做出动作时(或当计时器结束时),其他玩家开始回合,依此类推。
每次计时器结束时,服务器都会发送TimerEnded
消息。
我目前的目标是实现无缺陷的输入读取,当TimerEnded
消息到达客户端时,该输入会可能会中断。
为此,我创建了一个名为InputManager
的单例。此类处理所有输入的阅读材料。我创建了一个名为ask
的方法,该方法将回调作为参数。在这种方法中,我创建了一个新线程,并在其中等待Scanner.hasNextInt
的输入。
此类还具有方法closeInput
,该方法将Interrupt消息发送到上述线程。
这是该类的当前实现:
class InputManager{
private Thread thread;
private InputManager(){}
private static InputManager instance;
private static InputManager getInstance(){
if(instance == null){
instance = new InputManager();
}
return instance;
}
/**
* Ask user to type a number.
* @param onSelected When the user has made his choice, this callback will be executed
*/
public static void ask( Consumer<Integer> onSelected){
getInstance().thread = new Thread(() -> {
System.out.println("Type a number:");
Scanner sc = new Scanner(System.in);
int selection = -1;
while (selection == -1) {
if(Thread.currentThread().isInterrupted()){
return;
}
if(sc.hasNextInt()){
selection = sc.nextInt();
onSelected.accept(selection);
} else {
sc.next();
selection = -1;
}
}
});
getInstance().thread.start();
}
/**
* Reset input stream (?)
*/
public static void closeInput(){
try {
getInstance().thread.interrupt();
} catch(NullPointerException e){
// do nothing
}
}
}
此代码极其不可靠。一会儿,我会告诉你我的意思。
我制作了一个名为Client的玩具类,并在main
中使用计时器模拟了TimerEnd
的消息收入。
class Client {
/**
* Ask user to type a number and send it to the server
*/
void makeRequest(){
InputManager.closeInput();
InputManager.ask((selected) -> {
System.out.println("Sent message: " + selected);
});
}
public static void main(String[] args) {
Client client = new Client();
client.makeRequest();
// Simulate Server messages
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("Message received");
client.makeRequest();
}
}, 5000, 5000);
}
}
以下是其实际运作方式:
Type a number:
2
Sent message: 2
Message received
Type a number:
3
Sent message: 3
Message received
Type a number: // Here I don't type anything
Message received
Type a number:
Message received
Type a number:
Message received
Type a number: // Here I can send multiple messages on the same "turn"
1
Sent message: 1
2
Message received
当前,我猜想Scanner仍在等待输入,因此if(isInterrupted)
语句在给出输入之前不会被点击。如果是这样,我该如何避免这种行为?
我了解到,这个问题非常长(可能不必要),并且由于您阅读了它,让我感谢您抽出宝贵的时间。
package com.company;
import java.util.*;
import java.util.function.Consumer;
class InputManager{
private Thread thread;
private InputManager(){}
private static InputManager instance;
private static InputManager getInstance(){
if(instance == null){
instance = new InputManager();
}
return instance;
}
/**
* Ask user to type a number.
* @param onSelected When the user has made his choice, this callback will be executed
*/
public static void ask( Consumer<Integer> onSelected){
getInstance().thread = new Thread(() -> {
System.out.println("Type a number:");
Scanner sc = new Scanner(System.in);
int selection = -1;
while (selection == -1) {
if(Thread.currentThread().isInterrupted()){
return;
}
if(sc.hasNextInt()){
selection = sc.nextInt();
onSelected.accept(selection);
} else {
sc.next();
selection = -1;
}
}
});
getInstance().thread.start();
}
/**
* Reset input stream (?)
*/
public static void closeInput(){
try {
getInstance().thread.interrupt();
} catch(NullPointerException e){
// do nothing
}
}
}
class Client {
/**
* Ask user to type a number and send it to the server
*/
void makeRequest(){
InputManager.closeInput();
InputManager.ask((selected) -> {
System.out.println("Sent message: " + selected);
});
}
public static void main(String[] args) {
Client client = new Client();
client.makeRequest();
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
System.out.println("Message received: thread interrupted");
client.makeRequest();
}
}, 5000, 5000);
}
}
答案 0 :(得分:1)
如我所见,您可以使用3种类型的线程:
因此,我将使用2个生产者和1个消费者:
所有这些,这样您就不必弄乱中断任何正在运行的线程,也无需检查扫描仪是否已准备好…
import java.util.Scanner;
public class Main {
private static final Scanner SCAN = new Scanner(System.in);
//This is the Scanner's input Producer:
private static class UserInputProducer extends Thread {
private final UserInputConsumer uInConsumer;
public UserInputProducer(final UserInputConsumer uInConsumer) {
this.uInConsumer = uInConsumer;
}
@Override
public void run() {
while (true) {
final int input = SCAN.nextInt();
SCAN.nextLine(); //Ignore the new line character.
uInConsumer.userInput(input); //Fire user input event (for the current user).
}
}
}
//This is the time out event Producer:
private static class TimeOutEventProducer {
private final UserInputConsumer uInConsumer;
private int validReportId = Integer.MIN_VALUE; //IDs starting from Integer.MIN_VALUE and
//going step by step to Integer.MAX_VALUE, which means about 4 billion resets can be done
//to this Producer before an unhandled overflow occurs.
public TimeOutEventProducer(final UserInputConsumer uInConsumer) {
this.uInConsumer = uInConsumer;
}
public synchronized void reset() {
new TimerOnce(this, ++validReportId).start(); //Start a new TimerOnce. Could be javax.swing.Timer with "setRepeats(false)".
}
/*sleepDone(...) is called by ALL TimerOnce objects... So we need an up-to-date id (the
reportId) to verify that the LAST one TimerOnce finished, rather than any other.*/
public synchronized void sleepDone(final int reportId) {
if (reportId == validReportId) //Only the last one timeout is valid...
uInConsumer.timedOut(); //Fire time out event (for the current user).
}
}
//This is just a "Timer" object which blocks for 5 seconds:
private static class TimerOnce extends Thread {
private final TimeOutEventProducer timeout;
private final int reportId;
public TimerOnce(final TimeOutEventProducer timeout,
final int reportId) {
this.timeout = timeout;
this.reportId = reportId;
}
@Override
public void run() {
try { Thread.sleep(5000); } catch (final InterruptedException ie) {} //Wait.
timeout.sleepDone(reportId); //Report that the time elapsed...
}
}
//This is the Consumer:
private static class UserInputConsumer {
private final String[] names;
private int input;
private boolean timedOut, hasInput;
public UserInputConsumer(final String[] names) {
this.names = names;
}
public synchronized int play() {
new UserInputProducer(this).start(); //Start scanning any user's input...
final TimeOutEventProducer timeout = new TimeOutEventProducer(this);
int i = -1;
do {
i = (i + 1) % names.length;
hasInput = false;
timedOut = false;
timeout.reset(); //Start the input wait timer...
System.out.print("User " + names[i] + " enter a number: "); //Clarify who's player is the turn.
while (!hasInput && !timedOut)
try { wait(); } catch (final InterruptedException ie) {} //Wait for user input or timeout.
//Interpret notification event (either user input, either timeout):
if (timedOut)
System.out.println("Sorry, out of time.");
else if (!hasInput)
throw new UnsupportedOperationException("Probably messed with the flags in the while-condition.");
}
while (input != 5); //Here you test the win/loss condition.
//Lets say, for example, the user that enters number '5' wins...
return i; //Return the winner's index.
}
public synchronized void timedOut() {
timedOut = true;
notify();
}
public synchronized void userInput(final int input) {
this.input = input;
hasInput = true;
notify();
}
}
public static void main(final String[] args) {
System.out.print("Enter number of players: ");
final int numPlayers = SCAN.nextInt();
SCAN.nextLine(); //Ignore the new line character.
final String[] names = new String[numPlayers];
for (int i=0; i<names.length; ++i) {
System.out.print("User " + (i+1) + " enter your name: ");
names[i] = SCAN.nextLine();
}
//Start the consumer (which in turn starts the producers) and start the main logic:
System.out.println(names[new UserInputConsumer(names).play()] + " wins!");
}
}
注意,该程序永远不会终止,因为扫描是无限的。但是您可以通过弄乱while (true)
的{{1}}条件来改变这种行为。
答案 1 :(得分:0)
好的,我制定了一个解决方案。
如我所想,问题是while循环(当然)在Scanner.hasNext
中被阻塞。为了避免阻塞,我使用了BufferedReader
,它具有方便的功能ready
,每当在true
中输入新行时,它都会返回System.in
。
基本上,我将InputManager.ask
方法更改为:
void ask(Consumer<Integer> onSelected){
getInstance().thread = new Thread(() -> {
System.out.println("Type a number:");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
Scanner sc = new Scanner(reader);
int selection = -1;
try {
while (selection == -1) {
//While there is no input to be processed
while (!reader.ready()) {
//This lets throw an InterruptedException
Thread.sleep(100);
}
if (sc.hasNextInt()) {
selection = sc.nextInt();
onSelected.accept(selection);
} else {
sc.next();
selection = -1;
}
}
} catch (IOException | InterruptedException e) {
// do nothing: function ends
}
});
getInstance().thread.start();
}
我还添加了这段(极其丑陋的)代码,以在重置之前使用任何输入,以防止检测到先前键入的任何先前行(现在刷新最后一行)。 (如果有人对如何以一种更优雅的方式提出建议,我很高兴听到您的声音)
public static void closeInput(){
try {
BufferedReader tmp = new BufferedReader(new InputStreamReader(System.in));
if(tmp.ready()){
tmp.readLine();
}
getInstance().thread.interrupt();
} catch(NullPointerException e){
// do nothing
} catch (IOException e) {
e.printStackTrace();
}
}