Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Π’Ρ‹ΡˆΠ΅Π΄ΡˆΠ°Ρ Π² 2015 Π³ΠΎΠ΄Ρƒ Agar.io стала ΠΏΡ€Π°Ρ€ΠΎΠ΄ΠΈΡ‚Π΅Π»Π΅ΠΌ Π½ΠΎΠ²ΠΎΠ³ΠΎ ΠΆΠ°Π½Ρ€Π° ΠΈΠ³Ρ€ .io, ΠΏΠΎΠΏΡƒΠ»ΡΡ€Π½ΠΎΡΡ‚ΡŒ ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ³ΠΎ с Ρ‚Π΅Ρ… ΠΏΠΎΡ€ сильно возросла. Рост популярности ΠΈΠ³Ρ€ .io я испытал Π½Π° сСбС: Π·Π° послСдниС Ρ‚Ρ€ΠΈ Π³ΠΎΠ΄Π° я создал ΠΈ ΠΏΡ€ΠΎΠ΄Π°Π» Π΄Π²Π΅ ΠΈΠ³Ρ€Ρ‹ этого ΠΆΠ°Π½Ρ€Π°..

На случай, Ссли Π²Ρ‹ Π½ΠΈΠΊΠΎΠ³Π΄Π° Ρ€Π°Π½ΡŒΡˆΠ΅ Π½Π΅ ΡΠ»Ρ‹ΡˆΠ°Π»ΠΈ ΠΎ Ρ‚Π°ΠΊΠΈΡ… ΠΈΠ³Ρ€Π°Ρ…: это бСсплатныС ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΈΠ΅ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹, Π² ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Ρ… Π»Π΅Π³ΠΊΠΎ ΡƒΡ‡Π°ΡΡ‚Π²ΠΎΠ²Π°Ρ‚ΡŒ (Π½Π΅ трСбуСтся учётная запись). ΠžΠ±Ρ‹Ρ‡Π½ΠΎ ΠΎΠ½ΠΈ ΡΡ‚Π°Π»ΠΊΠΈΠ²Π°ΡŽΡ‚ Π½Π° ΠΎΠ΄Π½ΠΎΠΉ Π°Ρ€Π΅Π½Π΅ мноТСство ΠΏΡ€ΠΎΡ‚ΠΈΠ²ΠΎΠ±ΠΎΡ€ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ². Π”Ρ€ΡƒΠ³ΠΈΠ΅ Π·Π½Π°ΠΌΠ΅Π½ΠΈΡ‚Ρ‹Π΅ ΠΈΠ³Ρ€Ρ‹ ΠΆΠ°Π½Ρ€Π° .io: Slither.io ΠΈ Diep.io.

Π’ этом постС ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π°Π·Π±ΠΈΡ€Π°Ρ‚ΡŒΡΡ, ΠΊΠ°ΠΊ с нуля ΡΠΎΠ·Π΄Π°Ρ‚ΡŒ ΠΈΠ³Ρ€Ρƒ .io. Для этого достаточно Π±ΡƒΠ΄Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ знания Javascript: Π²Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΏΠΎΠ½ΠΈΠΌΠ°Ρ‚ΡŒ Ρ‚Π°ΠΊΠΈΠ΅ Π²Π΅Ρ‰ΠΈ, ΠΊΠ°ΠΊ синтаксис ES6, ΠΊΠ»ΡŽΡ‡Π΅Π²ΠΎΠ΅ слово this ΠΈ Promises. Π”Π°ΠΆΠ΅ Ссли Π²Ρ‹ Π·Π½Π°Π΅Ρ‚Π΅ Javascript Π½Π΅ Π² ΡΠΎΠ²Π΅Ρ€ΡˆΠ΅Π½ΡΡ‚Π²Π΅, Ρ‚ΠΎ всё Ρ€Π°Π²Π½ΠΎ смоТСтС Ρ€Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒΡΡ Π² большСй части поста.

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ ΠΈΠ³Ρ€Ρ‹ .io

Для ΠΏΠΎΠΌΠΎΡ‰ΠΈ Π² ΠΎΠ±ΡƒΡ‡Π΅Π½ΠΈΠΈ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΡΡΡ‹Π»Π°Ρ‚ΡŒΡΡ Π½Π° ΠΏΡ€ΠΈΠΌΠ΅Ρ€ ΠΈΠ³Ρ€Ρ‹ .io. ΠŸΠΎΠΏΡ€ΠΎΠ±ΡƒΠΉΡ‚Π΅ Π² ΡΡ‹Π³Ρ€Π°Ρ‚ΡŒ Π² Π½Π΅Ρ‘!

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Π˜Π³Ρ€Π° довольно проста: Π²Ρ‹ управляСтС ΠΊΠΎΡ€Π°Π±Π»Ρ‘ΠΌ Π½Π° Π°Ρ€Π΅Π½Π΅, Π³Π΄Π΅ Π΅ΡΡ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΈΠ³Ρ€ΠΎΠΊΠΈ. Π’Π°Ρˆ ΠΊΠΎΡ€Π°Π±Π»ΡŒ автоматичСски стрСляСт снарядами ΠΈ Π²Ρ‹ ΠΏΡ‹Ρ‚Π°Π΅Ρ‚Π΅ΡΡŒ ΠΏΠΎΠΏΠ°ΡΡ‚ΡŒ Π² Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ², Π² Ρ‚ΠΎ ΠΆΠ΅ врСмя избСгая ΠΈΡ… снарядов.

1. ΠšΡ€Π°Ρ‚ΠΊΠΈΠΉ ΠΎΠ±Π·ΠΎΡ€/структура ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΡŽ ΡΠΊΠ°Ρ‡Π°Ρ‚ΡŒ исходный ΠΊΠΎΠ΄ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π° ΠΈΠ³Ρ€Ρ‹, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π²Ρ‹ ΠΌΠΎΠ³Π»ΠΈ ΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚ΡŒ Π·Π° ΠΌΠ½ΠΎΠΉ.

Π’ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π΅:

  • Express β€” самый популярный Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊ для Node.js, ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‰ΠΈΠΉ Π²Π΅Π±-сСрвСром ΠΈΠ³Ρ€Ρ‹.
  • socket.io β€” Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΠ° websocket для ΠΎΠ±ΠΌΠ΅Π½Π° Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΌΠ΅ΠΆΠ΄Ρƒ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ΠΎΠΌ ΠΈ сСрвСром.
  • Webpack β€” ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ. О Ρ‚ΠΎΠΌ, Π·Π°Ρ‡Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Webpack, ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°Ρ‚ΡŒ здСсь.

Π’ΠΎΡ‚ ΠΊΠ°ΠΊ выглядит структура ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π° ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°:

public/
    assets/
        ...
src/
    client/
        css/
            ...
        html/
            index.html
        index.js
        ...
    server/
        server.js
        ...
    shared/
        constants.js

public/

Всё Π² ΠΏΠ°ΠΏΠΊΠ΅ public/ Π±ΡƒΠ΄Π΅Ρ‚ статичСски ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒΡΡ сСрвСром. Π’ public/assets/ содСрТатся ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Π΅ нашим ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠΌ изобраТСния.

src/

Π’Π΅ΡΡŒ исходный ΠΊΠΎΠ΄ находится Π² ΠΏΠ°ΠΏΠΊΠ΅ src/. Названия client/ ΠΈ server/ говорят сами Π·Π° сСбя, Π° shared/ содСрТит Ρ„Π°ΠΉΠ» констант, ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌΡ‹ΠΉ ΠΈ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΠΎΠΌ, ΠΈ сСрвСром.

2. Π‘Π±ΠΎΡ€ΠΊΠΈ/ΠΏΠ°Ρ€Π°ΠΌΠ΅Ρ‚Ρ€Ρ‹ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°

Как сказано Π²Ρ‹ΡˆΠ΅, для сборки ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π° ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ ΠΌΠΎΠ΄ΡƒΠ»Π΅ΠΉ Webpack. Π”Π°Π²Π°ΠΉΡ‚Π΅ взглянСм Π½Π° Π½Π°ΡˆΡƒ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ Webpack:

webpack.common.js:

const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  entry: {
    game: './src/client/index.js',
  },
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ['@babel/preset-env'],
          },
        },
      },
      {
        test: /.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
          },
          'css-loader',
        ],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].[contenthash].css',
    }),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'src/client/html/index.html',
    }),
  ],
};

Π‘Π°ΠΌΡ‹ΠΌΠΈ Π²Π°ΠΆΠ½Ρ‹ΠΌΠΈ здСсь ΡΠ²Π»ΡΡŽΡ‚ΡΡ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ строки:

  • src/client/index.js β€” это входная Ρ‚ΠΎΡ‡ΠΊΠ° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° Javascript (JS). Webpack Π±ΡƒΠ΄Π΅Ρ‚ Π½Π°Ρ‡ΠΈΠ½Π°Ρ‚ΡŒ ΠΎΡ‚ΡΡŽΠ΄Π° ΠΈ станСт рСкурсивно ΠΈΡΠΊΠ°Ρ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Π½Ρ‹Π΅ Ρ„Π°ΠΉΠ»Ρ‹.
  • Π’Ρ‹Ρ…ΠΎΠ΄Π½ΠΎΠΉ JS нашСй сборки Webpack Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°ΡΠΏΠΎΠ»Π°Π³Π°Ρ‚ΡŒΡΡ Π² ΠΊΠ°Ρ‚Π°Π»ΠΎΠ³Π΅ dist/. Π― Π±ΡƒΠ΄Ρƒ Π½Π°Π·Ρ‹Π²Π°Ρ‚ΡŒ этот Ρ„Π°ΠΉΠ» нашим ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ JS.
  • ΠœΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Babel, ΠΈ Π² частности ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡŽ @babel/preset-env для транспиляции (transpiling) нашСго ΠΊΠΎΠ΄Π° JS для старых Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ΠΎΠ².
  • ΠœΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΏΠ»Π°Π³ΠΈΠ½ для извлСчСния всСх CSS, Π½Π° ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΡΡΡ‹Π»Π°ΡŽΡ‚ΡΡ Ρ„Π°ΠΉΠ»Ρ‹ JS, ΠΈ для объСдинСния ΠΈΡ… Π² ΠΎΠ΄Π½ΠΎΠΌ мСстС. Π― Π±ΡƒΠ΄Ρƒ Π½Π°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π΅Π³ΠΎ нашим ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠΌ CSS.

Π’Ρ‹ ΠΌΠΎΠ³Π»ΠΈ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚ΡŒ странныС ΠΈΠΌΠ΅Π½Π° Ρ„Π°ΠΉΠ»ΠΎΠ² ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠ² '[name].[contenthash].ext'. Π’ Π½ΠΈΡ… содСрТатся подстановки ΠΈΠΌΡ‘Π½ Ρ„Π°ΠΉΠ»ΠΎΠ² Webpack: [name] Π±ΡƒΠ΄Π΅Ρ‚ Π·Π°ΠΌΠ΅Π½Ρ‘Π½ Π½Π° имя Π²Ρ…ΠΎΠ΄Π½ΠΎΠΉ Ρ‚ΠΎΡ‡ΠΊΠΈ (Π² нашСм случаС это game), Π° [contenthash] Π±ΡƒΠ΄Π΅Ρ‚ Π·Π°ΠΌΠ΅Π½Ρ‘Π½ Π½Π° Ρ…Π΅Ρˆ содСрТимого Ρ„Π°ΠΉΠ»Π°. ΠœΡ‹ Π΄Π΅Π»Π°Π΅ΠΌ это, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ для Ρ…Π΅ΡˆΠΈΡ€ΠΎΠ²Π°Π½ΠΈΡ β€” ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΈΠΊΠ°Π·Π°Ρ‚ΡŒ Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π°ΠΌ бСсконСчно ΠΊΠ΅ΡˆΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ наши ΠΏΠ°ΠΊΠ΅Ρ‚Ρ‹ JS, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ Ссли ΠΏΠ°ΠΊΠ΅Ρ‚ измСняСтся, Ρ‚ΠΎ мСняСтся ΠΈ Π΅Π³ΠΎ имя Ρ„Π°ΠΉΠ»Π° (измСняСтся contenthash). Π“ΠΎΡ‚ΠΎΠ²Ρ‹ΠΌ Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚ΠΎΠΌ Π±ΡƒΠ΄Π΅Ρ‚ имя Ρ„Π°ΠΉΠ»Π° Π²ΠΈΠ΄Π° game.dbeee76e91a97d0c7207.js.

Π€Π°ΠΉΠ» webpack.common.js β€” это Π±Π°Π·ΠΎΠ²Ρ‹ΠΉ Ρ„Π°ΠΉΠ» ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΡ‹ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΡƒΠ΅ΠΌ Π² ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΈ Π³ΠΎΡ‚ΠΎΠ²ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚Π°. Π’ΠΎΡ‚, Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, конфигурация Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ:

webpack.dev.js

const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
});

Для эффСктивности ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π² процСссС Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ webpack.dev.js, ΠΈ ΠΏΠ΅Ρ€Π΅ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π½Π° webpack.prod.js, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΎΠΏΡ‚ΠΈΠΌΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ€Π°Π·ΠΌΠ΅Ρ€Ρ‹ ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠ² ΠΏΡ€ΠΈ Ρ€Π°Π·Π²Ρ‘Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠΈ Π² ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½.

Π›ΠΎΠΊΠ°Π»ΡŒΠ½Π°Ρ настройка

Π Π΅ΠΊΠΎΠΌΠ΅Π½Π΄ΡƒΡŽ ΡƒΡΡ‚Π°Π½Π°Π²Π»ΠΈΠ²Π°Ρ‚ΡŒ ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ Π½Π° локальной машинС, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π²Ρ‹ ΠΌΠΎΠ³Π»ΠΈ ΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚ΡŒ Π·Π° этапами, пСрСчислСнными Π² этом постС. Настройка проста: Π²ΠΎ-ΠΏΠ΅Ρ€Π²Ρ‹Ρ…, Π² систСмС Π΄ΠΎΠ»ΠΆΠ½Ρ‹ Π±Ρ‹Ρ‚ΡŒ установлСны Node ΠΈ NPM. Π”Π°Π»Π΅Π΅ Π½ΡƒΠΆΠ½ΠΎ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ

$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install

ΠΈ Π²Ρ‹ Π³ΠΎΡ‚ΠΎΠ²Ρ‹ ΠΊ Ρ€Π°Π±ΠΎΡ‚Π΅! Для запуска сСрвСра Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ достаточно Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ

$ npm run develop

ΠΈ Π·Π°ΠΉΡ‚ΠΈ Π² Π²Π΅Π±-Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅ Π½Π° localhost:3000. Π‘Π΅Ρ€Π²Π΅Ρ€ Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π±ΡƒΠ΄Π΅Ρ‚ автоматичСски ΠΏΠ΅Ρ€Π΅ΡΠΎΠ±ΠΈΡ€Π°Ρ‚ΡŒ Π·Π°Π½ΠΎΠ²ΠΎ ΠΏΠ°ΠΊΠ΅Ρ‚Ρ‹ JS ΠΈ CSS Π² процСссС измСнСния ΠΊΠΎΠ΄Π° β€” просто ΠΎΠ±Π½ΠΎΠ²ΠΈΡ‚Π΅ страницу, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΡƒΠ²ΠΈΠ΄Π΅Ρ‚ΡŒ всС измСнСния!

3. Π’Ρ…ΠΎΠ΄Π½Ρ‹Π΅ Ρ‚ΠΎΡ‡ΠΊΠΈ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

Π”Π°Π²Π°ΠΉΡ‚Π΅ приступим ΠΊ самому ΠΊΠΎΠ΄Ρƒ ΠΈΠ³Ρ€Ρ‹. Для Π½Π°Ρ‡Π°Π»Π° Π½Π°ΠΌ потрСбуСтся страница index.html, ΠΏΡ€ΠΈ посСщСнии сайта Π±Ρ€Π°ΡƒΠ·Π΅Ρ€ Π±ΡƒΠ΄Π΅Ρ‚ Π·Π°Π³Ρ€ΡƒΠΆΠ°Ρ‚ΡŒ Π΅Ρ‘ ΠΏΠ΅Ρ€Π²ΠΎΠΉ. Наша страница Π±ΡƒΠ΄Π΅Ρ‚ довольно простой:

index.html

<!DOCTYPE html>
<html>
<head>
  <title>An example .io game</title>
  <link type="text/css" rel="stylesheet" href="/game.bundle.css">
</head>
<body>
  <canvas id="game-canvas"></canvas>
  <script async src="/game.bundle.js"></script>
  <div id="play-menu" class="hidden">
    <input type="text" id="username-input" placeholder="Username" />
    <button id="play-button">PLAY</button>
  </div>
</body>
</html>

Π­Ρ‚ΠΎΡ‚ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ ΠΊΠΎΠ΄Π° слСгка ΡƒΠΏΡ€ΠΎΡ‰Ρ‘Π½ для понятности, Ρ‚ΠΎ ΠΆΠ΅ самоС я сдСлаю ΠΈ со ΠΌΠ½ΠΎΠ³ΠΈΠΌΠΈ Π΄Ρ€ΡƒΠ³ΠΈΠΌΠΈ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π°ΠΌΠΈ поста. ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΊΠΎΠ΄ всСгда ΠΌΠΎΠΆΠ½ΠΎ ΠΏΠΎΡΠΌΠΎΡ‚Ρ€Π΅Ρ‚ΡŒ Π½Π° Github.

Π£ нас Π΅ΡΡ‚ΡŒ:

  • Π­Π»Π΅ΠΌΠ΅Π½Ρ‚ HTML5 Canvas (<canvas>), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ для Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° ΠΈΠ³Ρ€Ρ‹.
  • <link> для добавлСния нашСго ΠΏΠ°ΠΊΠ΅Ρ‚Π° CSS.
  • <script> для добавлСния нашСго ΠΏΠ°ΠΊΠ΅Ρ‚Π° Javascript.
  • Π“Π»Π°Π²Π½ΠΎΠ΅ мСню с ΠΈΠΌΠ΅Π½Π΅ΠΌ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ <input> ΠΈ ΠΊΠ½ΠΎΠΏΠΊΠΎΠΉ Β«PLAYΒ» (<button>).

ПослС Π·Π°Π³Ρ€ΡƒΠ·ΠΊΠΈ домашнСй страницы Π² Π±Ρ€Π°ΡƒΠ·Π΅Ρ€Π΅ Π½Π°Ρ‡Π½Ρ‘Ρ‚ Π²Ρ‹ΠΏΠΎΠ»Π½ΡΡ‚ΡŒΡΡ Javascript-ΠΊΠΎΠ΄, начиная с Ρ„Π°ΠΉΠ»Π° JS Π²Ρ…ΠΎΠ΄Π½ΠΎΠΉ Ρ‚ΠΎΡ‡ΠΊΠΈ: src/client/index.js.

index.js

import { connect, play } from './networking';
import { startRendering, stopRendering } from './render';
import { startCapturingInput, stopCapturingInput } from './input';
import { downloadAssets } from './assets';
import { initState } from './state';
import { setLeaderboardHidden } from './leaderboard';

import './css/main.css';

const playMenu = document.getElementById('play-menu');
const playButton = document.getElementById('play-button');
const usernameInput = document.getElementById('username-input');

Promise.all([
  connect(),
  downloadAssets(),
]).then(() => {
  playMenu.classList.remove('hidden');
  usernameInput.focus();
  playButton.onclick = () => {
    // Play!
    play(usernameInput.value);
    playMenu.classList.add('hidden');
    initState();
    startCapturingInput();
    startRendering();
    setLeaderboardHidden(false);
  };
});

Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΠΎΠΊΠ°Π·Π°Ρ‚ΡŒΡΡ слоТным, Π½ΠΎ Π½Π° самом Π΄Π΅Π»Π΅ здСсь происходит Π½Π΅ Ρ‚Π°ΠΊ ΠΌΠ½ΠΎΠ³ΠΎ дСйствий:

  1. Π˜ΠΌΠΏΠΎΡ€Ρ‚ Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Π΄Ρ€ΡƒΠ³ΠΈΡ… JS-Ρ„Π°ΠΉΠ»ΠΎΠ².
  2. Π˜ΠΌΠΏΠΎΡ€Ρ‚ CSS (Ρ‡Ρ‚ΠΎΠ±Ρ‹ Webpack Π·Π½Π°Π», Ρ‡Ρ‚ΠΎ Π½ΡƒΠΆΠ½ΠΎ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ ΠΈΡ… Π² наш ΠΏΠ°ΠΊΠ΅Ρ‚ CSS).
  3. Запуск connect() для установки соСдинСния с сСрвСром ΠΈ запуск downloadAssets() для скачивания ΠΈΠ·ΠΎΠ±Ρ€Π°ΠΆΠ΅Π½ΠΈΠΉ, Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹Ρ… для Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° ΠΈΠ³Ρ€Ρ‹.
  4. ПослС Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ этапа 3 отобраТаСтся Π³Π»Π°Π²Π½ΠΎΠ΅ мСню (playMenu).
  5. Настройка ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠ° наТатия ΠΊΠ½ΠΎΠΏΠΊΠΈ Β«PLAYΒ». ΠŸΡ€ΠΈ Π½Π°ΠΆΠ°Ρ‚ΠΈΠΈ ΠΊΠ½ΠΎΠΏΠΊΠΈ ΠΊΠΎΠ΄ ΠΈΠ½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ ΠΈΠ³Ρ€Ρƒ ΠΈ сообщаСт сСрвСру, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Π³ΠΎΡ‚ΠΎΠ²Ρ‹ ΠΈΠ³Ρ€Π°Ρ‚ΡŒ.

ОсновноС «мясо» нашСй ΠΊΠ»ΠΈΠ΅Π½Ρ‚-сСрвСрной Π»ΠΎΠ³ΠΈΠΊΠΈ находится Π² Ρ‚Π΅Ρ… Ρ„Π°ΠΉΠ»Π°Ρ…, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Π±Ρ‹Π»ΠΈ ΠΈΠΌΠΏΠΎΡ€Ρ‚ΠΈΡ€ΠΎΠ²Π°Π½Ρ‹ Ρ„Π°ΠΉΠ»ΠΎΠΌ index.js. БСйчас ΠΌΡ‹ рассмотрим ΠΈΡ… всС ΠΏΠΎ порядку.

4. ОбмСн Π΄Π°Π½Π½Ρ‹ΠΌΠΈ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

Π’ этой ΠΈΠ³Ρ€Π΅ для общСния с сСрвСром ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Ρ…ΠΎΡ€ΠΎΡˆΠΎ ΠΈΠ·Π²Π΅ΡΡ‚Π½ΡƒΡŽ Π±ΠΈΠ±Π»ΠΈΠΎΡ‚Π΅ΠΊΡƒ socket.io. Π’ Socket.io Π΅ΡΡ‚ΡŒ встроСнная ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠ° WebSockets, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ Ρ…ΠΎΡ€ΠΎΡˆΠΎ подходят для двустороннСй ΠΊΠΎΠΌΠΌΡƒΠ½ΠΈΠΊΠ°Ρ†ΠΈΠΈ: ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ сообщСния сСрвСру ΠΈ сСрвСр ΠΌΠΎΠΆΠ΅Ρ‚ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ сообщСния Π½Π°ΠΌ ΠΏΠΎ Ρ‚ΠΎΠΌΡƒ ΠΆΠ΅ соСдинСнию.

Π£ нас Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΠ΄ΠΈΠ½ Ρ„Π°ΠΉΠ» src/client/networking.js, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ займётся всСми коммуникациями с сСрвСром:

networking.js

import io from 'socket.io-client';
import { processGameUpdate } from './state';

const Constants = require('../shared/constants');

const socket = io(`ws://${window.location.host}`);
const connectedPromise = new Promise(resolve => {
  socket.on('connect', () => {
    console.log('Connected to server!');
    resolve();
  });
});

export const connect = onGameOver => (
  connectedPromise.then(() => {
    // Register callbacks
    socket.on(Constants.MSG_TYPES.GAME_UPDATE, processGameUpdate);
    socket.on(Constants.MSG_TYPES.GAME_OVER, onGameOver);
  })
);

export const play = username => {
  socket.emit(Constants.MSG_TYPES.JOIN_GAME, username);
};

export const updateDirection = dir => {
  socket.emit(Constants.MSG_TYPES.INPUT, dir);
};

Π­Ρ‚ΠΎΡ‚ ΠΊΠΎΠ΄ для понятности Ρ‚ΠΎΠΆΠ΅ слСгка сокращён.

Π’ этом Ρ„Π°ΠΉΠ»Π΅ происходят Ρ‚Ρ€ΠΈ основных дСйствия:

  • ΠœΡ‹ ΠΏΡ€ΠΎΠ±ΡƒΠ΅ΠΌ ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒΡΡ ΠΊ сСрвСру. connectedPromise Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅Ρ‚ΡΡ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‚ΠΎΠ³Π΄Π°, ΠΊΠΎΠ³Π΄Π° ΠΌΡ‹ установили соСдинСниС.
  • Если соСдинСниС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎ установлСно, ΠΌΡ‹ рСгистрируСм callback-Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ (processGameUpdate() ΠΈ onGameOver()) для сообщСний, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΎΡ‚ сСрвСра.
  • ЭкспортируСм play() ΠΈ updateDirection(), Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΈΡ… ΠΌΠΎΠ³Π»ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ Ρ„Π°ΠΉΠ»Ρ‹.

5. Π Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

Настало врСмя ΠΎΡ‚ΠΎΠ±Ρ€Π°Π·ΠΈΡ‚ΡŒ Π½Π° экранС ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ!

…но ΠΏΡ€Π΅ΠΆΠ΄Π΅ Ρ‡Π΅ΠΌ ΠΌΡ‹ смоТСм это ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ, Π½ΡƒΠΆΠ½ΠΎ ΡΠΊΠ°Ρ‡Π°Ρ‚ΡŒ всС изобраТСния (рСсурсы), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ для этого Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΡ‹. Π”Π°Π²Π°ΠΉΡ‚Π΅ напишСм ΠΌΠ΅Π½Π΅Π΄ΠΆΠ΅Ρ€ рСсурсов:

assets.js

const ASSET_NAMES = ['ship.svg', 'bullet.svg'];

const assets = {};
const downloadPromise = Promise.all(ASSET_NAMES.map(downloadAsset));

function downloadAsset(assetName) {
  return new Promise(resolve => {
    const asset = new Image();
    asset.onload = () => {
      console.log(`Downloaded ${assetName}`);
      assets[assetName] = asset;
      resolve();
    };
    asset.src = `/assets/${assetName}`;
  });
}

export const downloadAssets = () => downloadPromise;
export const getAsset = assetName => assets[assetName];

Π£ΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ рСсурсами Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π½Π΅ Ρ‚Π°ΠΊ слоТно! Основной смысл Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ assets, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΡ€ΠΈΠ²ΡΠ·Ρ‹Π²Π°Ρ‚ΡŒ ΠΊΠ»ΡŽΡ‡ ΠΈΠΌΠ΅Π½ΠΈ Ρ„Π°ΠΉΠ»Π° ΠΊ Π·Π½Π°Ρ‡Π΅Π½ΠΈΡŽ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π° Image. Когда рСсурс загрузится, ΠΌΡ‹ сохраняСм Π΅Π³ΠΎ Π² ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ assets для быстрого получСния Π² Π±ΡƒΠ΄ΡƒΡ‰Π΅ΠΌ. Когда Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π°Π·Ρ€Π΅ΡˆΠ΅Π½ΠΎ скачиваниС ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ рСсурса (Ρ‚ΠΎ Π΅ΡΡ‚ΡŒ Π±ΡƒΠ΄ΡƒΡ‚ Π·Π°Π³Ρ€ΡƒΠΆΠ΅Π½Ρ‹ всС рСсурсы), ΠΌΡ‹ Ρ€Π°Π·Ρ€Π΅ΡˆΠ°Π΅ΠΌ downloadPromise.

