Game Development / Reddwarf на примере онлайн-игры «Камень-ножницы-бумага»: Клиент

в 18:13, , рубрики: client, darkstar, game server, java, reddwarf, метки: , , , ,

В статье Reddwarf для создания Java-сервера на примере онлайн-игры «Камень-ножницы-бумага»: Сервер было описано, как сделать сервер. Но поиграть на таком сервере без клиента невозможно. Поэтому в этой статье попробуем написать клиент и поэкспериментировать с сервером.

Подготовка к работе

Для разработки клиента скачиваем клиентские библиотеки Reddwarf в архиве sgs-client-dist-0.10.2.zip отсюда.
Создаем проект и подключаем к нему все библиотеки из директории lib скачанного архива.

Коротко о клиенте

Клиентская часть нашей игры будет состоять из двух классов — клиента и GUI.
Клиент реализует интерфейс SimpleClientListener — в него приходят события подключения/отключения, и сообщения от сервера. В качестве реакции на входящие сообщения клиент будет уведомлять об этом GUI. Кроме того, вся информация о протоколе будет содержаться только в этом классе, так что он будет заведовать и отправкой сообщений.

Подключение и аутентификация

Подключаемся к указанному серверу на порт 62964 — это порт по умолчанию для Reddwarf.
Для аутентификации будем использовать простейший (и, наверное, самый часто используемый в Reddwarf) способ аутентификации — PasswordAuthentication. Поскольку наш сервер не проверяет пароль, пароль оставляем пустым.

package hello.reddwarf.client;  import com.sun.sgs.client.ClientChannel; import com.sun.sgs.client.ClientChannelListener; import com.sun.sgs.client.simple.SimpleClient; import com.sun.sgs.client.simple.SimpleClientListener; import hello.reddwarf.Messages;  import java.io.IOException; import java.net.PasswordAuthentication; import java.nio.ByteBuffer; import java.util.Properties;  public class Client implements SimpleClientListener  {      private SimpleClient simpleClient;     private final String host;     private final String username;     public static final String DEFAULT_PORT = "62964";      private final ClientFrame frame;      public Client(String host, String username, ClientFrame frame) {         this.host = host;         this.username = username;         this.frame = frame;         simpleClient = new SimpleClient(this);     }      @Override     public PasswordAuthentication getPasswordAuthentication() {         return new PasswordAuthentication(username, new char[]{});     }      @Override     public void loggedIn() {         frame.loggedIn();     }      @Override     public void loginFailed(String s) {         frame.setStatus("login failed " + username+": "+s);     }      @Override     public ClientChannelListener joinedChannel(ClientChannel clientChannel) {         return null;     }      @Override     public void receivedMessage(ByteBuffer packet) {          String text = Messages.decodeString(packet);          if (text.startsWith("SCORE")) {             frame.setScore(text);         } else if (text.startsWith("SCORE")) {             frame.setScore(text);         } else if (text.startsWith("BATTLE")) {             frame.startBattle(text);         } else if (text.startsWith("DRAW")) {             frame.setBattleResult(text);         } else if (text.startsWith("WON")) {             frame.setBattleResult(text);         } else if (text.startsWith("LOST")) {             frame.setBattleResult(text);         } else if (text.startsWith("ERROR")) {             frame.setStatus(text);         }     }      public void login() {         try {             Properties connectProps = new Properties();             connectProps.put("host", host);             connectProps.put("port", DEFAULT_PORT);             simpleClient.login(connectProps);         } catch (Exception e) {             e.printStackTrace();             disconnected(false, e.getMessage());         }      }      public void play() {         try {             simpleClient.send(Messages.encodeString("PLAY"));         } catch (IOException e) {             e.printStackTrace();         }     }      public void answer(String text) {         try {             simpleClient.send(Messages.encodeString(text));         } catch (IOException e) {             e.printStackTrace();         }     } } 

Интерфейс пользователя

GUI представляет собой простейшую формочку на Swing с полями ввода и кнопочками:
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент

package hello.reddwarf.client;  import sun.awt.VerticalBagLayout;  import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener;  public class ClientFrame extends JFrame {      private JTextField usernameField;     private JLabel scoreLabel;     private JButton loginButton;     private JButton playButton;     private JLabel statusLabel;     private JButton rockButton;     private JButton paperButton;     private JButton scissorsButton;     private Client client;      public static void main(String[] args) {          new ClientFrame("RPS").setVisible(true);      }      protected ClientFrame(String title) {         super(title);         Container c = getContentPane();         c.setLayout(new GridLayout(10,1));          c.add(new JLabel("Username:"));         c.add(usernameField = new JTextField("player"+Math.round(Math.random()*10)));         c.add(scoreLabel = new JLabel("SCORE"));         c.add(loginButton = new JButton("Log in"));         c.add(playButton = new JButton("Play!"));         c.add(statusLabel = new JLabel("Status"));         c.add(rockButton = new JButton("Rock"));         c.add(paperButton = new JButton("Paper"));         c.add(scissorsButton = new JButton("Scissors"));          loginButton.setEnabled(true);         playButton.setEnabled(false);         rockButton.setEnabled(false);         paperButton.setEnabled(false);         scissorsButton.setEnabled(false);          setSize(320, 240);         setDefaultCloseOperation(DISPOSE_ON_CLOSE);         setVisible(true);          loginButton.addActionListener(new ActionListener() {             @Override             public void actionPerformed(ActionEvent e) {                 setStatus("Logging in...");                 client = new Client("localhost", usernameField.getText(), ClientFrame.this);                 client.login();             }         });         playButton.addActionListener(new ActionListener() {             @Override             public void actionPerformed(ActionEvent e) {                 setStatus("Playing...");                 client.play();             }         });         rockButton.addActionListener(new SendActionListener("ROCK"));         paperButton.addActionListener(new SendActionListener("PAPER"));         scissorsButton.addActionListener(new SendActionListener("SCISSORS"));     }      public void setStatus(String text) {         statusLabel.setText(text);     }      public void loggedIn() {         setStatus("Logged in!");         loginButton.setEnabled(false);         playButton.setEnabled(true);     }      public void setScore(String text) {         scoreLabel.setText(text);     }      public void startBattle(String text) {         setStatus(text);         playButton.setEnabled(false);         rockButton.setEnabled(true);         paperButton.setEnabled(true);         scissorsButton.setEnabled(true);     }      public void setBattleResult(String text) {         setStatus(text);         playButton.setEnabled(true);         rockButton.setEnabled(false);         paperButton.setEnabled(false);         scissorsButton.setEnabled(false);     }      private class SendActionListener implements ActionListener {         private final String text;          public SendActionListener(String text) {             this.text = text;         }          @Override         public void actionPerformed(ActionEvent e) {             client.answer(text);             rockButton.setEnabled(false);             paperButton.setEnabled(false);             scissorsButton.setEnabled(false);         }     } } 

Код класса Messages берется из предыдущей статьи.

Ура, запускаем!

Ну вот, код и готов. Можно запускать сервер и клиенты и смотреть, что будет.
Запускаем сервер.

мар 03, 2012 6:53:29 PM com.sun.sgs.impl.kernel.Kernel <init> INFO: The Kernel is ready, version: 0.10.2.1 мар 03, 2012 6:53:29 PM com.sun.sgs.impl.service.data.store.DataStoreImpl <init> INFO: Creating database directory : C:sgs-server-dist-0.10.2.1datadsdb мар 03, 2012 6:53:29 PM com.sun.sgs.impl.service.watchdog.WatchdogServerImpl registerNode INFO: node:com.sun.sgs.impl.service.watchdog.NodeImpl[1,health:GREEN,backup:(none)]@black registered мар 03, 2012 6:53:30 PM hello.reddwarf.server.Server initialize INFO: Starting new Rock-Paper-Scissors Server. Initialized database. мар 03, 2012 6:53:30 PM com.sun.sgs.impl.kernel.Kernel startApplication INFO: RockPaperScissors: application is ready мар 03, 2012 6:53:30 PM hello.reddwarf.server.OnlineLoggerTask run INFO: Online: 0 

Запускаем два клиента:
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент

Логинимся с одного и другого клиента:
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент
Как видим, у обоих игроков 0 очков на счету.

Лог сервера не заставляет себя ждать: поскольку мы первый раз запустили сервер, то в базе нет ни одного игрока, происходит автоматическая регистрация игроков player1 и player2.

мар 03, 2012 6:53:47 PM hello.reddwarf.server.Server loggedIn INFO: Client login: player2 мар 03, 2012 6:53:47 PM hello.reddwarf.server.Server loadOrRegister INFO: Registering new player player2 мар 03, 2012 6:53:48 PM hello.reddwarf.server.Server loggedIn INFO: Client login: player1 мар 03, 2012 6:53:48 PM hello.reddwarf.server.Server loadOrRegister INFO: Registering new player player1 мар 03, 2012 6:53:50 PM hello.reddwarf.server.OnlineLoggerTask run INFO: Online: 2 

