Merging tiles into paths

Hi all, I’d like to pick your brains for a sketch I’m tackling.

I’m building off of this:

tiles

It’s a basic tile placement generator that for this example uses two tile types – straight and 90° curve – to fill the canvas so everything fits neatly together. (For transparency: I’m using a vector program to quickly illustrate these visuals.)

Mind you, creating this “carpet of tiles” is not the issue. There’s some neat Wave Function Collapse tutorials I’m following to achieve this. But I want to take this further now.

After filling the canvas with the tiles, I’d like to identify continuous paths through the tiles, e.g. like in the following image highlighted in magenta. Ultimately I’m trying to have agents move along the paths.

path

Here is where my questions arise and where I’d like to see if my initial thinking is sound enough:

How do I go from building a carpet using individual tiles to creating continuous paths independent of these tiles?

My best idea for now is having the tiles include points that contain “movement instructions” for any agent that’s following the path.

movementInstr

So every time an agent enters a tile and hits such a point (shown in orange), they receive some instruction, e.g. “move straight to the right” or “curve down 90°” or “curve right 90°”. I’m guessing this could be achieved by passing velocity and direction PVectors into the agent.

Another approach would be to somehow attach the agent onto the lines and curves in the tiles and have them move using some kind of path-following algorithm. (Though I wouldn’t even know where to start with this…)

Yet another foundational hurdle is that all Wave Function Collapse tutorials I’ve seen operate using pngs, which are great for generating static visuals, but useless for what I’m trying to build. So I’m also wondering if I need to tackle the whole tile generation part from a different angle as well?

For now I’m interested in a sanity-check on these considerations, before I start investing too much time into potential dead ends.

Thanks!

2 Likes

Hello @eightohnine,

This inspired me to write some related code to generating a path.

This may be something in here of interest in your research:

The last one is generating mazes and inspired me to solve a maze by looking ahead to see what was in the way and making a decision based on that. A lot of interesting and related stuff may come up in a maze related search.

In one example of code I simply looked ahead to see if there was a pixel color change. In others I kept track of nodes on a grid (where I have been) in an array and checked array to see if something was ahead.

Thinking about it already I am, green eggs and ham!

Pick a random direction from your current direction and look ahead and draw your *curves! It should be simple enough to check nodes in the path; the nodes being stored in an array. Since these are tiles the nodes can represent each tile.

*According to one source a line is a curve without any curvature. :)

I do not have any algorithms or approaches to suggest for this as I generally code from scratch to engage my brain and then may look at other approaches.

Have fun!

:)

2 Likes

Oh wow, those are some jam-packed threads! I’ll have a sit down and sift through all this new inspiraiton. Thanks!

1 Like

Hello again!

This certainly did inspire me!

I wrote some minimal code using PVectors to do this with small lines progressing.
It it only goes left, right or forward but does not look ahead yet for obstacles.

I can share if you are receptive to it.

:)

1 Like

Sure, I’m always open to new approaches and directions.

1 Like

Hello again!

A quick and minimal example I cooked up:

/*
 Project:   Random path with Vectors
 Author:    GLV
 Date:      2024-07-01
 Version:   1.0.0
*/

//println((int)random(4));
//println(choice(4)); //Similar to above in source code

PVector v, v0, v1, v2;

void setup()
  {
  size(400, 400);
  v0 = new PVector(20, 0);
  v1 = new PVector(0, 0);
  v2 = new PVector(20, 0);
  
  frameRate(10);
  }
  
void draw()
  {
  translate(width/2, height/2);  
  
  strokeWeight(2);
  line (v1.x, v1.y, v2.x, v2.y); 
  
  int ch = choice(3)-1; // 0 to 2 becomes -1, 0, 1
  println(ch);
  
  v1 = v2.copy();
  // Look ahead not implemented. Not yet...
  v0.rotate(ch*TAU/4);
  v2 = v1.copy().add(v0);
  v2.set(round(v2.x), round(v2.y)); // get rid of rounding errors!
  println (v2);
  }

image

They are still overlapping in this version.

I leave the fun part with you!

References:

:)

The player has a position p.

Once he enters a tile the tile manages the change of p.

To do this it has an ArrayList of the points of its path (a line or different types of circles)

When leaving the tile the next tile takes over.

You need

  • a Tile class containing the image and the
    Arraylist and
  • an animation manager within the tile and
  • a next tile manager in the main sketch
