แ
แแแแแแถแแแ
แแแแถแ 2015
แแแแปแแแแแธแแแแขแแแแแทแแแแแถแแแแถแแฎแขแแแธแ แแแแแแถแแแแแแแธแแปแแแ แแถแแแแแแแบแแถแ แแแแแขแแกแถแแแแแแถแแขแแแแแแแ
แแแพแแแแแฅแแแทแแแแแ แแแแแถแแแแแฝแแแแ (แแทแแ
แถแแแถแ
แแแถแแแแแธ)แ แแถแแแแแแถแแฝแแแแแแแแแแปแแแนแแขแแแแแแแแแแแถแแแแถแ
แแแพแแแ
แแแแปแแแแแแแแแแแฝแแ แ แแแแ .io แแแแธแแแแแแแแแแ
แแ
แแแแปแแแถแแแแแแถแแแแแแพแแแนแแแแแแแแแแแธแแแแ แแแแแพแแ แแแแ .io แแธแแแแผแ. แ
แแแแแแแแ แถแแแ แแถแแแแ
แแแแแแนแแขแแแธ Javascript แแแปแแแแแแแนแแแแแแแแแแถแแแ แขแแแแแแแผแแแแแขแแแธแแแผแ
แแถแแถแแแแแแแแแแแ this
ะธ
แงแแถแ แแแแแแ แแแแ .io
แแแแแถแแแแแแฝแแแถแแแแแแผแแแ แแพแแแนแแแแแแ
แ แแแแแแแแแบแแถแแแแแแถแแแ แขแแแแแแแแแแแแแแแแถแแแแ
แแแแปแแแแแแแแแแแแถแแขแแแแแแแแแแแแแแแ แแแแถแแแแแแแขแแแแแถแแแแถแแแแแฝแ
แแแแแแแแแแแแแแแแท แ แพแแขแแแแแแแถแแถแแแถแแแแแ แถแแขแแแแแแแแแแแแแแ แแแแแแแแแแแแแแถแแแถแแแถแแแแแแแแฝแแแแ
1. แแทแแแแแถแแแแแแแ / แแ แแถแแแแแแแแแแแแแแแแ
แแแแปแแแผแแแแแถแ
แแถแแแแแผแแแแแแ แงแแถแ แแแแ แแแแ แแผแ แแแแแขแแแแขแถแ แแแแพแแถแแแแแปแแ
แงแแถแ แแแแแแแพแแผแ แแถแแแแแแแ
Express แแบแแถแแแแแแแแแแแแ แแแแแ Node.js แแแแแแแแทแแแแแแปแแแแแแแแแแแแแแแแถแแแธแแแแแแ แแแแแแแแแแ แแแแแsocket.io - แแแแแถแแแ websocket แแแแแถแแแแแแถแแแแแแผแแแทแแแแแแแแแถแ browser แแทแ server แแแแถแแแแแ - แขแแแแแแแแแแแแแแแผแแปแแ แขแแแแขแถแ แขแถแแขแแแธแแผแแ แแแปแแแแแแแผแแแแแพ Webpack แแแ แแธแแแ .
แแแแแถแขแแแธแแแแแ แแถแแแแแแแแแแแแแแแแแแพแแแ แแผแ แ
public/
assets/
...
src/
client/
css/
...
html/
index.html
index.js
...
server/
server.js
...
shared/
constants.js
แแถแแถแแแ/
แขแแแธแแแแแแแแถแแแ
แแแแปแแแแแฝแแ public/
แแนแแแแแผแแแถแแแถแแแแผแแแแแแแถแแแธแแแแ IN public/assets/
แแถแแแผแแแถแแแแแแแแพแแแแแแแแแแแแแแแพแแ
src /
แแผแแแแแแแแถแแแขแแแแแแทแแแ
แแแแปแแแแฏแแแถแ src/
. แ
แแแแแพแ client/
ะธ server/
แแทแแถแแแแแแถแแแแแแฝแแแแแทแ shared/
แแถแแฏแแแถแแแแแแแแแแแผแแแถแแแถแแ
แผแแแแแแถแแแแแถแแแธแแแแแแ แแทแแแแถแแแธแแแแ
2. แแถแแแแแแผแแแแแปแ/แแถแแแแแแแแแแแแ
แแผแ
แแแแแถแแแแแแถแแแแถแแแพ แแพแแแแแพแแแแแแทแแธแแแแแแแแแแแแผแแปแแแพแแแแธแแแแแพแแแแแแแแ
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 build แแแแแแพแแแนแแแถแแแธแแถแแแแ
แแแแปแแแ
dist/
. แแแแปแแแนแแ แ แฏแแแถแแแแแแแแแแพแแ แแแแ แแ js. - แแพแโแแแแพ
แแถแแทแ แแทแแแถแแทแแแแแถแแแแแแแแ แแถแแแแแแแแ@babel/preset-env แแพแแแแธแ แแแแแแผแ JS แแแแแแพแแแแแแถแแแแแแแแทแแธแแปแแแแ แถแแแ - แแพแแแแแปแแแแแพแแแแแแทแแธแแแแฝแแแพแแแแธแแถแแแ CSS แแถแแแขแแแแแแแแแแแแฏแแแถแ JS แ แพแแแแแ แผแแแฝแแแถแแ แแแแแแแแแแฝแแ แแแแปแแแนแแ แ แแถแแแแถแแแแแแพแแ แแแแ แแ css.
แขแแแแแแแ แแแแถแแถแแแแแแแแแถแแแแพแแแแแแแฏแแแถแแแแแ
แแแ
แแแแแ '[name].[contenthash].ext'
. แแฝแแแถแแถแ [name]
แแนแแแแแผแแแถแแแแแฝแแแแแแแแแแแแ
แแแปแ
แแแแ
แผแ (แแแแปแแแแแธแแแแแแพแ แแแแ game
) แแทแ [contenthash]
แแนแแแแแผแแแถแแแแแฝแแแแ hash แแแแถแแทแแถแแแแแฏแแแถแแ แแพแแแแแพแแถแแ
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
แแพแแแแธแแแแแพแแแแแแทแแแแแถแแแแ แแแแแ
แแแแ
แแแแแถแแแแแแแถแแแ
แแแทแแแแแแ
แแถแแแแแแแแแแปแแแแแปแ
แแแแปแแแผแแแแแถแแฑแแแแแกแพแแแแแแแแแ
แแพแแแถแแแธแแแผแแแแแถแ แแผแ
แแแแแขแแแแขแถแ
แขแแปแแแแแแถแแแแ แถแแแแแแถแแแถแแแแแปแแแแแแถแแแแแ แแถแแแแกแพแแแบแแถแแแแ: แแแแผแแแแแแแแแแแแแผแแแแแแกแพแ
$ git clone https://github.com/vzhou842/example-.io-game.git
$ cd example-.io-game
$ npm install
แ แพแแขแแแแแแแแแแแแฝแแแ ! แแพแแแแธแ แถแแแแแแพแแแแถแแแธแแแแขแแทแแแแแแ แแแแถแแแแแแแแพแแแถแ
$ npm run develop
แ แพแแ
แผแแแ
แแถแแแแแแแแทแแธแแปแแแแแแแแถแ
3. แ แแแปแ แ แผแแแแแแขแแทแแทแแ
แแแแ
แปแแแผแแ แแแแแแแแฝแแฏแแ แแแแผแแแพแแแแแผแแแถแแแแแแ index.html
แแ
แแแแ
แผแแแ
แแถแแแแแ แแแแแ แแแแแแทแแธแแปแแแแแนแแแแแปแแแถแแถแแปแแแทแแ แแแแแแแแแแแพแแแนแแแถแแแแแแแแแถแแแแแ
index.html
แงแแถแ แแแ .io แ แแแแ แแแ
แงแแถแ แแแแแผแแแแแแแแผแแแถแแแแแพแฑแแแแถแแแแแแแแแทแ
แแแแแถแแแแถแแ
แแแถแแแแถแแ แ แพแแแแแปแแแนแแแแแพแแผแ
แแแแถแแถแแฝแแแนแแงแแถแ แแแแแแแถแแแแแ แแแแแแแแแแแแถแ
แแแพแแ แแแแแผแแแแแขแถแ
แแพแแแถแแแแแแแแแ
แแพแโแแถแ:
แแถแแปแแแแถแแแแแแแถแแ HTML5 (<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);
};
});
แแถแขแถแ แแแแถแแแแ แแแแปแแแแแถแ แแแปแแแแแแทแแแถแแขแแแธแ แแแพแแแแแ แแธแแแแ
- แแถแแ แผแแฏแแแถแ JS แแฝแแ แแแฝแแแแแ
- แแถแแแถแแ แผแ CSS (แแผแ แแแแ Webpack แแนแแแถแแแแ แผแแแฝแแแถแแ แแแแปแแแแแ แแ CSS แแแแแแพแ)แ
- แแพแแแแแพแแแถแ
connect()
แแพแแแแธแแแแแพแแแถแแแแแแถแแแแถแแฝแแแแถแแแธแแแ แ แพแแแแแพแแแถแdownloadAssets()
แแพแแแแธแแถแแแแแผแแแถแแแแแแแแผแแแถแแแพแแแแธแแแแ แถแแ แแแแแ - แแแแแถแแแแธแแแแ
แแแแแแถแแแแถแแแธ 3 แแแบแแปแแแแแแแผแแแถแแแแแ แถแ (
playMenu
). - แแแแแแงแแแแแแแแแแแแถแแแแแแถแแแแถแแ แปแ แแแผแแปแ "แแแ" แ แแ แแแแแแแแแผแแปแแแแแผแแแถแแ แปแ แแแแแผแแ แถแแแแแแพแแ แแแแ แ แพแแแแแถแแแแแถแแแธแแแแแถแแพแแแฝแ แแถแแแแแแปแแแถแแแแแ
"แแถแ
แ" แแแแถแแแแแแแแแแทแแแแถแแแถแแแธแแแแแแพแขแแทแแทแแแแแแแแพแแแบแแ
แแแแปแแฏแแแถแแแถแแแแแแแแแแแแผแแแถแแแถแแ
แผแแแแแฏแแแถแ index.js
. แฅแกแผแแแแแแพแแแนแแแทแ
แถแแแถแแฝแแแถแแถแแแขแแแแถแแแแแถแแแแแแแแ
4. แแถแแแแแถแแแแแแผแแแทแแแแแแแขแแทแแทแแ
แแ
แแแแปแแ แแแแแแแ แแพแแแแแพแแแแถแแแแแแแถแแแแแแแแแธแแฝแ แแพแแแแธแแแแถแแแแแแแแถแแฝแแแแถแแแธแแแ
แแพแแแนแแแถแแฏแแแถแแแฝแแ 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
แขแแปแแแแถแแแแแ แแแแแแแแพแแแถแแแแแแพแแแถแแแแแแถแแแ - แแแแแทแแแพแแถแแแแแแถแแแแถแแแแแแแ แแพแแ
แปแแแแแแแแปแแแถแแ แ
แแแแกแแแแแแทแ (
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
.
แแแแแถแแแแธแแถแแแแแแแถแ แขแแแแขแถแ
แ
แถแแแแแแพแแแแแ แถแแ แแผแ
แแแแแถแแแทแแถแแแธแแปแแแพแแแแธแแผแแแพแแแ แแแแแแแพแแแแแพ <canvas>
) แ แแแแแแแแแแพแแแบแแถแแแแแแถแแ แแผแ
แแแแแแพแแแแแถแแแแแแผแแแผแ
แแถแแแแแแแ
- แแถแแแถ
- แแถแแถแขแแแแแแ
- แขแแแแแแแแแแแแแแแแ แแแแปแแ แแแแ
- แแแแ
แแแโแแถโแขแแแแแโแแแแถแแแ 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. แแถแแแแแ แผแแแแแแขแแทแแทแแ
แแแแแแแแแแแพแแ แแแแแ แพแแ แขแถแ
แแแแแถแแ! แแแแแแแถแแแแแแแแถแแนแแแถแแแแแแแแแถแแแแแแถแแแ แแพแแแแธแแแแถแแแแแแผแแแทแแแ
แแแ
แแแถแขแแแแขแถแ
แแแแพแแแแแปแ (แแ
แแพแแปแแแแแผแแแ) แฌแแแแขแแแแแแ (แแ
แแพแงแแแแแแ
แแแ) แ แแพแแแแธแขแแปแแแแแแถแแพแแแนแแ
แปแแแแแแ
แแนแแแแแแแแถแขแแแธแแแถแแแขแแแแแ 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. แแแแถแแแถแแขแแทแแทแแ
แแแแแแแแแแบแแทแแถแแแแแปแแแ แแแแปแแแแแแแแแแผแแแแแแแแถแแ แแปแแแถแแแแนแแ แทแแแ แแพแแทแแแแแแแแขแถแแแพแแแแแผแ! แขแแแแแแแแถแแแขแถแ แแแแแแถ แ แพแแแแแแแแแแแถแแทแแแ แแแแแแแแแ
แแแแแแ แปแแแแแแแแแแแแแแแแแปแแแผแแแแแแแแผแแแถแแแพแแแแธแแแแแแแแแแผแแขแแทแแทแแ/แแแถแแแธแแแแแบ แแแแ. แ แแ แถแโแขแแแแแโแแผแโแแธโแแแแแโแแแแ แถแโแขแแทแแทแแโแฌ?
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แ แแแแแแถแแขแแแธแขแแแแแแแแแแแแฝแแแถแแแถแแขแถแแแแแแแแแ
- แขแแแแแแแแแแแแ แขแถแแแแแแแแแแถแแขแแแธแขแแแแแแแแแแแแแแแแแแ แผแแแฝแแแแแปแแ แแแแแแผแ แแแแถแ
- แแแแถแแแแถแแแแแพแแ แขแถแแแแแแแแแแถแแขแแแธ projectiles แแ แแแแปแแ แแแแแ
- แแถแแถแแแทแแแแปแ แแทแแแแแแแแถแแถแแแทแแแแปแแ แแ แปแแแแแแแ แแ แแแแปแแแถแแแแแแถแแแแแแพแแแนแแแทแแแทแ แถแแแถแแฝแแแแแแ
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;
}
แแแขแถแแ แพแแ แแแถแแ! แแแปแแแแแแแแแทแแแพแแถแแถแแแแแ แแผแแ แแแปแแฝแแแแแปแแ แแแแแ แแแปแแแแแแแถแแขแแปแแแแแแแแแถแแแแแ แถแ แแถแแแแแแขแแแแถแแแปแแแแแ แถแแแ แแนแแขแแแแถแแถแกแทแแถแแแถแแแธแแแ.
แขแแแแถแแแปแแ แ แแแฝแแแแปแ (แง
render()
) แแแแปแแแฝแแแทแแถแแธ แฌ FPS แ แ แแแแแแถแแแแแแถแแแแถแแถแแแพแแแแธแแแแแแ แแถแแแแถแแ แแ แแถแแ 60 FPS แ
แขแแแแถแแธแแ แแแแแแแแแแแแแถแแแธแแแแแแแพแแ แแ แปแแแแแแแแถแแ แแแแแแ แขแแทแแทแแแ แแถแแฟแแแแถแแถแแแถแแขแแแแถแแแปแ. แแ แแแแปแแ แแแแแแแแแแพแ แแแถแแแธแแแแแแแพแแแถแแแ แแแแแแแ 30 แแแแแแแแปแแแฝแแแทแแถแแธแ
แแแแแทแแแพแแพแแแแแถแแแแแแแแ แถแแแถแแขแถแแแแแแ
แปแแแแแแแแแ แแแแ แแแ FPS แแนแแแทแแแแแแพแแแธ 30 แแแแแ แแธแแแแแ แแพแแแทแแแแแแแฝแแแถแแแถแแขแถแแแแแแแพแแแธ 30 แแแแปแแแฝแแแทแแถแแธแแธแแแถแแแธแแแแแแ. แแแแแธแแถแแพแแ แ
render()
60 แแแแแแปแแแฝแแแทแแถแแธ แแแแแถแแแแแแถแแแแแแแถแแแแแถแแ แ
แแผแแแแแแแแถแแแแแแแนแแแแแถแแแแแแผแแกแพแแแทแแแผแแแฟแแแแแแแแ แแแแแทแแ
แถแแแถแ
แแแแแพแขแแแธแแถแแแขแแแ แแแแ แถแแฝแแแแแแถแแฝแแแถแแขแแปแแแแแแแแแแแแแแบแแถแแถแ แแถแแแนแแแแแแถแแแแ. แแถแแฝแแแนแแแแแฟแแขแแธแแแบแแทแแแแแแข แขแแทแแทแแแแนแแแแฝแแแถแแแถแแขแถแแแแแแ แแแแแแแถแแแทแแแแแถแแแแแแแถแแ 33ms (30 แแแแปแแแฝแแแทแแถแแธ)แ
แแถแขแแปแแ แแแแถแแขแแแธแแแขแฅแแแแ
แแแแแ แแผแแแถแแแถแแแแแแแแแถแแแแแแนแแแถแแ
แแถแแขแแปแแแแแแแแแแแแแแแแแบแแถแแแแธแแแขแถแแแแแแแแแปแแแ
แแแแแแแแถแแแแแแแถแแแบแแแแถแแ แแแแแทแแแพแแถแแขแถแแแแแแ แแแแแแแแผแแแถแแแแฝแแแถแแฝแแแนแแแถแแแแแแถแแแ 50ms แแแ แแผแแขแแทแแทแแ แแแแแแ 50ms แแแแแแแถแแ
แแแแแแ แถแแแแแถแแแถแแ แแแแแแธแแถแแขแถแแแแแแแธแแปแแ แขแแแโแขแถแ
โแแแแแโแแพแโแแถโแแพโแแถโแแทแโแแแแฝแโแแแแแถแแโแขแแแโแแแโแแแปแแแแถแ แแถแโแ
แถแแโแ แแแแแถแแโแแแโแแแแถแโแแนแโแแแแพโแฑแแโแ แแแแโแแถแโแขแถแแแแแแโแแแแแแแถแแ แแทแโแแทแโแแแแทแแแแแแแ
7.2 แแแแถแแแถแแขแแทแแทแแแแแแแพแแกแพแ
แแพแโแแนแโแแแแพโแแถแโแแโแแแแขโแแฝแโแ แแแฝแโแ แแแแโแแถแโแขแแปแแแแโแแแโแแแแแแแแแ แแแแผแแแพแแแแแพ แแถแแแแแแถแแแแแแแถแแแแแ แถแ แแแแแถแแ 100 ms แ แแแแแถแแแแแแถแแแแถแแแถแ "แแ แแ แปแแแแแแ" แแแแแขแแทแแทแแแแนแแแแแแแแบแแแแถแแแ แแธแแแแแแแแแถแแแถแแแแ แแแแแแ แแพแแแถแแแธแแแ 100ms แ แงแแถแ แแแแแแแแทแแแพแแแแแแแถแแ แแพแแแถแแแธแแแแแบ 150แแแแแถแแแแแแแถแแแธแแแแแแแแนแแแแแ แถแแแแแถแแแถแแแแแแแถแแแธแแแแแ แแแแแแแ 50:
แแถแแแแแแฑแแแแพแแแผแแแแทแแแแแแแขแถแแแแ 100ms แแพแแแแธแแแแแถแแแถแแแแแแแแถแแแแพแแ
แแ
แปแแแแแแแแถแแ แแแแแแแแแทแแขแถแ
แแถแแแปแแแถแแปแแแถแแ
แแถแแแผแแถแแแแแแแถแแแแถแแแแแแนแแแถแแแถแแแแแ แผแ
แแพแแแแขแถแ แแแแพแแ แแ แแแแแแแฝแแแแแ แ แแถ
แแถแแแแแถแแแแแแธแแถแแธแขแแทแแทแแ แแแแแแแพแแถแแแถแแแถแแแแขแแแแปแแแถแแแถแแแแแแแแแถแแแบแแแแถแแแแแถแแแแแแพแ แแแปแแแแแแนแแแทแแแแแผแแแถแแแแแแแแแแแแแ แแแแปแแแถแแแแแแถแแแแแแแ
แแถแแแแแแแขแแฝแแแแแแแแแพแแแแแปแแแแแพแแบ แขแแแแแแแผแแแธแแแขแแแ. แแแแแถแโแแถแโแแแแ แถแโแแถแแแบแแแแถแ แแถแแแแแแถโแแพแโแแถแโแแถแแขแถแแแแแโแแแถแแ แแ
แแถแแโแแฝแโแแปแโแแแโแแ
แแ
แปแแแแแแโแแ
แแแแปแโแแแถแแแธแแแแแแแ แแ
แแแแแแ แ
getCurrentState()
แแพแแขแถแ
แแแแแทแแแแแทแแถแแ
แแถแแแแแแแถแแแแแ แถแขแแแแถแแแปแแ แฅแกแผแแแแแแพแแขแถแ
แแแแ แถแแแแปแแแแแฝแแแแแแถแแขแแแแถแแแปแแแถแแฝแแแแแแพแแ
แแแแถแ!
7.3 แแถแแขแแปแแแแแแแแถแแแถแแขแแทแแทแแแแแแแแแแพแแกแพแ
แงแแถแ แแแแแแแถแแขแแปแแแแแแ
แแแแปแ src/client/state.js
แแแแพแแถแแ render lag แแทแ linear interpolation แแแปแแแแแแทแแแผแแแแ แ
แผแแแแแแแแผแแแถแแธแแแแแแแ แแแแแบแแถแแธแแฝแแ
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()
. แแผแ
แแแแแพแแแถแแแพแแแปแแแแ แแถแแแแถแแขแถแแแแแแ แแแแแแฝแแแแแ
แผแแแถแแแแแถแแแแแแแถแแแแแแแถแแแธแแแแ แแพแแ
แแแแแแพ render latency แแพแแแแธแแแแ แถแแแผแแแถแ 100ms แแ
แแถแแแแแแ server แแแปแแแแ แแพแแแนแแแทแแแนแแแธแแแแแแแถแแ
แแ
แปแแแแแแแแ
แแพแแแถแแแธแแแแแแแแแโแแถแโแแโแแพแโแแทแโแขแถแ
โแแนแโแแถโแแพโแแถโแแแแผแโแ
แแแถแโแแแโแแแปแแแแถแโแแแแแถแแโแแถแโแขแถแแแแแโแแถโแแฝแโแแพแแแแธโแแโแแแโแแพแแ แขแแธแแแบแแแแแทแแขแถแ
แแถแแแปแแแถแแปแแแถแ แ แพแแแแแฟแแแแแแแถแขแถแ
แแแแแแแแฝแแแแถแแแแแถแแ!
แแพแแแแธแแแแแแแถแแแแแ แถแแแ แแพแแขแถแ
แแแแพแแถแแแแถแแแแแแถแแแแ แแแปแแแ แแพแ แแแแพแแปแแแถแแถแแขแถแแแแแแแแแผแแแถแแแแแแแแแแถแแ. แแแแแทแแแพแแแแแถแแถแแแทแ แแแแแพแแแนแแแนแแแธแแแแแแแถแแแแแแแถแแแธแแแแแ
แแแแแแ! แแพแแแแแแถแแปแแแแแถแแแแแแแถแแแแแแแถแแแธแแแ firstServerTimestamp
แแทแแแแแแถแแแแแแพแแ แแแแปแแแแแปแ (แขแแทแแทแแ) แแแแถแแแแแแแถแแ
แแแแแแแฝแแแ
แแแแปแ gameStart
.
แขแผโแ
แถแโแแแแแทแ
แ แแพโแแถโแแทแโแแฝแโแแถโแแแแโแแแถแแแธแโแแแแแพ = แแแแโแแแแโแขแแทแแทแแ? แ แแแปแขแแแธแแถแแแถแแพแแแแแ
แแแแแถแ "แแแแถแแแแแแแถแแแถแแแธแแแ" แแทแ "แแแแถแแแแแแแถแขแแทแแทแแ"? แแแแแถแแแแฝแแแแขแแแ
แถแแแ! แแถแแแแแแถแแฝแแแแแทแแแแแแถแแฟแแแผแ
แแแแถแแแ Date.now()
แแนแแแแแกแแแแแแถแแแแแแแถแแแแแแแแแถแแ
แแแแปแแแแถแแแธแแแแแแ แแทแแแแถแแแธแแแ แ แพแแแถแขแถแแแแแแแพแแแแแถแแแแปแแแแแปแแ
แแแแแแแถแแแธแแแถแแแแแแ แแปแแแแแแแแแถแแแแถแแแแแแแถแแนแแแผแ
แแแแถแแ
แแพแแแถแแแธแแแถแแแขแแแ
แฅแกแผแแแแแแพแแแแแแธแขแแแธแแแแแแแพ currentServerTime()
แ แแถแแแแกแแแแแแทแ แแแแถแแแแแแแถแแแถแแแธแแแแแแแแแแแแถแแแแ แถแแแ
แแ
แปแแแแแแ. แแแแแถแแแทแแแแ แแแแแบแแถแแแแแแแถแแ
แแ
แปแแแแแแแแแแแแแถแแแธแแแ (firstServerTimestamp <+ (Date.now() - gameStart)
) แแแแถแแแแแแถแแแแแแแ แถแ (RENDER_DELAY
).
แฅแกแผแแแแ แแผแแแแแกแแแแพแแแธแแแแแแแแแพแแแแแแแแถแแแถแแขแถแแแแแแ แแแแแ แแ
แแแแแแฝแแแถแแแธแแแถแแแธแแแแขแถแแแแแ แแถแแแแผแแแถแแ แ
processGameUpdate()
แ แพแแแพแแแแแแถแแปแแแถแแขแถแแแแแแแแแธแแ
แขแถแแแแฝแแ gameUpdates
. แแแแแถแแโแแ แแพแแแแธโแแทแแทแแแโแแพแโแแถแโแแแแพโแขแแแโแ
แแ
แถแ แแพแโแแปแโแแถแโแขแถแแแแแโแ
แถแแโแแถแแโแขแแโแ
แแโแแธโแแปแแ แแถแแแแแพแแ
แแ
แปแแแแแแแแถแแแผแแแแแถแแแแแแถแแแแแพแแแทแแแแแผแแแถแแแฝแแแแแแแแแ
แแพแขแแแธแแ แแถ "แแถแแแแแพแแ แแ แปแแแแแแแแถแแแผแแแแแถแ"? แแแแ แแถแแแแแพแแ แแ แปแแแแแแแแถแแแแแผแแแแแแพแแแแแพแแแแแแแแถแแแแธแแแแแแแแแธแแแแแแแถแแ แแ แปแแแแแแแแแแแแแถแแแธแแแ. แ แแ แถแแแแแถแแแแถแแแแแแ?
แแถแแขแถแแแแแแ แแแแแแแแแแแถแแแแ
แแถแแแแแแแแ "แแแแแแแถแแแแ แถแแขแแทแแทแแ" แแบแแถแแถแแแแแพแแ
แแ
แปแแแแแแแแถแแแผแแแแแถแแ
แแพแแถแแแแแพแแ
แแ
แปแแแแแแแแถแแแผแแแแแถแแแแแพแแแแแถแแแขแแแธ? แ แแแปแขแแแธแแถแแแถแแพแแขแถแ
แแแแแถแแแแถแแขแถแแแแแแแ
แแถแแแแแแแถแแแแผแแแแแถแ? แแพแแแแธแแแแแแแถแแแฟแแแแ แแแ แแธแแแแปแ แแทแ
แถแแแถแแพแแถแแขแแปแแแแ 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),
};
}
}
แแพแแแแแแแแถแแแธแแแแธแ
base < 0
แแถแโแแแโแแถโแแทแโแแถแโแแถแโแแแแพโแฑแแโแแถแแโแแแแโแแ แผแโแแแโแแแโแแแแถโแแแแ แถแโแแ แแ แปแแแแแแ (แแพแโแแถแโแขแแปแแแแโแแถแโแแพgetBaseUpdate()
) แแถแขแถแ แแพแแกแพแแแแแถแแแแ แแแแ แถแแแแแแพแแ แแแแ แแแแแถแแแถแแแแแ แถแแแธแแถแแแบแแแแถแแ แแแแปแแแแแธแแแ แแพแแแแแพแแถแแขแถแแแแแแ แปแแแแแแแแแแปแแแแแแแฝแแแถแแbase
แแบแแถแแถแแขแถแแแแแแ แปแแแแแแแแแแปแแแแแแพแแแถแแ แแแแขแถแ แแแแแถแแแแแธแแถแแแแแแถแแแแแแแแถแ แฌแแถแแแแแแถแแแขแแธแแแบแแทแแแแแแแ แแแแปแแแแแธแแแ แแพแแแแแแแปแแแแแพแแถแแขแถแแแแแแ แปแแแแแแแแแแปแแแแแแพแแแถแแแแแแแ- แแพแแแถแแแถแแขแถแแแแแแแถแแแแปแ แแทแแแแแแแแแแแแแถ render แแ แแ แปแแแแแแ แแผแ แแแแแแพแแขแถแ แแแแพแแถแแ แขแแแแแแแผแแ!
แขแแแธแแถแแแขแแแแแแแ
แแแแแแ state.js
แแบโแแถโแแถแโแขแแปแแแแโแแโแแถแโแแถแแโแแแแ
แผแโแแธแแแขแแแโแแแโแแถแแแแ (แแแปแแแแโแแฝแโแฑแแโแแปแโ) แแแทแแแทแแแแถแ แแแแแทแแแพแขแแแแ
แแแแปแแแแแถแแแแแแแฝแแฏแ แแแแแถแแแแแแพแ state.js
แแ
แแพ
แแแแแแแธ 2. แแแถแแแธแแแแแแแแแแถแแแแแแ
แแ
แแแแปแแแแแแแแแ แแพแแแนแแแทแแทแแแแแพแแแแแแแแถแแแแแแ Node.js แแแแแแแแแแแแแแแแแแพแแ
1. แ แแแปแ แ แผแแแแถแแแธแแแ
แแพแแแแธแแแแแแแแแ web server แแพแแแนแแแแแพ web framework แแแแแแแทแแแแฝแแแแแแถแแ Node.js แแแแ แ
แแถ 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
แแบโแแพแแแแธโแแแกแพแโแแแถแแแธแโแแแแแพ
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 แแ
แแถแแแแแถแแแธแแแแแแแแแแแแ แแพแแแแแ
แแแแแแแทแแธแแแแแแแถแแแแแนแแแแทแแถแแแแแแแแถแแแแแแแแแแธแ แขแแแแแแแแแแถแแแแแนแแแแทแแถแแแแแแแแแแแแแแถแแแแแแถแแแแฝแแแธแขแแทแแทแแแแแแแแแแแแทแแแแทแแ
แแแแแป singleton 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
แแผแ
แแแแ แแพแโแแทแโแ
แถแแแถแ
แโแแถแแแแโแขแแแธโแแฟแโแแแโแแแ แแแแปแแแนแแ แ
แแถแแ แแแแแแแแถแแแขแแแแแแ.
แแแแแทแแแแแปแแ
แทแแแแแแ แ
แผแแแพแแแแแแแแแแแธแขแแแแงแแถแ แแแแแ
แแแแปแแแแแถแแแแฝแแ Game
:
sockets
แแบโแแถโแแแแแปโแแแโแแแแถแแโแแแโแแแแแถแแโแขแแแโแแแโแแ โแแนแโแแแแโแแแโแแถแโแแแแถแแแแแแโแแถแแฝแโแแนแโแขแแแโแแแแ แแถแขแแปแแแแถแแฑแแแแพแแ แผแแแแแพแแแแแแแแแแแแแแแถแแแขแแแแแแแแแแแแฝแแแแแแแปแแแแแแแแแแแplayers
แแบแแถแแแแแปแแแแแแแถแแ ID แขแแแแแแแแ แแนแแแผแ> แแแแแปแขแแแแแแ
bullets
แแบแแถแขแถแแแแแแแแแป Bullet
แแแแแทแแแถแแแแแถแแแ
แแแถแแแแถแแแ
lastUpdateTime
แแบแแถแแแแแแแถแ
แปแแแแแแแแแแ แแแแแแแแผแแแถแแขแถแแแแแแ แแพแโแแนแโแแพแโแแธโแแแแโแแแโแแถโแแแแผแโแแถแโแแแแพโแแแแปแโแแแโแแแแธแ
shouldSendUpdate
แแบแแถแขแแแแแแแฝแแ แแพแแแแแนแแแพแแแถแแแแแพแแแแถแแแแแแแแถแแแแปแแแแแแถแแแแแแแแแแแแ
แแทแแธแแถแแแแแแ addPlayer()
, removePlayer()
ะธ handleInput()
แแทแแ
แถแแแถแ
แแแแแแแแแ แแแแแแพแแแแปแ server.js
. แแแแแทแแแพแขแแแแแแแผแแแถแแแแแพแฑแแแแถแแ
แแ
แถแแแแแแขแแแแกแพแแแทแ แแผแแแแแแแแแ
แแแแแแแถแแแแแแแแแทแ
แ
แแแแแถแแแ
แปแแแแแแ constructor()
แ
แถแแแแแแพแ แแแแแแแแพแฑแแแแถแแแแแแ แ แแแแ (แแถแแฝแแแนแแแถแแแนแแแถแแแแแแถแแแแแพแแ
แแ
แปแแแแแแแแถแ 60 / s):
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()
แแแแ แแแแถแแถแแแแแแแแแแถแแแแแแปแแแแแแแแแทแแแแถแแถแแแแถแแแธแแแแ แแแแแถแขแแแธแแแแแถแแแแพแแถแแแแแถแแแแแแแแ
- แแแแถแแแแแแแแแปแแแแถแ
dt
แแถแแแแแแแแปแแแ แแถแแแแธแ แปแแแแแแupdate()
. - แแแแพแฑแแแแแแถแแแแแแแแแธแแฝแแแแแแแแกแพแแแทแ แ แพแแแแแแแถแแแฝแแแถแแแแแทแแแพแ
แถแแแถแ
แแ แแพแแแนแแแพแแแถแแขแแปแแแแแแปแแแถแแแแแแ
แแแแแแแแแ แแแแแถแแแแแแแแ แแถแแแแแแแแแถแแแแแแแถแแแแฝแแแพแแแพแแแแธแแนแแแฟแแแแแ
bullet.update()
แแแแกแแแแแแทแtrue
แแแแแทแแแพแแแแถแแแแแแแแแฝแแแแแแแผแแแถแแแแแแแถแ (แแถแแแแถแแ แถแแ แแแแธแแแแแแ) แ - แขแถแแแแแแขแแแแแแแแแแถแแแ แ แพแแแแแแพแแแแแถแแแแถแแแแแแแทแแแพแ
แถแแแถแ
แแ แแพแแแแแนแแแพแแแถแแขแแปแแแแแแแแแ
แแแแแแแแ โ
player.update()
แขแถแ แแแแกแแแแแแแปแแฝแแBullet
. - แแทแแทแแแแแแแพแแแถแแแแแแแแแทแ
แแแแถแแแถแแแแแถแแแแแแแ แแทแแขแแแแแแแแถแแฝแ
applyCollisions()
แแแแแแแกแแแขแถแแแแแแแแถแแแแแแแแแแแแถแแแปแแขแแแแแแแ แแแแแถแแแแถแแแถแแแแแแกแแแแแแทแแแธแแฝแแ แแพแแแแแแพแแแทแแแแปแแแแแขแแแแแแแแแแแถแแแถแแแแถ (แแแแแแแพplayer.onDealtDamage()
) แ แพแแแแแแถแแแแแแ projectile แ แแแแธ arraybullets
. - แแผแแแแแนแ แแทแแแแแแแถแแขแแแแแแแแแแแถแแแแแถแแแแถแแแขแแแ
- แแแแพแแถแแขแถแแแแแแ แแแแแแ
แแถแแแขแแแแแแแแถแแแขแแแ แแแแแถแแแแทแแถแแธ แแแแแแแแแ แ
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
แแถแแแแแแแแแแแแแแแแถแแแแแแแแแปแแแแแแ
- แแแแแแแพแแแแ
แแแแฝแแ
แแแแธ แแแแแถแแแแแแถแแแ แแแแแ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()
แแแแพแแถแแแถแแ
แแแพแ แแถแแทแแแ แแแแกแแ projectile แแแแแพแแแแแแพแแแแแธ แแแแแทแแแพแแแแถแแแแ 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;
}
แแถแแแแแพแแแถแแแแแแแแแทแ แแแแถแแแแแแแแแบแแแขแแแแพแแถแแแทแแแแแแถ แแแแแแแแธแแแแแแแแถ แแแแแทแแแพแ แแแถแแแแถแแ แแแปแ แแแแแถแแแแแแแถแแทแ แแถแแแแแผแแแแแถแแแแแแแถแ. แแแแแถแแแแธแแแแ แแแแถแแแแถแแ แแแปแ แแแแแถแแแแแแแแแแแธแแแบแแทแแแถแแแแพแแนแแแแแผแแแแแถแแแแแแแถแ
แแถแแแทแแแแแถแแแธแแแธแแแแแแแแแแผแแแทแ
แถแแแถแแ
แแธแแแแ
- แแแแถแแแแแแแแแทแแแแแผแแแแแขแแแแแแแแแแแแแแพแแแถแแแ แแแแขแถแ
แแแแแแ
แแถแแแแแแถแแแแแแแแแ
bullet.parentID
ัplayer.id
. - แแแแถแแแแแแแแแแแผแแแถแแแแแแแแแแแแแแปแแแแแธแแแแแแแแขแแแแแแแ
แแแพแแแถแแแแแแแแแถแแแแปแแแแแแแแฝแแ แแพแแแนแแแแแแแแถแแแแแ แถแแแแแแแแแแพแแแแแทแแแแแทแแ
break
แ แแแถแแแถแขแแแแแแแแแแแนแแแแแถแแแแแแแแแแแผแแแถแแแแแพแ แแพแแแแแแแแแถแแแแแแแแ แ แพแแแแแแแ แแแแถแแแแแแแถแแแแแแ
แแถแแแแแ แแ
แขแแแ แพแ! แแพแแแถแแแแแแแแแแแแขแแแธแแแแแแแแถแแแแแขแแแแแแแผแแแนแแแพแแแแธแแแแแพแแ แแแแแแแแแถแ .io แ แแถแโแขแแแธโแแแแแถแแ? แแแแแพแแ แแแแ .io แแแแถแแแแแแฝแแแแแแขแแแ!
แแผแแแแแผแแถแแแขแแแแบแแถแแแแแแแพแแ
แแ แแทแแแแแ แแแแ
แแพ
แแแแแ: www.habr.com