๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
2015๋…„ ์ถœ์‹œ Agar.io ์ƒˆ๋กœ์šด ์žฅ๋ฅด์˜ ์ฐฝ์‹œ์ž๊ฐ€ ๋˜๋‹ค ๊ฒŒ์ž„ .io๊ทธ ์ดํ›„๋กœ ์ธ๊ธฐ๊ฐ€ ๋†’์•„์กŒ์Šต๋‹ˆ๋‹ค. ์ €๋Š” ๊ฐœ์ธ์ ์œผ๋กœ .io ๊ฒŒ์ž„์˜ ์ธ๊ธฐ ์ƒ์Šน์„ ๊ฒฝํ—˜ํ–ˆ์Šต๋‹ˆ๋‹ค. ์ง€๋‚œ XNUMX๋…„ ๋™์•ˆ ์ €๋Š” ์ด ์žฅ๋ฅด์˜ ๋‘ ๊ฒŒ์ž„์„ ๋งŒ๋“ค๊ณ  ํŒ๋งคํ–ˆ์Šต๋‹ˆ๋‹ค..

์ด์ „์— ์ด๋Ÿฌํ•œ ๊ฒŒ์ž„์— ๋Œ€ํ•ด ๋“ค์–ด๋ณธ ์ ์ด ์—†๋Š” ๊ฒฝ์šฐ ํ”Œ๋ ˆ์ดํ•˜๊ธฐ ์‰ฌ์šด ๋ฌด๋ฃŒ ๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด ์›น ๊ฒŒ์ž„์ž…๋‹ˆ๋‹ค(๊ณ„์ • ํ•„์š” ์—†์Œ). ๊ทธ๋“ค์€ ์ผ๋ฐ˜์ ์œผ๋กœ ๊ฐ™์€ ๊ฒฝ๊ธฐ์žฅ์—์„œ ๋งŽ์€ ์ƒ๋Œ€ ํ”Œ๋ ˆ์ด์–ด์™€ ๋งˆ์ฃผํ•ฉ๋‹ˆ๋‹ค. ๊ธฐํƒ€ ์œ ๋ช…ํ•œ .io ๊ฒŒ์ž„: Slither.io ะธ Diep.io.

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๊ทธ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ๋ถ€ํ„ฐ .io ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ. ์ด๋ฅผ ์œ„ํ•ด์„œ๋Š” Javascript์— ๋Œ€ํ•œ ์ง€์‹๋งŒ ์žˆ์œผ๋ฉด ์ถฉ๋ถ„ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ๋ฌธ๊ณผ ๊ฐ™์€ ๊ฒƒ์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ES6, ํ‚ค์›Œ๋“œ this ะธ ์•ฝ์†. Javascript์— ๋Œ€ํ•œ ์ง€์‹์ด ์™„๋ฒฝํ•˜์ง€ ์•Š๋”๋ผ๋„ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒŒ์‹œ๋ฌผ์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

.io ๊ฒŒ์ž„ ์˜ˆ์‹œ

ํ•™์Šต ์ง€์›์— ๋Œ€ํ•ด์„œ๋Š” ๋‹ค์Œ์„ ์ฐธ์กฐํ•ฉ๋‹ˆ๋‹ค. .io ๊ฒŒ์ž„ ์˜ˆ์‹œ. ๊ทธ๊ฒƒ์„ ์žฌ์ƒํ•ด๋ณด์‹ญ์‹œ์˜ค!

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
๊ฒŒ์ž„์€ ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๊ฐ€ ์žˆ๋Š” ๊ฒฝ๊ธฐ์žฅ์—์„œ ๋ฐฐ๋ฅผ ์กฐ์ข…ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์„ ์€ ์ž๋™์œผ๋กœ ๋ฐœ์‚ฌ์ฒด๋ฅผ ๋ฐœ์‚ฌํ•˜๊ณ  ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด์˜ ๋ฐœ์‚ฌ์ฒด๋ฅผ ํ”ผํ•˜๋ฉด์„œ ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ๊ณต๊ฒฉํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”/๊ตฌ์กฐ

์ถ”์ฒœ ์†Œ์Šค ์ฝ”๋“œ ๋‹ค์šด๋กœ๋“œ ๋‹น์‹ ์ด ๋‚˜๋ฅผ ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ๋„๋ก ์˜ˆ์ œ ๊ฒŒ์ž„.

์ด ์˜ˆ์—์„œ๋Š” ๋‹ค์Œ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

  • Express ๊ฒŒ์ž„์˜ ์›น ์„œ๋ฒ„๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ€์žฅ ์ธ๊ธฐ ์žˆ๋Š” Node.js ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.
  • ์†Œ์ผ“.io - ๋ธŒ๋ผ์šฐ์ €์™€ ์„œ๋ฒ„ ๊ฐ„์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ตํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ์›น์†Œ์ผ“ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ.
  • ์›นํŒฉ - ๋ชจ๋“ˆ ๊ด€๋ฆฌ์ž. Webpack์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ์— ๋Œ€ํ•ด ์ฝ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์—ฌ๊ธฐ์—.

