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) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 09:59:16 +00:00
parent a4aee87b88
commit 5805fcc17a
4 changed files with 110 additions and 20 deletions

View File

@@ -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
}
},
{

View File

@@ -68,8 +68,11 @@ void WorkflowCameraFpsUpdateStep::Execute(
context.Set<float>("camera_pitch", pitch);
// Get player body position for eye position
// Crouch/stand height override from physics.fps.move (if set)
float actualEyeHeight = context.Get<float>("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<btRigidBody*>("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());
}
}

View File

@@ -51,6 +51,7 @@ void WorkflowInputPollStep::Execute(
context.Set<bool>("input_key_d", keyState[SDL_SCANCODE_D]);
context.Set<bool>("input_key_space", keyState[SDL_SCANCODE_SPACE]);
context.Set<bool>("input_key_shift", keyState[SDL_SCANCODE_LSHIFT]);
context.Set<bool>("input_key_ctrl", keyState[SDL_SCANCODE_LCTRL]);
}
}

View File

@@ -27,17 +27,21 @@ void WorkflowPhysicsFpsMoveStep::Execute(
auto* body = context.Get<btRigidBody*>("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<float>(p->numberValue) : def;
};
if (const auto* p = paramResolver.FindParameter(step, "move_speed")) {
if (p->type == WorkflowParameterValue::Type::Number) moveSpeed = static_cast<float>(p->numberValue);
}
if (const auto* p = paramResolver.FindParameter(step, "jump_force")) {
if (p->type == WorkflowParameterValue::Type::Number) jumpForce = static_cast<float>(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<float>("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<btDiscreteDynamicsWorld*>("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<float>("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<bool>("player_jumping", true);
context.Set<float>("player_jump_time", 0.0f);
jumpTime = 0.0f;
}
if (context.GetBool("player_jumping", false)) {
float dt = 1.0f / 60.0f;
jumpTime += dt;
context.Set<float>("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<bool>("player_jumping", false);
}
}
if (grounded && !keySpace) {
context.Set<bool>("player_jumping", false);
}
// Crouch: smoothly lerp eye height for natural feel
float targetHeight = keyCtrl ? crouchHeight : standHeight;
float currentHeight = context.Get<float>("camera_eye_height", standHeight);
float lerpSpeed = 8.0f; // units per second (fast but smooth)
float dt = context.Get<float>("physics_dt", 1.0f / 60.0f);
float newHeight = currentHeight + (targetHeight - currentHeight) * std::min(lerpSpeed * dt, 1.0f);
context.Set<float>("camera_eye_height", newHeight);
context.Set<bool>("player_crouching", keyCtrl);
context.Set<bool>("player_sprinting", keyShift && !keyCtrl);
body->activate(true);
}