Skip to content

Guides Step Jump

Devin edited this page Apr 27, 2026 · 1 revision

Guides · Step & Jump

What you'll build

A character struct with velocity, gravity, ground check, and a one-shot jump impulse. Position drives a SceneObject's transform; the rest of the rendering code never has to know it's a "character".

Prerequisites

The why

A character is just a state struct that gets ticked every frame and a transform that gets read every frame. Keep both small. The renderer doesn't care; the controller doesn't render. Two responsibilities, one dt.

Code

#include "VCK.h"
using namespace VCK;

struct Character {
    Vec3  position    = {0, 1.2f, 0};
    Vec3  velocity    = {0, 0, 0};
    float walkSpeed   = 4.0f;     // metres / sec
    float jumpSpeed   = 5.5f;     // initial upward velocity
    float gravity     = 18.0f;    // metres / sec^2 (snappier than 9.8)
    float capsuleHalf = 0.8f;     // matches BuildCapsule(...)
    float capsuleR    = 0.4f;
    bool  grounded    = true;
};

Character me;

grounded is a hint, not a contract. Real games use a swept collision query against the world; here we treat the floor as y = 0 and check the capsule's bottom against it.

Per-frame tick

void TickCharacter(GLFWwindow* w, Character& c, float dt, const FlyCam& cam)
{
    // 1. Horizontal input -- WASD relative to the camera's yaw,
    //    flattened onto the XZ plane so jumping doesn't depend on look pitch.
    Vec3 fwd   = Forward(cam);  fwd.y = 0; fwd = Normalize(fwd);
    Vec3 right = Right(cam);    right.y = 0; right = Normalize(right);

    Vec3 wish = {0, 0, 0};
    auto down = [w](int key) { return glfwGetKey(w, key) == GLFW_PRESS; };
    if (down(GLFW_KEY_W)) wish = wish + fwd;
    if (down(GLFW_KEY_S)) wish = wish - fwd;
    if (down(GLFW_KEY_D)) wish = wish + right;
    if (down(GLFW_KEY_A)) wish = wish - right;

    if (!(wish.x == 0 && wish.z == 0))
        wish = Normalize(wish);

    c.velocity.x = wish.x * c.walkSpeed;
    c.velocity.z = wish.z * c.walkSpeed;

    // 2. Jump -- one-shot impulse, only when grounded.
    if (c.grounded && down(GLFW_KEY_SPACE))
    {
        c.velocity.y = c.jumpSpeed;
        c.grounded   = false;
    }

    // 3. Gravity integrates vertical velocity.
    c.velocity.y -= c.gravity * dt;

    // 4. Integrate position.
    c.position = c.position + c.velocity * dt;

    // 5. Ground check (flat floor at y = 0; capsule sits on its lower hemisphere).
    const float floorY = 0.0f;
    const float feet   = c.position.y - (c.capsuleHalf + c.capsuleR);
    if (feet <= floorY)
    {
        c.position.y = floorY + (c.capsuleHalf + c.capsuleR);
        c.velocity.y = 0;
        c.grounded   = true;
    }
}

Five steps: input → walk velocity → jump impulse → gravity → integrate → resolve ground. The order matters — gravity has to apply after the jump check so the impulse isn't immediately cancelled by the ground clamp.

capsuleHalf + capsuleR is the distance from the capsule's centre to the bottom of its lower hemisphere. With Translate(c.position) placing the capsule at its geometric centre, that's the offset for "feet on floor".

Push state into the SceneObject

void DrawFrame()
{
    if (window.IsMinimized()) return;
    VCK::HandleLiveResize(window, device, swapchain, framebuffers, pipeline);

    const float dt = ComputeDt();    // chrono delta from last frame
    TickCharacter(rawWindow, me, dt, cam);

    // The character's SceneObject is index 1 in this hypothetical scene.
    scene[1].transform = Translate(me.position);

    Frame& f = scheduler.BeginFrame();

    // ... acquire, render pass, draw scene, end pass, submit, present ...
}

Tick first, render second. The character's velocity / position update only happens once per frame, and the GPU sees a stable transform for the whole vkCmdDraw... chain.

A few honest caveats

Variable timestep is what you get with dt from chrono::steady_clock. Frame spikes can let dt get large enough that the integrator over-shoots a thin floor. Real physics engines run at a fixed timestep (60 / 120 Hz), accumulating the leftover. Add an accumulator if your character starts ghosting through walls.

No real collision. The ground check above is a hard plane, not a swept capsule. For real geometry, you want SAT against triangle meshes or — practically — a physics library like Jolt or PhysX. VCK's scope ends at rendering (rule 16); collision lives outside.

Air control is missing — wish overrides horizontal velocity even mid-jump. Real platformers blend toward wish while airborne with a damping factor. Add it once it bothers you.

What's next

  • Cookbook — recipe 6 (animation), recipe 12 (procedural geometry)
  • ExamplesEasyCubeExample for the same pattern with a simpler controller

VCK · Vulkan Core Kit

Getting Started

Guides

Reference

More


Single source of truth for the full API surface is the doc block at the top of VCK.h.

Clone this wiki locally