用流星制作多人TicTacToe游戏

井字游戏。用Meteor制作井字游戏的多人游戏

Meteor是一个流行的全栈Web框架, 它使您可以非常轻松地将您的想法原型化,并非常快速地从开发到生产。 它的反应性和DDP的使用使其成为构建简单的多人浏览器游戏的理想选择。

在本教程中,我将向您展示如何使用流星使用其默认的前端模板引擎Blaze构建多人TicTacToe 。 我将假定您已经使用Meteor玩了一点,当然,您对使用JavaScript编码感到很满意。

如果您对Meteor的使用经验为零,建议您先按照Meteor官方网站上的TODO应用教程进行操作。

您可以在随附的GitHub repo中找到完整应用程序的代码。

创建应用

如果未安装Meteor,则应根据您的操作系统按照其网站上说明进行操作

生成脚手架

现在安装了Meteor,打开终端并运行以下命令:

meteor create TicTacToe-Tutorial

这将创建一个具有您应用程序名称的文件夹(在本例中为TicTacToe-Tutorial )。 此新文件夹包含应用程序的基本文件结构。 实际上里面有一个示例应用程序。

导航到文件夹:

cd TicTacToe-Tutorial

现在运行该应用程序:

meteor

我知道,我知道……这是一个很难记住的命令,并且您会经常使用它,因此您应该开始记住它!

如果一切正常,则控制台应该正在构建应用程序。 完成后,打开您的Web浏览器并转到http:// localhost:3000以查看该应用程序正在运行。 如果您以前从未这样做过,建议您试用该示例应用程序。 尝试弄清楚它是如何工作的。

让我们看一下文件结构。 打开应用程序的文件夹。 我们目前唯一关心的是客户端文件夹和服务器文件夹。 客户端文件夹中的文件将由客户端下载并执行。 服务器文件夹中的文件将仅在服务器上执行,客户端无法访问它们。

这些是新文件夹中的内容:

client/main.js        # a JavaScript entry point loaded on the client
client/main.html      # an HTML file that defines view templates
client/main.css       # a CSS file to define your app's styles
server/main.js        # a JavaScript entry point loaded on the server
package.json          # a control file for installing NPM packages
.meteor               # internal Meteor files
.gitignore            # a control file for git

搭建板

井字游戏板是一个简单的三乘三桌; 没有什么花哨的,这对于我们的第一个多人游戏非常有用,因此我们可以专注于功能。

该面板将由客户端下载,因此我们将在客户端文件夹中编辑文件。 首先删除main.html上的内容,然后将其替换为以下内容:

client / main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  <table id="board">
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
  </table>
</body>

进行更改后,请不要忘记保存文件! 否则,它们将不会被Meteor认可。

现在,让我们在板上添加一些CSS 。 打开main.css文件并添加以下内容:

客户端/ main.css

table
{
  margin: auto;
  font-family: arial;
}

.field
{
  height: 200px;
  width: 200px;
  background-color: lightgrey;
  overflow: hidden;
}

#ui
{
  text-align: center;
}

#play-btn
{
  width: 100px;
  height: 50px;
  font-size: 25px;
}

.mark
{
  text-align: center;
  font-size: 150px;
  overflow: hidden;
  padding: 0px;
  margin: 0px;
}

.selectableField
{
  text-align: center;
  height: 200px;
  width: 200px;
  padding: 0px;
  margin: 0px;
}

我们还添加了一些额外的ID和类,这些ID和类将在本教程的后面部分中使用。

最后,删除我们不需要的client / main.js ,然后在浏览器中打开应用程序以查看其外观。

一切都很好,但不是最佳解决方案。 让我们通过引入Blaze Templates进行一些重构。

创建模板

模板是具有自己功能的HTML代码,您可以在应用程序中的任何地方重复使用。 这是将您的应用分解为可重用组件的好方法。

在创建第一个模板之前,我们将在客户端文件夹内添加两个以上的文件夹。 我们将一个称为html ,将另一个称为js

在html文件夹中,创建一个包含以下内容的新board.html文件:

client / html / board.html

<template name="board">
  <table id="board">
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
    <tr>
      <td class="field"></td>
      <td class="field"></td>
      <td class="field"></td>
    </tr>
  </table>
