Hathora 1

Hathora

Hathora 2

Configuración inicial desde la página de Hathora

1. Vamos a hathora.dev y nos logueamos.

2. Menú Application → Create Application.

3. Dentro de la aplicación seleccionada → Settings:

4. Creamos un API Token en Hathora. Menú Tokens → + API Token → Copiamos el Token en el momento de su creación porque después no estará dispnible para ser copiado.

Configuración inicial en el código del proyecto de nodeJS

1. Instalamos Hathora en el sistema operativo

npm install -g hathora

2. Creamos nuestro proyecto de nodeJS con el comando:

npm init

3. Instalamos el módulo:

npm i socket.io

4. En la raíz del proyecto, creamos el fichero hathora.yml con el siguiente código:

name: hathora-karate-chat
type: tcp # Sets transportType to "tcp"
protocol: websocket # Optional, informative only
entrypoint:
command: node
args: [index.js]
ports:
default: 3000 # containerPort
plan: tiny # planName: tiny, small, medium, large
roomsPerProcess: 1

5. Creamos e ficherol .Dockerfile en la raíz del proyecto con el siguiente código:

FROM node:20
WORKDIR /app
COPY package\*.json ./
RUN npm install
COPY . .

EXPOSE 3000
CMD ["node", "./index.js"]

Código del juego

El backend

index.js de nodejs

import { Server } from "socket.io";

const io = new Server({
  cors: {
    origin: "http://localhost:8080",
  },
});

const rooms = [];

io.listen(4000);

io.on("connection", (socket) => {
  console.log("a user connected v2");

  socket.on("room-created", ({ roomId, hostId }) => {
    rooms.push({ roomId, hostId });
    io.emit("room-created", rooms);
    console.log("room-created", roomId, hostId);
  });

  socket.on("room-id-entered", ({ roomId, guestPlayerId }) => {
    const room = rooms.find((room) =>{
      console.log("room-id-entered", room);
      return room.roomId === roomId;
    });
    io.emit("room-id-entered", { room, guestPlayerId });

    io.emit("room-id-joined-success", { room });
    console.log("room-id-entered", rooms);
  });

  socket.on("other-player-points", ({ to, points }) => {
    io.to(to).emit("other-player-points", points);
    console.log("other-player-points", points);
  });

  socket.on("we-have-a-winner", ({ to }) => {
    io.to(to).emit("we-have-a-winner");
  });
});

El frontend (con phaser)

/src/scenes/lobby/AccessRoom.js

import { getRoomInfo } from "./services/hathora";
import {
  connectSocket,
  emitRoomIdEntered,
  onJoinedRoomSuccess,
} from "./services/sockets";

// Guest Player
export class AccessRoom extends Phaser.Scene {
  constructor() {
    super("AccessRoom");
    this.room = null;
  }

  preload() {
    this.load.html("roomForm", "/assets/main.html");
  }

  create() {
    const f = async () => {
      const form = this.add.dom(400, 100).createFromCache("roomForm");

      const roomInput = form.getChildByID("roomId");
      const playButton = form.getChildByID("conectar");

      playButton.onclick = async (event) => {
        if (event.target.name === "conectar") {
          const roomId = roomInput.value;

          const getRoomInfoInterval = setInterval(() => {
            getRoomInfo(roomId).then((info) => {
              console.log(info);
              if (info.status === "active") {
                this.room = info;
                console.log(info)
                clearInterval(getRoomInfoInterval);
                const { port, host } = info.exposedPort;
                connectSocket(host, port, (socketId) => {
                  console.log("Connected with socket ID:", socketId);
                  emitRoomIdEntered(this.room.roomId);
                  onJoinedRoomSuccess(({ room }) => {
                    this.scene.start("Boot", {
                      roomId: this.room.roomId,
                      otherPlayerId: room.hostId,
                    });
                  });
                });
              }
            });
          }, 1000);
        }
      };
    };
    f();
  }
}

/src/scenes/lobby/CreateRoom.js

import {
  onGuestPlayerJoined,
  connectSocket,
  onCreateRoom,
} from "./services/sockets";
import { createRoom, getRoomInfo } from "./services/hathora";
const randomNumber = Math.floor(1000 + Math.random() * 9000);

