SDL React JavaScript WebAssembly Game Development C++

Integrating SDL into React - Game Development Guide

Complete guide to integrating SDL (Simple DirectMedia Layer) with React applications for game development and multimedia applications.

Por Jesus Velez

Integrating SDL into React - Game Development Guide

SDL (Simple DirectMedia Layer) is a powerful cross-platform multimedia library that provides low-level access to audio, keyboard, mouse, and graphics hardware. In this comprehensive guide, we’ll learn how to integrate SDL with React applications for creating games and multimedia applications.

Understanding SDL and React Integration

Why Combine SDL with React?

  • Performance: SDL provides hardware-accelerated graphics and audio
  • Cross-platform: Write once, run everywhere
  • React Ecosystem: Leverage React’s component system and state management
  • Modern Development: Combine low-level graphics with modern web development
  • WebAssembly: Compile C/C++ SDL code to run in the browser

Architecture Overview

React Application
├── SDL Canvas Component
├── Game Logic (JavaScript/TypeScript)
├── SDL Wrapper (WebAssembly)
└── Asset Management

Environment Setup

Prerequisites Installation

# Install Emscripten for WebAssembly compilation
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

# Create React project
npx create-react-app sdl-react-game --template typescript
cd sdl-react-game

# Install additional dependencies
npm install --save-dev @types/emscripten

Project Structure Setup

# Create project directories
mkdir -p src/{components,game,assets,wasm}
mkdir -p public/assets/{sprites,sounds,music}
mkdir -p wasm-src

# Project structure
src/
├── components/
   ├── GameCanvas.tsx
   ├── GameControls.tsx
   └── GameUI.tsx
├── game/
   ├── GameEngine.ts
   ├── GameState.ts
   └── InputManager.ts
├── assets/
   └── AssetLoader.ts
├── wasm/
   └── sdl-wrapper.d.ts
└── App.tsx

wasm-src/
├── main.cpp
├── game.cpp
├── renderer.cpp
└── CMakeLists.txt

SDL WebAssembly Implementation

Core SDL Wrapper (C++)

// wasm-src/main.cpp
#include <SDL2/SDL.h>
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_mixer.h>
#include <emscripten.h>
#include <emscripten/html5.h>
#include <iostream>
#include <vector>
#include <map>
#include <memory>

class SDLRenderer {
private:
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Texture* backBuffer;
    int width, height;
    bool initialized;

public:
    SDLRenderer() : window(nullptr), renderer(nullptr), 
                   backBuffer(nullptr), initialized(false) {}

    bool initialize(int w, int h) {
        width = w;
        height = h;

        if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) {
            std::cout << "SDL initialization failed: " << SDL_GetError() << std::endl;
            return false;
        }

        // Initialize SDL_image
        int imgFlags = IMG_INIT_PNG | IMG_INIT_JPG;
        if (!(IMG_Init(imgFlags) & imgFlags)) {
            std::cout << "SDL_image initialization failed: " << IMG_GetError() << std::endl;
            return false;
        }

        // Initialize SDL_ttf
        if (TTF_Init() == -1) {
            std::cout << "SDL_ttf initialization failed: " << TTF_GetError() << std::endl;
            return false;
        }

