Steve's Roc Apps roc-apps on GitHub

Safari Ride Chapters

Chapter 27

The rider

Where the rider is depends on where it was a step ago, so the ride carries the rider in the moment and moves it one step at a time. The rider is the larger of the two stepped things, and these modules do its stepping, and the truck's:

RideInstant Rider  steps the rider TruckJourney  steps the truck RiderMind  asks for its controls Motorcycle  moves the bike RiderEyes  moves the eyes VehicleLimits  keeps to the limits Throttle  asks for an acceleration LeanSearch  asks for a lean Gaze  asks where to look  keeps to the limits Coast  coasts for the shoulder  coasts each try  steps the pretend bike RideInstant Rider  steps the rider TruckJourney  steps the truck RiderMind  asks for its controls Motorcycle  moves the bike RiderEyes  moves the eyes VehicleLimits  keeps to the limits Throttle  asks for an acceleration LeanSearch  asks for a lean Gaze  asks where to look  keeps to the limits Coast  coasts for the shoulder  coasts each try  steps the pretend bike
The modules that do the stepping

A box is a module, and an arrow points to a module it calls. This chapter explains the rider and its motorcycle. The throttle, the lean search and the gaze each get a chapter of their own after it, and the truck's chapters follow.

A rider is two parts: a motorcycle, and a pair of eyes.

