Monstre errant: comment se débarrasser des problèmes sur la carte

image

Déjà en train de créer The Witness est devenu l'un de mes jeux préférés. J'ai commencé à jouer à partir du moment où Jonathan Blow a commencé à le développer, et je ne pouvais pas attendre sa sortie.

Contrairement au jeu précédent de John Braid, l' échelle des ressources et de la programmation de The Witness était beaucoup plus proche des projets AAA que des jeux indépendants. Quiconque travaille sur de tels projets sait que la quantité de travail lors du choix de cette voie augmente considérablement. Il y avait beaucoup plus de personnes travaillant sur The Witness que sur Braid , mais comme pour tout projet de ce niveau, de nombreux aspects nécessitent plus d'attention que la gestion de projet ne peut se le permettre.

Par conséquent, j'ai toujours voulu trouver du temps libre pour aider à créer The Witness lors de la sortie du jeu. Alors un jour, Thanksgiving, John et moi nous sommes assis et avons regardé la liste des choses dans la base de code qui bénéficieraient des efforts supplémentaires d'un autre programmeur. Après avoir décidé de l'importance relative des éléments de la liste, nous avons décidé que le gameplay bénéficierait le plus si nous apportions des améliorations au code de mouvement du joueur.

Walkmonster dans le mur


Dans le contexte de The Witness , l'objectif du code de mouvement d'un joueur est d'être aussi discret que possible. Le joueur doit s'immerger complètement dans une réalité alternative, et dans cette expérience de jeu, chaque détail est important. La dernière chose que nous voulions était que le joueur remarque qu'il était assis devant l'ordinateur et bougeait la caméra virtuelle.

Par conséquent, le code de mouvement du joueur doit être absolument fiable. Si un joueur s'accroche aux coins, se coince dans les murs, tombe à travers le sol, descend d'une colline sans possibilité de revenir en arrière, etc., cela détruira instantanément l'illusion d'immersion et rappellera au joueur qu'il est dans un processus de jeu artificiel qui est gêné par un système peu fiable déplacements. Dans certaines circonstances, cela peut même entraîner des conséquences désastreuses pour le joueur s'il n'a pas la possibilité de résoudre le problème en redémarrant le jeu ou en rechargeant la sauvegarde (probablement très ancienne). Si vous jouez souvent à des jeux, vous devez avoir rencontré des problèmes de ce type et vous savez ce que je veux dire.

Après notre discussion, j'ai commencé à travailler sur cette tâche. Tout d'abord, j'ai décidé d'écrire des outils intégrés pour travailler avec le code de mouvement du joueur, afin que nous puissions l'analyser et observer son comportement actuel. Après avoir ouvert le projet, je suis tombé sur un grave problème déjà connu de moi: que dois-je nommer le premier fichier de code source? C'est toujours la partie la plus importante de tout projet ( comme l'a dit Bob Pollard à propos des noms des groupes musicaux et des albums ). Si vous donnez au fichier source un nom approprié, les travaux ultérieurs seront clairs et fluides. Choisissez le mauvais - vous pouvez détruire tout le projet.

Mais quel est le nom du système pour garantir la qualité du code de mouvement du joueur? Je n'ai jamais eu à écrire de code comme ça auparavant. Quand j'y ai pensé, j'ai réalisé que personnellement je n'ai vu un exemple d'un tel code qu'une seule fois: lors de la lecture de la première version bêta de Quake . Il contenait des bogues avec l'emplacement des monstres, et dans la fenêtre de la console, vous pouviez voir des messages d'erreur indiquant que des monstres, au lieu de créer à la surface de la terre, étaient créés, se croisant partiellement avec la géométrie des niveaux. Chaque message de débogage commençait par la phrase «walkmonster in wall at ...»

Bingo! Il est difficile de trouver un meilleur nom pour le fichier de code que "walk_monster.cpp". Et j'étais presque sûr que désormais le code serait créé sans problème.

Mouvement au point


Lorsque vous souhaitez tester le système, la chose la plus importante est de tester réellement le système . Bien que cette règle semble simple, les personnes qui écrivent des tests ne se conforment souvent pas.

Dans notre cas particulier, il est très facile d’ imaginer que nous testons le code de mouvement d’un joueur sans le tester réellement. Voici un exemple: vous pouvez analyser le volume de collisions et de surfaces sur lesquelles vous pouvez vous déplacer dans le jeu, rechercher de petites surfaces, des lacunes, etc. Après avoir éliminé tous ces problèmes, nous pouvons dire que le joueur peut désormais se déplacer et marcher en toute sécurité dans le monde.

Mais en fait, nous avons testé les données, pas le code. Il est très probable qu'il y aura des bogues dans le code de mouvement qui conduiront à un mauvais comportement même avec des données de haute qualité.

Pour éviter un tel piège, je voulais que le système de test soit aussi proche que possible du comportement de la personne qui contrôle réellement le mouvement du personnage dans le jeu. J'ai commencé par écrire deux procédures qui deviendraient les éléments constitutifs de ces tests.

La première procédure est la plus proche des actions humaines réelles. Il s'agit d'un appel de mise à jour qui se connecte au système de traitement d'entrée du témoin et lui transmet les événements synthétisés du clavier et de la souris. Il est capable de faire des choses simples qu'une personne peut faire: regarder autour, aller vers un point, regarder un point, etc. La procédure effectue ces actions en émulant simplement l'interaction de l'utilisateur avec le clavier et la souris, donc j'étais sûr que lors du traitement de l'entrée de The Witness, tout se fera exactement comme lors des tests. Dans les articles suivants, je parlerai davantage de ce système et de son utilisation.

La deuxième procédure est une étape qui n'est pas utilisée à ce niveau. Il s'agit d'une fonction appelée DriveTowardPoint , qui reçoit deux points dans le monde et, provoquant un système de collision existant d'un joueur, tente de se déplacer en toute transparence d'un point à un autre. En effectuant le retour, elle transmet des informations sur la tentative: quels obstacles elle a rencontrés sur le chemin et si elle a réussi à atteindre le point final.

Cette fonction n'est pas aussi fiable qu'une méthode de test avec entrée synthétisée, car elle élimine une partie du système de mouvement du joueur des tests. Par exemple, toute condition erronée associée à l'emplacement du joueur en cas de problèmes avec le système de collision n'affectera pas les tests utilisant cette fonction. Néanmoins, j'ai jugé ce niveau de test précieux, car il peut tester de vastes zones beaucoup plus rapidement, car il ne nécessite pas l'exécution de tout le cycle de jeu, c'est-à-dire qu'il peut être utilisé beaucoup plus souvent dans le monde, et pas seulement dans des cycles de test séparés. .

Il convient également de noter que cette fonction ne transmet pas de données d'entrée physiques; par exemple, les vitesses ne sont pas indiquées pour le point de départ. C'est parce que The Witness n'est pas un jeu d'action, donc le joueur a peu de propriétés physiques importantes. Les joueurs ne peuvent pas sauter, courir sur les murs, allumer la balle. Vous pouvez prendre en charge de tels comportements à l'aide de systèmes que je décrirai plus tard, mais ils ajoutent des niveaux de complexité qui n'étaient pas requis dans notre projet.

Quoi qu'il en soit, après avoir implémenté DriveTowardPoint, j'ai pu commencer à résoudre la première tâche du système: déterminer où le joueur peut se déplacer vers l'île des témoins .

Explorer rapidement des arbres aléatoires


Où les joueurs peuvent-ils aller? Cela semble être une question simple, mais vous serez surpris de savoir combien de jeux ont été publiés lorsque l'équipe de développement ne connaissait pas la vraie réponse. Si cela est possible, je voulais que The Witness soit l'un de ces rares jeux dans lesquels les développeurs avant la sortie savaient exactement où un joueur pouvait et ne pouvait pas obtenir - pas de surprise.

Cela rend l'énoncé du problème (mais probablement pas sa solution) très simple: s'il existe une fonction DriveTowardPoint qui détermine de manière fiable si le joueur peut se déplacer en ligne droite entre deux points, créez une carte de couverture montrant où le joueur peut être.

Pour une raison quelconque, sans écrire une seule ligne de code, pour une raison quelconque, j'ai pensé qu'il serait préférable d'utiliser Rapidly Exploring Random Tree . Pour ceux qui ne connaissent pas cet algorithme, je vais vous expliquer: il s'agit d'un processus très simple dans lequel nous enregistrons tous les points que nous avons visités en référence au point d'où nous venons. Pour ajouter un point à l'arbre, nous prenons un point cible aléatoire n'importe où dans le monde, sélectionnons le point le plus proche de lui, déjà dans l'arbre, et essayons d'aller de ce point à la cible. L'endroit où nous nous sommes retrouvés devient le prochain point d'échantillonnage.

Habituellement, cet algorithme est utilisé pour rechercher des chemins: alternativement pour des points aléatoires, nous sélectionnons toujours le même point que la cible. Cela incline l'exploration de l'espace vers le point cible, et c'est ce qui est requis lorsque notre seule tâche est d'atteindre le but. Mais dans ce cas, je voulais créer une carte complète des endroits où le joueur pourrait tomber, donc je n'utilise que des échantillons aléatoires.

Après avoir implémenté cet algorithme (heureusement, il est très simple et n'a pas demandé beaucoup de temps), j'ai vu qu'il effectuait un assez bon travail d'exploration de l'espace (les chemins indiqués sont représentés par des chemins blancs et les lignes rouges verticales indiquent les endroits où l'algorithme est entré en collision avec un obstacle) :


Cependant, après avoir observé son comportement, j'ai réalisé qu'en fait je n'ai pas besoin d'un tel algorithme. Par exemple, même après de nombreuses itérations, il est à peine capable d'explorer les pièces similaires à celles illustrées ci-dessous, malgré la couverture dense des zones extérieures. En effet, il n'est tout simplement pas en mesure de sélectionner des points suffisamment aléatoires à l'intérieur des pièces:


Si j'y réfléchissais avant de commencer le travail, je comprendrais que l'avantage d'algorithmes comme Rapidly Exploring Random Tree est qu'ils explorent efficacement les espaces de grande dimension. En fait, c'est généralement la principale raison de leur utilisation. Mais The Witness n'a pas d'espaces de grande dimension. Nous avons un espace à deux dimensions (oui, distribué sur une variété complexe, mais c'est toujours un espace à deux dimensions).

Dans cet espace de faible dimension, les avantages de Rapidly Exploring Random Tree sont faibles et son inconvénient est extrêmement important pour ma tâche: l'algorithme est conçu pour la recherche la plus efficace de chemins vers des paires de points connectés dans l'espace, et non pour une recherche efficace de tous les points accessibles de cet espace. Si vous avez une telle tâche, alors en fait l'exploration rapide de l'arbre aléatoire prendra beaucoup de temps pour la résoudre.

J'ai donc rapidement réalisé que je devais rechercher un algorithme qui couvrait efficacement les espaces de faible dimension.

Remplissage d'inondation 3D


Quand j'ai vraiment pensé à choisir un algorithme, il est devenu évident qu'en fait j'avais besoin de quelque chose comme le bon vieux remplissage bidimensionnel, qui est utilisé pour remplir des zones du bitmap. Pour tout point de départ, je devais juste remplir tout l'espace, en vérifiant de manière exhaustive toutes les manières possibles. Malheureusement, pour de nombreuses raisons, la solution pour The Witness sera beaucoup plus compliquée que pour un bitmap bidimensionnel.

Premièrement, nous n'avons pas de concept clair de la connexité finie d'un point. Tout l'espace est continu. C'est pour un pixel, nous pouvons facilement lister 4 endroits possibles qui peuvent être atteints à partir d'un point donné, et vérifier chacun d'eux à tour de rôle.

Deuxièmement, il n'y a pas de taille fixe de position dans l'espace, comme un pixel sur un bitmap. Les surfaces sur lesquelles le joueur se déplace et les obstacles peuvent être n'importe où, ils n'ont pas de taille topologique maximale ou minimale, ainsi que pas de liaison à une grille externe.

Troisièmement, bien que le mouvement à travers l'espace The Witness puisse être considéré localement comme se déplaçant le long d'un avion, l'espace lui-même est en fait une variété profondément interconnectée et changeante, dans laquelle les zones de marche du joueur sont directement au-dessus d'autres zones (parfois il peut y avoir plusieurs niveaux situés l'un au-dessus de l'autre) . De plus, il existe des connexions qui varient en fonction des conditions du monde (portes ouvertes / fermées, ascenseurs qui montent / tombent, etc.).

Compte tenu des difficultés décrites, il est très simple de trouver votre propre option de mise en œuvre pour le remplissage, qui sera par conséquent rempli de zones qui se croisent, de routes importantes manquantes, d'informations erronées sur les connexions dans des endroits complexes de la variété. En fin de compte, l'algorithme sera trop lourd à utiliser, car pour tenir compte des changements de l'état du monde, il doit être réexécuté.

Je n'ai pas pensé à une bonne solution tout de suite, alors j'ai décidé de commencer par des expériences simples. En utilisant le code d' arbre aléatoire à exploration rapide que j'ai écrit, j'ai changé la sélection des points cibles de aléatoire à très contrôlée. Chaque fois qu'un nouveau point a été ajouté à l'arbre, j'ai indiqué que les points sont à une distance unitaire le long des directions principales du point qui sera considéré comme le futur point cible, comme cela se produit dans un remplissage bidimensionnel simple.

Mais bien sûr, si ce n'est pas prudent, cela créera un cycle d'échantillonnage inutile. Le point se ramifiera dans les 8 points voisins autour de lui, mais ces 8 points essaieront à nouveau de revenir au point de départ, et cela continuera pour toujours. Par conséquent, en plus de la sélection contrôlée des points cibles, j'ai besoin d'une restriction simple: tout point cible qui ne se trouve pas dans une certaine distance utile minimale d'un point cible existant ne sera pas pris en compte. À ma grande surprise, ces deux règles simples créent un remplissage assez réussi:


Pas mal pour une expérience assez simple. Mais l'algorithme souffre de ce que j'appelle «l'écho des limites». Cet effet peut être vu dans la capture d'écran suivante prise lors de l'étude de la carte:


Dans les zones sans obstacles, l'algorithme fonctionne bien en échantillonnant à des distances relativement égales. Mais quand ils atteignent l'intersection, ils créent des points qui sont «en dehors de la grille», c'est-à-dire qu'ils ne sont pas alignés selon le motif des échantillons, selon lequel l'algorithme remplit la zone ouverte voisine. La raison pour laquelle les points «dans la grille» ne créent pas de pavage excessivement dense est que chaque nouveau point essayant de revenir à l'un des précédents y trouve le point précédent et refuse de le recompter. Mais lors de la création de nouveaux points sur la frontière, ils sont complètement désalignés, donc rien ne peut les empêcher de retourner dans l'espace déjà exploré. Cela conduit à la création d'une vague d'échantillons biaisés, qui se poursuit jusqu'à ce qu'elle atteigne une ligne aléatoire de points ailleurs suffisamment proche pour que l'algorithme puisse la trouver coïncidant avec le front mobile des points.

Bien que cela ne semble pas être un problème grave, il est en réalité critique. L'intérêt de ces algorithmes est de concentrer les échantillons dans les zones où ils sont le plus susceptibles de produire des résultats productifs. Plus nous passons de temps à échantillonner et à rééchantillonner de vastes zones ouvertes, moins nous consacrerons de temps au marquage des faces mêmes de cette zone, qui sont les informations dont nous avons besoin. Étant donné qu'il s'agit d'un espace continu et que seul un nombre infini d'échantillons peut décrire sa véritable forme, le rapport des échantillons significatifs aux échantillons insignifiants est littéralement une mesure de l'efficacité de l'algorithme à créer une surface praticable pour un joueur.

Cependant, il existe une solution simple à ce problème particulier: vous devez augmenter la distance à laquelle les deux points sont considérés comme "assez proches". Ce faisant, nous réduirons la densité d'échantillonnage dans des endroits qui ne sont pas importants pour nous, mais perdrons également la densité d'échantillonnage dans des endroits qui sont importants pour nous, par exemple, les zones autour des frontières que nous voulons vérifier soigneusement pour la présence de "trous".

Échantillonnage directionnel localisé


Probablement parce que j'ai commencé avec l'arbre aléatoire à exploration rapide, mon cerveau a supplanté toutes les autres idées, sauf l'idée de proximité. Tous les algorithmes précédents utilisaient la proximité pour leur tâche, par exemple, afin de déterminer un nouveau point à considérer ensuite ou de sélectionner un point à partir duquel commencer pour arriver à un nouveau point cible.

Mais après avoir réfléchi à la tâche pendant un certain temps, je me suis rendu compte que tout devient plus logique, si nous pensons non seulement à la proximité, mais aussi à la direction . Ensuite, cela devient évident, mais si vous avez travaillé sur des tâches similaires, vous savez qu'il est facile de tomber dans le piège de la pensée étroite et de ne pas voir la situation dans son ensemble, même si cela s'avère plus simple. C'est exactement ce qui m'est arrivé.

Lorsque j'ai changé ma vision des choses, l'approche correcte de l'échantillonnage semblait évidente. Chaque fois que je voulais étendre mon exploration de l'espace à partir d'un point, je faisais une demande pour l'existence de points proches dans l'environnement local. Cependant, au lieu d'utiliser la distance à ces points pour la recherche, je vais les classer par leurs directions (avant cela, je n'ai utilisé que huit directions principales, mais je voulais expérimenter avec d'autres noyaux).

Dans toutes les directions où je ne «vois» pas le point, je parcours la distance spécifiée et j'ajoute un point à n'importe quel endroit où je me suis arrêté (que je rencontre ou non quelque chose). Si je vois un point dans l'une des directions, je m'y déplace et je vérifie si je peux y arriver. Si je le peux, j'ajoute simplement un bord visible pour que l'utilisateur puisse facilement voir que les points sont connectés. Si je ne peux pas, j'ajoute un nouveau point au point de collision, définissant la limite de l'obstacle.

Cette méthode d'échantillonnage a très bien fonctionné. Il nous permet de contrôler très précisément l'échantillonnage à l'aide de paramètres personnalisables pratiques, d'économiser tous les points nécessaires et d'éviter une tessellation inutile, ce qui conduit à un remplissage très rapide de l'espace:


Étant donné que l'algorithme recherche dans les directions et n'utilise pas seulement la proximité, il est protégé des échos des limites et limite l'échantillonnage excessif aux seules frontières dont nous avons besoin:


De plus, l'algorithme n'est pas du tout affecté par les transitions d'état ou les problèmes avec les variétés complexes. Il ne traite que des points, et ces points peuvent être n'importe où, et de nouveaux peuvent être ajoutés à tout moment. Si vous avez déjà établi une carte de la zone avec la porte fermée, puis après avoir ouvert la porte, il vous suffit de placer le seul point de recherche de l'autre côté de la porte et d'ordonner à l'algorithme de continuer à développer cette carte, après quoi il se connectera correctement et examinera correctement toute la zone à l'extérieur de la porte.

De plus, à tout moment, vous pouvez modifier les paramètres de base et le système continuera de fonctionner. Vous voulez faire un échantillonnage de zone avec une densité plus élevée? Réduisez simplement la distance par défaut. Cela peut être fait déjà dans le processus de construction de la carte, et l'algorithme commencera l'échantillonnage avec une densité plus élevée sans avoir à réinitialiser les résultats précédents (ce qui peut prendre un certain temps).

Vérification rudimentaire des bords


L'algorithme par défaut échantillonne déjà les bordures avec beaucoup de soin, car les intersections créent des points supplémentaires qui ne font pas partie du modèle d'échantillonnage, mais il ne les vérifie pas nécessairement avec le soin nécessaire, car il n'effectue aucune action spéciale lorsqu'il rencontre des obstacles. J'ai réalisé que depuis que je savais quels points ont été créés lors des collisions, les deux points de collision détectés sont reliés par une arête, et nous pouvons appeler un échantillonnage supplémentaire pour essayer de trouver plus de points limites dans le voisinage.

Je n'ai pas activement recherché cette approche, mais j'ai créé une méthode rudimentaire pour tester cette théorie, et cela m'a semblé prometteur. Après avoir pris deux points de collision reliés par un bord, je me déplace vers le milieu du bord et j'essaie de tracer la perpendiculaire vers l'extérieur par rapport au bord. S'il n'intersecte pas la frontière à une très courte distance, je suppose que la frontière est plus complexe et ajoute un nouveau point cible pour continuer la recherche dans cette zone.

Même ce schéma simple crée un échantillonnage dense de très haute qualité le long de la frontière sans échantillonner inutilement les zones ouvertes voisines. Voici une zone avec plusieurs bordures, mais sans vérifier les bords:


Et voici la même zone avec des bords de vérification:


Peu importe à quel point j'étais satisfait de ce résultat, j'ai été surpris par le manque d'algorithmes significativement meilleurs pour l'échantillonnage des frontières, et je vais essayer de choisir quelques méthodes supplémentaires à l'avenir.

Gains rapides