        // Initialize SDL_mixer
        if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
            std::cout << "SDL_mixer initialization failed: " << Mix_GetError() << std::endl;
            return false;
        }

        // Create window
        window = SDL_CreateWindow("SDL React Game",
                                 SDL_WINDOWPOS_UNDEFINED,
                                 SDL_WINDOWPOS_UNDEFINED,
                                 width, height,
                                 SDL_WINDOW_SHOWN);

        if (!window) {
            std::cout << "Window creation failed: " << SDL_GetError() << std::endl;
            return false;
        }

        // Create renderer
        renderer = SDL_CreateRenderer(window, -1, 
                                    SDL_RENDERER_ACCELERATED | 
                                    SDL_RENDERER_PRESENTVSYNC);

        if (!renderer) {
            std::cout << "Renderer creation failed: " << SDL_GetError() << std::endl;
            return false;
        }

        // Create back buffer texture
        backBuffer = SDL_CreateTexture(renderer,
                                      SDL_PIXELFORMAT_RGBA8888,
                                      SDL_TEXTUREACCESS_TARGET,
                                      width, height);

        initialized = true;
        return true;
    }

    void clear(Uint8 r = 0, Uint8 g = 0, Uint8 b = 0, Uint8 a = 255) {
        if (!initialized) return;
        
        SDL_SetRenderTarget(renderer, backBuffer);
        SDL_SetRenderDrawColor(renderer, r, g, b, a);
        SDL_RenderClear(renderer);
    }

    void drawRect(int x, int y, int w, int h, 
                  Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) {
        if (!initialized) return;
        
        SDL_Rect rect = {x, y, w, h};
        SDL_SetRenderDrawColor(renderer, r, g, b, a);
        SDL_RenderFillRect(renderer, &rect);
    }

    void drawCircle(int centerX, int centerY, int radius,
                   Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255) {
        if (!initialized) return;
        
        SDL_SetRenderDrawColor(renderer, r, g, b, a);
        
        // Simple circle drawing algorithm
        for (int w = 0; w < radius * 2; w++) {
            for (int h = 0; h < radius * 2; h++) {
                int dx = radius - w;
                int dy = radius - h;
                if ((dx * dx + dy * dy) <= (radius * radius)) {
                    SDL_RenderDrawPoint(renderer, centerX + dx, centerY + dy);
                }
            }
        }
    }

    void present() {
        if (!initialized) return;
        
        // Copy back buffer to main buffer
        SDL_SetRenderTarget(renderer, nullptr);
        SDL_RenderCopy(renderer, backBuffer, nullptr, nullptr);
        SDL_RenderPresent(renderer);
    }

    void cleanup() {
        if (backBuffer) SDL_DestroyTexture(backBuffer);
        if (renderer) SDL_DestroyRenderer(renderer);
        if (window) SDL_DestroyWindow(window);
        
        Mix_Quit();
        TTF_Quit();
        IMG_Quit();
        SDL_Quit();
        
        initialized = false;
    }

    bool isInitialized() const { return initialized; }
    SDL_Renderer* getRenderer() { return renderer; }
    int getWidth() const { return width; }
    int getHeight() const { return height; }
};

// Game entities and logic
class GameObject {
public:
    float x, y;
    float velocityX, velocityY;
    int width, height;
    Uint8 r, g, b, a;

    GameObject(float x, float y, int w, int h) 
        : x(x), y(y), velocityX(0), velocityY(0), 
          width(w), height(h), r(255), g(255), b(255), a(255) {}

    virtual void update(float deltaTime) {
        x += velocityX * deltaTime;
        y += velocityY * deltaTime;
    }

    virtual void render(SDLRenderer& renderer) {
        renderer.drawRect(static_cast<int>(x), static_cast<int>(y), 
                         width, height, r, g, b, a);
    }

    bool checkCollision(const GameObject& other) const {
        return (x < other.x + other.width &&
                x + width > other.x &&
                y < other.y + other.height &&
                y + height > other.y);
    }
};

class Player : public GameObject {
private:
    float speed;

public:
    Player(float x, float y) : GameObject(x, y, 32, 32), speed(200.0f) {
        r = 0; g = 255; b = 0; // Green color
    }

    void handleInput(bool left, bool right, bool up, bool down) {
        velocityX = 0;
        velocityY = 0;

        if (left) velocityX = -speed;
        if (right) velocityX = speed;
        if (up) velocityY = -speed;
        if (down) velocityY = speed;
    }

    void update(float deltaTime) override {
        GameObject::update(deltaTime);
        
        // Keep player within screen bounds
        if (x < 0) x = 0;
        if (y < 0) y = 0;
        if (x + width > 800) x = 800 - width;
        if (y + height > 600) y = 600 - height;
    }
};

class Enemy : public GameObject {
private:
    float speed;
    float directionTimer;

public:
    Enemy(float x, float y) : GameObject(x, y, 24, 24), speed(100.0f), directionTimer(0) {
        r = 255; g = 0; b = 0; // Red color
        
        // Random initial direction
        velocityX = (rand() % 2 == 0) ? speed : -speed;
        velocityY = (rand() % 2 == 0) ? speed : -speed;
    }

    void update(float deltaTime) override {
        directionTimer += deltaTime;
        
        // Change direction every 2 seconds
        if (directionTimer > 2.0f) {
            velocityX = (rand() % 2 == 0) ? speed : -speed;
            velocityY = (rand() % 2 == 0) ? speed : -speed;
            directionTimer = 0;
        }

        GameObject::update(deltaTime);
        
        // Bounce off screen edges
        if (x <= 0 || x + width >= 800) velocityX = -velocityX;
        if (y <= 0 || y + height >= 600) velocityY = -velocityY;
        
        // Keep within bounds
        if (x < 0) x = 0;
        if (y < 0) y = 0;
        if (x + width > 800) x = 800 - width;
        if (y + height > 600) y = 600 - height;
    }
};

// Global game state
class GameState {
public:
    std::unique_ptr<Player> player;
    std::vector<std::unique_ptr<Enemy>> enemies;
    int score;
    bool gameOver;
    float enemySpawnTimer;