ํ”„๋กœ์ ํŠธ ๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

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

๊ณต๊ณต์˜/

ํด๋”์˜ ๋ชจ๋“  ๊ฒƒ public/ ์„œ๋ฒ„์— ์˜ํ•ด ์ •์ ์œผ๋กœ ์ œ์ถœ๋ฉ๋‹ˆ๋‹ค. ์•ˆ์— public/assets/ ํ”„๋กœ์ ํŠธ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ด๋ฏธ์ง€๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

src /

๋ชจ๋“  ์†Œ์Šค ์ฝ”๋“œ๋Š” ํด๋”์— ์žˆ์Šต๋‹ˆ๋‹ค. src/. ์ œ๋ชฉ client/ ะธ server/ ์Šค์Šค๋กœ ๋งํ•˜๊ณ  shared/ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๋ชจ๋‘์—์„œ ๊ฐ€์ ธ์˜จ ์ƒ์ˆ˜ ํŒŒ์ผ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

2. ์–ด์…ˆ๋ธ”๋ฆฌ/ํ”„๋กœ์ ํŠธ ์„ค์ •

์œ„์—์„œ ์–ธ๊ธ‰ํ–ˆ๋“ฏ์ด ๋ชจ๋“ˆ ๊ด€๋ฆฌ์ž๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ์ ํŠธ๋ฅผ ๋นŒ๋“œํ•ฉ๋‹ˆ๋‹ค. ์›นํŒฉ. 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์€ ์—ฌ๊ธฐ์—์„œ ์‹œ์ž‘ํ•˜์—ฌ ๊ฐ€์ ธ์˜จ ๋‹ค๋ฅธ ํŒŒ์ผ์„ ์žฌ๊ท€์ ์œผ๋กœ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
  • Webpack ๋นŒ๋“œ์˜ ์ถœ๋ ฅ JS๋Š” ๋””๋ ‰ํ† ๋ฆฌ์— ์žˆ์Šต๋‹ˆ๋‹ค. dist/. ์ด ํŒŒ์ผ์„ ์šฐ๋ฆฌ์˜ js ํŒจํ‚ค์ง€.
  • ์šฐ๋ฆฌ๋Š” ์‚ฌ์šฉ ๋ฐ”๋ฒจ, ํŠนํžˆ ๊ตฌ์„ฑ @babel/preset-env ์ด์ „ ๋ธŒ๋ผ์šฐ์ €์šฉ JS ์ฝ”๋“œ๋ฅผ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค.
  • ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์‚ฌ์šฉํ•˜์—ฌ JS ํŒŒ์ผ์—์„œ ์ฐธ์กฐํ•˜๋Š” ๋ชจ๋“  CSS๋ฅผ ์ถ”์ถœํ•˜๊ณ  ํ•œ ๊ณณ์—์„œ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. ๋‚˜๋Š” ๊ทธ๋ฅผ ์šฐ๋ฆฌ๋ผ๊ณ  ๋ถ€๋ฅผ ๊ฒƒ์ด๋‹ค CSS ํŒจํ‚ค์ง€.

