5

I've started on a pixel sim project and it doesn't run well. What can I do to optimize it? I'm fairly confident that the issue is in all the checks that are done involving the particles array. I'm looking for any solution, to do anything different, or any optimizations in general, not just about the particles array but throughout the whole code. My goal for this project is to create a pixel sim web app, and if it can't run well after a few seconds of spawning sand, it defeats the purpose of a web app. Any help is appreciated.

Main file:

let alter = true;

let particles = []

function setup() {
  let canvas = createCanvas(windowWidth, windowHeight);
  frameRate(120);
}

var a = ['rgb(244,220,148)', 'rgb(236,211,140)', 'rgb(252,228,156)', 'rgb(252,220,149)', 'rgb(244,212,148)', 'rgb(228,204,132)', 'rgb(240,220,156)']

function sandColor() {
  return color(a[Math.floor(Math.random() * a.length)]);
}

function drect(c, x, y, l, w) {
  noStroke();
  fill(c);
  rect(x, y, l, w);
}

class Particle {
  constructor(p, c, x, y, s) {
    this.p = p;
    this.c = c;
    this.x = x;
    this.y = y;
    this.s = s;
  }

  draw() {
    drect(this.c, this.x, this.y, this.s, this.s);
  }
}

function check(x, y) {
  return color(get(x, y));
}

function draw() {

  drect(color(37, 150, 190), 0, 0, windowWidth, windowHeight)

  tw = 4;
  th = 4;

  for (let i = 0; i < particles.length; i++) {
    particles[i].draw()
  }

  alter = !(alter)
  if (!alter) {

    for (let i = 0; i < particles.length; i++) {
      if (particles[i].p == 's') {
        let down = false
        if (JSON.stringify(check(particles[i].x, particles[i].y + 4).levels) == '[37,150,190,255]') {
          particles[i].y += 4;
          down = true;
        }
        if (!down) {
          let r = Math.floor(Math.random() * 2);
          if (r == 0) {
            if (JSON.stringify(check(particles[i].x - 4, particles[i].y + 4).levels) == '[37,150,190,255]') {
              particles[i].y += 4;
              particles[i].x -= 4;
            } else {
              if (JSON.stringify(check(particles[i].x + 4, particles[i].y + 4).levels) == '[37,150,190,255]') {
                particles[i].y += 4;
                particles[i].x += 4;
              }
            }
          }
        }
      }
    }

    if (mouseIsPressed) {
      for (let i = 0; i < 6; i++) {
        for (let j = 0; j < 6; j++) {
          let p = 's'
          let c = sandColor()
          let x = (Math.floor(mouseX / tw)) * tw + (i * 4) - 9;
          let y = (Math.floor(mouseY / th)) * th + (j * 4) - 9;
          let s = 4;

          let sand = new Particle(p, c, x, y, s)
          let d = true;
          for (let m = 0; m < particles.length; m++) {
            if (particles[m].x == x && particles[m].y == y && particles[m].p == "s") {
              d = false;
            }
          }
          if (d) {
            drect(c, x, y, s, s)
            particles.push(sand)
          }
        }
      }
    }
  }
}

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

document.addEventListener('contextmenu', event => event.preventDefault());
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

Hosted example: https://pixsim.loganstottle202.repl.co/ and if that doesn't work the code is here: https://replit.com/@LoganStottle202/pixsim?v=1

ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • 1
    Seeming dupe/repost of [pixel/sandbox simulator optimizations](https://stackoverflow.com/questions/73563093/pixel-sandbox-simulator-optimizations) with slightly different account name. – ggorlen Sep 02 '22 at 05:53

2 Answers2

2

Unfortunately I won't have the time to provide a nice detailed runnable example such as your fun sketch.

Hopefully I can give you a few directions:

  • as the p5 get() reference mentions: "Getting the color of a single pixel with get(x, y) is easy, but not as fast as grabbing the data directly from pixels[]. ". You'd call loadPixels() once at the start of the frame, then use pixels[4 * (x * d + i) + ((y * d + j) * width * d + )] where d = pixelDensity().
  • JSON.stringify() and comparing strings can get computationally heavy for many particles. Consider marking a particle's state (colliding, moving, still/inactive, etc.) with as an integer value which you can compare directly (e.g. again const integer state with descriptive names). (You can then reuse this state to render the particle in different colours if you like). If you really really want to use the colour, you could simplify by using the alpha value for different states (e.g. 255, 254, 253, etc.) where perceptually it would be hard to notice and you can just compare against .levels[3] avoiding JSON.stringify() and string comparison. I do recommend using global or static Particle state const variables with descriptive name an a basic state property for the Particle instances.
  • you can separate your graphics into two "layers" using createGraphics(): one would be used to render the "active/alive" alive particles (it could even be the global p5 graphics buffer). The other layer could be used to render the static particles that have settled. You can then re-use/redraw this layer using image(yourStaticSandLayer, 0, 0);, getting rid of rect() calls for static particles (since they've been drawn/cached into the layer already).

(Additionally, you can simplify sandColor() to simply return random(a); since p5's random() can also pick a random item in an array for you. This won't speed up anything, just simplify code: makes it easier to read/maintain)

Update Here's basic demo based roughly on your Particle class and using p5.Graphics:

let particles = [];
let activeParticlesLayer;
let inactiveParticlesLayer;

function setup() {
  createCanvas(300, 150);
  
  activeParticlesLayer = createGraphics(width, height);
  inactiveParticlesLayer = createGraphics(width, height);
  
  for(let i = 0;  i < 10; i++){
    particles.push(new Particle(color(0, 192, 0), random(width), random(90), 10, 10));
  }
}

function draw() {
  // clear only active layer: don't clear inactive layer
  activeParticlesLayer.background(255);
  
  // for debugging only: count active particles
  let numActiveParticles = 0;
  for(let i = 0;  i < 10; i++){
    let p = particles[i];
    // only update particles if they're active
    if(p.isActive){
      // if the current particle collided (with stage bottom for now)
      // then make it inactive (change colour as a visual cue) and cache to inactive layer
      // replace this with pixels[] collision logic
      if(p.y > height - p.s){
        p.isActive = false;
        p.c = color(0, 128, 0);
        // particle is inactive: cache into inactive layer (which isn't cleared)
        p.draw(inactiveParticlesLayer); 
      }
      // otherwise our particle is active: update position and render to active layer
      else{
        p.y += 1;
        p.draw(activeParticlesLayer);
        numActiveParticles++;
      }  
    }
  }
  
  // display layers
  image(activeParticlesLayer, 0, 0);
  image(inactiveParticlesLayer, 0, 0);
  text("active particles: " + numActiveParticles, 10, 15);
}

class Particle {
  constructor(c, x, y, s) {
    this.isActive = true;
    this.c = c;
    this.x = x;
    this.y = y;
    this.s = s;
  }
  
  update(){
    if(this.y > height - this.s){
      this.isActive = false;
      this.c = color(0, 128, 0);
    }else{
      this.y += 1;  
    }
  }

  draw(buffer) {
    buffer.fill(this.c);
    buffer.rect(this.x, this.y, this.s, this.s);
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>

Note: the above removes a of the functionalities in your original demo such as using colour to check collisions. For the sake of simplicity there's only a very small number of particles that only collide with the bottom of the canvas (and don't stack). Hopefully this is enough to illustrate the isActive state change and caching the rect() call only once, as the state changes from active to inactive.

Additionally I recommend looking at Developer Tools and looking into profiling the code to spot exactly what the slower bit of code are and focusing on those (instead of optimising bits of code that have little to no impact, but have the potential of making the code less readable / harder to maintian in the future)

George Profenza
  • 50,687
  • 19
  • 144
  • 218
  • Thanks for the help, I've implemented pixels[]. I am curious about the createGraphics() and particle states. If you could, would you message me on Discord sometime with some example code or how I could implement this into my project? My discord is bread#7194. –  Sep 02 '22 at 02:05
  • @breaad_2 I've just posted an update above including a commented example. Hopefully this illustrates the idea. (Sorry, not a lot of time left in the day for discord). – George Profenza Sep 02 '22 at 15:40
  • this is probably the best optimization I've come across so far. I would love to implement this system into my own code but I have one important question before I do: how would you wake particles back up after they have went inactive and how would you do these checks for each particle every frame without using just as many resources? Thank you for your help! –  Sep 02 '22 at 20:52
  • Note that the above isn't splitting the particles array into two arrays (active/inactive) and keeps moving back and forth between the two (though this may or may not be a useful technique for your use case). What the above is mainly doing is rendering a still of inactive particles just when they change state from active to inactive. The loop still iterates through **all** particles and currently only updates active ones. You're of course more than welcome to expand that and update inactive particles too based on your own logic to potentially reset the state from inactive to active... – George Profenza Sep 02 '22 at 21:18
  • ...this would make the logic more complex as you'd need to clear parts of the inactive layer where particles go from inactive to active (e.g. use blendMode(REMOVE); to draw the reactived rectangle (essentially erasing it from the cached layer)).It really depends how complex does your experiment need to be, if it's a learning exercise or something that needs to be pretty robust and performant. If it's the 2nd, I'd first try to not reinvent the wheel if possible and look at existing implementations. For example you could use [box2d.js (see pyramid)](http://www.iforce2d.net/embox2d/testbed.html) – George Profenza Sep 02 '22 at 21:38
  • I do want to try and see what kind of performance improvements this can yield, I don't fully however understand how the code works or what it does, so (when you have time) would it be alright if we can get in touch on discord so we can go back and forth? Thank you for this suggestion and the help you have given me. –  Sep 02 '22 at 21:50
  • If you don't want to do this that's completely fine, would you just send me a link to a resource on how this kind of thing works or tell me more about my specific code case and how I could implement it? Again, thank you for all of your help. :) –  Sep 02 '22 at 21:53
  • As previously mentioned: my time is quite limited: discord isn't an option. I recommend taking the time to understand the code: read it and imagine how it would work, make tweaks to it / even break it to test your assumptions then run it and double check if your assumptions were correct or not. Doing so for the whole sketch (which isn't long) will ensure you understand the code. If you run into unfamiliar p5.js functions use read the reference. For unfamiliar JS notions MDN Web Docs is great. The excercise will help you on the long run. – George Profenza Sep 02 '22 at 21:58
  • You need to understand code you read (this comes with practice). you should only use code you understand (otherwise code will easily get messy and out of control). It's not a matter of not wanting as much as not having enough time: between work/personal projects/family I can only contribute minimal answers and not provide detailed 1:1 tutorials. I'm not the only one on this site: feel free to post (well written) specific questions as you progress and I'm sure others can help too. Best of luck! – George Profenza Sep 02 '22 at 22:00
  • 1
    Thank you, I'll read over it and try to understand it. Also I usually try and make sure to understand the code I'm using, which is why I asked you before putting the system into my project. Have a great day! –  Sep 02 '22 at 22:09
0

I saw you had your GitHub link yesterday and created a PR to implement minor performance improvements.

They are mostly minor improvments. I think you need to change to a pixel based system for a performance overhaul, but if you are sticking with this approach, these improvements should help.

  1. Remove re-declarations.

You have constants like tw and th being declared right inside the draw() loop, unnecessarily being re-declared every cycle. I put them outside the draw() function.

  1. Change Loop logic

You had 2 for (i in particles) loops. Instead of looping it over twice, I put the logic inside a single loop, so you loop half as many times. Also, I changed for...in loop to a for loop for performance improvements.

  1. Remove dead particles

I saw that there is only code to add particles, but not to remove them. I added a check to see if the particles have fallen below windowHeight and removed them from particles array to prevent infinite growth of the array.

Hope this helps.

Overall, I think George provided great insights as to how you could further re-structure and improve your code. Hope that helps you create a fun project. I would love to see more!

cSharp
  • 2,884
  • 1
  • 6
  • 23
  • 1
    Thank you for the insight. A lot of the things you mentioned have been changed or overhauled, I'm keeping the replit updated so the current code is on there. I will look at the improvements on the pull request. I won't be checking this much so if you have any more improvements you see you can message me on discord at bread#7194. Thanks! –  Sep 02 '22 at 02:46