Создание сетевой игры в "крестики-нолики" на платформе Android

Создаем сетевую многопользовательскую игру в "крестики-нолики", используя технологии PHP, XML и комплект инструментальных средств для разработки приложений на платформе Android

Создаем серверную часть многопользовательской сетевой игры в "крестики-нолики", использующей клиентское Android-приложение.

Джек Д Херрингтон, главный инженер-программист, Leverage Software Inc.

Джек Д. Херрингтон (Jack D. Herrington) - главный инженер-программист с более чем двадцатилетним опытом работы. Он автор трех книг: "Генерирование кода в действии", "Podcasting Hacks" и "PHP Hacks". Написал более 30 статей. Вы можете связаться с Джеком по адресу jherr@pobox.com.



11.04.2012

Сетевые многопользовательские "крестики-нолики"

Часто используемые сокращения

  • API: Application Programming Interface (прикладной интерфейс программирования)
  • HTTP: Hypertext Transfer Protocol (протокол передачи гипертекста)
  • IP: Internet protocol (Интернет-протокол)
  • SDK: Software Development Kit (комплект инструментальных средств для разработки программного обеспечения)
  • SQL: Structured Query Language (язык структурированных запросов)
  • UI: User Interface (пользовательский интерфейс)
  • XML: Extensible Markup Language (расширяемый язык разметки)

Казуальные компьютерные игры (игры для широкого круга пользователей, в которые играют от случая к случаю) чрезвычайно популярны и очень рентабельны, что легко объяснимо. Далеко не каждый, не зависимо от возраста, интересуются интерактивными "стрелялками" от первого лица против полчищ врагов, которые требуют молниеносной реакции. Иногда интереснее поиграть в игры, в которых есть время на раздумья и выработку стратегии или для выигрыша следует применять кооперативную стратегию.

С точки зрения разработчика казуальные игры намного проще для разработки, чем графические "стрелялки" от первого лица или спортивные игры. Поэтому разработчику или группе разработчиков легче создать новую оригинальную игру.

В данной статье рассматриваются основы создания казуальной сетевой многопользовательской игры в “крестики-нолики”. Игровым сервером является Web-приложение с XML-интерфейсом, использующее MySQL и PHP. Клиентом является стандартное Android-приложение, работающее на Android-телефонах.


Создание серверной части

Серверная часть использует простую базу данных MySQL, имеющую две таблицы. В листинге 1 показана схема базы данных.

Листинг 1. db.sql
DROP TABLE IF EXISTS games;
CREATE TABLE games(
     id INT NOT NULL AUTO_INCREMENT,
     primary key ( id ) );

DROP TABLE IF EXISTS moves;
CREATE TABLE moves(
     id INT NOT NULL AUTO_INCREMENT,
     game INT NOT NULL,
     x INT NOT NULL,
     y INT NOT NULL,
     color INT NOT NULL,
     primary key ( id ) );

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

Вторая таблица moves содержит отдельные ходы игры и состоит из пяти столбцов. Первый столбец – это уникальный идентификатор хода. Второй столбец – идентификатор игры, которой принадлежит ход. Затем идут позиции x и y хода. Эти значения должны находиться в пределах между 0 и 2, поскольку используется игровое поле три на три. Последнее поле – это "цвет" хода, являющийся целым числом, указывающим X (крестик) или O (нолик).

Для создания базы данных прежде всего запустите программу mysqladmin и с помощью команды mysql выполните сценарий db.sql, как показано ниже:

% mysqladmin --user=root --password=foo create ttt
% mysql --user=root --password=foo ttt < db.sql

Данное действие создает новую базу данных ttt, содержащую схему игры в "крестики-нолики".

После создания схемы необходимо создать способ запуска игры. Для этого используется сценарий под названием start.php, приведенный в листинге 2.

Листинг 2. start.php
<?php
header( 'Content-Type:text/xml' );

