JS battle: comment j'ai écrit mon eval ()

Vous pouvez vous souvenir d' Alexander Korotayev dans la version navigateur des «Héros de la puissance et de la magie»: le décryptage de son rapport à son sujet a recueilli une énorme quantité de vues sur Habré. Et maintenant, il a fait un jeu axé sur les programmeurs: vous devez y jouer avec un code JS.

Cette fois, le développement n'a pas pris des semaines, mais des semaines, mais sans défis intéressants de toute façon, il n'a pas pu le faire. Comment rendre le jeu pratique même pour les développeurs qui n'ont pas encore touché JavaScript? Comment vous protéger des moyens simples de déjouer un jeu?



En conséquence, Alexander a de nouveau fait un rapport sur HolyJS, et nous (les organisateurs de la conférence) avons à nouveau préparé une version texte pour Habr.


Mon nom est Alexander Korotaev, je travaille chez Tinkoff.ru, je suis engagé dans le front-end. De plus, en tant que membre de la communauté front-end Spb, j'aide à organiser les mitaps. Je fais le podcast Drinkcast, nous invitons des personnes intéressantes et discutons de divers sujets.

Quelle est l'essence du jouet? Tout d'abord, vous devez choisir une unité parmi celles proposées, c'est un tel système RPG: chaque unité a ses propres forces et faiblesses. Vous voyez quelles unités l'ennemi a choisies, et choisissez pour vous venger. Ensuite, vous devez écrire en JavaScript un script pour le comportement de votre armée - en d'autres termes, le script "ce que chaque unité doit faire sur le champ de bataille".

Cela se fait en mode débogage: en fait, vous débitez le code, puis les deux adversaires poussent leur code, et la bataille entre les deux côtés commence.

Ainsi, vous pouvez voir comment deux scripts, deux logiques, deux algorithmes s'affrontent. J'ai toujours voulu faire quelque chose comme ça, et maintenant c'est enfin implémenté.

En action, tout ressemble à ceci:


Et à quoi ressemblait le travail? Beaucoup de travail a été consacré à la documentation. Lorsque le joueur s'assoit devant l'ordinateur portable, il voit la documentation, dans laquelle tout est décrit assez en détail.

Il m'a fallu beaucoup de temps pour sa mise en page, sa révision et son interrogation auprès des gens pour savoir si elle était claire. En conséquence, cela s'est avéré clairement pour sishnikov, javists et autres développeurs qui ne connaissent rien de JS. Vous pouvez même promouvoir JavaScript avec ce jouet: "Ce n'est pas effrayant, voyez comment vous pouvez écrire dessus, même quelque chose d'amusant se produit."



Nous avons organisé un grand tournoi dans notre entreprise, auquel pratiquement tous les programmeurs auxquels nous avons participé.

De la technologie, j'ai utilisé le moteur de jeu le plus populaire du monde de JS - Phaser. L'éditeur Ace le plus grand et le plus utilisé. Il s'agit d'un éditeur sur le web, très similaire à Sublime ou VSCode, il peut être intégré dans une page web. J'ai également utilisé RxJS pour travailler avec des interactions asynchrones de différents utilisateurs et Preact pour rendre le html. Parmi les technologies natives, il a surtout travaillé avec les travailleurs et le websocket.





Jeux pour programmeurs


Quels sont les jeux pour les programmeurs en général? À mon avis, ce sont des jeux où vous devez coder, puis obtenir un résultat amusant qui peut être comparé à quelqu'un, c'est une bataille. Parmi ces jeux en ligne disponibles, je connais "Elevator Saga" - vous écrivez des scripts pour les ascenseurs en fonction de certains paramètres. «Screeps» - sur la biologie, les molécules, écrivez des scripts pour elles.

Il y a aussi des jouets qui sont parfois à des conférences. Le plus populaire d'entre eux est «Code in the Dark», nous l'avons également présenté aujourd'hui. Au fait, «Code in the dark» m'a inspiré à certains égards.



Pourquoi cela at-il été fait? J'ai eu la tâche dont vous avez besoin pour trouver quelque chose de cool avec un stand à la conférence, quelque chose d'inhabituel. Non pas qu'il y ait des eychars avec des questionnaires. Il est clair que tout le monde veut attirer l'attention et recueillir des contacts. Nous avons décidé d'aller plus loin et avons trouvé quelque chose de cool et amusant pour les programmeurs. J'ai réalisé que les programmeurs veulent se battre, rivaliser et nous devons leur donner une telle opportunité. Il faut créer un stand sur lequel ils viendront et coderont.