    GameState() : score(0), gameOver(false), enemySpawnTimer(0) {
        player = std::make_unique<Player>(400, 300);
        
        // Spawn initial enemies
        for (int i = 0; i < 3; i++) {
            enemies.push_back(std::make_unique<Enemy>(
                rand() % 700 + 50,
                rand() % 500 + 50
            ));
        }
    }

    void update(float deltaTime) {
        if (gameOver) return;

        player->update(deltaTime);
        
        for (auto& enemy : enemies) {
            enemy->update(deltaTime);
            
            // Check collision with player
            if (player->checkCollision(*enemy)) {
                gameOver = true;
            }
        }

        // Spawn new enemies periodically
        enemySpawnTimer += deltaTime;
        if (enemySpawnTimer > 5.0f) {
            enemies.push_back(std::make_unique<Enemy>(
                rand() % 700 + 50,
                rand() % 500 + 50
            ));
            enemySpawnTimer = 0;
            score += 10;
        }
    }

    void render(SDLRenderer& renderer) {
        renderer.clear(50, 50, 50); // Dark gray background

        if (!gameOver) {
            player->render(renderer);
            
            for (auto& enemy : enemies) {
                enemy->render(renderer);
            }
        } else {
            // Game over screen
            renderer.drawRect(300, 250, 200, 100, 0, 0, 0, 200);
            // Text would be rendered here with TTF
        }

        renderer.present();
    }
};

// Global instances
std::unique_ptr<SDLRenderer> g_renderer;
std::unique_ptr<GameState> g_gameState;
bool g_keys[4] = {false, false, false, false}; // left, right, up, down
Uint32 g_lastTime = 0;

// Main game loop
void gameLoop() {
    Uint32 currentTime = SDL_GetTicks();
    float deltaTime = (currentTime - g_lastTime) / 1000.0f;
    g_lastTime = currentTime;

    // Handle events
    SDL_Event event;
    while (SDL_PollEvent(&event)) {
        if (event.type == SDL_KEYDOWN || event.type == SDL_KEYUP) {
            bool keyState = (event.type == SDL_KEYDOWN);
            
            switch (event.key.keysym.sym) {
                case SDLK_LEFT:
                case SDLK_a:
                    g_keys[0] = keyState;
                    break;
                case SDLK_RIGHT:
                case SDLK_d:
                    g_keys[1] = keyState;
                    break;
                case SDLK_UP:
                case SDLK_w:
                    g_keys[2] = keyState;
                    break;
                case SDLK_DOWN:
                case SDLK_s:
                    g_keys[3] = keyState;
                    break;
            }
        }
    }

    // Update game
    if (g_gameState) {
        if (g_gameState->player) {
            g_gameState->player->handleInput(g_keys[0], g_keys[1], g_keys[2], g_keys[3]);
        }
        g_gameState->update(deltaTime);
        g_gameState->render(*g_renderer);
    }
}

// Exported functions for JavaScript
extern "C" {
    EMSCRIPTEN_KEEPALIVE
    int initializeGame(int width, int height) {
        g_renderer = std::make_unique<SDLRenderer>();
        
        if (!g_renderer->initialize(width, height)) {
            return 0;
        }

        g_gameState = std::make_unique<GameState>();
        g_lastTime = SDL_GetTicks();

        // Set up main loop
        emscripten_set_main_loop(gameLoop, 0, 1);
        
        return 1;
    }

    EMSCRIPTEN_KEEPALIVE
    void handleKeyPress(int keyCode, int pressed) {
        switch (keyCode) {
            case 37: // Left arrow
            case 65: // A key
                g_keys[0] = pressed != 0;
                break;
            case 39: // Right arrow
            case 68: // D key
                g_keys[1] = pressed != 0;
                break;
            case 38: // Up arrow
            case 87: // W key
                g_keys[2] = pressed != 0;
                break;
            case 40: // Down arrow
            case 83: // S key
                g_keys[3] = pressed != 0;
                break;
        }
    }

    EMSCRIPTEN_KEEPALIVE
    int getScore() {
        return g_gameState ? g_gameState->score : 0;
    }

    EMSCRIPTEN_KEEPALIVE
    int isGameOver() {
        return g_gameState ? (g_gameState->gameOver ? 1 : 0) : 0;
    }

    EMSCRIPTEN_KEEPALIVE
    void restartGame() {
        if (g_gameState) {
            g_gameState = std::make_unique<GameState>();
        }
    }

    EMSCRIPTEN_KEEPALIVE
    void cleanupGame() {
        g_gameState.reset();
        if (g_renderer) {
            g_renderer->cleanup();
            g_renderer.reset();
        }
    }
}

Build Configuration

# wasm-src/CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(SDLReactGame)

