nodeJS Быстрый веб-сервер на javascript движке V8

7Янв/122

Пишем онлайн игру на NodeJS, Express и Socket.IO

Скриншот игры

Мало кто сегодня может сказать что не знает о NodeJS, последнее время о нём много говорят и пишут.
Я свой путь ознакомления с NodeJS начал полгода назад, тогда для меня это была просто интересное и новое, я и подумать не мог что уже через полгода это станет моим основным инструментом для разработки.

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

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

О том, что такое Express и Socket.IO уже писали много где, поэтому описывать ещё раз я не буду, уделив больше внимания процессу разработки.

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

Итак, решено! Начинаю делать крестики-нолики.

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

  • Общий какой-то интерфейс с информацией об игре
  • UI игрового поля с обработчиками
  • Серверная часть со всей логикой игры

Разработку я начал как обычно с интерфейса. Я выбрал фреймворк jQuery и jQueryUI соответственно.

Создав страничку и подключив необходимые стили и библиотеки jquery, jqueryUI:

<link rel="stylesheet" href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/themes/vader/jquery-ui.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.6.4/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.8.16/jquery-ui.min.js"></script>

Интерфейс составляет строку статуса, боковую панель статистики и само игровое поле, всё сделано простыми таблицами:

    <table border="0" width="100%">
        <thead>
            <th colspan="2" class="ui-widget ui-state-hover ui-corner-all">Подключаемся к серверу...</th>
        </thead>
        <tbody>
            <td class="ui-widget" valign="top"><br /><button>Новая игра</button><br /><br /></td>
            <td class="ui-widget" valign="top"><div class="ui-widget-shadow ui-corner-all ui-widget-overlay"></div>
                <table class="ui-widget ui-corner-all" cellpadding="0" cellspacing="0" align="left" id="board-table"></table>
            </td>
        </tbody>
    </table>

Тут думаю комментировать нечего, всё более чем понятно. Далее был быстро набросан CSS и я приступил к написанию клиентской обработки событий.

Весь код старался писать максимально чисто и логично с расчётом, что это обучающий материал, который должен учить только хорошему даже в мелочах :)

Был создан объект игры:

var TicTacToe = {
    gameId: null, // Данная переменная будет содержать уникальный ID игры.
    turn: null, // Чем будет ходить игрок, будет содержать X или O
    i: false, // Чей сейчас ход, игрока или соперника
    init: function() { ... }, // Здесь будут основные обработчики взаимодействия с серверной частью
    startGame: function (gameId, turn, x, y) { ... }, // Генерация игрового поля и установка всех параметров игры
    mask: function(state) { ... }, // Маска на игровое поля, чтобы нельзя было ничего нажимать когда ходит противник и просто красиво :)
    move: function (id, turn, win) { ... }, // Отметка хода на игровом поле
    endGame: function (turn, win) { ... } // Конец игры, вывод сообщения
}

Теперь рассмотрим каждую функцию по очереди, максимально комментировал, и так Init():

$(function() {
    // UI кнопки перезапуска игры
    $('#reload').button({icons:{primary:'ui-icon-refresh'}}).click(function(){window.location.reload();}); 
    // Подключаемся к серверу nodejs с socket.io
    var socket = io.connect(window.location.hostname + ':1337', {resource: 'api'});
    // Ниже идут event'ы (события) обрабатываемые библиотекой socket.io
    // Подключились
    socket.on('connect', function () {
        $('#status').html('Успешно подключились к игровому серверу');
    });
    // Переподключаемся
    socket.on('reconnect', function () {
        $('#connect-status').html('Переподключились, продолжайте игру');
    });
    // Соединение потеряно
    socket.on('reconnecting', function () {
        $('#status').html('Соединение с сервером потеряно, переподключаемся...');
    });
    // Ошибка
    socket.on('error', function (e) {
        $('#status').html('Ошибка: ' + (e ? e : 'неизвестная ошибка'));
    });
 
    // Далее уже идут обработчики игры посылаемые сервером
    // Ожидаем соперника
    socket.on('wait', function(){
        $('#status').append('... Ожидаем соперника...');
    });
    // Соперник отлючился
    socket.on('exit', function() {
        // Если вдруг соединение потеряно или соперник закрыл вкладку заканчиваем игру
        TicTacToe.endGame(TicTacToe.turn, 'exit');
    });
    // К нам подключился соперник, начинаем игру
    // Получаем ID игры, чем ходит данный игрок и размер поля x y
    socket.on('ready', function(gameId, turn, x, y) {
        $('#status').html('К вам подключился соперник! Игра началась! ' + (turn == 'X' ? 'Сейчас Ваш первый ход' : 'Сейчас ходит соперник') + '!');
        // Создаём поле и присваеваем игровые переменные объекту
        TicTacToe.startGame(gameId, turn, x, y);
        // Добавляем подсказку чем ходит игрок, чтобы не забыть :)
        $('#stats').append($('<div>').attr('class', 'turn ui-state-hover ui-corner-all').html('Вы играете: <b>' + (turn=='X'?'Крестиком':'Ноликом') + '</b>'));
        // На каждое поле вешаем событие клика мышки, значит ходим
        $("#board-table td").click(function (e) {
            // Ход сделан, отправляем событие на сервер с ID игры и ID игровой клетки, которая имеет значение XxY
            if(TicTacToe.i) socket.emit('step', TicTacToe.gameId, e.target.id);
        // Для красоты наведение будем подсвечивать выделением клетки
        }).hover(function(){
                $(this).toggleClass('ui-state-hover');
            }, function(){
                $(this).toggleClass('ui-state-hover');
            });
    });
    // Получаем ход
    socket.on('step', function(id, turn, win) {
        // Получаем ID клетки, которую надо пометить и чем ходили. win передаёт значение в случаи если этот ход был решающим
        TicTacToe.move(id, turn, win);
    });
    // Статистика
    socket.on('stats', function (arr) {
        var stats = $('#stats');
        stats.find('div').not('.turn').remove();
        for(val in arr) {
            stats.prepend($('<div>').attr('class', 'ui-state-hover ui-corner-all').html(arr[val]));
        }
    });
});

Теперь рассмотрим подробнее как стартует игра:

startGame: function (gameId, turn, x, y) {
// Уникальный ID игры
this.gameId = gameId;
// Чем ходит игрок
this.turn = turn;
// Чей сейчас ход, если X то понятно игрок в начале ходит первым :)
this.i = (turn == 'X');
// Очищаем игровое поле
var table = $('#board-table').empty();
// Создаем новое с указаными размерами
for(var i = 1; i <= y; i++) {
    var tr = $(&#39;<tr>');
    for(var j = 0; j < x; j++) {
        // Обратив внимание что каждая клетка имеет уникальный ID вида X и Y поля (id="2x3")
        tr.append($(&#39;<td>').attr('id', (j+1) + 'x' + i).addClass('ui-state-default').html('&nbsp;'));
    }
    table.append(tr);
}
// Показываем поле
$("#board").show();
// Если первый ходит другой игрок, накладываем маску
this.mask(!this.i);
},

Функцию маски описывать не буду, там всё банально.
Дальше функция получение хода игрока:

move: function (id, turn, win) {
// Получаем параметры: ID клетки куда был ход, чем ходили, решающий ли это ход
this.i = (turn != this.turn); // Чей следущий ход
$("#" + id).attr('class', 'ui-state-hover').html(turn); // Выставляем на поле ход
if (!win) { // Если победителей нет, продолжаем играть
    this.mask(!this.i); // Проверяем нужна маска на поле или нет
    $('#status').html('Сейчас ' + (this.i ? 'ваш ход' : 'ходит соперник')); // Выводим подсказку
} else {
    this.endGame(turn, win); // Если ход был решающим, завершаем игру
}
},

Завершение игры:

endGame: function (turn, win) {
// Переданные параметры: чей был ход, тип решающего хода
var text = '';
// Тип решающего хода имеет 3 вида
switch(win) {
    case 'none': text = 'Ничья!'; break; // Больше нет свободных клеток
    case 'exit': text = 'Соперник сбежал с поля боя! Игра закончена'; break; // Сокет завершил соединение
    default: text = 'Вы ' + (this.i ? 'проиграли! =(' : 'выиграли! =)'); // Есть победитель
}
// Выводим окошко с сообщением и кнопкой рестарта
$("<div>").html(text).dialog({
    title: 'Конец игры',
    modal: true,
    closeOnEscape: false,
    resizable: false,
    buttons: { "Играть по новой": function() {
        $(this).dialog("close");
        window.location.reload();
    }},
    close: function() {
        window.location.reload();
    }
});
}

На этом всё! Все созданные файлы положим в новую папку с именем public.

Комментарии (2) Пинги (0)
  1. Спасибо, хотя статья и была на Хабре :)
    А можешь показать цифры только от Node.js, а не всей системы? Буду премного благодарен!

  2. Цифр к сожалению не смогу точных показать, т.к. на тот момент ожидал великого хабра эффекта. А скачков то и не было, график плавненько поднялся немного и через пару дней отпустило) Если бы знал что придётся на спичках мерить, то профайлинг включил бы.


Оставить комментарий


Нет обратных ссылок на эту запись.