Player.IO. Пример пошаговой игры

Мы разобрали много теории и теперь настало время рассмотреть реальный пример пошаговой игры.

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

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

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

Уже готовый пример игры вы можете потестировать здесь...

Клиент игры

Первым делом, если вы еще этого не сделали, скачайте SDK Player.IO. Более подробно структуру папок этого SDK мы рассматривали во введении. ?з всего SDK нам нужна только папка «NewGame». Копируем папку «NewGame» куда-нибудь в удобноее место и удаляем все содержимое вложенной папки «Flash», кроме папки «playerio». Теперь нам в папке «Flash» нужно создать свой проект. Открываем Flash IDE, создаем новый файл «Flash File (Action Script 3)», оставляем его пустым и сразу сохраняем в папку «Flash» под именем «TurnBasedGame.fla». После этого создаем еще один новый файл «Action Script File» и сохраняем его в папку «Flash» под именем «TurnBasedGame.as». Эти два файла будут всем нашим игровым проектом.

Теперь напишем код класса TurnBasedGame.as:

package
{
  import flash.display.MovieClip;
    
  public class TurnBasedGame extends MovieClip
  {
        
    public function TurnBasedGame():void
    {
      trace("Hello World!");
    }
        
  }

}

Этот код — база нашего основного игрового класса. Далее в Document Class вписываем имя этого класса и выполняем компиляцию. Если все сделано правильно, то в окне Output появится строка «Hello World!». Теперь можно приступать к написанию непосредственно игрового кода.

Первое что нам нужно сделать — это выполнить подключение к игровому серверу. Для реализации подключения нам понадобится несколько переменных:

public var gameID:String = "id игры";
public var connection:Connection;
private var _isConnected:Boolean;

Первая переменная — это ID нашей игры. ID игры необходим, чтобы сервера Player.IO могли идентифицировать вашу игру и связать её с вашим профилем, и соответсвенно с серверным кодом. Вторая переменная — это указатель на экземпляр подключения, через него мы сможем отправлять и получать сообщения. Ну и третья переменная — это флаг, определяющий - произведено подключение к серверу или нет.

Чтобы получить ID игры, нужно зарегистрироваться на www.playerio.com и создать игру в панели управления.

Поскольку для инициализации Player.IO API необходим указатель на stage, мы должны быть уверенны, что экземпляр класса TurnBasedGame добавлен в струкруту Flash и имеет не нулевой указатель на stage. Для этого в конструкторе класса реализуем стандартную схему инициализации:

if (stage == null)
{
  addEventListener(Event.ADDED_TO_STAGE, init);
}
else
{
  init(null);
}

Далее напишем метод init():

private function init(event:Event):void
{
  if (event != null)
  {
    removeEventListener(Event.ADDED_TO_STAGE, init);
  }
  
  // Форматирование текста для текстовых меток
  var tf:TextFormat = new TextFormat("Tahoma", 12, 0x000000);
  
  // Создание отладочной текстовой метки.
  _debugLabel = new TextField();
  _debugLabel.autoSize = TextFieldAutoSize.LEFT;
  _debugLabel.defaultTextFormat = tf;
  addChild(_debugLabel);

  // Флаг, определяющий - удалось подключиться или нет
  _isConnected = false;

  // Подключаемся к серверу
  _debugLabel.text = "Подключаюсь к серверу…";
  PlayerIO.connect(stage, gameID, "public", "User" + Math.floor(Math.random()*1000).toString(), "", null, connectHandler, errorHandler);
}

Здесь нет ничего особо нового и сложного. В данном методе мы создаем отладочную текстовую метку, чтобы отображать ход подключения для игрока, и запускаем подключение к серверу. Теперь, прежде чем протестировать подключение, нам нужно написать методы connectHandler() и errorHandler(). Начем с последнего:

private function errorHandler(error:PlayerIOError):void
{
  _debugLabel.text = "> " + error;
}

Здесь мы пока выводим сообщение в ранее созданную текстовую метку. Кстати, если вы еще этого не сделали, то добавьте в переменные класса новую переменную, в которой мы будем хранить указатель на отладочную текстовую метку:

private var _debugLabel:TextField;

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

private function connectHandler(client:Client):void
{
  _debugLabel.text = "Подключаюсь к игре...";

  // Устанавливаем работу с локальным сервером для отладки.
  client.multiplayer.developmentServer = "localhost:8184";

  // Создаем или подключаемся к игровой комнате "test".
    client.multiplayer.createJoinRoom("test", "MyGame", true, {}, {}, joinHandler, errorHandler);
}

Тут у нас запускается очередное подключение к игровой комнате или её создание, если она еще не создана. Метод обработчик для обработки ошибки подключения к комнате у нас остается тот же самый errorHandler(), а вот обработчик успешного присоединения новый joinHandler():

private function joinHandler(connection:Connection):void
{
  // Сохраняем указатель на подключение.
  this.connection = connection;
            
  // Подписываемся на все сообщения от сервера.
  this.connection.addMessageHandler("*", messageHandler);
            
  // Подключение завершено успешно!
  _debugLabel.text = "Подключились!";
  _isConnected = true;
}

Здесь мы сохраняем указатель на успешное подключение, потом подписываемся на все сообщения от сервера, показываем игроку, что все хорошо и устанавливаем флаг о том, что подключение успешно произведено. Обратите внимание, что тут у нас появился еще один метод обработчик messageHandler(), давайте теперь реализуем его:

private function messageHandler(message:Message):void
{
  _debugLabel.text = message.type;
}

Пока в методе обработчике сообщений мы просто их будем выводить в текстовую метку.

Сейчас хотелось бы дать добро, нажать кнопку пуск (ctrl+Enter), но к запуску еще не все готово. Убедитесь, что вы подключили все пакеты которые мы используем в классе:

import flash.events.Event;
import playerio.*;
import flash.text.TextField;
import flash.events.MouseEvent;
import flash.text.TextFormat;
import flash.text.TextFieldAutoSize;

Впрочем и этого для запуска еще не достаточно. Конечно кнопку пуск (ctrl+enter) можно нажать, но сервер у нас еще не готов, поэтому в отладочной метке вы увидете только сообщение об ошибке.

Сервер игры

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

Открываем файл «Serverside Code.VS2010.sln» и переходим в инспекторе проекта к редактированию «Game.sc» — здесь будет основной код нашего сервера, который будет производить обработку игровой логики. Поскольку мы взяли стандартный шаблон сервера, то скорее всего в этом классе может быть лишний код, давайте подчистим все лишнее и убедимся, что он выглядит следущим образом:

using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using PlayerIO.GameLibrary;
using System.Drawing;

namespace MyGame
{
  public class Player : BasePlayer
  {
    public string Name;
  }

  [RoomType("MyGame")]
  public class GameCode : Game
  {

    public override void GameStarted()
    {
      //
    }

    public override void GameClosed()
    {
      //
    }

    public override void UserJoined(Player player)
    {
      // 
    }

     public override void UserLeft(Player player)
     {
       //
     }

     public override void GotMessage(Player player, Message message)
     {
       Console.WriteLine(message.Type);
     }
  }
}

Так должен выглядеть чистый код игры на сервере, прежде чем мы начнем писать свой код. Сейчас можно запустить компиляцию сервера. ? если при удалении лишнего кода вы не наделали ошибок и все пройдет успешно, то перед вами появится окно авторизации на сервере Player.IO, где нужно указать ваш логин и пароль. Рекомендую так же поставить галочку «Remember me», чтобы окно авторизации не появлялось каждый раз при запуске сервера. После успешной авторизации появится окно консоли сервера, в котором появится сообщение о том, что сервер ожидает подключение игроков, а так же будет выведена информация о том, какая в текущий момент загружена *.dll.

Обратите особое внимание на то, какая загружена библиотека. Если ранее вы уже компилировали примеры сервера, то вероятнее всего автоматически будет загружена библиотека от предыдущего примера. Чтобы загрузить правильную (от нового проекта) библиотеку — просто кликните в окне консоли на имя библиотеки возле логотипа Player.IO, и в открывшемся окне выберите свою, которая находится в папке проекта: «../Serverside Code/Game Code/bin/Debug/newGame.dll». После смены библиотеки сервер перезапускать не нужно.

