Birdoggydog's Builds

Swimming in deep water outdoors in Godot

Lakes and the sea are swum at half pace with no fighting. A water query for the rules, a float, a haul out over steep banks and a 40 m limit at sea.

The party swims in deep water outdoors now. Up to 1 m of water is waded at walking pace, and deeper than that the party floats, moves at half pace and can’t run, jump or fight. A swimmer pushing at a bank that’s too steep to walk hauls itself out, and the sea turns the party back 40 m from ground it can stand on.

57 seconds on the island, captioned. A lake waded and then swum, an attack refused, the crater lake, then the inner sea out to where it turns the party back. I recorded it before the last two small edits, so it shows neither.

Why does the game need a rule for water?

The game had no rule for water. The party dropped through a lake to its bed and walked on it. The crater lake’s banks are steeper than the 42 degree slope limit all the way around, so a party that got into it was stuck there until I loaded a save.

So the game had to answer whether deep water outdoors is a wall, scenery or something you swim in. I said “you should be able to swim in water even outdoors.” The rule I agreed to was that lakes and rivers are swum at about half walking speed, there’s no fighting while swimming, a current doesn’t push yet, and the sea turns the party back a short way out.

I had an agent build it on a separate branch, with the rules in one new script, so the party’s script only needed a few calls. Whether the party is swimming is worked out on every physics step from where it is, and never from a key.

How does the game know where the water is?

The water was drawn, but nothing in the rules could ask how high the water is at a point. So a small query class, WaterSurface, came first. It draws nothing. It treats every mesh in the world scene that uses the water shader as water, reads each one the first time it’s asked about, and keeps the result as meta on the WorldScene. There are three kinds of water.

  • A flat sheet is a PlaneMesh, like the sea or a pond. The class keeps a rectangle and a level for it.
  • A lake is a mesh over an outline. The class keeps its real triangles, sorted into 16 m buckets, so a point is tested with Geometry2D.point_is_inside_triangle against only the triangles in its bucket.
  • Running water is a WaterCourse. It’s asked through its level_at(), and only inside its rectangle, which is the ends of its sections grown by 2 m.

I used a lake’s triangles and not its bounding rectangle because the generator leaves the water out inside a lift head’s walls, and a rectangle would put a lake there. Where two waters overlap, the higher one is the surface.

The class has four calls. level(at) gives the surface over a point or NAN, and there’s also depth(at), is_sea(at) and near_land(at, level, reach, wade, step). I needed depth() because a sheet reaches into its bank so that no edge shows, which means a surface above a point doesn’t prove the point is under water. Depth is the surface minus WorldScene.height_at(). Where there’s no ground, like a hole or off the terrain, there’s no depth and nothing is swum. A query costs about 25 microseconds.

The island’s rivers and creeks are sheets made from sections with no level to ask, so they aren’t swum yet.

Floating without sinking or walking on the bed

The wade depth is 1.0 m. Water up to that depth is walked through like ground, with the same pace, run, jump and slope rule. The party is afloat when it’s outdoors over water deeper than that. A swimmer that’s already afloat stays afloat down to 0.9 m, so the edge doesn’t flicker. The feet float 1.0 m under the surface, the same as the wade depth, so the body doesn’t drop or jump where wading turns into swimming.

While it’s afloat, Party._physics_process calls PartySwim.afloat() in place of the slope rule. It keeps the level part of the velocity and replaces the vertical part with a lift toward the floating height, clamped to 3.0 m/s:

var lift := clampf((surface - DRAFT - at.y) / maxf(delta, 0.001), -RISE, RISE)
return Vector3(level.x, lift, level.z)

So the body doesn’t sink, doesn’t walk on the bed whatever the bed does under it, and no weight builds up. A drop into deep water ends at the surface and does no fall damage. The pace is capped at half the walking speed, which I measured as 2.5 m/s swimming against 5 m/s on land and wading. The run key does nothing and there’s no jump.

The eye sits 0.35 m over the surface and bobs 0.045 m, 0.55 times a second. It eases down when the party starts floating and back up on land, and it never goes under. The lowest I measured was 0.27 m over the water. The bob counts game time, so it follows the game clock. Footsteps stop while swimming because the sound system only plays them for a body on a floor, and nothing replaces them yet.

Why can’t the party fight while swimming?

