Mathématiques du minuteur d’événements en jeu : formules indépendantes du moteur pour calculer le minuteur d’événements en jeu

La réponse directe : comment calculer les valeurs de minuteur d’événement en jeu

Si vous avez besoin de connaître la logique de calcul de minuteur d’événement en jeu sans dépendre d’un moteur, ancrez-vous sur trois primitives : le temps réel delta, un multiplicateur d’échelle de temps et une référence d’époque. Le minuteur en cours est la somme du temps delta de chaque frame multiplié par votre échelle de temps. Pour un compte à rebours, le restant est égal à la durée moins cette somme accumulée.

Pour trouver l’écart entre l’événement A et l’événement B, soustrayez leurs horodatages d’époque ; multipliez par l’échelle si vous voulez du temps de jeu plutôt que du temps réel. Cet article vous donne un pseudo-code réutilisable et un convertisseur pour que vous puissiez l’implémenter dès aujourd’hui.

La plupart des tutoriels de moteurs vous entraînent vers Time.deltaTime d’Unity ou RunService de Roblox avant d’expliquer les mathématiques sous-jacentes. J’ai porté des systèmes de minuteurs sur quatre moteurs, et les mathématiques n’ont jamais changé—seule l’API a changé. Voici la version sans wrapper.

Lorsque j’ai d’abord construit un minuteur de récolte pour un RPG mobile de 2019, j’ai lié la croissance à des compteurs de frames à un FPS supposé de 60. Sur des appareils limités tournant à 30 FPS, les cultures prenaient deux fois plus de temps. Cela a été publié pendant un week-end avant les plaintes. La correction a été un accumulateur de temps delta, que nous détaillons maintenant.

Le modèle mental central : temps delta, échelle de temps et époques

Intégrez la Trinité du minuteur : temps delta, échelle de temps et époque. Le temps delta (dt) est le nombre de secondes réelles depuis la dernière mise à jour. L’échelle de temps est un multiplicateur convertissant les secondes réelles en secondes de jeu. L’époque est une référence absolue (généralement en millisecondes depuis 1970-01-01) pour la persistance et les écarts hors ligne.

Quand quelqu’un demande comment calculer le minuteur pour un événement générique, ils veulent dire un compte à rebours ou un chronomètre. La formule est :

accumulatedGameTime += dt * timeScale
remaining = durationGameTime - accumulatedGameTime

Ce que personne ne vous dit sur le temps delta, c’est qu’il n’est jamais parfaitement constant. Même avec une boucle à pas fixe, le système d’exploitation peut limiter votre processus. Si vous accumulez avec des flottants 32 bits, vous perdez la précision sub-milliseconde après environ 24 heures de jeu continu.

Pourquoi le comptage de frames échoue spectaculairement

Le comptage de frames suppose un taux fixe. J’ai vu des équipes publier des événements d’été où le compte à rebours ralentissait sur les appareils bas de gamme parce qu’elles incrémentaient un compteur à chaque Update(). La bonne approche est de demander à la plateforme le temps réel écoulé.

Lors d’un lancement en 2020, nous avons enregistré les horodatages d’événements en heure locale sur les builds coréens et en UTC sur les builds européens. L’intervalle entre l’événement A et B semblait négatif pour certains joueurs. Stockez toujours les époques en UTC ; convertissez uniquement pour l’affichage.

L’échelle de temps n’est pas seulement le ralenti

L’échelle de temps modélise aussi la progression hors ligne et les jours de jeu accélérés. Une échelle de 1 signifie temps réel. Une échelle de 60 signifie qu’une minute réelle équivaut à une heure de jeu. Quand vous calculez le temps de jeu, multipliez le delta réel par ce facteur.

Attention : si l’échelle change en cours d’événement (par exemple, un boost de week-end), vous devez segmenter l’accumulation ou stocker l’échelle par intervalle. J’ai un jour appliqué un boost d’échelle global rétroactivement, ce qui a divisé par deux les minuteurs restants de chaque joueur actif et déclenché des tickets de support.

Liste de contrôle : la Trinité du minuteur en pratique

  • Capturez le dt depuis une horloge monotone ou d’époque, jamais un index de frame.
  • Stockez le temps accumulé en millisecondes int64 ou en secondes double.
  • Enregistrez les horodatages d’événements en époque UTC, pas en heure locale.
  • Documentez si les minuteurs restants sont en temps réel ou en temps de jeu.

Pourquoi l’époque bat « secondes jouées »

Un fichier de sauvegarde naïf pourrait stocker secondsPlayed comme total cumulé. Cela fonctionne jusqu’à ce que le jeu plante avant d’écrire, ou que le joueur utilise un éditeur de sauvegarde. Un horodatage d’époque est ancré à une horloge murale absolue, donc même si vous manquez une écriture, le chargement suivant calcule le véritable écart.