// Host Player
export class CreateRoom extends Phaser.Scene {
  constructor() {
    super("CreateRoom");

    this.room = null;
  }

  create() {
    this.add.text(400, 100, "Create Room", {
      fontSize: "32px",
      fill: "#fff",
      fontFamily: "Arial",
    });
    this.status = this.add.text(400, 200, "Creating room...", {
      fontSize: "32px",
      fill: "#fff",
      fontFamily: "Arial",
    });

    createRoom(randomNumber, 'Dubai').then((room) => {
      const getRoomInfoInterval = setInterval(() => {
        getRoomInfo(room.roomId).then((info) => {
          console.log(info)
          if(info.status === "active") {
            this.room = info;
            clearInterval(getRoomInfoInterval);
            this.status.setText("Room ID: " + room.roomId);
            const { port, host } = info.exposedPort;
            connectSocket(host, port, (socketId) => {
              console.log("Connected with socket ID:", socketId);
              onCreateRoom(room.roomId);
              onGuestPlayerJoined((room, guestPlayerId) => {
                this.scene.start("Boot", {
                  roomId: room.roomId,
                  otherPlayerId: guestPlayerId,
                });
              });
            });
          }
        });
      }, 1000);
    });
  }
}

/src/scenes/lobby/services/hathora.js

import { HathoraCloud } from "@hathora/cloud-sdk-typescript";

const APP_ID = import.meta.env.VITE_HATHORA_APP_ID;

const hathoraCloud = new HathoraCloud({
  hathoraDevToken: import.meta.env.VITE_HATHORA_DEV_TOKEN,
  appId: APP_ID,
});

const loginResponse = await hathoraCloud.authV1.loginAnonymous();

export const createRoom = (roomName, region) =>
  hathoraCloud.lobbiesV3.createLobby(
    {
      playerAuth: loginResponse.token,
    },
    {
      visibility: "private",
      roomConfig: `{"name":"${roomName}"`,
      region: region,
    },
    APP_ID
  );


export const getRoomInfo = roomId =>
  hathoraCloud.roomsV2.getConnectionInfo(roomId, APP_ID);

/src/scenes/lobby/services/sockets.js

import { io } from "socket.io-client";

let socket;

export let localPlayerId = null;

export const connectSocket = (url, port, callback) => {
  socket = io(`wss://${url}:${port}`, {
    transports: ["websocket"],
    secure: true,
  });

  socket.on("connect", () => {
    localPlayerId = socket.id;
    callback(socket.id);
  });
};

export const onCreateRoom = (roomId) => {
  socket.emit("room-created", { roomId, hostId: localPlayerId });
};

export const emitRoomIdEntered = (roomId) => {
  socket.emit("room-id-entered", { roomId, guestPlayerId: localPlayerId });
};

export const emitWeHaveAWinner = (otherPlayerId) => {
  socket.emit("we-have-a-winner", { to: otherPlayerId });
};

export const onGuestPlayerJoined = (callback) => {
  socket.on("room-id-entered", (data) => {
    const { room, guestPlayerId } = data
    console.log(data, room);
    callback(room, guestPlayerId);
  });
};

export const onJoinedRoomSuccess = (callback) => {
  socket.on("room-id-joined-success", (data) => {
    callback(data);
  });
};

export const emitPlayerPoints = (points, otherPlayerId) => {
  console.log("emitPlayerPoints", points, otherPlayerId);
  socket.emit("other-player-points", {
    to: otherPlayerId,
    points,
  });
};

export const onOtherPlayerPoints = (callback) => {
  console.log("localPlayerId", localPlayerId);
  socket.on("other-player-points", callback);
};

export const onWeHaveAWinner = (callback) => {
  socket.on("we-have-a-winner", callback);
};

Deploy para subir el proyecto de nodejs a Hathora

1. Comprimimos todo el código fuente (excepto node_modules) en un fichero con extensión gzip o tgz.

2. Subimos el fichero comprimido a Hathora → Applications → Seleccionamos una aplicación → Subimos el fichero .gz y creamos la Build → Activamos la pestaña TLS.

Configuración inicial en el proyecto de nuestro frontend

npm install @hathora/cloud-sdk-typescript