Докуметация Cтарт Статьи Форум Лента Вход
Не официальное русскоязычное сообщество
Главная
    Документация jMonkeyEngine
        jMonkeyEngine Уроки и Документация
            Документация для продвинутых пользователей
                SpiderMonkey: Многопользовательская сеть

SpiderMonkey: Многопользовательская сеть

Опубликованно: 07.07.2017, 19:32
Последняя редакция, Andry: 09.07.2017 15:16

В этом документе вы познакомитесь с сетевым SpiderMonkey API. Вы будете использовать эту API при разработке игр, в которых несколько игроков соревнуются друг с другом в режиме реального времени. Многопользовательская игра состоит из нескольких клиентов, подключающихся к серверу:

  • Центральный сервер (одно безголовое SimpleApplication) координирует игру в фоновом режиме.
  • Каждый игрок запускает игровой клиент (стандартное SimpleApplication) и подключается к центральному серверу.

Каждый Клиент информирует Сервер о перемещениях и действиях своего игрока. Сервер централизованно поддерживает состояние игры и передает информацию о состоянии всем подключенным клиентам. Эта сетевая синхронизация позволяет всем клиентам совместно использовать один и тот же игровой мир. А каждый клиент отображает состояние игры каждому игроку с точки зрения этого самого игрока.

Обзор API SpiderMonkey

SpiderMonkey API представляет собой набор интерфейсов и вспомогательных классов в пакете com.jme3.network. Для большинства пользователей этот пакет и пакет «message» — это все, о чем им нужно беспокоиться. («base» и «kernel» пакеты вступают в игру только при реализации пользовательских сетевых транспортов или альтернативных клиент-серверных протоколов, что теперь возможно).

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

Виден от клиента   Виден с сервера
com.jme3.network.Client ==  com.jme3.network.HostedConnection

Вы можете зарегистрировать несколько типов слушателей, чтобы получать уведомления об изменениях.

  • MessageListener как на клиенте, так и на сервере уведомляются о поступлении новых сообщений. Вы можете использовать MessageListener для уведомления только о конкретных типах сообщений.
  • ClientStateListener информирует Клиента об изменениях в состоянии своего соединения, например когда клиент получает удалении с сервера.
  • ConnectionListener информируют Сервер о прибытии и удалении HostedConnection, например, если клиент присоединяется или завершает работу.
  • ErrorListener информируют Клиента о сетевых исключениях, которые произошли, например, если сервер выходит из строя, клиент выдает исключение ConnectorException, его можно получить, чтобы приложение могло что-то с этим сделать.

Клиент и Сервер

Создание сервера

Игровой сервер — «безголовый com.jme3.app.SimpleApplication:

public class ServerMain extends SimpleApplication {
  public static void main(String[] args) {
    ServerMain app = new ServerMain();
    app.start(JmeContext.Type.Headless); // безголовый тип для серверов!
  }
}
Безголовый(Headless) SimpleApplication выполняет метод simpleInitApp() и обычно запускает цикл обновления. Но приложение не открывает окно и не слушает ввод пользователя. Это типичное поведение для серверного приложения.

Создайте com.jme3.network.Server в методе simpleInitApp() и укажите порт связи, например 6143.

  public void simpleInitApp() {
    ...
    Server myServer = Network.createServer(6143);
    myServer.start();
    ...
  }

Когда вы запускаете это приложение на хосте, сервер готов принять клиентов. Давайте создадим следующий клиент.

Создание клиента

Игровой клиент является стандартным com.jme3.app.SimpleApplication.

public class ClientMain extends SimpleApplication {
  public static void main(String[] args) {
    ClientMain app = new ClientMain();
    app.start(JmeContext.Type.Display); // стандартный тип отображения
  }
}
Стандартный режим SimpleApplication с Display выполняет метод simpleInitApp(), запускает цикл обновления, открывает окно для визуализированного видеовыхода и слушатель ожидает ввод пользователя. Это типичное поведение для клиентского приложения.

