Smooth first person movement? (adding delta-time)

Hello, I am new to the panda3d engine (I started about 10 days ago). I created a first person camera that works with the bullet character controller. I am having issues with the character moving around smoothly though. It’s not a camera issue or even a bullet issue(I think), but rather I can’t figure out how to keep the character’s speed from being “Jumpy”.

Here is a quick video demonstrating the problem (kinda): https://www.youtube.com/watch?v=V6bc2dtFtUg

The main problem is that the speed is not consistent with the frame rate. I know I am supposed to implement delta-time from the ClockObject to make it sync up, but I can’t seem to get it right.

This is my move function from the video:

void move(){
	/* Tried using delta-time to replace the 25 below, 
	but it only made it more dramatic. */
	float dt = co->get_dt(); 
	LVector3f playMove(0,0,0);

	if(arr[0]){ // Forward (W/Up arrow)
		playMove.add_x(-sin(camera.get_h()*(PI/180))*25); // the *25 is the speed variable
		playMove.add_y(cos(camera.get_h()*(PI/180))*25);
	}
	if(arr[1]){ // Backward (S/Down arrow)		
		playMove.add_x(sin(camera.get_h()*(PI/180))*25);
		playMove.add_y(-cos(camera.get_h()*(PI/180))*25);
	}
	if(arr[2]){ // Left (A/Left arrow)
		playMove.add_x(-cos(-camera.get_h()*(PI/180))*25);
		playMove.add_y(sin(-camera.get_h()*(PI/180))*25);
	}
	if(arr[3]){ // Right (D/Right arrow)
		playMove.add_x(cos(-camera.get_h()*(PI/180))*25);
		playMove.add_y(-sin(-camera.get_h()*(PI/180))*25);
	}

	playerControllerNode->set_linear_movement(playMove,true); // Angular movement is not needed :)

	if(arr[4]){ // Spacebar
		playerControllerNode->do_jump();
	}
}

Is there any good way to implement delta-time into the trig functions? “-sin(camera.get_h()*(PI/180))*25”

Full source code if you need it: http://pastie.org/9721822

or here:

//#undef NDEBUG
#include "pandaFramework.h"
#include "pandaSystem.h"
 
#include "genericAsyncTask.h"
#include "asyncTaskManager.h" 

#include "bulletWorld.h"
#include "bulletPlaneShape.h"
#include "bulletBoxShape.h"

#include "bulletCharacterControllerNode.h"
#include "bulletCapsuleShape.h"

#include <load_prc_file.h>

#include <math.h>
#define PI 3.14159265

// Global stuff
PT(AsyncTaskManager) taskMgr = AsyncTaskManager::get_global_ptr(); 
NodePath camera, actor;
WindowFramework *window;
ClockObject *co = ClockObject::get_global_clock();
BulletCharacterControllerNode *playerControllerNode;
NodePath playerNP;

float cen_x, cen_y; // center of screen coordinates

bool unlockMouse = false; // If this is true, then the mouse will be ignored.

bool arr[5] = {false}; // Keyboard booleans (wheather or not the key is down or not)

BulletWorld *get_physics_world() {
    static BulletWorld *physics_world = new BulletWorld();
    return physics_world;
}

void move(){
	/* Tried using delta-time to replace the 25 below, 
	but it only made it more dramatic. */
	float dt = co->get_dt(); 
	LVector3f playMove(0,0,0);

	if(arr[0]){ // Forward (W/Up arrow)
		playMove.add_x(-sin(camera.get_h()*(PI/180))*25);
		playMove.add_y(cos(camera.get_h()*(PI/180))*25);
	}
	if(arr[1]){ // Backward (S/Down arrow)		
		playMove.add_x(sin(camera.get_h()*(PI/180))*25);
		playMove.add_y(-cos(camera.get_h()*(PI/180))*25);
	}
	if(arr[2]){ // Left (A/Left arrow)
		playMove.add_x(-cos(-camera.get_h()*(PI/180))*25);
		playMove.add_y(sin(-camera.get_h()*(PI/180))*25);
	}
	if(arr[3]){ // Right (D/Right arrow)
		playMove.add_x(cos(-camera.get_h()*(PI/180))*25);
		playMove.add_y(-sin(-camera.get_h()*(PI/180))*25);
	}

	playerControllerNode->set_linear_movement(playMove,true); // Angular movement is not needed :)

	if(arr[4]){ // Spacebar
		playerControllerNode->do_jump();
	}
}

void mouseLook(){
	if(window->get_graphics_window() && !unlockMouse){
	/* Gets the difference between the mouse's horiz position and the horiz center of the screen  */
    int mx = window->get_graphics_window()->get_pointer(0).get_x() - cen_x;

	/* Difference between the mouse's vert position and the vert center of the screen  */
    int my = window->get_graphics_window()->get_pointer(0).get_y() - cen_y;

	/* p is the camera's pitch, h is the camera's heading (A.K.A. Yaw) */
	float p = camera.get_p(), h = camera.get_h(); 

	/* The *0.15 is the speed (or sensitivity) */
	camera.set_h(h-mx*0.15); // Look Left/Right
	camera.set_p(p-my*0.15); // Look Up/Down

	window->get_graphics_window()->move_pointer(0,cen_x,cen_y); // Keeps mouse locked in window.

	if(p>90)camera.set_p(90); else if(p<-90)camera.set_p(-90); // Locks the camera's pitch, to prevent the player from looking upside down.

	}
}

// Task to move the camera
AsyncTask::DoneStatus GameLoop(GenericAsyncTask* task, void* data) {
	mouseLook(); // Mouse moves the rotation of the screen.
	move(); // Character Movement
    camera.set_pos(playerNP.get_x(),playerNP.get_y(),playerNP.get_z()+1.75); // Camera's position is put with the character nodepath.
    get_physics_world()->do_physics(co->get_dt(), 1, 1.0 / 60.0); // does physics 60 times a second.
    return AsyncTask::DS_cont; // loops the function
}

void keyEvent(const Event * e, void * data) {
	// I wonder if there is a better/efficient way of doing this.

	string name = e->get_name();

	if(name=="w" || name=="arrow_up") arr[0] = true;
	else if(name=="w-up" || name=="arrow_up-up") arr[0] = false;

	if(name=="s" || name=="arrow_down") arr[1] = true;
	else if(name=="s-up" || name=="arrow_down-up") arr[1] = false;

	if(name=="a" || name=="arrow_left") arr[2] = true;
	else if(name=="a-up" || name=="arrow_left-up") arr[2] = false;

	if(name=="d" || name=="arrow_right") arr[3] = true;
	else if(name=="d-up" || name=="arrow_right-up") arr[3] = false;

	if(name=="space") arr[4] = true; else if(name=="space-up") arr[4] = false;

	if(name=="escape") exit(0);
	
	if(name=="tab") if(unlockMouse)unlockMouse=false; else unlockMouse=true;
		
}