$dd = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');
$sql = 'INSERT INTO games VALUES ( 0 )';
$sth = $dd->prepare($sql);
$sth->execute( array() );
$qid = $dd->lastInsertId();

$doc = new DOMDocument();
$r = $doc->createElement( "game" );
$r->setAttribute( 'id', $qid );
$doc->appendChild( $r );

print $doc->saveXML();
?>

Сценарий начинается с подключения к базе данных. Затем выполняется выражение INSERT для таблицы games и возвращается сгенерированный идентификатор. После этого создается XML-документ, в тег game добавляется идентификатор и выполняется экспорт XML.

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

$ php start.php
<?xml version="1.0"?>
<game id="1"/>
$

Итак, у нас есть первая игра. Для просмотра списка игр используется сценарий games.php, приведенный в листинге 3.

Листинг 3. games.php
<?php
header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');

$sql = 'SELECT * FROM games';

$q = $dbh->prepare( $sql );
$q->execute( array() );

$doc = new DOMDocument();
$r = $doc->createElement( "games" );
$doc->appendChild( $r );

foreach ( $q->fetchAll() as $row) {
  $e = $doc->createElement( "game" );
  $e->setAttribute( 'id', $row['id'] );
  $r->appendChild( $e );
}

print $doc->saveXML();
?>

Этот сценарий, как и сценарий start.php, начинается с подключения к базе данных. Затем запрашивается таблица games для просмотра доступных игр. После этого создается новый XML-документ, добавляется тег games, а потом теги game для каждой доступной игры.

После запуска сценария из командной строки должна отобразиться следующая информация:

$ php games.php
<?xml version="1.0"?>
<games><game id="1"/></games>
$

Также этот сценарий можно запустить из Web-браузера.

Прекрасно! Теперь, имея интерфейс games, можно приступить к написанию серверного кода обработки ходов. Этот код начинается с создания вспомогательного сценария show_moves, получающего текущие ходы для данной игры и экспортирующего их в XML. В листинге 4 показан PHP-код для данной вспомогательной функции.

Листинг 4. show_moves.php
<?php
function show_moves( $dbh, $game ) {
  $sql = 'SELECT * FROM moves WHERE game=?';

  $q = $dbh->prepare( $sql );
  $q->execute( array( $game ) );

  $doc = new DOMDocument();
  $r = $doc->createElement( "moves" );
  $doc->appendChild( $r );

  foreach ( $q->fetchAll() as $row) {
    $e = $doc->createElement( "move" );
    $e->setAttribute( 'x', $row['x'] );
    $e->setAttribute( 'y', $row['y'] );
    $e->setAttribute( 'color', $row['color'] );
    $r->appendChild( $e );
  }

  print $doc->saveXML();
}
?>

Сценарий принимает дескриптор базы данных и идентификатор игры. Затем он выполняет SQL-запрос для получения списка ходов. После этого он создает XML-документ с ходами для данной игры.

Эту вспомогательную функцию используют два сценария. Первый из них – это moves.php, возвращающий текущие ходы указанной игры. Этот сценарий приведен в листинге 5.

Листинг 5. moves.php
<?php
require_once( 'show_moves.php' );

header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');

show_moves( $dbh, $_REQUEST['game'] );
?>

Этот простой сценарий подключает код вспомогательной функции, соединяется с базой данных и активизирует функцию show_moves с указанным идентификатором игры. Чтобы протестировать этот код, примените команду curl для активизации сценария на сервере из командной строки:

$ curl "http://localhost/ttt/moves.php?game=1"
<?xml version="1.0"?>
<moves/>
$

Пока не сделано ни одного хода, поэтому ничего интересного не выводится. Чтобы исправить положение, к серверному интерфейсу нужно добавить последний сценарий. В листинге 6 приведен сценарий move.php.

Листинг 6. move.php
<?php
require_once( 'show_moves.php' );

header( 'Content-Type:text/xml' );

$dbh = new PDO('mysql:host=localhost;dbname=ttt', 'root', '');
$sql = 'DELETE FROM moves WHERE game=? AND x=? AND y=?';
$sth = $dbh->prepare($sql);
$sth->execute( array(
  $_REQUEST['game'],
  $_REQUEST['x'],
  $_REQUEST['y']
) );

$sql = 'INSERT INTO moves VALUES ( 0, ?, ?, ?, ? )';
$sth = $dbh->prepare($sql);
$sth->execute( array(
  $_REQUEST['game'],
  $_REQUEST['x'],
  $_REQUEST['y'],
  $_REQUEST['color']
) );

show_moves( $dbh, $_REQUEST['game'] );
?>

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

Для тестирования можно выполнить ход:

$ curl "http://localhost/ttt/move.php?game=1&x=1&y=2&color=1"
<?xml version="1.0"?>
<moves><move x="1" y="2" color="1"/></moves>

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


Создание Android-интерфейса

Прежде всего установите Android SDK, несколько версий платформы Android и, наконец, Eclipse и плагин Android Eclipse. К счастью, все это хорошо описано на сайте Android (ссылки приведены в разделе Ресурсы). Подробное описание настройки среды разработки заняло бы целую статью и, вероятно, не одну.

После настройки среды разработки запустите Eclipse и начните новый Android-проект. На экране должна отобразиться информация, приведенная на рисунке 1.

Рисунок 1. Создание Android-приложения в Eclipse
Рисунок 1. Создание Android-приложения в Eclipse

На рисунке 1 изображен мастер проектов для Android-приложений. Введите название проекта, отметьте переключатель Create new project in workspace (создать новый проект в рабочей области) и укажите местоположение исходного кода с элементами интерфейса. В списке Build Target выберите Android platform. Для данного кода я использую Android 2.3.1. Код довольно прост, поэтому можно использовать любую версию по вашему выбору. Если в списке нет ни одной платформы, необходимо загрузить и установить одну из них, как описано в инструкциях по установке Android SDK. Предупреждаю, что загрузка всех платформ может занять очень много времени.

В разделе Properties введите название приложения и название пакета. Я использовал Tic Tac Toe и com.jherrington.tictactoe в соответствующих полях. Затем отметьте флажок Create Activity и введите имя действия. В качестве имени действия я использовал TicTacToeActivity.

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

Рисунок 2. Файлы проекта TicTacToe
Рисунок 2. Файлы проекта TicTacToe

На рисунке 2 показаны файлы и каталоги верхнего уровня для Android-приложения (каталоги src, gen, Android 2.3.1 и res; файлы assets, AndroidManifest.xml, default.properties и proguard.cfg). Важные элементы:

  • Каталог res, содержащий ресурсы.
  • Каталог src, содержащий исходный Java™-код.
  • Файл manifest, содержащий биографическую информацию о приложении.

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

Листинг 7. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      android:versionCode="1"
      android:versionName="1.0" package="com.jherrington.tictactoe">

    <uses-permission
        android:name="android.permission.INTERNET" />

    <uses-sdk android:minSdkVersion="5" />

    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <activity android:name="TicTacToeActivity"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Сделано единственное изменение – в начало файла добавлен тег uses-permission.

Следующая задача – разработать пользовательский интерфейс. Для этого модифицируем файл layout.xml, находящийся в каталоге res/layout. В листинге 8 приведено новое содержимое этого файла.

Листинг 8. layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <LinearLayout android:layout_height="wrap_content" 
                android:layout_width="match_parent" android:id="@+id/linearLayout1">
        <Button android:text="Play X" android:id="@+id/playx" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"></Button>
        <Button android:text="Play O" android:id="@+id/playo" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"></Button>
    </LinearLayout> 
    <com.jherrington.tictactoe.BoardView android:id="@+id/bview" 
                android:layout_width="wrap_content" 
                android:layout_height="wrap_content"
    ></com.jherrington.tictactoe.BoardView>
</LinearLayout>

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