set(CMAKE_CXX_STANDARD 17)

# Emscripten-specific settings
if(EMSCRIPTEN)
    set(CMAKE_EXECUTABLE_SUFFIX ".js")
    
    # SDL2 ports
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s USE_SDL_TTF=2 -s USE_SDL_MIXER=2")
    
    # WebAssembly settings
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s WASM=1 -s ALLOW_MEMORY_GROWTH=1 -s NO_EXIT_RUNTIME=1")
    
    # Export functions
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s EXPORTED_FUNCTIONS='[\"_initializeGame\",\"_handleKeyPress\",\"_getScore\",\"_isGameOver\",\"_restartGame\",\"_cleanupGame\",\"_malloc\",\"_free\"]'")
    
    # Export runtime methods
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -s EXTRA_EXPORTED_RUNTIME_METHODS='[\"ccall\",\"cwrap\"]'")
    
    # Optimization
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O3 -s ASSERTIONS=0")
    
    # Preload assets
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --preload-file assets")
endif()

# Source files
add_executable(game
    main.cpp
)

# Link libraries
if(EMSCRIPTEN)
    target_link_libraries(game)
else()
    find_package(PkgConfig REQUIRED)
    pkg_check_modules(SDL2 REQUIRED sdl2)
    pkg_check_modules(SDL2_IMAGE REQUIRED SDL2_image)
    pkg_check_modules(SDL2_TTF REQUIRED SDL2_ttf)
    pkg_check_modules(SDL2_MIXER REQUIRED SDL2_mixer)
    
    target_link_libraries(game ${SDL2_LIBRARIES} ${SDL2_IMAGE_LIBRARIES} ${SDL2_TTF_LIBRARIES} ${SDL2_MIXER_LIBRARIES})
    target_include_directories(game PRIVATE ${SDL2_INCLUDE_DIRS} ${SDL2_IMAGE_INCLUDE_DIRS} ${SDL2_TTF_INCLUDE_DIRS} ${SDL2_MIXER_INCLUDE_DIRS})
endif()

Build Script

#!/bin/bash
# build-wasm.sh

echo "Building SDL React Game for WebAssembly..."

# Navigate to wasm source directory
cd wasm-src

# Create build directory
mkdir -p build
cd build

# Configure with Emscripten
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release

# Build
emmake make -j4

# Copy output files to public directory
cp game.js ../../public/
cp game.wasm ../../public/

echo "Build complete! Files copied to public directory."

React Component Integration

Main Game Canvas Component

// src/components/GameCanvas.tsx
import React, { useEffect, useRef, useCallback, useState } from 'react';

interface GameModule {
  ccall: (name: string, returnType: string, argTypes: string[], args: any[]) => any;
  cwrap: (name: string, returnType: string, argTypes: string[]) => Function;
}

interface GameCanvasProps {
  width?: number;
  height?: number;
  onScoreChange?: (score: number) => void;
  onGameOver?: () => void;
}