Π‘ΠΊΠ°Ρ‡Π°Π² рСсурсы, ΠΌΠΎΠΆΠ½ΠΎ ΠΏΡ€ΠΈΡΡ‚ΡƒΠΏΠ°Ρ‚ΡŒ ΠΊ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Ρƒ. Как сказано Ρ€Π°Π½Π΅Π΅, для рисования Π½Π° Π²Π΅Π±-страницС ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ HTML5 Canvas (<canvas>). Наша ΠΈΠ³Ρ€Π° довольно проста, поэтому Π½Π°ΠΌ достаточно ΠΎΡ‚Ρ€ΠΈΡΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅Π΅:

  1. Π€ΠΎΠ½
  2. ΠšΠΎΡ€Π°Π±Π»ΡŒ ΠΈΠ³Ρ€ΠΎΠΊΠ°
  3. Π”Ρ€ΡƒΠ³ΠΈΡ… ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ², находящихся Π² ΠΈΠ³Ρ€Π΅
  4. Бнаряды

Π’ΠΎΡ‚ Π²Π°ΠΆΠ½Ρ‹Π΅ Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚Ρ‹ src/client/render.js, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΎΡ‚Ρ€ΠΈΡΠΎΠ²Ρ‹Π²Π°ΡŽΡ‚ ΠΈΠΌΠ΅Π½Π½ΠΎ пСрСчислСнныС Π²Ρ‹ΡˆΠ΅ Ρ‡Π΅Ρ‚Ρ‹Ρ€Π΅ ΠΏΡƒΠ½ΠΊΡ‚Π°:

render.js

import { getAsset } from './assets';
import { getCurrentState } from './state';

const Constants = require('../shared/constants');
const { PLAYER_RADIUS, PLAYER_MAX_HP, BULLET_RADIUS, MAP_SIZE } = Constants;

// Get the canvas graphics context
const canvas = document.getElementById('game-canvas');
const context = canvas.getContext('2d');

// Make the canvas fullscreen
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

function render() {
  const { me, others, bullets } = getCurrentState();
  if (!me) {
    return;
  }

  // Draw background
  renderBackground(me.x, me.y);

  // Draw all bullets
  bullets.forEach(renderBullet.bind(null, me));

  // Draw all players
  renderPlayer(me, me);
  others.forEach(renderPlayer.bind(null, me));
}

// ... Helper functions here excluded

let renderInterval = null;
export function startRendering() {
  renderInterval = setInterval(render, 1000 / 60);
}
export function stopRendering() {
  clearInterval(renderInterval);
}

Π­Ρ‚ΠΎΡ‚ ΠΊΠΎΠ΄ Ρ‚ΠΎΠΆΠ΅ сокращён для понятности.

render() β€” основная функция этого Ρ„Π°ΠΉΠ»Π°. startRendering() ΠΈ stopRendering() ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‚ Π°ΠΊΡ‚ΠΈΠ²Π°Ρ†ΠΈΠ΅ΠΉ Ρ†ΠΈΠΊΠ»ΠΎΠΌ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° с частотой 60 FPS.

ΠšΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹Π΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½Ρ‹Ρ… Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΉ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€ renderBullet()) Π½Π΅ Ρ‚Π°ΠΊ Π²Π°ΠΆΠ½Ρ‹, Π½ΠΎ Π²ΠΎΡ‚ ΠΎΠ΄ΠΈΠ½ простой ΠΏΡ€ΠΈΠΌΠ΅Ρ€:

render.js

function renderBullet(me, bullet) {
  const { x, y } = bullet;
  context.drawImage(
    getAsset('bullet.svg'),
    canvas.width / 2 + x - me.x - BULLET_RADIUS,
    canvas.height / 2 + y - me.y - BULLET_RADIUS,
    BULLET_RADIUS * 2,
    BULLET_RADIUS * 2,
  );
}

Π—Π°ΠΌΠ΅Ρ‚ΡŒΡ‚Π΅, Ρ‡Ρ‚ΠΎ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ ΠΌΠ΅Ρ‚ΠΎΠ΄ getAsset(), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Ρ€Π°Π½Π΅Π΅ Π²ΠΈΠ΄Π΅Π»ΠΈ Π² asset.js!

Если Π²Π°ΠΌ интСрСсно ΠΈΠ·ΡƒΡ‡ΠΈΡ‚ΡŒ Π΄Ρ€ΡƒΠ³ΠΈΠ΅ Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°, Ρ‚ΠΎ ΠΏΡ€ΠΎΡ‡ΠΈΡ‚Π°ΠΉΡ‚Π΅ ΠΎΡΡ‚Π°Π²ΡˆΡƒΡŽΡΡ Ρ‡Π°ΡΡ‚ΡŒ src/client/render.js.

6. ΠšΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΠΈΠΉ Π²Π²ΠΎΠ΄

Настало врСмя ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ ΠΈΠ³Ρ€Ρƒ ΠΈΠ³Ρ€Π°Π±Π΅Π»ΡŒΠ½ΠΎΠΉ! Π‘Ρ…Π΅ΠΌΠ° управлСния Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‡Π΅Π½ΡŒ простой: для измСнСния направлСния двиТСния ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΡ‹ΡˆΡŒ (Π½Π° ΠΊΠΎΠΌΠΏΡŒΡŽΡ‚Π΅Ρ€Π΅) ΠΈΠ»ΠΈ касаниС экрана (Π½Π° мобильном устройствС). Π§Ρ‚ΠΎΠ±Ρ‹ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ это, ΠΌΡ‹ зарСгистрируСм Event Listeners для событий Mouse ΠΈ Touch.
ВсСм этим займётся src/client/input.js:

input.js

import { updateDirection } from './networking';

function onMouseInput(e) {
  handleInput(e.clientX, e.clientY);
}

function onTouchInput(e) {
  const touch = e.touches[0];
  handleInput(touch.clientX, touch.clientY);
}

function handleInput(x, y) {
  const dir = Math.atan2(x - window.innerWidth / 2, window.innerHeight / 2 - y);
  updateDirection(dir);
}

export function startCapturingInput() {
  window.addEventListener('mousemove', onMouseInput);
  window.addEventListener('touchmove', onTouchInput);
}

export function stopCapturingInput() {
  window.removeEventListener('mousemove', onMouseInput);
  window.removeEventListener('touchmove', onTouchInput);
}

onMouseInput() ΠΈ onTouchInput() β€” это Event Listeners, Π²Ρ‹Π·Ρ‹Π²Π°ΡŽΡ‰ΠΈΠ΅ updateDirection() (ΠΈΠ· networking.js) ΠΏΡ€ΠΈ ΡΠΎΠ²Π΅Ρ€ΡˆΠ΅Π½ΠΈΠΈ события Π²Π²ΠΎΠ΄Π° (Π½Π°ΠΏΡ€ΠΈΠΌΠ΅Ρ€, ΠΏΡ€ΠΈ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Ρ‰Π΅Π½ΠΈΠΈ ΠΌΡ‹ΡˆΠΈ). updateDirection() занимаСтся ΠΎΠ±ΠΌΠ΅Π½ΠΎΠΌ сообщСниями с сСрвСром, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅Ρ‚ событиС Π²Π²ΠΎΠ΄Π° ΠΈ ΡΠΎΠΎΡ‚Π²Π΅Ρ‚ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ обновляСт состояниС ΠΈΠ³Ρ€Ρ‹.

7. БостояниС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

Π­Ρ‚ΠΎΡ‚ Ρ€Π°Π·Π΄Π΅Π» β€” самый слоТный Π² ΠΏΠ΅Ρ€Π²ΠΎΠΉ части поста. НС Ρ€Π°ΡΡΡ‚Ρ€Π°ΠΈΠ²Π°ΠΉΡ‚Π΅ΡΡŒ, Ссли Π½Π΅ ΠΏΠΎΠΉΠΌΡ‘Ρ‚Π΅ Π΅Π³ΠΎ с ΠΏΠ΅Ρ€Π²ΠΎΠ³ΠΎ прочтСния! ΠœΠΎΠΆΠ΅Ρ‚Π΅ Π΄Π°ΠΆΠ΅ ΠΏΡ€ΠΎΠΏΡƒΡΡ‚ΠΈΡ‚ΡŒ Π΅Π³ΠΎ ΠΈ Π²Π΅Ρ€Π½ΡƒΡ‚ΡŒΡΡ ΠΊ Π½Π΅ΠΌΡƒ ΠΏΠΎΠ·ΠΆΠ΅.

ПослСдний кусок ΠΏΠ°Π·Π»Π°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π½ΡƒΠΆΠ΅Π½ для Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ ΠΊΠ»ΠΈΠ΅Π½Ρ‚-сСрвСрного ΠΊΠΎΠ΄Π° β€” это state. ΠŸΠΎΠΌΠ½ΠΈΡ‚Π΅ Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ ΠΊΠΎΠ΄Π° ΠΈΠ· Ρ€Π°Π·Π΄Π΅Π»Π° Β«Π Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°Β»?

render.js

import { getCurrentState } from './state';

function render() {
  const { me, others, bullets } = getCurrentState();

  // Do the rendering
  // ...
}

getCurrentState() Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ Π²ΠΎΠ·ΠΌΠΎΠΆΠ½ΠΎΡΡ‚ΡŒ ΠΏΡ€Π΅Π΄ΠΎΡΡ‚Π°Π²ΠΈΡ‚ΡŒ Π½Π°ΠΌ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ состояниС ΠΈΠ³Ρ€Ρ‹ Π² ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ Π² любой ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π½Π° основании ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ, ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌΡ‹Ρ… ΠΎΡ‚ сСрвСра. Π’ΠΎΡ‚ ΠΏΡ€ΠΈΠΌΠ΅Ρ€ обновлСния ΠΈΠ³Ρ€Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ сСрвСр:

{
  "t": 1555960373725,
  "me": {
    "x": 2213.8050880413657,
    "y": 1469.370893425012,
    "direction": 1.3082443894581433,
    "id": "AhzgAtklgo2FJvwWAADO",
    "hp": 100
  },
  "others": [],
  "bullets": [
    {
      "id": "RUJfJ8Y18n",
      "x": 2354.029197099604,
      "y": 1431.6848318262666
    },
    {
      "id": "ctg5rht5s",
      "x": 2260.546457727445,
      "y": 1456.8088728920968
    }
  ],
  "leaderboard": [
    {
      "username": "Player",
      "score": 3
    }
  ]
}

КаТдоС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ содСрТит ΠΏΡΡ‚ΡŒ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹Ρ… ΠΏΠΎΠ»Π΅ΠΉ:

  • t: ΠΌΠ΅Ρ‚ΠΊΠ° Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ сСрвСра, ΠΎΠ±ΠΎΠ·Π½Π°Ρ‡Π°ΡŽΡ‰Π°Ρ ΠΌΠΎΠΌΠ΅Π½Ρ‚ создания этого обновлСния.
  • me: информация ΠΎΠ± ΠΈΠ³Ρ€ΠΎΠΊΠ΅, ΠΏΠΎΠ»ΡƒΡ‡Π°ΡŽΡ‰Π΅Π³ΠΎ это ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅.
  • others: массив ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ Π΄Ρ€ΡƒΠ³ΠΈΡ… ΠΈΠ³Ρ€ΠΎΠΊΠ°Ρ…, ΡƒΡ‡Π°ΡΡ‚Π²ΡƒΡŽΡ‰ΠΈΡ… Π² Ρ‚ΠΎΠΉ ΠΆΠ΅ ΠΈΠ³Ρ€Π΅.
  • bullets: массив ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΠΈ ΠΎ снарядах Π² ΠΈΠ³Ρ€Π΅.
  • leaderboard: Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Π΅ Ρ‚Π°Π±Π»ΠΈΡ†Ρ‹ Π»ΠΈΠ΄Π΅Ρ€ΠΎΠ². Π’ этом постС ΠΌΡ‹ ΠΈΡ… ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°Ρ‚ΡŒ Π½Π΅ Π±ΡƒΠ΄Π΅ΠΌ.

7.1 НаивноС состояниС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

Наивная рСализация getCurrentState() ΠΌΠΎΠΆΠ΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ нСпосрСдствСнно Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ Π΄Π°Π½Π½Ρ‹Π΅ самого послСднСго ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½ΠΎΠ³ΠΎ обновлСния ΠΈΠ³Ρ€Ρ‹.

naive-state.js

let lastGameUpdate = null;

// Handle a newly received game update.
export function processGameUpdate(update) {
  lastGameUpdate = update;
}

export function getCurrentState() {
  return lastGameUpdate;
}

ΠšΡ€Π°ΡΠΈΠ²ΠΎ ΠΈ понятно! Но Ссли Π±Ρ‹ всё Π±Ρ‹Π»ΠΎ Ρ‚Π°ΠΊ просто. Одна ΠΈΠ· ΠΏΡ€ΠΈΡ‡ΠΈΠ½, ΠΏΠΎ ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΌ такая рСализация ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°Ρ‚ΠΈΡ‡Π½Π°: ΠΎΠ½Π° ΠΎΠ³Ρ€Π°Π½ΠΈΡ‡ΠΈΠ²Π°Π΅Ρ‚ частоту ΠΊΠ°Π΄Ρ€ΠΎΠ² Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° частотой Ρ‚Π°ΠΊΡ‚ΠΎΠ² сСрвСра.

Частота ΠΊΠ°Π΄Ρ€ΠΎΠ² (Frame Rate): количСство ΠΊΠ°Π΄Ρ€ΠΎΠ² (Ρ‚.Π΅. Π²Ρ‹Π·ΠΎΠ²ΠΎΠ² render()) Π² сСкунду, ΠΈΠ»ΠΈ FPS. Π’ ΠΈΠ³Ρ€Π°Ρ… ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ стрСмятся Π΄ΠΎΡΡ‚ΠΈΡ‡ΡŒ Π½Π΅ ΠΌΠ΅Π½Π΅Π΅ 60 FPS.

Частота Ρ‚Π°ΠΊΡ‚ΠΎΠ² (Tick Rate): частота, с ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΉ сСрвСр отправляСт обновлСния ΠΈΠ³Ρ€Ρ‹ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°ΠΌ. Часто ΠΎΠ½Π° Π½ΠΈΠΆΠ΅, Ρ‡Π΅ΠΌ частота ΠΊΠ°Π΄Ρ€ΠΎΠ². Π’ нашСй ΠΈΠ³Ρ€Π΅ сСрвСр Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ с частотой 30 Ρ‚Π°ΠΊΡ‚ΠΎΠ² Π² сСкунду.

Если ΠΌΡ‹ просто Π±ΡƒΠ΄Π΅ΠΌ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ΡŒ послСднСС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹, Ρ‚ΠΎ FPS ΠΏΠΎ сути Π½ΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ смоТСт ΠΏΡ€Π΅Π²Ρ‹ΡΠΈΡ‚ΡŒ 30, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ ΠΌΡ‹ Π½ΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌ ΠΎΡ‚ сСрвСра большС 30 ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ Π² сСкунду. Π”Π°ΠΆΠ΅ Ссли ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ Π²Ρ‹Π·Ρ‹Π²Π°Ρ‚ΡŒ render() 60 Ρ€Π°Π· Π² сСкунду, Ρ‚ΠΎ ΠΏΠΎΠ»ΠΎΠ²ΠΈΠ½Π° этих Π²Ρ‹Π·ΠΎΠ²ΠΎΠ² Π±ΡƒΠ΄Π΅Ρ‚ просто ΠΏΠ΅Ρ€Π΅Ρ€ΠΈΡΠΎΠ²Ρ‹Π²Π°Ρ‚ΡŒ Ρ‚ΠΎ ΠΆΠ΅ самоС, ΠΏΠΎ сути Π½Π΅ дСлая Π½ΠΈΡ‡Π΅Π³ΠΎ. Π•Ρ‰Ρ‘ ΠΎΠ΄Π½Π° ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ° Π½Π°ΠΈΠ²Π½ΠΎΠΉ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² Ρ‚ΠΎΠΌ, Ρ‡Ρ‚ΠΎ ΠΎΠ½Π° ΠΏΠΎΠ΄Π²Π΅Ρ€ΠΆΠ΅Π½Π° Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ°ΠΌ. ΠŸΡ€ΠΈ идСальной скорости Π˜Π½Ρ‚Π΅Ρ€Π½Π΅Ρ‚Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚ Π±ΡƒΠ΄Π΅Ρ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ Ρ€ΠΎΠ²Π½ΠΎ Ρ‡Π΅Ρ€Π΅Π· ΠΊΠ°ΠΆΠ΄Ρ‹Π΅ 33 мс (30 Π² сСкунду):

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
К соТалСнию, Π½ΠΈΡ‡Ρ‚ΠΎ Π½Π΅ идСально. Π‘ΠΎΠ»Π΅Π΅ рСалистичной Π±ΡƒΠ΄Π΅Ρ‚ такая ΠΊΠ°Ρ€Ρ‚ΠΈΠ½Π°:
Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Наивная рСализация β€” это практичСски Π½Π°ΠΈΡ…ΡƒΠ΄ΡˆΠΈΠΉ случай, ΠΊΠΎΠ³Π΄Π° Π΄Π΅Π»ΠΎ Π΄ΠΎΡ…ΠΎΠ΄ΠΈΡ‚ Π΄ΠΎ Π·Π°Π΄Π΅Ρ€ΠΆΠ΅ΠΊ. Если ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ принимаСтся с Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ 50 мс, Ρ‚ΠΎ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ затормаТиваСтся Π½Π° лишниС 50 мс, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ ΠΎΠ½ ΠΏΠΎ-ΠΏΡ€Π΅ΠΆΠ½Π΅ΠΌΡƒ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ состояниС ΠΈΠ³Ρ€Ρ‹ ΠΈΠ· ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π΅Π³ΠΎ обновлСния. ΠœΠΎΠΆΠ΅Ρ‚Π΅ ΠΏΡ€Π΅Π΄ΡΡ‚Π°Π²ΠΈΡ‚ΡŒ, насколько это Π½Π΅ΡƒΠ΄ΠΎΠ±Π½ΠΎ для ΠΈΠ³Ρ€ΠΎΠΊΠ°: ΠΈΠ·-Π·Π° ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ»ΡŒΠ½Ρ‹Ρ… Ρ‚ΠΎΡ€ΠΌΠΎΠΆΠ΅Π½ΠΈΠΉ ΠΈΠ³Ρ€Π° Π±ΡƒΠ΄Π΅Ρ‚ ΠΊΠ°Π·Π°Ρ‚ΡŒΡΡ Π΄Ρ‘Ρ€Π³Π°Π½Π½ΠΎΠΉ ΠΈ Π½Π΅ΡΡ‚Π°Π±ΠΈΠ»ΡŒΠ½ΠΎΠΉ.

7.2 Π£Π»ΡƒΡ‡ΡˆΠ΅Π½Π½ΠΎΠ΅ состояниС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

ΠœΡ‹ внСсём Π² Π½Π°ΠΈΠ²Π½ΡƒΡŽ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ Π½Π΅ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΡ. Π’ΠΎ-ΠΏΠ΅Ρ€Π²Ρ‹Ρ…, ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ Π·Π°Π΄Π΅Ρ€ΠΆΠΊΡƒ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° Π½Π° 100 мс. Π­Ρ‚ΠΎ ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ Β«Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅Β» состояниС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π° всСгда Π±ΡƒΠ΄Π΅Ρ‚ ΠΎΡ‚ΡΡ‚Π°Π²Π°Ρ‚ΡŒ ΠΎΡ‚ состояния ΠΈΠ³Ρ€Ρ‹ Π½Π° сСрвСрС Π½Π° 100 мс. НапримСр, Ссли Π½Π° сСрвСрС врСмя Ρ€Π°Π²Π½ΠΎ 150, Ρ‚ΠΎ Π½Π° ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ Π±ΡƒΠ΄Π΅Ρ‚ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ΡŒΡΡ состояниС, Π² ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠΌ Π±Ρ‹Π» сСрвСр Π²ΠΎ врСмя 50:

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Π­Ρ‚ΠΎ Π΄Π°Ρ‘Ρ‚ Π½Π°ΠΌ Π±ΡƒΡ„Π΅Ρ€ Π² 100 мс, ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‰ΠΈΠΉ ΠΏΠ΅Ρ€Π΅ΠΆΠΈΡ‚ΡŒ нСпрСдсказуСмоС врСмя получСния ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ ΠΈΠ³Ρ€Ρ‹:

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Расплатой Π·Π° это Π±ΡƒΠ΄Π΅Ρ‚ постоянная Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ° Π²Π²ΠΎΠ΄Π° (input lag) Π½Π° 100 мс. Π­Ρ‚ΠΎ Π½Π΅Π·Π½Π°Ρ‡ΠΈΡ‚Π΅Π»ΡŒΠ½Π°Ρ ΠΆΠ΅Ρ€Ρ‚Π²Π° Π·Π° ΠΏΠ»Π°Π²Π½Ρ‹ΠΉ ΠΈΠ³Ρ€ΠΎΠ²ΠΎΠΉ процСсс β€” Π±ΠΎΠ»ΡŒΡˆΠΈΠ½ΡΡ‚Π²ΠΎ ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ² (особСнно ΠΊΠ°Π·ΡƒΠ°Π»ΡŒΠ½Ρ‹Ρ…) Π΄Π°ΠΆΠ΅ Π½Π΅ Π·Π°ΠΌΠ΅Ρ‚ΠΈΡ‚ этой Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ. Π›ΡŽΠ΄ΡΠΌ Π³ΠΎΡ€Π°Π·Π΄ΠΎ ΠΏΡ€ΠΎΡ‰Π΅ ΠΏΡ€ΠΈΡΠΏΠΎΡΠΎΠ±ΠΈΡ‚ΡŒΡΡ ΠΊ постоянной Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ΅ Π² 100 мс, Ρ‡Π΅ΠΌ ΠΈΠ³Ρ€Π°Ρ‚ΡŒ с нСпрСдсказуСмой Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ.

ΠœΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΈ Π΄Ρ€ΡƒΠ³ΡƒΡŽ Ρ‚Π΅Ρ…Π½ΠΈΠΊΡƒ ΠΏΠΎΠ΄ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ΠΌ Β«ΠΏΡ€ΠΎΠ³Π½ΠΎΠ·ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π½Π° сторонС ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°Β», которая Ρ…ΠΎΡ€ΠΎΡˆΠΎ справляСтся со сниТСниСм воспринимаСмых Π·Π°Π΄Π΅Ρ€ΠΆΠ΅ΠΊ, Π½ΠΎ Π² этом постС ΠΎΠ½Π° Ρ€Π°ΡΡΠΌΠ°Ρ‚Ρ€ΠΈΠ²Π°Ρ‚ΡŒΡΡ Π½Π΅ Π±ΡƒΠ΄Π΅Ρ‚.

Π•Ρ‰Ρ‘ ΠΎΠ΄Π½ΠΎ ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ β€” это линСйная интСрполяция. Из-Π·Π° Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° ΠΌΡ‹ ΠΎΠ±Ρ‹Ρ‡Π½ΠΎ ΠΊΠ°ΠΊ ΠΌΠΈΠ½ΠΈΠΌΡƒΠΌ Π½Π° ΠΎΠ΄Π½ΠΎ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ обгоняСм Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ врСмя Π² ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅. Когда вызываСтся getCurrentState(), ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Π²Ρ‹ΠΏΠΎΠ»Π½ΠΈΡ‚ΡŒ Π»ΠΈΠ½Π΅ΠΉΠ½ΡƒΡŽ ΠΈΠ½Ρ‚Π΅Ρ€ΠΏΠΎΠ»ΡΡ†ΠΈΡŽ ΠΌΠ΅ΠΆΠ΄Ρƒ обновлСниями ΠΈΠ³Ρ€Ρ‹ нСпосрСдствСнно ΠΏΠ΅Ρ€Π΅Π΄ ΠΈ послС Ρ‚Π΅ΠΊΡƒΡ‰ΠΈΠΌ Π²Ρ€Π΅ΠΌΠ΅Π½Π΅ΠΌ Π² ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅:

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Π­Ρ‚ΠΎ Ρ€Π΅ΡˆΠ°Π΅Ρ‚ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ с частотой ΠΊΠ°Π΄Ρ€ΠΎΠ²: Ρ‚Π΅ΠΏΠ΅Ρ€ΡŒ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ΡŒ ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹Π΅ ΠΊΠ°Π΄Ρ€Ρ‹ с любой Π½ΡƒΠΆΠ½ΠΎΠΉ Π½Π°ΠΌ частотой!

7.3 РСализация ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½Π½ΠΎΠ³ΠΎ состояния ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°

ΠŸΡ€ΠΈΠΌΠ΅Ρ€ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π² src/client/state.js ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ ΠΈ Π·Π°Π΄Π΅Ρ€ΠΆΠΊΡƒ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°, ΠΈ Π»ΠΈΠ½Π΅ΠΉΠ½ΡƒΡŽ ΠΈΠ½Ρ‚Π΅Ρ€ΠΏΠΎΠ»ΡΡ†ΠΈΡŽ, Π½ΠΎ это Π½Π΅Π½Π°Π΄ΠΎΠ»Π³ΠΎ. Π”Π°Π²Π°ΠΉΡ‚Π΅ Ρ€Π°Π·ΠΎΠ±ΡŒΡ‘ΠΌ ΠΊΠΎΠ΄ Π½Π° Π΄Π²Π΅ части. Π’ΠΎΡ‚ пСрвая:

state.js, Ρ‡Π°ΡΡ‚ΡŒ 1

const RENDER_DELAY = 100;

const gameUpdates = [];
let gameStart = 0;
let firstServerTimestamp = 0;

export function initState() {
  gameStart = 0;
  firstServerTimestamp = 0;
}

export function processGameUpdate(update) {
  if (!firstServerTimestamp) {
    firstServerTimestamp = update.t;
    gameStart = Date.now();
  }
  gameUpdates.push(update);

  // Keep only one game update before the current server time
  const base = getBaseUpdate();
  if (base > 0) {
    gameUpdates.splice(0, base);
  }
}

function currentServerTime() {
  return firstServerTimestamp + (Date.now() - gameStart) - RENDER_DELAY;
}

// Returns the index of the base update, the first game update before
// current server time, or -1 if N/A.
function getBaseUpdate() {
  const serverTime = currentServerTime();
  for (let i = gameUpdates.length - 1; i >= 0; i--) {
    if (gameUpdates[i].t <= serverTime) {
      return i;
    }
  }
  return -1;
}

ΠŸΠ΅Ρ€Π²Ρ‹ΠΌ Π΄Π΅Π»ΠΎΠΌ Π½ΡƒΠΆΠ½ΠΎ Ρ€Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒΡΡ с Ρ‚Π΅ΠΌ, Ρ‡Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚ currentServerTime(). Как ΠΌΡ‹ Π²ΠΈΠ΄Π΅Π»ΠΈ Ρ€Π°Π½Π΅Π΅, Π² ΠΊΠ°ΠΆΠ΄ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ Π²ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ сСрвСрная ΠΌΠ΅Ρ‚ΠΊΠ° Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ. ΠœΡ‹ Ρ…ΠΎΡ‚ΠΈΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Π·Π°Π΄Π΅Ρ€ΠΆΠΊΡƒ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΡ‚ΡŒ ΠΊΠ°Ρ€Ρ‚ΠΈΠ½ΠΊΡƒ, отставая ΠΎΡ‚ сСрвСра Π½Π° 100 мс, Π½ΠΎ ΠΌΡ‹ Π½ΠΈΠΊΠΎΠ³Π΄Π° Π½Π΅ ΡƒΠ·Π½Π°Π΅ΠΌ, Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ врСмя Π½Π° сСрвСрС, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ Π½Π΅ ΠΌΠΎΠΆΠ΅ΠΌ Π·Π½Π°Ρ‚ΡŒ, ΠΊΠ°ΠΊ Π΄ΠΎΠ»Π³ΠΎ Π΄ΠΎΠ±ΠΈΡ€Π°Π»ΠΎΡΡŒ Π΄ΠΎ нас любоС ΠΈΠ· ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ. Π˜Π½Ρ‚Π΅Ρ€Π½Π΅Ρ‚ нСпрСдсказуСм ΠΈ Π΅Π³ΠΎ ΡΠΊΠΎΡ€ΠΎΡΡ‚ΡŒ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΎΡ‡Π΅Π½ΡŒ сильно Π²Π°Ρ€ΡŒΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒΡΡ!

Π§Ρ‚ΠΎΠ±Ρ‹ ΠΎΠ±ΠΎΠΉΡ‚ΠΈ эту ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΡƒ, ΠΌΠΎΠΆΠ½ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ Ρ€Π°Π·ΡƒΠΌΠ½ΡƒΡŽ Π°ΠΏΠΏΡ€ΠΎΠΊΡΠΈΠΌΠ°Ρ†ΠΈΡŽ: ΠΌΡ‹ притворимся, Ρ‡Ρ‚ΠΎ ΠΏΠ΅Ρ€Π²ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΏΡ€ΠΈΠ±Ρ‹Π»ΠΎ ΠΌΠ³Π½ΠΎΠ²Π΅Π½Π½ΠΎ. Если Π±Ρ‹ это Π±Ρ‹Π»ΠΎ Π²Π΅Ρ€Π½ΠΎ, Ρ‚ΠΎ ΠΌΡ‹ Π±Ρ‹ Π·Π½Π°Π»ΠΈ врСмя сСрвСра Π² этот ΠΊΠΎΠ½ΠΊΡ€Π΅Ρ‚Π½Ρ‹ΠΉ ΠΌΠΎΠΌΠ΅Π½Ρ‚! ΠœΡ‹ сохраняСм ΠΌΠ΅Ρ‚ΠΊΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ сСрвСра Π² firstServerTimestamp ΠΈ сохраняСм Π½Π°ΡˆΡƒ Π»ΠΎΠΊΠ°Π»ΡŒΠ½ΡƒΡŽ (ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΡΠΊΡƒΡŽ) ΠΌΠ΅Ρ‚ΠΊΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π² Ρ‚ΠΎΡ‚ ΠΆΠ΅ ΠΌΠΎΠΌΠ΅Π½Ρ‚ Π² gameStart.

Ой, постойтС-ΠΊΠ°. Π Π°Π·Π²Π΅ Π½Π΅ Π΄ΠΎΠ»ΠΆΠ½ΠΎ Π±Ρ‹Ρ‚ΡŒ врСмя Π½Π° сСрвСрС = Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π² ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅? ΠŸΠΎΡ‡Π΅ΠΌΡƒ ΠΌΡ‹ Ρ€Π°Π·Π»ΠΈΡ‡Π°Π΅ΠΌ Β«ΠΌΠ΅Ρ‚ΠΊΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ сСрвСра» ΠΈ Β«ΠΌΠ΅Ρ‚ΠΊΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π°Β»? Π­Ρ‚ΠΎ ΠΎΡ‚Π»ΠΈΡ‡Π½Ρ‹ΠΉ вопрос! ΠžΠΊΠ°Π·Ρ‹Π²Π°Π΅Ρ‚ΡΡ, это Π½Π΅ ΠΎΠ΄Π½ΠΎ ΠΈ Ρ‚ΠΎ ΠΆΠ΅. Date.now() Π±ΡƒΠ΄Π΅Ρ‚ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Ρ‚ΡŒ Ρ€Π°Π·Π½Ρ‹Π΅ ΠΌΠ΅Ρ‚ΠΊΠΈ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π² ΠΊΠ»ΠΈΠ΅Π½Ρ‚Π΅ ΠΈ сСрвСра ΠΈ это зависит ΠΎΡ‚ Π»ΠΎΠΊΠ°Π»ΡŒΠ½Ρ‹Ρ… для этих машин Ρ„Π°ΠΊΡ‚ΠΎΡ€ΠΎΠ². Никогда Π½Π΅ допускайтС, Ρ‡Ρ‚ΠΎ ΠΌΠ΅Ρ‚ΠΊΠΈ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Π±ΡƒΠ΄ΡƒΡ‚ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²Ρ‹ΠΌΠΈ Π½Π° всСх ΠΌΠ°ΡˆΠΈΠ½Π°Ρ….

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π½Π°ΠΌ понятно, Ρ‡Ρ‚ΠΎ Π΄Π΅Π»Π°Π΅Ρ‚ currentServerTime(): ΠΎΠ½ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ ΠΌΠ΅Ρ‚ΠΊΡƒ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ сСрвСра Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°. Π”Ρ€ΡƒΠ³ΠΈΠΌΠΈ словами, это Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π΅ врСмя сСрвСра (firstServerTimestamp <+ (Date.now() - gameStart)) минус Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠ° Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° (RENDER_DELAY).

Π’Π΅ΠΏΠ΅Ρ€ΡŒ Π΄Π°Π²Π°ΠΉΡ‚Π΅ разбСрёмся, ΠΊΠ°ΠΊ ΠΌΡ‹ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ обновлСния ΠΈΠ³Ρ€Ρ‹. ΠŸΡ€ΠΈ ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠΈ с сСрвСра обновлСния вызываСтся processGameUpdate(), ΠΈ ΠΌΡ‹ сохраняСм Π½ΠΎΠ²ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ Π² массив gameUpdates. Π—Π°Ρ‚Π΅ΠΌ, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΏΡ€ΠΎΠ²Π΅Ρ€ΡΡ‚ΡŒ использованиС памяти ΠΌΡ‹ удаляСм всС старыС обновлСния Π΄ΠΎ Π±Π°Π·ΠΎΠ²ΠΎΠ³ΠΎ обновлСния, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ ΠΎΠ½ΠΈ Π½Π°ΠΌ большС Π½Π΅ Π½ΡƒΠΆΠ½Ρ‹.

Π§Ρ‚ΠΎ ΠΆΠ΅ Ρ‚Π°ΠΊΠΎΠ΅ Β«Π±Π°Π·ΠΎΠ²ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅Β»? Π­Ρ‚ΠΎ ΠΏΠ΅Ρ€Π²ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ ΠΌΡ‹ Π½Π°Ρ…ΠΎΠ΄ΠΈΠΌ, двигаясь Π½Π°Π·Π°Π΄ ΠΎΡ‚ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ сСрвСра. ΠŸΠΎΠΌΠ½ΠΈΡ‚Π΅ эту схСму?

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
ОбновлСниС ΠΈΠ³Ρ€Ρ‹ нСпосрСдствСнно слСва ΠΎΡ‚ Β«Client Render TimeΒ» ΠΈ являСтся Π±Π°Π·ΠΎΠ²Ρ‹ΠΌ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ΠΌ.

Для Ρ‡Π΅Π³ΠΎ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π±Π°Π·ΠΎΠ²ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅? ΠŸΠΎΡ‡Π΅ΠΌΡƒ ΠΌΡ‹ ΠΌΠΎΠΆΠ΅ΠΌ ΠΎΡ‚Π±Ρ€Π°ΡΡ‹Π²Π°Ρ‚ΡŒ обновлСния Π΄ΠΎ Π±Π°Π·ΠΎΠ²ΠΎΠ³ΠΎ? Π§Ρ‚ΠΎΠ±Ρ‹ Ρ€Π°Π·ΠΎΠ±Ρ€Π°Ρ‚ΡŒΡΡ Π² этом, Π΄Π°Π²Π°ΠΉΡ‚Π΅ Π½Π°ΠΊΠΎΠ½Π΅Ρ†-Ρ‚ΠΎ рассмотрим Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ getCurrentState():

state.js, Ρ‡Π°ΡΡ‚ΡŒ 2

export function getCurrentState() {
  if (!firstServerTimestamp) {
    return {};
  }

  const base = getBaseUpdate();
  const serverTime = currentServerTime();

  // If base is the most recent update we have, use its state.
  // Else, interpolate between its state and the state of (base + 1).
  if (base < 0) {
    return gameUpdates[gameUpdates.length - 1];
  } else if (base === gameUpdates.length - 1) {
    return gameUpdates[base];
  } else {
    const baseUpdate = gameUpdates[base];
    const next = gameUpdates[base + 1];
    const r = (serverTime - baseUpdate.t) / (next.t - baseUpdate.t);
    return {
      me: interpolateObject(baseUpdate.me, next.me, r),
      others: interpolateObjectArray(baseUpdate.others, next.others, r),
      bullets: interpolateObjectArray(baseUpdate.bullets, next.bullets, r),
    };
  }
}

ΠœΡ‹ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°Π΅ΠΌ Ρ‚Ρ€ΠΈ случая:

  1. base < 0 ΠΎΠ·Π½Π°Ρ‡Π°Π΅Ρ‚, Ρ‡Ρ‚ΠΎ Π΄ΠΎ Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π° ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ Π½Π΅Ρ‚ (см. Π²Ρ‹ΡˆΠ΅ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ getBaseUpdate()). Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ ΡΠ»ΡƒΡ‡ΠΈΡ‚ΡŒΡΡ сразу Π² Π½Π°Ρ‡Π°Π»Π΅ ΠΈΠ³Ρ€Ρ‹ ΠΈΠ·-Π·Π° Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°. Π’ Ρ‚Π°ΠΊΠΎΠΌ случаС ΠΌΡ‹ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ самоС послСднСС ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½Π½ΠΎΠ΅ ΠΎΠ±Π½ΠΎΠ»Π²Π΅Π½ΠΈΠ΅.
  2. base β€” это самоС послСднСС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Ρƒ нас Π΅ΡΡ‚ΡŒ. Π­Ρ‚ΠΎ ΠΌΠΎΠΆΠ΅Ρ‚ ΠΏΡ€ΠΎΠΈΠ·ΠΎΠΉΡ‚ΠΈ ΠΈΠ·-Π·Π° сСтСвой Π·Π°Π΄Π΅Ρ€ΠΆΠΊΠΈ ΠΈΠ»ΠΈ ΠΏΠ»ΠΎΡ…ΠΎΠΉ связи с Π˜Π½Ρ‚Π΅Ρ€Π½Π΅Ρ‚ΠΎΠΌ. Π’ этом случаС ΠΌΡ‹ Ρ‚ΠΎΠΆΠ΅ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌ самоС послСднСС ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅, ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Ρƒ нас Π΅ΡΡ‚ΡŒ.
  3. Π£ нас Π΅ΡΡ‚ΡŒ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈ Π΄ΠΎ, ΠΈ послС Ρ‚Π΅ΠΊΡƒΡ‰Π΅Π³ΠΎ Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ Ρ€Π΅Π½Π΄Π΅Ρ€ΠΈΠ½Π³Π°, поэтому ΠΌΠΎΠΆΠ½ΠΎ ΠΈΠ½Ρ‚Π΅Ρ€ΠΏΠΎΠ»ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ!

Всё, Ρ‡Ρ‚ΠΎ ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ Π² state.js β€” это рСализация Π»ΠΈΠ½Π΅ΠΉΠ½ΠΎΠΉ интСрполяции, ΠΏΡ€Π΅Π΄ΡΡ‚Π°Π²Π»ΡΡŽΡ‰Π°Ρ собой ΠΏΡ€ΠΎΡΡ‚ΡƒΡŽ (Π½ΠΎ ΡΠΊΡƒΡ‡Π½ΡƒΡŽ) ΠΌΠ°Ρ‚Π΅ΠΌΠ°Ρ‚ΠΈΠΊΡƒ. Если Π²Ρ‹ Ρ…ΠΎΡ‚ΠΈΡ‚Π΅ ΠΈΠ·ΡƒΡ‡ΠΈΡ‚ΡŒ Π΅Ρ‘ ΡΠ°ΠΌΠΎΡΡ‚ΠΎΡΡ‚Π΅Π»ΡŒΠ½ΠΎ, Ρ‚ΠΎ ΠΎΡ‚ΠΊΡ€ΠΎΠΉΡ‚Π΅ state.js Π½Π° Github.

Π§Π°ΡΡ‚ΡŒ 2. БэкСнд-сСрвСр

Π’ этой части ΠΌΡ‹ рассмотрим бэкСнд Node.js, ΡƒΠΏΡ€Π°Π²Π»ΡΡŽΡ‰ΠΈΠΉ нашим ΠΏΡ€ΠΈΠΌΠ΅Ρ€ΠΎΠΌ ΠΈΠ³Ρ€Ρ‹ .io.

1. Входная Ρ‚ΠΎΡ‡ΠΊΠ° сСрвСра

Для управлСния Π²Π΅Π±-сСрвСром ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ популярный Π²Π΅Π±-Ρ„Ρ€Π΅ΠΉΠΌΠ²ΠΎΡ€ΠΊ для Node.js ΠΏΠΎΠ΄ Π½Π°Π·Π²Π°Π½ΠΈΠ΅ΠΌ Express. Π•Π³ΠΎ настройкой займётся наш Ρ„Π°ΠΉΠ» Π²Ρ…ΠΎΠ΄Π½ΠΎΠΉ Ρ‚ΠΎΡ‡ΠΊΠΈ сСрвСра src/server/server.js:

server.js, Ρ‡Π°ΡΡ‚ΡŒ 1

const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackConfig = require('../../webpack.dev.js');

// Setup an Express server
const app = express();
app.use(express.static('public'));

if (process.env.NODE_ENV === 'development') {
  // Setup Webpack for development
  const compiler = webpack(webpackConfig);
  app.use(webpackDevMiddleware(compiler));
} else {
  // Static serve the dist/ folder in production
  app.use(express.static('dist'));
}

// Listen on port
const port = process.env.PORT || 3000;
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

ΠŸΠΎΠΌΠ½ΠΈΡ‚Π΅, Ρ‡Ρ‚ΠΎ Π² ΠΏΠ΅Ρ€Π²ΠΎΠΉ части ΠΌΡ‹ обсуТдали Webpack? ИмСнно здСсь ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ наши ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ Webpack. ΠœΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΏΡ€ΠΈΠΌΠ΅Π½ΡΡ‚ΡŒ ΠΈΡ… двумя способами:

  • Π˜ΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒ webpack-dev-middleware для автоматичСской пСрСсборки Π½Π°ΡˆΠΈΡ… ΠΏΠ°ΠΊΠ΅Ρ‚ΠΎΠ² Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ, ΠΈΠ»ΠΈ
  • БтатичСски ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ ΠΏΠ°ΠΏΠΊΡƒ dist/, Π² ΠΊΠΎΡ‚ΠΎΡ€ΡƒΡŽ Webpack Π±ΡƒΠ΄Π΅Ρ‚ Π·Π°ΠΏΠΈΡΡ‹Π²Π°Ρ‚ΡŒ наши Ρ„Π°ΠΉΠ»Ρ‹ послС сборки ΠΏΡ€ΠΎΠ΄Π°ΠΊΡˆΠ΅Π½Π°.

Π•Ρ‰Ρ‘ ΠΎΠ΄Π½Π° ваТная Π·Π°Π΄Π°Ρ‡Π° server.js Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² настройкС сСрвСра socket.io, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ просто ΠΏΠΎΠ΄ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ ΠΊ сСрвСру Express:

server.js, Ρ‡Π°ΡΡ‚ΡŒ 2

const socketio = require('socket.io');
const Constants = require('../shared/constants');

// Setup Express
// ...
const server = app.listen(port);
console.log(`Server listening on port ${port}`);

// Setup socket.io
const io = socketio(server);

// Listen for socket.io connections
io.on('connection', socket => {
  console.log('Player connected!', socket.id);

  socket.on(Constants.MSG_TYPES.JOIN_GAME, joinGame);
  socket.on(Constants.MSG_TYPES.INPUT, handleInput);
  socket.on('disconnect', onDisconnect);
});

ПослС ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΉ установки соСдинСния socket.io с сСрвСром ΠΌΡ‹ настраиваСм ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ событий для Π½ΠΎΠ²ΠΎΠ³ΠΎ сокСта. ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊΠΈ событий ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°ΡŽΡ‚ ΠΏΠΎΠ»ΡƒΡ‡Π°Π΅ΠΌΡ‹Π΅ ΠΎΡ‚ ΠΊΠ»ΠΈΠ΅Π½Ρ‚ΠΎΠ² сообщСния Π΄Π΅Π»Π΅Π³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρƒ-синглтону game:

server.js, Ρ‡Π°ΡΡ‚ΡŒ 3

const Game = require('./game');

// ...

// Setup the Game
const game = new Game();

function joinGame(username) {
  game.addPlayer(this, username);
}

function handleInput(dir) {
  game.handleInput(this, dir);
}

function onDisconnect() {
  game.removePlayer(this);
}

ΠœΡ‹ создаём ΠΈΠ³Ρ€Ρƒ ΠΆΠ°Π½Ρ€Π° .io, поэтому Π½Π°ΠΌ понадобится Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ экзСмпляр Game (Β«GameΒ») – всС ΠΈΠ³Ρ€ΠΎΠΊΠΈ ΠΈΠ³Ρ€Π°ΡŽΡ‚ Π½Π° ΠΎΠ΄Π½ΠΎΠΉ Π°Ρ€Π΅Π½Π΅! Π’ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌ Ρ€Π°Π·Π΄Π΅Π»Π΅ ΠΌΡ‹ посмотрим, ΠΊΠ°ΠΊ Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ этот класс Game.

2. Game сСрвСра

Класс Game содСрТит ΡΠ°ΠΌΡƒΡŽ Π²Π°ΠΆΠ½ΡƒΡŽ Π»ΠΎΠ³ΠΈΠΊΡƒ Π½Π° сторонС сСрвСра. Он ΠΈΠΌΠ΅Π΅Ρ‚ Π΄Π²Π΅ основныС Π·Π°Π΄Π°Ρ‡ΠΈ: ΡƒΠΏΡ€Π°Π²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌΠΈ ΠΈ симуляция ΠΈΠ³Ρ€Ρ‹.

Π”Π°Π²Π°ΠΉΡ‚Π΅ Π½Π°Ρ‡Π½Ρ‘ΠΌ с ΠΏΠ΅Ρ€Π²ΠΎΠΉ Π·Π°Π΄Π°Ρ‡ΠΈ – с управлСния ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌΠΈ.

game.js, Ρ‡Π°ΡΡ‚ΡŒ 1

const Constants = require('../shared/constants');
const Player = require('./player');

class Game {
  constructor() {
    this.sockets = {};
    this.players = {};
    this.bullets = [];
    this.lastUpdateTime = Date.now();
    this.shouldSendUpdate = false;
    setInterval(this.update.bind(this), 1000 / 60);
  }

  addPlayer(socket, username) {
    this.sockets[socket.id] = socket;

    // Generate a position to start this player at.
    const x = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    const y = Constants.MAP_SIZE * (0.25 + Math.random() * 0.5);
    this.players[socket.id] = new Player(socket.id, username, x, y);
  }

  removePlayer(socket) {
    delete this.sockets[socket.id];
    delete this.players[socket.id];
  }

  handleInput(socket, dir) {
    if (this.players[socket.id]) {
      this.players[socket.id].setDirection(dir);
    }
  }

  // ...
}

Π’ этой ΠΈΠ³Ρ€Π΅ ΠΌΡ‹ Π±ΡƒΠ΄Π΅ΠΌ ΠΈΠ΄Π΅Π½Ρ‚ΠΈΡ„ΠΈΡ†ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ² ΠΏΠΎ полю id ΠΈΡ… сокСта socket.io (Ссли Π²Ρ‹ Π·Π°ΠΏΡƒΡ‚Π°Π»ΠΈΡΡŒ, Ρ‚ΠΎ снова Π²Π΅Ρ€Π½ΠΈΡ‚Π΅ΡΡŒ ΠΊ server.js). Socket.io сам Π½Π°Π·Π½Π°Ρ‡Π°Π΅Ρ‚ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡƒ сокСту ΡƒΠ½ΠΈΠΊΠ°Π»ΡŒΠ½Ρ‹ΠΉ id, поэтому Π½Π°ΠΌ ΠΎΠ± этом Π±Π΅ΡΠΏΠΎΠΊΠΎΠΈΡ‚ΡŒΡΡ Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ. Π― Π±ΡƒΠ΄Ρƒ Π½Π°Π·Ρ‹Π²Π°Ρ‚ΡŒ Π΅Π³ΠΎ ID ΠΈΠ³Ρ€ΠΎΠΊΠ°.

Π—Π°ΠΏΠΎΠΌΠ½ΠΈΠ² это, Π΄Π°Π²Π°ΠΉΡ‚Π΅ ΠΈΠ·ΡƒΡ‡ΠΈΠΌ ΠΏΠ΅Ρ€Π΅ΠΌΠ΅Π½Π½Ρ‹Π΅ экзСмпляра Π² классС Game:

  • sockets β€” это ΠΎΠ±ΡŠΠ΅ΠΊΡ‚, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ привязываСт ID ΠΈΠ³Ρ€ΠΎΠΊΠ° ΠΊ сокСту, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ связан с ΠΈΠ³Ρ€ΠΎΠΊΠΎΠΌ. Он позволяСт Π½Π°ΠΌ Π·Π° постоянноС врСмя ΠΏΠΎΠ»ΡƒΡ‡Π°Ρ‚ΡŒ доступ ΠΊ сокСтам ΠΏΠΎ ΠΈΡ… ID ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ².
  • players β€” это ΠΎΠ±ΡŠΠ΅ΠΊΡ‚, ΠΏΡ€ΠΈΠ²ΡΠ·Ρ‹Π²Π°ΡŽΡ‰ΠΈΠΉ ID ΠΈΠ³Ρ€ΠΎΠΊΠ° ΠΊ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρƒ code>Player

bullets β€” это массив ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ΠΎΠ² Bullet, Π½Π΅ ΠΈΠΌΠ΅ΡŽΡ‰ΠΈΠΉ ΠΎΠΏΡ€Π΅Π΄Π΅Π»Ρ‘Π½Π½ΠΎΠ³ΠΎ порядка.
lastUpdateTime β€” это ΠΌΠ΅Ρ‚ΠΊΠ° Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ ΠΌΠΎΠΌΠ΅Π½Ρ‚Π° послСднСго обновлСния ΠΈΠ³Ρ€Ρ‹. ВскорС ΠΌΡ‹ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ, ΠΊΠ°ΠΊ ΠΎΠ½Π° ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ.
shouldSendUpdate β€” это Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Π°Ρ пСрСмСнная. Π•Ρ‘ использованиС ΠΌΡ‹ Ρ‚ΠΎΠΆΠ΅ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ вскорС.
ΠœΠ΅Ρ‚ΠΎΠ΄Ρ‹ addPlayer(), removePlayer() ΠΈ handleInput() ΠΎΠ±ΡŠΡΡΠ½ΡΡ‚ΡŒ Π½Π΅ Π½ΡƒΠΆΠ½ΠΎ, ΠΎΠ½ΠΈ ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΡŽΡ‚ΡΡ Π² server.js. Если Π²Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ ΠΎΡΠ²Π΅ΠΆΠΈΡ‚ΡŒ ΠΏΠ°ΠΌΡΡ‚ΡŒ, Π²Π΅Ρ€Π½ΠΈΡ‚Π΅ΡΡŒ Π½Π΅ΠΌΠ½ΠΎΠ³ΠΎ Π²Ρ‹ΡˆΠ΅.

ПослСдняя строка constructor() запускаСт Ρ†ΠΈΠΊΠ» обновлСния ΠΈΠ³Ρ€Ρ‹ (с частотой 60 ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ/с):

game.js, Ρ‡Π°ΡΡ‚ΡŒ 2

const Constants = require('../shared/constants');
const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // Calculate time elapsed
    const now = Date.now();
    const dt = (now - this.lastUpdateTime) / 1000;
    this.lastUpdateTime = now;

    // Update each bullet
    const bulletsToRemove = [];
    this.bullets.forEach(bullet => {
      if (bullet.update(dt)) {
        // Destroy this bullet
        bulletsToRemove.push(bullet);
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !bulletsToRemove.includes(bullet),
    );

    // Update each player
    Object.keys(this.sockets).forEach(playerID => {
      const player = this.players[playerID];
      const newBullet = player.update(dt);
      if (newBullet) {
        this.bullets.push(newBullet);
      }
    });

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // Check if any players are dead
    Object.keys(this.sockets).forEach(playerID => {
      const socket = this.sockets[playerID];
      const player = this.players[playerID];
      if (player.hp <= 0) {
        socket.emit(Constants.MSG_TYPES.GAME_OVER);
        this.removePlayer(socket);
      }
    });

    // Send a game update to each player every other time
    if (this.shouldSendUpdate) {
      const leaderboard = this.getLeaderboard();
      Object.keys(this.sockets).forEach(playerID => {
        const socket = this.sockets[playerID];
        const player = this.players[playerID];
        socket.emit(
          Constants.MSG_TYPES.GAME_UPDATE,
          this.createUpdate(player, leaderboard),
        );
      });
      this.shouldSendUpdate = false;
    } else {
      this.shouldSendUpdate = true;
    }
  }

  // ...
}