Gamification Nous l'avons fait non seulement parmi les programmeurs en exercice, mais aussi parmi les étudiants. Nous avons organisé de tels matchs dans les instituts le jour de la carrière. Nous devions en quelque sorte voir quel genre de gars il y avait, qui nous convenait ou non. Nous avons utilisé la gamification pour attirer les gens dans le processus, pour voir comment ils agissent, ce qu'ils font. Ils ont joué et ont été distraits, mais cela nous a donné des informations. Certains ont poussé du code sans même l'exécuter une seule fois, et il était immédiatement évident qu'il était trop tôt pour le développement.



À quoi cela ressemblait dans la première version. C'était un écran principal et deux ordinateurs portables pour les joueurs. Tout cela a contacté le serveur, le serveur a stocké l'état et l'a fouillé entre tous les clients connectés. Chaque écran était un client connecté. Les ordinateurs portables des joueurs étaient des écrans interactifs à partir desquels cet état pouvait être changé. Les écrans sont attachés de manière rigide à un serveur.

Manque de temps


La première histoire que j'ai rencontrée dans ce développement est l'histoire que j'ai eu très peu de temps. Littéralement en cinq minutes une idée est venue, en cinq secondes un nom a été inventé quand il a été nécessaire de créer un référentiel sur GitHub. Je pouvais tout dépenser quatre heures le soir, les éloigner même de ma femme. En conséquence, il ne me restait que trois semaines avant la conférence pour réaliser cela au moins d'une manière ou d'une autre. Tout a commencé de sorte qu'il était juste nécessaire de trouver une idée dans le cadre d'un brainstorming, en cinq minutes l'idée est née: «Écrivons une sorte d'intelligence artificielle pour RPG en JS». C'est cool, amusant, je peux l'implémenter.



Dans la première implémentation, l'écran avait un éditeur de code et un écran de bataille, sur lesquels se déroulait la bataille elle-même. Phaser, Ace Editor et pure Node.js ont été utilisés comme serveur sans framework. Ce que j'ai regretté plus tard, cependant, mais rien de spécial n'était requis du serveur.



J'ai réussi à réaliser alors Renderer, qui a peint la bataille elle-même. La partie la plus difficile était le bac à sable pour le code JS, c'est-à-dire un bac à sable dans lequel tout était censé être exécuté de manière isolée pour chaque joueur. Il y avait également un partage d'état qui provenait du serveur. Les joueurs ont en quelque sorte changé d'état, l'ont jeté sur le serveur, le serveur a envoyé le reste aux sockets Web. Le serveur était une source de vérité et tous les clients connectés faisaient confiance à ce qui provenait du serveur.

Sandbox


Qu'est-ce qui est si difficile à mettre en œuvre un bac à sable? Le fait est que le bac à sable est le monde entier pour le code dans lequel le code doit exister. Autrement dit, vous créez pour lui environ le même monde qu'autour, mais uniquement composé de quelques conventions, à partir d'une API avec laquelle vous pouvez interagir. Comment implémenter cela sur JS? Il semble que JS ne soit pas capable de cela, il est si plein de trous et gratuit qu'il ne fonctionnera tout simplement pas pour enfermer complètement le code utilisateur dans une boîte sans utiliser une machine virtuelle distincte avec un système d'exploitation distinct.



Que doit faire le bac à sable?

Tout d'abord, comme je l'ai dit, isolez le code. Ce doit être un monde dont on ne peut sortir.

De plus, l'API de gestion d'unité doit y être lancée. Les joueurs doivent interagir avec le champ de bataille, déplacer des unités, les diriger, leur donner un vecteur d'attaque.
Et toutes les actions des unités sont asynchrones, c'est-à-dire qu'elles devraient en quelque sorte fonctionner avec du code asynchrone.



Qu'est-ce que je voulais dire à propos de l'asynchronie? Le fait est que dans JS, il est essentiellement mis en œuvre à l'aide de promesses. Tout est clair pour tout le monde ici, les promesses sont une bonne chose, elles fonctionnent parfaitement, nous avons presque toujours été avec nous. Pendant de nombreuses années, tout le monde sait comment travailler avec eux, mais ce jouet n'était pas uniquement destiné aux javascripts. Imaginez si je commençais à expliquer aux Javists comment écrire un code de bataille en utilisant des promesses? Comment faire alors-alors-alors, pourquoi parfois ce n'est pas nécessaire ... Que faire des conditions ou des boucles?



Vous pouvez, bien sûr, emprunter la meilleure voie et prendre la syntaxe async / wait. [Diapositive 8:57] Mais pouvez-vous également imaginer comment les programmeurs non javascript peuvent expliquer que vous devez mettre en attente avant presque chaque ligne? Par conséquent, la meilleure façon de travailler avec l'asynchronie est de ne pas l'utiliser du tout.



Créez le code le plus synchrone et l'API la plus simple, semblable à presque tous les langages de programmation. Nous fabriquons un jouet non seulement pour les personnes qui écrivent en JS, nous voulons le rendre accessible à tous ceux qui savent coder.



Nous devons tous en quelque sorte commencer. L'utilisateur écrit le code, et nous devons l'exécuter, déplacer les unités sur la carte. La première chose qui me vient à l'esprit est que nous avons besoin de eval () plus la déclaration non recommandée avec, qui n'est pas recommandée pour une utilisation sur MDN . Cela fonctionnera, mais il y a des problèmes.



Par exemple, nous avons du code qui détruit complètement notre idée entière, ce qui nous empêche de faire quoi que ce soit d'autre. C'est du code qui bloque l'exécution. Quelque chose doit être fait pour que l'utilisateur ne puisse pas bloquer l'application. Par exemple, une boucle sans fin peut tout casser. Si alert () et prompt () peuvent encore être redéfinis, nous ne pouvons pas du tout redéfinir une boucle infinie.

eval () est mauvais


Nous arrivons donc au point où eval () est mauvais. Ce n'est pas pour rien qu'ils l'appellent le mal, car c'est une fonction insidieuse qui intègre en fait tout ce qui est le plus libre et ouvert qui soit dans JS, et nous laisse complètement sans défense. Avec une fonction simple, nous faisons un énorme trou dans notre application.



Mais si je vous dis, [dans la voix de Steve Jobs] que nous avons réinventé eval ()?

Nous avons fait eval () sur d'autres technologies, cela fonctionne presque de la même manière que le même eval () que nous avons déjà. En fait, j'ai une fonction eval () dans mon code, mais implémentée à l'aide de Workers, de l'instruction with et du proxy.



Pourquoi les travailleurs? Le fait est qu'ils créent un thread d'exécution distinct, c'est-à-dire que JS est un thread unique, mais grâce aux travailleurs, nous pouvons obtenir un autre thread. Cela nous donne de nombreux avantages. Par exemple, dans les mêmes boucles sans fin, nous pouvons interrompre le thread créé par le biais de worker à partir du thread principal, c'est peut-être la principale raison pour laquelle j'ai utilisé des travailleurs. Si le travailleur a réussi à travailler plus vite qu'en une seconde, on le considère comme réussi, on obtient son résultat. Sinon, nous le coupons. En fait, le code utilisateur pour une raison quelconque n'a pas fonctionné, des erreurs étranges se sont produites ou il a été ralenti en raison d'une boucle infinie. Beaucoup aujourd'hui ont essayé d'écrire alors que (vrai), j'ai averti que cela ne fonctionnerait pas.



Pour écrire notre travailleur, nous avons juste besoin de transmettre le script au constructeur du travailleur, qui sera téléchargé via http. Dans le script, nous devons créer un gestionnaire de messages à partir du thread principal. En utilisant la fonction postMessage () à l'intérieur de worker, nous pouvons router les messages vers le thread principal. De cette façon, nous faisons la communication entre les deux fils. Une API simple assez pratique, mais il manque quelque chose, à savoir le code utilisateur que nous devons exécuter dans ce travailleur. Nous ne générerons pas à chaque fois un fichier de script sur le serveur et le transmettrons au travailleur.



J'ai trouvé un moyen d'utiliser URL.createObjectURL (). Nous faisons un bloc et le nourrissons au travailleur src. Ainsi, il décharge notre code directement de la ligne. Soit dit en passant, cela fonctionne avec tous les objets dans le DOM qui ont src - image fonctionne comme ceci, par exemple, et même dans un iframe, vous pouvez charger du HTML simplement en le générant à partir d'une chaîne. Assez cool et flexible, je pense. Nous pouvons également gérer le travailleur en lui passant simplement notre objet spécialement généré à partir de l'URL. Nous pouvons également y mettre fin et cela fonctionne déjà selon nos besoins, et nous avons créé le premier bac à sable.



Les interactions asynchrones vont plus loin, car tout travail avec des travailleurs est asynchrone. Nous avons envoyé un message et nous ne pouvons pas attendre le message suivant de manière synchrone, le travailleur ne nous renvoie que l'instance et nous pouvons nous abonner aux messages. Nous interceptons le message à l'aide de RxJS, nous créons deux threads: l'un pour un message réussi du travailleur, le second pour l'achèvement par timeout. Deux threads que nous contrôlons ensuite avec leur fusion.



