Skip to content
Work in progress. These docs describe Spawnite at launch, and some parts are still being built.

Weather

weather() holds the world’s weather as a record of factors, which the room keeps and every page receives, so every player sees the same sky. A weather is a preset: clearWeather, rainWeather, snowWeather and the ones your game declares with defineWeather. A system sets it with setWeather, and the look blends from where it stands to the new weather’s factors over about six seconds. Unity, Unreal and Godot ship no weather of their own; the shape follows Ultra Dynamic Weather on Unreal’s Fab and Luminati, a preset of factors that a transition blends to.

List the plugin among the game’s plugins, then set the weather from a system. The following file declares a storm of its own and turns the weather on the minute:

packages/engine/test/outside/showers.ts
import {
defineGame,
definePlugin,
defineSystem,
Phase,
readTickRate,
} from "@spawnite/engine/core";
import {
clearWeather,
defineWeather,
rainWeather,
setWeather,
weather,
} from "@spawnite/engine/weather";
/** A storm of the game's own: darker, windier and wetter than the rain. */
export const storm = defineWeather("storm", {
sun: 0.15,
fill: 0.8,
fog: 2.8,
wind: 3.5,
godRays: 0,
overcastShare: 0.9,
temperatureOffset: -0.2,
saturationOffset: -0.35,
rainShare: 1,
fireflies: 0,
ground: { wetness: 1 },
});
/** The weather of each minute of the cycle. */
const cycle = [clearWeather, clearWeather, clearWeather, rainWeather, storm];
/** Clear for three minutes, a shower for one, a storm for one, and round
* again: a set to the weather it already blends to writes nothing. */
export const showers = definePlugin(() => ({
name: "showers",
requires: [weather],
systems: [
defineSystem({
name: "turn",
phase: Phase.Rules,
description: "Turns the weather on the minute.",
run: (step) => {
const minute = Math.floor(step.tick / readTickRate(step) / 60);
setWeather(step, cycle[minute % cycle.length]);
},
}),
],
}))();
export const game = defineGame({
name: "showers",
start: "meadow",
plugins: [
weather({ presets: [clearWeather, rainWeather, storm] }),
showers,
],
});

A preset names the factors that differ from clear’s, and each one it leaves out is the identity. The look reads them after its own day, so a rain at dusk is still dusk. Each factor is a field of LiveWeather:

Factor What it does Rain Snow
sun Multiplies the key light. 0.26 0.45
fill Multiplies the sky’s fill light. 0.9 1.15
fog Multiplies an exponential fog’s density, and draws a linear fog’s distances in. 2.1 1.9
exposure Multiplies the exposure. 0.92 0.76
wind Multiplies the look’s wind speed, which the grass, the leaves, the rain and the snow bend in. 2.3 1.3
godRays Multiplies every shaft of sunlight. 0 0.25
overcastShare Greys the fog, the sky’s horizon, the sun and the fill, 0 to 1, and lights the lamps by day. 0.72 0.62
temperatureOffset Adds to the grade’s white balance: below 0 cools the frame. -0.12 -0.3
saturationOffset Adds to the grade’s saturation multiplier: below 0 greys the frame. -0.24 -0.42
rainShare The share of the rain’s drops that fall, 0 to 1. 1 0
snowShare The share of the snow’s flakes that fall, 0 to 1. 0 1
dust, fireflies Multiply the floating dust by day and the fireflies by night, through an Emitter’s rateMultiplier. 0.15, 0 0, 0
leaves Multiplies the falling leaves, through an Emitter’s rateMultiplier. 1.6 0.3

The rain’s and the snow’s values are Luminati’s, the reference the diorama look copies, except the snow’s exposure: 0.76 to Luminati’s 1.04, so snowy ground under the diorama look’s brighter midday reads at the level of Luminati’s own snow frame. Under overcast, a lamp’s day level rises toward 0.45 and a glowing window’s toward 0.4 of its night, so a village reads lit in the rain at noon, as Point lights says.

A game’s own factor, such as a sandstorm’s sand, goes in a preset’s custom and is declared once in weather({ custom: { sand: 0 } }) with its identity: 0 for a share, 1 for a multiplier. It blends like the rest, and a game’s own effect reads it from useLookWeather().custom.sand.

Wetness is a level of the ground, not a factor: it rises while a weather that wets it stands and falls after, each at its own rate. A preset names the level the ground tends to, such as ground: { wetness: 1 }, and a state it leaves out falls to 0 under it. The built-in wetness soaks in 20 seconds and dries in 60, and the built-in snowCover lies in 15 seconds and melts in 8. weather({ ground: { snowCover: { riseSeconds: 60, fallSeconds: 30 } } }) sets a slower snow. A game declares another state, such as an ash cover, with its rates in weather({ ground: { ashCover: { riseSeconds: 30, fallSeconds: 90 } } }). Its effect reads the level from useLookWeather().ground.ashCover, as a system reads readWeather(step).ground.ashCover.