Сервер готов, и теперь, наконец можно выполнить тестовый запуск флешки с подключением! Если все сделано правильно, то мы должны увидеть во флеш клиенте надпись «Подключились!», а в окне консоли сервера мы увидим новую вкладку, которая отображает консоль созданной клиентом команты и некоторую статистику по подключению. Ну что ж, связка клиент-консоль настроена и теперь можно приступить непосредственно к реализации игрового процесса.

?гровой процесс

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

Первым делом давайте создадим клип, в котором нарисуем игровое поле размером 300x300 пикселей, расчерченное на клетки размером 30x30 пикселей, таким образом мы должны получить сетку из 10x10 ячеек. После того, как сетка нарисована, экспортируем её в код, как «Grid_mc». Данный шаг — это единственная творческая задача в данном уроке.

Теперь перейдем к написанию кода. Объявляем еще несколько необходимых нам переменных в основном классе программы:

// Цвет фишки игрока
private var _color:uint;
// Определяет может ли сделать игрок ход
private var _canMove:Boolean;
// Данные об игровом поле
private var _grid:Array;
// Графическое представление игрового поля
private var _background:Sprite;
// Клип, в который будем рисовать фишки
private var _display:Sprite;
// Текстовое поле для вывода информации о текущем состоянии игры
private var _actionLabel:TextField;

Переходим к методу init() и инициализируем все объявленные переменные, добавив следующий код:

// Создание метки, в которой будет демонстрироваться текущее действие игры.
_actionLabel = new TextField();
_actionLabel.autoSize = TextFieldAutoSize.LEFT;
_actionLabel.x = 200;
_actionLabel.y = 25;
_actionLabel.defaultTextFormat = tf;
_actionLabel.text = "Поставьте свою фишку на поле.";
addChild(_actionLabel);
            
// Клип с сеткой игрового поля.
_background = new Grid_mc();
_background.x = 200;
_background.y = 50;
addChild(_background);
            
// Клип, в который будут отрисовываться игровые фишки.
_display = new Sprite();
_display.x = _background.x;
_display.y = _background.y;
addChild(_display);
            
// Наш цвет.
_color = 0;
            
// Можем ли мы сделать ход.
_canMove = true;
            
// Создание игрового поля.
_grid = new Array(10);
for (var i:int = 0; i < 10; i++)
{
  _grid[i] = new Array(10);
  for (var j:int = 0; j < 10; j++)
  {
    _grid[i][j] = -1;
  }
}

Обратите внимание на то, что инициализацию всех новых переменных следует сделать перед тем, как мы вызовем подключение к серверу Player.IO. Сейчас можно протестировать то, что у нас получилось. Если все сделано правильно, то мы должны увидеть игровое поле и текстовую надпись о том, что можно сделать ход, но пока ничего не работает кроме подключения к серверу.

Прежде чем игрок сможет начать играть, он должен получить от сервера свой цвет и текущее состояние игры, поэтому сейчас перейдем к серверному коду. Переключаемся в Visual C# и первым делом объявляем две переменные для текущего игрока. Перейдите к описанию класса Player и добавьте объявление двух переменных:

// Цвет игрока
public int color = -1;
// Определяет сделал ли игрок свой ход
public bool placed = false;

Далее переходим к методу UserJoined() и пишем следующий код:

int color = (new Random()).Next(0xFFFFFF);
player.Send("assign", color);
player.color = color;

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

Переходим к методу messageHandler() в коде клиента и все его содержимое заменяем следующим кодом:

switch (message.type)
{
  // Получили сообщение с цветом игрока.
  case "assign" :
    _color = message.getInt(0);
  break;
}

Цвет нам известен, теперь его нужно показать игроку. Чтобы это сделать, мы просто нарисуем круг, закрашенный полученным от сервера цветом. По мимо цвета игрока, нам так же нужно отрисовывать все игровые фишки и для этого мы реализуем общий метод, который будет перерисовывать все игровое поле и текущий цвет игрока соотвественно, назовем его drawEverything():

private function drawEverything():void
{
  _display.graphics.clear();

  // Отрисовка цвета игрока.
  _display.graphics.lineStyle(1);
  _display.graphics.beginFill(_color);
  _display.graphics.drawCircle(0 - 100, 50, 50);
}

Пока мы рисуем только один круг, а отрисовку игровых фишек реализуем чуть позже. Теперь добавьте вызов этого метода в методе messageHandler() сразу после switch конструкции. Сейчас можно протестировать работу клиента и сервера, чтобы убедиться, что все работает правильно. Запускаем сервер, а потом клиент. Если все сделано правильно, то сразу после появления фразы «Подключились!» мы должны увидеть круг, закрашенный тем цветом, который нам определил сервер.

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

Возвращаемся в Visual C# и реализуем инициализацию игрового поля. Для этого нам нужно объявить массив, в котором будет храниться информация об игровом поле, в классе GameCode добавляем следующую строку:

int[,] grid = new int[10, 10];

Здесь у нас получается двумерный массив типа int, который сразу же создаем в количестве 10x10 ячеек. Далее нам нужно быть уверенными, что массив заполнен нужными нам значениями, для этого создадим новый метод clearGrid():

private void clearGrid()
{
  for (int i = 0; i < 10; i++)
  {
    for (int j = 0; j < 10; j++)
    {
      grid[i, j] = -1;
    }
  }
}

Данный код почти 1 в 1 как на as3, поэтому подробно его объяснять нет смысла. Заполнение массива отрицательными значениями мы будем вызывать при создании игровой комнаты и пре рестарте игры после того, как все поле будет заполнено фишками игроков. Чтобы выполнить очищение массива при запуске игры, нам следует добавить вызов метода clearGrid() в методе GameStarted():

public override void GameStarted()
{
  clearGrid();
}

?гровое поле создано и очищено, осталось только передать его  подключившемуся игроку. Для этого переходим в метод UserJoined() и добавляем следующий код сразу после того, как отправили информацию о цвете игрока:

// Создаем новое сообщение
Message gridSend = Message.Create("grid");

// Перебираем двумерный массив игрового поля и добавляем его значения в сообщение
for (int i = 0; i < 10; i++)
{
  for (int j = 0; j < 10; j++)
  {
    gridSend.Add(grid[i, j]);
  }
}

// Отправляем игроку сообщение
player.Send(gridSend);

В данном коде мы создаем новое сообщение, но в отличии от сообщения с цветом — не отправляем его сразу же. Мы сохраняем указатель на него в локальной переменной, после чего перебираем наше игровое поле и добавляем все значения в сообщение по отдельности. Только после того, как все поле было записано в сообщение, мы отправляем его методом Send(). Далее нам следует реализовать обработку этого сообщения на клиенте, возвращаемся во Flash и переходим к методу messageHandler(), где в switch добавим новый case:

case "grid" :
  for (var i:int = 0; i < 10; i++)
  {
    for (var j:int = 0; j < 10; j++)
    {
      _grid[i][j] = message.getInt(i * 10 + j);
    }
  }
break;

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

Настало время реализации хода игрока. Чтобы игрок сделал свой ход — он должен кликнуть по полю, а наша задача определить в какую именно клетку кликнул игрок и отправить об этом информацию на сервер. Таким образом первым делом нам следует сделать обработчик клика мышки, в котором мы будем отправлять сообщение с информацией о том, в какую клетку кликнул пользователь (поставил фишку). Для этого переходим в метод init() игрового клиента и добавляем там обработчик мыши:

stage.addEventListener(MouseEvent.CLICK, mouseClickHandler);

После чего создаем метод mouseClickHandler():

