github excaliburjs/Excalibur v1.0.0
Excalibur v1.0.0 Release

latest release: v1.1.0-alpha.327
one hour ago

excalibur logo

Thirteen years, nearly 4,000 commits, and over 120 contributors later: Excalibur.js is v1.0.0!

Highlights new in v1

  • 2D Lighting
  • Pause System
  • Serialization System
  • Faster Realistic Physics
  • MULTI-pass Materials & Post-processors
  • Sound: a real Web Audio mixer and sound manager
  • Plugin API
  • Excalibur CLI for doing upgrades and codemods!

Breaking Changes

We have done our best to write codemods to auto-upgrade everything https://github.com/excaliburjs/create-excalibur

  • ESM Only output

  • Time units are implicitly milliseconds everywhere, with no Ms suffix on public API:

    • Engine.currentFrameElapsedMs -> currentFrameElapsed, Engine.currentFrameLagMs -> currentFrameLag, FrameStats.elapsedMs -> elapsed, GifFrame.delayMs -> delay, ClockOptions.defaultUpdateMs -> defaultUpdate
    • Sound, SoundTrack and PlayOptions position/duration, seek(), getPlaybackPosition(), getTotalPlaybackDuration() and SoundTrack.scheduledStartTime are now in milliseconds (previously seconds). sound.play({ duration: 500 }) now plays for half a second.
  • API naming is now consistent: synchronous boolean state is always a getter (isX), side-effecting calls are verb methods, and async state is a Promise property that never has an isX() form. Zero-arg isX()/hasX() methods became getters, drop the parentheses:

    • Entity.isKilled (and Actor), Engine.isRunning, Engine.isDisposed, Clock.isRunning, Scene.isCurrentScene, CollisionContact.isCanceled, CoroutineInstance.isRunning/isComplete
    • Sound and SoundTrack: isPlaying, isPaused, isStopped
    • Loadable.isLoaded (Resource, ImageSource, Font, Gif, Sound, SoundManager, DefaultLoader)
    • Matrix/AffineMatrix.isIdentity, Transform.isMirrored, BoundingBox.hasZeroDimensions, Camera.hasChanged, ActionQueue.hasNext/isComplete, Graphic.isStale, RendererPlugin.hasPendingDraws, WebAudio.isUnlocked, Serializer.isInitialized
    • Methods that mutate or do real work stay verbs: PolygonCollider.isConvex -> checkConvex() (O(n)), the mutating hasChanged getter on SparseHashGrid and its collision processor -> refreshAndCheckChanged(), and every toggleEnabled(enabled) on the input classes -> setEnabled(enabled) (it takes an explicit value, it never toggled)
    • Settable booleans follow the same isX naming as the already-shipped isVisible: the visible option on ActorOptions, TriggerOptions and GraphicsComponentOptions is now isVisible, and Particle.visible is now Particle.isVisible
    • Tile.solid (TileMap and IsometricMap tiles) and the solid tile option are now isSolid
    • GarbageCollector.running() is now the getter isRunning; FontTextInstance.disposed is now isDisposed
    • Slide({ easingFunction }) is now Slide({ easing }), matching every other transition; previously easing was silently ignored by Slide
    • Pausing: Actor.paused (a PauseComponent, not a boolean) and Particle.paused are now the read-only pauseComponent, PauseComponent.paused is now the read-only isPaused, and PauseComponentInterface is now PauseComponentOptions. Use Actor.canPause and Actor.isPaused for everyday code
    • Spelling: isOffScreen -> isOffscreen (one word, like the web platform's OffscreenCanvas; "on screen" stays split: forceOnScreen, onScreenOnly, getOnScreenTiles()), isHiDpi -> isHiDPI on Engine and Screen, and Future.isCompleted -> isComplete
    • Renamed: Color.equal -> Color.equals
    • Renamed: Engine.loadingComplete -> Engine.isLoaded; Engine.ready (boolean) -> Engine.isReady; Engine.isReady() (Promise) -> await engine.ready; Animation.done -> Animation.isComplete; Timer.complete and Transition.complete -> isComplete; Gamepad.connected -> Gamepad.isConnected; SystemManager.initialized -> isInitialized; Engine.toggleInputEnabled(enabled) -> Engine.setInputEnabled(enabled); graph edge option directed -> isDirected; PointerEvent.active/WheelEvent.active -> isActive; Transition.started -> hasStarted; Gamepads.enabled (polling opt-in) -> Gamepads.pollingEnabled
  • Sound: the audio implementation abstraction has been removed and Web Audio is the only implementation. ex.WebAudioInstance is replaced by ex.SoundTrack (returned by Sound.instances and NativeSoundEvent.track), and the ex.Audio, ex.AudioImplementation, ex.ExResponse, ex.ExResponseType and ex.ExResponseTypesLookup exports are gone (pass the response type string to new ex.Resource(path, 'arraybuffer') instead). Per-track volume moved to the owning Sound, so SoundTrack has no volume.

  • Sound.play() always starts a new track and never changes the Sound's configuration. Previously play() resumed paused tracks, and play(volume)/play({ volume }) permanently set Sound.volume. Use the new Sound.resume() to continue paused (or seeked) tracks, and play({ volume, pitch, playbackRate, loop, position, duration, scheduledStartTime, onPlay }) for one-off overrides that apply to that track only (volume multiplies Sound.volume). play(volume: number) still works as a one-off volume but is deprecated. The unused Sound.scheduledStartTime property is removed, use the play option. The onPlay hook's source is now the track's per-play volume gain (an AudioNode); the buffer source is available as bufferSource for param automation.

  • SoundManager.play(name, volume) is now play(name, options?) taking the same PlayOptions, and SoundConfig.channels (a list) is now a single channel.

  • SoundManager is now a real Web Audio mixer (Sound.output → mix gain → SoundChannel → SoundManager.output → speakers) instead of multiplying volumes into Sound.volume on each play:

    • A sound routes through a single channel (SoundConfig.channel, SoundManager.setChannel()) rather than a list of channels; addChannel()/removeChannel() are removed. Channel volume and mute apply to the whole channel via SoundChannel (manager.getChannel('music'), manager.channel.setVolume/mute/...).
    • SoundManager.volume is the master volume (defaultVolume is removed) and play(name, volume) passes volume through to Sound.play() untouched; setVolume/getVolume read and write the sound's mix gain and never write Sound.volume.
    • Muting (sound, channel or master) silences playback instead of pausing it, so muted sounds still play and are audible mid-clip when unmuted. mute()/unmute()/toggle() with no argument act on the master output.
    • SoundManager.play() and SoundManager.channel.play() resolve to a boolean (false when the sound was unknown or dropped by a concurrency cap) instead of void. ChannelCollection is constructed with only the manager, and the quasi-internal _muted, _isMuted(), _getEffectiveVolume() and _activeTrackCount() members are replaced by isMuted() and playingCount().
  • Behavior change - Excalibur screen space is now consistently rooted at the top-left of the safe content area (Screen.contentArea) across the whole API. Previously the clipping display modes (FitScreenAndFill, FitContainerAndFill, FitScreenAndZoom, FitContainerAndZoom) disagreed about where screen space started: CoordPlane.Screen entities and pointer events were rooted at the content area's corner, but Screen.worldToScreenCoordinates returned raw canvas/resolution coordinates and Screen.contentArea.left/top carried the canvas-space inset. Every API now shares the single content-area-rooted definition, which fixes several pointer, transition, and unsafeArea bugs in the clipping display modes, but is a breaking change for code that relied on the old canvas-rooted values:

    • Screen.worldToScreenCoordinates(point) now returns coordinates rooted at the content area (previously the raw camera projection in canvas/resolution coordinates). Screen.screenToWorldCoordinates is the exact inverse, as before.
    • Screen.contentArea is now always rooted at (0, 0), and Screen.unsafeArea.topLeft is negative by the clip amount. The canvas-space inset that contentArea.left/top used to carry is available from the new Screen.contentAreaOffset.
    • Display modes that never clip (Fixed, FitScreen, FillScreen, FitContainer, FillContainer) have a (0, 0) offset and are unaffected.

    If you need the old canvas-rooted values, for example to position an HTML overlay over the canvas, shim them by adding Screen.contentAreaOffset back:

    // Before: worldToScreenCoordinates returned canvas coordinates
    const canvasPos = engine.screen.worldToScreenCoordinates(actor.pos).add(engine.screen.contentAreaOffset);
    
    // Before: contentArea.topLeft was the safe area corner in canvas coordinates
    const safeAreaCorner = engine.screen.contentAreaOffset.clone();
  • Behavior change - rotateBy({angleRadiansOffset, duration}) with no rotationType now rotates by exactly the signed offset provided, preserving full and multiple turns, instead of taking the shortest path to the canonicalized target orientation. Offsets larger than Math.PI now travel in the direction of their sign (previously they rotated the shorter way around), and 2 * Math.PI performs a full revolution (previously a no-op). Pass an explicit rotationType to keep path-based behavior

  • Behavior change - TileMap now uses 'separate' as the compositeStrategy as a better default. Commonly TileMap is used to build levels, so this default aligns with the common use.

  • Removed old legacy ex.EasingFunctions in favor of the [0, 1] common easing functions

  • Removed EaseTo/EaseBy/actor.actions.(easeTo|easeBy) Action in favor of MoveTo/MoveBy/actor.actions.(moveTo|moveBy) Action with easing support

  • Removed body.sleeping in favor of isSleeping

  • Removed entity.active in favor of isActive

  • Removed actor.getGlobalPos() in favor of actor.globalPos getter/setter

  • Removed actor.getGlobalRotation() in favor of actor.globalRotation getter/setter

  • Removed actor.getGlobalScale() in favor of actor.globalScale getter/setter

  • Removed BoundingBox.draw in favor of BoundingBox.debug

  • Removed typo in engine.isFullScreen now engine.isFullscreen

  • Removed unused tag-query.ts

  • Removed world.queryTags, world.query(...) can query tags

  • Removed Animation.durationPerFrameMs in favor of Animation.durationPerFrame

  • Removed Vector.size in favor of Vector.magnitude

  • Behavior change - Font/Text now render more accurately and faster be using less texture space, this unfortunately is a breaking change becuase text will render slightly different.

  • Breaking change - Material.update() now passes a MaterialContext to its callback instead of the composite Shader. The context carries the material's declarative uniforms dictionary (the recommended way to change values — they reach the composite shader and are forwarded to every pipeline pass), plus shaders/shadersByName for direct access to every compiled shader the material drives. Update code like material.update(shader => shader.trySetUniformFloatVector('iMouse', pos)) to material.update(context => context.uniforms.iMouse = pos)

  • Build: Docusaurus now throws the build on broken markdown links, broken anchors, and broken markdown images instead of warning. Any custom docs/blog content must have valid internal links, anchors, and images.

  • Build: The engine tsconfig.json now uses module: "esnext" with moduleResolution: "bundler" (previously module: "es2015" / moduleResolution: "node"), and downlevelIteration has been removed. The per-package tsconfig.json overrides in director, entity-component-system, graphics, and resources have been removed in favor of the single engine tsconfig.json. Consumers importing source directly may need bundler-compatible tooling.

  • Build: TypeDoc runner has been replaced by site/scripts/gen-typedoc-api.mjs.

  • Removed all remaining deprecated/legacy API ahead of v1:

    • ex.Shape in favor of ex.Colliders (Colliders.Box/Polygon/Circle/Edge/Capsule)
    • ColliderComponent.set() in favor of use()
    • ActorArgs in favor of ActorOptions, ParticleEmitterArgs in favor of ParticleEmitterOptions, ex.Pool in favor of ex.ArenaPool
    • PostProcessor.getShader()/getLayout() single-pass path, PostProcessor.process(source, destination) is now required (use ShaderPipelinePostProcessor or ShaderPass); ex.ScreenShader removed along with the v_texcoord varying (use v_uv)
    • Sound.play(volume: number) in favor of Sound.play({ volume })
    • Flags.useLegacyImageRenderer(), ExcaliburGraphicsContextWebGL.imageRenderer and the legacy ex.image renderer, ex.image-v2 is the only image renderer
    • ex.EventTypes, the lowercase event-name type aliases of ex.Events (ex.Events.kill, ex.Events.pointerup, ...), AddedComponent/RemovedComponent messages and type guards, TagsComponent
    • Util.addItemToArray (Util.removeItemFromArray stays, no longer deprecated)
    • CapturePointerConfig, Particle.DefaultConfig, and the exports of GifParser, Stream and LegacyWebAudioSource (internal only now)
    • rAF/cAF/AudioContext-prefix polyfills, only the requestIdleCallback shim remains

Deprecated

  • ex.Pool renamed to ex.ArenaPool

What's Changed

New Contributors

Full Changelog: v0.32.0...v1.0.0

Don't miss a new Excalibur release

NewReleases is sending notifications on new releases.