Thursday, January 14, 2010

Initial Movement

The "world" I had created wasn't all that exciting.  It just sat there and waited for the user to hit a key, so it could create a new maze.  The next logical (to me) step was the addition of movement.  I changed the key-handling code a little to make special cases for the arrow keys (Qt::Key_Left, etc.), which would increment or decrement x and y translation values.  This had the effect of making the camera "jump" from one cell in the maze to its neighbor.

I wasn't thrilled with the results.  Ideally, movement within any world would be smooth.  I decided that the camera's movement (and ultimately that of the various entities within the maze) should be built upon some basic laws of physics.  Pressing an arrow key should exert a force on the object, which should cause its velocity to change.  The object's velocity should, in turn, influence its position.

I came across a few problems, at that point:
  1. I was suddenly thinking about the program as a 3D world, but I hadn't implemented any of the building blocks a 3D world requires (3D points and vectors, and all the math associated with them).
  2. The window refresh would have to use a better mechanism.  Up until now, the widget had only been refreshed whenever the maze was re-created, or an arrow key had been pressed.
  3. I wasn't sure how much physics I remembered from my high school and college classes.
The next step, it seemed, was to get the widget refreshing at regular intervals.  Using the Tao framework, this would be accomplished using glutIdleFunc, and the method specified would be called whenever the system wasn't doing something else.  In Qt, the suggestion is to set up a repeating QTimer to fire at the desired interval (I used 4ms--mainly because the example I found also used 4ms).  When that timer's timeout signal is connected to a slot, it will call the slot method at regular intervals.  It's possible to connect the timer directly to the widget's updateGL slot, but it seems a bit cleaner to create your own slot to do some pre-rendering steps, and then call updateGL() explicitly.

In order to test that the widget was correctly andimated, I decided I would rotate the scene around the z-axis by a small amount each frame.  I set a static double to zero, and incremented it a little within my animate slot.  In paintGL, I used glRotated to rotate the scene around the z-axis by the continually-increasing amount.  Once it was clear that I had a properly-spinning maze (and a little bit of vertigo), I pulled out the test code, leaving me with a fully animated (though immobile) world.

In order to make the camera move, I had to make it somewhat self-aware (though not in the creepy, humanity-destroying AI kind of way).  I moved the existing x and y position values into a 3D point class, and gave the camera an instance of the class to mark its position.  I also gave it instances of a vector class, to mark its velocity and acceleration.  (I also gave it rotation information, but I'm not yet sure how to use it.)

During each call to animate(), the camera object determines its new velocity from its acceleration.  This is where I had to return to my high school physics lessons, though I took some liberties for my final calculations.  If an object is accelerating at 9.8 m/s2 for 0.004 s, the velocity increases by 9.8 * 0.004 = 0.0392 m/s. At a steady velocity of 0.0392 m/s, the position changes by 0.0392 * 0.004 = 0.0001568 m.

At that rate, it looks like it will take forever for the camera to move anywhere.  If the force was only applied for 0.004 seconds, it would take quite a while.  As the force is continually applied, however, the velocity quickly increases, and the camera really starts moving.  Even using an acceleration of only 1.0 m/s^2, it doesn't take long to get cruising.

I'm not aware of any excruciatingly easy way to query which keys are being held at any given time in Qt.  Qt does, however, offer some simple methods which are fired off when it receives any key press or release event.  On my QGLWidget sub-class, I added a member: QHash<int, bool> mPressedKeys to keep track of which keys are being pressed.  This makes keyPressEvent and keyReleaseEvent pretty short and simple:
void GLWidget::keyPressEvent( QKeyEvent *e )
{
    mPressedKeys[ e->key() ] = true;
}
void GLWidget::keyReleaseEvent( QKeyEvent *e )
{
    mPressedKeys[ e->key() ] = false;
}

This allows me to query the state of the keys I'm interested in during each frame.  If any of the arrow keys are being pressed, I add an acceleration vector in the desired direction to the camera's internal acceleration vector.  After these vectors are all accounted for, the camera does its own physics calculations, and then the scene gets rendered.  Since the acceleration vector is re-calculated on each frame, I reset it to (0, 0, 0) when I'm done using it to get the new camera position.

I was thrilled to see a smooth camera translation when I pressed any of the arrow keys.  I was somewhat disappointed by two things, though:
  1. Releasing the arrow key did not cause the camera's movement to slow.
  2. Continually holding the arrow key caused the camera to speed up to no discernible limit.
I decided to put a velocity cap of 1.0 m/s on the camera.  If the velocity vector grew larger than that, I would simply normalize it.  If the cap had been something other than 1.0 m/s, I would have multiplied the normalized vector by the cap value.

In order to get the camera to slow down when no force was applied, I decided I had to introduce some sort of friction.  This is where my grasp of high school physics pretty much failed me, so my solution is likely not very true to life.  I ultimately decided that, in order to slow the object, I needed a force in the opposite direction from its velocity.  I introduced what I'm calling a friction constant to multiply by the inverse velocity's normalized vector.  In the case that no acceleration vector exists at the time of the velocity calculation, the friction vector is assigned to the acceleration vector.

This appeared to work pretty well; the camera came to a near-stop as quickly as it started moving.  I say "near-stop," however, because it never quite came to a stand-still.  I introduced another variable, mVelocityThreshold.  When the velocity vector's length is less than this value, I set the velocity vector to zeros, which finally causes the object to completely stop.

***

This is where the program is, today.  So now I get to discuss some of the concerns I have with the current implementation, as well as what I intend to work on next.
  • The velocity threshold, while apparently useful, does not completely solve the problem.  If the camera is traveling in the x direction, and I start applying only a y directional force, the velocity's x component does not reach zero--much like it didn't before I introduced the threshold.  I think I need to investigate a per-axis velocity threshold.
  • I'm not convinced that the frame rate is completely consistent at 0.004 ms, or 250 fps.  Since the physics calculations (loosely) depend on this, I feel like I should set up a timer to query on each frame, and determine the number of milliseconds since the previous calculation.
  • The friction vector doesn't take into account forces like gravity.  Perhaps friction will have to be calculated on a per-component basis as well.
  • The camera will likely have to be completely reconsidered.  We intend to give this game a third-person, over-the-shoulder type of view, so the arrow keys will need to move the agent within the maze instead of the camera.  In addition to this, the up arrow will need to accelerate the agent along an arbitrary vector (and not just along the y-axis).  This implies that the agent will need, along with position, velocity and acceleration, a rotation variable.  I'm not yet confident as to how to use rotation effectively in three-space.
That last point is likely where I will spend my brain power.  The first two probably aren't as interesting, but hopefully they won't take much energy to implement, either.  The third point is a bridge I'll cross when I come to it.

No comments:

Post a Comment