TNKI je real-time multiplayerová hra založená na websocketech. Avšak, protože zábava je v programování, a ne ve hraní hry, dostanete pouze repozitář se základní funkcionalitou a bude na vás hru dokončit. Po dokončení úkolů si hru můžete upravit k vlastnímu obrazu.
Repozitář: https://github.com/kpostrava/tnki
💡 Pro snadnější oriantaci v kódu si do vscode nainstaluje rozšíření “better comments”: https://marketplace.visualstudio.com/items?itemName=aaron-bond.better-comments
Spuštění serveru
Je zapotřebí z githubu stáhnout výchozí bod k programu. Domluvte se v týmu, kdo vytvoří společný fork a všichni si jej naklonujte. Pomocí npm
následně nainstalujte potřebné balíčky. Server spusťte pomocí nodemon
, tato služba umožňuje automatický restart serveru při změně souboru.
git clone <URL_forknutého_repozitáře>
cd <Cesta_k_uloženému_repozitáři>/server
npm i
npm i nodemon -g
npx nodemon main.js
Popis kódu
Datová struktura asociativní pole (hashmap)
const map_id_room = new Map();
V asociativním poli map_id_room
používáme id tanku jako klíč a název místnosti jako hodnotu. Při každém připojení tanku do roomky zároveň do tohoto asociativního pole přidáme nový záznam.
Tímto máme zajištěno, že kdykoliv v programu můžeme okamžitě zjistit v jaké místosti se tank nachází. Naopak při odpojení tanku je důležité, abyste tank z tohoto pole smazali!
const map_id_room = new Map(["id1", "místnost1"], ["id2", "místnost2"]);
map_id_room.set("id3", "místnost3"); //přidání záznamu s klíčem "id3"
console.log(map_id_room.get("id2")); //načtení záznamu podle klíče - vypíše "místnost2"
map_id_room.delete("id1"); //smazání záznamu s klíčem "id1"
Příklad zapsání a získání dat z asociativního pole.
const map_key_value = new Map([
["ArrowUp", { x: 0, y: -1 }],
["ArrowLeft", { x: -1, y: 0 }],
["ArrowDown", { x: 0, y: 1 }],
["ArrowRight", { x: 1, y: 0 }],
]);
Dalším příkladem asociativního pole je map_key_value
. Umožňuje nám najít podle názvu klávesy koeficienty pro pohyb po jednotlivých osách.
Server:
Třídy:
Room
- Jeden server může hostovat více her součásně, každá hra má svou instanci třídy Room
._
Každá instance má svůj unikátní identifikátor - room_name
, který se následně používá v asociativních polích pro její identifikaci.
Konstruktor potřebuje:
session_id
- ID uživatele, který vytvořil místnost, pochází ze socketio
tank
- Instance třídy tank, který vytvořil místnost
max_players
- Maximální počet hráču, zadán tvůrcem místnosti, defaultně 2-4
room_name
- Název místnosti, zadán tvůrcem místnosti, musí být unikátní
game_map
- Herní mapa, může být konstanta, nebo výstup z funkce shuffle_map()
pro randomizaci mapy
Třída Room obsahuje vlastní asociativní pole tanks
. Toto pole slouží k rychlému přiřazení session_id
ke konkrétní instance třídy Tank.
Maximální počet hráčů a jméno místnosti zadává uživatel do formuláře na domovské stránce.
class Room {
started = false;
tanks = new Map();
constructor(session_id, tank, max_players, room_name, game_map) {
this.admin = session_id;
this.max_players = max_players;
this.room_name = room_name;
this.game_map = game_map;
this.tanks.set(session_id, tank);
map_id_room.set(session_id, room_name);
}
join(session_id, tank) {
this.tanks.set(session_id, tank);
map_id_room.set(session_id, this.room_name);
}
tanks_length() {
return this.tanks.size;
}
}
Tank
- Každý hráč má svou instanci. Všechny tanky musí patřit nějaké instanci třídy Room.
Konstruktor potřebuje:
index
- Pořadí tanku, získáme voláním tanks.length()
na instanci třídy Room
, do které daný tank patří
player_name
- Jméno hráče, zadává uživatel do formuláře na domovské stránce
session_id
- ID uživatele, pochází ze socketio
Každá instance třídy Tank
obsahuje informace o aktuální poloze tanku a jeho barvu.
class Tank {
constructor(index, player_name, session_id) {
this.x = start_positions[index].x;
this.y = start_positions[index].y;
this.color = colors[index];
this.player_name = player_name;
this.index = index;
this.session_id = session_id;
}
validate_move(coords) {}
move(key, shift) {}
shoot() {}
}
websockety
Veškerá komunikace klient - server je zprostředokována pomocí websocket.
Například při kliknutí na talčítko create room
na domovské stránce. Se emitne event.
socket.emit("create_room", {
room_name: room_name,
max_players: max_players,
player_name: player_name,
});
Server na něj umí reagovat:
io.on("connection", (socket) => {
socket.on("create_room", (msg) => {
console.log(msg);
});
socket.on("join_room", (msg) => {
})
Stejným způsobem to funguje i opačným směrem.
Klient
Změna obrazovek
Pro snadné přepínání mezi obrazovkami - home
, lobby
, game
, máme implementovanou jednoduchou funkci. Obrazovku, kterou cheme zobrazit zadáme jako argument při volání funkce set_screen()
. Funkce nastaví viditelnost obrazovky v argumentu a skryje všechny ostatní.
const screens = ["home", "lobby", "game"];
const set_screen = (visible_screen) => {
screens // Všechny obrazovky
.filter((hidden_screen) => hidden_screen != visible_screen) // Vybereme jen ty, jejichž jméno se nerovná jménu v argumentu funkce
.forEach((hidden_screen) => {
document.getElementById(hidden_screen).classList.add("hidden"); // Přidáme jim třídu "hidden" - skryjeme je
});
document.getElementById(visible_screen).classList.remove("hidden"); // Zobrazíme obrazovku z argumentu
};
(Můžeme také jednoduše skrýt všechny prvky a následně zobrazit jen prvek z argumentu)
Jako argument posíláme název obrazovky.
set_screen("lobby");
Můžeme takto jednoduše přepínat obrazovky. Pokud chcete přidat vlastí obrazovku, stačí jen přidat její id do pole screens
a vytvořit k ní příslušný prvek v grafickém rozhrání pomocí html.
Uživatelské vstupy
V základu aplikace používá šipky pro pohyb, shift pro rotaci a mezerník pro střelbu. Pokud je některá z těchto kláves stisknuta, informujeme o tom server. Zároveň voláme metodu preventDefault()
, která zabrání například nechtěnému posunu obrazovky v důsledku stisknutí šipky dolů.
const move_keys = ["ArrowUp", "ArrowLeft", "ArrowDown", "ArrowRight"];
document.onkeydown = (e) => {
if (move_keys.includes(e.code)) {
e.preventDefault();
socket.emit("update_move", { key: e.code, shift: e.shiftKey });
return;
}
//? Líbí se ti střelba při klávese "space"? Pokud ne, můžeš ji libovolně měnit!
if (e.code == "Space") {
e.preventDefault();
socket.emit("update_shoot");
}
};
Indikátory
Slouží k ukazování hráčů ve hře, jejich životů a nábojů. Při změně stavu nábojů, či životů je nutná jejich ruční aktualizace. Pomůže nám k tomu funkce set_indicator()
.
const set_indicator = (index, value, type) => {
const indicator = document.querySelector(`#indicator_${index} .${type}`); // Načteme indikátor podle indexu tanku a jeho typu (životy, náboje, vlastní...)
indicator.innerHTML = ""; // Vymažeme obsah indikátoru
// Několikrát přidáme ikonu v závislosti na "value"
for (let i = 0; i < value; i++) {
indicator.innerHTML += `<img src="assets/${type}.png" class="w-8 h-8" />`; // Přidáme ikonu (srdce, náboj, vlastní...)
}
};
Aktualizaci pak můžeme udělat velmi snadno.
set_indicator(tank.index, 3, "ammo");
set_indicator(tank.index, 3, "health");
💡 Index identifikátoru je shodný s indexem tanku o kterém informuje
Úkoly
💡 Veškerá logika je řešena na straně serveru. Klient pouze vykresluje získaná data z eventu a emituje event při uživatelském vstupu.
Server:
- Změnit mapu (* generovat novou / upravenou pro každou hru)
- Implementovat kolize -
- S okraji mapy (* dynamicky v závislosti na velikosti mapy)
- S barikádami
- Přidat třídě
Tank
potřebné atributy - životy, náboje, směr (* vlastní atributy pro dalši herní mechaniky) - Ověření dostatku nábojů při střelbě
- Trajektorie střely ve směru tanku
- Detekce zásahu tanku střelou
- Ubrání života při zásahu
- Ubrání náboje při střelbě
- Doplňování munice všech hráčů v pravidelných intervalech
- Stisknutím šipky společně s klávesou
shift
rotovat tank - Logika odpojování hráčů
- Oznámení výhry / prohry
- Validace vstupů pro udáje nutné k vytvoření / připojení do místosti
Klient
- Vlastní textury
- Měnit texturu / rotaci textury na základně atributu směru ⇒ tank se natáčí ve směru pohybu
- Textura výbuchu při zasažení hráče
- Tlačítko pro odpojení z místnosti v čekacím lobby
- Reakce na odpojení hráče ze hry (smrt, problém s připojením) - Skrýt jeho indetifikátor a odstranit ze seznamu hráčů
- Naimplementovat event handler, který aktualizuje stav nábojů na indetifikátorech
- Funkce pro reset stavu hry pro jejím skončení
- Oznámení výhru / prohry například na eventu
win
/lost
Bonusové
- Úkoly uvedeny výše, označené symbolem *
- Změňte ovládání ze šipek a mezerníku například na wsad a enter
- Vymyslete vlastní typ nábojů, například: splash demage, procházení barikádami, odrážení, více směrné
- Generujte na mapě náhodně bonusové životy / náboje
- Nastavení velikosti hracího pole adminem při vytváření roomky
- Vymysli něco nového!