1 Like

Fun part, indeed!

I’m currently trying to inject different steering forces into the agent, so that it can not only go straight, but also describe nice 90° curve segments. Might sent an update when I sort things out.

That’s about the gist of what I was able to come up with until now.

I believe I’ve got the basics down when it comes to creating classes and objects that stand for themselves. But as soon as it is multiple classes, some managing the behaviour of others, I tend to get my brain all knotted up. Will just have to approach this step by step…

1 Like

That is the best way!

What is your approach to steering and adding steps (without obstacle avoidance)?

The PVector approach I shared was just an initial inspiration. It was in my head in the moment and I had to release it.

I don’t need to see code.
Just a description to understand it.

:)

here is a quick demo for animate a player going through tiles



// We have a grid of tiles with different types / paths, e.g. - | or \ or / path.
// The goal is to animate a player through the grid.

// https://discourse.processing.org/t/merging-tiles-into-paths/44670

// Some player data :
// player gets animated by the tile class.
PVector playerPos = new PVector(0, 0);

// the current % of the animation of the player within a tile
float playerAmt; // animation

// the current Tile of the player (= index for grid)
int playerFieldIndexX=0;
int playerFieldIndexY=0;

// grid
Tile [][]  grid = new Tile[10][10];

// ----------------------------------------------------------------------------------------

void setup() {
  size( 1300, 900 );

  // the grid
  for (int x=0; x<10; x++) {
    for (int  y=0; y<10; y++) {
      // rect( x * 20, y * 20, 10,10);
      grid[x][y] = new Tile (x*50+55, y*50+55);
    }
  }

  // !!!!!!!!!!!!!!!!!!!!!!!!
  // type 0 is the default for every Tile which is ------->>>>

  grid[3][0].typeTile = 1; // going \

  grid[3][1].typeTile = 2; // going |
  grid[3][2].typeTile = 2;

  grid[3][3].typeTile = 2; // going |
  grid[3][4].typeTile = 2;
  grid[3][5].typeTile = 2;

  grid[3][5].typeTile = 3; // going \

  // set start and end PVector from type for all Tiles
  for (int x=0; x<10; x++) {
    for (int  y=0; y<10; y++) {
      grid[x][y].setPVectorFromType();
    }
  }

  // set player position within grid (the Tile the player starts on)
  playerFieldIndexX = 0;
  playerFieldIndexY = 0;

  // get PVector from Tile start and set player
  playerPos = grid[playerFieldIndexX][playerFieldIndexY].getPosOfTile();
  //
} // setup

void draw() {
  background(0);

  // show grid
  for (int x=0; x<10; x++) {
    for (int  y=0; y<10; y++) {
      grid[x][y].display();
    }
  }

  // show player
  fill(255, 0, 0);
  circle(playerPos.x, playerPos.y, 9);

  // when the current field of the player is ok
  if (playerFieldIndexX<10&&playerFieldIndexY<10) {
    // animate player
    grid[playerFieldIndexX][playerFieldIndexY].animatePlayer();
  } else {
    // game over - when we left the grid
    fill(255, 0, 0);
    text("game over", 600, 200);
  }
} // draw

// -------------------------------------------------------------------------------------------------------------

void keyPressed() {
  // Eval key

  if (key==' ') {
    // Space bar: reset player

    // reset
    playerAmt=0;

    // set player position within grid (the Tile the player starts on)
    playerFieldIndexX = 0;
    playerFieldIndexY = 0;

    // get PVector from Tile start and set player
    playerPos = grid[playerFieldIndexX][playerFieldIndexY].getPosOfTile();
  }//if
  //
}//func

//===========================================================================================================

class Tile {

  // position of Tile = upper left corner
  PVector posTile;

  // type = for example path going on right  ----- or | or / or \
  int typeTile = 0;

  // the start and end of the path the player moves on within the Tile
  PVector start, end;

  // this value is 0, 1 or -1 and tells us where the next Tile is relative to the current Tile.
  // Do we have to move the player right or down, or left or top?
  // The number gets added to the index of the grid (playerFieldIndexX,playerFieldIndexY) to reach the next neigbouring Tile.
  // This depends on the path: when the path is horizontal, the next Tile is right, when the path is vertical, the next Tile is down etc.
  int modifyIndexX, modifyIndexY;

  // ----------------------

  // constr
  Tile (float x, float y) {
    posTile=new PVector(x, y);
    // setPVectorFromType();
  }//constr

  // ----------------------

  void setPVectorFromType() {
    // set start and end PVector from type

    // Also set modifyIndexX and modifyIndexY because depending on the path the next Tile is either right or down or .....

    //if (random(1)<0.2) type=1;

    switch(typeTile) {
    case 0:
      start = new PVector( posTile.x, posTile.y + (50/2) );
      end = new PVector( posTile.x+50, posTile.y + (50/2) );
      modifyIndexX=1; // RIGHT
      modifyIndexY=0;
      break;
    case 1:
      start = new PVector( posTile.x, posTile.y + (50/2));
      end = new PVector( posTile.x+50/2, posTile.y + (50) );
      modifyIndexX=0;
      modifyIndexY=1; // DOWN
      break;
    case 2:
      start = new PVector( posTile.x + (50/2), posTile.y);
      end = new PVector( posTile.x+50/2, posTile.y + (50) );
      modifyIndexX=0;
      modifyIndexY=1; // DOWN
      break;
    case 3:
      start = new PVector( posTile.x + (50/2), posTile.y);
      end = new PVector( posTile.x+50, posTile.y + (50/2) );
      modifyIndexX=1; // RIGHT
      modifyIndexY=0;
      break;
    }
  }

  void display() {
    // display the Tile

    // show Tile as rect
    // noFill();
    fill(50);
    stroke(111);
    rect( posTile.x, posTile.y,
      50, 50);

    // show type of Tile / optional
    fill(255);
    text(typeTile, posTile.x+22-8-8, posTile.y+14);

    // show the path
    stroke(0, 111, 0);
    strokeWeight(4);
    line(start.x, start.y,
      end.x, end.y);
    strokeWeight(1);
  }//method

  void animatePlayer() {
    // 1. animate player on path
    playerPos.x = lerp (start.x, end.x, playerAmt); // see lerp() in the reference
    playerPos.y = lerp (start.y, end.y, playerAmt);
    // a) show playerAmt
    fill(255);
    text(playerAmt, 600, 200);

    // 2. increase % of animation
    playerAmt+=0.01; // this is the speed of the player

    // 3. when animation is over
    if (playerAmt >= 1 ) {
      // start all over
      // reset %
      playerAmt=0;

      // move to next Tile by adding modifyIndexX and modifyIndexY
      playerFieldIndexX+=modifyIndexX;
      playerFieldIndexY+=modifyIndexY;
    }// if
  }// method

  PVector getPosOfTile() {
    // return start PVector
    return
      new PVector(start.x, start.y );
  }//method
  //
}//class
//

2 Likes

So, this is just about on little manoeuvre… a 90° turn.

Step 1 – an object (orange) moves with a certain velocity (black arrow).
01

Step 2 – at a specific trigger (e.g. the object enters a new tile) the object picks up new movement instructions. In this case, a steering force (red arrow) pointing towards a center point. The velocity and steering force put the object on a circular orbit around this center point.
02

Step 3 – the object moves for a quarter of the orbit. (No idea yet how I’ll check this. I kinda want to do this without angles and angular velocities. Not entirely sure why, as I know this is more complex. But I believe it will keep the movement more flexible and powerful for some ideas down the line…)
03

Step 4 – after the completed turn, scrap the steering force and continue on a straight line with the velocity only (or pick up the next turning instructions).
04

This is all theory for now. I kinda didn’t get my brain into coding mode the past day. But I like to sketch these things out beforehand. Obs for me it’s all quick and dirty scribbles, for here I cleaned them up a little.

So how do you like my sketch

1 Like

Hello @eightohnine ,

A minimal example:

/*
 Project:   Path with 90° corners
 Author:    GLV
 Date:      2024-07-04
 Version:   1.0.0
*/

PVector v, v0, v1, v2;

void setup()
  {
  size(400, 400);
  v0 = new PVector(1, 0);  
  v1 = new PVector(0, 0); // Center of sketch
  v2 = new PVector(1, 0);
  background(255);
  }
  
boolean rot;
int rotStart;
int dir = 1;
  
void draw()
  {
  translate(width/2, height/2);  
  
  strokeWeight(2);
  
  v2 = v1.copy().add(v0);
  line (v1.x, v1.y, v2.x, v2.y); 
 
  int step = 30; //rotates for 30 steps
  if (rot)
    {  
    v0.rotate((TAU/4)/step); //rotates 90° (TAU/4) over 30 steps
    
    rotStart++;
    if (rotStart>=step)
      {
      rot=false;
      rotStart=0;
      }
    println(v2.x, v2.y);
    
    //v1 = v2.copy(); //Interesting error!
    }
  v1 = v2.copy();  
  }  
  
void mousePressed()
  {
  rot=true;
  }

My version (not posted) had more control:

image

Have fun!

Resource:
The Nature of Code

:)

1 Like

Totally appreciate your effort. Though I honestly ran the sketch just once and then realized that I wasn’t in the mindset to dissect and understand the code any further. As soon as that happens I’ll get back to you. Thanks!

1 Like

I did have angles in the last example.
It was easier for me for a first pass at this.

If you have fixed curves for the bends you could store the values in an array and step through them.

For many of my projects the final version may look nothing like initial attempts.
I am just sharing ideas with you and it is up to you to scrutinize.

Keep at it! Once step at a time…

:)

I have read this discussion with interest and it sounds and challenging project to implement. The ideas I present here are just that ultimately you must follow your own programming path but I hope you find my ideas interesting if not useful.

I have decided to implement something very similar but using Javascript. I created the sketch below to try some ideas out and share them with you. The each cell contains a path and that path is described by a pair of parametric equations x=f(t) and y=g(t). The variable t can have any value in the range \ge0 and \le1 but 0 defines the path start and 1 the path end, values between represent some point on the path.

Having a single Cell class that can be used for any path is not efficient because you are forever having to check the path type and direction and the code becomes messy and unmanageable . The solution I used is a simple class hierarchy which can easily be extended to include other paths.

Anyway this video shows the output from the sketch. If you have questions about the code then just ask away :smile:


ArrayList<Cell> cells = new ArrayList<Cell>();
ArrayList<String> types = new ArrayList<String>();

float t = 0f, dt = 0.005f;

public void settings() {
  size(500, 320);
}

public void setup() {
  float s = 80, x = 20, y = 100, dx = 120, dy = 120;
  textAlign(CENTER, CENTER);
  textSize(20);
  cells.add(getCell("NS", x, y, s));
  cells.add(getCell("EW", x + dx, y, s));
  cells.add(getCell("NE", x, y + dy, s));
  cells.add(getCell("ES", x + dx, y + dy, s));
  cells.add(getCell("SW", x + 2 * dx, y + dy, s));
  cells.add(getCell("WN", x + 3 * dx, y + dy, s));
}

public void draw() {
  background(192);
  t += dt;
  if (t < 0) {
    t=0;
    dt *= -1;
  } else if (t > 1) {
    t = 1;
    dt *= -1;
  }
  if (t > 1) t = 0;
  noStroke();
  fill(255, 255, 0);
  for (Cell c : cells) {
    c.draw();
    float[] p = c.getPos(t);
    ellipse(c.x + p[0], c.y + p[1], 10, 10);
  }
  // Travel direction and parametric details
  rect(240, 30, t*200, 30);
  noFill();
  stroke(0);
  strokeWeight(2);
  rect(240, 30, 200, 30);
  fill(0);
  text(nf(t, 0, 3), 240, 30, 200, 24);
  text("Direction:", 20, 0, 200, 24);
  text(dt >= 0 ? "Forward" : "Reverse", 220, 0, 200, 24);
  text("Parametric value (t):", 20, 30, 200, 24);

  // Display NS, EW , NE etc
  fill(0);
  for (int i = 0; i < types.size(); i++) {
    Cell c = cells.get(i);
    text(types.get(i), c.x, c.y - 32, 36, 30);
  }
}

public void exit() {
  super.exit();
}

/**
 * This is the key method for creating the tiles with the correct path.
 *
 * @param type two letter code indicating positive travel direction
 * @param x x coordinate for top-left corner for the tile
 * @param y y coordinate for top-left corner for the tile
 * @param size cell size
 * @return the tile or null if the type cannot be found
 */
