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.