export const GameCanvas: React.FC<GameCanvasProps> = ({
  width = 800,
  height = 600,
  onScoreChange,
  onGameOver
}) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const moduleRef = useRef<GameModule | null>(null);
  const [gameInitialized, setGameInitialized] = useState(false);
  const [score, setScore] = useState(0);
  const [isGameOver, setIsGameOver] = useState(false);

  // Game functions
  const initializeGameRef = useRef<Function | null>(null);
  const handleKeyPressRef = useRef<Function | null>(null);
  const getScoreRef = useRef<Function | null>(null);
  const isGameOverRef = useRef<Function | null>(null);
  const restartGameRef = useRef<Function | null>(null);
  const cleanupGameRef = useRef<Function | null>(null);

  // Load WebAssembly module
  useEffect(() => {
    const loadWasmModule = async () => {
      try {
        // Create script element to load the generated JavaScript
        const script = document.createElement('script');
        script.src = '/game.js';
        script.async = true;

        script.onload = () => {
          // Access the Module object created by Emscripten
          const Module = (window as any).Module;
          
          Module.onRuntimeInitialized = () => {
            moduleRef.current = Module;

            // Wrap exported functions
            initializeGameRef.current = Module.cwrap('initializeGame', 'number', ['number', 'number']);
            handleKeyPressRef.current = Module.cwrap('handleKeyPress', null, ['number', 'number']);
            getScoreRef.current = Module.cwrap('getScore', 'number', []);
            isGameOverRef.current = Module.cwrap('isGameOver', 'number', []);
            restartGameRef.current = Module.cwrap('restartGame', null, []);
            cleanupGameRef.current = Module.cwrap('cleanupGame', null, []);

            // Initialize the game
            if (initializeGameRef.current) {
              const success = initializeGameRef.current(width, height);
              if (success) {
                setGameInitialized(true);
                console.log('SDL Game initialized successfully');
              } else {
                console.error('Failed to initialize SDL game');
              }
            }
          };
        };

        script.onerror = () => {
          console.error('Failed to load WebAssembly module');
        };

        document.head.appendChild(script);

        return () => {
          document.head.removeChild(script);
        };
      } catch (error) {
        console.error('Error loading WebAssembly module:', error);
      }
    };

    loadWasmModule();
  }, [width, height]);

  // Handle keyboard input
  const handleKeyDown = useCallback((event: KeyboardEvent) => {
    if (handleKeyPressRef.current && gameInitialized) {
      handleKeyPressRef.current(event.keyCode, 1);
    }
  }, [gameInitialized]);

  const handleKeyUp = useCallback((event: KeyboardEvent) => {
    if (handleKeyPressRef.current && gameInitialized) {
      handleKeyPressRef.current(event.keyCode, 0);
    }
  }, [gameInitialized]);

  // Set up event listeners
  useEffect(() => {
    if (gameInitialized) {
      window.addEventListener('keydown', handleKeyDown);
      window.addEventListener('keyup', handleKeyUp);

      return () => {
        window.removeEventListener('keydown', handleKeyDown);
        window.removeEventListener('keyup', handleKeyUp);
      };
    }
  }, [gameInitialized, handleKeyDown, handleKeyUp]);

  // Game state polling
  useEffect(() => {
    if (!gameInitialized) return;

    const pollGameState = () => {
      if (getScoreRef.current && isGameOverRef.current) {
        const currentScore = getScoreRef.current();
        const gameOver = isGameOverRef.current() !== 0;

        if (currentScore !== score) {
          setScore(currentScore);
          onScoreChange?.(currentScore);
        }

        if (gameOver !== isGameOver) {
          setIsGameOver(gameOver);
          if (gameOver) {
            onGameOver?.();
          }
        }
      }
    };

    const interval = setInterval(pollGameState, 100); // Poll every 100ms

    return () => clearInterval(interval);
  }, [gameInitialized, score, isGameOver, onScoreChange, onGameOver]);

  // Restart game function
  const handleRestartGame = useCallback(() => {
    if (restartGameRef.current && gameInitialized) {
      restartGameRef.current();
      setScore(0);
      setIsGameOver(false);
    }
  }, [gameInitialized]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      if (cleanupGameRef.current) {
        cleanupGameRef.current();
      }
    };
  }, []);

  return (
    <div className="game-canvas-container">
      <canvas
        ref={canvasRef}
        width={width}
        height={height}
        style={{
          border: '2px solid #333',
          display: 'block',
          margin: '0 auto',
          backgroundColor: '#000'
        }}
        tabIndex={0}
      />
      
      {!gameInitialized && (
        <div className="loading-overlay">
          <p>Loading SDL Game...</p>
        </div>
      )}

      {isGameOver && (
        <div className="game-over-overlay">
          <div className="game-over-modal">
            <h2>Game Over!</h2>
            <p>Final Score: {score}</p>
            <button onClick={handleRestartGame}>Restart Game</button>
          </div>
        </div>
      )}
    </div>
  );
};

Game Controls Component

// src/components/GameControls.tsx
import React from 'react';

interface GameControlsProps {
  score: number;
  isGameOver: boolean;
  onRestart: () => void;
  onPause: () => void;
  isPaused: boolean;
}

