Créer un jeu pour ARCADE

Guide complet pour développer un jeu compatible avec l'architecture ARCADE

Introduction

L'architecture ARCADE permet de développer des jeux modulaires qui fonctionnent avec différentes bibliothèques graphiques. Voici comment créer votre propre jeu pour la plateforme ARCADE en respectant l'interface définie.

Interface IGameModule

Comprendre l'interface

Tous les jeux ARCADE doivent implémenter l'interface IGameModule qui définit les méthodes essentielles pour l'intégration avec le core.

class IGameModule {
public:
    virtual ~IGameModule() = default;
    virtual void init() = 0;
    virtual void processEvent(GameEvent event) = 0;
    virtual void update() = 0;
    virtual bool isGameOver() const = 0;
    virtual std::string getName() const = 0;
    virtual int getScore() const = 0;
    virtual bool isPaused() const = 0;
    virtual void reset() = 0;
    virtual std::vector> getGrid() const = 0;
    virtual std::vector getEntities() const = 0;
};

Méthodes principales

  • init() - Initialise le jeu et ses composants
  • processEvent(GameEvent) - Traite les événements du joueur
  • update() - Met à jour la logique du jeu
  • isGameOver() - Indique si la partie est terminée
  • getGrid() - Retourne la grille de jeu pour l'affichage
  • getEntities() - Retourne les entités du jeu pour un rendu plus avancé

Structure d'un jeu

Fichiers nécessaires

Pour créer un jeu compatible, vous aurez besoin des fichiers suivants:

  • MonJeu.hpp - Définition de la classe de jeu
  • MonJeu.cpp - Implémentation de la logique du jeu
  • MonJeuModule.cpp - Point d'entrée pour le chargement dynamique

Point d'entrée

Le fichier module doit implémenter les fonctions d'exportation pour le chargement dynamique:

extern "C" {
    arcade::IGameModule *createGame()
    {
        return new arcade::MonJeu();
    }
    
    void destroyGame(arcade::IGameModule *game)
    {
        delete game;
    }
}

Exemple complet: Snake

Définition de la classe

Exemple de définition de classe pour un jeu Snake:

class Snake : public IGameModule {
public:
    Snake();
    ~Snake() = default;

    void init() override;
    void update() override;
    void processEvent(GameEvent event) override;
    
    bool isGameOver() const override { return _gameOver; }
    int getScore() const override { return _score; }
    std::string getName() const override { return "Snake"; }
    bool isPaused() const override { return _isPaused; }
    std::vector> getGrid() const override;
    std::vector getEntities() const override { /* ... */ }
    
private:
    static const int GRID_WIDTH = 20;
    static const int GRID_HEIGHT = 15;
    
    std::deque _snake;
    Position _food;
    Direction _direction;
    Direction _pendingDirection;
    
    bool _gameOver;
    bool _isPaused;
    int _score;
    double _speed;
    
    void spawnFood();
    void moveSnake();
    void reset();
    // Autres méthodes internes...
};

Implémentation des composants clés

Initialisation du jeu

void Snake::init()
{
    reset();
    std::cout << "Snake game initialized" << std::endl;
}

void Snake::reset()
{
    _snake.clear();
    _gameOver = false;
    _score = 0;
    _speed = 5.0;
    
    int startX = GRID_WIDTH / 2;
    int startY = GRID_HEIGHT / 2;
    
    _snake.push_front({startX, startY});         
    _snake.push_back({startX - 1, startY});      
    _snake.push_back({startX - 2, startY});
    
    _direction = Direction::RIGHT;
    _pendingDirection = Direction::RIGHT;
    
    spawnFood();
    
    _lastUpdateTime = std::chrono::steady_clock::now();
}

Mise à jour du jeu

void Snake::update()
{
    if (_isPaused) return;
    if (_gameOver) return;
    
    auto currentTime = std::chrono::steady_clock::now();
    double deltaTime = std::chrono::duration(currentTime - _lastUpdateTime).count();
    
    if (deltaTime >= 1.0 / _speed) {
        _direction = _pendingDirection;
        moveSnake();
        _lastUpdateTime = currentTime;
    }
}

Traitement des événements

void Snake::processEvent(GameEvent event)
{
    if (event == GameEvent::PAUSE) {
        _isPaused = !_isPaused;
        return; 
    }
    if (_isPaused) return; 
    if (_gameOver) {
        if (event == GameEvent::RESTART) {
            reset();
        }
        return;
    }
    switch (event) {
        case GameEvent::UP:
            if (_direction != Direction::DOWN)
                _pendingDirection = Direction::UP;
            break;
        case GameEvent::DOWN:
            if (_direction != Direction::UP)
                _pendingDirection = Direction::DOWN;
            break;
        case GameEvent::LEFT:
            if (_direction != Direction::RIGHT)
                _pendingDirection = Direction::LEFT;
            break;
        case GameEvent::RIGHT:
            if (_direction != Direction::LEFT)
                _pendingDirection = Direction::RIGHT;
            break;
        case GameEvent::RESTART:
            reset();
            break;
        default:
            break;
    }
}

Système de grille et rendu

Représentation de la grille

ARCADE utilise un système de grille pour représenter l'état du jeu. Voici comment implémenter la méthode getGrid():

std::vector> Snake::getGrid() const {
    std::vector> grid(GRID_HEIGHT, std::vector(GRID_WIDTH, {CellType::EMPTY, 0}));
    
    grid[_food.y][_food.x] = {CellType::FOOD, 1};  
    if (!_snake.empty()) {
        grid[_snake.front().y][_snake.front().x] = {CellType::SNAKE_HEAD, 3};
    }
    for (size_t i = 1; i < _snake.size(); ++i) {
        grid[_snake[i].y][_snake[i].x] = {CellType::SNAKE_BODY, 3};
    }
    return grid;
}

Types de cellules

Les différents types de cellules permettent aux bibliothèques graphiques de savoir comment afficher chaque élément:

  • EMPTY - Cellule vide
  • SNAKE_HEAD - Tête du serpent
  • SNAKE_BODY - Corps du serpent
  • FOOD - Nourriture
  • WALL - Obstacle/mur
  • DOT, POWER_DOT - Pour des jeux type Pacman
  • GHOST - Pour des fantômes dans Pacman

Rendu avec différentes bibliothèques

Comment votre jeu sera affiché

Votre jeu sera rendu différemment selon la bibliothèque graphique utilisée. Le Core ARCADE se charge de cette adaptation:

SDL2

