Рубрики
Без рубрики

Построить онлайн-многопользовательскую игру с Socket.io, Redis и Angularjs

Хотите разработать многопользовательскую онлайн игру? Прочитайте этот пошаговый учебник о том, как использовать Socket.IO, Redis и Angularjs, чтобы создать свою многопользовательскую игру!

Автор оригинала: Dominic Scanlan.

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

Я хотел написать многопользовательскую игру через Socket.io на некоторое время сейчас. В дополнение к использованию Socket.io Добавьте что-нибудь еще в микс – я думал, что дам Redis идти на кэширующий слой.

Теперь я не стремился быть слишком амбициозным – я просто хотел что-то, что было бы весело играть, и поэтому я решил шашки.

Структура моей папки выглядит следующим образом:

    root
    -- bower_components (for client side modules)
    -- node_modules (for server side modules)
    -- public folder
        checkers.js
        index.html
        style.css
        fonts folder
    redis.js
    app.js

Я сломаю это на стороне сервера и на стороне клиента и надеюсь, что все присоединится к концу.

Серверная сторона

Я поклонник Экспресс И это то, с кем мне удобно – мы начнем, создавая кости экспресс-сервера. Для этого мы установимся экспресс с NPM Install Express http --save первый.

NB : Это –save И не –save-dev Отказ Это распространенная ошибка, которая изложена здесь Отказ

    var express = require('express');
    var app = express();
    var http = require('http');
    var uuid = require('node-uuid'); //used for creating games. 
    ...
    http.listen(3000, function(){
        console.log('http server listening on port 3000');
    });

Для самой интересной части я использовал модуль Socket.io из Чат Демофон Отказ Я тогда установил его с NPM установить Socket.IO --save и создал переменную сокета, которая интегрирована с HTTP-сервером.

    var socketio = require('socket.io')(http);

Затем я использовал переменную Socket.IO для прослушивания событий и пикап SessionId от клиента.

    socketio.on('connection', function(socket){
        console.log('user connected', socket.id);
        socket.on('disconnect', function(){
            console.log('user disconnected');
        }         
    ]);

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

  • мероприятие
  • сообщение для отправки
  • Идентификатор сеанса

Это то, как выглядит диспетчер …

    var dispatcher = function(type, obj, toid){
          console.log(obj);
    	    socketio.to(toid).emit(type, obj);
    };

Теперь, когда у нас есть голый слушатель для сервера и сокета, мы можем получить наш сервер Redis, установленный на моем коробке Linux с apt-get install redis-server Отказ Смотрите это Руководство для дополнительной информации.

NB : Чтобы запустить сервер Redis локально, используйте Redis-Server ; Для подключения к серверу Redis для терминала используйте Redis-Cli Отказ

Чтобы включить наш экспресс-сервер, чтобы поговорить с сервером Redis, мы установили модуль Redis с NPM установить Redis --save и создал новый файл _redis.js, чтобы разделить нашу логику Redis.

    var redis = require('redis');
    var client = redis.createClient();
    client.on('connect',function(){
        console.log('connected to redis');
    });

    var exports = module.exports;

Когда мы разделили разную логику в свои конкретные файлы JS, он был вернен к нашему файлу App.js через var (./_ redis.js); В файле App.js.

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

В файле Redis.js мы создали метод, проходящие в SessionId и диспетчер отправлять сообщения клиенту. Они также будут подвержены воздействию файла App.js, используя Экспорт Отказ

    exports.createGame = function(sessionid, dispatcher){
        var gamename = 'game';
        client.hmset(
            gamename: {
                player1: sessionid,
                player2: ''
            }
        );
        dispatcher('game', {name: gamename, player: 'player1'}, sessionid);
    });

Мы используем Hmset Когда мы пишем данные на сервер Redis.

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

Концепция игры Checkers – это то, что первый пользователь, также известный как Player1, создаст игру и когда второй пользователь подключен, он или она будет игрока 2.

