Pull to refresh

Пьеса «Разработка многопользовательской сетевой игры.» Часть 2: Это страшное слово «протокол»

Game development *
Recovery mode


Часть 1: Архитектура
Часть 3: Клиент-серверное взаимодействие
Часть 4: Переходим в 3D

Итак, продолжим создание многопользовательской игры.
Сегодня мы рассмотрим создание протокола передачи данных.
А также создадим заготовки TCP сервера и соответственно клиента.



Часть вторая. Действие первое: Протокол передачи данных.



На всякий случай напомню особо занудливым читателям, что мы не разрабатываем quake, SC или что-то подобное. Основная задача показать общие принципы и подходы на примере работающего приложения.

С виду это звучит очень страшно и сложно «создание протокола передачи данных», но не так страшен черт как его малюют…

Что в общем случае понимают под протоколом в геймдеве? Нет, это не своя реализация велосипед сокетов… и не свой http протокол… В общем случае это способ упаковки данных для передачи между сервером и клиентом. Например у вас есть объект Data в котором хранится информация о положении вашего танка на карте. Как передать эту информацию серверу? Есть вариант стандартной сериализации. Многие языки программирования позволяют выполнить процесс сериализации/десериализации очень просто. Буквально парой строк кода. И это будет работать… Но как всегда есть подводные камни. Например скорость сериализации/десериализации, размер полученного объекта… и если для клиента этой скоростью еще можно пренебречь, то серверу придется туго, ему ведь придется парсить сообщенияот каждого клиента… да и лишняя нагрузка на сеть никому не нужна. Есть еще один момент, это кроссплатформенность. Вот в нашем случае сервер на scala, а клиент на flash. Так просто напрямую не получится у них обмениваться объектами.

Поэтому есть разные рализации этого процесса. Глобально их можно разделить на два типа: бинарные и текстовые. Текстовые это например XML и JSON. С ними очень просто работать. XML наверно самый популярный вариант. Но для реалтаймовых игр они не подходят. Высокие накладные расходы на разбор XML и очень большой объем передаваемых данных (в хml очень много места занимает разметка) фактически ставят крест на его применении. JSON более легковесный, разметка занимает гораздо меньше места чем XML, но все равно с бинарными протоколами ему не сравниться. Хотя для пошаговых игр они вполне подходят.

Что касается бинарных протоколов, то в них нет разметки как в XML или JSON, за счет чего существенно сокращается объем передаваемых данных и время на разбор сообщения. Примерами бинарных протоколов являются AMF, protobuf. Protobuf вообще очень хорош. Его разработал и использует в своих сервисах Google. У него есть реализация на многих языках. И в общем случае я бы советовал использовать его или аналоги.

Но мы же здесь учимся и у нас относительно простой проект. Поэтому мы будем делать свой удобный велосипедик.

Давайте представим, что нам вообще требуется передавать между сервером и клиентом?

Сообщения от клиента серверу
1. Авторизация (логин)
2. Координаты танка при движении (х, у)
3. Координаты выстрела (х, у) место в котором игрок нажал огонь, чтобы рассчитать на сервере было ли попадание
4. Команда запроса количества игроков на сервере
5. Команда Выхода из игры

Сообщения от сервера к клиенту
1. Подтверждение авторизация
2. Координаты танка противника и его состояние (живой/убит), (x,y,s)
3. Команда «в нас попали»
4. Количество игроков
5. Отключение клиента по запросу

Подводя итог получаем, что можно все передаваемые данные уместить в 7 байт.



Это основной передаваемый пакет с координатами танка при движении и при выстреле. В случае когда нам надо передать другие данные, мы будем размещать их в этих 7 байтах. Например количество активных пользователей будем передавать вместо координат X,Y.

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

В дальнейшем мы усовершенствуем наш протокол. Но это будет в следующих частях.

Часть вторая. Действие второе: TCP сервер.



В данный момент нам надо создать заготовку сервера. Задачи следующие:
1. Подключение клиентов
2. Присваивание подключенному клиенту ID

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

 def main(args: Array[String]): Unit =
 {
  val port = 7777
  
  try
  {
   val listener = new ServerSocket(port)
   var numClients = 1
   
   println("Listening on port " + port)
   
   while (true)
   {
    new ClientHandler(listener.accept(), numClients).start()
    numClients += 1
   }
   
   listener.close()
  }
  catch
  {
   case e: IOException =>
    System.err.println( "Could not listen on port: " + port + "." )
    System.exit(-1)
  }
 }


