DEV Community

Cover image for Building a Real-Time Multiplayer Game with React and Firebase: Gladiator Taunt Wars
Gladiators Battle
Gladiators Battle

Posted on

Building a Real-Time Multiplayer Game with React and Firebase: Gladiator Taunt Wars

Introduction

In this in-depth guide, we’ll walk through building a real-time multiplayer game using Firebase and React, with a detailed example from Gladiator Taunt Wars. In this game mode, players engage in strategic taunt duels, taking turns selecting and responding to taunts to reduce their opponent’s health points (HP). This article will cover every aspect of building such a game, including Firebase setup, matchmaking, game state management, animations, real-time updates, and ELO-based leaderboard integration. By the end, you'll gain a solid understanding of how to implement a responsive, engaging, real-time multiplayer experience.

Setting Up Firebase and Project Initialization
Firebase Setup
Initialize Firebase with Firestore and Authentication for real-time data handling and player verification. These will provide the backbone for storing and managing match data, player information, and real-time leaderboard updates. Ensure you set up Firestore rules to restrict access to match data, allowing only the authenticated players to view and update relevant information.

React Project Structure
Organize your React project into reusable components that will represent each game element, such as the matchmaking system, game board, leaderboard, and chat. Structure components hierarchically for a clear and maintainable architecture.

Key Components of the Game

  1. Main Menu and Matchmaking The MainMenu component presents options to players, allowing them to join matchmaking, view stats, or access the leaderboard. The Matchmaking Component facilitates pairing players in real-time, leveraging Firestore transactions for consistency.

Matchmaking Logic
The startSearching function initiates the matchmaking process by adding the player to a queue in Firestore. If an opponent is found, a new match document is created, storing both players’ IDs and initializing game parameters.

const startSearching = async () => {
  const user = auth.currentUser;
  if (user && db) {
    try {
      const matchmakingRef = collection(db, 'tauntWars_matchmaking');
      const userDocRef = doc(matchmakingRef, user.uid);
      await runTransaction(db, async (transaction) => {
        const userDoc = await transaction.get(userDocRef);
        if (!userDoc.exists()) {
          transaction.set(userDocRef, { userId: user.uid, status: 'waiting', timestamp: serverTimestamp() });
        } else {
          transaction.update(userDocRef, { status: 'waiting', timestamp: serverTimestamp() });
        }

        const q = query(matchmakingRef, where('status', '==', 'waiting'));
        const waitingPlayers = await getDocs(q);
        if (waitingPlayers.size > 1) {
          // Pairing logic
        }
      });
    } catch (error) {
      setIsSearching(false);
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

The function uses Firestore transactions to ensure that a player isn't double-matched, which would disrupt the matchmaking system. Firebase’s serverTimestamp function is useful here to ensure consistent timestamps across multiple time zones.

  1. Real-Time Game State on the GameBoard Component Listening to Game State Changes The GameBoard component listens for changes in the tauntWars_matches collection. When a player selects a taunt or responds, the change is immediately reflected in Firestore, triggering a re-render for both players.
useEffect(() => {
  const matchRef = doc(db, 'tauntWars_matches', matchId);
  const unsubscribe = onSnapshot(matchRef, (docSnapshot) => {
    if (docSnapshot.exists()) {
      setMatchData(docSnapshot.data());
      if (docSnapshot.data().currentTurn === 'response') {
        setResponses(getAvailableResponses(docSnapshot.data().selectedTaunt));
      }
    }
  });
  return () => unsubscribe();
}, [matchId]);
Enter fullscreen mode Exit fullscreen mode

Handling Game Phases
Players alternate turns, each choosing a taunt or response. The currentTurn attribute indicates which action phase the game is in. Each action is updated in Firestore, triggering real-time synchronization across both clients. For instance, a player selecting a taunt switches currentTurn to “response,” alerting the opponent to choose a response.

  1. Player Actions and Taunt Selection ActionSelection Component This component displays available taunts and handles the selection process. Players select a taunt or response, which is stored in Firestore and triggers the next phase.
const handleTauntSelection = async (taunt) => {
  const otherPlayer = currentPlayer === matchData.player1 ? matchData.player2 : matchData.player1;
  await updateDoc(doc(db, 'tauntWars_matches', matchId), {
    currentTurn: 'response',
    turn: otherPlayer,
    selectedTaunt: taunt.id,
  });
};
Enter fullscreen mode Exit fullscreen mode

The Timer component restricts the duration of each turn. This timeout function maintains a steady game flow and penalizes players who fail to act in time, reducing their HP.

const Timer = ({ isPlayerTurn, onTimeUp }) => {
  const [timeLeft, setTimeLeft] = useState(30);
  useEffect(() => {
    if (isPlayerTurn) {
      const interval = setInterval(() => {
        setTimeLeft(prev => {
          if (prev <= 1) {
            clearInterval(interval);
            onTimeUp();
            return 0;
          }
          return prev - 1;
        });
      }, 1000);
      return () => clearInterval(interval);
    }
  }, [isPlayerTurn, onTimeUp]);
};
Enter fullscreen mode Exit fullscreen mode
  1. Animations with Konva for Health and Attacks CanvasComponent: Uses react-konva to animate health changes and attacks. The health bars visually represent the damage taken or inflicted based on taunts, enhancing engagement.
const animateAttack = useCallback((attacker, defender) => {
  const targetX = attacker === 'player1' ? player1Pos.x + 50 : player2Pos.x - 50;
  const attackerRef = attacker === 'player1' ? player1Ref : player2Ref;
  attackerRef.current.to({
    x: targetX,
    duration: 0.2,
    onFinish: () => attackerRef.current.to({ x: player1Pos.x, duration: 0.2 })
  });
});
Enter fullscreen mode Exit fullscreen mode

By simulating attacks in this way, we visually indicate the power and result of each taunt or response, creating a more immersive experience.

  1. Real-Time Chat with ChatBox The ChatBox component is a real-time chat that displays taunt and response messages. This chat interface gives players feedback and context, creating an interactive experience.
const ChatBox = ({ matchId }) => {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const chatRef = collection(db, 'tauntWars_matches', matchId, 'chat');
    const unsubscribe = onSnapshot(chatRef, (snapshot) => {
      setMessages(snapshot.docs.map((doc) => doc.data()));
    });
    return () => unsubscribe();
  }, [matchId]);
};
Enter fullscreen mode Exit fullscreen mode

Each message is rendered conditionally based on the user’s ID, differentiating sent and received messages with distinct styling.

  1. Leaderboard with ELO Rankings The EloLeaderboard component sorts players based on their ELO rating. Each match updates player ratings in Firestore, which is fetched and displayed in real-time.
const EloLeaderboard = () => {
  const [players, setPlayers] = useState([]);
  useEffect(() => {
    const q = query(collection(db, 'users'), orderBy('tauntWars.elo', 'desc'), limit(100));
    const unsubscribe = onSnapshot(q, (querySnapshot) => {
      setPlayers(querySnapshot.docs.map((doc, index) => ({
        rank: index + 1,
        username: doc.data().username,
        elo: doc.data().tauntWars.elo,
      })));
    });
    return () => unsubscribe();
  }, []);
};
Enter fullscreen mode Exit fullscreen mode

The leaderboard ranks players based on their ELO, providing competitive motivation and a way for players to track their progress.

Technical Challenges and Best Practices
Consistency with Firestore Transactions
Using transactions ensures that simultaneous reads/writes to Firestore maintain data integrity, especially during matchmaking and scoring updates.

Optimizing Real-Time Listeners
Employ listener cleanup using unsubscribe() to prevent memory leaks. Also, limiting queries can help reduce the number of Firestore reads, optimizing costs and performance.

Responsive Design with Canvas
The CanvasComponent adjusts its size based on the viewport, making the game responsive across devices. Use of the react-konva library allows for robust rendering of interactive elements, giving players visual feedback through animations.

Handling Edge Cases
Consider scenarios where a player disconnects mid-game. For this, implement a cleanup function that ensures match data is updated and any abandoned match instances are closed.

Wrapping Up
With Firebase and React, you can create a fast-paced multiplayer experience that adapts to real-time user actions. The examples in Gladiator Taunt Wars demonstrate how to integrate real-time updates, secure transactions, and dynamic animations to produce an engaging and visually appealing game.

Conclusion

Building Gladiator Taunt Wars for Gladiators Battle has been a rewarding journey, bringing React, Firebase, and immersive game mechanics together to capture the intensity of Roman arena combat in a web-based game. Leveraging Firebase’s real-time Firestore database, secure authentication, and robust hosting capabilities has allowed us to create a seamless, community-driven experience where players can face off in strategic battles. Integrating GitHub Actions for continuous deployment has also streamlined development, letting us concentrate on enhancing gameplay and user interaction.

As we continue to expand on Gladiator Taunt Wars, we’re excited about the potential for new features, including AI-driven opponents and enhanced match strategies, which will deepen the gameplay experience and make each battle feel even more immersive. The combination of historical strategy and modern technology provides a dynamic way for players to engage with the gladiator world.

Future articles in this series will dive into the technical details of creating interactive web applications with Firebase, including optimizing real-time data flows, managing complex game states, and leveraging AI to enhance player engagement. We’ll explore best practices for bridging front-end and back-end services to create a responsive, real-time multiplayer environment.

Whether you're developing your own interactive game or curious about the tech behind Gladiators Battle, this series offers valuable insights on building modern web applications with Firebase. Join us as we continue to merge ancient history with cutting-edge technology, reimagining the excitement of gladiator combat for today’s digital world.

🔹 Discover More:

Explore Gladiators Battle: Dive into the Roman world and experience strategy and combat at https://gladiatorsbattle.com
Check Out Our GitHub: See our codebase and contributions at https://github.com/HanGPIErr/Gladiators-Battle-Documentation.
Connect on LinkedIn: Follow along on LinkedIn for updates on my projects https://www.linkedin.com/in/pierre-romain-lopez/
Also my X account : https://x.com/GladiatorsBT

Top comments (0)