Comment concevoir un système de rejeu

75

Alors, comment pourrais-je concevoir un système de rejeu?

Vous le savez peut-être dans certains jeux tels que Warcraft 3 ou Starcraft, où vous pourrez regarder le jeu à nouveau après l'avoir joué.

Vous vous retrouvez avec un fichier de relecture relativement petit. Donc mes questions sont:

  • Comment sauvegarder les données? (format personnalisé?) (petite taille de fichier)
  • Qu'est-ce qui sera sauvé?
  • Comment le rendre générique afin qu'il puisse être utilisé dans d'autres jeux pour enregistrer une période de temps (et non une correspondance complète par exemple)?
  • Permettre d'avancer et de revenir en arrière (WC3 ne pouvait pas revenir en arrière autant que je me souvienne)
scable
la source
3
Bien que les réponses ci-dessous fournissent de nombreuses informations précieuses, je voulais simplement souligner l’importance de développer votre jeu / moteur de manière hautement déterministe ( en.wikipedia.org/wiki/Deterministic_algorithm ), car il est essentiel pour atteindre votre objectif.
Ari Patrick
2
Notez également que les moteurs physiques ne sont pas déterministes (Havok le prétend ...). Ainsi, la solution consistant à stocker les entrées et l'horodatage produiront des résultats différents à chaque fois si votre jeu utilise la physique.
Samaursa
5
La plupart des moteurs physiques sont déterministes tant que vous utilisez un pas de temps fixe, ce que vous devriez faire de toute façon. Je serais très surpris si Havok ne l’est pas. Le non-déterminisme est assez difficile à trouver sur les ordinateurs ...
4
Déterministe signifie que les mêmes entrées = les mêmes sorties. Si vous avez des flottants sur une plate-forme et en double sur une autre (par exemple), ou si vous avez volontairement désactivé votre implémentation standard à virgule flottante IEEE, cela signifie que vous n'utilisez pas les mêmes entrées, ce qui n'est pas déterministe.
3
Est-ce moi, ou cette question reçoit-elle une prime toutes les deux semaines?
Le canard communiste

Réponses:

39

Cet excellent article couvre bon nombre des problèmes: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php

Quelques choses que l'article mentionne et fait bien:

  • votre jeu doit être déterministe.
  • il enregistre l'état initial des systèmes de jeu sur la première image, et uniquement l'entrée du joueur pendant le jeu.
  • quantifier les entrées pour réduire le nombre de bits. C'est à dire. représentent des flotteurs dans diverses plages (par exemple, [0, 1] ou [-1, 1] dans une plage inférieure à bits. Les entrées quantifiées doivent également être obtenues au cours du jeu.
  • utilisez un seul bit pour déterminer si un flux d'entrée contient de nouvelles données. Comme certains flux ne changent pas souvent, cela exploite la cohérence temporelle des entrées.

Un moyen d'améliorer encore le taux de compression dans la majorité des cas consiste à découpler tous vos flux d'entrée et à les encoder de manière totalement continue. Ce sera une victoire sur la technique d'encodage delta si vous encodez votre exécution en 8 bits et que l'exécution elle-même dépasse 8 images (très probablement à moins que votre jeu ne soit un véritable masher). J'ai utilisé cette technique dans un jeu de course pour compresser 8 minutes d'entrées de 2 joueurs tout en courant sur une piste jusqu'à quelques centaines d'octets.

Pour ce qui est de rendre un tel système réutilisable, le système de relecture s’occupe des flux d’entrée génériques, mais fournit également des points d’accès permettant à la logique propre au jeu de cibler l’entrée clavier / manette de jeu / souris de ces flux.

Si vous souhaitez un retour rapide ou des recherches aléatoires, vous pouvez enregistrer un point de contrôle (votre statut de jeu complet) toutes les N images. N doit être choisi pour minimiser la taille du fichier de relecture et pour vous assurer que le temps que le joueur doit attendre est raisonnable pendant que l’état est relu au point choisi. Une façon de contourner ce problème consiste à s'assurer que des recherches aléatoires ne peuvent être effectuées que vers ces emplacements de points de contrôle exacts. Le rembobinage consiste à définir l'état du jeu sur le point de contrôle immédiatement avant le cadre en question, puis à rejouer les entrées jusqu'à ce que vous obteniez le cadre actuel. Cependant, si N est trop grand, vous pourriez avoir de l'attelage toutes les quelques images. Un moyen de remédier à ces problèmes consiste à pré-mettre en cache de manière asynchrone les images situées entre les 2 points de contrôle précédents lors de la lecture d'une image mise en cache à partir de la région de point de contrôle actuelle.

jpaver
la source
s'il y a du GNA impliqué, inclure les résultats dudit GNA dans les cours d'eau
phénomène du cliquet
1
@ratchet freak: Avec l'utilisation déterministe de PRNG, vous pouvez vous en tirer en ne stockant que ses semences lors des points de contrôle.
NonNumeric
22

Outre la solution "Assurez-vous que les frappes au clavier sont rejouables", ce qui peut être étonnamment difficile, vous pouvez simplement enregistrer l'état complet du jeu sur chaque image. Avec un peu de compression intelligente, vous pouvez le réduire considérablement. C'est ainsi que Braid gère son code de rembobinage temporel et qu'il fonctionne plutôt bien.

Étant donné que vous aurez quand même besoin de points de contrôle pour le rembobinage, vous voudrez peut-être simplement essayer de le mettre en œuvre simplement avant de compliquer les choses.

ZorbaTHut
la source
2
+1 Avec une compression intelligente, vous pouvez vraiment réduire la quantité de données que vous devez stocker (par exemple, ne stockez pas l'état s'il n'a pas changé par rapport au dernier état que vous avez stocké pour l'objet actuel). . J'ai déjà essayé cela avec la physique et ça marche vraiment bien. Si vous n'avez pas de physique et que vous ne voulez pas rembobiner le jeu complet, je choisirais la solution de Joe simplement parce que cela produirait les fichiers les plus petits possibles. Dans ce cas, si vous souhaitez également rembobiner, vous pouvez stocker les dernières nsecondes de le jeu.
Samaursa
@ Samaursa - Si vous utilisez des bibliothèques de compression standard (par exemple, gzip), vous obtiendrez la même compression (probablement meilleure) sans avoir à effectuer manuellement des opérations telles que vérifier si l'état a changé ou non.
Justin
2
@ Kragen: Pas vraiment vrai. Les bibliothèques de compression standard sont certes bonnes mais ne pourront souvent pas tirer parti des connaissances spécifiques à un domaine. Si vous pouvez les aider un peu, en plaçant des données similaires adjacentes et en supprimant des éléments qui n'ont pas vraiment changé, vous pouvez réduire considérablement les coûts.
ZorbaTHut
1
@ZorbaTHut En théorie, oui, mais est-ce que l'effort en vaut vraiment la peine?
Justin
4
Que cela en vaille la peine dépend entièrement de la quantité de données dont vous disposez. Si vous avez un RTS avec des centaines ou des milliers d'unités, c'est probablement important. Si vous avez besoin de stocker les rediffusions en mémoire comme Braid, c'est probablement important.
21

Vous pouvez voir votre système comme s'il était composé d'une série d'états et de fonctions, où une fonction f[j]avec entrée x[j]modifie l'état du système s[j]en état s[j+1], comme suit:

s[j+1] = f[j](s[j], x[j])

Un état est l'explication de votre monde entier. Les emplacements du joueur, l'emplacement de l'ennemi, le score, les munitions restantes, etc. Tout ce dont vous avez besoin pour dessiner un cadre de votre partie.

Une fonction est tout ce qui peut affecter le monde. Un changement de trame, une pression sur une touche, un paquet réseau.

L'entrée correspond aux données prises par la fonction. Un changement de trame peut prendre le temps écoulé depuis la dernière trame, le fait d'appuyer sur la touche peut inclure la touche réellement enfoncée, ainsi que le fait que la touche Maj soit enfoncée ou non.

Aux fins de cette explication, je vais faire les hypothèses suivantes:

Hypothèse 1:

La quantité d'états pour une exécution donnée du jeu est beaucoup plus grande que la quantité de fonctions. Vous avez probablement des centaines de milliers d'états, mais seulement plusieurs dizaines de fonctions (changement de trame, pression de touche, paquet réseau, etc.). Bien entendu, la quantité d’intrants doit être égale à la quantité d’états moins un.

Hypothèse 2:

Le coût spatial (mémoire, disque) du stockage d'un seul état est beaucoup plus élevé que celui du stockage d'une fonction et de son entrée.

Hypothèse 3:

Le coût temporel (temps) de la présentation d'un état est similaire, ou juste un ou deux ordres de grandeur plus long que celui du calcul d'une fonction sur un état.

Selon les exigences de votre système de lecture, il existe plusieurs façons de mettre en œuvre un système de lecture afin que nous puissions commencer par la plus simple. Je vais aussi faire un petit exemple en utilisant le jeu d’échecs, enregistré sur du papier.

Méthode 1:

Magasin s[0]...s[n]. C'est très simple, très simple. En raison de l'hypothèse 2, le coût spatial de ceci est assez élevé.

Pour les échecs, cela serait accompli en tirant tout le tableau pour chaque coup.

Méthode 2:

Si vous avez seulement besoin d'une relecture avant, vous pouvez simplement stocker s[0], puis stocker f[0]...f[n-1](rappelez-vous, il ne s'agit que du nom de l'id de la fonction) et x[0]...x[n-1](quelle était l'entrée pour chacune de ces fonctions). Pour rejouer, vous commencez simplement par s[0], et calculez

s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])

etc...

Je veux faire une petite annotation ici. Plusieurs autres commentateurs ont déclaré que le jeu "doit être déterministe". Tous ceux qui disent que cela doit recommencer avec Computer Science 101, car TOUS LES PROGRAMMES INFORMATIQUES SONT DETERMINISTES¹, à moins que votre jeu ne soit conçu pour être exécuté sur des ordinateurs quantiques. C'est ce qui rend les ordinateurs si géniaux.

Cependant, étant donné que votre programme dépend très probablement de programmes externes, allant des bibliothèques à la mise en œuvre réelle du processeur, il peut être assez difficile de s'assurer que vos fonctions se comportent de la même manière entre les plates-formes.

Si vous utilisez des nombres pseudo-aléatoires, vous pouvez soit stocker les nombres générés dans le cadre de votre saisie x, soit stocker l'état de la fonction prng dans le cadre de votre état set son implémentation dans le cadre de la fonction f.

Pour les échecs, cela serait accompli en tirant le tableau initial (qui est connu), puis en décrivant chaque coup en disant quelle pièce est allée où. C'est d'ailleurs ce qu'ils font en réalité.

Méthode 3:

Maintenant, vous voudrez probablement pouvoir rechercher dans votre relecture. C'est-à-dire, calculez s[n]pour un arbitraire n. En utilisant la méthode 2, vous devez calculer s[0]...s[n-1]avant de pouvoir calculer s[n], ce qui, selon l'hypothèse 2, peut être assez lent.

Pour implémenter cela, la méthode 3 est une généralisation des méthodes 1 et 2: stocker f[0]...f[n-1]et x[0]...x[n-1]tout comme la méthode 2, mais aussi stocker s[j], pour tous j % Q == 0pour une constante donnée Q. En termes plus simples, cela signifie que vous stockez un signet dans l'un des QÉtats. Par exemple, pour Q == 100, vous stockezs[0], s[100], s[200]...

Afin de calculer s[n]pour un arbitraire n, vous chargez d'abord le précédemment stocké s[floor(n/Q)], puis calculez toutes les fonctions de floor(n/Q)à n. Tout au plus, vous calculerez des Qfonctions. Les valeurs plus petites de Qsont plus rapides à calculer, mais consomment beaucoup plus d'espace, alors que les valeurs plus grandes, Qconsomment moins d'espace, mais prennent plus de temps à calculer.

La méthode 3 avec Q==1est identique à la méthode 1, tandis que la méthode 3 avec Q==infest identique à la méthode 2.

Pour les échecs, cela serait accompli en tirant chaque coup, ainsi qu’un tableau sur 10 (pour Q==10).

Méthode 4:

Si vous voulez inverser la relecture, vous pouvez faire une petite variation de la méthode 3. Supposons Q==100, et que vous voulez calculer s[150]par s[90]en sens inverse. Avec la méthode 3 non modifiée, vous devrez effectuer 50 calculs pour obtenir s[150]puis 49 autres calculs pour obtenir s[149]et ainsi de suite. Mais puisque vous avez déjà calculé s[149]pour obtenir s[150], vous pouvez créer un cache avec s[100]...s[150]lorsque vous calculez s[150]pour la première fois, puis vous êtes déjà s[149]dans le cache lorsque vous devez l'afficher.

Il vous suffit de régénérer le cache chaque fois que vous devez calculer s[j], pour j==(k*Q)-1pour tout k. Cette fois-ci, une augmentation Qentraînera une taille plus petite (uniquement pour le cache), mais des temps plus longs (juste pour recréer le cache). Une valeur optimale pour Qpeut être calculée si vous connaissez les tailles et les temps nécessaires au calcul des états et des fonctions.

Pour les échecs, cela se ferait en dessinant chaque coup, ainsi qu’un Q==10carton sur 10 (pour ), mais aussi, il faudrait dessiner sur un morceau de papier séparé, les 10 derniers tableaux que vous avez calculés.

Méthode 5:

Si les états consomment simplement trop d'espace ou que les fonctions prennent trop de temps, vous pouvez créer une solution qui implémente réellement la relecture inversée (et non des faux). Pour ce faire, vous devez créer des fonctions inverses pour chacune de vos fonctions. Cependant, cela nécessite que chacune de vos fonctions soit une injection. Si cela est faisable, alors pour f'dénoter l'inverse de fonction f, le calcul s[j-1]est aussi simple que

s[j-1] = f'[j-1](s[j], x[j-1])

Notez que, ici, la fonction et l'entrée sont les deux j-1, pas j. Cette même fonction et cette entrée seraient celles que vous auriez utilisées si vous calculiez

s[j] = f[j-1](s[j-1], x[j-1])

La création de l'inverse de ces fonctions est la partie la plus délicate. Cependant, vous ne pouvez généralement pas le faire, car certaines données d'état sont généralement perdues après chaque fonction du jeu.

Cette méthode, telle quelle, peut inverser le calcul s[j-1], mais seulement si vous l’avez s[j]. Cela signifie que vous ne pouvez regarder la retransmission qu'en arrière, à partir du moment où vous avez décidé de la rejouer en arrière. Si vous souhaitez rejouer en arrière à partir d'un point arbitraire, vous devez associer cela à la méthode 4.

Pour les échecs, cela ne peut pas être mis en œuvre, car avec un tableau donné et le mouvement précédent, vous pouvez savoir quelle pièce a été déplacée, mais pas d'où elle a été déplacée.

Méthode 6:

Enfin, si vous ne pouvez pas garantir que toutes vos fonctions sont des injections, vous pouvez faire un petit truc pour le faire. Au lieu que chaque fonction renvoie uniquement un nouvel état, vous pouvez également lui demander de renvoyer les données supprimées, comme suit:

s[j+1], r[j] = f[j](s[j], x[j])

r[j]sont les données ignorées. Et créez ensuite vos fonctions inverses afin qu’elles prennent les données supprimées, comme suit:

s[j] = f'[j](s[j+1], x[j], r[j])

En plus de f[j]et x[j], vous devez également stocker r[j]pour chaque fonction. Encore une fois, si vous voulez pouvoir chercher, vous devez stocker des signets, comme avec la méthode 4.

Pour les échecs, ce serait la même chose que la méthode 2, mais contrairement à la méthode 2, qui indique uniquement quelle pièce va où, vous devez également stocker d'où vient chaque pièce.

La mise en oeuvre:

Étant donné que cela fonctionne pour tous les types d'états, avec toutes sortes de fonctions, pour un jeu spécifique, vous pouvez faire plusieurs hypothèses qui faciliteront sa mise en œuvre. En fait, si vous implémentez la méthode 6 avec l’ensemble de l’état du jeu, vous pourrez non seulement rejouer les données, mais aussi remonter dans le temps et reprendre la lecture à tout moment. Ce serait vraiment génial.

Au lieu de stocker tout l'état du jeu, vous pouvez simplement stocker le minimum nécessaire pour dessiner un état donné et sérialiser ces données chaque fois que vous le souhaitez. Vos états seront ces sérialisations et votre entrée sera maintenant la différence entre deux sérialisations. Pour que cela fonctionne, il est essentiel que la sérialisation change peu si l’état du monde change aussi peu. Cette différence est complètement inversible, la mise en œuvre de la méthode 5 avec des signets est donc très possible.

J'ai vu cela implémenter dans certains jeux majeurs, principalement pour la lecture instantanée de données récentes lorsqu'un événement (un fragment dans une image par seconde ou un score dans un jeu sportif) se produit.

J'espère que cette explication n'était pas trop ennuyeuse.

¹ Cela ne signifie pas que certains programmes agissent comme s'ils n'étaient pas déterministes (comme MS Windows ^^). Maintenant sérieusement, si vous pouvez créer un programme non déterministe sur un ordinateur déterministe, vous pouvez être certain de remporter simultanément la médaille Fields, le prix Turing et probablement même un Oscar et un Grammy pour tout ce que vous méritez.


la source
Sur "TOUS LES PROGRAMMES INFORMATIQUES SONT DETERMINISTES", vous négligez de prendre en compte les programmes qui reposent sur le threading. Bien que le threading soit principalement utilisé pour charger des ressources ou pour séparer la boucle de rendu, il existe des exceptions, et à ce stade, vous ne pourrez peut-être plus revendiquer le vrai déterminisme, à moins que vous ne soyez proprement strict quant à l'application du déterminisme. Les mécanismes de verrouillage à eux seuls ne suffiront pas. Vous ne pourriez partager AUCUNE donnée mutable sans travail supplémentaire. Dans de nombreux scénarios, un jeu n'a pas besoin de ce niveau de rigueur pour son propre intérêt, mais pourrait le faire pour des choses comme les replays.
krdluzni
1
@krdluzni Le filetage, le parallélisme et les nombres aléatoires issus de véritables sources aléatoires ne rendent pas les programmes non déterministes. Le minutage des threads, les blocages, la mémoire non initialisée et même les conditions de concurrence ne sont que des entrées supplémentaires de votre programme. Votre choix de supprimer ces entrées ou même de ne pas les prendre en compte (pour une raison quelconque) n'aura aucune incidence sur le fait que votre programme s'exécutera exactement de la même manière compte tenu des mêmes entrées. "non déterministe" est un terme informatique très précis, évitez donc de l'utiliser si vous ne savez pas ce que cela signifie.
@oscar (peut être un peu sommaire, occupé, peut-être édité plus tard): Bien que dans un sens strict, théorique, vous puissiez demander le minutage des threads, etc., cela n'est d'aucune utilité dans la pratique, car ils ne peuvent généralement pas être observés. programme lui-même ou entièrement contrôlé par le développeur. En outre, un programme qui n'est pas déterministe est significativement différent du fait qu'il est non déterministe (au sens de la machine à états). Je comprends le sens du terme. J'aurais aimé qu'ils choisissent autre chose plutôt que de surcharger un terme préexistant.
krdluzni
@krdluzni Mon but dans la conception de systèmes de relecture avec des éléments imprévisibles tels que les timings de threads (s'ils affectent votre capacité à calculer avec précision une relecture), est de les traiter comme n'importe quelle autre source d'entrée, tout comme une entrée utilisateur. Je ne vois personne se plaindre qu'un programme est "non déterministe" car il nécessite une entrée utilisateur totalement imprévisible. Quant au terme, il est inexact et déroutant. Je préférerais qu'ils utilisent quelque chose comme "pratiquement imprévisible" ou quelque chose comme ça. Et non, ce n'est pas impossible, vérifiez le débogage de relecture de VMWare.
9

Une chose que d’autres réponses n’ont pas encore couverte est le danger des flotteurs. Vous ne pouvez pas faire une application totalement déterministe en utilisant des flottants.

En utilisant des floats, vous pouvez avoir un système complètement déterministe, mais seulement si:

  • Utiliser exactement le même binaire
  • Utiliser exactement le même processeur

En effet, la représentation interne des flotteurs varie d'un processeur à l'autre, notamment entre les processeurs AMD et intel. Tant que les valeurs sont dans les registres FPU, elles sont plus précises qu'elles ne le sont du côté C, de sorte que tous les calculs intermédiaires sont effectués avec une plus grande précision.

Il est tout à fait évident que cela affectera le bit AMD vs Intel - supposons par exemple que l'un utilise des flottants de 80 bits et que l'autre 64, par exemple - mais pourquoi la même exigence binaire?

Comme je l'ai dit, la précision la plus élevée est utilisée tant que les valeurs sont dans les registres FPU . Cela signifie que chaque fois que vous recompilez, l'optimisation de votre compilateur peut échanger des valeurs dans les registres FPU et en sortir, produisant des résultats légèrement différents.

Vous pourrez peut-être résoudre ce problème en définissant les indicateurs _control87 () / _ controlfp () de manière à utiliser la précision la plus faible possible. Cependant, certaines bibliothèques peuvent aussi toucher à cela (au moins une version de d3d l'a fait).

Jari Komppa
la source
3
Avec GCC, vous pouvez utiliser -ffloat-store pour forcer les valeurs à sortir des registres et les tronquer à une précision de 32/64 bits, sans avoir à vous soucier des autres bibliothèques manipulant vos indicateurs de contrôle. De toute évidence, cela aura un impact négatif sur votre vitesse (mais également toute autre quantification).
8

Sauvegardez l'état initial de vos générateurs de nombres aléatoires. Puis sauvegardez, horodaté, chaque entrée (souris, clavier, réseau, peu importe). Si vous avez un jeu en réseau, vous avez probablement déjà tout cela en place.

Reconfigurez les RNG et reproduisez l'entrée. C'est ça.

Cela ne résout pas le problème du rembobinage, pour lequel il n’existe pas de solution générale, à part reproduire le plus rapidement possible. Vous pouvez améliorer les performances à cet égard en contrôlant l'état complet du jeu toutes les X secondes. Vous n'aurez alors besoin que de rejouer le même nombre de fois. Toutefois, l'état de jeu entier peut également s'avérer extrêmement coûteux.

Les particularités du format de fichier importent peu, mais la plupart des moteurs disposent déjà d’un moyen de sérialiser les commandes et de les indiquer - pour la mise en réseau, la sauvegarde, etc. Il suffit d'utiliser ça.


la source
4

Je voterais contre le rejeu déterministe. C’est beaucoup plus simple et moins dangereux d’erreurs de sauvegarder l’état de chaque entité tous les 1 / Nème de seconde.

Enregistrez uniquement ce que vous souhaitez afficher lors de la lecture - s'il ne s'agit que de la position et du titre, d'accord, si vous souhaitez également afficher les statistiques, sauvegardez-le également, mais en général, économisez le moins possible.

Tweak l'encodage. Utilisez le moins de bits possible pour tout. La rediffusion ne doit pas nécessairement être parfaite tant qu'elle est suffisamment belle. Même si vous utilisez un float pour, par exemple, l'en-tête, vous pouvez l'enregistrer dans un octet et obtenir 256 valeurs possibles (précision de 1,4º). Cela peut suffire ou même trop pour votre problème particulier.

Utilisez le codage delta. Sauf si vos entités se téléportent (et si elles le font, traitez le cas séparément), encodez les positions comme faisant la différence entre la nouvelle position et l'ancienne position - pour des mouvements courts, vous pouvez vous en tirer avec beaucoup moins de bits que nécessaire pour des positions complètes. .

Si vous voulez revenir en arrière facilement, ajoutez des images clés (données complètes, pas de deltas) toutes les N images. De cette façon, vous pouvez vous en tirer avec une précision moindre pour les deltas et autres valeurs. Les erreurs d'arrondi ne seront plus aussi problématiques si vous réinitialisez périodiquement les valeurs "vraies".

Enfin, gzip le tout :)

Ggambett
la source
1
Cela dépend toutefois du type de jeu.
Jari Komppa le
Je serais très prudent avec cette déclaration. Surtout pour les projets plus importants avec des dépendances de tiers, sauver l'état peut être impossible. Tout en réinitialisant et en rejouant l'entrée est toujours possible.
TomSmartBishop
2

C'est dur. Tout d'abord, et surtout, lisez les réponses de Jari Komppa.

Une relecture effectuée sur mon ordinateur risque de ne pas fonctionner sur votre ordinateur car le résultat de flottement est légèrement différent. C'est une grosse affaire.

Mais après cela, si vous avez des nombres aléatoires, vous devez stocker la valeur initiale dans la relecture. Chargez ensuite tous les états par défaut et définissez le nombre aléatoire sur cette valeur initiale. À partir de là, vous pouvez simplement enregistrer l’état actuel de la clé / de la souris et la durée de son utilisation. Ensuite, exécutez tous les événements en utilisant cela comme entrée.

Pour contourner les fichiers (ce qui est beaucoup plus difficile), vous devez vider THE MEMORY. Par exemple, où chaque unité se trouve, l'argent, la durée écoulée, tout l'état du jeu. Puis avancez rapidement mais rejouez tout, sauf le rendu, le son etc. jusqu'à ce que vous arriviez à la destination de votre choix. Cela peut se produire toutes les minutes ou toutes les 5 minutes, en fonction de la rapidité de la transmission.

Les points principaux sont - Faire face à des nombres aléatoires - Copier une entrée (joueur (s), et joueur (s) distant (s)) - Etat de décharge pour sauter des fichiers et ...


la source
2

Je suis un peu surpris que personne ne mentionne cette option, mais si votre jeu comporte un composant multijoueur, vous avez peut-être déjà effectué beaucoup de travail acharné pour cette fonctionnalité. Après tout, qu’est-ce que le multijoueur est une tentative de rejouer les mouvements de quelqu'un d’autre à un moment (légèrement) différent sur votre propre ordinateur?

Cela vous permet également de profiter d'une taille de fichier réduite comme effet secondaire, à supposer que vous travailliez sur un code réseau convivial pour la bande passante.

À bien des égards, il combine les options "être extrêmement déterministe" et "garder une trace de tout". Vous aurez toujours besoin de déterminisme - si votre rejeu est essentiellement constitué de bots qui reprennent le jeu exactement comme vous le jouiez à l’origine, quelles que soient les actions qu’ils prennent qui peuvent avoir des résultats aléatoires doivent avoir le même résultat.

Le format des données pourrait être aussi simple qu’une simple décharge du trafic réseau, bien que j’imagine qu’il ne serait pas bon de le nettoyer un peu (vous n’aurez pas à vous soucier du décalage lors d’une nouvelle lecture, après tout). Vous pouvez ne rejouer qu'une partie du jeu en utilisant le mécanisme de point de contrôle déjà mentionné - généralement, un jeu multijoueur enverra de temps en temps un état complet de la mise à jour du jeu. Vous aurez donc peut-être déjà effectué ce travail.

Atiaxi
la source
0

Pour obtenir le fichier de relecture le plus petit possible, vous devez vous assurer que votre jeu est déterministe. Cela implique généralement de regarder votre générateur de nombre aléatoire et de voir où il est utilisé dans la logique de jeu.

Vous aurez probablement besoin d'un RNG de logique de jeu et d'un RNG pour tout le reste pour des éléments tels que l'interface graphique, les effets de particules et les sons. Une fois que cela est fait, vous devez enregistrer l’état initial de la logique de jeu RNG, puis les commandes de jeu de tous les joueurs à chaque image.

Pour de nombreux jeux, il existe un niveau d'abstraction entre l'entrée et la logique de jeu où l'entrée est transformée en commandes. Par exemple, si vous appuyez sur le bouton A du contrôleur, la commande numérique "jump" est définie sur true et la logique de jeu réagit aux commandes sans vérifier directement le contrôleur. En faisant cela, vous n'aurez plus qu'à enregistrer les commandes qui ont un impact sur la logique de jeu (pas besoin d'enregistrer la commande "Pause") et très probablement, ces données seront plus petites que l'enregistrement des données du contrôleur. Vous n'avez également pas à vous soucier de l'enregistrement de l'état du schéma de contrôle au cas où le joueur déciderait de remapper les boutons.

Le rembobinage est un problème difficile en utilisant la méthode déterministe et en dehors de la capture instantanée de l’état du jeu et de la transmission rapide au moment voulu, vous ne pouvez rien faire d’autre que d’enregistrer l’état complet du jeu à chaque image.

D'autre part, l'avance rapide est certainement faisable. Tant que votre logique de jeu ne dépend pas de votre rendu, vous pouvez l'exécuter autant de fois que vous le souhaitez avant de rendre une nouvelle image du jeu. La vitesse d'avance rapide sera simplement liée à votre machine. Si vous souhaitez avancer par incréments importants, vous devez utiliser la même méthode d'instantané que celle dont vous auriez besoin pour le rembobinage.

La partie la plus importante de l’écriture d’un système de relecture qui repose sur le déterminisme consiste probablement à enregistrer un flux de données de débogage. Ce flux de débogage contient un instantané contenant le plus d’informations possible à chaque image (semences RNG, transformations d’entités, animations, etc.) et permet de tester ce flux de débogage enregistré par rapport à l’état du jeu lors des replays. Cela vous permettra de vous informer rapidement des incohérences à la fin d'une image donnée. Cela vous épargnera d'innombrables heures à vouloir vous arracher les cheveux de bugs inconnus non déterministes. Quelque chose d'aussi simple qu'une variable non initialisée va tout gâcher à la 11e heure.

NOTE: Si votre jeu implique une diffusion dynamique de contenu ou si vous avez une logique de jeu sur plusieurs threads ou sur différents cœurs ... bonne chance.

Lathentar
la source
0

Pour activer à la fois l'enregistrement et le rembobinage, enregistrez tous les événements (générés par l'utilisateur, générés par la minuterie, la communication générée, ...).

Pour chaque heure d'enregistrement d'événement, ce qui a été modifié, les valeurs précédentes, les nouvelles valeurs.

Les valeurs calculées ne doivent pas être enregistrées sauf si le calcul est aléatoire
(dans ce cas, vous pouvez également enregistrer les valeurs calculées ou enregistrer les modifications apportées aux semences après chaque calcul aléatoire).

Les données enregistrées sont une liste de modifications.
Les modifications peuvent être enregistrées dans différents formats (binaire, xml, ...).
Le changement consiste en un identifiant d'entité, un nom de propriété, une ancienne valeur, une nouvelle valeur.

Assurez-vous que votre système peut lire ces modifications (accès à l'entité souhaitée, modification de la propriété souhaitée en passant au nouvel état ou retour à l'ancien).

Exemple:

  • temps à partir du début = t1, entité = joueur 1, propriété = position, changé de a en b
  • temps à partir du début = t1, entité = système, propriété = mode de jeu, changé de c en d
  • temps à partir du début = t2, entité = joueur 2, propriété = état, changé de e en f
  • Pour permettre un rembobinage / une avance rapide ou l'enregistrement plus rapide de certaines plages de temps,
    des images clés sont nécessaires - si vous enregistrez tout le temps, sauvegardez de temps en temps l'état du jeu dans son intégralité.
    Si vous n'enregistrez que certaines plages de temps, enregistrez au début l'état initial.

    Danny Varod
    la source
    -1

    Si vous avez besoin d’idées sur la façon de mettre en œuvre votre système de relecture, recherchez sur Google pour savoir comment mettre en oeuvre undo / redo. dans une application. Cela peut sembler évident pour certains, mais peut-être pas pour tous, que annuler / rétablir est conceptuellement identique à rejouer pour des jeux. Il s’agit simplement d’un cas particulier dans lequel vous pouvez revenir en arrière et, en fonction de l’application, rechercher à un moment précis.

    Vous verrez que personne n'implémentant undo / redo se plaint de variables déterministes / non déterministes, de variables flottantes ou de processeurs spécifiques.


    la source
    Undo / redo se produit dans des applications qui sont elles-mêmes fondamentalement déterministes, gérées par des événements et très claires (par exemple, l'état d'un document de traitement de texte est uniquement constitué du texte et de la sélection, et non de la mise en page complète, qui peut être recalculée).
    Il est alors évident que vous n’avez jamais utilisé d’applications CAO / FAO, de logiciels de conception de circuits, de logiciels de suivi des mouvements ou d’application avec annulation / restauration plus sophistiquée que celle d’un traitement de texte. Je ne dis pas que le code pour annuler / rétablir peut être copié pour être rejoué sur un jeu, mais qu'il est conceptuellement le même (enregistrer les états et les rejouer plus tard). Cependant, la structure de données principale n'est pas une file d'attente, mais une pile.