mirror of
https://github.com/nature-of-code/noc-book-2
synced 2024-11-17 07:49:05 +01:00
1586 lines
No EOL
138 KiB
HTML
1586 lines
No EOL
138 KiB
HTML
<section data-type="chapter">
|
||
<h1 id="chapter-5-autonomous-agents">Chapter 5. Autonomous Agents</h1>
|
||
<div class="chapter-opening-quote">
|
||
<blockquote data-type="epigraph">
|
||
<p>“Life is a journey, not a destination.”</p>
|
||
<p>— Ralph Waldo Emerson</p>
|
||
</blockquote>
|
||
</div>
|
||
<div class="chapter-opening-figure">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_1.png" alt="NOAA photo library, public domain. ">
|
||
<figcaption><strong>NOAA photo library, public domain.</strong></figcaption>
|
||
</figure>
|
||
<p><strong>The Mo‘i Fish</strong></p>
|
||
<p>Six-finger threadfins (<em>Polydactylus sexfilis</em>), also known as “fish of kings” or <em>mo</em>‘<em>i</em> in Hawaiian, are shown swimming in a shoal. The mo‘i fish held a special status for Hawaiian royalty and were raised in dedicated ponds to ensure their population growth and prevent their extinction. The fish display a delicate and coordinated dance in their collective movement, with each individual mo‘i subtly influencing and being influenced by its neighboring fish.</p>
|
||
</div>
|
||
<p></p>
|
||
<p>So far I’ve been demonstrating inanimate objects, lifeless shapes sitting in the canvas that flop around when affected by forces in their environment. But this is <em>The </em><strong><em>Nature</em></strong><em> of Code</em>. What if I could breathe life into those shapes? What if those shapes could live by their own rules? Can shapes have hopes and dreams and fears? These sorts of questions are the domain on this chapter. They’re what separate unthinking objects from something much more interesting: autonomous agents.</p>
|
||
<h2 id="forces-from-within">Forces from Within</h2>
|
||
<p>An <strong>autonomous agent</strong> is an entity that makes its own choices about how to act in its environment, without any influence from a leader or global plan. In this book, “acting” typically means moving. For example, instead of simulating a particle that’s passively drawn toward or repelled by another shape due to a force like gravity, I’d now like to design an entity that has the ability—or even the “desire”—to make decisions about its movements. It could <em>decide </em>to move toward or away from a target, like a moth drawn to a flame, or a small fish evading a predator.</p>
|
||
<p>The switch from inanimate objects to autonomous agents is a significant conceptual leap, but the code base itself will barely change. The “desire” for an autonomous agent to move is just another force, like the force of gravity or the force of the wind. It’s just that now the force is coming <em>from within</em>.</p>
|
||
<p>Here are three key components of autonomous agents to keep in mind as I build this chapter’s examples:</p>
|
||
<ul>
|
||
<li><strong>An autonomous agent has a limited ability to perceive its environment.</strong><strong> </strong>It makes sense that a living, breathing being should be aware of its environment. However, this awareness doesn’t just refer to the external environment but also to the agent’s internal state—its position, velocity, and potentially other properties or even simulated emotions. Throughout the chapter, I’ll explore ways agents can take their own state into account when making decisions. I’ll also cover programming techniques for objects to store references to other objects and therefore “perceive” their surroundings. It’s important to consider the word <em>limited</em> here. Are you designing an all-knowing circle that flies around a canvas, aware of everything else in that canvas? Or are you creating a shape that can only examine other shapes within 15 pixels of itself? Of course, there’s no right answer to this question; it all depends on what you want. I’ll explore several possibilities throughout this chapter, but in general, limitations are a good thing for a simulation to feel more “natural.” An insect, for example, may only be aware of the sights and smells that immediately surround it. To model a real-world creature, you could study the exact science of these limitations. Luckily, I can just make stuff up and try it out.</li>
|
||
<li><strong>An autonomous agent processes the information from its environment and calculates an action.</strong> This will be the easy part, as the action is a force. The environment might tell the agent that there’s a big, scary-looking shark swimming right at it, and the action will be a powerful force in the opposite direction.</li>
|
||
<li><strong>An autonomous agent has no leader.</strong> This third principle is something I care a little less about depending on the context. For example, if you’re designing a system where it makes sense to have a leader barking commands at various entities, then that’s what you’ll want to implement. Nevertheless, many of the chapter’s examples will have no leader for an important reason: toward the end of this chapter, I’ll examine group behaviors and look at designing collections of autonomous agents that exhibit the properties of <strong>complex systems</strong>. These are intelligent and structured group dynamics that emerge not from a leader, but from the local interactions of the elements themselves.</li>
|
||
</ul>
|
||
<p>There are many places where I could start my exploration of autonomous agents. Artificial simulations of ant and termite colonies are fantastic demonstrations of systems of agents, for example. (For more on this topic, I encourage you to read <em>Turtles, Termites, and Traffic Jams</em> by Mitchel Resnick.) However, I want to begin by examining agent behaviors that build on the work in the first four chapters of this book: modeling motion with vectors and forces. And so I’ll return to the book’s ever-changing hero class—once <code>Walker</code>, then <code>Mover</code>, then <code>Particle</code>—and give it yet another incarnation.</p>
|
||
<h2 id="vehicles-and-steering">Vehicles and Steering</h2>
|
||
<p>In the late 1980s, computer scientist <a href="http://www.red3d.com/cwr/">Craig Reynolds</a> developed algorithmic <strong>steering</strong> behaviors for animated characters. These behaviors allowed individual elements to navigate their digital environments in a “lifelike” manner, with strategies for fleeing, wandering, arriving, pursuing, evading, and more. Later, in his 1999 paper “Steering Behaviors for Autonomous Characters,” Reynolds uses the word “vehicle” to describe his autonomous agents. I’ll follow suit calling my autonomous agent class <code>Vehicle</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> class Vehicle {
|
||
|
||
constructor(){
|
||
this.position = createVector();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
}
|
||
|
||
/* What else do I need to add? */</pre>
|
||
<p>Like the <code>Mover</code> and <code>Particle</code> classes before it, the <code>Vehicle</code> class’s motion is controlled through its position, velocity, and acceleration vectors. This will make the steering behaviors of a single autonomous agent quite straightforward to implement, and yet by building a system of multiple vehicles that steer themselves according to simple, locally based rules, surprising levels of complexity emerge. The most famous example is Reynolds’s “boids” model for flocking or swarming behavior, which I’ll demonstrate later in the chapter.</p>
|
||
<div data-type="note">
|
||
<h3 id="why-vehicles">Why “Vehicles”?</h3>
|
||
<p>In his 1986 book <em>Vehicles: Experiments in Synthetic Psychology</em>, Italian neuroscientist and cyberneticist Valentino Braitenberg described a series of hypothetical vehicles with simple internal structures, writing, “This is an exercise in fictional science, or science fiction, if you like that better.” Braitenberg argues that his extraordinarily simple mechanical vehicles manifest behaviors such as fear, aggression, love, foresight, and optimism. Reynolds took his inspiration from Braitenberg, and I’ll take mine from Reynolds.</p>
|
||
</div>
|
||
<p>Reynolds describes the motion of <em>idealized</em> vehicles (idealized because he wasn’t concerned with the actual engineering of such vehicles, but rather started with the assumption that they work and respond to the rules defined) as a series of three layers—<strong>action selection</strong>, <strong>steering</strong>, and <strong>locomotion</strong>.</p>
|
||
<ol>
|
||
<li><strong>Action Selection.</strong> A vehicle has a goal (or goals) and can choose an action (or a combination of actions) based on that goal. This is essentially where I left off the discussion of autonomous agents. The vehicle takes a look at its environment and <em>selects</em> an action based on a desire: “I see a zombie marching toward me. Since I don’t want my brains to be eaten, I’m going to flee from the zombie.” The goal is to keep one’s brains, and the action is to flee. Reynolds’s paper describes many goals and associated actions, such as seeking a target, avoiding an obstacle, and following a path. In a moment, I’ll start building these examples out with p5.js code.</li>
|
||
<li><strong>Steering.</strong> Once an action has been selected, the vehicle has to calculate its next move. That next move will be a force—more specifically, a steering force. Luckily, Reynolds has developed a simple steering force formula that I’ll use throughout the examples in this chapter: <strong>steering force = desired velocity – current velocity</strong>. I’ll get into the details of this formula and why it works so effectively in the next section.</li>
|
||
<li><strong>Locomotion.</strong> For the most part, I’m going to ignore this third layer. In the case of fleeing from zombies, the locomotion could be described as “left foot, right foot, left foot, right foot, as fast as you can.” In a canvas, however, a rectangle, circle, or triangle’s actual movement across a window is irrelevant, given that the motion is all an illusion in the first place. This isn’t to say that you should ignore locomotion entirely, however. You’ll find great value in thinking about the locomotive design of your vehicle and how you choose to animate it. The examples in this chapter will remain visually bare; a good exercise would be to elaborate on the animation style. For example, could you add spinning wheels, oscillating paddles, or shuffling legs?</li>
|
||
</ol>
|
||
<p>Ultimately, the most important layer for you to consider is the first one, action selection. What are the elements of your system, and what are their goals? In this chapter, I’m going to cover a series of steering behaviors (that is, actions): seeking, fleeing, following a path, following a flow field, flocking with your neighbors, and so on. As I’ve said in other chapters, however, the point isn’t that you should use these exact behaviors in all of your projects. Rather, the point is to show you <em>how</em> to model a steering behavior—<em>any</em> steering behavior—in code, and to provide a foundation from which you can design and develop your own vehicles with new and exciting goals and behaviors.</p>
|
||
<p>What’s more, even though the examples in this chapter will be highly literal (follow that pixel!), you should allow yourself to think more abstractly (like Braitenberg). What would it mean for your vehicle to have “love” as its goal or “fear” as its driving force? Finally (and I’ll address this later in the chapter), you won’t get very far by developing simulations with only one action. Yes, the first example will be “seek a target.” But for you to be creative—to make these steering behaviors <em>your own</em>—it will all come down to mixing and matching multiple actions within the same vehicle. View the coming examples not as singular behaviors to be emulated, but as pieces of a larger puzzle that you’ll eventually assemble.</p>
|
||
<h3 id="the-steering-force">The Steering Force</h3>
|
||
<p>What exactly is a steering force? To answer, consider the following scenario: a vehicle with a current velocity is seeking a target. For fun, let’s think of the vehicle as a bug-like creature who desires to savor a delicious strawberry, as in Figure 5.1.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_2.png" alt="Figure 5.1: A vehicle with a velocity and a target">
|
||
<figcaption>Figure 5.1: A vehicle with a velocity and a target</figcaption>
|
||
</figure>
|
||
<p>The vehicle’s goal and subsequent action is to seek the target. Thinking back to Chapter 2, you might begin by making the target an attractor and applying a gravitational force that pulls the vehicle to the target. This would be a perfectly reasonable solution, but conceptually it’s not what I’m looking for here. I don’t want to simply calculate a force that pushes the vehicle toward its target; rather, I want to ask the vehicle to make an intelligent decision to steer toward the target based on its perception of its own state (how fast and in what direction it’s currently moving) and its environment (the location of the target). The vehicle should look at how it desires to move (a vector pointing to the target), compare that goal with how it’s currently moving (its velocity), and apply a force accordingly. That’s exactly what Reynolds’s steering force formula says.</p>
|
||
<div data-type="equation">\text{steering force} = \text{desired velocity} - \text{current velocity}</div>
|
||
<p>Or, as you might write in p5.js:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let steer = p5.Vector.sub(desired, velocity);</pre>
|
||
<p>The <em>current</em> velocity isn’t a problem: the <code>Vehicle</code> class already has a variable for that. However, the <em>desired</em> velocity has to be calculated. Take a look at Figure 5.2. If the vehicle’s goal is defined as “seeking the target,” then its desired velocity is a vector that points from its current position to the target position.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_3.png" alt="Figure 5.2: The vehicle’s desired velocity points from its position to the target. (The desired vector should point from the vehicle’s center to the target’s center but is shortened for illustration purposes.)">
|
||
<figcaption>Figure 5.2: The vehicle’s desired velocity points from its position to the target. (The desired vector should point from the vehicle’s center to the target’s center but is shortened for illustration purposes.)</figcaption>
|
||
</figure>
|
||
<p>Assuming a <code>p5.Vector</code> called <code>target</code> defining the target’s position, I then have:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let desired = p5.Vector.sub(target, position);</pre>
|
||
<p>There’s more to the story, however. What if the canvas is high-resolution and the target is thousands of pixels away? Sure, the vehicle might desire to teleport itself instantly to the target position with a massive velocity, but this won’t make for an effective animation. I’ll restate the desire as follows:</p>
|
||
<p><span class="highlight"><em>The vehicle desires to move toward the target </em><em>at the maximum possible speed</em><em>.</em></span></p>
|
||
<p>In other words, the <code>desired</code> vector should point from the vehicle’s current position to the target position, with a magnitude equal to the maximum speed of the vehicle, as shown in Figure 5.3.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_4.png" alt="Figure 5.3: The magnitude of the vehicle’s desired velocity is “max speed.”">
|
||
<figcaption>Figure 5.3: The magnitude of the vehicle’s desired velocity is “max speed.”</figcaption>
|
||
</figure>
|
||
<p>The concept of maximum speed was introduced in Chapter 1 to ensure that a mover’s speed remained within a reasonable range. However, I didn’t always use it in the subsequent chapters. In Chapter 2, other forces such as friction and drag kept the speed in check, while in Chapter 3, oscillation was caused by opposing forces that kept the speed limited. In this chapter, maximum speed is a key parameter for controlling the behavior of a steering agent, so I’ll include it in all the examples.</p>
|
||
<p>While I encourage you to consider how other forces such as friction and drag could be combined with steering behaviors, I’m going to focus only on steering forces for the time being. As such, I can include the concept of maximum speed as a limiting factor in the force calculation. First, I need to add a property to the <code>Vehicle</code> class setting the maximum speed.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
|
||
constructor(){
|
||
this.position = createVector();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
// Maximum speed
|
||
this.maxspeed = ????;
|
||
}
|
||
</pre>
|
||
<p>Then, in the desired velocity calculation, I’ll scale according to maximum speed.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let desired = p5.Vector.sub(target, this.position);
|
||
desired.setMag(this.maxspeed);</pre>
|
||
<p>Putting this all together, I can now write a method called <code>seek()</code> that receives a <code>p5.Vector</code> target and calculates a steering force toward that target.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> seek(target) {
|
||
//{!1} Calculating the desired velocity to target at max speed
|
||
let desired = p5.Vector.sub(target,this.position);
|
||
desired.setMag(this.maxspeed);
|
||
|
||
// Reynolds’s formula for steering force
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
//{!1} Using the physics model and applying the force
|
||
// to the object’s acceleration
|
||
this.applyForce(steer);
|
||
}</pre>
|
||
<p>Notice how I finish the method by passing the steering force into <code>applyForce()</code>. This assumes that the code is built on top of the foundation I developed in <a href="/force#">Chapter 2</a>.</p>
|
||
<p>To see why Reynolds’s steering formula works so well, take a look at Figure 5.4. It shows what the steering force looks like relative to the vehicle and target positions.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_5.png" alt="Figure 5.4: The vehicle applies a steering force equal to its desired velocity minus its current velocity.">
|
||
<figcaption>Figure 5.4: The vehicle applies a steering force equal to its desired velocity minus its current velocity.</figcaption>
|
||
</figure>
|
||
<p>This force looks quite different from gravitational attraction. Remember one of the principles of autonomous agents: an autonomous agent has a <em>limited</em> ability to perceive its environment, including its own state. Here’s that ability, subtly but powerfully embedded into Reynolds’s steering formula. In the case of gravitational attraction, the force pulling an object toward another is the same regardless of how that object is moving. But here, the vehicle is actively aware of its own velocity, and its steering force compensates accordingly. This adds a lifelike quality to the simulation, as the way in which the vehicle moves toward the target depends on its own understanding of its current motion.</p>
|
||
<p>In all of this excitement, I’ve missed one last step. What sort of vehicle is this? Is it a super sleek race car with amazing handling? Or a large city bus that needs a lot of advance notice to turn? A graceful panda, or a lumbering elephant? The example code, as it stands, has no feature to account for this variation in steering ability. For that, I need to limit the magnitude of the steering force. I’ll call this limit the “maximum force” (or <code>maxforce</code> for short).</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
|
||
constructor(){
|
||
this.position = createVector();
|
||
this.velocity = createVector();
|
||
this.acceleration = createVector();
|
||
// Maximum speed
|
||
this.maxspeed = ????;
|
||
// Now I also have a maximum force.
|
||
this.maxforce = ????;
|
||
}
|
||
</pre>
|
||
<p>Now I just need to impose that limit before applying the steering force.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> seek(target) {
|
||
let desired = p5.Vector.sub(target, this.position);
|
||
desired.setMag(this.maxspeed);
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
|
||
//{!1} Limit the magnitude of the steering force.
|
||
steer.limit(this.maxforce);
|
||
|
||
this.applyForce(steer);
|
||
}</pre>
|
||
<p>Limiting the steering force brings up an important point: the goal isn’t to get the vehicle to the target as fast as possible. If it were, I would just say “set position equal to target” and the vehicle would instantly teleport to there! Instead, as Reynolds puts it, the goal is to move the vehicle in a “lifelike and improvisational manner.”</p>
|
||
<p>I’m trying to make it appear as if the vehicle is steering its way to the target, and so it’s up to me to play with the forces and variables of the system to simulate a given behavior. For example, a large maximum steering force would result in a very different path than a small one (see Figure 5.5). One isn’t inherently better or worse than the other; it depends on the desired effect. (And of course, these values need not be fixed and could change based on other conditions. Perhaps a vehicle has an <em>energy</em> property: the higher the energy, the better it can steer.)</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_6.png" alt="Figure 5.5: The path for a stronger maximum force (left) versus a weaker one (right)">
|
||
<figcaption>Figure 5.5: The path for a stronger maximum force (left) versus a weaker one (right)</figcaption>
|
||
</figure>
|
||
<p>Here’s the full <code>Vehicle</code> class, incorporating the rest of the elements from the Chapter 2 <code>Mover</code> class.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-51-seeking-a-target">Example 5.1: Seeking a Target</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/Y74O77yxy" data-example-path="examples/05_steering/noc_5_01_seek"><img src="examples/05_steering/noc_5_01_seek/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
constructor(x, y) {
|
||
this.position = createVector(x, y);
|
||
this.velocity = createVector(0, 0);
|
||
this.acceleration = createVector(0, 0);
|
||
//{!1} Additional variable for size
|
||
this.r = 6.0;
|
||
//{!2} Arbitrary values for maxspeed and force; try varying these!
|
||
this.maxforce = 8;
|
||
this.maxspeed = 0.2;
|
||
}
|
||
|
||
// Standard update function
|
||
update() {
|
||
this.velocity.add(this.acceleration);
|
||
this.velocity.limit(this.maxspeed);
|
||
this.position.add(this.velocity);
|
||
this.acceleration.mult(0);
|
||
}
|
||
|
||
// Newton’s second law (skipping math)
|
||
applyForce(force) {
|
||
this.acceleration.add(force);
|
||
}
|
||
|
||
// The seek steering force algorithm
|
||
seek(target) {
|
||
let desired = p5.Vector.sub(target, this.position);
|
||
desired.setMag(this.maxspeed);
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
this.applyForce(steer);
|
||
}
|
||
|
||
show() {
|
||
//{!1} Vehicle is a triangle pointing in the direction of velocity
|
||
let angle = this.velocity.heading();
|
||
fill(127);
|
||
stroke(0);
|
||
push();
|
||
translate(this.position.x, this.position.y);
|
||
rotate(angle);
|
||
beginShape();
|
||
vertex(this.r * 2, 0);
|
||
vertex(-this.r * 2, -this.r);
|
||
vertex(-this.r * 2, this.r);
|
||
endShape(CLOSE);
|
||
pop();
|
||
}
|
||
}</pre>
|
||
<p>Note that, unlike the circles used to represent movers and particles in previous chapters, the <code>Vehicle</code> object here is drawn as a triangle, defined as three custom vertices set with <code>beginShape()</code> and <code>endShape()</code>. This allows the vehicle to be represented in a way that indicates its direction, determined using the <code>heading()</code> method, as demonstrated in Chapter 3.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-51">Exercise 5.1</h3>
|
||
<p>Implement a “fleeing” steering behavior (the desired velocity is the same as “seek,” but pointed in the opposite direction).</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-52">Exercise 5.2</h3>
|
||
<p>Implement a seeking behavior with a moving target, often referred to as “pursuit.” In this case, your desired vector won’t point toward the object’s current position, but rather its future position as extrapolated from its current velocity. You’ll see this ability for a vehicle to “predict the future” in later examples. The solution is covered in the <a href="https://thecodingtrain.com/tracks/the-nature-of-code-2/noc/5-autonomous-agents/3-pursue-and-evade">“Pursue and Evade” video</a> from my Nature of Code series at thecodingtrain.com.</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/RkDbZ0drr" data-example-path="examples/05_steering/exercise_5_2"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-53">Exercise 5.3</h3>
|
||
<p>Create a sketch where a vehicle’s maximum force and maximum speed don’t remain constant, but vary according to environmental factors.</p>
|
||
</div>
|
||
<h3 id="the-arrive-behavior">The Arrive Behavior</h3>
|
||
<p>After working for a bit with the seeking behavior, you’re probably are asking yourself, “What if I want the vehicle to slow down as it approaches the target?” Before I can even begin to answer this question, I should explain why the seek behavior causes the vehicle to fly past the target in the first place, forcing it to turn around and go back. Consider the brain of a seeking vehicle. What is it thinking at each frame of the animation?</p>
|
||
<ul>
|
||
<li>I want to go as fast as possible toward the target.</li>
|
||
<li>I want to go as fast as possible toward the target.</li>
|
||
<li>I want to go as fast as possible toward the target.</li>
|
||
<li>I want to go as fast as possible toward the target.</li>
|
||
<li>I want to go as fast as possible toward the target.</li>
|
||
<li>and so on . . .</li>
|
||
</ul>
|
||
<p>The vehicle is so gosh darn excited about getting to the target that it doesn’t bother to make any intelligent decisions about its speed. No matter the distance to the target, it always wants to go as fast as possible. When it’s very close, that means the vehicle will end up overshooting the target (see Figure 5.6, top).</p>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_7.png" alt="Figure 5.6: The top vehicle has a desired velocity at maximum speed and will overshoot the target. The bottom vehicle illustrates the idea of scaling the desired velocity according to the distance from the target. (Note that while I encourage you to continue thinking about the vehicle as a cute, bug-like creature, to keep things simple it will now be drawn as a triangle.)">
|
||
<figcaption>Figure 5.6: The top vehicle has a desired velocity at maximum speed and will overshoot the target. The bottom vehicle illustrates the idea of scaling the desired velocity according to the distance from the target. (Note that while I encourage you to continue thinking about the vehicle as a cute, bug-like creature, to keep things simple it will now be drawn as a triangle.)</figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>In some cases, this is the desired behavior. (Consider a puppy going after it’s favorite toy: it’s not slowing down, no matter how close it gets!) However, in many other cases (a car pulling into a parking spot, a bee landing on a flower), the vehicle’s thought process needs to consider its speed relative to the distance from its target (see Figure 5.6, bottom). For example:</p>
|
||
<ul>
|
||
<li>I’m very far away. I want to go as fast as possible toward the target.</li>
|
||
<li>I’m very far away. I want to go as fast as possible toward the target.</li>
|
||
<li>I’m somewhat far away. I still want to go as fast as possible toward the target.</li>
|
||
<li>I’m getting close. I want to go more slowly toward the target.</li>
|
||
<li>I’m almost there. I want to go very slowly toward the target.</li>
|
||
<li>I’m there. I want to stop!</li>
|
||
</ul>
|
||
<p>How can you implement this “arriving” behavior in code? Think back to the <code>seek()</code> method. Which part of the code sets the magnitude of the desired velocity?</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let desired = p5.Vector.sub(target, this.position);
|
||
desired.setMag(this.maxspeed);</pre>
|
||
<p>This always sets the magnitude of the <code>desired</code> vector to <code>maxspeed</code>, as in Figure 5.7.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_8.png" alt="Figure 5.7: The vehicles have a desired velocity with a magnitude set to maximum speed, regardless of their relative distance to the target.">
|
||
<figcaption>Figure 5.7: The vehicles have a desired velocity with a magnitude set to maximum speed, regardless of their relative distance to the target.</figcaption>
|
||
</figure>
|
||
<p>What if instead the desired velocity’s magnitude were equal to half the distance?</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let desired = p5.Vector.sub(target, this.position);
|
||
desired.mult(0.5);</pre>
|
||
<p>I’d still want to limit the magnitude of <code>desired</code> to no more than the maximum speed, to keep vehicles that are very far away from going ridiculously fast (Figure 5.8).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_9.png" alt="Figure 5.8: The magnitude of each vehicle’s desired velocity is equal to half the distance to the target. In the case of the left most vehicle, it’s constrained to the maximum speed.">
|
||
<figcaption>Figure 5.8: The magnitude of each vehicle’s desired velocity is equal to half the distance to the target. In the case of the left most vehicle, it’s constrained to the maximum speed.</figcaption>
|
||
</figure>
|
||
<p>While this change nicely demonstrates the goal of tying the desired speed to the distance from the target, it’s not a particularly good solution. After all, 10 pixels away is rather close, and a desired speed of 5 is rather large. Something like a desired velocity with a magnitude equal to 5 percent of the distance might work better.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let desired = p5.Vector.sub(target, this.position);
|
||
desired.mult(0.05);</pre>
|
||
<p>Reynolds describes an even more sophisticated approach. Imagine a circle around the target with a given radius <span data-type="equation">r</span>. If the vehicle is within that circle, it gradually slows down—from the maximum speed at the very edge of the circle to zero speed at the target itself (Figure 5.9).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_10.png" alt="Figure 5.9: Outside the circle, the magnitude of a vehicle’s desired velocity is set to the maximum speed. As vehicles enter the circle and approach the target, their desired velocity magnitude decreases.">
|
||
<figcaption>Figure 5.9: Outside the circle, the magnitude of a vehicle’s desired velocity is set to the maximum speed. As vehicles enter the circle and approach the target, their desired velocity magnitude decreases.</figcaption>
|
||
</figure>
|
||
<p>In other words, if the distance from the target is less than <span data-type="equation">r</span>, the desired speed is between 0 and maximum speed mapped according to that distance.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-52-arriving-at-a-target">Example 5.2: Arriving at a Target</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/v-yJm8WUx" data-example-path="examples/05_steering/noc_5_02_arrive"><img src="examples/05_steering/noc_5_02_arrive/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> arrive(target) {
|
||
let desired = p5.Vector.sub(target, this.position);
|
||
|
||
//{!1} The distance is the magnitude of
|
||
// the vector pointing from
|
||
// position to target.
|
||
let d = desired.mag();
|
||
//{!1} If we are closer than 100 pixels...
|
||
if (d < 100) {
|
||
//{!2} ...set the magnitude according to how close we are.
|
||
let m = map(d, 0, 100, 0, this.maxspeed);
|
||
desired.setMag(m);
|
||
} else {
|
||
//{!1} Otherwise, proceed at maximum speed.
|
||
desired.setMag(this.maxspeed);
|
||
}
|
||
|
||
//{!1} The usual steering = desired - velocity
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
this.applyForce(steer);
|
||
}</pre>
|
||
<p>The arrive behavior is a great demonstration of an autonomous agent’s perception of the environment—including its own state. This model differs from the inanimate forces of Chapter 2: a celestial body attracted to another body doesn’t know it is experiencing gravity, whereas a cheetah chasing its prey knows it’s chasing. The key is in how the forces are calculated. For instance, in the gravitational attraction sketch (Example 2.6), the force always pointed directly from the object to the target—the exact direction of the desired velocity. Here, by contrast, the vehicle perceives its distance to the target and adjusts its desired speed accordingly, slowing down as it gets closer. The force on the vehicle itself is therefore based not just on the desired velocity, but on the desired velocity <em>relative to its current velocity</em>. The vehicle accounts for its own state as part of its assessment of the environment.</p>
|
||
<p>Put another way, the magic of Reynolds’s “desired minus velocity” equation is that it essentially makes the steering force a manifestation of the current velocity’s <strong><em>error</em></strong>: “I’m supposed to be going this fast in this direction, but I’m actually going this fast in another direction. My error is the difference between where I want to go and where I’m currently going.” Sometimes, this can lead to seemingly unexpected results, as in Figure 5.10.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_11.png" alt="Figure 5.10: A vehicle moving toward its target faster than its desired velocity will result in a steering force pointing away from the target.">
|
||
<figcaption>Figure 5.10: A vehicle moving toward its target faster than its desired velocity will result in a steering force pointing away from the target.</figcaption>
|
||
</figure>
|
||
<p>In this example of the arrive behavior, the vehicle is moving too fast toward the target. The steering force, or error, tells it to slow down by actually pointing in the opposite direction, away from the target. By contrast, with gravitational attraction, you would never have a force pointing away from the target, no matter how close the target is. Taking the error and applying it as a steering force results in more dynamic, lifelike simulations.</p>
|
||
<h3 id="your-own-behaviors">Your Own Behaviors</h3>
|
||
<p>The first two examples I’ve covered—seek and arrive—boil down to calculating a single vector for each behavior: the <em>desired</em> velocity. In fact, every single one of Reynolds’s steering behaviors follows this same pattern. In this chapter, I’m going to walk through several more of Reynolds’s behaviors—flow field, path-following, flocking. First, however, I want to emphasize again that these are <em>examples</em>—demonstrations of common steering behaviors that are useful in procedural animation. They aren’t the end-all and be-all of what <em>you</em> can do. As long as you can come up with a vector that describes a vehicle’s <em>desired</em> velocity, then you’ve created your own steering behavior.</p>
|
||
<p>For example, let’s see how Reynolds defines the desired velocity for his wandering behavior.</p>
|
||
<blockquote data-type="epigraph">
|
||
<p>“Wandering is a type of random steering which has some long term order: the steering direction on one frame is related to the steering direction on the next frame. This produces more interesting motion than, for example, simply generating a random steering direction each frame.”</p>
|
||
<p><a href="http://www.red3d.com/cwr/steer/Wander.html">—Craig Reynolds</a></p>
|
||
</blockquote>
|
||
<p>For Reynolds, the goal of wandering isn’t random motion, but rather a sense of moving in one direction for a little while, wandering off in the next direction for a little bit, and so on and so forth. Figure 5.11 illustrates how Reynolds calculates a target to seek in order to achieve such an effect.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_12.png" alt="Figure 5.11: The wandering steering behavior is calculated as seeking a target that moves randomly along the perimeter of a circle projected in front of the vehicle.">
|
||
<figcaption>Figure 5.11: The wandering steering behavior is calculated as seeking a target that moves randomly along the perimeter of a circle projected in front of the vehicle.</figcaption>
|
||
</figure>
|
||
<p>First, the vehicle predicts its future position as a fixed distance in front of it (in the direction of its current velocity). Then it draws a circle with radius <span data-type="equation">r</span> centered around that position and picks a random point along the circumference of the circle. That point, which moves randomly around the circle each frame of animation, is the vehicle’s target, so its desired velocity points in that direction.</p>
|
||
<p>Sounds absurd, right? Or, at the very least, a bit arbitrary. In fact, this is a very clever and thoughtful solution—it uses randomness to drive a vehicle’s steering, but constrains that randomness along the path of a circle to keep the vehicle’s movement from appearing jittery, and, well, totally random.</p>
|
||
<p>The seemingly random and arbitrary nature of this solution should drive home the point I’m trying to make: these are <em>made-up</em> behaviors, even if they’re inspired by real-life motion. You can just as easily concoct some other elaborate scenario to compute a desired velocity yourself. And you should!</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-54">Exercise 5.4</h3>
|
||
<p>Write the code for Reynolds’s wandering behavior. Use polar coordinates to calculate the vehicle’s target along a circular path.</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/rgCpC9OV3" data-example-path="examples/05_steering/noc_5_04_exercise_wander"><img src="examples/05_steering/noc_5_04_exercise_wander/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>To give another example, say I want to create a steering behavior called “stay within walls.” To define the desired velocity, I’ll make a rule: if a vehicle comes within a distance d of a wall, it desires to move at maximum speed in the opposite direction of the wall (see Figure 5.12).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_13.png" alt="Figure 5.12: The desired velocity points away from the wall if the vehicle gets too close.">
|
||
<figcaption>Figure 5.12: The desired velocity points away from the wall if the vehicle gets too close.</figcaption>
|
||
</figure>
|
||
<p>If I define the walls of the space as the edges of a canvas and an <code>offset</code> distance equal to 25, I can write the code for this with a series of <code>if</code> statements.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-53-stay-within-walls-steering-behavior">Example 5.3: “Stay Within Walls” Steering Behavior</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/fGNwVP3h7" data-example-path="examples/05_steering/noc_5_03_stay_within_walls"><img src="examples/05_steering/noc_5_03_stay_within_walls/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> // The method receives an "offset" from the edges
|
||
boundaries(offset) {
|
||
// Start with a null desired velocity.
|
||
let desired = null;
|
||
|
||
//{.offset} Make a desired velocity that retains the y direction of
|
||
// the vehicle but points the x direction directly away from
|
||
// the canvas edges.
|
||
if (this.position.x < offset) {
|
||
desired = createVector(this.maxspeed, this.velocity.y);
|
||
} else if (this.position.x > width - offset) {
|
||
desired = createVector(-this.maxspeed, this.velocity.y);
|
||
}
|
||
if (this.position.y < offset) {
|
||
desired = createVector(this.velocity.x, this.maxspeed);
|
||
} else if (this.position.y > height - offset) {
|
||
desired = createVector(this.velocity.x, -this.maxspeed);
|
||
}
|
||
|
||
// If there is non-null desired velocity, apply steering.
|
||
if (desired !== null) {
|
||
desired.normalize();
|
||
desired.mult(this.maxspeed);
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
this.applyForce(steer);
|
||
}
|
||
}</pre>
|
||
<p>In this <code>boundaries()</code> method, you might be wondering why I set the the <code>desired</code> velocity to <code>null</code> at the outset. Why not just set <code>desired</code> to a vector of 0? Remember, the steering force equals the desired velocity minus the current velocity! If the vehicle desires to move at 0 velocity, the resulting force would slow the vehicle down to a stop. By initializing <code>desired</code> to <code>null</code> and checking that it's non-null before applying the steering force, the vehicle won’t be affected at all when it's comfortably away from the edges of the canvas.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-55">Exercise 5.5</h3>
|
||
<p>Come up with your own arbitrary scheme for calculating a desired velocity.</p>
|
||
</div>
|
||
<h2 id="flow-fields">Flow Fields</h2>
|
||
<p>Another one of Craig Reynolds’s steering behaviors is <strong>flow field following</strong>. But what is a flow field? Think of the canvas as a grid (Figure 5.13). In each cell of the grid lives an arrow pointing in some direction—you know, a vector. As a vehicle moves around the canvas, it asks, “Hey, what arrow is beneath me? That’s my desired velocity!”</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_14.png" alt="Figure 5.13: A two-dimension grid full of unit vectors pointing in random directions">
|
||
<figcaption>Figure 5.13: A two-dimension grid full of unit vectors pointing in random directions</figcaption>
|
||
</figure>
|
||
<p>Reynolds’s own flow field example involves the vehicle looking ahead to its future position and following the vector at that spot. For simplicity’s sake, however, I’ll instead have the vehicle follow the vector at its current position.</p>
|
||
<p>Before I can write the additional code for the <code>Vehicle</code> class to follow a flow field, I first need a class that describes the flow field itself, Since a flow field is essentially a grid of vectors, a two-dimensional array is a convenient data structure to represent it, as I can reference each element with two indices, the cell’s column and row in the grid. (If you aren’t familiar with 2D arrays, I suggest reviewing my video tutorial on <a href="https://thecodingtrain.com/tracks/p5-tips-and-tricks/more-p5/2d-arrays">2D Arrays in JavaScript</a> available at thecodingtrain.com.)</p>
|
||
<pre class="codesplit" data-code-language="javascript">class FlowField {
|
||
|
||
constructor(){
|
||
// Resolution of grid relative to canvas width and height in pixels.
|
||
this.resolution = ????;
|
||
// How many columns and how many rows in the grid?
|
||
this.cols = ????;
|
||
this.rows = ????;
|
||
//{!4} field will be a 2D array of vectors.
|
||
this.field = new Array(this.cols);
|
||
for (let i = 0; i < this.cols; i++) {
|
||
this.field[i] = new Array(this.rows);
|
||
}
|
||
}</pre>
|
||
<p>How should I fill in the missing values? Let’s say I have a canvas that’s 200 pixels wide by 200 pixels high. In theory, I could make a flow field that has a vector for every single pixel, meaning 40,000 vectors total (<span data-type="equation">200 \times 200</span>). This isn’t a terribly unreasonable number, but in this context, a vector per pixel is overkill. I can easily get by with, say, one every ten pixels (<span data-type="equation">20 \times 20 = 400</span>). My <code>resolution</code> variable sets the size of each cell in pixels. Then I can calculate the number of columns and rows based on the size of the canvas divided by the resolution.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> constructor(){
|
||
this.resolution = 10;
|
||
// Total columns equals width divided by resolution.
|
||
this.cols = floor(width / this.resolution);
|
||
//{!1} Total rows equals height divided by resolution.
|
||
this.rows = floor(height / this.resolution);
|
||
//{!4} field will be a 2D array of vectors.
|
||
this.field = new Array(this.cols);
|
||
for (let i = 0; i < this.cols; i++) {
|
||
this.field[i] = new Array(this.rows);
|
||
}
|
||
}</pre>
|
||
<p>Now that I’ve set up the data structure for the flow field, it’s time to compute the flow field’s vectors themselves. How do I do that? However I want! Perhaps I’d like every vector in the flow field pointing to the right (Figure 5.14).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_15.png" alt="Figure 5.14: A flow field with all vectors pointing to the right">
|
||
<figcaption>Figure 5.14: A flow field with all vectors pointing to the right</figcaption>
|
||
</figure>
|
||
<p>For that, I can just set each vector to <code>(1, 0)</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!2} Using a nested loop to hit every column
|
||
// and every row of the flow field
|
||
for (let i = 0; i < this.cols; i++) {
|
||
for (let j = 0; j < this.rows; j++) {
|
||
//{!1} Arbitrary decision to make each vector point to the right
|
||
this.field[i][j] = createVector(1, 0);
|
||
}
|
||
}</pre>
|
||
<p>Maybe I’d prefer the vectors to point in random directions (Figure 5.15).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_16.png" alt="Figure 5.15: A flow field with vectors pointing in random directions">
|
||
<figcaption>Figure 5.15: A flow field with vectors pointing in random directions</figcaption>
|
||
</figure>
|
||
<p>Easy. Just use the <code>p5.Vector</code> class’s <code>random2D()</code> method to assign each vector.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i < this.cols; i++) {
|
||
for (let j = 0; j < this.rows; j++) {
|
||
//{!1} A random vector
|
||
this.field[i][j] = p5.Vector.random2D();
|
||
}
|
||
}</pre>
|
||
<p>What about using 2D Perlin noise (Figure 5.16)?</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_17.png" alt="Figure 5.16: A flow field calculated with Perlin noise">
|
||
<figcaption>Figure 5.16: A flow field calculated with Perlin noise</figcaption>
|
||
</figure>
|
||
<p>Just map each noise value to an angle from <span data-type="equation">0</span> to <span data-type="equation">2\pi</span>, and create a vector from that angle.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let xoff = 0;
|
||
for (let i = 0; i < this.cols; i++) {
|
||
let yoff = 0;
|
||
for (let j = 0; j < this.rows; j++) {
|
||
//{!1 .offset-top} 2D Noise
|
||
let angle = map(noise(xoff, yoff), 0, 1, 0, TWO_PI);
|
||
this.field[i][j] = p5.Vector.fromAngle(angle);
|
||
yoff += 0.1;
|
||
}
|
||
xoff += 0.1;
|
||
}</pre>
|
||
<p>Now I’m getting somewhere. Calculating the direction of the vectors using Perlin noise is a great way to simulate a variety of natural effects, such as irregular gusts of wind or the meandering path of a river. I’ll note, however, that this noise mapping generates a field that prefers flowing left. Since Perlin noise has a Gaussian-like distribution, angles near <span data-type="equation">\pi</span> are more likely to be selected. For Figure 5.16, I used a range of 0 to <span data-type="equation">4\pi</span> to counteract this tendency, similar to how I applied <span data-type="equation">4\pi</span> in Chapter 4 to represent a range of angles for spinning confetti particles. Ultimately, of course, there’s no “correct” way to calculate the vectors of a flow field; it’s up to you to decide what you’re looking to simulate.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-56">Exercise 5.6</h3>
|
||
<p>Write the code to calculate a flow field so that the vectors swirl in circles around the center of the canvas. </p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_18.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
<pre class="codesplit" data-code-language="javascript">let x = <span class="blank">i * width / cols</span>;
|
||
let y = <span class="blank">j * height / rows</span>;
|
||
flowfield[i][j] = createVector(<span class="blank">width/2 - x</span>, <span class="blank">height/2 - y</span>);
|
||
flowfield[i][j].<span class="blank">rotate(PI / 2)</span>;</pre>
|
||
</div>
|
||
<p>Now that I have a two-dimensional array storing the flow field vectors, I need a way for the vehicle to look up its desired velocity. For that, I simply divide the vehicle’s x and y position by the resolution of the grid. This gives me the indices of the desired vector in the 2D array. For example, if the resolution is 10 and the vehicle is at (100, 50), I’ll want to look up column 10 and row 5.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let column = floor(this.position.x / this.resolution);
|
||
let row = floor(this.position.y / this.resolution);</pre>
|
||
<p>Because a vehicle could theoretically wander off the p5.js canvas, it’s also useful to employ the <code>constrain()</code> function to make sure I don’t look outside the bounds of the flow field array. Here’s a method called <code>lookup()</code>, which I’ll add to the <code>FlowField</code> class, that receives a vector (the position of the vehicle) and returns the corresponding flow field vector for that position.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> lookup(position) {
|
||
//{!2 .offset-top} Using constrain()
|
||
let column = constrain(floor(position.x / this.resolution), 0, this.cols - 1);
|
||
let row = constrain(floor(position.y / this.resolution), 0, this.rows - 1);
|
||
|
||
//{!1} Note the use of copy() to ensure a copy of the vector is returned.
|
||
return this.field[column][row].copy();
|
||
}</pre>
|
||
<p>Before moving on to the <code>Vehicle</code> class, let’s look at the <code>FlowField</code> class code all together, this time using Perlin noise to compute the vector directions.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class FlowField {
|
||
|
||
constructor(r) {
|
||
this.resolution = r;
|
||
//{!2} Determine the number of columns and rows.
|
||
this.cols = width / this.resolution;
|
||
this.rows = height / this.resolution;
|
||
//{!4} A flow field is a two-dimensional array of vectors. The example includes a separate function to create that array.
|
||
this.field = new Array(this.cols);
|
||
for (let i = 0; i < this.cols; i++) {
|
||
this.field[i] = new Array(this.rows);
|
||
}
|
||
this.init();
|
||
}
|
||
|
||
// The init() function fills the 2D array with vectors.
|
||
init() {
|
||
// Reseed noise for a new flow field each time
|
||
noiseSeed(random(10000));
|
||
let xoff = 0;
|
||
for (let i = 0; i < this.cols; i++) {
|
||
let yoff = 0;
|
||
for (let j = 0; j < this.rows; j++) {
|
||
//{.code-wide} In this example, use Perlin noise to create the vectors.
|
||
let angle = map(noise(xoff, yoff), 0, 1, 0, TWO_PI);
|
||
this.field[i][j] = p5.Vector.fromAngle(angle);
|
||
yoff += 0.1;
|
||
}
|
||
xoff += 0.1;
|
||
}
|
||
}
|
||
|
||
//{.code-wide} A function to return a vector based on a position.
|
||
lookup(position) {
|
||
let column = constrain(floor(position.x / this.resolution), 0, this.cols - 1);
|
||
let row = constrain(floor(position.y / this.resolution), 0, this.rows - 1);
|
||
return this.field[column][row].copy();
|
||
}
|
||
}</pre>
|
||
<p>Now let’s assume there’s a <code>FlowField</code> object called <code>flow</code>. Using that object’s <code>lookup()</code> method, a vehicle can then retrieve a desired velocity from the flow field and use Reynolds’s steering formula to calculate a force.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-54-flow-field-following">Example 5.4: Flow Field Following </h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/egribz8WV" data-example-path="examples/05_steering/noc_5_04_flow_field"><img src="examples/05_steering/noc_5_04_flow_field/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">class Vehicle {
|
||
|
||
follow(flow) {
|
||
// What is the vector at that spot in the flow field?
|
||
let desired = flow.lookup(this.position);
|
||
desired.setMag(this.maxspeed);
|
||
|
||
//{!3} Steering is desired minus velocity.
|
||
let steer = p5.Vector.sub(desired, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
this.applyForce(steer);
|
||
}</pre>
|
||
<p>Notice how <code>lookup()</code> is a method of the <code>FlowField</code> class, rather than of <code>Vehicle</code>. While you certainly could place <code>lookup()</code> within the <code>Vehicle</code> class instead, from my perspective, placing it in <code>FlowField</code> aligns best with the principle of encapsulation in object-oriented programming. The “look up” task, which involves retrieving a vector based on a position from the flow field, is inherently tied to the data of the <code>FlowField</code> object itself.</p>
|
||
<p>You may also notice some familiar elements from the particle system chapter, such as the use of an array of vehicles. Although the vehicles here operate independently, this is a great first step toward thinking about the group behaviors that I’ll introduced later this chapter.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-57">Exercise 5.7</h3>
|
||
<p>Adapt the flow field example so the vectors change over time. (Hint: try using the third dimension of Perlin noise!).</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-58">Exercise 5.8</h3>
|
||
<p>Can you create a flow field from an image? For example, try having the vectors point from dark to light colors (or vice versa).</p>
|
||
</div>
|
||
<h2 id="path-following">Path Following</h2>
|
||
<p>The next steering behavior formulated by Craig Reynolds I’d like to explore is path following. But let me quickly clarify something first. The behavior here is path <em>following</em>, not path <em>finding</em>. Pathfinding refers to an algorithm that involves solving for the shortest distance between two points, often in a maze. With <strong>path following</strong>, a predefined route or “path” already exists, and the vehicle simply tries to follow it.</p>
|
||
<p>In this section, I will work through the algorithm, including the corresponding mathematics and code. However, before doing so, it’s important to cover a key concept in vector math that I skipped over in Chapter 1: <strong>the dot product</strong>. I haven’t needed it yet, but it’s necessary here and likely will prove quite useful for you beyond just this example.</p>
|
||
<h3 id="the-dot-product">The Dot Product</h3>
|
||
<p>Remember all the vector math covered in Chapter 1? Add, subtract, multiply, and divide? Figure 5.17 has a recap of some of these operations.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_19.png" alt="Figure 5.17: Adding vectors, and multiplying a vector by a scalar">
|
||
<figcaption>Figure 5.17: Adding vectors, and multiplying a vector by a scalar</figcaption>
|
||
</figure>
|
||
<p>Notice how multiplication involves multiplying a vector by a scalar value. This makes sense; when you want a vector to be twice as large (but facing the same direction), multiply it by 2. When you want it to be half the size, multiply it by 0.5. However, there are several other <em>multiplication-like</em> operations involving a pair of vectors that are useful in certain scenarios—the dot product, the cross product, and something called the Hadamard product. For now, I’m going to focus on the dot product.</p>
|
||
<p>Assume vectors <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span>:</p>
|
||
<div data-type="equation">\vec{A}=(a_x,a_y)</div>
|
||
<div data-type="equation">\vec{B}=(b_x,b_y)</div>
|
||
<p>The formula for the dot product (represented by the <span data-type="equation">\cdot</span> character) is as follows:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B}=(a_x\times b_x) + (a_y\times b_y)</div>
|
||
<p>Crucially, the result of the dot product is a scalar value (a single number) and not a vector, even though the inputs are two vectors. For example, say you have these two vectors:</p>
|
||
<div data-type="equation">\vec{A}=(-3,5)</div>
|
||
<div data-type="equation">\vec{B}=(10,1)</div>
|
||
<p>Their dot product is:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B} = (-3 \times 10) + (5 \times 1) = -30 + 5 = -25</div>
|
||
<p>In p5.js, this translates to:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let a = createVector(-3, 5);
|
||
let b = createVector(10, 1);
|
||
|
||
// The p5.Vector class includes a function to calculate the dot product.
|
||
let n = a.dot(b);</pre>
|
||
<p>If you look in the guts of the <code>p5.Vector</code> source code, you’ll find a pretty simple implementation of this <code>dot()</code> method:</p>
|
||
<pre class="codesplit" data-code-language="javascript">dot(v) {
|
||
//{!1} For 2D vectors, z is 0.
|
||
return this.x * v.x + this.y * v.y + this.z * v.z;
|
||
}</pre>
|
||
<p>This formula is simple enough, but why is the dot product necessary, and when is it useful in coding? Well, one of the more common uses of the dot product is to find the angle between two vectors. In fact, the dot product can also be be expressed as:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B} = ||\vec{A}||\times||\vec{B}||\times\cos(\theta)</div>
|
||
<p>In other words, the dot product of <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span> is equal to the magnitude of <span data-type="equation">\vec{A}</span> times magnitude of <span data-type="equation">\vec{B}</span> times the cosine of theta (with theta being the angle between the two vectors <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span>).</p>
|
||
<p>The two dot product formulas can be derived from one another with <a href="http://mathworld.wolfram.com/DotProduct.html">trigonometry</a>, but I’m happy not to follow that path and instead just operate on the assumption that:</p>
|
||
<div data-type="equation">||\vec{A}||\times||\vec{B}||\times\cos(\theta)=(a_x\times b_x) + (a_y\times b_y)</div>
|
||
<p>This works since both sides of the equation equal <span data-type="equation">\vec{A} \cdot \vec{B}</span>. What does that assumption do for me? Say I have two vectors <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span>:</p>
|
||
<div data-type="equation">\vec{A}=(10,2)</div>
|
||
<div data-type="equation">\vec{B}=(4,-3)</div>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_20.png" alt="Figure 5.18: The angle between two vectors \vec{A} and \vec{B}">
|
||
<figcaption>Figure 5.18: The angle between two vectors <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>I now have a scenario where I know the components of the vectors, but I don’t know the angle <span data-type="equation">\theta</span>between them (see Figure 5.18). Using the dot product formula, I can solve for the cosine of <span data-type="equation">\theta</span>:</p>
|
||
<div data-type="equation">\cos(\theta)=\frac{(a_x\times b_x) + (a_y\times b_y)}{||\vec{A}||\times||\vec{B}||}</div>
|
||
<p>To solve for <span data-type="equation">\theta</span>, I can take the inverse cosine, or arccosine (<code>acos</code> in p5.js) of the right side of the equation.</p>
|
||
<div data-type="equation">\theta=\arccos\left(\frac{(a_x\times b_x) + (a_y\times b_y)}{||\vec{A}||\times||\vec{B}||}\right)</div>
|
||
<p>Doing the math now with actual numbers:</p>
|
||
<div data-type="equation">||\vec{A}||=10.2</div>
|
||
<div data-type="equation">||\vec{B}||=5</div>
|
||
<div data-type="equation">\theta=\arccos\left(\frac{(10\times4)+(2\times-3)}{10.2\times5}\right)</div>
|
||
<div data-type="equation">\theta=\arccos\left(\frac{34}{51}\right)</div>
|
||
<div data-type="equation">\theta \approx48^\circ</div>
|
||
<p>The p5.js version of this would be:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let a = createVector(10, 2);
|
||
let b = createVector(4, -3);
|
||
let angle = acos(a.dot(b) / (a.mag() * b.mag()));</pre>
|
||
<p>Turns out, if you again dig into the guts of the p5.js source code, you’ll find a method that implements this exact algorithm.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> angleBetween(v) {
|
||
let dot = this.dot(v);
|
||
let angle = Math.acos(dot / (this.mag() * v.mag()));
|
||
return angle;
|
||
}</pre>
|
||
<p>Sure I could have told you about this <code>angleBetween()</code>method to begin with, but understanding the dot product in detail will better prepare you for the upcoming path following examples and help you see how the dot product fits into a concept called “scalar projection.”</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-59">Exercise 5.9</h3>
|
||
<p>Create a sketch that shows the angle between two vectors.</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/G80pBHVOE" data-example-path="examples/05_steering/exercise_5_9_angle_between"><img src="examples/05_steering/exercise_5_9_angle_between/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>There are a couple things to note about dot products:</p>
|
||
<ol>
|
||
<li>If two vectors (<span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span>) are orthogonal (that is, perpendicular), their dot product (<span data-type="equation">\vec{A}\cdot\vec{B}</span>) is equal to 0.</li>
|
||
<li>If two vectors are unit vectors, then their dot product is equal to the cosine of the angle between them. In other words, <span data-type="equation">\vec{A}\cdot\vec{B}=\cos(\theta)</span> if <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span> are of length 1.</li>
|
||
</ol>
|
||
<p>Now that I’ve covered the fundamentals of the dot product, I can return to Craig Reynolds’s path-following algorithm.</p>
|
||
<h3 id="simple-path-following">Simple Path Following</h3>
|
||
<p>Figure 5.19 depicts all the ingredients of the path following behavior. There are a lot of components at play here beyond just a vehicle and target, so take some time to review the full diagram. I’ll then slowly unpack the algorithm piece by piece.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_21.png" alt="Figure 5.19: Path following includes a path, a vehicle, a future position, a “normal” to the path, and a target.">
|
||
<figcaption>Figure 5.19: Path following includes a path, a vehicle, a future position, a “normal” to the path, and a target.</figcaption>
|
||
</figure>
|
||
<p>First, what do I mean by a <em>path</em>? There are many techniques for implementing a path, but one simple way is to define a path as a series of connected points, as in Figure 5.20.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_22.png" alt="Figure 5.20: A path is a sequence of connected points. ">
|
||
<figcaption>Figure 5.20: A path is a sequence of connected points. </figcaption>
|
||
</figure>
|
||
<p>The simplest version of this path would just be a line between two points (Figure 5.21).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_23.png" alt="Figure 5.21: A path with a start, end, and radius.">
|
||
<figcaption>Figure 5.21: A path with a start, end, and radius.</figcaption>
|
||
</figure>
|
||
<p>I’m also going to consider a path to have a <em>radius</em>. If the path is a road, the radius is the road’s width. With a smaller radius, vehicles have to follow the path more closely; a wider radius allows them to stray a bit more to either side of the path.</p>
|
||
<p>Putting this into a class:</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/8SALyBTym" data-example-path="examples/05_steering/nature_of_code_example_5_5_path_only"><img src="examples/05_steering/nature_of_code_example_5_5_path_only/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
<pre class="codesplit" data-code-language="javascript">class Path {
|
||
constructor() {
|
||
// A path has a radius, how wide is it.
|
||
this.radius = 20;
|
||
//{!2} A path is only two points, start and end.
|
||
this.start = createVector(0, height / 3);
|
||
this.end = createVector(width, (2 * height) / 3);
|
||
}
|
||
|
||
//{!7} Display the path.
|
||
show() {
|
||
strokeWeight(this.radius * 2);
|
||
stroke(0, 100);
|
||
line(this.start.x, this.start.y, this.end.x, this.end.y);
|
||
strokeWeight(1);
|
||
stroke(0);
|
||
line(this.start.x, this.start.y, this.end.x, this.end.y);
|
||
}
|
||
}</pre>
|
||
<p>Now, assume there’s a vehicle outside the path’s radius, moving with a velocity, as in Figure 5.22.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_24.png" alt="Figure 5.22: Adding a vehicle moving off and away from the path">
|
||
<figcaption>Figure 5.22: Adding a vehicle moving off and away from the path</figcaption>
|
||
</figure>
|
||
<p>The first thing to do is predict (assuming a constant velocity) where that vehicle will be in the future.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// Start by making a copy of the velocity.
|
||
let future = vel.copy();
|
||
|
||
//{!2} Look 25 pixels ahead by setting the magnitude.
|
||
future.setMag(25);
|
||
|
||
// Add vector to position to find the future position.
|
||
future.add(this.position);</pre>
|
||
<p>Once I have that position, it’s time to determine the distance from that predicted position to the path. If it’s very far away, the vehicle has strayed from the path and needs to steer back toward it. If it’s on the path, all is well and the vehicle can continue on its way.</p>
|
||
<p>Essentially, I need to calculate the distance between a point (the future position) and a line (the path). That distance is defined as the length of the <strong>normal</strong>, a vector that extends from the point to the line and is perpendicular to the line (Figure 5.23).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_25.png" alt="Figure 5.23: The normal is a vector that extends from the future position to the path and is perpendicular to the path.">
|
||
<figcaption>Figure 5.23: The normal is a vector that extends from the future position to the path and is perpendicular to the path.</figcaption>
|
||
</figure>
|
||
<p>How do I find the normal? First, I can define a vector (call it <span data-type="equation">\vec{A}</span>) that extends from the path’s starting point to the vehicle’s future position.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let a = p5.Vector.sub(future, path.start);</pre>
|
||
<p>Next, I can define a vector (call it <span data-type="equation">\vec{B}</span>) that points from the start of the path to the end.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let b = p5.Vector.sub(path.end, path.start);</pre>
|
||
<p>Now, with a little trigonometry (the <em>cah</em> in <em>sohcahtoa</em>), I can calculate the distance from the path’s start to the normal point. As shown in Figure 5.24, it’s <span data-type="equation">||\vec{A}|| \times \cos(\theta)</span>.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_26.png" alt="Figure 5.24: The distance from the start of the path to the normal is ||\vec{A}|| \times \cos(\theta).">
|
||
<figcaption>Figure 5.24: The distance from the start of the path to the normal is <span data-type="equation">||\vec{A}|| \times \cos(\theta)</span>.</figcaption>
|
||
</figure>
|
||
<p>If I only knew <span data-type="equation">\theta</span>, I could find that normal point with the following code:</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} Get the distance from "start" to "normal."
|
||
let d = a.mag() * cos(theta);
|
||
// Scale vector b to that distance.
|
||
b.setMag(d);
|
||
// The normal point can be found by adding the scaled version of b to the path’s starting point.
|
||
let normalPoint = p5.Vector.add(path.start, b);</pre>
|
||
<p>Luckily, if the dot product has taught me anything, it’s that given two vectors, I can calculate the angle between those vectors!</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} What is theta? The angle between A and B!
|
||
let theta = p5.Vector.angleBetween(a, b);
|
||
let d = a.mag() * cos(theta);
|
||
b.setMag(d);
|
||
let normalPoint = p5.Vector.add(path.start, b);</pre>
|
||
<p>While this code will work, there’s one more simplification I can make. Looking again, you’ll see the magnitude for vector <span data-type="equation">\vec{B}</span> is set to <code>a.mag() * cos(theta)</code>, which is the code translation of:</p>
|
||
<div data-type="equation">||\vec{A}||\times\cos(\theta)</div>
|
||
<p>And, if you recall:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B}=||\vec{A}||\times||\vec{B}||\times\cos(\theta)</div>
|
||
<p>Now, what if <span data-type="equation">\vec{B}</span> is a unit vector , of length 1? Then:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B}=||\vec{A}||\times1\times\cos(\theta)</div>
|
||
<p>Or, more simply:</p>
|
||
<div data-type="equation">\vec{A}\cdot\vec{B}=||\vec{A}||\times\cos(\theta)</div>
|
||
<p>When <span data-type="equation">\vec{B}</span> is a unit vector, <span data-type="equation">||\vec{A}|| \times \cos(\theta)</span> is the same as the dot product of <span data-type="equation">\vec{A}</span> and <span data-type="equation">\vec{B}</span>. Turning <code>b</code> into a unit vector is as simple as calling <code>normalize()</code>. I can therefore bypass calculating <code>theta</code> with <code>angleBetween()</code> and simplify the code to:</p>
|
||
<pre class="codesplit" data-code-language="javascript">//{{!3} .line-through}
|
||
let theta = p5.Vector.angleBetween(a, b);
|
||
let d = a.mag() * cos(theta);
|
||
b.setMag(d);
|
||
|
||
// Normalize b and use the dot product to set b’s length.
|
||
b.normalize();
|
||
b.setMag(a.dot(b));
|
||
let normalPoint = p5.Vector.add(path.start, b);</pre>
|
||
<p>This process of scaling <span data-type="equation">\vec{B}</span> according to the normal point is commonly known as <strong>scalar projection</strong>. We say that <span data-type="equation">||\vec{A}||\times\cos(\theta)</span><strong><em> </em></strong><strong>is the scalar projection of </strong><span data-type="equation">\vec{A}</span><strong> onto </strong><span data-type="equation">\vec{B}</span>, as in Figure 5.25.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_27.png" alt="Figure 5.25: The scalar projection of \vec{A} onto \vec{B} is equal to ||\vec{A}||\times\cos(\theta).">
|
||
<figcaption>Figure 5.25: The scalar projection of <span data-type="equation">\vec{A}</span> onto <span data-type="equation">\vec{B}</span> is equal to <span data-type="equation">||\vec{A}||\times\cos(\theta)</span>.</figcaption>
|
||
</figure>
|
||
<p>Once I have the normal point along the path, the next step is to decide whether and how the vehicle should steer toward the path. Reynolds’s algorithm states that the vehicle should only steer toward the path if it’s danger of straying beyond the path—that is, if the distance between the normal point and the predicted future position is greater than the path’s radius. This is illustrated in Figure 5.26.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_28.png" alt="Figure 5.26: A vehicle with a future position on the path and one that’s outside the path.">
|
||
<figcaption>Figure 5.26: A vehicle with a future position on the path and one that’s outside the path.</figcaption>
|
||
</figure>
|
||
<p>I can encode that logic with a simple <code>if</code> statement, and use my earlier <code>seek()</code> method to steer the vehicle when necessary.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let distance = p5.Vector.dist(future, normalPoint);
|
||
|
||
// If the vehicle is outside the path, seek the target.
|
||
if (distance > path.radius) {
|
||
//{!1} The desired velocity and steering force can make use of the seek method created in Example 5.1.
|
||
this.seek(target);
|
||
}</pre>
|
||
<p>But what’s the target that the path follower is seeking? Reynolds’s algorithm involves picking a point ahead of the normal on the path. Since I know the vector that defines the path (<span data-type="equation">\vec{B}</span>), I can implement this “point ahead” by adding a vector that points in <span data-type="equation">\vec{B}</span>’s direction to the vector representing the normal point, as in Figure 5.27.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_29.png" alt="Figure 5.27: The target is 25 pixels (an arbitrary choice) ahead of the normal point along the path.">
|
||
<figcaption>Figure 5.27: The target is 25 pixels (an arbitrary choice) ahead of the normal point along the path.</figcaption>
|
||
</figure>
|
||
<p>I’ll arbitrarily say the target should be 25 pixels ahead of the normal.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let distance = p5.Vector.dist(future, normalPoint);
|
||
if (distance > path.radius) {
|
||
//{!2} Set magnitude to 25 pixels (picked arbitrarily).
|
||
b.setMag(25);
|
||
//{!1} Add b to normalPoint to find the target 25 pixels ahead on the path.
|
||
const target = p5.Vector.add(normalPoint, b);
|
||
//{!1} Seek the target.
|
||
this.seek(target);
|
||
}</pre>
|
||
<p>Putting it all together, here’s the path following method in the <code>Vehicle</code> class.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-55-simple-path-following">Example 5.5: Simple Path Following</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/zcH21K3T3" data-example-path="examples/05_steering/noc_5_05_path_following_simple"><img src="examples/05_steering/noc_5_05_path_following_simple/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> follow(path) {
|
||
//{!3} Step 1: Predict the vehicle’s future position.
|
||
let future = this.velocity.copy();
|
||
future.setMag(25);
|
||
future.add(this.position);
|
||
|
||
//{!1} Step 2: Find the normal point along the path.
|
||
let normalPoint = getNormalPoint(future, path.start, path.end);
|
||
|
||
//{!3} Step 3: Look a little further along the path and set a target.
|
||
// (To optimize, this could be moved into the if statement to only find the target if it's off the path.)
|
||
let b = p5.Vector.sub(path.end, path.start);
|
||
b.setMag(25);
|
||
let target = p5.Vector.add(normalPoint, b);
|
||
|
||
//{!5} Step 4: If we're off the path,
|
||
// seek that target in order to get on the path.
|
||
let distance = p5.Vector.dist(normalPoint, future);
|
||
if (distance > path.radius) {
|
||
this.seek(target);
|
||
}
|
||
}</pre>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_30.png" alt="Figure 5.28: The elements of the getNormalPoint() function: position, a, and b.">
|
||
<figcaption>Figure 5.28: The elements of the <code>getNormalPoint()</code> function: <code>position</code>, <code>a</code>, and <code>b</code>.</figcaption>
|
||
</figure>
|
||
</div>
|
||
<p></p>
|
||
<p>Notice that instead of using all that dot product and scalar projection code to find the normal point, I instead call a function: <code>getNormalPoint()</code>. In cases like this, it’s useful to break out the code that performs a specific task (finding a normal point) into a function that can be called when required. The function takes three vector arguments (see Figure 5.28): the first defines a point <span data-type="equation">p</span> in Cartesian space (the vehicle’s future position), and the second and third define a line segment between two points <span data-type="equation">a</span> and <span data-type="equation">b</span> (the path).</p>
|
||
<pre class="codesplit" data-code-language="javascript"> getNormalPoint(position, a, b) {
|
||
// Vector that points from a to position
|
||
let vectorA = p5.Vector.sub(position, a);
|
||
// Vector that points from a to b
|
||
let vectorB = p5.Vector.sub(b, a);
|
||
|
||
// Using the dot product for scalar projection
|
||
vectorB.normalize();
|
||
vectorB.mult(vectorA.dot(vectorB));
|
||
//{!1} Finding the normal point along the line segment
|
||
let normalPoint = p5.Vector.add(a, vectorB);
|
||
|
||
return normalPoint;
|
||
}</pre>
|
||
<p>What do I have so far? I have a <code>Path</code> class that defines a path as a line between two points. I have a <code>Vehicle</code> class with a method to follow the path (using steering to seek a target along the path). In all, it makes for a decent example, and yet it’s pretty darn limiting. What’s missing?</p>
|
||
<p>Take a deep breath. You’re almost there.</p>
|
||
<h3 id="path-following-with-multiple-segments">Path Following with Multiple Segments</h3>
|
||
<p>What if I want a vehicle to follow a more complex path than just a single straight line? Perhaps a curved path that moves in a variety of directions, as in Figure 5.29?</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_31.png" alt="Figure 5.29: A more complex path">
|
||
<figcaption>Figure 5.29: A more complex path</figcaption>
|
||
</figure>
|
||
<p>Maybe I’m being a little too ambitious. I <em>could</em> investigate algorithms for following a curved path, but I’m much less likely to end up needing a cool compress on my forehead if I stick with straight line segments, like in Figure 5.30. I could always still <em>draw</em> the path as a curve, but it’s best to approximate it behind the scenes with simplified geometric forms for the necessary calculations.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_32.png" alt="Figure 5.30: The same curved path, but approximated as connected line segments">
|
||
<figcaption>Figure 5.30: The same curved path, but approximated as connected line segments</figcaption>
|
||
</figure>
|
||
<p>If I made path following work with one line segment, how do I make it work with a series of connected line segments? The key is in how I find the target point along the path.</p>
|
||
<p>To find the target with just one line segment, I had to compute the normal to that line segment. Now that there’s a series of line segments, there's also a series of normal points to be computed—one for each segment (see Figure 5.31). Which one does the vehicle choose? The solution Reynolds proposed is to pick the normal point that is (a) closest and (b) on the path itself.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_33.png" alt="Figure 5.31: Finding the closest normal point along a series of connected line segments">
|
||
<figcaption>Figure 5.31: Finding the closest normal point along a series of connected line segments</figcaption>
|
||
</figure>
|
||
<p>If you have a point and an infinitely long line, you’ll always have a normal point that touches the line. But if you have a point and a finite line segment, you won’t necessarily find a normal that’s on the line segment itself. If this happens for any of the segments, I can disqualify those normals. Once I’m left with just those normals that are on the path itself (only two in Figure 5.31), I pick the one that’s shortest.</p>
|
||
<p>To write the code for this, I’ll expand the <code>Path</code> class to have an array of points (rather than just the start and end).</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/ntGaosFQ7" data-example-path="examples/05_steering/5_6_path_segments_only"><img src="examples/05_steering/5_6_path_segments_only/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
<pre class="codesplit" data-code-language="javascript"> class Path {
|
||
|
||
constructor() {
|
||
this.radius = 20;
|
||
//{!1} A path is now an array of points (p5.Vector objects).
|
||
this.points = [];
|
||
}
|
||
|
||
//{!4} This method allows us to add points to the path.
|
||
addPoint(x, y) {
|
||
let point = createVector(x, y);
|
||
this.points.push(point);
|
||
}
|
||
|
||
show() {
|
||
//{!8} Draw a thicker gray line for the path radius.
|
||
stroke(200);
|
||
strokeWeight(this.radius * 2);
|
||
noFill();
|
||
beginShape();
|
||
for (let i = 0; i < this.points.length; i++) {
|
||
vertex(this.points[i].x, this.points[i].y);
|
||
}
|
||
endShape();
|
||
|
||
//{!7} Draw a thin line for the path center.
|
||
stroke(0);
|
||
strokeWeight(1);
|
||
beginShape();
|
||
for (let i = 0; i < this.points.length; i++) {
|
||
vertex(this.points[i].x, this.points[i].y);
|
||
}
|
||
endShape();
|
||
}
|
||
}</pre>
|
||
<p>Now that the <code>Path</code> class has been updated, it’s the vehicle’s turn to learn how to accommodate multiple line segments. All it did before was find the normal for one line. Using a loop, it can find the normals for all the segments.</p>
|
||
<pre class="codesplit" data-code-language="javascript">for (let i = 0; i < path.points.length - 1; i++) {
|
||
let a = path.points[i];
|
||
let b = path.points[i + 1];
|
||
//{!1 .offset-top} Find the normal for each line segment.
|
||
let normalPoint = getNormalPoint(future, a, b);</pre>
|
||
<p>The next step is to test if the normal point is actually between points <code>a</code> and <code>b</code>. Since I know the path goes from left to right in this example, I can test if the <code>x</code> component of <code>normalPoint</code> is outside the <code>x</code> components of <code>a</code> and <code>b</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> if (normalPoint.x < a.x || normalPoint.x > b.x) {
|
||
//{!1} Use the end point of the segment
|
||
// as our normal point if we can’t find one.
|
||
normalPoint = b.copy();
|
||
}</pre>
|
||
<p>If it’s not within the line segment, I’ll just pretend the end point of that line segment is the normal. (You might also try the beginning point, depending on the particulars of your path.) This will ensure that the vehicle always stays on the path, even if it strays out of the bounds of the line segments themselves.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-510">Exercise 5.10</h3>
|
||
<p>A more general-purpose way to test if the normal point lies on the segment would be to sum the distances between <code>normalPoint</code> and <code>a</code> and <code>b</code>. If the result is greater than the length of the line segment, then the normal is outside the segment. Can you write this algorithm with p5.js?</p>
|
||
</div>
|
||
<p>Finally, I need to find the closest normal point to the vehicle. To accomplish this, I can start with a very high “world record” distance and iterate through each normal point to see if it beats (is less than) the record. Each time a normal point beats the record, the world record is updated, and the winning point is stored in a variable named <code>target</code>. At the end of the loop, <code>target</code> will hold the closest normal point.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-56-path-following">Example 5.6: Path Following</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/1T-6LupvJ" data-example-path="examples/05_steering/noc_5_06_path_following"><img src="examples/05_steering/noc_5_06_path_following/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">let target = null;
|
||
//{!1} Start with a very high record that can easily be beaten, like infinity!
|
||
let worldRecord = Infinity;
|
||
|
||
for (let i = 0; i < path.points.length - 1; i++) {
|
||
let a = path.points[i];
|
||
let b = path.points[i + 1];
|
||
let normalPoint = getNormalPoint(future, a, b);
|
||
|
||
if (normalPoint.x < a.x || normalPoint.x > b.x) {
|
||
normalPoint = b.copy();
|
||
}
|
||
|
||
let distance = p5.Vector.dist(future, normalPoint);
|
||
|
||
//{!4} If we beat the record, then this should be our target.
|
||
if (distance < worldRecord) {
|
||
worldRecord = distance;
|
||
target = normalPoint.copy();
|
||
}
|
||
|
||
//{!3} Look at the direction of the line segment in order to seek a little bit ahead of the normal
|
||
let dir = p5.Vector.sub(b, a);
|
||
dir.setMag(25);
|
||
target.add(dir);
|
||
}</pre>
|
||
<p>You may have noticed the use of <code>Infinity</code> to initialize <code>worldRecord</code>. In JavaScript, <code>Infinity</code> is a special numeric value that represents, well, infinity. It works in this case because I need a starting value that will always be higher than any plausible distance calculated in the code. The first calculated distance will always set a new “world record,” against which all the others will be compared.</p>
|
||
<p>I also want to highlight the hard-coded value of <code>25</code>, which sets the distance ahead on the path from the normal for the target. Craig Reynolds indicates that this value should be dynamic and calculated based on the vehicle's distance to the path and its speed. Give this a try and see how it improves the accuracy or responsiveness of the path following behavior!</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-511">Exercise 5.11</h3>
|
||
<p>Create a path that changes over time. Can the points that define the path itself have their own steering behaviors?</p>
|
||
</div>
|
||
<h2 id="complex-systems">Complex Systems</h2>
|
||
<p>I said the purpose of this chapter was to breathe life into the things that move around p5.js canvases. You’ve come along way by learning to write the code for an autonomous agent and playing with examples of that agent’s individual behaviors. But this is no place to stop. Yes, a vehicle is a simulated being that makes decisions about how to seek and flow and follow. But what is a life led alone, without the love and support of others?</p>
|
||
<p>And so, as a logical next step, I want to take the work I’ve done developing behaviors for individual autonomous agents and apply it to simulations that involve <em>many</em> autonomous agents operating in parallel—agents that have an ability to perceive not only their physical environment but also the actions of their fellow agents, and then act accordingly. In other words, I want to create complex systems with p5.js.</p>
|
||
<p>A <strong>complex system</strong> is typically defined as a system that’s “more than the sum of its parts.” While the individual elements of the system may be incredibly simple and easily understood, the behavior of the system as a whole can be highly complex, intelligent, and difficult to predict.</p>
|
||
<p>Think, for example, about a tiny, crawling ant—one single ant. An ant is an autonomous agent; it can perceive its environment (using antennae to gather information about the direction and strength of chemical signals) and make decisions about how to move based on those signals. But can a single ant acting alone build a nest, gather food, or defend its queen? An ant is a simple unit that can only perceive its immediate environment. A <em>colony</em> of ants, however, is a sophisticated, complex system, a “superorganism” in which the components work together to accomplish difficult, complicated goals.</p>
|
||
<p>Here are three key principles that will guide my work with complex systems:</p>
|
||
<ul>
|
||
<li><strong>Simple units have short-range relationships.</strong> This is what I’ve been building all along: vehicles that have a limited perception of their environment.</li>
|
||
<li><strong>Simple units operate in parallel.</strong> For every cycle through the <code>draw()</code> loop, each unit will calculate its own steering forces. This will create the appearance of all the units working in parallel.</li>
|
||
<li><strong>Systems as a whole exhibit emergent phenomena.</strong> Complex behaviors, patterns, and intelligence can emerge from the interactions between simple units. This phenomenon occurs in nature, such as in ant colonies, migration patterns, earthquakes, and snowflakes. The question is whether the same results can be achieved in a p5.js sketch?</li>
|
||
</ul>
|
||
<p>Beyond these core principles, here are three additional qualities of complex systems that will help frame the discussion, as well as provide guidelines for features to include in a software simulation. It’s important to acknowledge that this is a fuzzy set of characteristics, and not all complex systems have all of them.</p>
|
||
<ul>
|
||
<li><strong>Non-linearity.</strong> This aspect of complex systems is often casually referred to as “the butterfly effect,” coined by mathematician and meteorologist Edward Norton Lorenz, a pioneer in the study of chaos theory. In 1961, Lorenz was running a computer weather simulation for the second time and, perhaps to save a little time, typed in a starting value of 0.506 instead of 0.506127. The end result was completely different from the first result of the simulation. Stated more evocatively, the theory is that a single butterfly flapping its wings on the other side of the world could cause a massive weather shift and ruin your weekend at the beach. It‘s called “non-linear” because there isn’t a linear relationship between a change in initial conditions and a change in outcome. A small change in initial conditions can have a massive effect on the outcome. Non-linear systems are a superset of chaotic systems. In Chapter 7, you’ll see how even in a system of many zeros and ones, if you change just one bit, the result will be completely different.</li>
|
||
<li><strong>Competition and cooperation.</strong> One ingredient that often makes a complex system tick is the presence of both competition and cooperation between the elements. In the upcoming flocking system, there will be three rules—alignment, cohesion, and separation. Alignment and cohesion will ask the elements to “cooperate” by trying to stay together and move together. Separation, however, will ask the elements to “compete” for space. When the time comes, try taking out just the cooperation or just the competition, and you’ll see how the system loses its complexity. Competition and cooperation are found together in living complex systems, but not in non-living complex systems like the weather.</li>
|
||
<li><strong>Feedback.</strong> Complex systems often include a loop where the output of the system is fed back into the system to influence its behavior in a positive or negative direction. Let’s say you decide to take public transportation to work each day because it’s the most reliable and cost-effective solution, and you’re put off by the traffic congestion and environmental impact of driving. You aren’t alone: others turn to public transportation, too. The system grows more efficient and attractive, serving more people with the same resources, and meanwhile, vehicle traffic is reduced. Over time, however, the system may struggle to accommodate the rising demand, leading to overcrowding, delays, and increased fares to fund infrastructure improvements. As a result, you and others start to switch back to driving, thereby increasing traffic congestion once again and reducing public transport’s efficiency. As traffic worsens, the funds from increased fares are (hopefully) used to improve public transport infrastructure, making it more appealing once again. In this way, the cost and efficiency of public transportation are both the input of the system (determining whether you choose to use it or not) and the output (the degree of traffic congestion and subsequent cost and efficiency). Economic models are just one example of a human complex system. Others include fads and trends, elections, crowds, and traffic flow.</li>
|
||
</ul>
|
||
<p>Complexity will serve as a key theme for much of the remainder of the book. In this section, I’ll begin by introducing an additional feature to the <code>Vehicle</code> class: the ability to perceive neighboring vehicles. This enhancement will pave the way for a culminating example of a complex system, where the interplay of simple individual behaviors results in an emergent behavior: flocking<em>.</em></p>
|
||
<h3 id="implementing-group-behaviors-or-lets-not-run-into-each-other">Implementing Group Behaviors (or: Let’s Not Run Into Each Other)</h3>
|
||
<p>Managing a group of objects is certainly not a new concept. You’ve seen this before—in Chapter 4, where I developed the <code>Emitter</code> class to represent an overall particle system. There, I used an array to store a list of individual particles. I’ll start with the same technique here and store <code>Vehicle</code> objects in an array.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// Declare an array of Vehicle objects.
|
||
let vehicles;
|
||
|
||
function setup() {
|
||
//{!3} Initialize and fill the array
|
||
// with a bunch of Vehicles.
|
||
vehicles = [];
|
||
for (let i = 0; i < 100; i++) {
|
||
vehicles.push(new Vehicle(random(width), random(height)));
|
||
}
|
||
}</pre>
|
||
<p>Now, when it comes time to manipulate all the vehicles in <code>draw()</code>, I can loop through the array and call the necessary methods.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw(){
|
||
for (let vehicle of vehicles) {
|
||
vehicle.update();
|
||
vehicle.show();
|
||
}
|
||
}</pre>
|
||
<p>Maybe I want to add a behavior, a force to be applied to all the vehicles. This could be seeking the mouse.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> vehicle.seek(mouseX, mouseY);</pre>
|
||
<p>But that’s an <em>individual</em> behavior. and I’ve already spent the bulk of this chapter worrying about individual behaviors. You’re here because you want to apply a <em>group</em> behavior. I’ll begin with <strong><em>separation</em></strong>, a behavior that commands, “Avoid colliding with your neighbors!”</p>
|
||
<pre class="codesplit" data-code-language="javascript"> vehicle.separate();</pre>
|
||
<p>That looks good, but it’s not quite right. What’s missing? In the case of <code>seek()</code>, I said, “Seek <code>mouseX</code> and <code>mouseY</code>.” In the case of <code>separate()</code>, I’m saying, “Separate from <em>everyone else</em>.” Who is everyone else? It’s the list of all the other vehicles.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> vehicle.separate(vehicles);</pre>
|
||
<p>This is the big leap beyond what you saw before with particle systems. Instead of each element (particle or vehicle) operating on its own, I’m now saying, “Hey you, that vehicle there! When it comes time for you to operate, you need to operate with an awareness of everyone else. So I’m going to go ahead and pass you the list of everyone else.”</p>
|
||
<p>Putting together what I’ve done so far, here are the <code>setup()</code> and <code>draw()</code> functions for a sketch that exhibits group behavior.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let vehicles;
|
||
|
||
function setup() {
|
||
createCanvas(640, 240);
|
||
vehicles = [];
|
||
for (let i = 0; i < 100; i++) {
|
||
vehicles.push(new Vehicle(random(width), random(height)));
|
||
}
|
||
}
|
||
|
||
function draw() {
|
||
background(255);
|
||
|
||
for (let vehicle of vehicles) {
|
||
//{!1 .bold} This is really the only new thing we’re doing in this section. We’re asking
|
||
// a Vehicle object to examine all the other vehicles in the process of calculating a
|
||
// separation force.
|
||
vehicle.separate(vehicles);
|
||
vehicle.update();
|
||
vehicle.show();
|
||
}
|
||
}</pre>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_34.png" alt="Figure 5.32: The desired velocity for “separation” (equivalent to “fleeing”) is a vector that points in the opposite direction of a target.">
|
||
<figcaption>Figure 5.32: The desired velocity for “separation” (equivalent to “fleeing”) is a vector that points in the opposite direction of a target.</figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>Of course, this is just the beginning. The real work happens inside the <code>separate()</code> method itself. Reynolds defines the separation behavior as, “Steer to avoid crowding.” In other words, if a given vehicle is too close to you, steer away from that vehicle. Sound familiar? Remember the seek behavior, where a vehicle steers toward a target? Reverse that force and you have the flee behavior, which is what should be applied here to achieve separation (see Figure 5.32).</p>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_35.png" alt="Figure 5.33: Separation from multiple vehicles is the average of all desired fleeing velocities">
|
||
<figcaption>Figure 5.33: Separation from multiple vehicles is the average of all desired fleeing velocities</figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>But what if more than one vehicle is too close? In that case, I’ll define separation as the average of all the vectors pointing away from any close vehicles (Figure 5.33).</p>
|
||
<p>How do I turn that into code? Remember, I’m writing a method called <code>separate()</code> that receives an array of <code>Vehicle</code> objects as an argument.</p>
|
||
<pre class="codesplit" data-code-language="javascript">separate(vehicles) {
|
||
|
||
}</pre>
|
||
<p>Inside this method, I’ll loop through all of the vehicles and see if any are too close.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> // This variable specifies how close is too close.
|
||
let desiredSeparation = 20;
|
||
|
||
for (let other of vehicles) {
|
||
|
||
//{!1 .offset} What is the distance between this vehicle and the other vehicle?
|
||
let d = p5.Vector.dist(this.position, other.position);
|
||
|
||
if (this != other && d < desiredSeparation) {
|
||
//{!1} Any code here will be executed if the Vehicle is within 20 pixels.
|
||
|
||
}
|
||
}</pre>
|
||
<p>Notice how I’m not only checking if the distance is less than a desired separation but also if <code>this</code> is not equal to <code>other</code>. This is a key element. Remember, all the vehicles are in the array; without this extra check, the vehicle will attempt to flee from itself!</p>
|
||
<p>If the vehicles are too close, I compute a vector that points away from the offending vehicle.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> if (this != other && d < desiredseparation) {
|
||
//{!2 .offset} A vector pointing away from the other’s position
|
||
let diff = p5.Vector.sub(this.position, other.position);
|
||
diff.normalize();
|
||
}</pre>
|
||
<p>This isn’t enough. I have a fleeing vector now, but what I really need is the average of the fleeing vectors for all the vehicles that are too close. How do I compute an average? Add up all the vectors and divide by the total.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> //{!1 .bold} Start with an empty vector.
|
||
let sum = createVector();
|
||
//{!1 .bold} We have to keep track of how many Vehicles are too close.
|
||
let count = 0;
|
||
|
||
for (let other of vehicles) {
|
||
|
||
const d = p5.Vector.dist(this.position, other.position);
|
||
if (this != other && d < desiredseparation) {
|
||
let diff = p5.Vector.sub(this.position, other.position);
|
||
diff.normalize();
|
||
//{!2 .bold} Add all the vectors together and increment the count.
|
||
sum.add(diff);
|
||
count++;
|
||
}
|
||
}
|
||
|
||
//{!1 .bold} I have to make sure that there is at least one close
|
||
// vehicle. I don’t want to bother doing anything
|
||
// if nothing is too close (not to mention I can’t
|
||
// divide by zero!).
|
||
if (count > 0) {
|
||
//{.bold}
|
||
sum.div(count);
|
||
}</pre>
|
||
<p>Once I have the average vector (stored in the variable <code>sum</code>), that vector can be scaled to the maximum speed and become the desired velocity—the vehicle <em>desires</em> to move in that direction at maximum speed! (In fact, I really don't have to divide by <code>count</code> anymore since the magnitude is set manually.) And once I have the desired velocity, it’s the same old Reynolds story: steering equals desired minus velocity.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> if (count > 0) {
|
||
//{!1} Scale average to maxspeed
|
||
// (this becomes desired).
|
||
sum.setMag(this.maxspeed);
|
||
|
||
//{!1} Reynolds’s steering formula
|
||
const steer = p5.Vector.sub(sum, vel);
|
||
steer.limit(this.maxforce);
|
||
|
||
//{!1} Apply the force to the vehicle
|
||
this.applyForce(steer);
|
||
}</pre>
|
||
<p>Here’s the method in its entirety.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-57-separation">Example 5.7: Separation</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/S7YOOYs7T" data-example-path="examples/05_steering/noc_5_07_separation"><img src="examples/05_steering/noc_5_07_separation/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> separate(vehicles) {
|
||
//{!1 .bold} Note how the desired separation is based
|
||
// on the Vehicle’s size.
|
||
let desiredSeparation = this.r * 2;
|
||
let sum = createVector();
|
||
let count = 0;
|
||
for (let other of vehicles) {
|
||
const d = p5.Vector.dist(this.position, other.position);
|
||
if (this != other && d < desiredSeparation) {
|
||
let diff = p5.Vector.sub(this.position, other.position);
|
||
//{!1 .bold} What is the magnitude of the p5.Vector
|
||
// pointing away from the other vehicle?
|
||
// The closer it is, the more the vehicle should flee.
|
||
// The farther, the less. So the magnitude is set
|
||
// to be inversely proportional to the distance.
|
||
diff.setMag(1 / d);
|
||
sum.add(diff);
|
||
count++;
|
||
}
|
||
}
|
||
if (count > 0) {
|
||
sum.setMag(this.maxspeed);
|
||
let steer = p5.Vector.sub(sum, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
this.applyForce(steer);
|
||
}
|
||
}</pre>
|
||
<p>The <code>separate()</code> method includes two extra improvements. First, the desired separation now depends on the size of the vehicle, as opposed to an arbitrary constant. This way, the separation behavior adapts dynamically to the individual characteristics of the vehicles. Second, the magnitude of the vector pointing away from a neighboring vehicle is set to be inversely proportional to the distance. This means that the closer the neighbor, the more the vehicle wants to flee, and vice versa.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-512">Exercise 5.12</h3>
|
||
<p>Create a <code>cohere()</code> method that follows the opposite logic of <code>separate()</code>: if a vehicle is beyond a certain distance, steer toward that vehicle. This will keep the group together. (In a moment, I’ll look at what happens when both cohesion and separation play out together in the same simulation.)</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-513">Exercise 5.13</h3>
|
||
<p>Add the separation force to path following to create a simulation of Reynolds’s “Crowd Path Following.”</p>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/3KSPCN0x-z" data-example-path="examples/05_steering/exercise_5_13_crowd_path_following"><img src="examples/05_steering/exercise_5_13_crowd_path_following/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<h3 id="combining-behaviors">Combining Behaviors</h3>
|
||
<p>The most exciting and intriguing group behaviors come from mixing and matching multiple steering forces. After all, how could I even begin to simulate emergence in a complex system through a sketch that only has one rule?</p>
|
||
<p>When there are multiple steering forces at play, I need a mechanism for managing them all. You may be thinking, “This is nothing new. We juggle multiple forces all the time.” You would be right. In fact, this technique appeared as early as Chapter 2.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let wind = createVector(0.001, 0);
|
||
let gravity = createVector(0, 0.1);
|
||
mover.applyForce(wind);
|
||
mover.applyForce(gravity);</pre>
|
||
<p>Here, there’s a <code>Mover</code> object that responds to two forces. This all works nicely because of the way the <code>Mover</code> class was designed to accumulate the force vectors into its acceleration vector. In this chapter, however, the forces stem from the internal desires of the movers (now called vehicles). And those desires can be weighted, so that some hold more sway than others. For example, consider a sketch where all vehicles have two desires:</p>
|
||
<ul>
|
||
<li><strong><em>Seek the mouse position.</em></strong></li>
|
||
<li><strong><em>Separate from any vehicles that are too close.</em></strong></li>
|
||
</ul>
|
||
<p>Imagine the vehicles represent a school of fish. Although they want to avoid colliding with each other, their primary concern is seeking out a food source (the mouse). Being able to adjust the weights of the two steering forces is crucial to achieving this effect.</p>
|
||
<p>To begin, I’ll add a method called <code>applyBehaviors()</code> to the <code>Vehicle</code> class to manage all of the behaviors.</p>
|
||
<pre class="codesplit" data-code-language="javascript">applyBehaviors(vehicles) {
|
||
this.separate(vehicles);
|
||
this.seek(createVector(mouseX, mouseY));
|
||
}</pre>
|
||
<p>Here a single method takes care of calling the other methods that apply the forces—<code>separate()</code> and <code>seek()</code>. I could start mucking around within those methods to adjust the strength of the forces they’re calculating, but it might be easier to instead ask those methods to simply calculate and return the forces. Then I can adjust the forces’ strength and apply them to the vehicle’s acceleration within <code>applyBehaviors()</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> applyBehaviors(vehicles) {
|
||
let separate = this.separate(vehicles);
|
||
let seek = this.seek(createVector(mouseX, mouseY));
|
||
//{!2} Apply the forces here since seek() and separate() no longer do so.
|
||
this.applyForce(separate);
|
||
this.applyForce(seek);
|
||
}</pre>
|
||
<p>Here’s how this new approach changes the <code>seek()</code> method:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> seek(target) {
|
||
let desired = p5.Vector.sub(target, this.position);
|
||
desired.setMag(this.maxspeed);
|
||
const steer = p5.Vector.sub(desired, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
|
||
//{!1 .line-through}
|
||
this.applyForce(steer);
|
||
//{!1} Instead of applying the force return the vector.
|
||
return steer;
|
||
}</pre>
|
||
<p>This is a subtle change, but incredibly important: it allows the strength of these forces to be weighted all in one place.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-58-combining-steering-behaviors-seek-and-separate">Example 5.8: Combining Steering Behaviors (Seek and Separate)</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/UJEwENSN3" data-example-path="examples/05_steering/noc_5_08_separation_and_seek"><img src="examples/05_steering/noc_5_08_separation_and_seek/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">applyBehaviors(vehicles) {
|
||
let separate = this.separate(vehicles);
|
||
let seek = this.seek(createVector(mouseX, mouseY));
|
||
//{!2 .bold} These values can be whatever you want them to be!
|
||
// They can be variables that are customized for
|
||
// each vehicle, or they can change over time.
|
||
separate.mult(1.5);
|
||
seek.mult(0.5);
|
||
this.applyForce(separate);
|
||
this.applyForce(seek);
|
||
}</pre>
|
||
<p>In this code, I use <code>mult()</code> to adjust the forces. By multiplying each force vector by a factor, its magnitude is scaled accordingly. These factors, in this case 1.5 for <code>separate</code> and 0.5 for <code>seek</code>, represent the weight assigned to each force. However, the weights don’t have to be constants. Think about how they might vary dynamically based on conditions within the environment or properties of the vehicle. For example, what if the <code>seek</code> weight increases when the vehicle detects food nearby (imagine the vehicle as a creature with a <code>hunger</code> property) or the <code>separate</code> weight becomes larger if the vehicle enters a crowded area. This flexibility in adjusting the weights allows for more sophisticated and nuanced behaviors to emerge.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-514">Exercise 5.14</h3>
|
||
<p>Modify Example 5.8 so that the behavior weights change over time. For example, what if the weights were calculated according to a sine wave or Perlin noise? Or what if some vehicles are more concerned with seeking and others more concerned with separating? Can you introduce other steering behaviors as well?</p>
|
||
</div>
|
||
<h3 id="flocking">Flocking</h3>
|
||
<p><strong>Flocking</strong> is a group animal behavior found in many living creatures, such as birds, fish, and insects. In 1986, Craig Reynolds created a computer simulation of flocking behavior and documented the algorithm in his paper “Flocks, Herds, and Schools: A Distributed Behavioral Model.” Recreating this simulation in p5.js will bring together all the concepts in this chapter.</p>
|
||
<ol>
|
||
<li><em>I will use the steering force formula (steer = desired – velocity) to implement the rules of flocking.</em></li>
|
||
<li><em>These steering forces</em><em> will be </em><em>group behaviors and will require each vehicle to perceive all the other vehicles.</em></li>
|
||
<li><em>I will combine and weight multiple forces.</em></li>
|
||
<li><em>The result will be a complex system—intelligent group behavior will emerge from the simple rules of flocking without the presence of a centralized system or leader.</em></li>
|
||
</ol>
|
||
<p>The good news is, I’ve already demonstrated items 1 through 3 in this chapter, so this section can just be about putting it all together and seeing the result.</p>
|
||
<p>Before I begin, I should mention that I’m going to change the name of the <code>Vehicle</code> class (yet again). Reynolds uses the term “boid” (a made-up word that refers to a bird-like object) to describe the elements of a flocking system. I’ll do the same.</p>
|
||
<p>There are three rules that govern flocking.</p>
|
||
<ol>
|
||
<li><strong>Separation</strong> (also known as “avoidance”): Steer to avoid colliding with your neighbors.</li>
|
||
<li><strong>Alignment</strong> (also known as “copy”): Steer in the same direction as your neighbors.</li>
|
||
<li><strong>Cohesion</strong> (also known as “center”): Steer toward the center of your neighbors (stay with the group).</li>
|
||
</ol>
|
||
<p>These rules are illustrated in Figure 5.34.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_36.png" alt="Figure 5.34: The three rules of flocking: separation, alignment, and cohesion. The example vehicle and desired velocity are bold.">
|
||
<figcaption>Figure 5.34: The three rules of flocking: separation, alignment, and cohesion. The example vehicle and desired velocity are bold.</figcaption>
|
||
</figure>
|
||
<p>Just as with Example 5.8, where I combined separation and seeking, I’ll want the <code>Boid</code> objects to have a single method that manages all three behaviors. I’ll call it <code>flock()</code>.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> flock(boids) {
|
||
//{!3} The three flocking rules
|
||
let separation = this.separate(boids);
|
||
let alignment = this.align(boids);
|
||
let cohesion = this.cohere(boids);
|
||
|
||
//{!3} Arbitrary weights for these forces (Try different ones!)
|
||
separation.mult(1.5);
|
||
alignment.mult(1.0);
|
||
cohesion.mult(1.0);
|
||
|
||
//{!3} Applying all the forces
|
||
this.applyForce(separation);
|
||
this.applyForce(alignment);
|
||
this.applyForce(cohesion);
|
||
}</pre>
|
||
<p>Now, it’s just a matter of implementing the three rules. I did separation already; it’s identical to the previous example. Instead, I’ll focus on alignment, or steering in the same direction as the neighboring boids. As with all other steering behaviors, I have to boil down this concept into a desire: the boid’s desired velocity is the average velocity of its neighbors. The algorithm is therefore to calculate the average velocity of all the other boids and set that to the desired velocity.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> align(boids) {
|
||
// Add up all the velocities and divide by the total
|
||
// to calculate the average velocity.
|
||
let sum = createVector(0, 0);
|
||
for (let other of boids) {
|
||
sum.add(other.velocity);
|
||
}
|
||
sum.div(boids.length);
|
||
|
||
// Vehicle desires to go in that direction at maximum speed.
|
||
sum.setMag(this.maxspeed);
|
||
|
||
//{!3} Reynolds’s steering force formula
|
||
let steer = p5.Vector.sub(sum, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
return steer;
|
||
}</pre>
|
||
<p>This is pretty good, but it’s missing one rather crucial detail. One of the key principles behind complex systems like flocking is that the elements (in this case, boids) have <em>short-range</em> relationships. Thinking about ants again, it’s pretty easy to imagine an ant being able to sense its immediate environment, but less so an ant having an awareness of what another ant is doing hundreds of feet away. Indeed, the fact that the ants manifest such complex collective behavior from only these neighboring relationships is what makes them so exciting in the first place.</p>
|
||
<p>In the <code>align()</code> method, I’m currently taking the average velocity of <em>all</em> the boids, whereas I should really only be looking at the boids within a certain distance (see Figure 5.35). That distance threshold can be variable, of course. You could design boids that can see only 20 pixels away or boids that can see 100 pixels away.</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_37.png" alt="Figure 5.35: The example vehicle (bold) only interacts with the vehicles within its neighborhood (the circle).">
|
||
<figcaption>Figure 5.35: The example vehicle (bold) only interacts with the vehicles within its neighborhood (the circle).</figcaption>
|
||
</figure>
|
||
<p>I already applied similar logic when I implemented separation, calculating a force based only on other vehicles within a certain distance. Now I want to do the same for alignment (and eventually, cohesion).</p>
|
||
<pre class="codesplit" data-code-language="javascript"> align(boids) {
|
||
//{!1} This is an arbitrary value and could vary from boid to boid.
|
||
let neighborDistance = 50;
|
||
let sum = createVector(0, 0);
|
||
let count = 0;
|
||
for (let other of boids) {
|
||
let d = p5.Vector.dist(this.position, other.position);
|
||
if ((this != other) && (d < neighborDistance)) {
|
||
sum.add(other.velocity);
|
||
//{!1} For an average, need to keep track of
|
||
// how many boids are within the distance.
|
||
count++;
|
||
}
|
||
}
|
||
if (count > 0) {
|
||
sum.setMag(this.maxspeed);
|
||
let steer = p5.Vector.sub(sum, this.velocity);
|
||
steer.limit(this.maxforce);
|
||
return steer;
|
||
//{!3} If no close boids are found, the steering force is zero.
|
||
} else {
|
||
return createVector(0, 0);
|
||
}
|
||
}</pre>
|
||
<p>As with the <code>separate()</code> method, I’ve included the condition <code>this != other</code> condition to ensure that a boid doesn’t consider itself when calculating the average velocity. It would probably work regardless, but having each boid constantly be influenced by its own velocity could lead to a feedback loop that would disrupt the overall behavior.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-515">Exercise 5.15</h3>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_38.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>Can you rewrite the <code>align()</code> method so that boids only see other boids that fall within a direct line of sight?</p>
|
||
</div>
|
||
<p>The code for cohesion is quite similar to that for alignment. The only difference is that instead of calculating the average <em>velocity</em> of the boid’s neighbors, I want to calculate the average <em>position</em> of the boid’s neighbors (and use that as a target to seek).</p>
|
||
<pre class="codesplit" data-code-language="javascript"> cohesion(boids) {
|
||
let neighborDistance = 50;
|
||
let sum = createVector(0, 0);
|
||
let count = 0;
|
||
for (let other of boids) {
|
||
let d = p5.Vector.dist(this.position, other.position);
|
||
if ((this != other) && (d < neighborDistance)) {
|
||
//{!2} Adding up all the others’ positions
|
||
sum.add(other.position);
|
||
count++;
|
||
}
|
||
}
|
||
if (count > 0) {
|
||
sum.div(count);
|
||
//{!1 .bold} Here we make use of the seek() function we
|
||
// wrote in Example 5.8. The target
|
||
// we seek is the average position of
|
||
// our neighbors.
|
||
return this.seek(sum);
|
||
} else {
|
||
return createVector(0, 0);
|
||
}
|
||
}</pre>
|
||
<p>It’s also worth taking the time to write a class called <code>Flock</code> that manages the whole group of boids. It will be virtually identical to the <code>ParticleSystem</code> class from Chapter 4, with only one tiny change: when I call <code>run()</code> on each <code>Boid</code> object (as I did to each <code>Particle</code> object), I’ll pass in a reference to the entire array of boids.</p>
|
||
<pre class="codesplit" data-code-language="javascript">class Flock {
|
||
constructor() {
|
||
this.boids = [];
|
||
}
|
||
|
||
run() {
|
||
for (let boid of this.boids) {
|
||
//{!1 .bold} Each Boid object must know about
|
||
// all the other Boids.
|
||
boid.run(this.boids);
|
||
}
|
||
}
|
||
|
||
addBoid(boid) {
|
||
this.boids.push(boid);
|
||
}
|
||
}</pre>
|
||
<p>All that remains is to initialize the flock in <code>setup()</code> and run it in <code>draw()</code>.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-59-flocking">Example 5.9: Flocking</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/IkpBw96Sd" data-example-path="examples/05_steering/example_5_9_flocking"><img src="examples/05_steering/example_5_9_flocking/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript">//{!1} A Flock object manages the
|
||
// entire group.
|
||
let flock;
|
||
|
||
function setup() {
|
||
createCanvas(640, 240);
|
||
flock = new Flock();
|
||
for (let i = 0; i < 120; i++) {
|
||
let boid = new Boid(width / 2, height / 2);
|
||
//{!1} The Flock starts out with 120 boids.
|
||
flock.addBoid(boid);
|
||
}
|
||
}
|
||
|
||
function draw() {
|
||
background(255);
|
||
flock.run();
|
||
}</pre>
|
||
<p>Just as with the particle systems from Chapter 4, you can see the elegance of object-oriented programming in how it simplifies the <code>setup()</code> and <code>draw()</code> functions.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-516">Exercise 5.16</h3>
|
||
<p>Combine flocking with other steering behaviors.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-517">Exercise 5.17</h3>
|
||
<div class="half-width-right">
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_39.png" alt="">
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>In his book The <em>Computational Beauty of Nature</em> (MIT Press, 2000), Gary Flake describes a fourth rule for flocking, <strong>view</strong>: “Move laterally away from any boid that blocks the view.” Have your boids follow this rule.</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-518">Exercise 5.18</h3>
|
||
<p>Create a flocking simulation where all of the parameters (<em>separation weight</em>, <em>cohesion weight</em>, <em>alignment weight</em>, <em>maximum force</em>, <em>maximum speed</em>) change over time. They could be controlled by Perlin noise or by user interaction. (For example, you could use the p5.js <code>createSlider()</code> function to tie the values to slider positions that can be adjusted in real time.)</p>
|
||
</div>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-519">Exercise 5.19</h3>
|
||
<p>Visualize the flock in an entirely different way.</p>
|
||
</div>
|
||
<h2 id="algorithmic-efficiency-or-why-does-my-sketch-run-so-slowly">Algorithmic Efficiency (or: Why Does My Sketch Run So Slowly?)</h2>
|
||
<p>Group behaviors are wonderful, but it’s with a heavy heart that I must admit to you that they can also be slow. In fact, the bigger the group, the slower the sketch can be. I’d love to hide this dark truth from you, because I’d like you to be happy and live a fulfilling and meaningful life, free from concerns about the efficiency of your code. But I’d also like to be able to sleep at night without worrying about your inevitable disappointment when you try to run your flocking simulation with too many boids.</p>
|
||
<p>Usually, when I talk about p5.js sketches running slowly, it’s because drawing to the canvas can be slow—the more you draw, the slower your sketch runs. If you recall the discussion from Chapter 4, switching to a different renderer like WebGL can sometimes alleviate this issue, allowing for faster drawing of larger particle systems. With something like a flocking simulation, however, the slowness derives from the algorithm itself. Computer scientists put this problem in terms of something called<strong> big </strong><span data-type="equation">O</span><strong> notation</strong>, where the <span data-type="equation">O</span> stands for "order." This is a shorthand for describing the efficiency of an algorithm: how many computational cycles does the algorithm require to complete?</p>
|
||
<p>Consider a simple search problem. You have a basket containing 100 chocolate treats, only one of which is pure dark chocolate. That’s the one you want to eat. To find it, you pick the chocolates out of the basket one by one. You might be lucky and find it on the first try, but in the worst-case scenario you have to check all 100 before you find the dark chocolate. To find one thing in 100, you have to check 100 things (or to find one thing in <span data-type="equation">N</span> things, you have to check <span data-type="equation">N</span> times). The big <span data-type="equation">O</span> notation here is <span data-type="equation">O(N)</span>. This, incidentally, is also the big <span data-type="equation">O</span> notation that describes a simple particle system. If you have <span data-type="equation">N</span> particles, you have to run and display those particles <span data-type="equation">N</span> times.</p>
|
||
<p>Now, let’s think about a group behavior such as flocking. For every <code>Boid</code> object, you have to check the velocity and position of every other <code>Boid</code> object before you can calculate its steering force. Let’s say you have 100 boids. For boid #1, you need to check 100 boids; for boid #2, you need to check 100 boids; and so on and so forth. In all, for 100 boids, you need to perform 10,000 checks (<span data-type="equation">100 \times 100 = \text{10,000}</span>).</p>
|
||
<p>You might be thinking, “No problem. Computers are fast. They can do 10,000 things pretty easily.” But what if there are 1,000 boids?</p>
|
||
<div data-type="equation">\text{1,000} \times \text{1,000} = \text{1,000,000 cycles}</div>
|
||
<p>This is getting rather slow, but still somewhat manageable. What about 10,000 elements?</p>
|
||
<div data-type="equation">\text{10,000} \times \text{10,000} = \text{100,000,000 cycles}</div>
|
||
<p>Now, things are really getting slow. Really, really, really slow.</p>
|
||
<p>Notice a pattern? As the number of elements increases by a factor of 10, the number of required cycles increases by a factor of 100. More broadly, as the number of elements increases by a factor of <span data-type="equation">N</span>, the cycles increase by a factor of <span data-type="equation">N \times N</span>, or <span data-type="equation">N^2</span>. In big <span data-type="equation">O</span> notation, this is known as <span data-type="equation">O(N^2)</span>, or “n-squared.”</p>
|
||
<p>Perhaps you’re thinking, “No problem. With flocking, I only need to consider the boids that are close to the current boid. So even if I have 1,000 boids, I can just look at, say, the 5 closest boids to each one, and then I only have 5,000 cycles.” You pause for a moment, and then start thinking, “So for each boid I just need to check all the boids and find the 5 closest ones and I’m good!” See the catch-22? Even if you only want to look at the close ones, the only way to know what the close ones are would be to check all of them.</p>
|
||
<p>Or is there another way?</p>
|
||
<h3 id="spatial-subdivisions">Spatial Subdivisions</h3>
|
||
<p>In his 2000 paper <a href="http://www.red3d.com/cwr/papers/2000/pip.pdf">“Interaction with Groups of Autonomous Characters,”</a> Craig Reynolds (surprise, surprise) suggests a technique known as <strong>bin-lattice spatial subdivision</strong> (often called “binning” for short) for optimizing flocking algorithms and other group behaviors. This technique hinges around dividing the simulation space into a grid of smaller cells (or “bins”).</p>
|
||
<p>To demonstrate, imagine the canvas is divided into a grid of 10 rows and 10 columns, for a total of 100 cells (<span data-type="equation">10 \times 10 = 100</span>). And let’s say you have 2,000 boids—a number small enough for you to realistically want, but large enough to run too slowly (<span data-type="equation">\text{2,000} \times \text{2,000} = \text{4,000,000 cycles)}</span>. At any given moment, each boid falls within a cell in the grid, as shown in Figure 5.36. With 2,000 boids and 100 cells, on average there will be approximately 20 boids per cell (<span data-type="equation">\text{2,000} \div 100 = 20</span>).</p>
|
||
<figure>
|
||
<img src="images/05_steering/05_steering_40.png" alt="Figure 5.36: A square canvas full of vehicles, subdivided into a grid of square cells">
|
||
<figcaption>Figure 5.36: A square canvas full of vehicles, subdivided into a grid of square cells</figcaption>
|
||
</figure>
|
||
<p>Now say that in order to apply the flocking rules to a given boid, you only need to look at the other boids that are in that boid’s cell. With an average of 20 boids per cell, each cell would require 400 cycles (<span data-type="equation">20 \times 20 = 400</span>), and with 100 cells, that’s 40,000 cycles total (<span data-type="equation">400 \times 100 = \text{40,000}</span>). That’s a massive savings over 4,000,000!</p>
|
||
<p>To implement the bin-lattice spatial subdivision algorithm in p5.js, I’ll need multiple arrays. The first array keeps track of all the boids, just like in the original flocking example.</p>
|
||
<pre class="codesplit" data-code-language="javascript">let boids = [];</pre>
|
||
<p>The second is a two-dimensional array (repurposing the code from Example 5.4: Flow Field Following) representing the cells in the grid.</p>
|
||
<pre class="codesplit" data-code-language="javascript">// Each cell is 40x40 pixels
|
||
let resolution = 40;
|
||
// How many columns and rows based on the width and height?
|
||
let cols = floor(width / resolution);
|
||
let rows = floor(height / resolution);
|
||
// Create the 2D array
|
||
let grid = new Array(cols);
|
||
for (let i = 0; i < grid.length; i++) {
|
||
grid[i] = new Array(rows);
|
||
}</pre>
|
||
<p>Each value in the two-dimensional array is itself an array that will hold references to the <code>Boid</code> objects currently inside that cell in the grid. If you’re keeping score, that’s an array within an array within an array.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> for (let i = 0; i < cols; i++) {
|
||
for (let j = 0; j < rows; j++) {
|
||
//{!1} An array for every cell in the grid.
|
||
grid[i][j] = [];
|
||
}
|
||
}</pre>
|
||
<p>Every cycle through <code>draw()</code>, the array for each grid cell is first cleared. Then each boid registers itself in the appropriate cell according to its position. This way the boids’ cell assignments are updated as the boids move.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw() {
|
||
// Each frame, the grid is reset to empty arrays.
|
||
for (let i = 0; i < cols; i++) {
|
||
for (let j = 0; j < rows; j++) {
|
||
grid[i][j] = [];
|
||
}
|
||
}
|
||
|
||
// Place each boid into the appropriate cell in the grid.
|
||
for (let boid of flock.boids) {
|
||
// Find the right column and row
|
||
let column = floor(boid.position.x / resolution);
|
||
let row = floor(boid.position.y / resolution);
|
||
// Constrain to limits of array
|
||
column = constrain(column, 0, columns - 1);
|
||
row = constrain(row, 0, rows - 1);
|
||
// Add the boid.
|
||
grid[column][row].push(boid);
|
||
}</pre>
|
||
<p>Finally, when it comes time to have the boids check their neighbors, they can look at only those in their particular cell.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-510-bin-lattice-spatial-subdivision">Example 5.10: Bin-Lattice Spatial Subdivision</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/QXONgLiJ0" data-example-path="examples/05_steering/example_5_9_flocking_with_binning"><img src="examples/05_steering/example_5_9_flocking_with_binning/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<pre class="codesplit" data-code-language="javascript"> run(boids) {
|
||
let column = floor(this.position.x / resolution);
|
||
let row = floor(this.position.y / resolution);
|
||
column = constrain(column, 0, columns - 1);
|
||
row = constrain(row, 0, rows - 1);
|
||
// Only these boids will be checked. See the code online for how neighboring cells are also included.
|
||
let neighbors = grid[column][row];
|
||
this.flock(neighbors);
|
||
this.update();
|
||
this.borders();
|
||
this.render();
|
||
}</pre>
|
||
<p>I’ve only covering the basics of the bin-lattice algorithm here. In practice, each boid should also check the boids in the neighboring cells (above, below, left, right, and diagonals), as well as the boids in its own cell. (To find out how that’s done, see the full code on the book’s website.) Even with that extra checking, however, the algorithm is still much more efficient than checking every single boid.</p>
|
||
<p>There are still flaws with this approach, however. For example, what if all the boids congregate in the corner and live in the same cell? Doesn’t that take me right back to checking all 2,000 against all 2,000? In fact, bin-lattice spatial subdivision is best suited to situations where the elements are evenly distributed throughout the canvas. A data structure known as a <strong>quadtree</strong>, however, can handle unevenly distributed systems, preventing the worst-case scenario of all the boids crowding into a single cell.</p>
|
||
<p>The quadtree expands upon the spatial subdivision strategy by dynamically adapting the grid according to the distribution of the boids. Instead of a fixed grid, a quadtree starts with a single large cell that encompasses the entire space. If too many boids are found within this cell, it splits into four smaller cells. This process can repeat for each new cell that gets too crowded, creating a flexible grid that provides finer resolution when and where it's needed.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-511-quadtree">Example 5.11: Quadtree</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/J5XVww9xQ" data-example-path="examples/05_steering/5_11_quadtree_part_1"><img src="examples/05_steering/5_11_quadtree_part_1/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>The quadtree data structure is key to the Barnes-Hut algorithm, which I referenced briefly when building an n-body simulation in Chapter 2. This method uses a quadtree to approximate groups of bodies into a single one when calculating gravitational forces. This drastically reduces the number of calculations needed, allowing simulations with large numbers of bodies to run more efficiently. You can learn more about building a quadtree and applying it to a flocking system as part of <a href="https://thecodingtrain.com/challenges/98-quadtree">Coding Challenge #98 on thecodingtrain.com</a>.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-520">Exercise 5.20</h3>
|
||
<p>Expand the bin-lattice spatial subdivision flocking sketch from Example 5.10 to use a quadtree.</p>
|
||
</div>
|
||
<h3 id="more-optimization-tricks">More Optimization Tricks</h3>
|
||
<p>While I’m at it, here are a few more tips related to keeping your code in tip-top, speedy shape.</p>
|
||
<p><span class="highlight"><strong>1) Use the magnitude squared (or sometimes the distance squared)</strong><strong>.</strong></span></p>
|
||
<p>What is magnitude squared and when should you use it? Think back to how the magnitude of a vector is calculated.</p>
|
||
<pre class="codesplit" data-code-language="javascript">function mag() {
|
||
return sqrt(x * x + y * y);
|
||
}</pre>
|
||
<p>Magnitude requires the square root operation. And so it should! After all, if you want the magnitude of a vector, you have to break out the Pythagorean theorem (we did this in Chapter 1). However, if you could somehow skip taking the square root, your code would run faster.</p>
|
||
<p>Consider a situation where you just want to know the <em>relative</em> magnitude of some vector <code>v</code>. For example, is the magnitude greater than 10?</p>
|
||
<pre class="codesplit" data-code-language="javascript">if (v.mag() > 10) {
|
||
/* Do something! */
|
||
}</pre>
|
||
<p>Well, this is equivalent to saying:</p>
|
||
<pre class="codesplit" data-code-language="javascript">if (v.magSq() > 100) {
|
||
/* Do something! */
|
||
}</pre>
|
||
<p>And how is magnitude squared calculated?</p>
|
||
<pre class="codesplit" data-code-language="javascript">function magSq() {
|
||
return x * x + y * y;
|
||
}</pre>
|
||
<p>Same as magnitude, but without the square root. In the case of a single vector, using <code>magSq()</code> rather than <code>mag()</code> will never significantly improve the performance of a p5.js sketch. However, if you’re computing the magnitude of thousands of vectors each time through <code>draw()</code>, working with the magnitude squared could help your code run a wee bit faster.</p>
|
||
<p><span class="highlight"><strong>2) Calculate sine and cosine lookup tables.</strong></span></p>
|
||
<p>Taking the square root isn’t the only mathematical function that’s slow to compute. Trig functions like sine, cosine, and tangent are also slow. If you just need an individual sine or cosine value here or there in your code, you’re never going to run into a problem. But what if you had something like this?</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw() {
|
||
for (let i = 0; i < 10000; i++) {
|
||
print(sin(PI));
|
||
}
|
||
}</pre>
|
||
<p>Sure, this is a totally ridiculous code snippet that you would never write. But it illustrates a certain point. If you’re calculating the sine of pi 10,000 times, why not just calculate it once, save that value, and refer to it whenever necessary? This is the principle behind sine and cosine <strong>lookup tables</strong>. Instead of calling the sine and cosine functions in your code whenever you need them, you can build an array that stores the results of sine and cosine at angles between <span data-type="equation">0</span> and <span data-type="equation">2\pi</span>, and then just look up the precalculated values when you need them. For example, here are two arrays that store the sine and cosine values for every integer angle from 0 to 359 degrees. I’ll use <code>angleMode(DEGREES)</code> here to simplify the discussion, but the same technique can be applied with radians.</p>
|
||
<pre class="codesplit" data-code-language="javascript">angleMode(DEGREES);
|
||
let sinvalues = [];
|
||
let cosvalues = [];
|
||
for (let i = 0; i < 360; i++) {
|
||
sinvalues[i] = sin(i);
|
||
cosvalues[i] = cos(i);
|
||
}</pre>
|
||
<p>Now, what if you needed to print the sine of pi (or 180 degrees)?</p>
|
||
<pre class="codesplit" data-code-language="javascript">let angle = 180;
|
||
for (let i = 0; i < 10000; i++) {
|
||
print(sinvalues[angle]);
|
||
}</pre>
|
||
<p>The key here is that looking up a pre-calculated value from an array is incredibly fast compared to a complex operation like sine or cosine.</p>
|
||
<div data-type="example">
|
||
<h3 id="example-512-sincos-lookup-table">Example 5.12: Sin/Cos Lookup Table</h3>
|
||
<figure>
|
||
<div data-type="embed" data-p5-editor="https://editor.p5js.org/natureofcode/sketches/uqRJyVq8g" data-example-path="examples/05_steering/example_5_12_sine_cosine_lookup_table"><img src="examples/05_steering/example_5_12_sine_cosine_lookup_table/screenshot.png"></div>
|
||
<figcaption></figcaption>
|
||
</figure>
|
||
</div>
|
||
<p>The code accompanying Example 5.12 enhances the initial snippets by incorporating variables for the lookup table’s precision, allowing it to store values at increments less than 1 degree.</p>
|
||
<p><span class="highlight"><strong>3) Don’t make gajillions of unnecessary </strong><code><strong>p5.Vector</strong></code><strong> objects.</strong></span></p>
|
||
<p>In any sketch, every object you create occupies some space in the computer's memory. This might not be a concern with just a few objects, but when sketches generate a large number of objects, especially in loops or over time, it can impact performance. Sometimes, it turns out that not all the objects are really necessary.</p>
|
||
<p>I have to admit, I’m perhaps the biggest culprit when it comes to creating excessive objects. In the interest of writing clear and understandable examples, I often choose to make extra <code>p5.Vector</code> objects when I absolutely don’t need to. For the most part, this isn’t a problem at all. But sometimes, it can be. Take a look at this example:</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw() {
|
||
for (let v of vehicles) {
|
||
let mouse = createVector(mouseX, mouseY);
|
||
v.seek(mouse);
|
||
}
|
||
}</pre>
|
||
<p>Say the <code>vehicles</code> array has 1,000 vehicles in it. That means I’m also making 1,000 new <code>p5.Vector</code> objects for the mouse’s position every single time through <code>draw()</code>. On any standard laptop or desktop computer purchased in recent times, this sketch likely won’t register a complaint, run slowly, or have any problems. After all, modern computers have tons of RAM, and JavaScript will be able to handle making and disposing of 1,000 or so temporary objects without much of a problem.</p>
|
||
<p>If, however, the number of objects grows larger (and it easily could), there will almost certainly be a problem. As such, you should look for ways to reduce the number of <code>p5.Vector</code> objects you make. In this case, a simple fix is:</p>
|
||
<pre class="codesplit" data-code-language="javascript">function draw() {
|
||
let mouse = createVector(mouseX, mouseY);
|
||
for (let v of vehicles) {
|
||
v.seek(mouse);
|
||
}
|
||
}</pre>
|
||
<p>Now I’ve made just one vector instead of 1,000. Even better, I could turn the vector into a global variable, and then just assign the <code>x</code> and <code>y</code> value within <code>draw()</code> with <code>set()</code>:</p>
|
||
<pre class="codesplit" data-code-language="javascript">let mouse;
|
||
|
||
function setup() {
|
||
mouse = createVector();
|
||
}
|
||
|
||
function draw() {
|
||
mouse.set(mouseX, mouseY);
|
||
for (let v of vehicles) {
|
||
v.seek(mouse);
|
||
}
|
||
}</pre>
|
||
<p>Now I never make a new <code>p5.Vector</code> object once the sketch starts; I just use the same one over the whole length of the sketch!</p>
|
||
<p>Throughout the book’s examples, you’ll find lots of opportunities to reduce the number of temporary objects. (I told you, I’m a major offender.) For example, here’s a snippet from this chapter’s <code>seek()</code> method:</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let desired = p5.Vector.sub(target, this.position);
|
||
desired.normalize();
|
||
desired.mult(this.maxspeed);
|
||
|
||
//{!1 .bold} Create a new vector to store the steering force.
|
||
let steer = p5.Vector.sub(desired,this.velocity);
|
||
steer.limit(this.maxforce);
|
||
return steer;</pre>
|
||
<p>See how I’ve made two vector objects? First, I calculate the desired velocity vector, then the steering force. To be more efficient, I could rewrite this to create only one vector.</p>
|
||
<pre class="codesplit" data-code-language="javascript"> let desired = p5.Vector.sub(target, this.position);
|
||
desired.normalize();
|
||
desired.mult(this.maxspeed);
|
||
|
||
//{!3 .bold} Calculate the steering force in the desired vector.
|
||
desired.sub(this.velocity);
|
||
desired.limit(this.maxforce);
|
||
return desired;</pre>
|
||
<p>I don’t actually need a second vector called <code>steer</code>. I can just reuse the <code>desired</code> vector object and turn it into the steering force by subtracting <code>velocity</code>. I didn’t do this in my example because it makes the code more confusing to read. But in some cases, changes like this may improve efficiency.</p>
|
||
<div data-type="exercise">
|
||
<h3 id="exercise-521">Exercise 5.21</h3>
|
||
<p>Eliminate as many temporary <code>p5.Vector</code> objects from the flocking example as possible. Also use <code>magSq()</code> where possible.</p>
|
||
</div>
|
||
<div data-type="project">
|
||
<h3 id="the-ecosystem-project-5">The Ecosystem Project</h3>
|
||
<p>Step 5 Exercise:</p>
|
||
<p>Use the concept of steering forces to drive the behavior of the creatures in your ecosystem. Some possibilities:</p>
|
||
<ul>
|
||
<li>Create “schools” or “flocks” of creatures.</li>
|
||
<li>Use a seeking behavior for creatures to search for food (for chasing moving prey, consider “pursuit”).</li>
|
||
<li>Use a flow field for the ecosystem environment. For example, how does your system behave if the creatures live in a flowing river?</li>
|
||
<li>Build a creature with countless steering behaviors (as many as you can reasonably add). Think about ways to vary the weights of the behaviors so you can dial them up and down, mixing and matching on the fly. How are creatures’ initial weights set? What rules drive how the weights change over time?</li>
|
||
<li>Complex systems can be nested. Can you design a single creature out of a flock of boids? And can you then make a flock of those creatures?</li>
|
||
<li>Complex systems can have memory (and be adaptive). Can the history of your ecosystem affect the behavior in its current state? (This could be the driving force behind how the creatures adjust their steering force weights.)</li>
|
||
</ul>
|
||
</div>
|
||
</section> |