flutter3d_sim 0.6.0
flutter3d_sim: ^0.6.0 copied to clipboard
The simulation: a fixed step, an ECS, levels, navigation, saves and replays. Plain Dart, so it runs on a server.
0.6.0 #
- A floor, and no code. The step, the ECS, levels, navigation, saves and
replays are byte for byte 0.5.2's. The one line that changed is the floor on
flutter3d_physics, now^0.6.0, and it is stated for the reason it was stated before: ground is split across the two packages —Heightfieldhere is the data,CollisionHeightfieldthere is what a body stands on — so a resolver free to reach further back would hand a caller the first without the second. - Still plain Dart. Nothing here imports Flutter, and a server replaying a run needs no SDK to do it.
0.5.2 #
Ground made of samples, and a crowd that walks over it.
Heightfield: the first sloped ground this level format has. ABrushis a box, so until now the only slope a level could describe was the ramp a wedge makes. A field iscolumns * rowsheights,cellSizemetres apart, with anoriginwhere sample(0, 0)sits, and it answersheightAt,normalAtandslopeAtabout the ground rather than building anything. The answers are about the triangles that are drawn, not a bilinear sheet. Four samples make a quad and a quad is two triangles, so a quad is only flat when its corners agree; interpolating bilinearly instead describes a surface nobody draws, and at the centre of a cell the two differ by a quarter ofh00 + h11 - h10 - h01— a unit hovering over one half of the quad and sunk into the other. So the field finds the triangle the point is in. The split is fixed at(0,0)–(1,1)and written down once, because a mesh builder that chose the other diagonal would draw ground this class does not describe and the only symptom would be a body standing slightly in the air.slopeAtreports radians from flat and refuses to say what is walkable — a tank and a scout disagree about the same hillside — and it reaches forPortable.atan2rather thanmath.acos, becauseacosis the platform's libm and the rule a step asks no machine for an answer is what keeps a run verified in a browser agreeing with the run a player made.- A level can carry one.
Level.heightfieldis an optional section, read and written byHeightfield.fromJson/toJson. The heights travel as base64 of aFloat32List's bytes, the wayLevelVisibilitycarries its cells: sixteen thousand samples spelled out as JSON digits is a megabyte nobody reads and every editor reformats. The four numbers a person might edit by hand stay plain. HeightfieldGeometryemits aBrushSurface, not a new type. Terrain is not a brush, but what the type holds is plain arrays, and the twenty lines influtter3d_bridgethat interleave them into a vertex layout do not care where the triangles came from — so ground draws with no new code downstream. Its normals are averaged from central differences whileHeightfield.normalAtreturns the triangle's own: one answers what the ground looks like, the other what a body is standing on, and the disagreement is the point.NavGrid.bakeHeightfield: a second source for the same lattice.bakemeasures its grid from brushes and stamps each cell with the solid under it; ground made of samples has no brushes and its extent is the field's own, so the two share the cell format and nothing else. Steepness is what makes ground unwalkable here —maxSlopedefaults to 0.698 radians, a hair under forty degrees — withblockedfor the ground refused for a reason that is not its shape. The step height is derived from the slope rather than taken from a level. On terrain the rise between neighbouring cells is not a ledge, it is the hillside the slope test just allowed, so the default is the tallest rise the steepest walkable cell can have:tan(maxSlope) * cellSize * 1.001, which is 0.42 m at the default half-metre cell. Left at the 0.4 a level's bake uses, a two-metre grid over ground of one part in five puts a rise of 0.4 against a limit of 0.4 and lets float rounding decide: 112 of 240 uphill moves survived the comparison and the rest were called walls. The derived height allows all 240, and the half-metre bake all 4032 of its own.
0.5.1 #
Portable: the transcendental functions a step is allowed to call.sin,cos,sinCos,tan,atan,atan2,asinandexp, built out of+,-,*,/,sqrtand the bytes of a double — all of which the specification pins — so two platforms cannot disagree about them. That is not a theoretical worry:parity_test.dartswept twelvedart:mathfunctions over twenty thousand arguments under the VM and under Chrome, and onlysqrtandpowgave the same bits. A car built on the rest replayed differently in a browser at twenty-three checkpoints of forty, which is a verifying server that cannot verify. It is forty of forty now. Accuracy is held to two units in the last place againstdart:mathbyportable_math_test.dart, because portable and wrong is a physics bug no parity test could report.solver_parity_test.dart: the rigid-body solver replays bit for bit, in a browser and on the VM. Predicted —flutter3d_physicscalls no transcendental — and measured anyway, because the three ways it could still have diverged are the broadphase's ordering, a long chain of non-associative additions, and a browser'sintbeing adoubleunder the spatial grid.Motion.easeFactor,Interpolatedand the actor system's facing now call it rather thandart:math.tool/structure.dart's new rule a step asks no machine for an answer is what keeps them there.
0.5.0 #
Breaking. A step can say what happened, and four types stop being closed.
GameEventandGameEvents: what a step did, drained by whoever owns it. A buffer rather than a stream, because a stream delivers on a later microtask and two simulations here reproduce a recorded run exactly — an event arriving between two steps is the one state no replay can reproduce.ActorSystemwrites into the simulation's buffer at the moment of a death, so a monster killed by this step's shot lands after the shot that killed it; two lists read afterwards can only say that both occurred.ActorHurtis now the event rather than a value copied into one, andActorDiedsays who caused it.StepEvents.hasand.countanswer the two questions every reader asks of a drained step.Difficulty: four axes a genre applies where it decides. What the player is hurt by, what their attacks are worth, how quickly the opposition reacts, and how much of the genre's help is on. A value class rather than an enum, so a game writes its own — or builds one from a slider, which a list of four cannot express.opponentReactionis a duration, so the harder settings have less of it.ActivationOutcomeis open, andabstract base. Nothing ever switched over its three cases exhaustively, so sealing bought nothing and cost a game the ability to say what happened at its own door.StepSystemtakes aStepContext. A function type is frozen the day it is published; the context carriesdtand the phase, which the old shape could not, and can grow a field instead of breaking every system.Difficulty,Powers,ScoringandRunStats. Four pieces every genre wanted and at most one of them had written. Powers came out of the shooter, which keeps every question it answered and delegates the counting. Scoring is the question a tally cannot answer — what those counts were worth, and whether they came close enough together to be worth more. RunStats counts events, and counts nothing until a game says what to recognise.CharacterController.groundNormal— it measured the floor's normal to decide whether the body was standing on it and threw it away, so nothing above could tell a flat floor from a ramp.
0.4.2 #
- A brush can say how it casts, not only whether.
shadowCastingin the document is one ofon,off,doubleSidedorshadowsOnly— the engine has had four modes sinceShadowCastingModewas written and the format had two, so the two it could not ask for were the two it most needed: both faces recorded, for a wall one brush thick whose lit side and dark side are a metre apart, and a proxy that casts without being drawn.Brush.castsShadowis now the two-state view ofBrush.shadowCastingand goes on meaning what it meant, an unknown word is refused with the four in the message, and a document that said nothing goes on saying nothing. Surfaces are batched by the mode rather than by the boolean, because a batch is the smallest thing that can answer. - A breach keeps the baked light on the walls it did not touch.
Breaches.originssays which authored brush each current brush was cut out of, andBrushGeometry.build(origins:)uses it to find the planned face a piece's face is part of and measure its place inside it —LightmapLayout.uvOfPointandisOnPlaneare that arithmetic. Before it, redrawing a level after one hole meant no atlas at all and every wall in every room fell back to flat ambient at once. The faces the blast itself made take the neutral texel, which is the one part of a breached wall nothing ever baked. - A level can ask to be reflected.
EntityTypes.reflectionProbeis the format's word for a point a room is reflected from, andReflectionProbeKindis the kind that validates one:radius,intensity,faceSize,levels,nearandfarare all optional and each is refused where the renderer would otherwise assert on it at load. The two planes are resolved against the probe's own defaults before either is judged, so a document naming only a near plane past two hundred metres is refused here rather than at load: the far plane it did not name is still a far plane. Pure data to the simulation — nothing spawns and nothing is revealed — and a word a game has to put in its own vocabulary, so a game without probes reads the entity as unknown rather than growing a reflection it did not ask for.
0.4.1 #
- Lightmaps.
LightmapLayoutunwraps every visible brush face onto a planar atlas from the level alone, so the baker and the geometry agree without a table;LightmapBakerbakes the light the walls throw on each other by gathering — direct light with shadows through the level's own collision world, then bounces along cosine-weighted rays — seeded by the texel, so two bakes are the same bytes.Lightmapstores RGBM in RGBA8 with a hash of the brushes, lights and materials, anddart run flutter3d_sim:bake_lightmapwrites<level>.lightmap.bin.BrushGeometry.build(lightmap:)hands every vertex its second coordinate. - Jump links.
NavGrid.bake(jumps:)finds the gaps and ledges aJumpReachclears; aFlowFieldfilters them by its own body's reach with the body's width added, relaxes them backwards at their distance plus two cells so a walk of equal length wins, andjumpAtsays when the next step is a jump.Navigation.jumpAheadandActorSystemtake off through the controller's buffered request on the take-off cell. Mind.heading, for turning to face the way the field said.
0.4.0 #
- First release. It is
flutter3d_game's inside, moved out whole: the fixed step, the entity store, the level format and its validator, saves, demos and the rewind buffer, world logic, actors, navigation, the camera rig and the maths. Nothing changed behaviour; the imports moved and the package boundary is new. - Plain Dart, and that is the reason it exists. A server that verifies a
submitted run has to replay it through the same simulation the player ran,
and a Flutter SDK in that container is a blocker rather than an
inconvenience.
flutter3d_gamekeeps the eight files that reached Flutter — the touch and keyboard widgets, theMediaQueryread, the diagnostics sink — and re-exports this package, so no existing program changes a line. - The boundary is a check, not a comment:
the simulation names no Flutterintool/structure.dartscanslib/,test/andbin/. StateDigestandDigestTracecome with it — a 32-bit digest over the bits of a snapshot, computed the same way in a browser as in the VM, and a checkpoint trace that names the first step two runs disagree at.dart run flutter3d_sim:bake_visibilitymoves here fromflutter3d_game, where it had never needed Flutter either.