FPS Camera with Bullet

Hi,

I’ve bodged together some code based on the Bullet Character Controller tutorial and some code Consultit and Anon worked on in this thread [url]1st person camera, "free view" style] to control the camera.

I had a look at a real FPS to see how it actually worked (Serious Sam in this case) and it basically allows mouselook, but once you start to move it follows in the direction of the camera. With my code I have reparented the BulletCharacterController to the camera to attach the camera in an FPS style. This works ok, but if I look around I carry on in the same direction as always since the character hasn’t been rotated.

To solve this I thought of rotating the character using set_angular_movement(omega). The problem is since it’s attached to the camera the camera also rotates. Also I am using the get_h() from the camera and trying to set the BulletCharacterController using set_angular_movement which is not accurate enough as far as I can see. I tried setting my player Node to the heading of the camera but this doesn’t seem to work at all, the heading is completely different.

I could get the heading from the camera, detach the camera from the bullet controller temporarily and then rotate the bullet character controller, then re-attach but it seems like a really convoluted way of doing it.

I guess my main issues are how headings work relative to each other and whether they can (or should) be translated to angular movement.

AsyncTask::DoneStatus camera_task(GenericAsyncTask* task, void* data) {

		
		double dt = task->get_elapsed_time() - mytime;
         
        // handle mouse look
        MouseData md = window->get_graphics_window()->get_pointer(0);
		int x = md.get_x();
        int y = md.get_y();
         
		if(window->get_graphics_window()->move_pointer(0, centX, centY))
		{
			camera.set_h(player_np, camera.get_h(player_np) - (x - centX) * sensX);
			camera.set_p(camera, camera.get_p(camera) - (y - centY) * sensY);  
		}  
            
    mytime = task->get_elapsed_time();       
    return AsyncTask::DS_cont;
}
void Move(const Event* event, void* data)
{
    LVector3 speed(0, 0, 0);
	
	omega = 0.0;

	if(event->get_name() == "w") //180 to -180
	{
		if(camera.get_h() == player_np.get_h())
		{
			std::cerr << "match " << "\n";
			std::cerr << "camera: " << camera.get_h();
			std::cerr << "playernp: " << player_np.get_h() << "\n";
			omega = 0.0;
		}
		if(camera.get_h() > player_np.get_h())
		{
			std::cerr << "higher " << "\n";
			std::cerr << "camera: " << camera.get_h();
			std::cerr << "playernp: " << player_np.get_h() << "\n";
			float t = camera.get_h() - player_np.get_h();
			std::cerr << "t: " << t << "\n";
			//omega = 30.0;// * t;
			//player_np.set_h(camera.get_h());

		}
		if(camera.get_h() < player_np.get_h())
		{
			std::cerr << "lower " << "\n";
			std::cerr << "camera: " << camera.get_h();
			std::cerr << "playernp: " << player_np.get_h() << "\n";
			float k = camera.get_h() - player_np.get_h();
			std::cerr << "k: " << k << "\n";
			//omega = -30.0;// * k; //30 is equivalent to 1 heading
			//player_np.set_h(camera.get_h());
		}

		//playerNode->set_angular_movement(omega);
		omegaTotal += omega;
		std::cerr << "set angular movement to " << omega << "\n";
	//	std::cerr << "updated camera: " << camera.get_h();
	//	std::cerr << " updated playernp: " << player_np.get_h() << "\n";


	}

	if(event->get_name() == "w-repeat")
	{
		speed.set_y(3.0);
	}
	
	else if(event->get_name() == "w-up")
	{
		speed.set_y(0.0);
	}
	else if(event->get_name() == "a-repeat")
	{
		speed.set_x(-3.0);
	}
	else if(event->get_name() == "a-up")
	{
		speed.set_x(0.0);
	}
	else if(event->get_name() == "s-repeat")
	{
		speed.set_y(-3.0);
	}
	else if(event->get_name() == "s-up")
	{
		speed.set_y(0.0);
	}
	else if(event->get_name() == "d-repeat")
	{
		speed.set_x(3.0);
	}
	else if(event->get_name() == "d-up")
	{
		speed.set_x(0.0);
	}

	if (event->get_name() == "q-repeat")
		omega =  120.0;
	if (event->get_name() == "r-repeat") 
		omega = -120.0;
	if (event->get_name() == "q-up")
		omega =  0.0;
	if (event->get_name() == "r-up") 
		omega = 0.0;

	playerNode->set_angular_movement(omega);
    playerNode->set_linear_movement(speed, true);
}

Might it work to connect your nodes the other way around: attach the camera to the controller, and translate and rotate the controller, rather than the camera?

Hi Thaumaturge,

I’ve attached the camera to the BulletControllerNode, that’s how I get the FPS view, but you’re right in that the camera task should actually be setting the position of the player_np NodePath instead of the camera. So if I just change it to:

AsyncTask::DoneStatus camera_task(GenericAsyncTask* task, void* data) {

		
		double dt = task->get_elapsed_time() - mytime;
         
        // handle mouse look
        MouseData md = window->get_graphics_window()->get_pointer(0);
	int x = md.get_x();
        int y = md.get_y();
         
		if(window->get_graphics_window()->move_pointer(0, centX, centY))
		{
			//camera.set_h(player_np, camera.get_h(player_np) - (x - centX) * sensX);
			//camera.set_p(camera, camera.get_p(camera) - (y - centY) * sensY);
			player_np.set_h(camera,player_np.get_h(camera) - (x - centX) * sensX);
			player_np.set_p(player_np,player_np.get_h(player_np) - (y - centY) * sensY);
		}  
            
    mytime = task->get_elapsed_time();       
    return AsyncTask::DS_cont;
}

…it works closer to what I wanted in that the bullet character is being rotated by the mouse, though the sensitivity needs serious adjustment as the character is spinning by a huge amount every time I make a small mouse movement.

Cheers,
Zobbo

I’m glad that you seem to have made progress. :slight_smile:

As to the speed at which the controller turns, you don’t appear to be accounting for the delta-time (the value that you seem to be storing in the variable “dt”) when you apply the mouse-movement–although admittedly I don’t know how the values of “sensX” and “sensY” are calculated. If it’s not being included in “sensX” and “sensY”, then I suggest that when performing your rotations, instead of just multiplying by “sensX” and “sensY”, you also multiply by “dt”. (Importantly, this should also reduce the effects of variation in frame-rate on your rate of turning.)

Hi Thaumaturge,

I’ve added dt and it didn’t see to make much difference. The heading is jumping by quite a large amount, around 50-120, so that’s quite odd, the camera was very smooth. The sensX and sensY are just set to 0.2, I tried adjusting that but didn’t get much joy.

I wondered if my physics update task was maybe causing problems I don’t really know how the do_physics parameters affect things.

AsyncTask::DoneStatus update_scene(GenericAsyncTask* task, void* data) {
    // Get dt (from Python example) and apply to do_physics(float, int, int);
    ClockObject *co = ClockObject::get_global_clock();
    get_physics_world()->do_physics(co->get_dt(), 10, 1.0 / 180.0);
   return AsyncTask::DS_cont;
}

Cheers,
Zobbo

Hmm, don’ think it’s related to my other task after mucking about with it a bit. I wonder if it’s normal to move a BulletCharacterController via the node heading rather than applying forces to it? Or is that irrelevant?

Cheers,
Zobbo

Looking again at your code, I notice that you seem to be making the rotation relative to the camera–twice; on top of that, you’re moving a NodePath relative to one of its own children, the result of which I’m not sure of.

Specifically, in this line:

player_np.set_h(camera,player_np.get_h(camera) - (x - centX) * sensX);

If I’m not much mistaken, the first parameter there is a NodePath relative to which to rotate the NodePath being rotated; you’e specified the camera. Should this perhaps be “player_np” itself?

On top of that, you’re then getting “player_np”'s rotation relative to the camera (again), and applying the offset to that. Surely, since you’re using the relative version of the method already, you can just pass in the desired offset ("(x - centX)sensXdt")?

As to the controller itself, I’m afraid that I don’t know–I made my own when I came to want a character controller for use with Bullet.

Hi Thaumaturge,

Thanks again. I think understand what you mean now so I’ve changed the code a bit. I’ve got the camera/controller movement down to this (ditching pitch for now since I’m not worried about it yet):

AsyncTask::DoneStatus camera_task(GenericAsyncTask* task, void* data) {
		
	double dt = task->get_elapsed_time() - mytime;

       // handle mouse look
        MouseData md = window->get_graphics_window()->get_pointer(0);
	int x = md.get_x();
        int y = md.get_y();
         
		if(window->get_graphics_window()->move_pointer(0, centX, centY))
		{
			player_np.set_h(player_np.get_h() - (x - centX) * sensX * dt);
		}  
    
    mytime = task->get_elapsed_time();       
    return AsyncTask::DS_cont;
}

This works in as far as it moves the camera/player_np around. It also appears to rotate the capsule by looking in third person when I remove

camera.reparent_to(player_np); 

. I’ve jettisoned the CharacterController just to eliminate that as a problem. I’m using a capsule now.

However, I still have the original problem - if I apply linear velocity to the y axis, it will continue in the same direction regardless of the rotation of the capsule. The problem with the charactercontroller was that it wouldn’t rotate. So I guess it’s a kind of progress.

Edit:

I just checked with the CharacterController again and there does seem to be a problem related to that. It seems a bit better but the rotation is so severe it’s still unusable. What’s frustrating is that it’s actually working in the sense that it will accelerate in the direction of the camera after rotation.

Looking at the charactercontroller it has a boolean to set the linear velocity as local, which I assume means relative (if I set it to false I get the same behaviour as my capsule but with added extreme jerkiness). So that must be what’s missing from my capsule controller. If I can get that working (assuming it’s possible) I might be getting somewhere.

Cheers,
Zobbo

I tried a couple of things. First rotating the object using Bullet:

btScalar yaw(cameraAngle);
	btScalar pitch(0);
	btScalar roll(0);

	std::cerr << "cameraAngle:" << cameraAngle << "\n";

	btTransform tr;
	tr.setIdentity();
	btQuaternion quat;
	quat.setEulerZYX(yaw,pitch,roll);
	tr.setRotation(quat);
	br = static_cast<btRigidBody*>(playerNode_->get_object()); 
	br->setCenterOfMassTransform(tr);

That worked in that the controller was rotated, but I got some weird behaviour, the position/height etc of the capsule is reset each time. I take it I have to integrate that into Panda by inheriting a BulletPandaNode or some such.

I gave up on that for now and tried doing an old school transform:

float velocityY = cos( playerNP * M_PI / 180 ) * 2;
	speed.set_y(velocityY);
	float velocityX = sin( playerNP * M_PI / 180 ) * 2;
	speed.set_x(lVelocityX);
	playerController->getNode()->set_linear_velocity(speed);

It seems to work within a certain range but I’m guessing that the conversion to radian and the use of -180 to 180 degrees in the camera angle (playerNP) is something to do with it, but I’m not entirely sure. Also this could get confusing when strafing and moving backwards.

Cheers,
Zobbo

Ok, I went back to the drawing board and now just set the position of a kinematic RigidBody using

nodePath.set_pos(nodePath,0,posY*deltaTime,0);

and the same for X, changing the value of posY and posX depending on what key is pressed. So now I have the mouselook working and local translation with the keys. The movement isn’t as nice as with linear velocity, but it’s smooth and I still get the collisions, so I’m happy with that for now. Getting a proper character controller working with Bullet looks complicated so I’ll put it onto the ever expanding TODO list it for now :smiley:

Cheers,
Zobbo

Are you still working on this? THANKS A LOT!

Hi namic,

I stopped my project about a year ago but I’m trying to pick it up again now. By the end the code had changed a lot because it became multiplayer, but I’ll see if I can find some stand alone code that works. There is another good example of an FPS camera somewhere in the forum, rdb will know, that is better than mine but I forget the link, sorry.

Cheers,

Anyone has any updated version of this? :unamused: