JS battle: como escribí mi eval ()

Puedes recordar a Alexander Korotayev en la versión del navegador de "Heroes of Might and Magic": el descifrado de su informe sobre ella reunió una gran cantidad de puntos de vista sobre Habré. Y ahora hizo un juego centrado en los programadores: debes jugarlo con un código JS.

Esta vez, el desarrollo no llevó semanas, sino semanas, pero sin desafíos interesantes de todos modos, no podría funcionar. ¿Cómo hacer que el juego sea conveniente incluso para desarrolladores que no hayan tocado JavaScript anteriormente? ¿Cómo protegerse de formas simples de burlar un juego?



Como resultado, Alexander nuevamente hizo un informe sobre HolyJS, y nosotros (los organizadores de la conferencia) nuevamente preparamos una versión de texto para Habr.


Mi nombre es Alexander Korotaev, trabajo en Tinkoff.ru, estoy comprometido en el front-end. Además, como parte de la comunidad Spb-frontend, ayudo a organizar mitaps. Hago el podcast de Drinkcast, invitamos a personas interesantes y discutimos varios temas.

¿Cuál es la esencia del juguete? Primero, debe elegir una unidad de las propuestas, este es un sistema RPG: cada unidad tiene sus propias fortalezas y debilidades. Usted ve qué unidades eligió el enemigo y elige vengarse de él. Luego, debes escribir en JavaScript un script para el comportamiento de tu ejército; en otras palabras, el script "lo que cada unidad debe hacer en el campo de batalla".

Esto se hace en modo de depuración: de hecho, debitas el código, y luego ambos oponentes empujan su código, y comienza la batalla entre las dos partes.

Por lo tanto, puede ver cómo dos scripts, dos lógicas, dos algoritmos luchan entre sí. Siempre quise hacer algo como esto, y ahora finalmente se implementa.

En acción, todo se ve así:


¿Y cómo era el trabajo en él? Se dedicó mucho trabajo a la documentación. Cuando un jugador se sienta en una computadora portátil, ve la documentación, en la que todo se describe con cierto detalle.

Me tomó mucho tiempo para su diseño, revisión y cuestionamiento entre las personas si es claro. Como resultado, resultó claramente para sishnikov, javists y otros desarrolladores que no saben nada sobre JS. Incluso puedes promocionar JavaScript con este juguete: "No da miedo, mira cómo puedes escribir en él, incluso sucede algo divertido".



Tuvimos un gran torneo en nuestra empresa, en el que prácticamente cualquier programador en el que hemos participado.

Desde el punto de vista tecnológico, utilicé el motor de juegos más popular del mundo de JS: Phaser. El Ace Editor más grande y más utilizado. Este es un editor en la web, muy similar a Sublime o VSCode, se puede incrustar en una página web. También usé RxJS para trabajar con interacciones asincrónicas de diferentes usuarios y Preact para renderizar html. De las tecnologías nativas, trabajó especialmente con trabajadores y websocket.





Juegos para programadores


¿Qué son los juegos para programadores en general? En mi opinión, estos son juegos en los que necesitas codificar, luego obtener un resultado divertido que se pueda comparar con alguien, esta es una batalla. De estos juegos en línea disponibles, sé "Elevator Saga" : escribes guiones para ascensores de acuerdo con ciertos parámetros. "Screeps" - sobre biología, moléculas, escribir guiones para ellos.

También hay juguetes que a veces se encuentran en conferencias. El más popular de ellos es "Code in the Dark", también lo presentamos hoy. Por cierto, "Código en la oscuridad" me inspiró de alguna manera.



¿Por qué se hizo esto? Obtuve la tarea de que necesitas crear algo genial con un stand en la conferencia, algo inusual. No es que hubiera eychars con cuestionarios. Está claro que todos quieren llamar la atención y recolectar contactos. Decidimos ir más allá y se nos ocurrió algo genial y divertido para los programadores. Me di cuenta de que los programadores quieren pelear, competir, y tenemos que darles esa oportunidad. Es necesario crear un stand en el que vendrán y codificarán.

Gamificación Hicimos esto no solo entre los programadores practicantes, sino también entre los estudiantes. Celebramos tales partidos en los institutos el día de la carrera. Necesitábamos ver de algún modo qué tipo de tipos había, adecuados para nosotros o no. Utilizamos la gamificación para atraer a las personas al proceso, para ver cómo actúan, qué hacen. Jugaron y se distrajeron, pero eso nos dio información. Algunos empujaron el código sin siquiera ejecutarlo una vez, y fue inmediatamente obvio que era demasiado temprano para el desarrollo.



Cómo se veía en la primera versión. Era una pantalla principal y dos computadoras portátiles para jugadores. Todo esto contactó al servidor, el servidor almacenó el estado y lo revolvió entre todos los clientes conectados. Cada pantalla era un cliente conectado. Las computadoras portátiles de los jugadores eran pantallas interactivas desde las cuales se podía cambiar este estado. Las pantallas están unidas rígidamente a un servidor.

Falta de historia del tiempo


La primera historia que encontré en este desarrollo es la historia que tuve muy poco tiempo. Literalmente, en cinco minutos surgió una idea, en cinco segundos se inventó un nombre cuando se requería crear un repositorio en GitHub. Podría pasarlo todo solo cuatro horas por la noche, quitándoselo incluso a mi esposa. Como resultado, solo me quedaban tres semanas antes de la conferencia para darme cuenta de esto, al menos de alguna manera. Todo comenzó para que fuera necesario tener una idea en el marco de una lluvia de ideas, en cinco minutos nació la idea: "Escribamos algún tipo de inteligencia artificial para RPG en JS". Es genial, divertido, puedo implementarlo.



En la primera implementación, la pantalla tenía un editor de código y una pantalla de batalla, en la cual estaba la batalla misma. Phaser, Ace Editor y Node.js puro se utilizaron como servidor sin ningún marco. De lo que me arrepentí más tarde, sin embargo, pero luego no se requirió nada especial del servidor.



Logré darme cuenta entonces de Renderer, quien pintó la batalla en sí. La parte más difícil fue la caja de arena para el código JS, es decir, una caja de arena en la que se suponía que todo debía ejecutarse de forma aislada para cada jugador. También hubo intercambio de estado que vino del servidor. Los jugadores de alguna manera cambiaron de estado, lo arrojaron al servidor, el servidor envió el resto a los enchufes web. El servidor era una fuente de verdad, y todos los clientes conectados confiaban en lo que provenía del servidor.

Caja de arena


¿Qué es tan difícil de implementar un sandbox? El hecho es que el sandbox es el mundo entero para el código en el que debe existir el código. Es decir, usted crea para él aproximadamente el mismo mundo que a su alrededor, pero solo consiste en algunas convenciones, a partir de una API con la que puede interactuar. ¿Cómo implementar esto en JS? Parece que JS no es capaz de esto, está tan lleno de agujeros y es tan libre que simplemente no funcionará encerrar completamente el código de usuario en una caja sin usar una máquina virtual separada con un sistema operativo separado.



¿Qué debe hacer el sandbox?

Primero, como dije, aísle el código. Debe ser un mundo del que no se pueda salir.

Además, la API de administración de la unidad debe arrojarse allí. Los jugadores deben interactuar con el campo de batalla, moviendo unidades, dirigiéndolos, dándoles un vector de ataque.
Y cualquier acción de las unidades es asíncrona, es decir, de alguna manera debería funcionar con código asíncrono.



¿Qué quería decir sobre la asincronía? El hecho es que en JS se implementa básicamente mediante promesas. Aquí todo está claro para todos, las promesas son una gran cosa, funcionan perfectamente, casi siempre hemos estado con nosotros. Durante muchos años, todos saben cómo trabajar con ellos, pero este juguete no fue solo para javascripts. ¿Imagínese si comenzara a explicar a los javistas cómo escribir un código de batalla usando promesas? Cómo hacer entonces, luego, entonces, por qué a veces no es necesario ... ¿Qué hacer con las condiciones o los bucles?



Puede, por supuesto, ir de la mejor manera y tomar la sintaxis async / wait. [Diapositiva 8:57] Pero, ¿se imaginan también cómo los programadores que no utilizan JavaScript pueden explicar que deben esperar antes de casi todas las líneas? Como resultado, la mejor manera de trabajar con la asincronía es no usarla en absoluto.



Cree el código más sincrónico y la API más simple, similar a casi cualquier lenguaje de programación. Hacemos un juguete no solo para las personas que escriben en JS, queremos que sea accesible para todos los que sepan codificar.



Todos necesitamos comenzar de alguna manera. El usuario escribe el código, y necesitamos ejecutarlo, mover las unidades en el mapa. Lo primero que viene a la mente es que necesitamos eval () más la declaración no recomendada con, que no se recomienda para usar en MDN . Esto funcionará, pero hay problemas.



Por ejemplo, tenemos un código que destruye completamente nuestra idea, lo que nos impide hacer algo más. Este es un código que bloquea la ejecución. Es necesario hacer algo para que el usuario no pueda bloquear la aplicación. Por ejemplo, un bucle sin fin puede romperlo todo. Si alert () y prompt () aún pueden redefinirse, entonces no podemos redefinir un bucle infinito en absoluto.

eval () es malvado


Entonces llegamos al punto donde eval () es malo. No es de extrañar que se llame maldad, porque es una función insidiosa que realmente incorpora todo lo más libre y abierto que hay en JS, y nos deja completamente indefensos. Con una función simple, hacemos un gran agujero en nuestra aplicación.



Pero, ¿y si te digo, [en la voz de Steve Jobs] que reinventamos eval ()?

Evaluamos () en otras tecnologías, funciona casi igual que la evaluación () que ya tenemos. De hecho, tengo una función eval () en mi código, pero implementada usando Workers, la instrucción with y Proxy.



¿Por qué trabajadores? El hecho es que crean un hilo de ejecución separado, es decir, JS es de un solo hilo, pero gracias a los trabajadores podemos obtener otro hilo. Esto nos da muchos beneficios. Por ejemplo, dentro de los mismos bucles sin fin, podemos interrumpir el hilo creado a través del trabajador desde el hilo principal, tal vez esta es la razón principal por la que utilicé trabajadores. Si el trabajador logró trabajar más rápido que en un segundo, lo consideramos exitoso, obtenemos su resultado. Si no, entonces lo cortamos. De hecho, el código de usuario por alguna razón no funcionó, se produjeron algunos errores extraños o se ralentizó debido a un bucle infinito. Muchos trataron de escribir hoy mientras (cierto), advertí que esto no funcionaría.



Para escribir a nuestro trabajador, solo necesitamos alimentar el script al constructor del trabajador, que se descargará a través de http. Dentro del guión, necesitamos hacer un manejador de mensajes desde el hilo principal. Usando la función postMessage () dentro del trabajador, podemos enrutar mensajes al hilo principal. De esta manera hacemos comunicación entre los dos hilos. Una API simple bastante conveniente, pero falta algo, es decir, el código de usuario que debemos ejecutar en este trabajador. Cada vez no generaremos un archivo de script en el servidor y lo enviaremos al trabajador.



Encontré una manera usando URL.createObjectURL (). Hacemos un bloque y lo alimentamos al trabajador src. Por lo tanto, descarga nuestro código directamente de la línea. Por cierto, de esta manera funciona con cualquier objeto en el DOM que tenga src - image funciona así, por ejemplo, e incluso en un iframe puede cargar html simplemente generándolo desde una cadena. Bastante genial y flexible, creo. También podemos gestionar al trabajador simplemente pasándole nuestro objeto especialmente generado desde la URL. También podemos terminarlo y ya funciona como lo necesitamos, y creamos el primer sandbox.



Las interacciones asincrónicas van más allá, porque cualquier trabajo con trabajadores es asincrónico. Enviamos un mensaje, y no podemos esperar sincrónicamente el siguiente mensaje, el trabajador solo nos devuelve la instancia y podemos suscribirnos a los mensajes. Capturamos el mensaje usando RxJS, creamos dos hilos: uno para un mensaje exitoso del trabajador, el segundo para completar por tiempo de espera. Dos hilos que luego controlamos con su fusión.



