From a7c3632a598251c0593638a55b7840fe77b28c2d Mon Sep 17 00:00:00 2001 From: Felix Koppe Date: Wed, 9 Oct 2024 07:41:14 +0200 Subject: [PATCH] Add hit/miss effects --- Projekte/battleship/client/build.gradle | 1 + .../pp/battleship/client/NetworkDialog.java | 12 +- .../client/gui/ParticleHandler.java | 173 ++++++++++++++++++ .../client/gui/SeaSynchronizer.java | 12 +- .../pp/battleship/client/gui/ShipControl.java | 20 ++ Projekte/settings.gradle | 1 + 6 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ParticleHandler.java diff --git a/Projekte/battleship/client/build.gradle b/Projekte/battleship/client/build.gradle index a8aeff3c..248d1e83 100644 --- a/Projekte/battleship/client/build.gradle +++ b/Projekte/battleship/client/build.gradle @@ -9,6 +9,7 @@ implementation project(":jme-common") implementation project(":battleship:model") implementation libs.jme3.desktop + implementation libs.jme3.effects runtimeOnly libs.jme3.awt.dialogs runtimeOnly libs.jme3.plugins diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java index 0bd18047..379604ad 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/NetworkDialog.java @@ -91,19 +91,25 @@ private void connect() { } } + /** + * Starts a server or if not host connects to one + */ private void connectLocally() { if(hostServer){ startServer(); try { Thread.sleep(1000); } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, e.getMessage(), e); + LOGGER.log(Level.WARNING, e.getMessage(), e.getMessage()); } } connect(); } + /** + * Starts a host server in a new thread + */ private void startServer() { new Thread(() -> { try{ @@ -115,11 +121,13 @@ private void startServer() { }).start(); } + /** + * Toggles client/host mode + */ private void toggleServerHost(){ hostServer = !hostServer; } - /** * Creates a dialog indicating that the connection is in progress. */ diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ParticleHandler.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ParticleHandler.java new file mode 100644 index 00000000..67940fc9 --- /dev/null +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ParticleHandler.java @@ -0,0 +1,173 @@ +package pp.battleship.client.gui; + +import com.jme3.effect.ParticleEmitter; +import com.jme3.effect.ParticleMesh; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.scene.Node; +import com.jme3.scene.control.AbstractControl; +import com.jme3.scene.control.Control; +import pp.battleship.client.BattleshipApp; +import pp.battleship.model.Shot; + +public class ParticleHandler { + private final BattleshipApp app; + static final System.Logger LOGGER = System.getLogger(ParticleHandler.class.getName()); + + private Material material; + + /** + * Constructs a new ParticleHandler + * + * @param app the main battleship application + */ + public ParticleHandler(BattleshipApp app) { + this.app = app; + material = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Particle.j3md"); + } + + /** + * this method is used to create a hit effect at a position + * + * @param node the node of the battleship where the effect should be attached to + * @param shot the shot that hit a target + */ + public void createHitParticles(Node node, Shot shot) { + ParticleEmitter particles1 = new ParticleEmitter("Effect", ParticleMesh.Type.Triangle,30); + particles1.setMaterial(material); + particles1.setImagesX(2); + particles1.setImagesY(2); + particles1.setStartColor(ColorRGBA.White); + particles1.setEndColor(ColorRGBA.Red); + particles1.getParticleInfluencer().setInitialVelocity(new Vector3f(0,1,0)); + particles1.setStartSize(0.45f); + particles1.setEndSize(0.1f); + particles1.setGravity(0, -0.5f, 0); + particles1.setLowLife(1f); + particles1.setHighLife(4f); + particles1.setParticlesPerSec(0); + particles1.setLocalTranslation(shot.getY() + 0.5f, 0 , shot.getX() + 0.5f); + particles1.emitAllParticles(); + + ParticleEmitter particles2 = new ParticleEmitter("Effect", ParticleMesh.Type.Triangle,30); + particles2.setMaterial(material); + particles2.setImagesX(2); + particles2.setImagesY(2); + particles2.setStartColor(ColorRGBA.Orange); + particles2.setEndColor(ColorRGBA.Yellow); + particles2.getParticleInfluencer().setInitialVelocity(new Vector3f(0,2,0)); + particles2.setStartSize(0.7f); + particles2.setEndSize(0.1f); + particles2.setGravity(0, -0.5f, 0); + particles2.setLowLife(0.5f); + particles2.setHighLife(1f); + particles2.setParticlesPerSec(0); + particles2.setLocalTranslation(shot.getY() + 1f, 0 , shot.getX() + 1f); + particles2.emitAllParticles(); + + ParticleEmitter particles3 = new ParticleEmitter("Effect", ParticleMesh.Type.Triangle,30); + particles3.setMaterial(material); + particles3.setImagesX(1); + particles3.setImagesY(1); + particles3.setStartColor(ColorRGBA.Red); + particles3.setEndColor(ColorRGBA.Gray); + particles3.getParticleInfluencer().setInitialVelocity(new Vector3f(0,0.5f,0)); + particles3.setStartSize(0.8f); + particles3.setEndSize(0.1f); + particles3.setGravity(0, -0.5f, 0); + particles3.setLowLife(0.5f); + particles3.setHighLife(0.5f); + particles3.setParticlesPerSec(0); + particles3.setLocalTranslation(shot.getY() + 1f, 0 , shot.getX() + 1f); + particles3.emitAllParticles(); + + node.attachChild(particles1); + particles1.addControl((Control) new ParticleControl(particles1, node)); + + node.attachChild(particles2); + particles2.addControl((Control) new ParticleControl(particles2, node)); + + node.attachChild(particles3); + particles3.addControl((Control) new ParticleControl(particles3, node)); + + LOGGER.log(System.Logger.Level.DEBUG, "Hit-particles at {0}", particles1.getLocalTranslation().toString()); + } + + /** + * Creates a miss effect at a certain location + * @param shot the shot that missed + */ + public ParticleEmitter createMissParticles(Shot shot) { + ParticleEmitter particles = new ParticleEmitter("MissEffect", ParticleMesh.Type.Triangle, 15); + particles.setMaterial(material); + particles.setImagesX(2); + particles.setImagesY(2); + particles.setStartColor(ColorRGBA.Blue); + particles.setEndColor(ColorRGBA.Black); + particles.getParticleInfluencer().setInitialVelocity(new Vector3f(0.1f, 0.2f, 0)); + particles.setStartSize(0.6f); + particles.setEndSize(0.05f); + particles.setGravity(0, -0.1f, 0); + particles.setLowLife(0.5f); + particles.setHighLife(3.0f); + particles.setParticlesPerSec(0); + particles.setLocalTranslation(shot.getY() + 0.5f, 0 , shot.getX() + 0.5f); + particles.emitAllParticles(); + + LOGGER.log(System.Logger.Level.DEBUG, "Hit-particles at {0}", particles.getLocalTranslation().toString()); + + return particles; + } + + /** + * This inner class is used to control the effects + */ + private static class ParticleControl extends AbstractControl { + private final ParticleEmitter emitter; + private final Node parentNode; + + /** + * this constructor is used to when the particle should be attached to a specific node + * + * @param emitter the Particle emitter to be controlled + * @param parentNode the node to be attached + */ + public ParticleControl(ParticleEmitter emitter, Node parentNode) { + this.emitter = emitter; + this.parentNode = parentNode; + } + + /** + * This constructor is used when the particle shouldn't be attached to + * a specific node + * + * @param emitter the Particle emitter to be controlled + */ + public ParticleControl(ParticleEmitter emitter) { + this.emitter = emitter; + this.parentNode = null; + } + + /** + * The method which checks if the particle is not rendered anymore so it can be removed + * + * @param tpf time per frame (in seconds) + */ + @Override + protected void controlUpdate(float tpf) { + if (emitter.getParticlesPerSec() == 0 && emitter.getNumVisibleParticles() == 0) { + if (parentNode != null) + parentNode.detachChild(emitter); + } + } + + /** + * @param rm the RenderManager rendering the controlled Spatial (not null) + * @param vp the ViewPort being rendered (not null) + */ + @Override + protected void controlRender(com.jme3.renderer.RenderManager rm, com.jme3.renderer.ViewPort vp) { + } + } +} diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java index d54a6649..80fe6686 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/SeaSynchronizer.java @@ -45,8 +45,9 @@ class SeaSynchronizer extends ShipMapSynchronizer { private static final ColorRGBA SPLASH_COLOR = new ColorRGBA(0f, 0f, 1f, 0.4f); private static final ColorRGBA HIT_COLOR = new ColorRGBA(1f, 0f, 0f, 0.4f); - private final ShipMap map; + private final ShipMap map; private final BattleshipApp app; + private final ParticleHandler handler; /** * Constructs a {@code SeaSynchronizer} object with the specified application, root node, and ship map. @@ -59,6 +60,9 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) { super(app.getGameLogic().getOwnMap(), root); this.app = app; this.map = map; + + handler = new ParticleHandler(app); + addExisting(); } @@ -72,7 +76,7 @@ public SeaSynchronizer(BattleshipApp app, Node root, ShipMap map) { */ @Override public Spatial visit(Shot shot) { - return shot.isHit() ? handleHit(shot) : createCylinder(shot); + return shot.isHit() ? handleHit(shot) : handler.createMissParticles(shot); } /** @@ -87,9 +91,7 @@ private Spatial handleHit(Shot shot) { final Battleship ship = requireNonNull(map.findShipAt(shot), "Missing ship"); final Node shipNode = requireNonNull((Node) getSpatial(ship), "Missing ship node"); - final Geometry representation = createCylinder(shot); - representation.getLocalTranslation().subtractLocal(shipNode.getLocalTranslation()); - shipNode.attachChild(representation); + handler.createHitParticles(shipNode, shot); return null; } diff --git a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java index 81acfbb1..700a6c46 100644 --- a/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java +++ b/Projekte/battleship/client/src/main/java/pp/battleship/client/gui/ShipControl.java @@ -23,6 +23,12 @@ * The ship oscillates to simulate a realistic movement on water, based on its orientation and length. */ class ShipControl extends AbstractControl { + /** + * The sinking height, after wich the ship will get removed + */ + private static final Float SINKING_HEIGHT = -0.6f; + + /** * The axis of rotation for the ship's pitch (tilting forward and backward). */ @@ -48,6 +54,11 @@ class ShipControl extends AbstractControl { */ private float time; + /** + * The ship, that the ShipControl controls + */ + private final Battleship ship; + /** * Constructs a new ShipControl instance for the specified Battleship. * The ship's orientation determines the axis of rotation, while its length influences @@ -56,6 +67,8 @@ class ShipControl extends AbstractControl { * @param ship the Battleship object to control */ public ShipControl(Battleship ship) { + this.ship = ship; + // Determine the axis of rotation based on the ship's orientation axis = switch (ship.getRot()) { case LEFT, RIGHT -> Vector3f.UNIT_X; @@ -78,6 +91,13 @@ protected void controlUpdate(float tpf) { // If spatial is null, do nothing if (spatial == null) return; + if (ship.isDestroyed() && spatial.getLocalTranslation().getY() < SINKING_HEIGHT) { // removes the ship, if it is completely sunk + spatial.getParent().detachChild(spatial); + } + else if (ship.isDestroyed() && spatial.getLocalTranslation().getY() >= SINKING_HEIGHT) { // sink the ship, if it's not completely sunk + spatial.move(0, tpf * -0.03f, 0); + } + // Update the time within the oscillation cycle time = (time + tpf) % cycle; diff --git a/Projekte/settings.gradle b/Projekte/settings.gradle index 7440607c..bb7e8c72 100644 --- a/Projekte/settings.gradle +++ b/Projekte/settings.gradle @@ -19,6 +19,7 @@ library('jme3-testdata', 'org.jmonkeyengine', 'jme3-testdata').versionRef('jme') library('jme3-lwjgl', 'org.jmonkeyengine', 'jme3-lwjgl').versionRef('jme') library('jme3-lwjgl3', 'org.jmonkeyengine', 'jme3-lwjgl3').versionRef('jme') + library('jme3-effects', 'org.jmonkeyengine', 'jme3-effects').versionRef('jme') library('lemur', 'com.simsilica:lemur:1.16.0') library('lemur-proto', 'com.simsilica:lemur-proto:1.13.0')