private function mouseClickHandler(event:MouseEvent):void
{
  // Первым делом проверяем подключение и разрешено ли игроку делать ход
  if (!_isConnected || !_canMove)
  {
    return;
  }
    
  // Переводим координаты мыши из экранных в игровое поле
  var mx:int = stage.mouseX - _background.x;
  var my:int = stage.mouseY - _background.y;
            
    // Мы должны быть уверены, что мышка находится где-то над игровым полем.
  if (mx >= 0 && my >= 0 && mx <= _background.width && my <= _background.height)
  {
    // Отправляем на сервер команду о том, по какой клетке игрок кликнул.
    connection.send("place", Math.floor(mx / 30), Math.floor(my / 30));
                    
    // Отмечаем, что игрок уже сделал свой ход.
    _canMove = false;
                    
    // Выводим информацию для игрока.
    _actionLabel.text = "Ход принят, ждем других игроков.";
  }
}

Таким не хитрым способом мы отправляем на сервер информацию о том, куда кликнул игрок. Но сервер пока у нас не обрабатывает сообщение «place», и прежде чем мы его научим его обрабатывать, давайте сделаем еще обработку сообщения «place» на клиенте — ведь точно таким же сообщением сервер будет уведомлять всех подключенных игроков о сделанном ходе. Переходим к методу messageHandler() и добавляем в switch новый case:

case "place" :
  _grid[message.getInt(0)][message.getInt(1)] = message.getInt(2);
break;

Здесь мы получаем сообщение, количество данных которого на одно больше — отправляли только координаты клетки, а получили в итоге еще и цвет. ?нформацию о цвете на сервер мы не стали отправлять лишь потому, что серверу известен цвет игрока, который кликнул, а вот другим игрокам он не известен. Теперь бы нам написать код сервера, который обрабатывает ходы игроков, но прежде мы сделаем отрисовку игрового поля на клиенте. Переходим в метод drawEverything() и дописываем следующий код:

for(var i:int = 0; i < 10; i++)
{
  for(var j:int = 0; j < 10; j++)
  {
    if(_grid[i][j] != -1)
    {
      _display.graphics.beginFill(_grid[i][j]);
      _display.graphics.drawCircle(15 + 30 * i, 15 + 30 * j, 10);
    }
  }
}

Далее переходим к Visual C# и реализуем обработку и отправку сообщения «place». Не забываем, что сервер должен обрабатывать сообщения от игроков только в том случае, если игра еще не завершена, для проверки завершения игры нам понадобится логическая переменная, которую можно объявить сразу после объявления игрового поля:

bool gameOver = false;

После чего переходим к методу GotMessage(), удаляем старый код и пишем следующее:

if (gameOver)
{
  return;
}

switch (message.Type)
{
  case "place":
    if (!player.placed && message.GetInt(0) >= 0 &&
       message.GetInt(0) <= 9 && message.GetInt(1) >= 0 && message.GetInt(1) <= 9)
    {
      // Отмечаем фишку на игровом поле
      grid[message.GetInt(0), message.GetInt(1)] = player.color;
      // Отмечаем, что игрок сделал ход
      player.placed = true;
      // Отправляем всем информацию о ходе
      Broadcast("place", message.GetInt(0), message.GetInt(1), player.color);
    }
  break;
}

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

Сейчас можно в очередной раз запустить сервер и клиент, и проверить обработку действий игрока. Правда после первого хода игра пока останавливается, с фразой «Ход принят. Ждем хода других игроков». Наш северный код пока еще не умеет отслеживать всех игроков и переходить к следующему ходу, когда все игроки выполнили свои действия. Но уже сейчас вы можете проверить как игра работает с несколькими игроками. Для этого достаточно открыть как минимум два экземпляра флеш клиента и сделать ходы с разных клиентов — как будто вы играете с кем-то еще.

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

Переходим к Visual C# и добавляем новый метод checkPlacements():