int main(int argc, char *argv[]) {
    // Open a new window framework and set the title
    PandaFramework framework;
    framework.open_framework(argc, argv);
    framework.set_window_title("My Panda3D Window");
	
	load_prc_file_data("", "show-frame-rate-meter 1"); // shows framerate

	/* Defines keyboard keys */
    framework.define_key("arrow_up", "Moves forward", &keyEvent, NULL);
    framework.define_key("arrow_up-up", "Stops moving forward", &keyEvent, NULL);
    framework.define_key("arrow_down", "Moves backward", &keyEvent, NULL);
    framework.define_key("arrow_down-up", "Stops moving backward", &keyEvent, NULL);
    framework.define_key("arrow_right", "Moves right", &keyEvent, NULL);
    framework.define_key("arrow_right-up", "Stops moving right", &keyEvent, NULL);
    framework.define_key("arrow_left", "Moves left", &keyEvent, NULL);
    framework.define_key("arrow_left-up", "Stops moving left", &keyEvent, NULL);
    framework.define_key("w", "Moves forward", &keyEvent, NULL);
    framework.define_key("w-up", "Stops moving forward", &keyEvent, NULL);
    framework.define_key("a", "Moves left", &keyEvent, NULL);
    framework.define_key("a-up", "Stops moving left", &keyEvent, NULL);
    framework.define_key("s", "Moves backward", &keyEvent, NULL);
    framework.define_key("s-up", "Stops moving backward", &keyEvent, NULL);
    framework.define_key("d", "Moves right", &keyEvent, NULL);
    framework.define_key("d-up", "Stops moving right", &keyEvent, NULL);
    framework.define_key("escape", "Exits the game", &keyEvent, NULL);
    framework.define_key("tab", "unlocks the mouse", &keyEvent, NULL);
    framework.define_key("space", "Jump Button", &keyEvent, NULL);
    framework.define_key("space-up", "Stops Jumping", &keyEvent, NULL);

	window = framework.open_window();
	camera = window->get_camera_group();

	WindowProperties props = window->get_graphics_window()->get_properties();
	props.set_cursor_hidden(true); // Hides the mouse
	window->get_graphics_window()->request_properties(props);
		
	window->enable_keyboard();

	cen_x = window->get_graphics_window()->get_x_size()/2; // Horizontal center of screen.
	cen_y = window->get_graphics_window()->get_y_size()/2; // Vertical center of screen.

	Camera* c = window->get_camera(0);
	c->get_lens()->set_fov(90); // My preferred fov level is 90.
	c->get_lens()->set_near(0.1); // Prevents near clipping with objects.

	get_physics_world()->set_gravity(0, 0, -9.8);

	float height = 1.75, radius = 0.4;
	BulletCapsuleShape* shape = new BulletCapsuleShape(radius, height - 2*radius); // Capsule Shape for player
	 
	playerControllerNode = new BulletCharacterControllerNode(shape, 0.4, "Player"); // New Bullet Character Controller
	playerNP = window->get_render().attach_new_node(playerControllerNode); // attaches a new node based off the player controller
	playerNP.set_pos(-2, 0, 14); // Sets the inital position of the player nodepath
	CollideMask mask(BitMask32(0x10));
	playerNP.set_collide_mask(mask); 
	get_physics_world()->attach_character(playerControllerNode); // attaches the character to the world.
	
	BulletPlaneShape *floor_shape = new BulletPlaneShape(*new LVecBase3f(0, 0, 1), 1); // Infinite plane floor shape
    BulletRigidBodyNode *floor_rigid_node = new BulletRigidBodyNode("Ground"); // The floor's body node
 
    floor_rigid_node->add_shape(floor_shape);
 
    NodePath np_ground = window->get_render().attach_new_node(floor_rigid_node); // NodePath based off the floor
    np_ground.set_pos(0, 0, -2); // sets the position
    get_physics_world()->attach_rigid_body(floor_rigid_node); // attaches it to the world.

	
	// The example environment. Just for looks, no physics attached to it.
    NodePath environ = window->load_model(framework.get_models(), "models/environment");
    environ.reparent_to(window->get_render());
    environ.set_scale(0.25 , 0.25, 0.25);
    environ.set_pos(-8, 42, -1);
	
    BulletBoxShape *box_shape = new BulletBoxShape(*new LVecBase3f(1, 1, 1)); // Box Shape
    BulletRigidBodyNode *box_rigid_node = new BulletRigidBodyNode("Box"); // Box Node
 
    box_rigid_node->set_mass(1); // Sets Mass to the node
    box_rigid_node->add_shape(box_shape); // Adds the shape to the node

    NodePath np_box = window->get_render().attach_new_node(box_rigid_node); // The nodepath of the box's physics
    np_box.set_pos(0, 0, 5); // sets the box's initial position
    get_physics_world()->attach_rigid_body(box_rigid_node); // adds the box to the world.

    actor = window->load_model(framework.get_models(), "box"); // Loads the box's model
    actor.set_scale(2); // scales it up by 2 times.
	actor.set_pos(-1,-1,-1); // offsets the model to match the physical bounds.
	np_box.flatten_light();
    actor.reparent_to(np_box); // binds this to the box's physics.

    taskMgr->add(new GenericAsyncTask("The Game Loop", &GameLoop, (void*) NULL)); // gameloop
    framework.main_loop();
    framework.close_framework();
    return (0);
}

Problems like this come most likely from the stepping parameters. In your case:

do_physics(co->get_dt(), 1, 1.0 / 60.0);

The first param is the total time delta which should be simulated. Passing the elapsed time since the last call to do_physics is right.

Now Bullet can either simulate this delta time in one go (not recommended), or split it up in smaller substeps of a fixed size, which makes the simulation more smooth (recommended). How many substeps is a tradeof between performance (less substeps are faster) and quality (the more substeps the smoother).

The second param is the maximum number of substeps bullet should do.
The third parameter is the fixed size of each substep bullet.
The remaining time between the last substep and the elapsed time is interpolated by Bullet.

Let’s make an example. Large substeps for easier calculation.

  • elapsed time = 0.43 seconds
  • max num substeps = 10
  • substep size= 0.1 seconds.
    ==> Bullet will do four substeps of 0.1 sec each, and then interpolate the remaining 0.03 seconds.

Idea would be to always have a fixed step size, but since frame rendering and game logic are not that constant we will have a varying step size each frame. Usually the steps are 1/60 sec seconds or longerSo a good choice would be
world->do_physics(dt, 10, 1/200)

Every single substep is 1/180 seconds. A “normal” frame of 1/60 … 1/55 seconds will have three fixed size substeps and a tiny amount of interpolated time. If your frame rate goes down to 1/40 or 1/20 you will have more substeps, up to 10, and some more interpolated time. Don’t overdo it with the maximum number of substeps, since more substep mean less performance, and when your framerate is down anyway you don’t want to make things (much) worse.

After all it’s a bit of playing around with different substep sizes until you find something suitable for your game. don’t forget to keep slower computers in mind, which some of your players might still have. Perhaps a setting for “performance” (high/medium/low) with different max_substep/substep parameters might be reasonable.