Rider.roc, in Rider
# ...
Rider := { motorcycle : Motorcycle, eyes : RiderEyes }.{
# ...

Both fields are nominal records of their own, and between them they hold six values:

Part Field Type What it is
motorcycle route_place RoutePlace which segment, and where on it, along and across
yaw Angle.Turn which way the bike points
speed Speed how far it goes in a step
lean Angle.Lean how far it leans
eyes gaze_yaw Angle.Turn where the rider is looking
look_depth Fraction how deep the last look went

Three of the six are angles, of two types: a Lean is the bike's tilt, apart from a Turn, so passing one where the other belongs fails to compile, as a bearing and a turn did in the “Mountains” chapter.

The rider's step §

The step works in order, and the code reads that way: decide, move, cross a bend or stop, then choose where to look:

Rider.roc
## The rider a step on along `route`, with the encounter on their segment at
## `encounter_stage`: the move, the crossing onto the next segment, the stop
## at the route's end, and then where the rider looks.
rider_at_next_step : Rider, { route : Route, encounter_stage : Animation.Stage } -> Rider
rider_at_next_step = |rider, { route, encounter_stage }| {
	segment = rider.motorcycle.route_place.segment_on(route)
	moved = rider.motorcycle.motorcycle_at_next_step(
		RiderMind.controls(rider.motorcycle, { segment, encounter_stage }),
	)
	motorcycle =
		match segment.segment_ending() {
			Bends({ bend, .. }) => moved.cross_bend_if_on_road(bend)
			RouteEnds => stop_at_route_end(moved, segment)
		}
	gaze_target = RiderMind.gaze_target(motorcycle, motorcycle.route_place.segment_on(route))
	{ motorcycle, eyes: rider.eyes.eyes_at_next_step(gaze_target) }
}

The match on the segment's ending has two tags. Bends({ bend, .. }) takes the payload's bend field. In a pattern, .. stands for the rest of a record's fields.

Deciding §

Every decision the rider makes belongs to RiderMind:

RiderMind.roc
## The lean toward the best line, and the acceleration the throttle gives
## the motorcycle once it leans so, on `segment`, the one it is on, with the
## segment's encounter at `encounter_stage`.
controls : Motorcycle,
{
	segment : Segment,
	encounter_stage : Animation.Stage,
} -> Motorcycle.Controls
controls = |motorcycle, situation| {
	lean = LeanSearch.best_lean(motorcycle)
	{ lean, acceleration: Throttle.next_acceleration(motorcycle.with_lean(lean), situation) }
}

A decision is the bike's controls, a lean and an acceleration:

Motorcycle.roc
## What the rider sets for a step: the lean the motorcycle takes, and how
## hard it accelerates.
Controls : { lean : Angle.Lean, acceleration : Acceleration }

Look at the last line of controls. controls passes Throttle.next_acceleration a pretend motorcycle: this motorcycle as it would be at the new lean. Two small helpers serve such pretend motorcycles:

Motorcycle.roc
with_lean : Motorcycle, Angle.Lean -> Motorcycle
with_lean = |motorcycle, lean| { ..motorcycle, lean }
Motorcycle.roc
## The controls that keep this motorcycle's lean and speed as they are.
steady_controls : Motorcycle -> Motorcycle.Controls
steady_controls = |motorcycle| { lean: motorcycle.lean, acceleration: Acceleration.steady }

{ ..motorcycle, lean } is a record update with a shorthand field: the field and the argument share the name lean. with_lean makes a motorcycle as it would be at another lean, and the real motorcycle stays as it was. steady_controls makes the controls that keep the lean and the speed as they are. The rider's steering, in the “Variables and loops: rehearsal” chapter, uses both.

One step of physics §

A lean of one degree turns the bike by a tenth of a degree each step, so at a metre a step it goes round a circle with a radius of 573 metres. That's nearly all the physics. Here's the motorcycle:

Motorcycle.roc, in Motorcycle
# ...
Motorcycle := {
	route_place : RoutePlace,
	yaw : Angle.Turn,
	speed : Speed,
	lean : Angle.Lean,
}.{
# ...

The step is the whole of the physics, in order:

Motorcycle.roc
## The motorcycle one step on, at the lean its controls set and the speed
## their acceleration gives, held to the top speed.
motorcycle_at_next_step : Motorcycle, Motorcycle.Controls -> Motorcycle
motorcycle_at_next_step = |motorcycle, { lean, acceleration }| {
	speed = allowed_speed(motorcycle.speed.accelerated_by(acceleration))
	step_length = speed.step_length()
	turn = lean.steer(turn_per_lean)
	mid = motorcycle.yaw.plus(turn.times(0.5)).to_radians()
	place = motorcycle.route_place
	{
		route_place: {
			..place,
			road_point: {
				along: place.road_point.along.plus(step_length.times(mid.cos())),
				across: place.road_point.across.plus(step_length.times(mid.sin())),
			},
		},
		yaw: motorcycle.yaw.plus(turn),
		speed,
		lean,
	}
}

It works in three parts:

  1. The speed. accelerated_by adds the acceleration to the speed, and allowed_speed holds the result to the top speed.
  2. The turn. lean.steer(turn_per_lean) turns the lean into a change of direction, a Turn, and that's the only way a lean changes where the bike points.
  3. The move. The bike goes one step's length in the direction halfway through the step's turn, mid. along.plus(…) adds a Distance to a place along the road, as Clock.plus adds a length to a moment. The new place is a record update of route_place, inside a new motorcycle.

The bike turns the same amount per step at any speed. That's a cheat, chosen by eye, and it holds up because the rider chooses each step's lean by trying it out on this same physics.

The end of a segment §

The end of a segment uses a small version of the same trying out. cross_bend_if_on_road makes a motorcycle carried past the bend onto the next segment. If that motorcycle is on the road, it's the answer, and if it's off the road, the old one is:

Motorcycle.roc
## This motorcycle carried over `bend`, the bend out of its segment, when
## that puts it on the next segment's road: placed on that segment and turned
## to its axis. Short of that, it stays where it is.
cross_bend_if_on_road : Motorcycle, Geom.Bend -> Motorcycle
cross_bend_if_on_road = |motorcycle, bend| {
	on_next = motorcycle.route_place.route_place_past_bend(bend)
	if on_next.is_on_road() {
		{
			..motorcycle,
			route_place: on_next,
			yaw: motorcycle.yaw.minus(bend.turn),
		}
	} else {
		motorcycle
	}
}

At the end of the road, the throttle brakes the bike to rest. A last step can still carry it past the end, so stop_at_route_end holds it there with Motorcycle.stopped_at, from the “Records” chapter.

The picture leans too §

When the bike leans, the camera rolls with it, and the whole picture leans:

RiderCamera.roc
## How far the canvas rolls with the rider's `lean`: not at all inside the
## deadband, and with the lean past it.
rider_roll : Angle.Lean -> Angle.Lean
rider_roll = |lean|
	if lean.abs() < roll_deadband {
		Angle.upright
	} else {
		lean
	}

A lean inside roll_deadband, a thousandth of a radian, leaves the picture level, and any other lean passes straight through. The camera turns as well: the rider's head follows the gaze and turns a little into a bend, and the camera adds the head's turn to the motorcycle's yaw, as the “The rider's gaze” chapter shows.

So for each step, RiderMind.controls returns a lean and an acceleration, and motorcycle_at_next_step turns them into a new place on the road. The next chapter shows how the throttle chooses the acceleration. The “Variables and loops: rehearsal” chapter shows how the rider chooses the lean.