์ด์ƒํ•œ ํŒจํ‚ค์ง€ ํŒŒ์ผ ์ด๋ฆ„์„ ๋ฐœ๊ฒฌํ–ˆ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. '[name].[contenthash].ext'. ๊ทธ๋“ค์€ ํฌํ•จ ํŒŒ์ผ ์ด๋ฆ„ ๋Œ€์ฒด ์›นํŒฉ: [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ํ”„๋กœ๋•์…˜์— ๋ฐฐํฌํ•  ๋•Œ ํŒจํ‚ค์ง€ ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.

๋กœ์ปฌ ์„ค์ •

์ด ๊ฒŒ์‹œ๋ฌผ์— ๋‚˜์—ด๋œ ๋‹จ๊ณ„๋ฅผ ๋”ฐ๋ฅผ ์ˆ˜ ์žˆ๋„๋ก ๋กœ์ปฌ ์ปดํ“จํ„ฐ์— ํ”„๋กœ์ ํŠธ๋ฅผ ์„ค์น˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์„ค์ •์€ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ๋จผ์ € ์‹œ์Šคํ…œ์ด ์„ค์น˜๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋…ธ๋“œ ะธ 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 ํŽ˜์ด์ง€

.io ๊ฒŒ์ž„ ์˜ˆ์‹œ  ๋†€๋‹ค

์ด ์ฝ”๋“œ ์˜ˆ์ œ๋Š” ๋ช…ํ™•์„ฑ์„ ์œ„ํ•ด ์•ฝ๊ฐ„ ๋‹จ์ˆœํ™”๋˜์—ˆ์œผ๋ฉฐ ๋‹ค๋ฅธ ๋งŽ์€ ํฌ์ŠคํŠธ ์˜ˆ์ œ์—์„œ๋„ ๋™์ผํ•œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ๋Š” ํ•ญ์ƒ ๋‹ค์Œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๊นƒํ—ˆ๋ธŒ.

์šฐ๋ฆฌ๋Š”:

  • HTML5 ์บ”๋ฒ„์Šค ์š”์†Œ (<canvas>) ๊ฒŒ์ž„์„ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.
  • <link> CSS ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • <script> Javascript ํŒจํ‚ค์ง€๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
  • ์‚ฌ์šฉ์ž ์ด๋ฆ„์ด ์žˆ๋Š” ๊ธฐ๋ณธ ๋ฉ”๋‰ด <input> ๋ฐ ์žฌ์ƒ ๋ฒ„ํŠผ(<button>).

ํ™ˆํŽ˜์ด์ง€๋ฅผ ๋กœ๋“œํ•œ ํ›„ ๋ธŒ๋ผ์šฐ์ €๋Š” ์ง„์ž…์  JS ํŒŒ์ผ์—์„œ ์‹œ์ž‘ํ•˜์—ฌ Javascript ์ฝ”๋“œ ์‹คํ–‰์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. 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. ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ๊ตํ™˜

์ด ๊ฒŒ์ž„์—์„œ๋Š” ์ž˜ ์•Œ๋ ค์ง„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„์™€ ํ†ต์‹ ํ•ฉ๋‹ˆ๋‹ค. ์†Œ์ผ“.io. Socket.io์—๋Š” ๊ธฐ๋ณธ ์ง€์›์ด ์žˆ์Šต๋‹ˆ๋‹ค. WebSocket์„์–‘๋ฐฉํ–ฅ ํ†ต์‹ ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์— ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ะธ ์„œ๋ฒ„๋Š” ๋™์ผํ•œ ์—ฐ๊ฒฐ์—์„œ ์šฐ๋ฆฌ์—๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ํŒŒ์ผ์„ ๊ฐ€์งˆ ๊ฒƒ์ž…๋‹ˆ๋‹ค src/client/networking.js๋ˆ„๊ฐ€ ๋Œ๋ณผ ๊ฒƒ์ธ๊ฐ€ ๋ชจ๋“  ์„œ๋ฒ„์™€์˜ ํ†ต์‹ :

๋„คํŠธ์›Œํ‚น.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 ์—ฐ๊ฒฐ์ด ์„ค์ •๋œ ๊ฒฝ์šฐ์—๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • ์—ฐ๊ฒฐ์— ์„ฑ๊ณตํ•˜๋ฉด ์ฝœ๋ฐฑ ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค(processGameUpdate() ะธ onGameOver()) ์„œ๋ฒ„์—์„œ ๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ฉ”์‹œ์ง€์˜ ๊ฒฝ์šฐ.
  • ์šฐ๋ฆฌ๋Š” ์ˆ˜์ถœ play() ะธ updateDirection()๋‹ค๋ฅธ ํŒŒ์ผ์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

5. ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง

ํ™”๋ฉด์— ๊ทธ๋ฆผ์„ ํ‘œ์‹œํ•  ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค!

โ€ฆํ•˜์ง€๋งŒ ๊ทธ๋ ‡๊ฒŒ ํ•˜๊ธฐ ์ „์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ด๋ฏธ์ง€(๋ฆฌ์†Œ์Šค)๋ฅผ ๋‹ค์šด๋กœ๋“œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ž์› ๊ด€๋ฆฌ์ž๋ฅผ ์ž‘์„ฑํ•ด ๋ด…์‹œ๋‹ค.

์ž์‚ฐ.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>). ์šฐ๋ฆฌ ๊ฒŒ์ž„์€ ๋งค์šฐ ๊ฐ„๋‹จํ•˜๋ฏ€๋กœ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ทธ๋ฆฌ๊ธฐ๋งŒ ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  1. ๋ฐฐ๊ฒฝ
  2. ์„ ์ˆ˜์„ 
  3. ๊ฒŒ์ž„์˜ ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด
  4. ๊ป์งˆ

๋‹ค์Œ์€ ์ค‘์š”ํ•œ ์Šค ๋‹ˆํŽซ์ž…๋‹ˆ๋‹ค. src/client/render.js, ์œ„์— ๋‚˜์—ด๋œ ๋„ค ๊ฐ€์ง€ ํ•ญ๋ชฉ์„ ์ •ํ™•ํžˆ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

