**quadplay✜** Fantasy Console by [Casual Effects](https://casual-effects.com)
[*Quick Start*](#quickstart) ∙ [API](#standardlibrary) ([*Input*](#standardlibrary/input) ∙ [*Physics*](#standardlibrary/physics) ∙ [*Graphics*](#standardlibrary/graphics) ∙ [*Sound*](#standardlibrary/sound) ∙ [*AI*](#standardlibrary/ai) ∙ [*Network*](#network))
[Assets](assets.md.html target=_blank) ([*Sprites*](assets.md.html#sprites target=_blank) ∙ [*Fonts*](assets.md.html#fonts target=_blank) ∙ [*Sounds*](assets.md.html#sounds target=_blank))
[Controls](#controls) ∙ [Symbols](#symbols) ∙ [JSON Formats](#projectfiles) ∙ [Language Spec](#pyxlscriptlanguage) ∙ [Change Log](changelog.md.html)
> "Imagine the year 1989 in a fantasy timeline. Instead of chasing > realistic 3D, game consoles get modern tools and mechanics for > 2D. is the fantasy predecessor of Nintendo64 and > PlayStation that puts fun first in game jams and education. Its > retro constraints scope art and code for success. 's > modern scripting, physics, local and online multiplayer, and AI > empower easy implementation and great gameplay." [**quadplay✜**](../console/quadplay.html?IDE=1) is a web-based fantasy game console for sprite-based 2.5D games. It lets you create games with the power and consistency of a game console and the flexibility of modern tools and web deployment. Because it is free, is easy to use, and runs everywhere, is perfect for hobby coding, a game programming course, teaching yourself programming and game development, and game jams. The integrated development environment (IDE) is packaged as an emulator with the tools that you need to make games: an editor, debugger, and profiler. **Virtual Hardware Features** - Support for 1-4 players, including automatic remote play - Standardized controller with D-pad + 8-button gamepads (SNES/PS1/Saturn-style) and automatic device-specific button prompts on all platforms - Touch screen/mouse input - 60 fps, with autoscaling on low-end machines - 384 x 224 pixel screen = 12:7 aspect ≈ 16:9.3 - 4096 sRGB (4:4:4) colors - Native 2.5D graphics with order-independent 16-level transparency - Bloom, motion blur, and phosphor burn-in post-processing - 2D rigid body, joint, and motor physics - 2D positional audio with pitch, volume, and rate - 10.5 MB of total sprite memory - Max 128 sprite and font sheets, max size 1024x1024 - 262 kB offline save data per game - Pathfinding and board game AI - Local and online multiplayer - Optional I/O extension modes for 640 x 360, 320 x 180, 192 x 112, 128 x 128, and 64 x 64 screen modes; analog sticks and triggers; dual D-pads; multitouch **Software Features** - [Export](#deployinggames) to your own website, itch.io, or github pages - Games run on web, mobile, desktop, tablet, Jetson Nano, Raspberry Pi 4/400 - Free for commercial, personal, and educational use - Friendly and powerful PyxlScript Python-like scripting language - Run locally and offline, using a full development environment that requires only Python + web browser, no binary installation - Hundreds of built-in Creative Commons assets - Full command line, external editor, and external art tool support for power users with automatic file sync - Open source: use online, fork, or run locally - Runtime PNG, MP3, TMX, JSON, YAML, TXT, and CSV data formats - Built-in support for collaboration via git version control from the IDE **API** - Sprite, pixel, circle, line, triangle, rectangle, and polygon drawing with rotation and scaling - Font, tilemap, and UI window drawing - Optional entity parenting hierarchy - Ray casting and collision detection - Postprocessing effects - Persistence (saving and loading) - Nestable state-machine "modes" - Per-mode callback "hooks" is ideal for game jams, and it has been used for many including [Ludum Dare](https://ludumdare.com/) and the [Global Game Jam](https://globalgamejam.org/). It has special support of MIDI I/O for [Alt.Ctrl.GDC](https://gdconf.com/alt-ctrl-gdc) and a 64x64 screen mode for [Lowrezjam](https://itch.io/jam/lowrezjam-2022). Quick Start ==================================================================================== Welcome to !
Download Windows Installer *macOS and Linux:* 1. Install [Python 3.7](https://www.python.org/) or newer 2. Unzip [](https://github.com/morgan3d/quadplay/archive/main.zip) to wherever you want 3. Run `quadplay` from the terminal
To make a game, select the "Tools" menu from inside the quadplay development environment and then the "New Game" option. Look at the Built-In Games section for more examples. If you've worked with other scripting languages, then you may want to read the PICO-8 section, Python section, or JavaScript section of this document for some starting tips. If you're new to programming, just look at the many sample games and examples that are included and try modifying your own copies of them. !!! Info Pro Developers Experienced programmers can use external editors with for both programming and directly modifying game and asset `.json` files by:
  • Optionally [clone the SDK with git](https://github.com/morgan3d/quadplay) to manage SDK upgrades
  • Install a code editor (_[Visual Studio Code](https://code.visualstudio.com/), [Atom](https://atom.io/), [Sublime Text](https://www.sublimetext.com), Vim, Emacs, etc._)
  • Install the PyxlScript extension for your editor: _[Visual Studio Code](#advancedtools/visualstudiocode)_, _[Vim](#advancedTools/vim)_, _[Emacs](#advancedtools/emacs)_
  • Put your games under `~/my_quadplay/`. The IDE always creates new games in the root of that path, but you can reorganize it to form a hierarchy to group games or manage different games using different version control repositories.
On Windows, click on the shortcut on your desktop to run . Or launch from the command line on Windows CMD, macOS Terminal, or a Linux terminal with the `quadplay` command in the root of the SDK. Here are some examples: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bash # Run the emulator and IDE. You can use Open Game from the Tools # menu to load games quadplay # Load the games/quadpaddle/quadpaddle.game.json sample program quadplay quad://games/quadpaddle # Load the space.game.json game that you've made (on Mac and Linux) quadplay ~/my_quadplay/space # Load the space.game.json game that you've made (on Windows) quadplay C:\Users\MYNAME\my_quadplay\space # Load your game and allow mobile clients to connect by scanning # the QR code displayed in the IDE (Mac and Linux) quadplay --serve ~/my_quadplay/space # ... Windows quadplay --serve C:\Users\MYNAME\my_quadplay\space # Launch quadplay as a standalone game kiosk quadplay --kiosk # Load a game from the web (their server must support CORS) quadplay https://morgan3d.github.io/somegame/foo.game.json ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For more advanced controls and to make the quadplay local server visible, you can run `tools/quadplay-server` instead. These examples assume that your directory structure is: ***************************************************************************** * 📂 HOME (~/ on Mac and Linux, %HOMEDRIVE%%HOMEPATH% on Windows) * | * +-- 📂 my_quadplay * | | * | '-- 📂 space * | | * | +-- 📄 space.game.json * | | * ⋮ ⋮ * * 📂 quadplay * | * +-- 📄 quadplay * +-- 📄 quadplay.vbs * | * +-- 📂 fonts * | | * | ⋮ * | * +-- 📂 games * | | * | +-- 📂 quadpaddle * | | | * | | +-- 📄 quadpaddle.game.json * | | | * | ⋮ ⋮ * | * +-- 📂 sprites * | | * ⋮ ⋮ ***************************************************************************** [Directory structure with the `quadplay` script in the root.] The directory and `.game.json` file do not have to have the same name. You can list the full path to the actual game file instead. Your games do not have to be stored in a subdirectory of the SDK. They can be anywhere on your system. Every time you push the Run button (F5) or Reload button (Ctrl+R) in the emulator within the web browser, it will reload your code and assets from disk. So, you can mix an external editor with the built-in IDE without worrying about synchronization. You can use the built-in assets from the developer kit, existing ones from sites like [OpenGameArt.org](http://opengameart.org) and [FreeSound](http://freesound.org), or create your own using: - PNG pixel art editor for sprites and fonts (_[Gimp](https://www.gimp.org/), [AseSprite](https://www.aseprite.org/), [Krita](https://krita.org/en/), Photoshop, GraphicsGale, etc._) - MP3 sound tools (_[BFXR](https://www.bfxr.net/), [Audacity](https://www.audacityteam.org/), etc._) - TMX map tool ([Tiled](https://www.mapeditor.org/), [TileKit](https://rxi.itch.io/tilekit)) _People playing your game don't need Python, just a web browser. See the Deploying Games section for more information._ Workflow ------------------------------------------------------------------------------------ You should create your programs however works best for you. Here's how I like to work in : 1. I start with a project file to load standard assets from `quad://` and a single *mode* called `Play.pyxl` that has no sections. I write a little code to draw the game. 2. Pretty soon, I need some state that persists between frames, so I separate `Play.pyxl` into top level initialization code and a `frame` *section*. I then declare variables in in the top level of the mode, and as my main loop grows too long, I start to define helper functions there as well. 3. At some point, I'm ready for *another mode*. This is usually for gameplay reasons, and not polish, because it is still early in the project. This might be a map or inventory screen, for example. As soon as there is a new mode, I need to have variables that can be seen by both modes. So, I also create a `common.pyxl` file or other *script* to store the global variables. I continue this process as more modes are required, periodically checking the mode system diagram in the IDE to ensure that the game flow is what I expected. 4. As code starts to become duplicated in the modes, I move generally useful helper functions out to scripts. When the scripts themselves get long, I start grouping them by functionality. For example, `particle.pyxl`, `animation.pyxl`, and `hud.pyxl`. This is a workflow that is good for prototyping, especially in game jam and hobby projects. I feel like keeping everything as simple as possible and then refactoring at growth points is best for productivity. Otherwise, it is hard to iterate in the beginning because you're stuck with big program abstractions in what is still a small program. Many prototypes don't go the distance to become big programs, anyway. There's no reason to overdesign early on. Built-In Games ------------------------------------------------------------------------------------ Five complete games and many examples are included to show how to use the API. If you are accessing this manual from a web site or your local machine running the emulator, use the links here to launch the game in your browser, press the Run button (F5) to start the game. ### Playable
[Quadpaddle](../console/quadplay.html?game=quad://games/quadpaddle&IDE=1) is a four-player cooperative game inspired by [_Breakout_]("https://en.wikipedia.org/wiki/Breakout_(video_game)"): - Powerups - Level-end animations - Particle system - Screen shake - Frame hooks - Coordinates with +Y pointing down ----------------------------
[Doubles de Pon](../console/quadplay.html?game=quad://games/doublesdepon&IDE=1) is 1-4 player action puzzle matching game: - Custom music - High scores - Menus and character selection - Multiple play modes - Complex animation "juice" ----------------------------
[across the lake](../console/quadplay.html?game=quad://games/across_the_lake&IDE=1) is an endless runner created for Ludum Dare 46 based on skipping stones over a lake in the evening. - Crossfading - Simple custom physics - Custom camera - Coordinates with +Y pointing up - Procedural content - Fake reflections - Parallax and pseudo 3D ----------------------------
[Morgan's Mini Mecha](../console/quadplay.html?game=quad://games/minimecha&IDE=1) is turn-based strategy game created in a one week game jam. It is inspired by _Into The Breach_, _Advance Wars_, and Shogi. - Turn based UI - Board game artificial intelligence player - Complex scripted transitions and animations - High scores - Procedural map terrain ----------------------------
Friendly Fishing
[Friendly Fishing](../console/quadplay.html?game=quad://games/friendly_fishing&IDE=1) Join your friends for a relaxing afternoon of fishing and you just might top the lake records. - Animation using `sequence()` and `delay()` - The built-in shapes spritesheet - Complex high-score system - Use of `z` as depth - Color `make_spline()` - Tap, hold, and press button processing ----------------------------
[Serpitron](../console/quadplay.html?game=quad://games/serpitron&IDE=1) is a four-player competive game derived from Snake-like games, including [TRON]("https://en.wikipedia.org/wiki/Tron_(video_game)"): - Level transitions using post effects - Drop shadows - Player selection - Sorting - Collisions ----------------------------
[HexFlipper](../console/quadplay.html?game=quad://games/hexflipper&IDE=1) is a one to four-player competive game of area control: - Computer player AI - Hex grids - Boardgame style logic - Music ----------------------------
[Ice Time](../console/quadplay.html?game=quad://games/icetime&IDE=1) is a four-player competitive or cooperative 2 vs. 2 ice hockey game: - 192 x 112 graphics - AI players - Cut scenes - Persistent data - Reflection effects ----------------------------
[Duality](../console/quadplay.html?game=quad://games/duality&IDE=1) is a one- or two-player game inspired by Pong and Breakout: - Animations - Simulation - AI player - Post Effects - Slow motion ----------------------------
[Gravity](../console/quadplay.html?game=quad://games/gravity&IDE=1) is a 2-4 player game inspired by SPACEWAR! - Uses only built-in assets - Charging and cooldowns - Post-Effects afterglow - Simulation - Emergent behavior ----------------------------
[Black Firmament](../console/quadplay.html?game=quad://games/firmament&IDE=1) is a 1-2 player game extending Gravity with full fleets. - AI players - Multiple modes - Title menu - Complex control schemes - Unit selection - Post-Effects afterglow - Significant physics simulation - Emergent behavior ----------------------------
[Beat the Gobblins](../console/quadplay.html?game=quad://games/beat_the_gobblins&IDE=1) is a single-player arcade battle arena: - High scores - Custom music - Animated title - Animated explosions - Character spawning ----------------------------
[Beyond Control](../console/quadplay.html?game=quad://games/beyond_control&IDE=1) is a game for 1-4 players controlling broken robots: - Hexgrid - Asynchronous animations - Turn based system - In-game user interface - Dynamic layout - Dynamic map - Animated map ----------------------------
[R. P. S.](../console/quadplay.html?game=quad://games/rps&IDE=1) is a rock-paper-scissors battle royale for up to four players: - Extensive animation - Use of modes for simplicity - Timed sound effects - Private choices in multiplayer - Combinatorial logic ----------------------------
[Rescue Roguelike](../console/quadplay.html?game=quad://games/rescue_roguelike&IDE=1) is a tactical turn-based adventure game inspired by Into the Breach and Rogue: - Constants - Touch and mouse interaction - Aseprite sprites - Extensive data-driven design with external YAML files ----------------------------
[But...Skunks?!](../console/quadplay.html?game=quad://games/but_skunks&IDE=1) is a one or two player rhythm game. ----------------------------
[Drop Bloq](../console/quadplay.html?game=quad://games/drop_bloq&IDE=1) 5-in-a-row game at 8x8 resolution (!) that plays on any device, including an Ableton MIDI controller (Push, Launchpad, APC, etc.). Also demonstrates built-in AI routines. ### Examples
![](../examples/starter/label128.png) [Starter](../console/quadplay.html?game=quad://examples/starter&IDE=1) is a project with a basic setup from which you can build your own.
![](../examples/helloworld/label128.png) [Hello, World](../console/quadplay.html?game=quad://examples/helloworld&IDE=1) is a minimal example of a program with one mode, one asset, and no sections or optional elements, and that simply puts text on the screen.
![](../examples/rpg/label128.png) [RPG Demo](../console/quadplay.html?game=quad://examples/rpg&IDE=1) is a simple example of a single-player RPG game design: - Multiple [modes](#modes): Play, Inventory, and Shop - Multi-layer map - Player movement with obstructions - NPC interaction
![](../examples/dual-stick/label128.png) [Dual-Stick Example](../console/quadplay.html?game=quad://examples/dual-stick&IDE=1) shows how to use dual-stick controls. The body of the tank is controlled by player 1 and the turret is controlled by player 2. When using a dual-stick game controller or keyboard, a single player can also use the right stick or right side of the keyboard to control the turret. - Dual-stick controls - Working with angles - Entity parenting - 320x180 resolution
![](../examples/maze/label128.png) [Maze](../console/quadplay.html?game=quad://examples/maze&IDE=1) is a demonstration of `map_generate_maze()`.
![](../examples/robot/label128.png) [Robot Example](../console/quadplay.html?game=quad://examples/robot&IDE=1) shows how to create and animate a deep entity hierarchy. - Entity parenting - Pivots that are not at the center of sprites - Using scale and rotation together - Graphics tricks for reflections
![](../examples/animation/label128.png) [Animation Example](../console/quadplay.html?game=quad://examples/animation&IDE=1) shows how to handle complicated sprite animations. - Sprite animations - Basic jumping physics - Changing direction - Modular items
![](../examples/vehicles/label128.png) [Vehicles Example](../console/quadplay.html?game=quad://examples/vehicles&IDE=1) contains examples of several vehicle controls from a free-direction top-down perspective. - 2.5D camera perspective - Using the ⓔ and ⓕ buttons - Simple vehicle simulation for control feel - Entities with moving child parts - 2.5D sprite stacking - Dynamic shadows - Minimap
![](../examples/fluid/label128.png) [Fluid Example](../console/quadplay.html?game=quad://examples/fluid&IDE=1) is a cellular automata fluid flow simulation with gravity and pressure.
![](../examples/roguelike/label128.png) [Roguelike Example](../console/quadplay.html?game=quad://examples/roguelike&IDE=1) uses the roguelike sprite set for tilemap rendering with a simple text-heavy UI section.
![](../examples/dynamic_accel/label128.png) [Dynamic Acceleration Example](../console/quadplay.html?game=quad://examples/dynamic_accel&IDE=1) is a simple demonstration of how various acceleration curves feel, changing only three constants: top speed, acceleration time and deceleration time. Up and down change the constants to fit different games.
![](../examples/boids/label128.png) [Boids Example](../console/quadplay.html?game=quad://examples/boids&IDE=1) implements the famous ["boids" flocking algorithm](https://en.wikipedia.org/wiki/Boids).
![](../examples/clouds/label128.png) [Clouds Example](../console/quadplay.html?game=quad://examples/clouds&IDE=1) demonstrates per-pixel graphics and multi-octave `noise()` functions. The virtual GPU is fast enough to process one point per pixel, but this technique is not recommended in general because the computations to produce the points tend to be too slow, even if this simple.
![](../examples/dark_drive/label128.png) [A Dark Drive](../console/quadplay.html?game=quad://examples/dark_drive&IDE=1) A simple horror game jam project using polygon drawing to create darkness around car headlights.
![](../examples/entity/label128.png) [Entity Example](../console/quadplay.html?game=quad://examples/entity&IDE=1) Shows different ways of constructing entities with text and sprites.
![](../examples/physics/label128.png) [Physics Example](../console/quadplay.html?game=quad://examples/physics&IDE=1) shows all of the features of the physics engine, including the debugging visualization.
![](../examples/physics_arrow/label128.png) [Physics Example](../console/quadplay.html?game=quad://examples/physics_arrow&IDE=1) shows how to use dynamic attachments in the physics engine to make objects connect at runtime, and how to simulate aerodynamics so that arrows will fly straight.
![](../examples/text/label128.png) [Text Example](../console/quadplay.html?game=quad://examples/text&IDE=1) shows how to use `replace()`, `draw_text()`, `join()`, `format_number()`, and `draw_sprite_corner_rect()` to build text-heavy interfaces.
![](../examples/fontpreview/label128.png) [Font Preview](../console/quadplay.html?game=quad://examples/fontpreview&IDE=1) shows how to render text in various styles and acts as a tool for previewing fonts when creating new ones. Use the arrow keys to scroll down and see more examples in the program, and edit the `game.json` file to see a different font.
![](../examples/highscore/label128.png) [High Score Example](../console/quadplay.html?game=quad://examples/highscore&IDE=1) demonstrates `load_local()` and `save_local()` to maintain a high score list, with `push_mode()` for creating a popup dialog.
![](../examples/vaporwave/label128.png) [Vaporwave Example](../console/quadplay.html?game=quad://examples/vaporwave&IDE=1) uses pseudo-3D techniques of pre-rendered sprites and polygon meshes.
![](../examples/sproing/label128.png) [Sproing Example](../console/quadplay.html?game=quad://examples/sproing&IDE=1) shows a squash and stretch effect for transforming sprites.
![](../examples/perceptual_color/label128.png) [Perceptual Color](../console/quadplay.html?game=quad://examples/perceptual_color&IDE=1) shows the difference between `perceptual_lerp_color()` and `lerp()`.
![](../examples/zcar/label128.png) [Z-Car Example](../console/quadplay.html?game=quad://examples/zcar&IDE=1) draws a 3D wireframe car using 2.5D graphics and CRT orange-screen retro phosophor effects.
![](../examples/lift_team/label128.png) [Lift Team](../console/quadplay.html?game=quad://examples/lift_team&IDE=1) combines physics and dynamic constraints for an asymmetric platformer setup, where one player flies a helicopter that can lift the other player's mech.
![](../examples/anim_entity_example/label128.png) [Animated Entity Example](../console/quadplay.html?game=quad://examples/anim_entity_example&IDE=1) Entity animation example simplified from _Beat The Gobblins_.
![](../examples/bezier_eye_creature/label128.png) [Bezier Eye Creature](../console/quadplay.html?game=quad://examples/bezier_eye_creature&IDE=1) uses splines to create natural curves for a soft bodied creature.
![](../examples/sequence_demo/label128.png) [Sequence Demo (transition effect)](../console/quadplay.html?game=quad://examples/sequence_demo&IDE=1) Shows how to use the sequence function to create a transition effect, which is factored out into an easy to drop in library. The sequence function lets choreograph series of frame hooks one after the other.
![](../examples/gridmove/label128.png) [Grid Movement](../console/quadplay.html?game=quad://examples/gridmove&IDE=1) Atari style grid movement with smooth interpolation and wrapping, similar to PAC-MAN, Centipede, etc.
![](../examples/islands/label128.png) [Islands](../console/quadplay.html?game=quad://examples/islands&IDE=1) contains optimized implementations of per-pixel rendering and plausible water and sailboat simulation.
![](../examples/cards/label128.png) [Cards](../console/quadplay.html?game=quad://examples/cards&IDE=1) is an example of creating and manipulating a deck of cards, including `add_frame_hook()` for flipping animations.
![](../examples/dice/label128.png) [Dice](../console/quadplay.html?game=quad://examples/dice&IDE=1) uses `scripts/dice.pyxl` to show how to make and roll customizable 3D dice for virtual board games.
![](../examples/piano/label128.png) [Piano](../console/quadplay.html?game=quad://examples/piano&IDE=1) Using `pitch` with `play_sound()` to adjust frequency, and data-driven input testing.
![](../examples/spritestack/label128.png) [Spritestack](../console/quadplay.html?game=quad://examples/spritestack&IDE=1) is a little engine for Grand Theft Auto 2.5D rendering and physics of a 2D game using sprite stacking and camera perspective.
![](../examples/speedstreet/label128.png) [Speed Street](../console/quadplay.html?game=quad://examples/speedstreet&IDE=1) is the setup for a four-player competitive racing game inspired by [_Excitebike_](https://en.wikipedia.org/wiki/Excitebike) and Tony Hawk games, using: - Entity hierarchy - Simple custom physics - Orthographic 2.5D graphics - Coordinates with +Y pointing up - `override_color`
![](../examples/planetgen/label128.png) [Planet Generator](../console/quadplay.html?game=quad://examples/planetgen&IDE=1) uses built-in assets to create random 3D planets with moons, stars, atmosphere, and rings.
![](../examples/change_res/label128.png) [Change Resolution](../console/quadplay.html?game=quad://examples/change_res&IDE=1) is an example of using `set_screen_size()` to change resolution at runtime.
![](../examples/input/label128.png) [Input Example](../console/quadplay.html?game=quad://examples/input&IDE=1) is a demonstration and test of the full input API. It uses the cross-platform controllers and touch as well as the extended analog stick and mouse APIs. Shows how to completely hide the OS mouse cursor with `device_control("set_mouse_cursor", "none")`.
![](../examples/touch/label128.png) [Touch Example](../console/quadplay.html?game=quad://examples/touch&IDE=1) uses the mouse/touch API.
![](../examples/countdown/label128.png) [Countdown Example](../console/quadplay.html?game=quad://examples/countdown&IDE=1) shows how to used `local_time()` and perform time zone math.
![](../examples/hex/label128.png) [RPG Demo](../console/quadplay.html?game=quad://examples/hex&IDE=1) is an example of hex grids with coordinate system conversion, rendering, and click support.
![](../examples/tic_tac_toe/label128.png) [Tic Tac Toe](../console/quadplay.html?game=quad://examples/tic_tac_toe&IDE=1) shows how to take turns, and mix `gamepad_array`, `touch`, and `device_control("get_mouse_state")` to support mouseover/hover, touch screen, and gamepads in a single game user interface.
![](../examples/platformer/label128.png) [Platformer](../console/quadplay.html?game=quad://examples/platformer&IDE=1) is an example of classic 8-bit platforming physics, with jump, wall jump, wall slide, short jump, ledge forgiveness, hazards, and variable friction.
![](../examples/twin_analog/label128.png) [Twin Analog Example](../console/quadplay.html?game=quad://examples/twin_analog&IDE=1) shows how to use `device_control()` to access twin analog sticks and game controller triggers that are not standard on quadplay consoles.
![](../examples/kart/label128.png) [Kart Example](../console/quadplay.html?game=quad://examples/kart&IDE=1) is a perspective camera view with a textured ground plane similar to Space Harrier, Mario Kart, and Pilotwings.
![](../examples/warlock3D/label128.png) [Warlock Example](../console/quadplay.html?game=quad://examples/warlock3D&IDE=1) is 3D first person rendering similar to DOOM and Heretic, with shading, textured floors and walls, billboarded characters, pitch and yaw, and jumping. It demonstrates retro D-pad + shoulder button strafe, quadplay dual D-pad, conventional dual analog, and mouselook controls.
![](../examples/word_game/label128.png) [Word Game](../console/quadplay.html?game=quad://examples/word_game&IDE=1) Playful use of text and animated level transitions.
![](../examples/zoom2D/label128.png) [Zoom 2D](../console/quadplay.html?game=quad://examples/zoom2D&IDE=1) Simplified example of 2D zoom using `set_camera()`.
![](../examples/zoom/label128.png) [Zoom 3D](../console/quadplay.html?game=quad://examples/zoom&IDE=1) Simplified example of 3D perspective zoom using `set_camera()`. See also the Vehicle example.
![](../examples/private_view/label128.png) [Private Views](../console/quadplay.html?game=quad://examples/private_view&IDE=1) Private view example for online multiplayer using `set_screen_size()` and `VIEW_ARRAY`. Also shows how to streamline online game configuration using `start_guesting()`, `start_hosting()`, and `HOST_CODE`.
![](../examples/textspheres/label128.png) [Text Spheres](../console/quadplay.html?game=quad://examples/textspheres&IDE=1) Sample title screen animation converting text (from a pre-drawn PNG) into 3D shapes for animation.
![](../examples/midi_starrypad/label128.png) [MIDI Starrypad](../console/quadplay.html?game=quad://examples/midi_starrypad&IDE=1) Example of using the Donner Starrypad physical MIDI controller with , with all buttons, knobs, pads, and faders mapped.
![](../examples/midi_launchpad/label128.png) [MIDI Launchpad](../console/quadplay.html?game=quad://examples/midi_launchpad&IDE=1) Example of using the Novation Launchpad physical MIDI controller with and Sysex messages so that it acts as both an input and output display device.
![](../examples/midi_fcb1010/label128.png) [MIDI FCB1010](../console/quadplay.html?game=quad://examples/midi_fcb1010&IDE=1) Example of using the Behringer FCB1010 physical MIDI controller with with all default controls mapped.
![](../examples/midi_8x8/label128.png) [MIDI 8x8 Touch Jam](../console/quadplay.html?game=quad://examples/midi_8x8&IDE=1) Sample program for a MIDI controller jam, demonstrating the `midi_8x8.pyxl` helper script.
![](../examples/multitouch/label128.png) [Multitouch](../console/quadplay.html?game=quad://examples/multitouch&IDE=1) Example of `device_control()` for reading multitouch input on touch screens.
![](../examples/bezier_eye_creature/label128.png) [Bezier Eye Creature](../console/quadplay.html?game=quad://examples/bezier_eye_creature&IDE=1) Example of a library that uses a simple quadratic bezier + spring system to simulate bouncy cable/rope/arms.
![](../examples/anim_entity_example/label128.png) [Animation Entity Example](../console/quadplay.html?game=quad://examples/anim_entity_example&IDE=1) By way of an anim entity library, demonstrates how to read sprite frame timing from a sprite sheet authored in aseprite.
![](../examples/color_wheel/label128.png) [Color Wheel](../console/quadplay.html?game=quad://examples/color_wheel&IDE=1) shows the difference between `hsv()` and `artist_hsv_to_rgb()` hues and brightnesses.
### Inspiration Some games that were not made in , but which fit within the restrictions of quadplay and are sources of inspiration for their arcade graphics and gameplay follow. We've specifically determined that the main technical challenges of each of these can be met on .
- [TowerFall Ascension](http://www.towerfall-game.com/) - [Crawl](https://www.powerhoof.com/crawl/) - [Nuclear Throne](http://nuclearthrone.com/) - [Celeste](http://www.celestegame.com/) - [Undertale](https://undertale.com/) - [Monolith](https://store.steampowered.com/app/603960/Monolith/) - [Crypt of the Necrodancer](https://store.steampowered.com/app/247080/Crypt_of_the_NecroDancer/) - [Shovel Knight](https://yachtclubgames.com/shovel-knight/) - [Sonic Mania](https://www.sega.com/games/sonicmania) - [Spelunky Classic](https://spelunkyworld.com/original.html) - [Dwarf Fortress](http://www.bay12games.com/dwarves/) - [Contra]("https://en.wikipedia.org/wiki/Contra_(video_game)") - [Super Contra](https://en.wikipedia.org/wiki/Super_Contra) - [Kirby's Adventure](https://en.wikipedia.org/wiki/Kirby%27s_Adventure) - [Gradius](https://en.wikipedia.org/wiki/Gradius) - [Life Force](https://youtu.be/lgfTC3UVCVs) - [Gradius II](https://en.wikipedia.org/wiki/Gradius_II) - [Gradius III](https://en.wikipedia.org/wiki/Gradius_III) - [River City Ransom](https://en.wikipedia.org/wiki/River_City_Ransom) - [Double Dragon]("https://en.wikipedia.org/wiki/Double_Dragon_(video_game)") - [Super Double Dragon](https://en.wikipedia.org/wiki/Super_Double_Dragon) - [Castlevania II](https://en.wikipedia.org/wiki/Castlevania_II:_Simon%27s_Quest) - [Castlevania III](https://en.wikipedia.org/wiki/Castlevania_III:_Dracula%27s_Curse) - [Castlevania: Symphony of the Night](https://en.wikipedia.org/wiki/Castlevania:_Symphony_of_the_Night) - [Castlevania: Aria of Sorrow](https://en.wikipedia.org/wiki/Castlevania:_Aria_of_Sorrow) - [Castlevania: Dawn of Sorrow](https://en.wikipedia.org/wiki/Castlevania:_Dawn_of_Sorrow) - [Advance Wars](https://en.wikipedia.org/wiki/Advance_Wars) - [Advance Wars Dual Strike](https://en.wikipedia.org/wiki/Advance_Wars:_Dual_Strike) - [Out Run](https://en.wikipedia.org/wiki/Out_Run) - [Dig Dug](https://en.wikipedia.org/wiki/Dig_Dug) - [Batman](https://en.wikipedia.org/wiki/Batman:_The_Video_Game) - [Tetris](https://en.wikipedia.org/wiki/Tetris) - [Blades of Steel](https://en.wikipedia.org/wiki/Blades_of_Steel) - [Pac-Man](https://en.wikipedia.org/wiki/Pac-Man) - [PGA Tour Golf](https://en.wikipedia.org/wiki/PGA_Tour_Golf) - [Lode Runner](https://en.wikipedia.org/wiki/Lode_Runner) - [Star Fox](https://en.wikipedia.org/wiki/Star_Fox) - [Metroid](https://en.wikipedia.org/wiki/Metroid) - [The Legend of Zelda]("https://en.wikipedia.org/wiki/The_Legend_of_Zelda_(video_game)") - [Zelda II: The Adventure of Link](https://en.wikipedia.org/wiki/Zelda_II:_The_Adventure_of_Link) - [The Legend of Zelda: Link's Awakening](https://en.wikipedia.org/wiki/The_Legend_of_Zelda:_Link%27s_Awakening) - [The Legend of Zelda: A Link to the Past](https://en.wikipedia.org/wiki/The_Legend_of_Zelda:_A_Link_to_the_Past) - [Mega Man 2](https://en.wikipedia.org/wiki/Mega_Man_2) - [Super Mario Bros.](https://en.wikipedia.org/wiki/Super_Mario_Bros.) - [Super Mario Bros. 2](https://en.wikipedia.org/wiki/Super_Mario_Bros._2) - [Super Mario Bros. 3](https://en.wikipedia.org/wiki/Super_Mario_Bros._3) - [Rogue]("https://en.wikipedia.org/wiki/Rogue_(video_game)") - [NetHack](https://en.wikipedia.org/wiki/NetHack) - [Snake]("https://en.wikipedia.org/wiki/Snake_(video_game_genre)") - [Minit](https://store.steampowered.com/app/609490/Minit/) - [Chasm](https://bitkidinc.itch.io/chasm) - [Eliminator Boat Duel](https://www.youtube.com/watch?v=NM7ZNX0GUBI) - [The Eternal Castle](http://www.theeternalcastle.net/) - [DOOM]("https://en.wikipedia.org/wiki/Doom_(1993_video_game)") - [DOOM II](https://en.wikipedia.org/wiki/Doom_II) - [Heretic]("https://en.wikipedia.org/wiki/Heretic_(video_game)") - [Dark Forces](https://en.wikipedia.org/wiki/Star_Wars:_Dark_Forces) - [Marathon]("https://en.wikipedia.org/wiki/Marathon_(video_game)") - [Ultima Underworld](https://en.wikipedia.org/wiki/Ultima_Underworld:_The_Stygian_Abyss) - [Loom]("https://en.wikipedia.org/wiki/Loom_(video_game)") - [Day of the Tentacle](https://en.wikipedia.org/wiki/Day_of_the_Tentacle) - [Pilotwings]("https://en.wikipedia.org/wiki/Pilotwings_(video_game)") - [Super Mario Kart](https://en.wikipedia.org/wiki/Super_Mario_Kart) - [Sonic Drift](https://en.wikipedia.org/wiki/Sonic_Drift) - [F-Zero]("https://en.wikipedia.org/wiki/F-Zero_(video_game)") - [Space Harrier](https://en.wikipedia.org/wiki/Space_Harrier)
These are games that would fit the restrictions of except they use native vector displays in their original implementations: - [Asteroids]("https://en.wikipedia.org/wiki/Asteroids_(video_game)") - [Battlezone]("https://en.wikipedia.org/wiki/Battlezone_(1980_video_game)") - [Space War](https://en.wikipedia.org/wiki/Spacewar!) - [Tempest]("https://en.wikipedia.org/wiki/Tempest_(video_game)") These are games whose style and gameplay would fit, but that would exceed the resolution limitations of : - [Axiom Verge](http://www.axiomverge.com/) - [Into the Breach](https://subsetgames.com/itb.html) Programming Model ------------------------------------------------------------------------------------ The **PyxlScript** programming language is designed to make compact, readable games. It blends Python, Lua, and JavaScript syntax. Indentation and newlines signify blocks and ends of statements. The syntax is based on math notation and looks like pseudocode. Variables are explicitly declared and dynamically typed. There's lexical scope, first class functions, and literal expressions for objects and (zero-based) arrays. It is decidedly function-based instead of object-oriented. Most of PyxlScript programming is typical of any imperative, high-level language. It is similar to Python, Lua, JavaScript, C#, and Java. There are three special parts of the PyxlScript programming model that are designed to simplify programming arcade games. These special features are [*modes*](#modes), [*2.5D graphics*](#2.5dgraphics), and [*frame hooks*](#framehooks). ### Modes Your program consists of one or more *modes* which are game states that the player will experience. Common modes are "Play", "Title", "Inventory", "CutScene", and "GameOver". Use `set_mode()` to change which mode the program is in. Clicking on the "Modes" label on the left side of the IDE draws a diagram showing the modes and transitions for your program. ![Mode diagram for the Quadpaddle sample game, as shown in the IDE.](modes.png width=50% style="image-rendering:auto") For each mode, you specify what code runs every *frame*. This is the body of the inner loop. Modes have capitalized names and are defined in a script file with the same name. The simplest implementation of a mode is just this per-frame code. The `mode_frames` variable tracks the number of frames since this mode started. `set_mode()` changes the mode and leaves a note in the debugging output explaining why the mode changed. It is also used by the IDE to visualize the state machine of your program and label the transitions, as in the diagram above. `get_mode()` returns the current mode, which acts like a special constant. You can also separate the mode file into different sections: - Top level: runs once when the game is first started. Usually just variable and function declarations. - `enter`: event that runs when this mode becomes active by another executing `push_mode()` or `set_mode()`. `enter` may be followed by an argument list that will be filled with the values passed to `set_mode()` or `push_mode()`. - `frame`: event that runs every frame while this mode is active. Put your drawing and simulation code here. - `leave`: event that runs when the game changes to another mode by executing `pop_mode()` or `set_mode()` itself. - `pop_mode`: event that runs when the game returns to this mode from another by `pop_mode()`. Note that `leave` and `pop_mode` do not run on `reset_game()` or `quit_game()`. You can think of these as callbacks, although they are not regular functions and can only be invoked directly by the console. You may be familiar with the concept of a [state machine](https://en.wikipedia.org/wiki/Finite-state_machine), also called a finite automata or flow chart. Your program is a state machine with the modes as the states. !!! Note: Transition Hints The IDE automatically constructs the mode diagram from your program. It is [impossible](https://en.wikipedia.org/wiki/Undecidable_problem) to do this for all programs, but it can usually do a good job. When the automatic algorithm misses a link, you can give it a hint using a commented out `set_mode()` call in your mode file. For example, `// set_mode(GameOver) because "0 lives"` will add a link from the current mode to the `GameOver` mode. The syntax of a mode with multiple sections is similar to [Markdown](https://en.wikipedia.org/wiki/Markdown) format: 1. The name of the mode on the first line 2. A line of `=`, `═`, or `⚌` characters to underline the mode name 3. The top-level code 4. The sections, each with a name underlined by `-`, `-`, `─`, `—`, `━`, or `⎯` characters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript Menu ═══════════════════════════════════════════════════════════ // A good place to declare state only used by this mode let selection const choices = ["Continue", "Show Map", "Abandon Mission"] enter(value) ─────────────────────────────────────────────────────────── // Can also be enter without any arguments or parentheses // Reset any state selection = 0 play_sound(menuOpen) frame ─────────────────────────────────────────────────────────── set_background(rgb(1, 1, 0)) // Up and down to change the choice selection = (selection + 3 + gamepad_array[0].y) mod 3 // Draw the menu let pos = screen_size / 2 - xy(0, 50) draw_text(menuFont, "→", pos + xy(-50, 16 selection), #00F) for c in choices: draw_text(menu_font, c, pos, #000) pos.y += 16 // Button press to choose if gamepad_array[0].aa: if selection == 0: set_mode(Play) because "Chose play" else if selection == 1: set_mode(Map) because "Chose map" else: push_mode(ConfirmDialog) because "Chose abandon" pop_mode(confirmed) from ConfirmDialog ─────────────────────────────────────────────────────────── if confirmed: set_mode(Title) because "Confirmed abandon" leave ─────────────────────────────────────────────────────────── play_sound(menuClose) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [A sample in-game menu mode using all of the optional mode sections.] If you're accustomed to object-oriented languages, then you might think of the whole program as: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Java public class Menu extends Mode { // These variables are the top-level code protected int selection; public void enter(int value) { selection = 0; play_sound(menuOpen); } public void frame() { selection = (selection + gamepad.y + 3) % 3; } drawChoices(selection); if (gamepad.a) { if (selection == 0) { set_mode(play); } ... } } public void pop_mode(boolean confirmed, Mode from) { if (from == confirmDialog) { ... } } public void leave() { play_sound(menuClose); } } Mode menu = new Menu(); ... menu.enter(); while (get_mode() == menu) { menu.frame(); } menu.leave(); ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [This is the rough equivalent of the menu mode PyxlScript code in an object-oriented language. You don't write all of this boilerplate in PyxlScript!] #### Unmatched Push and Pop Mode Push and pop on modes aren't always paired, because sometimes an exceptional change to state flow occurs. ********************************************************************* * .-----. push_mode(B) .-----. set_mode(C) .-----. * ( A )--------------->( B )-------------->( C ) * '-----' '-----' '-----' ********************************************************************* [Actual code executed with unmatched push/pop mode calls.] For example, if mode `A` executes `push_mode(B)`, and then `B` executes `set_mode(C)` (instead of popping), the order of events triggered will be: 1. `B: enter` 2. `B: leave` 3. `A: pop_mode from B` 4. `C: enter` This is as if `B` had run the `pop_mode` and then `A` directly transitioned to C. ********************************************************************* * push_mode(B) * .-----.----------------->.--+--. .-----. * ( A ) ( B ) ( C ) * '--+--'<-----------------'-----' '-----' * | pop_mode() ^ * | | * '----------------------------------------------' * set_mode(C) ********************************************************************* [Equivalent behavior triggered by the actual code, so that PyxlScript can ensure matched push/pop] ### 2.5D Graphics Graphics in PyxlScript are similar to other 2D rendering APIs. You can set the background color, draw basic shapes and text, and render sprites with some features such as flipping and rotation. There's special support for drawing whole tile maps as well. All drawing commands support an [*alpha channel*](https://en.wikipedia.org/wiki/Alpha_compositing) for transparency. What is special about the 2.5D graphics in PyxlScript is that each drawing command has an associated *z value*. If two commands are at the same z value (after camera and transform are applied), then whichever comes second will draw on top of the other one. Commands that have explicit, different z values will render from low to high values. The z value for a drawing command can be set using the `z` argument. If the `z` argument is not specified, then the `pos.z` argument is used. If there is no `pos.z` value, then the pre-transform value of 0 is used. This means that you can issue drawing commands in whatever order is convenient and let the rendering system organize them as you wish. It also makes it easy to render orthographic games such as [_Double Dragon_]("https://en.wikipedia.org/wiki/Double_Dragon_(video_game)"), isometric games such as [_Populous_]("https://en.wikipedia.org/wiki/Populous_(video_game)"), and other games with semi-3D effects. The included [_Speed Street_](#playable) game demonstrates this. Although the screen is cleared to the background color every frame, all of the drawing commands are stored for one previous frame. This allows dialogue and inventory screens to re-render the previous mode's last frame using `draw_previous_mode()` so that they can render on top of them. This rendering retains z-order, so you can even inject new graphics in between objects from a previous mode's frozen image. ### Frame Hooks In addition to the mode's frame code, you can dynamically register hook functions to run at the beginning of every frame using `add_frame_hook()`. These are useful for timers and animations. For example, you can use this to draw an explosion every frame until it dissipates or to shake the screen for three frames. Frame hooks can be tied to only run in a specific mode or to run in all modes. You can also specify a separate hook to run each frame from the one to run when it expires. The closest relative to frame hooks is JavaScript's `setTimeout()` function, although it is much more limited. ### Network Multiplayer All games automatically support *shared-screen* network multiplayer through the built-in system menu's Online options. The programming model is identical for local and network games and the runtime handles everything for you. There are no explicit synchronization messages or connections required per game. See the Network section of the API for ways that games can customize online play. This includes simplifying the user experience for creating and joining games and *private per-player views*. Coming from... ------------------------------------------------------------------------------------ Here are some tips to get you started quickly when you're coming to PyxlScript from other popular programming languages. ### PICO-8 If you're used to programming in PICO-8 Lua, make these changes for the PyxlScript: - Create a *mode* for your project. - Put your top-level and `_init` code into the top level - Put your `_update` and `_draw` combined into the `frame` section - Declare variables with `let` and `const` instead of `local` - Arrays start at 0 - Use `size(x)` instead of `#x` - Use `:` between the beginning of a block and its body - Blocks have no `end` statements and indentation is significant - Use `:` within object literals instead of `=` - It is safe to mutate containers inside iterator loops - You must specify a font for `draw_text()`. [Several](assets.md.html#fonts) are provided. - Enjoy the overloaded operators, array functions, z-order, and alpha blend ### Python The PyxlScript language feels a lot like Python, with these changes: - Comments begin with `//` instead of `#` - Explicitly declare variables with `let` and `const` - Use `size(x)` instead of `len(x)` - Use `debug_print` instead of `print` - Use `for low <= i < high` instead of `for i in range(low, high)` - The `==` operator is pointer equality (Python `is`). Use `equivalent` for object structural equality tests. - Variables have block scope instead of function scope - Function variables have lexical scope (no `global` statement required) - No integer division (use `floor`) - No comparison operator chaining - Use `keys(obj)` and `values(obj)` instead of `obj.keys()` and `obj.values()` (although you almost never need either due to the `for` loop syntax) - Use `for value at key in obj:` instead of `for key, value in obj.items():` - Use `iterate()` instead of `map()`, `filter()`, or list comprehensions - Use `x = if t then a else b` for Python `x = a if t else b` - Use `true`, `false`, `nil` instead of `True`, `False`, `None` - Use `def foo(a, b, ...rest)` instead of `def foo(a, b, *rest)` - Use `foo(a, b, ...rest)` instead of `foo(a, b, *rest)` - Use `[...array1, ...array2]` or `extended(array1, array2)` instead of `array1 + array2` - Use `{...objA, ...objB}` for Python `listA + listB` or `{**objA, **objB}` - Use `extend(a, b)` for Python `objA.update(objB)` and `listA.extend(listB)` - Use `extend(array1, array2)` instead of `array1 += array2` - Use `def foo(x default y)` instead of `def foo(x = y)` - Default function parameters are evaluated once per call instead of once per program, so they do not capture and share values ### JavaScript The PyxlScript language feels a lot like JavaScript strict mode, with these changes: - Use PyxlScript `==` in place of JavaScript `===` - Use `:` and indentation instead of `{}` for defining blocks - Use `def` instead of `function` - No `;` required, except for multiple statements on the same line - Use `size(x)` instead of `x.length` - Use `for v at k in object:` instead of `for (k in object)` - Use `for v in array:` instead of `for (v of array)` - Use `for k < size(array):` instead of `for (k in array)` - It is safe to mutate containers inside iterator loops - There are no anonymous functions - Function names are not lifted to the containing function's scope - Use `x = if t then a else b` for JavaScript `x = t ? a : b` - Use `debug_print` instead of `console.log` - Use `a default b` instead of `a ?? b` - Enjoy the overloaded operators for vectors Online Multiplayer ==================================================================================== Quadplay supports safe online play for shared-screen multiplayer games over the internet. This is similar to the Steam "Remote Play Together" feature. Your quadplay *Host Code* is a series of six short words generated by quadplay. This secures your gaming session and lets your friends connect. Only share your Host Code with people you want to play with. You can change to a new, random Host Code at any time. It is almost impossible for someone to guess your Host Code--they would be more likely to win the lottery 100 times first. Online play requires the hosting player to send their secret Host Code to the other player _outside_ of the game. You can do this by text, email, telephone, Discord, Slack, and so on. The hosting player starts hosting either from the quadplay launcher's Online tab or from the pause menu's Online entry. The other players then connect from the launcher's Online tab or the pause menu's Online entry. Your computer will remember the Host Codes of other computers you have connected to recently. Although quadplay already restricts play to people you have trusted to exchange a Host Code with, for privacy and further safety for children, there is no in-game text or voice chat in quadplay. Use a third-party gaming chat service such as Discord, SteamChat, Overtone, TeamSpeak3, Mumble, or Slack if you wish to have text and voice chat. You can configure a 7-letter, all capital Online Name for your computer that is used to identify you. This will help you to know which Host Code goes with which person, and tells the host who you are when you connect. When running a kiosk or setting up a local development station on which you wish to prevent players from accessing the online multiplayer feature, launch the quadplay script with the `--offline` flag. Tools ==================================================================================== Development Environment ----------------------------------------------------------------------------- The browser development environment supports several alternative layouts, which are selected from the buttons on the upper right of the screen. I expect most experienced programmers to use external editors such as Visual Studio Code and Photoshop and put the development environment in Full Screen or Test layout. Most newer programmers will directly use the built-in integrated development environment within the browser in the Develop and Debug modes. ![](../console/ui-fullscreen.png width=32) Full Screen : Play your game full screen. Good for playtesting. ![](../console/ui-touchscreen.png width=32) Touch Screen : Play your game with single-player mobile touch controls. ![](../console/ui-test.png width=32) Test : Run with a large emulator and the debugger visible. This is good for using on the left or right half of the screen when running an external editor on the other half. ![](../console/ui-debug.png width=32) Debug : Pixel-doubled emulator and compressed text editor. Good for debugging graphics code and tuning constants. ![](../console/ui-develop.png width=32) Develop : 1:1 pixel emulator with text editor and debugger. Good for working on gameplay code. ![](../console/ui-edit.png width=32) Edit : Full-screen code editor. Good for writing lots of code. ![](../console/ui-ghost.png width=32) Ghost : Show-off mode for livestreaming, with the full screen code editor and the game running behind it full screen, like a ghost. Press the Play button to switch input to the game in the background. Game touch input is obviously not available in this layout. There's also a special "kiosk" mode that the `quadplay` script can run in for removing the top control bar when running on an arcade machine or other public display mode. Emulator ------------------------------------------------------------------------------------ The main tool is the console emulator, which you can launch with the `quadplay` command at the command line. The emulator accepts keyboard and game controller input, as well as directly pressing its buttons on screen for mobile. On a gamepad, you must press a button before directional input will register in a web browser. Pressing the "Play" button on the editor automatically transfers keyboard focus to the emulator. The emulator has special keyboard keys for: - F5: run - F6: screenshot - F8 or Ctrl+G: start/stop recording GIF [^safariGIF] - F10: step one frame - Ctrl+Break or Ctrl+C - Ctrl+R or Ctrl+F5: reload On macOS, either Control or Command can be used for "Ctrl". [^safariGIF]: On Safari, you have to explicitly allow popups for the website for this to work. Go to Preferences --> Websites --> Pop-up Windows and check "allow" for `localhost` (or whatever address you access from). ### Controls presents virtual controllers for four players. You can use the on-screen buttons on mobile and touch screens for a single player, a keyboard for up to two players, and connect most USB or Bluetooth game controllers for up to four players. Physical controllers work on all operating systems and mobile devices. For two-player keyboard input, you can use two USB or Bluetooth keyboards on the same computer, or have both players share a single keyboard. The order of physical controllers can be changed from the Controls panel in the quadplay debugger. This is needed when configuring an arcade kiosk with physical positions for the players, or to allow mixed keyboard and physical controller play.
![](../console/xbox_controller.png style="border:none; image-rendering:auto" width=50%) ![](../console/gamepad.png style="border:none; image-rendering:auto" width=50%) ![](../console/keyboard.png style="border:none; image-rendering:auto" width=75%)

Physical controllers give the best experience. We have successfully tested the following, and most are autodetected by quadplay:
![](sn30-pro.jpg style="image-rendering:auto")
[8BitDo SN30 Pro](https://amzn.to/35XGEl9) (SN or Classic)
![](sn30.jpg style="image-rendering:auto")
[8BitDo SN30](https://amzn.to/38ay83F)
![](zero2.jpg style="image-rendering:auto")
[8BitDo Zero 2](https://amzn.to/2Ny5nWN)

![](xboxone.jpg style="image-rendering:auto")
[Microsoft Xbox One Wired Controller](https://amzn.to/36Y7CdR)
![](xboxone.jpg style="image-rendering:auto")
[Microsoft Xbox One Wireless
Controller for Windows 10](https://amzn.to/35U36vz)
![](xbox360.jpg style="image-rendering:auto")
[Microsoft Xbox 360 Wired Controller](https://www.microsoft.com/accessories/en-ww/products/gaming/xbox-360-controller-for-windows/52a-00004)

![](joy-con.jpg style="image-rendering:auto")
[Nintendo Joy-Con](https://amzn.to/30recqP)
![](switch-pro.jpg style="image-rendering:auto")
[Nintendo Switch Pro Controller](https://www.amazon.com/Nintendo-Switch-Pro-Controller/dp/B01NAWKYZ0)
![](thrustmaster.jpg style="image-rendering:auto")
[Thrustmaster T-Flight HOTAS X Flight Stick](https://amzn.to/2Tvf8J4)

![](xarcade-dual.jpg style="image-rendering:auto")
[X-Arcade Dual](https://shop.xgaming.com/collections/arcade-joysticks/products/x-arcade-dual-joystick-usb-included)
![](genesis.jpg style="image-rendering:auto")
[retro bit Sega Genesis](http://retro-bit.com/sega-collaboration)
![](m30.jpg style="image-rendering:auto")
[8bitdo M30 2.4G](https://www.8bitdo.com/m30/)

![](dualshock4.jpg style="image-rendering:auto")
[PlayStation DualShock 4 Wireless Controller](https://www.playstation.com/en-us/explore/accessories/gaming-controllers/dualshock-4/)
When trying to get a new controller working, first use https://html5gamepad.com/ to verify that your OS and web browser can see it. Then use the controller with quadplay. 8BitDo controllers work in wired, wireless, and [XPad](https://support.8bitdo.com/xpad.html) mode. They seem most reliable on Windows and Linux. Be sure to install the latest firmware update from the 8BitDo website. Left and right Joy-Cons work individually in horizontal mode. They cannot be joined as on Switch. The Switch Pro controller and the SNES and NES controllers work normally. On macOS, Xbox wired controllers require a free, [open source driver](https://github.com/360Controller/360Controller/issues). Xbox wireless controllers do not work on Mac. Different controllers work with different browsers on macOS. For example, Xbox 360 controllers work well with Chrome and the Nintendo SNES controller works with Safari. On macOS PS4 controllers work in Bluetooth mode (Xbox wireless controllers may also work in Bluetooth mode). Press the PS and SHARE buttons simultaneously to pair. If you have any problems, delete `/Library/Preferences/com.apple.Bluetooth.plist`, shut down and then start (do not warm reboot) and then re-pair. Stadia and [Logitech](https://www.logitechg.com/en-ca/products/gamepads/f310-gamepad.html) controllers are intended to work but I have not tested them. Please let me know if you are using them successfully. All browsers have controller support, but Chrome seems to support the most different controllers reliably on all platforms. Input | Player 1 Key| Player 2 Key| Xbox | Playstation| SNES | Switch Pro :----------:|:-----------:|:-----------:|:----------:|:----------:|:------:|:-----------: | W or ↑ | I | ▲ | ▲ | ▲ | ▲ | A or ← | J | ◀ | ◀ | ◀ | ◀ | S or ↓ | K | ▼ | ▼ | ▼ | ▼ | D or → | L | ▶ | ▶ | ▶ | ▶ ⓐ | B or space | / | Ⓐ | ╳ | Ⓑ | Ⓑ ⓑ | H or enter | ' | Ⓑ | ◯ | Ⓐ | Ⓐ ⓒ | V | . | Ⓧ | ▢ | Ⓨ | Ⓨ ⓓ | G | ; | Ⓨ | △ | Ⓧ | Ⓧ ⓔ | L.shift | N | LB | L1 | L | L ⓕ | C or R.shift| alt/option | RB | R1 | R | R ⓠ | 1 or Q | 7 || Share | Select | ⓟ | 4 or P | 0 | ☰ | Options | Start | [Sample controller mappings. supports many more, including JoyCons and flight sticks.] After launching a quadplay game, you must press a button on the controller for the web browser to allow access to it. quadplay will automatically detect the controller and change to appropriate button prompts for most standard devices, including Xbox, PlayStation, Switch, Stadia, and Thrustmaster controllers. If your device is not mapped correctly, just enter the pause menu on any game and select "Set Controls". I've found that the easiest Bluetooth controller to pair on macOS is a Switch JoyCon. Turn on Bluetooth on your Mac and open the Bluetooth preferences. Hold the controller horizontally and hold the tiny sync button [you have to take off the black wrist strap holder temporarily to access this button]. You'll see the controller appear on your Mac. Click on its name and select "connect". When you want to re-pair the controller with your Switch, just slide it back onto the Switch and it will sync. 8BitDo controllers seem to work very reliably on Windows and Linux but are inconsistent on macOS. The [X-Arcade](https://shop.xgaming.com/) controllers work with quadplay. If you have a recent (2020 or later, or with the Tri-Mode PCB upgrade) X-Arcade controller, set XInput Mode 4 with the switch on the back. [Install the latest firmware](https://support.xgaming.com/support/solutions/articles/12000051593-x-arcade-tri-mode-firmware-update-download-instructions), version 19.4 or later. On macOS, also install the open source [Xbox360 controller](https://github.com/360Controller/360Controller/releases) driver. The Chromium-based browsers (Chrome, Edge, and Brave) provide the most reliable mapping. Safari and Firefox have inconsistent results. #### Arcade Controller Mappings The X-arcade default mapping is below. For other operating systems, browsers, or older controllers, just use keyboard mode and program the buttons to match the quadplay keyboard controls. ![On Windows, put your X-Arcade in X-input mode 4 and it will automatically provide this mapping.](xarcade-mapping.png style="border:none; image-rendering:auto" width=50%) ![When using an X-Arcade controller in keyboard mode 2, program it like this.](xarcade-keyboard.png style="border:none; image-rendering:auto" width=50%) Other ten-button arcade fightsticks have two common button layouts. The symmetric ECDF layout used by inexpensive Honcam and PXN sticks puts the "shoulder" buttons that correspond to ⓔ and ⓕ on the left and right of the other "face" buttons. The CDEF layout used by more expensive Mayflash, Hori, eTokki, Qanba, and other sticks puts the "shoulder" buttons to the right of the "face" buttons. The appropriate button prompts will appear in quadplay for each one, but you'll have to change which fingers you use to play games depending on the physical layout. ![Symmetric ECDF fightstick layout.
(Example only; this controller has not been tested with quadplay)](symmetric-arcade.jpg width=80% style="image-rendering:auto") ![Asymmetric CDEF fightstick layout.
(Example only; this controller has not been tested with quadplay)](right-arcade.jpg width=80% style="image-rendering:auto") #### Dual D-Pad When `dual_dpad` is enabled (in the game.json file or via the project checkbox), player 1's _physical_ controller right stick can override the the player 2 (`gamepad_array[1]`) _virtual_ controller D-pad. This allows you to make digital twin-stick games that work with both default keyboard controls and a single modern dual-stick controller. ### Analog Sticks Quadplay exposes system features beyond the standard quadplay capabilities using the `device_control("get_analog_axes", ...)` function for low-level system access. You can read analog sticks, steering wheels, foot pedals, flight sticks, etc. directly using this API. Games that use these features will not work on all quadplay devices. But, it is a great way to program for a specific platform that you have in mind or have built. ### Resolution The emulator screen tries to maintain an integer multiple of the native pixel resolution to prevent distortion of pixel art. When running on a very high resolution screen (greater than 2.5x), it relaxes this constraint to use the full screen effectively. As a result, the emulator is pixel-perfect on small displays and fills the screen on large ones. supports the following resolutions, all with 4:4:4:4 RGBA color and 60 Hz refresh: - 640x360 (½ 720p, ⅓ 1080p, ¼ 1440p) - 384x224x4 = 768x448, for network multiplayer private views - 320x180x4 = 640x360 (½ 720p), for network multiplayer private views - *384x224* (default and recommended) - 320x180 (¼ 720p, ⅙ 1080p, ⅛ 1440p) - 192x112 - 128x128 - 64x64 For reference when importing assets or designing games inspired by previous work, here are the resolutions of classic and retro machines: - 960x544 PlayStation Vita - 800x480 Raspberry Pi official 7" touchscreen - 640x480 PiBoy DMG - 480x320 Odroid Go Advance - 480x272 PlayStation Portable - 480x270 Hyperlight Drifter - 400x240 Playdate, 2DS, 3DS - 384x224 - 320x240 "240p", QVGA, TowerFall Ascension - 320x180 Celeste and other Switch pixel art games - 320x200 VGA, including Amiga 500 and Commodore 64 - 280x192 Apple II - 256x240 NES and TurboGrafx-16 - 256x224 SNES - 256x192 Nintendo DS and Sega Master System - 240x160 Game Boy Advance - 240x136 TIC-80 - 160x192 Atari 2600 - 160x144 Game Boy Color and Sega Game Gear - 160x102 Atari Lynx - 128x128 Pico 8 quantize✜ --------------------------------------------------------------------------------------------- [quantize✜](../tools/quantize.html) is a standalone browser tool for converting any image format that your browser can load into a sRGB8 PNG for . While the console emulator can load any PNG file, if you convert them using quantize✜ you will have control over the color reduction process and be able to produce much smaller files for distribution. fontgen✜ --------------------------------------------------------------------------------------------- [fontgen✜](../tools/fontgen.html) is a standalone browser tool for producing rough font images from fonts installed on your local computer. You can then screen capture these and edit them in your favorite pixel editor to clean them up. fontpack✜ --------------------------------------------------------------------------------------------- [fontpack✜](../tools/fontpack.html) is a standalone browser tool for taking a font PNG file with regular tiles (such as the one produced by fontgen✜) and packing it tightly. It will generate the output PNG and the `.font.json` file for you. fontpack✜ can also show what the generated characters will look like when loaded by . If the generated ones look good, _do not put them in the .png_. Instead, store your file with lots of empty space so that it will compress better, and let regenerate them at load time. Only for ones where generation produces poor quality do you need to explicitly draw them. Usually large fonts will generate characters well but for very small fonts you must hand-tweak characters with accents or button prompts to get a readable result in a few pixels. scalepix✜ --------------------------------------------------------------------------------------------- [scalepix✜](../tools/scalepix.html) is a standalone browser tool for upscaling a font or sprit PNG file to twice the resolution in each dimension, for upscaling existing assets or importing NES, PICO8, and Game Boy assets. Standard Library ========================================================= Standard library functions are first-class values. They can be stored in data structures and variables. In PyxlScript syntax, a parameter shown with `default` after it, for example: `i default 0` indicates that this is an optional parameter and the value after `default` be used if none is supplied. Explicitly specifying `nil` for any parameter forces its default value. Input ----------------------------------------------------------- ### Gamepads The gamepad objects in the four-element `gamepad_array` each have the following properties: Property | Values | Meaning -------------------|----------|---------------------------------- `index` | 0,1,2,3 | Index of this gamepad in `gamepad_array`. The player name is `"P" + (gamepad.index + 1)` `player_color` | rgb | Color identifying this player (not the color of the gamepad itself) `x` | -1,0,+1 | D-pad value on the _x_-axis at the start of the current frame frame `xx` | -1,0,+1 | If the D-pad moved from the center to a side on the _x_-axis at any time between the previous and current frame, this is the side that it moved to. When it hit both extremes, the last one is returned. `dx` | -2,-1,0,+1,+2 | Change in value of `x` between the previous and current frames on the _x_-axis `y` | -1,0,+1 | D-pad value on the _y_-axis at the start of the current frame `yy` | -1,0,+1 | If the D-pad moved from the center to a side on the _y_-axis at any time between the previous and current frame, this is the side that it moved to. When it hit both extremes, the last one is returned. `dy` | -2,-1,0,+1,+2 | Change in value of `y` between the previous and current frames `xy` | xy | `xy(x, y)` for the gamepad `dxy` | xy | `xy(dx, dy)` for the gamepad `angle` | -π to +π | `rotation_sign() * atan(y, x)` if both are not zero, otherwise the last angle `dangle` | -π to +π | Change in `angle` this frame `a` | number | The number of frames the ⓐ button has been held down, 0 if it was not held down at the start of the current frame `aa` or `pressed_a`| number | Nonzero if the ⓐ button went from up to down at least once since the previous frame `released_a` | number | Number of frames the ⓐ button was down if it went from down to up at least once since the previous frame, otherwise 0 `b` | number | The number of frames the ⓑ button has been held down, 0 if it was not held down at the start of the current frame `bb` or `pressed_b`| number | Nonzero if the ⓑ button went from up to down at least once since the previous frame `released_b` | number | Number of frames the ⓑ button was down if it went from down to up at least once since the previous frame, otherwise 0 `c` | number | The number of frames the ⓒ button has been held down, 0 if it was not held down at the start of the current frame `cc` or `pressed_c`| number | Nonzero if the ⓒ button went from up to down at least once since the previous frame `released_c` | number | Number of frames the ⓒ button was down if it went from down to up at least once since the previous frame, otherwise 0 `d` | number | The number of frames the ⓓ button has been held down, 0 if it was not held down at the start of the current frame `dd` or `pressed_d`| number | Nonzero if the ⓓ button went from up to down at least once since the previous frame `released_d` | number | Number of frames the ⓓ button was down if it went from down to up at least once since the previous frame, otherwise 0 `e` | number | The number of frames the ⓔ button has been held down, 0 if it was not held down at the start of the current frame `ee` or `pressed_e`| number | Nonzero if the ⓔ button went from up to down at least once since the previous frame `released_e` | number | Number of frames the ⓔ button was down if it went from down to up at least once since the previous frame, otherwise 0 `f` | number | The number of frames the ⓕ button has been held down, 0 if it was not held down at the start of the current frame `ff` or `pressed_e`| number | Nonzero if the ⓕ button went from up to down at least once since the previous frame `released_f` | number | Number of frames the ⓕ button was down if it went from down to up at least once since the previous frame, otherwise 0 `q` | number | The number of frames the ⓠ button has been held down, 0 if it was not held down at the start of the current frame `qq` or `pressed_q`| number | Nonzero if the ⓠ button went from up to down at least once since the previous frame `released_q` | number | Number of frames the ⓠ button was down if it went from down to up at least once since the previous frame, otherwise 0 `type` | string | The type of gamepad, as autodetected based on serial number or overriden by the player `prompt` | `{...}` | An object mapping button names to text prompts (see below) `status` | string | `"absent"` before any interaction this game on this gamepad. `"present"` if local interaction has occurred and not playing an online game. `"host"` if the host in an online game. `"guest"` if a guest in an online game. Starting or stopping online hosting/guesting resets to `"absent"`. `online_name` | string | Name of the computer this gamepad is connected to. Useful primarily for online play. !!! Use the single-letter gamepad properties (`x`, `a`) for temporally continuous inputs such as aiming or charging. Use double-letter properties (`xx`, `aa`) for discrete inputs such as firing or choosing a menu option. !!! Use `sign(gamepad.a)` etc. if you want to force the value to 1 or 0. There is never a need to convert to a boolean because 0 = false and nonzero = true for statements such as `if gamepad.a:` If a button is pressed and released very quickly, then `a` could be 0 for two frames in a row, because at the time that the frame runs it will be in the released state. However, `aa` will be nonzero for a frame in that case, so that the button press will not be missed. (Due to technical limitations of web browsers, there is a very small chance that `aa` will not capture a button press that is less than 1/60s on a physical gamepads. Our testing determined that fastest most people and gamepads can switch is about 1/45s, which is under this threshold. The gamepad will always capture button presses on keyboard.) The `xx` and `yy` values record the last change in value from zero between frames. It is theoretically possible for the D-pad to move so fast that it encounters along the x-axis values of 0, -1, 0, 1, 0 in that order between frames. If this is the case, then the `xx` property will only record the last non-zero that was switched to. So, this scenario would yield `x=0`, `xx=+1`, and `dx=0`. (On a physical gamepad, any given value that is held for less than 1/60s could be missed. Again, this is extremely unlikely because of the timescales involved.) The axes are affected by the current transformation _at the time that they are read by your program_, as set by `set_transform()`. Reading the same property twice with a transform change in between can change the sign of the value. The angles are always clockwise in screen space. `any_button_press(gamepad_or_touch default ∅)` : Returns true if any button on `gamepad_or_touch` was just pressed. If `gamepad_or_touch` is `nil`, returns true if any button on any gamepad was pressed or there was any touch pressed. `any_button_release(gamepad_or_touch default ∅)` : Returns true if any button on `gamepad_or_touch` was just released. This is useful on main menus to ensure that the button is no longer held at the end of the mode. If `gamepad_or_touch` is `nil`, returns true if any button on any gamepad was released or there was any touch released. `joy` : A variable equal to `gamepad_array[0]` as shorthand for single-person games. `gamepad_array[]` : The array of all gamepads. Regardless of how many physical controllers are connected, there are always four elements. ### Button Prompts The `prompt` property of a `gamepad_array[]` value is to be used with `replace()` to create strings for showing controls and button prompts to players with `draw_text()`. It maps `"ⓐ"`, `"ⓑ"`, `"ⓒ"`, `"ⓓ"`, `"ⓔ"`,`"ⓕ"`, `"ⓟ"`, `"ⓠ"`, `"⍐"`, `"⍗"`, `"⍇"`, and `"⍈"` as well as ASCII aliases `"(a)"`, `"(b)"`, `"(c)"`, `"(d)"`, `"(e)"`, `"(f)"`,`"(p)"`, `"(q")`, `"[^]"`, `"[v]"`, `"[<]"`, and `"[>]"` to strings of text. It maps `"##"` to the 1-based player number. These can be used to describe controls to players in a way mapped to the physical controller they are currently using instead of the native controls For example, `replace("Press (a) to jump", gamepad_array[0].prompt)` creates a string with the proper physical button label for the first player's primary button substituted for the string `"(a)"`. On a PlayStation controller, this string will be `"Press ⓧ to jump"`. On an Xbox controller, it will be `"Press ⓐ to jump"`. For a player using the keyboard, it will be `"Press ␣ to jump"`. The value of `gamepad_array[].prompt` can change during the game if the player uses the system menu to change the control scheme, so you should always generate the string right before drawing it instead of at program start. The `gamepad_array[i].type` property describes the control scheme used for prompts. ### Touch provides single-touch and single-button mouse input via virtual touch screen. Not all devices support touch input (although most do, through a touch screen, stylus, or mouse). `touch` : There is a single `touch` global object with the following properties: Property | Type | Meaning -------------------------|----------|---------------------------------- `touch.x` | number | Camera-space x position `touch.y` | number | Camera-space y position `touch.dx` | number | Change in `x` since the previous frame `touch.dy` | number | Change in `y` since the previous frame `touch.xy` | xy | `xy(touch.x, touch.y)` `touch.dxy` | xy | `xy(touch.dx, touch.dy)` `touch.a` | number | Number of frames that the touch has been held down, or 0 if not currently down `touch.pressed_a` | number | Nonzero if the touch started this frame `touch.aa` | number | Same as `pressed_a` `touch.released_a` | number | Nonzero if the touch ended this frame `touch.screen_x` | number | Screen-space x position `touch.screen_y` | number | Screen-space y position `touch.screen_dx` | number | Change in `screen_x` since the previous frame `touch.screen_dy` | number | Change in `screen_y` since the previous frame `touch.screen_xy` | xy | `xy(touch.screen_x, touch.screen_y)` for the touch `touch.screen_dxy` | xy | `xy(touch.screen_dx, touch.screen_dy)` for the touch `touch.hover` | xy | If the device supports hovering _and_ the hover moved this frame, the camera-space position of the hover. Otherwise `nil`. The `x` and `y` values are in camera-space coordinates using the current transform. Change or reset the transform to read them in a different coordinate system, or use the transformation routines. If there is no active touch, then the coordinates reflect the previous active touch location. There is no way to track a hovering mouse or detect a scroll wheel or second mouse button with `touch`, which would not be mapped to a mobile device anyway. `device_control()` allows low-level access to additional device-specific pointer properties. ### Main Menu The system pause menu is bound to the ⓟ button, which is not visible to games. Games can add up to three custom elements on the pause menu, and can change these at any point. `set_pause_menu(opt1 default ∅, opt2 default ∅, opt3 default ∅)` : Override the custom elements on the pause menu. Each argument is an object of the form `{text:string, callback:function, confirm:boolean}`. At 64x64 resolution, specifying `opt3` disables the automatic game credits option in the pause menu. - `text` is the menu item text. It will be clipped if too long. - `callback` is a function of no arguments to execute when this option is selected. - `confirm` (defaults to `false`) if true, is an additional confirmation dialog appears when this option is selected. If a non-empty string, displays that string as an additional message on the confirmation dialog. ### MIDI supports MIDI on all platforms except for macOS Safari. Other browsers on macOS support MIDI. The global `midi` object has the following properties: `midi.input_port_array` : Array of all input ports. Updated whenever devices are added or removed at runtime, preserving ordering. The input ports have state that can be polled similarly to a game pad (see below for `note` and `controller`). `midi.output_port_array` : Array of all output ports. Updated whenever devices are added or removed at runtime, preserving ordering. The output ports may be used with `midi_send_raw()`. `midi.message_queue` : The message queue array contains all incoming MIDI messages received since the previous frame, in the order that they were received. Messages are not tracked until the first time that the `midi` object is accessed, so the queue will initially be empty even if the MIDI input device was in use before the program checked it. In the `midi.message_queue`, each message has the form `{type: string, port: port, raw: [...], ...}`, where the `type` determines the other properties. The most common messages are `"NOTE_ON"`, which has `channel`, `note` and `velocity` properties; and `"NOTE_OFF"`, which has `channel` and `note` properties. The `raw` property is the underlying 1-3 byte MIDI message. Most devices send `"NOTE_ON"` with velocity zero instead of `"NOTE_OFF"` to enable the [running status compression hack](https://www.euclideanspace.com/art/music/midi/codes/index.htm). All ports have: `port.id` : Device's integer ID (which may be negative), which will be the ID of the adapter if the device is not natively using USB `port.name` : Device's name, which will be the name of the adapter if the device is not natively using USB `port.manufacturer` : Device's manufacturer, which will be the manufacturer of the adapter if the device is not natively using USB `port.state` : `"connected"` or `"disconnected"` `port.type` : `"input"` or `"output"` The input ports contain `note[]` and `controller[]` 128-element arrays that are aggregated across all channels, and a `channel[]` 16-element array for which each element has its own `note[]` and `controller[]`. This provides convenient access for simpler devices that do not take advantage of channels (such as an 88-key piano keyboard or 16-pad drum pad), as well as the full state of all 4096 input elements possible for each port if needed. For the purpose of these, `"NOTE_ON"` with zero velocity is treated as equivalent to `"NOTE_OFF"`. Each `note[]` element has the following fields: `note.on` : The number of frames the note has been held down, 0 if it was not held down at the start of the current frame `note.pressed` : Nonzero if the note went from off to on at least once since the previous frame `note.released` : Number of frames the note was on if it went from on to off at least once since the previous frame, otherwise 0 `note.velocity` : Velocity with which the note was last turned on. `note.pressure` : Last recorded pressure. This generally counts up continuously from 0 while a note is held at constant pressure, and then down when the pressure is reduced. Controllers are generally mapped to knobs and faders and notes are mapped to (pressure sensitive) pads or keys. Each `controller[]` element has the following properties: `controller.value` : Current value `controller.delta` : Change since last frame of value The MIDI send command is: `midi_send_raw(port, raw_message_array)` : The `port` must be one of the output ports from the `midi` object. The message must be an array of integers according to the [MIDI message format](https://www.songstuff.com/recording/article/midi_message_format/). The return value is either `"ok"` or an error message string. The common messages are: Message | Array -----------------|-------------------------------- `NOTE_ON` | `[0x90 + channel, note, velocity]` `NOTE_OFF` | `[0x80 + channel, note]` `CONTROL_CHANGE` | `[0xB0 + channel, controller, data]` Where `channel` is an integer 0-15, and `note`, `velocity`, and `controller` are integers 0-127. -------------------------------- supports MIDI input and output of connected devices. Unlike the gamepads and display I/O, no MIDI devices are guaranteed to be present. MIDI input can route footswitches, drum pads, piano keyboard, MIDI guitars, faders, and other devices as controllers for a game. MIDI output can drive synthesizers, audio FX units, and MIDI-controlled amplifiers, and when paired with a MIDI-DMX bridge can control party lights, fans, smoke machines, and other physical objects. Together these are a great way to use for music projects, as well as providing creative input and output devices for everything from rhythm games to escape rooms. Some relatively inexpensive, highly rated MIDI input (and in some cases, RGB button output) devices are: - [Donner Starrypad](https://www.amazon.ca/Donner-Maker-Assignable-Controller-STARRYPAD/dp/B01GYK2CKK) about US$50, has key velocity - [AKAI APC Mini Mk2](https://amzn.to/3Ne2UQK) about US$90, [RGB lights protocol](https://cdn.inmusicbrands.com/akai/attachments/APC%20mini%20mk2%20-%20Communication%20Protocol%20-%20v1.0.pdf) - [Behringer X-Touch Mini](https://amzn.to/3Ne2Lga) about US$100 - [AKAI MPKmini](https://amzn.to/42owKpK) about US$100 - [Novation Launchpad Mini Mk3](https://amzn.to/3CgZbeQ) about US$100, [RGB light protocol](https://www.djshop.gr/Attachment/DownloadFile?downloadId=10737), has square buttons - [Novation Launchpad X](https://amzn.to/43sZlLY) about US$160, [RGB light protocol](https://www.djshop.gr/Attachment/DownloadFile?downloadId=10737), has key velocity, has square buttons - [Behringer FCB1010 Foot Controller](https://amzn.to/42xEJ3P) about US$160; see [Morgan's notes](https://morgan3d.github.io/articles/2023-06-22-fcb1010/index.html) to use this device Many MIDI devices directly support USB and Bluetooth for connecting to a computer. Others use MIDI 5-pin or either 1/8" phono (headphone) cables and require a USB adapter, such as: - [FORE MIDI-to-USB adapter](https://amzn.to/3C96dlI) about US$20 (Note that its MIDI connectors are labelled backwards) - [M-Audio MIDI-to-USB adapter](https://amzn.to/42vT9BS) about US$35 MIDI devices generally don't need drivers on macOS or Windows. DMX devices and converters are more expensive than straight MIDI ones. Here are a few interesting examples chosen without considering reviews: - [DOREMiDi USB MIDI-to-DMX MTD-1024](https://www.doremidi.cn/h-pd-80.html) about US$90 - [DecaBox MIDI-to-DMX](https://www.amazon.ca/DecaBox-MIDI-to-DMX-Converter/dp/B00N36JQG2) about US$300 - [Colored light bar](https://amzn.to/3qssbxD) about US$70 - [Fog and light machine](https://amzn.to/42vSWP6) about US$130 - [Effects fan](https://amzn.to/3WRwsa3) about US$450 - [Flamethrower](https://amzn.to/3WYJdPZ) about US$450 For playing music, the frequency of MIDI note $n$ is $f = 440~\operatorname{Hz} \cdot 2^{(n - 69) / 12}$. $n=0$ is at 8.18 Hz, below the range of human hearing. It corresponds to the C two octaves below C1. $n=24$ is C1 at 32.70 Hz, the lowest pitched note on a full piano. $n=60$ corresponds to middle C4 at 261.63 Hz. $n=108$ is C8 at 4,186.01 Hz, the highest pitched note on a full piano. $n=127$ is G9 at 12,543.85 Hz, the highest note in MIDI and about half of the highest frequency (i.e., one octave) below the highest note in the range of human hearing. Protocol specifications for some common MIDI controllers are: - [Launchpad Mini Mk3](https://www.djshop.gr/Attachment/DownloadFile?downloadId=10737) - [Launchpad X](https://fael-downloads-prod.focusrite.com/customer/prod/s3fs-public/downloads/Launchpad%20X%20-%20Programmers%20Reference%20Manual.pdf) - [Ableton Push](https://github.com/Ableton/push-interface) - [AKAI APC Mini Mk2](https://cdn.inmusicbrands.com/akai/attachments/APC%20mini%20mk2%20-%20Communication%20Protocol%20-%20v1.0.pdf) For example, setting the color of each LED to match a sprite on a Novation Launchpad device is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pyxlscript def launchpad_show_sprite(sprite): // Apply gamma and map floating point to 7-bit unsigned normalized fixed point [0, 127] interval def map(v): return MIN(0x7F, FLOOR(MUL(0x80, MUL(v, v)))) const msg = [0xF0, 0x00, 0x20, 0x29, 0x02, 0x0D, 0x03] const c = #0000 const pos = xy(0, 0) // To access the 9th row and column of control buttons, // extend these loops one further. The indexing is consistent for x < 8: for y < 8: pos.x = x; pos.y = 7 - y get_sprite_pixel_color(sprite, pos, c) msg.push(0x03, (x + 1) + 10 (y + 1), map(c.r), map(c.g), map(c.b)) push(msg, 0xF7) midi_send_raw(port, msg) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To reduce permission dialogs for the player, MIDI is only available if the source code explicitly references an object or property directly on the `midi` object by name at least once. Exclusively renaming or passing the `midi` object as an argument and then accessing properties of it by another name will not trigger the initialization correctly. Modes ------------------------------------------------------------------------ `get_previous_mode()` : Return the previous mode, or `nil` if there was no previous mode. When returning to a mode via `pop_mode()`, the previous mode is the mode from which _that_ one was entered; they are on a stack. `get_mode()` : Return the current mode. This can be passed to hook functions or compared against mode names, but not to `set_mode`, which requires an explicit mode name. during mode initialization, `get_mode()` returns `nil`. `launch_game(url)` : Load a different game. This is primarily used by the built-in launcher, but is available to all programs. This may be removed in version 1.0. `pop_mode()` : Go back to the previous mode that invoked `push_mode`, restoring the draw calls and `mode_frames` as of that point. It is a runtime error to invoke `pop_mode()` when the mode was not entered from `push_mode()`. Invokes the `leave` section of the current mode but not the `enter` section of the mode being returned to. `push_mode(Mode, ...)` : Enter a new mode but remember the current mode. Combines their draw calls for `draw_previous_mode()`. Useful for modal dialogs, such as pause screens, inventory screens, and dialogue. Does not invoke the `leave` section of the current mode. Does invoke the `enter` section of the mode being entered. Any arguments after the `Mode` are passed to the `enter` section. `quit_game()` : Quit the game and exit to the IDE or the launcher, or close the tab depending on what the `quit` HTML query parameter was set to. Note that games do not form a stack. Quitting a game returns to the built-in launcher or IDE, and not the game that launched it. `reset_game()` : Reset the game, restoring all assets and starting again from the initial mode. `set_mode(Mode, ...)` : Immediately switch to the new mode. The current mode stops executing and the new mode begins. The mode name must be the literal name of the mode, not a variable, a value from a data structure, or the return value of the function. If the note is present, it must likewise be a string literal and not an expression. The note is a comment that is used by the IDE for creating state machine diagrams and in the IDE for debugging. It has no effect on the executing program. Erases the `push_mode` stack but remembers its draw calls. Any arguments after the `Mode` are passed to the `enter` section. When changing modes via `set_mode()`, the order of operations is: 1. `get_previous_mode()` switches to the current mode, `get_mode()` switches to the _new_ mode. 2. The _previous_ mode's graphics commands are captured for future `draw_previous_mode()` calls. 3. Any `leave` code executes on the _previous_ mode. 4. `mode_frames = 0`, 5. Any `enter` code executes on the _new_ mode and arguments supplied. Types --------------------------------------------------------- `is_array(value)` : Returns true for arrays and false for all other types. `is_boolean(value)` : Returns true for booleans and false for all other types. `is_function(value)` : Returns true for functions and false for all other types. `is_nil(value)` : Returns true for `nil` and false for all other types. `is_nan(value)` : Returns true for the floating point not-a-number value `nan` and false for all other values. Note that you cannot test for `nan` using `==` due to floating point nan rules. `is_number(value)` : Returns true for numbers and false for all other types. `is_object(value)` : Returns true for objects and false for all other types. `is_string(value)` : Returns true for strings and false for all other types. `type(value)` : Returns the type of the value as a string: `array`, `boolean`, `function`, `nil`, `number`, `object`, or `string`. Array --------------------------------------------------------- `array_value(array, i, extrapolate default (array.extrapolate or "clamp"))` : Applies `floor()` to `i` and then applies the extrapolation mode, which defaults to the extrapolation mode of the array itself if specified. Intended primarily for animation of values, including the named arrays of spritesheets, for example, `sprite = array_value(spritesheet[animName], frame)`. See also `make_spline()` and the Sprite JSON section. The `extrapolate` values accepted are: - `"oscillate"` - `"loop"` - `"clamp"` (default, other unsupported values are silentely treated as clamp instead of failing) `clone(array)` : Return a shallow clone of `array`. Pointer structures are preserved and loops are OK. Immutable objects such as spritesheets and functions are not cloned. `contains(array, value, comparator default ==)` : Return `true` if `value` appears at least once in `array` and `false` otherwise. `deep_clone(array)` : Return a deep clone of `array`. Pointer structures are preserved and loops are OK. Immutable objects such as spritesheets and functions are not deep cloned. Maps are the only asset that can be deep cloned. `extended(array1, array2)` : Create a new array that contains all elements of `array1` followed by all elements of `array2`. Identical to `[...array1, ...array2]`. See also `extend()`. `equivalent(a, b)` : Returns true if these arrays have the same length and the elements are `==` (does not recursively test if the elements are equivalent). Also returns true if both `is_nan(a)` and `is_nan(b)`. `extend(array1, array2)` : Mutate `array1` by pushing all elements of `array2` onto it. Identical to `push(array1, ...array2)`. See also `extended()`. `fast_remove_key(array, i)` : Delete element `i` from the array, swapping it with the last element. O(1) expected amortized time. `fast_remove_value(array, v)` : Delete one element that is `== v`, or do nothing if there is no such element. May reorder the array and may choose any such element, not necessarily the first. See also `remove_values()`. `find(array, x, i default 0, comparator default ==)` : Find the index of the element whose value is `x`, starting at optional index `i`. Returns `∅` if not found. Unless a function is for `comparator`, uses `==` to compare `x` to the array values. If a comparator is present, any non-falsy return value from the comparator indicates a match. The array element is passed as the first argument to the comparator. Runs in linear time in the length of the list from `i`. This is called `index_of` or `indexOf` in some other programming languages. Note that if `is_nan(x)`, then `find()` cannot find any match. `insert(array, i, x, ...)` : Inserts the values into the array, starting at index `i` (as in, `array[i] = x` on return) and returns the first value inserted. Similar to Python insert and JavaScript splice functions. `iterate(array, callback, ...args)` : Iterates through array in order using a callback function, with optional O(1) order-preserving removal. Returns `true` if any element was removed, otherwise returns `false`. This is an imperative version of map(), filter(), and list comprehension primitives from other languages. If `callback` is a string, iterate then calls `array[i][callback](array[i], ...args)` on each element for which `array[i][callback]` is not nil. If `callback` is a function, it instead calls `callback(array[i])` on each element. It is common to define a local function immediately before the `iterate()` call for use as the callback. Return values from the called function have the following effects: - `iterate.REMOVE`: remove the current value and continue iterating - `iterate.BREAK`: immediately stop iterating - `iterate.REMOVE_AND_BREAK`: remove the current element and then stop processing - `iterate.CONTINUE`: continue with iteration. This is not required; as long as the previous values are not returned by the callback, the iterator will still continue The callback may push values onto the array during iteration, but not otherwise mutate the array. `iterate_pairs(array, callback, ...args)` : Iterates over all n * (n - 1) / 2 uniquely indexed pairs of elements in the n-element array. This is useful for testing for collisions, for example. Invokes `callback(A, B, ...args)` for each pair. Returns `true` if any element was removed and false otherwise. The return value from `callback` may be an array of two elements, `[a_action, b_action]`. If a single `iterate` constant is returned, then it is converted to an array of two copies of that value. The return values are processed as follows. `a_action`: - `iterate.REMOVE`: Remove A immediately from the array and continue iteration, starting from the next value for A. - `iterate.REMOVE_AND_BREAK`: Remove A immediately from the array and stop all iteration. - `iterate.BREAK`: Stop all iteration. - `iterate.CONTINUE` or any other value: Continue iteration. `b_action`: - `iterate.REMOVE`: Remove B immediately from the array and continue iteration. B will not be seen by further values of A (unless it appeared in the array multiple times). - `iterate.REMOVE_AND_BREAK`: Remove B immediately from the array and stop iteration for the current A, but continue iterating on more As. B will not be seen by further values of A (unless it appeared in the array multiple times). - `iterate.BREAK`: Stop iteration for the current A, but continue iterating on more As. - `iterate.CONTINUE` or any other value: Continue iteration. Note that `[iterate.REMOVE, iterate.BREAK]` has the same effect as `[iterate.REMOVE, iterate.CONTINUE]`. All removals preserve order. Running time with no removal or break is necessarily O(n^2) for an array of length n. Unlike `iterate()`, each removal is O(n) instead of constant time. The callback may push values onto the array during iteration, but not otherwise mutate the array. See also `iterate()`. `join(array, separator default "", lastSeparator default separator, pairSeparator default lastSeparator, empty default "")` : Joins the elements of the array into a string. Each element `x` converted to a string with `unparse(x, 1)` if it is not already a string. The optional `lastSeparator` allows the final join to use a special case. The optional `pairSeparator` is a special separator used for arrays of length two. For example `join(inventory, ", ", ", and ", " and ", "nothing")`. The optional `empty` value is used if the array has length 0. `last_key(array)` : Returns `size(array) - 1`, the last key (that is, index). `last_value(array)` : Returns `array[last_key(array)]`. `make_array(size, value default nil, value_clone default nil)` : Creates a 1D (if `size` is a number), 2D (if `size` is an `xy()`), or 3D (if `size` is an `xyz()`) array of arrays. Each element is initialized independently to `if value_clone then value_clone(value) else value`. `clone()` and `deep_clone()` are common choices for `value_clone`. Multidimensional arrays are indexed as `array[x][y][z]`. For 2D arrays, the outer array has an extra `array2D.size` property on the outermost array that is an `xy()`. For 3D arrays, the outer array has an extra `array3D.size` property that is an `xyz()`, and the inner 2D arrays each have an extra `array2D.size` property on the that is a `yz()`. Use `size(array)` to determine the length of a 1D array. `pop(array)` : Remove the last element from the array and return it, or `∅` if the array is empty. O(1) expected amortized time. `pop_front(array)` : Remove the first element from the array and return it, or `∅` if the array is empty. O(n) expected amortized time. `push(array, x, ...)` : Append `x` and any other arguments to the array and then return the last argument. O(1) expected amortized time for a single argument and O(_m_) for _m_ arguments. This is the only mutation permitted while iterating over an array via `for value in array` or `iterate(array, callback)`. `push_front(array, x, ...)` : Insert `x` and any other arguments onto the array, preserving the order that they appear in the arguments and then return the first. O(_n_) expected amortized time for a single argument and O(_m_) for _m_ arguments and array of length _n_. `random_from_distribution(array, random default random)` : Return a random index (key) of the array, chosen proportional to the values. `random_key(array, random default random)` : Return a random index into the array. `random_value(array, random default random)` : Return a random element of the array. `remove_key(array, i)` : Remove element `i` from the array, preserving order. O(n) expected time. `remove_values(array, x)` : Remove every instance of `x` in the array, maintaining order. O(n) time in the original length of the array. Returns true if any value was removed.vDoes nothing if `x` is not in the array. See also `fast_remove_value()` `remove_all(array)` : Remove everything from the array. Same as `resize(array, 0)`. `resize(array, n)` : Change the length of the array. O(size(array) + n) time if growing, O(1) if shrinking. `reverse(array)` : Reverses the array in place, O(size(array)). Returns nothing to avoid confusion with `reversed()`. `reversed(array)` : Returns a shallow, reversed clone of the array, O(size(array)) See also `reversed(string)`. `shuffle(array, random default random)` : Randomizes the array in place using using Knuth-Fisher-Yates in O(n) time. Returns nothing to avoid confusion with `shuffled()`. `shuffled(array, random default random)` : Returns a shallow, shuffled clone of the array, O(size(array)) `size(array)` : Get the length of the array. O(1) time. `slice(array, s, e default size(array))` : Return the subarray of `array` between `s` (inclusive) and optional `e` (exclusive, defaults to length). `sort(array, k, reverse default false)` : Sort the array from least to greatest in place. If `k` is a string, then elements of the array are assumed to be objects and are sorted by that key. If `k` is a number, then elements of the array are assumed to be other arrays or strings and are sorted by that index. If `k` is not specified and the array contains objects, then elements are sorted by their alphabetically-first key. If `k` is a function, then it must be a function of the form `compare(a, b)` that returns a positive number if `a` is greater than `b`, a negative number if `b` is greater than `a`, and zero if they are equal. If `k` is not specified and the array contains numbers or strings, then they are sorted by natural order. If `reverse` is true, then the array is sorted in reverse order from greatest to least. `sorted(array, k, reverse default false)` : Clone the array, sort it, and return the sorted array. See `sort()` for details. String --------------------------------------------------------- `array_value(string, i, extrapolate default (string.extrapolate or "clamp"))` : Applies `floor()` to `i` and then applies the extrapolation mode, which defaults to the extrapolation mode of the array itself if specified. The `extrapolate` values accepted are: - `"oscillate"` - `"loop"` - `"clamp"` (default, other unsupported values are silentely treated as clamp instead of failing) `capitalized(string)` : Returns a new string with the first letter of each word capitalized. Existing capital letters are unchanged. `contains(string, substring)` : Returns `true` if `substring` appears at least once in `string` and `false` otherwise. `ends_with(string, target)` : Returns true if `string` ends with the substring `target`. `extended(a, b)` : Returns a new string that is `a + b`. `find(string, x, i default 0)` : Find the index of the substring `x`, starting at optional index `i` (default 0). Returns `∅` if not found. Runs in linear time in the length of the string from `i`. This is called `index_of` or `indexOf` in some other programming languages. `format_number(n, format)` : Produces a string from number `n` in one of the following formats. Each is followed by an example output. When `n` is a time, it is in seconds. - `"percent"`, `"%"`: 30% - `"commas"` (comma thousand separators): 1,000,000 - `"spaces"` (space thousand separators): 1 000 000 - `"binary"`: 0b10100001 - `"degrees"`, `"deg"`, `"°"`: 57° - `"0.#deg"`, `"0.#°"`: 57.3° - `"0.0deg"`, `"0.0°"`: 180.0° - `"hex"`: 0xa1 - `"scientific"`: 6.022×10²³ - `"clock12"`: 5:35pm - `"clock24"`: 17:35 - `"stopwatch"`: 1:38.71 - `"oldstopwatch"`: 1"38'71 - `"ordinal"`: second - `"ordinalabbrev"`: 2ⁿᵈ - `"fraction"`: ¼ - `".00"` (or any other trailing zero sequence): 4.91 - `"00"` (or any other leading zero sequence): 07 - `" 0"` (or any other leading space sequence followed by a zero sequence): " 7" The suffixes `°`, `degrees`, `deg`, and `%` can be added to the `0` forms to force the rounding to occur after to conversion to degree measure or percentage. For example, `format_number(0.8123, "0.0%")` produces `"81.2%"`. The prefix `+` can be added to all forms to force a leading + sign when the number is positive. `last_key(string)` : Returns `size(string) - 1`. `last_value(string)` : Returns `string[last_key(string)]`. `lowercase(s)` : Returns the lower case version of the string. `parse(s)` : Parse a string that was encoded using `unparse()` and convert it back into the original data structure. Functions, built-ins, and recursive arrays cannot be converted. `replace(string, src, dst)` : Replace all occurances of `src` in `string` with `dst`. `replace(string, object)` : Replace all occurances of each key of `object` with each `value` of object in `string` simultaneously. For example, `replace("Hey, PROPER_NOUN, use the ADJ NOUN!", {ADJ: "big", NOUN: "stick", PROPER_NOUN: "Runi"})`. `resized(string, size, pad default " ", right_align default false)` : Return a new string of the specified `size`, using `pad` to extend as needed. If `right_align` is true, padding and cropping occur at the start of the string instead of the end. `reversed(string)` : Returns a reversed version of the string in O(size) time. `shuffled(string)` : Returns a shuffled version of the string in O(size) time. `size(s)` : Get the length of the string. O(1) time. `slice(string, s, e default size(str))` : Return the substring of `string` between `s` (inclusive) and optional `e` (exclusive, defaults to length). `split(string, separator default "", max_splits default infinity)` : Returns an array of strings separated by `separator`. If the `separator` is the empty string, then all characters are separated. If a `max_splits` is specified, at most that many splits are made. `split(string, count default 0)` : Returns an array of strings that are every sequential `count` characters from `string`. `starts_with(string, target)` : Returns true if `string` begins with the substring `target`. See also `ends_with()`. `trim_spaces(string)` : Returns a new string that is the original with whitespace removed from the start and end. See also `resized()` `unparse(x, level default 1)` : Returns code that is equivalent to producing data object `x`, if it is not a recursive object or array or a function. Note that unparsing a string will add quotes around the contents. The values of `level` are: 0 = Compact as possible. No whitespace at all. Used internally for `save_local()` 1 = Readable single line, with spaces after ":" and ",". Default and used internally for string coercion. 2 = Containers with more than four elements or which contain other containers have newlines and indentation after "[{,". "]" and "}" on the line with the previous element. 3 = Containers with more than four elements or which contain other containers have newlines and indentation after "[{,". "]" and "}" on their own lines. Used internally by `debug_print()`. 4 = Newlines and indentation after all "[{,}]" except for empty containers `uppercase(s)` : Returns the upper case version of the string. Object --------------------------------------------------------- `clone(object)` : Return a shallow clone of `object`. Pointer structures are preserved and loops are OK. Built-in objects such as spritesheets and functions are not cloned. `contains(object, value, comparator default ==)` : Returns `true` if there is some key that maps to `value` in `object` and `false` otherwise. `deep_clone(object)` : Return a deep clone of `object`. Pointer structures are preserved and loops are OK. Built-in objects such as spritesheets and functions are not cloned. `extended(object1, object2)` : Create a new object that is the union of `object1` and `object2`, where `object2` keys override in the case of collisions. This is identical to using the spread operator, `{...object1, ...object2}`. See also `extend()`. `extend(object1, object2)` : Mutate `object1` by assigning all elements of `object2` into it, overriding any existing keys in the case of collisions. See also `extended()`. `fast_remove_key(object, k)` : Same as `remove_key` for a object. `fast_remove_value(object, value)` : Delete one element that is `== v`, or do nothing if there is no such element. See also `remove_values()`. `find(object, x, start_key default ∅, not_found_value default ∅, comparator default ==)` : Find a key for which the value is `x`. Returns `not_found_value` if not found. Uses `comparator` to compare `x` to array values. O(n) time in the number of key-value pairs. The `ignored` parameter is present to match the signature of the array and string versions of `find()` but does nothing in the object case. `keys(object)` : An array of the keys in the object. You almost never need this because `for k in object` iterates over the keys directly. `random_key(object, random default random)` : Return a random key of the object `random_value(object, random default random)` : Return a random key's value in the object `random_from_distribution(object, random default random)` : Return a random key from the object, chosen proportional to the values. `remove_key(object, k)` : Remove a key-value pair by key. Does nothing if `k` is not in the object. Use `object[k] ≠ ∅` to test for containment explicitly. `remove_values(object, v)` : Remove every instance of `v` from the object. Returns true if any value was removed. See also `fast_remove_value()`. `remove_all(object)` : Remove everything from the object. It is faster to assign to a new, empty object than to use this function. `remove_all()` is for when it is important to retain a pointer to the original object because it is shared. `size(object)` : The number of entries in the object. `values(object)` : An array of the values in the object. Random Values --------------------------------------------------------- ### 1D `make_random(seed default local_time().millisecond)` : Returns a new _function_ that acts like `random()` but has its own private seed and internal state. See also `set_random_seed()`. `random()` : `random(0, 1)`. Same as the `ξ` operator. `random(lo, hi)` : Random real number between `lo` and `hi`, excluding `hi`. See also the `ξ` operator, the no-argument version of `random()`. `random_gaussian()` : Same as `random_gaussian(0, 1, random)` `random_gaussian(mean, stddev, rng default random)` : Returns a Gaussian distributed random number on the distribution with the given mean and standard deviation. See also `random_truncated_gaussian()`. `random_integer(hi)` : Uniformly distributed random integer between 0 and hi, inclusive. This version is deprecated. Use `random_integer(0, hi)` instead. `random_integer(lo, hi, rng default random)` : Random integer between `lo` and `hi`, including both ends. The `random` argument defaults to common random number generator `random()`, but an alternative function previously created by `make_random()` can be passed instead. `random_sign(rng default random)` : Returns +1 or -1 with equal probability. `random_truncated_gaussian(mean, stddev, radius default (2 * stddev), rng default random)` : Returns a Gaussian distributed random number within `radius` of the mean. The default radius corresponds to about 96% of the total probability of the distribution and avoids very long tails. See also `random_gaussian()`. `set_random_seed(seed default local_time().millisecond)` : Reset the default random number generator, `random()`, using an optional seed. To match the default random number generator, use a seed of `0`. See also `make_random()`. ### 2D `random_direction2D(rng default random)` : Same as `random_on_circle()` `random_gaussian2D()` : Same as `random_gaussian2D(xy(0, 0), xy(1, 1), random)`. `random_gaussian2D(mean, stddev, rng default random)` : Returns a Gaussian distributed random point on the distribution with the given mean and standard deviation. The mean and standard deviation may each be scalar or `xy()` values. See also `random_gaussian()`, `random_truncated_gaussian2D()`. `random_on_circle(rng default random)` : Returns a uniformly distributed `xy()` on the perimeter of the circle with center `(0, 0)` with radius 1. Useful for random directions. `random_on_square(rng default random)` : Uniformly distributed `xy()` on the _perimeter_ of a square from `(-1, -1)` to `(1, 1)`. `random_within_circle(rng default random)` : Returns a uniformly distributed `xy()` in the disk with center `(0, 0)` with radius 1. Useful for random offsets within a circle. See also `random_on_circle()` `random_within_region(region, recurse default true, rng default random)` : Random world space 2D point generated within uniformly within the region described. A region may be: - an entity from `make_entity()` - any object with `pos` or `corner`, and optional `shape`, `angle`, `pivot`, and `size` properties - any object with `x` and `y` properties (including an `xy()`), which will be treated as a point If `recurse` is true (the default), recursively considers the area covered by all children as if they were non-overlapping. That is, the overlapping area is sampled twice. `random_within_square(rng default random)` : Returns a uniformly distributed `xy()` in the square from `(-1, -1)` to `(1, 1)`, that is, the square that has a "radius" of 1 or sides of length two, and is centered at the origin `(0, 0)`. Analogous to `randomDisk()`. `random_truncated_gaussian2D(mean, stddev, radius, rng default random)` : Returns a Gaussian distributed random point within `radius` of `mean`. The `mean` and `stddev` may be scalars or `xy()`s. If `radius` is a number, the truncation region is a disk. If `radius` is an `xy()`, then the truncation region is an ellipse that has those radii along each axis. See also `random_gaussian2D()`. ### 3D `random_direction3D(rng default random)` : Same as `random_on_sphere()`. `random_gaussian3D()` : Same as `random_gaussian3D(xy(0, 0), xy(1, 1), random)`. `random_gaussian3D(mean, stddev, rng default random)` : Returns a Gaussian distributed random point on the distribution with the given mean and standard deviation. The mean and standard deviation may each be scalar or `xyz()` values. See also `random_gaussian()`, `random_truncated_gaussian3D()`. `random_on_cube(rng default random)` : A random point on the _surface_ of the cube from `(-1, -1, -1)` to `(1, 1, 1)`. `random_on_sphere(rng default random)` : Returns a uniformly distributed `xyz()` on the [hollow] sphere with center `(0, 0, 0)` with radius 1. Useful for random directions in 3D. See also `random_within_sphere()` and `random_on_circle()`. `random_within_cube(rng default random)` : A random point inside the cube from `(-1, -1, -1)` to `(1, 1, 1)`. `random_within_sphere(rng default random)` : Returns a uniformly distributed `xyz()` within the unit sphere, that is, the ball with center `(0, 0, 0)` and radius 1. Useful for random offsets within a sphere. See also `random_on_sphere()`. `random_truncated_gaussian3D(mean, stddev, radius, rng default random)` : Returns a Gaussian distributed random point within `radius` of `mean`. The `mean` and `stddev` may be scalars or `xyz()`s. If `radius` is a number, the truncation region is a disk. If `radius` is an `xyz()`, then the truncation region is an ellipsoid that has those radii along each axis. See also `random_gaussian3D()`. Vector --------------------------------------------------------- Most of the math functions and operators defined for numbers _also_ operate component-wise on vectors represented as structures or arrays. For example, `clamp()`, `min()`, `+`, `/`, etc. `angle_to_xy(angle)` : Returns `xy(cos angle, rotation_sign() * sin angle)`, snapping to integer axes if within 10-12 `atan(vec2)` : Returns `atan(vec2.y, vec2.x)`. See also `xy_to_angle()` `cross(a, b)` : Return the vector cross product of two arrays of length three each or two vectors with `x`, `y`, and `z` properties. For arguments of length two or without a `z` component, returns `a.x * b.y - a.y * b.x`, which is the 2D wedge product. `direction(v)` : `v / magnitude(v)` if the magnitude is greater than 10-10, otherwise returns the original (nearly zero) vector. That is, returns a normalized or unit vector unless the input is close to zero. `dot(a, b)` : Return the dot (inner) product of any two structures or arrays with parallel elements. `equivalent(a, b)` : Returns true if these vectors have the same size and the elements are `==` (does not recursively test if the elements are equivalent). `find_max_value(v)` : Returns the key of the _first_ instance of the largest component, or `nil` if the array or object is empty. See also `max_value()` `find_min_value(v)` : Returns the key of the _first_ instance of the smallest component, or `nil` if the array or object is empty. See also `min_value()` `magnitude(v)` : The square root of the sum of the elements (L2 norm). `magnitude_squared(v)` : The sum of squares of the elements. `max_value(v)` : Largest element. See also `find_max_value()` `min_value(v)` : Smallest element. See also `find_min_value()` `perp(v)` : Returns a vector perpendicular to `v` by rotating it 90 degrees: `xy(-v.y, v.x)`, for `xy()` and 2-element array vectors only. `xy_to_angle(xy)` : Returns `rotation_sign() * atan(xy)`. `xy(x, y)` : Create the object with `x` and `y` properties. `xy(xy)` : Returns `{x:xy.x, y:xy.y}`. Useful for removing the `z` component or cloning. See also `xy_to_xz()`. `xy(array)` : Returns `{x:array[0], y:array[1]}`. Useful for converting from a matrix column. `xy_to_xz(xy)` : Returns `{x:xy.x, z:xy.y}`. See also `xz_to_xy()`. `xz_to_xyz(v, new_z default 0)` : Returns `{x:v.x, y:v.z, z:new_z}`. `xz(x, z)` : Create the object with `x` and `z` properties. `xz(xz)` : Returns `{x:xz.x, z:xz.z}`. Useful for removing the `y` property or cloning. `xz(array)` : Returns `{x:array[0], z:array[1]}`. Useful for converting from a matrix column. `xz_to_xy(xz)` : Returns `{x:xz.x, y:xz.z}`. See also `xy_to_xz()`. `xz_to_xyz(xz, new_z default 0)` : Returns `{x:xz.x, y:xz, z:new_z}`. See also `xz_to_xy()`. `xyz(x, y, z)` : Create the object with `x`, `y`, and `z` properties. `xyz(xy, z default 0)` : Creates `{x: xy.x, xy:y, z}` `xyz(xz, y default 0)` : Creates `{x: xz.x, y:y, z:xz.z}`. See also `xz_to_xyz()` `xyz(xyz)` : Clones the `xyz` value, stripping any other properties `xyz(array)` : Returns `{x:array[0], y:array[1], z:array[2]}`. Useful for converting from a matrix column. `xyz_to_rgb(v)` : Returns `{r: v.x, g: v.y, b: v.z}`. This does _not_ convert between the XYZ and sRGB color spaces. Function --------------------------------------------------------- `call(f, a, b, ...)` : Invoke function `f` with arguments `a`, `b`, etc. Math --------------------------------------------------------- `abs(x)` : Absolute value `acos(x)` : Inverse cosine in radians `atan(y, x)` : Arctangent of y/x, in radians. See also the version that operates on an `xy()`. `asin(x)` : Inverse sine in radians `cbrt(x)` : Cube root of `x`. Has the same sign as the argument. `ceil(x)` : Return the nearest integer value of `x` towards positive infinity. `ceil(x, u)` : Ceil to the nearest multiple of `u` using `ceil(x / u) * u`. `clamp(x, lo, hi)` : Clamp `x` to the range [`lo`, `hi`] inclusive. `clone(v)` : Return a shallow clone of `v`. For built-in objects and functions returns the original object, because they are immutable. For map assets, automatically deep clones the 3D layer array/2D XY array. `copy(s, d)` : Copy the properties of `s` onto `d`. Both must be arrays or objects. `cos(x)` : Cosine, `x` in radians `deep_clone(v)` : Return a deep clone of `v`. Pointer structures are preserved (two pointers to the same object will be cloned as identical pointers to a different object) and cycles are supported. Functions cannot be cloned. Assets and constants can be cloned. `floor(x)` : Return the nearest integer value of `x` towards negative infinity. `floor(x, u)` : Floor to the nearest multiple of `u` using `floor(x / u) * u`. Can also be applied with the operator syntax `⌊x, u⌋`. `hash(x, y default ∅)` : A hash value of string or number `x`, returning a value on [0, 1]. If `y` is specified, the hash considers the union of the values. Useful for stable procedural generation. `lerp(a, b, t)` : Returns `a + (b - a) * t`. See also `smoothstep()` and `smootherstep()`. See also `perceptual_lerp_color()`. See also `lerp_angle()` and the 5-argument version for remapping ranges. `lerp(a, b, t, t_min, t_max)` : Returns `a + (b - a) * ((t - t_min) / (t_max - t_min))`, linearly interpolating to remap/rescale the range of `t` from [`t_min`, `t_max`] to [`a`, `b`]. The 3-argument `lerp()` performs `lerp(a, b, t, 0, 1)`. `lerp_angle(a, b, t)` : Linear interpolation between angles `a` and `b` by `t`, taking wrapping into account and always going the short way. Returns a value between `-pi` and `+pi`. `log(x)` : Natural logarithm (base _e_) of `x`, _ln x_ in math notation. `log2(x)` : Logarithm base 2 `x`, _lg x_ in math notation. `log10(x)` : Logarithm base 10 `x`, _log x_ in math notation. `loop(x, lo, hi)` : Handy for making values wrap around. Real-number relative floored modulus. Wraps `x` to the range [`lo`, `hi`). See also `oscillate()` `loop(x, hi)` : Handy for making values wrap around. Real-number floored modulus. Wraps `x` to the range [0, `hi`). `hi` defaults to `1.0` if not specified. See also `oscillate()` `magnitude(x, y, ...)` : The square root of the sum of the arguments, which must all be numbers. Same as `magnitude([x, y, ...])`. `magnitude_squared(x, y, ...)` : The sum of the squares of the arguments, which must all be numbers. Same as `magnitude_squared([x, y, ...])`. `make_spline(time_array, value_array, order = 3, extrapolate = "stall")` : Returns a function mapping numbers to values by piecewise polynomial interpolation. The value array must have types for which `*` and `+` operate (which includes numbers, all objects, `xy()`, `xyz()`, `rgb()`, and `rgba()` values), and all elements of the `value_array` must have the same type. The spline is always interpolating. The `time_array` and `valueArray` must have the same length. `order`: - `0` = nearest-neighbor interpolation - `1` = linear (polyline) interpolation - `3` = cubic (Catmull-Rom) spline interpolation `extrapolate`: - `"loop"` = cyclic. The `time_array` must have one more element than the `value_array`. That final value is the looped time that matches the start time. Similar to OpenGL `GL_REPEAT` mode for textures. - `"clamp"` = hold the final value. Similar to OpenGL `GL_CLAMP` texture mode. - `"stall"` = interpolate to additional control points that match the final value. Unlike clamping, for a cubic curve this will ensure that the velocity smoothly ramps to zero rather than creating a sharp corner. - `"oscillate"` = treat like looping with twice the period, reversing direction. This is similar to OpenGL `GL_MIRROR` texture mode. - `"continue"` = continue the curve with the trend of the final control points `max(a, b, ...)` : Largest of the arguments `min(a, b, ...)` : Smallest of the arguments `mid(a, b, c)` : Middle argument once sorted. Can be used for clamping. `noise(octaves, v)` : Applies `noise()` to an `xy()`, `xyz()`, or array position. `noise(octaves, x, y default 0, z default 0)` : Value noise on the range [-1, 1], bicubically interpolated between integer locations at octave 0. Unused dimensions default to zero. `oscillate(x, m)` : Deprecated. `oscillate(x, 0, m)`. `oscillate(x)` : `oscillate(x, 0, 1)` `oscillate(x, lo, hi)` : Like `loop` but alternately counts up and down on the interval [`lo`, `hi`], including both ends. Works with negative and fractional values. Let `k = 2 m - 2; w = loop(x, k)`. `oscillate(x, m)` is `w` if `w < m`, otherwise it is `k - w`. `pow(a, b)` : Exponent. See also the `^` operator. `round(x)` : Return the nearest integer value of `x`. Half-integers round towards positive infinity. `round(x, q)` : Return the nearest value of `x` quantized to `q`. If `q` is zero then no rounding occurs. Otherwise, `round(x, q) == round(x * q) / q`. `sign(x)` : Sign of `x`. Returns -1 if `x` is less than zero, -0 if `x` is -0, +0 if `x` is +0, and +1 if `x` is greater than zero. `sign_nonzero(x)` : Returns -1 if `x` is less than zero, otherwise returns +1. `smoothstep(t_start, t_end, t)` : Transition from 0 at `t = t_start` to 1 at `t = t_end`. All arguments must be numbers. Typically used as `lerp(A, B, smoothstep(lo, hi, t))` where `A` and `B` are colors or points. This is a common graphics function for cubic Hermite interpolation. See also the [GLSL documentation](https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/smoothstep.xhtml). `smootherstep(t_start, t_end, t)` : Transition from 0 at `t = t_start` to 1 at `t = t_end` with more gradual acceleration than `smoothstep`. All arguments must be numbers. Typically used as `lerp(A, B, smootherstep(lo, hi, t))` where `A` and `B` are colors or points. This is a common graphics function for fifth-order interpolation. `sqrt(x)` : Square root. `sin(x)` : Sine of `x` radians. `tan(x)` : Tangent of `x` radians. `x` | -5 | -4 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 ------------------|-----:|-----:|-----:|-----:|-----:|----:|----:|----:|----:|----:|----:|-----: `clamp(x, 0, 4-1)`| 0 | 0 | 0 | 0 | 0 | 0 | 1 | 2 | 3 | 3 | 3 | 3 `x mod 4` | -1 | 0 | -3 | -2 | -1 | 0 | 1 | 2 | 3 | 0 | 1 | 2 `loop(x, 4)` | 3 | 0 | 1 | 2 | 3 | 0 | 1 | 2 | 3 | 0 | 1 | 2 `oscillate(x, 4)` | 1 | 2 | 3 | 2 | 1 | 0 | 1 | 2 | 3 | 2 | 1 | 0 [Table [tblCompare]: Comparison of range restriction functions useful for animation.] Sound --------------------------------------------------------- The sound object has a single property, `sound.duration`, which is the number of seconds that it will play before ending or looping. `get_audio_status(audio)` : Returns an object describing the current properties of this audio object: - `pitch` - `volume` - `playback_rate` - `pan` - `loop` - `state`: `"PLAYING"`, `"STOPPED"`, or `"ENDED"` - `sound`: the sound being played in this audio clip - `now`: Time since the start of the sound, measured seconds. This is useful for synchronizing visuals with audio. Audio follows a more strict clock than the mode's frame event, the `mode_frames` value, and the `game_frames` values, which run in 1/60 second increments but may drift slightly due to computational demands. Graphics are time scaled, so the visual frames may be independent of both the audio clock and the frame events. `play_sound(args)` : Object version of `play_sound()`, allowing keyword argument style call. Values default according to the positional argument version. `play_sound(sound, loop default false, volume default 100%, pan default 0%, pitch default 100%, time default 0, playback_rate default 100%, stopped default false)` : Play `sound`, which must have been loaded as an asset. The time is in seconds. Full pan left is `-100%` and full pan right is `100%`. Returns an audio object that can be used with `stop_audio()`. If `pan` is an `xy()` value, it is interpreted as a world space point and the pan is computed based on the fraction of `SCREEN_SIZE` under the current transformation and camera. Note that if `set_screen_size()` is used with `private_views = true`, `xy()` pan values will produce incorrect audio. Set an explicit pan number in that case. If `stopped` is true, the sound initially starts in the `"STOPPED"` mode and can be restarted with `resume_audio()`. Useful for setting up loops that do not play immediately. `resume_audio(audio)` : Resume a sound previously stopped with `stop_audio()`. Sets the state to `"PLAYING"`. `set_pan(audio, pan)` : Set the pan of an audio object that is currently playing. 0% is centered. -100% is full left, 100% is full right. If `pan` is an `xy()` value, it is interpreted as a world space point and the pan is computed based on the fraction of the screen under the current transformation and camera. `set_pitch(audio, pitch)` : Set the pitch (in relative Hz) of an audio object that is currently playing without affecting speed. 100% is the default pitch from the file. 200% is one octave higher, 50% is one octave lower. To instead set the pitch in musical cents where 100 cents = 1 semitone = 1 half-step, use: `set_pitch(audio, 2^(cents / 1200))`. `set_volume(audio, volume)` : Set the volume of an audio object that is currently playing. 100% is the default volume from the file. `set_playback_rate(audio, rate)` : Set the rate of an audio object that is currently playing, affecting both speed and pitch. 100% is the default rate from the file. `stop_audio(audio)` : Stop playing `audio`, which was created with `play_sound`. Sets the state to `"STOPPED"`. `stop_audio(nil)` : Does nothing. This is provided because it is often convenient to stop background sound if there is one on entering a mode and do nothing if there is no background sound value in a variable, e.g., `stop_audio(background_audio)` Coordinates ----------------------------------------------------------- The quadplay default coordinate system is: ************************************************************ * 2048 (out of the screen) * ^ * \ z * \ * \0 x * *----------------------->* SCREEN_SIZE.x * |\ | * | \ | * | \ | * y | v | * | -2047 | * v | * *------------------------* * SCREEN_SIZE.y *********************************************************** [Figure [coords]: Default quadplay coordinate system.] The z-axis is used for 3D perspective via the `camera.zoom`, isometric 3D via `transform.skew`, and for z-ordering so that closer objects occlude more distant ones in z. The z-ordering can be separated from the perspective and isometric aspect for certain special effects when desired. If you use default values of the camera, transform, and `y_up`, and do not use child entities, then the coordinate systems referred to as *world space* and *camera space* match the *screen space* coordinate system and you can ignore the rest of this section. Advanced users can use the transform and camera to produce separate world space, camera space, and screen space coordinate systems from this default. Almost all standard library functions take arguments in world space. This includes drawing routines, intersection routines, and the properties of entities without parents. The functions that don't use world space are obvious, since what they do requires another space. The full set of coordinate systems supported by the standard library are: - *screen* space (ss) - *camera* space (cs) - *world* space (ws) - *entity* space (es) - *sprite* space - pixels within in a sprite - *map* space - indices of sprites in a map Entities can also form hierarchies using the `entity.child_array`. The first four coordinate systems are related by:
Screen Space (ss) _Used by_: `set_clip()`, `reset_clip()`, `get_clip()`, `intersect_clip()`, `set_post_effects()`, `get_post_effects()`
`transform_cs_to_ss()`







`transform_ss_to_cs()` _Controlled by_: `y_up` in game.json, `set_transform()`, `reset_transform()`, `compose_transform()`, `get_transform()`
Camera Space (cs) _Used by_: `touch`, `joy`, `gamepad_array[]`, `text_width()`
`transform_ws_to_cs()`







`transform_cs_to_ws()` _Controlled by_: `set_camera()`, `reset_camera()`, `get_camera()`
World Space (ws) _Used by_: `draw_sprite()`, `draw_line()`, `draw_map()`, `draw_entity()`, `draw_text()`, `draw_point()`, `ray_intersect()`, `overlaps()`, etc.
`transform_es_to_ws()`







`transform_ws_to_es()` _Controlled by_: `entity.pos`, `entity.angle`, `entity.scale`
Entity Space (es)
`transform_to_parent()`







`transform_to_child()` `entity.pos_in_parent`, `entity.angle_in_parent`, `entity.scale_in_parent`
Child Entity Space



To simplify common game programming tasks and avoid certain kinds of mistakes, quadplay has some differences from traditional graphics systems under coordinate system transformations: - Angles are always measured counter-clockwise, and routines automatically change their angle direction to compensate. Use `rotation_sign()` to determine the current direction when using `sin()` and `cos()` to convert an angle to a vector. - Sprites automatically flip vertically to match when the y axis is inverted. - The anchor points for text will transform, but font characters are never rendered with scale or rotation because pixel fonts are generally unreadable if transformed and upside-down fonts are rarely what is intended when flipping an axis. Convert your text to a sprite if you want transformed text as an effect, or use a set of fonts available at different sizes (such as deja) if you want text at different sizes to correspond to zooming. - `touch`, `gamepad_array[]`, and `joy` `xy` values match the current screen-to-camera space transformation from `set_transform` at the time that they are read. This is mostly so that controls and event coordinates will match drawing coordinates when the axis is flipped or the screen origin changed. - Every `entity` contains both a transformation to its parent (`entity.pos_in_parent`, `entity.angle_in_parent`, `entity.scale_in_parent`, `entity.z_in_parent`) and a transformation directly to world space (`entity.pos`, `entity.angle`, `entity.scale`, `entity.z`). Storing the direct transformation improves performance and simplifies many common operations. `entity_update_children(root)` recursively updates the direct transformation for an entire hierarchy. In general, `set_transform()` is good for tasks such as, "I'm making the user interface and want Y=down and 0 at top left" or "I'm drawing the world and want Y=up and the origin in the center". It cannot rotate the viewpoint. `set_camera()` is for shifting the viewpoint in 2.5D within the world coordinate system. It cannot flip axes (it only allows positive, uniform zoom), but it can rotate, translate, and zoom. Use it to show different views of the same scene, follow a character, or render a minimap. Common situations are: - No camera or transform. Use `y_up` in the game.json file if you want the y axis to go up - Move (0, 0) to the center of the screen: `set_transform(SCREEN_SIZE / 2)` - Center on the player entity, without rotation: `set_transform(SCREEN_SIZE / 2); set_camera(pos:player_entity.pos})` - Center on the player and rotate the y-axis to match (good for sprites drawn facing up): `set_transform(SCREEN_SIZE / 2); set_camera(player_entity)` - Center on the player and rotate the x-axis to the y-axis (good if you want `player.angle=0` to be the x-axis) `set_transform(SCREEN_SIZE / 2); set_camera({pos:player_entity, angle:player.angle - 90deg})` The coordinate systems are defined by transformations _from_ a coordinate system to its parent always being applied in the order of scale, rotation (`angle`), translation (`pos`). That is, objects scale along their own axes and rotate about their own origin. The transformation routines are: `transform_cs_to_ss(cs_point, cs_z default 0)` : Transform `cs_point` at `cs_z` from camera space to screen space. The `cs_z` is needed only if a skew transformation is active. `transform_cs_z_to_ss_z(cs_z)` : Transform camera space `cs_z` to a screen space z value. `transform_ss_to_cs(ss_point, ss_z default 0)` : Transform `ss_point` from screen space to camera space. `transform_ss_z_to_cs_z(cs_z)` : Transform screen space `ss_z` to a camera space z value. If the current transform has a zero z-scale then the result will be positive infinity, negative infinity, or zero because the transformation is not invertible in that case. `transform_ss_to_ws(ss_point, ss_z default 0)` : Transform `ss_point` from screen space through camera space to world space. `transform_ws_to_ss(ws_point, ws_z default 0)` : Transform `ws_point` from world space through camera space to screen space. `transform_ws_z_to_cs_z(ws_z)` : Make `ws_z` relative to the camera. `transform_cs_z_to_ws_z(cs_z)` : Make `cs_z` absolute in world space. `transform_ws_to_cs(ws_point, ws_z default 0)` : Transform world space `ws_point` to camera space. There is no transformation function for `cs_z` value because you can transform it yourself `cs_z = ws_z - get_camera().z`. `transform_cs_to_ws(cs_point, cs_z default 0)` : Transform camera space `cs_point` to world space. There is no transformation function for `cs_z` because you can transform it yourself as `ws_z = cs_z + get_camera().z`. `transform_ws_to_es(entity, ws_point)` : Transform world space point `ws_point` into the entity space of `entity`, wherever it is in the hierarchy. There is no transformation function for `ws_z` because you can transform it yourself as `es_z = ws_z - entity.z`. `transform_es_to_ws(entity, es_point)` : Transform entity space point `es_point` into world space, wherever the entity is in the hierarchy. There is no transformation function for `es_z` because you can transform it yourself as `ws_z = es_z + entity.z`. `transform_to_parent(child, child_point)` : Takes a coordinate `child_point` in the `child` entity reference frame and transforms it to the `child.parent` entity reference frame. `transform_to_child(child, parent_point)` : Takes a coordinate `parent_point` in the `child.parent` entity reference frame and transforms it to the `child` entity reference frame. `transform_es_to_es(entity_from, entity_to, es_from_point)` : Transform the `es_from_point` in the reference frame of `entity_from` to the reference frame of `entity_to`. There is no transformation function for `z` values because you can transform it yourself as `z += entity_from.z - entity_to.z`. `transform_es_to_sprite_space(entity, es_point)` : Transform `es_point` in the reference frame of `entity` into the pixel coordinates of the current `entity.sprite`. `transform_sprite_space_to_es(entity, sprite_point)` : Transform `sprite_point` in the pixel coordinates of the current `entity.sprite` into the reference frame of the entity. `transform_to(pos, angle, scale, point)` : Transform `point` into the reference frame defined by `pos`, `angle`, and `scale`. `transform_from(pos, angle, scale, point)` : Transform `point` from the reference frame defined by `pos`, `angle`, and `scale`. ### Screen Space Note that `preserving_transform` also preserves the clipping rectangle. `compose_transform(args)` : Named argument version of `compose_transform()`. `compose_transform(pos default ∅, dir default ∅, z default ∅, zDir default ∅, skew default ∅)` : Apply this local transformation _before_ the current one. Used for entering a child coordinate system. Nil arguments or properties default to `(xy(0, 0), xy(1, 1), 0, 1, xy(0, 0))`. Use `preserving_transform` or `get_transform()` plus `set_transform()` to save and restore nested coordinate systems. `get_transform()` : Returns the current net transformation as an `args` object that could be passed to `set_transform(args)`. `preserving_transform:` : The special `preserving_transform:` block saves the current transform, camera, and clipping region, executes the code in the body, and then restores the previous transform and clipping region when the block is exited (including abruptly by `return`, `continue`, or `break`). It is useful for making modular functions for handling UI scrolling, minimaps, and screenshake. `rotation_sign()` : Returns -1 if `(t.dir.x * t.dir.y > 0)` for `t = get_transform()` and +1 if `(t.dir.x * t.dir.y < 0)`. Useful for computing the coordinate transform of an entity angle. Angles are always measured counter-clockwise on *screen*. In the *draw coordinate system*, they rotate counter-clockwise if `rotation_sign() = 1` and clockwise if `rotation_sign() = -1` (which is the default). This is used by `draw_sprite()` to determine the rotation direction. See also `up_y()`. `reset_transform()` : Reset to the default transformation, `set_transform(xy(0, 0), xy(1, 1), 0, 1, xy(0, 0))`. This overrides the `y_up` flag from the .game.json file. `set_transform(args)` : Named argument object version of `set_transform()`. `set_transform(pos default ∅, dir default ∅, z default ∅, z_dir default ∅, skew default ∅)` : Set the transformation applied to all camera coordinates to compute screen coordinates. Nil values or nil properties default to their previous value. The equation applied when mapping from camera space (cs) to screen space (ss) is: - `ss.x = (cs.x + skew.x * cs.z) * dir.x + pos.x`; - `ss.y = (cs.y + skew.y * cs.z) * dir.y + pos.y`; - `ss.z = cs.z * z_dir + z`. The `dir` parameters must be +1 or -1, for flipping axes. This affects the location of sprites and text but not their own axes; text will always render right side up and sprites must be independently flipped. By default, increasing z is towards the viewer, a negative direction can be used to make increasing values farther back. The skew transform applies after direction flipping and can be used for isometric 3D. up_y() : Returns `-sign(get_transform().dir.y)`, which is -1 if Y increases downwards (the default) and +1 if Y increases upwards (`"y_up": true` in the .game.json file). The clipping region is the only object interacted with in *screen space*, via `set_clip()`, `get_clip()`, and `intersect_clip()`. The screen space graphics coordinate system is: - x increasing from 0 at the left edge of the screen to `SCREEN_SIZE.x` at the right edge - y increasing from 0 at the top edge of the screen to `SCREEN_SIZE.y` at the bottom edge - z increasing from -2047 at the back to 2048 at the front Integer coordinates correspond to the top-left corner of a pixel. Pixel centers are at integers plus ½. The top-left pixel center is (½, ½). Drawing commands sample at pixel centers. The pixel ownership rules are bottom and right, so a primitive that has a top or left boundary exactly on a pixel center will not color that pixel. This is important for adjacent primitives to keep them from double-covering a pixel on the boundary. When thinking of the screen as a 2D array of discrete pixel elements, the snapping rule is essentially that a 1-pixel rectangle centered at `pos` colors pixel `⌊pos⌋`. For example, the rectangle from (0, 0) to (2, 1) covers two pixel centers: (0.5, 0.5) and (1.5, 0.5). Setting this as the clipping region makes two pixels visible. This coordinate system favors intuition for centered, even-sized objects such as sprites. It also gives the result that you might intuitively expect for odd-sized objects at integer or half-integer coordinates such as odd-width text, 1-pixel points at integers, and 1-pixel horizontal and vertical lines. Diagonal 1-pixel lines and points at non-integer, non-half locations will round in ways that are less intuitive. `draw_point(pos, #FFF)` draws a one-pixel rectangle from `pos - ½` to `pos + ½`. So, when passed integer arguments, `pos` = (0, 0) will color the top-left pixel and `SCREEN_SIZE - 1` is the lower-right. Any value from 0 to (1 - `ε`) will write to the the first pixel. When seeking to draw individual pixels aligned with the grid, I recommend that you use integer plus 1/2 coordinates for `draw_point()` for robustness. However, if you use integer coordinates, it will "do what you expect". `draw_line()` draws a line centered on its endpoints that extends ½ pixel beyond its endpoints. When seeking to draw horizontal and vertical lines aligned with the grid, I recommend that you use integer plus ½ coordinates for `draw_point()` for robustness. However, if you use integer coordinates, it will "do what you expect". For other lines, you may be surprised by the result if they aren't at half-pixel offsets. A 4x4 sprite (or circle or text character) with its top and left edge aligned with the screen edges is centered on coordinate (2, 2), that is, half its width and height. A 3x3 object aligned as such is centered at (1.5, 1.5) but will draw in the same way if centered at (1, 1), which would be consistent with integer pixel centers. The center of the entire screen is `½ SCREEN_SIZE`. ### Camera Space Note that `preserving_transform` also preserves the camera. `get_camera()` : Returns the current camera as an `args` object that could be passed to `set_camera(args)`. `reset_camera()` : `set_camera({pos: xy(0, 0), angle: 0deg, zoom: 100%, z: 0})` `set_camera(args)` : Named argument object version of `set_camera()`. `set_camera(pos default xy(0, 0), angle default 0, zoom default 1, z default 0)` : Positions the camera used to transform other draw calls. Note that `zoom` is the inverse of a scale transformation on the camera itself. Use `set_transform()` to move the on-screen origin. `zoom` can be a number or a function, `zoom(cs_z)`. If it is a function, it computes the zoom factor for an object at `cs_z` in camera space. This is useful for creating pseudo-3D effects. If you do not want your map layers to zoom in perspective, set the `"z_scale"` value in the map.json file to a small number or zero. Enabling a zoom function with nonzero `z_scale` can make map rendering significantly slower in the case of multiple layers because it disables occlusion culling. To render sprites with parallax but 1:1 pixel scaling when using a zoom function, set the sprite or entity `scale` to `1 / camera.zoom(entity.pos.z - camera.pos.z)`. ### Entity Space Each entity has its own coordinate system defined by `entity.pos`, `entity.scale`, and `entity.angle`. The `entity.scale` is often used to flip sprites from left to right, which also flips the entity's local coordinate system. In most cases, you won't have to think about entity space beyond setting those properties. Entitys drawn with sprites and interacting with physics automatically handle the transformations. Only if you're doing detailed manipulation of children or drawing explicitly in entity space do you need to apply transformations. The direction of `entity.angle` is always counter-clockwise in screen coordinates. The direction in draw coordinates is given by `rotation_sign()`. ****************************************************************** * Default Entity Space With flipped y-axis * * ^ y * | * .---------. .----|----. * | ↶ | | |↶ | * | *-------> x | *--------> x * | | | | | * '----|----' '---------' * | * v y ****************************************************************** The axes of the entity space in draw space are given by: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const θ = rotation_sign() * entity.angle const X = xy( cos θ, sin θ) * entity.scale const Y = xy(-sin θ, cos θ) * entity.scale // or const X = transform_entity_to_draw(entity, xy(1, 0)) - entity.pos const Y = transform_entity_to_draw(entity, xy(0, 1)) - entity.pos ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Entity space is mostly used for moving a character relative to its own axes or resolving attachment points. Drawing commands always take world space arguments and physics, per-pixel hit detection, and sprite drawing automatically handle the world-to-entity transformation for you. In the rare case where you want to draw in entity space, you can transform the arguments to world space: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript draw_disk(transform_es_to_ws(entity, es_pos), es_radius * entity.scale.x, color) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This works even if the entity is deep in an entity hierarchy. When drawing a shape with more parameters, the draw commands have additional arguments to help with this process. For example: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript draw_rect(transform_es_to_ws(entity, es_pos), size * entity.scale, fill, border, entity.angle, entity.z) draw_poly(es_vertex_array, fill, border, entity.pos, entity.angle, entity.scale, entity.z)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### Sprite Pixel Coordinates Sprite coordinates are used by `get_sprite_pixel_color()`. These are integers with (0,0) on the top left of the sprite and (`sprite.size.x` - 1, `sprite.size.y` - 1) on the lower right. ### The Z-Axis All drawing commands take one or more position values, color and other parameters, and a `z` value. The value `z default pos.z default 0` is used for z-order occlusion (or the equivalent specified default for the few drawing functions that have a position named other than `pos`). The value `pos.z default z default 0` is used for clipping, camera zoom, and transform skew. Note that because of the defaults, if only one of `pos.z` and `z` is specified, that one value is then used for all z-axis properties. For example, in the common case `pos` is an `xy()` value, so it has no `pos.z` and the `z` parameter will be used. In rare cases it is useful to specify different values for `pos.z` and `z`. For example, with volumetric shadows you can draw a drop shadow that has the projection of the perspective of plane (`pos.z=0`) but is drawn with a higher occlusion order (`z = 10`), so that the shadow is cast onto other objects. Another example is for performance. Drawing many sprites sequentially with different `pos.z` values produces correct skew and zoom for them, while and forcing their `z` to the same value will cause the individual draw calls can be aggregated for better performance. ### Spritesheet Indices *Sprite sheets* use integer coordinates with (0, 0) at the upper-left corner and (`size(sheet)` - 1, `size(sheet[0])` - 1) at the lower-right corner. Instead of functions for operating on these, sprite sheets are directly accessed as 2D arrays: `sheet[x][y]`. ### Map Cell Indices *Map space* integer coordinates used by maps have (0, 0) as upper-left tile and `map.size` - 1 at the lower-right tile. Maps are typically accessed as 2D arrays: `map.layer[L][x][y]`. For convenience, `map[x][y]` is a shorthand for `map.layer[0][x][y]`. The top-left corners of map cells are at integers when converting between map and draw coordinates. When rendered, a map therefore covers the same region as the drawing command `draw_corner_rect(xy(0,0), map.size * tileSize, #f)`. The map is aligned by default so that `draw_map()` renders the top-left corner of the map to the top-left corner of the screen. Use `offset` in the `.map.json` file or `set_transform()` to alter the location of the map when rendered. Animations ------------------------------------------------------------ Each named sprite animation `spritesheet["`animation_name`"]` or `spritesheet.`animation_name of a spritesheet is an array that has the following immutable properties: `animation.period` : Number of frames before the animation repeats, which is `0` for `clamp` animations. `animation.frames` : Number of frames before the animation ends, which is `infinity` for `loop` or `oscillate` animations. `animation_frame(animation, t)` : Return the sprite to show for the animation at time `t` measured in frames, obeying the individual `sprite.frames` durations and wrapping or clamping appropriately. Because it is an array, the ith sprite is `animation[i]` and you can loop that using `array_value(animation, i)` or choose a random one with `random_value(animation)`. To create an animation, there must be a `start` property in the named entry of the .sprite.json file. Sprite ------------------------------------------------------------ Each sprite element `spritesheet[x][y]` of a spritesheet has the following immutable properties. Sprites can also be referenced by name `spritesheet["`name`"]` or `spritesheet.`name, if the named sprite has an `x` and `y` property instead of a `start` property in the .sprite.json file. `sprite.animation` : If the sprite is in an animation (a multi-frame named array from a `sprite.json` file), this is the array that contains it. `sprite.base` : The original spritesheet sprite on which this one is based. For example `sprite.rotated_90.base === sprite`. Can be used to determine the relative orientation of a transformed sprite from a map. `sprite.frames` : Number of 1/60-second (≈ 16.7 ms) game frames to hold this sprite during animation. Set by the spritesheet's `default_frames`, the animation's `default_frames`, or the animation's per-sprite `frames` array. `sprite.id` : Unique integer identifying this sprite by spritesheet and tile_index, but excluding orientation. This may change every time the program is run. `sprite.orientation_id` : Unique integer identifying this sprite, including orientation. This may change every time the program is run. The flipped versions of a sprite have _different_ IDs. To tell if two sprites are the same except for flipping and rotation, use `sprite.id` instead. `sprite.rotated_90` : Counter-clockwise 90 degree rotated version of the sprite. The bounds may be different if the sprite is not square. This is not affected by the up direction of a map or game because it is always counter-clockwise in the spritesheet. `sprite.rotated_180` : Counter-clockwise 180 degree rotated version of the sprite. The bounds may be different if the sprite is not square. Note that `s.x_flipped.y_flipped == s.rotated_180 == s.y_flipped.x_flipped == s.rotated_90.rotated_90`. This is not affected by the up direction of a map or game because it is always counter-clockwise in the spritesheet. `sprite.rotated_270` : Counter-clockwise 270 degree/clockwise 90 degree rotated version of the sprite. The bounds may be different if the sprite is not square. Note that `s.x_flipped.y_flipped.rotated_90 == s.rotated_270`. This is not affected by the up direction of a map or game because it is always counter-clockwise in the spritesheet. `sprite.scale` : An `xy()` value that is `xy(1, 1)` for sprites extracted directly from the spritesheet and may be +1 or -1 in each axis for flipped sprites. `sprite.size` : `xy()` dimensions of the sprite in pixels. Note that for a rotated sprite, `sprite.size` is not the same as `sprite.spritesheet.sprite_size`. `sprite.spritesheet` : From which this sprite is taken. `sprite.tile_index` : Integer `xy()` index of the sprite in its spritesheet. `sprite.x_flipped` : The corresponding derived sprite that is this sprite flipped horizontally. Flipped sprites are created on spritesheet load but can only be accessed through the non-flipped sprites. Note that `s.x_flipped.x_flipped == s`. `sprite.y_flipped` : The corresponding derived sprite that is this sprite flipped vertically. Flipped sprites are created on spritesheet load but can only be accessed through the non-flipped sprites. Note that `s.y_flipped.y_flipped == s`. `sprite.mean_color` : The average color of the entire sprite (not rounded to 4-bit precision). The color is computed using premultiplied alpha weighting, so low-alpha pixels contribute less to the final average. Any properties attached to a named sprite or animation in the `sprite.json` file also appear as properties of the sprite (and its flipped variations). `sprite_transfer_orientation(orient_sprite, content_sprite)` : Returns `content_sprite` flipped and rotated in the same way that `orient_sprite` was relative to `orient_sprite.base`. Note that if `content_sprite` is _already_ transformed, the new transformations will apply on top of those. Use `content_sprite.base` to instead transform relative to the canonical version in the spritesheet. Entity ------------------------------------------------------------ The entity system is optional. It is convenient for providing fast, reliable implementations of common game-like simulation operations for prototyping. There are three categories of entity abstraction: basic, physics, and hierarchy. Use whichever level of complexity is useful to you, or simply build your own system if you wish. Note that due to "duck typing", you can apply entity functions to any object that has the right subset of properties for that function. In addition to the built-in entity properties, you can add whatever properties you wish. Beware that the following properties are available as user properties today, but are likely to be added as built-in properties in the future: `density` and `friction`. ### Basic `draw_entity(e, recurse default true)` : Draw the entity, if its sprite is defined. If `recurse` is true, also draws all of the elements of `e.child_array` recursively (see the Hierarchy section). `make_entity(obj default {}, child_table default {})` : Returns a new entity object that is `obj` with defaults applied for any properties not specified. This is forward compatible to any extended properties that might be added in future versions, however, those properties might conflict with any that you add on your own. The built-in entity properties are all cloned if specified, preserving types (for example, if `pos` is an `xyz()` instead of an `xy()`, the extra property is retained). All other properties of `obj` are copied over but not cloned. If the `child_table` second argument is provided (note that this is not a property of `obj`), then all of the keys become properties of the new entity and the values are added to the entity's `child_array`. This is a convenient way to build entity hierarchies. If a `child_array` property is specified on `obj`, then `entity_add_child()` is called for each element of this array and `entity_update_children()` is automatically invoked after the new entity is constructed. The basic entity properties and defaults are: `entity.angle default 0deg` : The world space angle from the x-axis to y-axis in radians. See `draw_sprite()`. Defaults to 0. See also `entity.angle_in_parent` and `entity.orient_with_parent`. `entity.name default "entity"` : A string useful for debugging and error messages. Defaults to `"entity"` + a unique number. `entity.offset default xy(0, 0)` : A world space value added to `entity.pos` by `draw_entity()` for placing the sprite. Useful for causing the drawn position to temporarily drift from the simulated position for effects like shake or blowback after a hit. Defaults to `xy(0, 0)`. _Does not affect bounds for `overlaps()`, physics, or ray intersection._ See also `entity.pivot`. `entity.opacity default 100%` : Visibility on the interval 0 to 1. Defaults to 1. `entity.pivot` : Point on the sprite that is mapped to the center of the entity. Defaults to the pivot form the intial sprite, or `xy(0, 0)` if there was no sprite. If you change the `entity.sprite`, then you may want to also change the `entity.pivot` at the same time. See also `entity.offset` for transient effects. `entity.pos default xy(0, 0)` : The world space `xy()` or `xyz()` position of the center of the entity. Defaults to `xy(0, 0)`. See also `entity.pos_in_parent`, `entity.offset`, and `entity_update_children()`. `entity.scale default xy(1, 1)` : The world space `xy()` scaling applied to `entity.size` before translation and rotation. See `draw_sprite()`. Defaults to `xy(1, 1)`. This can be used to grow, stretch, or flip the entity. If `entity.shape == "disk"`, then the scale must have the same magnitude in both x and y. The scale may be zero or negative along either axis. `entity.shape default "rect"` : A string that is `"disk"` or `"rect"`. Defaults to `"rect"`. A `"disk"` entity must have scale that is the same in x and y. `entity.size` : The draw-space `xy()` size of the width and height of the bounding box of the entire object in its own reference frame. The rendered/collision size is `entity.size * |entity.scale|`. If `entity.shape == "disk"`, then the `x` and `y` properties must have the same value. If not specified, size defaults to the sprite's size. The size is in entity space. It does not factor in the scale. Use `entity.size * |entity.scale|` for the rendered and collision size of the entity. `entity.sprite default nil` : A sprite from a sprite sheet. Can be `nil`. `entity.override_color default nil` : Blends over the color of the sprite with this color (only works if there is a sprite). See `draw_sprite()` for details. Defaults to `nil`. `entity.z` : z-order position (a number). Defaults to zero, unless `entity.pos` is an `xyz()` value, in which case this is left as `nil` so that the `entity.pos.z` value can override. Setting both `entity.pos.z` and `entity.z` allows separating the z-order for rendering. Examples of creating an entity with different shapes: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Disk e = make_entity({pos:P, shape:"disk", size: xy(diameter, diameter)}) // Square e = make_entity({pos:P, shape:"rect", size: xy(edge, edge)}) // Rectangle e = make_entity({pos:P, shape:"rect", size: xy(w, h)}) // Line e = make_entity({pos:(A + B) / 2, shape:"rect", size: xy(magnitude(B - A), 0), angle:atan(B.y - A.y, B.x - A.x)}) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The entity system is designed to be extensible. You can add your own properties to it, and aren't required to use it at all...make direct `draw_sprite()` calls if you don't like this. You can also use the entity system but replace the rendering if you prefer. For example, if you'd like to register your own draw function callbacks instead of using the sprite rendering, you can do this: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript def draw(e): if e.draw: e.draw(e) for c in e.child_array: draw(c) def drawInBox(e): draw_corner_rect(e.pos, e.size, #FFF) draw_sprite(e.sprite, e.pos) let e = make_entity({..., draw:drawInBox}) draw(e) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Example of replacing `draw_entity` with a version that uses arbitrary callbacks.] ### Label Properties A text label is drawn for the entity if `entity.text and entity.font` is not `nil`. This is useful for showing player names, map labels, RPG stats, scores, and character dialogue. The full set of label properties is: - `entity.text` - `entity.font` - `entity.text_x_align` - `entity.text_y_align` - `entity.text_offset` (this is not affected by entity rotation but is affected by scale) - `entity.text_color` - `entity.text_shadow` - `entity.text_outline` ### Physics Properties A key relating textbook symbols and definitions to the entity property names is: Property | Symbol | Definition ----------------|----------------------------------|------------------ `pos` | _x_(_t_) | Position `vel` | _v_(_t_) = _dx_(_t_) / _dt_ | Linear velocity `force` | _F_(_t_) = _m_ • _a_(_t_) | Force `angle` | _θ_(_t_) | Angle `spin` | _ω_(_t_) = _dθ_(_t_) / _dt_ | Angular velocity `torque` | _τ_(_t_) = _I_ • _α_(_t_) | Torque `inertia` | _I_ | Inertia `density` | _ρ_ | Density `friction` | _μ_ₖ | Coefficient of kinetic friction `stiction_factor`| _μ_ₛ/_μ_ₖ | Static friction multiplier `restitution` | _e_ | Coefficient of restitution You can choose the units and coordinate system. Pixels (of distance), pixel mass, and game frames are the most common choice of units in order to simplify conversion factors. See also the Physics section for relevant functions. `entity.force` : `xy()` or `xyz()` linear force. Defaults to `xy(0, 0)`. Has units of mass * pixels / frame^2 by default. Typical forces are on the scale 1-10. `entity.density` : In mass units per square pixel. Defaults to 1. If density is `infinity`, then the object can move others but is not itself affected by collisions, and also ignores gravity. `entity.spin` : Scalar counter-clockwise rotational velocity. Defaults to 0. Has units of radians/frame by default. `entity.torque` : Scalar counter-clockwise rotational torque. Defaults to 0. Has units of mass * radians/frame^2 by default. Typical torques are on the scale 1-10. `entity.vel` : `xy()` or `xyz()` linear velocity. Defaults to `xy(0, 0)`. Has units of pixels/frame by default. `entity.physics_sleep_state` : Current sleeping state of the entity in a physics engine. Default is `"awake"`. Legal values are are `"sleeping"`, `"awake"`, and `"vigilant"` (awake and can never go to sleep). Because objects that are sleeping do not maintain contacts with other sleeping objects or parts of the environment, if you want to check for contacts (such as with the ground) on a player controlled entity then it should be `"vigilant"`. `entity.restitution` : Amount of energy preserved in a collision. Useful range is 0%-100%. The default is 10%. The net restitution applied between two entities is the geometric mean of their restitution values. `entity.contact_category_mask` : Contact (collision) categories for this object, as 32-bit flags. Two objects can only contact if `(A.contact_category_mask bitand B.contact_hit_mask) and (B.contact_category_mask bitand A.contact_hit_mask)`. Defaults to 1. Does not affect `overlaps()`. Think of the category mask as where you set bits to indicate what the entity *is* and the hit mask as what other entities it collides with. A collision only occurs if both entities agree that they can collide with each other. For example, you could define `GRASS = 0b001`, `ROCK = 0b010`, and `SHEEP = 0b100`. Setting `sheep.contact_hit_mask = ROCK bitor SHEEP`, `rock.contact_hit_mask = ROCK bitor GRASS bitor SHEEP`, and `grass.contact_hit_mask = ROCK bitor GRASS` would enable sheep to pass through grass but not rocks or other sheep, and have all combinations of rocks and grass collide with each other. Note that masks can be set independently (and asymmetrically, so that A wants to collide with B but B doesn't want to collide with a). That is because an entity may be in multiple categories (example: `HELICOPTER = VEHICLE bitor AIRBORNE bitor ENEMY bitor FAST`), and have mutliple hit masks, rather than the category being 1:1 with the entity type. The masks have no impact on performance during collision detection, but there is a limit of 32 categories. See also `entity.contact_group`. `entity.is_sensor` : If `true`, collisions will occur with this entity as usual but will not cause motion to change as a result, like a ghost. Default is `false`. This is useful for detecting triggers such as a character walking through a doorway or touching acid via the physics engine without causing that to affect the object. Note that a sensor must have infinite density or be attached to another object or it will fall out of the world by gravity. If you want to never register collisions, instead set `entity.contact_category_mask = 0` to prevent all collisions and preclude them being reported. Collision sensors have density, and it must be nonzero. Set their density to a small value, such as `epsilon`, to minimize the effect on the motion of other objects that they are attached to. `entity.contact_group` : An integer index of a group of other entities that this one should not collide with, regardless of the `contact_hit_mask` or `contact_category_mask`. Zero means no group at all. Defaults to 0. Does not affect `overlaps()`. This is useful for putting all of the parts of a multi-part object into a single group to prevent self-collisions. For example, with a rope constructed as a chain of boxes or a car with wheels. Unlike an `entity.contact_category_mask`, an object can only be in a single group. However, there is no limit on the number of contact groups that can be used in the game, while there is a limit of 32 categories. `entity.contact_hit_mask` : See `entity.contact_category_mask`. Defaults to `0xffffffff`. Does not affect `overlaps()`. `entity.stiction_factor` : Multiplier applied to `entity.friction` in the static dry friction case when an object is at rest. Useful range is 0-10. Defaults to 1. The value 0 disables static friction (since it allows the object to immediately start moving, and thus have kinetic friction engage), so that only dynamic friction is applied. The net stiction_factor between two entities is the _maximum_ of each of their stiction_factor values. Set moving platforms and conveyor belts to have high stiction to help characters move with the surface that they are on. `entity.friction` : Kinetic dry friction applied when an object is in motion, damping the velocity. Useful range is 0%-100%. Defaults to 15%. The net friction applied between two entities is the geometric mean of their friction values. `entity.drag` : Drag friction applied when an object is moving freely. Useful range is 0-10. Defaults to 0.005. There is a single drag constant for the body independent of the orientation or direction of motion. This is not a drag _coefficient_ because it is independent of cross-sectional area. ### Hierarchy The hierarchy is not a traditional scene graph. Children can opt to either compose their properties with their ancestors recursively or have absolute properties in draw space. Hierarchy functions: `entity_add_child(parent, child)` : Adds the `child` entity to the `parent.child_array` and sets `child.parent = parent`. Removes the child from any previous parent's child array if there was one. Returns the `child`. `entity_remove_child(parent, child)` : Removes the `child` from `parent.child_array`, if it is a child. Does nothing if `child` is not in `parent`. Causes an error if `child` and `parent.child_array` are inconsistent. If the child was added by the `make_entity` `childTable` as a property on parent, that property is not modified. You can remove the property explicitly using `remove_key(parent, "childName")`. `entity_remove_all(parent)` : Removes all children. `entity_update_children(entity)` : Recursively update the `pos`, `scale`, `angle`, and `offset` fields of all children and descendants from their `pos_in_parent`, `scale_in_parent`, `angle_in_parent`, and `offset_in_parent` fields. Usually called after simulation or animation processing. The hierarchy properties are: `entity.child_array` : Array of other entities that position themselves relative to this one. Defaults to `[]`. `entity.parent` : Another entity that has this one as a child. Defaults to `nil`. `entity.angle_in_parent` : Scalar counter-clockwise angle in radians relative to the parent if `entity.orient_with_parent` is true, otherwise ignored. `entity.offset_in_parent` : `xy()` `offset` relative to the parent in entity space if `offset_with_parent` is true, otherwise ignored. `entity.offset_with_parent` : Boolean. If true, then `entity_update_children()` recomputes `entity.offset` from `entity.offset_in_parent` and `parent.offset`. `entity.pos_in_parent` : `xy()` or `xyz()` position value relative to the parent, in parent space. There is no way to opt out of this property being applied recursively. Defaults to `xyz(0, 0)`. `entity.orient_with_parent` : If true, `entity_update_children()` will override this entity's angle and scale with `entity.angle = entity.angle_in_parent + parent.angle` and `entity.scale = entity.parent.scale * entity.scale_in_parent`. `entity.scale_in_parent` : `xy()` scale relative to the parent axes for flipping the child if `scaleWithParent` is true, otherwise ignored. `entity.z_in_parent` : Scalar z value relative to the parent. There is no way to opt out of this property being applied recursively. Defaults to 0. Physics --------------------------------------------------------- You can implement your own physics simulation, use entity and the entity helpers, enable the full entity physics engine, or mix these techniques. ### Entity Helpers Also see the Intersections section for computing your own collisions and intersections. `entity_apply_force(entity, world_force, world_pos default entity.pos)` : Apply world-space `world_force` to `entity` at world-space position `world_pos`, updating both `entity.force` and `entity.torque`. To apply a force at the center of mass, you can also directly add to `entity.force`. To apply a torque without a net force at the center of mass, directly add to `entity.torque`. Does not support entities with children or pivots. `entity_apply_impulse(entity, world_impulse, world_pos default entity.pos)` : Apply world-space `world_impulse` (mass * velocity) to `entity` at world-space position `world_pos`, updating both `entity.vel` and `entity.spin`. To apply a velocity change at the center of mass, you can also directly add to `entity.vel`. To apply angular velicty without a net linear velocity impulse at the center of mass, directly add to `entity.spin`. Does not support entities with children or pivots. `entity_area(e)` : Returns the world space area of `e`, which must have `shape` and `size` properties and may have a `scale` property. `entity_inertia(entity)` : Scalar inertia tensor about the center of mass. Equal to `entity_mass(entity) * magnitude(entity.scale * entity.size)² / 12` when `shape` is `"rect"` and `⅛ entity_mass(entity) * (entity.scale.x * entity.size.x)²` when `entity.shape == "disk"`. Has units of mass * pixels^2. `entity_mass(entity)` : Scalar mass equal to `entity_area()` times the `entity.density` `default 1`. Returns generic mass units. `entity_move(entity, pos default entity.pos, angle default entity.angle)` : Instantaneously moves an entity to this position and angle _as if it had naturally reached that location in the previous frame_, setting the appropriate `entity.vel` and `entity.spin`. This is useful for moving entities with infinite density in a way that causes them to interact naturally with other entities that they come into contact with. For example, use this function for implementing moving platforms, scripted set-piece animations, and elevators. `entity_simulate(entity, t default 1, region default nil, border_behavior default "clamp")` : Modifies `vel *= 1 - drag; spin *= 1 - drag`. Then, integrates `force/mass` into `vel` into `pos` and `torque/inertia` into `spin` into `angle`. Then, resets `force` and `torque` to zero and invokes `entityUpdateChilren()`. Finally, if `region` is specified, wraps or clamps the `pos` to that region. The only region shape currently supported is a finite unrotated, unscaled `"rect"`. The `border_behavior` can be `"clamp"` or `"loop"`. All changes are by mutation, so the `pos`, `acc`, `vel`, and `force` point to the same objects afterward. Translation operates in 2D or 3D, depending on the type of `entity.pos`. Rotation is always 2D. Uses a forward Euler (1st order Runge-Kutta) integrator over duration `t` measured in frames. This tends to be a more stable method for small, discrete time steps than the analytic quadratic solution and avoids the need for additional data of higher-order integrators. Does not perform collision detection or response, or enforce attachment constraints. Do not call `entity_simulate()` if you are already using `physics_simulate()` and the entity is in the physics object. Doing so will double-simulate the object. `make_contact_group()` : Creates a unique value for an `entity.contact_group`. ### Intersections These are methods for determining intersections and collisions explicitly, without using the full physics engine. `axis_aligned_draw_box(e)` : Returns a box specified by `{pos:xy(), size:xy(), shape:"rect"}` that encloses the object `e` (and all of its children if it is an entity) in draw space. `overlaps(A, B, recurse default true)` : Returns true if the bounds of `A` and `B` overlap each other, taking shape and orientation into account and incuding the borders. Ignores `offset` and `z` for entities. Each of `A` and `B` can be any of: - an entity from `make_entity()` - an `xy()` point - any object with `pos` or `corner`, and optional `shape`, `angle`, `pivot`, and `size` properties - any object with `x` and `y` fields, which will be treated as a point If `recurse` is true (the default), compares `A` and all of its children against `B` and all of its children. This is quadratic in the number of elements of each and can be slow for very deep entity hierarchies. See `entity.pivot`, `entity.shape`, etc. for discussion of how those properties affect the collision detection. `ray_intersect(ray, entity)` : The `ray` object must have `xy()` fields for `pos`, `dir`, and an optional number field for `length default infinity`. The `ray.dir` will automatically be normalized. The `entity` has `shape`, `pos`, `size`, and optional `angle` properties (an entity satisfies this, but it doesn't have to be a full entity). If there is a `entity.child_array`, then that is recursively processed and the first intersection encountered is returned. Returns the entity hit, or `nil` if there was no intersection. The `ray.length` is shortened to the hit distance. `ray_intersect(ray, array)` : Returns the first object intersected when calling `ray_intersect(ray, array[i])` on each element of the array. `ray_intersect_map(ray, map, layer default 0, sprite_callback default nil, pixel_callback default nil, replacement_array default [], run_callback_on_nil_sprites default false)` : The `ray` object must have `xy()` fields for `pos`, `dir`, and an optional number field for `length default infinity`. The `ray.direction` will automatically be normalized. The ray is in _world coordinates_. The return value is the first non-`nil` return value of `sprite_callback` or `pixel_callback`, or `nil` if the end of the ray is reached. The callback signatures are: - `def sprite_callback(sprite, sprite_pixel_coord, ws_normal, ray, map, ws_distance, ws_coord, map_coord)` - `def pixel_callback(sprite, sprite_pixel_coord, ws_normal, ray, map, ws_distance, ws_coord, map_coord)` Where the `ws_normal` is a unit vector pointing out of the sprite or pixel along the face that was hit. The `sprite_pixel_coord`, `ws_normal`, `ws_coord`, and `map_coord` are shared by every callback invocation, so make a copy of them if you wish to retain those values after the callback returns. The iterator stops immediately after the first callback returns a non-`nil` value and the `ray.length` is shortened to the hit distance. Invokes the sprite callback first for each sprite entered, and then the pixel callback repeatedly for each pixel along the ray in the sprite until the next sprite is reached. Both the sprite callback and pixel callback are optional. If neither the sprite callback nor the pixel callback is specified, then the default pixel callback is equivalent to: `def default_pixel_callback(s, p): return if get_sprite_pixel_color(s, p).a >= 50% then s else nil` Does not respect map wrapping. _The pixel callback is not supported in this release. It will be implemented soon._ ### Engine The full physics engine provides collision detection, response, and constraints. Children of entities in the physics system have no effect on physics. In future releases they will affect the collision shape, moment of inertia, and mass. While an entity is in a physics context, the following properties may not be changed on that entity: - `shape` - `size` - `pivot`, which must always be `xy(0,0)` - `scale`, except for sign changes Attachments not moved for scale changes because it is ambiguous how the attached object should be changed. If you need attachments on entities that change scale signs, then detach the two entities, move them, and create a new attachment in the new position. For example, when a character flips direction and a rope tied to their hand should now be on the other side. Changing the `contact_group`, `contact_hit_mask`, or `contact_category_mask` on the bodies in an attachment with `collide = false` after the attachment been created may cause the attached bodies to start colliding with each other. `make_physics(options default {})` : Create and return a new physics context. There can be multiple, independent physics contexts in the game. Each entity may only be in one physics context at a time. The options are: - `gravity default xy(0, -up_y())`: Default gravitational acceleration on all objects. Set to `xy(0,0)` and explicitly apply gravity if your game is not set on a planetary surface or requires excluding most objects from gravity. - `allow_sleeping default true`: Allow simple rigid bodies to fall "asleep" and stop simulating until touched. This dramatically increases simulation performance, however some entities will fail to properly waken when the object supporting them is removed and can remain floating in the air. `physics_add_contact_callback(physics, callback, min_depth default 0, max_depth default ∞, contact_mask default 0xffffffff, sensors default "exclude")` : Adds a callback that processes collisions (new contacts) at the end of `physics_simulate()`. If either entity in the collision has a `entity.contact_category_mask` that intersects the `contact_mask` and the collision has `depth >= min_depth`, then your `callback(args)` will be invoked. The `callback`'s `args` object has the following properties: - `entityA`: An entity in the contact - `entityB`: The other entity in the contact - `point0`: An `xy()` in world coordinates for the contact point - `point1`: If two points are touching in the contact, the second `xy()` in world coordinates. Otherwise `nil` - `normal`: The `xy()` unit surface normal pointing out of `entityA` for the contact plane - `depth`: a coarse measure of how hard the objects hit each other. It has a useful scale from 0-4. This is good for triggering particle effects or sound on collisions, or causing damage to objects. When doing so, use the parameters to filter: for example, `min_depth` = 1 will only trigger on significant, new contacts. There is no way to remove a contact callback. To make per-entity object-oriented callbacks, install a generic one that defers to the entities. For example, ``` def on_contact(args): with entityA, entityB, normal in args: if entityA.on_contact: entityA.on_contact(entityA, entityB, normal) if entityB.on_contact: entityB.on_contact(entityB, entityA, -normal) ``` `physics_entity_has_contacts(physics, entity, child_region default ∅, world_normal default ∅, mask default 0xffffffff, sensors default "exclude")` : Returns true if `physics_entity_contacts()` would produce any contacts with the same arguments. This is more efficient, for the case where the actual contacts are not needed. `physics_entity_contacts(physics, entity, child_region default ∅, world_normal default ∅, mask default 0xffffffff, sensors default "exclude")` : Returns an array of all current contacts, where each contact has the form `{entityA, entityB, depth, point0, point1, normal}`. The contact points are in draw coordinates. The region, mask, normal, and sensors setting are used to filter the contacts returned. A contact must have nonzero `contact_mask bitand otherEntity.contact_category_mask` for the _other_ entity involved. The average position of point0 and point1 must also lie within the `child_region`, which is in the entity's reference frame. `child_region` is either `nil` (all contacts) or an argument suitable for passing to `overlaps()`. If `normal` is specified, then the contact normal for the other entity must point within ±80° degrees of this vector. This is a draw-space vector. `sensors` may be `"include"`, `"exclude"`, or `"only"` (exclusively return contacts with sensors). In the returned contacts, the `entity` may be either `entityA` or `entityB`. This function is ueseful for determining if entities are currently in resting contact, such as whether a character is standing on the ground and can jump. In that case, make the `child_region` a box around the character's feet and set the `world_normal` to the up vector for the world. The `child_region` may have to be very wide (e.g., 2x as wide as the character) to encompass locations where contact was briefly made in a previous frame. Note that sleeping entities have no contacts. See also `physics_add_contact_callback()`. `physics_add_entity(physics, entity)` : Add `entity` to this physics context. An entity may only be in one physics context at a time. Returns the `entity`. Once an entity is added to the physics system, it will be retained in the physics system until `physics_remove_entity()` is called. Use the `draw_physics()` command or IDE button to debug physics objects that correspond to entities which are no longer being drawn. `physics_attach(physics, type, {entityA, entityB, pointA, pointB, collide default ∅, ...})` : Attaches two entities to each other or `entityB` to the world. `entityA` may be `nil` (the world) or have infinite density. `entityB` must be non-`nil` and have finite density. The points are optional attachment points in the reference frame of each entity. The attachment points do not have to be within the bounds of the entities. The entities are immediately moved to satisfy the constraint of the attachment. Two entities may not have multiple attachments directly between them. Doing so will cause unstable and undefined behavior. _There is no relationship to the entity parent/child hierarchy_. Entity children are used for explicit positioning. Attachments are for letting simulation position objects. Returns an attachment object, which can be passed to `physics_detach()`. All attachment types are massless, except for weld, which adds a slight amount of mass at the weld point and thus affects net mass and moment of inertia. If `collide` is false, then the entities cannot collide with each other while the attachment is maintained. The default for `collide` is `true` for spring and rod attachments and `false` for all other types. You can also disable collisions by putting the objects in the same `entity.contact_group`, which is more efficient. Attached entities cannot sleep. The supported values of `type` and the additional named parameters supported for each are:
`type` Description Parameters
`"pin"` Prevents translation, allows rotation. A.k.a. revolute joint.
`"spring"` Applies force to pull length back to a target distance. `length default ‖pointB - pointA‖`,
`stiffness default 0.5%`,
`damping default 0.2%`
`"torsion_spring"` Prevents translation, allows rotation, and applies torque to pull the angle back to a target. `angle default entityB.angle -` `(if entityA then entityA.angle` `else 0°)`,
`stiffness default 0.5%`,
`damping default 0.2%`
`"rod"` Rigidly maintain a fixed distance `length default ‖pointB - pointA‖`
`"weld"` Rigid attachment with the specified relative angle. Requires `entityA` to be non-`nil`. `angle default entityB.angle - entityA.angle`
`"gyro"` Free translation, but frozen rotation relative to each other. Useful for keeping characters upright. `entityA` must be `nil`. This kind of attachment is referred to in other APIs as a Cartesian joint or generalized 2D prismatic joint. `angle default entityB.angle`
`physics_detach(physics, attachment)` : Remove this attachment, which was previously created by `physics_attach()`. `physics_remove_all(physics)` : Remove all entities and attachments from this physics context. `physics_remove_entity(physics, entity)` : Remove `entity` and all attachments for it from the physics context. Does nothing if the `entity` was not already in this physics context. If called from within a collision callback, removal calls are delayed until the end of `physics_simulate()`. See also `physics_add_entity()`. `physics_simulate(physics, frames default 1)` : Simulate physics for this many frames, treating all forces and torques as constant over that period. Automatically calls `entity_update_children()` on all entities in the context. Unlike `entity_simulate()`, `physics_simulate()` does not simulate z velocity or force, however, it preserves those values if they are specified using an `xyz()` `pos`, `vel`, and `force()`. Do not call `entity_simulate()` on objects that are in the physics world or they will be double simulated. Graphics --------------------------------------------------------- ### Colors Colors in quadplay are interpreted in the [sRGB](https://en.wikipedia.org/wiki/SRGB) color space and are on the range 0 to 1 for each channel. Although the screen and GPU operate in 4-bit per channel mode, arbitrary colors (including negative and out of gamut ones) can be represented using these types. All drawing routines accept an `rgb()`, `rgba()`, `hsv()`, `hsva()`, or a sprite as a color. When a sprite is passed, the center pixel of the sprite is used. Sprites as colors are the fastest, which is useful for significant numbers of `draw_point()` calls. See the [Clouds](../console/quadplay.html?game=quad://examples/clouds&IDE=1) example program. The performance difference is negligible for other drawing routines. Quadplay internally stores 16-bit per pixel colors in the sRGB color space for sprites and the framebuffer. Drawing commands accept floating point sRGBA or HSVA colors, where all fields are on the range 0-1. HSL is not directly supported, but can be converted to and from HSV using: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript def hsl_to_hsv(src): const S = src.s * min(src.l, 1 - src.l) const V = S + src.l return hsv(src.h, 2 S / max(V, ε), V) def hsv_to_hsl(src): const L = src.v * (1 - ½ src.s) const S = if (L ≤ 0 or L ≥ 1) then 0 else (src.v - L) / min(L, 1 - L) return {h:src.h, s:S, l:L} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `artist_hsv_to_rgb(hsv)` : Object version. `artist_hsv_to_rgb(h, s, v)` : Returns an `rgb` that uses a perceptual and RYB-primary system to interpret the hue and value. In this system, `h=0/6` is red, `h=2/6` is yellow, `h=3/6` is green, and `h=4/6` is blue. The blue primary is tilted a little towards cyan in sRGB space and the values of the blue hues are boosted. See also `perceptual_lerp_color()`. `gray(n)` : Returns `rgb(n, n, n)`, with `n` clamped to the range 0 to 1. `gray(color)` : Returns `rgb(n, n, n)`, with `n` equal to a perceptually-weighted average of the rgb values from color. The RGB --> gray conversion algorithm weights will likely change in a future release. `hsva(h, s, v, a default 1)` : Returns an object with field `h` _wrapped_ to [0, 1], and fields `s`, `v`, and `a` clamped to [0, 1]. `hsva(hsv, a default 1)` : Clone the `hsv` object, adding an `a` property. `hsva(rgb, a default 1)` : Convert the `rgb` value to hsv and add an `a` property. `hsva(rgba)` : Convert the `rgba` to hsva. `hsva(hsva)` : Clone the `hsva` object, stripping other properties. `hsva(array)` : Reinterpret the array elements as an `hsva` `hsva(sprite)` : Returns the hsva value of the center pixel of the sprite. `hsv(hsv)` : Clone the `hsv` object, stripping other properties (such as an `a` channel). `hsv(rgb)` : Convert the `rgb` to hsv. `hsv(array)` : Reinterpret the array elements as an `hsv` `hsv(h, s, v)` : Returns an object with field `h` _wrapped_ to [0, 1], and fields `s` and `v` clamped to [0, 1]. `h=0` and `h=1` are red, `h=1/3` is green, and `h=2/3` is blue, with values inbetween interpolated. These are the standard hues in a computer RGB-primary system. See also `artist_hsv_to_rgb()` for RYB-primary hues. `hsv(sprite)` : Returns the hsv value of the center pixel of the sprite. `perceptual_lerp_color(c1, c2, t)` : Both `c1` and `c2` must have the same type: `hsv()`, `hsva()`, `rgb()`, or `rgba()`. Performs a linear interpolation in CIELAB perceptual space and returns a value in the original color space. See also `lerp()`, `smoothstep()`, and `smootherstep()`, which can be applied to colors, and `artist_hsv_to_rgb()` for initializing from RYB-primary hues. `rgb(hsv)` : Convert the `hsv` result to rgb, stripping other properties. `rgb(rgb)` : Clone the `rgb` value (stripping other properties, such as an `a` property). `rgb(array)` : Reinterpret the array elements as an `rgb` `rgb(r, g, b)` : Create a structure with properties `r`, `g`, and `b` clamped to [0, 1]. `rgb(sprite)` : Returns the rgb value of the center pixel of the sprite. `rgb(string)` : Returns the rgb value of hex-constant string such as "#F03". `rgb_to_xyz(c)` : Returns `{x: c.r, y: c.g, z: c.b}`. `rgba(rgb, a default 1)` : Clone the `rgb` value and add an alpha property. `rgba(rgba)` : Clone the `rgba` value, stripping other properties. `rgba(array)` : Reinterpret the array elements as an `rgba` `rgba(r, g, b, a default 1)` : Create a structure with properties `r`, `g`, `b`, and `a` clamped to [0, 1]. `rgba(hsva)` : Convert the `hsva` result to rgba. `rgba(hsv, a default 1)` : Convert the `hsv` result to rgb and add an `a` property. `rgba(string)` : Returns the rgba value of hex-constant string such as "#F03" `rgba(sprite)` : Returns the rgb value of the center pixel of the sprite. `unparse_hex_color(c)` : Returns a color string of the form `#rrggbb` or `#rrggbbaa` from any color input. Colors can also be constructed using the syntax of HTML hexadecimal colors (which are also shown in Photoshop and other drawing programs) using the syntax: - #RRGGBBAA - #RRGGBB - #RGBA - #RGB - #LL (gray) - #L (gray) For example, `#F08` is equivalent to `rgb(1, 0, 0.5333)` and `#CC` is equivalent to `gray(0.8039)`. -------------------------------------------------- The uses 4:4:4:4 RGBA color, for a total of 4096 different colors and 16 levels of transparency including fully-transparent. It can draw perfect 16-shade gradients of red, green, blue, yellow, magenta, cyan, and gray, where the darkest shade of each of those is black. Blending into the screen is based on z-order, not draw order, and is always the Porter-Duff over-compositing rule: `result.r = back.r * (1 - front.a) + front.r * front.a` and so on for `g` and `b`. The runtime is free to implement this blending exactly using sorted draw calls or an A-buffer, or to approximate it with alpha-to-coverage and multisampling or fixed-memory order-independent pixel shading techniques. Blending guarantees at least five bits per channel of intermediate precision and four bits per channel of framebuffer storage, but implementations may use more. Colors are specified as objects that have either `r`, `g`, and `b`; or `h`, `s`, and `v` fields, and an optional `a` field. All color channels are on the range 0-1 and snapped to discrete values. Additional fields in color objects are ignored. Many routines accept both a "fill" color for the interior and a "border" color for the perimeter. Set either to transparent (`nil`) to efficiently disable that portion of rendering. The `nil` value is transparent. Borders are drawn inside shape coordinate boundaries and on top of fills, so a transparent border is identical to a border that is the same color as the fill. Shapes include their endpoints and borders. ### Drawing `draw_corner_rect(args)` : Named argument version of `draw_corner_rect()`. `draw_corner_rect(corner, size, color, outline default ∅, z default corner.z default 0)` : Draw a rectangle covering all of the pixel centers between `corner` and `corner + size`. This is primarily for UI window drawing. `size` may be negative. If the top or right sides exactly touch a pixel center (at a half-integer), then they do not cover it. The direction in which the `size` applies is determined by the current transform. Note that if `up_y()` is positive, then the rectangle will extend _upward_ from the corner. See also `draw_rect()`. `draw_disk(args)` : Object version of `draw_disk()`. `draw_disk(pos, radius, color, outline default ∅, z default pos.z default 0)` : Draw a circle or filled disk centered at (`pos.x`, `pos.y`) with radius `radius` and z-order `z`. Choose the radius to be a half-integer, such as 3.5, and place the position at a half-integer if you want it to be centered on a pixel. Guarantees: - zero self-overdraw within the circle (important when using alpha) - zero overdraw between the circle outline and filled interior disk - disk exactly inscribes the containing rectangle - the total number of pixels covered will not change size under subpixel translation if the circle radius is an integer or half-integer `draw_line(args)` : Named argument version of `draw_line()`. `draw_line(A, B, color, z default A.z default 0, width default 1)` : Draw a line from (`A.x`, `A.y`) to (`B.x`, `B.y`) using z-order `z`. When using the default transformation, put the endpoints at half-pixel positions for the best alignment with the pixel grid. The endpoints of the line are transformed independently by `camera.zoom`, so 3D perspective can be obtained. The `width` will be scaled by the `camera.zoom`. The line is always at least one pixel thick in screen space. `draw_point(args)` : Named argument version of `draw_point()`. `draw_point(pos, color, z default pos.z default 0)` : Set the color of pixel (`pos.x`, `pos.y`) to color `color` with z-order `z`. A series of `draw_point` calls with the same `z` value are about twice as fast as ones with mixed `z` values or intermixed with other draw calls. For the best alignment under the default transformation, put the endpoints at half-pixel positions. `draw_poly(args)` : Named argument version of `draw_poly()`. `draw_poly(vertex_array, color, outline = (nil), pos default xy(0, 0), angle default 0, scale default xy(1, 1), z default pos.z default 0)` : Draws the _convex_ polygon defined by the `vertex_array`, first scaling by `scale`, rotating by `angle`, and then translating by `pos`. The polygon may have any winding direction. If the polygon is not convex, then the outcome of the drawn result is undefined and may change across platforms (but there will never be an error). `draw_previous_mode()` : Re-issue all of the drawing commands from the previous mode's frame that completed before the `set_mode()` call. These are not affected by the current `clip` or `transform`. The calls are in full 2.5D, so calls from this mode may insert graphics between them in the z-order. Useful for drawing the main game background during inventory, pause, menu, and dialogue modes. `draw_rect(args)` : Named argument version of `draw_rect()`. `draw_rect(pos, size, color, outline default ∅, angle default 0, z default pos.z default 0)` : Draws the rectangle _centered_ at `pos` with the given `size` and `angle`. No `scale` argument is provided because it is easy for the caller to explicitly multiply `size` by `scale`. See also `draw_corner_rect()` and `draw_sprite_corner_rect()`. `draw_sprite(args)` : Keyword version of `draw_sprite()`. Pass an object that has properties whose names match the arguments of the positional version. For example, `draw_sprite({sprite: wizard, pos: xy(100, 100), z: 2})`. Values default according to the positional argument version. `draw_sprite(sprite, pos, angle default 0°, scale default xy(100%, 100%), opacity default 100%, z default pos.z default 0, override_color default rgba(0,0,0,0), override_blend default "lerp", pivot default sprite.pivot)` : Draw sprite `sprite` with its center at `pos`. `pos` must have at least `x` and `y` fields. The sprite appears at `z` in the z-order (defaults to 0). If specified, the `scale` parameter must be a number that is not zero, or an `xy()` with nonzero fields. Scale is applied before the sprite is rotated. Sprites are not flipped or scaled by `set_transform` values, but the rotation and center are affected by the `set_transform` scaling. If it is not `nil`, the `override_color` blends with the red, green, and blue values read from each pixel of the sprite. The default blend mode is `"lerp"`, which interpolates between the sprite color and the `override_color` based on `override_color.a`. The other blend operation supported is `"multiply"`, which multiplies the `r`, `g`, `b` values and ignores the `override_color.a`. The alpha value of the result is always the sprite's original alpha value. Useful for simulating lighting, turning a sprite into a shadow silhouette, or flashing a sprite a different color to indicate hits or invincibility. The `pivot` is in the sprite's coordinate system relative to its center, so it is affected by the `angle` and `scale`. The `z` value is used for ordering. The value `pos.z default z` is used for camera zoom perspective and transform skew. `draw_sprite_corner_rect(sprite, corner, size, z default 0)` : Draw a rectangle enclosing points `corner` and `corner+size` filled with `sprite` tiled as needed (and aligned to the center), and surrounded by the neighbors of `sprite` in its spritesheet. The edges surround the specified coordinates. Used for drawing user interface elements (GUI) of dynamic size such as windows and health bars. Keep in mind that some artwork is designed to only be stretched in one dimension, or to tile only at integer multiples. See also `draw_corner_rect()` and `draw_rect()`. This routine ignores the global camera. That may change before SDK version 1.0. `draw_text(args)` : Keyword version of `draw_text()`. Pass an object that has properties whose names match the arguments of the positional version. For example, `draw_text({text: "Fire!", pos: xy(100, 100), x_align:"left", z: 2})`. Values default according to the positional argument version. `draw_text(font default ∅, text, pos, color default ∅, shadow default ∅, outline default ∅, x_align default "center", y_align default "center", z default pos.z default 0, wrap_width default ∅, text_size default size(text), markup default false)` : Print string `text` relative to `pos`. If the font is unspecified, it defaults to the system 8-pixel font. Returns a camera-space bounding box that is clipped to the current clipping region. It has fields `{pos: xy(), size: xy(), height:h, z:z}` that contains the text but not shadows and outlines. This bounding box is conservative in height and accounts for the highest ascender and accent and lowest descender. The `height` is the vertical size before clipping, which is useful for layout. `overlaps(bounds, touch)` is convenient for recognizing when the player has touched the text. The bounds are tight in width and tailored to the exact characters. The bounding box also does not include the spacing that should be added between consecutive lines. The `pos` property will have `nan` values if the bounds are entirely clipped. _In future versions, there may be more fields on the bounding box object to identify the location of markup sections._ Use `font.line_height` to know how far to place separate lines vertically or simply use strings with `"\n"` characters in them to print multiple lines. The positioning of text will follow the transform and camera, but individual letters always draw upright and 1:1 pixel perfect, without flipping, rotation, or scaling, no matter what the transform or camera are. `x_align` options: - `-1` or `"left"` = left - `0` or `"center"` = center - `+1` or `"right"` = right `y_align` options: - `-1` or `"top"` = top - `0` or `"center"` = center vertically based on the font (not actual characters, so that alignment won't change with different characters in the text) - `+1` or `"baseline"` = baseline of the first line of text - `+2` or `"bottom"` = bottom of the block of text `font` must be loaded from the assets. Color, shadow, and outline default to transparent. Any non-transparent outline causes the shadow to move below both the outline and main color. If `nil`, the system's default 8-pixel high font is used. If `wrap_width` is specified, breaks the text at the maximum width and renders multiple lines. Each line is offset by the font's vertical spacing and aligned as specified. See also `text_width()`. `text_size` restricts draw_text to only printing that many characters, *after* word-wrapping. This allows gradually revealing text in a stable way under word wrap. `markup = true` enables special syntax for changing the style within a single string and removes single linebreaks, in order to make it easier to import long text strings from constants or data files. The markup syntax is `"...{property:value text...}..."`. The valid property names are `color`, `font`, `shadow`, and `outline`. That property is valid from the beginning brace to the end one. Only one property can be changed per brace, and braces can be nested. For strings that word wrap, the vertical spacing of lines is based on the original font specified as an argument to `draw_text()`, so using very different fonts can result in poorly formatted output. The legal values are colors specified using `rgba()`, `rgb()`, `hsva()`, `hsv()`, or `gray()` with numbers, the `#` color syntax, or the names of constants specified in the `game.json` file. Variables and expressions are not permitted in the values. To insert a regular brace within markup, use a slash escape, which must be typed in a string using a double slash: `"\\{"` or `"\\}"`. To insert a single linebreak within markup, use `"{br}"`. `draw_tri(args)` : Named argument version of `draw_tri()`. `draw_tri(A, B, C, color, outline default ∅, pos default xy(0, 0), angle default 0, scale default xy(1, 1), z default pos.z default 0)` : Scale `A`, `B`, and `C` by `scale`, rotate them around the origin by `angle`, translate by `pos`, and then draw the triangle. The `pos` argument is not first because in many cases this is called with draw-space vertices and no translation. The triangle may have any winding direction. `get_background()` : Returns the value previously set with `set_background()`. `get_clip()` : Returns the current net clipping region as an object that can be passed to `set_clip()`. `get_sprite_pixel_color(sprite, pos, result default ∅)` : Returns the color of pixel `pos` in the sprite as an `rgba` value. Sprite pixel indexing puts (0, 0) in the top left corner. When `result` is not `nil`, it is used to store the result to avoid memory allocation. If the pixel is out of bounds for that sprite, returns `nil` if there is no `result` specified and `rgba(0,0,0,0)` in `result` otherwise. `intersect_clip(args)` : Object version of `intersect_clip()`. `intersect_clip(corner default ∅, size default ∅, z default ∅, z_size default ∅)` : Intersect the current clipping region with this new one. Ignores the current transformation. `reset_clip()` : Reset to the default clipping region, `set_clip(xy(0, 0), SCREEN_SIZE, -2047, 4096)`. `set_background(color default #000)` : Replace the background with this color. The background is automatically drawn each frame with the last value set. `set_background(sprite)` : Replace the background with this sprite, which must be the exact size of the screen and in a spritesheet that is the exact size of the screen. `set_clip(args)` : Object version of `set_clip()`. `set_clip(corner default ∅, size default ∅, z default ∅, z_size default ∅)` : Set the clipping region to this corner rectangle in screen space. Unspecified and `nil` values retain their current value. `corner` is the upper-left corner. `corner` and `size` must be `nil` or `xy()` values. Coordinates are clamped to the physical screen and z to the range [-2047, 2048]. `size` and `z_size` may be negative. It is impossible to set a clipping region smaller than 1x1x1 pixels. `text_width(font, s, markup default false)` : Returns the pixel width of string `s` in this font, including border padding. Note that leading and trailing numbers may incur additional empty space for column alignment. If `markup` is true, then `s` is processed using the markup rules from `draw_text()`. `xy(x, y)` : Create a structure with values `x` and `y`, which can be used with most drawing routines. `xy(xy)` : Clone the `xy` value, stripping other properties (such as a `z` property). `xyz(x, y, z)` : Create an object `x`, `y`, and `z` properties, which can be used with most drawing routines. `xyz(xyz)` : Clone the `xyz` value, stripping other properties. `xyz(xy, z default 0)` : Clone the `xy` value, adding a `z` property. -------------------------------------------- ![Text styling options](text-style.png width=500%) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript set_background(#FFF) // Make the text color clear to draw outlines. draw_text(goodNeighbors, "Transparent", xy(83, 155), rgba(0,0,0,0), nil, #0) // Make the bottom outline look thick by making the shadow and outline // the same color. draw_text(goodNeighbors, "Thick Bottom", xy(83, 170), #2AF, #0, #0) // Draw an explicit shadow wherever you want it draw_text(goodNeighbors, "Offset Shadow", xy(85, 184), rgba(0,0,0,25%)) draw_text(goodNeighbors, "Offset Shadow", xy(83, 182), #0A0) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [Tricks with text styling] ![Tricks with text styling](text-tricks.png width=200%) ------------------------------------ Font assets have the following properties: `font.line_height` : Vertical spacing between baselines in multiline text. Advance the `y` coordinate of a position by this much if explicitly rendering multiline text (you can also just put newlines in the text or use the `wrap_width` argument for automatic multiline). The `line_height` is not the vertical bounds of all characters. Some characters may exceed the spacing because they have especially high accents or ascenders or low hooks and descenders. `font.spacing` : `xy()` spacing between characters vertically and horizontally. `font.glyph_size` : `xy()` width and height that will bound all characters in this font, not including border or shadow. The `font.glyph_size.y` is usually less than `font.line_height`, which only considers the typically highest and lowest characters in English. Good bounds for centered text are `xy(text_width(font, text), font.line_height)`. Some accents or descenders for exceptional cases may stick out of these bounds vertically. ### Post Effects The final image drawn can be transformed in limited ways during scan out to the display, as if it were a sprite being drawn to the screen. Setting any post effects may slow down rendering slightly. `get_post_effects()` : Return the current post effects in a form that can be passed to `set_post_effects()`. `set_post_effects(args)` : The arguments have the following fields, all optional. `nil` values default to their previous value. Invoke `reset_post_effects()` first for a clean set. - `color` is an `rgba()` to combine with the the new frame before it is composited. Setting the `a` to 0 disables the color. - `color_blend` is one of the following strings: `"source-over"`, `"difference"`, `"hue"`, `"multiply"` describing how the `color` affects the frame before compositing. - `scale` is an `xy` for how to stretch the new frame. - `angle` is a counter-clockwise angle in radians to rotate the image about its center by. - `pos` is an `xy` translation from the center, with +y = down. Not visible in screenshots or GIFs - `motion_blur` is the amount of the previous frame to maintain into the new frame. The initial value is `0%`. - `afterglow` simulates CRT phosphor afterglow level using a fade color for both hue and intensity. The initial value is `rgb(0, 0, 0)`. - `bloom` is the amount of overdrive to use on bright areas of the screen. This may affect performance on slower machines. The default of `0%` disables the effect, `50%` is a reasonable value, and `100%` is an extreme value. The visual impact of non-default `motion_blur` and `afterglow` is accumulative and so is sensitive to graphics frame rate scaling. *`bloom` and `burn_in` effects are not visible in built-in screenshot or GIF recording capture.* Use separate whole screen capture software to record these effects. `scale`, `angle`, and `pos` are applied to the entire screen, so do not interact well with multiplayer private views configured via `set_screen_size()`. `reset_post_effects()` : Restore the defaults, which directly composite each frame. ### Resolution `set_screen_size(size, private_views default false)` : Erases the current and previous graphics drawing state, including all drawing commands for the current frame. Resets the clipping region, transform, and camera transform. Switches the screen to the new resolution immediately. It is an error to call with an unsupported resolution. `SCREEN_SIZE` will immediately be bound to a new immutable `xy()` object that has the new resolution. `VIEW_ARRAY` will immediately be bound to a new immutable array of corner rect objects. The `private_views` flag is for implementing per-player private screens for online multiplayer play, at reduced resolution. The screen configuration is the same as splitscreen multiplayer, except that each player can see only their own screen. This is an advanced feature and I only recommend it for experienced quadplay developers. Quadplay is primarily intended for shared-screen multiplayer and private screens are hard to use effectively. If `private_views == false`, then: - the supported resolutions are 384x224, 320x180, 192x112, 128x128, and 64x64 - global `VIEW_ARRAY[0].size = xy(SCREEN_SIZE)` If `private_views == true`, then: - the supported resolutions are 384x224 and 128x128 (corresponding to 192x112 and 64x64 player views) unless other features are enabled with `device_control()` - global `VIEW_ARRAY[0].size = SCREEN_SIZE / 2` - only the upper left 1/4 of the screen will be displayed on the local host machine. When playing online, the other 1/4 screens are streamed to P2 (top left), P3 (bottom left), and P4 (bottom right) when connected - screenshots and video on the host will capture all players's screens - the Show Private Views option in the Tools menu overrides the cropping when debugging In private screen mode with y increasing upwards, the player with index `0 ≤ p < 3` has screen corner at `½ SCREEN_SIZE * xy(p ∩ 1, p >> 1)`. To set up rendering for both private views and unified screens, use: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pyxlscript for view at view_index in VIEW_ARRAY: set_clip(view) set_transform(view.corner) const gamepad = gamepad_array[view_index] // Your draw code here ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Always draw all views for connected players. All of the code runs on the host machine. Program exactly as if it was split-screen local multiplayer, but with the knowledge that each player can only see their own screen. In the current API, private views are for pixels only--there is no per-player private _audio_.
(0,0)
*P1*
Host
(192,0)
*P2*
1st Guest
(0,112)
*P3*
2nd Guest
(192,112)
*P4*
3rd Guest
Map --------------------------------------------------------------------------- A map asset has the following fields. The sprites in the map can be changed at runtime (maps are the only mutable assets). Use `deep_clone(map)` to make a copy of a map at runtime if you will want the original and plan to mutate the asset. `map[x][y]` : The sprite at `x, y` in integer map coordinates in layer 0, or `nil` if empty. `map.layer[L]` : A 2D array of the contents of layer `L`. `map.layer[L][x][y]` : The sprite at `x`, `y` in layer `L`, or `nil` if empty. The elements *can* be changed at runtime. `map.size` : `xy()` size in map coordinates (not pixels). `map.size_pixels` : `xy()` size in pixels. `map.spritesheet_table` : A table mapping TMX tile set names to spritesheets for this map. `map.spritesheet` : For maps with a single spritesheet, this is the spritesheet. `map.sprite_size` : The `xy()` size of source sprites in pixels. `map.offset` : `xy()` offset of the map when reading: `ws_coord = map_coord * map.sprite_size + map.offset` `map.loop_x` : If true, the map wraps horizontally. Array access does not automatically wrap, but functions such as `get_map_sprite()`, `map_find_path()`, and `get_map_pixel_color()` will implement wrapping. See details on each map function to see how it supports wrapping. `map.loop_y` : If true, the map wraps vertically. Array access does not automatically wrap, but functions such as `get_map_sprite()`, `map_find_path()`, and `get_map_pixel_color()` will implement wrapping. See details on each map function to see how it supports wrapping. `map.z_offset` : Amount to add to each layer index when computing a z value for rendering. `layer = (z - map.z_offset) / map.z_scale`; `z = layer * map.z_scale + map.z_offset`. See also `transform_ws_to_map_space()` and `transform_map_space_to_ws()`. `map.z_scale` : Amount to multiply each layer index to compute a z value for rendering. `layer = (z - map.z_offset) / map.z_scale`; `z = layer * map.z_scale + map.z_offset`. See also `transform_ws_z_to_map_layer()` and `transform_map_layer_to_ws_z()`. Any other custom properties set on the map in the TMX file are added to this set of properties. The following functions relate maps, pixels, and sprites. In them, a "pixel coordinate" is in the _pre-transformed_ space in which graphics commands operate and a "map coordinate" is in the space of the map indices. The map's z-axis corresponds to layers. `draw_map(args)` : Draw the map using the named argument object. `draw_map(map, min_layer default 0, max_layer default length(map.layer), replacement_array default ∅, pos default xy(0, 0), angle default 0, scale default xy(1, 1), z default pos.z default 0, override_color default rgba(0,0,0,0), override_blend default "lerp")` : Draws the map with the current transform. If there is no transform, the map is drawn with the first tile's center at (0, 0), which means that the tile will be half off the screen. If specified, `min_layer` and `max_layer` determine the inclusive range of layers to draw. Map rendering ignores skew transformations...typically the skew will be implicit in the map's layout and rendering. If `replacement_array` is specified, it must be an even length array of the form `[from0, to0, from1, to1, ...]`. Each `from` value is a sprite in the map. It will be replaced during drawing with the corresponding `to` value. The `to` values may also be `nil`. If specified in the source `.json` file, the map will wrap appropriately horizontally or vertically when drawn, if it is large enough to covers at least 1/3 of the current clipping area. The `pos`, `angle`, and `scale` apply the position of the map as if it was a giant object, with the origin centered at the center of tile (0, 0). Using a non-default `angle` and `scale` will approximately double the cost of drawing the map. For wrapping maps, the map is rendered 9x (at very little performance penalty). It does not tile infinitely. If the game explicitly relocates the camera when it has moved off of the end of the map, then it will _appear_ infinite in game. The `z` value is used for z ordering. The value `(pos.z default z)` is used for transform skew and perspective camera zoom. The `map.z_scale` and `map.z_offset` affect both. `draw_map_span(args)` : Object version of `draw_map_span()`. `draw_map_span(start, size, map, map_coord0, map_coord1, min_layer default 0, max_layer_exclusive default +infinity, replacement_array default [], z default start.z default 0, override_color default rgba(0,0,0,0), override_blend default "lerp", invert_sprite_y default (get_transform().dir.y < 0), quality default 100%)` : Draws a perfectly horizontal (regardless of the camera) span of constant z with pixel colors taken from the map. This is a key primitive for building fast perspective rendering of horizontal surfaces. See the Kart and Warlock 3D examples. It can also be used for 2D textured polygon rendering. `start` is an `xy()` or `xyz()` value on screen at the left edge of the span. `size` is the extent of the span, which must be 0 along the `y` axis (vertical spans are reserved for future expansion). The span will be transformed by the `camera.pos`, but does not rotate with the `camera.angle`. `map_coord0` is an `xy()` in map coordinates for the source colors, which corresponds to the `start` pixel on screen. `map_coord1` is the end coordinate in map coordinates. `map`, `replacement_array`, `min_layer`, `max_layer_exclusive`, and `invert_sprite_y` are as for `get_map_pixel_color()`. `override_color` and `override_blend` are as for `draw_sprite()`. `quality` is a number between 0% and 100% that affects the texture mapping precision. The implementation details may change in the future. In the current implementation, any value below 50% samples at half resolution. Respects the wrapping and wraps infinitely. `map_find_path(map, start_xy, goal_xy, edge_cost, cost_layer default 0, use_sprite_id default true)` : Specialized version of `find_path` for maps. The start and end positions are rounded down to the nearest integer map coordinates. The `edge_cost` is either a function or an array. The function returns an array of map coordinates to visit or `nil` if no traversable path exists. The function version is `def edge_cost(A, B, map)`. It returns the cost of traversing from `A` to `B` in the map, where each is an `xy()`. That cost should always be nonnegative. It should be 1 for a typical edge, `infinity` for an edge that is not traversable, and higher or lower than 1 for edges of varying difficulty such as walking through rough terrain or downhill. The array version of `edge_cost` is an array of pairs of the form `[sprite0, cost0, sprite1, cost1, ...]`. Each cost corresponds to entering that sprite on `map.layer[cost_layer]`. These are the `B` values from `edge_cost`. Empty map cells and sprites that are not specified in the array cost 1 to traverse, and it costs `infinity` to leave the map bounds. Sprites are matched by their `sprite.id` if `use_sprite_id` is true, otherwise they must exactly match, including orientation. Supports map wrapping. `get_map_pixel_color(map, map_coord, min_layer default -infinity, max_layer_exclusive default +infinity,replacement_array default ∅, result default ∅, invert_sprite_y default false)` : Returns the color of the pixel in `map` at the corresponding `map_coord` and all layers between `min_layer` and `max_layer_exclusive`. If an `rgba()` value `result` is provided, then that object is mutated and returned to avoid memory allocation. Returns `nil` for out of bounds values and empty sprites if `result` is not specified, and sets `result.a = 0` in those cases if it is present. See also `get_map_pixel_color_by_ws_coord()`. If `invert_sprite_y` is true, then individual sprites pixels are read upside down. This is needed in rare cases where the transform's y direction was changed between when the map itself was drawn and when the pixel was read. `get_map_pixel_color_by_ws_coord(map, ws_coord, ws_z_min default -infinity, ws_z_max_exclusive default +infinity, replacement_array default ∅, result default nil)` : Returns the pixel color in `map` at the corresponding `ws_coord` by compositing all layers between `ws_z_min` and `ws_z_max_exclusive`. The result is guaranteed to match what was drawn by `draw_map()`. If `result` is specified, that `rgba()` value is mutated and returned instead of allocating a new color. Returns `nil` for out of bounds values and empty sprites, or a result with `result.a = 0` if `result` is specified. Respects the wrapping and can wrap infinitely. See also `get_map_pixel_color()`. `get_map_sprite(map, map_coord, layer default 0, replacement_array default ∅)` : Returns the sprite in `map` at the corresponding `map_coord` and `layer` using rounding. Returns `nil` for out of bounds values. See also `get_map_sprite_by_ws_coord()`. Respects the wrapping and can wrap infinitely. `get_map_sprite_by_ws_coord(map, ws_coord, ws_z default 0, replacement_array default ∅)` : Returns the sprite in `map` at the corresponding `draw_coord` and `ws_z` using `transform_ws_to_map_space()` and rounding to compute the coordinates. Returns `nil` for out of bounds values. See also `get_map_sprite()`. Respects the wrapping and can wrap infinitely. `map_generate_maze(args)` : Replace a layer of the map with a maze. The map is approximately the same as its original size, but may be slightly larger or smaller if that is needed to satisfy the options. - `map: map` - `layer: number default 0` - `hall: {thickness: integer default 1, sprite: sprite default nil} default {}`. Description of the passageways. The `sprite` should be from `map.spritesheet` or `nil`. `thickness` is measured in map cells. - `wall: {thickness: integer default 1, sprite: sprite default map.spritesheet[0][0]} default {}`. Description of the dividers between passageways and inaccessible aeas. Same options as for `hall`. - `horizontal: {border: number default 1, loop: boolean default false, symmetric: boolean default false} default {}`. The border is the number of walls (not cells) around the outside of the map. Because the wall layers can themselves cover more than one cell, the border can be a fractional value to create partial walls on the exterior. - `vertical` same options as `horizontal` - `shortcuts: number default 0%`. Amount of additional hall connections beyond a continous labryinth path. - `straightness: number default 0%`. Preference for long straight halls (100%) vs. short twisty ones (0%). - `coverage: number default 100%`. Amount of the map layer reachable by halls. The remainder is wall. - `dead_end_array: array default []`. If provided, then this array will be filled with the `xy()` map coordinates of dead-end passageways, which are good locations for spawn points or building rooms. - `random: function default random`. Random number generator to use. `map_resize(map, width default map.size.x, height default map.size.y, layers default size(map.layer))` : Resizes the map in place. Newly revealed values will be `nil`. Maps should only be resized using this function. There is no guarantee that the individual arrays themselves will be preserved--they may be reallocated and copied, although the values will be preserved. So, do not hold pointers into the map across `map_resize()` calls. `set_map_sprite(map, map_coord, sprite, layer default 0)` : Sets the corresponding map sprite. Does nothing if `map_coord` is out of bounds. Respects the wrapping and can wrap infinitely. `set_map_sprite_by_ws_coord(map, ws_point, sprite, ws_z default 0)` : Sets the corresponding map sprite. Does nothing if `ws_point` is out of bounds. Respects the wrapping and can wrap infinitely. `transform_ws_to_map_space(map, ws_point)` : Returns the _fractional_ map coordinates of `ws_point`. See also `transform_map_space_to_ws()`. Note that integer map coordinates are at the corners of map tiles unless the map JSON has an `offset`. To account for a map that is rendered by `draw_map()` with its own `pos`, `angle`, and `scale`, use `transform_ws_to_map_space(map, transform_to(pos, angle, scale, ws_coord))`. `transform_ws_z_to_map_layer(map, ws_z)` : Using the current transformation, returns the _fractional_ map layer of `z`. `transform_map_layer_to_ws_z(map, L)` : Returns the `z` value that will be used for `map` under the current transformation. `transform_map_space_to_ws(map, map_coord)` : Returns the draw coordinates of `map_coord`. Integer `map_coord`s correspond to the corners of tiles if the offset on the map is zero (the default). See also `transform_ws_to_map_space()` and `get_map_pixel_color()`. To account for a map that is rendered by `draw_map()` with its own `pos`, `angle`, and `scale`, use `transform_from(pos, angle, scale, transform_map_space_to_ws(map, map_coord))`. Frame Hooks ---------------------------------------------------------- Frame hooks are useful for registering animation callbacks that happen at the beginning of every frame or after a set number of frames. The order of execution between frame hooks is undefined and can change as hooks expire. The transform for a frame hook is whatever was set at the end of the previous frame. When drawing, use `z` arguments to ensure that drawn objects overlap in the desired order regardless of the order in which drawing executes. `add_frame_hook(callback, end_callback, frames, run_in_mode default get_mode(), data default nil)` : Register a `callback` function to run at the end of the "frame" event for the given number of `frames`. The return value of `add_frame_hook()` is a hook that can be passed to `remove_frame_hook()`. - `callback`: `def callback(frames_left, total_frames, data)`, runs every frame. `frames_left` will be `total_frames - 1` on the first frame and `0` on the last one. This argument may be `nil`. If the callback returns `true`, the hook is removed without running the `end_callback`, otherwise it keeps running. - `end_callback`: `def end_callback(data)`, runs on the last frame. Can be `nil`. - `frames`: Number of frame events to run for. May be `infinity`. - `run_in_mode`: Only execute while the game is in this mode. Use `"all"` to run in every mode. The frame counter is only decremented if the hook returns. If the mode change happens due to `push_mode()`, `pop_mode()`, or `set_mode()` inside of the `callback` then the frame counter remains and the same callback will execute when the mode returns unless it removes itself. See also `sequence()` and `delay()` for common cases. `delay(callback, delay_frames default 0, data default nil)` : Same as `add_frame_hook(nil, callback, delay_frames, nil, data)`. It is often convenient to use a local function defined immediately before the `delay()` as the `callback`. The delayed callback only runs in the current mode. Returns the new frame hook. `remove_frame_hook(hook)` : Remove this frame hook immediately, without executing its `end_callback`. If the hook has already ended or is not present, nothing happens. If the hook is removed while frame hooks are executing, it may still execute this frame. `remove_frame_hooks_by_mode(mode)` : Remove all frame hooks that are locked to the `mode`. Use `nil` to remove all hooks with no mode at all. Do not invoke the `endCallback`s on these hooks. `sequence(...)` : Add a series of frame hooks that run in the current mode. Returns a master hook that can be used with `remove_frame_hook()` to cancel the entire sequence. Each argument may be either a function, `nil`, a number of frames to skip, or `{callback: function, begin_callback: function, end_callback: function, frames: number, data: (any value)}`. The callbacks only run the current mode. If not `nil`, the `callback` signature is `def callback(frames_left, total_frames, data)`. If not `nil`, the `begin_callback` signature is `def begin_callback(data)` and it runs immediately before the first `callback` for that sequence entry. If not `nil`, the `end_callback` signature is `def end_callback(data)` and it runs immediately after the last `callback` for that sequence entry. The callbacks run for the specified number of `frames`, rounded if fractional. If the `frames` is less than 1, then then they do not run at all. Example: `sequence(6, draw_explosion, {callback: draw_fire, frames: 20}, {callback: draw_smoke, frames: 4, end_callback: cleanup})`. Skips 6 frames, runs `draw_explosion` after the 7th, `draw_fire` after each of the next 20 frames after that, `draw_smoke` for the next 4 frames after _that_, and then `cleanup` immediately after the last `draw_smoke` in the same frame. If a callback returns the special value `sequence.NEXT`, then the sequence runs the `end_callback` for that entry and advances to the next step regardless of the frame count. If any callback returns `sequence.BREAK`, then the sequence immediately terminates and does not run the `end_callback` for the current entry. All other return values are ignored. A `sequence` entry can itself trigger a new sequence or `delay()`, although be careful not to trigger uncontrolled recursive chains. Note that `sequence(...array)` allows passing an array of the individual callbacks instead of as separate parameters. Returns a main frame hook that can be removed to cancel the entire sequence. Time ---------------------------------------------------------- always runs your `frame` event code at 60 fps. You can hardcode this assumption into your program. For example, the built-in simulation routines all use "frame" as the unit of time (and "pixel" as the unit of distance). The _display_ may not refresh at 60 fps if your program is consuming too much computation, because the runtime will start dropping graphics calls to maintain the simulation rate. It will scale all the way down to 12 fps on refresh while still trying to execute your main loop at 60 fps so that simulation is unaffected. `game_frames` : Number of elapsed 1/60 frames since the program started. The program may mutate this global variable. `mode_frames` : Number of elapsed 1/60 frames since the current mode was most recently entered. The program may mutate this global variable. `now()` : Returns a time in seconds that is accurate to at least one millisecond. The origin for the time is arbitrary but constant per program run. Do not use this time for simulation because it will have inconsistent jumps between frames dues to frame rate scaling. `local_time()` : Returns an object with the following properties, set in the current time zone: - `year`, e.g., 2019 - `month` 0-11 - `day` 1-31 - `hours` 0-23 - `minute` 0-59 - `second` 0-59 - `millisecond` 0-999 - `weekday` 0-6, where 0 = Sunday - `day_second` seconds since midnight, including a fractional part - `timezone` minutes from UTC - `absolute_milliseconds` Milliseconds since 1970-01-01 in UTC See `format_number()` for the various clock formatting options for numbers in seconds, and `now()` for a high-precision timer. `local_time(args)` : Returns an object with the same structure as `localtime()` initialized from a specific [time zone](https://www.timeanddate.com/time/zones/), which is then converted to local time and returned. If `args` is a single string of the form `"hh:mm:ss DD MM YYYY TZ"` such as `"16:30:10 21 Nov 2022 PST"`, it is parsed. All elements are optional and default to the lowest possible value, except for the time zone which defaults to the current time zone. `args` may instead be a structure with `year`, `month`, `day`, `hour`, `minute`, `second`, `millisecond`, and `timezone` minutes from UTC fields, which will then be converted using the `timezone` field to a local time. AI ---------------------------------------------------------- ### AI Gamepads Virtual gamepads follow the same API as `gamepad_array[]` elements. These allow implementing AI players by synthetic control inputs instead of direct logic within simulation. `make_bot_gamepad(color default gray(80%), name default "bot", index default nil)` : Just as with the `gamepad_array[]` gamepads, the gamepad's directional properties will be read with respect to the current transform at the time they are read. Has the same properties as a `gamepad_array[]` element, including the derived properties `xy`, `angle`, `pressed_a`, etc. `update_bot_gamepad(gamepad, controls, absolute default false)` : Updates the state of the bot gamepad by sending it virtual data for polling. The `controls` object has the following properties representing the current state of the gamepad controls. Unspecified elements are assumed to have the value `0`. If `absolute` is true, the current transform is ignored when interpreting directional input. `x`, `y`, `a`, `b`, `c`, `d`, `e`, `f`, `q`. The `p` button is reserved for pause and cannot be pressed on an AI gamepad. In absolute (default ) coordinates, `y` = +1 is pulling the gamepad D-pad down and `y` = -1 is pushing it up. In relative coordinates it will follow the current transform at the time this function is called. ### Board Games `find_move(player_index, game_state, generate_moves, apply_move, static_evaluate, max_depth default 2, unpredictability default 0, debug_move_to_string default nil)` : Chooses the best move for the current player in a two player, zero sum game with perfect information and discrete moves by game tree search using the negamax algorithm. This can handle most two player board games. - `player_index`: 0 for the maximizing player, 1 for the minimizing player. This must be the current player in `game_state` - `game_state`: state of the game, an arbitrary data structure (which probably also keeps track of the `player_index` itself…) - `generate_moves(game_state)`: function that returns an array of moves, an arbitrary game-specific data structure - `apply_move(game_state, move)`: function that returns a new game state after a move has been performed. This must clone the state and not mutate it - `static_evaluate(game_state)`: function that evaluates a game position as a signed number. Higher is better for `player_index` 0, lower is better for `player_index` 1. `inf` is a win for player 0, `-inf` is a win for player 1. Returns `"draw"` for a draw (tie). - `max_depth`: integer number of turns to look ahead. Minimum 1. Time cost is exponential in max_depth - `unpredictability`: magnitude of random number to add to the `static_evaluate()` values to create variation in play. Use a small value, like `ε` to add variation when choosing between equally good moves and a large number (like the value of a minor piece) to add surprising or suboptimal play. - `debug_move_to_string(move)`: function that maps a move to a string. If specified, this is used for printing the tree as it is searched to the output window. `make_move_finder(player_index, game_state, generate_moves, do_move, static_evaluate, max_depth default 2, unpredictability default 0, debug_move_to_string default nil)` : This allows the search to continue over multiple frames when it goes deep into a game tree. The arguments are the same as for `find_move()`. Returns a function `finder(max_time default 0.25 * 1/60)`. That function runs for about `max_time` in seconds and then returns an object `{progress: percentage, move: move}`. If it has found a good move for this player, then `progress = 100%` and `move` is the best move known to the AI. Otherwise, `progress` is less than 100% and `move` is `nil`. ### Path Finding `find_path(start_node, goal_node, estimate_path_cost, edge_cost, get_neighbors, node_to_ID, graph_obj default ∅)` : `find_path()` is a general-purpose path-finding routine for any kind of world representation. It uses the "A*" (A-star) algorithm. _See also `map_find_path()` for a simpler, map-specific version._ The return value is an array of nodes to traverse to follow the shortest path discovered. The first node in the returned path will be the `start_node` and the last node will be the `goal_node`. `find_path()` will return `∅` if no path exists between the `start_node` and `goal_node`. To recover the cost of the path, use the idiom: ````````````` PyxlScript let cost = 0 for i < size(path) - 1: cost += edge_cost(path[i], path[i + 1]) ````````````` The arguments are below. See the example of using these on a map for a better understanding of how they are typically computed. `start_node` : The starting location, in your chosen node representation. `goal_node` : The ending location, in your chosen node representation. `estimate_path_cost(node_from, node_to, graph_obj default nil)` : Heuristic function used to significantly accelerate approximate path finding. Returns a numerical estimate of the cost for the path between `node_from` and `node_to`. *For example, a good common implementation is to return the straight line distance between the nodes*. The more accurate this estimate is, the faster and better that the algorithm will run. If this function returns 0 for all inputs, then the algorithm will run in the worst case quadratic time but is guaranteed to find the exact shortest path instead of an approximation. Note that the function must work for _any_ two nodes in the graph. `node_from` and `node_to` are not generally not the `start_node` and `end_node`, and are not generally neighbors or even connected by a viable path to each other. `edge_cost(node_from, node_to, graph_obj default nil)` : The actual cost of going from `node_from` to `node_to`, which are guaranteed to be neighbors that were previously discovered by `get_neighbors()`. Together, `edge_cost()` and `get_neighbors()` define the graph structure. `get_neighbors(node, graph_obj default nil)` : Returns an array of all neighbor nodes reachable from node. It is not required that neighbors are bidirectional. For example, a ledge may create an edge traversable in only one direction `node_to_ID(node, graph_obj)` : Returns a compact integer or string representation of the node, which is required internally by the algorithm for maintaining a table of nodes. `graph_obj` : Any object that represents your world graph. This can be `nil` if the other function arguments use global state. ### Map Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const sprite = map.spritesheet_table["mapTiles"] const wall = sprite[0][0] const swamp = sprite[0][1] const dir = [xy(-1, 0), xy(0, -1), xy(0, 1), xy(1, 0)] // Assume all edges cost 1 def estimate_path_cost(A, B, map): return |B.x - A.x| + |B.y - A.y| // Moving through swamps is slow def edge_cost(A, B, map): return 1 + (get_map_sprite(map, A) == swamp) // Block traversal at map edges and walls def get_neighbors(A, map): const neighbors = [] for d in dir: const B = A + d; const s = get_map_sprite(map, B) if s and s != wall: push(neighbors, B) return neighbors // Map to integer indices def node_to_ID(A, map): return A.x + map.width * A.y // In map coordinates let start_node = xy(1, 1) let goal_node = xy(20, 20) let path = find_path(start_node, goal_node, estimate_path_cost, edge_cost, get_neighbors, node_to_ID, map) // Print the path for P in path: debug_print(P) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Debugging ------------------------------------------------------------------------------------ The following are provided for debugging and development support only. They may do nothing on some platforms and may crash on others. These APIs are subject to change. None of these are regular functions or variables. They are special syntax that cannot be bound as values. For enabling or disabling large sets of debugging statements at once in a program, they can be masked using standard boolean operators. For example: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Error on unimplemented routine todo("Implement explosions") // To customize the startup mode per user when working on a team, // add a conditional set_mode() to your program's general startup mode (for example, Title) if IDE_USER == "forrester": set_mode(Inventory) because "Forrester is debugging inventory" // Make a global flag that jumps to gameplay in a debug build by putting // this in your Title or other startup mode if DEBUG: set_mode(Play) because "in DEBUG build" // Only error if your own debug_graphics constant is true. // You can change a constant from the IDE while the program // is running! debug_graphics and todo("Implement explosions") // Error if running in the IDE using a local quadplay server // and the current developer's local machine username is "stephan" (IDE_USER == "stephan") and todo("Implement explosions") // Only print if debug_graphics is true. debug_graphics and debug_print("Running iteration" + i) // Print when your own LOG_LEVEL constant is greater than 5 (LOG_LEVEL > 5) and debug_print("Simulating") // Print when the 2nd bit of your own LOG_MASK constant // is set (LOG_MASK bitand 0b010) and debug_print(format_number(x, "%")) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ `assert(x, msg default "Assertion Failed")` : If `x` is true, does nothing. If `x` is false, creates the error `msg`. Assertions can be completely disabled from the Tools menu to make them have zero cost. In the current implementation, the `msg` is always evaluated when assertions are enabled even if the assertion test passes. `debug_pause()` : Pause program execution. `debug_print(x, ...)` : If `x` is a string, directly prints it to the Output window, otherwise prints `unparse(x, 3)` to the Output window. When multiple arguments are provided they are processed individually with spaces between them. Debug print statements can be completely disabled from the Output window, which causes them to consume no processing time. In the output pane, click on the output to go to the line that produced it in the code editor. `debug_watch(expr)` : Make the expression appear along with its value at the time that `debug_watch` executs in the Watch pane of the debugger. If there are multiple expressions that are syntactically identical, they will be collapsed and only the last to execute each frame will be shown. This is *not* a function. It is a special syntax. You may not pass the `debug_watch` command as if it were a function. In the watch pane, click on the expression to go to the line that contains it in the code editor. `draw_bounds(e, color default gray(60%), recurse default true)` : Render a debugging view of the bounds of an entity or other object `e` with `pos` and optional `angle`, `size`, `scale`, and `shape` properties. If `recurse` is true, descend into any `e.child_array` property. `draw_physics(physics)` : Render a debugging view of the physics system `IDE_USER` : A string constant bound to the `USER` environment variable for the operating system. This is "anonymous" when not running the IDE or a local server. `todo(message default "")` : If todo errors are enabled in the IDE, reports the error `"Unimplemented: " + message`, otherwise does nothing. The `message` must be a compile-time constant string, similar to `because`. Persistence ----------------------------------------------------------- The local persistence API stores values in the browser's local storage. It is tied to a specific web browser and the URL from which is hosted, as well as the URL of the game. Some browsers will automatically synchronize local storage across computers, however. The storage API performs string manipulation and can be slow relative to other commands. Avoid using it every frame. This API is useful for save games, GUI state defaults, high scores, and persistent worlds. `load_local(key)` : The key is loaded and deserialized with `parse()` to return an object that was previously saved with `save_local()`. If the `key` is not found, then `∅` is returned. The idiom `load_local(key) default def_val` is convenient for specifying a default when no previous value was stored. `load_local()` : Return an object mapping all stored keys to their values. The empty object `{}` is returned if there are no keys. `save_local(key, value)` : The value will be serialized to a string with `unparse(value, 0)` and then stored associated with `key` and the URL of the game. Save a value of `∅` to delete the key. There is a maximum size of 4096 characters for the unparsed value for each key and a maximum size of 64 keys per application. There is no way for two different games to read each other's values. `save_local(object)` : Replace all previous keys with these key value pairs. The same size restrictions apply as for the individual key-value pair. `save_local()` : Delete all previous key-value pairs Network ------------------------------------------------------------------------------------ All games with local multiplayer automatically have shared screen network multiplayer support. Online games are programmed exactly the same as local games. Players can begin hosting or join another game as a guest using the system pause menu or the game launcher. You can make your own games easier to join or support private screens using the network API. These are still programmed with an entirely local model. The network API is for improving the user experience when joining games and supporting optional private per-player screens, not state synchronization or messages. `HOST_CODE` : A string of the six words that are the online ID this host uses. Can change at runtime if the player intentionally changes it. `start_hosting(show_buttons default true)` : Start hosting as soon as possible, if the `--offline` flag was not set. Check `HOST_CODE` to display to the players so they know where to tell guests where to connect to. If already hosting, just changes the state of the button display if the argument has changed. Check `gamepad_array[0].status == "host"` to see when hosting has started. If `show_buttons` is true, then the Copy Host Code and Copy Host URL buttons are displayed on the bottom of the screen _in this mode_ until the game stops hosting or `start_hosting(false)` is called. `stop_hosting()` : Stop hosting as soon as possible. Check `gamepad_array[0].status != "host"` to see when hosting has ended. `push_guest_menu_mode()` : Show the menu for connecting to hosts and changing your own name if the `--offline` flag was not set. If the player connects to a host, then the local game remains paused. See also `start_hosting()` `disconnect_guest(gamepad_index)` : Immediately drop the player with `1 <= gamepad_index <= 3` if they are connected as a guest. See also `gamepad.online_name` and `gamepad.status` for information about guests. See `set_screen_size()` for enabling per-player private screens instead of shared-screen multiplayer. _The current implementation of online multiplayer is in testing. It has the following limitations, some of which will be removed in the final version. Current online play can only be entered from 384x224 and 192x112 resolution, although it can be used with games in any resolution. Disconnecting and reconnecting could change the order of players. Only one player is supported per computer. Games with complicated animated backgrounds and background music are more likely to be laggy (pausing the game can reset the lag). As host, you cannot currently kick or ban players, or change the order of players. As a guest, you cannot currently leave without restarting . Raw Device Access ----------------------------------------------------------- `device_control(command, ...)` : Reserved function for executing device-specific features such as the game launcher, GPIO, and alternative input devices. `command` is a string telling the host which control feature to execute, and the remaining arguments are passed to it. All `device_control()` commands may change in future releases and may fail silently on any given platform or when not using the IDE.
Parameters Effect
`"start_GIF_recording"` Begin GIF recording.
`"stop_GIF_recording"` End GIF recording. When the compression completes, the GIF will appear in a new tab.
`"start_preview_recording"` Begin preview video recording. There is no "stop" command because this terminates automatically after a set time.
`"take_screenshot"` Take a screenshot and download it to the local disk.
`"take_label_image"` Capture the center of the image and save it to disk, overwriting the label128 and label64 images (only works when running in the IDE on an editable project).
`"set_pad_type", index, "type"` Set `gamepad_array[index].type` and the corresponding prompts. There is no "get" version because you can read the property directly from the gamepad object.
`"get_analog_axes",` `player_index default 0,` `stick_index default 0` Return an `xy()` of the analog axis values for `gamepad_array[player_index]`, without D-pad snapping and unlocking reading of the values beyond the first stick. Needed for accessing Atari paddles, car pedals, steering wheels, and flight stick throttles in a meaningful way. There is currently no way to read the D-pad directly (it will be overriden by the analog axis if moved) or to read the analog triggers or other axes.
`"get_mouse_state"` Return `{x:number, y:number, ` `dx:number, dy:number, ` `xy:xy(...), dxy:xy(...), ` `button_array:[...],` `cursor:string, lock:boolean}` for the current mouse position and buttons. Each element of `button_array` is 0 or 1. There are at least two buttons, but may be more. The `dx`, `dy`, and `dxy` values may have higher precision than the absolute numbers because they are tracked at subpixel resolution on browsers that support this.
`"set_mouse_lock", bool` Enable or disable mouse lock. Mouse lock hides the OS mouse cursor and prevents the mouse from moving from the center of the screen, so that it returns only `dxy` values and absolute `xy` values will be fixed. This is useful for first-person mouselook.
`"set_mouse_cursor", name` Change the OS mouse cursor to any [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor) cursor. The default is `"crosshair"`. Note that the OS cursor is at a different resolution than quadplay, so you may want to set to `"none"` and draw your own cursor from a sprite. The OS cursor has 1-4 frames less lag than a game-drawn cursor, however.
`"set_debug_flag", flagname, bool` Set the enabled state of an IDE debugging flag. The flagnames are `"entity_bounds"`, `"assert"`, `"debug_print"`, `"debug_watch"`, `"physics"`.
`"get_debug_flag", flagname` Return the state of an IDE debugging flag.
`"console.dir", ...` Execute `console.dir(...)` if running in a web browser.
`"save", filename, value, callback` Serializes `value` to JSON using WorkJSON extensions and writes to `filename` on disk in the same directory as the game.json file. `filename` cannot contain slashes and must end in `.json`. This is intended for creating development tools within quadplay itself, for example, level editors, NPC ghost tracks, and demo sequences. The JSON files can be loaded as game constants with the `raw` type. If present, `callback` is invoked with `value, filename` when the asynchronous save is complete. This will only work with the IDE enabled and a quadplay server.
`"load", filename, callback`Loads `filename` and (asynchronously) runs the callback with the arguments `value, filename` when the load is complete. This will only work with the IDE enabled and a quadplay server.
`"enable_feature", feature`Enables the given feature, which is nonstandard and may fail on some devices. The only allowed value of `feature` are: `"768x448,private_views"`, which enables this resolutions for `set_screen_size()` with `private_views=true`, which is too slow to make framerate on Raspberry Pi and some phones.
`"multitouch"`Returns an array of touch objects for every currently-active touch, which have the form `{id:string, x:number, y:number, xy:xy, screen_x:number, screen_y:number, screen_xy:xy}`. The touch with id == -1 is the mouse.
Intrinsics ----------------------------------------------------------- can execute about 10,000 vector operations per frame even on embedded platforms. This is sufficient to program most mathematical routines in a readable way and intrinsics are usually not required. For some _very_ math intensive routines, such as those with thousands of particles, it is necessary to run slightly faster at the expense of code that is harder to maintain. In these cases, use the function calls below, which produce inline assembly instructions for the platform. These calls have no argument checking. The vector functions execute about twice as fast as regular overloaded operator arithmetic. The scalar versions may be substantially faster than overloaded operators. Intrinsics are never needed on literal values. `5 + 6` compiles to the same code as `ADD(5, 6)`, for example. Intrinsics are not provided for exponents, square root, bit shifts, trigonometric, modulo (remainder), and other operations that naturally compile with maximum efficiency. `ADD(s1, s2)` : Returns the scalar sum. `DIV(s1, s2)` : Returns the scalar quotient. `MAD(s1, s2, s3)` : Returns `s1 * s2 + s3`. `MUL(s1, s2)` : Returns the scalar product. `SUB(s1, s2)` : Returns the scalar difference. `ABS(s1)` : Returns the absolute value of a scalar. `LERP(s1, s2, s3)` : Returns `lerp(s1, s2, s3)`. `MAX(s1, s2)` : Returns the larger scalar. `MEAN(s1, s2)` : Returns the average scalar. `MEAN3(s1, s2, s3)` : Returns the average scalar. `MEAN4(s1, s2, s3, s4)` : Returns the average scalar. `MIN(s1, s2)` : Returns the smaller scalar. `FLOOR(s1)` : Returns the floor of the scalar. `CEIL(s1)` : Returns the ceil of the scalar. `ROUND(s1)` : Returns the round of the scalar. `SIGN(s1)` : Returns +1, -1, or 0 as the sign of the scalar. `CLAMP(s, slo, shi)` : Clamps `s` to [`slo`, `shi`] where all are scalar and returns the result. `SUM(array)` : Returns the sum of the numbers in the array. The result is zero if the array has zero length. `PROD(array)` : Returns the product of the numbers in the array. The result is one if the array has zero length. `RGB_ADD_RGB(srcrgb, srcrgb, dstrgb)` : Element addition. `RGB_SUB_RGB(srcrgb1, srcrgb2, dstrgb)` : Element subtraction. `RGB_MUL_RGB(srcrgb1, srcrgb2, dstrgb)` : Element multiplication. `RGB_DIV_RGB(srcrgb1, srcrgb2, dstrgb)` : Element division. `RGB_MUL(srcrgb, srcs, dstrgb)` : Vector-scalar product. `RGB_DIV(srcrgb, srcs, dstrgb)` : Vector-scalar quotient. `RGB_DOT_RGB(srcrgb1, srcrgb2)` : Returns the scalar result. `RGB_LERP(srcrgb1, srcrgb2, s1, dstrgb)` : Interpolates between the two source values and returns the result in `dstrgb`. The destination may be the same as one of the sources. `RGB_DISTANCE(srcrgb1, srcrgb2)` : `magnitude(srcrgb1, srcrgb2)` `RGBA_ADD_RGBA(srcrgba1, srcrgba2, dstrgba)` : Element addition. `RGBA_SUB_RGBA(srcrgba1, srcrgba2, dstrgba)` : Element subtraction. `RGBA_MUL_RGBA(srcrgba1, srcrgba2, dstrgba)` : Element multiplication. `RGBA_DIV_RGBA(srcrgba1, srcrgba2, dstrgba)` : Element division. `RGBA_MUL(srcrgba, srcs, dstrgba)` : Vector-scalar product. `RGBA_DIV(srcrgba, srcs, dstrgba)` : Vector-scalar quotient. `RGBA_DOT_RGBA(srcrgba1, srcrgba2)` : Returns the scalar result. `RGBA_LERP(srcrgba1, srcrgba2, s1, dstrgba)` : Interpolates between the two source values and returns the result in `dstrgba`. The destination may be the same as one of the sources. `XY_ADD_XY(srcxy1, srcxy2, dstxy)` : Element addition. `XY_SUB_XY(srcxy1, srcxy2, dstxy)` : Element subtraction. `XY_MUL_XY(srcxy1, srcxy2, dstxy)` : Element multiplication. `XY_MAD_XY_XY(srcxy1, srcxy2, srcxy3, dstxy)` : `dstxy = srcxy1 * srcxy2 + srcxy3` `XY_MAD_S_XY(srcxy1, srcs2, srcxy3, dstxy)` : `dstxy = srcxy1 * srcs2 + srcxy3` `XY_DIV_XY(srcxy1, srcxy2, dstxy)` : Element division. `XY_MUL(srcxy1, srcs2, dstxy)` : Vector-scalar product. `XY_DIV(srcxy1, srcs2, dstxy)` : Vector-scalar quotient. `XY_DIRECTION(srcxy, dstxy)` : Normalize vector (or zero vector). `XY_MAGNITUDE(srcxy)` : `magnitude(srcxy)` `XY_DISTANCE(srcxy1, srcxy2)` : `magnitude(srcxy1 - srcxy2)` `XY_LERP(srcxy1, srcxy2, s1, dstxy)` : Interpolates between the two source values and returns the result in `dstxy`. The destination may be the same as one of the sources. `XY_DOT_XY(srcxy1, srcxy2)` : Returns the scalar result. `XY_CRS_XY(srcx1, srcx2)` : 2D cross product (z component of treating as a 3D cross product). Returns the scalar result. `XZ_ADD_XZ(srcxz1, srcxz2, dstxz)` : Element addition. `XZ_SUB_XZ(srcxy1, srcxz2, dstxz)` : Element subtraction. `XZ_MUL_XZ(srcxz1, srcxz2, dstxz)` : Element multiplication. `XZ_DIV_XZ(srcxz1, srcxy2, dstxz)` : Element division. `XZ_MUL(srcxz1, srcs2, dstxz)` : Vector-scalar product. `XZ_DIV(srcxz1, srcs2, dstxz)` : Vector-scalar quotient. `XZ_DIRECTION(srcxz, dstxz)` : Normalize vector (or zero vector). `XZ_MAGNITUDE(srcxz)` : `magnitude(srcxz)` `XZ_DISTANCE(srcxz1, srcxz2)` : `magnitude(srcxz1 - srcxz2))` `XZ_LERP(srcxz1, srcxz2, s1, dstxz)` : Interpolates between the two source values and returns the result in `dstxz`. The destination may be the same as one of the sources. `XZ_DOT_XZ(srcxz1, srcxz2)` : Returns the scalar result. `XYZ_ADD_XYZ(srcxyz1, srcxyz2, dstxyz)` : Element addition. `XYZ_SUB_XYZ(srcxyz1, srcxyz2, dstxyz)` : Element subtraction. `XYZ_MAD_S_XYZ(srcxyz1, s2, srcxyz3, dstxyz)` : Fused multiply-add with a scalar. `XYZ_MUL_XYZ(srcxyz1, srcxyz2, dstxyz)` : Element multiplication. `XYZ_DIV_XYZ(srcxyz1, srcxyz2, dstxyz)` : Element division. `XYZ_MUL(srcxyz1, srcs2, dstxyz)` : Vector-scalar product. `XYZ_DIV(srcxyz1, srcs2, dstxyz)` : Vector-scalar quotient. `XYZ_DIRECTION(srcxyz, dstxyz)` : Normalize vector (or zero vector). `XYZ_MAGNITUDE(srcxyz)` : `magnitude(srcxyz)` `XYZ_DISTANCE(srcxyz1, srcxyz2)` : `magnitude(srcxyz1 - srcxyz2)` `XYZ_LERP(srcxyz1, srcxyz2, s1, dstxyz)` : Interpolates between the two source values and returns the result in `dstxyz`. The destination may be the same as one of the sources. `XYZ_DOT_XYZ(srcxyz1, srcxyz2)` : Returns the scalar result. `XYZ_CRS_XYZ(srcxyz1, srcxyz2, dstxyz)` : Cross product. `dstxyz` can be the same object as one of the input arguments. `MAT2x2_MATMUL_XY(src2x2, srcxy, dstxy)` : Multiply matrix `src2x2` represented as `[[m11, m12], [m21, m22]]` by vector `(srcxy.x, srcxy.y)` and put the result in `dstxy`, which can be the same object as `srcxy`. `MAT2x2_MATMUL_XZ(src2x2, srcxz, dstxz)` : Multiply matrix `src2x2` represented as `[[m11, m12], [m21, m22]]` by vector `(srcxy.x, srcxy.z)` and put the result in `dstxz`, which can be the same object as `srcxz`. `MAT3x3_MATMUL_XYZ(src3x3, srcxyz, dstxyz)` : Multiply matrix `src3x3` represented as an array of length three row arrays, each of length three by vector `(srcxyz.x, srcxyz.y, srcxyz.z)` and put the result in `dstxyz`, which can be the same object as `srcxyz`. `MAT3x3_ORTHONORMALIZE(m3x3)` : Snap to the nearest 3x3 rotation matrix in place, where all rows and columns have unit length and are orthogonal to each other. `MAT3x4_MATMUL_XYZ(src3x4, srcxyz, dstxyz)` : Multiply matrix `src3x4` represented as an array of length three row arrays of length four by vector `(srcxyz.x, srcxyz.y, srcxyz.z, 1)` and put the result in `dstxyz`, which can be the same object as `srcxyz`. You can use `MAT3x3_MATMUL_XYZ()` on the same `src3x4` to effectively multiply by `(srcxyz.x, srcxyz.y, srcxyz.z, 1)`. `MAT3x4_MATMUL_XYZW(src3x4, srcxyzw, dstxyzw)` : Multiply matrix `src3x4` represented as an array of length three row arrays of length four by vector `(srcxyzw.x, srcxyzw.y, srcxyzw.z, srcxyzw.w)` and put the result in `dstxyzw`, which can be the same object as `srcxyzw`. The `dstxyzw.w = srcxyzw.w`. Project Files ============================================================================================= When importing assets using the IDE as JSON or raw PNG, MP3, or TMX files, the subdirectories `journal`, `screenshots`, `metadata`, `backlot` and `graveyard` are excluded. !!! supports the extended [WorkJSON](https://github.com/morgan3d/workjson) format, which permits C++ style comments, hexadecimal numbers, IEEE 754 constants, raw decimals, leading plus signs, trailing commas, and multiline backquote JavaScript strings within JSON files for convenience when editing and debugging. This is not legal in strict JSON and may be incompatible with other tools. Game JSON --------------------------------------------------------------------------------------------- The project file is a JSON file describing all of the parts of the program. The project file and all of its dependencies are reloaded every time that you press play or reload in the IDE. All of the `.game.json` properties can be modified through the user interface in the IDE using buttons, textboxes, and sliders. Certain advanced changes require directly editing the JSON files with the built-in editor. External editors can also be used to edit any data and will automatically sync. An example project is below, annotated with comments explaining the options. Project files should have the double extension `.game.json`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { "title": "Your Game", "developer": "Your Name or Company", "copyright": "© Year Whomever", "license": "Distribution and source code license", // Not yet supported. These files will be loaded and inserted at the top // of your code, before the constant and asset definitions. "packages": ["https://someaccount.github.io/someproject/somefile.package.json", "somefileinyourproject.package.json"], // Documents for players or for yourself as a game developer, such // as a TODO list, game design doc, manual, or library specification. // These can be HTML, Markdeep (md.html), Markdown (.md), or text (.txt) format "docs": [ "gdd.md.html", "todo.md" ], // Files that form the global scope. Put most of your code in these. They are // loaded in order. "scripts": ["utils.pyxl", "juice.pyxl", "https://someaccount.github.io/someproject/inventory.pyxl"], // The name of the mode at which the program should begin. "start_mode": "Init", // These are the names of modes, which must start with a capital letter and // be legal identifiers in PyxlScript. These correspond to files that must // be stored in the same path as the project file. So, "Init" is in "Init.pyxl". // // Modes become global constants in your program. "modes": ["Init", "Play" "Credits"], // Assets are bound as global constants in your program. // They also properties of the global ASSETS object so that // they can be referenced by string names. "assets": { // Asset names must be legal PyxlScript identifiers "mysprite": "mysprite.sprite.json", "myfont": "arial.font.json", "boom": "boom.sound.json", "overworld": "overworld.map.json" }, // The text added after the date to screenshot filenames "screenshot_tag": "Your Game", "version": 1.0, "credits" : { // This is for major developer credits to appear // in a large font at the top. "main": [ "left", "right", ... ], // For code licenses, e.g., "wfc.pyxl" : "Copyright 2018 that dude" // Assets are automatically handled from their own .json files. The // credits renderer can aggregate files that share licenses when // presenting them. "code": { "file or element": "license", ...}, // e.g., "Dedicated to Wolfgang. Thanks to Fran for the catering." "extra": "" }, // Defined for build scripts, but not used directly by the IDE or // automatic credits in the current version of quadplay "links": { "game" : { // Main website for the game "www": "https://game...", // Arbitrary platforms "twitter": "@game...", "facebook": "...", "instagram": "..." }, // Same as above "developer" : ... } }, // Properties affecting your game description in the launcher program: "min_players": 1, "max_players": 1, "cooperative": false, "competitive": false, "achievements": false, "highscores": false, "description": "Description of your game in about 22 words.", // False by default. If true, P1's right analog stick can override P2's // D-pad. This is useful for making single player dual D-pad games that // automatically work on both keyboard (WASD + IJKL) and physical game // controller. You can also make controller-only dual analog stick // games using device_control("get_analog_axes", ...) "dual_dpad": false, // False by default. If true, this game will send MIDI sysex messages // and requires elevated permissions from the web browser that will // be prompted for at start. "midi_sysex": false, // Other global constants. You could define these in a global source code file // as well. The constants specified here have immutable *values*, not just immutable // bindings, and are available to editing tools. These are bound as global // variables and are also properties of the global CONSTANTS object so that // they can be referenced by string names. "constants": { // If the number value is in quotes it will be parsed. // This allows degrees, percents, infinity, and nan. // Raw numbers, strings, and booleans are permitted but // restricted to JSON limitations. "player_speed": {"type": "number", "value": "3"}, "background": {"type": "rgb", "value": {"r": {"type": "number", "value": 0.8}, "g": {"type": "number", "value": 0.8}, "b": {"type": "number", "value": 0.8}}, "data": {"type": "object", "value": { "height": {"type": "number", "value": 1}, "foo": {"type": "object", "value": {"a": {"type": "boolean", "value": true}}, "starmap": {"type": "raw", "url": "stars.yml"}, "name": "Destrier", "knight": {"type": "object", "value": { "upgrades": {"type": "array", "value": [10, 7, 3, 1]}, "hp": 50, "inventory": {"type": "object", "value": {"food": 4, "sword": "a sword"}}}}, "start_role": {"type": "reference", "value": "knight"}, "numbers": {"type": "array", "value": [ {"type": "number", "value": "10"}, {"type": "number", "value": "infinity"}]}, "level_table": {"type": "table", "url": "level_table.csv"}, "book_text": {"type": "string", "url": "alice.txt"} }, // The default Y axis increases down the screen, like old arcade games. // Set this to true for a modern vertical axis good for physics games. // You can also invoke set_transform() in code. "y_up": false, // Supported sizes: // // 384 x 224 (maximum, native, default) // 320 x 200 (VGA), // 192 x 112 (half resolution), // 128 x 128 (PICO-8), // 64 x 64 (lowrezjam and nano JAMMER) // // The non-native screen sizes may render with substantial black // borders on some implementations to ensure integer pixel scaling. "SCREEN_SIZE": {"x": 384, "y": 224}, } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each game must also contain: - 64x64 `label64.png` box art image in the same folder as the `.game.json` file. This is used for display in the launcher. - 128x128 `label128.png` box art image in the same folder as the `.game.json` file. This is used for the open dialog. - 1152x1120 `preview.png` image in the same folder as the `.game.json` file that contains 6x10 frames of 192x112 video at 20 fps (3 seconds total) used for the preview video in the launcher. You can create these images however you wish, including manually drawing them. The IDE has some tools to automate the process. You can create the `label128.png` and `label64.png` images by pressing *Shift+F6* while your game is playing. It will capture a 128x128 region from the center of the screen and resize it using bilinear interpolation to create the 64x64 image, and then save them with the appropriate filenames in the game's directory. Edit and compress these images as you would a sprite. You can create the `preview.png` image by pressing *Shift+F8* while your game is playing. It will record 3s of video, process it to the correct resolution, and save it to disk. Compress it as you would a sprite. Seek to keep the total size around 100 kB for the image by choosing content that compresses well and using a good compression tool. If the image is much larger, then there will be delay for players browsing your game in the discover tab. Assets and constants are both data used by your program. Assets include standalone JSON metadata files of their own to make them easy to reuse between projects and are usually relatively large media files. Constants have metadata embedded directly in the game to reduce files and maintenance and are usualy relatively small individual data. The `table` and `raw` constant types can use external files as constants, but still have metadata embedded in the game itself and use raw data instead of media formats. ### Constants Unlike constants declared in code, the constants in the `.game.json` file appear in the IDE in the project list and have GUI editors. They are immutable at runtime by the program, but can be changed by the GUI editors for debugging and tuning at runtime. For some formats (e.g., tables, text), there are similarities between *constants* and *assets*. The distinction is that constants are treated as part of the game code for intellectual property purposes, and assets are treated as separate units that may have their own licenses, credits, and copyrights. Each constant has the format `"..name..": {"type": "..type..", "value": ..., "description": "..."}`. The `description` property is optional and the `raw` type permits a `url` instead of a `value`. Other types may have additional metadata used for the IDE. Explicit types allow support for the full PyxlScript literal syntax beyond that permitted natively in JSON, appropriate GUI editors, and annotations. The available constants types are: - `number` - Decimal number, with optional double quotes - Unicode fraction in double quotes - `"nan"` - `"infinity"`, `"-infinity"`, `"∞"`, `"-∞"` - Number ending in `deg`, `°`, or `%`, all in double quotes - `"π"` - `string` - inline or from a text file - `boolean` - `nil` (no `value` needed) - vectors - `xy` - `xz` - `xyz` - Each of these may have a `"nudge"` property, which is another vector of the same type that specifies the amount that the IDE should shift by along each axis when nudging. The nudge vector expects strings of numbers or numbers directly, rather than recursive properties with type and value elements. - `rgb` - `rgba` - `hsv` - `hsva` - `raw` - Loaded from a json or yml file or inline as a json value. When in an external file, must be at top level and not nested inside an array or object. - `distribution` - Like an `object`, but may contain only `number` valued properties - `object` - `array` - inline in the `value` field or from a CSV file `url` - `table` - from a CSV file, interpreted as an object of arrays, array of objects, object of objects, or object of arrays depending on options specified - `reference` - The value of a reference is the name of another constant, asset, or named sprite of the form `sprite_asset_name.sprite_name`. These can be recursive, but obviously cannot form a cycle. References must be defined at top level and not nested inside an array or object. ### `number` Constants Number contants may have the following additional parameters for controlling the IDE: - `min`: Smallest value (inclusive) - `max`: Smallest value (inclusive) - `quantum`: Values are rounded to `round(value, quantum)` - `scale`: Must be `"linear"` if specified - `format`: Values are shown in the IDE using `format_number(value, format)` ### `table` Constants A `table` entry in the game constants has additional properties: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ json { "type": "table", "transpose": "false", // Optional, default false "column_type": "object", // Optional, default "object" "ignore_first_column": false, // Optional, default to false. Removes the source 1st column if true "row_type": "array", // Optional, default "object" "ignore_first_row": false, // Optional, default to false. Removes the source 1st row if true "url": "somefile.csv", // Must use a "url" property, not "value". Must be a csv file. "trim": true // If true, trim extra whitespace. Default is true } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, tables are indexed in-game as `table[x][y]` a.k.a. `table[col][row]`. To use `table[y][x]` a.k.a. `table[row][col]` indexing, set `transpose == true`. The `column_type`, `ignore_first_column`, `row_type`, and `ignore_first_row` refer to the data in the original format, before any transpose has occured. This is consistent with named animation indexing in sprite sheets. If `column_type` is `"object"` (the default), then the first row of the data is treated as property names. If `column_type` is `"array"`, then the first row is considered element 0 of the column. If `row_type` is `"object"` (the default), then the first column of the data is treated as property names. If `row_type` is `"array"`, then the first column is considered element 0 of the row. If both `row_type` and `column_type` are `"object"` (the default), then the top-left element is not used. Quadplay parses values in [CSV](https://tools.ietf.org/html/rfc4180) files with these additional rules for data types: CSV Value | Quadplay Type -------------------------|----------------- number | number number ending in `%` | number date | string `TRUE` or `FALSE` | boolean `$`, `¥`, `€`, `£`, or `§` + number | number (ignores currency unit) double-quoted text | string (without quotes) other text | string empty | (empty) string `""` in text | `"` `infinity` | number (`infinity`) `-infinity` | number (`-infinity`) `nil`, `null` | `nil` `nan` | `nan` number ending in `deg` | number (in radians, same as native quadplay constants) Double-quoted text may contain newlines. #### Object Examples Consider the following CSV files, produced using a spreadsheet program or by hand: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ csv state, bird, flower Louisiana, Brown pelican, Magnolia Maine, Chickadee, White pine cone and tassel Wisconsin, American robin, Wood violet ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`state.csv`] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ csv slot, fighter, rogue, wizard main, sword, dagger, fireball off, shield, blackjack, teleport aux, rope, lockpick, potion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`rpg.csv`] The following examples show how the data can be accessed in code, depending on the properties set on the constants ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // "state": {"type": "table", "url": "state.csv", "transpose": true} debug_print( state["Louisiana"]["bird"] ) // "Brown pelican" debug_print( state.Louisiana.bird ) // "Brown pelican" // "represent": {"type": "table", "url": "state.csv"} debug_print( represent.bird.Maine ) // "Chickadee" // "role": {"type": "table", "url": "rpg.csv"} debug_print( role.fighter.main ) // "slot": {"type": "table", "url": "rpg.csv", "transpose": true} debug_print( slot.main.fighter ) // "sword" debug_print( 'off-hand actions: ', slot.off) // {fighter:"shield", rogue:"blackjack", wizard:"teleport"} ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #### Array Examples Consider the following CSV files, produced using a spreadsheet program or by hand: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ csv x,y 101,89 106,88 88,98 124,29 130,34 131,46 136,48 204,72 205,66 208,62 202,60 279,90 289,103 293,110 345,98 348,94 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`positions.csv`] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ csv level, strength, hit_points, total_skills 0, 0, 0, 0 1, 16, 10, 1 2, 18, 15, 1 3, 18, 20, 2 4, 20, 25, 2 5, 21, 30, 2 6, 21, 35, 3 7, 22, 40, 3 8, 22, 45, 4 9, 22, 50, 5 10, 24, 55, 6 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`level_table.csv`] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ csv 0, 10, 20, 30 1, 11, 21, 31 2, 12, 22, 32 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`grid.csv`] The following examples show how the data can be accessed in code, depending on the properties set on the constants ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // "position_array": {"type": "table", "url": "positions.csv", "transpose": true, "column_type": "array"} for pos in position_array: draw_point(pos, #F00) // "level": {"type": "table", "url": "stats.csv", "transpose": true, "column_type": "array", "ignore_first_column": true} debug_print( level[1].strength ) // 16 // "grid": {"type": "table", "url": "grid.csv", "column_type": "array", "row_type": "array"} debug_print( grid[1][2] ) // 12 debug_print( grid[3][1] ) // 31 // "matrix": {"type": "table", "url": "grid.csv", "transpose": true, "column_type": "array", "row_type": "array"} debug_print( matrix[1][2] ) // 21 debug_print( matrix[3][1] ) // 13 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### `raw` Format The `raw` format allows embedding a JSON-expressible value directly. `raw` format also supports the format {"type":"raw", "url": "your url"} instead of an inline value. The URL must be for a [JSON](https://www.json.org/) or [YAML](https://yaml.org/) file. Note that the `raw` format is restricted to values that are expressible in those interchange formats and does not support the full GUI editors and other format features. `raw` type constants with URLs must be top-level constants. They cannot be embedded within other constants. This is required in order to separate the asynchronous loading from constant evaluation in the implementation and to ensure that GUI editors can be built effectively for objects and arrays. Debug JSON --------------------------------------------------------------------------------------------- When working with multiple developers on a game, it is often useful to have slightly different settings for each developer. For example, one developer may want a `DEBUG_GRAPHICS` constant set to `true` while the usual value is false. Or the gameplay programmer may experiment with different player velocity than what is in the main game branch. One way to accomplish this is to explicitly use the built-in `IDE_USER` constant described in the Debugging section. Another solution is to use a `.debug.json` file. A debug JSON file must be in the same directory as the `.game.json` file and use the same basename. For example, if your game is `space.game.json`, then the debug file is `space.debug.json`. Typically you will _not_ add the debug file to version control, so that each developer may have their own. No `.debug.json` file is required, and if one is present it will be ignored unless running on a quadplay server using the IDE. The `.debug.json` format is: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ json { "start_mode": "Init", "start_mode_enabled": true, "constants" : { // Constants in the same format as in the .game.json. "player_speed": { "type": "number", "value": "3", // If false, then this debug value is // disabled in the IDE but remembered for later // enabling. "enabled": true } } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `start_mode` overrides the `.game.json` start mode, if it is present. The constants override the equivalent constants from the `.game.json` file (unless they are not enabled). Extra constants specified only in the `.debug.json` file are ignored; they are not mapped at runtime. Constant types that require external data are not supported in the `debug.json` file. These are `raw` constants loaded from a JSON or YAML url, `string` constants loaded from a text file url, and `table` constants loaded from a CSV url. Sprite JSON --------------------------------------------------------------------------------------------- The sprite sheet JSON descriptor looks like: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { "url": "hero.png", // Optional. Defaults to the size of the png. "sprite_size": {"x": 8, "y": 8}, // Optional blank pixels between sprites on the bottom and right. Default is zero. "gutter": 0, // Optional. Default is false. If true, indexing is transposed // in game. "transpose": false, // Optional name of the file from which this PNG was generated. // This is the file that will be loaded with an external editor from the IDE // instead of the PNG when editing. This file will also be managed by version control // alongside the PNG nad JOSN. // // Useul if you export from another tool such as Aseprite, Krita, or Photoshop. "source_url": "hero.psd", // Default value for sprite.frames, the number of 1/60-second frames // to play a sprite during animation. "default_frames": 1, // Optional table mapping color value strings to strings. // These must be exactly (4-bit) #RGB values; they cannot have // alpha or use the 8-bit form #RRGGBB. They are applied to the source // image after loading to recolor it. Alpha values are preserved. "palette_swap": { "#F00": "#0F0", "#DD0": "#C0D" }, // Optional. Applies to pre-transpose coordinates. Each becomes // an array on the spritesheet with the `extrapolate` property set. // A name cannot be `sprite_size` or any other spritesheet property. // // If an object with `x` and `y` properties is specified, creates a single sprite. // // If an object with `start` is specified, creates an array of sprites. // // The contents between `start` and `end` are treated as a 1D array. By default // they are row major. To switch to column-major set "major_axis": "y". // // To strip frames from the end of the array, set `ignore` to the number of frames to // ignore. // // If `extrapolate` is unspecified, defaults to looping. // `extrapolate` options are "loop", "oscillate", and "clamp" // // Each object may have its own `default_frames` that overrides the // spritesheet's default_frames, as well as a `frames` array with // one element per sprite in the animation. // // `end` defaults to `start`, and each property of `end` defaults // to the corresponding part of `start`. "names": { "idle": {"start": {"x": 0, "y": 0}, "end": {"x": 0, "y": 3}, "extrapolate": "oscillate", "frames": [1, 2, 2, 1]}, "run": {"start": {"x": 1, "y": 0}, "end": {"y": 3}, "frames": 2}, "jumpUp": {"start": {"x": 2, "y": 0}}, "jumpDown": {"start": {"x": 2, "y": 1}}, "dead": {"x": 3, "y": 0}, "parachute":{"start": {"x": 3, "y": 1}, "pivot": {"x": 4, "y": 4}, "is_item": true // Arbitrary properties can be attached here, using the // same syntax as game constants }, }, // Optional offset and extent. Using this conserves memory if the sprites // do not fill the entire PNG. "region": { "corner": {"x": 0, "y": 0}, // Optional, defaults to (0, 0) "size": {"x": 128, "y": 32} // Optional, defaults to the remainder of the sheet }, // Optional location on the sprites that aligns with (0, 0) in an entity's reference // frame. Defaults to the sprite size / 2. "pivot": {"x": 4, "y": 7}, // Optional license and copyright information. "license": "GPL 3.0 (c) 2023 Morgan McGuire" } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This will appear as the global variable `heroSpriteSheet` in memory, which is a 2D array of individual sprites. !!! I recommend a two-step process for compressing sprites to minimize loading time and game size. First, use the [Quantize✜](../tools/quantize.html) tool to reduce bit depth. Second, use [ImageOptim](https://imageoptim.com/mac) (macOS), [PNGGauntlet](https://pnggauntlet.com/) (Windows), [TriImage](https://trimage.org/) (Linux), or [PNG Crush online](https://richardassar.github.io/pngcrush.js/) to improve the PNG encoding. For a sprite sheet named `S`, individual sprites are accessed as `S[x][y]` in game. If the sprite sheet contains a single row, then `S[x]` is sufficient. If there's a single image, just use `S`. Setting `transpose` to true flips the indexing to `S[y][x]`, which is more convenient for some sprites. Animations appear to game code as arrays of sprites that have the following additional properties: `extrapolate` : Equal to `"clamp"`, `"loop"`, or `"oscillate"` as specified in the spritesheet. `frames` : Equal to `infinity` for `"loop"` and `"oscillate"` animations and the total number of 60 Hz game frames of animation for finite `"clamp"` animations. `period` : Equal to `nan` for `"clamp"` animations and the number of 60 Hz game frames for one repeat of a `"loop"` or `"oscillate"` animation. A game may use a total of **5,505,024 pixels** of sprite memory (10.5 MB) across **128 sprite sheets** (including fonts), each no larger than **1024x1024**. You can allocate this however you wish within those restrictions. That's equivalent to 5,376 sprites at 32x32, 21,504 sprites at 16x16, or 64 full-screen images. These are of course soft limits because you can modify the open source emulator to remove them. The intent of the limits is to guarantee that your programs will work on all future implementations of the runtime: web software, Raspberry Pi, WebGL, GLES, and desktop OpenGL and Vulkan. If the same sprite sheet is loaded by multiple `.sprite.json` files using the same URL then it only counts once towards the resource limits. This allows you to pack sprites with different sizes into the same physical sheet at no penalty and treat them as different logical sprite sheets. Multiple spritesheets loaded from the same sprite.json file will be pointers to the same object, which is immutable. There is no performance advantage or requirement for making sprites or spritesheets even sizes or power of two sizes, although many traditional sprites happen have sides of 8, 16, or 32 pixels. The screen resolution is designed to allow an integer number of such tiles. You can use the Python script `tools/sprite_json_generator.py` to generate these json blobs for you. Use the `--help` argument for more details. Font JSON --------------------------------------------------------------------------------------------- Fonts have a JSON descriptor file that looks like: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { "url": "sundown-7x13.png", // Optional copyright and license information "license": "Public Domain 2018 by Danika Farheed", // Size of the glyph bounding boxes. Include gutter pixels in the // char_size. The loader repacks glyphs tightly, so extra padding // or horizontal alignment is irrelevant at runtime (but affects // distribution size and loading time). "char_size": {"x": 8, "y": 14}, // Optional, default is 0. The minimum width of a glyph for // layout purposes. This can be used to create fixed-width // (monospace) fonts, or to enforce minimum spacing around very // thin characters such as |. This is the width without an outline. "char_min_width": 8, // Spacing between letters and lines during rendering. If the // x spacing is zero (for script fonts), there will be no space between // letters or digits, but space will still be added around symbols. "letter_spacing": {"x": 2, "y": 3}, // Defaults to 1 pixel "shadow_size": 1, // Zero-based Y value of the baseline relative to char_size. // This is one less than the "height" of the baseline because // of the zero based value. "baseline": 11, // The current format. No other is accepted "format": "20211015" } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can generate a default version of this JSON with the [fontpack✜](../tools/fontpack.html) tool from a PNG image. You can generate a PNG template for the glyphs by taking a screenshot from the [fontgen✜](../tools/fontgen.html) tool. By convention, the image filename contains the maximum size of the individual glyphs, which is usually smaller than the char_size due to gutters. The file can have any name, however. The image must be grayscale, where white is the font character and black will become transparent. The current implementation does not allow shades of gray for antialiasing. The image tiles must follow this exact character format: ![Figure [figFont]: The font character layout. To save disk space/download time and font drawing time, characters shown in magenta will automatically be generated at load time if not present in the font sheet.](font-layout.png) At load time, many missing characters can be generated. This may not look ideal, so use the [fontpack✜](../tools/fontpack.html) tool when creating a font to inspect the generated characters, and hand-tune any for which the generation is not good enough. Also, many characters in the font sheet perform double duty and will be used for producing similar-looking unicode characters. Empty spaces are reserved for for future extensions. Fonts count 1/2 as much towards the spritesheet pixel limit because they are internally compressed to 8 bits per pixel. They still count fully towards the maximum spritesheet dimensions. Because a font is 32x14 glyphs, this means that the largest `char_size` can be is 29x70 pixels when the `shadow_size` is one pixel. Fonts enforce uniform width on all full-size digits 0..9 so that numbers print in aligned columns. Avoid drawing fonts with one digit that is larger than most of the others (usually the "4") because they result in extra padding around all digits. `font.line_height` : When loaded into memory, the font object exposes a `font.line_height` property that is the spacing used between consecutive baselines. This is chosen to make typical letters well spaced. It may leave some symbols overlapping on adjacent lines, especially for decorative fonts. This is the height of the actual characters plus the `char_size.y` value. Multiple fonts loaded from the same sound.json file will be pointers to the same object, which is immutable. Sound JSON --------------------------------------------------------------------------------------------- The sound JSON descriptor looks like: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { "url": "boom.mp3", // Optional base pan offset on [-1, 1] "pan": 0, // Optional base volume multiplier "volume": 1, // Optional base rate multipler "rate": 1, // Optional base pitch multiplier "pitch": 1, // Optional license and copyright "license": "Released into the public domain by Edouard Gauthier" } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Sounds must be in MP3 format. Multiple sounds loaded from the same sound.json file will be pointers to the same object, which is immutable. Map JSON --------------------------------------------------------------------------------------------- The easy way to create a map is within the IDE using the "Create new asset..." option in the project. Select asset type Map and set the map options as you wish. Once the map is added to the project, right-click on the map in the project to open it in Tiled for editing. ![Map creation dialog in the quadplay IDE](new-map-dialog.png width=50% style="image-rendering:auto") Multiple maps loaded from the same map.json file will produce different map objects in memory because maps are mutable. The spritesheets that they load will be shared if they come from the same sprite.json files. The map JSON descriptor looks like: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { "url": "overworld.tmx", // Optional. Shift the map by this amount in pixels for all coordinate // transformations and rendering. Default = (0,0), which puts integer // map coordinates at the corners of tiles. "offset": {"x": 7.5, "y": 7.5}, // z value of the first layer. Optional, default = 0 "z_offset": 0, // Optional. Default = false. If true, flip the map // vertically during load so that (0,0) is the bottom // of the TMX map instead of the top. "y_up": false, // Optional. Default = false. "loop_x": false, // Optional. Default = false. "loop_y": false, // Optional. Default is 1.0. This is the z difference // between layers when rendering. Can be negative to // reverse layering direction. "z_scale": 2, // Optional license and copyright "license": "Released into the public domain by Edouard Gauthier", // Optional if the map only uses one spritesheet "sprite_url": "tiles.sprite.json" // If no sprite_url is specified, then an entire table // mapping each spritesheet referenced in the map to a sprite file "sprite_url_table": { "tiles": "tiles.sprite.json" } } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The url must reference a map file in [TMX format](https://doc.mapeditor.org/en/stable/reference/tmx-map-format/) and in files ending with `.tmx`. Only the following properties are currently supported: - `Orthogonal` or `Hexagonal (Staggered)` - `CSV` format - `Tile layers` (other layers are ignored) - `Embedded tileset` (a PNG reference in the TMX, _not_ a TSX file) - For an Orthogonal map, set the map tile size equal to the tileset tile size (e.g., 32x32) - For a Hexagonal map, set the map tile size to be 1.5x larger along the "pointy" direction (e.g., 32x48 for a 32x32 hex with points up and down) When accessing a Hexagonal map in memory, even rows should be rendered offset by 1/2 a tile width. ![Tiled program options for creating a new map.](tiled-new-map.png width=50% style="image-rendering:auto") The tile render order from the map is ignored. The easiest way to create these is using the open source [Tiled](https://www.mapeditor.org/) editor, which has pay-what-you-want precompiled binaries for Windows, macOS, and Linux. The sprite sheet from the map file will be used for visualization if it is available in the IDE, but map rendering is performed using a sprite sheet at runtime. For more sophisticated map drawing, [TileKit](https://rxi.itch.io/tilekit) is a US$20 Windows and Linux mapping program. When drawing, layer zero for `draw_map()` corresponds to the lowest layer in Tiled and the layer indices increase upwards. Data JSON --------------------------------------------------------------------------------------------- The special data asset type allows importing a large amount of data in an arbitrary format. It supports PNG, JPG, GIF, CSV, TXT, JSON, YAML, and XML. Unlike a CSV, JSON, or YAML _constant_, an asset can contain metadata, a license, and arbitrary extra properties. Unlike a sprite, a PNG, JPG, or GIF data asset presents the raw 8-bit per pixel data as a 2D array to the program instead of quantizing it to 4 bits per channel and only supporting PNG. The data JSON descriptor looks like: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ JavaScript { // can be .png, .gif, .jpg, .csv, .txt, .json, or .yml "url": "heightfield.png", "license": "your license here", // all of the table constant options for if URL is a CSV file } ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Images are loaded as `image.color[x][y]` where `color` can be `r`, `g`, `b`, or `a`. The values are on the range [0, 255]. Future support may extend that range when the input has 16 or 32 bits per channel. PyxlScript Language ========================================================================= Numbers ------------------------------------------------------------------------- Numbers are stored internally as double-precision (64-bit) IEEE floating point values. Integer operators truncate the decimal places of their arguments. The first 2^53=9007199254740992 contiguous positive integers are exactly representable. The largest finite representable number is about 1.7977x10^308. There are about seventeen decimal digits of precision on arbitrary numbers. `nan` and positive and negative infinity are also exactly representable. Finite decimal numeric literals contain an optional single plus or minus sign and one of: - a series of digits, followed by an optional decimal point and a series of digits - a decimal point followed by a series of digits - one of the following single-character fractions, representing one-half to one-tenth: `½ ⅓ ⅔ ¼ ¾ ⅕ ⅖ ⅗ ⅘ ⅙ ⅐ ⅛ ⅑ ⅒` Two optional suffixes are permitted: - Percent `%` divides the number by 100 - Degrees `deg` or `°` multiplies the number by `π/180` There are no _literals_ for not-a-number or infinity. See the Built-In Objects section for the constant bindings that produce these values. Hexadecimal constants are `0x` followed by series of digits 0-F or 0-f. Binary constants are `0b` followed by a series of 0s and 1s. Examples of legal numeric literals: ~~~~~~~~~~~~~~ PyxlScript 1 -21.4 0 -0 .0 0.5 -½ +72.41 15% // 0.15 90° // ½π 45deg // ¼π 0xFF0DC 0b11000101 ~~~~~~~~~~~~~~ No octal or exponential float notation are supported. Trailing decimal points are not allowed. For example, `0.` is illegal. Booleans -------------------------------------------------------------------- The boolean literals are `true` and `false`. Logical operators work with any values. For logical operators and flow control, `0`, `∅`, and empty string `""` act as `false`. Strings -------------------------------------------------------------------- String literals are enclosed in double quotes (`"..."`). They may not contain newlines or double quotes. To enter a double quotation mark or slash, escape it with a slash: `"\\"` is the \ character and `"\""` is the `"` character. The `+` operator performs string concatenation, coercing the right-hand argument if it is not a string. String characters are read-only substrings accessed using zero-based array syntax. Arrays -------------------------------------------------------------------- Arrays are ordered sets of values. They are zero-based and have dynamically typed elements. Array literals are surrounded in square brackets and separated by commas. Square brackets are also used for l-value and r-value dereference. Array examples: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript a = [8, 4, 1] a[0] = "hello" debug_print(a[0]) debug_print(size(a)) a₁ += 2 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Negative indices are permitted on arrays but will not be reported in the length. Reading an out-of-bounds value from an Array returns `∅`. Objects --------------------------------------------------------------------------- Objects are unordered sets of keys. Objects (also known as dictionaries and tables in other languages) map these keys to values. Literals are created using key-value pairs separated by a colon, surrounded by curly braces, and delimited by commas. Object elements can be accessed using the string name of a property in square brackets or by a period. This allows them to act as objects/structs. Object examples: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript t = {x:3, y:10} t.x += 1 debug_print(t["x"]) debug_print(size(t)) debug_print(keyArray(t)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Functions --------------------------------------------------------------------------- Functions are first-class values. They can be passed to and returned from other functions, stored in data structures, and bound to variables. Declare functions with `def` and return values from them with `return`. Separate the argument list from the body with `:`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Multi-line function block def solve(a, b, c): return ½(sqrt(b² - 4 a * c) - b) / a // Inline function def foo(x): x += 1; return 2 x // Call a user function text(foo(3)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Functions create a local scope for their variables and are lexically scoped (as in JavaScript) for free variables. Local functions can be declared anywhere a statement can appear, and local function definitions are efficient. It is common practice to declare little callbacks right before they are used, as in: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript def destroy(entity): play_sound(explosion_sound) spawn_particles(entity.pos) def cleanup(): remove_values(entity_array, entity) delay(cleanup, 20) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Some API functions that use callbacks are `iterate()`, `delay()`, `sequence()`, `add_frame_hook()`, `set_pause_menu()`, `ray_intersect_map()`, and `physics_add_contact_callback()`. There is no way to declare an anonymous function or use a function definition as an expression. If a function body takes too long to execute, then the interpreter may terminate the program to prevent the game from becoming unresponsive. To call a function returned from an expression that ends in parentheses, use the `call` function. Variables --------------------------------------------------------------------------- Variable name identifiers consist of an optional leading underscore `_`, optional upper case delta `Δ`, a series of Roman letters or one of the unambiguous-looking Greek letters `αβγδζηθιλμρσϕχτψωΩ` by itself, and optionally trailing a series of digits and underscores. The identifier also must not be a reserved word. No other unicode characters are allowed in identifiers. Examples of legal identifiers: ~~~~~~~~~~~~~~~~~~~ none col screen long_variable longVariable L i damage Y2 Δx θ θ_in _health ~~~~~~~~~~~~~~~~~~~ Variables are declared to the scope of the containing block or mode. Scope blocks are denoted by a statement ending in a colon and zero or more indented lines. Use `let` to declare variable names that can later be re-bound. Use `const` to declare constants that cannot be rebound. Note that `const` marks the *binding* as constant, but the bound value itself may be mutated if it is an object or array. Note that only a single variable is permitted per `let` or `const` statement. Multiple statements may be chained using semi-colons on a single line, however. ~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const x = 7 // x may not be rebound let y // Declare y let z = 9 // Declare and initialize z // Declare and initialize two variables on the same line: let a = [1, 2]; let b = "hello" ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The variables captured by `for` and `with` and the arguments to a `def` are bound to the scope of the body. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const scale = 2 let y = 100 for i < 3: y += 1 sprite(hero[anim][0], xy(scale * i, y)) def applyScale(x): return x * scale // i is out of scope here ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Variables declared within a mode are local to that mode and reinitialized every frame. Variables declared in a global file are global to the entire program. It is an error to assign to a variable before it is declared. ### Reserved Words These are reserved tokens that cannot be used as variable names. Some of them are keywords or built-in syntax, and some of them do nothing now but are reserved for future use. `and`, `arguments`, `args`, `as`, `async`, `at`, `auto`, `await`, `assert`, `because`, `begin`, `bitand`, `bitnot`, `bitor`, `bitashl`, `bitshl`, `bitshr`, `break`, `catch`, `class`, `const`, `cont`, `coroutine`, `continue`, `constructor`, `deg`, `do`, `delete`, `def`, `default`, `debug_watch`, `debug_print`, `elif`, `end`, `extern`, `enum`, `final`, `finally`, `for`, `from`, `freeze`, `function`, `get`, `global`, `go`, `goto`, `has`, `HOST_CODE`, `if`, `in`, `implements`, `inherits`, `include`, `import`, `interface`, `launch_game`, `let`, `local`, `nan`, `NaN`, `new`, `nil`, `nonlocal`, `not`, `null`, `main`, `mod`, `module`, `mode`, `or`, `otherwise`, `public`, `private`, `protected`, `prototype`, `package`, `preserve`, `preserving_transform`, `quit_game`, `ret`, `return`, `reset_game`, `require`, `requires`, `seal`, `set`, `show`, `static`, `strict`, `SUB`, `swap`, `SOURCE_LOCATION`, `SCREEN_SIZE`, `to`, `todo`, `to_string`, `then`, `try`, `throw`, `template`, `touch`, `until`, `use`, `using`, `unless`, `var`, `VIEW_ARRAY`, `while`, `with`, `when`, `xor`, `yield`. Built-in functions such as `draw_rect` that are legal identifiers can be rebound. They are not reserved. `cos`, `sin`, and `tan` by themselves are bound to first-class functions, but the special syntax that allows them to be invoked without parentheses on single variables is tied to their exact names. If you rebind them, then the new functions that you have bound will be invoked instead. ### `with` Statement The `with` statement creates a local scope in which explicitly named keys from an object are variables aliased to the properties on the object. This allows compact syntax for repeated use of those keys. It fills the role that methods and member variables do within object-oriented languages. The local variables shadow any global (or enclosing `with` statement) variables. Example: ~~~~~~~~~~~~~~~~~~~~~~ PyxlScript G = {x:1, y:2, z:0} with x,y in G: x = 100 y = 0 z = 4 // G.x is now 100, G.y is now 0, G.z is still 0, // and global z is now 4 ~~~~~~~~~~~~~~~~~~~~~~ Within the body, the local variables and the properties are true aliases. So, `G.x` and `x` may be used interchangably within the body in the above example. The single-line version of the `with` statement is comparable to other single-line flow control: ~~~~~~~~~~~~~~~~~~~~~~ PyxlScript G = {x:1, y:2, z:0} with x, y in G: x = 100; y = 0; z = 4 ~~~~~~~~~~~~~~~~~~~~~~ `with` statements can make code clean but are expensive relative to the overhead of other statements, so avoid them for performance-sensitive inner loops. ### `local` Statement The `local` statement allows you to create a block without any other bindings or flow control. It is occasionally useful for creating variables with a tight scope inside a long function or mode. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let x = 4 local: // This is a local scope and variables // can shadow those in the outer scopes let x = 7 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Comments ------------------------------------------------------------------- Comments follow C/C++/Java/JavaScript syntax of `//` for single line comments and `/*`...`*/` for multi-line comments. `because "reason"` : The special `because` statement allows a compile-time string comment to follow specific statements such as `set_mode()`. It acts as a machine-readable comment that the IDE will use to automatically annotate the program in the debug output at runtime and the mode diagram. Operators ------------------------------------------------------------------- Built-in arithmetic operators and functions are overloaded for numbers and objects. Some operators support a compact Unicode version and an ASCII version that is easier to type on a conventional keyboard. Unlike other languages, all mutating operators (for example, `++`, `+=`, and `=`) return `nil`. ASCII | Unicode | Meaning ---------|---------|-- `++` | | increment `--` | | decrement `=` | | assignment `==` |`≟` | equality test `!=` |`≠` | inequality test `+` | | addition and string concatenation `-` | | negation `+=` | | mutating addition, string concatenation `-=` | | mutating subtraction `<` | | less-than `<=` |`≤` | less-than or equal to `>` | | greater-than `>=` |`≥` | greater-than or equal to `in` |`∈` | `for` loop container element-of and `with` statement object `^` | | exponentiation (see also unicode exponents) `*` | | multiplication (see also implicit multiplication) `*=` | | mutating multiplication `/` | | floating-point division `/=` | | mutating division `mod` | | remainder (modulo) |`⌊⌋` | floor function |`⌈⌉` | ceiling function `||` | | absolute value |`‖‖` | magnitude (of a vector or array) `[]` | | object/array element reference, or array constructor `{}` | | object constructor (follows JavaScript syntax) `.` | | object property `()` | | grouping or function call `...` | `…` | spread operator for arrays and objects `bitand` |`∩` | bitwise AND (bit set intersection) |`∩=` | mutating bitwise AND `bitor` |`∪` | bitwise OR (bit set union) |`∪=` | mutating bitwise OR `or` | | logical OR `not` | | logical NOT `and` | | logical AND `bitxor` |`⊕` | bitwise XOR |`⊕=` | mutating bitwise XOR `bitnot` |`~` | bitwise NOT `~=` | | mutating bitwise NOT `<<` or `bitshl` |`◀` | arithmetic bit shift left `<<=` |`◀=` | mutating arithmetic bit shift left `>>` or `bitshr` |`▶` | arithmetic bit shift right `>>=` |`▶=` | mutating arithmetic bit shift right `if`...`then`...`else`| | conditional (C/Java `?:` a.k.a. "ternary" operator) `default`| | default value operator Assignment and mutating operators all return `nil`; they cannot be chained or used in expressions. There is no logical XOR or logical shift right. The `default` operator has the form: _expr1_ `default` _expr2_. If _expr1_ is not `nil`, then the value of the operation is the value of _expr1_. If it is `nil`, then _expr2_ is evaluated and the value of the expression is the value of _expr2_. Note that the second expression is not evaluated unless it is needed, as with `and`, `or`, and `if` operators. The `default` operator allows compact statements handling default arguments or out of bounds array accesses, for example: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Returns a transparent gray. Opacity is optional // and defaults to 1 def transparent_gray(g, opacity): return rgba(g, g, g, opacity default 1) // Print the first element of array matching target, or // the first element of the entire array if not found. debug_print(array[find(array, target) default 0]) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The conditional operator (distinct from the `if` block statement) has the form: `if testexpr then truevalue else falsevalue`. Note that there are no colons (:), because it does not create blocks. The conditional operator is convenient when initializing constants, or data structures. For example: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const y = if x > 3 then "cat" else "dog" spawnMonster(if pos then pos else xy(0,0)) let data = {name: if i > 2 then "Peter" else "Amit", height: 7} setTarget(if ‖pos - P.pos‖ > 10 then ∅ else P) // Because "and" and "or" operate on non-booleans, the examples // above could have been written less clearly with alternative // logic: const y = (x > 3) and "cat" or "dog" spawnMonster(pos or xy(0,0)) let data = {name: (i > 2) and "Peter" or "Amit", height: 7} // Note that this one had to be restructured // because ∅ acts as false for "and" setTarget(‖pos - P.pos‖ ≤ 10 and P or ∅) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To nest conditionals, you must put parentheses around at least the `falsevalue`, for example `y = if x > 3 then "cat" else (if x > 2 then "dog" else "llama")`. `a default b` is equal to `a` if `a` is not `nil` and `b` otherwise. It does not evaluate `b` if `a` was not `nil`. This is equivalent to the JavaScript and C# [nullish](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) `??` operator. Some common uses are: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Default value on load num_players = load_local("num_players") default 2 // Default value on missing object property height = character_height[name] default 170 // Default value on going out of array bounds vel = array[i + 2] default xy(2, 3) // Fallback value on find() failing debug_print(array[find(array, target) default 0]) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Spaces are not required around operators in most cases, so long as the text parses unambiguously. For example, `1or3` is equivalent to `1 or 3`. There is no ASCII operator for floor or ceiling; use the `floor` and `ceil` functions. There is no way to express mutating bitwise operators using ASCII. Behavior of these operators is consistent with the JavaScript definition of the operation (even though JavaScript uses different operators). For example, the logical-AND expression `4 and 1` evaluates to `1` because `4` is non-false and the result of non-false AND another value is the other value. The trigonometric functions `cos`, `sin`, and `tan` may be invoked as if they were operators when the order of operations is unambiguous. For example, `cos a` ==> `cos(a)`, `cosβ` ==> `cos(β)`, and `cosθ*sinφ` ==> `cos(θ) * sin(φ)`. To avoid confusion and reserve future syntax, they may not be invoked in this form when a high-precedence operator follows the argument. Those operators are `.`, `[`, and exponentiation. Note that the `call()` function can in most cases be replaced simply with the function expression followed by arguments: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript x = (if a then abs else cos)(θ) y = tbl["myfunc"](3, 4) z = f(a)(b) x = call(if a then abs else cos, θ) y = call(tbl["myfunc"], 3, 4) z = call(f(a), b) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### Assignment and Equality As in Python and JavaScript, all assignment operators perform pointer assignment and all equality operators check for pointer equality. All function call parameters are passed by pointer. Strings, `nil`, numbers, and booleans are immutable and unique. This means that variables will be aliased after assignment: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let v = xy(1, 2) let k = v k.x = 3 // v.x is 3 now ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Use `clone()`, `deep_clone()`, or one of the vector-specific helper functions for copying assignment: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let v = xy(4, 5) let k = xy(v) // copy k = clone(v) // generic version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A global game constant that is cloned becomes mutable. There is no way to declare immutable objects or arrays at runtime in code. `const` makes the _binding_ constant, but has no effect on the value (as in JavaScript). ### Exponents The characters `⁺⁻⁰¹²³⁴⁵⁶⁷⁸⁹ᵃᵝⁱʲᵏⁿᵘˣʸᶻ⁽⁾` can form exponent expressions that operate as if the `^` operator or `pow` function was invoked using those expressions as a second argument. The entire exponent expression is implicitly surrounded in parentheses. The variables `a`, `β`, `i`, `j`, `x`, `y`, `z`, `n`, `k`, and `u` may appear only as single-character variable names in exponents. Exponentiation, including via exponent characters, may not be performed on a negative number without parentheses. This follows the rules from JavaScript's `**` operator. For example, `x=y-1²` is legal, but `x=y+-1²` is illegal and must be rewritten as `x=y+(-1)²` or (the confusing and redundant expression, if that's really what was intended) `x=y+-(1²)`. ### Implicit Multiplication Multiplication is implicit wherever a number is immediately followed by a parenthesized expression or variable (including a function call). This also ocurrs when any exponent appears immediately preceeding a parenthesized expression or variable, and where two parenthesized expressions are back to back. For example, `2x`, `x²y`, `3(x+y)`, `(2 + x) y`. Implicit multiplication will not occur if the expression on the left of the pair ends in any bracket: `| ) ] ⌋ ⌉ ‖` and the expression on the right also begins with a bracket. For example, `(x + 1) (x + 2)` and `|y| x` are syntax errors, not implicit products. Implicit multiplication can be used within exponents. ### Spread Operator The spread operator (`…` or `...`) allows defining and calling functions with a variable number of arguments and creating an extended clone of an object or array. It is the same as `*rest` in Python and `...rest` in JavaScript, and similar to varargs in C++ and `...` in Java. In a function declaration it designates the final argument as an array of all further arguments. For example, `def foo(a, b, …rest):` takes any number of arguments and maps them to `a`, `b`, and then an array `rest` of the remainder. For example, `def bar(…args):` takes any number of arguments and maps them all into array `args`. In a function call or array literal, the spread operator expands an array as if its elements had been entered inline. So, `bar(…a)` calls a function with all of the arguments from array `a`. `[1, 2, …a, 10]` creates an array that begins with 1 and 2, then has all of the elements of `a`, followed by the number 10. For objects, `{a:1, b:2, …rest}` creates the object `{a:1, b:2}` and then creates a new extended version in which the properties of `rest` are added, overriding any with the same name in the original object (as with the `extended()` function). This is helpful for the case of calling a function with an args object: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript const text_args = {color: #FFF, x_align: "center", font: roman_font} draw_text({text: "Hello", pos: xy(10, 100), …text_args}) draw_text({text: "World", pos: xy(10, 120), …text_args}) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### Built-in Objects The following are built-in constants and special zero-argument operators. They are not variable names and thus cannot be redefined. Several have both unicode and ASCII names. Literals (replaced by the parser at compile tile): Symbol | ASCII | Value ---------------|-----------------|---------------------------------------- `ε` | `epsilon` | `0.000001` `π` | `pi` | `3.141592653589793115997963468544185161590576171875` `∞` | `infinity` | infinity | `nan` | floating-point "not-a-number" value `∅` | `nil` | ("undefined") The following are lowercase because they contain values that can change without game code specifically triggering that: Symbol | ASCII | Value ---------------|-----------------|---------------------------------------- `ξ` | `random()` | random number on [0, 1), resampled every time the expression is used | `gamepad_array` | array gamepad of objects | `touch` | the object for tracking touch and mouse input | `joy` | shorthand for `gamepad_array[0]` (deprecated) These have values that will not change unless game code specifically invokes a routine to change them, such as `set_screen_size()` (or if the player uses the system GUI for `HOST_CODE`): Symbol | ASCII | Value ---------------|-----------------|---------------------------------------- | `CREDITS` | object of credit and license statements from assets and code | `HOST_CODE` | string that is the code guests can use to connect when hosting | `SCREEN_SIZE` | the size of the full screen in pixels, defaults to `xy(384, 224)`. See also `VIEW_ARRAY[0].size` | `VIEW_ARRAY` | array of corner rects for host and each guest's view in private view online multiplayer. See `set_screen_size()` | `CONSTANTS` | an object mapping all game-specific constants to their values | `ASSETS` | an object mapping all game-specific assets to their values | `SOURCE_LOCATION` | an object that has properties `{url:, filename:, line_number:}` from its location in the source code Loops and Flow Control -------------------------------------------------------------------------- Whitespace is significant, as in Python. A block is a line ending with a `:` whose body is indented. All blocks create a new scope line (unlike Python). The flow control statements are: - Blocks: - `if` conditional with optional `else` and `else if` clauses - `while` loop - `until` loop - `for` loop - `for-in` loop. _See also the `iterate()` function_. - `for-with` loop - `break` and `continue` within loops - `preserving_transform` and `local`, which do not have branching flow but do create local scopes - `def`, which creates a local scope for a function but does not execute immediately - Modes: - `set_mode(ModeName)` to switch game modes - `push_mode(ModeName)` and `pop_mode()` to temporarily enter a mode game mode - Game: - `reset_game()` to reset all state and restart the game. - `quit_game()` to end the current game and return to the launcher. - `launch_game(url)` to launch a different game. `set_mode()`, `push_mode()`, `pop_mode()`, `reset_game()`, `quit_game()`, and `launch_game()` may be followed by the keyword `because` and a reason that is a compile-time string. For example, `quit_game() because "lives = 0"`. The reason allows the IDE to label the transitions on the mode diagram for the game. There is no switch, do loop, goto, or try/catch. For flow control purposes, the empty string, 0, and `∅` are false. All other objects (including empty data structures) are true. Multi-line `if` statements may be followed by `else:` and `else if ...:` clauses. These must always be multi-line. Single-line `for`, `if`, `while`, and `until` statements are permitted. There can be multiple single-line expressions on the same line. No `else` is permitted with single-line `if` statement. In any loop, `break` immediately terminates the tightest enclosing loop and `continue` immediately begins the next iteration of the tightest enclosing loop. `while` and `until` loops evaluate the condition at the top of the loop for every iteration. The `until` loop is a `while` loop with an inverted test: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript until x == "end": x = find_next(x, data) // Is the same as: while x != "end": x = find_next(x, data) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### `&` Elision Trivial blocks can increase indentation so much that code is hard to read because it extends too far to the right. All blocks except for `else` can be elided with an immediately-previous block to avoid excess indentation. To elide blocks, leave the trailing `:` off the outer block and begin the inner block with `&` at the same indentation level: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ for x < 3 & for y < 3: & for z < 3: debug_print(x, y, z) def player_update(player) & preserving_transform & with angle, pos, vel, gamepad in player: vel = lerp(vel, gamepad.xy, 10%) if ‖vel‖ > 0: angle = xy_to_angle(vel) pos = loop(pos + vel, xy(0, 0), SCREEN_SIZE) set_transform(pos) draw_sprite(arrow_sprite) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### `for` Loop Numeric `for` loops increment by 1 each iteration and always count up. The loop variable must be on the left-side of a single comparison loop expression or the center of a multi-comparison expression. The iteration variable in any `for` loop is freshly bound for each iteration. This means that if it is captured by a function as in: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let array = [] for i < 3 def capture(): return i push(array, capture) for f in array: debug_print(f()) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ that the value will reflect the time of the capture. This avoids many common bugs when using first class functions with loops. `for` loops evaluate the bounds once, before the first iteration. The Java/C++ equivalents of `for`-loop conditions are: | Java/C++ ----------------------|------------------------ `for i ≤ k` | `for (i = 0; i <= k; ++i)` `for i < k` | `for (i = 0; i < k; ++i)` `for 4 ≤ i < k` | `for (i = 4; i < k; ++i)` `for 4 < i < k` | `for (i = 5; i < k; ++i)` The comparison operators in a `for` loop declaration are an overloaded syntax for comparison operators (just as many languages overload the `=` operator for `for` loops). They have the lowest precedence. For example, `for i<a||b` is equivalent to `for i<(a||b)`. To use regular comparison operators within `for` loop preamble, you *must* use parentheses to make the parsing unambiguous, for example, `for i < (a < b)`. ### `for-in` Loop Container iteration `for` loops iterate over the keys of a object, the characters of a string, or the elements of an array. The main variations of a `for-in` loop are: - `for value in array:` - `for value in object:` - `for char in string:` - `for value at index in array:` - `for char at index in string:` - `for value at key in object:` The value variable is bound with `let`, so assignment to the value variable is legal but will not affect the underlying value in the array. `for-in` loops evaluate the container expression once, before the first iteration. It is illegal (and will usually result in an error) to add or remove values from the container of a `for-in` loop. This semantic avoids confusing situations for the programmer and is necessary for performance optimizations in the iterator implementation. Changing the order of values is legal, but discouraged because the iterator will touch each slot in order and will skip or duplicate values if they change slots. Writing to the current value is legal and frequently useful. If you would like to add or remove from the container, transform your code from `for x in container:` to `for x in clone(container):`. Cloning is fairly fast in PyxlScript. Or, use the syntax `let i = 0; while i < size(container): let x = container[i] ... ++i` and account for the index and size changes in your own iteration. The Java/C++ equivalents of `for-in`-loop conditions are: | Java/C++ ----------------------|------------------------ `for x ∈ S` | `for (x : S)` ### `for-with` Loop You can combine a `for` and `with` statement together by chaining `∈` expressions, in what is called a `for`-`with` loop. The properties exposed by the `with` are references to the actual values and can be mutated directly. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript for x,y ∈ point ∈ set: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ is the same as: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript for point ∈ set: with x,y ∈ point: ... ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `for`-loop variable (in this case, `point`) cannot be the same as any of the `with` variables (in this case, `x` and `y`). The variable list in a `for`-`with` or `with` expression can be split across multiple lines by breaking after commas: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript for health, power, stamina in character: health += stamina * power ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ### Examples #### `for` loop variations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript // Iterate over each current enemy once, destroying those // that overlap the missile for enemy in clone(enemy_array): if overlaps(enemy, missile): remove_value(enemy_array, enemy) // Regenerate hit points for every player for health, regen_rate in player in player_array: health += regen_rate // Position each equally-spaced satellite for satellite at i in satellite_array: const a = i * 360deg / size(planet_array) satellite.pos = xy(cos a, sin a) * ORBIT_RADIUS // Iterate over an object's keys for rating at stat in player.stats: draw_text(font, stat + ": " + format_number(rating, "%"), pos, #FFF) pos.y += font.line_height // Iterate over an object used as a set already_visited[current_room] = true for room_name in already_visited: draw_text(font, room_name, pos, #FFF) pos.y += font.line_height // Make a segmented-body centipede move if joy.x or joy.y: // Don't use += because the loop passes objects instead of copying them centipede[0] = centipede[0] + joy.xy for 0 < i < size(centipede): centipede[i] = centipede[i - 1] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ [`for` loop examples] #### Array Iteration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let names = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"] // Iterate over array elements let pos = xy(½ SCREEN_SIZE.x, 10) for planet in names: pos.y += draw_text(font, planet, pos, #f) // Iterate over values and modify (this is safe, no elements will be // skipped. You can even remove elements ahead of the iterator and // they will still be processed) for planet in names: // Remove any name with an "a" in it if find(planet, "a") ≠ ∅: remove_values(names, planet) // Add some random moons...the current loop will not // iterate over these, but they are immediately visible // in the array. if random() > 50%: push(names, planet + " moon") // Iterate over array keys pos = xy(½ SCREEN_SIZE.x, 10) for i < size(names): pos.y += draw_text(font, names[i], pos, #f) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #### Object Iteration ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let poemBy = { Angelou: "Still I Rise", Frost: "Stopping by Woods on a Snowy Evening", Dickenson: "Because I could not stop for Death", Whitman: "Leaves of Grass", Hughes: "Montage of a Dream Deferred", Williams: "In the American Grain", Yeats: "Sailing to Byzantium", Browning: "How Do I Love Thee?" } // Iterate over array elements let pos = xy(½ SCREEN_SIZE.x, 10) for author in poemBy: pos.y += draw_text(font, poemBy[author] + " by " + author, pos, #f) // Iterate over values and modify (this is safe, no elements will be skipped) for author in poemBy: // Remove this entry if it contains an "a" in the value if find(poemBy[authpr], "a") ≠ ∅: remove_key(poemBy, author) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ #### Array of Objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let spots = [xy(1, 1), xy(100, 2), xy(10, 20), xy(50, 20)] for x, y in S in spots: // Remove any element that is higher than 15 if y > 15: remove_values(spots, S) let i = 0 while i < size(spots): // More efficient O(1) removal without preserving order // if spots is *really* long if spots[i].x > 15: fast_remove_key(spots, i) else: ++i ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Debugging --------------------------------------------------------- There are two special forms for debugging that can be enabled or disabled at runtime for efficiency on the Output pane. These are not functions and cannot be passed as values like functions or stored in data structures. `debug_print(msg)` sends text output to the IDE Output pane. It runs `unparse()` on `msg` if it is not already a string. These statements are automatically removed from the program when debug printing is disabled. `assert(condition)` or `assert(condition, msg)` halts program execution if `condition` is false. The `msg` is evaluated regardless of the value of condition. These statements are automatically removed from the program when assertions are disabled. Multiline Expressions --------------------------------------------------------------- Newlines end PyxlScript expressions, unless there are unbalanced brackets (), [], {}. This allows you to spread function calls, array literals, and object literals over multiple lines. For example: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PyxlScript let player = { center: xy(100, 40), angle: 45deg, inventory: ["hat", "sword", "money", "watch", "apple"] } let x = (43 + 17) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If an error occurs in a multiline expression, the line number reported will be that of the beginning of the expression. If a line contains the end of a multiline expression, it may not also contain multiple statements. Symbols --------------------------------------------------------------- The IDE supports autocorrect for typing the optional special symbols such as `nil` --> `∅`, `1/2` --> `½`, and `>=` --> `≥`. To generate paired symbols, type the name of the corresponding function without arguments. For example: `floor()` --> `⌊⌋`, `ceil()` --> `⌈⌉`, `magnitude()` --> `‖‖`, and `abs()` --> `||`. For convenience when using an external editor or typing the full set of characters supported by fonts into strings on a US-EN keyboard, you can also copy and paste from here: ````````````````````````````````````````````````````` none big ∅ ° ½ ² ³ ξ ∞ ε ≠ ≤ ≥ π ∊ ⌊ ⌋ ⌈ ⌉ ‖ ≟ θ Mode ══════════════════════════════════════════ frame ────────────────────────────────────────── ````````````````````````````````````````````````````` [Most common symbols] ````````````````````````````````````````````````````` none big 1ˢᵗ 2ⁿᵈ 3ʳᵈ 4ᵗʰ 1ᵉʳ 1ʳᵉ 2ᵉ 2ᵈ 2ᵈᵉ 1ᵒ 1ᵃ ````````````````````````````````````````````````````` [Ordinals in different languages] ````````````````````````````````````````````````````` none big x⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁽ ⁾ yᵃ ᵝ ⁱ ʲ ᵏ ⁿ ᵘ ˣ ʸ ᶻ ````````````````````````````````````````````````````` [Superscripts. These are legal math in code and can be used in strings.] ````````````````````````````````````````````````````` none big ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₍ ₎ ₐ ᵦ ᵢ ⱼ ₓ ₖ ᵤ ₙ ````````````````````````````````````````````````````` [Subscript symbols, usable only in strings] ````````````````````````````````````````````````````` none big A B Γ Δ E Z H Θ I K Λ M N Ξ O Π P Σ T Φ X Ψ Ω α β γ δ ε ζ η θ ι κ λ μ ν ξ o π ρ σ τ ϕ χ ψ ω ````````````````````````````````````````````````````` [Greek alphabet] ````````````````````````````````````````````````````` none big ½ ⅓ ⅔ ¼ ¾ ⅕ ⅖ ⅗ ⅘ ⅙ ⅐ ⅛ ⅑ ⅒ ∩ ∪ ≟ ≠ ≤ ≥ ∊ ⌊ ⌋ ⌈ ⌉ ∅ ∞ ° ````````````````````````````````````````````````````` [Math symbols legal in code and strings.] ````````````````````````````````````````````````````` none big À à È è Ò ò Ì ì Ù ù Ł ł Ś Š Ş ś š ş Ø ẞ Б Д Ґ Á á É é Ó ó Í í Ú ú Ć ć Ž ž Ź ź Ż ż ø ß б д ґ  â Ê ê Ô ô Î î Û û Ñ ñ Ж З И Й Л П Ц Ч Ш Щ Є Ä ä Ë ë Ö ö Ï ï Ü ü Ń ń ж з и й л п ц ч ш щ є Å å Ř ř Ď ď Ý ý Ů ů Ň ň в н κ м т à ã Ę ę Õ õ Ў ў Ç ç Æ Œ Э Ю Я Ъ Ы Ь Ą ą Ě ě Ť ť Ğ ğ Č č æ œ э ю я ъ ы ь ````````````````````````````````````````````````````` [Accented and Cyrillic characters] ````````````````````````````````````````````````````` none big ¥ € £ § × ⊢ ¬ ≈ ⊗ ⊕ ⊖ ∫ ± « » ★ ☆ ○ ● ◻ ◼ △ ▲ ▼ ◀ ▶ ✓ ♠ ♥ ♡ ♣ ♦ ♢ … ∙ ¿ ↑ ↓ ← → ↖ ↗ ↙ ↘ ❖ ✜ ♆ © ````````````````````````````````````````````````````` [Others] ````````````````````````````````````````````````````` none big ⓐ ⓑ ⓒ ⓓ ⓔ ⓕ ⓖ ⓗ ⓜ ⓝ ⓟ ⓠ ⓤ ⓥ ⓧ ⓨ ⓩ ⍐ ⍇ ⍗ ⍈ Ⓠ Ⓩ Ⓦ Ⓐ Ⓢ Ⓓ Ⓘ Ⓙ Ⓚ Ⓛ ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ⓪ ⊖⊕ Ⓞ ⍍ ▣ ⧉ ☰ ````````````````````````````````````````````````````` [Button prompts. Device-aware and full button prompts are available via the `prompt` property of `gamepad_array[]` elements] Note that the superscripts `ᵈᵉʰᵐᵒʳˢᵗ` are legal in text strings but not as code. These are present specfically to support ordinals such as "1ˢᵗ, 2ⁿᵈ, 3ʳᵈ, 4ᵗʰ, 1ᵉʳ, 1ʳᵉ, 2ᵉ, 2ᵈ, 2ᵈᵉ, 1ᵒ, 1ᵃ, …" in various languages. The encircled characters and arrows are intended for use as button prompts. The characters `Ⓞ ⍍ ▣ ⧉ ☰` are drawn in fonts to represent the buttons with approximately those symbols on PlayStation and Xbox controllers. The `ⓧ` character is drawn to ambiguously represent the "X"/cross buttons on PlayStation, Xbox, and Nintendo controllers. Note that the preferred way of generating button prompts is using the `gamepad.prompt` object with the string `replace()` function, rather than hardcoding. Version Control ==================================================================================== is designed to work well with version control systems such as git, svn, Perforce, etc. The metadata and project files are all simple JSON text files and assets are all common binary formats. No hidden directories or temporary files are generated that could confuse a version control system. If you use git in particular, there is additional built-in support for simplifying workflow. The "Git Sync" button appears at the bottom of the project tree when detects that your game is stored in a git repository. Pressing the "Git Sync" button will download changes by others from the repo on the current branch to your local machine. These will be merged into any changes you have made. If there was a substantial merge, then you should probably test the game before proceeding further. If you have changes of your own, will ask if you want to upload them. If you choose to upload, it will prompt for a commit message. Sometimes a merge will produce a conflict. This happens when both you and your collaborator changed the same line of a file in different ways, or edited the same art asset. If a merge conflict occurs, you will be presented with the following choices: - Prefer Mine: Merge everything, and on lines where there is a conflict replace their changes with yours. - Prefer Theirs: Merge everything, and on lines where there is a conflict replace your changes with theirs. - Undo Sync: Stop the merge and put all of the files back the way they were before you pressed "Sync". You can then choose to address the problem with command line tools or ask a collaborator for help. For advanced git users, all of the git commands used and their outputs are shown on screen. You can also freely mix git command line or other GUI git tools with the built-in git support (although don't try it _while_ a command is being processed). Advanced Tools ==================================================================================== In addition to the friendlier browser-based tools from the Tools menu, the distribution includes command-line tools for experienced programmers. These support external editors, running the server locally, and performing some IDE tasks at the command line. Unlike the IDE, some of these have additional dependencies and assume that you know how to install those on your own. Local Emulator ----------------------------------------------------------------------------- You can launch the IDE on your local machine. This allows you to run without a network connection and to work with local files instead of Google Drive ones for the GitHub-hosted IDE. Launch your program in the emulator and debugger from the command line on Windows, macOS, or Linux with the `quadplay` command: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bash # Load the games/quadpaddle/quadpaddle.game.json sample program quadplay quad://games/quadpaddle # Load a mygames/space/space.game.json game that you've made quadplay mygames/space # Load from the internet (server must support CORS) quadplay https://morgan3d.github.io/somegame/foo.game.json # Just run the emulator. You can modify the URL in your browser # to add "?game=" and the game's URL or relative path quadplay ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Run `quadplay --help` to see more options for the script. These examples assume that your directory structure is: ********************************************************** * * 📂 quadplay * | * +-- 📄 quadplay * | * +-- 📂 fonts * | | * | ⋮ * | * +-- 📂 games * | | * | +-- 📂 quadpaddle * | | | * | | +-- 📄 quadpaddle.game.json * | | | * | ⋮ ⋮ * | * +-- 📂 mygames * | | * | '-- 📂 space * | | * | +- 📄 space.game.json * | | * | ⋮ * | * +-- 📂 sprites * | | * ⋮ ⋮ ********************************************************** [Directory structure with the `quadplay` script in the root.] The directory and `.game.json` file do not have to have the same name. You can list the full path to the actual game file instead. Every time you push the play button (F5) or reload button (Ctrl+R) in the emulator, it will reload your code and assets from disk. By default, `quad://` assets are cached for fast reload peformance. To disable this behavior, force a reload of the entire page with shift+browser reload button, or remove `&fastReload=1` from the URL. Visual Studio Code ------------------------------------------------------------------------------------ Visual Studio Code natively supports the `.json` files. To configure it with PyxlScript file support as well: 1. Run Visual Studio Code 2. Press F1 and type `install code` and then press enter 3. Open a terminal and change directory to `quadplay/tools` 4. At the command line, run `code --install-extension vscode-pyxlscript.vsix` Vim ------------------------------------------------------------------------------------ To install the PyxlScript mode for Vim, you'll need to modify your .vimrc. If you're using a plugin manager like [dein](https://github.com/Shougo/dein.vim): ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ call dein#add("path/to/quadplay/tools/vim-pyxlscript-syntax", {'frozen':1}) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you want to directly copy the files into your ~/.vim directory, make sure you copy (or symlink) all the files into the corresponding directories like they structured in the plugin. [`pyxlscript.vim`](../tools/pyxlscript.vim) Emacs ------------------------------------------------------------------------------------ To install the PyxlScript mode for Emacs, add the following lines to your `~/.emacs` file: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Lisp ; Optional if you don't want to copy the files: (add-to-list 'load-path (expand-file-name "path/to/quadplay/tools")) (autoload 'rainbow-mode "rainbow-mode") (autoload 'pyxlscript-mode "pyxlscript-mode") (add-to-list 'auto-mode-alist '("\\.pyxl\\'" . pyxlscript-mode)) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Note that you have to put the real path in for the `"path/to/quadplay/tools"`, such as `~/quadplay/tools`, or wherever you installed quadplay to. Other Editors ------------------------------------------------------------------------------------ Fortunately, PyxlScript syntax is very close to Python syntax. If you tell your editor to treat `.pyxl` files as Python files, then it will probably handle indenting and most keywords correctly. Comments are the one element that will probably confuse your syntax highlighter. Command-Line Tools ------------------------------------------------------------------------------------ `quadplay` : Launches the emulator from the command line on any operating system. Requires Python. See the arguments above. (Depending on operating system, this command may run the `quadplay.cmd`, `quadplay.vbs`, or `quadplay` script.) `quaddepend.py` : Prints dependencies of a `.game.json` file, recursively. Run the script with `--help` to see the arguments. Run as `tools/quaddepend.py ...` or `python3 tools/quaddepend.py ...` from the main quadplay directory. `export.py` : Export a static HTML site for a single game, either lightweight by referencing `https://morgan3d.github.io/quadplay` or standalone by embedding a full copy of the emulator. Run the script with `--help` to see the arguments. Run as `tools/export.py ...` on Linux and macOS, or `py tools/export.py ...` on Windows from the main quadplay directory. This script itself depends on `tools/quaddepend.py`. `project_linter.py` : Linter for quadplay projects. Intended to help when finalling a project, it has a number of checks including: - unused assets - unused constants - missing licenses - missing quadplay metadata (preview.png, etc) - report of licenses used in a project (use the --license-audit flag) For a full list of features, use the --help argument. Run it in the same directory that containst the game.json for your project. `sprite_json_generator.py` : Generator for `.sprite.json` files. Requires Python and PIL. If you are exporting from aseprite, use the following settings with `sprite_json_generator.py`: - Sheet type: "by row" (quadplay doesn't support animations wrapping around image rows - Constraints: "None" - Sprite: - Layers: "visible layers" (quadplay doesn't support layers") - Frames: "all frames" (unless you only want some frames) - "split layers" should be unchecked (OFF) - "split tags" should be checked (ON) - Borders: - "spacing", "trim", and "extrude" should all be off (set to 0) - Output: - "export json": yes and under "meta" for this: - "layers" should be off - "frame tags" should be checked - "slices" should be off - "Array" (_not_ "Hash") mode - Output file and JSON Data should be checked (these are what you feed into sprite_json_generator) - "open generated sprite sheet" is up to you, if you want it to show you the sprite sheet when its done. If you export with these settings, you can pass the generated json file to the sprite_json_generator program and it will convert the tags and sprite size automatically from the file. Note that while aseprite supports sprites of varying size, quadplay does not. `quadplay.html` ------------------------------------------------------------------------------------ The `quadplay.html` application launched by the `quadplay` script accepts the following HTML query arguments: `IDE` : If set, load in development mode with access to the full IDE and stop and pause buttons. Cannot be set with `kiosk` `fastReload` : If `1`, do not force reloading of `quad://` files from disk but instead allow browser caching. Those resources are unlikely to change (unless you are modifying quadplay itself!), so this speeds up game reloading, especially for music. `game` : URL of a game to load, which can be relative to the `quadplay` script. `mode` : Initial mode to display: `Maximal`, `Emulator`,`IDE`, `Test`, `WideIDE`, `Editor`, `Ghost`, or `DefaultWindow`. The default is `IDE` when running the IDE and otherwise `Maximal` for non-touch screens and `Emulator` for touch screens. `DefaultWindow` reverts to the default but suppresses automatic full-screen on launch when not on a touch screen. `nofs` : If `1`, do not try to `kiosk` : If `1`, hide the entire menu bar. Cannot be set with `IDE` `nosleep` : If `1`, prevent sleeping in kiosk and normal mode, no matter what the autosleep setting is. `quit` : What the "quit to system" menu option in the pause menu and the `quit_game()` function do: - `launcher`: Return to the launcher or IDE. This is forced when `IDE=1`. Default when `game` is not specified. - `close`: Close the browser tab or standalone app. Default when `game` is specified and `IDE=0` and `kiosk=0`. Unless the page was opened by JavaScript from another page, browsers prohibit closing the tab from code and this will instead set the page to blank. - `none`: Do not offer a quit option to the player. Default when `game` is specified and `kiosk=1` - `reload`: Reload the iframe or tab that the game is in, sending it back to the welcome screen. This value prevents the game from launching in fullscreen until the first press, which is good for games embedded in a page and is the way that exported web games are configured. `nativeapp` : If `1` and running on a quadplay server, quitting the IDE shuts down the server. `update` : If `1` and running on a quadplay server, check for newer versions of quadplay. Physical Consoles --------------------------------------------------------------------------------------------- ### Build I expect most people will use on phones, laptops, and desktops as a _virtual_ console. But, if you have some maker experience you can make a _real_ console. If you want an arcade form factor, then a arcade cabinet can be built with some carpentry and any low-cost PC and display, or by purchasing some (expensive!) pre-built arcade cabinets. #### Processor Any Mac, Windows, or Linux computer can be used to power a physical build. An old laptop often works well. Alternatively, an embedded processor can be used. The [Rasberry Pi 400](https://www.raspberrypi.org/products/raspberry-pi-400/) is a complete computer for only $70 that can run and plug directly into a television or monitor. With a 3D-printed case and some Bluetooth or wired game controllers, you can build your own retro mini console to connect to a TV or monitor. [Jetson Nano](https://developer.nvidia.com/embedded/jetson-nano-developer-kit) and [Raspberry Pi 4](https://www.raspberrypi.org/products/raspberry-pi-4-model-b/) cost less than $100 and can power such a box using Linux and Chromium. Both will hit 60 Hz for most games, and can easily run at 30 Hz for all games ( handles framerate scaling for you seamlessly, so you won't even notice most of the time). ![Quadplay mini-console built by Morgan McGuire from a Jetson Nano with a 3D-printed case, USB Xbox 360 controller, and small external monitor.](nano-arcade.jpg width=50%) I've built Jetson Nano and Raspberry Pi consoles and the result is satisfyingly comparable to the Nintendo [NES Classic Edition](https://www.nintendo.com/nes-classic/), [Super NES Classic Edition](https://www.nintendo.com/super-nes-classic/), Sega [Genesis Mini](https://genesismini.sega.com/), and PlayStation [Classic](https://www.playstation.com/en-ca/explore/playstation-classic/) devices. The big difference from commercial mini retro consoles is that you can program your own quadplay console. The commercial consoles only play their own games. ![Self-contained quadplay console built by Morgan McGuire from a Raspberry Pi and standard screen case, with Bluetooth controllers.](rpi-arcade.jpg style="image-rendering:none" width=50%) My Raspberry Pi 4 software configuration from the default Raspberry Pi OS instalation is simply: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bash sudo apt install snapd sudo reboot # After reboot: sudo snap install core sudo snap install chromium sudo update-alternatives --install /usr/bin/x-www-browser x-www-browser /snap/bin/chromium 200 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is an alternative using the provided Debian package for automatic management, but gives an older version of Chromium that is slower: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bash sudo apt install chromium sudo update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/bin/chromium-browser ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For the Raspberry Pi 400 (the model built into a keyboard), I also [overclock to 2 GHz](https://www.raspberrypi-spy.co.uk/2020/11/overclocking-the-raspberry-pi-400/). A Jetson Xavier, Intel NUC, or old PC/Mac is a more expensive device for powering a mini console, but has a lot more graphics power than an embedded processor and will comfortably run all games at 60 Hz. #### Arcade Controller When you're color coding the physical controls for a full four-player setup, the standard arcade player colors and physical layout order is: P3 Yellow, P1 Magenta ("pink"), P2 Cyan ("blue"), P3 Lime ("green"). Graphically:
SCREEN
*P3*
#fd3
*P1*
#f5a
*P2*
#0af
*P4*
#4e4


There are a lot of physical [arcade controllers](https://shop.xgaming.com/products/x-arcade-dual-joystick-usb-included) available at reasonable prices for building out the controls without needing any custom electronics. Note that supports (but does not require) a touch screen or mouse, which will not be available on a normal cabinet setup. One alternative is to use a small trackpad mounted on top of the joystick controller. ![Arcade station built by Morgan McGuire running quadplay on a 2012 MacBook with an X-Arcade Dual controller.](xarcade-arcade.jpg style="image-rendering:none" width=50%) ![Arcade station custom built by Nick Porcino running quadplay on Raspberry Pi](arcade.jpg style="image-rendering:none" width=50%) For arcade configurations, the order of the physical controllers in the `gamepad_array[]` matters because they are at fixed locations. The default order of controllers is set by the operating system. From the Controllers screen of the in-game pause menu or the Controls tab of the IDE's debugger, you can bring up the quadplay dialog to change this order. When building your own controller, ensure that it maps correctly using the online tool at https://html5gamepad.com/. The specific mapping between HTML buttons and is: | HTML Gamepad :-------------------:|:-------------------------------: Analog x-axis | axis 0 (left is negative) Analog y-axis | axis 1 (forward is negative) ⓐ | button 0 ⓑ | button 1 ⓒ | button 2 ⓓ | button 3 ⓔ | button 4 ⓕ | button 5 ⓠ | button 8 ⓟ | button 9 D-pad | button 12 D-pad | button 13 D-pad | button 14 D-pad | button 15 automatically maps the analog stick into D-pad inputs as well as providing analog access under `device_control()`. See the Arcade Controller Mappings Section for more detail on some common arcade/fighting game controller mappings. ******************************************************** * | * ⓠ ⓟ | ⓠ ⓟ * ⍐ ⓓ ⓕ | ⍐ ⓔ ⓒ ⓓ ⓕ * ⍇ ⍈ ⓒ ⓑ | ⍇ ⍈ * ⍗ ⓔ ⓐ | ⍗ ⓔ ⓐ ⓑ ⓕ * | ******************************************************** [Sample layouts for physical controller buttons on arcade sticks] If you have the option to choose your button layout and mapping, then try to match one of the versions shown above. Specifically, games are designed for the ⓔ button to be placed where it can be easily held while using the main buttons and stick. For example, as a hat button on the stick or under the right thumb or pinkie. The ⓕ button should be placed where it can be used similarly, but ⓔ should be the more convenient of the two and favor the left side. Note the positioning of the player 1 and 2 ⓔ and ⓕ on the keyboard compared to on a NES-style gamepad...the keyboard controls place these buttons _lower_ than on a gamepad, so that they are in reach of the pinkies for ease holding down. They are not "shoulder" buttons, even though that is the best location for them on a physical controller. #### Display For a physical cabinet, a 24" screen will match a traditional arcade size. For a handheld or mini installation, the screen size is constrained by your form factor. Anything larger than 5" will be easily readable even for the smallest 4-pixel text (and most quadplay game text is 8 pixels high). has a default 16:9 screen resolution. However, aspect ratio is not as important to image quality as display that can be filled with integer multiple of 384x224 resolution. I recommend using a 1280x720 ("720p"), 1600x900, 1920x1200 ("16:10 WUXGA"), or 2560x1440 ("WQHD 2K") resolution display. A 2560x1440 display can be run at the OS level in 1280x720 resolution to reduce GPU load for a low end or embedded GPU. Any display will scale down to 60 Hz refresh, so refresh rate is not a consideration. Those resolutions can efficiently fill the screen with integer pixel scaling. For 24" monitors, these cost around $200 new as of 2024 but can often be found used for less (or free) as most people have upgraded their computers to higher resolutions. If you are not a purist, you can find a more inexpensive 1920x1080 display. This will use non-integer scaling where some of the pixels are 4 display pixels wide and most are 5 pixels wide, or leave a black border around the screen when using perfectly uniform 5x scaling. The autoscaler will adjust to fill the screen unless the display resolution is very low or only slightly above an integer ratio, in which case it will favor the integer ratio. Efficiency and scaling ratios when using 's default 384x224 resolution are: Screen | Integer | Used | Efficiency | Autoscaler | Used | Efficiency ----------:|------------:|----------:|------------:|-----------:|----------:|---------: 480x320 | 1x | 384x224 | 56% | 1.00x | 384x224 | 56% 640x360 | 1x | 384x224 | 37% | 1.00x | 384x224 | 37% 640x480 | 1x | 384x224 | 28% | 1.00x | 384x224 | 28% 800x480 | 2x | 768x448 | 72% | 2.00x | 768x448 | 72% 1024x768 | 2x | 768x448 | 44% | 2.00x | 768x448 | 44% *1280x720* | *3x* |*1152x672* | *84%* | 3.21x | 1234x720 | 96% 1280x800 | *3x* | 1152x672 | 76% | 3.33x | 1280x747 | 93% 1366x768 | 3x | 1152x672 | 74% | 3.41x | 1316x768 | 96% 1440x900 | 3x | 1152x672 | 60% | 3.75x | 1440x840 | 93% *1600x900* | *4x* |*1536x896* | *96%* | 4.02x | 1544x900 | 97% 1680x1050 | 4x | 1536x896 | 78% | 4.38x | 1680x980 | 93% 1920x1080 | 4x | 1536x896 | 66% | 4.82x | 1852x1080 | 96% *1920x1200*| *5x* |*1920x1120*| *93%* | 5.00x | 1920x1120 | 93% *2560x1440*| *6x* |*2304x1344*| *84%* | 6.42x | 2468x1440 | 96% ### Buy works on all Mac, Windows, and Linux devices, so any computer you buy can be a dedicated device for playing or making games. For a _handheld_ form factor there are a few options. We've tested on the Steam Deck, GPD Win 2, GPD Win Max, and GPD Win 3. All run well: - 1280x800 [Steam Deck](https://www.steamdeck.com/), SteamOS/Linux in "Desktop" mode = full-res 3x/3.33x scaling, 4x scaling of 320x180 - 1280x800 [GPD Win Max](http://gpd.hk/gpdwinmax), Windows and Linux = full-res 3x integer or 3.33x scaling, 4x scaling of 320x180 - 1280x720 [GPD Win 3](https://www.gpd.hk/gpdwin3) and [GPD Win 2](http://gpd.hk/gdpwin2), Windows and Linux = full-res 3x/3.33x scaling, 4x scaling of 320x180 ![Quadplay running on Steam Deck under Firefox in Desktop mode with no installation.](steamdeck.jpg style="image-rendering:none" width=50%) Some other physical handhelds that may be capable of running games include: - 2560x1600 [GPD Win Max 2](https://gpd.hk/gpdwinmax2) - 1280x800 [AYA Neo, Neo Pro, Neo Next](https://store.ayaneo.com/) - TBD [Lyra+](https://www.creoqode.com/lyra-plus) - 2560x1600 [OneXPlayer](https://onexplayerstore.com/) - 1920x1080 [OneXPlayer Mini](https://onexplayerstore.com/) - 1280x720 [Anbernic Win600](https://anbernic.com/products/new-anbernic-win600) - 1280x720 [DragonBox Pyra](https://pyra-handheld.com/boards/pages/pyra/), Linux = full-res 3x/3.33x scaling - 2560x1600 [AYN Odin](https://www.indiegogo.com/projects/odin-the-ultimate-gaming-handheld/x/27430815#/) with sideloaded Windows 10 - 480x320 [Odroid-Go Advance Black](https://liliputing.com/2020/05/the-new-odroid-go-advance-adds-wifi-and-more-buttons-linux-handheld-game-console.html), Linux = full-res 1x scaling; poor fit ![GPD Win 3](gpd-win3.jpg style="image-rendering:none" width=50%) Mobile Development ===================================================== The quadplay IDE allows you to easily test your game on a mobile device while developing from a laptop or desktop computer. First, launch the `quadplay` script on your computer with the `--serve` flag. For example, ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ bash quadplay --serve games/quadpaddle/ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ (Use your own game, not the `quadpaddle` sample, of course). Second, in the web browser that opens on your computer, select the Project or Test view at the upper right. Click on the Host tab of the debugging pane on the lower right: ![The QR code in the Host tab of the IDE](host-tab.png) Third, _on your mobile device_, either scan the QR code and then open the URL it shows in a browser, or manually type the URL into your mobile browser. Your mobile device will now play the game direct from your computer. You can edit the game or assets on your computer and reload the browser page on your mobile device to see the changes. You do not need to re-scan the QR code for each change. !!! note Mobile Controls On a mobile device, quadplay games be played with the on-screen touch controls or a physical game controller paired over BlueTooth or plugged in directly over USB. Deploying Games ==================================================================================== Your games can be played on any major web browser on desktop/laptop, phone, tablet, or embedded processor such as Raspberry Pi. You can also distribute them as phone or desktop apps. You may charge money for your games and may distribute them as closed source. It is your responsibility to satisfy copyright law and any licensing fees or royalties for content that you use when you distribute your game. The content provided with is all royalty-free Creative Commons and contains its own licensing data. Content that you find elsewhere will have its own rules and restrictions. The code provided with is all Open Source (even though it permits you to use it with your own closed-source came). See the Credits section for more information. itch.io ------------------------------------------------------------------------------------ Deploying to [itch.io](https://itch.io/) as a Browser Game allows you to create a self contained online game that others can buy from you, or play for free if you choose. The steps are: 1. On Windows, run `py -3 path-to-quadplay\tools\export.py` inside the directory containing your `game.json` file. On macOS and Linux, use `path-to-quadplay/tools/export.py`. 2. [Click here](https://itch.io/game/new target="_blank") and in the tab that opens, enter the following settings: - Title: _your game name_ - Project URL: _your game name_ - Classification: Games - Kind of project: HTML - Embed Options: - How should your project be run in your page?: Embed in page, Manually set size - Mobile friendly: Yes - Orientation: Landscape - Automatically start on page load - Visibility & Access: Public 3. In the Uploads section on the itch.io game page, upload the zipfile generated in step 1. 4. Check "This file will be played in the browser" To update your game or upgrade the underlying distribution, repeat the process of creating the zipfile and then upload it as a replacement for the original file. ![](itch.png style="image-rendering:none" width=50%) GitHub ------------------------------------------------------------------------------------ ### Combined develop and deployment repo This is the easiest method to deploy your game. It also allows you to continuously update the game as well as gain the benefit of improvements to itself continuously. Store your game in a git repository on [github.com](https://github.com), or in a subdirectory of a GitHub repository. Do not fork quadplay into the repo because that will make it large and disrupt the user's saved per-console state. Just keep your game there in the repo by itself as you create it. Assume that your GitHub account is named `USER` and your repo is named `REPO`. To deploy, go to the settings page for your repo on GitHub. For example, https://github.com/USER/REPO/settings. In the section titled "GitHub Pages" on the settings page, select "main" from the Source section dropdown. The URL to play your game is then: `https://morgan3d.github.io/quadplay/console/quadplay.html?game=https://USER.github.io/REPO&mode=Maximal`. If you'd like to make a friendlier URL, then you can also create an `index.html` file in the root of your repo with the contents: People visiting the URL https://USER.github.io/REPO will then be redirected to the correct page. ### Separate repos To keep your development repository private or just separate from the deployment one, create a second repository on GitHub. Clone both to your local machine and at the command line, enter the _development_ repo and run: ````````````````````````````````````````````````````````````` # macOS and Linux: tools/export.py -o ../PUBLIC_REPO path/to/game # Windows: py -3 tools\export.py -o ..\PUBLIC_REPO path\to\game ````````````````````````````````````````````````````````````` You can then add and commit the public repo. If you want your game to be standalone and not depend on the quadplay repo at runtime, just add the `--standalone` argument. This will generate an `index.html` file as well as copying all dependencies. Web Server ------------------------------------------------------------------------------------ You can host your game using only your own files on any web server that supports [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS), or as a copy of all of on _any_ web server, regardless of whether it supports CORS. To deploy your game on a web server with CORS named `myserver.com` in the `mygame` directory, run just upload your own game files to the server. The URL for your game is: https://morgan3d.github.io/quadplay/console/quadplay.html?game=https://myserver.com/mygame To deploy a complete package that is independent of the current version of , run `tools/export.py -o path/to/output path/to/game` on macOS or Linus and `py -3 tools\export.py -o path\to\output path\to\game` on Windows. Then, upload your files from the output directory. Your URL will be just https://myserver.com/mygame. Phones ------------------------------------------------------------------------------------ Any game hosted on a web page using the methods from the previous sections can be played on a mobile phone. The emulator automatically creates on-screen buttons when it detects a phone, and it also supports gamepads connected to a phone. You can also deploy your game through a phone app store in order to sell it. This is an advanced process. You must wrap your game with a binary program. [Cordova](https://cordova.apache.org/) is a free tool that you can use to automate this process. It requires familiarity with [npm](https://www.npmjs.com/), web apps, the app store that you are deploying on, and the tools needed (such as Xcode for iOS) to compile on the platform. The basic idea is to use the method described in the itch.io section to create a standalone web app, and then use Cordova to create the phone app. Desktop ------------------------------------------------------------------------------------ [Electron](https://electronjs.org/) is a tool that turns web apps into desktop binary projects which can be distributed through the Windows or Mac App stores, Steam, the Epic Store, etc. It works by bundling an entire web browser with your application, wrapped so that it looks like a native app. The process is similar to that of using Cordova. Performance ==================================================================================== is designed to make it easy to write small games that have very readable source code. I recommend designing your code primarily for readability instead of performance. However, at some point you'll probably hit the performance limits and want to optimize at least some of the program. Here's where to focus your efforts. The emulator is fastest in Full Screen or Touch Screen mode. This eliminates the extra IDE content and avoids several milliseconds per frame of updates required for those. To avoid input lag, process game pad input _before_ rendering and simulation each frame. This allows the players' input to affect the current frame instead of being delayed one frame. Game pads (and browsers) have 1-2 frames of input lag in them. To conceal this, allow critical input such as a jump, block, or dodge to happen up to one or two frames _after_ the period in which it should have. For example, do not damage the player character until one frame after they are attacked and allow a late dodge or block to save them retroactively. In the "Performance" tab of the IDE, you can see the amount of time spent per frame. If time spent is less than 16 ms, then your program is running at 60 fps. If time spent is higher than 16 ms, then you may need to optimize. `draw_map()` is substantially faster than making the same `draw_sprite()` calls by yourself. It culls all off-screen sprites. Drawing a map with `camera.zoom = 1` or `z_scale = 0` is faster than with perspective and zoom. Tiles without alpha will cull everything behind them when not using perspective, so you don't have to leave out the hidden map tiles for performance when drawing maps. Maps with opaque tiles that are not rotated or scaled draw extremely fast. `draw_map_span()` is substantially faster than your own per-pixel rendering using `get_map_pixel_color()` and `draw_point()`. It is fastest when using opaque map tiles, a single layer of a map, and no override color. The Kart example program intentionally uses a slower case as a stress test. Built-in math functions are faster than math functions that you write to do equivalent operations because they are designed to minimize memory allocation internally. The intrinsics are maximum speed but can make code hard to read. There is generally no need to cull objects that are off screen. The rendering code is very good at removing those for you. `draw_sprite()` and `draw_map()` are fastest for `opacity=1` sprites that are not rotated or scaled and do not have alpha. It can directly copy the memory in this case. It can be substantially faster (but use more memory) to use pre-drawn transitions between map terrain tiles instead of layering an alpha-masked tile on top of another one to blend them. If you can tell that an object is covered by another object in `z`, remove it (this is called occlusion culling). The renderer must process both otherwise even though the object that is behind is unseen. All of the drawing calls are much faster for opaque colors than ones with fractional alpha values. Perfectly horizontal lines are much faster to draw than vertical lines. Vertical lines are slightly faster than diagonal lines. Sequential `draw_point()` calls that have the same `z` value are significantly optimized in order to enable efficient per-pixel graphics. Sequential `draw_sprite()` calls that have the same `z` value and the same clipping region are slightly optimized to enable efficient custom map rendering and particle systems. Numeric for loops, such as `for i < N` are slightly faster than iterator for loops such as `for x in A`. `with` statements are relatively slow, as are `for`-`with` loops on large arrays. Use them for readability on individual or small sets of objects but not on large arrays or in loops that repeat many times. It is slightly faster to perform math on individual elements than vectors. For example, `pos.x += vel.x; pos.y += vel.y` is slightly faster than `pos += vel`. The tradeoff of expanding out math is generally not worth the readability cost unless processing hundreds of vectors. The performance advantage diminishes as the objects grow larger. For example, `rgba` vectors or adding whole arrays to each other have relatively less overhead than `xy` vectors. The IDE is designed to collapse gracefully so that you can use an external editor. Here's one useful layout: ![](external-editor.png width=50% style="image-rendering:auto") The editor on the left is emacs running in a terminal. On the right is the browser reduced to the debugging output and emulator. Be careful to always end your *filenames* with _two_ extensions: `.game.json`, `.sprite.json`, `.map.json`, etc. This is how the IDE and compiler knows what type it is loading. Take note of the 4-bit sprite [quantizing tool](../tools/quantize.html). I find it better than the quality produced by Photoshop for color reduction. Road Map ==================================================================================== The API is stable, and many people are using it to make hobby games, participate in game jams, and to learn/teach programming. The soul of as a console is described by: - Up to four players with a shared screen - Minimal time from launch to main play - 1985 output limitations, 2023 development power The overarching design principles of the quadplay IDE and language are: - *Make small game development friendly and fun* - Limitations that: - Allow games to run effectively on a wide range of displays, processors, and input devices without any per-platform work - Focus developers on gameplay and innovation - Help developers prevent scope creep - Allow developers to bypass limitations to customize for a specific platform or game - Support working wherever developers choose: - Entirely in a browser - Primarily in external tools - Or any combination! There's a long feature list and a short bug list that I'm working on. The highlights of this are, in priority order: [ ] Built-in IDE [ ] Map editor [ ] Extended constant editor [ ] Sprite editor [ ] Font editor [ ] Shadowing/line of sight [ ] Standalone IDE program (Python and browser not required) [ ] macOS [ ] Windows [ ] Perfect outlined-disk and outlined-triangle blending [ ] Polyline rendering without double-blending endpoints [ ] Watertight rasterization of triangle meshes [ ] Watertight rasterization of polylines [ ] Watertight rotated sprite and map rasterization [ ] Export to: [ ] macOS binary executable [ ] Windows binary executable Credits ==================================================================================== was created by Morgan McGuire. Stephan Steinbach provided extensive testing and design feedback, Josh Minor helped with the networking design, and Mauricio Vives proofread the manual. The console's influences include Atari 2600, Commodore VIC-20, APL, Python, [PICO-8](https://www.lexaloffle.com/pico-8.php), [Shadertoy](https://www.shadertoy.com), [Arduboy](https://arduboy.com), and its predecessors, [codeheart.js](https://casual-effects.com/codeheart/) and [the nano JAMMER](https://morgan3d.github.io/nano). The IDE and compiler are copyright 2019-2023 Morgan McGuire. They are licensed under the [LGPL 3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html), but may be relicensed in the future. This includes the contents of: - [`quadplay.html`](../console/quadplay.html) - [`quadplay.css`](../console/quadplay.css) - [`quadplay-runtime-cpu.js`](../console/quadplay-runtime-cpu.js) - [`quadplay-runtime-gpu.js`](../console/quadplay-runtime-gpu.js) - [`quadplay-runtime-ai.js`](../console/quadplay-runtime-ai.js) - [`quadplay-runtime-physics.js`](../console/quadplay-runtime-physics.js) - [`quadplay-profiler.js`](../console/quadplay-profiler.js) - [`quadplay-browser.js`](../console/quadplay-browser.js) - [`quadplay-network.js`](../console/quadplay-network.js) - [`quadplay-ide.js`](../console/quadplay-ide.js) - [`quadplay-mode.js`](../console/quadplay-mode.js) - [`quadplay-edit-asset.js`](../console/quadplay-edit-asset.js) - [`quadplay-edit-code.js`](../console/quadplay-edit-code.js) - [`quadplay-edit-game.js`](../console/quadplay-edit-game.js) - [`quadplay-edit-constant.js`](../console/quadplay-edit-constant.js) - [`quadplay-font.js`](../console/quadplay-font.js) - [`quadplay-load.js`](../console/quadplay-load.js) - [`quadplay-debug.js`](../console/quadplay-debug.js) - [`pyxlscript-compiler.js`](../console/pyxlscript-compiler.js) and excludes the built-in PyxlScript examples, games, and content described below. The Vim mode is by [Stephan Steinbach](https://twitter.com/stephan_gfx). The VS Code extension is by [Josef Spjut](http://josef.spjut.me/). GIF support via [gif.js](https://github.com/jnordberg/gif.js) (c) 2013 Johan Nordberg, used under the [MIT license](https://github.com/jnordberg/gif.js/blob/master/LICENSE). It includes code by Kevin Weiner, Thibault Imbert, and Anthony Dekker. QR code support via [qrcodejs](https://github.com/davidshimjs/qrcodejs) (c) 2012 [@davidshimjs](https://twitter.com/davidshimjs) used under the [MIT license](https://raw.githubusercontent.com/davidshimjs/qrcodejs/master/LICENSE). [PeerJS](https://peerjs.com/) is Copyright 2015 Michelle Bu and Eric Zhang. It is used under the MIT license. The random number generator is derived from [an implementation](https://github.com/AndreasMadsen/xorshift/blob/master/xorshift.js) of xorshift (c) 2014 Andreas Madsen and Emil Bay used under the [MIT license](https://github.com/AndreasMadsen/xorshift/blob/master/LICENSE.md). The code editor for the IDE is [ace.js](https://ace.c9.io/) used under the [BSD license](https://github.com/ajaxorg/ace/blob/master/LICENSE). The physics engine is [matter.js](https://brm.io/matter-js/) and [poly-decomp.js](https://github.com/schteppe/poly-decomp.js), both used under the [MIT license](https://raw.githubusercontent.com/liabru/matter-js/master/LICENSE). The compiler uses [vectorify.js](https://github.com/morgan3d/misc/tree/main/jsvectorify) (c) 2018 Morgan McGuire under the MIT license, [recast](https://github.com/benjamn/recast) (c) 2012 Ben Newman under the [MIT license](https://github.com/benjamn/recast/blob/master/LICENSE), and [estraverse](https://github.com/estools/estraverse) (c) 2012-2016 Yusuke Suzuki under the [BSD 2-clause license](https://github.com/estools/estraverse/blob/master/LICENSE.BSD). [`LoadManager.js`](https://github.com/morgan3d/misc/tree/main/jsloadmanager) is (c) 2018 Morgan McGuire, [BSD-2-Clause](https://opensource.org/licenses/BSD-2-Clause) license. [`WorkJSON.js`](https://github.com/morgan3d/workjson) is (c) 2020 Morgan McGuire, [MIT](https://github.com/morgan3d/workjson/blob/main/LICENSE) license. [`js-yaml`](https://github.com/morgan3d/misc/tree/main/jsloadmanager) is (C) 2011-2015 by Vitaly Puzrin, [MIT](https://raw.githubusercontent.com/nodeca/js-yaml/master/LICENSE) license. [`dagre.js`](https://github.com/dagrejs/dagre) is (C) 2012-2014 Chris Pettitt, [MIT](https://github.com/dagrejs/dagre/blob/master/LICENSE) license. The boot screen font used in the emulator is [https://int10h.org/oldschool-pc-fonts](PxPlus_AmstradPC1512-2y) copyright 2016 [Int10h](https://int10h.org/), used under the [CC-BY 4.0](http://creativecommons.org/licenses/by-sa/4.0/) license. It is based on the Amstrad PC1512 BIOS font. All built-in assets are Creative Commons licensed have complete license and copyright information in the "license" property of the corresponding JSON file. Logos ------------------------------------------------------------------------------------- The following logos may be used for promoting or linking to : ![](logos/quadplay-logo-gradient-round.png) ![](logos/quadplay-logo-gradient.png) ![](logos/quadplay-logo-gray.png) Notes for Beta Testers ===================================================================================== The IDE will receive major new features throughout and after the beta phase. This includes editors for sprites and maps. The private view online multiplayer mode is very new and has several known limitations and bugs: - There is no way to send audio to only some of the players - Some of the system menus do not work in private screen mode (credits, controls menu, set controls menu, set controller order) - Only 384x224 and 128x128 screen resolution are supported (192x112 or 64x64 per player) Expect rasterization alignment rules for triangles to change slightly in order to better align for subpixel cases. The current map, disk, line, and rectangle rendering alignment is final. Sprite alignment is final but has a known 1/2 pixel bug that only manifests under rotation and scaling. I will add separate functions for watertight triangle and line rasterization, which give different results than what is typically useful for individual primitives. The `make_spline()` function using `"continue"` mode will change to be a third-order curve outside of the bounds instead of a line. The package manager is not implemented and may actually be removed instead of being implemented, depending on the needs that arise in the beta. You can load relative URLs for scripts and assets right now. So, the need for packages isn't urgent as you can reuse across projects already. What packages will provide are namespaces as well as assets bundled together with a script. manual✜