From 5805fcc17a9bb2aacd3efd47d20fcf8ad72e4f07 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 18 Mar 2026 09:59:16 +0000 Subject: [PATCH] feat(gameengine): sprint, crouch, variable-height jump, air control - Shift to sprint (1.8x speed), Ctrl to crouch (0.4x speed + lower camera) - Smooth animated crouch via eye height lerp (no snap) - Variable-height jump: hold space longer = jump higher, release for short hop - Raycast ground detection for reliable jump triggering - Air control for mid-air strafing (Quake-style) - Configurable gravity scale for floaty/snappy feel - All parameters driven from workflow JSON (no hardcoded values) - Ctrl polled in input.poll step - Camera reads crouch height override from physics.fps.move context Co-Authored-By: Claude Opus 4.6 (1M context) --- .../packages/seed/workflows/frame_tick.json | 9 +- .../workflow_camera_fps_update_step.cpp | 7 +- .../workflow_input_poll_step.cpp | 1 + .../workflow_physics_fps_move_step.cpp | 113 +++++++++++++++--- 4 files changed, 110 insertions(+), 20 deletions(-) diff --git a/gameengine/packages/seed/workflows/frame_tick.json b/gameengine/packages/seed/workflows/frame_tick.json index ea41c70df..3707facaf 100644 --- a/gameengine/packages/seed/workflows/frame_tick.json +++ b/gameengine/packages/seed/workflows/frame_tick.json @@ -27,7 +27,14 @@ "position": [200, 0], "parameters": { "move_speed": 6.0, - "jump_force": 5.0 + "sprint_multiplier": 1.8, + "crouch_multiplier": 0.4, + "jump_height": 2.0, + "jump_duration": 0.5, + "air_control": 0.3, + "gravity_scale": 0.5, + "crouch_height": 0.7, + "stand_height": 1.5 } }, { diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_camera_fps_update_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_camera_fps_update_step.cpp index 40a6d6f9c..8cccfd1d7 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_camera_fps_update_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_camera_fps_update_step.cpp @@ -68,8 +68,11 @@ void WorkflowCameraFpsUpdateStep::Execute( context.Set("camera_pitch", pitch); // Get player body position for eye position + // Crouch/stand height override from physics.fps.move (if set) + float actualEyeHeight = context.Get("camera_eye_height", eyeHeight); + auto playerName = context.GetString("physics_player_body", ""); - glm::vec3 eyePos(0.0f, eyeHeight, 0.0f); + glm::vec3 eyePos(0.0f, actualEyeHeight, 0.0f); if (!playerName.empty()) { auto* body = context.Get("physics_body_" + playerName, nullptr); @@ -77,7 +80,7 @@ void WorkflowCameraFpsUpdateStep::Execute( btTransform transform; body->getMotionState()->getWorldTransform(transform); btVector3 pos = transform.getOrigin(); - eyePos = glm::vec3(pos.x(), pos.y() + eyeHeight, pos.z()); + eyePos = glm::vec3(pos.x(), pos.y() + actualEyeHeight, pos.z()); } } diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp index 7930ed4cd..a9ccebfbc 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_input_poll_step.cpp @@ -51,6 +51,7 @@ void WorkflowInputPollStep::Execute( context.Set("input_key_d", keyState[SDL_SCANCODE_D]); context.Set("input_key_space", keyState[SDL_SCANCODE_SPACE]); context.Set("input_key_shift", keyState[SDL_SCANCODE_LSHIFT]); + context.Set("input_key_ctrl", keyState[SDL_SCANCODE_LCTRL]); } } diff --git a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp index 558ebe02f..949de4dd9 100644 --- a/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp +++ b/gameengine/src/services/impl/workflow/workflow_generic_steps/workflow_physics_fps_move_step.cpp @@ -27,17 +27,21 @@ void WorkflowPhysicsFpsMoveStep::Execute( auto* body = context.Get("physics_body_" + playerName, nullptr); if (!body) return; - // Read move speed from parameters + // Read parameters from workflow JSON WorkflowStepParameterResolver paramResolver; - float moveSpeed = 6.0f; - float jumpForce = 5.0f; + auto getNum = [&](const char* name, float def) -> float { + const auto* p = paramResolver.FindParameter(step, name); + return (p && p->type == WorkflowParameterValue::Type::Number) ? static_cast(p->numberValue) : def; + }; - if (const auto* p = paramResolver.FindParameter(step, "move_speed")) { - if (p->type == WorkflowParameterValue::Type::Number) moveSpeed = static_cast(p->numberValue); - } - if (const auto* p = paramResolver.FindParameter(step, "jump_force")) { - if (p->type == WorkflowParameterValue::Type::Number) jumpForce = static_cast(p->numberValue); - } + const float moveSpeed = getNum("move_speed", 6.0f); + const float sprintMultiplier = getNum("sprint_multiplier", 1.8f); + const float crouchMultiplier = getNum("crouch_multiplier", 0.4f); + const float jumpForce = getNum("jump_force", 5.0f); + const float crouchHeight = getNum("crouch_height", 0.8f); + const float standHeight = getNum("stand_height", 1.6f); + const float airControl = getNum("air_control", 0.3f); + const float gravityScale = getNum("gravity_scale", 1.0f); // Read input state from context (set by input.poll) bool keyW = context.GetBool("input_key_w", false); @@ -45,6 +49,8 @@ void WorkflowPhysicsFpsMoveStep::Execute( bool keyS = context.GetBool("input_key_s", false); bool keyD = context.GetBool("input_key_d", false); bool keySpace = context.GetBool("input_key_space", false); + bool keyShift = context.GetBool("input_key_shift", false); + bool keyCtrl = context.GetBool("input_key_ctrl", false); // Read camera yaw (set by camera.fps.update from previous frame) float yaw = context.Get("camera_yaw", 0.0f); @@ -64,22 +70,95 @@ void WorkflowPhysicsFpsMoveStep::Execute( if (keyA) { moveX -= rightX; moveZ -= rightZ; } if (keyD) { moveX += rightX; moveZ += rightZ; } + // Apply sprint/crouch speed modifiers + float speed = moveSpeed; + if (keyCtrl) { + speed *= crouchMultiplier; + } else if (keyShift) { + speed *= sprintMultiplier; + } + // Normalize horizontal movement float len = std::sqrt(moveX * moveX + moveZ * moveZ); if (len > 0.001f) { - moveX = (moveX / len) * moveSpeed; - moveZ = (moveZ / len) * moveSpeed; + moveX = (moveX / len) * speed; + moveZ = (moveZ / len) * speed; } - // Preserve vertical velocity (gravity) + // Check grounded state via raycast btVector3 currentVel = body->getLinearVelocity(); - body->setLinearVelocity(btVector3(moveX, currentVel.y(), moveZ)); - - // Jump - only if approximately grounded (vertical velocity near zero) - if (keySpace && std::abs(currentVel.y()) < 0.1f) { - body->applyCentralImpulse(btVector3(0, jumpForce, 0)); + bool grounded = false; + auto* world = context.Get("physics_world", nullptr); + if (world) { + btTransform bodyTransform; + body->getMotionState()->getWorldTransform(bodyTransform); + btVector3 from = bodyTransform.getOrigin(); + btVector3 to = from + btVector3(0, -1.2f, 0); + btCollisionWorld::ClosestRayResultCallback rayResult(from, to); + world->rayTest(from, to, rayResult); + grounded = rayResult.hasHit(); } + if (grounded) { + // Full ground control + body->setLinearVelocity(btVector3(moveX, currentVel.y(), moveZ)); + } else { + // Air control: blend input with current horizontal velocity (Quake-style) + float curX = currentVel.x(); + float curZ = currentVel.z(); + float newX = curX + (moveX - curX) * airControl; + float newZ = curZ + (moveZ - curZ) * airControl; + body->setLinearVelocity(btVector3(newX, currentVel.y(), newZ)); + + // Extra downward gravity for snappy Quake-style landing + if (currentVel.y() < 0.0f) { + body->applyCentralForce(btVector3(0, -9.81f * body->getMass() * (gravityScale - 1.0f), 0)); + } + } + + // Jump - hold space for higher jump, release early for short hop + bool wasJumping = context.GetBool("player_jumping", false); + float jumpTime = context.Get("player_jump_time", 0.0f); + float jumpDuration = getNum("jump_duration", 1.2f); + float jumpHeight = getNum("jump_height", 3.5f); + + if (keySpace && !keyCtrl && grounded && !wasJumping) { + context.Set("player_jumping", true); + context.Set("player_jump_time", 0.0f); + jumpTime = 0.0f; + } + + if (context.GetBool("player_jumping", false)) { + float dt = 1.0f / 60.0f; + jumpTime += dt; + context.Set("player_jump_time", jumpTime); + + // Keep rising while space is held and under max duration + if (keySpace && jumpTime < jumpDuration) { + float t = jumpTime / jumpDuration; + float upSpeed = (jumpHeight / jumpDuration) * (1.0f - t * t); + btVector3 vel = body->getLinearVelocity(); + body->setLinearVelocity(btVector3(vel.x(), upSpeed, vel.z())); + } else { + // Released space or hit max duration - start falling + context.Set("player_jumping", false); + } + } + + if (grounded && !keySpace) { + context.Set("player_jumping", false); + } + + // Crouch: smoothly lerp eye height for natural feel + float targetHeight = keyCtrl ? crouchHeight : standHeight; + float currentHeight = context.Get("camera_eye_height", standHeight); + float lerpSpeed = 8.0f; // units per second (fast but smooth) + float dt = context.Get("physics_dt", 1.0f / 60.0f); + float newHeight = currentHeight + (targetHeight - currentHeight) * std::min(lerpSpeed * dt, 1.0f); + context.Set("camera_eye_height", newHeight); + context.Set("player_crouching", keyCtrl); + context.Set("player_sprinting", keyShift && !keyCtrl); + body->activate(true); }