public Cell getCell(String type, float x, float y, float size) {
  switch(type.toUpperCase()) {
  case "NS":
    types.add("NS");
    return new CellLine(x, y, size, new float[] {0.5f, 0, 0.5f, 1});
  case "EW":
    types.add("EW");
    return new CellLine(x, y, size, new float[] {1, 0.5f, 0, 0.5f});
  case "NE":
    types.add("NE");
    return new CellArc(x, y, size, new float[] {1, 0, PI, 0.5f * PI});
  case "ES":
    types.add("ES");
    return new CellArc(x, y, size, new float[] {1, 1, 1.5f * PI, PI});
  case "SW":
    types.add("SW");
    return new CellArc(x, y, size, new float[] {0, 1, 2 * PI, 1.5f * PI});
  case "WN":
    types.add("WN");
    return new CellArc(x, y, size, new float[] {0, 0, 0.5f * PI, 0});
  }
  return null;
}

/**
 * The base class for all cells. All useful cells are created
 * from sub-classes.
 *
 */
public class Cell {
  public float x, y, size, t;
  public float pathLength;

  public Cell(float x, float y, float size) {
    this.x = x;
    this.y = y;
    this.size = size;
  }

  public void drawCellBorder() {
    rectMode(CORNER);
    noFill();
    strokeWeight(1);
    stroke(255, 255, 0);
    rect(0, 0, size, size);
  }

  public void draw() {
  }
  public float[] getPos(float t) {
    return new float[] {0, 0};
  }
  public float getPathLength() {
    return pathLength;
  }
}

/** Straight path cell */
public class CellLine extends Cell {
  public float tx0, tx1, ty0, ty1;

  public CellLine(float x, float y, float size, float[] path_0_1) {
    super(x, y, size);
    this.tx0 = path_0_1[0];
    this.ty0 = path_0_1[1];
    this.tx1 = path_0_1[2];
    this.ty1 = path_0_1[3];
    this.pathLength = this.size;
  }

  public float[] getPos(float t) {
    t = constrain(t, 0, 1);
    float x = map(t, 0, 1, tx0, tx1) * size;
    float y = map(t, 0, 1, ty0, ty1) * size;
    return new float[] {x, y};
  }

  public void draw() {
    push();
    translate(x, y);
    drawCellBorder();
    stroke(0);
    line(tx0 * size, ty0 * size, tx1 * size, ty1 * size);
    pop();
  }
}

/** Arc path cell */
public class CellArc extends Cell {
  public float ta0, ta1;
  public float orgX, orgY;

  public CellArc(float x, float y, float size, float[] path_0_1) {
    super(x, y, size);
    this.orgX = path_0_1[0];
    this.orgY = path_0_1[1];
    this.ta0 = path_0_1[2];
    this.ta1 = path_0_1[3];
    this.pathLength = this.size * PI /4;
  }

  public float[] getPos(float t) {
    t = constrain(t, 0.0f, 1.0f);
    float ang = map(t, 0.0f, 1f, ta0, ta1);
    float x = (orgX + 0.5f * cos(ang)) * size;
    float y = (orgY + 0.5f * sin(ang)) * size;
    return new float[] {x, y};
  }

  public void draw() {
    push();
    translate(x, y);
    drawCellBorder();
    ellipseMode(CENTER);
    stroke(0);
    arc(orgX * size, orgY * size, size, size,
      min(ta0, ta1), max(ta0, ta1));
    pop();
  }
}
3 Likes

Back at it…

I appreciate the effort you put in with commenting all relevant parts of your sketch, makes it super easy to make sense of it all. Overall a clean solution and great foundation to expand step by step with more capabilities.

One observation…

The sketch decouples Player position management (via the 2D matrix) and the Player animation. It makes for a clean structure, a neat sequential execution. I feel though, that I would want more flexibility to the movement of the Player and thus need to bring position and animation back together again.

Especially the fact that it’s the animation – if (playerAmt >= 1 ) – that defines when the Player moves at a new tile, makes it somewhat rigid.
I’d like to have the Player move along and be able to at any point in time pick up a new movement instruction.

Thanks for your for thought!

1 Like

@glv

Very true. The sketches I end up with rarely are exactly what I envisioned at the start. If an unexpected but interesting outcome happens, I’m all for going with the flow.

Though I DO need to watch out that I don’t suddenly get interested by a totally new thing halfway through a sketch and then abandon it.

@quark

Oh, this looks neat! And very robust.

Although I will say that extending classes is not yet a concept that I’ve understood well.

I’ll have to dissect this one for sure.
Correct me if I’m wrong, but for now I understand this as follos.

There’s ONE parent class – Cell – that handles basic parameters like loc and size.
From this you create SIX sub-classes.
TWO handle the straight line movement,
FOUR handle the curved movement.

Fun!