private void checkPlacements()
{
  foreach (Player guy in Players)
  {
    if (!guy.placed)
    {
      // Выходим - кто-то еще не поставил фишку.
      return;
    }
  }

  // Переходим к следущему ходу.
  doTurn();
  Broadcast("nextTurn");

  // Сбрасываем флаги для всех игроков о том, что они сделали ходы
  foreach (Player guy in Players)
  {
    guy.placed = false;
  }

  // Проверяем, есть ли на поле еще свободные игровые клетки
  for (int i = 0; i < 10; i++)
  {
    for (int j = 0; j < 10; j++)
    {
      if (grid[i, j] == -1)
      {
        // Если на поле есть хотя бы одна свободная клетка, то прерываем выполение метода
        return;
      }
    }
  }

  // Если не одной свободной клетки не оказалось и выполнение метода не прервалось, то конец игры
  gameOver = true;

  // Устанавливаем вызов метода refresh через 5 сек для перезапуска игры
  ScheduleCallback(refresh, 5000);
}

Первым делом в этом методе мы перебираем всех игроков в комнате и проверяем значение их переменных placed — если кто-то из игроков еще не поставил фишку, то прерываем выполнение этого метода. ?наче мы вызываем метод doTurn() и отправляем сообщение всем игрокам о новом ходе. Метод doTurn() будет размножать все игровые фишки на поле. После перехода к новому ходу мы сбрасываем для всех игроков флаги о том, что они выполнили ход. А потом перебираем все игровое поле и определяем остались ли еще свободные клетки. Если свободные клетки еще есть, то выполнение метода прерывается. А если клеток свободных больше нет, то мы ставим флаг, что игра завершена и устанавливаем таймер, который вызовет через 5 секунд метод refresh() — который в свою очередь перезапустит игровой процесс.

Метод ScheduleCallback() в C# — это фактически тоже самое, что метод setInterval() во Flash.

Давайте добавим вызов метода checkPlacements() в методе GotMessage() сразу после рассылки сообщения «place» и напишем метод doTurn():

private void doTurn()
{
  // Копируем игровое поле во временный массив, чтобы корректно размножить фишки
  int[,] oldGrid = (int[,])grid.Clone();

  // Перебираем игровое поле и размножаем игровые фишки
  for (int i = 0; i < 10; i++)
  {
    for (int j = 0; j < 10; j++)
    {
      if (oldGrid[i, j] != -1)
      {
        if (i != 0 && grid[i - 1, j] == -1)
        {
          grid[i - 1, j] = grid[i, j];
        }
        if (j != 0 && grid[i, j - 1] == -1)
        {
          grid[i, j - 1] = grid[i, j];
        }
        if (j != 9 && grid[i, j + 1] == -1)
        {
          grid[i, j + 1] = grid[i, j];
        }
        if (i != 9 && grid[i + 1, j] == -1)
        {
          grid[i + 1, j] = grid[i, j];
        }
      }
    }
  }
}

Данный метод лишь выполняет «размножение» установленных игроками фишек в соседние свободные клетки. Теперь напишем метод перезапуска игры refresh():

private void refresh()
{
  clearGrid();
  Broadcast("newGame");
  gameOver = false;
}

Тут все просто: очищаем поле, отправляем всем сообщение о начале новой игры, сбрасываем флаг о завершении игры, чтобы вновь обрабатывать сообщения от клиентов. На этом мы завершаем работу с серверным кодом и возвращаемся во Flash к клиенту, чтобы научить его реагировать на новые сообщения «nextTurn» и «newGame». Давайте напишем в клиенте новый метод nextTurn():

private function nextTurn():void
{
  var i:int;
  var j:int;
  var oldGrid:Array = new Array();

  // Копируем игровое поле, необходимо чтобы корректно размножить фишки.
  for(i = 0; i < 10; i++)
  {
    oldGrid[i] = new Array(10);
    for(j = 0; j < 10; j++)
    {
      oldGrid[i][j] = _grid[i][j];
    }
  }
  
  // Размножаем все игровые фишки.
  for(i = 0; i < 10; i++)
  {
    for(j = 0; j < 10; j++)
    {
      if (oldGrid[i][j] != -1)
      {
        if(i != 0 && _grid[i - 1][j] == -1)
        {
          _grid[i - 1][j] = _grid[i][j];
        }
        if(i != 9 && _grid[i + 1][j] == -1)
        {
          _grid[i + 1][j] = _grid[i][j];
        }
        if(j != 0 && _grid[i][j - 1] == -1)
        {
          _grid[i][j-1] = _grid[i][j];
        }
        if(j != 9 && _grid[i][j + 1] == -1)
        {
          _grid[i][j + 1] = _grid[i][j];
        }
      }
    }
  }

  _canMove = true;
  _actionLabel.text = "Поставьте свою фишку на поле.";

  // Проверяем есть ли еще свободные клетки на поле.
  for(i = 0; i < 10; i++)
  {
    for(j = 0; j < 10; j++)
    {
      if(_grid[i][j] == -1)
      {
        // Если свободные клетки есть, прерываем выполнение метода.
        return;
      }
    }
  }

  // Свободных клеток не осталось, завершение игры.
  _actionLabel.text = "?гра закончена. Ожидаем перезапуска...";
  _canMove = false;
}

Как можно видеть, данный метод делает фактически все тоже самое, что мы уже реализовали на сервере. Первым делом мы размножаем все игровые фишки на поле. Обратите внимание — нам не нужно отправлять информацию с сервера о новом состоянии игрового поля, так как мы можем это легко и непринужденно рассчитать на клиенте, сэкономив при этом траффик. После того, как фишки на клиенте размножены, проверяем не закончилась ли игра, если места на поле еще есть, то прерываем выполнение метода. А иначе информируем игрока о том, что игра закончена и ожидаем пока сервер не пришлет сообщение «newGame».

Теперь вновь перейдем к методу messageHandler() и добавим новые case для новых сообщений:

case "nextTurn" :
  nextTurn();
break;
                
case "newGame" :
  clearGrid();
  _canMove = true;
  _actionLabel.text = "Поставьте свою фишку на поле.";
break;

При получении сообщения «nextTurn» мы просто вызываем метод nextTurn(), а при получении сообщения «newGame» мы очищаем игровое поле и разрешаем игроку сделать ход.

Все, игра готова! Запускаем сервер, запускаем пару клиентов и можем насладиться хоть и примитивным но настоящим многопользовательским игровым процессом.

Поиграйтесь с этим примером, попробуйте доработать игру на свое усмотрение. Например, добавьте отображение количества пользователей в игре. Еще можно добавить условие для победы. Может быть добавить отображение победителя при завершении игры и тому подобные мелочи. Такая практика позволит вам закрепить материал.

 

Заключение

В данной игре мы реализовали возможность моментального подключения к игровому процессу. Хотя чаще всего игры не позволяют подключаться к комнатам, в которых уже идет игровой процесс, так как в этом случае, скорее всего, придется оправлять большой объем информации о текущем состоянии игрового процесса. Но если необходимо реализовать «горячее подключение», то старайтесь отправлять всю необходимую информацию в одном сообщении, так как разбивка информации на отдельные сообщения считается не самым оптимальным решением.

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

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

Во-вторых, хакер может попытаться подделать сообщения в свою пользу. Но сервер работает только с одним видом сообщения «place» и, к сожалению для хакера, сервер проверяет может ли игрок выполнить свой ход и дает ли он точные координаты для своей фишки. ? тут, в худшем случае, хакер может потерять только свой ход.

По ходу разработки игры и появлении нового функционала, становится все сложнее выполнить все 100% мер безопасности, вроде тех, о которых мы поговорили. Но помните, что программная безопасность целиком и полностью в ваших руках!

Это вольный перевод статьи: «Building Flash Multiplayer Games — Example game»

Содержание

  1. Как создать онлайн игру? Анонс
  2. Player.IO. Введение
  3. Player.IO. Основы или первая онлайн игра
  4. Player.IO. Пошаговые игры
  5. Player.IO. Сетевые архитектуры
  6. Player.IO. Безопасность в онлайн играх
  7. Player.IO. Пример пошаговой игры
  8. Player.IO. Онлайн игры в реальном времени
  9. Player.IO. Синхронизация игроков
  10. Player.IO. ?нтерполяция или удивительный мир обмана
  11. Player.IO. Решение проблемы задержек
  12. Player.IO. Советы и рекомендации

 

 

Урааа!!! Долой сон!:)))