Жмем у одного из игроков на Play — сервер автоматически выбирает второго игрока в качестве соперника и начинается бой:
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент

По логу видно, что сервер создал новую битву. Сервер ждет ответов игроков

мар 03, 2012 6:53:54 PM hello.reddwarf.server.Player play INFO: Choosing enemy for player1 мар 03, 2012 6:53:54 PM hello.reddwarf.server.Battle <init> INFO: Created Battle{2} for {player1} and {player2} мар 03, 2012 6:53:54 PM hello.reddwarf.server.Battle start INFO: Started Battle{2} 

Игроки отвечают:

мар 03, 2012 6:53:56 PM hello.reddwarf.server.Battle answer INFO: Battle{2} Player {player1} answer ROCK мар 03, 2012 6:53:57 PM hello.reddwarf.server.Battle answer INFO: Battle{2} Player {player2} answer PAPER 

Сервер ждет окончания времени битвы и подводит итоги:

мар 03, 2012 6:53:59 PM hello.reddwarf.server.Battle finish INFO: Battle{2} finished. Answers: {player1}->ROCK {player2}->PAPER 

На клиенте можно увидеть результаты боя и как изменилось количество очков: выигравший получил 2 очка, проигравший — ноль.
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент

Для того, чтобы убедиться, что данные сохранены — отключимся клиентом, перезапустим сервер и подключимся заново игроком player2.

java -jar bin/sgs-stop.jar java -jar bin/sgs-boot.jar 

Подключаемся клиентом: честно заработанные 2 очка сохранились.
Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент Game Development / Reddwarf на примере онлайн игры «Камень ножницы бумага»: Клиент

Во время перезапуска в логе могут возникать подобные сообщения (со стектрейсом):

WARNING: Task has been retried 25 times: com.sun.sgs.impl.service.session.ClientSessionServiceImpl$RemoveNodeSpecificDataTask[owner:app:RockPaperScissors] WARNING: Task has been retried 25 times: com.sun.sgs.impl.service.channel.ChannelServiceImpl$RemoveChannelServerProxyTask[owner:app:RockPaperScissors] WARNING: Task has been retried 50 times: com.sun.sgs.impl.service.session.ClientSessionServiceImpl$RemoveNodeSpecificDataTask[owner:app:RockPaperScissors] WARNING: Task has been retried 50 times: com.sun.sgs.impl.service.channel.ChannelServiceImpl$RemoveChannelServerProxyTask[owner:app:RockPaperScissors] WARNING: Task has been retried 75 times: com.sun.sgs.impl.service.session.ClientSessionServiceImpl$RemoveNodeSpecificDataTask[owner:app:RockPaperScissors] WARNING: Task has been retried 75 times: com.sun.sgs.impl.service.channel.ChannelServiceImpl$RemoveChannelServerProxyTask[owner:app:RockPaperScissors] 

Они появляются из-за того, что при старте сервера происходит много одновременных обращений к общим объектам сервера. Поскольку многопоточность в Reddwarf базируется на неблокирующей синхронизации, то конфликтующие транзакции отказываются и производится повторная попытка выполнения действий. Платформа настроена таким образом, что при большом количестве откатов одной и той же транзакции выводится сообщение в лог. При старте сервера эти сообщения не должны вызывать беспокойства, а вот появление подобных сообщений во время работы сервера говорит об узких местах в коде серверной логики.

Исходный код клиента и сервера выложен на code.google.com, его можно закачать с SVN:

svn checkout http://reddwarf-rock-paper-scissors-example.googlecode.com/svn/trunk/ reddwarf-rock-paper-scissors-example-read-only

А если вы не хотите возиться с исходниками, то скомпилированные версии клиента и сервера можно найти тут: code.google.com/p/reddwarf-rock-paper-scissors-example/downloads/list
Клиент запускается с помощью команды

java -jar HelloReddwarfClient.jar

Для запуска сервера файл deploy.jar необходимо скопировать в директорию sgs-server-dist-0.10.2.1/deploy/ и выполнить команду запуска сервера:

java -jar bin/sgs-boot.jar

Спасибо за внимание. Буду рад любой конструктивной критике.

Автор: dvb

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js