Остальная часть исходного кода – это класс BoardView, отображающий игровое поле текущей игры. Исходный код класса BoardView приведен в листинге 11.

Имея схему, можно написать Java-код приложения. Он начинается с класса TicTacToeActivity, приведенного в листинге 9. Действия (activities) – это базовые блоки Android-приложения. Каждое приложение имеет одно или несколько действий, представляющих различные состояния приложения. По мере прохождения по приложению создается стек действий, к которым затем можно вернуться, используя кнопку "назад" мобильного телефона. Приложение TicTacToe имеет только одно действие.

Листинг 9. TicTacToeActivity.java
package com.jherrington.tictactoe;

import java.util.Timer;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.Gallery;
import android.widget.LinearLayout;

public class TicTacToeActivity extends Activity implements OnClickListener {
  @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.main);

      Button playx = (Button)this.findViewById(R.id.playx);
      playx.setOnClickListener( this );

      Button playo = (Button)this.findViewById(R.id.playo);
      playo.setOnClickListener( this );

      Timer timer = new Timer();
      UpdateTimer ut = new UpdateTimer();
      ut.boardView = (BoardView)this.findViewById(R.id.bview);
      timer.schedule( ut, 200, 200 );
    }

    public void onClick(View v) {
      BoardView board = (BoardView)this.findViewById(R.id.bview);
      if ( v.getId() == R.id.playx ) {
        board.setColor( 2 );
      }
      if ( v.getId() == R.id.playo ) {
        board.setColor( 1 );
      }
    }
}

Действие имеет два метода. Первый – это метод onCreate, который создает пользовательский интерфейс, подключает обработчик onClick к кнопкам X и O и запускает таймер update. Этот таймер используется для обновления состояния игры каждые 200 миллисекунд. Данная функциональность позволяет обоим игрокам видеть ходы друг друга.

Обработчик onClick устанавливает текущий цвет игрового поля, основываясь на последней нажатой пользователем кнопке X или O.

Класс GameService, приведенный в листинге 10, является singleton-классом, представляющим игровой сервер и текущее состояние данной игры.

Листинг 10. GameService.java
package com.jherrington.tictactoe;

import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import android.util.Log;

public class GameService {
  private static GameService _instance = new GameService();

  public int[][] positions = new int[][] { 
      { 0, 0, 0 },
      { 0, 0, 0 },
      { 0, 0, 0 }
  };

  public static GameService getInstance() {
    return _instance;
  }

  private void updatePositions( Document doc ) {
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        positions[x][y] = 0;
      }
    }
    doc.getDocumentElement().normalize();
    NodeList items = doc.getElementsByTagName("move");
    for (int i=0;i<items.getLength();i++){
      Element me = (Element)items.item(i);
      int x = Integer.parseInt( me.getAttribute("x") );
      int y = Integer.parseInt( me.getAttribute("y") );
      int color = Integer.parseInt( me.getAttribute("color") );
      positions[x][y] = color;
    }
  }

  public void startGame( int game ) {
    HttpClient httpclient = new DefaultHttpClient();
    HttpPost httppost = new HttpPost("http://10.0.2.2/ttt/moves.php");

    try {
      List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
      nameValuePairs.add(new BasicNameValuePair("game", Integer.toString(game)));
      httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

      HttpResponse response = httpclient.execute(httppost);
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = dbf.newDocumentBuilder();
      updatePositions( db.parse(response.getEntity().getContent()) );
    } catch (Exception e) {
      Log.v("ioexception", e.toString());
    }
  }

  public void setPosition( int game, int x, int y, int color ) {
    HttpClient httpclient = new DefaultHttpClient();
    HttpPost httppost = new HttpPost("http://10.0.2.2/ttt/move.php");

    positions[x][y] = color;

    try {
      List<NameValuePair> nameValuePairs = new ArrayList<NameValuePair>(2);
      nameValuePairs.add(new BasicNameValuePair("game", Integer.toString(game)));
      nameValuePairs.add(new BasicNameValuePair("x", Integer.toString(x)));
      nameValuePairs.add(new BasicNameValuePair("y", Integer.toString(y)));
      nameValuePairs.add(new BasicNameValuePair("color", Integer.toString(color)));
      httppost.setEntity(new UrlEncodedFormEntity(nameValuePairs));

      HttpResponse response = httpclient.execute(httppost);
      DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
      DocumentBuilder db = dbf.newDocumentBuilder();
      updatePositions( db.parse(response.getEntity().getContent()) );
    } catch (Exception e) {
      Log.v("ioexception", e.toString());
    }
  }
}

Это самый интересный код в приложении. Во-первых, он содержит метод updatePositions, который принимает возвращенный из сервера XML и ищет элементы move, а затем обновляет массив positions, в котором указан текущий набор ходов. В массиве positions содержится значение для каждой позиции на игровом поле; ноль – это пустое место, 1 – это O, а 2 – это X.

Две другие функции, startGame и setPosition, реализуют способ взаимодействия с сервером. Метод startGame запрашивает у сервера текущее состояние ходов и обновляет список позиций. Метод setPosition передает ход на сервер, создавая HTTP-запрос post и задавая данные этого post-запроса на основе массива пар имя-значение, которые затем кодируются для передачи. Затем метод анализирует ответный XML и обновляет список позиций.

Присмотревшись, можно увидеть, что используемый для подключения к серверу используется необычный IP-адрес. Это не localhost и не 127.0.0.1, а 10.0.2.2, являющийся псевдонимом машины, на которой выполняется эмулятор. Поскольку Android-телефон сам по себе является UNIX®-системой, он имеет свои собственные службы на localhost. Чудеса, не так ли? Не часто столь очевидно проявляется тот факт, что это, собственно, не телефон, а полноценный помещающийся на ладони компьютер, в котором по воле случая оказался встроенный телефон.

Итак, на чем мы остановились? У нас есть действие (основной компонент приложения), настроенная схема пользовательского интерфейса и Java-код для подключения к серверу. Теперь нам необходимо нарисовать игровое поле. Этим занимается класс BoardView, приведенный в листинге 11.

