Scripts
Cómo es el script de un juego: dónde corre, cómo se importa la API, las ocho reglas, un esqueleto, selectores, textos traducibles, varios jugadores y lo que la API todavía no ofrece.
Corresponde aFormato 1, API 1 Para quien escribe código
En esta página
El script es la lógica del juego: qué pasa cuando alguien toca una moneda, entra a la meta, pulsa un botón o se queda sin tiempo. Se escribe en TypeScript o JavaScript, en el archivo que nombra script en el manifiesto (lo habitual: scripts/principal.ts).
Dónde corre#
El script es un programa que corre dentro de la sala: la simulación de la partida, que decide todo lo que ocurre (física, jugadores, puntajes). La sala corre aislada, en un Web Worker dentro de un iframe con sandbox. Ahí no hay página, red, almacenamiento del sitio, temporizadores del navegador ni eval. Solo existen JavaScript estándar y el objeto greengames con los servicios de la API.
Eso tiene dos consecuencias prácticas:
fetch,document,window,localStorage,setTimeoutysetIntervalno existen. Nombrarlos es un error del validador y un error de tipos.- Todo lo que el juego quiera hacer (mostrar un texto, tocar un sonido, guardar un récord) lo pide a un servicio. Por qué es así: Aislamiento.
El import y el global greengames#
import { juego, mundo, jugadores, interfaz, sonido, progreso, puntajes, azar, reloj, actores, camara } from '@greengames/sdk';Importa solo los servicios que uses. Dentro del .ggx el script queda como JavaScript clásico y esos nombres salen del objeto global greengames. Un script escrito a mano, sin herramientas, puede empezar así:
const { juego, mundo, jugadores } = greengames;| Servicio | Para qué | Referencia |
|---|---|---|
juego | Iniciar, cada cuadro, terminar, cambiar de escena, registro | juego |
mundo | Piezas, zonas, apariciones, luces, mapas de baldosas, toques, rayos | mundo |
jugadores | Quién está, sus acciones, sus caídas, su estado | jugadores |
interfaz | Textos, HUD, cronómetros, mensajes, marcador, menús | interfaz |
sonido | Sonidos predefinidos, tonos y secuencias de notas | sonido |
progreso | Guardar datos del jugador entre partidas | progreso |
puntajes | Tablas de puntajes locales | puntajes |
azar | Números al azar que salen de la semilla | azar |
reloj | Tiempo de partida y temporizadores | reloj |
actores | Personajes que maneja el script: rivales, vecinos, enemigos | actores |
camara | La vista y la cámara de cada jugador, y tomas temporales | camara |
Los once servicios, con todo lo que suman las versiones 1.1 (el terreno de cubos, el puntero y la barra rápida) y 1.2 (modelos, texturas y sonidos grabados) del contrato, están en el portal, en la herramienta greengames, en el launcher 0.4.0 y en el editor 0.4.0. Las aplicaciones anteriores traían menos: las 0.3.0, sin lo de la versión 1.2; las 0.2.0, los once servicios sin lo de la versión 1.1; las 0.1.0, nueve servicios. Qué le faltaba a cada una está en Disponibilidad.
Las ocho reglas del script#
- Solo puede importar
@greengames/sdky archivos relativos del propio juego (./niveles.ts). - Toda la API es síncrona. No hay
async,awaitni promesas que esperar. - Para esperar, usa
reloj.despues(segundos, fn). Para repetir,reloj.cadaojuego.cadaCuadro. - Para el azar, usa
azar, que sale de la semilla de la partida.Math.randomdentro de la sala también sale de la semilla, pero el validador avisa. - No existe "el jugador": hay una lista. Cada evento trae al jugador que lo provocó, y lo que se muestra o suena acepta
para: jugador. Así el juego ya funciona cuando hay bots en la partida y cuando juegan varias personas en el mismo equipo: ver Varios jugadores. - El estado propio de cada jugador se guarda en
jugador.datos. - Registra los eventos dentro de
juego.alIniciar. Los cambios de un cuadro se ven en el siguiente. - Toda partida debe terminar con
juego.terminar({ resultado, puntaje }).
Un esqueleto para empezar#
Este esqueleto es el de la guía del autor de Green Games. Pasa greengames validar y greengames probar sobre una escena que tenga piezas con la etiqueta moneda, una zona meta y una tabla puntos en el manifiesto.
import { interfaz, juego, jugadores, mundo, sonido, type Jugador } from '@greengames/sdk';
const AJUSTES = [{ segundos: 60 }, { segundos: 45 }, { segundos: 35 }][Math.max(0, juego.nivelDificultad)];
juego.alIniciar(() => {
jugadores.alEntrar((j) => { j.datos = { vidas: 3 }; });
mundo.alTocar('#moneda', (moneda, j) => { moneda.eliminar(); j.puntos++; sonido.tocar('moneda', { para: j }); });
mundo.alEntrar('meta', (_zona, j) => juego.terminar({ resultado: 'victoria', puntaje: j.puntos, tabla: 'puntos', de: j }));
});Cómo leerlo:
AJUSTESelige una fila según la dificultad.juego.nivelDificultades el índice de la dificultad en el manifiesto (0, 1, 2), o -1 si el juego no declara dificultades: por eso elMath.max(0, ...).jugadores.alEntrarcorre para cada jugador, también para los que ya estaban. Es el lugar para darle a cada uno su estado inicial.mundo.alTocar('#moneda', ...)atiende a todas las piezas con esa etiqueta. Recibe la pieza tocada y el jugador que la tocó.juego.terminarcontablayderegistra el puntaje de ese jugador en esa tabla.
Un script completo y comentado de 60 líneas viene en la plantilla de greengames crear. Uno más grande, explicado por partes: Caso de estudio.
Costumbres que evitan errores#
Registra todo dentro de
juego.alIniciar. El código de nivel superior corre antes de que la partida esté lista.Estado por jugador en
j.datos; estado de la partida en variables del archivo.Textos siempre con
interfaz.t('clave', { variables }), y la clave entextos.esytextos.endel manifiesto. Ningún texto escrito directo en el script.Tiempo con
relojeinterfaz.cronometro; azar conazar. Nada desetTimeout,DateniMath.random.Para mover una pieza, asigna un vector nuevo:
pieza.posicion = [x, y, z], o usapieza.moverA(...). Leerpieza.posicionda una copia:pieza.posicion[0] = 5no mueve nada.Termina siempre:
juego.terminar({ resultado, puntaje, tabla }).Un menú no pausa la partida. Si nadie debe moverse mientras está abierto, congela a los jugadores (
j.congelado = true).En teléfonos los textos largos se cortan. Un chip del HUD muestra cómodo unos 24 caracteres por línea.
Personajes que no son jugadores:
actores.crear(...), no cajas animadas a mano.Varias piezas que se mueven juntas (una puerta, un vehículo): la misma propiedad
grupoen la escena ymundo.grupo(nombre).Texto sobre algo del mundo (la vida de una torre, el nombre de un lugar):
interfaz.rotulo. Números que saltan al recoger o golpear:interfaz.flotante.En un puzle visto desde arriba, fija la cámara:
camara.fijar({ inclinacion: 70, distancia: 24, libre: false }). Si todo el juego se ve así, declara la vista en la escena: Vistas.
Selectores#
Los eventos del mundo (mundo.alTocar, mundo.alDejarDeTocar, mundo.alEntrar, mundo.alSalir) reciben un selector:
| Selector | Ejemplo | A qué responde |
|---|---|---|
| Un id | 'meta' | A esa pieza o esa zona. |
| Una etiqueta, con numeral | '#moneda' | A todas las piezas o zonas que tengan esa etiqueta. |
Las etiquetas se evalúan en el momento del toque. Puedes etiquetar y desetiquetar piezas mientras se juega (pieza.etiquetar('peligro')) y el evento las toma o las suelta al instante.
alTocar se dispara cuando un jugador empieza a tocar una pieza, sea sólida o no; pararse encima cuenta. Una pieza con "fisica": "ninguna" no avisa toques: para algo que se atraviesa pero avisa, usa "solida": false.
Textos traducibles#
Un texto de la interfaz puede ser tres cosas:
| Forma | Ejemplo | Cuándo |
|---|---|---|
| Una clave del manifiesto | interfaz.t('monedas', { n: 3, total: 6 }) | Siempre que puedas. Cada reproductor lo muestra en el idioma del jugador. |
| Un objeto con los dos idiomas | { es: 'Meta', en: 'Finish' } | Textos que no están en el manifiesto. |
| Una cadena tal cual | '3', jugador.nombre | Solo para números y nombres propios. |
interfaz.t devuelve un objeto, no una cadena. No lo concatenes: interfaz.t('ganaste') + ' algo' muestra [object Object]. Pon la frase completa en la plantilla, con sus variables: "monedas": "{n} de {total}".
Varios jugadores#
Un juego declara en el manifiesto cómo se juega de a varios: multijugador. Con multijugador.local, de 2 a 4 personas juegan en el mismo equipo, con pantalla dividida o compartida. Un mismo script sirve para una persona, para varias y para los bots si respeta estas reglas, que son las de siempre:
| Qué | Dónde vive | Cómo se muestra |
|---|---|---|
| Estado de una persona (vidas, monedas, llave) | jugador.puntos, jugador.datos, jugador.equipo | Con para: jugador (HUD, mensajes, sonidos, menús, cámara) |
| Estado de todos (reloj, ronda, puerta abierta) | Variables del script, pieza.datos | Sin para |
| Quién está | jugadores.todos(), nunca "el jugador" | interfaz.marcadorDePuntos, interfaz.marcador |
- Entrar y salir con la partida en marcha.
jugadores.alEntrar(fn)corre para los que ya están y para cada uno que llega después;jugadores.alSalir(fn)avisa cuando alguien se va. No guardes listas propias de jugadores: pregunta cada vez. - Equipos.
jugador.equipo = 'rojo'(texto libre); los actores también tienenequipo. - Vistas. Son por jugador: en pantalla dividida cada parte puede estar en una vista distinta.
- Pantalla compartida. Solo se ve el HUD del primer jugador y lo que es para todos; muestra lo de cada uno con el marcador o con rótulos sobre los avatares.
- Controles en el mismo equipo. Con mandos, uno por persona. Sin mandos suficientes, los dos primeros jugadores comparten el teclado (W A S D con Espacio, E y Q; flechas con Mayús derecha, Enter y Ctrl derecho) y los mandos que haya son para los últimos. El ratón y la pantalla táctil son del primer jugador.
Para probarlo: greengames probar <slug> --jugadores 4 corre la partida con cuatro jugadores que siguen el guion, y en la vista previa ?auto=1&locales=2 abre la pantalla dividida. En el editor: Multijugador local.
Qué sobrevive a un cambio de escena#
juego.cambiarEscena(id) descarga la escena, carga la otra en el cuadro siguiente, lleva a los jugadores a su primera aparición y llama a lo registrado con juego.alCambiarEscena.
| Se conserva | Se pierde |
|---|---|
Todos los eventos registrados (alTocar, alEntrar, cadaCuadro, alAccion...): los de selector se vuelven a aplicar a las piezas y zonas de la escena nueva que calcen | Las piezas, zonas, luces, actores y rótulos de la escena anterior, también los creados por script |
Temporizadores (reloj.despues, reloj.cada) y cronómetros | Los eventos registrados con pieza.alTocar(fn): se van con la pieza |
| HUD, marcador, menús abiertos y sonidos | Los grupos, que son de las piezas |
Jugadores: datos, puntos, rapidez, salto, progreso | La aparicion de cada jugador: pasa a ser la primera de la escena nueva |
Señales de juego.senalar y ajustes de cámara | El ambiente y la vista: toman los de la escena nueva |
Lo que la API todavía no ofrece#
Para que no pierdas tiempo buscándolo.
- Jerarquía de piezas. Hay grupos (
mundo.grupo) para mover, girar y ocultar varias piezas juntas, pero no una pieza "hija" que siga sola a otra. - Partículas y estelas. Se simulan con piezas chicas o con
interfaz.flotante. - Sacudida de cámara.
- Objetos en la mano. Un jugador o un actor dibujado con un modelo usa solo sus clips
quieto,caminar,saltarycaer; otros clips se reproducen en piezas con modelo, no en el personaje. - Choque físico entre personajes. Jugadores y actores se atraviesan; se detectan con eventos.
- Actores que saltan huecos o usan plataformas por su cuenta. Caminan y rodean; no planean saltos.
- Entrada libre: teclas arbitrarias, la posición de la flecha del ratón sobre el mundo, texto escrito. Hay movimiento, cámara, tres acciones y, desde la versión 1.1, dos más (
primariaysecundaria, los clics del ratón) con un puntero que apunta por la mira del centro de la pantalla: Servicio jugadores. - HUD con disposición propia: rejillas, imágenes, botones del juego en pantalla fuera de los menús.
- Chat, amigos y tablas globales. No hay servidor.
- En los mapas de baldosas: baldosas que se animan solas y pendientes.
Modelos 3D, texturas de imagen y sonidos grabados sí están, desde la versión 1.2 del contrato: Modelos, texturas y sonidos.
Qué de esto está en desarrollo: Hoja de ruta.
Comportamientos que conviene conocer#
- Una pieza creada con
mundo.crearexiste de inmediato, pero los toques, los rayos,mundo.piezasEny las colisiones la ven desde el cuadro siguiente. - Las plataformas en movimiento llevan a los jugadores y a los actores, pero no a las piezas dinámicas que tengan encima.
- Una plataforma que se mueve de lado empuja al jugador solo de forma aproximada; una que baja sobre él no lo aplasta. Para aplastar o empujar con certeza, usa una etiqueta de peligro y
jugador.reaparecer(). rotacionde una pieza dinámica es aproximada: sale de su orientación física.mundo.rayochoca solo con piezas sólidas e ignora a los jugadores.- La cámara trata cada pieza como su caja envolvente para decidir si tapa la vista; las de
cristaly las chicas no cuentan. Si molesta junto a piezas altas:camara.fijar({ evitarObstaculos: false }). - Los rótulos se dibujan sobre el mundo: no los tapan las piezas que tengan delante.
Sigue con#
- Referencia de la API: cada servicio, función por función.
- Pruebas con el bot: comprobar que el juego se puede jugar y terminar.
- Buenas prácticas: la lista de comprobación y los errores frecuentes.