A wet surface that faces up darkens to 0.8 and takes a sheen of the sky. Where snow lies, a surface that faces up whitens toward the look’s snow colour, from 0.5 to 0.86 of the way up, and keeps some of its own pixels under it. A roof whitens by its pitch, and a wall stays bare. Snow over a wet street draws as snow, not as wet snow.

A leaf card or a tuft of ground cover whitens on its top, by how far up the card a pixel stands, to the look’s foliageShare at most.

Every lit material of a scene the weather reaches wets and takes snow, a Landscape with lighting={false} too. A material with a shader of its own reads useLookWeather().wetness and .snowCover. A system reads the same levels from readWeather(step).ground, such as a footprint system that reads the snow.

Lit sprites, water and skinned models stay dry and bare, and leaf cards stay dry and take snow. setMaterialWeather(material, { wetness: false }) keeps another material dry, such as a cloak. { snowCover: false } keeps one bare, such as a roof under an awning or a forge’s, and true lets either back. One weather level covers the whole world: nothing shelters the ground under a bridge or a roof yet.

setWeather(step, preset) blends from where the world stands, so a change mid-blend never jumps. It takes the following options:

  • blendSeconds: the seconds the blend takes, 6.3 unless set, which follows Luminati’s ease. 0 sets the look at once.
  • settled: whether the ground stands at the new weather’s levels at once, such as a world that opens in the rain.

A set to the weather the world already blends to writes nothing, so a system may set it every step. One with settled or a blendSeconds of 0 writes until the world stands so. A target set by its factors in place of a preset, such as a storm a game builds from its own state, blends the same way and reads with no name.

The world opens in the plugin’s start, clear unless set, with its ground settled. A start function names the weather from the world’s own state, such as a saved calendar’s, and returns null until that state is ready. The record is not saved, so a world that opens again opens there. It holds both ends of the blend and the tick it set out at, so a player who joins mid-blend reads the same weather as the room.

A system reads readWeather(step), at its step’s tick, so a re-run reads the same: the preset’s name, the blended factors and the ground’s levels. A system that reads it every step passes a reading it keeps, readWeather(step, reading) with one from createWeatherReading() at module scope, so the read makes nothing. A HUD reads useWeatherName(), the preset’s name, which draws again when it changes. WeatherChangedEvent tells the room and every page when the preset changes, and a target set by its factors changes it to none.

setWeather refuses a preset the plugin was not given, or a factor out of its range, with a WeatherRefusal. It throws in development and logs in a deployed room.

The look asks the listed plugin’s weather rule each frame and lays two layers over its answer:

  1. The Landscape’s, or the Lighting’s, weather: false draws none of the world’s weather, as an interior or a cave stands in, with each of the game’s own factors at its identity, and a partial record replaces the fields it names. A map’s interior level takes false on its own.
  2. setLightingWeather(world, weather), the page’s own weather for the devtools, a photo mode or a shot, which the room never hears. Pass null to let it go.

Each layer is checked as a preset is, by findLiveWeatherProblem: a factor out of its range throws in development, and a deployed page keeps the weather it drew. A page’s own wetness wets only a scene whose plugin or own layer already brings weather, since wetting another would compile every lit program again.

A frame callback reads the resolved record from useLookWeather(). Without the plugin, a game writes the scene’s layer itself, or declares its own weatherRule, as a kit that rebuilds the weather does.

The look’s rain, snow and snowCover set how the rain and the snow draw, each field Luminati’s when left out. Its 2,600 streaks fall at 15 m/s through a box 44 m by 16 m by 44 m that follows the camera’s focus. The wind carries them 0.9 of its speed, and they draw #c4d8f6 at 0.5. rain: false draws none, for a game that draws its own from useLookWeather().rainShare.

The snow’s 2,400 flakes fall at 1 m/s, each 0.09 m to 0.15 m across, through a box 44 m by 15 m by 44 m. Each flake sways 0.35 m each way over 5.7 s, and the wind carries it 0.45 of its speed. A flake is 4 by 4 pixels, white at 0.95. snow: false draws no flakes, for a game that draws its own from useLookWeather().snowShare.

The lying snow takes snowCover.color, #e7ecf6, and a card’s top whitens to snowCover.foliageShare, 0.72. snowCover: false lays none, for a game that draws its own cover from useLookWeather().snowCover. The quality level’s precipitationShare draws a share of the drops and the flakes: all at High, 0.75 at Medium, 0.4 at Low and 0.25 at Minimum. A quality change draws fewer of the same drops and compiles nothing.

The frame cost is not profiled yet.