Snake: Survival of the Fittest Edition (Final)

Introduction

Snake: Survival of the Fittest Edition adds a modern twist to the classic snake game you know and love. This game pits you, the player, against a computer-controlled snake in a battle. As you maneuver to collect food and grow in size, you’ll need to avoid obstacles, strategize your movements, and ensure that you don’t become the prey.

 

Code Overview

let scl; // Grid scale (calculated dynamically)
let cols, rows;
let playerSnake, computerSnake;
let food;
let obstacles = [];
let obstacleTimer = 0; // Timer to track when to change obstacles
let obstacleInterval = 300; // Minimum frames before obstacles relocate
let help = "Press 'f' (possibly twice) to toggle fullscreen";
let gameState = 'idle'; // 'idle', 'playing', or 'gameOver' // NEW CODE
let hasEnded = false;

function setup() {
  createCanvas(windowWidth, windowHeight);
  frameRate(10); // Control the game speed
  print(help);

  updateGridDimensions(); // Calculate grid dimensions and scale

  // Initialize snakes
  playerSnake = new Snake(color(0, 0, 255)); // Blue snake for the player
  computerSnake = new Snake(color(255, 0, 0)); // Red snake for the computer
  computerSnake.chaseState = "food"; // Initial chase state

  // Place food and obstacles
  placeFood();
  placeObstacles();
}

function draw() {
  if (gameState === 'idle') {
    drawStartScreen(); // NEW CODE
  } else if (gameState === 'playing') {
    runGame(); // NEW CODE
  } else if (gameState === 'gameOver') {
    drawGameOverScreen(); // NEW CODE
  }
}

function runGame() {
  background(220);

  // Update obstacles at random intervals
  if (frameCount > obstacleTimer + obstacleInterval) {
    placeObstacles();
    obstacleTimer = frameCount; // Reset the timer
  }

  // Draw the grid
  drawGrid();

  // Update the chase state
  if (computerSnake.body.length > 0 && playerSnake.body.length > 0) {
    if (computerSnake.len >= playerSnake.len * 3) {
      computerSnake.chaseState = "user"; // Switch to chasing the user
    } else {
      computerSnake.chaseState = "food"; // Chase food
    }
  }

  // Update and show the snakes
  if (playerSnake.body.length > 0) {
    playerSnake.update();
    playerSnake.show();
  }

  if (computerSnake.body.length > 0) {
    computerSnake.update();
    computerSnake.show();
  }

  // Handle computer snake logic
  if (computerSnake.body.length > 0 && computerSnake.chaseState === "food") {
    computerSnake.chase(food); // Chase food
  } else if (
    computerSnake.body.length > 0 &&
    playerSnake.body.length > 0 &&
    computerSnake.chaseState === "user"
  ) {
    computerSnake.chase(playerSnake.body[playerSnake.body.length - 1]); // Chase player's head

    if (snakeCollidesWithSnake(computerSnake, playerSnake)) {
      playerSnake.body.shift(); // Remove one cell from the player's snake
      playerSnake.len--; // Decrease player's snake length

      // NEW CODE: Check if player snake is engulfed completely
      if (playerSnake.len <= 0) {
        gameState = 'gameOver';
        drawGameOverScreen();
        fill(255, 255, 0);
        textSize(32);
        textAlign(CENTER, CENTER);
        text("YOU WERE ENGULFED BY THE COMPUTER SNAKE!", width / 2, (2 * height) / 3);
        return; // Exit the function immediately
      }
    }
  }

  // Check if the player snake eats the food
  if (playerSnake.body.length > 0 && playerSnake.eat(food)) {
    placeFood();
  }

  // Check if the computer snake eats the food
  if (computerSnake.body.length > 0 && computerSnake.eat(food)) {
    placeFood();
  }

  // Check for collisions with obstacles (only for the player snake)
  if (playerSnake.body.length > 0 && snakeCollidesWithObstacles(playerSnake)) {
    gameState = 'gameOver'; // NEW CODE: End the game
    drawGameOverScreen();
    return; // Exit the function immediately
  }

  // Draw the food
  fill(0, 255, 0);
  rect(food.x * scl, food.y * scl, scl, scl);

  // Draw the obstacles
  fill(100); // Gray obstacles
  for (let obs of obstacles) {
    rect(obs.x * scl, obs.y * scl, scl, scl);
  }

  // Check for game over conditions
  if (playerSnake.body.length > 0 && playerSnake.isDead()) {
    gameState = 'gameOver';
    drawGameOverScreen();
    return;
  }

  if (computerSnake.body.length > 0 && computerSnake.isDead()) {
    gameState = 'gameOver';
    drawGameOverScreen();
    return;
  }
}


function drawStartScreen() { // UPDATED FUNCTION
  background(0);
  fill(255, 255, 0); // Bright yellow color for arcade-like feel
  textFont('monospace'); // Arcade-style font
  textSize(48); // Large text size
  textAlign(CENTER, CENTER);
  text("SNAKE: SURVIVAL OF THE FITTEST", width / 2, height / 3);
  fill(0, 255, 0); // Green color for instructions
  textSize(24);
  text("PRESS SPACE TO START", width / 2, height / 2);
  fill(255, 0, 0); // Red color for help
  textSize(18);
  text(help, width / 2, (2 * height) / 3);
}

function drawGameOverScreen() { // UPDATED FUNCTION
  background(0);
  fill(255, 0, 0); // Red for game over message
  textFont('monospace'); // Arcade-style font
  textSize(48);
  textAlign(CENTER, CENTER);
  text("GAME OVER!", width / 2, height / 3);
  fill(0, 255, 0); // Green for restart instructions
  textSize(24);
  text("PRESS SPACE TO RESTART", width / 2, height / 2);
  noLoop(); // Stop the game loop
}



function updateGridDimensions() {
  // Set desired number of columns and rows
  cols = 40;
  rows = 30;

  // Calculate scale to fit the window size
  scl = min(floor(windowWidth / cols), floor(windowHeight / rows));

  // Adjust cols and rows to fill the screen exactly
  cols = floor(windowWidth / scl);
  rows = floor(windowHeight / scl);

  // Resize the canvas to match the new grid
  resizeCanvas(cols * scl, rows * scl);
}

function windowResized() {
  // Update grid and canvas dimensions when the window is resized
  updateGridDimensions();

  // Reposition snakes, food, and obstacles to ensure they are within the new bounds
  playerSnake.reposition();
  computerSnake.reposition();
  placeFood();
  placeObstacles();
}

function resetGame() { // NEW FUNCTION
  playerSnake = new Snake(color(0, 0, 255)); // Reset player snake
  computerSnake = new Snake(color(255, 0, 0)); // Reset computer snake
  computerSnake.chaseState = "food"; // Reset chase state
  placeFood();
  placeObstacles();
  loop(); // Start the game loop // FIX
}

function keyTyped() {
  if (key === 'f') {
    toggleFullscreen(); // Toggle fullscreen mode
  }
}

function toggleFullscreen() {
  let fs = fullscreen();
  fullscreen(!fs); // Flip fullscreen state
}
function keyPressed() {
  if (gameState === 'idle' && key === ' ') { // NEW CODE
    gameState = 'playing';
    resetGame(); // NEW CODE
  } else if (gameState === 'gameOver' && key === ' ') { // NEW CODE
    gameState = 'playing';
    resetGame(); // NEW CODE
  } else if (gameState === 'playing') { // NEW CONDITION
    switch (keyCode) {
      case UP_ARROW:
        if (playerSnake.ydir !== 1) playerSnake.setDir(0, -1);
        break;
      case DOWN_ARROW:
        if (playerSnake.ydir !== -1) playerSnake.setDir(0, 1);
        break;
      case LEFT_ARROW:
        if (playerSnake.xdir !== 1) playerSnake.setDir(-1, 0);
        break;
      case RIGHT_ARROW:
        if (playerSnake.xdir !== -1) playerSnake.setDir(1, 0);
        break;
    }
  }
}


function drawGrid() {
  stroke(200);
  for (let i = 0; i <= cols; i++) {
    line(i * scl, 0, i * scl, rows * scl);
  }
  for (let j = 0; j <= rows; j++) {
    line(0, j * scl, cols * scl, j * scl);
  }
}

function placeFood() {
  // Place food at a random position, avoiding obstacles and snakes
  food = createVector(
    floor(random(1, cols - 1)), // Avoid edges
    floor(random(1, rows - 1))
  );

  // Ensure food does not overlap with obstacles or snakes
  while (
    obstacles.some(obs => obs.x === food.x && obs.y === food.y) ||
    playerSnake.body.some(part => part.x === food.x && part.y === food.y) ||
    computerSnake.body.some(part => part.x === food.x && part.y === food.y)
  ) {
    food = createVector(
      floor(random(1, cols - 1)),
      floor(random(1, rows - 1))
    );
  }
}

function placeObstacles() {
  // Place random obstacles
  obstacles = []; // Clear existing obstacles
  let numObstacles = floor(random(4, 9)); // Random number of obstacles between 4 and 8

  for (let i = 0; i < numObstacles; i++) {
    let obs = createVector(floor(random(cols)), floor(random(rows)));

    // Ensure obstacles do not overlap with food or snakes
    while (
      (food && food.x === obs.x && food.y === obs.y) ||
      playerSnake.body.some(part => part.x === obs.x && part.y === obs.y) ||
      computerSnake.body.some(part => part.x === obs.x && part.y === obs.y)
    ) {
      obs = createVector(floor(random(cols)), floor(random(rows)));
    }
    obstacles.push(obs);
  }
}

function snakeCollidesWithObstacles(snake) {
  // Check if the snake's head collides with any obstacle
  if (snake === playerSnake) {
    let head = snake.body[snake.body.length - 1];
    return obstacles.some(obs => head.x === obs.x && head.y === obs.y);
  }
  return false; // Obstacles do not affect the computer snake
}

function snakeCollidesWithSnake(snake1, snake2) {
  // Check if snake1's head collides with any part of snake2
  let head1 = snake1.body[snake1.body.length - 1];
  return snake2.body.some(part => part.x === head1.x && part.y === head1.y);
}

function endGame(message) {
  // End the game and display a message
  noLoop();
  fill(0); // Black color for text
  textSize(32);
  textAlign(CENTER, CENTER);
  text(message, width / 2, height / 2);
}

class Snake {
  constructor(snakeColor) {
    // Initialize snake properties
    this.body = [createVector(floor(cols / 2), floor(rows / 2))];
    this.xdir = 0;
    this.ydir = 0;
    this.len = 1;
    this.dead = false;
    this.snakeColor = snakeColor;
  }

  setDir(x, y) {
    // Set snake's movement direction
    this.xdir = x;
    this.ydir = y;
  }

  update() {
    // Update snake's position
    let head = this.body[this.body.length - 1].copy();
    head.x += this.xdir;
    head.y += this.ydir;

    // Check for wall collision
    if (head.x < 0 || head.x >= cols || head.y < 0 || head.y >= rows) {
      this.dead = true;
    }

    this.body.push(head);

    // Remove the tail if the snake has not grown
    if (this.body.length > this.len) {
      this.body.shift();
    }
  }

  eat(pos) {
    // Check if the snake's head is at the same position as the food
    let head = this.body[this.body.length - 1];
    if (head.x === pos.x && head.y === pos.y) {
      this.len++;
      return true;
    }
    return false;
  }

  isDead() {
    // Check if the snake runs into itself
    let head = this.body[this.body.length - 1];
    for (let i = 0; i < this.body.length - 1; i++) {
      let part = this.body[i];
      if (part.x === head.x && part.y === head.y) {
        return true;
      }
    }
    return this.dead;
  }

  show() {
    // Display the snake on the canvas
    fill(this.snakeColor);
    for (let part of this.body) {
      rect(part.x * scl, part.y * scl, scl, scl);
    }
  }

  chase(target) {
    // Chase a target (food or player's head)
    let head = this.body[this.body.length - 1];
    if (target.x > head.x && this.xdir !== -1) {
      this.setDir(1, 0); // Move right
    } else if (target.x < head.x && this.xdir !== 1) {
      this.setDir(-1, 0); // Move left
    } else if (target.y > head.y && this.ydir !== -1) {
      this.setDir(0, 1); // Move down
    } else if (target.y < head.y && this.ydir !== 1) {
      this.setDir(0, -1); // Move up
    }
  }

  reposition() {
    // Reposition the snake if the window is resized
    for (let part of this.body) {
      part.x = constrain(part.x, 0, cols - 1);
      part.y = constrain(part.y, 0, rows - 1);
    }
  }
}

 

Here’s a high-level breakdown of code:

1. Game Structure

The game is divided into three primary states:

  1. Idle State: Displays the start screen with instructions and the game title.
  2. Playing State: Runs the game loop where the snakes move, food is consumed, and collisions are handled.
  3. Game Over State: Displays a game-over message and prompts the user to restart.

This is achieved using a game state variable, which transitions between ‘idle’, ‘playing’, and ‘gameover’.

2. Grid System

The game grid dynamically adjusts to fit the browser window. The grid is divided into cells, and all game elements (snakes, food, obstacles) are positioned on this grid. The scale of the grid (scl) is calculated based on the window size to ensure the game looks consistent on any screen.

3. Snakes

Both the player and computer-controlled snakes are objects created using the snake class. Each snake:

  • Has a body represented as an array of segments (each segment is a grid cell).
  • Moves in a specified direction and grows when it consumes food.
  • Can “die” if it collides with itself, obstacles, or the grid boundaries.

The computer snake is programmed to switch between two behaviors:

  • Chasing Food: Moves toward food on the grid.
  • Chasing the Player: Pursues the player’s snake when it is three times larger, adding a competitive element.
4. Game Logic

The core gameplay logic is handled in the runGame function, which:

  • Updates the positions of the snakes.
  • Checks for collisions (e.g., snake colliding with obstacles or itself).
  • Manages interactions, such as consuming food or one snake engulfing the other.
  • Adjusts the state of the game based on player or computer actions.
5. Visuals

The game’s visuals, including the grid, snakes, food, and obstacles, are drawn dynamically using p5.js functions like rect and line. To create an arcade-like feel:

  • The title screen and game-over messages use bold, vibrant colors and pixelated fonts.
  • The game background, snakes, and food are kept simple yet visually distinct for clarity.
6. Interaction

The player controls their snake using the arrow keys. Pressing the space bar transitions the game between the start screen and gameplay or restarts the game after it ends. Additionally, pressing ‘f’ toggles fullscreen mode for an immersive experience.

7. Obstacles

Randomly placed obstacles add complexity to the game. They are repositioned periodically to ensure dynamic gameplay. The player’s snake must avoid these obstacles, while the computer snake is immune to them, giving it an advantage.

User Testing

I got feedback to include an info text about the game details and also the user suggested to reduce the speed of the computer’s snake.

IMG_0104

Future Considerations

While the game is already engaging, there are plenty of opportunities for enhancement:

  1. Improved AI:
    • Introduce smarter pathfinding for the computer snake, like using the AI
  2. Multiplayer Support:
    • Allow two players to compete head-to-head on the same grid.
  3. Power-Ups:
    • Include items that provide temporary benefits, such as invincibility, speed boosts, or traps for the computer snake.
  4. Dynamic Environments:
    • Add levels with different grid layouts, moving obstacles, or shrinking safe zones.
  5. Scoring System and Leaderboards:
    • Introduce a scoring mechanism based on time survived or food consumed.
    • Display high scores for competitive play.
  6. Sound Effects and Music:
    • Add classic arcade sound effects for eating food, colliding, and game-over events.
    • Include background music that changes tempo based on the intensity of gameplay.

 

Final Project Drat 2

The changes I have made are mostly visual and I also changed the hard-coded word generation into a real-time one using the data muse API. Most of the changes made are visual. I used CSS and vanilla JS to improve the side style of the game. But I would like to include more mechanics and also work on adding more visually aesthetic assets such as using a proper jar instead of a rectangle.

Final Project Draft

Concept

The idea for this project is to create a game that is similar to collecting balls. Instead of collecting balls in a jar in this game, the player will collect letters given the phrase P. To complete the game the player has to collect all the letters of the phrase P and letters will be falling from the top of the screen. Collecting the wrong letter will result in losing. The player also has 1 mins to collect all the letters. Most of the basic functionality is done for this draft.

Game

Remaining Features

The basic logic of the game is done. But right now once the player collects all letters a second round with longer letters does not appear and that will be implemented in the final version.  The phrases are hard coded right now and I will use an LLM to generate the phrases for all the rounds in real time. The length and complexity of the phrase will increase every round but the player will still have 1 minute to collect all the letters. The visuals are also basic right now. I will improve the visuals for the final version.

Ripples With Cellular Automata

Concept

This project mimics wave-like patterns that appear and disappear over time, drawing inspiration from the natural phenomena of water ripples. The project employs basic mathematical principles to create ripples on the canvas by representing every grid cell as a point in a system. By enabling users to create waves through clicks, drags, or even randomly generated events, user involvement brings the picture to life.

Each cell’s value is determined by the values of its neighboring cells, and the basic algorithm changes the grid state frame by frame. The simulation is both aesthetically pleasing and scientifically sound because of this behavior, which mimics how ripples dissipate and interact in real life.

 Code Review

let cols;
let rows;
let current;
let previous;

let dampening = 0.99; // Controls ripple dissipation
let cellSize = 4; // Size of each cell
let baseStrength = 5000; // Base intensity of interaction
let interactStrength = baseStrength; // Dynamic intensity
let autoRipples = false; // Automatic ripple generation
let mousePressDuration = 0; // Counter for how long the mouse is pressed

function setup() {
  createCanvas(windowWidth, windowHeight);
  initializeGrid();
  textSize(16);
  fill(255);
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
  initializeGrid();
}

function initializeGrid() {
  cols = floor(width / cellSize);
  rows = floor(height / cellSize);

  current = new Array(cols).fill(0).map(() => new Array(rows).fill(0));
  previous = new Array(cols).fill(0).map(() => new Array(rows).fill(0));
}

function mouseDragged() {
  mousePressDuration++;
  interactStrength = baseStrength + mousePressDuration * 50; // Increase ripple strength
  addRipple(mouseX, mouseY);
}

function mousePressed() {
  mousePressDuration++;
  interactStrength = baseStrength + mousePressDuration * 50; // Increase ripple strength
  addRipple(mouseX, mouseY);
}

function mouseReleased() {
  mousePressDuration = 0; // Reset the counter when the mouse is released
  interactStrength = baseStrength; // Reset ripple strength
}

function keyPressed() {
  if (key === 'A' || key === 'a') {
    autoRipples = !autoRipples; // Toggle automatic ripples
  } else if (key === 'R' || key === 'r') {
    initializeGrid(); // Reset the grid
  } else if (key === 'W' || key === 'w') {
    dampening = constrain(dampening + 0.01, 0.9, 1); // Increase dampening
  } else if (key === 'S' || key === 's') {
    dampening = constrain(dampening - 0.01, 0.9, 1); // Decrease dampening
  } else if (key === '+' && cellSize < 20) {
    cellSize += 1; // Increase cell size
    initializeGrid();
  } else if (key === '-' && cellSize > 2) {
    cellSize -= 1; // Decrease cell size
    initializeGrid();
  }
}

function addRipple(x, y) {
  let gridX = floor(x / cellSize);
  let gridY = floor(y / cellSize);
  if (gridX > 0 && gridX < cols && gridY > 0 && gridY < rows) {
    previous[gridX][gridY] = interactStrength;
  }
}

function draw() {
  background(0);

  noStroke();

  // Display ripples
  for (let i = 1; i < cols - 1; i++) {
    for (let j = 1; j < rows - 1; j++) {
      // Cellular automata ripple algorithm
      current[i][j] =
        (previous[i - 1][j] +
          previous[i + 1][j] +
          previous[i][j - 1] +
          previous[i][j + 1]) /
          2 -
        current[i][j];

      // Apply dampening to simulate energy dissipation
      current[i][j] *= dampening;

      // Map the current state to a color intensity
      let intensity = map(current[i][j], -interactStrength, interactStrength, 0, 255);

      // Render each cell as a circle with its intensity
      fill(intensity, intensity * 0.8, 255); // Blue-tinted ripple effect
      ellipse(i * cellSize, j * cellSize, cellSize, cellSize);
    }
  }

  // Swap buffers
  let temp = previous;
  previous = current;
  current = temp;

  if (autoRipples && frameCount % 10 === 0) {
    // Add a random ripple every 10 frames
    addRipple(random(width), random(height));
  }

  // Display info text
  displayInfoText();
}

function displayInfoText() {
  fill(255);
  noStroke();
  textAlign(LEFT, TOP);
  text(
    `Controls:
  A - Toggle auto ripples
  R - Reset grid
  W - Increase dampening (slower fade)
  S - Decrease dampening (faster fade)
  + - Increase cell size
  - - Decrease cell size
Click and drag to create ripples.`,
    10,
    10
  );
}

The grid is represented as two 2D arrays to store the current and past states of the simulation. The cell’s new value is computed as the average of its neighboring cells’ values, minus its value from the previous frame.  This controllers the ripple propagation. I also used a dampening factor that reduces the intensity of the ripples over time, simulating the gradual dissipation of energy.

Sketch


The sketch has the following user interactions.

  • A or a: Toggles automatic ripples on or off.
  • R or r: Resets the grid, clearing all current ripples.
  • W or w: Increases the dampening factor, making the ripples fade slowly.
  • S or s: Decreases the dampening factor, making the ripples fade faster.
  • +: Increases the cell size, which reduces the number of grid cells but increases their size.
  • -: Decreases the cell size, increasing the grid resolution for finer ripples.