Создайте com.jme3.network.Client в методе simpleInitApp() и укажите IP-адрес сервера и тот же порт связи, что и для сервера, здесь 6143.

public void simpleInitApp() {
   ...
   Client myClient = Network.connectToServer("localhost", 6143);
   myClient.start();
   ...

Адрес сервера может быть в формате «localhost» или «127.0.0.1» (для локального тестирования) или IP-адрес удаленного хоста в формате «123.456.78.9». В этом примере мы предполагаем, что сервер работает на локальном хосте.

При запуске этого клиента он подключается к серверу.

Получение информации о клиенте

Сервер ссылается на подключенных клиентов как на объекты com.jme3.network.HostedConnection. Сервер может получать информацию о клиентах следующим образом:

Accessor  Назначение
myServer.getConnections()  Сервер получает коллекцию всех подключенных объектов HostedConnection (все подключенные клиенты).
myServer.getConnections().size()  Сервер получает количество всех подключенных объектов HostedConnection (количество клиентов).
myServer.getConnection(0)  Сервер получает первый (0), второй (1) и.т.д. Подключенный объект HostedConnection (один клиент).

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

Accessor  Назначение
conn.setAttribute(“MyState, new MyState());  Сервер может изменить атрибут HostedConnection.
MyState state = conn.getAttribute(“MyState)  Сервер может читать атрибут HostedConnection.

Обмен сообщениями

Создание типов сообщений

Каждое сообщение представляет данные, которые вы хотите передать между клиентом и сервером. Общие примеры сообщений включают в себя обновление преобразований или игровые действия. Для каждого типа сообщения вы создаёте свой класс сообщения, который наследует com.jme3.network.AbstractMessage. Используйте аннотацию @Serializable из com.jme3.network.serializing.Serializable и создайте пустой конструктор по умолчанию. Ваши конструкторы, поля и методы зависят от вас, и зависят от данных сообщений, которые вы хотите передать.

@Serializable
public class HelloMessage extends AbstractMessage {
  private String hello;       // данные пользовательских сообщений
  public HelloMessage() {}    // пустой конструктор
  public HelloMessage(String s) { hello = s; } // пользовательский конструктор
}

Вы должны зарегистрировать каждый тип сообщения в com.jme3.network.serializing.Serializer, как на сервере, так и на клиенте!

Serializer.registerClass(HelloMessage.class);

Ответ на сообщения

После получения Сообщения Слушатель реагирует на него. Слушатель может получать доступ к полям сообщения и отправить сообщение назад, запускать новые потоки и.т.д. Есть два слушателя: один на сервере, один на клиенте. Для каждого типа сообщения вы реализуете ответы в методе Слушателя messageReceived().

ClientListener.java

Создайте один ClientListener.java наследующий com.jme3.network.MessageListener.

public class ClientListener implements MessageListener<Client> {
  public void messageReceived(Client source, Message message) {
    if (message instanceof HelloMessage) {
      // сделать что-то с сообщением
      HelloMessage helloMessage = (HelloMessage) message;
      System.out.println("Client #"+source.getId()+" received: '"+helloMessage.getSomething()+"'");
    } // иначе...
  }

Каждый тип сообщений зарегистрируйте в клиентском слушатель для клиента.

myClient.addMessageListener(new ClientListener(), HelloMessage.class);

ServerListener.java

Создайте один ServerListener.java наследующий com.jme3.network.MessageListener.

public class ServerListener implements MessageListener<HostedConnection> {
  public void messageReceived(HostedConnection source, Message message) {
    if (message instanceof HelloMessage) {
      // сделать что-то с сообщением
      HelloMessage helloMessage = (HelloMessage) message;
      System.out.println("Server received '" +helloMessage.getSomething() +"' from client #"+source.getId() );
    } // иначе....
  }

Каждый тип сообщений зарегистрируйте в слушатель сервера для сервере:

myServer.addMessageListener(new ServerListener(), HelloMessage.class);

Создание и отправка сообщений

Давайте создадим новое сообщение типа HelloMessage:

Message message = new HelloMessage("Hello World!");

Теперь клиент может отправить это сообщение на сервер:

myClient.send(message);

Или сервер может передавать это сообщение всем HostedConnection(клиентам):

Message message = new HelloMessage("Welcome!");
myServer.broadcast(message);

Или сервер может отправить сообщение определенному подмножеству клиентов (например, в HostedConnection conn1, conn2 и conn3):

myServer.broadcast( Filters.in( conn1, conn2, conn3 ), message );

Или сервер может отправить сообщение всем, кроме нескольких выбранных клиентов (например, ко всем HostedConnections, но не conn4):

myServer.broadcast( Filters.notEqualTo( conn4 ), message );

В последних двух методах отправки сообщений используется com.jme3.network.Filters, чтобы выбрать подмножество получателей. Если вы знаете точный список получателей, всегда отправляйте им сообщения непосредственно с помощью фильтров; Избегайте наводнения сети ненужными для всех вещами.

Идентификация и отказ

ID клиента и HostedConnection одинаковы на обоих концах соединения. ID авторизируется сервером.

... myClient.getId() ...

У сервера есть версия игры и название игры. Каждый клиент ожидает связи с сервером с определенным названием и версией игры. Сначала проверьте, совпадает ли название игры, а затем соответствует ли версия игры, перед отправкой любых сообщений! Если они не совпадают, SpiderMoney отклонит этого клиента для вас, у вас нет выбора в mater. Это поможет клиенту и серверу избежать недопонимания.

Как правило, ваша сетевая игра определяет свои собственные атрибуты (например, ID игрока) на основе любых критериев, которые вы захотите. Если вы хотите найти информацию о игроке/клиенте, отличную от версии игры, вы можете установить эту информацию непосредственно на объекте Клиенте/HostedConnection (см. «Получение информации о клиенте»).

Закрытие клиентов и очистка сервера

Закрытие клиента

Вы должны переопределить метод destroy() клиента, чтобы закрыть соединение, когда игрок клиент покидает сервер:

  @Override
  public void destroy() {
      ... // пользовательский код
      myClient.close();
      super.destroy();
  }

Закрытие сервера

Вы должны переопределить метод destroy() сервера, чтобы закрыть соединение, когда сервер завершает работу:

  @Override
  public void destroy() {
      ... // пользовательский код
      myServer.close();
      super.destroy();
  }

Вызов клиента

Сервер может удалить HostedConnection, чтобы отключить его. Вы должны предоставить в виде String дополнительную информацию (объяснение пользователю, что произошло, например, «Выключение для обслуживания») для отправки сервером. Это информационное сообщение может использоваться (отображается пользователю) с помощью ClientStateListener. (Смотри ниже)

conn.close("We kick cheaters.");

Слушатель уведомлений о подключении

Сервер и клиенты уведомляются о изменениях соединения.

ClientStateListener

com.jme3.network.ClientStateListener уведомляет Клиента, когда Клиент полностью подключился к серверу (включая любое внутреннее подтверждение связи), а также когда Клиент удаляется (отключается) от сервера.

Когда ClientStateListener, получает сетевое исключение, он применяет действие закрытия по умолчанию. Это просто останавливает клиента, и вам придется описать для этого случая, что ваше приложение должно делать в этой ситуации. Если вам нужно больше контроля, над ситуацией когда происходит сетевое исключение и клиент закрывается, вам может потребоваться исследовать ErrorListener.
Методы интерфейса ClientStateListener  Назначение
public void clientConnected(Client c){}  Реализуйте здесь, то что произойдет, как только этот клиент полностью подключится к серверу.
public void clientDisconnected(Client c, DisconnectInfo info){}  Реализуйте здесь, то что произойдет после того, как сервер удалит этого клиента. Например, отобразите информацию об отключении пользователю.

Сначала реализуйте интерфейс ClientStateListener в классе Client. Затем зарегистрируйте myClient в MyGameClient метода simpleInitApp() :

myClient.addClientStateListener(this);

ConnectionListener

com.jme3.network.ConnectionListener уведомляет Сервер, когда новые HostedConnection (клиенты) приходят и уходят. Слушатель уведомляет сервер после того, как клиентское соединение полностью установлено (включая любое внутреннее подтверждение).

Методы интерфейса ConnectionListener  Назначение
public void connectionAdded(Server s, HostedConnection c){}  Реализуйте здесь, то что произойдет после присоединения нового хост-соединения к серверу.
public void connectionRemoved(Server s, HostedConnection c){}  Реализуйте здесь, то что произойдет после того, как HostedConnection ушел. Например игрок вышел из игры, и сервер удаляет его персонажа.

Сначала реализуем интерфейс ConnectionListener в классе Server. Затем зарегистрируйте myServer в MyGameServer метода simpleInitApp().

myServer.addConnectionListener(this);

ErrorListener

com.jme3.network.ErrorListener — это слушатель, реагирует когда происходит сетевое исключение. Этот слушатель построен таким образом, что вы можете переопределить действия по умолчанию, когда происходит сетевое исключение.

Если вы собираетесь использовать сетевую механику по умолчанию, не используйте его! Если вы переопределили его, то убедитесь, что вы добавили механику, которая может закрыть клиента, иначе ваш клиент застрянет в открытом состоянии и вызовет ошибки.
Методы интерфейса ErrorListener  Назначение
public void handleError(Client c, Throwable t){}  Реализуйте здесь, то что происходит после того, как влияние на сеть породило исключение.
Этот интерфейс был создан для клиента и сервера, но код никогда не помещался на сервер для обработки этого слушателя.

Сначала реализуем интерфейс ErrorListener в классе клиента. Затем вам нужно зарегистрировать myClient в MyGameClients метода simpleInitApp().

myClient.addErrorListener(this);

В классе, который реализует ErrorListener, будет добавлен метод вызова handleError(Client s, Throwable t). Внутри этого метода, вы начинаете работу над ошибкой полученной от слушателя. Для этого вам понадобится такой код.

if(t instanceof exception) {
     //Добавьте свой код сюда
}

Заменяйте часть с exception в инструкции if для нужного вам типа исключения, которое вы хотите обработать.

UDP и TCP

SpiderMonkey поддерживает как UDP (ненадежный, быстрый), так и TCP (надежный, медленный) перенос сообщений.

message1.setReliable(true); // TCP
message2.setReliable(false); // UDP
  • Выберите надежный и медленный транспорт для сообщений, если вы хотите удостовериться, что сообщение будет доставлено (доставлено повторно), если оно уже было потеряно, и если порядок серий сообщений является релевантным. Например игровые действия, такие как «1. Владеть оружием, 2. атаковать, 3. уклоняться.
  • Выберите ненадежную и быструю транспортировку сообщений, если следующее сообщение заставляет любое ранее отложенное или потерянное сообщение устаревать и снова синхронизирует состояние. Например. Ряд новых мест во время ходьбы.

Важно: используйте многопоточность

Вы не можете изменять граф сцены непосредственно из сетевого потока. Общим примером таких изменений является синхронизация позиции игрока в сцене. Вы должны использовать Многопоточный режим Java.

Многопоточность означает, что вы создаете Callable. Callable — это Java-класс, представляющий любую (возможно, требующую много времени) автономную задачу, которая влияет на граф сцены (например, изменение положение игрока). Вы завершаете Callable в Executor потока OpenGL клиента. Callable обеспечивает выполнение игровых изменений в синхронизации с циклом обновления.

app.enqueue(callable);

Подробнее об использовании многопоточности в jME3 читайте здесь.

Общие советы см. В статьях MultiPlayer Networking и Latency Compensating Methods in Client/Server In-game Protocol Design and Optimization от сообщества Valve Developer.

Исправление проблем

Если вы настроили сервер в своей домашней сети, а игровые клиенты не могут подключится к серверу извне. Значит пришло время вам узнать о переадресации портов.


Переведено для jmonkeyengine.ru, оригинал
Автор перевода: Andry

Добавить комментарий

jMonkeyEngine.ru © 2017. Все права сохранены.