Interactive Simulation
Design Challenge
Create a sketch that would simulate an existing natural system - look at physics, biology, and other natural sciences for good examples. Start with the environment - where is the system situated? What are the forces the environment might exert on the system?
Examine the agents in the system, their relationships to each other and the environment they are in. Then look at how this system would develop over time. What are the rules that you are going to come up with? What are the parameters you are going to feed into them and what effect will the changes have on the development of the system?
Design tools used: p5.js javascript
Ideation and concepts
In approaching this design challenge, I started to think about natural systems and how some forces attract and repel each other. One of the first ideas I had was a school of fish avoiding a large shark. The idea works fine, but practically, I didn't really have a clue for how it could be coded
This sketch illustrates how the smaller objects would scatter away from the predator, but if was in it's way, it would be consumed.
I moved on to another idea of attraction and repulsion, traffic. Cars must avoid each other and other dangers on the road in order to survive to their destination. Similarly to the shark idea, the largest mechanical predator is a train. Avoid it or get destroyed. So I started thinking about how traffic flows and some basic movements. The train would cut across the traffic and it would be controlled by the user.
Designing this concept and creating javascript code for proved to be very challenging. So I simplified the movements.
The train now travels from north to south, while traffic move east to west on the screen. Here is a sketch of that concept. The idea here is that traffic is free flowing and cars move at different speeds, avoiding each other without colliding. Once the train enters the screen, the cars would detect it and stop traffic so there is no collision.
The interaction of this concept is the control of the train. By pressing the spacebar, it signals the train to start moving south until it reaches the other side to its destination. Another press would reverse course.
If a car would be in its path as it is moving, it would be consumed by train, simulation a tragic event.
If a car would be in its path as it is moving, it would be consumed by train, simulation a tragic event.
Once there is not train on the screen, the traffic would resume its natural course of flow.
The JS Sketch
These randomly colorful rectangles represent the cars in traffic. The top 3 lanes are going west, and the bottom 3 lanes are going east.
The initial collision detection code did make the cars stop but right next to the train as it moved from north to south. The cars were able to make each other stop without crashing into each other, as intended.
However, there are a few bugs that catches the cars as they are moving to their destination. So i increased the radius around the train. And that seemed to work.
Now, any car that is caught in the path of the train still gets consumed and removed from the array, resetting a new car in its place.
Final Thoughts
This code simulates how traffic moves in the world. However, a few tweaks could make this even better. I would like to learn how get cars to see ahead of time and anticipate that a train is coming. Currently, the cars can only detect the train if it is right in front of it. Ideally, each car would be able to recognize that a train is about to cross their path. Also, additional details such as traffic signals and the roads could make this simulation a bit more easier to understand.
You may notice that there is a bug that I have not been able to figure out. The cars stop in front of the train, but they stack on top of each other before resuming their direction of travel.
You may notice that there is a bug that I have not been able to figure out. The cars stop in front of the train, but they stack on top of each other before resuming their direction of travel.
The P5.js Code
let traffics = [];
let train;
let trainMoving = true; // Train will start moving immediately
const lanes = 6; // Number of lanes
const laneHeight = 50; // Height of each lane
const laneYPositions = []; // Array to store the y-coordinate of each lane
const minSpacing = 2; // Minimum spacing between cars (buffer)
const bufferZone = 20; // Buffer zone for stopping cars
const trainBuffer = 50; // Buffer space around the train for stopping
const maxCarsPerLane = 6; // Maximum number of cars per lane
const carWidth = 50; // Width of the car
let train;
let trainMoving = true; // Train will start moving immediately
const lanes = 6; // Number of lanes
const laneHeight = 50; // Height of each lane
const laneYPositions = []; // Array to store the y-coordinate of each lane
const minSpacing = 2; // Minimum spacing between cars (buffer)
const bufferZone = 20; // Buffer zone for stopping cars
const trainBuffer = 50; // Buffer space around the train for stopping
const maxCarsPerLane = 6; // Maximum number of cars per lane
const carWidth = 50; // Width of the car
function setup() {
createCanvas(800, lanes * laneHeight);
createCanvas(800, lanes * laneHeight);
// Calculate y-coordinates for each lane
for (let i = 0; i < lanes; i++) {
laneYPositions.push(i * laneHeight + laneHeight / 2 - 10); // Centering cars in each lane
}
for (let i = 0; i < lanes; i++) {
laneYPositions.push(i * laneHeight + laneHeight / 2 - 10); // Centering cars in each lane
}
// Create multiple traffic objects for each lane
for (let i = 0; i < lanes; i++) {
traffics.push(new Traffic(laneYPositions[i], i < 3 ? -1 : 1)); // Top 3 lanes go right-to-left, bottom 3 go left-to-right
}
for (let i = 0; i < lanes; i++) {
traffics.push(new Traffic(laneYPositions[i], i < 3 ? -1 : 1)); // Top 3 lanes go right-to-left, bottom 3 go left-to-right
}
// Create a single train object with an initial direction
train = new Train(width / 2 - 50, -300, 1);
}
train = new Train(width / 2 - 50, -300, 1);
}
function draw() {
background(220);
background(220);
// Update and display each traffic group
for (let traffic of traffics) {
traffic.update();
traffic.display();
}
for (let traffic of traffics) {
traffic.update();
traffic.display();
}
// Update and display train only if it's moving
if (trainMoving) {
train.move();
if (train.isOffScreen()) {
// Resume all cars' original speed when train exits screen
for (let traffic of traffics) {
traffic.resume();
}
trainMoving = false; // Stop train movement until re-triggered
}
}
train.display();
}
if (trainMoving) {
train.move();
if (train.isOffScreen()) {
// Resume all cars' original speed when train exits screen
for (let traffic of traffics) {
traffic.resume();
}
trainMoving = false; // Stop train movement until re-triggered
}
}
train.display();
}
// Traffic class to manage a group of cars in a lane
class Traffic {
constructor(y, direction) {
this.y = y;
this.direction = direction; // 1 for left-to-right, -1 for right-to-left
this.cars = [];
this.createCars(); // Initialize cars
}
class Traffic {
constructor(y, direction) {
this.y = y;
this.direction = direction; // 1 for left-to-right, -1 for right-to-left
this.cars = [];
this.createCars(); // Initialize cars
}
createCars() {
let numberOfCars = Math.floor(random(1, maxCarsPerLane + 1)); // Random number of cars between 1 and 6
let positions = []; // To store x positions of cars
let numberOfCars = Math.floor(random(1, maxCarsPerLane + 1)); // Random number of cars between 1 and 6
let positions = []; // To store x positions of cars
// Create cars ensuring spacing
for (let i = 0; i < numberOfCars; i++) {
let speed = random(2, 5);
let startX;
for (let i = 0; i < numberOfCars; i++) {
let speed = random(2, 5);
let startX;
// Check for spacing with existing cars
let carSpacing = false;
let attempts = 0; // Limit the number of attempts to find a position
do {
startX = this.direction === 1 ? -random(200) : width + random(200);
carSpacing = true;
let carSpacing = false;
let attempts = 0; // Limit the number of attempts to find a position
do {
startX = this.direction === 1 ? -random(200) : width + random(200);
carSpacing = true;
// Check if the new car's position conflicts with existing cars
for (let pos of positions) {
if (this.direction === 1) {
// Check right-to-left direction
if (startX + carWidth + minSpacing > pos && startX < pos + carWidth + minSpacing) {
carSpacing = false; // Collision detected
break;
}
} else {
// Check left-to-right direction
if (startX < pos + carWidth + minSpacing && startX + carWidth + minSpacing > pos) {
carSpacing = false; // Collision detected
break;
}
}
}
attempts++;
} while (!carSpacing && attempts < 20); // Try up to 20 times to find a valid position
for (let pos of positions) {
if (this.direction === 1) {
// Check right-to-left direction
if (startX + carWidth + minSpacing > pos && startX < pos + carWidth + minSpacing) {
carSpacing = false; // Collision detected
break;
}
} else {
// Check left-to-right direction
if (startX < pos + carWidth + minSpacing && startX + carWidth + minSpacing > pos) {
carSpacing = false; // Collision detected
break;
}
}
}
attempts++;
} while (!carSpacing && attempts < 20); // Try up to 20 times to find a valid position
if (carSpacing) {
positions.push(startX); // Add position if no collision
this.cars.push(new Car(startX, this.y, speed, this.direction));
}
}
}
positions.push(startX); // Add position if no collision
this.cars.push(new Car(startX, this.y, speed, this.direction));
}
}
}
update() {
for (let i = this.cars.length - 1; i >= 0; i--) { // Loop backwards to safely remove cars
let car = this.cars[i];
for (let i = this.cars.length - 1; i >= 0; i--) { // Loop backwards to safely remove cars
let car = this.cars[i];
// Check if the car is in the buffer zone
if (car.isInBufferZone(train)) {
car.stop(); // Stop the car if it's in the buffer zone
if (car.isInBufferZone(train)) {
car.stop(); // Stop the car if it's in the buffer zone
// Check if the car is in the train's path
if (car.isInTrainPath(train)) {
car.reset(); // Reset car if it collides with the train
this.cars.splice(i, 1); // Remove the car from the array
addNewCar(this.cars, car.y, this.direction); // Add a new car in the same lane
}
} else {
car.resume(); // Resume car's original speed
car.move();
}
car.display();
}
}
if (car.isInTrainPath(train)) {
car.reset(); // Reset car if it collides with the train
this.cars.splice(i, 1); // Remove the car from the array
addNewCar(this.cars, car.y, this.direction); // Add a new car in the same lane
}
} else {
car.resume(); // Resume car's original speed
car.move();
}
car.display();
}
}
display() {
for (let car of this.cars) {
car.display();
}
}
for (let car of this.cars) {
car.display();
}
}
// Resume all cars in this traffic group
resume() {
for (let car of this.cars) {
car.resume();
}
}
}
resume() {
for (let car of this.cars) {
car.resume();
}
}
}
// Car class
class Car {
constructor(x, y, speed, direction) {
this.x = x;
this.y = y;
this.baseSpeed = speed;
this.speed = speed;
this.direction = direction; // 1 for left-to-right, -1 for right-to-left
this.stopped = false;
this.color = color(random(255), random(255), random(255)); // Random color for each car
}
class Car {
constructor(x, y, speed, direction) {
this.x = x;
this.y = y;
this.baseSpeed = speed;
this.speed = speed;
this.direction = direction; // 1 for left-to-right, -1 for right-to-left
this.stopped = false;
this.color = color(random(255), random(255), random(255)); // Random color for each car
}
move() {
if (!this.stopped) {
this.x += this.speed * this.direction;
if (!this.stopped) {
this.x += this.speed * this.direction;
// Reset car position and randomize speed when it goes off-screen
if (this.direction === 1 && this.x > width) {
this.x = -50;
this.randomizeSpeed();
} else if (this.direction === -1 && this.x < -50) {
this.x = width;
this.randomizeSpeed();
}
}
}
if (this.direction === 1 && this.x > width) {
this.x = -50;
this.randomizeSpeed();
} else if (this.direction === -1 && this.x < -50) {
this.x = width;
this.randomizeSpeed();
}
}
}
// Randomize speed within a specified range
randomizeSpeed() {
this.speed = random(2, 5);
this.baseSpeed = this.speed; // Update baseSpeed to maintain consistency
}
randomizeSpeed() {
this.speed = random(2, 5);
this.baseSpeed = this.speed; // Update baseSpeed to maintain consistency
}
stop() {
this.stopped = true;
this.speed = 0; // Set speed to zero when stopped
}
this.stopped = true;
this.speed = 0; // Set speed to zero when stopped
}
resume() {
this.stopped = false;
this.speed = this.baseSpeed; // Restore the car’s original speed
}
this.stopped = false;
this.speed = this.baseSpeed; // Restore the car’s original speed
}
display() {
fill(this.color); // Use the car's random color
rect(this.x, this.y, carWidth, 20);
}
fill(this.color); // Use the car's random color
rect(this.x, this.y, carWidth, 20);
}
// Reset car to a starting position when it collides with the train
reset() {
this.x = this.direction === 1 ? -random(200) : width + random(200);
this.randomizeSpeed(); // Set a new random speed
}
reset() {
this.x = this.direction === 1 ? -random(200) : width + random(200);
this.randomizeSpeed(); // Set a new random speed
}
// Check if the car is in the buffer zone (20 pixels) around the train
isInBufferZone(train) {
return (
this.y > train.y - trainBuffer && this.y < train.y + 350 + trainBuffer && // Buffer around train vertically
this.x + carWidth > train.x - 50 - bufferZone && this.x < train.x + 90 + bufferZone // Buffer around train horizontally
);
}
isInBufferZone(train) {
return (
this.y > train.y - trainBuffer && this.y < train.y + 350 + trainBuffer && // Buffer around train vertically
this.x + carWidth > train.x - 50 - bufferZone && this.x < train.x + 90 + bufferZone // Buffer around train horizontally
);
}
// Check if the car is in the train's path (as a single unit)
isInTrainPath(train) {
return (
this.y > train.y - trainBuffer && this.y < train.y + 350 + trainBuffer && // Buffer around train vertically
this.x + carWidth > train.x - 50 - 1 && this.x < train.x + 90 + 1 // Check 1 pixel away from train
);
}
}
isInTrainPath(train) {
return (
this.y > train.y - trainBuffer && this.y < train.y + 350 + trainBuffer && // Buffer around train vertically
this.x + carWidth > train.x - 50 - 1 && this.x < train.x + 90 + 1 // Check 1 pixel away from train
);
}
}
// Train class (now a composite of 3 vertical rectangles with a 5-pixel gap)
class Train {
constructor(x, y, speed) {
this.x = x;
this.y = y;
this.speed = speed;
this.direction = 1; // 1 for moving down
}
class Train {
constructor(x, y, speed) {
this.x = x;
this.y = y;
this.speed = speed;
this.direction = 1; // 1 for moving down
}
move() {
this.y += this.speed * this.direction;
}
this.y += this.speed * this.direction;
}
display() {
fill(0, 0, 255);
for (let i = 0; i < 3; i++) {
rect(this.x, this.y + i * 105, 40, 100);
}
}
fill(0, 0, 255);
for (let i = 0; i < 3; i++) {
rect(this.x, this.y + i * 105, 40, 100);
}
}
// Check if the train is completely off the screen
isOffScreen() {
return (this.y > height && this.direction === 1) || (this.y < -320 && this.direction === -1);
}
isOffScreen() {
return (this.y > height && this.direction === 1) || (this.y < -320 && this.direction === -1);
}
// Change the direction of the train
toggleDirection() {
this.direction *= -1; // Reverse direction
}
}
toggleDirection() {
this.direction *= -1; // Reverse direction
}
}
// Add a new car in the same lane after a collision
function addNewCar(carsArray, y, direction) {
let speed = random(2, 5);
let startX = direction === 1 ? -random(200) : width + random(200);
carsArray.push(new Car(startX, y, speed, direction));
}
function addNewCar(carsArray, y, direction) {
let speed = random(2, 5);
let startX = direction === 1 ? -random(200) : width + random(200);
carsArray.push(new Car(startX, y, speed, direction));
}
// Handle spacebar input
function keyPressed() {
if (key === ' ') {
trainMoving = true;
train.toggleDirection(); // Toggle train direction on spacebar press
}
}
function keyPressed() {
if (key === ' ') {
trainMoving = true;
train.toggleDirection(); // Toggle train direction on spacebar press
}
}