-
Notifications
You must be signed in to change notification settings - Fork 912
TUTORIAL Drawing Pixels
In this step-by-step tutorial we will make a simple game in the style of "BreakOut" or "Arkanoid". The premise of the game is to control a paddle to deflect a ball into a pattern of bricks. The ball will destroy the bricks. The objective is to destroy all of the bricks, without letting the ball leave the bottom of the screen.
To start, we'll use the skeleton application from Hello World. Notice that I've derived a class from olc::PixelGameEngine, and overridden two functions. OnUserCreate() is called once at the start of the game. OnUserUpdate() is called every frame. The olc::PixelGameEngine requests that "frames" are repeatedly drawn, and quite rapidly too - This provides the video output to the player. Typically, you want your games to render at high frame rates (the number of frames displayed per second). This makes the motion smoother, and therefore more pleasing to the player. As your game becomes more sophisticated, you'll notice the frame rate begins to decrease. This is normal, and it should encourage you to spend time making sure your code is performing well. However, don't worry about that for now, the olc::PixelGameEngine is quite a fast little thing.
#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"
class BreakOut : public olc::PixelGameEngine
{
public:
BreakOut()
{
sAppName = "TUTORIAL - BreakOut Clone";
}
public:
bool OnUserCreate() override
{
return true;
}
bool OnUserUpdate(float fElapsedTime) override
{
return true;
}
};
int main()
{
BreakOut demo;
if (demo.Construct(512, 480, 2, 2))
demo.Start();
return 0;
}
At the bottom of this file you'll see we implement the C++ "main()" function. This is where your program starts. In this simple demonstration, all we need to do is create an instance of our game class, in this case its called "demo", and is of type "BreakOut". We then need inform the olc::PixelGameEngine what size window it should use, and how big we want the pixels to be. In this case, I've specified that a frame should consist of 512x480 pixels, and each pixel, should equate to 2x2 physical screen pixels. If the construction was successful, the demo.Construct() function will return true, at which point it's safe to "Start()" the engine.
A sequence of events takes place at this point. The engine will create a window, and initialise some graphics hardware and various things the engine requires internally. The Start() function will only return when the engine is instructed to shut down. This can occur either by closing the window, or returning "false" from either of the two overridden functions.
In the constructor for the BreakOut class, you can specify the name of your application. This will appear in the title bar for the window.
HINT: If you are not familiar with classes and inheritence, dont worry! For now all you need to concern yourself with are the two functions OnUserCreate() and OnUserUpdate(). All game related activity occurs here.
Let's compile and run the program so far...
...well, perhaps understandably, it's not very interesting. What we see is a window of the specified size, but the contents are all black. This is because we don't draw anything right now, and by default the screen is set to show black pixels.
Let's add a nicer background, and a simple border to show the playing area for the game.
bool OnUserUpdate(float fElapsedTime) override
{
// Erase previous frame
Clear(olc::DARK_BLUE);
// Draw Boundary
DrawLine(10, 10, 502, 10, olc::YELLOW);
DrawLine(10, 10, 10, 470, olc::YELLOW);
DrawLine(502, 10, 502, 470, olc::YELLOW);
return true;
}
In the OnUserUpdate() function, Ive cleared the whole frame to olc::DARK_BLUE. There are a bunch of colour constants defined in olc::Pixel and you can specify colours precisely if you want to. I then draw three lines. The first line is along the top edge of the screen, then the left edge, then the right edge. By default, drawing things uses a white colour, but again, you can specify whatever colour you want.
The DrawLine() function takes two pairs of numbers, which represent coordinates on the screen. In the case of the top line, we are starting at (10,10), and drawing a line to (502, 10). These leaves a 10-pixel wide border around the screen. In this instance, I'm hard-coding values and there is nothing wrong with that. However, at anytime you can get the screen width and height in pixels by calling the ScreenWidth() and ScreenHeight() functions. Removing the hard coded numbers is good practice as you may want to change the size of your game screen later, and you would need to recalculate all of the constants. So let's do that too. (We could even change the 10 to a const variable for further flexibility)
bool OnUserUpdate(float fElapsedTime) override
{
// 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);
return true;
}
Let's compile and run again to take a look.
Well that looks ok! But lets just observe the frame rate. You may have notice it drop dramatically. This is expected because now we are giving the engine some work to do each frame, therefore each frame takes longer to complete, thus the frame rate decreases. Don't worry about it too much, but there may be something worth checking. Are you building and running your application in "Release" mode? C++ compilers have the option to produce debuggable code (which runs slower) or releasable code (which runs faster). In Visual Studio, change this "Debug" to "Release"...
...and compile and run again.
Just something to be aware of. Non Visual Studio compilers may require specific flags setting to achieve the same result, so consult the manual for the tools you are using.
It's time to add a bat and a ball. The bat can only move horizontally along the bottom of the screen, so I only need to store the x-coordinate. I feel in the future, I may want the bat to change width (larger bats make it easier, smaller bats make it harder, to hit the ball). So I'll add to the BreakOut class two private variables:
class BreakOut : public olc::PixelGameEngine
{
public:
BreakOut()
{
sAppName = "TUTORIAL - BreakOut Clone";
}
private:
float fBatPos = 20.0f;
float fBatWidth = 40.0f;
public:
bool OnUserCreate() override
{
return true;
}
I want to draw the bat as a rectangle. I can either draw a rectangular outline only using the DrawRect() function, or draw a filled in rectangle using the FillRect() function. The parameters for either function are the same.
bool OnUserUpdate(float fElapsedTime) override
{
// 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);
return true;
}
The first two parameters specify the (x, y) position of the top left of the rectangle. The second two parameters specify the width and height (w, h) of the rectangle. Note, that my bat position and width are stored as floating point types, and we'll see why in the next tutorial. The drawing functions expect integer type arguments. If you can live with a few warnings you can use the floating point variables directly, but to keep the compiler happy I've cast the floating point types to integer types. Also notice that I've used ScreenHeight() again to position the bat along the bottom of the screen.
We've a boundary, a bat, it's time to add a ball. Since the ball is going to move in two dimensions around the screen, i'm going to represent its position with the olc::vf2d vector type.
private:
float fBatPos = 20.0f;
float fBatWidth = 40.0f;
olc::vf2d vBall = { 200.0f, 200.0f };
public:
bool OnUserCreate() override;
Here, I create the variable vBall and initialise its position to (200,200). To draw the ball, I'm going to use either the DrawCircle() or FillCircle() function. Like the rectangle functions, the parameters are the same for either.
bool OnUserUpdate(float fElapsedTime) override
{
// 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, 5, olc::CYAN);
return true;
}
Here I've specified that a filled in circle is draw with its center at vBall, with a radius of 5 pixels, and coloured it with cyan.
At this point I encourage you to experiment with drawing different shapes. You can draw individual pixels, lines, circles, rectangles and triangles, either just the outline or filled in, in any colour you want. In the next tutorial, we'll add movement to the bat and the ball.