2

My p5.js sketch is freezing for a couple of frames every 2-3 seconds. I know p5 is not performant, but I feel like my sketch is optimized enough to not warrant this.

I think it may have to do with how the particles are handled when driving, so I used a particle pool, but that had no effect on performance. Here is my code

game.js:

const canvasSize = 400;
let car;

function setup() {
    frameRate(60);
    angleMode(RADIANS);
    createCanvas(canvasSize, canvasSize);
    noStroke();
    car = new Car(200, 200);     
}

function draw() {
    background(100, 100, 100);

    car.drawCar();
    car.drawGroundMarks();
    car.updatePoints();

    let turnDirection = 0;
    if (keyIsDown(LEFT_ARROW) || keyIsDown(65)) {
        turnDirection = -1;  // Turn left
    }

    if (keyIsDown(RIGHT_ARROW) || keyIsDown(68)) {
        turnDirection = 1;   // Turn right
    }

    car.rotateCar(turnDirection);

    if (keyIsDown(UP_ARROW) || keyIsDown(87)) {
        car.accelerateCar(1);
    }

    if (keyIsDown(DOWN_ARROW) || keyIsDown(83)) {
        car.accelerateCar(-1);
    }
}

car.js:

class Car {
    constructor(x, y, width = 40, height = 20, rotation = 0, speed = 0.05) {
        this.carX = x;
        this.carY = y;
        this.carW = width;  
        this.carH = height;
        this.carSpeed = speed;
        this.carRotation = rotation;
        this.maxSpeed = 9;
        this.carAcceleration = createVector(0, 0);
        this.carVelocity = createVector(0, 0);
        this.carPoints = [];
        this.color = [255, 0, 0];
        this.maxTurnSpeed = radians(20);  
        this.turnSpeed = 0;              
        this.turnAcceleration = radians(1);
        this.friction = 0.98;  // Friction to slow down car

        this.groundMarks = [];
        this.particlePool = new ParticlePool(); // Create the particle pool

    }

    drawCar() {
        // Draw the car body

        if (this.carPoints.length == 0)
            return;

        fill(...this.color);
        beginShape();
        this.carPoints.forEach(point => vertex(point.x, point.y));
        endShape(CLOSE);

        //fill(0, 0, 0);
        //ellipse(this.carPoints[0].x, this.carPoints[0].y, 5, 5); //back left wheel
        //ellipse(this.carPoints[3].x, this.carPoints[3].y, 5, 5); //back right wheel
        //ellipse(this.carPoints[2].x, this.carPoints[2].y, 5, 5); //front right wheel
        //ellipse(this.carPoints[1].x, this.carPoints[1].y, 5, 5); //front left wheel
        
        let windshieldLeftPoint = createVector((this.carPoints[0].x*.25 + this.carPoints[1].x*.75), (this.carPoints[0].y*.25 + this.carPoints[1].y*.75));
        let windshieldRightPoint = createVector((this.carPoints[3].x*.25 + this.carPoints[2].x*.75), (this.carPoints[3].y*.25 + this.carPoints[2].y*.75));
        fill(180, 180, 255);
        beginShape();
        vertex(this.carPoints[2].x, this.carPoints[2].y);
        vertex(this.carPoints[1].x, this.carPoints[1].y);
        vertex(windshieldLeftPoint.x, windshieldLeftPoint.y);
        vertex(windshieldRightPoint.x, windshieldRightPoint.y);
        endShape();

    }

    drawGroundMarks() {
        // Draw active particles
        this.groundMarks.forEach((mark) => {
            mark.draw();
        });

        // Remove dead particles and return them to the pool
        this.groundMarks = this.groundMarks.filter((mark) => {
            if (mark.dead) {
                this.particlePool.release(mark); // Return dead particles to the pool
                return false;
            }
            return true;
        });

        //console.log(this.groundMarks.length);
    }

    updatePoints() {
        this.carVelocity.add(this.carAcceleration);
        this.carVelocity.limit(this.maxSpeed);
        
        // Apply friction to gradually slow down the car when no acceleration
        this.carVelocity.mult(this.friction);

        this.carX += this.carVelocity.x;
        this.carY += this.carVelocity.y;
        this.carAcceleration.mult(0); // Reset acceleration after each frame

        let halfW = this.carW / 2;
        let halfH = this.carH / 2;

        let p1 = createVector(-halfW, -halfH); 
        let p2 = createVector(halfW, -halfH);  
        let p3 = createVector(halfW, halfH);   
        let p4 = createVector(-halfW, halfH);  

        this.carPoints = [p1, p2, p3, p4];

        this.carPoints.forEach(point => {
            let x = point.x;
            let y = point.y;
            let cosA = cos(this.carRotation);
            let sinA = sin(this.carRotation);
            let rotatedPoint = createVector(x * cosA - y * sinA, x * sinA + y * cosA);
            let translatedPoint = rotatedPoint.add(createVector(this.carX, this.carY));
            point.set(translatedPoint.x, translatedPoint.y);
        });
    }

