From 053f0048bc33860558f32b26686773c45b561384 Mon Sep 17 00:00:00 2001 From: aleandro Date: Fri, 12 Jun 2026 19:49:38 +0200 Subject: [PATCH] Fix: lowert GPU consumption due to particles --- .../gc14/View/GUI/FireParticleSystem.java | 198 ++++++++++-------- .../it/polimi/ingsw/gc14/View/GUI/GUI.java | 2 +- 2 files changed, 111 insertions(+), 89 deletions(-) diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java index de21fbc..5ce5c59 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/FireParticleSystem.java @@ -1,10 +1,10 @@ package it.polimi.ingsw.gc14.View.GUI; 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.layout.Pane; +import javafx.scene.paint.Color; +import javafx.scene.shape.Circle; import java.util.ArrayList; import java.util.Iterator; @@ -13,31 +13,35 @@ import java.util.Random; public class FireParticleSystem { - private static final int MAX_PARTICLES = 20; - private static final long SPAWN_INTERVAL_NS = 100_000_000L; // ~10 spawns/sec + private static final int MAX_PARTICLES = 30; + 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 particles = new ArrayList<>(); - private final Random rnd = new Random(); - private AnimationTimer timer; - private long lastSpawn = 0; - private boolean warmedUp = false; + private final Random rnd = new Random(); + private AnimationTimer timer; + private long lastSpawn = 0; + private long lastFrame = 0; + private boolean warmedUp = false; public FireParticleSystem(Scene scene) { - canvas = new Canvas(); - canvas.widthProperty().bind(scene.widthProperty()); - canvas.heightProperty().bind(scene.heightProperty()); - canvas.setMouseTransparent(true); + this.scene = scene; + pane = new Pane(); + pane.setMouseTransparent(true); + pane.setPickOnBounds(false); } - public Canvas getCanvas() { - return canvas; - } + public Pane getPane() { return pane; } public void start() { timer = new AnimationTimer() { @Override public void handle(long now) { + if (now - lastFrame < FRAME_INTERVAL_NS) return; + lastFrame = now; tick(now); } }; @@ -49,103 +53,110 @@ public class FireParticleSystem { } private void tick(long now) { - double w = canvas.getWidth(); - double h = canvas.getHeight(); + double w = scene.getWidth(); + double h = scene.getHeight(); if (w == 0 || h == 0) return; if (!warmedUp) { 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++) { 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(); - 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) { 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++) { - 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 it = particles.iterator(); while (it.hasNext()) { Particle p = it.next(); - p.update(); + for (int s = 0; s < STEPS_PER_FRAME; s++) p.update(); if (p.isDead(w, h)) { + p.removeFrom(pane); it.remove(); } else { - p.draw(gc, w, h); + p.updateVisual(w, 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 x, y; switch (corner) { - case 0 -> { x = rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-left - case 1 -> { x = w - rnd.nextDouble() * w * margin; y = h - rnd.nextDouble() * h * margin; } // bottom-right - case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-left - default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } // top-right + 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; } + case 2 -> { x = rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } + default -> { x = w - rnd.nextDouble() * w * margin; y = rnd.nextDouble() * h * margin; } } return new Particle(x, y, w, h, corner, rnd); } + // ── Particle ────────────────────────────────────────────────────────────── + private static class Particle { - double x, y; - double vx, vy; - double life; // only sparks use this for burnout + + double x, y, vx, vy; + double life; double size; - double wobblePhase; - double wobbleSpeed; - double wobbleAmp; - final int type; + double wobblePhase, wobbleSpeed, wobbleAmp; + 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) { - this.x = x; - this.y = y; + this.x = x; + this.y = y; this.life = 1.0; + this.rnd = 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 targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75; - double dx = targetX - x; - double dy = targetY - y; - 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 targetX = (corner == 0 || corner == 2) ? w * 0.75 : w * 0.25; + double targetY = (corner == 0 || corner == 1) ? h * 0.25 : h * 0.75; + double baseAngle = Math.atan2(targetY - y, targetX - x); + double angle = baseAngle + (rnd.nextDouble() - 0.5) * (Math.PI / 3.5); double speed; - if (type == 0) { // ember + if (type == 0) { // ember speed = 0.5 + rnd.nextDouble() * 0.7; - size = rnd.nextDouble() * 3 + 2; - } else if (type == 1) { // spark + size = rnd.nextDouble() * 3 + 2; + glow = new Circle(size * 2.2); + core = new Circle(size / 2); + } else if (type == 1) { // spark speed = 1.5 + rnd.nextDouble() * 2.0; - size = rnd.nextDouble() * 1.5 + 0.5; - } else { // dust + size = rnd.nextDouble() * 1.5 + 0.5; + glow = null; + core = new Circle(size / 2); + } else { // dust 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; vy = Math.sin(angle) * speed; - wobblePhase = rnd.nextDouble() * Math.PI * 2; wobbleSpeed = 0.025 + rnd.nextDouble() * 0.04; - wobbleAmp = 0.1 + rnd.nextDouble() * 0.4; + wobbleAmp = 0.1 + rnd.nextDouble() * 0.4; } private static int weightedType(Random rnd) { @@ -155,47 +166,58 @@ public class FireParticleSystem { 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() { wobblePhase += wobbleSpeed; vx += Math.sin(wobblePhase) * wobbleAmp * 0.05; vy += Math.cos(wobblePhase) * wobbleAmp * 0.02; - x += vx; - y += vy; - if (type == 1) life -= 0.008 + Math.random() * 0.006; + x += vx; + y += vy; + if (type == 1) life -= 0.008 + rnd.nextDouble() * 0.006; } boolean isDead(double w, double h) { double pad = size * 4; - boolean offScreen = x < -pad || x > w + pad || y < -pad || y > h + pad; - return offScreen || (type == 1 && life <= 0); + return x < -pad || x > w + pad || y < -pad || y > h + pad + || (type == 1 && life <= 0); } - void draw(GraphicsContext gc, double w, double h) { - // Fade only within 5% of any edge - double edge = Math.min(w, h) * 0.05; - 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)); + void updateVisual(double w, double h) { + double edge = Math.min(w, h) * 0.05; + 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 posAlpha = Math.max(0, Math.min(fadeX, fadeY)); - double lifeAlpha = (type == 1) ? Math.min(life * 2.0, 1.0) : 1.0; - double alpha = posAlpha * lifeAlpha; - if (alpha <= 0.01) return; + double alpha = posAlpha * lifeAlpha; - if (type == 0) { // ember: orange, glows - double dist = Math.sqrt((x - w / 2) * (x - w / 2) + (y - h / 2) * (y - h / 2)); + 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 hotness = Math.max(0, 1.0 - dist / (Math.sqrt(w * w + h * h) * 0.4)); - double g = 0.3 + hotness * 0.5; - gc.setFill(Color.color(1.0, g * 0.35, 0.0, alpha * 0.10)); - gc.fillOval(x - size * 2.2, y - size * 2.2, size * 4.4, size * 4.4); - gc.setFill(Color.color(1.0, g, 0.0, alpha * 0.88)); - gc.fillOval(x - size / 2, y - size / 2, size, size); - } else if (type == 1) { // spark: white-yellow + double g = 0.3 + hotness * 0.5; + core.setFill(Color.color(1.0, g, 0.0)); + core.setOpacity(alpha * 0.88); + glow.setTranslateX(x); + glow.setTranslateY(y); + 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); - gc.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15, alpha)); - gc.fillOval(x - size / 2, y - size / 2, size, size); - } else { // dust: gray-brown translucent - gc.setFill(Color.color(0.55, 0.38, 0.22, alpha * 0.20)); - gc.fillOval(x - size, y - size * 0.5, size * 2, size); + core.setFill(Color.color(1.0, brightness * 0.85 + 0.15, brightness * 0.15)); + core.setOpacity(alpha); + } else { + core.setOpacity(alpha * 0.20); } } } diff --git a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java index 78111d8..058137d 100644 --- a/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java +++ b/src/main/java/it/polimi/ingsw/gc14/View/GUI/GUI.java @@ -106,7 +106,7 @@ public class GUI extends Application implements IView { wrapScene(leaderboardScene); FireParticleSystem fireParticles = new FireParticleSystem(mainScene); - ((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getCanvas()); + ((StackPane) mainScene.getRoot()).getChildren().add(fireParticles.getPane()); fireParticles.start(); primaryStage.setScene(loginScene);