Challenges and Future Improvements

The challenge in this project is managing large grids which makes the simulation computationally expensive. Also, achieving a smooth and seamless ripple effect was a bit challenging. For future improvements. Implementing this in 3D could be quite interesting.

Reference

thecodingtrain.com/challenges/102-2d-water-ripple

 

Week 10: Collecting Boxes

Concept

For this assignment, I just wanted to create a simple game. The collecting ball game has a paddle and boxes that are done from the top of the screen and the user moves the paddle to collide with as many boxes as possible. The concept is simple right now and it could be a starting point for a more complicated game

The Game

The users use the left and right arrows to play and some of the challenges I faced in this game were making the movement movement of the paddles smooth and also using matter.js itself was a bit challenging.

Future Improvements

As I have said earlier this could be a starting point for a more complicated game. One way would be to allow users to focus on collecting a specific shape and on other occasions a specific color and sometimes both specific shapes and colors and also changing the area of the screen to focus in the game from left to right meaning asking users only to collect boxes dropping from the top right or top left.  Adding these levels would make the game more interesting. Also, some of the mechanics like the movement of the paddle could be improved.

Dynamic Flocking

Concept

Here I tried to simulate flocking behavior by creating a “school” of boids that move together in a coordinated, natural pattern. Each boid follows three core rules—separation, alignment, and cohesion—to avoid crowding, match direction, and stay close to neighbors. Together, these rules give rise to a lifelike group movement, where each boid is influenced by its surroundings to form a cohesive, dynamic flock.

Code Breakdown

Each Boid has properties for position, velocity, and acceleration, and the draw loop continuously applies flocking behaviors, updates the boid’s position, and renders it on the canvas with a trailing effect for smooth movement. The flock method combines the three behaviors by calculating forces based on nearby boids within defined distances. The separation force keeps boids from colliding, alignment adjusts direction to match neighboring boids, and cohesion steers toward the group center. The wrapEdgesmethod ensures that boids reappear on the opposite side if they move off-screen. An interactive feature, mouseDragged, adds new boids at the mouse location when dragged, adding flexibility to the simulation.

let boids = []; // Array to store boid instances
let w = 600, h = 600;

function setup() {
  createCanvas(w, h);
  background(0);

  // Initialize boids with random positions
  for (let i = 0; i < 50; i++) {
    boids.push(new Boid(random(width), random(height)));
  }
}

function draw() {
  background(255, 5); // Slight trail effect
  
  boids.forEach(boid => {
    boid.flock(boids);  // Apply flocking behavior
    boid.update();      // Update position and velocity
    boid.wrapEdges();   // Wrap around edges of the canvas
    boid.show();        // Draw the boid
  });
}

// Constants for boid behavior
let M = 2;        // Max speed
let F = 0.28;     // Max force
let sepDist = 25; // Desired separation distance
let aliDist = 50; // Alignment neighbor distance
let cohDist = 150; // Cohesion neighbor distance

// Class to represent a single boid
class Boid {
  constructor(x, y) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-1, 1), random(-1, 1));
    this.acceleration = createVector(0, 0);
  }

  // Method to apply flocking behavior
  flock(boids) {
    let separation = this.separate(boids); // Separation
    let alignment = this.align(boids);     // Alignment
    let cohesion = this.cohesion(boids);   // Cohesion

    // Adjust weights for the forces
    separation.mult(1.5);
    alignment.mult(0.99);
    cohesion.mult(0.99);

    // Apply forces to acceleration
    this.acceleration.add(separation);
    this.acceleration.add(alignment);
    this.acceleration.add(cohesion);
  }

  // Update position based on velocity and acceleration
  update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(M); // Limit speed
    this.position.add(this.velocity);
    this.acceleration.mult(0); // Reset acceleration
  }

  // Wrap boids around the screen edges
  wrapEdges() {
    this.position.x = (this.position.x + w) % w;
    this.position.y = (this.position.y + h) % h;
  }

  // Draw boid as a small circle
  show() {
    fill(255, 0, 0);
    ellipse(this.position.x, this.position.y, 10);
  }

  // Separation behavior: Avoid crowding neighbors
  separate(boids) {
    let steer = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < sepDist) {
        let diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(distance);
        steer.add(diff);
        count++;
      }
    });
    if (count > 0) steer.div(count);
    if (steer.mag() > 0) {
      steer.normalize();
      steer.mult(M);
      steer.sub(this.velocity);
      steer.limit(F);
    }
    return steer;
  }

  // Alignment behavior: Steer towards the average heading of local flockmates
  align(boids) {
    let sum = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < aliDist) {
        sum.add(other.velocity);
        count++;
      }
    });
    if (count > 0) {
      sum.div(count);
      sum.normalize();
      sum.mult(M);
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(F);
      return steer;
    }
    return createVector(0, 0);
  }

  // Cohesion behavior: Steer towards the average position of local flockmates
  cohesion(boids) {
    let sum = createVector(0, 0);
    let count = 0;
    boids.forEach(other => {
      let distance = p5.Vector.dist(this.position, other.position);
      if (distance > 0 && distance < cohDist) {
        sum.add(other.position);
        count++;
      }
    });
    if (count > 0) {
      sum.div(count);
      return this.seek(sum);
    }
    return createVector(0, 0);
  }

  // Seek method to steer boid towards a target position
  seek(target) {
    let desired = p5.Vector.sub(target, this.position);
    desired.normalize();
    desired.mult(M);
    let steer = p5.Vector.sub(desired, this.velocity);
    steer.limit(F);
    return steer;
  }
}

// Function to add a new boid on mouse drag
function mouseDragged() {
  boids.push(new Boid(mouseX, mouseY));
}

Future Improvements

While the current simulation focuses on realistic flocking dynamics, future enhancements will center around visual and auditory immersion. Adding a colorful background and subtle sound effects could heighten the simulation’s atmosphere, making the school of boids feel more alive and enhancing the viewer’s experience.

MUJO Reflection

I was genuinely surprised by how the small, foundational concepts we’ve covered in class were applied to create something as complex and captivating as the digital sand dune simulation in Mujo. Concepts like layering textures, oscillations, and shadow, and adjusting particle movement seemed straightforward in isolation, yet seeing them combined in such a way brought an unexpected level of realism to the dunes. Each grain of sand appeared to move independently, yet it all worked together to mimic the fluid, shifting nature of real sand in the wind. It made me realize that these basic tools and techniques, which seemed almost too simple at first, can be powerful building blocks when used thoughtfully.

Week 8: Follower Simulation

Concept

The idea for this assignment is to simulate the behavior of different living organisms that follow a certain leader. I just thought the idea of following something was interesting. In this case, the lead insect is wandering; however, the rest of the colony is seeking him.

Code

let leader;
let followers = [];
let numFollowers = 100;
let desiredSeparation = 50;  // Minimum distance between flies

function setup() {
  createCanvas(windowWidth, windowHeight);
  
  // Create leader fly with random starting position
  leader = new Vehicle(random(width), random(height), true);

  // Create follower flies
  for (let i = 0; i < numFollowers; i++) {
    followers.push(new Vehicle(random(width), random(height), false));
  }
}

function draw() {
  background(220, 30);  // Transparent background for trail effect
  
  // Leader wanders around
  leader.wander();
  leader.update();
  leader.checkEdges();  // Ensure leader stays in bounds
  leader.show();

  // Followers seek the leader and avoid overlapping
  for (let follower of followers) {
    follower.separate(followers);  // Avoid overlapping with other followers
    follower.seek(leader.position);  // Seek the leader's current position
    follower.update();
    follower.checkEdges();  // Ensure followers stay in bounds
    follower.show();
  }
}

// Vehicle class definition
class Vehicle {
  constructor(x, y, isLeader = false) {
    this.position = createVector(x, y);
    this.velocity = createVector(random(-1, 1), random(-1, 1));
    this.acceleration = createVector(0, 0);
    this.r = 6;
    this.maxspeed = isLeader ? 2 : 2.5;  // Leader moves slightly faster
    this.maxforce = 0.05;  // Reduce max force for smoother movements
    this.isLeader = isLeader;
    this.wanderTheta = 0;  // Used for wander behavior for leader
  }

  // Wander behavior for the leader
  wander() {
    let wanderR = 25;    // Radius for our "wander circle"
    let wanderD = 80;    // Distance for the wander circle to be ahead of the vehicle
    let change = 0.3;
    this.wanderTheta += random(-change, change);  // Randomly change wanderTheta

    // Now we have to calculate the new target
    let circleLoc = this.velocity.copy();
    circleLoc.setMag(wanderD);  // Move circle ahead of vehicle
    circleLoc.add(this.position);  // Make it relative to the vehicle's position

    let h = this.velocity.heading();  // Heading angle of the vehicle
    let circleOffset = createVector(wanderR * cos(this.wanderTheta + h), wanderR * sin(this.wanderTheta + h));

    let target = p5.Vector.add(circleLoc, circleOffset);
    this.seek(target);  // Seek the wandering target
  }

  // Method to update location
  update() {
    this.velocity.add(this.acceleration);
    this.velocity.limit(this.maxspeed);  // Limit speed
    this.position.add(this.velocity);
    this.acceleration.mult(0);  // Reset acceleration to 0 after each update
  }

  // A method that calculates a steering force towards a target
  seek(target) {
    let desired = p5.Vector.sub(target, this.position);  // A vector pointing from the location to the target
    desired.setMag(this.maxspeed);  // Scale to maximum speed

    let steer = p5.Vector.sub(desired, this.velocity);  // Steering = Desired minus velocity
    steer.limit(this.maxforce);  // Limit to maximum steering force

    this.applyForce(steer);
  }

  // Separation behavior: Avoid overlapping
  separate(vehicles) {
    let sum = createVector(0, 0);
    let count = 0;
    
    // Check all other vehicles
    for (let other of vehicles) {
      let d = p5.Vector.dist(this.position, other.position);
      if ((d > 0) && (d < desiredSeparation)) {
        // Calculate vector pointing away from neighbor
        let diff = p5.Vector.sub(this.position, other.position);
        diff.normalize();
        diff.div(d);  // Weight by distance
        sum.add(diff);
        count++;  // Keep track of how many are too close
      }
    }

    // Average out the forces
    if (count > 0) {
      sum.div(count);
      sum.setMag(this.maxspeed);  // Implement Reynolds: Steering = Desired - Velocity
      let steer = p5.Vector.sub(sum, this.velocity);
      steer.limit(this.maxforce);
      this.applyForce(steer);
    }
  }

  // Apply force to the vehicle
  applyForce(force) {
    this.acceleration.add(force);
  }

  // Check edges to ensure vehicle stays within the canvas
  checkEdges() {
    if (this.position.x > width) {
      this.position.x = width;
      this.velocity.x *= -1;
    } else if (this.position.x < 0) {
      this.position.x = 0;
      this.velocity.x *= -1;
    }
    
    if (this.position.y > height) {
      this.position.y = height;
      this.velocity.y *= -1;
    } else if (this.position.y < 0) {
      this.position.y = 0;
      this.velocity.y *= -1;
    }
  }

  // Display the vehicle as a triangle pointing in the direction of velocity
  show() {
    let angle = this.velocity.heading() + PI / 2;
    fill(this.isLeader ? color(255, 0, 0) : color(0, 255, 0));  // Leader is red, followers are green
    stroke(0);
    push();
    translate(this.position.x, this.position.y);
    rotate(angle);
    beginShape();
    vertex(0, -this.r * 2);
    vertex(-this.r, this.r * 2);
    vertex(this.r, this.r * 2);
    endShape(CLOSE);
    pop();
  }
}

The simulation uses a vehicle class that has different attributes including and is leader attribute which becomes true when the vehicle is a leader.  The class also has a seek-and-wander method. The wander method makes the vehicle wander around by creating a circle around the head of the vehicle and making the vehicle seek that circle.  The seek method takes a target and makes the vehicle seek the target object.

Reflection and Future Improvements

The mechanics of the motion look good however the sketch might need more styling and polishing.  Another thing would be making the leader controllable using a mouse.

 

Mid-Term Project

Digital Print


These are the A3 digital prints of the visualization.

Pen Plotting

Concept and Inspiration

Initially, I had a different idea for my midterm project in mind, but after several attempts to implement it, I realized it wasn’t working as expected. I was looking for something fresh yet technically challenging to help me express my creativity. During my search, I stumbled upon a YouTube video about Perlin flow fields, which instantly clicked with my vision.

What is a Perlin Flow Field?

Perlin noise, developed by Ken Perlin, is a type of gradient noise used in computer graphics to create natural-looking textures, movement, and patterns. Unlike purely random noise, Perlin noise produces smoother transitions, making it ideal for simulating natural phenomena like clouds, terrain, or, in this case, particle motion.

A flow field, on the other hand, is a vector field that controls the movement of particles. When combined with Perlin noise, it creates a smooth, organic movement that feels like the particles are being guided by invisible forces.

Features

To add more interactivity for the project I added an expllosion and attraction effects. I took this from my previous project on super novas (exploding starts). These are the features contained in the project:

  • Mouse click: triggers attraction to the point on the screen where you cliked the mouse
  • Mouse release: triggers repulstion from the point on the screen where you are releasing the mouse
  • p: triggers a perlin noise i.e chnages the attraction or repusion motions to a smooth perlin noise.
  • a: adds 500 particles at random position
  • r: removes 500 particles

 

Code

let particles = [];
const initialNumParticles = 9000;
let numParticles = initialNumParticles;
let noiseScale = 0.005; // adjust for smoother noise transitions
let speed = 0.1; // lower the speed multiplier to slow down particles
let particleSize = 4;
const maxParticles = 9000; // set the maximum number of particles
const maxSpeed = 0.5; // limit maximum speed for each particle
let colorPalette = []; // define a color palette
let targetFlow = false; // control if flow should go towards the mouse
let targetPosition; // position of the mouse when pressed
let explode = false; // control the explosion effect
let perli = true;

// variables for high-resolution export
let scaleRatio = 1;
let exportRatio = 4; // scale down by 4x for working, export at 4x full resolution
let buffer;
let canvas;
let a3Paper = {
  width: 3508,   // a3 width in pixels at 300 PPI
  height: 4960   // a3 height in pixels at 300 PPI
};

// initialize a color palette (e.g., warm, cool, or any themed palette)
function createColorPalette() {
  colorPalette = [
    color(244, 67, 54),  // red
    color(255, 193, 7),  // yellow
    color(33, 150, 243), // blue
    color(76, 175, 80),  // green
    color(156, 39, 176)  // purple
  ];
}

// particle class definition using vector methods
class Particle {
  constructor(x, y) {
    this.position = createVector(x, y); // particle's position
    this.velocity = createVector(random(-0.5 / 16, 0.5 / 16), random(-0.5 / 16, 0.5 / 16)); // smaller initial velocity
    this.size = particleSize;
    this.color = random(colorPalette); // assign color from the color palette
  }

  // update the position of the particle using Perlin noise or towards the mouse
  update() {
    if (explode && targetPosition) {
      let repulsion = p5.Vector.sub(this.position, targetPosition).normalize().mult(0.3); // stronger repulsion force
      this.velocity.add(repulsion);
    } else if (targetFlow && targetPosition) {
      let direction = p5.Vector.sub(targetPosition, this.position).normalize().mult(speed * 10); // stronger force towards the mouse
      this.velocity.add(direction);
    } else if (perli) {
      let noiseVal = noise(this.position.x * noiseScale, this.position.y * noiseScale, noiseScale);
      let angle = TAU * noiseVal;
      let force = createVector(cos(angle), sin(angle)).normalize().mult(speed); // normal flow
      this.velocity.add(force);
    }

    this.velocity.limit(maxSpeed);
    this.position.add(this.velocity);
  }

  // respawn the particle if it hits the canvas edges
  checkEdges() {
    if (this.position.x >= width || this.position.x <= 0 || this.position.y >= height || this.position.y <= 0) {
      this.position = createVector(random(width), random(height)); // respawn at a random position
      this.velocity = createVector(random(-0.5 / 16, 0.5 / 16), random(-0.5 / 16, 0.5 / 16)); // reset velocity with lower values
    }
  }

  // render the particle on the canvas
  render() {
    fill(this.color); // use the particle's color
    noStroke();
    ellipse(this.position.x, this.position.y, this.size * 2, this.size * 2); // draw particle as an ellipse
  }
}

// setup function to initialize particles and canvas
function setup() {
  let w = a3Paper.width / exportRatio; // scaled-down width
  let h = a3Paper.height / exportRatio; // scaled-down height

  buffer = createGraphics(w, h); // create off-screen buffer for scaled drawings
  canvas = createCanvas(w, h); // create main canvas

  exportRatio /= pixelDensity(); // adjust export ratio based on pixel density of screen
  createColorPalette(); // initialize color palette
  for (let i = 0; i < numParticles; i++) {
    particles.push(new Particle(random(width), random(height))); // create particles
  }
  stroke(255);
  background(0);
}

// draw function to update and render particles
function draw() {
  background(0, 10); // lower opacity for longer fading trails

  // clear buffer and render particles to buffer
  buffer.clear();
  for (let i = 0; i < numParticles; i++) {
    particles[i].update();
    particles[i].checkEdges();
    particles[i].render();
  }

  // draw buffer to the canvas
  image(buffer, 0, 0);
}

// add particles dynamically (with maximum threshold)
function addParticles(n) {
  let newCount = numParticles + n;
  if (newCount > maxParticles) {
    n = maxParticles - numParticles; // limit to the maxParticles threshold
  }

  for (let i = 0; i < n; i++) {
    particles.push(new Particle(random(width, height))); // add new particles
  }
  numParticles += n;
}

// remove particles dynamically
function removeParticles(n) {
  numParticles = max(numParticles - n, 0); // prevent negative number of particles
  particles.splice(numParticles, n); // remove particles
}

// key press handling for dynamic control
function keyPressed() {
  if (key === 'a') {
    addParticles(500); // add 500 particles
  } else if (key === 'r') {
    removeParticles(500); // remove 500 particles
  } else if (key === 'p') {
    perli = true;
    explode = false;
    targetFlow = false;
  } else if (key === 's') {
    save('Wagaye_FlowField.png'); // save canvas as PNG
  } else if (key === 'e') {
    exportHighResolution();
  }
}

// mouse press handling to redirect flow towards mouse position
function mousePressed() {
  targetFlow = true; // activate flow towards mouse
  explode = false; // no explosion during mouse press
  targetPosition = createVector(mouseX, mouseY); // set the target position to the mouse press location
}

// mouse release handling to trigger explosion
function mouseReleased() {
  targetFlow = false; // disable flow towards mouse
  explode = true; // trigger explosion effect
  targetPosition = createVector(mouseX, mouseY); // use the mouse release position as the repulsion center
}

// export high-resolution A3 print
function exportHighResolution() {
  scaleRatio = exportRatio; // set scaleRatio to the export size

  // create a new buffer at the full A3 size
  buffer = createGraphics(scaleRatio * width, scaleRatio * height);

  // redraw everything at the export size
  draw();

  // get current timestamp for file naming
  let timestamp = new Date().getTime();

  // save the buffer as a PNG file
  save(buffer, `A3_Print_${timestamp}`, 'png');

  // reset scale ratio back to normal working size
  scaleRatio = 1;

  // re-create buffer at the original working size
  buffer = createGraphics(width, height);
  draw();
}


As you can see from the code the plot has three states explosion, target flow and perlin flow. And depending on the users interaction the plot changes. Maybe the most important part of this code is the perlin noise snippet. The code takes the particle’s coordinates, scales them down to control the smoothness of the transitions, and feeds them into the Perlin noise function. The result is a value between 0 and 1, which is then mapped to an angle in radians to determine the particle’s direction. This angle is used to create a vector, setting the direction and speed at which the particle moves. By continuously updating the particle’s velocity with these noise-driven vectors, the particles move in a way that feels organic, mimicking natural phenomena like wind currents or flowing water.

Challanges

The main challange was finding an otimal value eof noise scale and particle numbers to make the flow natural. Creating the explosion and attraction feature was also a bit challanging.

Future Improvments

A potential improvement to this project is the integration of music to make the flow fields react dynamically to beats. By incorporating an API or sound analysis tool that extracts key moments in the music, such as the kick drum or snare, the flow fields could “dance” to the rhythm. For instance, when a kick is detected, the particles could explode outward, and when the snare hits, they could contract or move toward a central point. During quieter sections, the particles could return to a smooth, flowing motion driven by Perlin noise. This interaction would create a synchronized visual experience where the flow fields change and evolve with the music, adding an extra layer of engagement and immersion.

Week 5: MidTerm Project Progress

Concept

The project’s main idea is to create a particle system with different interactive features. The generated art is still abstract to me right now. I was inspired by Memo Akten’s work which contains a particle system-like feature. Right now the project is still in its early stages but hopefully, I will find a more solid version of the visualization by the end of this week.

Features

The features I want this generative visualization to have include:

  • Movement in the direction of the mouse
  • Line connections
  • Random motion
  • Concentric polygon arrangements
  • Particle system features: lifetime and individual particle motion

Current Progress

Currently, I have done some of the features of this visual system. However, Maintaining a line connection when the arrangement of particles is changing is a bit difficult because the line connection creates a random motion. You can test this in the draft visualization provided below.

Press 1:  to create a random motion
Press 2: to create line connections
Prees 4: to create a circular arrangement
Press 5: to create a triangular arrangement

Reflection and Improvement

So far what I have looks okay but I want to add interactivity with a mouse and also add tails to the particles. Still, the most important thing I have to figure out is to maintain the arrangement when there is a line connection.