๋ Œ๋”.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() 60FPS์—์„œ ๋ Œ๋”๋ง ๋ฃจํ”„์˜ ํ™œ์„ฑํ™”๋ฅผ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค.

๊ฐœ๋ณ„ ๋ Œ๋”๋ง ๋„์šฐ๋ฏธ ๊ธฐ๋Šฅ์˜ ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„(์˜ˆ: renderBullet())๋Š” ๊ทธ๋‹ค์ง€ ์ค‘์š”ํ•˜์ง€ ์•Š์ง€๋งŒ ๋‹ค์Œ์€ ๊ฐ„๋‹จํ•œ ์˜ˆ์ž…๋‹ˆ๋‹ค.

๋ Œ๋”.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/ํด๋ผ์ด์–ธํŠธ/render.js.

6. ํด๋ผ์ด์–ธํŠธ ์ž…๋ ฅ

๊ฒŒ์ž„์„ ๋งŒ๋“ค ์‹œ๊ฐ„์ด๋‹ค ํ•  ์ˆ˜ ์žˆ๋Š”! ์ œ์–ด ์ฒด๊ณ„๋Š” ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ด๋™ ๋ฐฉํ–ฅ์„ ๋ณ€๊ฒฝํ•˜๋ ค๋ฉด ๋งˆ์šฐ์Šค(์ปดํ“จํ„ฐ์—์„œ)๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ํ™”๋ฉด์„ ํ„ฐ์น˜(๋ชจ๋ฐ”์ผ ์žฅ์น˜์—์„œ)ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋งˆ์šฐ์Šค ๋ฐ ํ„ฐ์น˜ ์ด๋ฒคํŠธ์˜ ๊ฒฝ์šฐ.
์ด ๋ชจ๋“  ๊ฒƒ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค 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() ํ˜ธ์ถœํ•˜๋Š” ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ์ž…๋‹ˆ๋‹ค. updateDirection() ~์˜ networking.js) ์ž…๋ ฅ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ(์˜ˆ: ๋งˆ์šฐ์Šค๊ฐ€ ์›€์ง์ผ ๋•Œ). updateDirection() ์ž…๋ ฅ ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ด์— ๋”ฐ๋ผ ๊ฒŒ์ž„ ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ์„œ๋ฒ„์™€์˜ ๋ฉ”์‹œ์ง•์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

7. ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ

์ด ์„น์…˜์€ ๊ฒŒ์‹œ๋ฌผ์˜ ์ฒซ ๋ฒˆ์งธ ๋ถ€๋ถ„์—์„œ ๊ฐ€์žฅ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. ์ฒ˜์Œ ์ฝ์—ˆ์„ ๋•Œ ์ดํ•ด๊ฐ€ ๋˜์ง€ ์•Š๋”๋ผ๋„ ๋‚™๋‹ดํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค! ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‚˜์ค‘์— ๋‹ค์‹œ ๋Œ์•„์˜ฌ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ/์„œ๋ฒ„ ์ฝ”๋“œ๋ฅผ ์™„์„ฑํ•˜๋Š” ๋ฐ ํ•„์š”ํ•œ ํผ์ฆ์˜ ๋งˆ์ง€๋ง‰ ์กฐ๊ฐ์€ ์ƒํƒœ. ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง ์„น์…˜์˜ ์ฝ”๋“œ ์Šค๋‹ˆํŽซ์„ ๊ธฐ์–ตํ•˜์‹ญ๋‹ˆ๊นŒ?

๋ Œ๋”.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
    }
  ]
}

๊ฐ ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ์—๋Š” XNUMX๊ฐœ์˜ ๋™์ผํ•œ ํ•„๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • t: ์ด ์—…๋ฐ์ดํŠธ๊ฐ€ ์ƒ์„ฑ๋œ ์‹œ๊ฐ„์„ ๋‚˜ํƒ€๋‚ด๋Š” ์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„์ž…๋‹ˆ๋‹ค.
  • me: ์ด ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›๋Š” ํ”Œ๋ ˆ์ด์–ด์— ๋Œ€ํ•œ ์ •๋ณด์ž…๋‹ˆ๋‹ค.
  • ๋‹ค๋ฅธ ์‚ฌ๋žŒ: ๊ฐ™์€ ๊ฒŒ์ž„์— ์ฐธ์—ฌํ•˜๋Š” ๋‹ค๋ฅธ ํ”Œ๋ ˆ์ด์–ด์— ๋Œ€ํ•œ ์ •๋ณด์˜ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค.
  • ์ด์•Œ: ๊ฒŒ์ž„ ๋‚ด ๋ฐœ์‚ฌ์ฒด์— ๋Œ€ํ•œ ์ •๋ณด์˜ ๋ฐฐ์—ด์ž…๋‹ˆ๋‹ค.
  • ๋ฆฌ๋”: ํ˜„์žฌ ์ˆœ์œ„ํ‘œ ๋ฐ์ดํ„ฐ์ž…๋‹ˆ๋‹ค. ์ด ๊ฒŒ์‹œ๋ฌผ์—์„œ ์šฐ๋ฆฌ๋Š” ๊ทธ๊ฒƒ๋“ค์„ ๊ณ ๋ คํ•˜์ง€ ์•Š์„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