ΠœΠ΅Ρ‚ΠΎΠ΄ update() содСрТит, Π½Π°Π²Π΅Ρ€Π½ΠΎ, ΡΠ°ΠΌΡƒΡŽ Π²Π°ΠΆΠ½ΡƒΡŽ Ρ‡Π°ΡΡ‚ΡŒ Π»ΠΎΠ³ΠΈΠΊΠΈ Π½Π° сторонС сСрвСра. По порядку пСрСчислим всё, Ρ‡Ρ‚ΠΎ ΠΎΠ½ Π΄Π΅Π»Π°Π΅Ρ‚:

  1. ВычисляСт, сколько Π²Ρ€Π΅ΠΌΠ΅Π½ΠΈ dt ΠΏΡ€ΠΎΡˆΠ»ΠΎ с послСднСго update().
  2. ΠžΠ±Π½ΠΎΠ²Π»ΡΠ΅Ρ‚ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ снаряд ΠΈ ΠΏΡ€ΠΈ нСобходимости ΡƒΠ½ΠΈΡ‡Ρ‚ΠΎΠΆΠ°Π΅Ρ‚ ΠΈΡ…. Π Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ этого Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΎΠ½Π°Π»Π° ΠΌΡ‹ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ ΠΏΠΎΠ·ΠΆΠ΅. Пока Π½Π°ΠΌ достаточно Π·Π½Π°Ρ‚ΡŒ, Ρ‡Ρ‚ΠΎ bullet.update() Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ true, Ссли снаряд Π΄ΠΎΠ»ΠΆΠ΅Π½ Π±Ρ‹Ρ‚ΡŒ ΡƒΠ½ΠΈΡ‡Ρ‚ΠΎΠΆΠ΅Π½ (ΠΎΠ½ Π²Ρ‹ΡˆΠ΅Π» Π·Π° Π³Ρ€Π°Π½ΠΈΡ†Ρ‹ Π°Ρ€Π΅Π½Ρ‹).
  3. ΠžΠ±Π½ΠΎΠ²Π»ΡΠ΅Ρ‚ ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ ΠΈΠ³Ρ€ΠΎΠΊΠ° ΠΈ ΠΏΡ€ΠΈ нСобходимости создаём снаряд. Π­Ρ‚Ρƒ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ ΠΌΡ‹ Ρ‚ΠΎΠΆΠ΅ ΡƒΠ²ΠΈΠ΄ΠΈΠΌ ΠΏΠΎΠ·ΠΆΠ΅ β€” player.update() ΠΌΠΎΠΆΠ΅Ρ‚ Π²ΠΎΠ·Π²Ρ€Π°Ρ‚ΠΈΡ‚ΡŒ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Bullet.
  4. ΠŸΡ€ΠΎΠ²Π΅Ρ€ΡΠ΅Ρ‚ ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΈ ΠΌΠ΅ΠΆΠ΄Ρƒ снарядами ΠΈ ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌΠΈ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ applyCollisions(), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ массив снарядов, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΠΎΠΏΠ°Π»ΠΈ Π² ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ². Для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Ρ‘Π½Π½ΠΎΠ³ΠΎ снаряда ΠΌΡ‹ ΡƒΠ²Π΅Π»ΠΈΡ‡ΠΈΠ²Π°Π΅ΠΌ ΠΎΡ‡ΠΊΠΈ ΠΈΠ³Ρ€ΠΎΠΊΠ°, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ Π΅Π³ΠΎ выпустил (с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ player.onDealtDamage()), Π° Π·Π°Ρ‚Π΅ΠΌ удаляСм снаряд ΠΈΠ· массива bullets.
  5. УвСдомляСт ΠΈ ΡƒΠ½ΠΈΡ‡Ρ‚ΠΎΠΆΠ°Π΅Ρ‚ всСх ΡƒΠ±ΠΈΡ‚Ρ‹Ρ… ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ².
  6. ΠžΡ‚ΠΏΡ€Π°Π²Π»ΡΠ΅Ρ‚ всСм ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌ ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ Π²Ρ‚ΠΎΡ€ΠΎΠΉ Ρ€Π°Π· ΠΏΡ€ΠΈ Π²Ρ‹Π·ΠΎΠ²Π΅ update(). Π­Ρ‚ΠΎ Π½Π°ΠΌ ΠΏΠΎΠΌΠΎΠ³Π°Π΅Ρ‚ ΠΎΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ упомянутая Π²Ρ‹ΡˆΠ΅ Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Π°Ρ пСрСмСнная shouldSendUpdate. Π’Π°ΠΊ ΠΊΠ°ΠΊ update() вызываСтся 60 Ρ€Π°Π·/с, ΠΌΡ‹ отправляСм обновлСния ΠΈΠ³Ρ€Ρ‹ 30 Ρ€Π°Π·/с. Π’Π°ΠΊΠΈΠΌ ΠΎΠ±Ρ€Π°Π·ΠΎΠΌ, частота Ρ‚Π°ΠΊΡ‚ΠΎΠ² сСрвСра Ρ€Π°Π²Π½Π° 30 Ρ‚Π°ΠΊΡ‚Π°ΠΌ/с (ΠΌΡ‹ Π³ΠΎΠ²ΠΎΡ€ΠΈΠ»ΠΈ ΠΎ частотС Ρ‚Π°ΠΊΡ‚ΠΎΠ² Π² ΠΏΠ΅Ρ€Π²ΠΎΠΉ части).

Π—Π°Ρ‡Π΅ΠΌ ΠΎΡ‚ΠΏΡ€Π°Π²Π»ΡΡ‚ΡŒ обновлСния ΠΈΠ³Ρ€Ρ‹ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‡Π΅Ρ€Π΅Π· Ρ€Π°Π· ? Для экономии ΠΊΠ°Π½Π°Π»Π°. 30 ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ ΠΈΠ³Ρ€Ρ‹ Π² сСкунду – это ΠΎΡ‡Π΅Π½ΡŒ ΠΌΠ½ΠΎΠ³ΠΎ!

ΠŸΠΎΡ‡Π΅ΠΌΡƒ Π±Ρ‹ Ρ‚ΠΎΠ³Π΄Π° просто Π½Π΅ Π²Ρ‹Π·Ρ‹Π²Π°Ρ‚ΡŒ update() 30 Ρ€Π°Π· Π² сСкунду? Для ΡƒΠ»ΡƒΡ‡ΡˆΠ΅Π½ΠΈΡ симуляции ΠΈΠ³Ρ€Ρ‹. Π§Π΅ΠΌ Ρ‡Π°Ρ‰Π΅ вызываСтся update(), Ρ‚Π΅ΠΌ Ρ‚ΠΎΡ‡Π½Π΅Π΅ Π±ΡƒΠ΄Π΅Ρ‚ симуляция ΠΈΠ³Ρ€Ρ‹. Но Π½Π΅ стоит слишком ΡƒΠ²Π»Π΅ΠΊΠ°Ρ‚ΡŒΡΡ количСством Π²Ρ‹Π·ΠΎΠ²ΠΎΠ² update(), ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ это Π²Ρ‹Ρ‡ΠΈΡΠ»ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎ затратная Π·Π°Π΄Π°Ρ‡Π° β€” 60 Π² сСкунду Π²ΠΏΠΎΠ»Π½Π΅ достаточно.

ΠžΡΡ‚Π°Π²ΡˆΠ°ΡΡΡ Ρ‡Π°ΡΡ‚ΡŒ класса Game состоит ΠΈΠ· Π²ΡΠΏΠΎΠΌΠΎΠ³Π°Ρ‚Π΅Π»ΡŒΠ½Ρ‹Ρ… ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ², ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅ΠΌΡ‹Ρ… Π² update():

game.js, Ρ‡Π°ΡΡ‚ΡŒ 3

class Game {
  // ...

  getLeaderboard() {
    return Object.values(this.players)
      .sort((p1, p2) => p2.score - p1.score)
      .slice(0, 5)
      .map(p => ({ username: p.username, score: Math.round(p.score) }));
  }

  createUpdate(player, leaderboard) {
    const nearbyPlayers = Object.values(this.players).filter(
      p => p !== player && p.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );
    const nearbyBullets = this.bullets.filter(
      b => b.distanceTo(player) <= Constants.MAP_SIZE / 2,
    );

    return {
      t: Date.now(),
      me: player.serializeForUpdate(),
      others: nearbyPlayers.map(p => p.serializeForUpdate()),
      bullets: nearbyBullets.map(b => b.serializeForUpdate()),
      leaderboard,
    };
  }
}

getLeaderboard() довольно прост – ΠΎΠ½ сортируСт ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ² ΠΏΠΎ количСству ΠΎΡ‡ΠΊΠΎΠ², Π±Π΅Ρ€Ρ‘Ρ‚ ΠΏΡΡ‚ΡŒ Π»ΡƒΡ‡ΡˆΠΈΡ… ΠΈ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ для ΠΊΠ°ΠΆΠ΄ΠΎΠ³ΠΎ имя ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Ρ ΠΈ счёт.

createUpdate() ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ΡΡ Π² update() для создания ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠΉ ΠΈΠ³Ρ€Ρ‹, ΠΊΠΎΡ‚ΠΎΡ€Ρ‹Π΅ ΠΏΠ΅Ρ€Π΅Π΄Π°ΡŽΡ‚ΡΡ ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌ. Π•Π³ΠΎ основная Π·Π°Π΄Π°Ρ‡Π° Π·Π°ΠΊΠ»ΡŽΡ‡Π°Π΅Ρ‚ΡΡ Π² Π²Ρ‹Π·ΠΎΠ²Π΅ ΠΌΠ΅Ρ‚ΠΎΠ΄ΠΎΠ² serializeForUpdate(), Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π½Π½Ρ‹Ρ… для классов Player ΠΈ Bullet. Π—Π°ΠΌΠ΅Ρ‚ΡŒΡ‚Π΅, Ρ‡Ρ‚ΠΎ ΠΎΠ½ ΠΏΠ΅Ρ€Π΅Π΄Π°Ρ‘Ρ‚ ΠΊΠ°ΠΆΠ΄ΠΎΠΌΡƒ ΠΈΠ³Ρ€ΠΎΠΊΡƒ Π΄Π°Π½Π½Ρ‹Π΅ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎ Π±Π»ΠΈΠΆΠ°ΠΉΡˆΠΈΡ… ΠΈΠ³Ρ€ΠΎΠΊΠ°Ρ… ΠΈ снарядах – Π½Π΅Ρ‚ нСобходимости ΠΏΠ΅Ρ€Π΅Π΄Π°Π²Π°Ρ‚ΡŒ ΠΈΠ½Ρ„ΠΎΡ€ΠΌΠ°Ρ†ΠΈΡŽ ΠΎΠ± ΠΈΠ³Ρ€ΠΎΠ²Ρ‹Ρ… ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Π°Ρ…, находящихся Π΄Π°Π»Π΅ΠΊΠΎ ΠΎΡ‚ ΠΈΠ³Ρ€ΠΎΠΊΠ°!

3. Π˜Π³Ρ€ΠΎΠ²Ρ‹Π΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ Π½Π° сСрвСрС

Π’ нашСй ΠΈΠ³Ρ€Π΅ снаряды ΠΈ ΠΈΠ³Ρ€ΠΎΠΊΠΈ Π½Π° самом Π΄Π΅Π»Π΅ ΠΎΡ‡Π΅Π½ΡŒ ΠΏΠΎΡ…ΠΎΠΆΠΈ: это абстрактныС ΠΊΡ€ΡƒΠ³Π»Ρ‹Π΅ ΠΏΠΎΠ΄Π²ΠΈΠΆΠ½Ρ‹Π΅ ΠΈΠ³Ρ€ΠΎΠ²Ρ‹Π΅ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹. Π§Ρ‚ΠΎΠ±Ρ‹ Π²ΠΎΡΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚ΡŒΡΡ этой ΡΡ…ΠΎΠΆΠ΅ΡΡ‚ΡŒΡŽ ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ² ΠΈ снарядов, Π΄Π°Π²Π°ΠΉΡ‚Π΅ Π½Π°Ρ‡Π½Ρ‘ΠΌ с Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π±Π°Π·ΠΎΠ²ΠΎΠ³ΠΎ класса Object:

object.js

class Object {
  constructor(id, x, y, dir, speed) {
    this.id = id;
    this.x = x;
    this.y = y;
    this.direction = dir;
    this.speed = speed;
  }

  update(dt) {
    this.x += dt * this.speed * Math.sin(this.direction);
    this.y -= dt * this.speed * Math.cos(this.direction);
  }

  distanceTo(object) {
    const dx = this.x - object.x;
    const dy = this.y - object.y;
    return Math.sqrt(dx * dx + dy * dy);
  }

  setDirection(dir) {
    this.direction = dir;
  }

  serializeForUpdate() {
    return {
      id: this.id,
      x: this.x,
      y: this.y,
    };
  }
}

Π—Π΄Π΅ΡΡŒ Π½Π΅ происходит Π½ΠΈΡ‡Π΅Π³ΠΎ слоТного. Π­Ρ‚ΠΎΡ‚ класс станСт Ρ…ΠΎΡ€ΠΎΡˆΠ΅ΠΉ ΠΎΠΏΠΎΡ€Π½ΠΎΠΉ Ρ‚ΠΎΡ‡ΠΊΠΎΠΉ для Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ. Π”Π°Π²Π°ΠΉΡ‚Π΅ посмотрим, ΠΊΠ°ΠΊ класс Bullet ΠΈΡΠΏΠΎΠ»ΡŒΠ·ΡƒΠ΅Ρ‚ Object:

bullet.js

const shortid = require('shortid');
const ObjectClass = require('./object');
const Constants = require('../shared/constants');

class Bullet extends ObjectClass {
  constructor(parentID, x, y, dir) {
    super(shortid(), x, y, dir, Constants.BULLET_SPEED);
    this.parentID = parentID;
  }

  // Returns true if the bullet should be destroyed
  update(dt) {
    super.update(dt);
    return this.x < 0 || this.x > Constants.MAP_SIZE || this.y < 0 || this.y > Constants.MAP_SIZE;
  }
}

РСализация Bullet ΠΎΡ‡Π΅Π½ΡŒ ΠΊΠΎΡ€ΠΎΡ‚ΠΊΠ°! ΠœΡ‹ Π΄ΠΎΠ±Π°Π²ΠΈΠ»ΠΈ ΠΊ Object Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰ΠΈΠ΅ Ρ€Π°ΡΡˆΠΈΡ€Π΅Π½ΠΈΡ:

  • ИспользованиС ΠΏΠ°ΠΊΠ΅Ρ‚Π° shortid для случайной Π³Π΅Π½Π΅Ρ€Π°Ρ†ΠΈΠΈ id снаряда.
  • Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ поля parentID, Ρ‡Ρ‚ΠΎΠ±Ρ‹ ΠΌΠΎΠΆΠ½ΠΎ Π±Ρ‹Π»ΠΎ ΠΎΡ‚ΡΠ»Π΅ΠΆΠΈΠ²Π°Ρ‚ΡŒ ΠΈΠ³Ρ€ΠΎΠΊΠ°, создавшСго этот снаряд.
  • Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΠΎΠ³ΠΎ значСния Π² update(), ΠΊΠΎΡ‚ΠΎΡ€ΠΎΠ΅ Ρ€Π°Π²Π½ΠΎ true, Ссли снаряд находится Π·Π° ΠΏΡ€Π΅Π΄Π΅Π»Π°ΠΌΠΈ Π°Ρ€Π΅Π½Ρ‹ (ΠΏΠΎΠΌΠ½ΠΈΡ‚Π΅, ΠΌΡ‹ Π³ΠΎΠ²ΠΎΡ€ΠΈΠ»ΠΈ ΠΎΠ± этом Π² ΠΏΡ€ΠΎΡˆΠ»ΠΎΠΌ Ρ€Π°Π·Π΄Π΅Π»Π΅?).

ΠŸΠ΅Ρ€Π΅ΠΉΠ΄Ρ‘ΠΌ ΠΊ Player:

player.js

const ObjectClass = require('./object');
const Bullet = require('./bullet');
const Constants = require('../shared/constants');

class Player extends ObjectClass {
  constructor(id, username, x, y) {
    super(id, x, y, Math.random() * 2 * Math.PI, Constants.PLAYER_SPEED);
    this.username = username;
    this.hp = Constants.PLAYER_MAX_HP;
    this.fireCooldown = 0;
    this.score = 0;
  }