</template>

现在,在main.html文件夹上,将body标记内的内容替换为以下代码:

client / main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  {{>board}}
</body>

这将在body标签内插入具有属性name="board"模板。

但这是我们以前使用过的硬编码板。 直到现在,它都在模板内部,因此让我们利用模板助手来动态构建我们的电路板。

使用助手

我们将在电路板模板中声明一个帮助器 ,它将为我们提供一个数组,该数组的长度与我们希望电路板具有的尺寸相同。

js文件夹中,创建一个名为board.js的文件,其内容如下:

客户端/js/board.js

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

Template.board.helpers({
  sideLength: () => {
    let side = new Array(3);
    side.fill(0);

    return side;
  }
});

现在,我们将在开发板的模板HTML中使用此帮助器,以对该帮助器提供的数组中的每个元素重复一行。 为了帮助我们,我们将使用Every -in空格键块帮助器。

用以下内容替换board.html文件中的内容:

client / html / board.html

<template name="board">
  <table id="board">
    {{#each sideLength}}
      {{#let rowIndex=@index}}
      <tr>
        {{#each sideLength}}
        <td class="field" id="{{rowIndex}}{{@index}}">
          {{{isMarked rowIndex @index}}}
        </td>
        {{/each}}
      </tr>
      {{/let}}
    {{/each}}
  </table>
</template>

注意,我们遍历数组两次,一次遍历 ,一次遍历 ,并在实例化时实例化相应的标签( trtd )。 我们还将它们的id属性设置为@index + 列的 @index 。 我们得到的是一个两位数的数字,它将帮助我们识别该元素及其在板上的位置。

http:// localhost:3000上查看该应用程序,以了解到目前为止的情况。

用户界面

现在我们有了一个漂亮的面板,我们需要一个播放按钮和一个标签来显示当前游戏的信息。

让我们从在html文件夹内创建ui.html文件开始……您知道该练习。 现在,向其中添加以下内容:

client / html / ui.html

<template name ="ui">
  <div id="ui">
    {{#if inGame}}
      <p id="status">
      {{status}}
      </p>
    {{else}}
      <button id="play-btn">Play</button>
    {{/if}}
  </div>
</template>

如您所见,我们使用#if空格键块帮助程序和inGame帮助程序(我们尚未定义)作为条件。 p标签内也有status助手。 我们还将在以后定义它。

它是如何工作的? #if如果inGame帮助程序返回true ,则玩家将看到status帮助程序中的内容。 否则,我们将仅显示播放按钮。

别忘了,要显示此组件,我们需要将其添加到我们的主要客户端模板中:

client / main.html

<head>
  <title>tic-tac-toe</title>
</head>

<body>
  {{>ui}}
  {{>board}}
</body>

在登录

我们不会处理任何登录界面。 我们将安装一个非常有用的软件包brettle:accounts-anonymous-auto ,它将自动将所有用户匿名登录到我们的应用程序。

转到控制台并运行以下命令:

meteor add brettle:accounts-anonymous-auto

现在,在添加此软件包后首次打开应用程序时,它将创建一个新用户,并且每次在同一浏览器上打开应用程序时,它都会记住您。 如果我们不保留该用户的任何数据,则最好在注销时将其删除。 但是我们不会在本教程中进行介绍。

建立游戏

最后,我们将开始构建游戏本身! 让我们回顾一下我们将要实现的功能,以清楚地了解下一步的发展。

我们需要以下功能:

  • 制作游戏
  • 加入现有游戏
  • 采取行动
  • 建立胜利条件
  • 向玩家显示游戏状态
  • 销毁完成的游戏实例

为了利用Meteor的延迟补偿,我们会将大部分代码放在客户端和服务器均可访问的位置。

为此,我们将在项目的根目录下创建一个名为lib的文件夹。 无论我们放入哪里,客户端都会下载,因此我们必须非常谨慎。 您不希望偶然给客户端提供任何API密钥或访问隐藏功能。

游戏合集

流星使用Mongo Collections 。 如果您对Mongo不太熟悉,但是您使用了其他任何面向文档的数据库 ,就可以了。 否则,将集合视为表,其中每一行都独立于下一行。 一行可以有六列,而同一表中的另一行可以有四列完全不同的列。

我们需要创建一个集合,并且我们需要客户端和服务器都可以访问它。 因此,我们将在lib文件夹中创建一个games.js文件,并在其中创建一个名为“游戏”的集合实例,并将其存储在全局变量Games

lib / games.js

import { Mongo } from 'meteor/mongo';

Games = new Mongo.Collection("games");

现在,您可能想知道为什么我们要赋予玩家访问数据库和游戏逻辑的权限。 好吧,我们只授予播放器本地访问权限。 Meteor为客户提供了一个本地小型mongo数据库 ,我们只能使用Publish-Subscribe模式来填充它,稍后我将向您展示。 那是客户唯一有权访问的东西。 即使客户端写入其本地数据库,如果信息与服务器数据库中的信息不匹配,也会被覆盖。

也就是说,Meteor默认情况下会安装几个非常不安全的软件包。 一种称为autopublish ,它会自动发布所有集合并订阅客户端。 另一个称为不安全 ,它为客户端提供对数据库的写访问权限。

这两个软件包都非常适合用于原型制作,但是我们应该立即将其卸载。 转到控制台并运行以下命令:

meteor remove insecure
meteor remove autopublish

有了这种方式,现在我们需要一种将客户端中的操作与服务器上的操作同步的方法。 输入流星方法

games.play方法

Meteor.methods是一个对象,我们可以在其中注册可以由客户端使用Meteor.call函数调用的方法。 它们将首先在客户端上执行,然后在服务器上执行。 因此,借助本地的Mongo数据库,客户将能够立即看到更改的发生。 然后,服务器将在主数据库上运行相同的代码。

让我们在games集合下面创建一个空的games.play方法:

lib / games.js

Meteor.methods({
  "games.play"() {

  }
});

制作游戏

创建一个名为gameLogic.js的lib文件夹中的文件,并在我们将创建GameLogic带班newGame方法,在这里我们将插入一个新的文档到我们的游戏合集:

lib / gameLogic.js

class GameLogic
{
  newGame() {
    if(!this.userIsAlreadyPlaying()) {
      Games.insert({
        player1: Meteor.userId(),
        player2: "",
        moves: [],
        status: "waiting",
        result: ""
      });
    }
  }
}

在这段代码中,我们要在插入新游戏之前询问玩家是否已经在玩游戏,因为我们不会为每个玩家一次支持多个游戏。 这是非常重要的一步,否则我们可能最终会遇到一个巨大的错误。

让我们在newGame()下面添加userIsAlreadyPlaying方法:

lib / gameLogic.js

userIsAlreadyPlaying() {
  const game = Games.findOne({$or:[
    {player1: Meteor.userId()},
    {player2: Meteor.userId()}]
  });

  if(game !== undefined)
    return true;

  return false;
}

让我们看一下开始新游戏的过程。

当玩家按下“播放”按钮时,我们会寻找现有游戏加入他们。 如果所述玩家找不到要加入的游戏,则会创建一个新游戏。 在我们的模型中, player1是创建游戏的玩家, player2是空字符串,并且status默认为“正在等待”。

因此,如果另一个玩家按下播放按钮,他们将寻找一个游戏,该游戏包含一个空的player2字段和一个status字段,值为“ waiting”。 然后,我们将该玩家设置为player2并相应地更改其status

现在,我们必须使game.js中的Meteor方法可以访问我们的GameLogic类。 我们将导出类的实例,然后将其导入games.js文件中。 在类之外,在gameLogic.js文件的底部添加以下行:

export const gameLogic = new GameLogic();

games.js文件顶部添加以下行:

import { gameLogic } from './gameLogic.js';

现在,我们可以向空的games.play()方法添加逻辑。 首先,我们寻找状态为“正在等待”的游戏,然后如果未找到其他游戏,则调用newGame()

lib / games.js

Meteor.methods({
  "games.play"() {
    const game = Games.findOne({status: "waiting"});

    if(game === undefined) {
      gameLogic.newGame();
    }
  }
});

刊物

为了找到游戏,我们需要向客户授予games集合的访问权限。 为此,我们将创建一个Publication 。 出版物使我们仅向客户显示我们希望他们看到的数据。 然后,我们为客户端订阅 发布 ,以使他们能够访问该数据。

为了使玩家能够访问游戏集合,我们将创建一个“游戏”出版物。 但是,当将玩家添加到新游戏中时,我们将授予他们访问该特定游戏中所有字段的权限。 因此,还将有一个“我的游戏”出版物。

转到服务器文件夹内的main.js文件,并将其内容替换为以下内容:

服务器/ main.js

import { Meteor } from 'meteor/meteor';

Meteor.publish('Games', function gamesPublication() {
  return Games.find({status: "waiting"}, {
    fields:{
      "status": 1,
      "player1": 1,
      "player2": 1
    }
  });
});

Meteor.publish('MyGame', function myGamePublication() {
  return Games.find({$or:[
      {player1: this.userId},
      {player2: this.userId}]
    });
});

现在我们需要订阅“游戏”出版物。 我们将在UI模板的onCreated方法回调中进行此操作。

使用以下代码在client / js /中创建ui.js文件:

import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';

Template.ui.onCreated(() => {
  Meteor.subscribe('Games');
});

播放事件

模板提供了一个事件对象,我们可以在其中注册…。 你猜怎么了? 答对了! 活动。 我们将在UI模板中创建一个事件。 每当玩家单击ID为'play-btn'的DOM元素时,我们都会将会话变量inGame设置为true,我们将调用games.play方法,并订阅MyGame集合。

会话变量可以在客户端代码中的任何位置使用,甚至可以在模板之间使用。 要使用它们,我们需要添加Session包

meteor add session

转到ui.js文件,并在onCreated方法之后添加以下行:

客户端/js/ui.js

Template.ui.events({
  "click #play-btn": () => {
    Session.set("inGame", true);
    Meteor.call("games.play");
    Meteor.subscribe('MyGame');
  }
});

导入每个文件中使用的软件包是一个好习惯。 由于我们在ui.js文件中使用Session包,因此我们应该导入它。 只需在顶部添加以下行:

import { Session } from 'meteor/session';

好! 现在我们需要添加几个助手。 还记得ui.html吗? 快速浏览。 我们使用了inGame帮助程序和status帮助程序。 让我们在events对象下面声明它们:

客户端/js/ui.js

Template.ui.helpers({
  inGame: () => {
    return Session.get("inGame");
  },
  status: () => {

  }
});

如您所见, inGame帮助器返回存储在inGame会话变量中的值。 我们暂时将status助手保留为空。

参加游戏

毕竟,到目前为止,您已经做好了准备,加入游戏应该很简单。

首先,我们将joinGame方法添加到GameLogic类中:

lib / gameLogic.js

joinGame(game) {
  if(game.player2 === "" && Meteor.userId() !== undefined) {
    Games.update(
      {_id: game._id},
      {$set: {
        "player2": Meteor.userId(),
        "status": game.player1
        }
      }
    );      
  }
}

如您所见,我们传递一个游戏变量,并将player2字段设置为玩家的_id ,将status字段设置为_id_player1 。 这样我们才能知道轮到谁了。

现在,我们将从games.play()调用此方法。 转到games.js文件,并用以下内容替换games.play方法的内容:

lib / games.js

Meteor.methods({
  "games.play"() {
    const game = Games.findOne({status: "waiting"});

    if(game === undefined) {
      gameLogic.newGame();
    } else if(game !== undefined && game.player1 !== this.userId && game.player2 === "") {
      gameLogic.joinGame(game);
    }
  }
});

因此,现在,我们在三个条件下添加了else if :如果我们找到一个游戏, player1不是该玩家, player2是一个空字符串,则我们加入游戏。

采取行动–逻辑

当我们为每个新游戏定义模型时,我们声明了一个带有空数组( [] )的moves字段作为默认值。 移动将是由进行移动的玩家的_id和所选位置组成的JSON对象。

转到games.js文件,并在games.play()下面添加以下方法。 请记住, Meteor.methods需要一个JSON对象,因此方法应以逗号分隔:

lib / games.js

"games.makeMove"(position) {
  check(position, String);

  gameLogic.validatePosition(position);

  let game = Games.findOne({status: this.userId});

  if(game !== undefined) {
    gameLogic.addNewMove(position);

    if(gameLogic.checkIfGameWasWon()) {
      gameLogic.setGameResult(game._id, this.userId);
    } else {
      if(game.moves.length === 8) {
        gameLogic.setGameResult(game._id, "tie");
      } else {
        gameLogic.updateTurn(game);
      }
    }
  }
}

让我们逐行介绍此方法。 它以字符串position作为参数。 首先,我们使用检查包确保接收到的是字符串,而不是可能危害服务器的恶意代码,然后验证位置。

之后,我们找到一个游戏,其中status字段与进行此操作的玩家的_id相同; 这样我们知道轮到他们了。 如果我们找到了该游戏,或者换句话说,轮到该玩家了,我们将把该移动添加到moves数组中。 然后,我们检查该举动后游戏是否获胜。 如果确实获胜,那么我们将当前玩家设为获胜者。 否则,如果没有获胜,但数组中已经有八步棋,那么我们将宣布平局。 如果还没有八步棋,我们会更新回合以让下一位棋手移动。

就像我们对ui.js文件中的Session包所做的一样 。 我们应该将check包导入games.js文件中。 您知道怎么回事...在顶部添加以下行。

import { check } from 'meteor/check';

我们正在使用GameLogic类中的许多方法,这些方法尚未定义。 因此,让我们继续进行。

转到gameLogic.js,并在GameLogic类中添加以下方法:

validatePosition()

validatePosition(position) {
  for (let x = 0; x < 3; x++) {
    for (let y = 0; y < 3; y++) {
      if (position === x + '' + y)
        return true;
    }
  }

  throw new Meteor.Error('invalid-position', "Selected position does not exist... please stop trying to hack the game!!");
}

在这里,我们只需遍历3×3网格以确保发送的位置在其限制之内。 如果找不到客户端发送的头寸,则在网格中会抛出错误。

addNewMove()

addNewMove(position) {
  Games.update(
    {status: Meteor.userId()},
    {
      $push: {
        moves: {playerID: Meteor.userId(), move: position}
      }
    }
  );
}

在这里,我们使用$ push Mongo运算符,将包含当前玩家_idposition的新动作推入数组。

setGameResult()

setGameResult(gameId, result) {
  Games.update(
    {_id: gameId},
    {
      $set: {
        "result": result,
        "status": "end"
      }
    }
  );
}

使用$组再次操作,我们更新结果字段的值result ,其可以是参数_id的玩家或“扎”之一,我们设置status ,以“结束”。

updateTurn()

updateTurn(game) {
  let nextPlayer;

  if(game.player1 === Meteor.userId())
    nextPlayer = game.player2;
  else
    nextPlayer = game.player1;

  Games.update(
    {status: Meteor.userId()},
    {
      $set: {
        "status": nextPlayer
      }
    }
  );
}

这很简单。 我们将两个玩家作为参数,然后找出哪个是当前玩家,然后将status字段设置为另一玩家的_id

赢得比赛

games.makeMove方法中还有一个方法需要声明; 获胜算法。 还有其他更有效的方法来计算TicTacToc游戏中的获胜者,但我决定采用本教程可以想到的最直观,最简单的解决方案。

转到gameLogic.js文件,并在GameLogic类中添加以下方法:

lib / gameLogic.js

checkIfGameWasWon() {
  const game = Games.findOne({status: Meteor.userId()});

  const wins = [
  ['00', '11', '22'],
  ['00', '01', '02'],
  ['10', '11', '12'],
  ['20', '21', '22'],
  ['00', '10', '20'],
  ['01', '11', '21'],
  ['02', '12', '22']
  ];

  let winCounts = [0,0,0,0,0,0,0];

  for(let i = 0; i < game.moves.length; i++) {
    if(game.moves[i].playerID === Meteor.userId()) {
      const move = game.moves[i].move;

      for(let j = 0; j < wins.length; j++) {
        if(wins[j][0] == move || wins[j][1] == move || wins[j][2] == move)
        winCounts[j] ++;
      }
    }
  }

  for(let i = 0; i < winCounts.length; i++) {
    if(winCounts[i] === 3)
      return true;
  }

  return false;
}

让我们仔细看看这个方法。

首先,我们找到当前的游戏。 然后,我们声明一个包含所有可能的获胜组合的矩阵,以及一个包含七个零的数组的变量:每个组合一个。 之后,我们将循环浏览当前玩家的所有举动,并将其与每种组合的每个位置进行比较。 对于每个巧合,我们在对应的winCount索引位置加1。 如果任何一个winCount指数加起来winCount 3,我们就会知道当前玩家已经赢了。

如果您第一次没有得到,请不要担心。 休息片刻,喝杯咖啡,稍后再以新鲜的眼睛再次阅读。 对代码的解释可能会造成混淆。 有时,最好只是阅读代码并弄清楚它的作用。

采取行动–控制器

我们对此游戏的玩家控制仅是简单的单击。 因此,实现这一点应该是小菜一碟。 让我们转到board.js文件,并在helpers程序之后将事件模板对象添加到我们的文件中:

客户端/js/board.js

Template.board.events({
  "click .selectableField": (event) => {
    Meteor.call("games.makeMove", event.target.id);
  }
});

简单吧? 当玩家单击类为“ selectableField”的DOM元素时,我们将调用games.makeMove方法,并将DOM元素的ID作为位置参数传递。 请记住,我们是在元素在网格中的位置之后命名ID。 如果需要,请查看board.html文件以刷新内存。

显示动作

现在,在同一文件中,我们将创建一个名为isMarked的帮助isMarked ,该帮助程序将在markselectableFields之间切换。 这样,我们将能够看到已选择了哪些位置,并选择了空位置。

将此助手添加到sideLength助手下方:

客户端/js/board.js

isMarked: (x, y) => {
  if(Session.get("inGame")) {
    let myGame = Games.findOne();

    if(myGame !== undefined && myGame.status !== "waiting") {
      for(let i = 0; i < myGame.moves.length; i++) {
        if(myGame.moves[i].move === x + '' + y) {
          if(myGame.moves[i].playerID === Meteor.userId())
            return "<p class='mark'>X</p>";
          else
            return "<p class='mark'>O</p>";
        }
      }
      if(myGame.status === Meteor.userId())
        return "<div class='selectableField' id='"+x+y+"'></div>";
    }
  }
}

并将助手添加到模板:

client / html / board.html

...
<td class="field" id="{{rowIndex}}{{@index}}">
  {{{isMarked rowIndex @index}}}
</td>
...

让我们来看一下这个功能。 我们将一行和一列作为参数(x,y)。 如果我们是inGame ,我们将寻找该游戏。 如果找到它并且status为“正在等待”,我们将遍历所有移动,如果给定的行+列与我们的moves之一匹配,我们将在板上画一个X。 如果它与其他玩家的动作之一匹配,我们将画一个O。

在每局比赛中,我们的移动将始终为X,而对手的将始终为O。 虽然,您的对手会看到他们的举动是X。 因为我们在不同的设备上(甚至在不同的国家/地区)玩游戏,所以我们实际上并不关心谁拥有XO。 这里重要的是每个玩家都知道哪些是他们的举动,哪些是对手的。

显示状态

我们快完成了! 还记得ui.js文件中的空status助手吗? 用以下代码填充它:

客户端/js/ui.js

status: () => {
  if(Session.get("inGame")) {
    let myGame = Games.findOne();

    if(myGame.status === "waiting")
      return "Looking for an opponent...";
    else if(myGame.status === Meteor.userId())
      return "Your turn";
    else if(myGame.status !== Meteor.userId() && myGame.status !== "end")
      return "opponent's turn";
    else if(myGame.result === Meteor.userId())
      return "You won!";
    else if(myGame.status === "end" && myGame.result !== Meteor.userId() && myGame.result !== "tie")
      return "You lost!";
    else if(myGame.result === "tie")
      return "It's a tie";
    else
      return "";
  }
}

这是很明显的,但我以防万一。 如果我们是inGame ,我们将寻找当前的游戏。 如果status等于“等待中”,我们告诉玩家等待对手。 如果status等于玩家的_id ,我们告诉他们轮到他们了。 如果status不是他们的_id并且比赛还没有结束,我们告诉他们轮到对手了。 如果结果等于玩家的_id ,我们告诉玩家他们赢了。 如果比赛结束,结果不是他们的_id ,也不是“平局”,那么他们输了。 如果结果等于“平局”,我们告诉他们这是平局………! ;)

现在,您可以试一下。 是! 继续打开一个普通的浏览器窗口和一个私有选项卡,与自己对战。 尽量不要过分开心,否则您将终生孤身一人(我发誓,这是对的)。

注销

Buuuuuut,我们尚未完成。 不! 如果我们断开连接并自己离开其他玩家怎么办? 所有这些完成的游戏如何填补我们数据库中的宝贵空间呢? 我们需要跟踪玩家的连接并采取相应的措施。

但是首先,我们需要一种删除游戏并从游戏中删除玩家的方法。 转到gamesLogic.js,并在GameLogic类中添加以下方法:

lib / gameLogic.js

removeGame(gameId) {
  Games.remove({_id: gameId});
}

removePlayer(gameId, player) {
  Games.update({_id: gameId}, {$set:{[player]: ""}});
}

removeGame方法将gameId作为参数并将其删除。
removePlayer()gameIdplayer (可以是player1player2的字符串)作为参数,并清空该特定游戏中该玩家的字段。

为了跟踪用户的连接,我们将安装一个有用的软件包mizzao:user-status 。 转到控制台,使用以下命令关闭正在运行的应用程序 ctrl + C 并运行以下命令:

meteor add mizzao:user-status

该程序包具有connectionLogout回调,该回调提供了一个参数,其中包含重要信息,例如断开连接的用户的userId

转到服务器文件夹中的main.js文件,并在底部添加以下回调。

/server/main.js

UserStatus.events.on("connectionLogout", (fields) => {
  const game = Games.findOne(
  {$or:[
    {player1: fields.userId},
    {player2: fields.userId}]
  });

  if(game != undefined) {
    if(game.status !== "waiting" && game.status !== "end") {
      if(game.player1 === fields.userId) {
        gameLogic.setGameResult(game._id, game.player2);
        gameLogic.removePlayer(game._id, "player1");
      } else if(game.player2 === fields.userId) {
        gameLogic.setGameResult(game._id, game.player1);
        gameLogic.removePlayer(game._id, "player2");
      }
    } else {
      if(game.player1 === "" || game.player2 === "") {
        gameLogic.removeGame(game._id);
      } else {
        if(game.player1 === fields.userId)
          gameLogic.removePlayer(game._id, "player1");
        else if(game.player2 === fields.userId)
          gameLogic.removePlayer(game._id, "player2");
      }
    } 
  }
});

所以,如果我们能找到一个游戏,断开播放器或者是player1player2 ,我们检查,如果那场比赛的状态不是“等待”,游戏还没有走到尽头。 如果有的话,我们将胜利给予对手并删除与之断开连接的玩家。 否则,我们将删除游戏(如果任何玩家字段为空)或。 如果不是这种情况,我们将从游戏中移除断开连接的玩家。

与其他软件包一样,我们应该导入UserStatus软件包。 我们还在connectionLogout回调中使用了GameLogic类中的一些方法,因此请继续将它们都导入server / main.js文件的顶部:

import { UserStatus } from 'meteor/mizzao:user-status';
import { gameLogic } from '../lib/gameLogic.js';

包起来

最后,您应该有一款可以正常工作的游戏! 照原样,您可以上传它,然后与您的朋友(或您自己)一起尝试。

如果您刚才所做的任何事情对您几乎没有意义,那就不用担心。 如果您继续研究代码,这将很快变得有意义。 您只需要一些时间来整理一些概念。 那是一个完全自然的过程。 如果您遇到困难,请不要忘记签出完整应用程序的代码

当您对代码足够满意时,应该开始尝试添加一些功能。 也许实施不同的获胜算法,可能会让您增加董事会的人数。 也许对玩家实施持久性以保存统计数据并保留游戏记录。 您甚至可以实现登录界面,并让玩家选择用户名。 挑战朋友呢? 当然,您也可以使用相同的概念来创建完全不同的游戏。

我希望知道您的想法,所以请告诉我! 我希望您喜欢本教程,在评论中留下您的疑问和评论。 下一个见!

From: https://www.sitepoint.com/building-multiplayer-tictactoe-game-with-meteor/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值