7.1 ์ˆœ์ง„ํ•œ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ

์ˆœ์ง„ํ•œ ๊ตฌํ˜„ getCurrentState() ๊ฐ€์žฅ ์ตœ๊ทผ์— ๋ฐ›์€ ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ ๋ฐ์ดํ„ฐ๋งŒ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ˆœ์ง„ํ•œ ์ƒํƒœ.js

let lastGameUpdate = null;

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

export function getCurrentState() {
  return lastGameUpdate;
}

์ข‹๊ณ  ๋ช…ํ™•ํ•ฉ๋‹ˆ๋‹ค! ๊ทธ๋Ÿฌ๋‚˜ ๊ทธ๊ฒƒ์ด ๊ทธ๋ ‡๊ฒŒ ๊ฐ„๋‹จํ•˜๋‹ค๋ฉด. ์ด ๊ตฌํ˜„์ด ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ์ด์œ  ์ค‘ ํ•˜๋‚˜๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค. ๋ Œ๋”๋ง ํ”„๋ ˆ์ž„ ์†๋„๋ฅผ ์„œ๋ฒ„ ํด๋Ÿญ ์†๋„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค..

ํ”„๋ ˆ์ž„ ์†๋„: ํ”„๋ ˆ์ž„ ์ˆ˜(์ฆ‰, ํ˜ธ์ถœ render()) ์ดˆ๋‹น ๋˜๋Š” FPS. ๊ฒŒ์ž„์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์ตœ์†Œ 60FPS๋ฅผ ๋‹ฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ๋…ธ๋ ฅํ•ฉ๋‹ˆ๋‹ค.

ํ‹ฑ ๋น„์œจ: ์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์— ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด๋‚ด๋Š” ๋นˆ๋„์ž…๋‹ˆ๋‹ค. ์ข…์ข… ํ”„๋ ˆ์ž„ ์†๋„๋ณด๋‹ค ๋‚ฎ์Šต๋‹ˆ๋‹ค.. ์šฐ๋ฆฌ ๊ฒŒ์ž„์—์„œ ์„œ๋ฒ„๋Š” ์ดˆ๋‹น 30์ฃผ๊ธฐ์˜ ๋นˆ๋„๋กœ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

๊ฒŒ์ž„์˜ ์ตœ์‹  ์—…๋ฐ์ดํŠธ๋งŒ ๋ Œ๋”๋งํ•˜๋ฉด FPS๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ 30์„ ๋„˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” ์„œ๋ฒ„์—์„œ ์ดˆ๋‹น 30๊ฐœ ์ด์ƒ์˜ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์ง€ ์•Š์Šต๋‹ˆ๋‹ค.. ๋ถˆ๋Ÿฌ๋„ render() ์ดˆ๋‹น 60ํšŒ, ๊ทธ๋Ÿฌ๋ฉด ์ด๋Ÿฌํ•œ ํ˜ธ์ถœ์˜ ์ ˆ๋ฐ˜์€ ๋ณธ์งˆ์ ์œผ๋กœ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š๊ณ  ๋™์ผํ•œ ๊ฒƒ์„ ๋‹ค์‹œ ๊ทธ๋ฆด ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ˆœ์ง„ํ•œ ๊ตฌํ˜„์˜ ๋˜ ๋‹ค๋ฅธ ๋ฌธ์ œ๋Š” ์ง€์—ฐ๋˜๊ธฐ ์‰ฌ์šด. ์ด์ƒ์ ์ธ ์ธํ„ฐ๋„ท ์†๋„๋กœ ํด๋ผ์ด์–ธํŠธ๋Š” ์ •ํ™•ํžˆ 33ms(์ดˆ๋‹น 30)๋งˆ๋‹ค ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐ›์Šต๋‹ˆ๋‹ค.

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
๋ถˆํ–‰ํžˆ๋„ ์™„๋ฒฝํ•œ ๊ฒƒ์€ ์—†์Šต๋‹ˆ๋‹ค. ๋ณด๋‹ค ํ˜„์‹ค์ ์ธ ๊ทธ๋ฆผ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
์ˆœ์ง„ํ•œ ๊ตฌํ˜„์€ ๋Œ€๊ธฐ ์‹œ๊ฐ„๊ณผ ๊ด€๋ จํ•˜์—ฌ ์‚ฌ์‹ค์ƒ ์ตœ์•…์˜ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ๊ฐ€ 50ms ์ง€์—ฐ๋˜์–ด ์ˆ˜์‹ ๋˜๋ฉด ํด๋ผ์ด์–ธํŠธ ์ค‘๋‹จ ์—ฌ์ „ํžˆ ์ด์ „ ์—…๋ฐ์ดํŠธ์˜ ๊ฒŒ์ž„ ์ƒํƒœ๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ถ”๊ฐ€ 50ms์ž…๋‹ˆ๋‹ค. ์ด๊ฒƒ์ด ํ”Œ๋ ˆ์ด์–ด์—๊ฒŒ ์–ผ๋งˆ๋‚˜ ๋ถˆํŽธํ•œ์ง€ ์ƒ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž„์˜์˜ ์ œ๋™์œผ๋กœ ์ธํ•ด ๊ฒŒ์ž„์ด ๋ถˆ์•ˆ์ •ํ•˜๊ณ  ๋ถˆ์•ˆ์ •ํ•˜๊ฒŒ ๋Š๊ปด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