RxJS a des opérateurs qui nous permettent de travailler avec des threads. En fait, c'est comme lodash pour les opérations synchrones. Nous pouvons indiquer une fonction et ne pas penser à la façon dont elle est implémentée à l'intérieur, cela nous soulage d'un mal de tête. Nous devons commencer à penser dans les threads, l'opérateur de fusion fusionne nos threads, répond à tout message. Il répondra à la fois au délai d'expiration et au message. Nous n'avons besoin que du tout premier message, respectivement, après le premier message que nous terminons travailleur. En cas d'erreur, imprimez cette erreur; en cas de succès, nous résolvons.



Tout est assez simple ici. Notre code devient déclaratif, la complexité de l'asynchronie va quelque part. L'essentiel est d'apprendre ces opérateurs.



C'est ainsi que nous travaillons avec l'API Unit. Je voulais que l'API Unit soit aussi simple que possible. En parlant de JS, beaucoup de gens pensent que c'est difficile, il faut grimper quelque part, apprendre quelque chose. Et je voulais le rendre aussi simple que possible: tout dans le domaine global, il n'y a que la portée de l'API Unit, rien de plus. Tout pour la gestion des unités, même la saisie semi-automatique.



[Diapositive 15:20] La solution suggère que tout cela peut être poussé dans le très interdit avec déclaration. Comprenons pourquoi c'est interdit.

Le fait est qu'avec a ses problèmes. Par exemple, avec, malheureusement, fuit en dehors de la portée que nous y avons jetée, car il essaie de regarder plus profondément que l'API Unit et regarde dans la portée globale.


Ici, le dernier exemple est particulièrement cool, car même un quatre peut être dangereux pour notre code, car toutes ces fonctions peuvent être effectuées par le code utilisateur. L'utilisateur peut tout faire. C'est un jeu pour les programmeurs, et ils aiment explorer les problèmes et les moyens de pirater quelque chose.



Comme je l'ai dit, les étendues sont très fuyantes, donc une étendue globale est toujours disponible. Quel que soit le nombre d'étendues que nous délimitons sur notre code personnalisé, quel que soit le nombre d'étendues que nous enveloppons, l'étendue globale sera toujours visible. Et tout cela grâce à.

En fait, il n'isole rien, il nous ajoute juste une nouvelle couche d'abstraction, une nouvelle portée globale. Mais nous pouvons changer ce comportement avec Proxy.



Le fait est que Proxy s'occupe de tous nos appels à l'objet qui sont mandatés via la nouvelle API, et nous pouvons contrôler le comportement des nouvelles demandes de données dans cet objet.



En fait, avec des œuvres tout simplement. Lorsque nous lui fournissons une sorte de variable, il vérifie sous le capot si cette variable est dans l'objet (c'est-à-dire qu'elle exécute l'opérateur in), et si c'est le cas, elle l'exécute dans l'objet, et sinon, elle s'exécute dans la portée supérieure, dans Notre cas est mondial. C'est assez simple ici. La principale chose que le proxy nous aide est que nous pouvons remplacer ce comportement.



Il existe des crochets dans Proxy. Une chose merveilleuse qui nous permet de proxy toutes les demandes à l'objet. Nous pouvons changer le comportement de la demande d'attribut, changer le comportement de la tâche d'attribut et, surtout, nous pouvons changer le comportement de ceci dans l'opérateur. Il y a un hameçon, auquel on ne peut retourner que vrai. Ainsi, nous prenons et trompons complètement notre déclaration with, ce qui rend notre API beaucoup plus sécurisée qu'auparavant.



Si nous essayons d'exécuter eval (), il demandera d'abord si cet eval () est dans unitApi, ils répondront «oui» et obtiendront «undefined n'est pas une fonction». Cela semble être la première fois que je suis satisfait de cette erreur! Cette erreur est exactement ce que nous aurions dû recevoir. Nous l'avons pris et avons dit à l'utilisateur: "Désolé, oubliez tout ce que vous saviez sur l'objet fenêtre, ce n'est plus." Nous avons déjà laissé certains des problèmes, mais ce n’est pas tout.



Le fait est que l'instruction with est tout de même de JS, JS est dynamique et un peu bizarre. Ce qui est étrange, c'est que tout ne fonctionne pas comme nous le souhaiterions, sans examiner les spécifications. Le fait est qu'avec fonctionne également avec les propriétés du prototype. Autrement dit, nous pouvons ringard lui nourrir un tableau, exécuter ce code obscur. Toutes les fonctions de tableau sont disponibles comme globales dans cette portée, ce qui semble un peu étrange.



