Skip to content

v0.97.0

Latest

Choose a tag to compare

@github-actions github-actions released this 03 Nov 05:28
· 26 commits to master since this release

Added

  • Pack several textures onto one canvas at run time: TextureAtlas.pack. It draws the textures you give it onto one new canvas (the smallest power-of-two size that holds them, up to maxSize, with padding between pictures) and returns where each landed: atlas.sprite(source) gives the texture, uvOffset and uvScale a Sprite takes, atlas.get(source) the pixel rectangle. Sprites cut from one canvas batch into one draw. Pictures are copied without blending so their transparency is kept, and the canvas is drawn again from the sources after a graphics device is lost. Its pixels are checked on WebGL2 only, as RenderTexture's are. The Render Texture example builds four cards and packs them.
  • Rich text reads the tags other engines write. <size=24>…</size>, a colour by name (<color=red>), and <br/> now work beside <color=#RRGGBB>, <font size=…>, <b> and <i>. <u>, <s>, <outline …> and <on click=…> are accepted so text written for another engine shows no raw tags: the words are kept, the underline, outline and click are not drawn. Unknown tags still show as text.
  • Scroll areas can be pulled past an end and spring back. A scroll area whose movement is Elastic now does what the setting always said: a drag past an end is stiffer the further out it goes, letting go returns the content, and a flick that reaches an end bounces off it. It is opt-in per area (ScrollMovement.Elastic; Clamped, the default, behaves as before), and the gesture model (ScrollMotion, with ScrollContainer.dragTo and getOverscroll) is exported for scrolling anything else the same way.
  • More easing curves. Sine, quart, quint, expo and circ (each in, out and in-out), bounce in and in-out, and smoothstep and smootherstep join the quad, cubic, back, elastic and out-bounce curves. They are new EasingType members after the existing ones, so a saved tween keeps its curve, and they ease the same in a C++ tween and a script value tween.
  • World content has a rectangle: ContentRect. A size and the pivot the entity's transform sits on, which the Sprite and Text on the entity follow and which a UIMask on it clips to — no UINode needed, so a clipping region or a scroll viewport can stand in the world. contentRectContains tests a world point against it.
  • RectAnchors: world content that follows its parent. A new engine component pins content placed by its Transform and sized by a ContentRect to the edges or centre of its parent rectangle (stretch, pin to an edge, centre with an offset, margins in units or as a fraction), or, with no parent rectangle, to the screen, which is centred on the world origin. An engine system aligns parents before the children that depend on them, in the editor edit mode as well as in play, on ONCE / ON_RESIZE / ALWAYS. The Cocos Widget is now a view on it, and a converted project carries RectAnchors instead of a compatibility-layer component, so a scene is laid out where the editor shows it. The visible area of the Cocos layer is centred on the origin, as the engine screen is.
  • The script bundle is not rebuilt for nothing. The project-script bundle is stamped with every file it read (size and modification time) and the build tool that made it, and Play builds it again only when one of those changed: a project of thousands of scripts goes from a multi-second build at every Play to a check of a few hundred milliseconds.
  • Large projects open faster. The asset dependency scan no longer asks the path index about every string in every document (names, types and labels cannot be paths), which took the open of a 5000-document project from 17 s to under 5 s, and the Cocos script rewrite makes two passes over a file instead of one per declaration.
  • A model texture the project does not have is left off. A model whose material names a texture file that is not there is imported without it, with a warning, instead of keeping a reference that fails to load on every use. A texture the model names by a path on the artist's machine is looked for by its file name beside the model first.
  • An unlit mesh variant with tangents links. The fragment stage declared the tangent varying wherever a mesh had tangents, though the vertex stage wrote it only when lit, so an unlit variant failed to link.
  • Bursts on a schedule. ParticleEmitter.bursts is a list of { time, count }: each releases its particles once the emitter has been emitting for time seconds, and a looping emitter repeats the list every duration. It runs beside burstCount, which still repeats one burst on an interval.
  • A 3D collider can sit off its entity. BoxCollider3D, SphereCollider3D and CapsuleCollider3D take an optional center, the shape's place from the entity origin in world units. The body stays at the entity, so a dynamic body turns about its origin and the shape swings round it.
  • Text can underline, and a box can grow to its text. Text.underline draws a bar under each line, taken from the font's own underscore. TextOverflow.ResizeHeight keeps every line and sets the entity's ContentRect height to the height they take, so what sits below a paragraph can follow it.
  • An emitter runs on its own clock. ParticleEmitter.simulationSpeed scales how fast that emitter's time runs: 2 plays it twice as fast, 0.5 at half, 0 holds it. Emission, lifetimes and motion all follow it. The default, 1, is the behaviour before.
  • An animator state can play at the speed of a parameter. speedParameter on an AnimatorState names a float parameter; the state's clock advances by dt times its value, so a run clip follows a character's speed and a value of 0 holds the pose. It applies to every kind of motion, since it is the state's own clock that is scaled.
  • Particles can carry an added velocity and be held to a speed limit. ParticleEmitter.addedVelocityMin and addedVelocityMax give each particle a velocity on top of its own, chosen between the two when it is born and untouched by gravity, damping and noise. speedLimit pulls a faster particle back toward the limit, by limitDampen of the excess every 1/60 s so the braking does not depend on the frame rate. Both are run by the CPU simulation.
  • An emitter can start late. ParticleEmitter.startDelay holds back emission for that many seconds after the emitter starts playing; the duration of a non-looping emitter is counted from the end of the delay. Zero, the default, is the behaviour before.
  • Particles can be stretched along their velocity. ParticleEmitter.renderMode is Billboard (as before) or Stretched, which turns each particle's quad so its Y axis runs along the particle's velocity and makes it size * lengthScale + speed * velocityScale long: sparks, rain and streaks. A particle at rest keeps its own rotation. It is drawn by the CPU simulation; an emitter simulated on the GPU still draws as a billboard.
  • Fade a whole group of world content with one value: GroupOpacity. On an entity it fades that entity and everything under it; the opacities multiply down the hierarchy into each descendant's own alpha, as UINode.opacity does for a UI subtree. Sprites, text, shapes, trails and tilemap layers follow it. It is not a composited layer, so children that overlap show through one another while the group is partly faded.
  • A held pointer reports where it goes, and a hold can be given up. pointer_move goes to the entity a pointer is holding for as long as it is held, press_cancel to one whose hold was taken away (UIControlPointers.cancelHold, optionally handing the gesture to another entity, so a scroll area can take a drag from the button under it), and every pointer event now says which pointer it was and where, in the world and on the surface. An entity that is only a ContentRect with an Interactable can be the one pressed.
  • The engine's runtime tuning is a project setting. Project Settings → Game → Runtime sets how a scene switch fades (duration and colour), how far one frame may advance the game and how many fixed steps it may catch up, how long an asset may take to load and how soon a failed one is tried again, how much memory the texture, audio and device-loss caches keep, the text canvas size and the default font family. They were reachable only from code (RuntimeConfig) and no project file could carry them. The project holds features.runtime with only what differs from the engine's own values (RUNTIME_BUILD_DEFAULTS), a build ships the same block, and initRuntime applies it before the first asset loads on the web, desktop, native, mini-game and playable targets and in Play. The edit viewport keeps the engine's values.
  • Rows for the choices that could only be written into the project file. Under Rendering: compile shaders in the background, warm them up behind the loading screen and how long to wait, draw only changing frames and how many frames to keep after a change, each level's anti-aliasing (MSAA or temporal; the MSAA count is hidden where temporal ignores it) and its depth pre-pass. Under Game → Display: the render resolution (screen, design resolution or integer multiple). Under the web build: the servers the page may connect to, each checked by the parser the export uses. check-project-settings now reads the keys of QualityConfig, QualityProfile and RuntimeBuildConfig and fails a key that has no row and no stated reason, which is how these went unnoticed behind the one row the block had.
  • Rain for a 2D scene. The rain2d post-process effect is rain for a scene that reads no depth: streaks, and rings, splashes and puddles on the ground in world units (intensity, wetness, puddles, puddleScale, rippleScale, wind, speed, streakLength, colour). RainShelter on a roof or a tree crown keeps the ground it covers dry, as far as that ground can be seen, so a roof faded to show the room under it has a dry room. A scene has as many shelters as it has roofs: the ones in view are painted into a small mask each frame, whose size is the effect's Shelter Mask Size (maskSize, 64 to 2048) and at most the quality level's shelterMaskSize (256 low, 512 medium, 1024 high, set on the quality page of Project Settings). RainUpright on a sprite that stands on the ground (a character, a trunk, a crate) keeps the rings, puddles and wet darkening off it, while the falling streaks still cross it.
  • Mist, wind in the grass, and roofs that fade over the player. The mist2d effect lays drifting banks of mist over the ground in world units, and what is lit glows through it. The Sway sprite material (sprite-sway) leans grass, reeds and cloth in the wind with the foot fixed and the tip free, scaled by the weather's wind, and a SwayPusher on a character makes the plants lean away from it as it walks through. CoverFade on a roof or a crown eases it down to opacity of its alpha while a CoverFadeTarget (the player) stands on the ground it covers, and back when the target leaves.
  • Footprints and ripples. Footsteps on what walks and StepSurface on the ground worth marking: every stride world units a step falls, left and right in turn, as a ring that spreads and fades on a Water surface or a footprint that fades over printLife on a Print surface (mud, snow). The steps use the engine's own ring and print or textures of your own, are drawn on the sprite layer you name, are never saved, and at most 256 are alive at once.
  • A 2D light can keep to a range of sprite layers. Light.layerMin and layerMax limit the sprite layers a light reaches, so a lamp inside a hut does not light the roof drawn over it; the roof still takes the scene's ambient light. The default is every layer, a directional light takes a range too, and a custom lit sprite shader joins in by calling applyLighting2DLayer instead of applyLighting2D.
  • More 2D lights cast shadows at once. The 2D shadow mask has eight channels (four in a frame where some light has a shape), and lights whose reach and shadows cannot meet share a channel, so a camp of torches spread round a clearing all cast. Every light can cast, the lights shaded through the screen tiles past the lighting loop's sixteen as well (up to 96 more, brightest first). A light left without a channel still lights the scene and drops only its shadow, the dimmest first, and the profiler's render.shadow2d.unplaced counts them.
  • A soft 2D shadow's smoothness is a quality setting. A light with a size draws its penumbra as that many silhouettes added up, and how many is the quality profile's shadowSoftSamples (1 to 32; 8 low, 12 medium, 16 high), with a row on the quality page of Project Settings. Fewer is cheaper and shows the edge as bands.
  • What the player can see, and what they have seen. SightViewer on the player and SightTarget on a sprite fade the sprite out while no viewer has a clear line to it past the ShadowCaster2D walls, for what the light alone does not hide: an unlit enemy, its health bar, a pickup that glows on its own. segmentBlocked, seesPoint and currentSightRings give gameplay the same answer. FogOfWar with a fog2d effect keeps the ground no viewer has seen dark and the ground seen and left dimmed and drained of colour, and exploredToString / exploredFromString let a save keep what the player found.
  • A palette effect. The palette post-process effect holds the whole picture to the colours of a strip image (up to 64), after an ordered dither whose scale can match the art's pixel size.
  • A 2D world streams on x and y. A streamed world, a procedural chunk layer and a cooked world manifest can be cut on the screen plane (plane: 'xy', StreamPlane.Screen, Screen (x, y) in the editor) instead of the ground, so a 2D scene streams as a grid rather than as one row along x, and the editor's streamed-world grid is drawn on the plane the world is cut on. An infinite tile layer can let a chunk go (removeChunk), its mesh with it, so a layer that follows a walker holds only the chunks near it.
  • A generated 2D world. createWorld2D in esengine/worldgen lays a world out on the screen plane by the same rules as a terrain world (biomes, recipes, layers, zones) and publishes it in chunks round the camera; what a placement looks like and how the ground is painted are the game's, said in publish. A chunk is the same whatever order it is made in, and a PlacementDiffs keeps what players changed.
  • Grid placement, and what stands nearest. esengine/gameplay has snapFootprint, snapToGrid, gridCellAt, gridCellCentre and checkPlacement, which says whether a footprint is clear of colliders, solid tiles and the cells the game counts as taken, for a build mode; and nearestMarkers and nearestEntities, the markers or entities nearest a point filtered by type, properties, radius and count, for an interact button.
  • Save power when the battery is low or the device is hot. QualityConfig.power, with rows in Project Settings: while the battery is under lowBattery and not charging, or the platform reports thermal pressure at thermal or worse, the engine lowers the frame cap to fps and holds the quality level at or below ceiling, and puts both back when the device recovers; at critical pressure it halves the cap and holds the level at Low. Off by default, and what a platform does not report never triggers it. Device.power() gives a game the same readings.
  • Bitmap fonts. Text draws a bitmap font (.fnt) from its page texture through the same layout as any font, so wrapping, alignment and overflow work as they do for any text, and the baseline comes from the font's own base. A glyph page is read by a mode (plain, distance field, mask or fill), so a font with its outline baked into the page draws in two passes with the outline in any colour.
  • Text can shrink to fit its box. TextOverflow.Shrink sets the type smaller, a whole pixel at a time, until the text fits both sides of its box.
  • A layer can be painted in hierarchy order. Renderer.setTreeSortLayers(mask) names the layers whose sprites, labels and shapes stack in the order of the hierarchy (a parent before its children, an earlier sibling's subtree before a later one) instead of by shader and depth, so a panel covers the text of the panel under it.
  • A text field can stand in the world. TextInput works on an entity with a ContentRect as well as on a UINode, and TextInputFilter has Decimal and Phone beside Digits.
  • UI pages that exist only while they are wanted: UIPages. A page registered as a prefab is built the first time it is opened (opens at the same moment share one build), its nodes are found by name inside it, and on close it is switched off (keep) or taken out with its assets (destroy). A page goes under a parent as a new root, or under a mount already in the scene so it keeps its place in the draw order.
  • Virtual prefab instances. A prefab instance marked virtual (the Virtual toggle in the prefab bar of Details, or the outliner's context menu; deferred in its metadata) is shown and edited whole in the editor, but a running scene makes only its root and builds the rest when it is opened, so a screen of many pages does not pay for the ones nobody opens; UIPages registers such instances as pages. An instance that adds, removes or re-parents parts is made whole. ?expand-virtual in the page address makes every virtual instance whole at boot, for a harness that expects every node, leaving out one marked as another layout, and deferredEntities and deferredRecord are exported for a game that keeps two layouts this way.
  • The loading screens say how far they are. One bar, a line per step with its count and time, and the time so far; the engine step says which step of the boot it is in, and the asset scan reports how many documents it has read of how many.
  • A material can read a mesh's second uv set. In a mesh variant that carries one (MESH_LIGHTMAP), the vertex stage hands the material the mesh's second uv set as authored, v_texCoord1, beside the lightmap coordinates, for a mask or a dissolve map.
  • Paste textures onto a blank texture: TextureCanvas. TextureCanvas.create(width, height) gives a canvas whose draw(source, x, y, width?, height?) pastes a texture at a pixel position, in the order you call it, and whose texture a Sprite shows. Where TextureAtlas.pack lays textures out for you, this puts each one where you say.

Changed

  • The profiler's tables sort by their column header. Click a column of the cost tree or the systems list to order it by name or by time, and again to reverse it; the choice is remembered. Ranking by time moved a name whenever two systems passed each other, which is every few frames on a live window, so a reader could not follow one; by name the rows stay where they are. The systems list names twelve systems instead of five, so the members change less often too.
  • A selection stays off the running game unless Inspect is on. While the game plays, the selected entity's outline and handles are not drawn over it unless Inspect is on; a new selection shows where it is for a moment.
  • UI gears cost nothing on a frame where nothing they read changed. The gear pass visited every UIGear entity every frame, walking up to its controller, which was 2 to 4 ms for a still screen of 2530 gears. It now skips a frame in which no controller, gear or parent changed, the world's structure is the same and no tween is in flight.
  • A second scan of a large project reads nothing it has already read. The asset scan keeps what each document can refer to, keyed by the file's size and modification time (.esengine/cache/ref-candidates.json), so a document that has not changed is not read and parsed again.

Fixed

  • A 3D contact point is in the units of Transform. The point a Physics3DEvents.contactEnters entry carries was in the physics world's own units, so with the default 100 pixels to the unit it sat a hundredth of the way from the origin, and the contact crosses of the physics debug draw landed there instead of where the bodies met. It is now scaled as a ray hit is.
  • RectAnchors, AnchorFlag and AnchorMode can be imported from esengine. They were exported from the UI module only, so the import the manual shows did not resolve.
  • Lit materials draw on WebGPU again. A lit material on the engine's own vertex stage, and a 2D lit shader, built a WebGPU program with a preprocessor line left in it, so the material drew nothing (and a desktop build of a 3D scene came up black). #if and #elif conditions with defined, !, &&, || and parentheses now resolve in a WGSL twin as they do in GLSL.
  • A scanned preview reaches the game's own server. The page a phone opens from the preview QR code allowed connections only to itself, so a game that logged in to a server on the LAN worked in Play and in a single-file build but not on the phone. The preview now connects as Play does; a shipping web export still connects only to the servers listed in Project Settings → Allowed server origins.
  • The ground has no dark patches on a Mac. With the depth prepass on (quality high and medium), a material written as a fragment shader alone, the terrain among them, drew its colour from a vertex stage that did not declare its position invariant, while its depth came from one that did. Metal is free to compute the two a bit apart, and wherever they differed the colour failed the equal-depth test: sand, sea floor and grass showed dark triangles. Both stages now declare it, on WebGL2 and WebGPU.
  • No "uniform not found" warnings for a texture a program does not read. A material sets each of its textures on every program it is drawn with, and the depth program of a terrain or a lit material reads few of them, so opening a scene filled the log with warnings that read like typos. A texture a program does not use is now skipped quietly.
  • A terrain with texture layers no longer keeps a second copy of them in memory. Its layer arrays were made as content the graphics device keeps a CPU copy of, to restore after a GPU loss: 12 MiB on Terrain & Water, which took the copies past their 16 MiB budget and logged a warning. The terrain now says how to make them again: after a loss it reads and packs its layer files once more and puts them back behind the same textures.
  • No "NaN is an invalid value for height" error when a project opens. The content browser measured its grid before the editor had placed the panel, read the panel's padding as a number it was not, and gave the grid a height of NaN for that first frame.
  • A web game comes back after the GPU goes away. A browser can take the GPU from a page (a driver reset, a laptop waking, too many tabs). Three things stood in the way of the way back, all seen on Terrain & Water: while the engine waited for its textures, the frames it refused still gathered geometry and never emptied it, until an allocation of gigabytes stopped the module; the restored context had none of the WebGL extensions the engine relies on, so an HDR scene came back black; and the terrain's page atlases were never refilled, so the ground came back flat and black. The game now recovers to the picture it had.
  • Rain wets hillsides and plants, not only flat ground. The rain effect darkened only what faced almost straight up, so in a storm a slope began to stay dry-bright at about 18 degrees, and past about 31 was not darkened at all, beside the darkened flat, and every plant stood light on dark ground: from a distance a hillside read as pale wedges and white speckle. Everything under the rain darkens now, a little less where it faces away from the sky; standing water still only gathers on the flat.
  • A subpath of an official package resolves. Importing estella-plugin-…/something from a project script no longer hangs the script build.
  • Background shader compilation, the warm-up and on-demand drawing reach a build whose quality level is Off. A build dropped the whole quality block when the level was Off, though these three choices hold whatever the level: a project that switched asyncShaderCompile, prewarmShaders or renderOnDemand on read them in the editor and shipped a game that ignored them. The block now ships when any of them is on.
  • A terrain and a streamed world run on the Android and iOS templates. The terrain's layer textures were refused on a device (it has no wasm heap to copy them through), and a chunked world's maker threw on every tick because QuickJS has no AbortController. The layers now go over as bytes, as every other texture does, and the world uses a handle of its own where the host has none.
  • A pointer lands on what is drawn on top. Where two pressable things overlap at the same depth and layer, the press went to the one created last; a node taken from a pool and added under a panel is created last and drawn first, so it took presses meant for what covered it. It now goes to the one drawn later by its place in the hierarchy. In the Cocos layer, a Toggle belongs to the container its parent holds, and assigning isChecked switches the others of its group off.
  • The edit viewport frames and lays out the scene as the game does. The design and device frame is drawn round the camera that shows the scene, so a project whose camera is off the origin sees the frame where the camera looks, and the edit world lays UI out in the project's screen fit, so a root pinned to the screen takes the design box instead of the viewport's pixels.
  • A project script bundle that throws while it loads is reported. Play logged nothing and ran as though the project had no scripts; the error is now logged.
  • A playable far over its size cap is built at once and says its size. The search for a smaller page built the whole page, a base64 copy of every asset, for each candidate encoding, so a game of 140 images never finished; the size is now worked out from the parts, the search is skipped when what no encoder can shrink is already over the cap, each stage reports itself, and a page over the network's limit is a warning with its size instead of a failed build. A 57 MB game went from no answer to 4 s.
  • The status strip fits a narrow window. Each readout keeps to one line, and when the strip is short of room it drops the long message and the least useful readouts instead of wrapping.
  • A texture keeps the colour under its clear texels when it is made smaller or read back. A texture that stores numbers in its colour where it is clear (a terrain weight map) came out black after a Max Size resize or a read back through a 2D canvas, which keeps what it holds premultiplied. The resize and the read back now keep the colour.
  • A camera that clears no colour is drawn over the main view. A full-frame camera that only clears depth (a UI camera) competed with the world camera to be the main view and hid its picture; it is now drawn over it.
  • Stopping Play closes the connections the game opened. The play window is kept between plays, and a socket the last play opened stayed connected, so a game that logs in as the same account on the next play was told it had signed in somewhere else.
  • A drop on an Outliner row lands. A drop that arrived before the hover had been drawn was lost; the drop now asks for its own answer when the one it holds is for another row.
  • A play that cannot start says why. Every compile error of the project scripts is printed to the output log, and the status line gives the reason instead of only saying it failed.