Когда сделано новое соединение, мы должны проверить, есть ли какие-либо игры, которые не имеют назначенного им проигрывателя2. Мы создали новую функцию в Redis.js, чтобы справиться с этим: Redis.CheckawaitingGames (сокет, ассоциацияGame, диспетчер);

    exports.checkAwaitingGames = function(socket, callback, dispatcher){
        ...
    }

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

    var foundGame = false; // we'll use this to exit if a game is found
    client.keys('*', function(err, games){
        // '*' is a wild card to return all the keys.

        // games is an array so check its length.
        if(games.length > 0){

        }
        else {
            callback({game: '', found: false, socket: socket});
        }
    }

Если мы не найдем никаких доступных игр, мы позвоним AssociateGame создать другую игру. Если мы найдем доступные игры, мы позвоним AssociateGame назначить игру игроку также. (Мы определим AssociateGame позже в статье.)

Если у нас есть список игр, мы можем закрутить список. Как Redis хранит пары ключевых значений, мы бы использовали HGGETALL вместе с ключом Игра Чтобы вернуть стоимость и тест, чтобы увидеть, есть ли проигрыватель 2 SessionId Отказ Если мы найдем один, мы бы сказали AssociateGame Что у нас есть игра и автоматически отправляйте сообщение для Player1.

    games.forEach(function(game, i){
        if(!foundGame){
            client.hgetall(game, function(err, reply){
                if(reply.player2 === '' && reply.player1 !== socket.id){
                    callback({game: game, found: true, socket: socket});
                    foundGame = true;
                    dispatcher('player2 found', {player: socket.id}, reply.player1);
                }
            });
        }
    });

Мы говорили о AssociateGame не определяя это. Это обработчик, определенный в App.js для создания или назначения игры, которая выполняется Checker_redis.js.

    var associateGame = function(obj){
        if(obj.found){
            //console.log('associateGame assignGame');
            redis.assignGame(obj.game, obj.socket.id, dispatcher);
        }
        else{
            //console.log('associateGame createGame');
            redis.createGame(obj.socket.id, dispatcher);
        }
    };

Как вы можете себе представить, НазначениеGame . & Creategame Будут писать вещи для Redis, используя HGGETALL & Hmset Отказ Затем он отправит сообщение для подключения пользователя с Диспетчер Отказ

    exports.assignGame = function(game, socketid, dispatcher){
        client.hgetall(game, function(err, reply){
            if(err){
                exports.createGame(socketid, dispatcher);
            }
            else{
                reply.player2 = socketid;
                client.hmset(game, reply);

                var message = {
                    name: game,
                    player: 'player2'
                }
                dispatcher('game', message, socketid);
            }
        });
    };

Первая часть НазначениеGame . вернет значение для пропущенного ключа, но если произойдет ошибка, она вернется, тогда позвоните Creategame вместо. Мы будем обновлять значение с игроком2 SessionId а затем обновите ключ с Hmset Отказ Наконец, сообщение будет отправлено клиенту в отношении деталей игры и его или ее статуса Player2.

Creategame намного проще. В этом случае мы знаем, что нет никаких доступных игр. Мы используем Creategame Добавить новые игры. Я использовал UUID Чтобы я не пытался добавить игру с тем же ключом и пройти в новом хеш вместе с UUID Отказ

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

    exports.createGame = function(sessionid, dispatcher){
        var uuid1 = uuid.v4();
        client.hmset(uuid1, {
            'player1': sessionid,
            'player2': ''
        });

        var message = {
            name: game,
            player: 'player1'
        };

        dispatcher('game',message, sessionid);
    }

Пока что мы смогли подключиться к серверу, поискать игру, присоединиться к нему, если нет игрока2, и создайте новую игру. Теперь мы собираемся использовать обработчик, когда на самом деле сделан шаг. Для этого нам потребуется прослушивание другого типа сокета в нашем файле App.js, «Переместить взят».

    socket.on('move taken', function(obj){
    console.log('move taken', obj);
    redis.getOpponent(obj, dispatcher);
    
  });

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

Сообщение будет содержать игровой ключ и состояние клиента, которое является либо Player1, либо Player2. Мы используем Экспорт так что Геллоппонент доступен, когда redis.js создается.

    client.hgetall(obj.name, function(err, found){
    
    if(obj.player ==='player1'){
      dispatcher('move taken', obj, found.player2);
      
    }
    else{
      dispatcher('move taken', obj, found.player1);
    }
  });

Клиент называется вместе с HGGETALL , чтобы найти значение ключа значения, чтобы правильный сеанс получит сообщение.

Чтобы информировать оппозицию, что одна из их произведений была взята, мы использовали аналогичный обработчик под названием SendTakensipiver Отказ Розетка будет слушать «взятые предметы».

  client.hgetall(obj.name, function(err, found){
    if(obj.player ==='player1'){
      dispatcher('taken piece', obj, found.player2);
    }
    else{
      dispatcher('taken piece', obj, found.player1);
    }
  });

Наконец, для того, чтобы сохранить Redis в самым легким состоянии, когда Player1 отключается, я удалю игру. Socket.io будет слушать «Отключить» и позвонит Redis.closegame Отказ

В идеале мы бы знали, какую игру мы удалении; Тем не менее, Отключить не даст нам эту информацию. Вместо этого он даст нам SessionId от отключения клиента. В ClackGame , мы будем зацикливаться над играми, которые мы имеем и проверяли, является ли Game.player1 клиент SessionId Отказ

    client.keys('*', function(err, games){
    games.forEach(function(game, i){

      client.hgetall(game, function(err, reply){

        if(reply.player1 === sessionid){
          client.del(game);
        }

        if(reply.player2 ===sessionid){
          dispatcher('player offline', {player: sessionid}, reply.player1);
        }
        
      });
    });
  });

Просто чтобы сохранить немного контроля, мы только удаляем игру, когда Player1 отключается.

Это оно! У вас есть сервер, который будет обрабатывать сообщения из кода клиент-сервера Checkers.

Сторона клиента

Сторона клиента просто состоит из трех файлов:

  • Checkers.js.
  • index.html.
  • still.css

Это то, что выглядит HTML для макета доски:

Многопользовательская онлайн игра
    
            
            ...
       

Там будет 6 тр и 4 TD. Просто чтобы все было простым, я решил предопределил размер таблицы, создать массив, чтобы держать каждый квадрат на доске под названием Штуки и предопределен, где участки игрока уже расположены.

    vm.pieces = 
      [
        {position: 'A1', class: vm.icons.player2, player: 'player2', queen: false},
        {position: 'A2', class: vm.icons.blank, player: undefined, queen: false},
        {position: 'A3', class: vm.icons.player2, player: 'player2', queen: false},
        {position: 'A4', class: vm.icons.blank, player: undefined, queen: false},...

Каждый кусок предоставляется (изначально) классы, проигрыватель и значение queen – все эти значения могут быть изменены, поскольку игре продолжается. Класс – это значок, который отображается и метод области охвата – $ Scope.getPositionClass – используется для возврата класса, чтобы запросить массив.

    $scope.getPositionClass = function(position){
      return _.where(vm.pieces, {position: position})[0].class;
    };

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

NGClass используется для отображения значков проверки. Вы заметите, что я принял контроллер как подход. Я использовал и var; Так что такое тенение не будет проблемой (используя NGIF создает отдельный объем).

Я использовал Очередная проверка Чтобы проверить, имеет ли положение на сетке на сетке на нем.

    $scope.checkPosition = function(position){
      if(_.where(vm.pieces, { position: position })[0].player === undefined){
        return false;
      }
      return true;
    };

Чтобы запустить соединение сокета, мы использовали `var ();, который открывает соединение сокета с сервером. Первая функция, которую мы создали, использовались для обработки создания игры на подключении.

        socket.on('game', function(game){
      vm.game = game;
      if(vm.game.player==='player1'){
        //vm.myturn = true;
      }
      $scope.$apply();
    });

Розетка ищет событие Игра Когда пользователь сначала подключается и устанавливает локальную переменную детали игры в игру, переданную в мероприятии. $ Scope. $ prime () используется для запуска $ digest цикл, поэтому привязки обновляются. Игрок 2 нашел ждет, чтобы сервер сказать, что второй игрок присоединился к игре, чтобы она могла начать. VM.myturn Обеспечивает, что никакие движения не могут быть сделаны без присутствия другого игрока и предотвратить мошенничество, когда это другой поворот игрока:

        socket.on('player2 found', function(game){
      vm.myturn = true;
      vm.player2Found = true;
      $scope.$apply();
    });

Если противник выходит из игры, мы справимся с этим с:

        socket.on('player offline', function(msg){
      if(vm.game.player ==='player1'){
        vm.player2Found = false;
      }

      vm.myturn = false;
      $scope.$apply();
    });

Поскольку игра является Player1, приводимая к тому, что она имеет значение только для системы, если проигрыватель 2 выходит из игры (если вы вспоминаете, мы удаляем игру с панели инструментов Redis, если проигрыватель 1 выходит). Затем мы установили VM.myturn до false, чтобы убедиться, что никакие движения не могут быть сделаны.

Переезд на что-то более интересное: с помощью NGIF Доска разделена на сетки, которые имеют кусочки на сетке и тех без. Когда игрок нажимает на кусок, чтобы переместить его, эта часть и его сетка будут записаны в $ Scope.pieceselected Отказ

    $scope.pieceselected=function(position){	
      vm.validMoves = [];
      vm.selectedpiece = _.where(vm.pieces, { position: position })[0];
      vm.GetValidMoves();
    };

Здесь мы использовали Lodyash для поиска по массиву, чтобы найти правильный элемент. Как только это было найдено, он будет храниться в vm.selectedifeper для будущего использования. В качестве бокового примечания я не могу не рекомендовать Lodash достаточно, если вы используете массивы, чтобы удерживать данные и данные запроса.

Как часть выбора Piece, VM.GetValidMoves () вызывается для создания списка действительных ходов, которые могут сделать кусок. Поскольку оба игрока начинаются с перемещения в противоположных направлениях, должен быть отдельный способ построения списка.

    vm.GetValidMoves = function(){
        var index = rows.indexOf(vm.selectedpiece.position.split('')[0]);
        var colindex = parseInt(vm.selectedpiece.position.split('')[1]);
        if(vm.selectedpiece.player==='player2' || vm.selectedpiece.queen ===true){
            if(index < rows.indexOf('F')){
                var Row = rows[index + 1];
            ...
            }
        }
        if(vm.selectedpiece.player==='player1' || vm.selectedpiece.queen ===true){
            if(index > rows.indexOf('A')){
                var Row = rows[index - 1];
            ...
            }
        }
    }
```javascript

The row and the column are then passed to another method  `vm.checkDiagonal(Row + (colindex + 1));` to ensure that the position is not already occupied and added the position to the array `vm.validMoves`.

When a player makes a move, Angular will call `$scope.movehere` passing in the position in the checkers grid that the players piece is moving to.

```javascript
    $scope.movehere=function(position){
    ....
    }

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

        if(!vm.validateMove(vm.selectedpiece.position, position))
        {
          return false;
        }
        vm.myturn = false;
        
        
        
        
        vm._movedTo = _.where(vm.pieces, {position: position})[0];
        vm._movedTo.queen = vm.selectedpiece.queen;
        
        if(vm.game.player ==='player1'){
          vm._movedTo.class=vm.selectedpiece.class;

          if(!vm._movedTo.queen){
            var atAway = vm.player2Home.indexOf(position);
            
            if(atAway >= 0){
              vm._movedTo.queen = true;
              vm._movedTo.class=vm.icons.player1Queen;
            }
          }
        }
        else{
          vm._movedTo.class=vm.selectedpiece.class;
          if(!vm._movedTo.queen){
            var atAway = vm.player1Home.indexOf(position);
            if(atAway >= 0){
              vm._movedTo.queen = true;
              vm._movedTo.class=vm.icons.player2Queen;
            }
          }
        }
        vm._movedTo.player=vm.game.player;
        vm.selectedpiece.class= vm.icons.blank;
        vm.selectedpiece.player=undefined;
        vm.selectedpiece.queen = false;
    vm._movedTo, player: vm.game.player, name: vm.game.name});
        socket.emit('move taken', {piece: vm.selectedpiece, movedTo: vm._movedTo, player: vm.game.player, name: vm.game.name});
      }

Последнее, что нужно сделать, это отправить сообщение для перемещения на сервер.

Обертывание

Удовка! Это был самый длинный блог на сегодняшний день, но я надеюсь, что вы нашли это интересно!

Репозиторий Git здесь и должен быть доступен для вас для вилки.

Если вы заинтересованы в создании многопользовательского приложения, используя Socket.io, посмотрите эти два поста: создайте многопользовательское приложение, используя Socket.io (часть 1): легкое приложение для чата и построить многопользовательское приложение, используя сокет .о (часть 2): создание игрового игрового сервера