Fix: lowert GPU consumption due to particles

This commit is contained in:
2026-06-12 19:49:38 +02:00
parent d8b2688968
commit 053f0048bc
2 changed files with 111 additions and 89 deletions
@@ -1,10 +1,10 @@
package it.polimi.ingsw.gc14.View.GUI; package it.polimi.ingsw.gc14.View.GUI;
import javafx.animation.AnimationTimer; import javafx.animation.AnimationTimer;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
@@ -13,31 +13,35 @@ import java.util.Random;
public class FireParticleSystem { public class FireParticleSystem {
private static final int MAX_PARTICLES = 20; private static final int MAX_PARTICLES = 30;
private static final long SPAWN_INTERVAL_NS = 100_000_000L; // ~10 spawns/sec private static final long SPAWN_INTERVAL_NS = 1_00_000_000L;
private static final long FRAME_INTERVAL_NS = 1_000_000_000L / 30; // 30 fps cap
private static final int STEPS_PER_FRAME = 3; // physics steps per frame → 3× speed
private final Canvas canvas; private final Pane pane;
private final Scene scene;
private final List<Particle> particles = new ArrayList<>(); private final List<Particle> particles = new ArrayList<>();
private final Random rnd = new Random(); private final Random rnd = new Random();
private AnimationTimer timer; private AnimationTimer timer;
private long lastSpawn = 0; private long lastSpawn = 0;
private long lastFrame = 0;
private boolean warmedUp = false; private boolean warmedUp = false;
public FireParticleSystem(Scene scene) { public FireParticleSystem(Scene scene) {
canvas = new Canvas(); this.scene = scene;
canvas.widthProperty().bind(scene.widthProperty()); pane = new Pane();
canvas.heightProperty().bind(scene.heightProperty()); pane.setMouseTransparent(true);
canvas.setMouseTransparent(true); pane.setPickOnBounds(false);
} }
public Canvas getCanvas() { public Pane getPane() { return pane; }
return canvas;
}
public void start() { public void start() {
timer = new AnimationTimer() { timer = new AnimationTimer() {
@Override @Override
public void handle(long now) { public void handle(long now) {
if (now - lastFrame < FRAME_INTERVAL_NS) return;
lastFrame = now;
tick(now); tick(now);
} }
}; };
@@ -49,100 +53,107 @@ public class FireParticleSystem {
} }
private void tick(long now) { private void tick(long now) {
double w = canvas.getWidth(); double w = scene.getWidth();
double h = canvas.getHeight(); double h = scene.getHeight();
if (w == 0 || h == 0) return; if (w == 0 || h == 0) return;
if (!warmedUp) { if (!warmedUp) {
warmedUp = true; warmedUp = true;
// Pre-populate: spawn a full batch and advance each particle a random
// number of frames so they're spread across the screen from frame 1.
for (int i = 0; i < MAX_PARTICLES; i++) { for (int i = 0; i < MAX_PARTICLES; i++) {
Particle p = spawnParticle(w, h); Particle p = spawnParticle(w, h);
int advance = rnd.nextInt(800) + 100; // 100-900 frames ahead int advance = rnd.nextInt(800) + 100;
for (int f = 0; f < advance; f++) p.update(); for (int f = 0; f < advance; f++) p.update();
if (!p.isDead(w, h)) particles.add(p); if (!p.isDead(w, h)) {
p.addTo(pane);
p.updateVisual(w, h);
particles.add(p);
}
} }
} }
if (now - lastSpawn > SPAWN_INTERVAL_NS && particles.size() < MAX_PARTICLES) { if (now - lastSpawn > SPAWN_INTERVAL_NS && particles.size() < MAX_PARTICLES) {
lastSpawn = now; lastSpawn = now;
int count = rnd.nextInt(2) + 1; // 1-2 per spawn int count = rnd.nextInt(2) + 1;
for (int i = 0; i < count && particles.size() < MAX_PARTICLES; i++) { for (int i = 0; i < count && particles.size() < MAX_PARTICLES; i++) {
particles.add(spawnParticle(w, h)); Particle p = spawnParticle(w, h);
p.addTo(pane);
particles.add(p);
} }
} }
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.clearRect(0, 0, w, h);
Iterator<Particle> it = particles.iterator(); Iterator<Particle> it = particles.iterator();
while (it.hasNext()) { while (it.hasNext()) {
Particle p = it.next(); Particle p = it.next();
p.update(); for (int s = 0; s < STEPS_PER_FRAME; s++) p.update();
if (p.isDead(w, h)) { if (p.isDead(w, h)) {
p.removeFrom(pane);
it.remove(); it.remove();
} else { } else {
p.draw(gc, w, h); p.updateVisual(w, h);
} }
} }
} }
private Particle spawnParticle(double w, double h) { private Particle spawnParticle(double w, double h) {
// One of 4 corners, with a small random offset inside the corner area
int corner = rnd.nextInt(4); int corner = rnd.nextInt(4);
double margin = 0.12; double margin = 0.12;
double x, y; double x, y;
switch (corner) { switch (corner) {
case 0 -> { x = rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-left case 0 -> { x = rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; }
case 1 -> { x = w - rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-right case 1 -> { x = w - rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; }
case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-left case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; }
default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-right default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; }
} }
return new Particle(x, y, w, h, corner, rnd); return new Particle(x, y, w, h, corner, rnd);
} }
// ── Particle ──────────────────────────────────────────────────────────────
private static class Particle { private static class Particle {
double x, y;
double vx, vy; double x, y, vx, vy;
double life; // only sparks use this for burnout double life;
double size; double size;
double wobblePhase; double wobblePhase, wobbleSpeed, wobbleAmp;
double wobbleSpeed;
double wobbleAmp;
final int type; final int type;
final Random rnd;
final Circle core;
final Circle glow; // non-null only for embers
Particle(double x, double y, double w, double h, int corner, Random rnd) { Particle(double x, double y, double w, double h, int corner, Random rnd) {
this.x = x; this.x = x;
this.y = y; this.y = y;
this.life = 1.0; this.life = 1.0;
this.rnd = rnd;
this.type = weightedType(rnd); this.type = weightedType(rnd);
// Direction: roughly toward the opposite corner, with angular spread
double targetX = (corner == 0 || corner == 2) ? w * 0.75 : w * 0.25; double targetX = (corner == 0 || corner == 2) ? w * 0.75 : w * 0.25;
double targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75; double targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75;
double dx = targetX - x; double baseAngle = Math.atan2(targetY - y, targetX - x);
double dy = targetY - y; double angle = baseAngle + (rnd.nextDouble() - 0.5) * (Math.PI / 3.5);
double len = Math.sqrt(dx * dx + dy * dy);
double baseAngle = Math.atan2(dy, dx);
double spread = Math.PI / 3.5; // ~51 degree spread
double angle = baseAngle + (rnd.nextDouble() - 0.5) * spread;
double speed; double speed;
if (type == 0) { // ember if (type == 0) { // ember
speed = 0.5 + rnd.nextDouble() * 0.7; speed = 0.5 + rnd.nextDouble() * 0.7;
size = rnd.nextDouble() * 3 + 2; size = rnd.nextDouble() * 3 + 2;
glow = new Circle(size * 2.2);
core = new Circle(size / 2);
} else if (type == 1) { // spark } else if (type == 1) { // spark
speed = 1.5 + rnd.nextDouble() * 2.0; speed = 1.5 + rnd.nextDouble() * 2.0;
size = rnd.nextDouble() * 1.5 + 0.5; size = rnd.nextDouble() * 1.5 + 0.5;
glow = null;
core = new Circle(size / 2);
} else { // dust } else { // dust
speed = 0.2 + rnd.nextDouble() * 0.35; speed = 0.2 + rnd.nextDouble() * 0.35;
size = rnd.nextDouble() * 8 + 5; size = rnd.nextDouble() * 8 + 5;
glow = null;
core = new Circle(size);
core.setFill(Color.color(0.55, 0.38, 0.22));
} }
vx = Math.cos(angle) * speed; vx = Math.cos(angle) * speed;
vy = Math.sin(angle) * speed; vy = Math.sin(angle) * speed;
wobblePhase = rnd.nextDouble() * Math.PI * 2; wobblePhase = rnd.nextDouble() * Math.PI * 2;
wobbleSpeed = 0.025 + rnd.nextDouble() * 0.04; wobbleSpeed = 0.025 + rnd.nextDouble() * 0.04;
wobbleAmp = 0.1 + rnd.nextDouble() * 0.4; wobbleAmp = 0.1 + rnd.nextDouble() * 0.4;
@@ -155,47 +166,58 @@ public class FireParticleSystem {
return 2; return 2;
} }
void addTo(Pane pane) {
if (glow != null) pane.getChildren().add(glow);
pane.getChildren().add(core);
}
void removeFrom(Pane pane) {
pane.getChildren().remove(core);
if (glow != null) pane.getChildren().remove(glow);
}
void update() { void update() {
wobblePhase += wobbleSpeed; wobblePhase += wobbleSpeed;
vx += Math.sin(wobblePhase) * wobbleAmp * 0.05; vx += Math.sin(wobblePhase) * wobbleAmp * 0.05;
vy += Math.cos(wobblePhase) * wobbleAmp * 0.02; vy += Math.cos(wobblePhase) * wobbleAmp * 0.02;
x += vx; x += vx;
y += vy; y += vy;
if (type == 1) life -= 0.008 + Math.random() * 0.006; if (type == 1) life -= 0.008 + rnd.nextDouble() * 0.006;
} }
boolean isDead(double w, double h) { boolean isDead(double w, double h) {
double pad = size * 4; double pad = size * 4;
boolean offScreen = x < -pad || x > w + pad || y < -pad || y > h + pad; return x < -pad || x > w + pad || y < -pad || y > h + pad
return offScreen || (type == 1 && life <= 0); || (type == 1 && life <= 0);
} }
void draw(GraphicsContext gc, double w, double h) { void updateVisual(double w, double h) {
// Fade only within 5% of any edge
double edge = Math.min(w, h) * 0.05; double edge = Math.min(w, h) * 0.05;
double fadeX = Math.min(x / edge, Math.min((w - x) / edge, 1.0)); double fadeX = Math.min(x / edge, Math.min((w - x) / edge, 1.0));
double fadeY = Math.min(y / edge, Math.min((h - y) / edge, 1.0)); double fadeY = Math.min(y / edge, Math.min((h - y) / edge, 1.0));
double posAlpha = Math.max(0, Math.min(fadeX, fadeY)); double posAlpha = Math.max(0, Math.min(fadeX, fadeY));
double lifeAlpha = (type == 1) ? Math.min(life * 2.0, 1.0) : 1.0; double lifeAlpha = (type == 1) ? Math.min(life * 2.0, 1.0) : 1.0;
double alpha = posAlpha * lifeAlpha; double alpha = posAlpha * lifeAlpha;
if (alpha <= 0.01) return;
if (type == 0) { // ember: orange, glows core.setTranslateX(x);
core.setTranslateY(y);
if (type == 0) {
double dist = Math.sqrt((x - w/2) * (x - w/2) + (y - h/2) * (y - h/2)); double dist = Math.sqrt((x - w/2) * (x - w/2) + (y - h/2) * (y - h/2));
double hotness = Math.max(0, 1.0 - dist / (Math.sqrt(w * w + h * h) * 0.4)); double hotness = Math.max(0, 1.0 - dist / (Math.sqrt(w * w + h * h) * 0.4));
double g = 0.3 + hotness * 0.5; double g = 0.3 + hotness * 0.5;
gc.setFill(Color.color(1.0, g * 0.35, 0.0, alpha * 0.10)); core.setFill(Color.color(1.0, g, 0.0));
gc.fillOval(x - size * 2.2, y - size * 2.2, size * 4.4, size * 4.4); core.setOpacity(alpha * 0.88);
gc.setFill(Color.color(1.0, g, 0.0, alpha * 0.88)); glow.setTranslateX(x);
gc.fillOval(x - size / 2, y - size / 2, size, size); glow.setTranslateY(y);
} else if (type == 1) { // spark: white-yellow glow.setFill(Color.color(1.0, g * 0.35, 0.0));
glow.setOpacity(alpha * 0.10);
} else if (type == 1) {
double brightness = Math.min(life * 2.5, 1.0); double brightness = Math.min(life * 2.5, 1.0);
gc.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15, alpha)); core.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15));
gc.fillOval(x - size / 2, y - size / 2, size, size); core.setOpacity(alpha);
} else { // dust: gray-brown translucent } else {
gc.setFill(Color.color(0.55, 0.38, 0.22, alpha * 0.20)); core.setOpacity(alpha * 0.20);
gc.fillOval(x - size, y - size * 0.5, size * 2, size);
} }
} }
} }
@@ -106,7 +106,7 @@ public class GUI extends Application implements IView {
wrapScene(leaderboardScene); wrapScene(leaderboardScene);
FireParticleSystem fireParticles = new FireParticleSystem(mainScene); FireParticleSystem fireParticles = new FireParticleSystem(mainScene);
((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getCanvas()); ((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getPane());
fireParticles.start(); fireParticles.start();
primaryStage.setScene(loginScene); primaryStage.setScene(loginScene);