Mécanisme d'inversion du temps dans les jeux

10

Je me demande comment les mécanismes de manipulation du temps dans les jeux sont généralement conçus. Je suis particulièrement intéressé par le retournement du temps (un peu comme dans le dernier SSX ou Prince of Persia).

Le jeu est un jeu de tir 2D de haut en bas.

Le mécanisme que j'essaye de concevoir / implémenter a les exigences suivantes:

1) Les actions d'entités en dehors du personnage du joueur sont complètement déterministes.

  • L'action d'une entité est basée sur les images progressées depuis le début du niveau et / ou la position du joueur sur l'écran
  • Les entités sont générées à une heure définie pendant le niveau.

2) L'inversion du temps fonctionne en inversant en arrière en temps réel.

  • Les actions des joueurs sont également inversées, il rejoue à l'envers ce que le joueur a effectué. Le joueur n'a aucun contrôle pendant le temps inverse.
  • Il n'y a pas de limite sur le temps passé à inverser, nous pouvons inverser jusqu'au début du niveau si vous le souhaitez.

Par exemple:

Images 0-50: le joueur avance de 20 unités pendant cette période. L'ennemi 1 apparaît à l'image 20 L'ennemi 1 se déplace de 10 unités vers la gauche pendant l'image 30-40 Le joueur tire la balle à l'image 45 La balle se déplace 5 vers l'avant (45-50) et tue l'ennemi 1 à cadre 50

Inverser cela jouerait en temps réel: le joueur recule de 20 unités pendant ce temps. L'ennemi 1 ressuscite à l'image 50 La balle réapparaît à l'image 50 La balle recule de 5 et disparaît (50-45) L'ennemi se déplace à gauche 10 (40-30) L'ennemi est retiré à cadre 20.

En regardant simplement le mouvement, j'avais quelques idées sur la façon d'y parvenir, je pensais avoir une interface qui changerait le comportement lorsque le temps avançait ou s'inversait. Au lieu de faire quelque chose comme ça:

void update()
{
    movement += new Vector(0,5);
}

Je ferais quelque chose comme ça:

public interface movement()
{
    public void move(Vector v, Entity e);
}

public class advance() implements movement
{
    public void move(Vector v, Entity e)
    {
            e.location += v;
    }
}


public class reverse() implements movement
{
    public void move(Vector v, Entity e)
    { 
        e.location -= v;
    }
}

public void update()
{
    moveLogic.move(new vector(5,0));
}

Cependant, j'ai réalisé que ce ne serait pas des performances optimales et que cela deviendrait rapidement compliqué pour des actions plus avancées (comme un mouvement fluide le long de chemins courbes, etc.).

Jkh2
la source
1
Je n'ai pas regardé tout cela (caméra tremblante YouTube, 1,5 heures) , mais peut-être qu'il y a des idées de Jonathan Blow qui ont fonctionné cela dans son jeu Braid.
MichaelHouse
Copie
Hackworth

Réponses:

9

Vous voudrez peut-être jeter un œil au modèle de commande .

Fondamentalement, chaque action réversible prise par vos entités est implémentée en tant qu'objet de commande. Tous ces objets implémentent au moins 2 méthodes: Execute () et Undo (), plus tout ce dont vous avez besoin, comme une propriété d'horodatage pour un timing correct.

Chaque fois que votre entité effectue une action réversible, vous créez d'abord un objet de commande approprié. Vous l'enregistrez sur une pile Annuler, puis alimentez votre moteur de jeu et l'exécutez. Lorsque vous voulez inverser le temps, vous faites éclater des actions du haut de la pile et appelez leur méthode Undo (), qui fait le contraire de la méthode Execute (). Par exemple, en cas de saut du point A au point B, vous effectuez un saut de B vers A.

Après avoir sauté une action, enregistrez-la sur une pile de rétablissement si vous souhaitez avancer et reculer à volonté, tout comme la fonction annuler / rétablir dans un éditeur de texte ou un programme de peinture. Bien sûr, vos animations doivent également prendre en charge un mode "rembobinage" pour les lire à l'envers.

Pour plus de manigances de conception de jeux, laissez chaque entité stocker ses actions sur sa propre pile, afin que vous puissiez les annuler / refaire indépendamment les unes des autres.

Un modèle de commande présente d'autres avantages: par exemple, il est assez trivial de créer un enregistreur de relecture, car il vous suffit de sauvegarder tous les objets des piles dans un fichier, et au moment de la relecture, il suffit de l'introduire dans le moteur de jeu un par une.

Hackworth
la source
2
Notez que la réversibilité des actions dans un jeu peut être une chose très délicate, en raison de problèmes de précision en virgule flottante, d'horaires variables, etc. il est beaucoup plus sûr de sauvegarder cet état que de le reconstruire dans la plupart des situations.
Steven Stadnicki
@StevenStadnicki Peut-être, mais c'est certainement possible. Du haut de ma tête, C&C Generals le fait de cette façon. Il peut lire jusqu'à 8 joueurs pendant plusieurs heures, pesant au moins quelques centaines de Ko, et c'est ainsi que je suppose que la plupart, sinon tous les jeux RTS, font leur multijoueur: vous ne pouvez tout simplement pas transmettre l'état du jeu complet avec potentiellement des centaines d'unités à chaque image, vous devez laisser le moteur faire la mise à jour. Alors oui, c'est définitivement viable.
Hackworth
3
La relecture est très différente du rembobinage, car les opérations qui sont toujours reproductibles vers l'avant (par exemple, trouver la position à l'image n, x_n, en commençant par x_0 = 0 et en ajoutant les deltas v_n pour chaque étape) ne sont pas nécessairement reproductibles en arrière ; (x + v_n) -v_n n'est pas toujours égal à x dans les mathématiques à virgule flottante. Et il est facile de dire «contournez-le», mais vous parlez d'une refonte potentielle complète, notamment le fait de ne pas pouvoir utiliser de nombreuses bibliothèques externes.
Steven Stadnicki
1
Pour certains jeux, votre approche peut être réalisable, mais AFAIK la plupart des jeux qui utilisent l'inversion du temps comme mécanicien utilisent quelque chose de plus proche de l'approche Memento de OriginalDaemon où l'état pertinent est enregistré pour chaque image.
Steven Stadnicki
2
Qu'en est-il du rembobinage en recalculant les étapes, mais en enregistrant une image clé toutes les deux secondes? Les erreurs en virgule flottante ne sont pas susceptibles de faire une différence significative en quelques secondes (en fonction de la complexité, bien sûr). Il fonctionne également en compression vidéo: P
Tharwen
1

Vous pouvez jeter un œil au modèle Memento; son intention principale est d'implémenter des opérations d'annulation / rétablissement en annulant l'état de l'objet, mais pour certains types de jeux, cela devrait suffire.

Pour un jeu dans une boucle en temps réel, vous pouvez considérer chaque image de vos opérations comme un changement d'état et la stocker. Il s'agit d'une approche simple à mettre en œuvre. L'alternative consiste à intercepter lorsque l'état d'un objet est modifié. Par exemple, détecter quand les forces agissant sur un corps rigide sont modifiées. Si vous utilisez des propriétés pour obtenir et définir des variables, cela peut également être une implémentation relativement simple, la partie difficile est d'identifier quand restaurer l'état, car ce ne sera pas le même temps pour chaque objet (vous pouvez stocker le temps de restauration en tant que nombre d'images depuis le début du système).

OriginalDaemon
la source
0

Dans votre cas particulier, la gestion de la restauration en rembobinant la motion devrait fonctionner correctement. Si vous utilisez n'importe quelle forme de recherche de chemin avec les unités AI, assurez-vous simplement de la recalculer après la restauration pour éviter le chevauchement des unités.

Le problème est la façon dont vous gérez le mouvement lui-même: un moteur physique décent (un tireur 2D de haut en bas sera très bien avec un très simple) qui garde une trace des informations des étapes passées (y compris la position, la force nette, etc.) fournira une base solide. Ensuite, en décidant d'une restauration maximale et d'une granularité pour les étapes de restauration, vous devriez obtenir le résultat souhaité.

Darkwings
la source
0

Bien que ce soit une idée intéressante. Je le déconseille.

La relecture du jeu en avant fonctionne bien, car une opération aura toujours le même effet sur l'état du jeu. Cela n'implique pas que l'opération inverse vous donne l'état d'origine. Par exemple, évaluez l'expression suivante dans n'importe quel langage de programmation (désactivez l'optimisation)

(1.1 + 3 - 3) == 1.1

En C et C ++ au moins, elle renvoie false. Bien que la différence puisse être faible, imaginez combien d'erreurs peuvent s'accumuler à 60 images par seconde en 10 secondes de minutes. Il y aura des cas où un joueur ne manquera que quelque chose, mais le frappera pendant que le jeu sera rejoué à l'envers.

Je recommanderais de stocker des images clés toutes les demi-secondes. Cela ne prendra pas trop de mémoire. Vous pouvez alors soit interpoler entre les images clés, soit encore mieux, simuler le temps entre deux images clés, puis le rejouer à l'envers.

Si votre jeu n'est pas trop compliqué, stockez simplement les images clés de l'état du jeu 30 fois par seconde et jouez à l'envers. Si vous aviez 15 objets chacun avec une position 2D, cela prendrait une bonne 1,5 minutes pour atteindre un Mo, sans compression. Les ordinateurs ont des gigaoctets de mémoire.

Alors ne le compliquez pas trop, il ne sera pas facile de rejouer un jeu à l'envers, et cela causera beaucoup d'erreurs.

Hannesh
la source