Ce n'est pas important pour nous, il est important pour nous que l'utilisateur puisse exécuter valueOf () et obtenir tout notre sandbox. Prenez et ramassez directement, voyez ce qu'il contient. Je ne le voulais pas non plus, donc une chose intéressante a été introduite dans la spécification: Symbol.unscopables. Autrement dit, dans la nouvelle spécification pour les symboles, Symbol.unscopables a été introduit spécifiquement pour l'instruction with, ce qui est interdit. Parce qu'ils croient que quelqu'un d'autre l'utilise. Par exemple, moi!



Ainsi, nous allons faire un autre intercepteur, où nous vérifierons spécifiquement si ce symbole est dans la liste de tous les attributs non extensibles. Sinon, retournez-le, mais si oui, alors désolé, nous ne reviendrons pas. Nous ne l'utilisons pas non plus. Et donc, avec nous ne pouvons même pas obtenir un prototype de notre bac à sable.



Nous avons toujours l'environnement Worker. C'est quelque chose qui se bloque dans une zone globale et qui est toujours accessible. Le fait est que si vous remplacez simplement cela, il sera disponible dans le prototype. Presque tout peut être retiré via le prototype de JS. Étonnamment, toutes ces méthodes sont toujours disponibles via le prototype.



Je devais juste prendre et nettoyer tout ça. Nous passons en revue toutes les clés et nettoyons tout.



Et puis nous laissons un petit œuf de Pâques à l'utilisateur, qui essaie toujours d'appeler cela. Nous prenons la fonction habituelle, l'essentiel n'est pas la fonction flèche, qui a une portée, et changeons sa portée en notre objet, dans lequel nous laissons un petit œuf de Pâques pour un utilisateur particulièrement curieux qui veut afficher ceci ou lui-même dans la console. Je pense que les œufs de Pâques sont merveilleux et devraient être laissés dans le code.



Il s'avère en outre qu'ils ne sont restés qu'avec notre API d'unité. Nous avons tout bloqué complètement - en fait, nous avons laissé une liste blanche. Nous devons ajouter les API utiles et nécessaires. Par exemple, l'API Math, qui a une fonction aléatoire utile que de nombreuses personnes utilisent lors de l'écriture de code pour les unités.

Nous avons également besoin d'une console et de nombreuses autres fonctions utilitaires qui n'ont aucune fonction destructrice. Nous créons une liste de temps pour nos API. , blacklist, , .



whitelist, try-catch . , .



, Worker . , worker, , « , » . , JavaScript , .

, , - . , . , webpack .



patchMethod(), , postMessage(). postMessage() console log, error, warn, info. , . , <div>, , , , , .



, . : - - — , , - . , , promises. , actions, .



actions. , real-time ? , real-time workers , worker, . - , . , , . , , . .



workers, . workers , . , . , , ( , ), . : — .


Math.random()


, , , . Math.random().

, , , . , Math.random() - .



, , ( ), JS , . .



, , , . , - .

, . , random(), .



, random() — « » , . , - , random() , . - , . , , random() .



. , , random() . - seed ( , , ).


, . , random(). , random() -.

, worker, , JS . «» — . . , JS . random() unit API. , worker.



State sharing: RxJS,


Nous avons donc compris ce que nous avons avec le client. Parlons maintenant du partage de l'État, pourquoi cela est nécessaire et comment il a été organisé. Nous avons l'état stocké sur le serveur, le serveur doit le tâtonner avec les clients connectés. [Diapositive 28:48]

Nous avons quatre rôles de clients différents qui peuvent se connecter au serveur: «utilisateur gauche», «utilisateur droit», un visualiseur qui regarde l'écran principal et un administrateur qui peut tout faire.

L'écran de gauche ne peut pas changer l'état du bon joueur, le spectateur ne peut rien changer et l'administrateur peut tout faire.



Pourquoi était-ce un problème? Tout est arrangé simplement. Tout client connecté peut lancer une session, le serveur l'accepte et la fusionne avec state, qui se trouve à l'intérieur du serveur, puis la distribue à tous les clients. Il tâtonne pour tous les changements qui lui viennent. Il fallait en quelque sorte le filtrer.



Tout d'abord, je dirai pourquoi le serveur a également RxJS. Toutes les interactions avec deux ou plusieurs utilisateurs connectés deviennent asynchrones. Il faut attendre les résultats des deux utilisateurs. Par exemple, les deux utilisateurs ont cliqué sur le bouton "Terminer", vous devez attendre que les deux cliquent, puis effectuer l'action. C'était assez simple sur RxJS de cette façon:



