-
Notifications
You must be signed in to change notification settings - Fork 912
TUTORIAL Managing Time
In this tutorial we will add movement and collision detection to our simple BreakOut clone, it follows on directly form the previous tutorial.
Time in video games is important. Each frame is a static image, and to give the illusion of movement we re-position things in successive frames. If we want things to move quickly we re-position objects further away than where they where in the previous frame. Going back to very basic physics, we know that:
speed = distance / time
Rearranging for distance:
distance = speed * time
Therefore we know we should move an object X pixels, if we know the speed of it, and how much time has passed. One way to know how much time has passed is to assume that each frame presented represents a fixed time-step, so let's say 0.01 seconds. If it is always assumed 0.01s has passed each frame, then you can hard-code values in. We did this in the previous tutorial for moving the bat:
if (GetKey(olc::Key::LEFT).bHeld) fBatPos -= fBatSpeed;
if (GetKey(olc::Key::RIGHT).bHeld) fBatPos += fBatSpeed;
And it looked OK, BUT this approach sucks!
Notice that the frame rate is not stable. Your PC is doing lots of other things as well as running the game, and this means your game does not get a consistent amount of CPU time over a given time period. If we move the bat by a fixed amount per frame, then the perceived speed of the bat entirely depends on the frame rate of the game. Imagine we added a really fancy explosion effect that required hundreds of drawings, the frame rate would decrease, and the bat would appear to move slower than normal. This is called "lag" and was present in most early video games. Lag should be avoided at all costs, because it ruins the "immersion" for the player - all of a sudden, the game does not behave the way the player expects it too.
A better, though not perfect, solution exists - move to pseudo-real-time. If we approximate how much real time has elapsed per frame, we can move our objects based on that. As the duration of the frame changes due to changes in algorithms, or rendering more stuff, the elapsed real time changes accordingly, meaning we can move things in such a way that is perceived as consistent. We can get this approximation of elapsed time by timing how long the previous frame took to render. Most of the time, your game will be doing normal game things, i.e it's unusual to have huge variation in frame duration. Since we are basing our current frames timing off the previous frame, it only suffices as an approximation, but it's good enough.
The olc::PixelGameEngine provides the previous frame duration to the OnUserUpdate() function via the parameter "fElapsedTime". And it is likely this will be the most used variable in your applications. fElapsedTime represents the previous frame duration in seconds, so a value of 0.002 would be 2 milliseconds.
So let's look at the physics of moving an object again. Consider A = acceleration, V = velocity and P = position. The amount the velocity (speed) changes per frame is based upon acceleration:
V = V + A * fElapsedTime
The amount the position changes due to velocity is:
P = P + V * fElapsedTime
In our simple example game, I don't need accelerations, my objects will start and stop moving instantaneously (breaks the laws of physics, but it's ok, it's a game!).
Therefore I'll update the bat's position with constant velocities:
// Handle User Input
if (GetKey(olc::Key::LEFT).bHeld) fBatPos -= fBatSpeed * fElapsedTime;
if (GetKey(olc::Key::RIGHT).bHeld) fBatPos += fBatSpeed * fElapsedTime;
Now we are operating in real-time, the fBatSpeed value indicates how many pixels the object will move per second. It's currently set at 0.1, which is very slow, it would take 10 seconds to move 1 pixel! Since the arena is about 500 pixels across, I want it to take about 2 seconds for the player to full cross it, so thats speed = distance / time, speed = 500/2, so 250 pixels/s. Let's change the fBatSpeed value to 250.
At really high frame rates you may encounter visual "jittering" using this technique and think it's simply not working at all. This appears because even though the program is running at 1000s of frames per second, there is no feasible way your display is being updated at that rate. Since the display is a guest of the OS, the visual frame is actually updated when the OS says it's OK to do so. Its very possible many hundreds of frames can elapse before the next visual image is sent to the screen. Your eyes are exceptionally good at detecting this, but be assured, behind the scenes the bat is where you think it is, therefore any subsequent physics will remain accurate. In addition, at such high frame rates, its likely that one frame will be anomalously long, due to something else happening on your PC. This will have the effect of causing a larger movement. On average though, the movement will be consistent. And as your frame rate decreases to more normal levels, the discrepancies between frame durations will tend towards zero.
Optional Advanced Note In certain situations you may need to work with a fixed time step. Advanced physics simulations are particularly prone to error with variable time steps. In olc::PixelGameEngine there are two methods you can use to work with a fixed update interval. The first is to simple enable vertical synchronisation to your monitor's refresh rate. This is done by passing in true to the "vsync" flag in the construct function:
int main()
{
BreakOut demo;
if (demo.Construct(512, 480, 2, 2, false, true))
demo.Start();
return 0;
}
The second method is to allow the olc::PixelGameEngine to run at full speed, but only do anything in OnUserUpdate() once enough time has elapsed. Here I've added two private variables:
float fTargetFrameTime = 1.0f / 100.0f; // Virtual FPS of 100fps
float fAccumulatedTime = 0.0f;
and in OnUserUpdate():
bool OnUserUpdate(float fElapsedTime) override
{
fAccumulatedTime += fElapsedTime;
if (fAccumulatedTime >= fTargetFrameTime)
{
fAccumulatedTime -= fTargetFrameTime;
fElapsedTime = fTargetFrameTime;
}
else
return true; // Don't do anything this frame
// Continue as normal
In the above example we accumulate each frames fElapsedTime until we've reached our target frame duration. At this point the accumulated time may have slightly more than the desired time, so we dont reset to 0, instead we subtract the desired time. This allows the error to be passed on to the next duration.
NOTE: There is a risk with this approach that user input events can be missed. This will need to be managed on a case-by-case basis, so just be aware of it.
For our BreakOut clone, I'm going to keep things simple and assume that multiplying by fElapsedTime will be fine in our physics integrations. I use this technique in pretty much everything I make with the olc::PixelGameEngine and it's served me well so far.
Let's add ball movement and collisions with the arena and boundary, turning this into a simple but playable game.
I've added an olc::vf2d called vBallVel, which represents the velocity vector of the ball, and added the following code to OnUserUpdate():
// Handle User Input
if (GetKey(olc::Key::LEFT).bHeld) fBatPos -= fBatSpeed * fElapsedTime;
if (GetKey(olc::Key::RIGHT).bHeld) fBatPos += fBatSpeed * fElapsedTime;
if (fBatPos < 11.0f) fBatPos = 11.0f;
if (fBatPos + fBatWidth > float(ScreenWidth()) - 10.0f) fBatPos = float(ScreenWidth()) - 10.0f - fBatWidth;
// Update Ball
vBall += vBallVel * fElapsedTime;
// Really crude arena detection - this approach sucks
if (vBall.y <= 10.0f) vBallVel.y *= -1.0f;
if (vBall.x <= 10.0f) vBallVel.x *= -1.0f;
if (vBall.x >= float(ScreenWidth()) - 10.0f) vBallVel.x *= -1.0f;
// Check Bat
if (vBall.y >= (float(ScreenHeight()) - 20.0f) && (vBall.x > fBatPos) && (vBall.x < fBatPos + fBatWidth))
vBallVel.y *= -1.0f;
// Check if ball has gone off screen
if (vBall.y > ScreenHeight())
{
// Reset ball location
vBall = { 200.0f, 200.0f };
// Choose Random direction
float fAngle = (float(rand()) / float(RAND_MAX)) * 2.0f * 3.14159f;
vBallVel = { 300.0f * cos(fAngle), 300.0f * sin(fAngle) };
}
This checks the balls (x, y) position against the arena boundaries. If it has gone beyond them, the velocity of the ball, in the opposite access is flipped. This only works for axis-aligned collision boundaries. Fortunately, in BreakOut all collision boundaries are axis aligned, so we can cheat like this.
If the ball is at the same level as the bat, then the x coordinate is checked to see if it lies with the bat's space horizontally, if it does the y velocity is flipped. If the ball has gone off the bottom of the screen, its position is reset, and a random direction is chosen for its velocity.
THIS APPROACH ALSO SUCKS! Why? Firstly it only uses the ball center, so the ball can overlap the boundaries, which looks bad. Secondly, the center of the ball has passed the boundary. Lets assume it hits a vertical boundary at a really shallow angle. The x velocity is really small. We flip it, but it could be that it's so small, it's not enough to enter the arena again, meaning on the next frame it's still technically beyond the boundary, and thus its velocity is flipped again, and this will keep happening, causing the ball to just jitter vertically along the edge boundary - i.e. it never gets out of collision. The same applies to the bat too. Thirdly, it's inaccurate! Over a given frame duration, for a fixed velocity, the ball should travel a fixed distance, yet the collision can occur between successive ball locations. To overcome this we need to start using some geometry - you can check out my "Programming Balls!" video series where I detail the maths required, but for this example tutorial we will approach things differently. Because...
Up until this point, everything has been a lie! (mwa ha ha). Well sort of. I wanted to introduce basic drawing and user input routines because they are essential. However our BreakOut clone is going to structure things slightly differently, and we are going to start using sprites!
Here is the complete final code for what we have so far anyway...
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
class BreakOut : public olc::PixelGameEngine
{
public:
BreakOut()
{
sAppName = "TUTORIAL - BreakOut Clone";
}
private:
float fBatPos = 20.0f;
float fBatWidth = 40.0f;
olc::vf2d vBall = { 200.0f, 200.0f };
olc::vf2d vBallVel = { 200.0f, -100.0f };
float fBatSpeed = 250.0f;
float fBallRadius = 5.0f;
public:
bool OnUserCreate() override
{
srand(100);
return true;
}
bool OnUserUpdate(float fElapsedTime) override
{
// Handle User Input
if (GetKey(olc::Key::LEFT).bHeld) fBatPos -= fBatSpeed * fElapsedTime;
if (GetKey(olc::Key::RIGHT).bHeld) fBatPos += fBatSpeed * fElapsedTime;
if (fBatPos < 11.0f) fBatPos = 11.0f;
if (fBatPos + fBatWidth > float(ScreenWidth()) - 10.0f) fBatPos = float(ScreenWidth()) - 10.0f - fBatWidth;
// Update Ball
vBall += vBallVel * fElapsedTime;
// Really crude arena detection - this approach sucks
if (vBall.y <= 10.0f) vBallVel.y *= -1.0f;
if (vBall.x <= 10.0f) vBallVel.x *= -1.0f;
if (vBall.x >= float(ScreenWidth()) - 10.0f) vBallVel.x *= -1.0f;
// Check Bat
if (vBall.y >= (float(ScreenHeight()) - 20.0f) && (vBall.x > fBatPos) && (vBall.x < fBatPos + fBatWidth))
vBallVel.y *= -1.0f;
// Check if ball has gone off screen
if (vBall.y > ScreenHeight())
{
// Reset ball location
vBall = { 200.0f, 200.0f };
// Choose Random direction
float fAngle = (float(rand()) / float(RAND_MAX)) * 2.0f * 3.14159f;
vBallVel = { 300.0f * cos(fAngle), 300.0f * sin(fAngle) };
}
// Erase previous frame
Clear(olc::DARK_BLUE);
// Draw Boundary
DrawLine(10, 10, ScreenWidth() - 10, 10, olc::YELLOW);
DrawLine(10, 10, 10, ScreenHeight() - 10, olc::YELLOW);
DrawLine(ScreenWidth() - 10, 10, ScreenWidth() - 10, ScreenHeight() - 10, olc::YELLOW);
// Draw Bat
FillRect(int(fBatPos), ScreenHeight() - 20, int(fBatWidth), 10, olc::GREEN);
// Draw Ball
FillCircle(vBall, int(fBallRadius), olc::CYAN);
return true;
}
};
int main()
{
BreakOut demo;
if (demo.Construct(512, 480, 2, 2))
demo.Start();
return 0;
}