  // Returns a newly created bullet, or null.
  update(dt) {
    super.update(dt);

    // Update score
    this.score += dt * Constants.SCORE_PER_SECOND;

    // Make sure the player stays in bounds
    this.x = Math.max(0, Math.min(Constants.MAP_SIZE, this.x));
    this.y = Math.max(0, Math.min(Constants.MAP_SIZE, this.y));

    // Fire a bullet, if needed
    this.fireCooldown -= dt;
    if (this.fireCooldown <= 0) {
      this.fireCooldown += Constants.PLAYER_FIRE_COOLDOWN;
      return new Bullet(this.id, this.x, this.y, this.direction);
    }
    return null;
  }

  takeBulletDamage() {
    this.hp -= Constants.BULLET_DAMAGE;
  }

  onDealtDamage() {
    this.score += Constants.SCORE_BULLET_HIT;
  }

  serializeForUpdate() {
    return {
      ...(super.serializeForUpdate()),
      direction: this.direction,
      hp: this.hp,
    };
  }
}

Π˜Π³Ρ€ΠΎΠΊΠΈ слоТнСС, Ρ‡Π΅ΠΌ снаряды, поэтому Π² этом классС Π΄ΠΎΠ»ΠΆΠ½ΠΎ Ρ…Ρ€Π°Π½ΠΈΡ‚ΡŒΡΡ Π΅Ρ‰Ρ‘ нСсколько ΠΏΠΎΠ»Π΅ΠΉ. Π•Π³ΠΎ ΠΌΠ΅Ρ‚ΠΎΠ΄ update() выполняСт Π±ΠžΠ»ΡŒΡˆΡƒΡŽ Ρ€Π°Π±ΠΎΡ‚Ρƒ, Π² частности, Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅Ρ‚ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Ρ‡Ρ‚ΠΎ созданный снаряд, Ссли Π½Π΅ ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ fireCooldown (ΠΏΠΎΠΌΠ½ΠΈΡ‚Π΅, ΠΌΡ‹ Π³ΠΎΠ²ΠΎΡ€ΠΈΠ»ΠΈ ΠΎΠ± этом Π² ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π΅ΠΌ Ρ€Π°Π·Π΄Π΅Π»Π΅?). Π’Π°ΠΊΠΆΠ΅ ΠΎΠ½ Ρ€Π°ΡΡˆΠΈΡ€ΡΠ΅Ρ‚ ΠΌΠ΅Ρ‚ΠΎΠ΄ serializeForUpdate(), ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ Π½Π°ΠΌ Π½ΡƒΠΆΠ½ΠΎ Π²ΠΊΠ»ΡŽΡ‡ΠΈΡ‚ΡŒ Π² ΠΎΠ±Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΈΠ³Ρ€Ρ‹ Π΄ΠΎΠΏΠΎΠ»Π½ΠΈΡ‚Π΅Π»ΡŒΠ½Ρ‹Π΅ поля для ΠΈΠ³Ρ€ΠΎΠΊΠ°.

НаличиС Π±Π°Π·ΠΎΠ²ΠΎΠ³ΠΎ класса Object β€” Π²Π°ΠΆΠ½Ρ‹ΠΉ шаг, ΠΏΠΎΠ·Π²ΠΎΠ»ΡΡŽΡ‰ΠΈΠΉ ΠΈΠ·Π±Π΅ΠΆΠ°Ρ‚ΡŒ повторяСмости ΠΊΠΎΠ΄Π°. НапримСр, Π±Π΅Π· класса Object ΠΊΠ°ΠΆΠ΄Ρ‹ΠΉ ΠΈΠ³Ρ€ΠΎΠ²ΠΎΠΉ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΈΠΌΠ΅Ρ‚ΡŒ ΠΎΠ΄ΠΈΠ½Π°ΠΊΠΎΠ²ΡƒΡŽ Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΡŽ distanceTo(), ΠΈ синхронизация копипасты всСх этих Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ Π² Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… Ρ„Π°ΠΉΠ»Π°Ρ… Π±Ρ‹Π»Π° Π±Ρ‹ ΠΊΠΎΡˆΠΌΠ°Ρ€ΠΎΠΌ. Π­Ρ‚ΠΎ становится особо Π²Π°ΠΆΠ½ΠΎ для ΠΊΡ€ΡƒΠΏΠ½Ρ‹Ρ… ΠΏΡ€ΠΎΠ΅ΠΊΡ‚ΠΎΠ², ΠΊΠΎΠ³Π΄Π° количСство Ρ€Π°ΡΡˆΠΈΡ€ΡΡŽΡ‰ΠΈΡ… Object классов растёт.

4. РаспознаваниС коллизий

ЕдинствСнноС, Ρ‡Ρ‚ΠΎ Π½Π°ΠΌ ΠΎΡΡ‚Π°Π»ΠΎΡΡŒ – Ρ€Π°ΡΠΏΠΎΠ·Π½Π°Π²Π°Ρ‚ΡŒ, ΠΊΠΎΠ³Π΄Π° снаряды ΠΏΠΎΠΏΠ°Π΄Π°ΡŽΡ‚ Π² ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ²! ВспомнитС этот Ρ„Ρ€Π°Π³ΠΌΠ΅Π½Ρ‚ ΠΊΠΎΠ΄Π° ΠΈΠ· ΠΌΠ΅Ρ‚ΠΎΠ΄Π° update() Π² классС Game:

game.js

const applyCollisions = require('./collisions');

class Game {
  // ...

  update() {
    // ...

    // Apply collisions, give players score for hitting bullets
    const destroyedBullets = applyCollisions(
      Object.values(this.players),
      this.bullets,
    );
    destroyedBullets.forEach(b => {
      if (this.players[b.parentID]) {
        this.players[b.parentID].onDealtDamage();
      }
    });
    this.bullets = this.bullets.filter(
      bullet => !destroyedBullets.includes(bullet),
    );

    // ...
  }
}

Нам Π½ΡƒΠΆΠ½ΠΎ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Ρ‚ΡŒ ΠΌΠ΅Ρ‚ΠΎΠ΄ applyCollisions(), Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΡŽΡ‰ΠΈΠΉ всС снаряды, попавшиС Π² ΠΈΠ³Ρ€ΠΎΠΊΠΎΠ². К ΡΡ‡Π°ΡΡ‚ΡŒΡŽ, это Π½Π΅ Ρ‚Π°ΠΊ слоТно ΡΠ΄Π΅Π»Π°Ρ‚ΡŒ, ΠΏΠΎΡ‚ΠΎΠΌΡƒ Ρ‡Ρ‚ΠΎ

  • ВсС ΡΡ‚Π°Π»ΠΊΠΈΠ²Π°ΡŽΡ‰ΠΈΠ΅ΡΡ ΠΎΠ±ΡŠΠ΅ΠΊΡ‚Ρ‹ ΡΠ²Π»ΡΡŽΡ‚ΡΡ ΠΊΡ€ΡƒΠ³Π°ΠΌΠΈ, Π° это ΠΏΡ€ΠΎΡΡ‚Π΅ΠΉΡˆΠ°Ρ для Ρ€Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ распознавания ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ Ρ„ΠΈΠ³ΡƒΡ€Π°.
  • Π£ нас ΡƒΠΆΠ΅ Π΅ΡΡ‚ΡŒ ΠΌΠ΅Ρ‚ΠΎΠ΄ distanceTo(), ΠΊΠΎΡ‚ΠΎΡ€Ρ‹ΠΉ ΠΌΡ‹ Π² ΠΏΡ€Π΅Π΄Ρ‹Π΄ΡƒΡ‰Π΅ΠΌ Ρ€Π°Π·Π΄Π΅Π»Π΅ Ρ€Π΅Π°Π»ΠΈΠ·ΠΎΠ²Π°Π»ΠΈ Π² классС Object.

Π’ΠΎΡ‚ ΠΊΠ°ΠΊ выглядит наша рСализация распознавания ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ:

collisions.js

const Constants = require('../shared/constants');

// Returns an array of bullets to be destroyed.
function applyCollisions(players, bullets) {
  const destroyedBullets = [];
  for (let i = 0; i < bullets.length; i++) {
    // Look for a player (who didn't create the bullet) to collide each bullet with.
    // As soon as we find one, break out of the loop to prevent double counting a bullet.
    for (let j = 0; j < players.length; j++) {
      const bullet = bullets[i];
      const player = players[j];
      if (
        bullet.parentID !== player.id &&
        player.distanceTo(bullet) <= Constants.PLAYER_RADIUS + Constants.BULLET_RADIUS
      ) {
        destroyedBullets.push(bullet);
        player.takeBulletDamage();
        break;
      }
    }
  }
  return destroyedBullets;
}

Π­Ρ‚ΠΎ простоС распознаваниС ΠΊΠΎΠ»Π»ΠΈΠ·ΠΈΠΉ основано Π½Π° Ρ‚ΠΎΠΌ Ρ„Π°ΠΊΡ‚Π΅, Ρ‡Ρ‚ΠΎ Π΄Π²Π° ΠΊΡ€ΡƒΠ³Π° ΡΡ‚Π°Π»ΠΊΠΈΠ²Π°ΡŽΡ‚ΡΡ, Ссли расстояниС ΠΌΠ΅ΠΆΠ΄Ρƒ ΠΈΡ… Ρ†Π΅Π½Ρ‚Ρ€Π°ΠΌΠΈ мСньшС суммы ΠΈΡ… радиусов. Π’ΠΎΡ‚ случай, ΠΊΠΎΠ³Π΄Π° расстояниС ΠΌΠ΅ΠΆΠ΄Ρƒ Ρ†Π΅Π½Ρ‚Ρ€Π°ΠΌΠΈ Π΄Π²ΡƒΡ… ΠΊΡ€ΡƒΠ³ΠΎΠ² Ρ‚ΠΎΡ‡Π½ΠΎ Ρ€Π°Π²Π½ΠΎ суммС ΠΈΡ… радиусов:

Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΌΠ½ΠΎΠ³ΠΎΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΡΠΊΠΎΠΉ Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ Π² ΠΆΠ°Π½Ρ€Π΅ .io
Π—Π΄Π΅ΡΡŒ Π½ΡƒΠΆΠ½ΠΎ Π²Π½ΠΈΠΌΠ°Ρ‚Π΅Π»ΡŒΠ½ΠΎ ΠΎΡ‚Π½Π΅ΡΡ‚ΠΈΡΡŒ Π΅Ρ‰Ρ‘ ΠΊ ΠΏΠ°Ρ€Π΅ аспСктов:

  • Бнаряд Π½Π΅ Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΠΎΠΏΠ°Π΄Π°Ρ‚ΡŒ Π² создавшСго Π΅Π³ΠΎ ΠΈΠ³Ρ€ΠΎΠΊΠ°. Π­Ρ‚ΠΎΠ³ΠΎ ΠΌΠΎΠΆΠ½ΠΎ Π΄ΠΎΡΡ‚ΠΈΡ‡ΡŒ, сравнивая bullet.parentID с player.id.
  • Бнаряд Π΄ΠΎΠ»ΠΆΠ΅Π½ ΠΏΠΎΠΏΠ°Π΄Π°Ρ‚ΡŒ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ ΠΎΠ΄ΠΈΠ½ Ρ€Π°Π· Π² ΠΏΡ€Π΅Π΄Π΅Π»ΡŒΠ½ΠΎΠΌ случаС ΠΎΠ΄Π½ΠΎΠ²Ρ€Π΅ΠΌΠ΅Π½Π½ΠΎΠ³ΠΎ столкновСния с нСсколькими ΠΈΠ³Ρ€ΠΎΠΊΠ°ΠΌΠΈ. Π­Ρ‚Ρƒ Π·Π°Π΄Π°Ρ‡Ρƒ ΠΌΡ‹ Ρ€Π΅ΡˆΠΈΠΌ с ΠΏΠΎΠΌΠΎΡ‰ΡŒΡŽ ΠΎΠΏΠ΅Ρ€Π°Ρ‚ΠΎΡ€Π° break: ΠΊΠ°ΠΊ Ρ‚ΠΎΠ»ΡŒΠΊΠΎ Π½Π°ΠΉΠ΄Π΅Π½ ΠΈΠ³Ρ€ΠΎΠΊ, ΡΡ‚ΠΎΠ»ΠΊΠ½ΡƒΠ²ΡˆΠΈΠΉΡΡ со снарядом, ΠΌΡ‹ ΠΏΡ€Π΅ΠΊΡ€Π°Ρ‰Π°Π΅ΠΌ поиск ΠΈ ΠΏΠ΅Ρ€Π΅Ρ…ΠΎΠ΄ΠΈΠΌ ΠΊ ΡΠ»Π΅Π΄ΡƒΡŽΡ‰Π΅ΠΌΡƒ снаряду.

ΠšΠΎΠ½Π΅Ρ†

Π’ΠΎΡ‚ ΠΈ всё! ΠœΡ‹ рассмотрСли всё, Ρ‡Ρ‚ΠΎ Π½Π΅ΠΎΠ±Ρ…ΠΎΠ΄ΠΈΠΌΠΎ Π·Π½Π°Ρ‚ΡŒ для создания Π²Π΅Π±-ΠΈΠ³Ρ€Ρ‹ ΠΆΠ°Π½Ρ€Π° .io. Π§Ρ‚ΠΎ дальшС? Π‘ΠΎΠ±Π΅Ρ€ΠΈΡ‚Π΅ ΡΠΎΠ±ΡΡ‚Π²Π΅Π½Π½ΡƒΡŽ ΠΈΠ³Ρ€Ρƒ .io!

Π’Π΅ΡΡŒ ΠΊΠΎΠ΄ ΠΏΡ€ΠΈΠΌΠ΅Ρ€Π° ΠΈΠΌΠ΅Π΅Ρ‚ ΠΎΡ‚ΠΊΡ€Ρ‹Ρ‚Ρ‹Π΅ исходники ΠΈ Π²Ρ‹Π»ΠΎΠΆΠ΅Π½ Π½Π° Github.

Π˜ΡΡ‚ΠΎΡ‡Π½ΠΈΠΊ: habr.com