Nous fonctionnons à nouveau avec des threads, il y a un flux depuis le socket, qui est appelé socket. Pour créer un thread qui ne surveille que le joueur de gauche, nous prenons et filtrons simplement les messages de cette prise par le joueur de gauche (et de même avec le droit). Ensuite, nous pouvons les combiner en utilisant l'opérateur forkJoin (), qui fonctionne comme Promise.all () et est son analogue. Nous attendons ces deux actions et appelons la méthode setState (), qui définit notre état sur «prêt». Il s'avère que nous attendons les deux joueurs et changeons l'état du serveur. Sur RxJS, cela sort aussi déclaratif que possible, c'est pourquoi je l'ai utilisé.

Il reste un problème avec le fait que les joueurs peuvent changer d'état entre eux. Il faut leur interdire de le faire. Pourtant, ce sont des programmeurs, il y avait des précédents que quelqu'un essayait. Créons pour eux des classes distinctes héritées du client.



Ils auront la logique de base de la communication du joueur avec le serveur, et dans chaque classe particulière, il y aura sa logique personnalisée pour filtrer les données.

Le client est en fait un pool de connexions, des connexions avec des clients.



Il les stocke simplement et il a le flux onUnsafeMessage, qui est complètement dangereux: il ne peut pas lui faire confiance, ce ne sont que des messages bruts de l'utilisateur qu'il reçoit. Nous écrivons ces messages bruts dans le flux.

De plus, lors de la mise en œuvre d'un lecteur spécifique, nous prenons ceci surUnsafeMessage et le filtrons.



Nous devons filtrer uniquement les données que nous pouvons obtenir de ce joueur, en qui nous pouvons avoir confiance. Le joueur de gauche ne peut changer que l'état du joueur de gauche, respectivement, nous prenons de toutes les données qu'il pourrait envoyer, uniquement l'état du joueur de gauche. Si vous ne l'avez pas envoyé, d'accord. Si envoyé - nous prenons. Ainsi, à partir de messages complètement dangereux, nous obtenons des messages sûrs auxquels nous pouvons faire confiance lorsque nous travaillons dans la pièce.



Nous avons des salles de jeux qui réunissent les joueurs. À l'intérieur de la salle, nous pouvons écrire les fonctions mêmes qui peuvent changer d'état directement, simplement en souscrivant à ces flux, auxquels nous pouvons déjà faire confiance. Nous avons résumé un tas de chèques. Nous avons effectué les vérifications en fonction des rôles et les avons appelés classes distinctes. Nous avons divisé le code de telle manière qu'à l'intérieur du contrôleur, où les fonctions importantes de changement d'état sont exécutées, le code est devenu aussi simple et déclaratif que possible.

RxJS est également utilisé sur le client, il est connecté à la prise au verso, émet des événements et les redirige de toutes les manières.

Dans ce cas, je voudrais faire un exemple lorsque je dois changer l'armée de l'adversaire droit.



Pour y souscrire, nous créons un flux à partir de la même socket et le filtrons. Nous nous assurons que c'est vraiment le bon joueur, et prenons un message de lui sur ce qu'est son armée. S'il n'y a pas un tel message, le flux ne renverra rien, il n'y aura pas un seul message, il restera silencieux jusqu'à ce que le joueur change d'armée. Nous résolvons immédiatement de manière déclarative le problème du filtrage des événements, nous ne l'avons pas ringard.

Et quand quelque chose est déjà venu du flux, nous appelons la fonction setState (). C'est assez simple, l'approche même qui nous permet de tout faire de manière transparente et déclarative. La chose même pour laquelle j'ai pris le projet RxJS et en quoi cela m'a beaucoup aidé.



Je crée des flux que j'ai clairement nommés, avec lesquels je comprends comment travailler, tout est déclaratif, les fonctions nécessaires sont appelées, il n'y a pas de problème avec beaucoup d'événements if et de filtrage, tout cela est fait par RxJS.

Histoire: du solo au multijoueur



Donc, la première version de mon jouet a été écrite. On peut dire que c'était un seul joueur, car seulement deux joueurs pouvaient y jouer, le nombre de clients connectés était fixe. Nous avions un joueur de gauche, un joueur de droite et l'écran derrière, le tout connecté directement au serveur. Tout était enduit dur, mais en trois semaines, cela a été fait.