Листинг 11. BoardView.java
package com.jherrington.tictactoe;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class BoardView extends View {
  private int _color = 1;
  
  public void setColor( int c ) {
    _color = c;
  }
  
  public BoardView(Context context) {
      super(context);
      GameService.getInstance().startGame(0);
  }
  
  public BoardView(Context context, AttributeSet attrs) {
      super(context,attrs);
      GameService.getInstance().startGame(0);
  }
  
  public BoardView(Context context, AttributeSet attrs, int defStyle) {
      super(context,attrs,defStyle);
      GameService.getInstance().startGame(0);
  }

  public boolean onTouchEvent( MotionEvent event ) {
    if ( event.getAction() != MotionEvent.ACTION_UP )
      return true;
    int offsetX = getOffsetX();
    int offsetY = getOffsetY();
    int lineSize = getLineSize();
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        Rect r = new Rect( ( offsetX + ( x * lineSize ) ),
            ( offsetY + ( y * lineSize ) ),
            ( ( offsetX + ( x * lineSize ) ) + lineSize ),
            ( ( offsetY + ( y * lineSize ) ) + lineSize ) );
        if ( r.contains( (int)event.getX(), (int)event.getY() ) ) {
          GameService.getInstance().setPosition(0, x, y, _color);
          invalidate();
          return true;
        }
      }
    }
    return true;
  }

  private int getSize() {
    return (int) ( (float) 
    ( ( getWidth() < getHeight() ) ? getWidth() : getHeight() ) * 0.8 );
  }

  private int getOffsetX() {
    return ( getWidth() / 2 ) - ( getSize( ) / 2 );
  }
  
  private int getOffsetY() {
    return ( getHeight() / 2 ) - ( getSize() / 2 );
  }
  
  private int getLineSize() {
    return ( getSize() / 3 );
  }

  protected void onDraw(Canvas canvas) {
    Paint paint = new Paint();
    paint.setAntiAlias(true);
    paint.setColor(Color.BLACK);
    canvas.drawRect(0,0,canvas.getWidth(),canvas.getHeight(), paint);
    
    int size = getSize();
    int offsetX = getOffsetX();
    int offsetY = getOffsetY();
    int lineSize = getLineSize();
    
    paint.setColor(Color.DKGRAY);
    paint.setStrokeWidth( 5 );
    for( int col = 0; col < 2; col++ ) {
      int cx = offsetX + ( ( col + 1 ) * lineSize );
      canvas.drawLine(cx, offsetY, cx, offsetY + size, paint);
    }
    for( int row = 0; row < 2; row++ ) {
      int cy = offsetY + ( ( row + 1 ) * lineSize );
      canvas.drawLine(offsetX, cy, offsetX + size, cy, paint);
    }
    int inset = (int) ( (float)lineSize * 0.1 );
    
    paint.setColor(Color.WHITE);
    paint.setStyle(Paint.Style.STROKE); 
    paint.setStrokeWidth( 10 );
    for( int x = 0; x < 3; x++ ) {
      for( int y = 0; y < 3; y++ ) {
        Rect r = new Rect( ( offsetX + ( x * lineSize ) ) + inset,
            ( offsetY + ( y * lineSize ) ) + inset,
            ( ( offsetX + ( x * lineSize ) ) + lineSize ) - inset,
            ( ( offsetY + ( y * lineSize ) ) + lineSize ) - inset );
        if ( GameService.getInstance().positions[ x ][ y ] == 1 ) {
          canvas.drawCircle( ( r.right + r.left ) / 2, 
                  ( r.bottom + r.top ) / 2, 
                  ( r.right - r.left ) / 2, paint);
        }
        if ( GameService.getInstance().positions[ x ][ y ] == 2 ) {
          canvas.drawLine( r.left, r.top, r.right, r.bottom, paint);
          canvas.drawLine( r.left, r.bottom, r.right, r.top, paint);
        }
      }
    }
  }

}

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

Метод onTouch использует функции sizing для вычисления прямоугольника позиции каждой клетки. Затем он вызывает для прямоугольника метод contains, чтобы определить, нажал ли пользователь клетку. Если да, активизируется запрос сервиса game для выполнения хода.

Функция onDraw использует функции sizing как для прорисовки линий игрового поля, так и для прорисовки всех поставленных крестиков и ноликов. Синглтон GameServer используется для массива positions, в котором сохраняется текущее состояние каждого квадрата игрового поля.

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

Листинг 12. UpdateTimer.java
package com.jherrington.tictactoe;

import java.util.TimerTask;

public class UpdateTimer extends TimerTask {
  public BoardView boardView;
  
  @Override
  public void run() {
    GameService.getInstance().startGame( 0 );
    boardView.post(new Runnable(){ public void run(){ boardView.invalidate(); } });
  }
}

Таймер инициализируется классом TicTacToeActivity во время запуска приложения. Таймер реализует polling-механизм. Это не самый эффективный способ взаимодействия между клиентом и сервером, но он является самым простым и надежным. Наиболее эффективный способ – использовать версию 1.1 HTTP-протокола для поддержания соединения в активном состоянии и реализовать отправку сервером обновлений клиенту при совершении хода. Этот подход намного сложнее; он требует поддержки протокола 1.1 и клиентом и сервером и имеет ограничения на количество соединений. Рассмотрение этого подхода выходит за рамки данной статьи. Для простых демонстрационных игр, подобных нашей, отлично подходит polling-механизм.

После написания исходного кода можно протестировать приложение. Для этого нужно запустить эмулятор. После запуска на экране должна отобразиться информация, приведенная на рисунке 3.

