Skip to content
Docs

Scripts

What a game's script is like: where it runs, how the API is imported, the eight rules, a skeleton, selectors, translatable texts, several players and what the API does not offer yet.

DescribesFormat 1, API 1 For those who write code

On this page

The script is the game's logic: what happens when someone touches a coin, enters the goal, presses a button or runs out of time. You write it in TypeScript or JavaScript, in the file named by script in the manifest (usually scripts/principal.ts).

The API names are in Spanish (juego.alIniciar, mundo.alTocar) and stay that way in every language. The code on this page is quoted as it is in its sources.

Where it runs#

The script is a program that runs inside the room: the simulation of the match, which decides everything that happens (physics, players, scores). The room runs isolated, in a Web Worker inside a sandboxed iframe. There is no page there, no network, no site storage, no browser timers and no eval. Only standard JavaScript and the greengames object with the API services exist.

That has two practical consequences:

  • fetch, document, window, localStorage, setTimeout and setInterval do not exist. Naming them is a validator error and a type error.
  • Everything the game wants to do (show a text, play a sound, save a record) it asks of a service. Why it is like this: Isolation.

The import and the greengames global#

scripts/principal.ts
import { juego, mundo, jugadores, interfaz, sonido, progreso, puntajes, azar, reloj, actores, camara } from '@greengames/sdk';

Import only the services you use. Inside the .ggx the script ends up as classic JavaScript and those names come from the global greengames object. A script written by hand, with no tools, can start like this:

ts
const { juego, mundo, jugadores } = greengames;
ServiceWhat forReference
juegoStart, every frame, finish, change scene, logjuego
mundoPieces, zones, spawn points, lights, tile maps, touches, raysmundo
jugadoresWho is there, their actions, their falls, their statejugadores
interfazTexts, HUD, stopwatches, messages, scoreboard, menusinterfaz
sonidoPreset sounds, tones and note sequencessonido
progresoSave the player's data between matchesprogreso
puntajesLocal score tablespuntajes
azarRandom numbers that come from the seedazar
relojMatch time and timersreloj
actoresCharacters the script drives: rivals, neighbors, enemiesactores
camaraEach player's view and camera, and temporary shotscamara

All eleven services, with everything that versions 1.1 (the cube terrain, the pointer and the hotbar) and 1.2 (models, textures and recorded sounds) of the contract add, are in the portal, in the greengames tool, in launcher 0.4.0 and in editor 0.4.0. The earlier apps had less: the 0.3.0 ones, without the version 1.2 additions; the 0.2.0 ones, the eleven services without the version 1.1 additions; the 0.1.0 ones, nine services. What each one lacked is in Availability.

The eight rules of the script#

  1. It can only import @greengames/sdk and relative files of the game itself (./niveles.ts).
  2. The whole API is synchronous. There is no async, no await and no promises to wait for.
  3. To wait, use reloj.despues(segundos, fn). To repeat, reloj.cada or juego.cadaCuadro.
  4. For randomness, use azar, which comes from the match's seed. Math.random inside the room also comes from the seed, but the validator warns.
  5. There is no "the player": there is a list. Each event brings the player who caused it, and whatever is shown or played accepts para: jugador. That way the game already works when there are bots in the match and when several people play on the same computer: see Several players.
  6. Each player's own state is kept in jugador.datos.
  7. Register events inside juego.alIniciar. The changes of one frame are seen in the next.
  8. Every match must end with juego.terminar({ resultado, puntaje }).

A skeleton to start from#

This skeleton is the one from the Green Games author's guide. It passes greengames validar and greengames probar on a scene that has pieces with the tag moneda, a zone meta and a table puntos in the manifest.

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 }));
});

How to read it:

  • AJUSTES picks a row according to the difficulty. juego.nivelDificultad is the index of the difficulty in the manifest (0, 1, 2), or -1 if the game declares no difficulties: hence the Math.max(0, ...).
  • jugadores.alEntrar runs for each player, including those who were already there. It is the place to give each one their initial state.
  • mundo.alTocar('#moneda', ...) handles all the pieces with that tag. It receives the touched piece and the player who touched it.
  • juego.terminar with tabla and de records that player's score in that table.

A complete, commented 60-line script comes in the template of greengames crear. A bigger one, explained part by part: Case study.

Habits that prevent errors#

  • Register everything inside juego.alIniciar. Top-level code runs before the match is ready.

  • Per-player state in j.datos; match state in variables of the file.

  • Texts always with interfaz.t('clave', { variables }), and the key in textos.es and textos.en of the manifest. No text written directly in the script.

  • Time with reloj and interfaz.cronometro; randomness with azar. No setTimeout, Date or Math.random.

  • To move a piece, assign a new vector: pieza.posicion = [x, y, z], or use pieza.moverA(...). Reading pieza.posicion gives a copy: pieza.posicion[0] = 5 moves nothing.

  • Always finish: juego.terminar({ resultado, puntaje, tabla }).

  • A menu does not pause the match. If nobody should move while it is open, freeze the players (j.congelado = true).

  • On phones, long texts get cut off. A HUD chip comfortably shows about 24 characters per line.

  • Characters that are not players: actores.crear(...), not boxes animated by hand.

  • Several pieces that move together (a door, a vehicle): the same grupo property in the scene and mundo.grupo(nombre).

  • Text over something in the world (a tower's health, the name of a place): interfaz.rotulo. Numbers that pop up when you collect or hit: interfaz.flotante.

  • In a puzzle seen from above, fix the camera: camara.fijar({ inclinacion: 70, distancia: 24, libre: false }). If the whole game looks like that, declare the view in the scene: Views.

Selectors#

The world events (mundo.alTocar, mundo.alDejarDeTocar, mundo.alEntrar, mundo.alSalir) take a selector:

SelectorExampleWhat it responds to
An id'meta'That piece or that zone.
A tag, with a hash sign'#moneda'All the pieces or zones that have that tag.

Tags are evaluated at the moment of the touch. You can tag and untag pieces while the game is played (pieza.etiquetar('peligro')) and the event picks them up or drops them at once.

alTocar fires when a player starts touching a piece, solid or not; standing on it counts. A piece with "fisica": "ninguna" does not report touches: for something you pass through but that still reports, use "solida": false.

Translatable texts#

An interface text can be three things:

FormExampleWhen
A key of the manifestinterfaz.t('monedas', { n: 3, total: 6 })Whenever you can. Each game player shows it in the player's language.
An object with both languages{ es: 'Meta', en: 'Finish' }Texts that are not in the manifest.
A plain string'3', jugador.nombreOnly for numbers and proper names.

interfaz.t returns an object, not a string. Do not concatenate it: interfaz.t('ganaste') + ' algo' shows [object Object]. Put the whole sentence in the template, with its variables: "monedas": "{n} de {total}".

Several players#

A game declares in the manifest how it is played by several people: multiplayer. With multijugador.local, 2 to 4 people play on the same computer, with a split or a shared screen. One script serves one person, several people and the bots if it follows these rules, which are the usual ones:

WhatWhere it livesHow it is shown
One person's state (lives, coins, a key)jugador.puntos, jugador.datos, jugador.equipoWith para: jugador (HUD, messages, sounds, menus, camera)
Everyone's state (clock, round, an open door)Variables of the script, pieza.datosWithout para
Who is therejugadores.todos(), never "the player"interfaz.marcadorDePuntos, interfaz.marcador
  • Joining and leaving mid-match. jugadores.alEntrar(fn) runs for those already there and for each one who arrives later; jugadores.alSalir(fn) tells you when someone leaves. Do not keep your own lists of players: ask every time.
  • Teams. jugador.equipo = 'rojo' (free text); actors have equipo too.
  • Views. They are per player: on a split screen each part can be in a different view.
  • Shared screen. Only the first player's HUD and whatever is for everyone are shown; show each player's own data with the scoreboard or with labels over the avatars.
  • Controls on the same computer. With gamepads, one per person. Without enough gamepads, the first two players share the keyboard (W A S D with Space, E and Q; the arrow keys with right Shift, Enter and right Ctrl) and whatever gamepads there are go to the last players. The mouse and the touch screen belong to the first player.

To test it: greengames probar <slug> --jugadores 4 runs the match with four players that follow the bot script, and in the preview ?auto=1&locales=2 opens the split screen. In the editor: Local multiplayer.

What survives a scene change#

juego.cambiarEscena(id) unloads the scene, loads the other one on the next frame, takes the players to its first spawn point and calls whatever was registered with juego.alCambiarEscena.

KeptLost
All registered events (alTocar, alEntrar, cadaCuadro, alAccion...): those with a selector are applied again to the matching pieces and zones of the new sceneThe pieces, zones, lights, actors and labels of the previous scene, including those created by script
Timers (reloj.despues, reloj.cada) and stopwatchesEvents registered with pieza.alTocar(fn): they go with the piece
HUD, scoreboard, open menus and soundsGroups, which belong to the pieces
Players: datos, puntos, rapidez, salto, progressEach player's aparicion: it becomes the first one of the new scene
Signals from juego.senalar and camera settingsThe environment and the view: they take those of the new scene

What the API does not offer yet#

So that you do not waste time looking for it.

  • Piece hierarchy. There are groups (mundo.grupo) to move, rotate and hide several pieces together, but no "child" piece that follows another on its own.
  • Particles and trails. You simulate them with small pieces or with interfaz.flotante.
  • Camera shake.
  • Objects held in the hand. A player or an actor drawn with a model only uses its clips quieto, caminar, saltar and caer; other clips play on pieces with a model, not on the character.
  • Physical collision between characters. Players and actors pass through each other; you detect them with events.
  • Actors that jump gaps or use platforms on their own. They walk and go around; they do not plan jumps.
  • Free input: arbitrary keys, the position of the mouse arrow over the world, typed text. There is movement, camera, three actions and, since version 1.1, two more (primaria and secundaria, the mouse clicks) with a pointer that aims through the crosshair at the center of the screen: Players service.
  • HUD with a custom layout: grids, images, on-screen game buttons outside the menus.
  • Chat, friends and global tables. There is no server.
  • In tile maps: tiles that animate on their own, and slopes.

3D models, image textures and recorded sounds are there, since version 1.2 of the contract: Models, textures and sounds.

Which of these are in development: Roadmap.

Behaviors worth knowing#

  • A piece created with mundo.crear exists at once, but touches, rays, mundo.piezasEn and collisions see it from the next frame.
  • Moving platforms carry players and actors, but not the dynamic pieces on top of them.
  • A platform that moves sideways pushes the player only approximately; one that comes down on them does not crush them. To crush or push for certain, use a danger tag and jugador.reaparecer().
  • The rotacion of a dynamic piece is approximate: it comes from its physical orientation.
  • mundo.rayo hits only solid pieces and ignores players.
  • The camera treats each piece as its bounding box when deciding whether it blocks the view; cristal pieces and small ones do not count. If it gets in the way next to tall pieces: camara.fijar({ evitarObstaculos: false }).
  • Labels are drawn on top of the world: pieces in front of them do not cover them.

Next#