J'ai reçu une nouvelle offre: étendre le jouet à tous les programmeurs de l'entreprise afin qu'ils puissent l'ouvrir et le jouer sur leurs ordinateurs. Pour que nous obtenions une liste de leaders, multijoueurs afin qu'ils puissent jouer ensemble. Puis j'ai réalisé que j'avais beaucoup de refactoring.



Ce ne fut pas si difficile. J'ai simplement combiné toutes les entités que j'avais dans des pièces séparées. J'ai eu l'essence de "Room", qui pouvait combiner tous les rôles. Ce ne sont plus les joueurs eux-mêmes qui communiquent directement avec le serveur, mais les salles. Les chambres ont déjà envoyé des requêtes par proxy directement au serveur, remplaçant l'état, et l'état est devenu distinct pour chaque chambre.



J'ai tout pris et réécrit, ajouté une liste de leaders, nous avons décerné les meilleurs prix. Il fallait juste avoir un grand nombre d'utilisateurs, il était déjà impossible de suivre tout le monde, il fallait écrire quelque chose où collecter toutes les données.

JS Gamedev et ses problèmes



Ainsi, je me suis familiarisé plus sérieusement avec le JS-gamedev. Je me suis dandiné sur le dernier projet pendant environ trois ans, me reposant périodiquement. Et ici, j'ai eu les deux fois pendant trois semaines. Chaque jour, je m'asseyais et faisais quelque chose le soir.

Quels sont les problèmes de développement de jeux sur JS? Tout est différent de nos applications d'entreprise, où il n'est pas difficile d'écrire quelque chose à partir de zéro. De plus, beaucoup de choses sont même les bienvenues: nous ferons notre propre truc, nous souviendrons des histoires avec NPM et nous laisserons vous-même.



Il est impossible de le faire dans JS Gamedev, car toutes les technologies d'affichage des graphiques sont si bas qu'il est économiquement peu rentable d'écrire quelque chose dessus. Si je prenais ce jouet et commençais à l'écrire à partir de zéro sur WebGL, je m'asseyais également derrière lui pendant environ six mois, essayant juste de découvrir quelques bugs étranges. Le moteur de jeu le plus populaire Phaser m'a enlevé ces problèmes ...



... et m'en a ajouté de nouveaux: 5 mégaoctets dans un bundle. Et rien ne pouvait être fait à ce sujet; il ne sait pas du tout ce qu'est le tremblement des arbres. De plus, seule la dernière version de Phaser peut fonctionner avec le pack Web et les bundles. Avant cela, Phaser n'était connecté que dans la balise html du script, c'était étrange pour moi.

Je viens de toutes sortes de scripts webpacks, et dans le développement de jeux JS, presque rien ne peut le faire. Tous les modules ont un typage extrêmement médiocre ou ne l'ont pas du tout, ou ne savent pas comment utiliser le webpack, il a fallu trouver des moyens de le boucler. Il s'est avéré que même Ace Editor dans sa forme pure ne fonctionne pas du tout avec webpack. Pour commencer à travailler, vous devez télécharger un package séparé où il est déjà enveloppé (accolade).

C'était à peu près la même chose avec Phaser, mais dans la nouvelle version, ils l'ont fait plus ou moins normalement. J'ai continué à écrire sur Phaser et j'ai trouvé comment faire en sorte que tout fonctionne avec webpack comme nous le faisions auparavant: à la fois la frappe et les tests pouvaient être associés à tout cela. J'ai trouvé que vous pouvez prendre PixiJS séparément, qui est un rendu de webpack, et y trouver de nombreux modules prêts à fonctionner avec.



PixiJS est une excellente bibliothèque qui peut être rendue sur WebGL ou sur Canvas. De plus, vous pouvez même écrire du code comme pour Canvas, et il sera rendu dans WebGL. Cette bibliothèque peut rendre 2D très rapidement. L'essentiel est de savoir comment cela fonctionne avec la mémoire, afin de ne pas tomber dans une position lorsque la mémoire est terminée.

Je recommande séparément le référentiel awesome-pixijs sur GitHub, où vous pouvez trouver différents modules. J'ai surtout aimé React-pixi . Nous pouvons simplement ignorer la résolution de problèmes avec la vue lorsque nous écrivons des fonctions impératives directement dans le contrôleur pour dessiner des formes géométriques, des sprites, des animations, etc. Nous pouvons tous annoter dans JSX. Nous venons du monde JSX avec notre application métier et pouvons les utiliser plus avant. C'est pour cela que j'aime l'abstraction. React-pixi nous donne cette abstraction familière.