Pandorin
31 Октября 2012
— 01:57
#

Pandorin +1

Wily
31 Октября 2012
— 08:26
#

с нетерпением жду про реалтайм

.nodoxi
4 Ноября 2012
— 12:24
#

Спасибо за статью, некоторое время присматривался к PlayerIO, но все же решил пока для освоения архитектуры клиент-серверного взаимодействия написать свой сервер. Несколько вопрососв:
1. Есть ли возможность сериализации классов сообщений? Для примера - в моем велосипеде на Java используется AMF-сериализация, в итоге сообщение на стороне клиента имеет такой вид:
[Bindable]
[RemoteClass(alias="Messages.UnitMessage")]
public class UnitMessage extends CommonMessage
***
public var UnitType:int;
public var Y:int;
public var Time:int;
public var Player:String;
public var SessionID:int;
***
CommonMessage при этом реализует интерфейс IExternalizable, что позволяет при чтении объекта с сокета сразу приводить его к нужном типу. Этот класс содержит только общую информацию(тип сообщения + опционально, версию сервера). На стороне сервера система наследования аналогичная.
2. Возможно ли использование стратегий при обработке типов сообщений? А то простыни из case-ов в первой реализации моего сервера были необъятные :) . Опять-таки, мой велосипед:
public static function Link():void
{
AddLink(ID_NewClient, NEW_CLIENT);
AddLink(ID_ClientExit, CLIENT_EXIT);
AddLink(ID_ConnectionRejected, CONNECTION_REJECTED);
AddLink(ID_UsersList, USERS_LIST);
***
} Метод AddLink записывает в массив строк значение второго аргумента по индексу первого. При получении сообщения сервер обращается к этому массиву по индексу типа сообщения(int) и генерирует событие с типом, полученым из массива. Если индекса нету - вызывается исключение, обработать его можем во внешних классах, исходя из критичности сообщения.
3. Есть ли возможность работы с БД на стороне сервера? Есть ли некий клиентский API специально для работы с БД?
4. В предыдущих статьях встречал упоминания о воможности организации игровых сеансов на Р2Р-архитектуре. Какая роль PlayerIO при этом?

Думаю, на большинство этих вопросов можно ответить, основательно(или не очень), почитав мануалы, но раз уж ты(прости моя фамильярность, но как-то непривычно в ?нтернете, помимо деловых переговоров, общаться, как на светском рауте) в этом всем разобрался - может прояснишь некоторые моменты.

C4
6 Ноября 2012
— 00:23
#

Антон, спасибо большое за статьи. Все очень доступно и детально описано.

P.S. ? предупреждение,о том какая библиотека используюется и как её изменить, продублируй пожалуйста в посте: "Player.IO. Основы или первая онлайн игра". А то я на этом этапе столкнулся с этой проблемой. Может я не один :)

elbik
26 Ноября 2012
— 18:54
#

Player.IO. конечно хорошо, не спорю, но есть ли смысл использовать в качестве серверсайда PHP фреймворк? Не для реалтаймовых, конечно, игр а для пошаговых, там где не важна скорость вычислений? Просто в случае с пхп серверсайд будет проще разрабатывать на мой взгляд, но это чисто субъективное мнение.

хеллоуворлдщик
19 Апреля 2013
— 23:26
#

Да и кстати спасибо за сей великолепный блог. Прописан на хитри и флешере. Ваш блог теперь тоже в основных закладках. Большое спасибо вам за столь актуальный и доходчиво изложенный материал. Я во флеше далеко не заходил но скилл ООП прокачан неплохо (сужу только по ПХП) . Сейчас курю книжку Мука. Еще раз гигантское спасибо возможно и у меня сбудется мечта написать приличный платформер с серьезной физикой.

хеллоуворлдщик
19 Апреля 2013
— 23:46
#

@хеллоуворлдщик, PHP для сетевых пошаговых игр вполне годится. Главное только о хостинге и защите хорошо позаботится. А в остальном, что вам нравится и удобно, то и используйте.

Спасибо за отзыв! :)

Ant.Karlov
20 Апреля 2013
— 10:47
#