Vous n’avez pas besoin qu’une tâche ait véritablement lieue à un moment précis (heure, voire minute ou seconde). En pratique, vous avez généralement juste besoin qu’elle soit calculée comme si elle avait eue lieu à ce moment précis.

Le soucis de la performance

D’abord, sachez que vous pouvez parfaitement faire un “ à la seconde”. Suivant la technologie utilisée, c’est tout à fait envisageable. Par exemple, pour du /, il vous suffit de lancer un CRON horaire et de faire durer ce script pendant 60 minutes (en priant toutefois pour qu’il ne crash pas…). La fonction sleep() vous sera alors certainement utile. Ce genre de bricolage, immonde mais rapide, peut se faire pour des petits jeux qui ne se soucient pas de tenir dans le temps. En revanche, pour des jeux qui veulent se maintenir dans le temps, ce genre de montage me semble peu viable. Enfin, s’il peut se faire d’un point de vue industriel (= pour atteindre le résultat attendu, à savoir un jeu qui tourne), il me semble bancal et hors de propos pour quelqu’un qui veut apprendre à “bien coder”, ou plus précisément, “bien architecturer son jeu web”.

L’âge du capitaine

En fait, s’il est possible techniquement de faire une tâche CRON à la seconde, ce genre de solution s’assimile à un développement de débutant. En effet, quand un développeur monte un mécanisme de tâche “CRON” plus récurrent que “toutes les heures” (une tâche CRON peut être lancée chaque minute, mais pas plus souvent; pour moi, elle ne devrait pas être lancée plus souvent qu’une fois par heure), c’est souvent à cause d’un mauvais design de sa base de données, et d’une dénormalisation de celle-ci. Si vous faites (ou avez fait) des études informatiques sur les bases de données, vous avez sûrement croisé cette question:

Comment stocker l’âge d’un utilisateur en base de données?

La réponse d’un débutant sera: “Avec un entier tout simplement!”. Ce qui amène au problème: l’âge est incrémenté de 1 chaque année: comment le faire en BDD? Là, on pourrait répondre “avec une tâche CRON qui se lance une fois par an”, mais d’autres effets collatéraux apparaissent alors (l’incrément de l’âge ne se fait pas forcément le jour de l’anniversaire de l’utilisateur). Ou alors, on peut répondre: “En normalisant la base de données, et en stockant non pas l’âge mais la date d’anniversaire du joueur”. Avec cette seconde solution, on ne stocke plus une donnée volatile dans le temps (qui périme et doit être remise à jour régulièrement), mais on stocke un point précis de l’échelle du temps (la date d’anniversaire), qui permet ensuite de faire un calcul donnant l’information voulue (l’âge).

CRON à la seconde = Mauvais modèle volatile

Dans le cas des tâches CRON “plus qu’une fois par heure”, on a généralement la même réponse: ce qu’on stocke en BDD n’est pas la bonne donnée, et il faut revoir sa conception. Cela clarifiera votre , simplifiera votre code, et améliorera les performances du jeu.

L’exemple des points d’action

Cette problématique ressort régulièrement sur JeuWeb: comment faire pour qu’une action ait lieue à une date précise, parfois à la seconde près, et parfois récurrente? Par exemple, un système de points d’action incrémental peut être nécessaire à votre jeu: toutes les 2 minutes, les joueurs gagnent 1 point d’action, ce qui leur permet de faire des choses, comme se déplacer.

La méthode bourrin: CRON (non!)

Une méthode bourrin consiste à lancer une tâche CRON qui va mettre régulièrement à jour la BDD. Bannissez l’idée. En effet, cela va obliger le serveur SQL à faire des requêtes de mise à jour très (trop) régulières, avec des appels au disque: les performances risquent de s’en ressentir!

Le démon (non!)

Autre solution un peu bourrin: se servir d’un démon, c’est à dire un programme tournant en continue sur le serveur, pour exécuter des requêtes SQL de mise à jour. Le démon sera donc une boucle infinie qui, toutes les 2 minutes (ou chaque seconde suivant la finesse désirée), va exécuter une requête SQL de mise à jour. Si on gagne en légèreté et en finesse temporelle face aux CRONs, on a toujours des requêtes SQL lourdes exécutées en boucle. En revanche, certaines autres architectures (autres que PHP/MySQL) peuvent envisager cette solution.

On pourrait mettre les données dans le démon lui-même et non dans le CRON pour retirer le défaut de lenteur, mais vous aurez alors de gros soucis d’intégrité et de dialogue entre le démon et votre SQL. Cette autre idée est aussi à proscrire.

La normalisation et les vues (oui!)

Une solution bien plus légère consiste à normaliser son schéma de données pour passer par les vues pour calculer des données “dynamiques”. Prenons un exemple simple (cas d’école): comment sauver l’âge d’un utilisateur en BDD? On ne va pas sauver son âge réel dans une colonne AGE TINYINT UNSIGNED car cet âge change chaque année! La solution normale consiste à stocker la date de naissance (élément fixe dans le temps) et à calculer l’âge de l’utilisateur à la volée.

Application: normalisation du problème

En reportant ce principe à notre problème (qui est: chaque joueur a des points d’action qui augmentent dans le temps), on en déduit qu’il faut stocker le nombre de points que le joueur avait à une date T, et calculer à la volée le nombre de points qu’il a maintenant. Pour cela, je vous propose la table suivante:

CREATE TABLE `players` (
    `id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
    `name` TINYTEXT NOT NULL COLLATE 'utf8_bin',
    `pointsDate` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `points` INT(10) UNSIGNED NOT NULL,
    PRIMARY KEY (`id`)
)
COLLATE='utf8_bin'
ENGINE=InnoDB
;


INSERT INTO players (players.name, players.pointsDate, players.points) VALUES
('Glados', NOW(), 0),
('Andrew Ryan', NOW(), 0),
('Francis', NOW(), 0)
;

SELECT * FROM players;
La table de données
Table de données des joueurs

Dans cette table, on stocke donc le nombre de points que le joueur a (points) à une date donnée (pointsDate). Ces informations sont fixes dans le temps: la BDD est au moins de FN1.

Récupérer les points à tout instant

Si on veut le nombre de points courant du joueur, c’est à dire le nombre de points qu’il a maintenant, là, tout de suite, on se sert de ces deux colonnes:

SELECT
    players.id, players.name, players.pointsDate, players.points,
    (NOW()-players.pointsDate)/60*2+players.points AS currentPoints
FROM players
La requête renvoie le nombre de points courant du joueur
Le calcul des points courants du joueur

Cette requête nous renvoie le nombre de points du joueur à l’instant où la requête est exécutée. On sait donc combien chacun a de points actuellement.

Faciliter l’interface avec les vues

Généralement, on voudra le nombre de points courant du joueur: il serait donc intéressant de ne pas se farcir cette requête à chaque fois, et de laisser faire le SQL. Le SQL doit donc se taper les calculs sans que l’utilisateur (le développeur exécutant la requête) n’ait à s’en soucier. Pour cela, on peut utiliser les vues:

CREATE ALGORITHM = UNDEFINED
    VIEW `view_players`
    AS SELECT
        players.id, players.name, players.pointsDate, players.points,
        (NOW()-players.pointsDate)/60*2+players.points AS currentPoints
    FROM players
    WITH CASCADED CHECK OPTION;
   
SELECT * FROM players;
SELECT * FROM view_players;
La vue = la table avec une colonne de plus
La vue permet d’avoir le même résultat avec une requête plus simple (SELECT * FROM view_players)

Une vue n’est qu’une table virtuelle qui, pour l’utilisateur, fonctionne comme une table usuelle (en tous cas, pour les SELECT). Ainsi, l’utilisateur (le développeur PHP par exemple) remplace la requête précédente complexe par un simple SELECT * FROM view_players. Le serveur SQL fait alors le calcul interne, et renvoie une colonne supplémentaire avec le nombre total de points du joueur.

Ici, j’ai conservé pointsDate et points pour vous montrer: en pratique, ces deux colonnes ne devraient pas apparaitre dans la vue.

Mises à jour des données

Pour mettre à jour les données, on procède avec la vue comme avec une table:

UPDATE view_players SET points=currentPoints - 4, pointsDate=NOW() WHERE view_players.id=2;

SELECT * FROM players;
SELECT * FROM view_players;
Les données sont mises à jour par la vue
Mise à jour des données

Ainsi, le SQL va calculer les points courants du joueur (grâce à la vue currentPoints) et les sauver à la placer des points précédents, sans oublier la date du calcul.

On peut remarquer que points=currentPoints – 4 → points = 30 – 4 → points = 26 et non 27. Cet écart est dû au temps qu’il m’a fallu pour taper mes queries. Cette méthode est donc toute aussi “réactive” qu’un CRON à la seconde (voire même plus) tout en étant plus propre et plus légère.

N’oubliez pas de mettre toujours la date à jour lorsque vous utilisez currentPoints. Vous pouvez éventuellement centraliser cela dans une fonction PHP, ou une procédure stockée SQL, ou via un TRIGGER, ou encore en utilisant ON UPDATE CURRENT_TIMESTAMP.

L’exemple d’une attaque datée

Un autre cas récurrent est une action sur un élément de jeu à une date donnée. Par exemple, une attaque sur un village qui doit avoir lieu à un moment précis. Ou un déplacement de vaisseaux spatiaux. Ou de ressources. etc. La meilleure approche consiste alors à différer le calcul: ce qui compte, ce n’est pas d’exécuter le calcul au moment exact où la tâche doit avoir lieu, mais d’exécuter le calcul comme si la tâche avait eue lieu à un instant précis.

Pour cela, il suffit d’enregistrer la date à laquelle la tâche doit avoir lieue. Ensuite, quand un visiteur demande une page web, il vous suffit d’aller voir la liste des tâches qui sont censées avoir déjà eue lieu, et de les traiter. Ensuite, vous renvoyez les données de la page. Si les tâches sont longues, n’hésitez pas à joindre également un CRON quotidien voire horaire qui se chargera de régulièrement vider la liste des tâches en attente, pour éviter que le nombre de tâches à faire avant d’afficher une page soit trop important.

A nouveau: CRON à la seconde = Mauvaise modélisation

Cette solution peut évidemment s’adapter aussi en démon, mais c’est généralement “overkill” pour un simple petit jeu web (en tous cas, pour la stack classique et basique PHP/MySQL; les autres langages qui ne jurent que par les démons gèreront cela autrement, mais n’auront probablement pas recours à un SQL classique). Le principe essentiel est surtout de se dire qu’on ne va pas exécuter une tâche à une heure précise, mais qu’on souhaite simplement que, passée cette heure, la tâche soit exécutée comme si elle avait eue lieu à l’heure précise. Cela permet d’éviter les problèmes de congestion (quand on demande une page web, on traite toutes les tâches dont la date d’échéance est dépassée: cette liste est fixe, et ne se remplira pas pendant que ces tâches sont traitées) et pour la grande majorité des jeux web, c’est bien suffisant.

Le plus souvent (si ce n’est toujours), votre base de données contient des informations “ancrées” à une date, et vous n’avez plus qu’à calculer leur nouvelle valeur à la volée lorsque vous récupérez des informations de cette BDD.