Je vous conseille également de prendre tween.js - le même célèbre moteur d'animation de Phaser, qui vous permet de faire des animations déclaratives qui sont quelque peu similaires aux animations CSS: nous faisons une transition entre les états, et tween.js décide pour nous exactement comment déplacer l'objet.

Types de joueurs: qui ils sont et comment se lier d'amitié avec eux


J'ai rencontré différents joueurs et je voudrais également vous parler du test du jouet. J'ai rassemblé des collègues dans une pièce fermée et je ne les ai pas laissés sortir jusqu'à la fin du match. Malheureusement, tout le monde n'a pas pu terminer le jeu, car au tout début, j'avais beaucoup de bugs. Heureusement, j'ai commencé les tests dès qu'au moins un prototype fonctionnel est apparu. Honnêtement, le premier test a échoué car quelques joueurs n'ont rien commencé. C'était dommage, mais ça m'a donné un coup de pied qui m'a permis d'avancer.

Lorsque votre jouet est prêt, vous pouvez très bien être reçu, ou avec une fourche et des torches. Tout le monde attend une sorte de fan des jeux, attendant le bonheur que vous leur donnerez. Et vous leur donnez quelque chose qui ne fonctionne pas du tout, même si cela semble fonctionner pour vous. Lorsque vous avez un jouet en ligne, il y a encore plus de tels bugs.

En conséquence, les personnes les plus agréables que j'ai rencontrées sont des «chercheurs» qui trouvent toujours plus dans votre jouet qu'ils ne le sont vraiment. Ils peuvent agréablement le compléter avec toutes sortes de petites choses, vous invitant à ajouter quelque chose. Mais, malheureusement, la communication avec ces personnes n'a pas donné la chose importante - la stabilité du jouet.

Il y a des joueurs ordinaires qui viennent uniquement pour le plaisir du fan. Parfois, ils ne remarquent même pas les insectes, les glissant d'une manière ou d'une autre sur leur chemin vers le plaisir.

Les collecteurs de bogues sont une autre catégorie, pour laquelle presque tout ne fonctionne pas. Vous devez être amis avec de telles personnes, bien qu'elles parlent beaucoup de négativité. Nous devons nouer d'étranges relations avec eux: ils vous font du mal et vous essayez de leur retirer quelque chose d'utile, «asseyons-nous devant votre ordinateur et voyons». Vous devez travailler avec eux, car ce sont ces personnes qui rendront votre jeu de qualité.

Vous devez tester uniquement sur des personnes vivantes. Votre œil est flou et les tests montreront certainement ce qui est caché. Vous développez un jouet et plongez plus profondément, en sciant certaines fonctionnalités, mais elles peuvent même ne pas être nécessaires. Vous allez directement à vos consommateurs, leur montrez et regardez comment ils jouent, sur quelles touches ils appuient. Cela vous incite à faire exactement ce dont vous avez besoin. Vous voyez que certaines personnes appuient constamment sur Ctrl + S, car elles sont habituées à enregistrer le code - enfin, au moins faire fonctionner le code sur Ctrl + S, le joueur se sentira plus à l'aise. Vous devez créer un environnement confortable pour le joueur, pour cela, vous devez l'aimer et le suivre.

La règle 80/20 fonctionne: vous faites une démo 20% du temps à partir du développement complet du jeu, et pour le joueur, cela ressemble à un jeu terminé à 80%. La perception fonctionne pour que les mécanismes de base soient prêts, tout bouge et fonctionne, ce qui signifie que le jeu est presque prêt et que le développeur le terminera bientôt. Mais en réalité, le développeur a encore une sortie de 80%. Comme je l'ai dit, j'ai dû longtemps travailler sur la documentation pour qu'elle soit compréhensible par tous. Je l'ai montré à beaucoup de gens qui ont parlé de leurs commentaires, je les ai filtrés, essayant de comprendre l'essence des déclarations. Et il m'a fallu beaucoup de temps pour rechercher des bogues.

Donc, dans le développement de jeux, je ne pouvais que vous conseiller de faire des démos: elles ravissent tout le monde, ne nécessitent pas beaucoup de temps, et personne n'attend vraiment quoi que ce soit des démos. Terminer les jeux est un processus ennuyeux, mais commencer est super.

Enfin, je vous laisse des liens:

HolyJS 2019 Piter , une conférence pour les développeurs JavaScript, se tiendra du 24 au 25 mai à Saint-Pétersbourg. Les premiers intervenants sont déjà apparus sur le site.
Vous pouvez également demander un rapport, l' appel à communications est ouvert jusqu'au 11 mars.
Le prix des billets augmentera le 1er février.

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


All Articles