Bugs bezüglich Footssteps und Animationen behoben

This commit is contained in:
2026-08-21 08:07:10 +02:00
parent 024a7ffaff
commit 5d8e4db468
5 changed files with 68 additions and 34 deletions

View File

@@ -1713,6 +1713,7 @@ public class EditorApp extends Application {
input.activeLayer = SharedInput.LAYER_PLAY_TOOL; input.activeLayer = SharedInput.LAYER_PLAY_TOOL;
input.playToolMode = SharedInput.PlayToolMode.EDIT; input.playToolMode = SharedInput.PlayToolMode.EDIT;
root.setRight(buildPlayToolPanel()); root.setRight(buildPlayToolPanel());
Platform.runLater(() -> { if (gamePlayBtn != null) gamePlayBtn.requestFocus(); });
}); });
worldHintLabel = new Label(camHintText()); worldHintLabel = new Label(camHintText());

View File

@@ -12,7 +12,7 @@ public class KeyBindings {
public int jump = KeyInput.KEY_SPACE; public int jump = KeyInput.KEY_SPACE;
public int sprint = KeyInput.KEY_LSHIFT; public int sprint = KeyInput.KEY_LSHIFT;
public int walk = KeyInput.KEY_LMENU; public int walk = KeyInput.KEY_LMENU;
public int interact = KeyInput.KEY_E; public int interact = KeyInput.KEY_F;
public int turnLeft = KeyInput.KEY_Q; public int turnLeft = KeyInput.KEY_Q;
public int turnRight = KeyInput.KEY_E; public int turnRight = KeyInput.KEY_E;
public int inventory = KeyInput.KEY_I; public int inventory = KeyInput.KEY_I;

View File

@@ -64,8 +64,6 @@ public class PlayerInputControl {
// Bone-Namen nach Phase-0-Bone-Log eintragen: // Bone-Namen nach Phase-0-Bone-Log eintragen:
private static final String BONE_LEFT_FOOT = "mixamorig:LeftFoot"; private static final String BONE_LEFT_FOOT = "mixamorig:LeftFoot";
private static final String BONE_RIGHT_FOOT = "mixamorig:RightFoot"; private static final String BONE_RIGHT_FOOT = "mixamorig:RightFoot";
// Y-Schwelle im Model-Space: Fuß gilt als am Boden wenn Y < FOOT_GROUND_Y
private static final float FOOT_GROUND_Y = 0.05f;
/** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */ /** Wassertiefe (m) ab der Laufen/Sprinten gesperrt ist und Watgeräusch spielt. */
public static final float WATER_WADING_DEPTH = 0.5f; public static final float WATER_WADING_DEPTH = 0.5f;
@@ -75,16 +73,23 @@ public class PlayerInputControl {
private com.jme3.audio.AudioNode wadingNode; private com.jme3.audio.AudioNode wadingNode;
/** Aktuell berechnete Wassertiefe einmal pro Update() gesetzt, in pollFootsteps() gelesen. */ /** Aktuell berechnete Wassertiefe einmal pro Update() gesetzt, in pollFootsteps() gelesen. */
private float currentWaterDepth = 0f; private float currentWaterDepth = 0f;
private float lastLeftFootY = Float.MAX_VALUE; // Velocity-based footstep detection: fires at the local Y-minimum of each foot (no fixed threshold).
private float lastRightFootY = Float.MAX_VALUE; private float lastLeftFootY = Float.MAX_VALUE;
private float lastRightFootY = Float.MAX_VALUE;
private boolean leftFootDescending = false;
private boolean rightFootDescending = false;
private float stepCooldown = 0f;
private static final float MIN_STEP_INTERVAL = 0.15f;
// ── Anim-Offsets ───────────────────────────────────────────────────────── // ── Anim-Offsets ─────────────────────────────────────────────────────────
private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>(); private Map<String, AnimOffset> animOffsets = new LinkedHashMap<>();
/** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */ /** Basis-Local-Translation des Visual-Nodes; wird beim Laden des AnimSet einmalig gespeichert. */
private Vector3f visualBaseTranslation = new Vector3f(); private Vector3f visualBaseTranslation = new Vector3f();
private final Vector3f animOffsetCurrent = new Vector3f(); private final Vector3f animOffsetCurrent = new Vector3f();
private final Vector3f animOffsetFrom = new Vector3f();
private final Vector3f animOffsetTarget = new Vector3f(); private final Vector3f animOffsetTarget = new Vector3f();
private float animOffsetSpeed = 0f; // Zeit-basierter Lerp über AnimPlayer.BLEND_DURATION exakt wie im AnimPreviewState (Editor).
private float animOffsetElapsed = Float.MAX_VALUE; // MAX_VALUE = stabil / kein Lerp aktiv
// ── Navigation ──────────────────────────────────────────────────────────── // ── Navigation ────────────────────────────────────────────────────────────
private CharacterNavigator navigator = null; private CharacterNavigator navigator = null;
@@ -422,6 +427,7 @@ public class PlayerInputControl {
if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO); if (physicsChar != null) physicsChar.setWalkDirection(Vector3f.ZERO);
autopilotDir = null; autopilotDir = null;
clearAnimOffset(); clearAnimOffset();
if (animPlayer != null) animPlayer.stop();
if (arriveRadius > 0f) { if (arriveRadius > 0f) {
navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed); navigator.navigateTo(target, speed, arriveRadius, onArrival, onFailed);
} else { } else {
@@ -466,16 +472,11 @@ public class PlayerInputControl {
runningClip = animPlayer.getCurrentClip(); runningClip = animPlayer.getCurrentClip();
} }
if (visual != null && animOffsetSpeed > 0f) { if (visual != null && animOffsetElapsed < AnimPlayer.BLEND_DURATION) {
Vector3f delta = animOffsetTarget.subtract(animOffsetCurrent); animOffsetElapsed += tpf;
float dist = delta.length(); float t = Math.min(1f, animOffsetElapsed / AnimPlayer.BLEND_DURATION);
float step = animOffsetSpeed * tpf; animOffsetCurrent.set(animOffsetFrom);
if (dist <= step) { animOffsetCurrent.interpolateLocal(animOffsetTarget, t);
animOffsetCurrent.set(animOffsetTarget);
animOffsetSpeed = 0f;
} else {
animOffsetCurrent.addLocal(delta.normalizeLocal().multLocal(step));
}
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
} }
@@ -626,7 +627,7 @@ public class PlayerInputControl {
currentAnim = target; currentAnim = target;
} }
pollFootsteps(); pollFootsteps(tpf);
} }
private void playAction(AnimationAction action) { private void playAction(AnimationAction action) {
@@ -670,26 +671,31 @@ public class PlayerInputControl {
log.info("{}", sb); log.info("{}", sb);
} }
private void pollFootsteps() { private void resetFootstepState() {
lastLeftFootY = Float.MAX_VALUE;
lastRightFootY = Float.MAX_VALUE;
leftFootDescending = false;
rightFootDescending = false;
stepCooldown = 0f;
}
private void pollFootsteps(float tpf) {
if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) { if (!physicsChar.onGround() || blockingAnimActive || lockedInPlace) {
stopWading(); stopWading();
lastLeftFootY = Float.MAX_VALUE; resetFootstepState();
lastRightFootY = Float.MAX_VALUE;
return; return;
} }
float speed = physicsChar.getWalkDirection().length(); float speed = physicsChar.getWalkDirection().length();
if (speed < 0.001f) { if (speed < 0.001f) {
stopWading(); stopWading();
lastLeftFootY = Float.MAX_VALUE; resetFootstepState();
lastRightFootY = Float.MAX_VALUE;
return; return;
} }
if (currentWaterDepth > WATER_WADING_DEPTH) { if (currentWaterDepth > WATER_WADING_DEPTH) {
// Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken // Im Wasser: Watgeräusch abspielen, Fußgeräusche unterdrücken
startWading(); startWading();
lastLeftFootY = Float.MAX_VALUE; resetFootstepState();
lastRightFootY = Float.MAX_VALUE;
return; return;
} }
@@ -699,20 +705,29 @@ public class PlayerInputControl {
String gait = currentAnimToGait(currentAnim); String gait = currentAnimToGait(currentAnim);
if (gait == null) return; if (gait == null) return;
stepCooldown = Math.max(0f, stepCooldown - tpf);
com.jme3.anim.Joint lf = armature.getJoint(BONE_LEFT_FOOT); com.jme3.anim.Joint lf = armature.getJoint(BONE_LEFT_FOOT);
com.jme3.anim.Joint rf = armature.getJoint(BONE_RIGHT_FOOT); com.jme3.anim.Joint rf = armature.getJoint(BONE_RIGHT_FOOT);
if (lf != null) { if (lf != null) {
float y = lf.getModelTransform().getTranslation().y; float y = lf.getModelTransform().getTranslation().y;
if (y < FOOT_GROUND_Y && lastLeftFootY >= FOOT_GROUND_Y) { // Descend→ascend transition = local Y-minimum = foot touches ground
boolean desc = (lastLeftFootY - y) > 0.001f;
if (!desc && leftFootDescending && stepCooldown <= 0f) {
triggerStep(gait); triggerStep(gait);
stepCooldown = MIN_STEP_INTERVAL;
} }
leftFootDescending = desc;
lastLeftFootY = y; lastLeftFootY = y;
} }
if (rf != null) { if (rf != null) {
float y = rf.getModelTransform().getTranslation().y; float y = rf.getModelTransform().getTranslation().y;
if (y < FOOT_GROUND_Y && lastRightFootY >= FOOT_GROUND_Y) { boolean desc = (lastRightFootY - y) > 0.001f;
if (!desc && rightFootDescending && stepCooldown <= 0f) {
triggerStep(gait); triggerStep(gait);
stepCooldown = MIN_STEP_INTERVAL;
} }
rightFootDescending = desc;
lastRightFootY = y; lastRightFootY = y;
} }
} }
@@ -770,9 +785,10 @@ public class PlayerInputControl {
if (off == null) { clearAnimOffset(); return; } if (off == null) { clearAnimOffset(); return; }
Quaternion facing = visual.getLocalRotation().clone(); Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz)); Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
animOffsetTarget.set(worldOffset);
if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) { if (animOffsetCurrent.distanceSquared(worldOffset) > 1e-4f) {
animOffsetSpeed = 5.0f; animOffsetFrom.set(animOffsetCurrent);
animOffsetTarget.set(worldOffset);
animOffsetElapsed = 0f;
} }
log.debug("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z); log.debug("[AnimOffset] Clip '{}' → Ziel ({},{},{})", clip, worldOffset.x, worldOffset.y, worldOffset.z);
} }
@@ -790,9 +806,10 @@ public class PlayerInputControl {
if (off == null) { clearAnimOffsetInstant(); return; } if (off == null) { clearAnimOffsetInstant(); return; }
Quaternion facing = visual.getLocalRotation().clone(); Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz)); Vector3f worldOffset = facing.mult(new Vector3f(off.tx, off.ty, off.tz));
animOffsetFrom.set(worldOffset);
animOffsetTarget.set(worldOffset); animOffsetTarget.set(worldOffset);
animOffsetCurrent.set(worldOffset); animOffsetCurrent.set(worldOffset);
animOffsetSpeed = 0f; animOffsetElapsed = Float.MAX_VALUE;
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
log.info("[AnimOffset] Sofort: action={} clip='{}' → ({},{},{})", action, clip, log.info("[AnimOffset] Sofort: action={} clip='{}' → ({},{},{})", action, clip,
worldOffset.x, worldOffset.y, worldOffset.z); worldOffset.x, worldOffset.y, worldOffset.z);
@@ -800,8 +817,11 @@ public class PlayerInputControl {
public void clearAnimOffset() { public void clearAnimOffset() {
log.info("[AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{})", animOffsetCurrent.x, animOffsetCurrent.y, animOffsetCurrent.z); log.info("[AnimOffset] clearAnimOffset() → Lerp zu 0, current=({},{},{})", animOffsetCurrent.x, animOffsetCurrent.y, animOffsetCurrent.z);
animOffsetTarget.set(0, 0, 0); if (animOffsetCurrent.lengthSquared() > 1e-4f) {
animOffsetSpeed = 5.0f; animOffsetFrom.set(animOffsetCurrent);
animOffsetTarget.set(0, 0, 0);
animOffsetElapsed = 0f;
}
} }
/** /**
@@ -812,18 +832,20 @@ public class PlayerInputControl {
if (visual == null) return; if (visual == null) return;
Quaternion facing = visual.getLocalRotation().clone(); Quaternion facing = visual.getLocalRotation().clone();
Vector3f worldOffset = facing.mult(new Vector3f(tx, ty, tz)); Vector3f worldOffset = facing.mult(new Vector3f(tx, ty, tz));
animOffsetFrom.set(worldOffset);
animOffsetTarget.set(worldOffset); animOffsetTarget.set(worldOffset);
animOffsetCurrent.set(worldOffset); animOffsetCurrent.set(worldOffset);
animOffsetSpeed = 0f; animOffsetElapsed = Float.MAX_VALUE;
visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent)); visual.setLocalTranslation(visualBaseTranslation.clone().addLocal(animOffsetCurrent));
log.info("[AnimOffset] Instant: ({},{},{}) → world ({},{},{})", tx, ty, tz, worldOffset.x, worldOffset.y, worldOffset.z); log.info("[AnimOffset] Instant: ({},{},{}) → world ({},{},{})", tx, ty, tz, worldOffset.x, worldOffset.y, worldOffset.z);
} }
/** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */ /** Setzt den Visual-Versatz sofort auf 0 zurück (kein Lerp). */
public void clearAnimOffsetInstant() { public void clearAnimOffsetInstant() {
animOffsetFrom.set(0, 0, 0);
animOffsetTarget.set(0, 0, 0); animOffsetTarget.set(0, 0, 0);
animOffsetCurrent.set(0, 0, 0); animOffsetCurrent.set(0, 0, 0);
animOffsetSpeed = 0f; animOffsetElapsed = Float.MAX_VALUE;
if (visual != null) { if (visual != null) {
visual.setLocalTranslation(visualBaseTranslation.clone()); visual.setLocalTranslation(visualBaseTranslation.clone());
} }

View File

@@ -148,7 +148,9 @@ public class WorldInteractableState extends BaseAppState {
@Override @Override
protected void onEnable() { protected void onEnable() {
inputManager.addMapping(INTERACT_ACTION, new KeyTrigger(keyBindings.interact)); inputManager.addMapping(INTERACT_ACTION,
new MouseButtonTrigger(MouseInput.BUTTON_LEFT),
new KeyTrigger(keyBindings.interact));
inputManager.addMapping(GET_UP_ACTION, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); inputManager.addMapping(GET_UP_ACTION, new MouseButtonTrigger(MouseInput.BUTTON_RIGHT));
inputManager.addListener(interactListener, INTERACT_ACTION); inputManager.addListener(interactListener, INTERACT_ACTION);
inputManager.addListener(getUpListener, GET_UP_ACTION); inputManager.addListener(getUpListener, GET_UP_ACTION);

View File

@@ -0,0 +1,9 @@
{
"id": "9ac9943d-0e12-4d5c-8323-0e2b92eebdec",
"benchType": "Simple",
"sitzX": 236.63928,
"sitzY": 6.9953,
"sitzZ": -888.1745,
"sitzRotY": 4.7967663,
"sitzSet": true
}