๋ชจ๋ฐ์ผ RTS ์๋ฒ ํ๋ก๊ทธ๋๋จธ๊ฐ ๋๊ธฐ ์ํ ๋ชจ๋ ๊ฒ์ ๋จ๊ณ๋ณ๋ก ํ์ตํ์ธ์. C#, Node.js, WebSocket, ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ถํฐ ์ค์๊ฐ PvP ์๋ฒ ๊ตฌํ๊น์ง!
์๋ ์์๋๋ก ํ์ตํ๋ฉด ๊ฒ์ ์๋ฒ ๊ฐ๋ฐ์์ ๊ธฐ์ด๋ฅผ ํํํ ์์ ์ ์์ต๋๋ค.
๋ณ์, ํจ์, ํด๋์ค
์๋ฒ ๋ง๋ค๊ธฐ ๊ธฐ์ด
TCP/UDP, HTTP
์ค์๊ฐ ์๋ฐฉํฅ ํต์
MySQL & MongoDB
์๋ฒ ์ด์ ๊ธฐ์ด
๋๊ธฐํ & ๋ฃจํ
์นด๋๋ฅผ ํด๋ฆญํ๋ฉด ์์ธ ๋ด์ฉ๊ณผ ์ฝ๋ ์์ ๋ฅผ ๋ณผ ์ ์์ต๋๋ค.
C#์ ๊ฒ์ ๊ฐ๋ฐ์์ ๊ฐ์ฅ ๋๋ฆฌ ์ฌ์ฉ๋๋ ์ธ์ด์ ๋๋ค. Unity ๊ฒ์ ์์ง๋ C#์ ์ฌ์ฉํ์ฃ . ์๋ฒ ๊ฐ๋ฐ์์๋ ASP.NET์ ํ์ฉํ ๊ณ ์ฑ๋ฅ ์๋ฒ ๊ตฌ์ถ์ด ๊ฐ๋ฅํฉ๋๋ค.
// C# ๊ธฐ๋ณธ ์๋ฃํ (Data Types)
// ์ซ์ํ
int hp = 100; // ์ ์: -21์ต ~ 21์ต
float speed = 3.5f; // ์ค์ (์์์ )
double damage = 125.75; // ์ ๋ฐ ์ค์
// ๋ฌธ์์ด
string name = "์ ์ฌ"; // ํ
์คํธ
// ์ฐธ/๊ฑฐ์ง
bool isAlive = true; // true ๋๋ false
// ๊ฒ์์์ ์์ฃผ ์ฐ์ด๋ ํจํด
int[] scores = { 100, 85, 92 }; // ๋ฐฐ์ด
List<string> players = new(); // ๋์ ๋ฆฌ์คํธ
// ๋์
๋๋ฆฌ (key-value ์)
Dictionary<string, int> inventory = new()
{
{ "๊ฒ", 1 },
{ "ํฌ์
", 5 }
};
// ๊ฒ์์์ ์ฌ์ฉ ์์
Console.WriteLine($"{name}์ HP: {hp}");
// ์ถ๋ ฅ: ์ ์ฌ์ HP: 100
๊ฒ์ ์๋ฒ์์๋ int๋ก HP/์ ์, float๋ก ์ขํ/์๋, string์ผ๋ก ์ ์ ID, Dictionary๋ก ํ๋ ์ด์ด ๋ฐ์ดํฐ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค.
// ํจ์ (Method) - ๋์์ ์ ์
// ๊ธฐ๋ณธ ํจ์
static int CalculateDamage(int attack, int defense)
{
int damage = attack - defense;
return Math.Max(damage, 0); // ์ต์ 0
}
// ๊ธฐ๋ณธ๊ฐ์ด ์๋ ๋งค๊ฐ๋ณ์
static void Heal(ref int hp, int amount = 20)
{
hp += amount;
Console.WriteLine($"ํ๋ณต! HP: {hp}");
}
// ์ฌ๋ฌ ๊ฐ ๋ฐํ (ํํ)
static (int x, int y) GetPosition()
{
return (100, 200);
}
// ์ฌ์ฉ ์์
int dmg = CalculateDamage(50, 20);
Console.WriteLine($"๋ฐ๋ฏธ์ง: {dmg}"); // ๋ฐ๋ฏธ์ง: 30
int playerHp = 80;
Heal(ref playerHp); // ํ๋ณต! HP: 100
var pos = GetPosition();
Console.WriteLine($"์์น: ({pos.x}, {pos.y})");
// ํด๋์ค - ๊ฒ์ ๊ฐ์ฒด๋ฅผ ๋ง๋ค๊ธฐ ์ํ ์ค๊ณ๋
class Player
{
// ์์ฑ (Properties)
public string Name { get; set; }
public int Hp { get; private set; }
public int Attack { get; set; }
// ์์ฑ์ (Constructor)
public Player(string name, int hp, int attack)
{
Name = name;
Hp = hp;
Attack = attack;
}
// ๋ฉ์๋
public void TakeDamage(int damage)
{
Hp -= damage;
if (Hp < 0) Hp = 0;
Console.WriteLine($"{Name}์ด {damage} ๋ฐ๋ฏธ์ง! HP: {Hp}");
}
public bool IsAlive() => Hp > 0;
}
// ์ฌ์ฉ ์์
var warrior = new Player("์ ์ฌ", 100, 25);
var mage = new Player("๋ง๋ฒ์ฌ", 70, 40);
warrior.TakeDamage(mage.Attack);
// ์ถ๋ ฅ: ์ ์ฌ์ด 40 ๋ฐ๋ฏธ์ง! HP: 60
// ๋น๋๊ธฐ ํ๋ก๊ทธ๋๋ฐ - ๊ฒ์ ์๋ฒ์ ํต์ฌ!
// async/await = ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๋ค๋ฅธ ์ผ์ ํ ์ ์์
class GameServer
{
// ๋น๋๊ธฐ๋ก ํด๋ผ์ด์ธํธ ์ฐ๊ฒฐ ๋ฐ๊ธฐ
public async Task StartServer()
{
Console.WriteLine("์๋ฒ ์์!");
while (true)
{
// ํ๋ ์ด์ด ์ฐ๊ฒฐ์ ๊ธฐ๋ค๋ฆผ (๋ค๋ฅธ ์์
๊ฐ๋ฅ)
var player = await AcceptPlayer();
Console.WriteLine($"{player} ์ ์!");
// ๋น๋๊ธฐ๋ก ๊ฒ์ ์ฒ๋ฆฌ (blocking ์์)
_ = HandlePlayer(player); // fire-and-forget
}
}
async Task<string> AcceptPlayer()
{
await Task.Delay(1000); // 1์ด ๋๊ธฐ
return "Player_001";
}
async Task HandlePlayer(string player)
{
// ๋น๋๊ธฐ๋ก DB์์ ์ ์ ๋ฐ์ดํฐ ๋ก๋
var data = await LoadFromDB(player);
await SendToClient(player, data);
}
async Task<string> LoadFromDB(string playerId)
{
await Task.Delay(100);
return $"{{\"hp\": 100, \"score\": 0}}";
}
async Task SendToClient(string player, string data)
{
await Task.Delay(50);
Console.WriteLine($"{player}์๊ฒ ๋ฐ์ดํฐ ์ ์ก ์๋ฃ");
}
}
๋๊ธฐ(Sync) ๋ฐฉ์์ ํ ๋ช ์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ๋์ ๋ค๋ฅธ ๋ชจ๋ ํ๋ ์ด์ด๊ฐ ๊ธฐ๋ค๋ ค์ผ ํฉ๋๋ค. ๋น๋๊ธฐ(Async)๋ ๊ธฐ๋ค๋ฆฌ๋ ๋์ ๋ค๋ฅธ ํ๋ ์ด์ด๋ฅผ ์ฒ๋ฆฌํ ์ ์์ด ์์ฒ ๋ช ๋์์ ์์ด ๊ฐ๋ฅํฉ๋๋ค.
Node.js๋ JavaScript๋ก ์๋ฒ๋ฅผ ๋ง๋๋ ๋ฐํ์์ ๋๋ค. ๋น๋๊ธฐ์ ์ต์ ํ๋์ด ์์ด ์ค์๊ฐ ๊ฒ์ ์๋ฒ์ ์ ํฉํฉ๋๋ค.
// server.js - ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ HTTP ์๋ฒ
const http = require('http');
// ์๋ฒ ์์ฑ
const server = http.createServer((req, res) => {
// ๋ชจ๋ ์์ฒญ์ ์๋ต
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: '๊ฒ์ ์๋ฒ๊ฐ ์คํ์ค์
๋๋ค!',
status: 'online',
players: 0
}));
});
// ํฌํธ 3000์์ ๋๊ธฐ
server.listen(3000, () => {
console.log('๐ฎ ์๋ฒ ์์: http://localhost:3000');
});
์คํ: node server.js โ ๋ธ๋ผ์ฐ์ ์์ localhost:3000 ์ ์
// Express๋ก ๊ฒ์ API ์๋ฒ ๋ง๋ค๊ธฐ
// ์ค์น: npm install express
const express = require('express');
const app = express();
app.use(express.json());
// ํ๋ ์ด์ด ๋ฐ์ดํฐ (๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅ)
const players = new Map();
// [POST] ํ๋ ์ด์ด ์์ฑ
app.post('/api/players', (req, res) => {
const { name } = req.body;
const player = {
id: P${players.size + 1},
name,
hp: 100,
score: 0,
createdAt: new Date()
};
players.set(player.id, player);
res.json({ success: true, player });
});
// [GET] ํ๋ ์ด์ด ์กฐํ
app.get('/api/players/:id', (req, res) => {
const player = players.get(req.params.id);
if (!player) {
return res.status(404).json({ error: 'ํ๋ ์ด์ด ์์' });
}
res.json(player);
});
// ์๋ฒ ์์
app.listen(3000, () => {
console.log('๐ฎ API ์๋ฒ ์์!');
});
// EventEmitter - ๊ฒ์ ์ด๋ฒคํธ ์์คํ
const EventEmitter = require('events');
const gameEvents = new EventEmitter();
// ์ด๋ฒคํธ ๋ฆฌ์ค๋ ๋ฑ๋ก
gameEvents.on('playerJoin', (player) => {
console.log(`๐ฎ ${player}๋์ด ์
์ฅ!`);
});
gameEvents.on('playerAttack', (attacker, target, damage) => {
console.log(`โ๏ธ ${attacker} โ ${target}: ${damage} ๋ฐ๋ฏธ์ง`);
});
gameEvents.on('gameOver', (winner) => {
console.log(`๐ ${winner} ์น๋ฆฌ!`);
});
// ๊ฒ์ ์๋ฎฌ๋ ์ด์
gameEvents.emit('playerJoin', '์ ์ฌ');
gameEvents.emit('playerJoin', '๋ง๋ฒ์ฌ');
gameEvents.emit('playerAttack', '์ ์ฌ', '๋ง๋ฒ์ฌ', 25);
gameEvents.emit('gameOver', '์ ์ฌ');
"A๊ฐ ์ผ์ด๋๋ฉด B๋ฅผ ํด๋ผ"๋ฅผ ๋ฏธ๋ฆฌ ๋ฑ๋กํด๋๋ ๋ฐฉ์. ๊ฒ์์์ ํ๋ ์ด์ด ์ ์, ๊ณต๊ฒฉ, ์ฃฝ์ ๋ฑ์ ์ด๋ฒคํธ๋ก ์ฒ๋ฆฌํ๋ฉด ์ฝ๋๊ฐ ๊น๋ํด์ง๋๋ค.
๊ฒ์ ์๋ฒ์ ํต์ฌ์ ๋คํธ์ํฌ์ ๋๋ค. ๋ฐ์ดํฐ๋ฅผ ์ด๋ป๊ฒ ์ฃผ๊ณ ๋ฐ์์ง ์ดํดํด์ผ ํด์.
| ํญ๋ชฉ | TCP | UDP |
|---|---|---|
| ์ ๋ขฐ์ฑ | โ ๋ฐ์ดํฐ ๋ณด์ฅ (์ฌ์ ์ก) | โ ๋ณด์ฅ ์ ๋จ (ไธขๅคฑ ๊ฐ๋ฅ) |
| ์๋ | ์๋์ ์ผ๋ก ๋๋ฆผ | ๋งค์ฐ ๋น ๋ฆ |
| ์ฐ๊ฒฐ | ์ฐ๊ฒฐ ํ์ (3-way handshake) | ์ฐ๊ฒฐ ์์ด ์ ์ก |
| ๊ฒ์ ์ฉ๋ | ๋ก๊ทธ์ธ, ์ฑํ , ์์ดํ , ๊ฒฐ์ | ์ค์๊ฐ ์ ํฌ, ์ด๋, ์คํฌ |
// TCP ์์ผ ์๋ฒ - ๊ฒ์ ์๋ฒ์ ๊ธฐ๋ณธ ๊ตฌ์กฐ
const net = require('net');
const clients = new Set();
const server = net.createServer((socket) => {
console.log('๐ ์ ํ๋ ์ด์ด ์ฐ๊ฒฐ!');
clients.add(socket);
// ๋ฐ์ดํฐ ์์
socket.on('data', (data) => {
const message = data.toString();
console.log(`๐จ ์์ : ${message}`);
// ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ธ๋ก๋์บ์คํธ
for (const client of clients) {
if (client !== socket) {
client.write(message);
}
}
});
// ์ฐ๊ฒฐ ํด์
socket.on('end', () => {
clients.delete(socket);
console.log('๐ ํ๋ ์ด์ด ํด์ฅ');
});
});
server.listen(5000, () => {
console.log('๐ฎ TCP ๊ฒ์ ์๋ฒ: ํฌํธ 5000');
});
WebSocket์ HTTP๋ฅผ ์ ๊ทธ๋ ์ด๋ํ์ฌ ์๋ฐฉํฅ ์ค์๊ฐ ํต์ ์ ๊ฐ๋ฅํ๊ฒ ํฉ๋๋ค. ๊ฒ์์์ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉ๋๋ ํต์ ๋ฐฉ์์ด์์.
// WebSocket ๊ฒ์ ์๋ฒ
// ์ค์น: npm install ws
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// ๊ฒ์ ์ํ
const gameState = {
players: new Map(),
bullets: []
};
wss.on('connection', (ws) => {
const playerId = P${gameState.players.size + 1};
console.log(`๐ฎ ${playerId} ์ ์!`);
// ์ด๊ธฐ ์ํ ์ ์ก
ws.send(JSON.stringify({
type: 'init',
playerId,
players: Object.fromEntries(gameState.players)
}));
// ๋ฉ์์ง ์์
ws.on('message', (data) => {
const msg = JSON.parse(data);
switch (msg.type) {
case 'move':
// ์ด๋ โ ๋ชจ๋ ํ๋ ์ด์ด์๊ฒ ์๋ฆผ
gameState.players.set(playerId, {
x: msg.x, y: msg.y
});
broadcast({
type: 'playerMoved',
playerId,
x: msg.x,
y: msg.y
});
break;
case 'attack':
// ๊ณต๊ฒฉ โ ๋ฐ๋ฏธ์ง ๊ณ์ฐ ํ ์ ํ
broadcast({
type: 'attack',
attacker: playerId,
target: msg.targetId,
damage: msg.damage
});
break;
}
});
// ์ฐ๊ฒฐ ํด์
ws.on('close', () => {
gameState.players.delete(playerId);
broadcast({ type: 'playerLeft', playerId });
});
});
// ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์ก
function broadcast(data) {
const msg = JSON.stringify(data);
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(msg);
}
});
}
console.log('๐ WebSocket ์๋ฒ: ws://localhost:8080');
<!-- ๊ฒ์ ํด๋ผ์ด์ธํธ (๋ธ๋ผ์ฐ์ ) -->
<script>
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('์๋ฒ์ ์ฐ๊ฒฐ๋จ!');
};
// ์๋ฒ์์ ๋ฉ์์ง ์์
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
switch (msg.type) {
case 'init':
console.log(`๋ด ID: ${msg.playerId}`);
break;
case 'playerMoved':
updatePlayerPosition(msg.playerId, msg.x, msg.y);
break;
case 'attack':
showAttackEffect(msg.attacker, msg.target, msg.damage);
break;
}
};
// ์๋ฒ๋ก ์ด๋ ๋ฐ์ดํฐ ์ ์ก
function sendMove(x, y) {
ws.send(JSON.stringify({ type: 'move', x, y }));
}
// ์๋ฒ๋ก ๊ณต๊ฒฉ ์ ์ก
function sendAttack(targetId, damage) {
ws.send(JSON.stringify({
type: 'attack', targetId, damage
}));
}
</script>
ํ๋ ์ด์ด ์ ๋ณด, ์์ดํ , ์ ์ ๋ฑ์ ์๊ตฌ ์ ์ฅํ๋ ค๋ฉด ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ํ์ํฉ๋๋ค.
| DB | ํน์ง | ๊ฒ์์์์ ์ฉ๋ |
|---|---|---|
| MySQL | ๊ด๊ณํ, SQL ๋ฌธ๋ฒ | ์ ์ ์ ๋ณด, ์์ดํ , ์ ์ |
| MongoDB | NoSQL, JSON ํํ | ๋ก๊ทธ, ์ค์ , ์ ์ฐํ ๋ฐ์ดํฐ |
-- ๊ฒ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์ฑ
CREATE DATABASE game_server;
USE game_server;
-- ํ๋ ์ด์ด ํ
์ด๋ธ
CREATE TABLE players (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100),
level INT DEFAULT 1,
gold INT DEFAULT 0,
hp INT DEFAULT 100,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ์์ดํ
ํ
์ด๋ธ
CREATE TABLE items (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50),
type ENUM('weapon', 'armor', 'potion'),
power INT
);
-- CRUD ๊ธฐ๋ณธ ์์
-- ์์ฑ (Create)
INSERT INTO players (username, email) VALUES ('์ ์ฌ', 'warrior@game.com');
-- ์กฐํ (Read)
SELECT * FROM players WHERE level >= 10;
-- ์์ (Update)
UPDATE players SET gold = gold + 100 WHERE username = '์ ์ฌ';
-- ์ญ์ (Delete)
DELETE FROM players WHERE hp <= 0;
// MySQL ์ฐ๋ - ์ค์น: npm install mysql2
const mysql = require('mysql2/promise');
// ์ฐ๊ฒฐ ํ ์์ฑ (์ฑ๋ฅ ์ต์ ํ)
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'password',
database: 'game_server',
waitForConnections: true,
connectionLimit: 10
});
// ํ๋ ์ด์ด ์กฐํ
async function getPlayer(username) {
const [rows] = await pool.execute(
'SELECT * FROM players WHERE username = ?',
[username]
);
return rows[0];
}
// ํ๋ ์ด์ด ์์ฑ
async function createPlayer(username, email) {
const [result] = await pool.execute(
'INSERT INTO players (username, email) VALUES (?, ?)',
[username, email]
);
return result.insertId;
}
// ๊ณจ๋ ์
๋ฐ์ดํธ
async function addGold(playerId, amount) {
await pool.execute(
'UPDATE players SET gold = gold + ? WHERE id = ?',
[amount, playerId]
);
}
// ์ฌ์ฉ ์์
async function main() {
const id = await createPlayer('๋ง๋ฒ์ฌ', 'mage@game.com');
await addGold(id, 500);
const player = await getPlayer('๋ง๋ฒ์ฌ');
console.log(player);
// { id: 1, username: '๋ง๋ฒ์ฌ', gold: 500, ... }
}
๊ฒ์ ์๋ฒ๋ ๋๋ถ๋ถ Linux์์ ์คํ๋ฉ๋๋ค. ๊ธฐ๋ณธ ๋ช ๋ น์ด๋ฅผ ์์๋๋ฉด ์๋ฒ๋ฅผ ๊ด๋ฆฌํ ์ ์์ด์.
# ํ์ผ & ๋๋ ํ ๋ฆฌ
ls -la # ํ์ผ ๋ชฉ๋ก (์จ๊น ํ์ผ ํฌํจ)
cd /home # ๋๋ ํ ๋ฆฌ ์ด๋
mkdir game # ํด๋ ์์ฑ
cp file1 file2 # ํ์ผ ๋ณต์ฌ
mv old new # ํ์ผ ์ด๋/์ด๋ฆ ๋ณ๊ฒฝ
rm file.txt # ํ์ผ ์ญ์
# ํ์ผ ๋ด์ฉ ๋ณด๊ธฐ
cat server.log # ์ ์ฒด ์ถ๋ ฅ
tail -f log.txt # ์ค์๊ฐ ๋ก๊ทธ ๋ชจ๋ํฐ๋ง โญ
head -20 log.txt # ์ 20์ค
grep "error" log.txt # "error" ํฌํจ ์ค ์ฐพ๊ธฐ
# ๋คํธ์ํฌ
curl http://localhost:3000 # HTTP ์์ฒญ ํ
์คํธ
netstat -tlnp # ์ด๋ฆฐ ํฌํธ ํ์ธ
ping google.com # ๋คํธ์ํฌ ์ฐ๊ฒฐ ํ์ธ
# ํ์ผ ํธ์ง (nano ์๋ํฐ)
nano server.js # ํ์ผ ํธ์ง (Ctrl+X โ Y โ Enter ์ ์ฅ)
# ๊ถํ ๊ด๋ฆฌ
chmod +x start.sh # ์คํ ๊ถํ ๋ถ์ฌ
ls -la # ๊ถํ ํ์ธ
# ํ๋ก์ธ์ค ํ์ธ
ps aux # ๋ชจ๋ ํ๋ก์ธ์ค ๋ชฉ๋ก
ps aux | grep node # node ํ๋ก์ธ์ค๋ง ์ฐพ๊ธฐ
top # ์ค์๊ฐ CPU/๋ฉ๋ชจ๋ฆฌ ๋ชจ๋ํฐ
# ํ๋ก์ธ์ค ์ข
๋ฃ
kill PID # ์ ์ ์ข
๋ฃ (์๊ทธ๋ 15)
kill -9 PID # ๊ฐ์ ์ข
๋ฃ (์๊ทธ๋ 9)
# โ
๊ฒ์ ์๋ฒ๋ฅผ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์คํ (์ค์!)
nohup node server.js > output.log 2>&1 &
# nohup: ํฐ๋ฏธ๋ ๋ซ์๋ ๊ณ์ ์คํ
# > output.log: ๋ก๊ทธ๋ฅผ ํ์ผ๋ก ์ ์ฅ
# &: ๋ฐฑ๊ทธ๋ผ์ด๋ ์คํ
# โ
systemd๋ก ์๋ฒ ์๋ ์์ (ํ๋ก๋์
์ฉ)
# /etc/systemd/system/game-server.service ์์ฑ:
cat > /etc/systemd/system/game-server.service << 'EOF'
[Unit]
Description=Game Server
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/game-server
ExecStart=/usr/bin/node server.js
Restart=always
[Install]
WantedBy=multi-user.target
EOF
# ์๋น์ค ๋ฑ๋ก & ์์
systemctl daemon-reload
systemctl enable game-server
systemctl start game-server
systemctl status game-server # ์ํ ํ์ธ
nohup์ ๊ฐ๋จํ์ง๋ง ์๋ฒ๊ฐ ์ฃฝ์ผ๋ฉด ์๋์ผ๋ก ์ฌ์์ ์ ๋ฉ๋๋ค. systemd๋ ์๋ฒ๊ฐ ์ฃฝ์ด๋ ์๋์ผ๋ก ์ด๋ฆฌ๊ณ , ๋ถํ ์ ์๋ ์์๋ฉ๋๋ค. ์ค๋ฌด์์ ํ์!
์ด์ ์ง์ง ๊ฒ์ ์๋ฒ์ ํต์ฌ ๊ฐ๋ ๋ค์ ๋ฐฐ์๋ด ์๋ค!
๊ฒ์ ์๋ฒ๋ 1์ด์ ์ฌ๋ฌ ๋ฒ(ํฑ) ์ํ๋ฅผ ์ ๋ฐ์ดํธํฉ๋๋ค. ๋ณดํต 20~60ํฑ/์ด๋ก ๋์ํฉ๋๋ค. ํฑ๋ง๋ค ๋ชจ๋ ์ ๋์ ์์น, HP, ์ํ๋ฅผ ๊ณ์ฐํด์.
// ๊ฐ๋จํ ๊ฒ์ ๋ฃจํ (20ํฑ/์ด)
const TICK_RATE = 20; // ์ด๋น 20ํ
const TICK_INTERVAL = 1000 / TICK_RATE; // 50ms
const gameWorld = {
units: new Map(),
update() {
// ๋ชจ๋ ์ ๋ ์
๋ฐ์ดํธ
for (const [id, unit] of this.units) {
// ์ด๋ ์ฒ๋ฆฌ
unit.x += unit.vx;
unit.y += unit.vy;
// ์ถฉ๋ ๊ฒ์ฌ
this.checkCollisions(unit);
// ๋ฒํ/๋๋ฒํ ํ์ด๋จธ
this.updateEffects(unit);
}
// ๊ฒ์ ์น๋ฆฌ ์กฐ๊ฑด ์ฒดํฌ
this.checkWinCondition();
},
broadcastState(clients) {
const state = {
type: 'gameState',
tick: this.tickCount,
units: Array.from(this.units.values())
};
const data = JSON.stringify(state);
clients.forEach(c => c.send(data));
}
};
// ๊ฒ์ ๋ฃจํ ์์
let tickCount = 0;
const gameLoop = setInterval(() => {
gameWorld.update();
gameWorld.broadcastState(wss.clients);
tickCount++;
}, TICK_INTERVAL);
console.log(`๐ฎ ๊ฒ์ ๋ฃจํ ์์ (${TICK_RATE} tick/s)`);
๋ชจ๋ ํ๋ ์ด์ด๊ฐ ๊ฐ์ ๊ฒ์ ํ๋ฉด์ ๋ณด๋ ค๋ฉด ์๋ฒ๊ฐ ๊ฒ์ ์ํ๋ฅผ ๋ชจ๋ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํด์ผ ํฉ๋๋ค.
// ์ ๋ต 1: ์ ์ฒด ์ํ ์ ์ก (๋จ์ํ์ง๋ง ๋ฐ์ดํฐ้ ํผ)
// ๋งค ํฑ ๋ชจ๋ ์ ๋์ ์ ์ฒด ์ํ๋ฅผ ๋ณด๋
// ์ฅ์ : ๊ตฌํ ์ฌ์ / ๋จ์ : ๋์ญํญ ๋ง์ด ์ฌ์ฉ
// ์ ๋ต 2: ๋ธํ ์ ์ก (๋ณ๊ฒฝ๋ถ๋ง ์ ์ก) โญ ๊ถ์ฅ
function getDeltaState(prevState, currentState) {
const delta = {};
for (const [id, unit] of currentState) {
const prev = prevState.get(id);
if (!prev || JSON.stringify(prev) !== JSON.stringify(unit)) {
delta[id] = unit; // ๋ณ๊ฒฝ๋ ๊ฒ๋ง ํฌํจ
}
}
return delta;
}
// ์ ๋ต 3: ๋ณด๊ฐ (Interpolation)
// ํด๋ผ์ด์ธํธ์์ ๋ถ๋๋ฌ์ด ์์ง์์ ์ํด
// ์๋ฒ: 50ms๋ง๋ค ์์น ์ ์ก
// ํด๋ผ์ด์ธํธ: ์ค๊ฐ ์์น๋ฅผ ๊ณ์ฐํด์ ๋ถ๋๋ฝ๊ฒ ํ์
function interpolate(from, to, t) {
return from + (to - from) * t; // t: 0~1
}
PvP ๊ฒ์์์ ์ค๋ ฅ์ด ๋น์ทํ ํ๋ ์ด์ด๋ผ๋ฆฌ ๋งค์นญํด์ฃผ๋ ์์คํ ์ ๋๋ค.
// ๋งค์น๋ฉ์ดํน ํ ์์คํ
class MatchMaker {
constructor() {
this.queue = []; // ๋๊ธฐ์ด
this.ratingRange = 100; // ๋งค์นญ ํ์ฉ ๋ฒ์
}
// ํ๋ ์ด์ด ํ์ ์ถ๊ฐ
addPlayer(player) {
this.queue.push({
...player,
joinedAt: Date.now()
});
console.log(`[๋งค์น๋ฉ์ดํน] ${player.name} ๋๊ธฐ์ด ์ง์
(rating: ${player.rating})`);
this.tryMatch();
}
// ๋งค์นญ ์๋
tryMatch() {
// rating ์์ผ๋ก ์ ๋ ฌ
this.queue.sort((a, b) => a.rating - b.rating);
for (let i = 0; i < this.queue.length - 1; i++) {
const p1 = this.queue[i];
const p2 = this.queue[i + 1];
// rating ์ฐจ์ด๊ฐ ๋ฒ์ ์์ด๋ฉด ๋งค์นญ!
if (Math.abs(p1.rating - p2.rating) <= this.ratingRange) {
this.queue.splice(i, 2); // ํ์์ ์ ๊ฑฐ
this.createMatch(p1, p2);
return;
}
}
}
// ๋งค์น ์์ฑ
createMatch(p1, p2) {
console.log(`๐ฎ ๋งค์น ์ฑ์ฌ! ${p1.name} vs ${p2.name}`);
// ์ฌ๊ธฐ์ ๊ฒ์ ๋ฃธ์ ๋ง๋ค๊ณ ๋ ํ๋ ์ด์ด๋ฅผ ์ด๋
}
}
// ์ฌ์ฉ ์์
const matchmaker = new MatchMaker();
matchmaker.addPlayer({ name: '์ ์ฌ', rating: 1200 });
matchmaker.addPlayer({ name: '๋ง๋ฒ์ฌ', rating: 1180 });
// ์ถ๋ ฅ: ๐ฎ ๋งค์น ์ฑ์ฌ! ๋ง๋ฒ์ฌ vs ์ ์ฌ
๋ฐฐ์ด ๋ด์ฉ์ ํ์ธํด๋ณด์ธ์!