Vite & Socket.IO: A Real-Time Party Game
Hey there, fellow tech enthusiasts! Ever wanted to create a fun, interactive party game? Well, get ready to dive into a code repository snapshot that shows exactly how it's done! We're talking about a real-time party game built with Vite for the frontend, Socket.IO for real-time communication, and a touch of JavaScript magic. Let's break down this awesome project, file by file, and see what makes it tick. This isn't just about the code; it's about understanding the core concepts and how they come together to create a seamless user experience. We will be going over the server and client setup and the key concepts in each file.
Client-Side Fun: counter.js, main.js, and style.css
Let's start with the client-side code, which is the heart of the user experience. The client-side consists of three key files: counter.js, main.js, and style.css. These files work in harmony to create a dynamic and visually appealing interface for the party game. We will explore how these files are set up, and some of the key concepts of each file.
counter.js
The counter.js file is a small but mighty module that demonstrates a basic counter functionality. It's a great example of how to manage state and handle user interactions in JavaScript. The code is concise and easy to understand.
export function setupCounter(element) {
let counter = 0;
const setCounter = (count) => {
counter = count;
element.innerHTML = `count is ${counter}`;
};
element.addEventListener('click', () => setCounter(counter + 1));
setCounter(0);
}
setupCounter(element): This function takes an HTML element as input and sets up the counter functionality within that element. It's an example of modular code. It makes the code reusable and easy to integrate into different parts of the application.let counter = 0: Initializes a counter variable to 0. This variable will keep track of the count.const setCounter = (count) => { ... }: This is a function that updates the counter and displays the current count in the HTML element. It's a good practice to encapsulate the state update logic within a function.element.innerHTML = extit{count is ${counter}}: Updates the content of the HTML element to display the current count.element.addEventListener('click', () => setCounter(counter + 1)): This adds a click event listener to the element. Whenever the element is clicked, thesetCounterfunction is called, incrementing the counter.setCounter(0): Initializes the counter to 0 when the function is first called.
main.js
The main.js file is the entry point of the client-side application. This file is responsible for setting up the basic structure of the application and importing the necessary modules. It demonstrates how to bring together the different components of the application. The code imports the CSS file, images, and the counter functionality.
import './style.css'
import javascriptLogo from './javascript.svg'
import viteLogo from '/vite.svg'
import { setupCounter } from './counter.js'
document.querySelector('#app').innerHTML = `
<div>
<a href="https://vite.dev" target="_blank">
<img src="${viteLogo}" class="logo" alt="Vite logo" />
</a>
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
</a>
<h1>Hello Vite!</h1>
<div class="card">
<button id="counter" type="button"></button>
</div>
<p class="read-the-docs">
Click on the Vite logo to learn more
</p>
</div>
`
setupCounter(document.querySelector('#counter'))
import './style.css': Imports the CSS file to style the application.import javascriptLogo from './javascript.svg': Imports the JavaScript logo image.import viteLogo from '/vite.svg': Imports the Vite logo image.import { setupCounter } from './counter.js': Imports thesetupCounterfunction fromcounter.js.document.querySelector('#app').innerHTML = extit{...}: Sets the HTML content of the#appelement. This is where the main content of the application is rendered.setupCounter(document.querySelector('#counter')): Calls thesetupCounterfunction to initialize the counter, attaching it to the button element.
style.css
The style.css file contains the CSS styles for the application. It defines the visual appearance of the elements. It provides a clean and modern look, and it also includes some basic styling for the counter button. This makes it easy to read and understand.
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #f7df1eaa);
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
:root { ... }: Defines the root variables, which set the basic styles for the entire document.body { ... }: Styles the body element, setting the margin, display, and minimum size.button { ... }: Styles the button element, including its border, padding, font, background color, and transition effects.@media (prefers-color-scheme: light) { ... }: Provides styles for light mode, adjusting the colors to fit the light color scheme.
Server-Side: server/index.js
Now, let's switch gears and explore the server-side code. The server/index.js file is the heart of the real-time functionality of the game. It uses express for the web server and socket.io to handle real-time communication between clients. It shows how to use socket.io to manage connections, handle input, and broadcast game state. Let's see some of the key concepts.
// server/index.js
const express = require("express");
const http = require("http");
const cors = require("cors");
const { Server } = require("socket.io");
/* -------------------- Config -------------------- */
const PORT = process.env.PORT || 3000;
// Match your host frame: strokeRoundedRect(10, 10, 1260, 700, 34)
const FRAME = { x: 10, y: 10, w: 1260, h: 700 };
// Rates
const TICK_MS = 16; // physics ~60 Hz
const SNAPSHOT_MS = 33; // broadcast ~30 Hz
// Movement
const SPEED_PPS = 220;
const SPEED_PER_TICK = SPEED_PPS * (TICK_MS / 1000);
// Avatar collision (big circles only)
const PLAYER_RADIUS = 48;
const BOUNCE = 0.12;
/* -------------------- Server -------------------- */
const app = express();
app.use(cors());
app.use(express.static("public");
app.get("/health", (_, res) => res.send("OK"));
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: "*" } });
/* -------------------- State -------------------- */
const players = new Map(); // id -> { id,name,tint,team,n,photo,pos:{x,y},dir:{x,y} }
/* -------------------- Helpers -------------------- */
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const midX = () => FRAME.x + FRAME.w / 2;
function spawnPos(id) {
const xo = FRAME.x + 130 + (id.charCodeAt(0) % 10) * 110;
const yo = FRAME.y + 150 + (id.charCodeAt(1) % 7) * 80;
return { x: xo, y: yo };
}
function computeTeam(x) {
return x < midX() ? "X" : "O";
}
function lobbyPayload() {
return [...players.values()].map(p => ({
id: p.id,
name: p.name,
tint: p.tint,
team: p.team,
n: p.n,
photo: p.photo || null,
}));
}
function snapshotPayload() {
return {
players: [...players.values()].map(p => ({
id: p.id,
x: p.pos.x | 0,
y: p.pos.y | 0,
team: p.team || null, // include live team for host UI
})),
};
}
/* -------------------- Sockets -------------------- */
io.on("connection", (socket) => {
socket.on("join", (data) => {
const name = String(data?.name || "anon").slice(0, 16);
const tint = data?.tint || "#66ccff";
const n = (typeof data?.n === "number") ? data.n : undefined;
const photo = typeof data?.photo === "string" ? data.photo : undefined;
const pos = spawnPos(socket.id);
const team = computeTeam(pos.x);
players.set(socket.id, {
id: socket.id,
name, tint, team, n, photo,
pos,
dir: { x: 0, y: 0 },
});
socket.emit("joined", { id: socket.id });
io.emit("lobby", lobbyPayload());
// If join contained a photo, also broadcast it so current host updates immediately
if (photo) {
io.emit("addtexture", { id: socket.id, data: photo });
}
});
// Controller uploads/updates avatar image
socket.on("addtexture", (msg) => {
const p = players.get(socket.id);
if (!p) return;
const data = typeof msg?.data === "string" ? msg.data : null;
if (!data) return;
p.photo = data; // persist
io.emit("addtexture", { id: socket.id, data }); // live update
io.emit("lobby", lobbyPayload()); // hydrate late joiners
});
// Live input
socket.on("input", (msg) => {
const p = players.get(socket.id);
if (!p) return;
const dx = clamp(Number(msg?.dx || 0), -1, 1);
const dy = clamp(Number(msg?.dy || 0), -1, 1);
p.dir.x = Number.isFinite(dx) ? dx : 0;
p.dir.y = Number.isFinite(dy) ? dy : 0;
if (msg?.action === 1) io.emit("action", { by: p.id, action: 1 });
if (msg?.action === 2) io.emit("action", { by: p.id, action: 2 });
});
socket.on("disconnect", () => {
players.delete(socket.id);
io.emit("lobby", lobbyPayload());
});
});
/* -------------------- Physics -------------------- */
setInterval(() => {
for (const p of players.values()) {
p.pos.x += p.dir.x * SPEED_PER_TICK;
p.pos.y += p.dir.y * SPEED_PER_TICK;
const minX = FRAME.x + PLAYER_RADIUS;
const maxX = FRAME.x + FRAME.w - PLAYER_RADIUS;
const minY = FRAME.y + PLAYER_RADIUS;
const maxY = FRAME.y + FRAME.h - PLAYER_RADIUS;
p.pos.x = clamp(p.pos.x, minX, maxX);
p.pos.y = clamp(p.pos.y, minY, maxY);
// Auto-assign team based on current x-position
const newTeam = computeTeam(p.pos.x);
if (newTeam !== p.team) {
p.team = newTeam;
// Optional: emit a small event if you want instant UI updates
io.emit("teamchange", { id: p.id, team: p.team });
}
}
// Avatar-Avatar collisions only
const list = [...players.values()];
for (let i = 0; i < list.length; i++) {
for (let j = i + 1; j < list.length; j++) {
const a = list[i], b = list[j];
let dx = b.pos.x - a.pos.x;
let dy = b.pos.y - a.pos.y;
let dist = Math.hypot(dx, dy);
const minDist = PLAYER_RADIUS + PLAYER_RADIUS;
if (dist < 1e-6) { dx = 1; dy = 0; dist = 1; }
if (dist < minDist) {
const overlap = minDist - dist;
const nx = dx / dist;
const ny = dy / dist;
const push = overlap / 2;
a.pos.x -= nx * push; a.pos.y -= ny * push;
b.pos.x += nx * push; b.pos.y += ny * push;
a.pos.x -= a.dir.x * BOUNCE * overlap;
a.pos.y -= a.dir.y * BOUNCE * overlap;
b.pos.x -= b.dir.x * BOUNCE * overlap;
b.pos.y -= b.dir.y * BOUNCE * overlap;
const minX = FRAME.x + PLAYER_RADIUS;
const maxX = FRAME.x + FRAME.w - PLAYER_RADIUS;
const minY = FRAME.y + PLAYER_RADIUS;
const maxY = FRAME.y + FRAME.h - PLAYER_RADIUS;
a.pos.x = clamp(a.pos.x, minX, maxX);
a.pos.y = clamp(a.pos.y, minY, maxY);
b.pos.x = clamp(b.pos.x, minX, maxX);
b.pos.y = clamp(b.pos.y, minY, maxY);
}
}
}
}, TICK_MS);
/* -------------------- Snapshots -------------------- */
setInterval(() => {
io.emit("snapshot", snapshotPayload());
}, SNAPSHOT_MS);
/* -------------------- Start -------------------- */
server.listen(PORT, () => {
console.log(`Server on http://localhost:${PORT}`);
});
- **`const express = require(