You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Sound, SoundTrack and PlayOptionsposition/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:
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
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 coordinatesconstcanvasPos=engine.screen.worldToScreenCoordinates(actor.pos).add(engine.screen.contentAreaOffset);// Before: contentArea.topLeft was the safe area corner in canvas coordinatesconstsafeAreaCorner=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
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Thirteen years, nearly 4,000 commits, and over 120 contributors later: Excalibur.js is v1.0.0!
Highlights new in v1
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
Mssuffix on public API:Engine.currentFrameElapsedMs->currentFrameElapsed,Engine.currentFrameLagMs->currentFrameLag,FrameStats.elapsedMs->elapsed,GifFrame.delayMs->delay,ClockOptions.defaultUpdateMs->defaultUpdateSound,SoundTrackandPlayOptionsposition/duration,seek(),getPlaybackPosition(),getTotalPlaybackDuration()andSoundTrack.scheduledStartTimeare 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 anisX()form. Zero-argisX()/hasX()methods became getters, drop the parentheses:Entity.isKilled(andActor),Engine.isRunning,Engine.isDisposed,Clock.isRunning,Scene.isCurrentScene,CollisionContact.isCanceled,CoroutineInstance.isRunning/isCompleteSoundandSoundTrack:isPlaying,isPaused,isStoppedLoadable.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.isInitializedPolygonCollider.isConvex->checkConvex()(O(n)), the mutatinghasChangedgetter onSparseHashGridand its collision processor ->refreshAndCheckChanged(), and everytoggleEnabled(enabled)on the input classes ->setEnabled(enabled)(it takes an explicit value, it never toggled)isXnaming as the already-shippedisVisible: thevisibleoption onActorOptions,TriggerOptionsandGraphicsComponentOptionsis nowisVisible, andParticle.visibleis nowParticle.isVisibleTile.solid(TileMapandIsometricMaptiles) and thesolidtile option are nowisSolidGarbageCollector.running()is now the getterisRunning;FontTextInstance.disposedis nowisDisposedSlide({ easingFunction })is nowSlide({ easing }), matching every other transition; previouslyeasingwas silently ignored bySlideActor.paused(aPauseComponent, not a boolean) andParticle.pausedare now the read-onlypauseComponent,PauseComponent.pausedis now the read-onlyisPaused, andPauseComponentInterfaceis nowPauseComponentOptions. UseActor.canPauseandActor.isPausedfor everyday codeisOffScreen->isOffscreen(one word, like the web platform'sOffscreenCanvas; "on screen" stays split:forceOnScreen,onScreenOnly,getOnScreenTiles()),isHiDpi->isHiDPIonEngineandScreen, andFuture.isCompleted->isCompleteColor.equal->Color.equalsEngine.loadingComplete->Engine.isLoaded;Engine.ready(boolean) ->Engine.isReady;Engine.isReady()(Promise) ->await engine.ready;Animation.done->Animation.isComplete;Timer.completeandTransition.complete->isComplete;Gamepad.connected->Gamepad.isConnected;SystemManager.initialized->isInitialized;Engine.toggleInputEnabled(enabled)->Engine.setInputEnabled(enabled); graph edge optiondirected->isDirected;PointerEvent.active/WheelEvent.active->isActive;Transition.started->hasStarted;Gamepads.enabled(polling opt-in) ->Gamepads.pollingEnabledSound: the audio implementation abstraction has been removed and Web Audio is the only implementation.
ex.WebAudioInstanceis replaced byex.SoundTrack(returned bySound.instancesandNativeSoundEvent.track), and theex.Audio,ex.AudioImplementation,ex.ExResponse,ex.ExResponseTypeandex.ExResponseTypesLookupexports are gone (pass the response type string tonew ex.Resource(path, 'arraybuffer')instead). Per-track volume moved to the owningSound, soSoundTrackhas novolume.Sound.play()always starts a new track and never changes the Sound's configuration. Previouslyplay()resumed paused tracks, andplay(volume)/play({ volume })permanently setSound.volume. Use the newSound.resume()to continue paused (or seeked) tracks, andplay({ volume, pitch, playbackRate, loop, position, duration, scheduledStartTime, onPlay })for one-off overrides that apply to that track only (volumemultipliesSound.volume).play(volume: number)still works as a one-off volume but is deprecated. The unusedSound.scheduledStartTimeproperty is removed, use the play option. TheonPlayhook'ssourceis now the track's per-play volume gain (anAudioNode); the buffer source is available asbufferSourcefor param automation.SoundManager.play(name, volume)is nowplay(name, options?)taking the samePlayOptions, andSoundConfig.channels(a list) is now a singlechannel.SoundManageris now a real Web Audio mixer (Sound.output → mix gain → SoundChannel → SoundManager.output → speakers) instead of multiplying volumes intoSound.volumeon each play:channel(SoundConfig.channel,SoundManager.setChannel()) rather than a list ofchannels;addChannel()/removeChannel()are removed. Channel volume and mute apply to the whole channel viaSoundChannel(manager.getChannel('music'),manager.channel.setVolume/mute/...).SoundManager.volumeis the master volume (defaultVolumeis removed) andplay(name, volume)passesvolumethrough toSound.play()untouched;setVolume/getVolumeread and write the sound's mix gain and never writeSound.volume.mute()/unmute()/toggle()with no argument act on the master output.SoundManager.play()andSoundManager.channel.play()resolve to aboolean(false when the sound was unknown or dropped by a concurrency cap) instead ofvoid.ChannelCollectionis constructed with only the manager, and the quasi-internal_muted,_isMuted(),_getEffectiveVolume()and_activeTrackCount()members are replaced byisMuted()andplayingCount().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.Screenentities and pointer events were rooted at the content area's corner, butScreen.worldToScreenCoordinatesreturned raw canvas/resolution coordinates andScreen.contentArea.left/topcarried the canvas-space inset. Every API now shares the single content-area-rooted definition, which fixes several pointer, transition, andunsafeAreabugs 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.screenToWorldCoordinatesis the exact inverse, as before.Screen.contentAreais now always rooted at(0, 0), andScreen.unsafeArea.topLeftis negative by the clip amount. The canvas-space inset thatcontentArea.left/topused to carry is available from the newScreen.contentAreaOffset.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.contentAreaOffsetback:Behavior change -
rotateBy({angleRadiansOffset, duration})with norotationTypenow 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 thanMath.PInow travel in the direction of their sign (previously they rotated the shorter way around), and2 * Math.PIperforms a full revolution (previously a no-op). Pass an explicitrotationTypeto keep path-based behaviorBehavior change - TileMap now uses 'separate' as the
compositeStrategyas a better default. Commonly TileMap is used to build levels, so this default aligns with the common use.Removed old legacy
ex.EasingFunctionsin favor of the [0, 1] common easing functionsRemoved EaseTo/EaseBy/actor.actions.(easeTo|easeBy) Action in favor of MoveTo/MoveBy/actor.actions.(moveTo|moveBy) Action with easing support
Removed
body.sleepingin favor ofisSleepingRemoved
entity.activein favor ofisActiveRemoved
actor.getGlobalPos()in favor ofactor.globalPosgetter/setterRemoved
actor.getGlobalRotation()in favor ofactor.globalRotationgetter/setterRemoved
actor.getGlobalScale()in favor ofactor.globalScalegetter/setterRemoved
BoundingBox.drawin favor ofBoundingBox.debugRemoved typo in
engine.isFullScreennowengine.isFullscreenRemoved unused
tag-query.tsRemoved
world.queryTags,world.query(...)can query tagsRemoved
Animation.durationPerFrameMsin favor ofAnimation.durationPerFrameRemoved
Vector.sizein favor ofVector.magnitudeBehavior 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 aMaterialContextto its callback instead of the compositeShader. The context carries the material's declarativeuniformsdictionary (the recommended way to change values — they reach the composite shader and are forwarded to every pipeline pass), plusshaders/shadersByNamefor direct access to every compiled shader the material drives. Update code likematerial.update(shader => shader.trySetUniformFloatVector('iMouse', pos))tomaterial.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.jsonnow usesmodule: "esnext"withmoduleResolution: "bundler"(previouslymodule: "es2015"/moduleResolution: "node"), anddownlevelIterationhas been removed. The per-packagetsconfig.jsonoverrides indirector,entity-component-system,graphics, andresourceshave been removed in favor of the single enginetsconfig.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.Shapein favor ofex.Colliders(Colliders.Box/Polygon/Circle/Edge/Capsule)ColliderComponent.set()in favor ofuse()ActorArgsin favor ofActorOptions,ParticleEmitterArgsin favor ofParticleEmitterOptions,ex.Poolin favor ofex.ArenaPoolPostProcessor.getShader()/getLayout()single-pass path,PostProcessor.process(source, destination)is now required (useShaderPipelinePostProcessororShaderPass);ex.ScreenShaderremoved along with thev_texcoordvarying (usev_uv)Sound.play(volume: number)in favor ofSound.play({ volume })Flags.useLegacyImageRenderer(),ExcaliburGraphicsContextWebGL.imageRendererand the legacyex.imagerenderer,ex.image-v2is the only image rendererex.EventTypes, the lowercase event-name type aliases ofex.Events(ex.Events.kill,ex.Events.pointerup, ...),AddedComponent/RemovedComponentmessages and type guards,TagsComponentUtil.addItemToArray(Util.removeItemFromArraystays, no longer deprecated)CapturePointerConfig,Particle.DefaultConfig, and the exports ofGifParser,StreamandLegacyWebAudioSource(internal only now)requestIdleCallbackshim remainsDeprecated
ex.Poolrenamed toex.ArenaPoolWhat's Changed
EntitywithoutCollisionComponentdon't rendering on prod build #3696] instanceof in vite prod mode by @eonarheim in fix: [#3696] instanceof in vite prod mode #3742NineSliceStretch.Tileoption not behaving as expected #3777 by @jyoung4242 in fix: update NineSlice rendering logic and add visual test images #3777 #3782New Contributors
Full Changelog: v0.32.0...v1.0.0
This discussion was created from the release Excalibur v1.0.0 Release.
All reactions