Simulating movement with ES6 generators

A more complex and practical example to infinite collections

Author's image
Tamás Sallai
4 mins

Motivation

One particularly good example to infinite collections and ES6 generators is movement simulation. This requires a possibly complex calculation of the points along the path, and you need to take care of a few additional cases like collision detection and eventual termination. In a traditional way, you might use a while-loop that takes care of all the necessary parts. This results in an intermingled function that does many things. Using ES6 generators, you can nicely separate the different concerns, making the code easier to understand and maintain.

This post is intended only to give an example on how to use infinite collections for a more complex use case. It is not meant to give a thorough guide on every aspect of movement simulation.

Typical parts

There are a few typical building blocks when you want to calculate movement. You might want to

  • calculate the series of points that forms the path
  • control when to terminate (for example collision detection)
  • protect against infinite loops
  • emit the points at a lower resolution for display

Example

Live demo and full source are available.

For an example to this post, I'll use a simple gravitational simulation. The user can hover over the canvas to set the initial velocity of a rock, then the path is calculated based on the gravitational pull of three planets. If the rock crashes into a planet or the infinite loop protection kicks in (after 3000 points) then the simulation terminates. Also because there is no need to have subpixel-level precision, only the points that moves at least a pixel are emitted.

Using a while-loop

Without using an infinite collection, you might use a while-loop. In this function, all concerns are mixed, and it is quite hard to reuse this function.

const generatePath = (vx, vy, gravityFn) => {
	// Contains the results
	const result = [];
	const vc = 0.1;

	// The current position and velocity
	let position = {
		x: 150,
		y: 150
	}

	let velocity = {
		x: vx,
		y: vy
	}

	// How many points we've calculated so far
	let numPoints = 0;

	// Does the ball crashed to a planet?
	const isNotCrashed = () => {
		return holes.every((hole) => {
			return dist(hole, position) >= 3.5;
		})
	};

	// Keeps track of the last outputted position so that we know when we need to output another one
	let lastPosition = undefined;

	// Run until not crashed and not limited
	while (numPoints++ < 3000 && isNotCrashed()) {

		// Calculate the next position
		const {
			ax,
			ay
		} = gravityFn(position);
		velocity = {
			x: velocity.x + ax,
			y: velocity.y + ay
		}

		position = {
			x: position.x + velocity.x * vc,
			y: position.y + velocity.y * vc
		}

		// Calculate whether we should output a new point
		const shouldDraw = !lastPosition || dist(lastPosition, position) >= 1;
		if (shouldDraw) {
			lastPosition = position;
			result.push(position);
		}
	}
	return result;
}

Using an infinite collection

If you are using an infinite collection, then the different parts can be separated. The path calculation does not need to know about neither stop conditions nor display resolution. It just emits the path infinitely.

const pathGenerator = function*(vx, vy, gravityFn) {
	const vc = 0.1;
	let position = {
		x: 150,
		y: 150
	}

	let velocity = {
		x: vx,
		y: vy
	}

	while (true) {
		const {
			ax,
			ay
		} = gravityFn(position);
		velocity = {
			x: velocity.x + ax,
			y: velocity.y + ay
		}

		position = {
			x: position.x + velocity.x * vc,
			y: position.y + velocity.y * vc
		}

		yield position;
	}
}

Using the generator function from above, we can then fit the other parts in:

  • limit the number of points, so that it never results in an infinite loop
  • add a stop condition
  • lower the display resolution

The first part uses the limit operation. The order is important; if you put it to a later stage, it can easily result in an infinite loop, which we aim to prevent. 3000 is an arbitrary number, the exact value is not that important.

The stop condition is implemented using takeWhile. It is somewhat similar to the filter, but terminates once the predicate is false. It ensures that the simulation finishes when the trajectile is crashed into a planet.

Resolution conversion is done with a simple filter. It stores the last emitted point internally, and compare the stream to see if is displaced by at least a pixel. If not, then the point is filtered out.

This example uses the gentoo library, but you can use any others you like that provides these functions.

const newPath = gentoo.chain(pathGen)
	.limit(3000)
	.takeWhile((point) => {
		return holes.every((hole) => {
			return dist(hole, point) >= 3.5;
		})
	})
	.filter((() => {
		let lastPoint = undefined;
		return (point) => {
			const result = !lastPoint || dist(lastPoint, point) >= 1;
			if (result) {
				lastPoint = point;
			}
			return result;
		}
	})())
	.value()

Closing remarks

Infinite collections are useful only in a handful of situations and I've found that path calculation is a great example. They allow better separation, readability, and cleaner code overall over the traditional while-loop way, all without sacrificing performance.

June 14, 2016
In this article