Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add Rivet example #42

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/rivet-asteroids.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/rivet-url-mappings.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ This example uses Node.js, React, and [Colyseus](https://colyseus.io/), a multip
### [Nested Messages](/examples/nested-messages)

This example implements an Embedded App using a nested framework like a game engine. When using a game engine, you need to send messages between a parent iframe and the nested framework.

### [Asteroids Rivet](/examples/asteroids-rivet)

This example uses Node.js, Socket.io, and [Rivet](https://rivet.gg/), a game-hosting service, to guide the user through an end-to-end deployment of a multiplayer game to Discord.
5 changes: 5 additions & 0 deletions examples/asteroids-rivet/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
serverDist
client
.vscode
.dev.env
36 changes: 36 additions & 0 deletions examples/asteroids-rivet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
node_modules
dist
serverDist
build

.rivet
.vscode

.env
.dev.env
.env.local

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

.yarn/*
!.yarn/releases
!.yarn/patches
!.yarn/plugins
!.yarn/sdks
!.yarn/versions

yarn.lock

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
2 changes: 2 additions & 0 deletions examples/asteroids-rivet/.prettierrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
tabWidth: 4
printWidth: 120
23 changes: 23 additions & 0 deletions examples/asteroids-rivet/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1

# === Builder ===
FROM node:20-alpine AS build
WORKDIR /app/build
COPY package.json yarn.lock tsconfig.json ./
RUN yarn install --frozen-lockfile
COPY ./server ./server
COPY ./shared ./shared
RUN yarn run build:server
RUN rm -rf ./node_modules

# === Runner ===
FROM node:20-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY --from=build /app/build/package.json /app/build/yarn.lock ./
COPY --from=build /app/build/build/server ./
COPY .env .env
RUN yarn install --frozen-lockfile --production
RUN adduser -D server
USER server
CMD ["node", "-r", "module-alias/register", "server/index.js"]
21 changes: 21 additions & 0 deletions examples/asteroids-rivet/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Rivet

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
68 changes: 68 additions & 0 deletions examples/asteroids-rivet/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Rivet Asteroids Example

This repo contains an example of a fully multiplayer game running as a Discord activity via [Rivet](https://rivet.gg/)'s gaming infrastructure that lets you host your game in just two commands.

## Client architecture

- ViteJS

## Server architecture

- Express
- Socket.io

## Setting up your Discord Application

Before we write any code, lets follow the instructions [here](https://discord.com/developers/docs/activities/building-an-activity#step-1-creating-a-new-app) to make sure your Discord application is set up correctly.

## Setting up your environment variables

In this directory (`/examples/asteroids-rivet`) we need to create a `.env` file with the OAuth2 variables, as described [here](https://discord.com/developers/docs/activities/building-an-activity#find-your-oauth2-credentials).

```env
DISCORD_CLIENT_ID=123456789012345678
DISCORD_CLIENT_SECRET=abcdefghijklmnopqrstuvwxyzabcdef
```

## Running your app locally

### Deploying your game

First, we deploy the game to Rivet with the Rivet CLI. See how to install the Rivet CLI [here](https://github.com/rivet-gg/cli?tab=readme-ov-file#installation).

After installation, run this command to get your project linked to Rivet Cloud. You will be asked to sign in and create a game to link to.

```sh
rivet init
```

Next, we can deploy to Rivet:

```sh
rivet deploy prod
```

If successful, you should see a message in your terminal that looks like so:

```
Deploy Succeeded https://asteroids-xxx.rivet.game/
```

### Updating your URL mappings

Using the URL of the successfully deployed game, you can update your activity's URL mappings to match like so:

![Screenshot of the configured URL mappings](/assets/rivet-url-mappings.png)

- **Important:** The other URL bindings are required for API requests to Rivet.
- More regions can be added by updating the `rivet.yaml` file and adding a new URL mapping in the config.
Note that this will not be required once
[this support request](https://discord.com/channels/613425648685547541/1219417813438173184) is resolved;
you will only need one URL mapping for lobby connections:
`/ws/{region}/{lobby} -> {lobby}.lobby.{region}.rivet.run`

### Playing Asteroids

Finally, you can follow the guide [here](https://discord.com/developers/docs/activities/building-an-activity#enable-developer-mode-in-your-client) on getting the activity running in Discord.

![Screenshot of the Rivet Asteroids demo](/assets/rivet-asteroids.png)
160 changes: 160 additions & 0 deletions examples/asteroids-rivet/client/client-gamestate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { cloneParticles } from "@shared/particles";
import { cloneAsteroid } from "../shared/asteroid";
import { cloneBullet } from "../shared/bullet";
import GameState, { asteroids, bullets, cloneGameState, players, updateGame } from "../shared/gamestate";
import { applyPlayerInput, clonePlayer } from "../shared/player";
import { clamp, lerp, lerpAngle } from "../shared/utils";

export default interface ClientGameState {
serverGameState: GameState;
clientGameState: GameState;
serverSync: number;

running: boolean;
}

export function initClientGamestate(state: GameState, now: number): ClientGameState {
return {
serverGameState: state,
clientGameState: cloneGameState(state, true),
serverSync: now,
running: false,
};
}

function removeDuplicates(clientState: ClientGameState, playerId: string | null) {
const serverGame = clientState.serverGameState;
const clientGame = clientState.clientGameState;

const serverAsteroids = serverGame.asteroids;
const clientAsteroids = clientGame.asteroids;

const serverBullets = serverGame.bullets;
const clientBullets = clientGame.bullets;

const serverPlayers = serverGame.players;
const clientPlayers = clientGame.players;

const serverParticles = serverGame.particleSets;
const clientParticles = clientGame.particleSets;

for (const asteroid in serverAsteroids) {
if (!clientAsteroids[asteroid]) {
clientAsteroids[asteroid] = cloneAsteroid(serverAsteroids[asteroid]);
}
}
for (const bullet in serverBullets) {
if (!clientBullets[bullet]) {
clientBullets[bullet] = cloneBullet(serverBullets[bullet]);
}
}
for (const player in serverPlayers) {
if (!clientPlayers[player]) {
clientPlayers[player] = clonePlayer(serverPlayers[player]);
}
}
for (const particleSet in serverParticles) {
if (!clientParticles[particleSet]) {
clientParticles[particleSet] = cloneParticles(serverParticles[particleSet]);
}
}


for (const asteroid in clientAsteroids) {
if (!serverAsteroids[asteroid]) {
delete clientAsteroids[asteroid];
}
}
for (const bullet in clientBullets) {
if (!serverBullets[bullet]) {
delete clientBullets[bullet];
}
}
for (const player in clientPlayers) {
if (player === playerId) continue;

if (!serverPlayers[player]) {
delete clientPlayers[player];
}
}
for (const particleSet in clientParticles) {
if (!serverParticles[particleSet]) {
delete clientParticles[particleSet];
}
}
}

export function serverSync(
clientState: ClientGameState,
serverGame: GameState,
playerId: string | null,
now: number,
): void {
clientState.serverGameState = serverGame;
clientState.serverSync = now;
clientState.clientGameState.physicsTime = serverGame.physicsTime;

removeDuplicates(clientState, playerId);
}

const interpTimeMs = 500;
function interpolate(clientState: ClientGameState, lerpAmount: number) {
for (const asteroid of asteroids(clientState.clientGameState)) {
const serverAsteroid = clientState.serverGameState.asteroids[asteroid.id];
if (!serverAsteroid) continue;

asteroid.posX = lerp(asteroid.posX, serverAsteroid.posX, lerpAmount);
asteroid.posY = lerp(asteroid.posY, serverAsteroid.posY, lerpAmount);
asteroid.velX = lerp(asteroid.velX, serverAsteroid.velX, lerpAmount);
asteroid.velY = lerp(asteroid.velY, serverAsteroid.velY, lerpAmount);

asteroid.angle = lerpAngle(asteroid.angle, serverAsteroid.angle, lerpAmount);
asteroid.rotationSpeed = lerp(asteroid.rotationSpeed, serverAsteroid.rotationSpeed, lerpAmount);

asteroid.dead = serverAsteroid.dead;
}
for (const bullet of bullets(clientState.clientGameState)) {
const serverBullet = clientState.serverGameState.bullets[bullet.id];
if (!serverBullet) continue;

bullet.posX = lerp(bullet.posX, serverBullet.posX, lerpAmount);
bullet.posY = lerp(bullet.posY, serverBullet.posY, lerpAmount);
bullet.velX = lerp(bullet.velX, serverBullet.velX, lerpAmount);
bullet.velY = lerp(bullet.velY, serverBullet.velY, lerpAmount);
}
for (const player of players(clientState.clientGameState)) {
const serverPlayer = clientState.serverGameState.players[player.id];
if (!serverPlayer) continue;

player.posX = lerp(player.posX, serverPlayer.posX, lerpAmount);
player.posY = lerp(player.posY, serverPlayer.posY, lerpAmount);
player.velX = lerp(player.velX, serverPlayer.velX, lerpAmount);
player.velY = lerp(player.velY, serverPlayer.velY, lerpAmount);

player.angle = lerpAngle(player.angle, serverPlayer.angle, lerpAmount);

player.id = serverPlayer.id;
player.name = serverPlayer.name;

player.dead = serverPlayer.dead;
player.invincibilityTimeLeft = serverPlayer.invincibilityTimeLeft;
player.playerInput = { ...serverPlayer.playerInput };
player.score = { ...serverPlayer.score };
player.lastShot = serverPlayer.lastShot;
}
}

export function update(clientState: ClientGameState, playerId: string, elapsed: number, now: number): void {
removeDuplicates(clientState, playerId);

const prevLerpVar = clamp(0, (now - elapsed - clientState.serverSync) / interpTimeMs, 0.99);
const lerpVar = clamp(0, (now - clientState.serverSync) / interpTimeMs, 1);
const lerpAmount = clamp(0, (lerpVar - prevLerpVar) / (1 - prevLerpVar), 1);
interpolate(clientState, lerpAmount);

for (const player of players(clientState.clientGameState)) applyPlayerInput(player, elapsed / 1000);
for (const player of players(clientState.serverGameState)) applyPlayerInput(player, elapsed / 1000);

updateGame(clientState.clientGameState, elapsed / 1000, playerId);
updateGame(clientState.serverGameState, elapsed / 1000, playerId);
}
Loading