RxJS tiene operadores que nos permiten trabajar con hilos. De hecho, es como lodash para operaciones sincrónicas. Podemos indicar alguna función y no pensar en cómo se implementa en el interior, nos alivia del dolor de cabeza. Necesitamos comenzar a pensar en hilos, el operador de fusión fusiona nuestros hilos, responde a cualquier mensaje. Responderá tanto al tiempo de espera como al mensaje. Solo necesitamos el primer mensaje, respectivamente, después del primer mensaje que terminamos con el trabajador. En caso de error, imprima este error; en caso de éxito, lo resolvemos.



Aquí todo es bastante simple. Nuestro código se vuelve declarativo, la complejidad de la asincronía va a alguna parte. Lo principal es aprender estos operadores.



Así es como trabajamos con la API de la Unidad. Quería que la API de la unidad fuera lo más simple posible. Hablando de JS, muchas personas piensan que es difícil, hay que subir a algún lado, aprender algo. Y quería hacerlo lo más simple posible: todo en el área global, solo existe el alcance de la API de la Unidad, nada más. Todo para la gestión de la unidad, incluso autocompletar.



[Diapositiva 15:20] La solución sugiere que todo esto puede ser arrojado a lo muy prohibido con una declaración. Comprendamos por qué está prohibido.

El hecho es que tiene sus problemas. Por ejemplo, con, desafortunadamente, tiene fugas fuera del alcance que le agregamos, porque trata de mirar más profundo que la API de la Unidad y mira dentro del alcance global.


Aquí el último ejemplo es especialmente genial, porque incluso un cuatro puede ser peligroso para nuestro código, ya que todas estas funciones pueden ser realizadas por el código de usuario. El usuario puede hacer cualquier cosa. Este es un juego para programadores, y les gusta explorar los problemas y las formas de piratear algo.



Como dije, los ámbitos son muy permeables, por lo que siempre hay un alcance global disponible. No importa cuántos ámbitos apliquemos a nuestro código personalizado, no importa cuántos ámbitos lo envuelvamos, el alcance global seguirá siendo visible. Y todo por culpa de.

De hecho, no aísla nada, solo nos agrega una nueva capa de abstracción, un nuevo alcance global. Pero podemos cambiar este comportamiento con Proxy.



El hecho es que Proxy se ocupa de todas nuestras llamadas al objeto que se representan mediante la nueva API, y podemos controlar cómo se comportan las nuevas solicitudes de datos en este objeto.



De hecho, con obras bastante simples. Cuando lo alimentamos con algún tipo de variable, verifica debajo del capó si esta variable está en el objeto (es decir, ejecuta el operador in), y si es así, la ejecuta en el objeto, y si no, se ejecuta en el ámbito superior, en Nuestro caso es global. Es bastante simple aquí. Lo principal con lo que el proxy nos ayuda es que podemos anular este comportamiento.



Existen los ganchos en Proxy. Una cosa maravillosa que nos permite enviar cualquier solicitud al objeto. Podemos cambiar el comportamiento de la solicitud de atributo, cambiar el comportamiento del trabajo de atributo y, lo más importante, podemos cambiar el comportamiento de esto en el operador. Hay un gancho, al que solo podemos volver verdadero. Por lo tanto, tomamos y engañamos completamente nuestra declaración, haciendo que nuestra API sea mucho más segura que antes.



Si intentamos ejecutar eval (), primero preguntará si eval () está en unitApi, responderán "sí" y obtendrán "undefined no es una función". ¡Esta parece ser la primera vez que estoy contento con este error! Este error es exactamente lo que deberíamos haber recibido. Lo tomamos y le dijimos al usuario: "Lo siento, olvídate de todo lo que sabías sobre el objeto de la ventana, esto ya no existe". Ya hemos dejado algunos de los problemas, pero eso no es todo.



El hecho es que la declaración with es la misma de JS, JS es dinámica y un poco extraña. Lo extraño es que no todo funciona como nos gustaría, sin tener en cuenta las especificaciones. El hecho es que también funciona con propiedades prototipo. Es decir, podemos curny alimentarlo con una matriz, ejecutar este código oscuro. Todas las funciones de matriz están disponibles como globales en este ámbito, lo que parece un poco extraño.



Esto no es importante para nosotros, es importante para nosotros que el usuario pueda ejecutar valueOf () y obtener todo nuestro sandbox. Toma y recoge directamente, mira lo que contiene. Yo tampoco quería esto, así que se introdujo algo interesante en la especificación: Symbol.unscopables. Es decir, en la nueva especificación para símbolos, Symbol.unscopables se introdujo específicamente para la declaración with, que está prohibida. Porque creen que alguien más lo está usando. Por ejemplo yo!



Por lo tanto, haremos otro interceptor, donde verificaremos específicamente si este símbolo se encuentra en la lista de todos los atributos que no se pueden eliminar. Si no, devuélvelo, pero si es así, perdón, no volveremos. Nosotros tampoco lo usamos. Y así, con ni siquiera podemos obtener un prototipo de nuestro sandbox.



Todavía tenemos el entorno de los trabajadores. Esto es algo que cuelga en un área global y aún es accesible. El hecho es que si simplemente anula esto, estará disponible en el prototipo. Casi todo se puede extraer a través del prototipo en JS. Sorprendentemente, todos estos métodos todavía están disponibles a través del prototipo.



Solo tenía que tomar y limpiar todo esto. Revisamos todas las llaves y lo limpiamos todo.



Y luego dejamos un pequeño huevo de Pascua para el usuario, que todavía intenta llamar a esto. Tomamos la función habitual, lo principal no es la función de flecha, que tiene un alcance, y cambiamos su alcance a nuestro objeto, en el que dejamos un pequeño huevo de Pascua para un usuario particularmente curioso que quiere mostrar algo de esto o de sí mismo en la consola. Creo que los huevos de Pascua son maravillosos y deben dejarse en el código.



Resulta que se quedaron solo con nuestra Unidad API. Bloqueamos completamente todo; de hecho, dejamos una lista blanca. Necesitamos agregar aquellas API que son útiles y necesarias. Por ejemplo, la API de matemáticas, que tiene una función aleatoria útil que muchas personas usan al escribir código para unidades.

También necesitamos consola y muchas otras funciones utilitarias que no tienen ninguna función destructiva. Creamos una lista de tiempo para nuestras API.Esto es bueno, porque si creáramos una lista negra, dependeríamos de cualquier actualización del navegador que ocurra sin nuestro conocimiento.



Al crear una lista blanca, podemos comenzar a usar try-catch en nuestro código. Nuestro código envuelto ya detecta errores y puede enviarlos al usuario, lo cual es muy importante para la depuración.



Pero el hecho es que los métodos de consola de Worker no se manifiestan en el código de usuario. Es decir, si abre la consola y entra en el entorno de trabajo, estarán disponibles, pero sería un error decirle al usuario "abra la consola y vea qué le sucedió". Decidí hacer una consola amigable para el jugador, donde el jugador, incluso sin experiencia en JavaScript, pudiera ver de manera amigable lo que sucedió.

, , - . , . , 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() -.

todo el código que se envía al trabajador es necesario para que JS sea completamente seguro. La "inserción" naranja es el mismo código de usuario. que se inyecta allí. Esa es la cantidad de código que necesita para que JS sea seguro. Las API aleatorias () y de unidad también se inyectan allí. A través de inyecciones, recibo aún más código que se envía al trabajador.



Estado compartido: RxJS, servidor y clientes


Entonces descubrimos lo que tenemos con el cliente. Ahora hablemos sobre compartir el estado, por qué es necesario y cómo se organizó. Tenemos el estado almacenado en el servidor, el servidor debe confundirlo con los clientes conectados. [Diapositiva 28:48]

Tenemos cuatro roles de diferentes clientes que pueden conectarse al servidor: "usuario izquierdo", "usuario derecho", un espectador que mira la pantalla principal y un administrador que puede hacer cualquier cosa.

La pantalla izquierda no puede cambiar el estado del jugador derecho, el espectador no puede cambiar nada y el administrador puede hacer todo.



¿Por qué fue esto un problema? Todo está organizado de manera bastante simple. Cualquier cliente conectado puede lanzar una sesión, el servidor la acepta y fusiona con el estado, que está dentro del servidor y luego la distribuye a todos los clientes. Él busca a tientas cualquier cambio que se le ocurra. Era necesario filtrarlo de alguna manera.



Primero, diré por qué el servidor también tiene RxJS. Todas las interacciones con dos o más usuarios conectados se vuelven asíncronas. Debemos esperar los resultados de ambos usuarios. Por ejemplo, ambos usuarios hicieron clic en el botón "Finalizar", debe esperar a que ambos hagan clic y solo luego realizar la acción. Todo fue bastante sencillo en RxJS de esta manera:



Nuevamente estamos operando con subprocesos, hay un flujo desde el socket, que se llama socket. Para crear un hilo que monitoree solo al jugador izquierdo, simplemente tomamos y filtramos mensajes de este socket por el jugador izquierdo (y de manera similar con el derecho). Luego podemos combinarlos usando el operador forkJoin (), que funciona como Promise.all () y es su análogo. Esperamos ambas acciones y llamamos al método setState (), que establece nuestro estado en "listo". Resulta que estamos esperando a ambos jugadores y cambiamos el estado del servidor. En RxJS, esto sale lo más declarativo posible, por eso lo usé.

Sigue habiendo un problema con el hecho de que los jugadores pueden cambiar de estado entre sí. Se les debe prohibir hacer esto. Aún así, son programadores, hubo precedentes que alguien intentó. Creemos clases separadas para ellos que se heredan del Cliente.



Tendrán la lógica básica de la comunicación del jugador con el servidor, y en cada clase en particular habrá su lógica personalizada para filtrar datos.

El cliente es en realidad un grupo de conexiones, conexiones con clientes.



Simplemente los almacena y tiene la transmisión onUnsafeMessage, que es completamente insegura: no se puede confiar, estos son solo mensajes sin procesar del usuario que recibe. Escribimos estos mensajes en bruto en la secuencia.

Además, al implementar un reproductor específico, tomamos esto enUnsafeMessage y lo filtramos.



Necesitamos filtrar solo los datos que podemos obtener de este reproductor, en quien podemos confiar. El jugador izquierdo solo puede cambiar el estado del jugador izquierdo, respectivamente, tomamos de todos los datos que puede enviar, solo el estado del jugador izquierdo. Si no lo enviaste, está bien. Si se envía, tomamos. Por lo tanto, de mensajes completamente inseguros, obtenemos mensajes seguros en los que podemos confiar cuando trabajamos en la habitación.



Tenemos salas de juegos que reúnen a jugadores. Dentro de la sala, podemos escribir las funciones que pueden cambiar el estado directamente, simplemente suscribiéndose a estos flujos, en los que ya podemos confiar. Nos abstraemos de un montón de cheques. Hicimos las comprobaciones basadas en roles y las llamamos clases separadas. Dividimos el código de tal manera que dentro del controlador, donde se realizan las funciones importantes de cambio de estado, el código se ha vuelto lo más simple y declarativo posible.

RxJS también se usa en el cliente, está conectado al zócalo en el reverso, emite eventos y los redirige en todos los sentidos.

En este caso, me gustaría dar un ejemplo cuando necesito cambiar el ejército del adversario correcto.



Para suscribirse, creamos una secuencia desde el mismo socket y la filtramos. Nos aseguramos de que este sea realmente el jugador correcto, y tomamos un mensaje de él sobre cuál es su ejército. Si no hay tal mensaje, la transmisión no devolverá nada, no habrá un solo mensaje en él, permanecerá en silencio hasta que el jugador cambie el ejército. Inmediatamente resolvemos de manera declarativa el problema de filtrar eventos, no lo tenemos cursi.

Y cuando algo ya ha venido de la secuencia, llamamos a la función setState (). Esto es bastante simple, el enfoque que nos permite hacer todo de manera transparente y declarativa. Lo mismo por lo que asumí el proyecto RxJS y en lo que me ayudó mucho.



Creo secuencias que he nombrado claramente, con las que entiendo cómo trabajar, todo es declarativo, se llaman las funciones necesarias, no hay problemas con muchos eventos if y de filtrado, todo esto lo hace RxJS.

Historia: de un jugador a multijugador



Entonces, la primera versión de mi juguete fue escrita. Podemos decir que era un solo jugador, ya que solo dos jugadores podían jugarlo, el número de clientes conectados fue fijo. Teníamos un jugador izquierdo, un jugador derecho y la pantalla detrás, todo esto conectado directamente al servidor. Todo estaba recubierto, pero en tres semanas ya estaba hecho.

Recibí una nueva oferta: expandir el juguete para todos los programadores de la compañía para que puedan abrirlo y jugarlo en sus computadoras. Para que obtengamos una lista de líderes, multijugador para que puedan jugar juntos. Entonces me di cuenta de que tenía muchas refactorizaciones.



Resultó no ser tan difícil. Simplemente combiné todas las entidades que tenía en habitaciones separadas. Obtuve la esencia de "Room", que podría combinar todos los roles. Ahora, no son los jugadores mismos quienes se comunican directamente con el servidor, sino las salas. Las salas ya han enviado solicitudes directamente al servidor, reemplazando el estado, y el estado se ha separado para cada sala.



Tomé y reescribí todo, agregué una lista de líderes, otorgamos los mejores premios. Solo era necesario tener una gran cantidad de usuarios, ya era imposible seguir a todos, era necesario escribir algo donde recolectar todos los datos.

JS Gamedev y sus problemas



Por lo tanto, me familiaricé más con JS-gamedev. Di una vuelta sobre el último proyecto durante unos tres años, descansando periódicamente. Y aquí tuve las dos veces durante tres semanas. Todos los días me sentaba e hacía algo por las tardes.

¿Qué problemas hay en el desarrollo de juegos en JS? Todo es diferente de nuestras aplicaciones comerciales, donde no es un problema escribir algo desde cero. Además, mucho de eso es bienvenido: haremos lo nuestro, recordaremos las historias con NPM y nos iremos.



Es imposible hacer esto en JS Gamedev, porque todas las tecnologías para mostrar gráficos tienen un nivel tan bajo que no es rentable económicamente escribir algo en ellas. Si tomé este juguete y comencé a escribirlo desde cero en WebGL, también me sentaría detrás de él durante unos seis meses, solo tratando de descubrir algunos errores extraños. El motor de juegos más popular Phaser me quitó estos problemas ...



... y me agregó otros nuevos: 5 megabytes en un paquete. Y no se pudo hacer nada al respecto; no sabe en absoluto qué es sacudir árboles. Además, solo la última versión de Phaser puede funcionar con paquetes web y paquetes. Antes de esto, Phaser estaba conectado solo en la etiqueta html del script, era extraño para mí.

Vengo de todo tipo de webpacks-scripts, y en el desarrollo de juegos JS, casi nada puede hacerlo. Todos los módulos tienen una tipificación extremadamente pobre o no lo tienen en absoluto, o básicamente no saben cómo usar webpack, fue necesario encontrar formas de envolverlo. Al final resultó que, incluso Ace Editor en su forma pura no funciona con webpack en absoluto. Para comenzar a trabajar, debe descargar un paquete separado donde ya está envuelto (llave).

Fue casi lo mismo con Phaser, pero en la nueva versión lo hicieron más o menos normalmente. Seguí escribiendo en Phaser y descubrí cómo hacer que todo funcionara con el paquete web de la forma en que solíamos hacerlo: tanto la escritura como las pruebas podrían adjuntarse a todo esto. Descubrí que puede tomar PixiJS por separado, que es un render de paquete web, y encontrar muchos módulos para él que están listos para trabajar con él.



PixiJS es una gran biblioteca que puede renderizar en WebGL o en Canvas. Además, incluso puede escribir código como para Canvas, y se representará en WebGL. Esta biblioteca puede renderizar 2D muy rápidamente. Lo principal es saber cómo funciona con la memoria, para no caer en una posición cuando la memoria ha terminado.

Recomiendo por separado el impresionante repositorio pixijs en GitHub, donde puedes encontrar diferentes módulos. Sobre todo me gustó React-pixi . Simplemente podemos ignorar la resolución de problemas con la vista cuando escribimos funciones imperativas directamente en el controlador para dibujar formas geométricas, sprites, animaciones y más. Todos podemos marcar en JSX. Venimos del mundo JSX con nuestra aplicación comercial y podemos usarlos más. Para eso me encanta la abstracción. React-pixi nos da esta abstracción familiar.

También le aconsejo que tome tween.js, el mismo famoso motor de animación de Phaser, que le permite realizar animaciones declarativas que son algo similares a las animaciones CSS: hacemos una transición entre estados, y tween.js decide por nosotros exactamente cómo mover el objeto.

Tipos de jugadores: quiénes son y cómo hacerse amigos de ellos


Me encontré con diferentes jugadores, y también me gustaría contarte sobre cómo probar el juguete. Reuní colegas en una habitación cerrada y no los dejé salir hasta que terminaron el juego. Desafortunadamente, no todos pudieron terminar el juego, porque al principio tenía muchos errores. Afortunadamente, comencé a probar tan pronto como apareció al menos un prototipo funcional. Honestamente, la primera prueba falló porque un par de jugadores no comenzaron nada. Fue una pena, pero me dio una patada que me permitió seguir adelante.

Cuando tu juguete esté listo, puedes ser recibido muy bien, o con una horca y antorchas. Todas las personas esperan algún tipo de fanático de los juegos, esperando la felicidad que les darás. Y les das algo que no funciona en absoluto, aunque parece funcionar para ti. Cuando tienes un juguete en línea, hay incluso más errores de este tipo.

Como resultado, las personas más agradables con las que me he encontrado son "investigadores" que siempre encuentran más en su juguete de lo que realmente son. Pueden complementarlo agradablemente con todo tipo de pequeñas cosas, lo que te lleva a agregar algo. Pero, desafortunadamente, la comunicación con estas personas no dio lo importante: la estabilidad del juguete.

Hay jugadores comunes que vienen únicamente por el fanático. Es posible que a veces ni siquiera se den cuenta de los errores, de alguna manera se les escapen en su camino hacia el placer.

Otra categoría son los recolectores de errores, para los cuales casi todo no funciona. Debes ser amigo de esas personas, aunque hablarán mucho de negatividad. Necesitamos entablar relaciones extrañas con ellos: te lastiman y estás tratando de tomar algo útil para ti, "sentémonos en tu computadora y veamos". Necesitas trabajar con ellos, porque al final son estas personas las que harán que tu juego sea de calidad.

Solo necesita probar en personas vivas. Su ojo está borroso, y las pruebas ciertamente mostrarán lo que está oculto. Estás desarrollando un juguete y buceando más profundo, cortando algunas características, pero puede que ni siquiera sean necesarias. Acude directamente a sus consumidores, les muestra y observa cómo juegan, qué teclas presionan. Esto le brinda un incentivo para hacer exactamente lo que necesita. Verá que algunas personas presionan constantemente Ctrl + S, porque están acostumbrados a guardar el código; bueno, al menos haga que el código se ejecute en Ctrl + S, el jugador se sentirá más cómodo. Necesitas crear un ambiente cómodo para el jugador, para esto necesitas amarlo y seguirlo.

La regla 80/20 funciona: realiza una demostración el 20% del tiempo del desarrollo completo del juego, y para el jugador parece un juego completado al 80%. La percepción funciona para que la mecánica básica esté lista, todo se mueva y funcione, lo que significa que el juego está casi listo y el desarrollador lo terminará pronto. Pero en realidad, el desarrollador todavía tiene una salida del 80%. Como dije, durante mucho tiempo tuve que trabajar en la documentación para que fuera comprensible para todos. Se lo mostré a muchas personas que hablaron sus comentarios, los filtré, tratando de entender la esencia de las declaraciones. Y me llevó mucho tiempo buscar errores.

Entonces, en el desarrollo de juegos, solo podría aconsejarle que haga demostraciones: deleitan a todos, no requieren mucho tiempo y nadie realmente espera nada de las demostraciones. Terminar juegos es un proceso aburrido, pero comenzar es genial.

Finalmente, te dejo enlaces:

HolyJS 2019 Piter , una conferencia para desarrolladores de JavaScript, se llevará a cabo del 24 al 25 de mayo en San Petersburgo. Los primeros oradores ya aparecieron en el sitio.
También puede solicitar un informe, Call for Papers está abierto hasta el 11 de marzo.
Los precios de las entradas subirán el 1 de febrero.

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


All Articles