Dans un portage console sur lequel j’ai travaillé, la certification exigeait qu’un jeu en pause n’avance aucun minuteur. Utiliser la soustraction d’époque a rendu la conformité triviale : en pause, nous ne faisions simplement pas d’appel de mise à jour avec la nouvelle époque.

Comment calculer le temps entre deux événements (événement A et événement B)

La question « comment calculer le temps entre deux événements ? » est une mathématique d’intervalle. Quand l’événement A se déclenche, enregistrez epochA. Quand l’événement B se déclenche, enregistrez epochB. L’intervalle en temps réel est epochB – epochA. Multipliez par l’échelle de temps active pour l’intervalle en temps de jeu.

Cela répond directement à « comment calculer l’événement A et l’événement B » sans ambiguïté. Pseudo-code :

function timeBetween(eventA, eventB, timeScale):
    realMs = eventB.epoch - eventA.epoch
    if realMs < 0: error('Event B preceded A')
    gameMs = realMs * timeScale
    return {realMs: realMs, gameMs: gameMs}

Dans un projet de survie coopératif de 2021, nous avons mesuré le temps entre les morts de joueurs avec cette méthode. Le piège était de stocker initialement uniquement des minuteurs relatifs (« mort 120 s après l'apparition »). Quand le serveur s'est arrêté pour maintenance, notre intervalle a rétréci parce que les minuteurs relatifs ignoraient la pause.

Gestion des fuseaux horaires et de l'autorité du serveur

Si l'événement A est enregistré sur un client en UTC+9 et l'événement B sur un client en UTC-5, la soustraction naïve des horloges locales donne des absurdités. Convertissez toujours en époque UTC avant le stockage. Pour les jeux compétitifs, l'époque doit provenir d'un serveur pour empêcher les clients de simuler des complétions rapides.

Selon la Division du temps et des fréquences du NIST, UTC fournit la référence standard pour une telle synchronisation. Je traite l'UTC du serveur comme la seule source de vérité pour les événements inter-joueurs.

Cas limite : événements s'étendant sur sauvegarde/chargement

Supposons que l'événement A soit « quête acceptée » à l'époque 1 700 000 000 000. Le joueur quitte. L'événement B « quête rendue » après rechargement à 1 700 036 000 000. L'intervalle est de 36 000 000 ms (10 heures de jeu si échelle 1). Parce que les époques sont absolues, le calcul est identique que le jeu ait tourné en continu ou ait été fermé.

La plupart des gens ne réalisent pas que si vous stockez seulement « secondes depuis le lancement », vous ne pouvez pas calculer les intervalles après un crash. La persistance d'époque est le héros méconnu de l'analytique fiable des événements.

Valeur absolue et intervalles négatifs

Parfois, l'événement B est enregistré avant l'événement A en raison d'un réordonnancement réseau. La soustraction brute donne un nombre négatif. En analytique, je prends la valeur absolue et signale la paire pour examen plutôt que de faire planter le pipeline d'équilibrage. Ne laissez jamais un intervalle négatif corrompre silencieusement un classement.

Conversion temps réel en temps de jeu : comment calculer le temps de jeu

Une autre recherche courante est « comment calculer le temps de jeu ? » La conversion générique est linéaire mais a un décalage si votre jeu commence à une horloge de jeu non nulle. Formule :

gameTimeNow = gameTimeStart + (realNowEpoch - realStartEpoch) * timeScale

Si votre jeu commence au temps de jeu zéro, le décalage disparaît. Pour un cycle jour/nuit où 1 heure réelle = 24 heures de jeu, l'échelle = 24. Une session réelle de 90 minutes équivaut à 36 heures de jeu—exactement 1,5 jour de jeu.

Pour éviter le calcul manuel, le Calculateur de minuteur d'événement en jeu sur notre site applique cette conversion exacte et vous permet de changer d'échelle instantanément. Je l'utilise pour équilibrer les fenêtres d'événements pour les opérations en direct car il élimine les erreurs de doigt des formules de tableur.

Tableau de référence d'échelles courantes

Durée réelle Échelle Durée de jeu Cas d'utilisation
1 seconde réelle 1 1 seconde de jeu Action en temps réel
1 minute réelle 60 1 heure de jeu Artisanat passif
1 heure réelle 24 1 jour de jeu Cycle jour/nuit
1 jour réel 7 1 semaine de jeu Verrouillage de raid hebdomadaire

Ce tableau est un point de départ ; j’ai utilisé l’échelle 1000 pour les builds de test « hyper-speed » afin de simuler une saison complète en quelques minutes. Assurez-vous simplement que votre UI affiche la valeur convertie, sinon les joueurs penseront que le minuteur est cassé.

Créer un convertisseur réutilisable

Voici un pseudo-code indépendant du moteur que vous pouvez intégrer dans n’importe quel projet :

struct TimeConverter:
    realStartEpoch: int64
    gameStart: float
    scale: float

    function toGame(realEpoch):
        return gameStart + (realEpoch - realStartEpoch) * scale / 1000.0

    function toReal(gameTime):
        return realStartEpoch + (gameTime - gameStart) / scale * 1000.0

La plupart des gens ne réalisent pas que diviser par l’échelle peut produire des millisecondes de jeu fractionnaires que vous devez arrondir de manière cohérente. Dans un jeu au tour par tour que j’ai livré, nous avons tronqué ; les joueurs ont remarqué que des conversions répétées perdaient 1 à 2 secondes de jeu par jour. Passer à l’arrondi a corrigé la dérive.

Formater le temps de jeu pour l’UI

Convertir en millisecondes de jeu brutes ne suffit pas ; les joueurs attendent « 3j 4h 12m ». Écrivez un formateur qui divise les ms de jeu par des constantes de jour/heure indépendantes de l’échelle. Je garde une fonction pure formatGameTime(gameMs) dans le code partagé pour que chaque événement utilise un libellé identique.

FPS variable et progression hors ligne : garder les minuteurs honnêtes

Les taux de rafraîchissement variables sont la norme. La solution est d’ignorer les compteurs de frames et d’accumuler le temps delta. Mais qu’en est-il lorsque l’application est fermée ? C’est la progression hors ligne, et elle utilise la même soustraction d’epoch que nous avons utilisée pour l’événement A/B.

Au chargement, calculez elapsedReal = currentEpoch – savedEpoch. Multipliez par l’échelle, ajoutez au temps de jeu stocké, et plafonnez si votre conception interdit les gains hors ligne infinis. Lorsque j’ai implémenté pour la première fois la croissance des cultures hors ligne, j’ai oublié de plafonner ; un joueur revenant après deux semaines avait plus de monnaie que notre économie ne pouvait en supporter. Nous avons corrigé à chaud avec un maximum de 12 heures.

Pour les genres idle, notre Calculateur de gains hors ligne pour jeux idle modélise ces plafonds en utilisant la même mathématique delta-epoch, ce qui vous évite de répéter mon erreur qui a cassé l’économie.

Horloges monotones vs horloge murale

Si vous n’avez besoin que de minuteurs pendant une session, préférez une horloge monotone (temps depuis le démarrage) pour éviter les sauts lorsque le système d’exploitation synchronise l’heure. Pour les sessions croisées, vous avez besoin de l’epoch de l’horloge murale, mais vous devriez envisager un horodatage serveur pour empêcher les joueurs de régler leur téléphone sur 2099 pour accélérer les minuteurs.

Ce compromis entre commodité et sécurité est réel ; les jeux solo peuvent l’ignorer, les jeux multijoueurs ne le peuvent pas. J’ai appris cela quand un classement affichait des temps de complétion d’événements impossibles parce que les clients utilisaient des horloges locales.

Les boucles à pas fixe ont toujours besoin de delta

Même avec un tick logique fixe à 30 Hz, vous devriez calculer dt à partir de l’horloge réelle et l’accumuler, puis le consommer en tranches fixes. Cela évite la spirale de la mort sur les longues frames. Le modèle est : acc += dtReal; while(acc >= step) { update(step); acc -= step; }. C’est indépendant du moteur et garde les minuteurs fluides.

Tester hors ligne avec des epochs factices

Avant de livrer, j’écris un hook de débogage qui définit savedEpoch à huit jours en arrière. Si la logique de chargement respecte le plafond et ne divise pas par zéro, vous êtes en sécurité. Sauter ce test est la façon dont un collègue a un jour livré un minuteur qui accordait des récompenses négatives au retour.

Un framework pseudo-code unifié pour les minuteurs d’événements

Voici un framework compact et réutilisable qui couvre les compte à rebours, les intervalles et les durées mises à l’échelle. Il répond à comment calculer le minuteur dans n’importe quel contexte. J’ai utilisé une variante de cela dans trois titres livrés sur HTML5, Unity et mobile natif.

class GameEventTimer:
    durationGameMs: float
    accumulatedGameMs: float = 0
    lastRealEpoch: int64
    scale: float = 1.0

    function start(nowEpoch):
        lastRealEpoch = nowEpoch
        accumulatedGameMs = 0

    function update(nowEpoch):
        dtRealMs = nowEpoch - lastRealEpoch
        lastRealEpoch = nowEpoch
        accumulatedGameMs += dtRealMs * scale
        if accumulatedGameMs > durationGameMs:
            fireEvent()

    function remainingGameMs():
        return max(0, durationGameMs - accumulatedGameMs)

    function remainingRealMs():
        return remainingGameMs() / scale

Cette classe gère automatiquement le FPS variable car elle ne suppose jamais un dt fixe. Elle gère aussi la pause : si vous n’appelez pas update() pendant la pause, aucun temps ne s’accumule. Pour le hors ligne, appelez update(currentEpoch) une fois après le chargement et il sautera en conséquence—respectant votre plafond si vous en ajoutez un.

Calcul d’intervalle entre événements arbitraires

Pour les analyses ou les chaînes de quêtes, vous avez aussi besoin d’un enregistreur d’intervalles persistant :

struct EventLog:
    epochs: map<string, int64>

    function record(name, nowEpoch):
        epochs[name] = nowEpoch

    function intervalMs(nameA, nameB, scale):
        return (epochs[nameB] - epochs[nameA]) * scale

Remarquez que cela réutilise le même principe de soustraction. Je garde cette structure dans un fichier de sauvegarde pour que l’ajustement de l’équilibrage post-hoc puisse être fait sur de vraies données de joueurs.

Matrice de décision : quelle méthode de minuteur

Approche Avantages Inconvénients Meilleure pour
Compteur de frames Trivial à coder Casse sur changement de FPS, pas de hors ligne Prototype uniquement
Accumulateur delta Indépendant du FPS, pausable Nécessite une epoch pour la persistance La plupart des jeux temps réel
Intervalle d’epoch Sauvegarde/chargement parfait et hors ligne Pas granulaire pour sous-frame Idle, récompenses quotidiennes
Autorité serveur Anti-triche, équitable Latence, coût d’infrastructure Événements compétitifs

Utilisez cette matrice pour décider de votre architecture. L’accumulateur delta plus l’epoch est le défaut que je recommande sauf si vous gérez un serveur compétitif.

Extension aux événements répétitifs

De nombreux événements live-ops se répètent chaque jour de jeu. Ajoutez un champ cycleGameMs et utilisez le modulo sur accumulatedGameMs. J’utilise cela pour un minuteur de récompense quotidienne : nextReset = cycleGameMs - (accumulatedGameMs % cycleGameMs). Cela survit au hors ligne car le modulo opère sur le total mis à l’échelle.

Pièges courants, compromis et les choses que personne ne vous dit

Même avec des formules correctes, les minuteurs de production échouent de manière prévisible. D’abord, la dérive en virgule flottante : un accumulateur flottant 32 bits à 60 FPS perd des millisecondes après environ 12 heures. Utilisez double ou int64 en millisecondes. Ensuite, la triche d’horloge : l’epoch locale peut être manipulée.

Troisièmement, l’idée fausse du « minuteur équitable » : beaucoup pensent qu’une epoch de départ synchronisée suffit pour le multijoueur. Ce n’est pas le cas—si un client lag, son minuteur local se déclenche en retard. Vous avez besoin d’une confirmation serveur de la complétion de l’événement, pas seulement d’un compte à rebours local.

Enfin, la plupart des gens ne réalisent pas que les changements d’échelle de temps cassent rétroactivement les restes stockés naïvement. Si un joueur a 100 secondes de jeu restantes à l’échelle 1, puis vous passez l’échelle à 2, a-t-il 50 secondes réelles ou toujours 100 secondes de jeu ? Définissez cela dans la conception. Je préfère garder le reste en temps de jeu, donc le reste réel diminue de moitié—mais vos producteurs peuvent être en désaccord.

Liste de contrôle du minuteur avant livraison

  • Tester avec un écart hors ligne simulé de 30 jours pour vérifier les plafonds.
  • Vérifier que les epochs sont en UTC sur toutes les cibles de build.
  • Confirmer que l’accumulateur utilise 64 bits ou double.
  • Documenter le temps réel vs le temps de jeu dans l’UI côté joueur.
  • Pour le multijoueur, déplacer l’autorité du minuteur vers le serveur.

Localisation et confiance des joueurs

Si votre jeu sort dans plusieurs régions, rappelez-vous que certains paramètres régionaux formatent l'heure en HH:mm, d'autres en mm:HH. J'ai un jour semé la confusion en affichant « 2:30 » pour signifier 2h30 dans un marché où cela se lisait comme 2m30s. Utilisez des étiquettes d'unités explicites générées par votre formateur, et non des nombres bruts.

Les mathématiques indépendantes du moteur ci-dessus—accumulation de delta, intervalles d'époque, conversion d'échelle—vous serviront d'un prototype de Game Jam à un RPG en service continu. Appliquez la Trinité des minuteurs, testez avec des écarts hors ligne extrêmes, et vos minuteurs d'événements resteront fiables.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *