Build a Multiplayer Car Lobby with React Three Fiber and Playroom Kit
Learn how to build a 3D multiplayer car selection lobby for a racing game. You’ll use React Three Fiber for 3D rendering, Playroom Kit for multiplayer matchmaking, and create a sleek garage environment where players can choose their cars.
Getting Started
This tutorial shows you how to create a multiplayer car lobby where players can select their vehicles before entering a game. The lobby features a 3D garage environment with animated lights, player name editing using myPlayer, car model switching with animations, and seamless multiplayer synchronization using usePlayersList and useMultiplayerState.
The application uses Playroom Kit to handle real-time multiplayer state, allowing players to see each other’s selected cars and names in real-time. The game includes a physics-based driving mode where players can drive their selected cars around a map using on-screen joysticks via Joystick.
Vibe Coding System Prompt
Vibe-code friendly
If you are using an AI coding tool, copy this prompt:
You are a senior software engineer at Little Umbrella specializing in building real-time multiplayer applications using Playroom Kit and React.Your task is to help me build a "Multiplayer Car Game Lobby".The application should be created using React as a frontend framework and Playroom Kit as a multiplayer framework.Do not assume the existence of any Playroom Kit APIs or hooks. Only use APIs documented in the official Playroom Kit documentation.I already have a React project set up.I will provide step-by-step instructions. After each step, clearly explain what was implemented and ask if I want customizations before proceeding.
You can adjust the prompt according to whether or not you have an existing application.
Guide
If you haven’t cloned the starter repository yet, clone it before moving to the first step. The starter code contains all the 3D assets, audio files, and basic React setup you’ll need.
If you’re vibe-coding, start by copying the Vibe Coding System prompt. Then copy each step one by one into your coding assistant by clicking “Copy Prompt”.
Step 1: Initialize Playroom Kit with insertCoin
Initialize Playroom Kit to handle multiplayer state and matchmaking using insertCoin. This enables the application to connect players and synchronize game state across all clients.
import { insertCoin } from "playroomkit";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
insertCoin({
skipLobby: true,
}).then(() =>
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
);Step 2: Set Up the Main App with Canvas and UI
Set up the main App component with React Three Fiber Canvas, post-processing effects, and the UI overlay. This creates the foundation for the 3D scene and user interface.
import { Canvas } from "@react-three/fiber";
import { Bloom, EffectComposer } from "@react-three/postprocessing";
import { Leva } from "leva";
import { myPlayer } from "playroomkit";
import { Experience } from "./components/Experience";
import { UI } from "./components/UI";
function App() {
const me = myPlayer();
return (
<>
<UI />
<Leva hidden />
<Canvas
shadows
camera={{ position: [4.2, 1.5, 7.5], fov: 45, near: 0.5 }}
>
<color attach="background" args={["#333"]} />
<Experience />
<EffectComposer>
<Bloom luminanceThreshold={1} intensity={1.22} />
</EffectComposer>
</Canvas>
</>
);
}
export default App;Step 3: Create the Experience Component with Game State
Create the Experience component that handles game state switching between lobby and game modes using useMultiplayerState. This component acts as the main coordinator for rendering different game phases.
import { useMultiplayerState } from "playroomkit";
import { Game } from "./Game";
import { Lobby } from "./Lobby";
export const Experience = () => {
const [gameState] = useMultiplayerState("gameState", "lobby");
return (
<>
{gameState === "lobby" && <Lobby />}
{gameState === "game" && <Game />}
</>
);
};Step 4: Create the AudioManager Utility
Create a simple audio manager utility to handle sound playback for car switching animations.
export const audios = {
car_start: new Audio("/audios/car_start.mp3"),
};
export const playAudio = (audio) => {
audio.currentTime = 0;
audio.play();
};Step 5: Create the Car Component
Create the Car component that renders 3D car models with custom material properties. This component handles loading and displaying different car types with visual enhancements.
import { Clone, useGLTF } from "@react-three/drei";
import { useEffect } from "react";
import { MeshStandardMaterial } from "three";
import { degToRad } from "three/src/math/MathUtils";
export const CAR_MODELS = [
"sedanSports",
"raceFuture",
"taxi",
"ambulance",
"police",
"truck",
"firetruck",
];
export const Car = ({ model = CAR_MODELS[0], ...props }) => {
const { scene } = useGLTF(`/models/cars/${model}.glb`);
useEffect(() => {
scene.traverse((child) => {
if (child.isMesh) {
if (child.material.name === "window") {
child.material.transparent = true;
child.material.opacity = 0.5;
}
if (
child.material.name.startsWith("paint") ||
child.material.name === "wheelInside"
) {
child.material = new MeshStandardMaterial({
color: child.material.color,
metalness: 0.5,
roughness: 0.1,
});
}
if (child.material.name.startsWith("light")) {
child.material.emissive = child.material.color;
child.material.emissiveIntensity = 4;
child.material.toneMapped = false;
}
}
});
}, [scene]);
return (
<group {...props}>
<Clone
object={scene}
rotation-y={degToRad(180)}
castShadow
/>
</group>
);
};
CAR_MODELS.forEach((model) => {
useGLTF.preload(`/models/cars/${model}.glb`);
});Step 6: Create the Lobby Component
Create the Lobby component that renders the 3D garage environment with player cars, name labels, and car switching functionality using usePlayersList, myPlayer, and getState/setState for car selection state.
import {
Billboard,
Box,
CameraControls,
Image,
PerspectiveCamera,
Text,
useGLTF,
} from "@react-three/drei";
import { useFrame, useThree } from "@react-three/fiber";
import { useAtom } from "jotai";
import { myPlayer, usePlayersList } from "playroomkit";
import { useEffect, useRef, useState } from "react";
import { MathUtils, Vector3 } from "three";
import { degToRad } from "three/src/math/MathUtils";
import { audios, playAudio } from "../utils/AudioManager";
import { Car } from "./Car";
import { NameEditingAtom } from "./UI";
const CAR_SPACING = 2.5;
export const Lobby = () => {
const [nameEditing, setNameEditing] = useAtom(NameEditingAtom);
const controls = useRef();
const cameraReference = useRef();
const me = myPlayer();
const players = usePlayersList(true);
players.sort((a, b) => a.id.localeCompare(b.id));
const { scene } = useGLTF("/models/garage.glb");
useEffect(() => {
scene.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
}, [scene]);
const animatedLight = useRef();
useFrame(({ clock }) => {
animatedLight.current.position.x =
Math.sin(clock.getElapsedTime() * 0.5) * 2;
controls.current.camera.position.x +=
Math.cos(clock.getElapsedTime() * 0.5) * 0.25;
controls.current.camera.position.y +=
Math.sin(clock.getElapsedTime() * 1) * 0.125;
});
const shadowBias = -0.005;
const shadowMapSize = 2048;
const viewport = useThree((state) => state.viewport);
const adjustCamera = () => {
const distFactor =
10 /
viewport.getCurrentViewport(cameraReference.current, new Vector3(0, 0, 0))
.width;
controls.current.setLookAt(
4.2 * distFactor,
2 * distFactor,
7.5 * distFactor,
0,
0.15,
0,
true
);
};
useEffect(() => {
adjustCamera();
}, [players]);
useEffect(() => {
const onResize = () => {
console.log("on resize");
adjustCamera();
};
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
return (
<>
<PerspectiveCamera ref={cameraReference} position={[0, 1, 10]} />
<CameraControls
ref={controls}
mouseButtons={{
left: 0,
middle: 0,
right: 0,
wheel: 0,
}}
touches={{
one: 0,
two: 0,
}}
/>
<directionalLight position={[6, 4, 6]} intensity={0.4} color="white" />
<group scale={0.66}>
<primitive object={scene} />
<group position={[5.5, 0.5, -1.2]}>
<pointLight
intensity={3}
distance={15}
decay={3}
color="#4124c9"
/>
<Box scale={0.1} visible={false}>
<meshBasicMaterial color="white" />
</Box>
</group>
<group position={[-3, 3, -2]}>
<pointLight
intensity={3}
decay={3}
distance={6}
color="#a5adff"
/>
<Box scale={0.1} visible={false}>
<meshBasicMaterial color="white" />
</Box>
</group>
<group position={[0, 2.5, 0.5]} ref={animatedLight}>
<pointLight
intensity={0.9}
decay={2}
distance={10}
castShadow
color="#f7d216"
shadow-bias={shadowBias}
shadow-mapSize-width={shadowMapSize}
shadow-mapSize-height={shadowMapSize}
/>
<Box scale={0.1} visible={false}>
<meshBasicMaterial color="white" />
</Box>
</group>
{players.map((player, idx) => (
<group
position-x={
idx * CAR_SPACING - ((players.length - 1) * CAR_SPACING) / 2
}
key={player.id}
scale={0.8}
>
<Billboard position-y={2.1} position-x={0.5}>
<Text fontSize={0.34} anchorX={"right"}>
{player.state.name || player.state.profile.name}
<meshBasicMaterial color="white" />
</Text>
<Text
fontSize={0.34}
anchorX={"right"}
position-x={0.02}
position-y={-0.02}
position-z={-0.01}
>
{player.state.name || player.state.profile.name}
<meshBasicMaterial color="black" transparent opacity={0.8} />
</Text>
{player.id === me?.id && (
<>
<Image
onClick={() => setNameEditing(true)}
position-x={0.2}
scale={0.3}
url="images/edit.png"
transparent
/>
<Image
position-x={0.2 + 0.02}
position-y={-0.02}
position-z={-0.01}
scale={0.3}
url="images/edit.png"
transparent
color="black"
/>
</>
)}
</Billboard>
<group position-y={player.id === me?.id ? 0.15 : 0}>
<CarSwitcher player={player} />
</group>
{player.id === me?.id && (
<>
<pointLight
position-x={1}
position-y={2}
intensity={2}
distance={3}
/>
<group rotation-x={degToRad(-90)} position-y={0.01}>
<mesh receiveShadow>
<circleGeometry args={[2.2, 64]} />
<meshStandardMaterial
color="pink"
toneMapped={false}
emissive={"pink"}
emissiveIntensity={1.2}
/>
</mesh>
</group>
<mesh position-y={0.1} receiveShadow>
<cylinderGeometry args={[2, 2, 0.2, 64]} />
<meshStandardMaterial color="#8572af" />
</mesh>
</>
)}
</group>
))}
</group>
</>
);
};
const SWITCH_DURATION = 600;
const CarSwitcher = ({ player }) => {
const changedCarAt = useRef(0);
const container = useRef();
const [carModel, setCurrentCarModel] = useState(player.getState("car"));
useFrame(() => {
const timeSinceChange = Date.now() - changedCarAt.current;
if (timeSinceChange < SWITCH_DURATION / 2) {
container.current.rotation.y +=
2 * (timeSinceChange / SWITCH_DURATION / 2);
container.current.scale.x =
container.current.scale.y =
container.current.scale.z =
1 - timeSinceChange / SWITCH_DURATION / 2;
} else if (timeSinceChange < SWITCH_DURATION) {
container.current.rotation.y +=
4 * (1 - timeSinceChange / SWITCH_DURATION);
container.current.scale.x =
container.current.scale.y =
container.current.scale.z =
timeSinceChange / SWITCH_DURATION;
if (container.current.rotation.y > Math.PI * 2) {
container.current.rotation.y -= Math.PI * 2;
}
}
if (timeSinceChange >= SWITCH_DURATION) {
container.current.rotation.y = MathUtils.lerp(
container.current.rotation.y,
Math.PI * 2,
0.1
);
}
}, []);
const newCar = player.getState("car");
if (newCar !== carModel) {
playAudio(audios.car_start);
changedCarAt.current = Date.now();
setTimeout(() => {
setCurrentCarModel(newCar);
}, SWITCH_DURATION / 2);
}
return (
<group ref={container}>
<Car model={carModel} />
</group>
);
};
useGLTF.preload("/models/garage.glb");Step 7: Create the Game Component
Create the Game component that sets up the physics world and renders all players’ cars in the game arena using onPlayerJoin and Joystick.
import { Environment, Gltf, Lightformer } from "@react-three/drei";
import { CuboidCollider, Physics, RigidBody } from "@react-three/rapier";
import { Joystick, onPlayerJoin } from "playroomkit";
import { useEffect, useState } from "react";
import { CarController } from "./CarController";
import { GameArea } from "./GameArea";
export const Game = () => {
const [players, setPlayers] = useState([]);
useEffect(() => {
onPlayerJoin((state) => {
const controls = new Joystick(state, {
type: "angular",
buttons: [{ id: "Respawn", label: "Spawn" }],
});
const newPlayer = { state, controls };
setPlayers((players) => [...players, newPlayer]);
state.onQuit(() => {
setPlayers((players) => players.filter((p) => p.state.id !== state.id));
});
});
}, []);
return (
<group>
<ambientLight intensity={0.4} />
<Environment>
<Lightformer
position={[5, 5, 5]}
form="rect"
intensity={1}
color="white"
scale={[10, 10]}
target={[0, 0, 0]}
/>
</Environment>
<pointLight position={[0, 5, 0]} intensity={2.5} distance={10} />
<pointLight
position={[5, 5, 0]}
intensity={10.5}
distance={10}
color="pink"
/>
<pointLight
position={[-5, 5, 0]}
intensity={10.5}
distance={15}
color="blue"
/>
<directionalLight position={[10, 10, 10]} intensity={0.4} />
<Physics>
{players.map(({ state, controls }) => (
<CarController key={state.id} state={state} controls={controls} />
))}
<RigidBody type="fixed" colliders="hull" rotation-y={Math.PI}>
<GameArea />
</RigidBody>
<RigidBody
type="fixed"
sensor
colliders={false}
position-y={-5}
name="void"
>
<CuboidCollider args={[20, 3, 20]} />
</RigidBody>
<Gltf src="/models/map_road.glb" />
</Physics>
</group>
);
};Step 8: Create the GameArea Component
Create the GameArea component that renders the game map with buildings and ground.
import { Gltf, useGLTF } from "@react-three/drei";
import { degToRad } from "three/src/math/MathUtils";
export const GameArea = () => {
return (
<group>
<Gltf src="/models/map_buildings.glb" />
<mesh rotation-x={degToRad(-90)} position-y={0.05}>
<planeGeometry args={[18, 18]} />
<meshBasicMaterial />
</mesh>
</group>
);
};
useGLTF.preload("/models/map_buildings.glb");
useGLTF.preload("/models/map_road.glb");Step 9: Create the CarController Component
Create the CarController component that handles physics-based car movement, player input, camera following, and multiplayer synchronization using isHost, myPlayer, setState, and getState.
import { Html, PerspectiveCamera } from "@react-three/drei";
import { useFrame } from "@react-three/fiber";
import { RigidBody, euler, quat, vec3 } from "@react-three/rapier";
import { useControls } from "leva";
import { isHost, myPlayer, usePlayerState } from "playroomkit";
import { useEffect, useRef } from "react";
import { Vector3 } from "three";
import { randInt } from "three/src/math/MathUtils";
import { Car } from "./Car";
const UP = new Vector3(0, 1, 0);
const CAR_SPEEDS = {
sedanSports: 4,
raceFuture: 2,
taxi: 5.5,
ambulance: 8.5,
police: 5.5,
truck: 4.8,
firetruck: 10,
};
export const CarController = ({ state, controls }) => {
const rb = useRef();
const me = myPlayer();
const { rotationSpeed, carSpeed } = useControls({
carSpeed: {
value: 3,
min: 0,
max: 10,
step: 0.1,
},
rotationSpeed: {
value: 3,
min: 0,
max: 10,
step: 0.01,
},
});
const lookAt = useRef(new Vector3(0, 0, 0));
useFrame(({ camera }, delta) => {
if (!rb.current) {
return;
}
if (me?.id === state.id) {
const targetLookAt = vec3(rb.current.translation());
lookAt.current.lerp(targetLookAt, 0.1);
camera.lookAt(lookAt.current);
}
const rotVel = rb.current.angvel();
if (controls.isJoystickPressed()) {
const angle = controls.angle();
const dir = angle > Math.PI / 2 ? 1 : -1;
rotVel.y = -dir * Math.sin(angle) * rotationSpeed;
const impulse = vec3({
x: 0,
y: 0,
z: (CAR_SPEEDS[carModel] || carSpeed) * delta * dir,
});
const eulerRot = euler().setFromQuaternion(quat(rb.current.rotation()));
impulse.applyEuler(eulerRot);
rb.current.applyImpulse(impulse, true);
}
rb.current.setAngvel(rotVel, true);
if (isHost()) {
state.setState("pos", rb.current.translation());
state.setState("rot", rb.current.rotation());
} else {
const pos = state.getState("pos");
if (pos) {
rb.current.setTranslation(pos);
rb.current.setRotation(state.getState("rot"));
}
}
if (controls.isPressed("Respawn")) {
respawn();
}
});
const respawn = () => {
if (isHost()) {
rb.current.setTranslation({
x: randInt(-2, 2) * 4,
y: 2,
z: randInt(-2, 2) * 4,
});
rb.current.setLinvel({ x: 0, y: 0, z: 0 });
rb.current.setRotation({ x: 0, y: 0, z: 0, w: 1 });
rb.current.setAngvel({ x: 0, y: 0, z: 0 });
}
};
const [carModel] = usePlayerState(state, "car");
useEffect(() => {
respawn();
}, []);
return (
<group>
<RigidBody
ref={rb}
colliders={"hull"}
key={carModel}
position={vec3(state.getState("pos"))}
rotation={euler().setFromQuaternion(quat(state.getState("rot")))}
onIntersectionEnter={(e) => {
if (e.other.rigidBodyObject.name === "void") {
respawn();
}
}}
>
<Html position-y={0.55}>
<h1 className="text-center whitespace-nowrap text-white drop-shadow-md backdrop-filter bg-slate-300 bg-opacity-30 backdrop-blur-lg rounded-md py-2 px-4 text-xl transform -translate-x-1/2">
{state.state.name || state.state.profile.name}
</h1>
</Html>
<Car model={carModel} scale={0.32} />
{me?.id === state.id && (
<PerspectiveCamera makeDefault position={[0, 1.5, -3]} near={1} />
)}
</RigidBody>
</group>
);
};Step 10: Create the UI Component
Create the UI component that handles car selection, name editing, game state controls, and player interactions using isHost, myPlayer, useMultiplayerState, usePlayersList, and startMatchmaking.
import { atom, useAtom } from "jotai";
import {
isHost,
myPlayer,
startMatchmaking,
useMultiplayerState,
usePlayersList,
} from "playroomkit";
import { useEffect, useState } from "react";
import { CAR_MODELS } from "./Car";
export const NameEditingAtom = atom(false);
export const UI = () => {
const me = myPlayer();
const [gameState, setGameState] = useMultiplayerState("gameState", "lobby");
const [loadingSlide, setLoadingSlide] = useState(true);
const [nameEditing, setNameEditing] = useAtom(NameEditingAtom);
const [nameInput, setNameInput] = useState(
me?.getState("name") || me?.state.profile.name
);
const [invited, setInvited] = useState(false);
const invite = () => {
navigator.clipboard.writeText(window.location.href);
setInvited(true);
setTimeout(() => setInvited(false), 2000);
};
useEffect(() => {
setLoadingSlide(true);
if (gameState !== "loading") {
const timeout = setTimeout(() => {
setLoadingSlide(false);
}, 1000);
return () => clearTimeout(timeout);
}
}, [gameState]);
usePlayersList(true);
const [loadingContent, setLoadingContent] = useState(0);
useEffect(() => {
if (loadingSlide) {
const interval = setInterval(() => {
setLoadingContent((prev) => (prev + 1) % CAR_MODELS.length);
}, 200);
return () => clearInterval(interval);
}
}, [loadingSlide]);
return (
<>
<div
className={`fixed z-30 top-0 left-0 right-0 h-screen bg-white flex items-center justify-center gap-1 text-5xl pointer-events-none transition-transform duration-500
${loadingSlide ? "" : "translate-y-[100%]"}
`}
>
VROOM, VROOM
<img src={`images/cars/${CAR_MODELS[loadingContent]}.png`} />
</div>
<div
className={
"fixed z-10 bottom-4 left-1/2 flex flex-wrap justify-center items-center gap-2.5 -translate-x-1/2 w-full max-w-[75vw]"
}
>
{CAR_MODELS.map((model, idx) => (
<div
key={model}
className={`min-w-14 min-h-14 w-14 h-14 bg-white bg-opacity-50 backdrop-filter backdrop-blur-lg rounded-full shadow-md cursor-pointer
${
me?.getState("car") === model ||
(!me?.getState("car") && idx === 0)
? "ring-4 ring-blue-500"
: ""
}
`}
onClick={() => me?.setState("car", model)}
>
<img
src={`/images/cars/${model}.png`}
alt={model}
className="w-full h-full"
/>
</div>
))}
</div>
{gameState === "lobby" && isHost() && (
<div className="fixed bottom-4 right-4 z-10 flex flex-col gap-2 items-end">
<button
className="px-4 py-2 bg-gray-100 text-black text-lg rounded-md"
onClick={() => {
setGameState("loading");
setTimeout(() => {
setGameState("game");
}, 500);
}}
>
Private
</button>
<button
className="px-8 py-2 bg-gray-100 text-black text-2xl rounded-md"
onClick={async () => {
setGameState("loading");
await startMatchmaking();
setGameState("game");
}}
>
Online
</button>
</div>
)}
<button
className="z-20 fixed top-4 right-4 px-8 py-2 bg-gray-100 text-black text-2xl rounded-md flex items-center gap-2"
onClick={invite}
disabled={invited}
>
{invited ? (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m8.9-4.414c.376.023.75.05 1.124.08 1.131.094 1.976 1.057 1.976 2.192V16.5A2.25 2.25 0 0 1 18 18.75h-2.25m-7.5-10.5H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V18.75m-7.5-10.5h6.375c.621 0 1.125.504 1.125 1.125v9.375m-8.25-3 1.5 1.5 3-3.75"
/>
</svg>
Link copied to clipboard
</>
) : (
<>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M18 7.5v3m0 0v3m0-3h3m-3 0h-3m-2.25-4.125a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0ZM3 19.235v-.11a6.375 6.375 0 0 1 12.75 0v.109A12.318 12.318 0 0 1 9.374 21c-2.331 0-4.512-.645-6.374-1.766Z"
/>
</svg>
Invite
</>
)}
</button>
{nameEditing && (
<div className="fixed z-20 inset-0 flex items-center justify-center flex-col gap-2 bg-black bg-opacity-20 backdrop-blur-sm">
<input
autoFocus
className="p-3"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
me?.setState("name", nameInput);
setNameEditing(false);
}
}}
/>
<div className="flex items-center gap-2">
<button
className="px-8 py-2 bg-red-400 text-white text-2xl rounded-md"
onClick={() => {
setNameEditing(false);
}}
>
✗
</button>
<button
className="px-8 py-2 bg-green-400 text-white text-2xl rounded-md"
onClick={() => {
me?.setState("name", nameInput);
setNameEditing(false);
}}
>
✓
</button>
</div>
</div>
)}
</>
);
};Improvements
- Add more car models with different handling characteristics
- Implement a countdown timer before game starts
- Add obstacle course elements to the game arena
- Create team-based game modes
- Add nitro boost power-ups during gameplay
- Implement a spectating mode for eliminated players