7.2 ํ–ฅ์ƒ๋œ ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ

์ˆœ์ง„ํ•œ ๊ตฌํ˜„์„ ์ผ๋ถ€ ๊ฐœ์„ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ฒซ์งธ, ์šฐ๋ฆฌ๋Š” ์‚ฌ์šฉ ๋ Œ๋”๋ง ์ง€์—ฐ 100ms ๋™์•ˆ. ์ด๋Š” ํด๋ผ์ด์–ธํŠธ์˜ "ํ˜„์žฌ" ์ƒํƒœ๊ฐ€ ์„œ๋ฒ„์˜ ๊ฒŒ์ž„ ์ƒํƒœ๋ณด๋‹ค ํ•ญ์ƒ 100ms ๋’ค์ฒ˜์ง„๋‹ค๋Š” ๊ฒƒ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ์„œ๋ฒ„์˜ ์‹œ๊ฐ„์ด 150, ํด๋ผ์ด์–ธํŠธ๋Š” ๋‹น์‹œ ์„œ๋ฒ„์˜ ์ƒํƒœ๋ฅผ ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. 50:

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์˜ˆ์ธกํ•  ์ˆ˜ ์—†๋Š” ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ ์‹œ๊ฐ„์—์„œ ์‚ด์•„๋‚จ์„ ์ˆ˜ ์žˆ๋Š” 100ms ๋ฒ„ํผ๊ฐ€ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค.

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
์ด๊ฒƒ์— ๋Œ€ํ•œ ๋ณด์ƒ์€ ์˜๊ตฌ์ ์ผ ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ž…๋ ฅ ์ง€์—ฐ 100ms ๋™์•ˆ. ์ด๋Š” ์›ํ™œํ•œ ๊ฒŒ์ž„ ํ”Œ๋ ˆ์ด๋ฅผ ์œ„ํ•œ ์‚ฌ์†Œํ•œ ํฌ์ƒ์ž…๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ํ”Œ๋ ˆ์ด์–ด(ํŠนํžˆ ์บ์ฃผ์–ผ ํ”Œ๋ ˆ์ด์–ด)๋Š” ์ด๋Ÿฌํ•œ ์ง€์—ฐ์„ ์•Œ์•„์ฐจ๋ฆฌ์ง€๋„ ๋ชปํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ๋žŒ๋“ค์ด ์˜ˆ์ธกํ•  ์ˆ˜ ์—†๋Š” ๋Œ€๊ธฐ ์‹œ๊ฐ„์œผ๋กœ ํ”Œ๋ ˆ์ดํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค ์ผ์ •ํ•œ 100ms ๋Œ€๊ธฐ ์‹œ๊ฐ„์— ์ ์‘ํ•˜๋Š” ๊ฒƒ์ด ํ›จ์”ฌ ์‰ฝ์Šต๋‹ˆ๋‹ค.