* This source code was highlighted with Source Code Highlighter.


и обработчик соединения.

class ClientHandler (socket : Socket, clientId : Int) extends Actor
{
 def act
 {
  try
  {
   val out = new PrintWriter( socket.getOutputStream(), true )
   val in = new BufferedReader( new InputStreamReader(socket.getInputStream()) )

   print( "Client connected from " + socket.getInetAddress() + ":" + socket.getPort )
   println( " assigning id " + clientId)

   var inputLine = in.readLine()
   while (inputLine != null)
   { 
    println(clientId + ") " + inputLine)

    inputLine = in.readLine()
   }

   socket.close()

   println("Client " + clientId + " exit")
  }
  catch
  {
   case e: SocketException => System.err.println(e)

   case e: IOException => System.err.println(e.getMessage)

   case e => System.err.println("Unknown error " + e)
  }
 }
}


* This source code was highlighted with Source Code Highlighter.


Код не является истиной в последней инстанции. Буду стараться писать его макимально понятным для всех, без оптимизаций.Поэтому сильно не пинайте, а если есть конструктивная критика — как говорится добро пожаловать в каменты.

Надеюсь код пояснять ненадо? Он очень простой.

Это всего лишь заготовка, поэтому такие вещи как номер порта за харткожены. В следующей части проведем рефакторинг, причешем наш сервер.

Часть вторая. Действие третье: Клиент.



Для реализации и проверки протокола нам понадобится так же заготовка клиента.
Задачи требуемые на данном этапе очень простые:
1. Подключится к серверу.
2. Отобразить свое состояние.
И все… больше сейчас нам ничего не надо.

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

  public class Main extends Sprite
  {
    public var socket:Socket = new Socket();
    
    public var host:String   = "127.0.0.1";
    public var port:int     = 7777;
    
    public var status:TextField = new TextField();
    
    public function Main():void
    {
      if (stage) init();
      else addEventListener(Event.ADDED_TO_STAGE, init);
    }

    private function init(e:Event = null):void
    {
      removeEventListener(Event.ADDED_TO_STAGE, init);
      // entry point
      
      status.text = "Player";
      addChild( status );
      
      socket.addEventListener(Event.CONNECT, socketConnectHandler);
      socket.addEventListener(IOErrorEvent.IO_ERROR, ioErrorHandler);
      socket.addEventListener(DataEvent.DATA, dataHandler);  
      
      socket.connect(host, port);
    }
    
    
    //Подключение серверу
    private function socketConnectHandler(e:Event):void
    {
      status.text = "Player - connectrd";
    }
    
    //Ошибка при подключении
    private function ioErrorHandler(e:IOErrorEvent):void
    {
      status.text = "Player - error";
    }
    
    //Пришли данные от сервера
    private function dataHandler(e:DataEvent):void
    {
      switch (e.data)
      {
        // Здесь будем обрабатывать данные
      }
    }
    
    //Отправляем сообщение
    public function sendMessage(val:String):void {
      if (val != "" && socket.connected)
      {
        //socket.writeBytes( val);
      }
    }
    
    public function exitGame():void
    {
      if (socket.connected)
      {
        socket.close();
      }
    }    

  }


* This source code was highlighted with Source Code Highlighter.


Итак все готово, чтобы произвести первый коннект между сервером и клиентом.
Запускаем сервер, запускаем клиент… и вуаля…

Результат на клиенте



Мы видим, что клиент благополучно подключился к серверу.

Результат на сервере



Мы видим, что сервер запустился на порту 7777, принял коннект от клиента и выдал ему ID.

На сегодня все.
В следующей части мы реализуем протокол, и проверим на деле получившееся клиент серверное взаимодействие.

Как всегда все исходники можно посмотреть на Github

upd. Комментируя учитывайте пожалуйста, что это не туториал «как написать свой квейк за 5 минут»… это последовательное, от простого к сложному, изложение разработки сетевой игры. Изначальная задача — это сделать сетевое взаимодействие между Scala сервером и Flash клиентом. И потом на основе этого взаимодействия сделать игру.
Tags:
Hubs:
Total votes 88: ↑77 and ↓11 +66
Views 18K
Comments Comments 69