export const GameControls: React.FC<GameControlsProps> = ({
  score,
  isGameOver,
  onRestart,
  onPause,
  isPaused
}) => {
  return (
    <div className="game-controls">
      <div className="score-display">
        <h3>Score: {score}</h3>
      </div>

      <div className="control-buttons">
        <button onClick={onRestart} className="control-button restart">
          Restart Game
        </button>
        
        <button 
          onClick={onPause} 
          className="control-button pause"
          disabled={isGameOver}
        >
          {isPaused ? 'Resume' : 'Pause'}
        </button>
      </div>

      <div className="instructions">
        <h4>Controls:</h4>
        <ul>
          <li>WASD or Arrow Keys: Move player</li>
          <li>Avoid red enemies</li>
          <li>Survive as long as possible!</li>
        </ul>
      </div>

      <style jsx>{`
        .game-controls {
          display: flex;
          flex-direction: column;
          align-items: center;
          gap: 20px;
          padding: 20px;
          background: linear-gradient(145deg, #f0f0f0, #e6e6e6);
          border-radius: 10px;
          box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }

        .score-display h3 {
          margin: 0;
          font-size: 1.5em;
          color: #333;
          text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
        }

        .control-buttons {
          display: flex;
          gap: 15px;
        }

        .control-button {
          padding: 12px 24px;
          border: none;
          border-radius: 25px;
          font-size: 1em;
          font-weight: bold;
          cursor: pointer;
          transition: all 0.3s ease;
          text-transform: uppercase;
          letter-spacing: 1px;
        }

        .control-button:hover:not(:disabled) {
          transform: translateY(-2px);
          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        }

        .control-button:active {
          transform: translateY(0);
        }

        .control-button:disabled {
          opacity: 0.6;
          cursor: not-allowed;
        }

        .restart {
          background: linear-gradient(145deg, #4CAF50, #45a049);
          color: white;
        }

        .pause {
          background: linear-gradient(145deg, #2196F3, #1976D2);
          color: white;
        }

        .instructions {
          text-align: center;
          color: #666;
        }

        .instructions h4 {
          margin-bottom: 10px;
          color: #333;
        }

        .instructions ul {
          list-style: none;
          padding: 0;
          margin: 0;
        }

        .instructions li {
          margin: 5px 0;
          padding: 5px 10px;
          background: rgba(255, 255, 255, 0.5);
          border-radius: 15px;
          display: inline-block;
          margin-right: 5px;
        }

        @media (max-width: 768px) {
          .control-buttons {
            flex-direction: column;
          }
          
          .control-button {
            width: 100%;
          }
        }
      `}</style>
    </div>
  );
};

Main App Integration

// src/App.tsx
import React, { useState, useCallback } from 'react';
import { GameCanvas } from './components/GameCanvas';
import { GameControls } from './components/GameControls';
import './App.css';

interface GameStats {
  score: number;
  highScore: number;
  gamesPlayed: number;
}