๋ผ๋Š” ๋˜ ๋‹ค๋ฅธ ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ ์ธก ์˜ˆ์ธก์ธ์ง€ ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์ค„์ด๋Š” ๋ฐ ํšจ๊ณผ์ ์ด์ง€๋งŒ ์ด ๊ฒŒ์‹œ๋ฌผ์—์„œ๋Š” ๋‹ค๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ๊ฐ€ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š” ๋˜ ๋‹ค๋ฅธ ๊ฐœ์„  ์‚ฌํ•ญ์€ ์„ ํ˜• ๋ณด๊ฐ„. ๋ Œ๋”๋ง ์ง€์—ฐ์œผ๋กœ ์ธํ•ด ์ผ๋ฐ˜์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ ํ˜„์žฌ ์‹œ๊ฐ„๋ณด๋‹ค ํ•œ ๋ฒˆ ์ด์ƒ ์—…๋ฐ์ดํŠธ๋ฉ๋‹ˆ๋‹ค. ๋ถ€๋ฅผ ๋•Œ 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(). ์•ž์„œ ์‚ดํŽด๋ณธ ๊ฒƒ์ฒ˜๋Ÿผ ๋ชจ๋“  ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ์—๋Š” ์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ํฌํ•จ๋ฉ๋‹ˆ๋‹ค. ๋ Œ๋”๋ง ๋Œ€๊ธฐ ์‹œ๊ฐ„์„ ์‚ฌ์šฉํ•˜์—ฌ ์„œ๋ฒ„ ๋’ค์—์„œ 100ms ๋’ค์— ์ด๋ฏธ์ง€๋ฅผ ๋ Œ๋”๋งํ•˜๋ ค๊ณ  ํ•˜์ง€๋งŒ ์šฐ๋ฆฌ๋Š” ์„œ๋ฒ„์˜ ํ˜„์žฌ ์‹œ๊ฐ„์„ ๊ฒฐ์ฝ” ์•Œ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค, ์—…๋ฐ์ดํŠธ๊ฐ€ ๋„์ฐฉํ•˜๋Š” ๋ฐ ์‹œ๊ฐ„์ด ์–ผ๋งˆ๋‚˜ ๊ฑธ๋ ธ๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ธํ„ฐ๋„ท์€ ์˜ˆ์ธกํ•  ์ˆ˜ ์—†์œผ๋ฉฐ ์†๋„๊ฐ€ ํฌ๊ฒŒ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!

์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ํ•ฉ๋ฆฌ์ ์ธ ๊ทผ์‚ฌ์น˜๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฒซ ๋ฒˆ์งธ ์—…๋ฐ์ดํŠธ๊ฐ€ ์ฆ‰์‹œ ๋„์ฐฉํ•œ ์ฒ™. ์ด๊ฒƒ์ด ์‚ฌ์‹ค์ด๋ผ๋ฉด ์šฐ๋ฆฌ๋Š” ์ด ํŠน์ • ์ˆœ๊ฐ„์˜ ์„œ๋ฒ„ ์‹œ๊ฐ„์„ ์•Œ ๊ฒƒ์ž…๋‹ˆ๋‹ค! ์šฐ๋ฆฌ๋Š” ์„œ๋ฒ„์˜ ํƒ€์ž„ ์Šคํƒฌํ”„๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค firstServerTimestamp ๊ทธ๋ฆฌ๊ณ  ์šฐ๋ฆฌ์˜ ํ˜„์ง€์˜ (ํด๋ผ์ด์–ธํŠธ) ํƒ€์ž„์Šคํƒฌํ”„ gameStart.

์˜ค ์ž ๊น๋งŒ. ์„œ๋ฒ„์‹œ๊ฐ„=ํด๋ผ์ด์–ธํŠธ์‹œ๊ฐ„ ์•„๋‹Œ๊ฐ€์š”? "์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„"์™€ "ํด๋ผ์ด์–ธํŠธ ํƒ€์ž„์Šคํƒฌํ”„"๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์ข‹์€ ์งˆ๋ฌธ์ž…๋‹ˆ๋‹ค! ๊ทธ๋“ค์€ ๊ฐ™์€ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค. Date.now() ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ ์ด๋Ÿฌํ•œ ์‹œ์Šคํ…œ์˜ ๋กœ์ปฌ ์š”์ธ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค. ํƒ€์ž„์Šคํƒฌํ”„๊ฐ€ ๋ชจ๋“  ์‹œ์Šคํ…œ์—์„œ ๋™์ผํ•  ๊ฒƒ์ด๋ผ๊ณ  ๊ฐ€์ •ํ•˜์ง€ ๋งˆ์‹ญ์‹œ์˜ค.

์ด์ œ ์šฐ๋ฆฌ๋Š” ๋ฌด์—‡์„ ์ดํ•ด currentServerTime(): ๋ฐ˜ํ™˜ ํ˜„์žฌ ๋ Œ๋”๋ง ์‹œ๊ฐ„์˜ ์„œ๋ฒ„ ํƒ€์ž„์Šคํƒฌํ”„. ์ฆ‰, ์ด๊ฒƒ์€ ์„œ๋ฒ„์˜ ํ˜„์žฌ ์‹œ๊ฐ„(firstServerTimestamp <+ (Date.now() - gameStart)) ๋นผ๊ธฐ ๋ Œ๋”๋ง ์ง€์—ฐ(RENDER_DELAY).

์ด์ œ ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. ์—…๋ฐ์ดํŠธ ์„œ๋ฒ„์—์„œ ์ˆ˜์‹ ํ•˜๋ฉด ํ˜ธ์ถœ processGameUpdate()์ƒˆ๋กœ์šด ์—…๋ฐ์ดํŠธ๋ฅผ ๋ฐฐ์—ด์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. gameUpdates. ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ด์ „ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ชจ๋‘ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์—…๋ฐ์ดํŠธ๋” ์ด์ƒ ํ•„์š”ํ•˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

