Skip to content

Releases: PurpleKingdomGames/indigo

v0.17.0

30 Apr 19:09
Compare
Choose a tag to compare

This release brings Indigo up to Scala 3.4.1 and Scala.js to 1.16.0. You will need to upgrade your projects to at least these version numbers.

As always, a big thank you to all those who contributed to this release in any capacity.

Give it a try, and please report any issues!

Notable changes

Many of these changes represent breaking changes to Indigo's APIs.

Physics iterative solver

Indigo's physics solution continues to be crude and naive, but now includes a simple iterative solver. This provides more accurate results by solving for up to a configurable maximum number of attempts per frame, or until a stable solution is found.

SubSystems have read-only access to the game model

Previously, sub-systems where kept strictly separate from the main game. They were mini-games unto themselves, and could only communicate via events. This worked well, but could be a little cumbersome.

The SubSystem definition now includes a reference method, that you must implement:

type ReferenceData

def reference(model: Model): ReferenceData = ???

This allows you to extract information from your game's model that the sub-system would like access to. Access is provided in the SubSystemFrameContext under a new reference field.

This means that you no longer need to tediously pass model data to sub-systems via events, should the sub-system's logic require state information from the game in order to do it's job.

Layer improvements

Prior to this release, a SceneUpdateFragment held a Batch (a list) of layers, and those layers could be given keys (identifiers) to help you manage the order - particularly when merging scene fragments.

That approach has served us well until now ...but what if you want to dynamically create N layers and have them positioned in a known place in the tree, without interfering with something else? You could try and wrangle a pool of keys, but a much simpler solution is to allow layers to be nested.

Solving that problem creates another, because keys suddenly become difficult to manage. The solution is to move the keys out of the layer, like this:

Original:

Layer(key, ...)

Now:

LayerEntry(key, Layer(...))

Which for convenience, you can construct like this:

SceneUpdateFragment(
  key1 -> layer1,
  key2 -> layer2
)

So far, all we've done is rearrange things a bit, but what about the nesting? Layer is now an ADT formed of Layer.Content which is the equivalent of the old Layer format (all the old constructors assume a content layer, to make migration/upgrading easier), and Layer.Stack, allowing you to create a tree of layers and assign it to a single key. Stacks are flattened during the rendering process, so they are a purely organisational device.

Finally, Layers (that is, content layers: Layer.Content) have gotten a little cleverer, and can now hold CloneBlanks - which could previously only be done by SceneUpdateFragments. This makes sense, as layers are all about organising elements to be rendered, and some of them will need clone blanks.

Plugin: Font generator

Another exciting improvement (prompted by @mprevel): The plugin generators have a new addition! You can now generate font sheets and FontInfo instances from font files! This makes it much, much easier to work with font's and Text instances. No doubt, there is further room for improvement, but it's a great step forward.

Example:

    val options: FontOptions =
      FontOptions("my font", 16, CharSet.Alphanumeric)
        .withColor(RGB.Green)
        .withMaxCharactersPerLine(16)
        .noAntiAliasing

    val files =
      IndigoGenerators("com.example.test")
        .embedFont("MyFont", sourceFontTTF, options, targetDir / Generators.OutputDirName / "images")
        .toSourcePaths(targetDir)

Generates this font sheet:

image

And this FontInfo code:

package com.example.test

import indigo.*

// DO NOT EDIT: Generated by Indigo.
object MyFont {

  val fontKey: FontKey = FontKey("my font")

  val fontInfo: FontInfo =
    FontInfo(
      fontKey,
      151,
      68,
      FontChar(" ", 129, 51, 9, 17)
    ).isCaseSensitive
      .addChars(
        Batch(
          FontChar("a", 0, 0, 9, 17),
          FontChar("b", 9, 0, 9, 17),
          FontChar("c", 18, 0, 9, 17),
          FontChar("d", 27, 0, 9, 17),
          FontChar("e", 36, 0, 9, 17),
          FontChar("f", 45, 0, 9, 17),
          FontChar("g", 54, 0, 9, 17),
          ...
          FontChar("9", 120, 51, 9, 17),
          FontChar(" ", 129, 51, 9, 17)
        )
      )

}

Custom HTML templates

Finally, a long requested improvement (ok, mostly by @hobnob 😄): You can now provide your own HTML templates for your games, rather than being stuck with the defaults we generate. This mechanism isn't clever, we assume you know what you're doing!

To use this feature, you can modify your build's IndigoOptions as follows:

Default

  // Set up your game options with the default template, meaning Indigo will generate the template as normal.
  val indigoOptions =
    IndigoOptions.defaults
      .withAssets(indigoAssets)
      .useDefaultTemplate // You can set this explicitly, but it is the default. 

Custom

  // Set up your game options with a custom template
  val indigoOptions =
    IndigoOptions.defaults
      .withAssets(indigoAssets)
      .useCustomTemplate(
        IndigoTemplate.Inputs(os.pwd / "test-custom-template"), // A directory containing you template files
        IndigoTemplate.Outputs(
          os.rel / "custom-assets-directory", // The name of the assets directory in your output directory
          os.rel / "custom-scripts-directory" // The name of the assets directory in your output directory
        )
      )

Other Improvements

  • *.eot added to list of supported font types
  • Camera.topLeft can take a Size param
  • New options to get a Camera's bounds/frustum
  • SceneEvent First and Last events added
  • SceneEvent LoopNext and LoopPrevious events added

Additional bug fixes

  • ConfigGen takes mixed case hex values (#688)
  • SceneFinder: Jump to index is clamped

Further generated notes:

What's Changed

Full Changelog: v0.16.0...v0.17.0

0.16.0

07 Jan 22:04
Compare
Choose a tag to compare

Great to have a couple of contributors on this release, thank you for your help @JPonte and @mprevel, much appreciated!

As usual, this release has been driven by using Indigo, finding problems, and fixing them.

Noteworthy changes

Scala.js 1.15.0

All newly release Purple Kingdom Games libs are now on Scala.js 1.15.0, and Indigo is no exception.

Electron Executable option is now working properly

This was a tiny fix that is worth it's weight in gold. By default, when you use the indigoRun command to run your game via Electron, your build will install the latest version via NPM and then run. This works ok, and gives you the latest and greatest Electron version with almost no effort on your part.

The only problem is that NPM will try to check the install is up to date on every single run, slowing down development.

What you can now do instead, is install Electron by some other means, e.g. yarn add --dev electron, and then add the following line to your build's IndigoOptions:

.useElectronExecutable("npx electron")

This does the local install manually just once, and then just reuses it with no installation checks, resulting in much faster game start times.

Polygon shader fixed!

During the original port to Ultraviolet, a mistake was made in the polygon shader translation from GLSL to Scala 3. This has now been corrected by @JPonte.

Tyrian / Indigo bridge

The Tyrian / Indigo bridge is a module that allows Tyrian web apps to communicate with embedded Indigo games.

The release of Tyrian 0.9.0 removed the tyrian/indigo bridge module in order the correct the library dependency order.

This module is now published with Indigo, and so follows Indigo version scheme, i.e. it has jumped from 0.8.0 to 0.16.0.

Improved PathFinder

This is the first release of a new A* PathFinder by @mprevel. It is both more flexible and more correct than previous attempts!

Please note that the old, suspect SearchGrid implementation has now been marked as deprecated, and will likely be removed soon.

Improved Physics Scene Scaling

Indigo includes a rudimentary physics engine that is squarely aimed at pixel art games and nothing more. It uses discrete rather than continuous simulation, and so suffers from issues like tunnelling (to be addressed in future updates), but nonetheless is good fun and quite helpful.

Previous versions did not scale well as the simulation had no 'broad phase' to cull unnecessary collision comparisons. This has now been fixed, allowing for much larger simulations.

Adding this new phase has required some changes to the API:

  1. Worlds are now bounded, i.e. you will need to provide a BoundingBox covering the simulation area.
  2. You must also supply a SimulationSettings instance as part of the world set up, that gives clues about the size and density of the simulation.

Physics: Transient colliders are static

Another change is that transient colliders (colliders that only exist in this frame and are not officially part of the world instance, typically user controller) are now treated as static in all cases, and you no longer need to set them as such.

Physics: Collider Terminal Velocity

You can now set a terminal velocity on your colliders. This gives you much better control over how your game's play, and helps reduce the possibility of tunnelling.

Bresenham's line algorithm

Bresenham's line algorithm is a useful tool for working out a line across a grid with no overlapping squares. Super handy for drawing pixel art lines or lines of sight across a grid.

There is now an implementation of the algorithm in the Indigo extras library, that was ported from the Roguelike-Starterkit:

indigoextras.utils.Bresenham.line(from, to)

QuadTree overhaul

QuadTree's are data structures used to store spatial data so that it can be quickly queried. QuadTree's are now the underlying data structure used to allow the physics engine to scale.

There has been a "QuadTree" implementation bundled with Indigo in the extras package for many years now. It was based on work done in Indigo's core that handles the texture atlases. Unfortunately, borne out of that very specific use case, it wasn't a true QuadTree, and could only handle data associated with vertices, and then only one entry per leaf, leading to some interesting design choices. What it ended up being used for, in a number of places, was actually not a query-able tree at all, but a sort of sparse data look up for grids.

The QuadTree implementation has now been totally re-worked to meet the physics use case, and can now support data stored against all of the standard spatial types, i.e. Point, Rectangle, Circle, BoundingBox, BoundingCircle, LineSegment, and Vertex, or any custom type with a SpatialOps instance. The implementation is also much more robust, and can store many entries at the leaf level. How many entries and how deep the tree should go is specified by you.

All this change has resulted in a substantial API rethink and the behaviour is now significantly different. QuadTree's are also now part of the main Indigo library, not the extras package.

SparseGrid

The use case that the old QuadTree's was fulfilling as a sparse data grid was a real need, and since QuadTree's can no longer meet that requirement, a new data type has been made, cunningly named: SparseGrid!

SparseGrid is a straight-up grid data structure, that provides a nice API for managing the data in that grid.

Minor bug fixes and improvements

  • BoundingBox.resizeBy now takes Doubles instead of Ints
  • Fixed #658: Large box overlap of small circle check
  • Vector2/3/4 all now have a clamp operation that takes the same type, i.e. you can clamp a Vector2 with another Vector2.

Generated notes below

What's Changed

New Contributors

Full Changelog: v0.15.2...v0.16.0

0.15.2

26 Nov 21:27
Compare
Choose a tag to compare

Many small fixes...

Bug fixes

  • lastOption now works correctly on Batch whether the underlying structure was a Batch.Combine.
  • Support multiple newlines in Text (and therefore InputField) instances.
  • Plugin: Use of period (.) in a cell does not throw a number exception.
  • Plugin: Negative Ints and Doubles are correctly recognised.
  • Plugin: Generators can handle trailing quotes.
  • Camera.LookAt behaves correctly with different levels / combinations or global + layer magnification.
  • Camera.LookAt does no produce gaps between CloneTiles due to rounding errors.

General Improvements

  • Text can now have it's letter space and line height set.
  • Rectangle.toCircle deprecated - Disambiguation of whether the circle is inside or outside the rectangle. Use toIncircle or toCircumcircle instead.
  • Rectangle and BoundingBox now have resizeBy methods for relative sizing.
  • Canvas name is now simply the parent ID plus a -canvas suffix, to make CSS work easier by removing the square brackets.
  • Friendly mouse event extractors - the mouse event extractors now only extracts the common case of the position field (e.g. case MouseEvent.onClick(position) => ???) instead of all 8-10 fields, and all other properties need to be accessed from a reference to the event.
  • LineSegment from and to aliases are provided for start and end.
  • SceneUpdateFragment now has a withLayers method to replace the existing layers with another set.
  • SceneUpdateFragment now has a mapLayers method, for example if you want to set the magnification on all layers.
  • Added an alias to QuickCache under mutable.QuickCache. QuickCache is a handy type if you need to avoid recalculating values, but it's been hidden under the mutable package as a warning to tread carefully.
  • Batch: Added last operation.
  • Batch: Added distint and distinctBy operations.
  • Batch: Added padTo operation.

Plugin Improvements

  • IndigoOptions now supports antialiasing flag, defaults to false.
  • Generators: embedText now supports a present function simply of type String => String that allows you to transform any arbitrary file contents into Scala code, to be included as a source file in your game.
  • Generators: Trailing delimiters are supported.
  • Generators: Empty cells are seen as null values (the word null is assumed to be a string!).
  • Generators: Columns containing null values are typed as optional.

Full Changelog: v0.15.1...v0.15.2

0.15.1

16 Oct 23:20
Compare
Choose a tag to compare

Bug fix release

Indigo 0.15.0 included the biggest changes to the Indigo plugins, more or less since they were created, and with that... a couple of bugs, too.

Issue with fullLinkJS in sbt

At the point when Scala.js switched naming from fullOpt to fullLinkJS and fastOptJS to fastLinkJS, the output files and paths were changed, and work was done to support both the old and new arrangements in sbt and Mill. Unfortunately an issue has crept in with fullLinkJS's output file not be found correctly. This has now been resolved.

Mill Indigo Plugin Generators were inadequate...

Before any release, Indigo goes through quite a lot of testing, but not enough on this occasion. After the release of 0.15.0, two issues quickly became apparent with how the new generators worked with Mill, specifically.

  1. mill clean <module> was not removing generated classes. This wasn't noticed because during plugin development, a lot of "hard cleaning" (i.e. removing the out directory) happens to be sure of what has been loaded, and in that scenario, mill clean has no work to do.
  2. Working with generators on multi-module builds was terrible, because unlike the sbt version, all the generated files were being written to a common shared output folder, instead of a folder that was part of each module's build.

The fix for both was to make generators adhere to Mill's T.dest, which is obvious in hindsight... oh well...

Reformulating the plugin's a little to make this work properly now requires the following changes (taken from real examples):

Mill

Before:

val indigoGenerators: IndigoGenerators =
  IndigoGenerators
    .mill("snake.generated")
    .listAssets("Assets", indigoOptions.assets)
    .generateConfig("SnakeConfig", indigoOptions)

After:

val indigoGenerators: IndigoGenerators =
  IndigoGenerators("snake.generated")
    .listAssets("Assets", indigoOptions.assets)
    .generateConfig("SnakeConfig", indigoOptions)

sbt

Before:

IndigoGenerators
  .sbt((Compile / sourceManaged).value, "pirate.generated")
  .listAssets("Assets", pirateOptions.assets)
  .generateConfig("Config", pirateOptions)
  .embedAseprite("CaptainAnim", baseDirectory.value / "assets" / "captain" / "Captain Clown Nose Data.json")
  .toSourceFiles
  .toSet

After:

IndigoGenerators("pirate.generated")
  .listAssets("Assets", pirateOptions.assets)
  .generateConfig("Config", pirateOptions)
  .embedAseprite("CaptainAnim", baseDirectory.value / "assets" / "captain" / "Captain Clown Nose Data.json")
  .toSourceFiles((Compile / sourceManaged).value)
  .toSet

0.15.0

25 Sep 19:24
Compare
Choose a tag to compare

Indigo 0.15.0

There are quite a few changes an improvements in this release, the two big ones to note (as they are breaking changes) are:

  1. New game resizing behaviour
  2. Indigo plugin overhaul.

Read on for more details.

Warning! New game resizing behaviour!

An excellent piece of work has been done that allows you to set the resizing behaviour of your game in your game config, and which has enabled us to deprecate the old (and rather suspect) debounce script.

You can now set resizing your game's resizing policy, like this:

// Resizes to fill the available space
GameConfig.default.withResizePolicy(ResizePolicy.Resize)

// Stays at a fixed size
GameConfig.default.withResizePolicy(ResizePolicy.NoResize)

// Resizes to fill the available space, but retains the original aspect ratio
GameConfig.default.withResizePolicy(ResizePolicy.ResizePreserveAspect)

When building or running an Indigo game via the plugin, the new behaviour will be dealt with automatically.

However, if you have an existing web-page, or you are embedding your game manually, then you'll need to do some additional set up to avoid the game trying to aggressively fill an ever expanding screen!

There are two ways to handle the new behaviour:

  1. Change your GameConfig by calling the .noResize method, which is the same as .withResizePolicy(ResizePolicy.Resize).
  2. Copy (and adjust) the following CSS (included by default by the plugin):
#indigo-container {
  display: flex;
  align-items: center;
  justify-content: center;
  padding:0px;
  margin:0px;
  width: 100vw;
  height: 100vh;
}

New Features

Focus events

New events have been added to let you know when the application or canvas has gained or lost focus.

  • ApplicationGainedFocus - The application is in focus
  • ApplicationLostFocus - The application has lost focus
  • CanvasGainedFocus - The game canvas is in focus
  • CanvasLostFocus - The game canvas has lost focus

Online / Offline network events

New events have been added to indicate whether your game is on/offline. Beware that this is not a complete test, since your machine will be considered online, if your local network is available.

  • Online
  • Offline

Mouse and Pointer events now have a lot more detail

The main example of this is MouseEvent.Click which used to take a simple argument "position", but now has all of the following, inherited from the browser.:

MouseEvent.Click(position, buttons, isAltKeyDown, isCtrlKeyDown, isMetaKeyDown, isShiftKeyDown, movementPosition, button)

Indigo plugin overhaul

This release includes an unusually large amount of work on Indigo's Mill / sbt plugins. The motivation behind the work stems from the question: "Does indigo need an editor?"

Indigo is a code-first engine for people who like making games by writing code. However, writing code alone can be inconvenient, boring, and slow when there is a lot of manual wiring or repeat tasks to perform, and this is where a game editor typically comes in. Your game is ultimately code, but you get a load of tools to speed up the dev process.

Is it possible to make the plugin do more? To reduce the delta between having and not having a full editor by providing more tools in the plugin?

One thing that the Scala community has become somewhat allergic to is a framework. Frameworks however, provide convenience and speed at the cost of having to conform to how they work ...but that sort of sounds like what we want.

Indigo can now be thought of as a library that can optionally be upgraded to a framework:

  • Indigo itself is (pretty much) a library - you can combine it with any other web targeted Scala.js project or framework.
  • The Indigo plugin - Now adds more functionality and takes Indigo into framework territory for game building speed and convenience.

There is a lot more to do in this space, but this release contains two specific changes:

  1. IndigoOptions - A simple builder object that replaces all the old sbt / mill settings with a standard interface and expanded options. (When you upgrade an existing project to Indigo 0.15.0, you'll likely get a build error here.)
  2. IndigoGenerators - A set of code generators that help perform/automate a variety of tasks.

Indigo Options

The experience of setting up an Indigo game in Mill and sbt has now been unified with the new IndigoOptions type, which provides a fluent API for IDE support, examples below:

Mill

The MillIndigo trait requires the presence of an indigoOptions instance.

  val indigoOptions: IndigoOptions =
    IndigoOptions.defaults
      .withTitle("My Game")
      .withWindowWidth(720)
      .withWindowHeight(516)
      .withBackgroundColor("black") // defaults to 'white'
      .excludeAssets {
        case p if p.startsWith(os.RelPath("data")) => true
        case _                                     => false
      }

sbt

It is recommended that you do NOT inline your game options, because you will probably want to reference them in you generators later.

import indigoplugin.IndigoOptions

// In sbt, you must use a lazy val.
lazy val myGameOptions: IndigoOptions =
  IndigoOptions.defaults
    .withTitle("My Game")
    .withWindowSize(1280, 720)
    .withBackgroundColor("#21293f") // You can use valid CSS colors, like 'black', hex values, or rgb() / rgba()
    .excludeAssetPaths {
      case p if p.endsWith(".json")     => true
      case p if p.startsWith("shaders") => true
    }

lazy val mygame =
  project
    .enablePlugins(ScalaJSPlugin, SbtIndigo)
    .settings(
      name := "mygame",
      indigoOptions := myGameOptions
    )

Asset paths and filtering

For the most part, the new IndigoOptions type is simply a re-organisation of the old settings, however, assets paths have changed.

You can set the path to your asset directory using withAssetDirectory, as follows:

IndigoOptions.defaults
  .withAssetDirectoy(os.RelPath.rel / "assets")

The argument is a relative path and can be expressed with a String, os-lib / Mill RelPath (recommended, even in sbt), or File. The path is now relative to the project directory, not the module.

You can also use (as above) excludeAssets or includeAssets to filter your assets (these methods use os.Paths, if you'd prefer a String path, use excludeAssetPaths and includeAssetPaths instead). For example you could exclude a whole directory of in-development assets / files, but include one specific file from that folder.

The rules are, in order:

  1. If there is a specific include rule that covers the file, include it.
  2. If there is a specific exclude rule that covers the file, exclude it.
  3. Otherwise, include it.

This behaviour is important when used in conjunction with the asset listing generator (see below).

Indigo Generators

The purpose of these generators is to automate some of the less glamorous work, or fiddly tasks. Much of this work was originally prototyped and validated in the Roguelike demo.

The generators are once again based on a fluent API that you can explore with the assistance of your IDE, however here are simple Mill / sbt examples and a list of the generators available:

Mill

  val indigoGenerators: IndigoGenerators =
    IndigoGenerators
      .mill("com.mygame") // Full qualified package for generated code to live under.
      .listAssets("Assets", indigoOptions.assets) // Generate asset information
      .generateConfig("MyConfig", indigoOptions) // Generate Config sync'd to build settings.

sbt

Please refer to sbt documentation for ways to tell sbt how to cache these values, if desired.

Compile / sourceGenerators += Def.task {
    IndigoGenerators
      .sbt((Compile / sourceManaged).value, "com.mygame")
      .listAssets("Assets", pirateOptions.assets)
      .generateConfig("Config", myGameOptions)
      .toSourceFiles
}

Generator list

There are variations of most of the generators to allow you to supply paths as either String, File, or os.Path/os.RelPath (as appropriate).

Wiring generators:

  • listAssets - takes in your IndigoOptions assets configuration and generates a module that contains details of all of your assets, respecting asset filter rules, ready to be loaded in your game. Names that would generate duplicates / collisions will be reported (and will also fail compilation).
  • generateConfig - takes in you IndigoOptions settings and generates a game config instance that conforms to GameConfig.default with your build details embedded, namely the background colour and viewport size.

Data embedding helpers:

  • embedText - Read any text file and embeds it into code.
  • embedGLSLShaders - Reads a pair of GLSL shader files (vertex and fragment), and embeds them as an Indigo shader, optionally validates the code (requires local install of glslValidator).
  • embedAseprite - Reads an Aseprite json export (array type), and converts it directly into an Aseprite instance and adds it to your source code. This means you do not need to load the separate JSON at runtime. Handy for loading screens.

Data generators:

Data generators generate data! The intended use case here is for when you have a table of data that you'd like to edit conveniently, say, a table of item stats in a spreadsheet that you export to CSV, and then put into your game. Clearly you could load these as a text file, but that means (at runtime) loading the data, parsing it, validating it, and all look ups being subject to things like null checks. The data generators by pass all of that by embedding your data as compile time values...

Read more

0.15.0-RC3

16 Jul 17:37
Compare
Choose a tag to compare

Release Candidate 3

This release is an unexpected addition to the previous RC versions. All the core functionality can be considered stable. A full release version will be cut as soon as the documentation site is overhauled (in progress).

Hope you like it, and please report any bugs and issues you come across!

Details of Changes

Axis-Aligned Physics Support

Indigo now includes a basic physics engine. The engine is what is known as a rigid body axis-aligned physics engine. This is as simple as physics gets! Axis aligned means that there is no support for rotation, but for the kinds of game Indigo is intended for, this is still good enough for a wide range of use cases.

(The framerate in this gif isn't great, but the simulation runs smoothly at 60 fps.)

indigo-physics-5

Pong!

There is a playable game of Pong! (Single player) made with Indigo's new physics capabilities here:

The Cursed Pirate

There will also be a new version of the cursed pirate demo where all the hand rolled collision work has been replaced by the physics engine.

pirate_bounds_2

Limitations and issues, bullets and paper walls

This is a limited Physics model, and it will have problems both in terms of physical accuracy (even within the scope of what we model) and in terms of initial feature set. You are welcome and encouraged to help by playing with it, reporting issues, and helping with code / maths / physics if you can.

One example of a known limitation of the current model, is that it suffers from what is sometimes called the 'bullet through paper' problem.

Simply put: If a projectile is moving fast enough and an object in it's flight path is small / thin enough, then on frame A the projectile's calculated position is on one side of the object it should collide with, and on the next frame B it's calculated position is on the other side of the object in it's way. The result is that the two objects never collide in the engine and carry on unobstructed, even though they clearly would collide in the real world.

You can see this problem for yourself if you go and play the pong demo until the ball is going really, really fast! (It doesn't take long provided you can keep up.)

The question is: Is this problem... a problem? Well, probably not for the most part. It's all about relative scale and speed. Of course work will be done to try and fix it, but for the kinds of games Indigo is designed for, the expectation is that this probably won't come up too often, and can be coped with by the game developer with a little defensive coding. Time will tell if that turns out to be the case or not!

There are no docs! How do I use it? (Doc's coming soon-ish...)

All of the physics primitives are available with the following import:

import indigo.physics.*

As always, a lot of work has gone into the API to let the types and IDE discoverable methods guide you, however, here are a few pointers to get you started.

Everything starts with a world, that you keep in your Model:

World.empty[String]

Notice the String type there? That is the type of your collider 'tags', and can be any type you like, an enum is a good idea. The tags are used to help you access and manipulate the scene. For example the world has methods like findByTag.

Initially, your world is a lot like outer space. No general forces or resistances. Let's add some gravity and wind resistance:

World.empty[String]
  .addForces(Vector2(0, 1000))
  .withResistance(Resistance(0.01))

What are these magic numbers? Well here you need to use some discretion. The physics engine does not understand the scale of your game - the scale can be anything! In this case we're adding a force that acts on all elements at a rate of 1000 'units' per second, meaning, at full speed an item will be moving 1000 'thingys' downwards. How do we decide what the units are/mean? Well in this case we have a screen that is 600 pixels high, so we're saying that terminal velocity (maximum speed by falling) allows you to cover just under 2 times the height of the screen in 1 second. Again, this scale is arbitrary, you will need to think about what makes sense for your game.

Now we need a bouncy ball and a platform for it to land on. Our screen is 800x600 in size, so positions are based on that:

World.empty[String]
  .addForces(Vector2(0, 1000))
  .withResistance(Resistance(0.01))
  .withColliders(
    Collider("ball", BoundingCircle(400.0, 80.0, 25.0)).withRestitution(Restitution(0.8)),
    Collider("platform", BoundingBox(10.0d, 550.0, 780.0, 20.0)).makeStatic
  )

Physics engines use things called 'colliders' to represent physical things in the world that can ...collide! Colliders can be Collider.Circle or Collider.Box types. We have two colliders:

  1. The first collider is our ball, it's positioned near the top of the screen, centered horizontally. The ball has been given a 'restitution' value of 0.8. Restitution is how much energy is retained when it bounces, so 0 would be no bounce at all, and 1 would be a perfect elastic bounce.
  2. The second is our platform, notice that the platform has been made 'static'. This has a few effects internally, but all you need to know right now is that it won't move no matter what happens.

This simulation is entirely self contained, but you can interact with it using the various world 'find', 'modify' and 'remove' methods. You can also add transient colliders, which are colliders that don't exist in the world but are added deliberately on update / presentation for some reason. A good example of this is the paddles in the Pong demo, which are added every frame because they are directly controlled by the player (Tip: Make things like this static!). They need to be in the world for the ball to interact with, but otherwise aren't an on going part of the simulation.

Ok, so we have a simulation set up, how do we use it?

  1. First, we place it in our model during the initialModel call.
  2. On each FrameTick of the updateModel function, we need to update it using: world.update(context.delta) (delta is the time delta, or the time that has elapsed since the last frame. All calculations are frame rate independent.)
  3. We need to decide how to present our world...

Presentation comes in a few flavours, which you can mix and match to suit your needs. You can:

  1. Call present on the world and render a Batch[SceneNode] based on what is in the simulation. Present can also do a partial presentation based on the filter arguments.
  2. Look up the colliders or map over them yourself and render the bits you care about. Perhaps your simulation is abstract and you just need information rather than it being a direct 1-1 collider to renderable relationship.
  3. Render things in your model directly, that only affect the sim as transient colliders (like the paddles in Pong).

In our case, let's just render the world (as in the gif at the top) and see what's going on*:

def present(world: World[String]): Outcome[SceneUpdateFragment] =
  Outcome(
    SceneUpdateFragment(
      world.present {
        case Collider.Circle(_, bounds, _, _, _, _, _, _, _) =>
          Shape.Circle(
            bounds.position.toPoint,
            bounds.radius.toInt,
            Fill.Color(RGBA.White.withAlpha(0.2)),
            Stroke(1, RGBA.White)
          )

        case Collider.Box(_, bounds, _, _, _, _, _, _, _) =>
          Shape.Box(
            bounds.toRectangle,
            Fill.Color(RGBA.White.withAlpha(0.2)),
            Stroke(1, RGBA.White)
          )
      }
    )
  )

(* Rendering colliders on top of your game can also be handy for visual debugging.)

Wrapping up

...and that's about it for now. There are other options to explore, for example you can emit events on collision, but hopefully there's enough information here to get you started.

Explore the features. Raise issues and bugs. See what you can make, and don't forget to show us your creations on Discord!

Other minor changes

  • Construct Shape.Circle with a Circle
  • Batch.sorted added

Bug fixes

  • BoundingBox lineIntersect doesn't fail at 0,0

What's Changed

**Full Change...

Read more

0.15.0-RC2

04 Jun 21:10
Compare
Choose a tag to compare

Release Candidate 2

Since the previous release candidate, we've been putting Indigo through its paces and all seems stable and ok.

However we're not quite ready for a full release, a few improvements have been added and there may be others in the works. Nonetheless, you can use the RC versions with a high degree of confidence.

Hope you like it, and please report any bugs and issues you come across!

Details of Changes

Scala 3.3.0

Since the last RC version, Scala 3.3.0 has been released, and Indigo 0.15.0-RC2 is now up to date with that, as well as the freshly minted Ultraviolet 0.1.2 (which has also been updated).

Note: You will need to upgrade your Scala version.

For the most part, everything works exactly as it always did, the main motivation was just to get onto the LTS Scala 3 version.

The only slight wrinkle is a change in how Ultraviolet uses external inline functions, which is detailed here:

https://github.com/PurpleKingdomGames/ultraviolet#using-ultraviolet-with-scala-330

Geometry Changes & Improvements

Most the other changes in this release are to do with the various geometry primitives.

New Circle datatype

A new Circle type has been added to complement Rectangle. This was proposed by someone who was trying to re-create the classic game 'Pong' and needed to represent a ball. How this has been overlooked until now is anyone's guess.

All under one roof

The geometry package has been promoted out of the indigo-extras library into indigo proper. You're getting geometry primitives whether you like it or not!

Where previously you needed to import types like BoundingBox from indigoextras.geometry, they are now first class citizens under import indigo.*.

Better interop with other data types

Before the geometry package was promoted, the geom types had a difficult relationship with the main data types. For example, you could say Vertex(1, 0).toPoint, but because of the dependency relationship, you could not say Point(1, 0).toVertex.

These sorts of minor irritations have now been fixed.

BoundingCircle (and Circle) improvements

BoundingCircle is relatively new and hasn't had the same amount of love as BoundingBox, but improvements have now been made to bring it up to scratch:

  1. More resizing options, including expand and contract.
  2. Conversation methods for interop with other types.
  3. Ability to check if a BoundingBox overlaps a BoundingCircle.
  4. Create a BoundingCircle from three vertices where all three lie on the circles circumference.
  5. Added a new 'approximately equals' operator: ~==.

Other minor changes

  • Radians now has a toDegrees method
  • Vertex now has simple arithmatic operators that work with Vector2
  • Shape.Circle uses Circle under the hood.

What's Changed

Full Changelog: v0.15.0-RC1...v0.15.0-RC2

0.15.0-RC1

15 Apr 21:37
Compare
Choose a tag to compare

Release Candidate

This release is expected to become a full release of Indigo 0.15.0, as long as no serious issues are discovered in the next few months.

It's been over six months since the last release of Indigo, but that is a reflection of the magnitude of the changes the engine has undergone. The aim of all this work is eventually to break Indigo away from being reliant on Scala.js and WebGL, so that it can move onto new platforms like WebGPU, and one day Scala Native / JVM. There is much more work to do, however.

If you're excited about that work, please consider contributing to or sponsoring the project.

New / Noteworthy stuff!

Ultraviolet Support

This is the bulk of the change in this release. Indigo no longer has any GLSL code in it, all of its shaders have been rewritten in Scala 3 using Ultraviolet.

Ultraviolet is an early and slightly rough around the edges DSL + macro that converts Scala 3 into shader code, and at the moment that code is GLSL flavoured. However in the future, the aim is to expand the output formats to support other shader dialects allowing Indigo shaders to work on other platforms. There is more work to do here, but it really does work!

From an end user perspective, if you aren't writing custom shaders then you shouldn't notice any difference. If you have written custom GLSL shaders, these will still work, you just need to be aware of one change, which is that two of the standard function hooks must now take an argument and return a value.

These functions:

void vertex() {}
void fragment() {}

Must now be:

vec4 vertex(vec4 v){ // where v is the vertex as calculated so far...
  return v;
}
vec4 fragment(vec4 c){ // where c is the colour of the fragment as calculated so far...
  return c;
}

The other standard hooks of composite, prepare, and light are unchanged.

You can use Ultraviolet to write shaders for Indigo in two ways:

  1. As generated source code in a Source shader, which just takes a string.
  2. Using the new UltravioletShader shader type.

Full documentation will be provided at some point in the near future (hopefully), but at the moment the Indigo website is undergoing a rebuild. In the meantime, please refer to the Ultraviolet repo, examples that are being produced, and come and ask questions on our Discord server.

Pointer support

Huge thanks to @grzegorz-bielski for adding support for pointer events, allowing Indigo games to response to touch inputs on things like phones and tablets. This is also an overhaul of mouse input events. The work is a combination of a new set of Pointer events, and Pointer state in the InputState object in the FrameContext.

Frame Rate Limiter

The framerate limiter has never worked. At various times it was thought to work but didn't. In a nutshell this is because the browser chooses when a frame will be rendered, and any attempt to massage or throttle that time is an uphill battle.

However! The frame rate limiter (err... once again?) now works! And this is very important, so much so that a previous change to disable it by default has been reverted and new games are limited to 60fps by default.

What is behind this move?

  1. Improved electron performance.
  2. VSync.

If you're using Electron, then in an Indigo game's build you can tell Electron to disable it's frame rate limiter. In older version of Electron this does not present a problem, but in newer versions the performance has improved significantly, and you can now get frame rates in excess of 1000 fps! (On a simple game doing very little, with up to date graphics drivers, and so on and so on...)

Unfortunately at these high frame rates strange things begin to happen, so as a recommended default, it's been capped to 60 fps. You can still turn it off if you so wish.

The other reason is vsync. VSync is when your graphics card updates your game at the same rate as your monitor. For a pixel art game of the kind Indigo was designed for, 60 fps is ...lets just say it's ample, but if your game is running in a browser and the monitor has a 144 refresh rate, then your game will try and run at 144 fps! More than double our recommended cap of 60. Bear in mind that this doesn't just update your rendering, but also all your model processing and so on, which is needless work.

So again, the default has now been capped at a sensible 60 fps.

You can modify the framerate limit or disable it as you wish via your GameConfig. Existing games are recommended to enable it at some specific frame rate rather than just leaving it to peoples machines to decide.

Improvements!

General Shader Improvements

  1. Custom/Blend UBO offsets have been reviewed and we've been able to make more room for custom data UBO's, meaning you can now send more data to your shaders.
  2. The binding of the VERTEX variable now happens in a different place, so that in general you need only manipulate UV's in vertex shaders without needing to worry about how that translates onto texture atlases. (There is more work to do here to make this stuff easier...)
  3. The ShaderData APIs such as UniformBlockName and the various data types have seen a range of improvements, and there are more helpful conversion extension methods now under the indigo.syntax.* and indigo.syntax.shaders.* imports.

BlankEntity

A new super simple BlankEntity primitive has been added. This is for all you shader writers out there. BlankEntity is a way to describe a space on the screen with nothing in it, that you can then draw to using a shader.

This was made for the IndigoShader game type described below, but in fact has more general purpose uses.

IndigoShader game type

Along side the existing IndigoSandbox, IndigoDemo, and IndigoGame game types, we now have IndigoShader. Again this is aimed at people who just want to practice writing shaders for fun. You can see a few examples of it in use in this repo.

Essentially this is a new game type with a pared back API that only renders a single shader that fills the window, somewhat like the brilliant ShaderToy.

Other bug fixes and improvements

Here are some of the general improvements in this release. These have been made to smooth out development and were generally discovered while working on a new demo.

  1. The confusing sceneTime has been replaced with sceneStartTime and sceneRunning.
  2. AudioPlayer now throws when an asset is missing, to avoid confusion as to why your audio isn't playing.
  3. Transparent backgrounds really are transparent now!
  4. Added withSize and withPosition to Rectangle.
  5. Improved and expanded local storage events.
  6. Add Dice.rollRange helper
  7. Indigo's sbt and Mill plugins now support fast/fullLinkJS
  8. Added Outcome.fromOption and syntax improvements
  9. You can now multiply Point / Size / Rectangle by Doubles
  10. You can construct SceneUpdateFragments from Batch/Option of Layers
  11. Bug fix: Volume of a playing track can now be changed! 🤦
  12. Improved asset loading
  13. Ability to query timeline animations for their length
  14. Added Timeline.atOrElse, timelines return an optional value, and this helper allows you to quickly ensure you always get something back.
  15. Added Timeline.atOrLast, when a timeline ends, instead of vanishing, the last frame continues to be rendered.

What's Changed

New Contributors

Full Changelog: v0.14.0...v0.15.0-RC1

0.14.0

25 Sep 13:00
Compare
Choose a tag to compare

Notable changes:

New Build Target Versions:

  • Scala.js 1.11.0
  • Scala 3.2.0
  • Mill 0.10.7

Timeline animations!

This is a brand new and frankly very exciting feature! You can now describe timeline-style animations which can happen consecutively and concurrently (just like you can in animation tools). Here is an example of a timeline that animates two sequential stages of movement for a graphic, and also affects it's appearance throughout the whole animation via the modifier:

  val tl: Seconds => Timeline[Graphic[_]] = delay =>
    timeline(
      layer(
        startAfter(delay),
        animate(5.seconds) {
          easeInOut >>> lerp(Point(0), Point(100)) >>> move(_)
        },
        animate(3.seconds) {
          easeInOut >>> lerp(Point(100), Point(100, 0)) >>> move(_)
        }
      ),
      layer(
        startAfter(delay),
        animate(8.seconds, modifier)
      )
    )

This is great for animating things like title screens, but we've also added 'scrubbing' support to the existing Clip and Sprite types so that you can control keyframe animations (such as those exported form Aseprite) using this method.

The DSL is available via import indigo.syntax.animation.*.

Timelines do not really know what they are animating, they are just a way to produce a value of a certain type over time. Thus they can return renderable nodes, Outcomes, or perhaps even model values if you get creative!

Electron installs do not re-do local module installs

This seemed like a small improvement at the time but has changed the way I work.

Global installs of Electron (particularly if you're a Nix user...) display extreme variation in terms of performance and capabilities across different platforms. However using a global install was convenient because it was nice and fast compared to doing indigoplugin.ElectronInstall.Version("^18.0.0") or indigoplugin.ElectronInstall.Latest in your sbt or Mill build. This was mostly down to npm reinstalling artefacts on every build, but this has now been fixed.

You are now advised to prefer a Version or Latest over Global.

Batch improvements and bug fixes

Batch is a cornerstone data type for Indigo that was introduced in the previous release. It was introduced for performance reasons with an expectation that the usability would need some work. Batch is still a bit rough around the edges, but it's getting better. Here are a few of the latest improvements:

  • Better tail operations + uncons
  • Better mkString
  • Catch only NonFatal errors during Batch.equals
  • Added flatten to Batch
  • Better pattern matching (still needs work!)
  • Improved constructors

Scene's now have their on context type

All the standard functions supply a FrameContext which provides a suite of useful tools and bits of information, but Scenes now have their own variation. SceneContext does everything that FrameContext does, but also provides the running sceneTime so that you can start animations from when you entered the scene, and the current scene name.

Other Improvements

  • Add advanced config parameter to disable browser context menu (#385)
  • Add ability to customize html background color
  • Replaced SceneAudio.None with Option types (#403)
  • Outcome can sequence List, Batch, and their non-empty their variations (other sequencing syntax options also added)
  • Improve the JS launch method so you can now supply an element (#407 & Fixed #400)
  • Many new SignalFunction helper methods
  • Vector2, 3, 4 and Vertex now have ceil and floor methods
  • Syntax: 1.second
  • Sprite's support ScrubTo animation action
  • SceneUpdateFragment's will accept an optional node
  • Layer's will accept an optional node
  • Clips can be exported to Graphics.
  • Clips can be fixed with toFrame
  • Clips support scrubbing
  • Clips expose frame info and clip length
  • Enabled/Disable lighting at the material level
  • Shape enhancements, modifier methods

Bug fixes

  • Fixed #390: closestPointOnLine works in all directions (Line Segment)
  • UBO hashing fixed
  • UBO data is only set if it has changed
  • Reset lastFrameAdvance after a AnimationAction.jumpTo in order to start animation in the correct place
  • Fixed #419: Signaleasing functions use SignalFunction versions
  • Fixed #420: Polygons with stroke 1 render better

What's Changed

New Contributors

Full Changelog: v0.13.0...v0.14.0

0.13.0

29 May 21:55
Compare
Choose a tag to compare

Notable changes:

New Build Target Versions:

  • Scala.js 1.10.0
  • Scala 3.1.2
  • Mill 0.10.4

New Batch type (Breaking change)

Since the time Indigo was first created, all scenes have been described as Lists of things. The great thing about using List is that it is immutable and very friendly to use when compared to something like Array.

Unfortunately, List's friendly syntax comes with some fairly punishing performance penalties. Since game loops run very frequently, it is quite easy to get yourself into trouble with Indigo, just by doing some fairly ordinary list-like things. Under the hood, Indigo runs almost entirely on js.Arrays which give us better performance, but until now we had to convert all those user defined lists into js.Arrays on every frame.

The nature of how Lists were primarily utilised by Indigo’s API’s was for construction rather than traversal. So what we need is a new type that is super fast to construct, very low friction to convert to js.Array, and is at least no worse (performance-wise) than using a list in every other regard.

For this we have a new type called Batch, and this has replaced List and other collection types almost everywhere.

Batch is very similar to Chain from Cats! The main difference is just that it’s backed by js.Array and named differently to avoid confusion. Batch is not particularly clever. It is immutable and borrows from Chain to speed up construction, while delegating to js.Array for pretty much everything else.

Batch has a lot of the usual list-like operations, and can be empty (Batch.empty) or populated as expected, e.g. Batch(1, 2, 3). To aid construction there are a number of convertors like Batch.fromList(myList), and you can also import indigo.syntax.* to allow myList.toBatch. Of course, you should avoid doing conversions like that in performance sensitive areas of code.

Option to use non-global Electron

You can now supply a flag to the plugin instructing it as to how it should source the Electron version you’d like to use. By default, this preserves the existing behaviour of assuming a global install (here in sbt syntax, but works in Mill too).

electronInstall := indigoplugin.ElectronInstall.Global

Other options include: Latest, Version("^18.0.0"), and PathToExecutable("/bin/../electron").

What's Changed

New Contributors

Full Changelog: v0.12.1...v0.13.0