No fighting while swimming was part of the rule I agreed to. Party.attack() asks PartySwim.refuses() first and returns 0 if it’s true, so there’s no blow, no shot and no spell, and nothing is spent.

A line under the crosshair says “SWIMMING // half pace // no hand free to fight” in amber while the party is afloat. A refused attack replaces it for 2.2 seconds with “NOT AFLOAT. EVERY HAND IS HOLDING THE MEAT UP.” and a tick sound. The same refusal shows at most once every 1.5 seconds. These lines are placeholders.

Hauling out over a bank too steep to walk

The first idea for getting out of the crater lake was to change the land. I ruled that out, because the rule had to do it and not the terrain. So a swimmer pushing at a steep bank looks along the push for ground that would hold the party, and hauls itself out onto it.

The probe steps along the push 0.25 m at a time, out to 4.0 m, and reads the ground at each step.

  1. If the ground is more than 1.5 m over the surface, it gives up.
  2. If the water there is still deep, it carries on, but only within 1.0 m of the swimmer. Past that it gives up, so nobody hauls out across open water.
  3. If the slope there is steeper than the walk limit, it notes that it met something steep and goes on.
  4. Otherwise, if the ground has no more than 0.3 m of water on it, isn’t sliding, and something steep was met on the way, that’s the landing, as long as the standing body fits there.

If nothing steep was met, it gives up, because that bank can just be walked up. The fit test is an intersect_shape query with a capsule a little smaller than the standing body, against the party’s collision mask.

The haul takes 1.2 seconds of play. The party moves horizontally on a smoothstep and vertically on a smoothstep that finishes at 0.7 of the time, so the body goes up first, clears the lip and then goes over. The party can do nothing else until it’s standing on the bank. The 1.0 m reach and the 1.2 seconds are there so the haul isn’t a free teleport.

Why does the sea turn the party back?

The sea is a flat sheet at the world’s sea level, and it’s swum like a lake. Every 0.25 seconds of play the game looks along 16 bearings, every 4 m out to 40 m, for ground no more than wade depth under the surface. If it finds none, the party is too far out, and the nearest ground it found earlier is the shore.

While the party is too far out, the part of each stroke that points away from the shore is subtracted, and the part along the shore or back toward it is kept. The line “NO FARTHER. THE SEA KEEPS WHAT SWIMS OUT. TURN BACK.” shows under the crosshair. So it isn’t a wall at the shoreline. The party swims freely until it’s 40 m from ground it can stand on, and any stroke back works.

I measure the distance from ground the party can stand on and not from the water’s edge. That way the limit follows the shape of the shore, and a shallow bank a long way out counts as ground. Off a beach of the inner sea the party was stopped at 41.0 m, 22 seconds from the beach. If the party is put down far out with no shore remembered, like after a load, a second look runs out to 4,000 m in 40 m steps to find the way back.

Keeping walking monsters out of deep water

A walking monster doesn’t step to a point just ahead of it if the water there is deeper than wade depth. It stops at the edge and gives up the chase like it does at a wall. In the test a walking monster stopped where the water was 0.70 m deep at most. Flyers aren’t stopped. A monster that shoots still shoots from the edge, and I’ll decide whether that’s fair after playing it.

One of the two edits after the film was for a monster that’s already in deep water. It can step toward ground where the water is shallower than where it’s standing, so it isn’t trapped. The other edit made the haul out probe cheaper.

Getting out of the crater lake

The lake that trapped a party lets it out now. A party dropped off the path floats 0.9 seconds after the drop, unhurt, over 13 m of water. It swims 47 m in 19 seconds to the nearest low place of the rim, and hauls out 0.76 m up and 2 m along onto the strand that runs all the way around the lake under the rim. At 5 of 180 bearings the strand can be walked right up to the rim’s crest.

It isn’t finished. From the strand there’s no way back up to the path, the yard or the lift, because the deck is 4 m over the water and a haul only lands on ground. Over the crest, the outer flanks still slide a party down to the volcano’s foot.

What’s saved, and what’s left?

A save has one new section with one value, whether the party is swimming. A save from before swimming loads with the party on its feet. The test has 68 checks, runs on the island and keeps its own save folder. The island test’s lake part used to check that the party stands on the bed, and now it checks that the party floats and gets out. One check in that test fails without this change too, and I haven’t traced it.

What’s left is a sound for swimming, real wording for the lines, the island’s rivers and creeks, and a way back up from the crater’s strand.