void ArcadeCore::displaySDL2Snake()
{
    auto grid = _gameModule->getGrid();
            
    int cellWidth = WINDOW_WIDTH / GRID_WIDTH;
    int cellHeight = WINDOW_HEIGHT / GRID_HEIGHT;
    
    for (std::size_t y = 0; y < GRID_HEIGHT && y < grid.size(); y++) {
        for (std::size_t x = 0; x < GRID_WIDTH && x < grid[y].size(); x++) {
            CellType cellType = grid[y][x].type;
            int r, g, b, a = 255;
            
            switch (cellType) {
                case CellType::EMPTY:
                    r = 0; g = 0; b = 0;
                    break;
                case CellType::SNAKE_HEAD:
                    r = 0; g = 255; b = 0;
                    break;
                case CellType::SNAKE_BODY:
                    r = 0; g = 200; b = 0;
                    break;
                case CellType::FOOD:
                    r = 255; g = 0; b = 0;
                    break;
                default:
                    r = 128; g = 128; b = 128;
            }
            
            _displayModule->drawRect(x * cellWidth, y * cellHeight, 
                                  cellWidth, cellHeight, r, g, b, a);
        }
    }

SFML

void ArcadeCore::displaySFMLSnake() 
{
    auto grid = _gameModule->getGrid();

    int cellWidth = WINDOW_WIDTH / GRID_WIDTH;
    int cellHeight = WINDOW_HEIGHT / GRID_HEIGHT;

    for (std::size_t y = 0; y < GRID_HEIGHT && y < grid.size(); y++) {
        for (std::size_t x = 0; x < GRID_WIDTH && x < grid[y].size(); x++) {
            CellType cellType = grid[y][x].type;
            int r, g, b, a = 255;

            switch (cellType) {
                case CellType::EMPTY:
                    r = 0; g = 0; b = 0;
                    break;
                case CellType::SNAKE_HEAD:
                    r = 0; g = 0; b = 240;
                    break;
                case CellType::SNAKE_BODY:
                    r = 0; g = 0; b = 255;
                    break;
                case CellType::FOOD:
                    r = 255; g = 0; b = 0;
                    break;
                default:
                    r = 128; g = 128; b = 128;
                    break;      
            }
                
            _displayModule->drawRect(x * cellWidth, y * cellHeight, 
                                 cellWidth, cellHeight, r, g, b, a);
        }
    }

NCurses

void ArcadeCore::displayNcursesSnake()
{
    auto grid = _gameModule->getGrid();

    for (int x = 0; x < GRID_WIDTH + 2; x++) {
        _displayModule->drawText("#", x, 0, 255, 255, 255, 255);
        _displayModule->drawText("#", x, GRID_HEIGHT + 1, 255, 255, 255, 255); 
    }
    for (int y = 0; y < GRID_HEIGHT + 2; y++) {
        _displayModule->drawText("#", 0, y, 255, 255, 255, 255); 
        _displayModule->drawText("#", GRID_WIDTH + 1, y, 255, 255, 255, 255); 
    }

    for (std::size_t y = 0; y < GRID_HEIGHT && y < grid.size(); y++) {
        for (std::size_t x = 0; x < GRID_WIDTH && x < grid[y].size(); x++) {
            int displayX = x + 1;
            int displayY = y + 1;
            
            switch (grid[y][x].type) {
                case CellType::SNAKE_HEAD:
                    _displayModule->drawText("O", displayX, displayY, 0, 255, 0, 255);
                    break;
                case CellType::SNAKE_BODY:
                    _displayModule->drawText("o", displayX, displayY, 0, 200, 0, 255);
                    break;
                case CellType::FOOD:
                    _displayModule->drawText("8", displayX, displayY, 255, 0, 0, 255);
                    break;
                case CellType::EMPTY:
                    _displayModule->drawText(" ", displayX, displayY, 50, 50, 50, 255);
                    break;
                default:
                    break;
            }
        }
    }

Guide pas à pas

Créer le squelette du jeu

Commencez par créer les fichiers nécessaires (MonJeu.hpp, MonJeu.cpp, MonJeuModule.cpp) et implémentez l'interface IGameModule.

- Définir les structures de données

Définissez les structures nécessaires pour représenter l'état de votre jeu (positions, directions, entités, etc.).

- Implémenter la logique du jeu

Développez les méthodes update(), processEvent() et les autres méthodes internes pour gérer la logique du jeu.

- Définir la représentation visuelle

Implémentez la méthode getGrid() pour fournir une représentation de l'état du jeu aux bibliothèques graphiques.

- Compiler en bibliothèque partagée

Compilez votre jeu en tant que bibliothèque partagée (.so) avec le préfixe correct.

g++ -std=c++17 -fPIC -shared MonJeu.cpp MonJeuModule.cpp -o arcade_monjeu.so

- Tester votre jeu

Placez la bibliothèque dans le répertoire ./lib/ et lancez le programme ARCADE en spécifiant une bibliothèque graphique.

./arcade ./lib/arcade_sfml.so

Bonnes pratiques

Indépendance graphique

Concevez votre jeu de manière à ce qu'il soit totalement indépendant du système de rendu graphique. Tout le rendu est géré par le Core et les bibliothèques graphiques.

Séparation des préoccupations

Séparez clairement la logique du jeu (mise à jour de l'état) de sa représentation (grille ou entités).

Gestion du temps

Utilisez des mécanismes basés sur le temps plutôt que sur le nombre d'appels pour assurer une vitesse de jeu cohérente sur différentes plateformes.

Documentation

Commentez votre code et documentez les comportements spécifiques de votre jeu pour faciliter son intégration et sa maintenance.