GDG Logo

Game Dev with React Workshop

GameBoard Component

The GameBoard component is responsible for rendering the main game area, managing obstacles, and tracking the score. Below, we break down each part of the component.

1. Client-Side Setup and Core Imports

"use client"
      import React, { useEffect, useCallback, useState } from 'react';
      import { useAtom } from 'jotai';
      import { 
        playerPositionAtom, scoreAtom, gameOverAtom, 
        obstaclesAtom, bulletsAtom, timerAtom 
      } from '../store/atom';

Enables client-side rendering in Next.js and imports essential React hooks and Jotai for atomic state management. The 'use client' directive is crucial for interactive components that need browser APIs.

2. Component Imports

import Bullet from './Bullet';
      import Spaceship from './Spaceship';
      import Obstacle from './Obstacle';
      import GameOverMenu from './GameOverMenu';
      import CollisionManager from './CollisionManager';
      import Timer from './Timer';

Imports modular game components. Each component handles a specific game element, following the single responsibility principle.

3. State Initialization

const GameBoard = () => {
        const [playerPosition, setPlayerPosition] = useAtom(playerPositionAtom);
        const [score, setScore] = useAtom(scoreAtom);
        const [gameOver, setGameOver] = useAtom(gameOverAtom);
        const [obstacles, setObstacles] = useAtom(obstaclesAtom);
        const [bullets, setBullets] = useAtom(bulletsAtom);
        const [, setTime] = useAtom(timerAtom);

Initializes game state using Jotai atoms. Each piece of state is managed independently, allowing for granular updates and optimized rendering.

4. Responsive Container Setup

const [containerDimensions, setContainerDimensions] = useState({
          width: 800,
          height: 600
        });
      
        useEffect(() => {
          const updateDimensions = () => {
            const width = Math.min(window.innerWidth * 0.95, 800);
            const height = Math.min(window.innerHeight * 0.9, 600);
            setContainerDimensions({ width, height });
          };
      
          window.addEventListener('resize', updateDimensions);
          updateDimensions();
          return () => window.removeEventListener('resize', updateDimensions);
        }, []);

Manages responsive game container sizing. Dynamically adjusts to window size while maintaining maximum dimensions. Includes cleanup to prevent memory leaks.

5. Movement Constants and Calculations

const MOVE_DISTANCE = containerDimensions.width * 0.025;
      const MAX_POSITION = containerDimensions.width - 80;
      const BULLET_OFFSET_X = containerDimensions.width * 0.029;
      const BULLET_START_Y = containerDimensions.height * 0.88;

Defines movement-related constants using relative measurements. These ensure consistent gameplay feel across different screen sizes.

6. Keyboard Input Handler

const handleKeyPress = useCallback((e) => {
          if (gameOver) return;
          
          switch(e.key) {
            case 'ArrowLeft':
              setPlayerPosition(prev => ({
                ...prev,
                x: Math.max(0, prev.x - MOVE_DISTANCE)
              }));
              break;
            case 'ArrowRight':
              setPlayerPosition(prev => ({
                ...prev,
                x: Math.min(MAX_POSITION, prev.x + MOVE_DISTANCE)
              }));
              break;
            case ' ':
              setBullets(prev => [...prev, {
                id: Date.now(),
                x: playerPosition.x + BULLET_OFFSET_X,
                y: BULLET_START_Y
              }]);
              break;
            default:
              break;
          }
        }, [playerPosition, gameOver, containerDimensions, setPlayerPosition, setBullets]);

Handles keyboard input for player movement and shooting. Uses memoization to prevent unnecessary re-renders and includes boundary checking.

7. Game Loop Logic

useEffect(() => {
          window.addEventListener('keydown', handleKeyPress);
          
          const BULLET_SPEED = containerDimensions.height * 0.017;
          const OBSTACLE_SPEED = containerDimensions.height * 0.005;
          
          const gameLoop = setInterval(() => {
            if (!gameOver) {
              // Bullet movement
              setBullets(prev => 
                prev
                  .map(bullet => ({ 
                    ...bullet, 
                    y: bullet.y - BULLET_SPEED 
                  }))
                  .filter(bullet => bullet.y > 0)
              );
      
              // Obstacle management
              setObstacles(prev => {
                const updated = prev
                  .map(obstacle => ({
                    ...obstacle,
                    y: obstacle.y + OBSTACLE_SPEED
                  }))
                  .filter(obstacle => obstacle.y < containerDimensions.height);
      
                if (Math.random() < 0.05) {
                  updated.push({
                    id: Date.now(),
                    x: Math.random() * (containerDimensions.width - 20),
                    y: -20
                  });
                }
                return updated;
              });
            }
          }, 50);
      
          return () => {
            window.removeEventListener('keydown', handleKeyPress);
            clearInterval(gameLoop);
          };
        }, [gameOver, handleKeyPress, containerDimensions, setBullets, setObstacles]);

Implements the main game loop with relative speed calculations. Manages bullet and obstacle movement, spawning new obstacles randomly. Includes proper cleanup of event listeners and intervals.

8. Game Reset Handler

const handleRestart = useCallback(() => {
          setGameOver(false);
          setScore(0);
          setTime(0);
          setObstacles([]);
          setBullets([]);
          setPlayerPosition({ 
            x: containerDimensions.width / 2 - 40, 
            y: 20 
          });
        }, [containerDimensions, setGameOver, setScore, setTime, setObstacles, setBullets, setPlayerPosition]);

Handles game reset functionality. Resets all game state to initial values and positions player relative to container size.

9. Game Board Rendering

return (
          <div 
            className="game-container"
            style={{
              width: `${containerDimensions.width}px`,
              height: `${containerDimensions.height}px`
            }}
          >
            <Timer gameOver={gameOver} />
            <div className="score-display">Score: {score}</div>
            
            {obstacles.map(obstacle => (
              <Obstacle 
                key={obstacle.id} 
                x={obstacle.x} 
                y={obstacle.y} 
              />
            ))}
            
            {bullets.map(bullet => (
              <Bullet 
                key={bullet.id} 
                x={bullet.x} 
                y={bullet.y} 
                id={bullet.id}
              />
            ))}
            
            <Spaceship position={playerPosition} />
            
            <CollisionManager
              obstacles={obstacles}
              playerPosition={playerPosition}
              setGameOver={setGameOver}
              gameOver={gameOver}
              containerDimensions={containerDimensions}
            />
            
            {gameOver && (
              <GameOverMenu 
                score={score} 
                onRestart={handleRestart}
              />
            )}
          </div>
        );

Renders the complete game interface with dynamic sizing. Components are positioned relative to container dimensions. Includes score display, game elements, and conditional rendering of the game over menu.