Saltar al contenido
Docs

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, setTimeout y setInterval no 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#

scripts/principal.ts
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í:

ts
const { juego, mundo, jugadores } = greengames;
ServicioPara quéReferencia
juegoIniciar, cada cuadro, terminar, cambiar de escena, registrojuego
mundoPiezas, zonas, apariciones, luces, mapas de baldosas, toques, rayosmundo
jugadoresQuién está, sus acciones, sus caídas, su estadojugadores
interfazTextos, HUD, cronómetros, mensajes, marcador, menúsinterfaz
sonidoSonidos predefinidos, tonos y secuencias de notassonido
progresoGuardar datos del jugador entre partidasprogreso
puntajesTablas de puntajes localespuntajes
azarNúmeros al azar que salen de la semillaazar
relojTiempo de partida y temporizadoresreloj
actoresPersonajes que maneja el script: rivales, vecinos, enemigosactores
camaraLa vista y la cámara de cada jugador, y tomas temporalescamara

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#

  1. Solo puede importar @greengames/sdk y archivos relativos del propio juego (./niveles.ts).
  2. Toda la API es síncrona. No hay async, await ni promesas que esperar.
  3. Para esperar, usa reloj.despues(segundos, fn). Para repetir, reloj.cada o juego.cadaCuadro.
  4. Para el azar, usa azar, que sale de la semilla de la partida. Math.random dentro de la sala también sale de la semilla, pero el validador avisa.
  5. 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.
  6. El estado propio de cada jugador se guarda en jugador.datos.
  7. Registra los eventos dentro de juego.alIniciar. Los cambios de un cuadro se ven en el siguiente.
  8. 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.

scripts/principal.ts
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:

  • AJUSTES elige una fila según la dificultad. juego.nivelDificultad es el índice de la dificultad en el manifiesto (0, 1, 2), o -1 si el juego no declara dificultades: por eso el Math.max(0, ...).
  • jugadores.alEntrar corre 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.terminar con tabla y de registra 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 en textos.es y textos.en del manifiesto. Ningún texto escrito directo en el script.

  • Tiempo con reloj e interfaz.cronometro; azar con azar. Nada de setTimeout, Date ni Math.random.

  • Para mover una pieza, asigna un vector nuevo: pieza.posicion = [x, y, z], o usa pieza.moverA(...). Leer pieza.posicion da una copia: pieza.posicion[0] = 5 no 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 grupo en la escena y mundo.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:

SelectorEjemploA 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:

FormaEjemploCuándo
Una clave del manifiestointerfaz.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.nombreSolo 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 viveCómo se muestra
Estado de una persona (vidas, monedas, llave)jugador.puntos, jugador.datos, jugador.equipoCon para: jugador (HUD, mensajes, sonidos, menús, cámara)
Estado de todos (reloj, ronda, puerta abierta)Variables del script, pieza.datosSin 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 tienen equipo.
  • 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 conservaSe 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 calcenLas piezas, zonas, luces, actores y rótulos de la escena anterior, también los creados por script
Temporizadores (reloj.despues, reloj.cada) y cronómetrosLos eventos registrados con pieza.alTocar(fn): se van con la pieza
HUD, marcador, menús abiertos y sonidosLos grupos, que son de las piezas
Jugadores: datos, puntos, rapidez, salto, progresoLa aparicion de cada jugador: pasa a ser la primera de la escena nueva
Señales de juego.senalar y ajustes de cámaraEl 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, saltar y caer; 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 (primaria y secundaria, 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.crear existe de inmediato, pero los toques, los rayos, mundo.piezasEn y 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().
  • rotacion de una pieza dinámica es aproximada: sale de su orientación física.
  • mundo.rayo choca 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 cristal y 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#