Même après avoir investi seulement un peu de temps dans le développement et la création d'un code assez simple, je me suis assuré que Walk Monster crée déjà une sortie tout à fait appropriée qui peut détecter de vrais problèmes dans le jeu. Voici des exemples de problèmes que j'ai trouvés lors du développement de l'algorithme:


Les pentes sur les côtés de cette plate-forme ne doivent pas être praticables, mais le joueur peut marcher dessus. Cela est arrivé parce que dans le code de mouvement du joueur, il existe une mauvaise façon pathologique de traiter la géométrie oblique. Maintenant je sais qu'il est là, et je vais le corriger quand il s'agit d'assurer sa fiabilité.


Le Témoin était censé être un jeu contemplatif, mais se demandant pourquoi il semble qu'il y ait une pierre, même si elle ne l'est pas, n'était pas l'un de ses koans. Comme vous pouvez le deviner, ce problème est survenu parce que quelqu'un a laissé la quantité de collision dans le jeu après avoir supprimé la géométrie qui la dénotait. Cela peut facilement se produire, et il est très bien que nous ayons un outil qui puisse rapidement reconnaître de telles erreurs afin que les gens n'aient pas à le faire.



Ces objets étaient censés être des roches infranchissables, mais Walk Monster a constaté que cela ne s'était pas produit. Pire, Walk Monster a constaté que pour une raison quelconque, le chemin n'est que dans un sens (de la capture d'écran de gauche à droite), mais cela ne devrait pas l'être. J'ai fait en sorte que le joueur puisse vraiment faire ça (j'ai réussi). Il est très intéressant d'observer la survenue de telles erreurs!

Questions ouvertes


Quand vous voyez de bons résultats qui peuvent être développés davantage, c'est inspirant. Comme je l'ai dit, si vous choisissez un nom approprié pour les fichiers source, alors tout ira comme sur des roulettes! Mais tout ce travail a été achevé en quelques jours, il est donc loin d'être exhaustif, et beaucoup a été fait complètement improvisé. Si j'ai assez de temps pour le développement de ces systèmes, cela vaut la peine de répondre à plusieurs questions.

Premièrement, quel post-traitement doit être fait avec les données afin qu'elles soient plus faciles à visualiser? Il sera difficile pour les gens de comprendre un réseau non traité de points et d'arêtes, mais si vous améliorez la description des données, cela rendra probablement difficile l'évaluation des zones praticables difficiles à première vue.

Deuxièmement, comment améliorer les modèles d'échantillonnage autour des frontières pour garantir que le nombre maximal de «trous» est trouvé? Existe-t-il de bons moyens de caractériser la réduction des chiffres en réseau et existe-t-il des schémas de pavage de haute qualité qui maximisent la probabilité de traverser et de traverser ces chiffres?

Troisièmement, quels modèles d'échantillonnage sont les meilleurs pour remplir les espaces - réguliers ou randomisés? Je peux facilement changer les critères de choix des points cibles pour créer des modèles plus aléatoires, mais il n'est pas très clair si cela en vaut la peine, et si oui, quels types de modèles aléatoires seront meilleurs.

Quatrièmement, quelles autres informations voulons-nous obtenir des cartes des zones praticables si nous avons déjà appris à les construire? Par exemple, il est très simple d'étendre un système existant avec des fonctions telles que la recherche de chemins ou de cartes de distance, afin que l'utilisateur puisse sélectionner un point et demander le chemin le plus court entre celui-ci et un autre point, ou voir une carte thermique de la distance entre un point et d'autres points de la carte. Ces requêtes seront-elles utiles? Quelles autres requêtes puis-je utiliser?

Pour le moment, les visualisations Walk Monster des zones praticables sont plus que suffisantes pour montrer que le code de mouvement du joueur est assez mauvais. J'avais prévu de passer à la création d'un système de test nocturne des cartes en utilisant la méthode de simulation de l'entrée utilisateur, mais il est évident que nous avons déjà suffisamment de problèmes à résoudre sans cette étape. Par conséquent, la prochaine étape sera d'augmenter la fiabilité du code de mouvement du joueur. Et pendant que je travaille là-dessus, je voudrais vérifier s'il est possible d'augmenter la vitesse d'exécution d'un ou deux ordres de grandeur, car alors que le travail de Walk Monster est très ralenti par le système de freinage des collisions.

Source: https://habr.com/ru/post/fr423519/


All Articles