    accelerateCar(direction) {
        let forwardVelocityX = this.carSpeed * direction * cos(this.carRotation);
        let forwardVelocityY = this.carSpeed * direction * sin(this.carRotation);
        this.carAcceleration.add(createVector(forwardVelocityX, forwardVelocityY));
        
        // Get recycled particles from the pool instead of creating new ones
        if (frameCount % 5 !== 0)
            return;
        
        this.groundMarks.push(this.particlePool.get(this.carPoints[0].x, this.carPoints[0].y));
        this.groundMarks.push(this.particlePool.get(this.carPoints[3].x, this.carPoints[3].y));
    }

    rotateCar(direction) {
        if (direction !== 0) {
            this.turnSpeed += this.turnAcceleration * direction;
            this.turnSpeed = constrain(this.turnSpeed, -this.maxTurnSpeed, this.maxTurnSpeed);
        } else {
            this.turnSpeed *= 0.9;
        }

        // If the car is moving, allow rotation, else stop turning
        if (this.carVelocity.mag() > 0.1) {
            this.carRotation += this.turnSpeed * (this.carVelocity.mag() / this.maxSpeed); // More natural turning at speed
        }
    }
}

drift particle.js

class driftParticle {
    constructor(x, y) {
        this.reset(x, y);
    }

    reset(x, y) {
        this.x = x;
        this.y = y;
        this.timeAlive = 0;
        this.alpha = 255;
        this.dead = false; // Reset dead flag
    }

    draw() {
        if (this.dead) return;
        
        this.alpha = 255 - this.timeAlive * 2;
        if (this.alpha <= 0) {
            this.dead = true;
        }
        fill(50, 50, 50, this.alpha);
        ellipse(this.x, this.y, 3, 3);
        this.timeAlive++;
    }
}

particle pool.js

class ParticlePool {
    constructor() {
        this.pool = [];
    }

    get(x, y) {
        let particle;
        if (this.pool.length > 0) {
            particle = this.pool.pop(); // Reuse an old particle if available
            particle.reset(x, y); // Reset its position and state
        } else {
            particle = new driftParticle(x, y); // Create a new one if none available
        }
        return particle;
    }

    release(particle) {
        this.pool.push(particle); // Return particle to the pool for reuse
    }
}
2
  • Nice question ! Unfortunately I won't have the time for a detailed answer, however I wonder: do you need all the trails particles for anything other than rendering on screen ? If not, you might be a able to get away with a p5.Graphics buffer/layer to render trails into. (this answer is somewhat similar) Commented Oct 16 at 9:29
  • I suggest simplifying the code to a minimal reproducible example, preferably as a runnable stack snippet. I'd have to do a bit of work to piece this together just to get things running before I can even see the problem. Doing that work makes it easier for folks to jump in and help. Thanks and good luck!
    – ggorlen
    Commented Oct 19 at 18:48

2 Answers 2

1

Please include a minimal reproducible example when you ask a question.

For now I have made one with your code. https://editor.p5js.org/Metabyte/sketches/l10Cctomw

There doesn't seem to be any stutters when running this version, so the system you must be running on has to have someone else making the mainloop busy.

Try running this sketch on another machine and see if the stutters still continue.

2
  • 1
    +1 for putting the code on p5 editor. (one minor request: can you please remove the duplicate setup()/draw() calls at the top ? (might be accidental copy/pasta)) Commented Nov 28 at 17:43
  • Oops, I'm surprised that doesnt get flagged automatically. Has been fixed!
    – Souf149
    Commented Nov 29 at 8:08
1

As mentioned in the comment, if you can get away with p5.Graphics if you can, primarily because you won't need to manage the array of trails. It will be a little less flexible: you'll need workout how much transparency you need to take out per frame, however.

Realistically though, when optimizing, you should start with profiling to have clear picture of where most time is spent and focus the effort where it makes most impact.

Running your sketch as is looks like this (using the Profiling tools in Chrome Dev Tools and looking at the call Tree sorted so functions that take most time are at the top):

screenshot of Chrome Dev Tools profiler with the Call Tree showing drawGroundMarks() as taking a decent amount of time

Notice drawGroundMarks() takes a decent amount of time.

I suspect you already have a good understanding of optimizing:

  • you use beginShape() / endShape()
  • you setup a trails object pool

ellipse() can be slower to render than beginShape() / endshape(). You can try something like this:

   push();
    noFill();
    strokeWeight(3);
    beginShape(POINTS);
    this.groundMarks.forEach((mark) => {
      stroke(0, 0, 0, mark.alpha);
      vertex(mark.x, mark.y);
      // mark.draw();
    });
    endShape();
    pop();

(Unfortunately it looks like alpha is ignored)

Alternatively you can look at blitting: caching trail shapes as pixels then using image() to render. (this is again somewhat similar to the p5.Graphics idea).

One thing to point out is that even background() takes a decent amount of time to run in p5.js.

There may be a limit to how much you can squeeze out of p5.js. If that's the case, it's worth porting the code to pixi.js.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.