Weiter gearbeitet an allem möglichen

This commit is contained in:
2026-07-06 07:11:55 +02:00
parent 76a192df67
commit 06bb955c78
22 changed files with 987 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

View File

@@ -0,0 +1,281 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.bullet.control.CharacterControl;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;
import de.blight.common.model.MainCharacter;
import de.blight.game.control.PlayerInputControl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Ertrink-Sequenz:
* 1. MONITORING Tiefe überwachen
* 2. FADING_OUT Tiefe > 1,5 m: Eingabe weg, Charakter läuft weiter,
* Bild + Ton in 3 s ausblenden
* 3. BLACK 2 s Schwarzbild, dann Teleport + HP/Mana/Stamina=1
* 4. WAIT_PHYSICS 1,5 s warten damit Terrain-Physik lädt
* 5. FADING_IN 3 s Einblenden (REVIVE eingefroren)
* 6. REVIVE_PLAYING REVIVE-Animation läuft durch
* → zurück zu MONITORING
*/
public class DrownState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(DrownState.class);
/** Tiefe (m) die den Ertrink-Vorgang auslöst. */
private static final float TRIGGER_DEPTH = 1.5f;
private static final float FADE_OUT_DUR = 3.0f;
private static final float BLACK_DUR = 2.0f;
private static final float WAIT_PHYSICS_DUR = 1.5f;
private static final float FADE_IN_DUR = 3.0f;
// Strand-Suche
private static final float MAX_BEACH_H = 5f;
private static final float MAX_SLOPE_DEG = 10f;
private static final float SCAN_STEP_R = 3f;
private static final float SCAN_MAX_R = 300f;
private static final int SCAN_DIRS = 16;
private final TerrainChunkState terrain;
private final CharacterControl physicsChar;
private final PlayerInputControl playerInput;
private final MainCharacter mainCharacter;
private SimpleApplication app;
private Geometry overlay;
private Material overlayMat;
/** Gesetzt von WorldScene nach setupAnimationContext(). */
private String reviveClip = null;
private float reviveLength = 0f;
private enum Phase { MONITORING, FADING_OUT, BLACK, WAIT_PHYSICS, FADING_IN, REVIVE_PLAYING }
private Phase phase = Phase.MONITORING;
private float timer = 0f;
public DrownState(TerrainChunkState terrain, CharacterControl physicsChar,
PlayerInputControl playerInput, MainCharacter mainCharacter) {
this.terrain = terrain;
this.physicsChar = physicsChar;
this.playerInput = playerInput;
this.mainCharacter = mainCharacter;
}
public void setReviveInfo(String clip, float length) {
this.reviveClip = clip;
this.reviveLength = length;
log.info("[DrownState] REVIVE-Info: clip='{}' {}s", clip, length);
}
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
float w = app.getCamera().getWidth();
float h = app.getCamera().getHeight();
overlay = new Geometry("drown_overlay", new Quad(w, h));
overlayMat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, 0f));
overlayMat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
overlay.setMaterial(overlayMat);
overlay.setQueueBucket(RenderQueue.Bucket.Gui);
overlay.setLocalTranslation(0f, 0f, 50f);
}
@Override
protected void cleanup(Application application) {
detachOverlay();
restoreVolume();
}
@Override protected void onEnable() {}
@Override protected void onDisable() {}
@Override
public void update(float tpf) {
switch (phase) {
case MONITORING -> updateMonitoring();
case FADING_OUT -> updateFadingOut(tpf);
case BLACK -> updateBlack(tpf);
case WAIT_PHYSICS -> updateWaitPhysics(tpf);
case FADING_IN -> updateFadingIn(tpf);
case REVIVE_PLAYING -> updateRevivePlaying(tpf);
}
}
// ── MONITORING ────────────────────────────────────────────────────────────
private void updateMonitoring() {
if (physicsChar == null) return;
Vector3f pos = physicsChar.getPhysicsLocation();
float depth = Math.max(0f, -terrain.getHeightAt(pos.x, pos.z));
if (depth >= TRIGGER_DEPTH) {
log.info("[DrownState] Tiefe {}m Ertrink-Sequenz gestartet", depth);
playerInput.blockInputsKeepMoving();
attachOverlay();
setAlpha(0f);
setListenerScale(1f);
timer = FADE_OUT_DUR;
phase = Phase.FADING_OUT;
}
}
// ── FADING_OUT (3 s) ──────────────────────────────────────────────────────
private void updateFadingOut(float tpf) {
timer -= tpf;
float alpha = Math.max(0f, 1f - timer / FADE_OUT_DUR);
setAlpha(alpha);
setListenerScale(1f - alpha);
if (timer <= 0f) {
setAlpha(1f);
setListenerScale(0f);
timer = BLACK_DUR;
phase = Phase.BLACK;
}
}
// ── BLACK (2 s) ───────────────────────────────────────────────────────────
private void updateBlack(float tpf) {
timer -= tpf;
if (timer <= 0f) {
executeDrown();
}
}
private void executeDrown() {
// Vollständig blockieren (stoppt auch Laufrichtung)
playerInput.blockForIntro();
if (mainCharacter != null) {
mainCharacter.setCurrentHp(1);
mainCharacter.setCurrentMana(1);
mainCharacter.setCurrentStamina(1);
}
Vector3f pos = physicsChar.getPhysicsLocation();
Vector3f beach = findNearestBeach(pos.x, pos.z);
physicsChar.setPhysicsLocation(beach);
playerInput.setGroundGrace(60);
log.info("[DrownState] Teleportiert zu {}", beach);
timer = WAIT_PHYSICS_DUR;
phase = Phase.WAIT_PHYSICS;
}
// ── WAIT_PHYSICS (1,5 s) ──────────────────────────────────────────────────
private void updateWaitPhysics(float tpf) {
timer -= tpf;
if (timer <= 0f) {
if (reviveClip != null) {
playerInput.startFrozenRevive(reviveClip);
}
timer = FADE_IN_DUR;
phase = Phase.FADING_IN;
}
}
// ── FADING_IN (3 s) ───────────────────────────────────────────────────────
private void updateFadingIn(float tpf) {
timer -= tpf;
float alpha = Math.max(0f, timer / FADE_IN_DUR);
setAlpha(alpha);
setListenerScale(1f - alpha);
if (timer <= 0f) {
detachOverlay();
restoreVolume();
if (reviveClip != null) {
playerInput.unfreezeRevive();
timer = reviveLength;
phase = Phase.REVIVE_PLAYING;
} else {
playerInput.unblockInputs();
phase = Phase.MONITORING;
}
}
}
// ── REVIVE_PLAYING ────────────────────────────────────────────────────────
private void updateRevivePlaying(float tpf) {
timer -= tpf;
if (timer <= 0f) {
playerInput.unblockInputs();
phase = Phase.MONITORING;
log.info("[DrownState] Sequenz abgeschlossen Spieler freigegeben");
}
}
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
private void attachOverlay() {
if (overlay.getParent() == null) app.getGuiNode().attachChild(overlay);
}
private void detachOverlay() {
if (overlay != null && overlay.getParent() != null) app.getGuiNode().detachChild(overlay);
}
private void setAlpha(float alpha) {
overlayMat.setColor("Color", new ColorRGBA(0f, 0f, 0f, alpha));
}
private void setListenerScale(float scale) {
AudioSettingsState as = app.getStateManager().getState(AudioSettingsState.class);
float master = (as != null) ? as.getMaster() : 1f;
app.getListener().setVolume(master * Math.max(0f, Math.min(1f, scale)));
}
private void restoreVolume() {
AudioSettingsState as = app.getStateManager().getState(AudioSettingsState.class);
float master = (as != null) ? as.getMaster() : 1f;
app.getListener().setVolume(master);
}
/**
* Sucht radial vom Ausgangspunkt aus den nächsten flachen Landpunkt.
* Nahe Punkte liegen im selben Terrain-Chunk → Physik ist bereits geladen.
*/
private Vector3f findNearestBeach(float startX, float startZ) {
float maxSlope = (float) Math.tan(Math.toRadians(MAX_SLOPE_DEG));
float slopeStep = 4f;
double angleStep = 2 * Math.PI / SCAN_DIRS;
for (float r = SCAN_STEP_R; r <= SCAN_MAX_R; r += SCAN_STEP_R) {
for (int d = 0; d < SCAN_DIRS; d++) {
float x = startX + r * (float) Math.cos(d * angleStep);
float z = startZ + r * (float) Math.sin(d * angleStep);
float h = terrain.getHeightAt(x, z);
if (h < 0.2f || h > MAX_BEACH_H) continue;
float hxp = terrain.getHeightAt(x + slopeStep, z);
float hxn = terrain.getHeightAt(x - slopeStep, z);
float hzp = terrain.getHeightAt(x, z + slopeStep);
float hzn = terrain.getHeightAt(x, z - slopeStep);
float dhdx = (hxp - hxn) / (2f * slopeStep);
float dhdz = (hzp - hzn) / (2f * slopeStep);
float slope = (float) Math.sqrt(dhdx * dhdx + dhdz * dhdz);
if (slope > maxSlope) continue;
log.info("[DrownState] Strandpunkt: ({}, {}), h={}, r={}m", x, z, h, r);
return new Vector3f(x, h + 1.0f, z);
}
}
log.warn("[DrownState] Kein Strandpunkt gefunden Fallback");
return new Vector3f(startX, Math.max(terrain.getHeightAt(startX, startZ), 0f) + 3f, startZ);
}
}

View File

@@ -0,0 +1,286 @@
package de.blight.game.state;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.BaseAppState;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.VertexBuffer;
import com.jme3.texture.Texture;
import com.jme3.ui.Picture;
import com.jme3.util.BufferUtils;
import de.blight.common.model.MainCharacter;
import de.blight.game.animation.AnimationLibrary;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* HUD-Balken (unten rechts): Health (rot), Stamina (gelb), Mana (blau).
* PNG-basiert Dateien in Textures/hud/ können durch eigene Grafiken ersetzt werden.
*/
public class HudState extends BaseAppState {
private static final Logger log = LoggerFactory.getLogger(HudState.class);
// Dimensionen des Fülbereichs (ohne Rahmen)
private static final float BAR_W = 200f;
private static final float BAR_H = 18f;
private static final float BORDER = 2f;
private static final float MARGIN = 12f;
private static final float GAP = 5f;
private static final float SLOT_W = BAR_W + BORDER * 2;
private static final float SLOT_H = BAR_H + BORDER * 2;
private static final String[] FILL_ASSETS = {
"Textures/hud/bar_fill_health.png",
"Textures/hud/bar_fill_stamina.png",
"Textures/hud/bar_fill_mana.png",
};
private static final ColorRGBA[] FILL_COLORS = {
new ColorRGBA(0.85f, 0.10f, 0.10f, 1f),
new ColorRGBA(0.90f, 0.80f, 0.10f, 1f),
new ColorRGBA(0.10f, 0.30f, 0.90f, 1f),
};
private final MainCharacter mc;
private SimpleApplication app;
private Node hudNode;
private final Geometry[] fills = new Geometry[3];
private final float[] lastRatios = { -1f, -1f, -1f };
public HudState(MainCharacter mc) {
this.mc = mc;
}
@Override
protected void initialize(Application application) {
app = (SimpleApplication) application;
ensureAssets();
hudNode = new Node("hud_bars");
buildBars();
app.getGuiNode().attachChild(hudNode);
}
@Override
protected void cleanup(Application application) {
if (hudNode.getParent() != null) {
app.getGuiNode().detachChild(hudNode);
}
}
@Override
protected void onEnable() {
hudNode.setCullHint(Spatial.CullHint.Inherit);
}
@Override
protected void onDisable() {
hudNode.setCullHint(Spatial.CullHint.Always);
}
@Override
public void update(float tpf) {
if (mc == null) return;
int maxHp = mc.getMaxHp();
int maxSt = mc.getMaxStamina();
int maxMn = mc.getMaxMana();
applyRatio(0, maxHp > 0 ? (float) mc.getCurrentHp() / maxHp : 0f);
applyRatio(1, maxSt > 0 ? (float) mc.getCurrentStamina() / maxSt : 0f);
applyRatio(2, maxMn > 0 ? (float) mc.getCurrentMana() / maxMn : 0f);
}
// ── Aufbau ───────────────────────────────────────────────────────────────────
private void buildBars() {
float screenW = app.getCamera().getWidth();
// Stapelung von unten: idx=2 (mana) ganz unten, idx=0 (health) ganz oben
for (int i = 0; i < 3; i++) {
int stackPos = 2 - i; // health → stack 2 (höchste Y), mana → stack 0
float x = screenW - MARGIN - SLOT_W;
float y = MARGIN + stackPos * (SLOT_H + GAP);
buildBar(i, x, y);
}
}
private void buildBar(int idx, float x, float y) {
// Rahmen (Frame) transparent innen, Rand sichtbar
Picture frame = loadPicture("Textures/hud/bar_frame.png", SLOT_W, SLOT_H,
new ColorRGBA(0.65f, 0.65f, 0.65f, 1f));
frame.setLocalTranslation(x, y, 1f);
hudNode.attachChild(frame);
// Hintergrund (schwarz)
Picture bg = loadPicture("Textures/hud/bar_bg.png", BAR_W, BAR_H, ColorRGBA.Black);
bg.setLocalTranslation(x + BORDER, y + BORDER, 2f);
hudNode.attachChild(bg);
// Füllbalken (UV-geclippt, kein Strecken des Textur-Gradienten)
Geometry fill = buildFillGeometry(idx, x + BORDER, y + BORDER, 3f);
fills[idx] = fill;
hudNode.attachChild(fill);
}
private Geometry buildFillGeometry(int idx, float x, float y, float z) {
Mesh mesh = new Mesh();
fillMesh(mesh, BAR_W, 1f); // initial: voll
Material mat;
try {
Texture tex = app.getAssetManager().loadTexture(FILL_ASSETS[idx]);
tex.setWrap(Texture.WrapMode.EdgeClamp);
mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setTexture("ColorMap", tex);
} catch (Exception e) {
mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", FILL_COLORS[idx]);
}
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
Geometry g = new Geometry("hud_fill_" + idx, mesh);
g.setMaterial(mat);
g.setQueueBucket(RenderQueue.Bucket.Gui);
g.setLocalTranslation(x, y, z);
return g;
}
/** Setzt Breite und UV-Clipping des Fill-Meshes auf den gegebenen Ratio (01). */
private void applyRatio(int idx, float ratio) {
ratio = Math.max(0f, Math.min(1f, ratio));
if (Math.abs(ratio - lastRatios[idx]) < 0.001f) return;
lastRatios[idx] = ratio;
Geometry g = fills[idx];
float w = BAR_W * ratio;
Mesh mesh = g.getMesh();
FloatBuffer pos = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.Position).getData();
pos.put(3, w); // Vertex 1: x
pos.put(6, w); // Vertex 2: x
mesh.getBuffer(VertexBuffer.Type.Position).setUpdateNeeded();
// UV-Clipping: nur ratio-Anteil der Textur sichtbar (kein Strecken)
FloatBuffer uv = (FloatBuffer) mesh.getBuffer(VertexBuffer.Type.TexCoord).getData();
uv.put(2, ratio); // UV 1: u
uv.put(4, ratio); // UV 2: u
mesh.getBuffer(VertexBuffer.Type.TexCoord).setUpdateNeeded();
mesh.updateBound();
}
/** Erstellt ein Quad-Mesh mit UV-Koordinaten die bei maxU enden (für Clipping). */
private static void fillMesh(Mesh mesh, float w, float maxU) {
float h = BAR_H;
mesh.setBuffer(VertexBuffer.Type.Position, 3,
BufferUtils.createFloatBuffer(
0, 0, 0,
w, 0, 0,
w, h, 0,
0, h, 0
));
mesh.setBuffer(VertexBuffer.Type.TexCoord, 2,
BufferUtils.createFloatBuffer(
0f, 0f,
maxU, 0f,
maxU, 1f,
0f, 1f
));
mesh.setBuffer(VertexBuffer.Type.Index, 3,
BufferUtils.createShortBuffer((short)0,(short)1,(short)2,(short)0,(short)2,(short)3));
mesh.setMode(Mesh.Mode.Triangles);
mesh.updateBound();
}
// ── PNG-Loader ────────────────────────────────────────────────────────────────
private Picture loadPicture(String assetPath, float w, float h, ColorRGBA fallback) {
Picture p = new Picture("hud_" + assetPath);
try {
p.setImage(app.getAssetManager(), assetPath, true);
} catch (Exception e) {
Material mat = new Material(app.getAssetManager(), "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", fallback);
mat.getAdditionalRenderState().setBlendMode(RenderState.BlendMode.Alpha);
p.setMaterial(mat);
}
p.setWidth(w);
p.setHeight(h);
p.setQueueBucket(RenderQueue.Bucket.Gui);
return p;
}
// ── Placeholder-PNGs erzeugen ─────────────────────────────────────────────────
private void ensureAssets() {
Path root = AnimationLibrary.findAssetRoot();
Path dir = root.resolve("Textures").resolve("hud");
try {
Files.createDirectories(dir);
} catch (IOException e) {
log.warn("[HUD] Verzeichnis nicht erstellbar: {}", dir);
return;
}
// Rahmen: grauer Rand, transparente Mitte
ensureFrame(dir.resolve("bar_frame.png"), (int) SLOT_W, (int) SLOT_H, (int) BORDER,
180, 180, 180);
// Hintergrund: schwarz
ensureSolid(dir.resolve("bar_bg.png"), 4, 4, 0, 0, 0, 255);
// Füllfarben (kleine Kacheln, werden skaliert/geclippt)
ensureSolid(dir.resolve("bar_fill_health.png"), 4, 4, 217, 25, 25, 255);
ensureSolid(dir.resolve("bar_fill_stamina.png"), 4, 4, 229, 204, 25, 255);
ensureSolid(dir.resolve("bar_fill_mana.png"), 4, 4, 25, 76, 229, 255);
}
private static void ensureSolid(Path path, int w, int h,
int r, int g, int b, int a) {
if (Files.exists(path)) return;
try {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int argb = (a << 24) | (r << 16) | (g << 8) | b;
for (int py = 0; py < h; py++) {
for (int px = 0; px < w; px++) {
img.setRGB(px, py, argb);
}
}
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[HUD] PNG nicht erstellbar: {}", path);
}
}
private static void ensureFrame(Path path, int w, int h, int border,
int r, int g, int b) {
if (Files.exists(path)) return;
try {
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
int borderArgb = (255 << 24) | (r << 16) | (g << 8) | b;
for (int py = 0; py < h; py++) {
for (int px = 0; px < w; px++) {
boolean isBorder = px < border || py < border
|| px >= w - border || py >= h - border;
img.setRGB(px, py, isBorder ? borderArgb : 0);
}
}
ImageIO.write(img, "PNG", path.toFile());
} catch (Exception e) {
log.warn("[HUD] Frame-PNG nicht erstellbar: {}", path);
}
}
}

Binary file not shown.

View File

@@ -0,0 +1,420 @@
/*
* Copyright (c) 2009-2025 jMonkeyEngine
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of 'jMonkeyEngine' nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jme3.app;
import com.jme3.app.state.AppState;
import com.jme3.app.state.ConstantVerifierState;
import com.jme3.audio.AudioListenerState;
import com.jme3.font.BitmapFont;
import com.jme3.font.BitmapText;
import com.jme3.input.FlyByCamera;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.profile.AppStep;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial.CullHint;
import com.jme3.scene.threadwarden.SceneGraphThreadWarden;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeContext.Type;
import com.jme3.system.JmeSystem;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* `SimpleApplication` is the foundational base class for all jMonkeyEngine 3 (jME3) applications.
* It provides a streamlined setup for common game development tasks, including scene management,
* camera controls, and performance monitoring.
*
* <p>By default, `SimpleApplication` attaches several essential {@link com.jme3.app.state.AppState} instances:
* <ul>
* <li>{@link com.jme3.app.StatsAppState}: Displays real-time frames-per-second (FPS) and
* detailed performance statistics on-screen.</li>
* <li>{@link com.jme3.app.FlyCamAppState}: Provides a convenient first-person fly-by camera
* controller, allowing easy navigation within the scene.</li>
* <li>{@link com.jme3.audio.AudioListenerState}: Manages the audio listener, essential for 3D sound.</li>
* <li>{@link com.jme3.app.DebugKeysAppState}: Enables debug functionalities like displaying
* camera position and memory usage in the console.</li>
* <li>{@link com.jme3.app.state.ConstantVerifierState}: A utility state for verifying constant
* values, primarily for internal engine debugging.</li>
* </ul>
*
* <p><b>Default Key Bindings:</b></p>
* <ul>
* <li><b>Esc:</b> Closes and exits the application.</li>
* <li><b>F5:</b> Toggles the visibility of the statistics view (FPS and debug stats).</li>
* <li><b>C:</b> Prints the current camera position and rotation to the console.</li>
* <li><b>M:</b> Prints memory usage statistics to the console.</li>
* </ul>
*
* <p>Applications extending `SimpleApplication` should implement the
* {@link #simpleInitApp()} method to set up their initial scene and game logic.
*/
public abstract class SimpleApplication extends LegacyApplication {
protected static final Logger logger = Logger.getLogger(SimpleApplication.class.getName());
public static final String INPUT_MAPPING_EXIT = "SIMPLEAPP_Exit";
public static final String INPUT_MAPPING_CAMERA_POS = DebugKeysAppState.INPUT_MAPPING_CAMERA_POS;
public static final String INPUT_MAPPING_MEMORY = DebugKeysAppState.INPUT_MAPPING_MEMORY;
public static final String INPUT_MAPPING_HIDE_STATS = "SIMPLEAPP_HideStats";
protected Node rootNode = new Node("Root Node");
protected Node guiNode = new Node("Gui Node");
protected BitmapText fpsText;
protected BitmapFont guiFont;
protected FlyByCamera flyCam;
protected boolean showSettings = true;
private final AppActionListener actionListener = new AppActionListener();
private class AppActionListener implements ActionListener {
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (!isPressed) {
return;
}
if (name.equals(INPUT_MAPPING_EXIT)) {
stop();
} else if (name.equals(INPUT_MAPPING_HIDE_STATS)) {
StatsAppState statsState = stateManager.getState(StatsAppState.class);
if (statsState != null) {
statsState.toggleStats();
}
}
}
}
/**
* Constructs a `SimpleApplication` with a predefined set of default
* {@link com.jme3.app.state.AppState} instances.
* These states provide common functionalities like statistics display,
* fly camera control, audio listener, debug keys, and constant verification.
*/
public SimpleApplication() {
this(new StatsAppState(),
new FlyCamAppState(),
new AudioListenerState(),
new DebugKeysAppState(),
new ConstantVerifierState());
}
/**
* Constructs a `SimpleApplication` with a custom array of initial
* {@link com.jme3.app.state.AppState} instances.
*
* @param initialStates An array of `AppState` instances to be attached
* to the `stateManager` upon initialization.
*/
public SimpleApplication(AppState... initialStates) {
super(initialStates);
}
@Override
public void start() {
// set some default settings in-case
// settings dialog is not shown
boolean loadSettings = false;
if (settings == null) {
logger.log(Level.INFO, "AppSettings not set, creating default settings.");
setSettings(new AppSettings(true));
loadSettings = true;
}
// show settings dialog
if (showSettings) {
if (!JmeSystem.showSettingsDialog(settings, loadSettings)) {
return;
}
}
//re-setting settings they can have been merged from the registry.
setSettings(settings);
super.start();
}
/**
* Returns the current speed multiplier of the application.
* This value affects how quickly the game world updates relative to real time.
* A value of 1.0f means normal speed, 0.5f means half speed, 2.0f means double speed.
*
* @return The current speed of the application.
*/
public float getSpeed() {
return speed;
}
/**
* Changes the application's speed multiplier.
* A `speed` of 0.0f effectively pauses the application's update cycle.
*
* @param speed The desired speed multiplier. A value of 1.0f is normal speed.
* Must be non-negative.
*/
public void setSpeed(float speed) {
this.speed = speed;
}
/**
* Retrieves the `FlyByCamera` instance associated with this application.
* This camera allows free-form navigation within the 3D scene.
*
* @return The `FlyByCamera` object, or `null` if `FlyCamAppState` is not attached
* or has not yet initialized the camera.
*/
public FlyByCamera getFlyByCamera() {
return flyCam;
}
/**
* Retrieves the `Node` dedicated to 2D graphical user interface (GUI) elements.
* Objects attached to this node are rendered on top of the 3D scene,
* typically without perspective effects, suitable for HUDs and UI.
*
* @return The `Node` object representing the GUI root.
*/
public Node getGuiNode() {
return guiNode;
}
/**
* Retrieves the root `Node` of the 3D scene graph.
* All main 3D spatial objects and models should be attached to this node
* to be part of the rendered scene.
*
* @return The `Node` object representing the 3D scene root.
*/
public Node getRootNode() {
return rootNode;
}
/**
* Checks whether the settings dialog is configured to be shown at application startup.
*
* @return `true` if the settings dialog will be displayed, `false` otherwise.
*/
public boolean isShowSettings() {
return showSettings;
}
/**
* Sets whether the jME3 settings dialog should be displayed before the application starts.
*
* @param showSettings `true` to show the settings dialog, `false` to suppress it.
*/
public void setShowSettings(boolean showSettings) {
this.showSettings = showSettings;
}
/**
* Creates the font that will be set to the guiFont field
* and subsequently set as the font for the stats text.
*
* @return the loaded BitmapFont
*/
protected BitmapFont loadGuiFont() {
return assetManager.loadFont("Interface/Fonts/Default.fnt");
}
@Override
public void initialize() {
super.initialize();
//noinspection AssertWithSideEffects
assert SceneGraphThreadWarden.setup(rootNode);
//noinspection AssertWithSideEffects
assert SceneGraphThreadWarden.setup(guiNode);
// Several things rely on having this
guiFont = loadGuiFont();
guiNode.setQueueBucket(Bucket.Gui);
guiNode.setCullHint(CullHint.Never);
viewPort.attachScene(rootNode);
guiViewPort.attachScene(guiNode);
if (inputManager != null) {
// Special handling for FlyCamAppState:
// Although FlyCamAppState manages the FlyByCamera, SimpleApplication
// historically initializes and configures a default FlyByCamera instance
// and sets its initial speed. This allows subclasses to directly access
// 'flyCam' early in simpleInitApp().
FlyCamAppState flyCamState = stateManager.getState(FlyCamAppState.class);
if (flyCamState != null) {
flyCam = new FlyByCamera(cam);
flyCam.setMoveSpeed(1f); // Set a default movement speed for the camera
flyCamState.setCamera(flyCam); // Link the FlyCamAppState to this camera instance
}
// Register the "Exit" input mapping for the Escape key, but only for Display contexts.
if (context.getType() == Type.Display) {
inputManager.addMapping(INPUT_MAPPING_EXIT, new KeyTrigger(KeyInput.KEY_ESCAPE));
}
// Register the "Hide Stats" input mapping for the F5 key, if StatsAppState is active.
StatsAppState statsState = stateManager.getState(StatsAppState.class);
if (statsState != null) {
inputManager.addMapping(INPUT_MAPPING_HIDE_STATS, new KeyTrigger(KeyInput.KEY_F5));
inputManager.addListener(actionListener, INPUT_MAPPING_HIDE_STATS);
}
// Attach the action listener to the "Exit" mapping.
inputManager.addListener(actionListener, INPUT_MAPPING_EXIT);
}
// Configure the StatsAppState if it exists.
StatsAppState statsState = stateManager.getState(StatsAppState.class);
if (statsState != null) {
statsState.setFont(guiFont);
fpsText = statsState.getFpsText();
}
// Call the user's application initialization code.
simpleInitApp();
}
@Override
public void stop(boolean waitFor) {
//noinspection AssertWithSideEffects
assert SceneGraphThreadWarden.reset();
super.stop(waitFor);
}
@Override
public void update() {
if (prof != null) {
prof.appStep(AppStep.BeginFrame);
}
// Executes AppTasks from the main thread
super.update();
// Skip updates if paused or speed is zero
if (speed == 0 || paused) {
return;
}
float tpf = timer.getTimePerFrame() * speed;
// Update AppStates
if (prof != null) {
prof.appStep(AppStep.StateManagerUpdate);
}
stateManager.update(tpf);
// Call user's per-frame update method
simpleUpdate(tpf);
// Update scene graph nodes (logical and geometric states)
if (prof != null) {
prof.appStep(AppStep.SpatialUpdate);
}
rootNode.updateLogicalState(tpf);
guiNode.updateLogicalState(tpf);
rootNode.updateGeometricState();
guiNode.updateGeometricState();
// Render AppStates and the scene
if (prof != null) {
prof.appStep(AppStep.StateManagerRender);
}
stateManager.render(renderManager);
if (prof != null) {
prof.appStep(AppStep.RenderFrame);
}
renderManager.render(tpf, context.isRenderable());
// Call user's custom render method
simpleRender(renderManager);
stateManager.postRender();
if (prof != null) {
prof.appStep(AppStep.EndFrame);
}
}
/**
* Controls the visibility of the frames-per-second (FPS) display on the screen.
*
* @param show `true` to display the FPS, `false` to hide it.
*/
public void setDisplayFps(boolean show) {
StatsAppState statsState = stateManager.getState(StatsAppState.class);
if (statsState != null) {
statsState.setDisplayFps(show);
}
}
/**
* Controls the visibility of the comprehensive statistics view on the screen.
* This view typically includes details about memory, triangles, and other performance metrics.
*
* @param show `true` to display the statistics view, `false` to hide it.
*/
public void setDisplayStatView(boolean show) {
StatsAppState statsState = stateManager.getState(StatsAppState.class);
if (statsState != null) {
statsState.setDisplayStatView(show);
}
}
public abstract void simpleInitApp();
/**
* An optional method that can be overridden by subclasses for per-frame update logic.
* This method is called during the application's update loop, after AppStates are updated
* and before the scene graph's logical state is updated.
*
* @param tpf The time per frame (in seconds), adjusted by the application's speed.
*/
public void simpleUpdate(float tpf) {
// Default empty implementation; subclasses can override
}
/**
* An optional method that can be overridden by subclasses for custom rendering logic.
* This method is called during the application's render loop, after the main scene
* has been rendered and before post-rendering for states.
* Useful for drawing overlays or specific rendering tasks outside the main scene graph.
*
* @param rm The `RenderManager` instance, which provides access to rendering functionalities.
*/
public void simpleRender(RenderManager rm) {
// Default empty implementation; subclasses can override
}
}