"๊ธฐ๋ณธ ์—…๋ฐ์ดํŠธ"๋ž€ ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์ด๊ฒƒ ์„œ๋ฒ„์˜ ํ˜„์žฌ ์‹œ๊ฐ„์—์„œ ๋’ค๋กœ ์ด๋™ํ•˜์—ฌ ์ฐพ์€ ์ฒซ ๋ฒˆ์งธ ์—…๋ฐ์ดํŠธ. ์ด ๋‹ค์ด์–ด๊ทธ๋žจ์„ ๊ธฐ์–ตํ•˜์‹ญ๋‹ˆ๊นŒ?

๋ฉ€ํ‹ฐํ”Œ๋ ˆ์ด์–ด .io ์›น ๊ฒŒ์ž„ ๋งŒ๋“ค๊ธฐ
"ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง ์‹œ๊ฐ„" ๋ฐ”๋กœ ์™ผ์ชฝ์— ์žˆ๋Š” ๊ฒŒ์ž„ ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ธฐ๋ณธ ์—…๋ฐ์ดํŠธ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์—…๋ฐ์ดํŠธ๋Š” ์–ด๋–ค ์šฉ๋„๋กœ ์‚ฌ์šฉ๋˜๋‚˜์š”? ์—…๋ฐ์ดํŠธ๋ฅผ ๊ธฐ์ค€์„ ์œผ๋กœ ๋–จ์–ด๋œจ๋ฆด ์ˆ˜ ์žˆ๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ž…๋‹ˆ๊นŒ? ์ด๊ฒƒ์„ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด, ๋งˆ์นจ๋‚ด ๊ตฌํ˜„์„ ๊ณ ๋ ค 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 ์— ๊นƒํ—ˆ๋ธŒ.

ํŒŒํŠธ 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 ๊ตฌ์„ฑ์„ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋‘ ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์œผ๋กœ ์‚ฌ์šฉํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

  • ์‚ฌ์šฉ ์›นํŒฉ ๊ฐœ๋ฐœ ๋ฏธ๋“ค์›จ์–ด ๊ฐœ๋ฐœ ํŒจํ‚ค์ง€๋ฅผ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋นŒ๋“œํ•˜๊ฑฐ๋‚˜
  • ์ •์ ์œผ๋กœ ํด๋” ์ „์†ก dist/, ํ”„๋กœ๋•์…˜ ๋นŒ๋“œ ํ›„ Webpack์ด ํŒŒ์ผ์„ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค.

๋˜ ๋‹ค๋ฅธ ์ค‘์š”ํ•œ ์ž‘์—… server.js ์„œ๋ฒ„๋ฅผ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค ์†Œ์ผ“.ioExpress ์„œ๋ฒ„์— ์—ฐ๊ฒฐ๋ฉ๋‹ˆ๋‹ค.

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.

2. ๊ฒŒ์ž„ ์„œ๋ฒ„

ํด๋ž˜์Šค 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๋ฅผ ์ฝ”๋“œ>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() ๋งค์šฐ ๊ฐ„๋‹จํ•ฉ๋‹ˆ๋‹ค. ์ ์ˆ˜๋ณ„๋กœ ํ”Œ๋ ˆ์ด์–ด๋ฅผ ์ •๋ ฌํ•˜๊ณ  ์ƒ์œ„ XNUMX๊ฐœ๋ฅผ ์„ ํƒํ•œ ๋‹ค์Œ ๊ฐ๊ฐ์˜ ์‚ฌ์šฉ์ž ์ด๋ฆ„๊ณผ ์ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

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 ๋‹ค์Œ ํ™•์žฅ์ž๋งŒ:

  • ํŒจํ‚ค์ง€ ์‚ฌ์šฉ ์งง์€ ๋ฌด์ž‘์œ„ ์ƒ์„ฑ 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.

์ถฉ๋Œ ๊ฐ์ง€ ๊ตฌํ˜„์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์ถฉ๋Œ.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 ๊ฒŒ์ž„์„ ๋งŒ๋“œ์„ธ์š”!

๋ชจ๋“  ์ƒ˜ํ”Œ ์ฝ”๋“œ๋Š” ์˜คํ”ˆ ์†Œ์Šค์ด๋ฉฐ ๊ฒŒ์‹œ๋ฉ๋‹ˆ๋‹ค. ๊นƒํ—ˆ๋ธŒ.

์ถœ์ฒ˜ : habr.com

์ฝ”๋ฉ˜ํŠธ๋ฅผ ์ถ”๊ฐ€