function App() {
  const [gameStats, setGameStats] = useState<GameStats>({
    score: 0,
    highScore: parseInt(localStorage.getItem('sdl-react-high-score') || '0'),
    gamesPlayed: parseInt(localStorage.getItem('sdl-react-games-played') || '0')
  });
  
  const [isGameOver, setIsGameOver] = useState(false);
  const [isPaused, setIsPaused] = useState(false);
  const [showInstructions, setShowInstructions] = useState(true);

  const handleScoreChange = useCallback((score: number) => {
    setGameStats(prev => ({
      ...prev,
      score
    }));
  }, []);

  const handleGameOver = useCallback(() => {
    setIsGameOver(true);
    
    setGameStats(prev => {
      const newHighScore = Math.max(prev.score, prev.highScore);
      const newGamesPlayed = prev.gamesPlayed + 1;
      
      // Save to localStorage
      localStorage.setItem('sdl-react-high-score', newHighScore.toString());
      localStorage.setItem('sdl-react-games-played', newGamesPlayed.toString());
      
      return {
        ...prev,
        highScore: newHighScore,
        gamesPlayed: newGamesPlayed
      };
    });
  }, []);

  const handleRestart = useCallback(() => {
    setIsGameOver(false);
    setGameStats(prev => ({ ...prev, score: 0 }));
  }, []);

  const handlePause = useCallback(() => {
    setIsPaused(prev => !prev);
  }, []);

  const dismissInstructions = useCallback(() => {
    setShowInstructions(false);
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        <h1>SDL + React Game</h1>
        <div className="stats-bar">
          <span>High Score: {gameStats.highScore}</span>
          <span>Games Played: {gameStats.gamesPlayed}</span>
        </div>
      </header>

      {showInstructions && (
        <div className="instructions-modal">
          <div className="modal-content">
            <h2>Welcome to SDL + React Game!</h2>
            <p>This game demonstrates the integration of SDL with React using WebAssembly.</p>
            <ul>
              <li>Use WASD or Arrow Keys to move the green player</li>
              <li>Avoid the red enemies</li>
              <li>Enemies spawn faster as your score increases</li>
              <li>Try to survive as long as possible!</li>
            </ul>
            <button onClick={dismissInstructions} className="start-button">
              Start Playing
            </button>
          </div>
        </div>
      )}

      <main className="game-container">
        <div className="game-area">
          <GameCanvas
            width={800}
            height={600}
            onScoreChange={handleScoreChange}
            onGameOver={handleGameOver}
          />
        </div>

        <aside className="controls-area">
          <GameControls
            score={gameStats.score}
            isGameOver={isGameOver}
            onRestart={handleRestart}
            onPause={handlePause}
            isPaused={isPaused}
          />

          <div className="performance-info">
            <h4>Technical Info</h4>
            <ul>
              <li>Engine: SDL2 + WebAssembly</li>
              <li>Language: C++ → WASM</li>
              <li>Frontend: React + TypeScript</li>
              <li>Graphics: Hardware accelerated</li>
            </ul>
          </div>
        </aside>
      </main>

      <footer className="App-footer">
        <p>
          Built with SDL2, WebAssembly, and React. 
          <a href="https://github.com/your-repo" target="_blank" rel="noopener noreferrer">
            View Source Code
          </a>
        </p>
      </footer>
    </div>
  );
}

export default App;

Styling

/* src/App.css */
.App {
  text-align: center;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  min-height: 100vh;
  color: white;
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}

.App-header {
  padding: 20px;
  background: rgba(0, 0, 0, 0.1);
  backdrop-filter: blur(10px);
}

.App-header h1 {
  margin: 0 0 10px 0;
  font-size: 2.5em;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

.stats-bar {
  display: flex;
  justify-content: center;
  gap: 30px;
  font-size: 1.1em;
}

.game-container {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 30px;
  padding: 30px;
  flex-wrap: wrap;
}

.game-area {
  flex: 1;
  max-width: 800px;
}

.controls-area {
  flex: 0 0 300px;
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.performance-info {
  background: rgba(255, 255, 255, 0.1);
  padding: 20px;
  border-radius: 10px;
  backdrop-filter: blur(5px);
}

.performance-info h4 {
  margin-top: 0;
  color: #fff;
}

.performance-info ul {
  list-style: none;
  padding: 0;
  text-align: left;
}

.performance-info li {
  padding: 5px 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}

.instructions-modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  background: white;
  color: #333;
  padding: 30px;
  border-radius: 15px;
  max-width: 500px;
  text-align: left;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.modal-content h2 {
  text-align: center;
  color: #4CAF50;
  margin-top: 0;
}

.modal-content ul {
  margin: 20px 0;
}

.modal-content li {
  margin: 10px 0;
  padding-left: 20px;
  position: relative;
}

.modal-content li::before {
  content: "→";
  position: absolute;
  left: 0;
  color: #4CAF50;
  font-weight: bold;
}

.start-button {
  width: 100%;
  padding: 15px;
  background: linear-gradient(145deg, #4CAF50, #45a049);
  color: white;
  border: none;
  border-radius: 25px;
  font-size: 1.2em;
  font-weight: bold;
  cursor: pointer;
  transition: transform 0.3s ease;
}

.start-button:hover {
  transform: translateY(-2px);
}

.loading-overlay {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 20px;
  border-radius: 10px;
  z-index: 10;
}

.game-over-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, 0.8);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.game-over-modal {
  background: linear-gradient(145deg, #ff6b6b, #ee5a52);
  color: white;
  padding: 40px;
  border-radius: 15px;
  text-align: center;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}

.game-over-modal h2 {
  margin-top: 0;
  font-size: 2.5em;
  text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}

.game-over-modal button {
  margin-top: 20px;
  padding: 15px 30px;
  background: white;
  color: #ff6b6b;
  border: none;
  border-radius: 25px;
  font-size: 1.1em;
  font-weight: bold;
  cursor: pointer;
  transition: transform 0.3s ease;
}

.game-over-modal button:hover {
  transform: scale(1.05);
}

.App-footer {
  margin-top: 50px;
  padding: 20px;
  background: rgba(0, 0, 0, 0.2);
  backdrop-filter: blur(5px);
}

.App-footer a {
  color: #4CAF50;
  text-decoration: none;
  margin-left: 10px;
}

.App-footer a:hover {
  text-decoration: underline;
}

@media (max-width: 1200px) {
  .game-container {
    flex-direction: column;
    align-items: center;
  }
  
  .controls-area {
    flex: 1;
    max-width: 500px;
  }
}

@media (max-width: 768px) {
  .App-header h1 {
    font-size: 2em;
  }
  
  .stats-bar {
    flex-direction: column;
    gap: 10px;
  }
  
  .game-container {
    padding: 15px;
  }
}

Advanced Features and Optimization

Asset Management System

// src/assets/AssetLoader.ts
export interface GameAsset {
  id: string;
  type: 'image' | 'audio' | 'font';
  url: string;
  loaded: boolean;
  data?: any;
}

export class AssetLoader {
  private assets: Map<string, GameAsset> = new Map();
  private loadingPromises: Map<string, Promise<any>> = new Map();

  async loadAssets(assetList: Omit<GameAsset, 'loaded' | 'data'>[]): Promise<void> {
    const loadPromises = assetList.map(asset => this.loadAsset(asset));
    await Promise.all(loadPromises);
  }

  private async loadAsset(asset: Omit<GameAsset, 'loaded' | 'data'>): Promise<void> {
    if (this.loadingPromises.has(asset.id)) {
      return this.loadingPromises.get(asset.id)!;
    }

    const loadPromise = this.performLoad(asset);
    this.loadingPromises.set(asset.id, loadPromise);

    try {
      const data = await loadPromise;
      this.assets.set(asset.id, {
        ...asset,
        loaded: true,
        data
      });
    } catch (error) {
      console.error(`Failed to load asset ${asset.id}:`, error);
      this.assets.set(asset.id, {
        ...asset,
        loaded: false,
        data: null
      });
    }
  }

  private async performLoad(asset: Omit<GameAsset, 'loaded' | 'data'>): Promise<any> {
    switch (asset.type) {
      case 'image':
        return this.loadImage(asset.url);
      case 'audio':
        return this.loadAudio(asset.url);
      case 'font':
        return this.loadFont(asset.url);
      default:
        throw new Error(`Unknown asset type: ${asset.type}`);
    }
  }

  private loadImage(url: string): Promise<HTMLImageElement> {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.onload = () => resolve(img);
      img.onerror = reject;
      img.src = url;
    });
  }

  private loadAudio(url: string): Promise<HTMLAudioElement> {
    return new Promise((resolve, reject) => {
      const audio = new Audio();
      audio.oncanplaythrough = () => resolve(audio);
      audio.onerror = reject;
      audio.src = url;
    });
  }

  private loadFont(url: string): Promise<FontFace> {
    return new Promise((resolve, reject) => {
      const font = new FontFace('GameFont', `url(${url})`);
      font.load().then(resolve).catch(reject);
    });
  }

  getAsset(id: string): GameAsset | null {
    return this.assets.get(id) || null;
  }

  isLoaded(id: string): boolean {
    const asset = this.assets.get(id);
    return asset ? asset.loaded : false;
  }

  getLoadProgress(): { loaded: number; total: number; percentage: number } {
    const total = this.assets.size;
    const loaded = Array.from(this.assets.values()).filter(a => a.loaded).length;
    return {
      loaded,
      total,
      percentage: total > 0 ? (loaded / total) * 100 : 100
    };
  }
}

Performance Monitoring

// src/game/PerformanceMonitor.ts
export interface PerformanceMetrics {
  fps: number;
  frameTime: number;
  memoryUsage: number;
  renderTime: number;
  updateTime: number;
}

export class PerformanceMonitor {
  private fpsCounter = 0;
  private lastFpsTime = 0;
  private frameTimeHistory: number[] = [];
  private currentFps = 0;

  private renderTimeStart = 0;
  private updateTimeStart = 0;
  private renderTime = 0;
  private updateTime = 0;

  update(): void {
    const currentTime = performance.now();
    this.fpsCounter++;

    if (currentTime - this.lastFpsTime >= 1000) {
      this.currentFps = this.fpsCounter;
      this.fpsCounter = 0;
      this.lastFpsTime = currentTime;
    }

    // Track frame time
    if (this.frameTimeHistory.length > 0) {
      const frameTime = currentTime - this.frameTimeHistory[this.frameTimeHistory.length - 1];
      this.frameTimeHistory.push(currentTime);
      
      if (this.frameTimeHistory.length > 60) {
        this.frameTimeHistory.shift();
      }
    } else {
      this.frameTimeHistory.push(currentTime);
    }
  }

  startRenderTiming(): void {
    this.renderTimeStart = performance.now();
  }

  endRenderTiming(): void {
    this.renderTime = performance.now() - this.renderTimeStart;
  }

  startUpdateTiming(): void {
    this.updateTimeStart = performance.now();
  }

  endUpdateTiming(): void {
    this.updateTime = performance.now() - this.updateTimeStart;
  }

  getMetrics(): PerformanceMetrics {
    const frameTime = this.frameTimeHistory.length > 1 
      ? this.frameTimeHistory[this.frameTimeHistory.length - 1] - 
        this.frameTimeHistory[this.frameTimeHistory.length - 2]
      : 0;

    return {
      fps: this.currentFps,
      frameTime,
      memoryUsage: (performance as any).memory?.usedJSHeapSize || 0,
      renderTime: this.renderTime,
      updateTime: this.updateTime
    };
  }
}

Production Build Script

// package.json scripts
{
  "scripts": {
    "start": "react-scripts start",
    "build": "npm run build-wasm && react-scripts build",
    "build-wasm": "bash build-wasm.sh",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "serve": "npm run build && serve -s build",
    "analyze": "npm run build && npx source-map-explorer 'build/static/js/*.js'"
  }
}

This comprehensive guide demonstrates how to integrate SDL with React for creating high-performance games and multimedia applications. The combination provides the best of both worlds: SDL’s powerful graphics and audio capabilities with React’s modern component architecture and development tools.

Deja un comentario