Рисунок 3. Запуск Android-эмулятора
Рисунок 3. Запуск Android-эмулятора

Этот эмулятор загружает фантастический интерфейс "A N D R O I D". После его загрузки вы должны увидеть экран запуска приложения, изображенный на рисунке 4.

Рисунок 4. Эмулятор запущен и готов к работе
Рисунок 4. Эмулятор запущен и готов к работе

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

Рисунок 5. Игра до выполнения первого хода
Рисунок 5. Игра до выполнения первого хода

В зависимости от состояния вашего сервера вы либо увидите ходы, либо нет. В нашем случае игра еще не начиналась. Кнопки Play X и Play O находятся над игровым полем, расположенном в центре экрана. Нажмите кнопку Play X, а затем центральный квадрат. Отобразится экран, аналогичный приведенному на рисунке 6.

Рисунок 6. Естественно, ход крестиком делается в центральный квадрат
Рисунок 6. Естественно, ход крестиком делается в центральный квадрат

На рисунке 6 показано игровое поле с крестиком в центральном квадрате. Для проверки подключения к серверу можно выполнить команду curl со сценарием moves.php на сервере для получения самого последнего списка ходов.

Для тестирования работы ноликов нажмите Play O и выберите угловой квадрат, как показано на рисунке 7.

Рисунок 7. Нолик в угловом квадрате
Рисунок 7. Нолик в угловом квадрате

Можно играть и крестиками, и ноликами. Приложение подключается к серверу для сохранения состояния игры в общедоступном месте. А благодаря таймеру update любой игрок может видеть ходы, сделанные другим игроком.

Заключение

Является ли эта игра законченной? Конечно же, нет. Отсутствует проверка условия победы, игроки могут повторно занимать одни и те же поля, отсутствует контроль очередности ходов. Но основные технологические блоки присутствуют: игровой сервер с доступным для всех игроков сохраненным состоянием игры и стандартное графическое приложение на мобильном устройстве, подключающееся к игровому серверу для предоставления интерфейса игры. Вы можете использовать эту игру в качестве отправной точки для своего собственного игрового приложения и сделать его таким, каким пожелаете. Просто помните, что игра должна быть интересной и увлекательной, и вы, возможно, создадите конкурента Words With Friends или многопользовательский вариант "Angry Birds".

Ресурсы

Научиться

  • Оригинал статьи: Create a networked tic-tac-toe game for Android (EN).
  • Eclipse: справочная информация об используемой в данной статье среде IDE для разработки Android-приложения. Также приведены ссылки на загружаемые файлы Eclipse и плагины.
  • Средства разработки PHP-приложений для Eclipse. Нужна среда IDE для PHP? Проект Eclipse имеет нужное расширение, а также другие плагины практически для любых языков.
  • Android Market: после написания сетевой многопользовательской Android-игры загрузите ее на сайт Android marketplace. И сообщите нам об этом в комментариях к данной статье.
  • Сайт PHP: лучшая справочная информация по PHP.
  • W3C: отличный сайт по стандартам, в частности, по стандартам XML, имеющим отношение к данной статье.
  • Другие статьи данного автора (Джек Херрингтон (Jack Herrington), developerWorks, с марта 2005 года по настоящее время): статьи об Ajax, JSON, PHP, XML и других технологиях (EN).
  • Сертификация IBM по XML: информация о получении сертификата IBM-Certified Developer по XML и смежным технологиям.
  • Технические мероприятия и Web-трансляции на developerWorks. Следите за новыми технологиями.
  • developerWorks в Твиттере. Следите за твитами developerWorks.
  • Демонстрации по запросу на developerWorks. Смотрите на developerWorks предоставляемые по запросу демонстрации – от установки и настройки продуктов для начинающих до продвинутых функциональных возможностей для опытных разработчиков.

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=XML, Open source
ArticleID=809720
ArticleTitle=Создание сетевой игры в "крестики-нолики" на платформе Android
publish-date=04112012