Simply, this record describes a rectangle, hence the name. The variables x and y correspond to the x/y coordinates of the left upper corner of the rectangle, related to the origin 0/0 which is the left upper corner of, e.g. a texture, window,… The variable w is the width and h the height of the rectangle. That’s it. The next step is to define the rectangle by assign some values for x, y, w and h.
If you use PSDL_Rect, you free the required memory by Pascal’s new procedure as you would for any simple record pointer.
Using Rectangles for Movement and Scaling
The following code demonstrates the basic principle how to achieve the impression of movement of images (sprites) and how scaling works. You will be impressed how simple it actually is.
program SDL_RectanglesScaling;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlTexture1: PSDL_Texture;
sdlRectangle: TSDL_Rect;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Halt;
if SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer) <> 0
then Halt;
// set scaling quality
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 'nearest');
// create surface from file
sdlSurface1 := SDL_LoadBMP('fpsdl.bmp');
if sdlSurface1 = nil then
Halt;
// load image file
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface1);
if sdlTexture1 = nil then
Halt;
// prepare rectangle
sdlRectangle.x := 12;
sdlRectangle.y := 25;
sdlRectangle.w := 178;
sdlRectangle.h := 116;
// render texture
SDL_RenderCopy(sdlRenderer, sdlTexture1, @sdlRectangle, nil);
SDL_RenderCopy(sdlRenderer, sdlTexture1, nil, @sdlRectangle);
// render to window for 2 seconds
SDL_RenderPresent(sdlRenderer);
SDL_Delay(2000);
// clear memory
SDL_DestroyTexture(sdlTexture1);
SDL_FreeSurface(sdlSurface1);
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//closing SDL2
SDL_Quit;
end.
After initializing SDL2 and setting up the window and renderer as known, the rectangle is getting some values. It just encloses the words “Free Pascal meets SDL” in the original image (see above).
Scaling in SDL2
Scaling Quality
Right before creating the surface and texture, there is this line in code.
// set scaling quality
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, 'nearest');
It sets the render quality. It has to be set before creating the texture. The SDL_SetHint(hint name, hint value) function is no specific function for setting scaling quality, but here we use it for exactly that. Possible values are
nearest or 0
nearest pixel sampling
linear or 1
linear filtering
support by OpenGL and Direct3D
best or 2
anisotropic filtering
support by Direct3D.
All of the values have to be set as string values, so ‘nearest’ or ‘0’. Here is a comparision of the nearest- and the linear filter.
Nearest pixel sampling filter for scaling.
Linear filter for scaling.
The anisotropic filter doesn’t do anything for me, even if I used Direct3D.
At this point happens the magic that leads to the resulting image. By the way, since the SDL_RenderCopy() function requires the rectangle arguments to be of PSDL_Rect, we use the @-operator (pointer operator) here.
This means, copy the area described by “sdlRectangle” from the source (“sdlTexture1” here) to the whole area (because of nil value) of the destination, hence the window.
Since the window has a width and height of 500 px each, the source rectangle just a width of 178 px and a height of 116 px, SDL2 automatically scales the image to fit into the larger (or smaller) dimensions of the destination.
This means, copy the whole source (because of nil value) to the area described by “sdlRectangle”. The source is the 200×200 px image, which has to squeezed to the 178×116 px rectangle at position (12/25). This is just what you see in the resulting image (above) where the whole image is squeezed into this area.
Movement of Images (Sprites)
Although not covered directly by this code example, you get the picture how movement works. Every frame you adjust the (x/y) coordinates of the rectangle for the destination to bring the sprite about to move.
After cleaning up the memory the program finishes.
Let’s start on the left in the diagram. The easiest way to get a bitmap (BMP) image file for a game or application ready for usage is to create one in a drawing application. Or use the example bitmap “fpsdl.bmp” we used in the code.
The bmp image file is stored on your hard drive and can be loaded by SDL_LoadBMP function to a SDL2 surface. This SDL2 surface is then transformed into a SDL2 texture by SDL_CreateTextureFromSurface function (whose name is just explaining what is does). And finally this texture is rendered by SDL_RenderPresent, this function we know already.
And now let’s see how it is done in code.
program SDL_LoadingRenderingBMP;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlTexture1: PSDL_Texture;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Halt;
if SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer) <> 0
then Halt;
// create surface from file
sdlSurface1 := SDL_LoadBMP('fpsdl.bmp');
if sdlSurface1 = nil then
Halt;
// create texture from surface
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface1);
if sdlTexture1 = nil then
Halt;
// render texture
if SDL_RenderCopy(sdlRenderer, sdlTexture1, nil, nil) <> 0 then
Halt;
// render to window for 2 seconds
SDL_RenderPresent(sdlRenderer);
SDL_Delay(2000);
// clear memory
SDL_DestroyTexture(sdlTexture1);
SDL_FreeSurface(sdlSurface1);
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//closing SDL2
SDL_Quit;
end.
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlTexture1: PSDL_Texture;
contains two new variables, namely “sdlSurface1” and “sdlTexture1” of the pointer types PSDL_Surface and PSDL_Texture, respecitvely.
After setting up SDL2, a window and a renderer as known, we find this.
Step 1: Loading the BMP file to a SDL2 Surface
// create surface from file
sdlSurface1 := SDL_LoadBMP('fpsdl.bmp');
if sdlSurface1 = nil then
Halt;
SDL_LoadBMP(name of bmp image file) does what you expect, it loads the image file and generates a SDL2 surface from it. Attention though, if you just give a file name, it is assumed that the file is found in the same folder as the executing application. Optionally you can also give a full file path, e.g. in Windows something like ‘C:\MyImages\fpsdl.bmp’. The function is declared as
SDL_LoadBMP(_file: PAnsiChar): PSDL_Surface
and return nil on error, e.g. if the file is not found.
Step 2: Creating a SDL2 Texture from the SDL2 Surface
The next step is to get a SDL2 texture. That’s achieve as follows.
// create texture from surface
sdlTexture1 := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface1);
if sdlTexture1 = nil then
Halt;
The function to use is SDL_CreateTextureFromSurface(renderer, surface).
It just does what you expect and transforms the SDL2 surface into a SDL2 texture with the help of the given renderer.
Step 3: Prepare the SDL2 Texture to be Rendered
Before actually rendering the texture, we need to copy it to the rendering target (our window) by SDL_RenderCopy(renderer, texture, source rectangle (texture), destination rectangle (rendering target)).
// render texture
if SDL_RenderCopy(sdlRenderer, sdlTexture1, nil, nil) <> 0 then
Halt;
So the texture is copied to the rendering target (which is the window). The first nil argument means that we want to copy the whole rectangle. The second nil means that we want to copy to the whole dimensions of the rendering target. Let’s have a closer look at the function.
This will run without any problem, though SDL_CreateTextureFromSurface() will not free the surface created by SDL_LoadBMP(). And you have no handle to free this surface. This creates a memory leak.
This chapter treats some basics you should know to understand the way SDL2 works.
Briefly: The Basics of Graphics Programming
Loading and the movement of images in a game (or other applications) is a major concept in (game) programming. These images are then refered to as sprites, usually. Let’s have a look at a simple example:
Left: Window is cleared between each new drawn frame. Right: Window is not cleared.
Here are two screenshots from a simple game. The player has to move the yellow-green paddle up- and downwards to prevent the blue ball from getting through to the right side. The game uses two sprites, the blue ball sprite and the yellow-green paddle sprite (see left screenshot). The background color is set to black. The left screenshot is how the game usually appears to the player, here between each frame that got drawn, the former frame has been cleared. The right screenshot demonstrates what happens if the former frame hasn’t been erased before the next one is drawn. – Now it is clearly visible that the sprites are redrawn again and again with sligthly different coordinates, and that is how (game) graphics work (even for the most sophisticated 3d games):
Draw the frame
Show the frame (in a window on screen)
Clear the frame (and go back to step 1)
Briefly: The Relation between Graphic Objects (e.g. Sprites) and Hardware
Actually there are just three locations where these images are stored in your computer system. All images (photo images, drawings, sprites for 2d games, textures for 3d games) are stored on your harddrive somewhere. If you start a photo viewer, a paint program, a 2d game or a 3d game, in all cases the corresponding images need to be loaded from your harddrive to RAM (Random-Access Memory) since displaying and manipulation (e.g. rotation of a photo image by 90°) of images loaded to RAM is much, much faster. Especially for games a fast access to the image data is highly important! And finally there isn’t just one RAM but two, a CPU controlled one located on the motherboard used by every program/application that needs some RAM. The second RAM is located right at your graphic board and controlled by the so-called GPU (graphics processing unit). This is what we want to use if we develop games since it is dedicated, optimized and just hungry for tasks related to fast image processing.
Many games and applications do not only target at common computer systems, but for mobile devices, e.g. smart phones. The principles described are also true for these devices even though there may be differences in detail.
The SDL2 Surface
The SDL2 surface allows you to represent graphic objects like sprites. Every SDL2 surface has a width and height, a pixel format and other properties. Nevertheless, it is a concept which originates from the outdated SDL 1.2 and therefore should not be used anymore. Still, there are reasons why we need to introduce it here. This will be clear soon.
The SDL2 Texture
The SDL2 texture allows you to represent graphic objects just like the SDL2 surface does, although there is a major difference: It is hardware accalerated. So the graphic object is stored in the graphic board’s RAM and any manipulation is done by the graphic board’s GPU.
So as a rule,
always use SDL2 Textures to store your graphic objects (sprites) for SDL 2.0,
then you go for high performance!
Three ways to SDL_Texture
So, how to get a SDL_Texture? In principle there are three ways to create SDL2 textures. For way 2 and 3 the flow diagram may illustrate how it works.
Way 1: From Scratch
You create a SDL_Texture from scratch, so you set a pixel format and texture access format and have to fill in your texture data manually. This is the most sophisticated way and is usually not necessary, unless you work with raw pixel data.
Way 2: The path from the file to the surface, to the texture and to the screen. Way 3: The path rom the file to the texture and to the screen.
Way 2: From SDL2 Surface
2) You have or create a SDL_Surface from an image file first and then you create the SDL_Texture from the SDL_Surface. This way is shown in the diagram but it means two steps.
Way 3: Directly from Image File
3) You create a SDL_Texture from and image file directly. This is shown in the diagram, too. This is the simplest way to create a SDL_Texture.
Every SDL2 program that shall show some graphic output has to have at least one SDL2 window and a SDL2 renderer. The window is the entity that is showing the graphic output and the renderer is the “machine” that is generating the output to be shown in the window. The code to set up a window and a renderer is as follows.
program SDL_WindowAndRenderer;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Halt;
// full set up
sdlWindow1 := SDL_CreateWindow('Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN);
if sdlWindow1 = nil then Halt;
sdlRenderer := SDL_CreateRenderer(sdlWindow1, -1, 0);
if sdlRenderer = nil then Halt;
// quick set up
{
if SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer) <> 0
then Halt;
}
// render to window for 2 seconds
SDL_RenderPresent(sdlRenderer);
SDL_Delay(2000);
// clear memory
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//closing SDL2
SDL_Quit;
end.
Let’s have closer look at the var clause.
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
The SDL2 Window
In SDL 2.0 you can create as many windows as you like, and each window is adressed by its PSDL_Window variable. We just need one window for now, let’s call it “sdlWindow1”. It defines the window’s properties, e.g. size, appearance, border, title name and so on. And it holds the content it shows.
Creation of a Window
// full set up
sdlWindow1 := SDL_CreateWindow('Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN);
if sdlWindow1 = nil then Halt;
The creation of a SDL2 window is simple as using the function SDL_CreateWindow(title, x, y, width, height, flags) or more specific:
In our example the window is titled “Window1”, it is located at position x = 50 and y = 50 pixels (relative to your screen). It has a width and height of 500 pixels respecitvly. And we have used the flag SDL_WINDOW_SHOWN. More about these flags later. First let’s get an understanding of the coordinate system in SDL2.
Creation of a Window on MacOS
If you use Linux or Windows, you may skip this paragraph.
It is important that the code shown above will not work on MacOS. For technical reasons you need to add an event loop to your code. Event loops are covered in a later chapter.
In short to make your code work:
add a variable “ExitLoop” of type “Boolean” to your var clause and preset it to False
add a variable “Event” of type “TSDL_Event” to your var clause
replace the call “SDL_Delay(2000);” with the following code snippet
while not ExitLoop do
begin
while (SDL_PollEvent(@Event) <> 0) do
begin
if (Event.type_ = SDL_QUITEV) then
ExitLoop := True;
end;
end;
Instead of a 2 second delay you need to quit your window (click on “X”) and the program will terminate.
The Coordinate System in SDL 2.0
This rule applies:
The origin from where to count to place a window is always the left upper corner of your screen.
So if you choose (0/0) as coordinates the window’s left upper corner will be placed right at the left upper corner of your screen. The diagram below may help to understand this. You may try SDL_WINDOWPOS_CENTERED for each or both coordinates which will lead to a centered window with respect of the screen. If you choose SDL_WINDOWPOS_UNDEFINED you don’t care for the window’s position.
Now let’s talk about the flags. They decide for the properties of the window. Look at the following table (source) of possible flags and you may get an idea what they do.
Flag
Description
SDL_WINDOW_FULLSCREEN
fullscreen window
SDL_WINDOW_FULLSCREEN_DESKTOP
fullscreen window at the current desktop resolution
SDL_WINDOW_OPENGL
window usable with OpenGL context
SDL_WINDOW_SHOWN
window is visible
SDL_WINDOW_HIDDEN
window is not visible
SDL_WINDOW_BORDERLESS
no window decoration
SDL_WINDOW_RESIZABLE
window can be resized
SDL_WINDOW_MINIMIZED
window is minimized
SDL_WINDOW_MAXIMIZED
window is maximized
SDL_WINDOW_INPUT_GRABBED
window has grabbed input focus
SDL_WINDOW_INPUT_FOCUS
window has input focus
SDL_WINDOW_MOUSE_FOCUS
window has mouse focus
SDL_WINDOW_FOREIGN
window not created by SDL
SDL_WINDOW_ALLOW_HIGHDPI
window should be created in high-DPI mode if supported (available since SDL 2.0.1)
As you can see, these flags determine different properties of the window. E.g. SDL_WINDOW_FULLSCREEN will create a fullscreen window and SDL_WINDOW_BORDERLESS will create a borderless window. You may combine several flags by OR (if appropriate). For our purpose SDL_WINDOW_SHOWN is a good choice because we just create a shown window without any further restrictions.
The SDL2 Renderer
In computer graphics rendering means the process of synthesizing the final image on your screen from the individual basic data structures. To draw some content to the window, we need therefore a renderer. The PSDL_Renderer (which we declared in the var clause) is responsible for synthesizing all the content in a window, be it some lines, a flat background, a texture, a 3d object, or whatever. We call our PSDL_Renderer “sdlRenderer”.
Creation of a Renderer
sdlRenderer := SDL_CreateRenderer(sdlWindow1, -1, 0);
if sdlRenderer = nil then Halt;
The creation of the renderer is as simple as one function call of SDL_CreateRenderer(window, index, flags) or
First we need the renderer to know where to render the finished/rendered output. That will be “Window1” in our case. Next the shown function asks for a cryptic “index”. Well, each driver which is capable of rendering (e.g. OpenGL, Direct3d, Software,…) is indexed in SDL 2.0. In principle you could choose one specific driver here by choosing the corresponding index. Since we don’t know too much about the drivers at the moment the best choice is -1. -1 means that the first driver which is supporting the chosen flag(s) is chosen. Talking about flags, there are four flags you may choose:
SDL_RENDERER_SOFTWARE
SDL_RENDERER_ACCELERATED
SDL_RENDERER_PRESENTVSYNC
SDL_RENDERER_TARGETTEXTURE
You should always prefer SDL_RENDERER_ACCELERATED because this means the graphics board is responsible for rendering, SDL_RENDERER_SOFTWARE in contrast means, the CPU has to do the rendering. As discussed before for best performance the graphic board is the best choice for rendering/graphic related tasks. SDL_RENDERER_PRESENTVSYNC allows for so called vertical synchronization which means that the display of the rendered image is synchronized with the refresh rate of the monitor. SDL_RENDERER_TARGETTEXTURE allows for rendering to a texture. You may have noticed that none of these flags but 0 was used in the example code. This automatically gives priority to hardware accelerated renderers.
Quick Creation of a Window and a Renderer
// quick set up
{
if SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer) <> 0
then Halt;
}
Instead of creating the window and the renderer separately as demonstrated, you may use SDL_CreateWindowAndRenderer(width, height, window flags, window pointer pointer, renderer pointer pointer). This has the advantage that you just need one line to set up a window and a renderer, though setting a window title, a window position or specific renderer flags have to be done afterwards if necessary.
Just remove the curly brackets and enclose the “full set up” -part to try it.
This function returns 0 on success and -1 on failure.
Rendering a SDL2 Scene
The actual rendering is achieved by SDL_RenderPresent(renderer). As a sidenote for people coming from SDL 1.2, this is what formerly has been achieved by SDL_Flip().
SDL_RenderPresent(renderer: PSDL_Renderer)
Freezing (delaying) a running program in SDL 2.0
SDL_Delay(time in milliseconds) is a simple, yet powerful and important procedure to stop the program running for a certain time in milliseconds. 2000 milliseconds are two seconds. This is kind of a twin of Pascal’s Delay procedure.
Clean up the memory in SDL 2.0
Now the final lines of code are discussed. One of the most important rules for sophisticated programming is followed here:
Always clean up the memory on program finish.
For nearly any pointer type generated by SDL 2.0, there is a destroy procedure to remove it from memory. These procedures are comparable to Pascal’s dispose procedure to remove pointer types from memory. Make sure to destroy the objects in the opposite sequence of their generation. We first created a window, then a renderer. So now we go the opposite way, first destroy the renderer and then the window by the procedures SDL_DestroyRenderer(renderer) and SDL_DestroyWindow(window) respectively.
This is a classical situation to use viewports. The game screen is clearly parted into three distinguished areas. The main screen is the large part left with the mountains and the castles. Then there is the minimap in the right-upper corner. And a statistics overview under the minimap. These areas and the corresponding viewports are highlightened in the following screenshot.
The advantage of viewports is that each of them behaves like an own window, so if you draw to the right outside of viewport 1 in the screenshot above, the texture will just be clipped and there is no overlap into viewport 2 oder 3.
The relation of your screen, a SDL2 window and a viewport within this window are outlined here.
Let’s have a look at the code.
program SDL2_Viewport;
uses SDL2;
const
Viewport1: TSDL_Rect = (x: 0; y: 0; w: 400; h: 500);
Viewport2: TSDL_Rect = (x: 400; y: 0; w: 100; h: 300);
Viewport3: TSDL_Rect = (x: 400; y: 300; w: 100; h: 200);
BlackDot: TSDL_Rect = (x: 10; y: 10; w: 3; h: 3);
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Halt;
SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer);
if (sdlWindow1 = nil) or (sdlRenderer = nil) then Halt;
// fill every viewport with background color and draw a black dot into it
SDL_RenderSetViewport(sdlRenderer, @Viewport1);
SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, nil);
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, @BlackDot);
SDL_RenderSetViewport(sdlRenderer, @Viewport2);
SDL_SetRenderDrawColor(sdlRenderer, 255, 255, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, nil);
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, @BlackDot);
SDL_RenderSetViewport(sdlRenderer, @Viewport3);
SDL_SetRenderDrawColor(sdlRenderer, 0, 255, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, nil);
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, @BlackDot);
// render to window for 2 seconds
SDL_RenderPresent(sdlRenderer);
SDL_Delay(2000);
// clear memory
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//shutting down video subsystem
SDL_Quit;
end.
“Viewport1” represents the red viewport (left), “Viewport2” the yellow (upper-right) and “Viewport3” the green (lower-right) viewport in the result image.
Notice how we just prepare one “BlackDot” rectangle for a black dot of 3×3 px dimension at location (10/10).
After setting up SDL2, a renderer and a window as known, we start to set up the first (red, left) viewport.
// fill every viewport with background color and draw a black dot into it
SDL_RenderSetViewport(sdlRenderer, @Viewport1);
SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, nil);
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(sdlRenderer, @BlackDot);
It is simple as that. Use the function SDL_RenderSetViewport(renderer, rectangle pointer) to set up a viewport. This function returns 0 on success or -1 on failure.
After we set up the viewport, we set the draw color by SDL_SetRenderDrawColor(renderer, red, green, blue, alpha) to red (255/0/0/no transparency). Then we use SDL_RenderFillRect(renderer, rectangle pointer) to fill the whole viewport by not specifying a rectangle (nil). Both functions are known from a previous chapter.
Then the color is set to black and a tiny 3×3 rectangle is drawn at location (10/10).
This procedure is repeated for the other two viewports. Notice again, how we use the same rectangle for the black dot though and where it is shown in the result image. The black dot is always drawn at location (10/10) relative to the respective viewport’s location!
As general rule it applies:
The coordinates are always relative to the currently set viewport.
Well, the remaining parts of the code provides nothing new, just the rendering for 2 seconds and some clean up.
Let’s close with some helpful remarks.
No SDL_RenderClear for Viewports!
Do not use SDL_RenderClear(renderer). It will ignore the viewports and clear the whole window with the set drawing color.
Resetting the Viewport
The resetting is done simple by SDL_RenderSetViewport(renderer, nil) as one would expect.
Any good game has a custom mouse cursor. You may think it would be a good idea to have a SDL2 surface or SDL2 texture and render it as any other sprite right at the mouse position to simulate a mouse cursor. DO NOT do this! The mouse cursor is handled separatly from the other rendering to have it smooth and working in critical situations.
The following code shows how to set up a custom mouse cursor with SDL2 the correct way.
program SDL_MouseCursor;
uses SDL2, SDL2_image;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface1: PSDL_Surface;
sdlMouseCursor: PSDL_Cursor;
sdlEvent: TSDL_Event;
ExitLoop: Boolean = False;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Halt;
SDL_CreateWindowAndRenderer(500, 500, SDL_WINDOW_SHOWN, @sdlWindow1, @sdlRenderer);
if (sdlWindow1 = nil) or (sdlRenderer = nil) then Halt;
sdlSurface1 := IMG_Load('Cursor.png' );
if sdlSurface1 = nil then Halt;
// create and set new mouse cursor
sdlMouseCursor := SDL_CreateColorCursor(sdlSurface1, 8, 8);
if sdlMouseCursor = nil then Halt;
SDL_SetCursor(sdlMouseCursor);
while ExitLoop = False do
begin
// exit loop if mouse button pressed
while SDL_PollEvent(@sdlEvent) = 1 do
if sdlEvent.type_ = SDL_MOUSEBUTTONDOWN then
ExitLoop := True;
SDL_SetRenderDrawColor(sdlRenderer, 128, 128, 128, SDL_ALPHA_OPAQUE);
SDL_RenderClear(sdlRenderer);
SDL_RenderPresent(sdlRenderer);
SDL_Delay( 20 );
end;
SDL_FreeCursor(sdlMouseCursor);
SDL_FreeSurface(sdlSurface1);
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//shutting down video subsystem
SDL_Quit;
end.
To have a custom mouse cursor we need a variable of type PSDL_Cursor. We call it “sdlMouseCursor” here.
sdlSurface1 := IMG_Load('Cursor.png' );
if sdlSurface1 = nil then Halt;
// create and set new mouse cursor
sdlMouseCursor := SDL_CreateColorCursor(sdlSurface1, 8, 8);
if sdlMouseCursor = nil then Halt;
SDL_SetCursor(sdlMouseCursor);
This is the interesting part of the code with regard to creating a custom mouse cursor. The cursor’s image is defined by a SDL surface. We create the SDL surface as known from a previous chapter from a png image file to “sdlSurface1” here.
The custom mouse cursor is created by the following function, which returns nil on error.
It needs the surface to use as cursor image and two coordinates (hot_x/hot_y) as arguments. They determine where the actual hitting point for this cursor is. Since the example cursor image is of dimensions 16×16 px and represents a cross, the “hot” (hitting) coordiates are (8/8), hence the cross’ center is used for hitting a button or something. In contrast you may imagine a typical arrow shaped mouse cursor, where the hitting point has to be adjusted to be right on the tip of the arrow in the arrow’s image.
If the cursor creation has been successful, it is necessary to set it to be the actual cursor. You may have created many different cursors, so tell SDL which one to use by the following procedure.
SDL_SetCursor(cursor: PSDL_Cursor)
The remaining part of the code is just rendering a 500 by 500 pixels window with a grey (128, 128, 128) background that is updated as long as no mouse button has been pressed.
Finally do not forget to free the mouse cursor by SDL_FreeCursor(mouse cursor) as shown.
This chapter illustrates quickly how to set up a Free Pascal and SDL2 development environment within a few minutes under Linux.
Attention: The following instruction particularly work for many Debian and Ubuntu based Linux distributions (like Linux Mint used here). In general it outlines the way to go, though.
The distribution and software I used:
Linux Distribution: Linux Mint 19.3 (Ubuntu/Debian based)
Download and install FPC, FPC sourc code and Lazarus
The first step is to install the Free Pascal compiler (version 3.0.4 or higher), the Compiler’s source code (same version as the compiler) and the Lazarurs IDE (version 2.0.6 or higher). To get the most recent, stable environment, download these three files from the official Lazarus website: https://www.lazarus-ide.org/index.php?page=downloads.
Important: Do not intermix FPC or Lazarus installs from the package manager. This will lead to troubles because these installs are not compatible. Purge any of these installs. Use, e.g.:
Choose one of the “Linux DEB Releases” according to your system (32 bit or 64 bit). Most probably your running on a 64 bit system, which is the standard case for Linux.
In case of a 64 bit system download these three files:
fpc-laz_3.0.4-1_amd64.deb
fpc-src_3.0.4-2_amd64.deb
lazarus-project_2.0.6-0_amd64.deb
The download page looks somewhat like this:
SourceForge download page for all three files necessary. The original description is kept in the image.
If you downloaded these three files successfully, you run them in the same order! First FPC, then FPC’s sources and finally Lazarus.
If everything went right, Lazarus can be started up by typing “startlazarus” in the terminal or by finding the program in the application menue.
Start up Lazarus the first time
On start up of Lazarus the directories for FPC and the FPC source code were found and set already. As a hint I show where these are located on my system:
FPC: /usr/bin/fpc
FPC Source code: /usr/share/fpcsrc/3.0.4 (because $(FPCVER) equals the version number, see screenshot)
The configurations screen may look somewhat different to the screen of the following screenshot, but that is because the screenshot is outdated a bit.
Either detected automatically or can be manually added by Tools > Options …
Before proceeding, my suggestion is to simply compile the project (press F9 in Lazarus) which is presented to you. It should compile and show the form.
Get the SDL2 units
Get the latest version of the translated SDL2 units.
Choose the master branch (1), click on “Clone or download” (2) and click on Download ZIP (3).
Make sure you have the master branch chosen and then click on “Clone or download”, then “Download ZIP”.
After extracting the ZIP file I suggest to rename the new folder into “sdl2” or “SDL2” and place it at any location, perhaps your development folder, e.g.:
(DO NOT use the suggested folder in the screenshot)
This folder is suggested as a place for the SDL2 units (DO NOT use this folder!). By the way, “Chap7” is just a random name for this project and you may have anything else there instead (I was trying out Chapter 7 tutorial code).
Get the SDL2 shared object files
If you are looking for the most recent pre-compiled SDL2 dynamic link library files (e.g. libSDL2.so) on the official SDL2 website, you just find a remark that reads like this:
Linux:
Please contact your distribution maintainer for updates.
Since SDL2 is very widespread it is very likely that your distribution maintainer already included the files.
Find SDL2 and all necessary libraries in your distribution’s package manager, here it is the synaptic package manager. Look for libsdl and install every package shown in the screenshot:
Find all necessary libraries and install them. These you should install:
libsdl2
libsdl2-dev
libsdl2-gfx
libsdl2-gfx-dev
libsdl2-image
libsdl2-image-dev
libsdl2-mixer
libsdl2-mixer-dev
libsdl2-net
libsdl2-net-dev
libsdl2-ttf
libsdl2-ttf-dev
The dev-packages are necessary to compile SDL2 applications. The other packages are necessary to run SDL2 applications.
The version of these libraries does not necessarily need to be the most recent unfortunately. If you really need the most recent versions here, you may try to contact the maintainer to ask to update the version.
Congratulation! After that, everything should run smoothly :-)!
This chapter will introduce you on how to combine the SDL library with the famous Open Graphics Library (OpenGL).
What is OpenGL?
OpenGL is the first choice when it comes to platform independent 2d and 3d graphics programming. The emphasis is on graphics programming only though!
Why and when to combine SDL and OpenGL?
SDL is an excellent choice if you need platform independent 2d graphics. OpenGL is capable of 2d graphics, too, but why using the more complicated library if you could use the easy to use SDL library? – And by the way, underneath SDL is actually using OpenGL (or similar libraries depending upon the system) to achieve its hardware accelerated 2d graphics.
However, if your project needs 3d graphics, which isn’t covered by SDL, you can set up your system for this quite easy with SDL. The setup of an OpenGL environment is very easy and platform independent with SDL. Without SDL you would’ve to write different code to set up OpenGL for each operating system. In fact, even professional developers use SDL to set up their OpenGL applications.
Furthermore, since OpenGL is a pure graphics library, any other task is further done by SDL (e.g. keyboard handling, sound,…).
At this point I’d like to quote Klaus Vor der Landwehr (professional developer) from Turtle-Games, who described the relation of SDL and OpenGL in a very clear way.
Although the graphics are often in the foreground, it is for me as a game programmer only one aspect of many with which I have to deal. And the graphics do not even require the most work. OpenAL for example costs much more time and effort if you want to build a 3D sound channel management. And there are many other interfaces. Here is a list of categories in which SDL has been a great help:
As of version 2.0 of OpenGL, the so-called fixed pipeline has been replaced by a programmable pipeline (modern OpenGL). In general, the pipeline makes your input data appear on the screen in a hardware accelerated manner by using your graphics card. For the fixed pipeline it was easy to draw something to the screen but, as the name suggests, it was quite fixed and unflexible. The programmable pipeline which is controlled by a newly introduced shader (script) language is far more flexible, though, the ease is gone :-D.
Anyway, some people refer to OpenGL version 3.0 and up as modern OpenGL. This is because a lot of typical functionality was deprecated as of this version. The backwards compatibility is gone.
In this chapter I will demonstrate how to use SDL 3.0 and up to set up a modern OpenGL environment using some basic shaders. I based the description heavily on an excellent C++ tutorial over at opengl-tutorial.org and their second chapter. You may look for further OpenGL stuff there or have a look at this WikiBook OpenGL Introduction (C++). I’m not aware of OpenGL tutorials for Free Pascal or Delphi treating modern OpenGL (let me know if you know).
OpenGL Pascal units (headers)
Similar to SDL 2.0, you need specific units which translate and connect your code to the OpenGL library. There is a native OpenGL unit GL which covers the main functionality of OpenGL. Additionally for modern OpenGL you need the unit GLext which covers the functionality up to OpenGL version 4.0. These units are shipped right along with the Free Pascal compiler.
In case you are interested in support of OpenGL version 4.4, you should look into the dglOpenGL.pas. This unit is not shipped natively along with the Free Pascal compiler.
Let the fun begin
Let’s have a look at the code:
program chap10_SDL2;
uses Classes, SysUtils, SDL2, GL, GLext;
const
vertexShaderFile = 'VertexShader.txt';
fragmentShaderFile = 'FragmentShader.txt';
triangleData: array[0..8] of GLfloat = ( -1.0, -1.0, 0.0,
1.0, -1.0, 0.0,
0.0, 1.0, 0.0 );
var
sdlWindow1: PSDL_Window;
sdlGLContext1: TSDL_GLContext;
i: Word;
VertexArrayID: GLuint;
triangleVBO: GLuint;
VertexShaderID: GLuint;
VertexShaderCode: PGLchar;
FragmentShaderID: GLuint;
FragmentShaderCode: PGLchar;
ShaderCode: TStringList;
ProgramID: GLuint;
compilationResult: GLint = GL_FALSE;
InfoLogLength: GLint;
ErrorMessageArray: array of GLChar;
begin
if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;
//get an OpenGL window and create OpenGL context
sdlWindow1 := SDL_CreateWindow( 'OpenGL window', 50, 50, 500, 500, SDL_WINDOW_OPENGL );
if sdlWindow1 = nil then HALT;
sdlGLContext1 := SDL_GL_CreateContext( sdlWindow1 );
if @sdlGLContext1 = nil then HALT;
//init OpenGL and load extensions
if Load_GL_VERSION_4_0 = false then
if Load_GL_VERSION_3_3 = false then
if Load_GL_VERSION_3_2 = false then
if Load_GL_VERSION_3_0 = false then
begin
writeln(' ERROR: OpenGL 3.0 or higher needed. '); readln;
HALT;
end;
//print out OpenGL vendor, version and shader version
writeln( 'Vendor: ' + glGetString( GL_VENDOR ) );
writeln( 'OpenGL Version: ' + glGetString( GL_VERSION ) );
writeln( 'Shader Version: ' + glGetString( GL_SHADING_LANGUAGE_VERSION ) );
//create Vertex Array Object (VAO)
glGenVertexArrays( 1, @VertexArrayID );
glBindVertexArray( VertexArrayID );
//creating Vertex Buffer Object (VBO)
glGenBuffers( 1, @triangleVBO );
glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
glBufferData( GL_ARRAY_BUFFER, SizeOf( triangleData ), @triangleData, GL_STATIC_DRAW );
//creating shaders
VertexShaderID := glCreateShader( GL_VERTEX_SHADER );
FragmentShaderID := glCreateShader( GL_FRAGMENT_SHADER );
//load shader code and get PChars
ShaderCode := TStringList.Create;
ShaderCode.LoadFromFile( VertexShaderFile );
VertexShaderCode := ShaderCode.GetText;
if VertexShaderCode = nil then HALT;
ShaderCode.LoadFromFile( FragmentShaderFile );
FragmentShaderCode := ShaderCode.GetText;
if FragmentShaderCode = nil then HALT;
ShaderCode.Free;
//compiling and error checking vertex shader
write('Compiling and error checking Vertex Shader... ' );
glShaderSource( VertexShaderID, 1, @VertexShaderCode, nil );
glCompileShader( VertexShaderID );
glGetShaderiv( VertexShaderID, GL_COMPILE_STATUS, @compilationResult );
glGetShaderiv( VertexShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
//compiling and error checking fragment shader
write('Compiling and error checking Fragment Shader... ' );
glShaderSource( FragmentShaderID, 1, @FragmentShaderCode, nil );
glCompileShader( FragmentShaderID );
glGetShaderiv( FragmentShaderID, GL_COMPILE_STATUS, @compilationResult );
glGetShaderiv( FragmentShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
//creating and linking program
write('Creating and linking program... ' );
ProgramID := glCreateProgram();
glAttachShader( ProgramID, VertexShaderID );
glAttachShader( ProgramID, FragmentShaderID );
glLinkProgram( ProgramID );
glGetShaderiv( ProgramID, GL_LINK_STATUS, @compilationResult );
glGetShaderiv( ProgramID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
for i := 0 to 400 do
begin
glClearColor( 0.0, 1.0-i/400, 0.0+i/400, 1.0 );
glClear( GL_COLOR_BUFFER_BIT );
glUseProgram( ProgramID );
glEnableVertexAttribArray( 0 );
glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nil );
glDrawArrays( GL_TRIANGLES, 0, 3 );
glDisableVertexAttribArray( 0 );
SDL_Delay( 20 );
SDL_GL_SwapWindow( sdlWindow1 );
end;
//clean up
glDetachShader( ProgramID, VertexShaderID );
glDetachShader( ProgramID, FragmentShaderID );
glDeleteShader( VertexShaderID );
glDeleteShader( FragmentShaderID );
glDeleteProgram( ProgramID );
StrDispose( VertexShaderCode );
StrDispose( FragmentShaderCode );
glDeleteBuffers( 1, @triangleVBO );
glDeleteVertexArrays( 1, @VertexArrayID );
SDL_GL_DeleteContext( sdlGLContext1 );
SDL_DestroyWindow( sdlWindow1 );
SDL_Quit;
end.
Wow that is a lot of code. What you will get is this:
The background will change slowly from green to blue. And you will get this:
The vendor, OpenGL version and shader version information will be different according to your system. Also, if your system doen’t support the needed OpenGL version you’ll not have “success” but rather “failure” after the compiling and linking processes. Additional information may be shown then.
The program is called “chap10_SDL2” for obvious reason.
Additionally to the SDL2 unit we load the native FPC units Classes (for TStringList support), SysUtils (for PChar functions) and GL and GLext for OpenGL support.
Thre are three constants declared. The first two are defined as the filenames of the so-called shader source files. Basically they are simple text files which contain a script. More about shaders and the script later. The third is an array of nine GLfloat values. GLfloat is the OpenGL float variable type which in fact is translated as Pascal’s Single type. In short, these nine values describe three points in 3d space which, if connected, form a triangle. More about this later.
The first variable “sdlWindow1” is well known from previous chapters. Any variable to follow is new though. Most of them are related to OpenGL.
“sdlGLContext1” is of type TSDL_GLContext needed to create a so-called OpenGL context. In fact, this variable type is provided by SDL and a key type to set up an OpenGL conext in a simple and cross-platform manner.
The variable “i” is a simple Word variable for counting purposes.
OpenGL’s Integers and Strings
Most of the following variables are either of type GLuint or of type PGLchar. The last variable is an dynamic array of GLchars. Their specific meaning will be discussed later but GLuint is the OpenGL unsigned integer type (no negative values) which translates to Pascal’s Cardinal/Longword type. Text handling in OpenGL works by null-terminated strings of type PGLchar which translate to Pascal’s PChar. GLchar translates to Char then, obviously.
At this point you may wonder why as for SDL the null-terminated strings are used instead of simple strings (see Chapter 7 for the PAnsiChar variable type discussion). The answer again is that OpenGL is based upon C which handles strings this way. PChar equals PAnsiChar by the way.
The remaining variables “ShaderCode” of type TStringList will be used to handle the shader text files. “compilationResult” and “InfoLogLength” are of GLint type. In contrast to GLuint they allow for negative values.
begin
if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;
//get an OpenGL window and create OpenGL context
sdlWindow1 := SDL_CreateWindow( 'OpenGL window', 50, 50, 500, 500, SDL_WINDOW_OPENGL );
if sdlWindow1 = nil then HALT;
sdlGLContext1 := SDL_GL_CreateContext( sdlWindow1 );
if @sdlGLContext1 = nil then HALT;
//init OpenGL and load extensions
if Load_GL_VERSION_4_0 = false then
if Load_GL_VERSION_3_3 = false then
if Load_GL_VERSION_3_2 = false then
if Load_GL_VERSION_3_0 = false then
begin
writeln(' ERROR: OpenGL 3.0 or higher needed. '); readln;
HALT;
end;
//print out OpenGL vendor, version and shader version
writeln( 'Vendor: ' + glGetString( GL_VENDOR ) );
writeln( 'OpenGL Version: ' + glGetString( GL_VERSION ) );
writeln( 'Shader Version: ' + glGetString( GL_SHADING_LANGUAGE_VERSION ) );
First SDL2 is initilized as known. “sdlWindow1” is created as known by SDL_CreateWindow. Be careful though, in order to work with OpenGL the flag SDL_WINDOW_OPENGL has to be set!
SDL 2.0 and the OpenGL context
An OpenGL context is kind of an abstract name. It doesn’t represents just a window, even though it is created from a SDL2 window, but rather it contains everything (including the window information) that is related to this OpenGL context. The OpenGL context is therefore kind of “broader” than just a window, that is why it is called context rather than just a OpenGL window.
The function to create an OpenGL context from a SDL2 window is:
function SDL_GL_CreateContext(window: PSDL_Window): TSDL_GLContext
So, it is simple as that, just use the SDL2 window as argument and voila, you’ll get a OpenGL context, platform-independent. That is why everybody loves SDL2 to work with OpenGL. Note that the returned Context isn’t a pointer but an actual instance. So to error check against nil you need to refer to the instance’s addresse by the @ operator.
OpenGL version check and initialization
The nested if-then-statements check if at least version 3.0 of OpenGL is installed. If so, the highest available version is loaded. If not, the program is stopped and returns a text message.
If your hardware doen’t support OpenGL 3.0 or higher you should try to update your graphics driver. There is a good chance that you are able to use OpenGL 3.0 or higher then. Anyway, if the upgrade doesn’t work out or you wouldn’t want to update, you may have a look into the JEDI-SDL Chapter about OpenGL, there the old OpenGL is treated (although that chapter treats SDL 1.2, it shouldn’t be too hard to make it work with SDL 2.0 with minor changes).
Next there are three text messages printed out. These present the Vendor, the OpenGL version and the Shading language version. To get them in a readable form you need to transform the constants into strings by function glGetString. Let’s have a look at the command window again:
Have a look at the first three lines and you see what it could look like.
If you are new to OpenGL, OpenGL as kind of machine with many switches. Each switch
Briefly, a Vertex Array Object (VAO) is a specific OpenGL object which contains important settings (e.g. format of vertex data) and references to other objects, including Vertex Buffer Objects (VBO). Notice, it doesn’t store the object’s data (content) itself, it just stores the reference to these objects.
The Vertex Buffer Object (VBO) contains the actual data (content). In the example case these are three vertices, each described by three float point values in cartesian space.
OpenGL Object name or ID
Because it is important to understand, in contrast to SDL where objects usually are directly submitted to a function by its pointer reference, in OpenGL you have a so-called OpenGL Object name, which actually is an integer value of GLuint type. Therefore ID is a suitable name, too. Let’s see how this works:
The VAO is created by function glGenVertexArrays( number of VAO names, pointer to VAO names array ). The first parameter determines how many VAO names I’d like to create. We just need 1. The second parameter asks for a pointer to an array of VAO names. Since VAO names are just simple GLuints, it is a simple array of GLuints. Anyway, since we just need one, a pointer to a simple GLuint variable will be suitable, too. In our case that is “VertexArrayID”. To bind (“activate”) the corresponding VAO to the OpenGL context, the function glBindVertexArray( name of VAO ) is used. The argument is the name of the VAO we just created in “VertexArrayID”.
Similar to the VAO, the VBO is created by function glGenBuffers( number of VBO names, pointer to VBO names array ). Again, we just need 1 VBO whose name should be returned to “triangleVBO”. This variable just stores an ID (object name) of GLuint type.
From the naming of “triangleVBO” it is clear to us what we intent here (representing a triangle by three vertices), anyway, how should OpenGL know? – We explain the meaning of this buffer object to OpenGL by using glBindBuffer ( target, VBO name ). There are numerous options as target but GL_VERTEX_BUFFER is the right choice here.
The actual VBO is created by glBufferData( target, size of object’s data store in bytes, pointer to data to be copied into VBO, expected usage ). This functions takes four arguments. The target is GL_VERTEX_BUFFER again. The size of the VBO’s data store in bytes is determined by Pascal’s SizeOf function applied to “triangleData”. The “triangleData” constant also holds the data to be copied into the VBO, so its pointer is suitable as the third argument. Since we are not going to change the data a lot, we should use GL_STATIC_DRAW as fourth argument.
If you are a newcomer to OpenGL, don’t worry if you are confused the first time. Most people are. And now it may even get worse :-(.
Shaders and OpenGL Shading Language
When starting with modern OpenGL the so-called Shaders are talked about a lot. Shaders are scripts written in a C-like script language called OpenGL Shading Language (GLSL). These scripts are compiled at runtime and influence the way how the graphics data is processed at certain steps in the so-called rendering pipeline of OpenGL. In fact, you can create rather complex and special effects with shaders without even changing one line of code of your source code.
Vertex Shader and Fragment Shader
There are two Shaders that are crucial and have to be set up to work with modern OpenGL. They are called Vertex Shader and Fragment Shader. There are more Shaders not covered here, though. Each type of Shader influences different aspects of the rendering.
The Vertex Shader is the first Shader program executed in the rendering pipeline. Every vertex is “put through” the Vertex Shader program and processed accordingly, hence the name. Often this Shader is used to perform transformation operations. The Shader script used in this tutorial is shown next:
This GLSL source code is saved into a file VertexShader.txt and located in the same directory as the source code of this chapter’s example source code. I’m not going to explain this GLSL code in detail here, but a detailed explanation is found over at opengl-tutorial.org Chapter 2 where I got this Shader code from, by the way.
The Frgament Shader is the last Shader program executed in the renderin pipeline. The so-called rasterization process produces fragments. Every fragment is “put through” the Fragment Shader program and processed accordingly. The Shader script used for the Fragment Shader is:
#version 330 core
out vec3 color;
void main(){
color = vec3(1,0,0);
}
This code is in file FragmentShader.txt and located in the same directory as the VertexShader.txt. The detailed explanation is found over at opengl-tutorial.org Chapter 2 again. Anyway, you’ll notice that there is a “color” variable (three component vector). As you see, it sets the (red,green,blue) values for the fragments to (1,0,0) which means red should be the result, red = 100%, green and blue = 0%. You may play around with these values.
//creating shaders
VertexShaderID := glCreateShader( GL_VERTEX_SHADER );
FragmentShaderID := glCreateShader( GL_FRAGMENT_SHADER );
//load shader code and get PChars
ShaderCode := TStringList.Create;
ShaderCode.LoadFromFile( VertexShaderFile );
VertexShaderCode := ShaderCode.GetText;
if VertexShaderCode = nil then HALT;
ShaderCode.LoadFromFile( FragmentShaderFile );
FragmentShaderCode := ShaderCode.GetText;
if FragmentShaderCode = nil then HALT;
ShaderCode.Free;
Both Shaders are created by function glCreateShader( Shader type ). It returns the reference (or name) as GLuint as seen before for the VAO and VBO. We store them in the VertexShaderID and FragmenShaderID, respecitvely.
The next part is about loading the source code from the two Shader files (VertexShader.txt, FragmentShader.txt) and converting them to be used with OpenGL. First a “ShaderCode” variable of TStringList type is created. Its LoadFromFile method let us load the file contents into the variable conveniently. First for the Vertex Shader, whose file name is stored in constant “VertexShaderFile”. The variable “VertexShaderCode” is of type PGLchar, which is the way OpenGL handles strings. Since PGLchar is of type PChar anyway, the method GetText is perfectly suitable here to convert the source code string into a null-terminated array of chars. Finally, there is a simple check if the PGLchars are empty (nil), which shouldn’t be the case if the source code is pointed to as expected.
Exactly the same is done for the FragmentShader and the source code associated with “FragmentShaderCode”.
Finally, the dummy variable “ShaderCode” is free’d.
//compiling and error checking vertex shader
write('Compiling and error checking Vertex Shader... ' );
glShaderSource( VertexShaderID, 1, @VertexShaderCode, nil );
glCompileShader( VertexShaderID );
glGetShaderiv( VertexShaderID, GL_COMPILE_STATUS, @compilationResult );
glGetShaderiv( VertexShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
//compiling and error checking fragment shader
write('Compiling and error checking Fragment Shader... ' );
glShaderSource( FragmentShaderID, 1, @FragmentShaderCode, nil );
glCompileShader( FragmentShaderID );
glGetShaderiv( FragmentShaderID, GL_COMPILE_STATUS, @compilationResult );
glGetShaderiv( FragmentShaderID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
To associate the source code we stored in “VertexSourceCode” to “VertexShaderID” of GLuint type, the function glShaderSource( Shader reference, number of array elements, pointer to array of source code PGLchars, pointer to array of lengths ). The Vertex Shader reference is stored in “VertexShaderID” which is the first argument. We just have one source code, so the second argument is 1. The source code is stored VertexShaderCode, and its pointer is addressed by @VertexShaderCode as the third argument. As seen before, since we just have one element here, it is not necessary to have really an array. The fourth parameter allows for some length specification, but if set to nil it expects null-terminated arrays of chars.
The compilation is straight forward done by glCompileShader( Shader reference ). It is really advised to to error checking here, that is why it is shown how to do that. The function glGetShaderiv( Shader reference, object parameter, pointer of correct type for return value ) is used to request information about objects. First we like to know if the compilation was successful. The Shader reference is stored in “VertexShaderID”, the object parameter is GL_COMPILE_STATUS. This will return a GLint value, which can be interpreted as GL_FALSE or GL_TRUE. The result is stored in “compilationResult” by using its pointer (@compilationResult) as argument.
Right after that we request the length of the information log by GL_INFO_LOG_LENGTH. It will be greater than 0 if some information were logged (probably an error occured on compilation then). The result is returned to “InfoLogLength” by its pointer @InfoLogLength.
If an error occurs, “compilationResult” is GL_FALSE. In this case “failure” along with more specific information is printed out. I’m not going into detail here, since this shouldn’t happen. Otherwise (and that should be the case), “success” is printed out.
The very same way the Fragment Shader is compiled and checked.
//creating and linking program
write('Creating and linking program... ' );
ProgramID := glCreateProgram();
glAttachShader( ProgramID, VertexShaderID );
glAttachShader( ProgramID, FragmentShaderID );
glLinkProgram( ProgramID );
glGetShaderiv( ProgramID, GL_LINK_STATUS, @compilationResult );
glGetShaderiv( ProgramID, GL_INFO_LOG_LENGTH, @InfoLogLength );
if compilationResult = GL_FALSE then
begin
writeln( 'failure' );
SetLength( ErrorMessageArray, InfoLogLength+1 );
glGetShaderInfoLog( VertexShaderID, InfoLogLength, nil, @ErrorMessageArray[0] );
for i := 0 to InfoLogLength do write( String( ErrorMessageArray[i] ) );
writeln;
end else writeln( 'success' );
The shaders have to be attached and linked by a Shader program. A Shader program is created by glCreateProgram(). The parenthesis are important here. It returns an reference of GLuint type which is stored in ProgramID.
The Shaders are attached to this Shader program by glAttacheShader( Program reference, Shader reference ). The program is linked by glLinkProgram( Program reference ). The reference for the Shader program is “ProgramID”. The references for the Shaders are “VertexShaderID” and “FragmentShaderID”, respectively.
By complete analogy to the error checking for the Shader compilation, the Shader program linking is checked. Anyway, instead of GL_COMPILE_STATUS, GL_LINK_STATUS is used.
for i := 0 to 400 do
begin
glClearColor( 0.0, 1.0-i/400, 0.0+i/400, 1.0 );
glClear( GL_COLOR_BUFFER_BIT );
glUseProgram( ProgramID );
glEnableVertexAttribArray( 0 );
glBindBuffer( GL_ARRAY_BUFFER, triangleVBO );
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, nil );
glDrawArrays( GL_TRIANGLES, 0, 3 );
glDisableVertexAttribArray( 0 );
SDL_Delay( 20 );
SDL_GL_SwapWindow( sdlWindow1 );
end;
The for-loop counts from 0 to 400. Within each cycle it first changes the background color by glClearColor( red, green, blue, alpha ). Red and alpha are constant, green and blue are varied each cycle dependend upon variable i. This makes the background slowly changing from green to blue, feel free to play around with the rgba values. To actually clear the color buffer glClear( buffer ) with GL_COLOR_BUFFER_BIT as argument is used.
glUseProgram( Shader program reference ) is used to apply the Shader program to the rendering state. “ProgramID” is the Shader program reference in the example code.
glEnableVertexAttribArray( array index ) is used in order to make the attribute array available for rendering by glDrawArrays. The index is 0 here. The “triangleVBO” is bound by glBindBuffer( target, buffer ) to the GL_BUFFER_ARRAY target to change attribute data of said VBO. Latter is done by glVertexAttribPointer( index, size, type, normalized, stride, offset of first component ) with the given arguments. Hence, the index is 0, 3 components per generic vertex attribute, each of float point type (thus, GL_FLOAT), not normalized (thus GL_FALSE), no strides between vertex attributes, no offset for the first component.
The rendering is done by glDrawArrays( type of primitive, starting index, number of elements ). The type of primitive is a triangle, hence GL_TRIANGLES is the first argument. We start at the very beginning, so index is 0. We have 3 sequential elements (vertices).
glDisableVertexAttribArray( array index ) is the counter function to glEnableVertexAttribArray( array index ), obviously. It disables the vertex attribute array.
SDL_Delay delays the loop by 20 milliseconds.
The procedure
procedure SDL_GL_SwapWindow(window: PSDL_Window)
is used to actually display the rendering result to the the window “sdlWindow1”. Keep in mind that this window has to be initialized as an OpenGL window. This procedure is comparable to SDL’s SDL_RenderPresent.
For the clean up, the shaders have to be detached from the shader program by glDetachShader( program, shader ). After that they can be deleted by glDeleteShader( shader ), and the program by glDeleteProgram( program ).
The shader script PChars are disposed by StrDispose( PChar ).
The VBO and the vertex array have to be free’d by glDeleteBuffers( number of buffer objects, pointer to array of buffers ) and glDeleteVertexArrays( number of VAOs, pointer to array of VAOs ) respectively. The first parameter is the number of objects to be deleted, which is 1 in both our cases.
This chapter will introduce you on how to load music and sounds since these are key features of every game and many applications.
SDL_mixer 2.0 for easy music and sound support
Although SDL 2.0 supports music and sound handling natively, there is an easier way to play music and sound files. The official unit SDL_mixer 2.0 (unit’s name SDL2_mixer) has been created exactly for this purpose and is maintained by the same authors (Sam Lantinga, Stephane Peter, Ryan Gordon) as SDL 2.0 itself. The Pascal translation is fortunately available, too in Tim Blume’s header translations.
A simple bitmap image file which displays the menu options for the tutorial program.
You should extract the zip-file and get several files. These are four license text files and a readme text file, furthermore you have eight dll files including the important SDL2_mixer.dll. The other dll files are necessary to play the different sound and music formats. Copy all the files to the Windows system32-folder (or the corresponding place). If you forget this and run the examples below you will get an error with exitcode = 309.
Now that you are prepared, let’s have an overview of the code.
program SDL_MusicSound;
uses SDL2, SDL2_mixer;
var
sdlWindow: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface: PSDL_Surface;
sdlTexture: PSDL_Texture;
sdlKeyboardState: PUInt8;
Music: PMix_Music;
Sound: PMix_Chunk;
Run: Boolean = True;
begin
if SDL_Init(SDL_INIT_VIDEO or SDL_INIT_AUDIO) < 0 then Exit;
// Get window and renderer
if SDL_CreateWindowAndRenderer(
640, 640, SDL_WINDOW_SHOWN, @sdlWindow,@sdlRenderer) <> 0 then Exit;
// Create and render menu texture
sdlSurface := SDL_LoadBMP('music-menu.bmp');
if sdlSurface = nil then Exit;
sdlTexture := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface);
if sdlTexture = nil then Exit;
if SDL_RenderCopy(sdlRenderer, sdlTexture, nil, nil) <> 0 then Exit;
SDL_RenderPresent(sdlRenderer);
// Prepare mixer
if Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
MIX_DEFAULT_CHANNELS, 4096) < 0 then Exit;
// Load music
Music := Mix_LoadMUS('In-my-mind-1.ogg');
if Music = nil then Exit;
Mix_VolumeMusic(MIX_MAX_VOLUME);
// Load sound
Sound := Mix_LoadWAV('dial-1.wav');
if Sound = nil then Exit;
Mix_VolumeChunk(Sound, MIX_MAX_VOLUME);
while Run = True do
begin
SDL_PumpEvents;
sdlKeyboardState := SDL_GetKeyboardState(nil);
// ESC pressed
if sdlKeyboardState[SDL_SCANCODE_ESCAPE] = 1 then
Run := False;
// Music effect keys
if sdlKeyboardState[SDL_SCANCODE_1] = 1 then
if Mix_PlayMusic(Music, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_2] = 1 then Mix_PauseMusic;
if sdlKeyboardState[SDL_SCANCODE_3] = 1 then Mix_ResumeMusic;
if sdlKeyboardState[SDL_SCANCODE_4] = 1 then Mix_RewindMusic;
if sdlKeyboardState[SDL_SCANCODE_5] = 1 then
if Mix_FadeInMusic(Music, 10, 3000) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_6] = 1 then
if Mix_FadeOutMusic(3000) = 0 then Writeln(SDL_GetError);
// Sound effect keys
if sdlKeyboardState[SDL_SCANCODE_7] = 1 then
if Mix_PlayChannel(1, Sound, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_8] = 1 then
if Mix_PlayChannel(-1, Sound, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_9] = 1 then Mix_Pause(-1);
if sdlKeyboardState[SDL_SCANCODE_0] = 1 then Mix_Resume(-1);
if sdlKeyboardState[SDL_SCANCODE_A] = 1 then
if Mix_FadeInChannel(1, sound, 0, 2000) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_S] = 1 then
if Mix_FadeOutChannel(1, 1000) < 0 then Writeln(SDL_GetError);
// Channel effect keys
if sdlKeyboardState[SDL_SCANCODE_G] = 1 then
if Mix_SetPanning( 1, 255, 32 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_H] = 1 then
if Mix_SetPanning( 1, 255, 255 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_J] = 1 then
if Mix_SetDistance( 1, 223 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_K] = 1 then
if Mix_SetDistance( 1, 0 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_L] = 1 then
if Mix_SetPosition( 1, 45, 127 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_M] = 1 then
if Mix_SetPosition( 1, 0, 0 ) = 0 then Writeln(SDL_GetError);
end;
// Clean up
if Assigned(sdlSurface) then SDL_FreeSurface(sdlSurface);
if Assigned(sdlTexture) then SDL_DestroyTexture(sdlTexture);
if Assigned(sdlRenderer) then SDL_DestroyRenderer(sdlRenderer);
if Assigned(sdlWindow) then SDL_DestroyWindow(sdlWindow);
if Assigned(Music) then Mix_FreeMusic(Music);
if Assigned(Sound) then Mix_FreeChunk(Sound);
Mix_CloseAudio;
SDL_Quit;
end.
When running this program, you’ll get a window with a menue. For this to work, make sure to download the music-menu.bmp file and place it in the program folder.
Let’s start with the first part of the code.
program SDL_MusicSound;
uses SDL2, SDL2_mixer;
var
sdlWindow: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlSurface: PSDL_Surface;
sdlTexture: PSDL_Texture;
sdlKeyboardState: PUInt8;
Music: PMix_Music;
Sound: PMix_Chunk;
Run: Boolean = True;
begin
if SDL_Init(SDL_INIT_VIDEO or SDL_INIT_AUDIO) < 0 then Exit;
// Get window and renderer
if SDL_CreateWindowAndRenderer(
640, 640, SDL_WINDOW_SHOWN, @sdlWindow,@sdlRenderer) <> 0 then Exit;
// Create and render menu texture
sdlSurface := SDL_LoadBMP('music-menu.bmp');
if sdlSurface = nil then Exit;
sdlTexture := SDL_CreateTextureFromSurface(sdlRenderer, sdlSurface);
if sdlTexture = nil then Exit;
if SDL_RenderCopy(sdlRenderer, sdlTexture, nil, nil) <> 0 then Exit;
SDL_RenderPresent(sdlRenderer);
The program is called “SDL”. It uses the SDL2 and the new SDL2_mixer unit.
We’re going to use event handling as discussed in Chapter – Keyboard State and Key States to check what keys have been pressed. Consequently we need a variable “sdlWindow” for the SDL 2.0 window and a keyboard state variable “sdlKeyboardState”.
The pointer variable “Music” is of the new type PMix_Music and points at the loaded music file data later. The “Sound” pointer variable is of type PMix_Chunk and points at the loaded sound file data later. Note: Each song is referenced by an own PMix_Music pointer and each sound effect (e.g. explosions, shots, …) is referenced by an own PMix_Chunk pointer.
What is the difference between music and sound?
Music is associated with PMix_Music pointers and sounds are associated with PMix_Chunk pointers, but why distinguish them at all? – Well, it is convenient to have them separated since they have rather different properties. Music is usually several minutes long, sounds are usually just a few seconds short. It is usually just one song played at a time, sounds need to mix up if necessary. For music it usually doesn’t plays a role if it is sligthly delayed to when it should start, for sounds a larger delay usually means a strange feeling for the player. So, for music there is exactly one channel reserved where the music is played. For sounds eight sound channels are available to make it possible for them to mix up. Now let’s get back to the code.
The variable “Run” is a boolean variable to determine when the main loop of the program should be exited.
The program is initialized by SDL_Init as known. Note though that not only SDL_INIT_VIDEO but additionally SDL_INIT_AUDIO is set. They are combined by the or operator (not the and operator :-)!). The window is set up afterwards by SDL_CreateWindow as known. Also the menu texture file is created and renderered as known. Check Chapter – Loading and rendering a Bitmap file as a reminder, in case.
// Prepare mixer
if Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
MIX_DEFAULT_CHANNELS, 4096) < 0 then Exit;
// Load music
Music := Mix_LoadMUS('In-my-mind-1.ogg');
if Music = nil then Exit;
Mix_VolumeMusic(MIX_MAX_VOLUME);
// Load sound
Sound := Mix_LoadWAV('dial-1.wav');
if Sound = nil then Exit;
Mix_VolumeChunk(Sound, MIX_MAX_VOLUME);
To initialize SDL2_mixer
function Mix_OpenAudio(frequency: cint; format: cuint16; channels: cint; chunksize: cint): cint
has to be called. It requests four parameters. These are the frequency, the audio format, the channels (mono or audio) and the chunksize. All these parameters are of Integer type. Function Mix_OpenAudio returns 0 on success and -1 on errors.
The frequency in Hertz (1/s) usually is 22050 Hz or MIX_DEFAULT_FREQUENCY in games but double as high for CD quality sound (44100 Hz). The higher the frequency, the more CPU power is needed.
Next we have to define the audio format. It determines how the audio data is stored. MIX_DEFAULT_FORMAT equals AUDIO_S16SYS. Different values are given in the following list (from official SDL2_mixer documentation):
Format constant name Description
AUDIO_U8 Unsigned 8-bit samples
AUDIO_S8 Signed 8-bit samples
AUDIO_U16LSB Unsigned 16-bit samples, in little-endian byte order
AUDIO_S16LSB Signed 16-bit samples, in little-endian byte order
AUDIO_U16MSB Unsigned 16-bit samples, in big-endian byte order
AUDIO_S16MSB Signed 16-bit samples, in big-endian byte order
AUDIO_U16 same as AUDIO_U16LSB (for backwards compatability probably)
AUDIO_S16 same as AUDIO_S16LSB (for backwards compatability probably)
AUDIO_U16SYS Unsigned 16-bit samples, in system byte order
AUDIO_S16SYS Signed 16-bit samples, in system byte order
Then we decide for a sound channel type, which means either stereo or mono. For stereo sound we choose 2 and for mono we choose 1 as value. MIX_DEFAULT_CHANNELS equals stereo output.
Finally the chunksize has to be set, where 4096 bytes per sample is a good and default value. Too low values may lead to skipping samples. Too high values may lead to delayed playing.
Loading music and sounds from files in SDL 2.0
We will load the music by
function Mix_LoadMUS(_file: PAnsiChar): PMix_Music.
_file can be the absolute path. If the Pascal file and the music file are in the same folder the file name is sufficient (as shown in the example). If the loading was unsuccessful, the function returns nil. The volume can be set by
function Mix_VolumeMusic(volume: cint): cint.
The volume can be set between 0 (silence) to 128 (maximum volume). The latter equals MIX_MAX_VOLUME. By the way, if you set -1 as argument, the return value corresponds to the set volume.
Quite similar sound files are loaded by
function Mix_LoadWAV(_file: PAnsiChar): PMix_Chunk.
Although the function’s name doesn’t indicate this, use this function to load the sound files of any format (Wave, Aiff, Riff, Ogg, Voc), not only for .wav sound files. It return nil on error. There is alittle difference when setting the volume by
function Mix_VolumeChunk(chunk: PMix_Chunk; volume: cint): cint.
Instead of just setting a general volume, the volume is bound to a certain sound. In the example case the sound is in variable “sound”. Just to mention it here, there is a third possibility, to set the volume for a certain channel:
function Mix_Volume(channel: cint; volume: cint): cint.
This can be quite handy, anyway, channels are discussed a little bit later below. If your argument is -1 the average volume will be returned.
Playing, pausing, resuming music and sounds
while Run = True do
begin
SDL_PumpEvents;
sdlKeyboardState := SDL_GetKeyboardState(nil);
// ESC pressed
if sdlKeyboardState[SDL_SCANCODE_ESCAPE] = 1 then
Run := False;
// Music effect keys
if sdlKeyboardState[SDL_SCANCODE_1] = 1 then
if Mix_PlayMusic(Music, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_2] = 1 then Mix_PauseMusic;
if sdlKeyboardState[SDL_SCANCODE_3] = 1 then Mix_ResumeMusic;
if sdlKeyboardState[SDL_SCANCODE_4] = 1 then Mix_RewindMusic;
if sdlKeyboardState[SDL_SCANCODE_5] = 1 then
if Mix_FadeInMusic(Music, 10, 3000) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_6] = 1 then
if Mix_FadeOutMusic(3000) = 0 then Writeln(SDL_GetError);
// Sound effect keys
if sdlKeyboardState[SDL_SCANCODE_7] = 1 then
if Mix_PlayChannel(1, Sound, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_8] = 1 then
if Mix_PlayChannel(-1, Sound, 0) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_9] = 1 then Mix_Pause(-1);
if sdlKeyboardState[SDL_SCANCODE_0] = 1 then Mix_Resume(-1);
if sdlKeyboardState[SDL_SCANCODE_A] = 1 then
if Mix_FadeInChannel(1, sound, 0, 2000) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_S] = 1 then
if Mix_FadeOutChannel(1, 1000) < 0 then Writeln(SDL_GetError);
The main application loop is entered and should be left if variable “Run” is false which is achieved by pressing the ESCAPE key.
For each key state a different music or sound related function is used to do something. Let’s discuss them now in detail. We start with the functions triggered by the keys 1 – 4:
function Mix_PlayMusic(music: PMix_Music; loops: cint): cint
procedure Mix_PauseMusic
procedure Mix_ResumeMusic
procedure Mix_RewindMusic
Mix_PlayMusic plays music. The first argument is the music itself which we previously got from the example .ogg music file and associated with the “Music” pointer of PMix_Music type. The second argument determines how often the music is played. Additionally -1 means infinite repetition. This function returns 0 on success and -1 on error.
The meaning of the three procedures Mix_PauseMusic, Mix_ResumeMusic and Mix_RewindMusic ist obvious from their names. They are used to pause, resume (if paused before) and rewind the played music.
The analogous function and procedures for sound channels triggered by the keys 7 – 9 and 0 are shown now:
function Mix_PlayChannel(channel: cint; chunk: PMix_Chunk; loops: cint): cint
procedure Mix_Pause(channel: cint)
procedure Mix_Resume(channel: cint)
As you can see the function Mix_PlayChannel has three parameters. The first parameter asks for a channel with which the sound should be associated. SDL2_mixer provides eight sound channels. Usually you should use -1 which means the first free channel will be used to play the sound. Anyway, you could specifiy a certain channel here if you like. The second parameter asks for a sound pointer of PMix_Chunk type. The third parameter is the same as for music, -1 means an infinite loop. Attention though for specific repetitions. The value for loops is increased by 1. So, if you want a sound to be played once, you need the argument to be 0. The return value corresponds to the sound channel the sound is played on, it is -1 on error.
The procedures Mix_Pause and Mix_Resume are self-explanatory. Use them to pause or resume a certain channel, which is the argument. If the argument is -1 (as in the example code) you pause or resume all channels. By the way, you may have noticed that there is not a rewind analogue, because sounds are not expected to be rewound.
Fading effects
...
if sdlKeyboardState[SDL_SCANCODE_5] = 1 then
if Mix_FadeInMusic(Music, 10, 3000) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_6] = 1 then
if Mix_FadeOutMusic(3000) = 0 then Writeln(SDL_GetError);
// Sound effect keys
...
if sdlKeyboardState[SDL_SCANCODE_A] = 1 then
if Mix_FadeInChannel(1, sound, 0, 2000) < 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_S] = 1 then
if Mix_FadeOutChannel(1, 1000) < 0 then Writeln(SDL_GetError);
// Channel effect keys
if sdlKeyboardState[SDL_SCANCODE_G] = 1 then
if Mix_SetPanning( 1, 255, 32 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_H] = 1 then
if Mix_SetPanning( 1, 255, 255 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_J] = 1 then
if Mix_SetDistance( 1, 223 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_K] = 1 then
if Mix_SetDistance( 1, 0 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_L] = 1 then
if Mix_SetPosition( 1, 45, 127 ) = 0 then Writeln(SDL_GetError);
if sdlKeyboardState[SDL_SCANCODE_M] = 1 then
if Mix_SetPosition( 1, 0, 0 ) = 0 then Writeln(SDL_GetError);
end;
Important to know about effects like fading. They are performed on the fly to your data, so the more effects you use, the more computational power they require. It is important to unregister effects you don’t need anymore.
These are the four fade functions triggered by the keys 5, 6, A and S :
function Mix_FadeInMusic(music: PMix_Music; loops: cint; ms: cint): cint
function Mix_FadeOutMusic(ms: cint): Integer
function Mix_FadeInChannel(channel: cint; chunk: PMix_Chunk; loops: cint; ms: cint): cint
function Mix_FadeOutChannel(which: cint; ms: cint): cint
The function Mix_FadeInMusic works the very same way as Mix_PlayMusic above, but there is a further parameter which allows for a fade in effect in milliseconds. 3000 milliseconds equals 3 seconds. It returns 0 on success and -1 on error.
Mix_FadeOutMusic fades out the playing music within the given time in milliseconds in the moment the function is called (hence the key is pressed in the example program). Attention, it returns 1 on success and 0 on error.
Quite similar the functions Mix_FadeInChannel and Mix_FadeOutChannel work. Instead of the music pointer, the sound pointer and the channel has to be given. By the way, also here the number of times the sound is played is increased by 1, so if you want to play a sound with fade in effect once, the argument has to be 0. The fade effect applies only for the first run. Both functions return 0 on success and -1 on error.
Panning and distance effects
The functions triggered by the keys G – M are:
function Mix_SetPanning(channel: cint; left: cuint8; right: cuint8): cint
For setting the panning you need the channel to which panning should be applied and the volume values which may vary from 0 (quiet) to 255 (loudest). For a consistent panning effect you may substract the left value from the right value (left: volume, right: 255-volume). To unregister this effect, you should set both values to 255, which is recommended to be done if you don’t need panning anymore. The function returns 0 on errors.
function Mix_SetDistance(channel: cint; distance: cuint8): cint
The higher the distance, the quieter the sound appears. The value can range between 0 (near) and 255 (far). If distance is set to 0 the effect is unregistered. It returns 0 on errors.
function Mix_SetPosition(channel: cint; angle: cint16; distance: cuint8): cint
This function adjusts volumes according to an angle and a distance value, hence it combines the effects of the two functions discussed before for panning and distance. The distance value works the same way as discussed for Mix_SetDistance. Angle of 0 (degree) means in front of you, 90 means directly to the right, 180 means directly behind, 270 means directly to the left.
Closing the SDL2 mixer and clean up
// Clean up
if Assigned(sdlSurface) then SDL_FreeSurface(sdlSurface);
if Assigned(sdlTexture) then SDL_DestroyTexture(sdlTexture);
if Assigned(sdlRenderer) then SDL_DestroyRenderer(sdlRenderer);
if Assigned(sdlWindow) then SDL_DestroyWindow(sdlWindow);
if Assigned(Music) then Mix_FreeMusic(Music);
if Assigned(Sound) then Mix_FreeChunk(Sound);
Mix_CloseAudio;
SDL_Quit;
end.
For the clean up the surface, textre renderer and the window are destroyed as known. New are the procedures Mix_FreeMusic and Mix_FreeChunk to free the music and sound data.
procedure Mix_FreeMusic(music: PMix_Music)
procedure Mix_FreeChunk(chunk: PMix_Chunk)
The arguments have to be the music and sound pointer respectively.
SDL_mixer 2.0 has to be closed by
procedure Mix_CloseAudio.
Finally SDL 2.0 and the application are shut down as known.
SDL abbreviates Simple Direct Media Layer. In August 2013 the official successor of original SDL (SDL 1.2) has been released, simply called SDL2 (or SDL 2.0). It is a library to develop powerful applications for many different operating systems (platforms) by learning only one set of commands. This is called cross-platform development. The following platforms are officially supported by SDL2:
Windows
Mac OS X
Linux
iOS
Android.
Internally the SDL library layer translates your commands to the platform specific commands, which is reflected by the following diagram.
Image by Adriatikus at English Wikipedia [Public domain], Wikimedia SDL2 Layer Diagram
It’s especially meaningful to use the SDL library if you plan to develop games or need fast paced rendering. Jump and run-, Role-playing-, Real time/turn based strategy-, side-scrolling-, arcade-, board-, card-, simulation-, multi-user dungeon-, puzzle-, shooter-, network games and so on are possible, and any combination of these ;-).
Licensing
The SDL2 library, as well as the Pascal units for using SDL2, are licensed under the zlib license. This license grants a high degree of freedom. So even closed-source, commercial games are possible. As a sidenote, you are free to choose the MPL license for the units, if you like.
How to get SDL2 for Free Pascal or Object Pascal?
There are several SDL2 units out there (see comparison and discussion here) which provide bindings for Pascal to use SDL2, since originally SDL2 has been written in C. Over time it turned out that the PGD community SDL2 units are the best choice to use if you like to get into SDL2 development in Free Pascal or Object Pascal (Delphi). They are updated regularly.
A detailed installation instruction to set your system up for SDL2 development under Pascal is found in Chapter 2 for Windows or Linux. If you did install SDL2 for Free Pascal or Object Pascal already, proceed to Chapter 3. Please choose:
installation guide for SDL 2.0 under Microsoft Windows
installation of Free Pascal Compiler (FPC)
installation of Free Pascal IDE and Lazarus IDE
installation of SDL 2.0
configuration of FP IDE and Lazarus IDE to work with SDL 2.0
after this you are ready to code awesome SDL 2.0 stuff 🙂
Free Pascal IDE or Lazarus IDE
IDE stands for integrated development environment. Although you could write your programs in a simple text editor, an IDE makes your life much easier. For example, it highlights keywords, makes it easier for you to set up the compiler, and much more. Now let’s have a look at these two IDEs:
Free Pascal IDE
Lazarus IDE
You may notice that the native Free Pascal IDE (left) looks old, but it is shipped right along with the Free Pascal Compiler. In contrast to that the Lazarus IDE (right) looks up-to-date, but requires it to be installed additionally. My advise is nonetheless, use the Lazarus IDE.
The Free Pascal Compiler (FPC)
We need three different software packages. First of all we need the stand-alone Free Pascal Compiler or Lazaruse IDE (which already has a copy of the Free Pascal Compiler included). If you are not sure, I recommend to go with the Lazarus IDE package.
SDL Library and SDL 2.0 units
Additionaly we need the original SDL library files (.dll files). And finally we need units that connect our SDL 2.0 code written in Free Pascal to the SDL library files. The latter will be done by SDL 2.0 units.
This table provides all information and sources you need with respect to installation steps 1) – 3). Keep in mind, you have to download either the Lazarus IDE or the stand-alone Free Pascal Compiler, not both.
Software
Version
approx. Size [MB]
Filenames
Source link, Details and remarks
Lazarus IDE (has FPC included)
4.2 or later
lazarus-4.2-fpc-3.2.2-win32.exe or lazarus-4.2-fpc-3.2.2-win64.exe
SDL2-2.32.10-win32-x86.zip or SDL2-2.32.10-win32-x64.zip
https://github.com/libsdl-org/SDL/releases Scroll down until you find the last 2.x.x-release (e. g. 2.32.10). For SDL2 the first number has to be a 2 and no 3! Click on “Assets” and download the file.
Download Lazarus or the stand-alone Free Pascal Compiler (FPC)
If you have Free Pascal or the Lazarus IDE installed already, you can skip the steps 1a) and 1b).
1a)For Lazarus IDE: Download the latest version of the Lazarus IDE. It has the latest Free Pascal Compiler accompanied already, so you can skip step 1b).
1b) For Free Pascal IDE: 32 bit Windows: Download the latest stable Free Pascal compiler, version 3.2.2 or higher. 64 bit Windows: You have two choices, 1) Simple way: Go with the 32 bit version of Free Pascal. (Recommended), 2) Install the 64 bit version of Free Pascal on top of the 32 bit version of Free Pascal. In this case you need both files. Hence, you need to download both Free Pascal installers shown above.
Download SDL 2.0 and Header translation
2) Download the latest version of the SDL2-for-Pascal units as described in the screenshot below in three steps or simply via the releases page.
Choose the master branch (1), click on “Clone or download” (2) and click on Download ZIP (3).
3) Download the latest version of SDL 2.0 runtime library, version 2.32.10 or higher.
Install Lazarus or the Free Pascal Compiler
4a) For Lazarus: 32 and 64 bit Windows: Execute the lazarus-*.exe to install Lazarus for a 32 bit or 64 bit Windows system and follow the self-installer. Let the self-installer create a shortcut on your desktop.
4b) For Free Pascal IDE:32 bit Windows: Execute fpc-3.2.2.i386-win32.exe to install Free Pascal for a 32 bit or 64 bit Windows system. Let the self-installer create a shortcut on your desktop. Don’t modify any checked options during installation process. The default path is: C:\FPC\[Compiler version]\. [Compiler version] should be 3.2.2 or higher numbers. 64 bit Windows only: The installer will install FPC 32 bit and on top the 64 bit version automatically, execute fpc-3.2.2.win32.and.win64.exe for that.
Install SDL 2.0 and Header translation
5) Extract SDL2-2.32.10-win32-x86.zip or SDL2-2.32.10-win32-x64.zip to get the SDL runtime library. This leads to two extracted files. There is a text file and the very important SDL2.dll.
6) 32bit Windows: Copy those files (especially the SDL2.dll!) to your system32-folder, C:\WINDOWS\system32\. 64bit Windows: Copy those 64bit files to your system32-folder (!), C:\WINDOWS\system32\, Windows is expecting you to do this. Do not use the SysWoW64-folder (Hint: 32 bit files on 64 bit Windows go here).
If it is not possible to copy the files into this folder (or you are unsure about its location) alternatively you can copy the SDL2.dll right into the same folder where the SDL 2.0 application (.exe) is located. That is because SDL 2.0 first looks for the SDL2.dll in the folder from which it is executed and if it isn’t found there, in the corresponding system folder! Anyway, if it isn’t found anywhere, the program will raise an error.
Now you have installed Free Pascal and the SDL runtime library on your system. Finally SDL 2.0 units have to be installed.
7a) For Lazarus IDE: Extract the SDL2-for-Pascal-*.zip. The Free Pascal Compiler is located in a subfolder called “fpc”. I suggest to copy all the files to a folder with full path C:\Lazarus\fpc\[Compiler Version]\units\SDL2\. [Compiler version] should be 3.2.2 or higher numbers. Skip step 7b).
7b) For Free Pascal IDE: Extract the SDL2-for-Pascal-*.zip. I suggest to copy all the files to a folder with full path C:\FPC\[Compiler Version]\units\SDL2\. [Compiler version] should be 3.2.2 or higher numbers. All later chapters will assume you installed to this path.
Configuring the Lazarus IDE
Now the compiler has to be told where to find the new units.
8) Open the Free Pascal IDE (for example by clicking the shortcut on desktop).
9) In Lazarus the path to external units has to be set for each project. Create a project if no project is set up (Project > New Project …). In the menue choose the following item Project > Project Options …. Now a window should pop up.
10) Under “Compiler Options” choose “Paths” (see red mark). Choose the three dots button (see red mark on the right side) for the “Other units files (-Fu)” field (red mark). This makes the “Path Editor” pop up. In the area “Search paths:”, click on the yellow folder symbol in the right bottom corner. Choose the path to your SDL-units (e.g. C:\Lazarus\fpc\3.2.2\units\SDL2). Make sure the path leads to the folder, where the sdl2.pas file is located! This file contains all the basic features of SDL 2.0.
Confirm by clicking “OK” until all windows are closed. Your Lazarus IDE is now set up for SDL 2.0 application development :-)!
Configuring the Free Pascal IDE (skip if you use Lazarus)
Now the compiler has to be told where to find the new units.
8) Open the Free Pascal IDE (for example by clicking the shortcut on desktop).
9) In the menue choose the following item Options > Directories…. Now a window should pop up.
10) The first tab whitin this new window is called “Units”. Here you add the full path to your SDL-units (e.g. C:\FPC\3.2.2\units\SDL2) right below the other paths. Leave the last backslash out. Make sure the path leads to the folder, where the sdl2.pas file is located! This file contains all the basic features of SDL 2.0.
Since the screenshot is old, the compiler version has been 2.6.2, which is reflected in the shown pathes. It should be 3.0.0 or higher numbers for you.
Confirm by clicking “OK”.
Finally
Congratulations! You have configured your system for developing SDL 2.0 applications with the Lazarus IDE or the Free Pascal IDE! Simple, isn’t it? 🙂
For those of you who try running a program and get an abortion together with a messege saying “exitcode = 309”: You skipped step 6! Did you copy the SDL2.dll to your system32- or system-folder respectively? If so and the error still occurs you should copy the SDL2.dll into the folder where the programs are placed. Now it should work.
For this tutorial it is presumed, that you are familiar with procedures, functions, loops, pointers and usual commands of Free Pascal or its dialects. If this is not the case you may face problems because this tutorial deals with the features and usage of the SDL2 library and will not explain basic concepts of Pascal programming. For a quick refresh on some Pascal basics, have a look into this (good) article Modern Object Pascal Introduction For Programmers by Michalis Kamburelis.
You can easily copy the example source code directly from the source code box for each chapter.
The SDL2 unit and the first application
program FirstSteps;
uses SDL2;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then Exit;
{your SDL2 application/game}
//shutting down video subsystem
SDL_Quit;
end.
That’s it, your first SDL2 program. You may copy the code directly in your environment and run it. It should work without any errors if everything is set up correctly. The most important rule to get ready for SDL2 in your Pascal programs is obviously:
Always include the SDL2 unit in the uses clause.
The SDL2 unit is the heart of every SDL2 application! If you do so, you are perfectly prepared to start coding.
The different features of SDL2 (screen/video handling, audio handling, keyboard handling, and so on) have to be initilized individually using this function:
function SDL_Init(flags: TSDL_Init): cint.
It will return 0 on success and a negative error code on failure. The return value is of integer type cint. What this means, we will discuss later.
In the first example we initialize the SDL2 video subsystem for screen handling by using the flag SDL_INIT_VIDEO. Instead of SDL_INIT_VIDEO you could initilize the respective subsystem by using SDL_INIT_AUDIO for audio subsystem, SDL_EVENTS for event subsystem, and so on. What subssystem you need to initialize depends on what you want to do. The table shows an overview of all the possible flags and their meaning:
Flag
Description
SDL_INIT_TIMER
Initilizes timer subsystem for handling of time related events.
SDL_INIT_AUDIO
Initilizes audio subsystem for playing music or sound effects.
SDL_INIT_VIDEO
Initilizes video subsystem for drawing/showing/manipulating of graphics, textures and screen, usually the most important subsystem.
SDL_INIT_JOYSTICK
Initilizes joystick subsystem for handling of joysticks.
SDL_INIT_HAPTIC
Initilizes haptic (force feedback) subsystem.
SDL_INIT_GAMECONTROLLER
Initilizes game controller subsystem.
SDL_INIT_EVENTS
Initilizes events subsystem for handling of mouse or keyboard input.
SDL_INIT_EVERYTHING
Initilizes all the subsystems above.
SDL_INIT_NOPARACHUTE
Ignores fatal signals. In SDL2 this is set by default and it isn’t possible to change this state. The explanation is given in the official migration guide I will cite here:There’s no SDL parachute anymore. What 1.2 called SDL_INIT_NOPARACHUTE is a default and only state now. This would cause problems if something other than the main thread crashed, and it would interfere with apps setting up their own signal/exception handlers. On the downside, some platforms don’t clean up fullscreen video well when crashing. You should install your own crash handler, or call SDL_Quit() in an atexit() function or whatnot if this is a concern. Note that on Unix platforms, SDL still catches SIGINT and maps it to an SDL_QUIT event.
List of subsystem flags.
Of course you are allowed to combine several subsystem flags by OR, e.g. “SDL_Init(SDL_INIT_VIDEO OR SDL_INIT_AUDIO)” to initilize video and audio support. If you are running some subsystems already but need to load further ones you would use
function SDL_InitSubSystem(flags: TSDL_Init): cint
respectivly. Again 0 ist returned on success and the negativ error code in case of failure.
The flag SDL_INIT_EVERYTING will initialize all subsystems. Use this if you are unsure on what subsystem you should use, although it will need a little bit more resources.
Quitting your programs
Every SDL program has to be closed by
procedure SDL_Quit.
It cleans up your system. Never forget it! This procedure ensures that all subsystems initilized get unloaded. There is a corresponding procedure to unload specific subsystems defined as
procedure SDL_QuitSubSystem(flags: TSDL_Init).
You are allowed to quit two or more subsystems by this function by using OR operator. It is advised to always quit SDL 2.0 applications by SDL_Quit even if you quit all of them individually by SDL_QuitSubSystem before.
Try the example program. You will not see much but if you didn’t get an error message you are successful. Before proceeding, let’s have a quick look into error handling.
Pascal and the C variable types of SDL2
You may wonder about the return value cint in the SDL_Init function declaration. What a strange variable type for a return value, right? Let me explain.
SDL2 has been written in the C programming language. As a consequence, the functions of the SDL2 library expect arguments of C variable type and have return values of C variable type. The prefix c and the variable type name int in cint simply indicate for us, that the function returns a C-language integer value. Further examples of regularly used variable types in the SDL2 library are cuint8 for a C unsigned integer of 8 bit size, cint32 for a C signed integer of 32 bit size, cfloat for a C float point variable.
This notification of the handled C variable types helps you pick the suitable Pascal variable types when working with the SDL2 functions. E. g. ,if you like to store a cint32 return value, a suitable pick would be Longint (or SInt32) which represents a 32 bit (4 byte) signed integer according to Free Pascal’s ordinal types. For cint usually Integer is a good choice (not in Free Pascal or Turbo Pascal compiler mode though, where the Integer size is 2 bytes).
If you would like to use the C-language variable type names in your code yourself, you need to add the Free Pascal’s ctypes unit. For Delphi there is a ctypes.inc file shipped with the bindings.
As a beginner working with the units, you don’t have to be too concerned with these details, but now you know why these variable types have these names.
SDL2 functions and errors
Every SDL2 function returns an error value for you to check if the function runs properly at runtime. The values returned are of integer or pointer type. There is no general rule what values correspond to which status. In SDL2 usually an integer value of 0 means “function runs/ran succesfully”, values lower than 0 correspond to a status “function couldn’t be run, something is wrong”. For pointers nil means error and any non-nil pointer means success. However, in most cases I won’t do error checking in the examples to keep the code short. I will mention the error values to be expected though.
You should know that there is a function called
SDL_GetError: PAnsiChar
which translates the last error received into a message (of type PAnsiChar) that can be read out and printed to the screen by any function that can handle strings as well (e.g. the Pascal’s common write() function). Since SDL2 is written in C originally, the PAnsiChar type is used in contrast to the String type (which is more common among Pascal programmers for message storage and handling).
A quick way to return the error message is to use the SDL2 message box feature:
function SDL_ShowSimpleMessageBox(flags: TSDL_MessageBoxFlags; title: PAnsiChar; _message: PAnsiChar; window: PSDL_Window): cint
The flags for the message box could be:
SDL_MESSAGEBOX_ERROR
SDL_MESSAGEBOX_WARNING
SDL_MESSAGEBOX_INFORMATION
SDL_MESSAGEBOX_BUTTONS_LEFT_TO_RIGHT
SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT
While the flags indicate the reason for the message box, the title and the message argument are used to set a box title and a message. The window argument can be set to the nil pointer.
The following short code snippet demonstrates a convenient way of using the box for showing error messages in the case of SDL2 initialization as done above.
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then
begin
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, 'Error Box', SDL_GetError, nil);
Exit;
end;
Well, we have initilized the video subsystem and released it afterwards, and we learned about showing error messages in a simple message box. Simple Directmedia Layer deserves its name, doesn’t it?
Only if you agree, you may proceed to the next chapter ;-)!
Displaying of textures as discussed in previous chapters are sometimes accompanied by drawing operations. For some applications and games drawing operations are even essential, e.g. you need to draw buttons of dynamic dimension in your application. Discussion of a more detailed case is found right after the code example.
Supported Primitives: Points, Lines, Rectangles
SDL2 supports natively the drawing of
Points
Lines
Rectangles (outline only)
Filled Rectangles.
Drawing in SDL 2.0
Now lets jump right into the code:
program Chapter5_SDL2;
uses SDL2;
var
i: Integer;
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlRect1: TSDL_Rect;
sdlPoints: array[0..499] of TSDL_Point;
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then
halt;
sdlWindow1 := SDL_CreateWindow('Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN);
if sdlWindow1 = nil then
halt;
sdlRenderer := SDL_CreateRenderer(sdlWindow1, -1, 0);
if sdlRenderer = nil then
halt;
//render and show cleared window with background color
SDL_SetRenderDrawColor(sdlRenderer, 0, 255, 255, 255);
SDL_RenderClear(sdlRenderer);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
//render and show a line
SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 0, 255);
SDL_RenderDrawLine(sdlRenderer, 10, 10, 490, 490);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
//render and draw points diagonally with distance between each other
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, 255);
for i := 0 to 47 do
SDL_RenderDrawPoint(sdlRenderer, 490-i*10, 10+i*10);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
//prepare, render and draw a rectangle
sdlRect1.x := 260;
sdlRect1.y := 10;
sdlRect1.w := 230;
sdlRect1.h := 230;
SDL_SetRenderDrawColor(sdlRenderer, 0, 255, 0, 255);
SDL_RenderDrawRect(sdlRenderer, @sdlRect1);
//relocate, render and draw the rectangle
sdlRect1.x := 10;
sdlRect1.y := 260;
SDL_SetRenderDrawBlendMode(sdlRenderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 255, 128);
SDL_RenderFillRect(sdlRenderer, @sdlRect1);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
//prepare, render and draw 500 points with random x and y values
Randomize;
for i := 0 to 499 do
begin
sdlPoints[i].x := Random(500);
sdlPoints[i].y := Random(500);
end;
SDL_SetRenderDrawColor(sdlRenderer, 128, 128, 128, 255);
SDL_RenderDrawPoints(sdlRenderer, sdlPoints, 500);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(3000);
//clean memory
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//shut down SDL2
SDL_Quit;
end.
Wow, that looks like a big load of new functions, but I promise, drawing in SDL2 is very simple. What the executed program will look like is shown in the following screenshot.
Now, let’s have a closer look to the first lines of code.
program Chapter5_SDL2;
uses SDL2;
var
i: Integer;
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlRect1: TSDL_Rect;
sdlPoints: array[0..499] of TSDL_Point;
The program is called “Chapter5_SDL2”. We will need a counter variable “i” of native Pascal type integer later. We need a window and a renderer and call them “sdlWindow1” and “sdlRenderer” as known from the previous chapters. Next we declare a variable “sdlRect1” which is of type TSDL_Rect. The same is true for “sdlPoints” which is an array of 500 elements of type TSDL_Point.
begin
//initilization of video subsystem
if SDL_Init(SDL_INIT_VIDEO) < 0 then
halt;
sdlWindow1 := SDL_CreateWindow('Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN);
if sdlWindow1 = nil then
halt;
sdlRenderer := SDL_CreateRenderer(sdlWindow1, -1, 0);
if sdlRenderer = nil then
halt;
Nothing new here, we initilize SDL2, create a window with 500 pixels width and 500 pixels height and associate the renderer with this window.
Colours, alpha value and RGB(A) notation in SDL 2.0
//render and show cleared window with background color
SDL_SetRenderDrawColor(sdlRenderer, 0, 255, 255, 255);
SDL_RenderClear(sdlRenderer);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
Now we have something new here, SDL_SetRenderDrawColor() sets a colour for drawing operations, just as if you choose for a pencil colour to draw something. This function doesn’t draw anything though. It returns 0 on success and the negative error code on failure.
First you need to set the renderer for which this drawing colour is meant. After that you have to set the colours red, green, blue and the alpha value. They can range from 0 to 255 (integer values only). Think in terms of 255 being 100% and 0 being 0% of that colour or the alpha value. As a start let’s neglect the alpha value.
If you like to have a red colour, you need to set the value for the red colour high, e.g. 100%, the maximum value is 255, and since you don’t want to mix in green and blue, they get the minimum value of 0 (hence 0%). Setting up red therefore corresponds to 255/0/0 in terms of r/g/b. For comparision, 0/255/0 will lead to green, 255/255/255 is white and 0/0/0 is black, and so on. In the example code we used 0/255/255 which leads to cyan (mixing up green and blue additively). With these three values you can generate every colour possible.
So what is the meaning of the alpha value then? Well, it determines the transparency. 255 means fully opaque and 0 means full transparency. This is very important for blend effects in computer graphics and will be demonstrated later on in the code. By the way, instead of 255 you could use SDL_ALPHA_OPAQUE. If you count the alpha value also as colour variation you have 4.29 billion different possibilities.
Setting up a background in SDL2
The function SDL_RenderClear() is for clearing the screen with the drawing colour. As argument you just need to tell the renderer. It is simple as that :-). Since we set the drawing colour to cyan before, the screen will be cleared with a cyan colour.
SDL_RenderClear(renderer: PSDL_Renderer): SInt32
This function will return 0 on success and a negative error code on failure. The cleared screen will be shown for one second by SDL_RenderPresent() and SDL_Delay(). These two procedures are known from the previous chapter.
Drawing Lines and Points in SDL2
//render and show a line
SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 0, 255);
SDL_RenderDrawLine(sdlRenderer, 10, 10, 490, 490);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
Now we change the drawing colour by SDL_SetRenderDrawColor() to red and use SDL_RenderDrawLine() to draw a simple line.
Again you need the renderer, which is “sdlRenderer” in our case. After that you specify the x/y coordinates where the line should begin and then the x/y coordinates where the line should end. Remember that the origin 0/0 is at the top left corner of the window. The coordinates 10/10 mean to start at the point ten pixels to the right and ten pixel to the bottom relative to the origin. Thus, the coordinates 490/490 for the second point will lead to a diagonal line across the window. This diagonal line will be 10 pixels short with respect to the window edge. This function returns 0 on success and a negative error code on failure.
After that again we ask to render this line to the screen by SDL_RenderPresent() and wait one second by SDL_Delay().
//render and draw points diagonally with distance between each other
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, 255);
for i := 0 to 47 do
SDL_RenderDrawPoint(sdlRenderer, 490-i*10, 10+i*10);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(1000);
Now we change the colour to black and draw some points by the function SDL_RenderDrawPoint(). It is nearly identical to SDL_RenderDrawLine() but instead of four coordinates you need just two coordinates where the point should be drawn.
This function returns 0 on success and the negative error code on failure.
I thought it would be nice to have more than just one point to be drawn, so the function is used in a for-loop to draw altogether 48 points. Here we need the counter variable “i”. Maybe you can guess from the code where the points are and how they are arranged, if not, just run the code ;-). Finally the result is rendered to the screen by SDL_RenderPresent() and the program waits one second by SDL_Delay().
It requires the renderer, which is “sdlRenderer” for us and a PSDL_Rect , thus we use the @ operator for the declared rectangle of TSDL_Rect type to get its pointer value. This function returns 0 on success and the negative error code on failure. Notice, we neither render the result to the screen now nor do we delay here. Anyway, we want a second rectangle! 🙂
We change the rectangles x/y coordinates for the second rectangle but keep its width and height. What we are looking for is a filled rectangle that has some transparency. Until now we always used 255 (opaque) as alpha value. We set the colour to draw the second rectangle to blue by SDL_SetRenderDrawColor(). Notice that the fourth value is 128 (half-transparent) instead of 255 (opaque). So everything behind the blue rectangle, e.g. the cyan background, should therefore shine through. To generate a filled rectangle SDL_RenderFillRect() is used:
The renderer and the rectangle of type PSDL_Rect are the parameters of this function. So we use “sdlRenderer” and “sdlRect1” (with the @ operator) again to draw the rectangle. This function returns 0 on success and the negative error code on failure.
The Blend Mode in SDL 2.0
But to be honest, even if you change the alpha value it will be opaque. This is because the blend mode is set to SDL_BLENDMODE_NONE by default. We need to change this to be able to use the alpha value as desired. SDL_SetRenderDrawBlendMode() is what we are looking for:
First the renderer for which the blend mode has to be set is chosen. In our case it is “sdlRenderer” again. Then there are four blend modes available. Their description is taken from the official SDL2 Wiki.
SDL_BLENDMODE_NONE
no blending
dstRGBA = srcRGBA
SDL_BLENDMODE_BLEND
alpha blending
dstRGB = (srcRGB * srcA) + (dstRGB * (1-srcA))
dstA = srcA + (dstA * (1-srcA))
SDL_BLENDMODE_ADD
additive blending
dstRGB = (srcRGB * srcA) + dstRGB
dstA = dstA
SDL_BLENDMODE_MOD
color modulate
dstRGB = srcRGB * dstRGB
dstA = dstA
We are looking for alpha blending, so we use SDL_BLENDMODE_BLEND as argument for the blend mode. This function returns 0 on success and the negative error code on failure.
After doing so the result is rendered to the screen by SDL_RenderPresent() and shown for one second by SDL_Delay(). Both rectangles, the green one and the half-transparent blue one appear at the same time.
Spread Points Randomly using PSDL_Point
//prepare, render and draw 500 points with random x and y values
Randomize;
for i := 0 to 499 do
begin
sdlPoints[i].x := Random(500);
sdlPoints[i].y := Random(500);
end;
Randomize is a Free Pascal procedure (from system unit) to initilize the random number generator. Imagine this as shaking the dice.
Let’s have a look at TSDL_Point. It is just a record of this structure:
As expected, the TSDL_Point record has two values, the x/y coordinates of the point. Notice how PSDL_Point is just a pointer to it. Free Pascal’s Random() function generates random numbers between 0 and the number which is used as argument substracted by one. So we generate 500 times a random x and y value between 0 and 499 and save them into the 500 SDL_Point records.
This function returns 0 on success and the negative error code on failure. First you need to set the renderer, then just a pointer to an array of TSDL_Point (returned by simply passing the array’s name) and finally the number of points. The latter should be consistent with the array. So, if your array has 500 elements, the count should be 500 at maximum. Notice how we are not using a loop here to draw all 500 points by calling a certain function 500 times. Instead we just pass an array of points once. This way we save a lot of time at runtime, especially if you think of a real application where even more points have to be drawn. There are similar functions for lines, rectangles and filled rectangles. They are not used in the example but it may be interesting to know, so here they are:
To put your array in the heap, you would want to use an array of PSDL_Point instead of an array of TSDL_Point. For some reason it doesn’t seem to work well with the SDL_RenderDrawPoints() function. This means, instead of, let’s say 500 points just a quarter of all points (125 in the example) is shown unless you specifiy the count as being four times greater (2000 in the example). The reason behind this, is not clear to me.
Case: Drawing for Dynamic Colouring
As an example when drawing can be very helpful, think of the following situation. You create a game, let’s say a racing game. There are different players. Of course, each player’s car has to have a different colour. There are generally two ways to achieve this:
1) You create several images with differently coloured cars and store them on your harddrive.
This is perfectly okay if you have a small number of cars (and colours) to be chosen. But what if you want the player to choose from a large variety of colours, 100 colours or more, or what if you want to let the player to choose the colour of his car by himself? Notice, in case of 16 bit colouring that means 65536 possibilities after all! Would you want to create that many images? In case of 32 bit colouring you have fantastic 4.29 billion colours!! Amazing, but you will never be able to create so many images in just one human being’s life. Furthermore, this would take up a lot of hard drive memory (4.29 billion times the file size of the car image) for just a simple car racing game. Look at the following image. It contains the discussed method from left to the middle. On the right to the middle is the solution :-).
Instead of having each car coloured as an image file, why not using just one image file of a car without colouring? It is kind of a template. Now you can easily ask the player what colour he prefers (and he may pick from 4.29 billion colours if necessary), and then you simply colour the car on the fly. This is the second way:
2) You create one template image on your harddrive and colour it during runtime of the program.
This is just one example where drawing is very helpful.
Understanding Colours in Computer Graphics
The colour is composed of four components, RGBA, that is red, green, blue and the alpha value. The physical screen consists of many small units. Every unit itself consists of three different coloured lights. These colours are red, green and blue. If you mix them up, you can get every other colour. These colours are mixed up additively. For example if you mix red and green you get yellow. For three colours that can be mixed with each other, there are eight combinations possible which lead to different colours (RGB, RG, RB, R, GB, G, B, all lights off). If you mix red, green and blue (all lights on, RGB), you get white, and if all lights are off, you get black. Some may say, white and black are no colours at all. Well, that is right but doesn’t matter here and to keep simpliness I will talk of colours even if I talk of black and white.
Your screen definitvly has more than eight colours, doesn’t it? The reason is, your screen isn’t just able to switch lights on or off. Besides it is able to differ the intensities of the lights. The more intensity levels you have the more colours you can display. The case that you have eight colours as discussed before means that you just have one intensity level, on or off. If your screen is in 8 bit mode every pixel on the screen has the possibility to display 2 power 8 colours. That are 256 different colours. Every of the three lights has therefore a certain amount of different light intensity levels. If you have 16 bit mode you have 2 power 16 and that are 65536 colours. Each light therefore has the appropriate amount of intensity levels. Since we prefer 32 bit mode, we have 4.29 billion different colours!
For mouse motions, mouse buttons and the mouse wheel there are three different mouse event structures: SDL_MouseMotionEvent, SDL_MouseButtonEvent and SDL_MouseWheelEvent.
Mouse motion handling in SDL 2.0
If you are moving the mouse, the SDL_MOUSEMOTION event is triggered. The record structure of the SDL_MouseMotionEvent is shown below:
TSDL_MouseMotionEvent = record
type_: UInt32; // SDL_MOUSEMOTION
timestamp: UInt32;
windowID: UInt32; // The window with mouse focus, if any
which: UInt32; // The mouse instance id, or SDL_TOUCH_MOUSEID
state: UInt8; // The current button state
padding1: UInt8;
padding2: UInt8;
padding3: UInt8;
x: SInt32; // X coordinate, relative to window
y: SInt32; // Y coordinate, relative to window
xrel: SInt32; // The relative motion in the X direction
yrel: SInt32; // The relative motion in the Y direction
end;
Again there are the type_, the timestamp and the windowID fields. Nothing new here. The field which contains the mouse id. This is important if you have more than one mouse device attached to your computer. Think for example of a laptop with a touchpad area to move the mouse cursor and at the same time there is an usb mouse attached to the laptop. To distinguish between the two, you may retrieve their id’s.
The state field is known from the SDL_KeyBoardEvent structure. It may be a difference if you have a mouse button pressed and move the mouse or if you don’t have a button pressed. The most famous example is if you want to select a bunch of files on your desktop or in a folder. By the way, the state field encodes a number which is different depending on which buttons you pressed actually. This works similar to the key modifiers, if you keep two mousebuttons pressed while moving, the state is the sum of each individual mouse button state value. As an example for my mouse: No mouse button 0, left mouse button 1, right mouse button 4, middle mouse button 2, thumb button 8. If I keep pressed left and right mouse button 5 (sum 1 + 4).
The x and y fields contain the coordinate of the mouse cursor in pixels. These coordinates are relative to the window of the SDL 2.0 application. Keep in mind, the coordinates (0/0) correspond to the left upper corner. Positive x values are counted from left to right and positive y values are counted from top to bottom.
The fields xrel and yrel are used to determine how fast the mouse has been moved from one point to another. Let’s assume you move the mouse surcor from left to right in your application’s window. The first time you do it slowly, xrel might be 1, means, you just moved pixel from left to right between two mouse motion events. If you move fast, xrel might be 50, meaning that this time you moved by 50 pixels between two mouse motion events. Especially for game programming this can be a extremely important information. E.g., think of first person shooter, if the movement speed of the first person view would be independent of the actual movement of the mouse, this game wouldn’t make much sense.
To access these fields the event’s motion field has to be read out. In the example code the (x/y) coordinates and the relative positions xrel and yrel are read out by
sdlEvent^.motion.x
sdlEvent^.motion.y
sdlEvent^.motion.xrel
sdlEvent^.motion.yrel
and simply printed out to the screen. Let’s go for the next chunk of code.
As for the SDL_KeyBoardEvent, you would want to know if a mouse button and which one is pressed or released. If a mouse button is pressed, a SDL_MOUSEBUTTONDOWN event is triggered. On releasing a mouse button a SDL_MOUSEBUTTONUP event is triggered. It has SDL_MouseButtonEvent structure. Let’s have a look into the structure:
TSDL_MouseButtonEvent = record
type_: UInt32; // SDL_MOUSEBUTTONDOWN or SDL_MOUSEBUTTONUP
timestamp: UInt32;
windowID: UInt32; // The window with mouse focus, if any
which: UInt32; // The mouse instance id, or SDL_TOUCH_MOUSEID
button: UInt8; // The mouse button index
state: UInt8; // SDL_PRESSED or SDL_RELEASED
padding1: UInt8;
padding2: UInt8;
x: SInt32; // X coordinate, relative to window
y: SInt32; // Y coordinate, relative to window
end;
If you compare this structure to the structure of the SDL_MouseMotionEvent, you will find, only the two field xrel and yrel are gone and a new field, a crucial one to be clear, is new, which is button of 8 bit unsigned integer type.
I won’t discuss all the fields again we discussed for the SDL_MouseMotionEvent structure. Attention, if you compare the state field of the SDL_MouseButtonEvent, it works another way. It allows just for two values, SDL_PRESSED or SDL_RELEASED, as known from SDL_KeyBoardEvent. As a reminder: For the SDL_MouseMotionEvent, it represented that full state of all the buttons being pressed while mouse motion.
The button field allows to recognize which button has triggered the SDL_MouseButtonEvent. Each button of the mouse has its own index. As an example for my mouse they are as follows: Left mouse button 1, right mouse button 3, middle mouse button 2, thumb mouse button 4. Do not confuse these index numbers with the mouse button state values of the SDL_MouseMotionEvent structure. There can only be one button which triggered this event! Combinations as for the motion event are not possible. In the example code this value is just printed to the screen using
sdlEvent^.button.button.
You see, to access the fields of this record you need to address the event’s button field. This mustn’t be confused with SDL_MouseButtonEvent’s button field discussed above.
The x and y fields contain the (x/y) coordinates when the mouse button (whose index is stored in field button) has been pressed (or released). This is crucial to know. What would an application be worth if you could recognized that a certain button has been pressed but you don’t know where exactly? Not shown in the code but you could access these fields by
sdlEvent^.button.x
sdlEvent^.button.y.
The mouse wheel in SDL 2.0
If the mouse wheel is used a SDL_MOUSEWHEEL event is triggered. Let’s look into the corresponding SDL_MouseWheelEvent structure.
TSDL_MouseWheelEvent = record
type_: UInt32; // SDL_MOUSEWHEEL
timestamp: UInt32;
windowID: UInt32; // The window with mouse focus, if any
which: UInt32; // The mouse instance id, or SDL_TOUCH_MOUSEID
x: SInt32; // The amount scrolled horizontally
y: SInt32; // The amount scrolled vertically
end;
New fields here are the x and y field which do not correspond to the mouse cursor position this time. Instead of that they refer to the direction of the mouse wheel being scrolled. If you scroll the mouse wheel upwards or forward it will return 1, and if you scroll it backwards it will return -1. If you have a mouse wheel which can be scrolled horizontally, it will be similar. By the way scrolling a mouse wheel can be considered as pressing a button very quickly for that direction.
In the code, if a SDL_MOUSEWHEEL event is triggered, the y value is checked to be positive or negative. This decides if an upward or downward scrolling has happened and the corresponding value (1 or -1) will be returned and printed out. Anyway, could you guess what happens if anyone would use a mouse wheel that is scrolling horizontally? – The same block will be executed since a SDL_MOUSEWHEEL event is triggered. Instead of y being 1 or -1, it will be 0 but the x value will be 1 or -1. Nevertheless, the else-block will be executed since y is not greater than 0. So for the example program it will print out wrongly that an backward scroll has happened. Anyway, you get the idea.
Window handling in SDL 2.0
Modern applications are always run in windows. The famous operation system “Windows” by Microsoft even derived it’s name from this. The first task for most of SDL 2.0 applications is the creation of a window. The example code creates a window of width 500 pixels and height 500 pixels. It may be important to know if the user interacts with the application window. Whenever this happens a SDL_WINDOWEVENT is triggered.
If the an event of type SDL_WINDOWEVENT is triggered, the text message “Window event: ” is printed out.
To access the window event fields, you need to access the event’s window field. The field event contains the window event’s type information, hence what window event has been triggered.
sdlEvent^.window.event
In contrast to the keyboard and the mouse event we discussed before, the different event types are not distinguished by the type_ field but by an additional field event.
In the example code six different window event types are checked: SDL_WINDOWEVENT_SHOWN, SDL_WINDOWEVENT_MOVED, SDL_WINDOWEVENT_MINIMIZED, SDL_WINDOWEVENT_MAXIMIZED, SDL_WINDOWEVENT_ENTER and SDL_WINDOWEVENT_LEAVE. From the texts printed out you can guess when they get triggered. I think no further explanation is needed here.
By the way, there are more window event types which are shown a little bit later. Sometimes if one of these is triggered, only the text that a window event has been triggered is shown but without any further details since the example code doesn’t covers further treatment. Feel free to extent the code yourself.
Let’s have a look at the event structure of SDL_WindowEvent.
TSDL_WindowEvent = record
type_: UInt32; // SDL_WINDOWEVENT
timestamp: UInt32;
windowID: UInt32; // The associated window
event: UInt8; // SDL_WindowEventID
padding1: UInt8;
padding2: UInt8;
padding3: UInt8;
data1: SInt32; // event dependent data
data2: SInt32; // event dependent data
end;
The fields type_, timestamp and windowID are known and have the same meaning as discussed before.
The field event stores an identifier (SDL_WindowEventID) to distinguish between different window events. Here they are listed and in brackets you find the window related action which has triggered them:
SDL_WINDOWEVENT_SHOWN (window has been shown)
SDL_WINDOWEVENT_HIDDEN (window has been hidden)
SDL_WINDOWEVENT_EXPOSED (window has been exposed and should be redrawn)
SDL_WINDOWEVENT_MOVED (window has been moved to data1, data2)
SDL_WINDOWEVENT_RESIZED (window has been resized to data1xdata2; this is event is always preceded by SDL_WINDOWEVENT_SIZE_CHANGED)
SDL_WINDOWEVENT_SIZE_CHANGED (window size has changed, either as a result of an API call or through the system or user changing the window size; this event is followed by SDL_WINDOWEVENT_RESIZED if the size was changed by an external event, i.e. the user or the window manager)
SDL_WINDOWEVENT_MINIMIZED (window has been minimized)
SDL_WINDOWEVENT_MAXIMIZED (window has been maximized)
SDL_WINDOWEVENT_RESTORED (window has been restored to normal size and position)
SDL_WINDOWEVENT_ENTER (window has gained mouse focus)
SDL_WINDOWEVENT_LEAVE (window has lost mouse focus)
SDL_WINDOWEVENT_FOCUS_GAINED (window has gained keyboard focus)
SDL_WINDOWEVENT_FOCUS_LOST (window has lost keyboard focus)
SDL_WINDOWEVENT_CLOSE (the window manager requests that the window be closed)
This list is based upon information found at the SDL 2.0 wiki.
If you read through the list carefully you will notice the mention of data1 and data2 which rather explains their occurance in the event structure :-)! They need to be read out for SDL_WINDOWEVENT_MOVED and SDL_WINDOWEVENT_RESIZED to get the new window position or dimensions.
At the moment I’m not sure why for window events the distinction between the individual window events (e.g. SDL_WINDOWEVENT_MOVED, SDL_WINDOWEVENT_RESIZED, and so on) is not done by the type_ field as for keyboard, mouse and other events, but rather by the additional event field.
end;
end;
end;
SDL_Delay( 20 );
end;
dispose( sdlEvent );
SDL_DestroyWindow ( sdlWindow1 );
//shutting down video subsystem
SDL_Quit;
end.
Not much to learn in the final part. The loop is delayed by 20 milliseconds for better recognizability of the text output.
If the loop is left, the event pointer gets free’d, the SDL 2.0 window gets destroyed and SDL 2.0 quit. That’s it :-)!
Touchscreen events, Joystick events and many more!
This chapter covered keyboard, mouse and window events in some detail. Keep in mind, SDL 2.0 has much more to show! – There are many more events you can use for application development. They basically cover any modern type of interaction you could wish for. This includes touchscreen events (important for smartphone development), joystick events (game console development), and even a dropfile event (drag and drop files) and more.
What’s an event and event handling in programming?
Event handling is a major concept in game and application programming. It allows for the user to interact with your program. Whenever you move your mouse cursor, press or release a key on the keyboard or use a touch screen, all these interactions are recognized as so-called events.
Events in SDL 2.0, SDL_Event
In SDL 2.0 whenever an event occurs, for instance a key gets pressed on the keyboard, all the information related to that specific event is stored in a SDL_Event record. Depending on the event type (e.g. mouse motion, key pressed on a keyboard, maximizing the application window) there are very different fields which can be accessed. For a mouse motion you can read out the x and y position of the cursor whereas there is no sense in having x and y values for a pressed key on the keyboard, but to know which specific key has been pressed on the keyboard.
Event types available in SDL 2.0
There are many more types of events than a mouse motion and a key being pressed on a keyboard. Think of using a joystick, using a touchscreen, minimizing/maximizing the application window, and so forth. There are plenty of different events that could occur.
All these event types have certain names, e.g. the event type which indicates a mouse motion is called SDL_MOUSEMOTION. The full list according to the official SDL 2.0 documentation is:
This list is overwhelmingly long but don’t worry, as soon as you get the concept behind events you will easily understand which of these event types will play a role for the applications you like to develop. The most important events are covered by this tutorial in detail anyway. You will be able to work with the remaining events once you got the concept.
In contrast to SDL 1.2 there are some event types gone and many new types available in SDL 2.0 which are useful to use new forms of interaction between the user and the application (e.g. touch screen technology).
What is the difference between event type, event structure and the event field?
In the table above you’ll notice the first column covers the event type. It determines which type of event occured, e.g. a key is pressed down on the keyboard (SDL_KEYDOWN).
The event structure in the second column is the record structure which is dependend upon the event type. As discussed before, a pressed key will need a record structure which stores the key identifier rather than x,y-coordinated (which would in turn be necessary for a mouse motion). Each event type has a certain apropriate (event) record structure to hold the event information.
Many event types can share the same event structure. The event types SDL_KEYDOWN and SDL_KEYUP which are generated by pressing or releasing a key share the same event structure SDL_KeyboardEvent since the information are the same, e.g. the key identifier.
The third column shows the SDL_Event field name to access the event specific fields. In case of an event type SDL_KEYDOWN the event structure is SDL_KeyboardEvent. The specific information, e.g. the key identifier, is accessible via the field key in the SDL_Event record.
This may sound confusing. Later on the relation is discussed in more detail. Let’s right proceed into the code.
program Chapter8_SDL2;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlEvent: PSDL_Event;
exitloop: boolean = false;
text1: string = '';
begin
//initilization of video subsystem
if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;
sdlWindow1 := SDL_CreateWindow( 'Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN );
if sdlWindow1 = nil then HALT;
new( sdlEvent );
while exitloop = false do
begin
while SDL_PollEvent( sdlEvent ) = 1 do
begin
write( 'Event detected: ' );
case sdlEvent^.type_ of
//keyboard events
SDL_KEYDOWN: begin
writeln( 'Key pressed:');
writeln( ' Key code: ', sdlEvent^.key.keysym.sym );
writeln( ' Key name: "', SDL_GetKeyName( sdlEvent^.key.keysym.sym ), '"' );
writeln( ' Scancode: ', sdlEvent^.key.keysym.scancode );
writeln( ' Scancode name: "', SDL_GetScancodeName( sdlEvent^.key.keysym.scancode ), '"' );
writeln( ' Key modifiers: ', sdlEvent^.key.keysym._mod );
case sdlEvent^.key.keysym.sym of
SDLK_ESCAPE: exitloop := true; // exit on pressing ESC key
//switching text input mode on/off
SDLK_F1: begin
if SDL_IsTextInputActive = SDL_True then SDL_StopTextInput
else SDL_StartTextInput;
writeln(' Text Input Mode switched' );
end;
end;
end;
SDL_KEYUP: writeln( 'Key released ' );
SDL_TEXTINPUT: begin
writeln( 'Text input: "', sdlEvent^.text.text, '"' );
text1 := text1 + sdlEvent^.text.text;
writeln( 'Full string: ' + text1 );
end;
//mouse events
SDL_MOUSEMOTION: begin
writeln( 'X: ', sdlEvent^.motion.x, ' Y: ', sdlEvent^.motion.y,
' dX: ', sdlEvent^.motion.xrel, ' dY: ', sdlEvent^.motion.yrel );
end;
SDL_MOUSEBUTTONDOWN: writeln( 'Mouse button pressed: Button index: ', sdlEvent^.button.button );
SDL_MOUSEBUTTONUP: writeln( 'Mouse button released' );
SDL_MOUSEWHEEL: begin
write( 'Mouse wheel scrolled: ' );
if sdlEvent^.wheel.y > 0 then writeln( 'Scroll forward, Y: ', sdlEvent^.wheel.y )
else writeln( 'Scroll backward, Y: ', sdlEvent^.wheel.y );
end;
//window events
SDL_WINDOWEVENT: begin
write( 'Window event: ' );
case sdlEvent^.window.event of
SDL_WINDOWEVENT_SHOWN: writeln( 'Window shown' );
SDL_WINDOWEVENT_MOVED: writeln( 'Window moved' );
SDL_WINDOWEVENT_MINIMIZED: writeln( 'Window minimized' );
SDL_WINDOWEVENT_MAXIMIZED: writeln( 'Window maximized' );
SDL_WINDOWEVENT_ENTER: writeln( 'Window got mouse focus' );
SDL_WINDOWEVENT_LEAVE: writeln( 'Window lost mouse focus' );
end;
end;
end;
end;
SDL_Delay( 20 );
end;
dispose( sdlEvent );
SDL_DestroyWindow ( sdlWindow1 );
//shutting down video subsystem
SDL_Quit;
end.
The result will not be seen in the actually SDL 2.0 window but in the command line window (which usually is showing up along with the SDL 2.0 window on Windows environments).
The initial lines of code are:
program Chapter8_SDL2;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlEvent: PSDL_Event;
exitloop: boolean = false;
text1: string = '';
begin
//initilization of video subsystem
if SDL_Init( SDL_INIT_VIDEO ) < 0 then HALT;
sdlWindow1 := SDL_CreateWindow( 'Window1', 50, 50, 500, 500, SDL_WINDOW_SHOWN );
if sdlWindow1 = nil then HALT;
new( sdlEvent );
The program is called “Chapter8_SDL2”. Since event handling is a basic feature of SDL 2.0, no further units except for SDL2 itself necessary.
We need three variables. “sdlWindow1” is necessary to create a window as known from previous chapters. This time we won’t draw anything to it but use it to recognize events (e.g. mouse clicks into the window).
The SDL_Event variable “sdlEvent” stores the events generated by the user of the application. It is of pointer type PSDL_Event. Then there is a simple Pascal boolean variable “exitloop” which is set to false since we don’t want to leave the program loop initially. Also we have a “text1” string variable we will need to demonstrate text input later.
Nothing new for the following lines, SDL 2.0 is initilised and the SDL 2.0 window is created.
Since “sdlEvent” is a pointer type variable we need to allocate some memory. This is done by the new command, as known.
while exitloop = false do
begin
while SDL_PollEvent( sdlEvent ) = 1 do
begin
write( 'Event detected: ' );
First a while-do loop is started which will run as long as the variable “exitloop” is false. If it is changed to true the loop will be exited. (This is triggered by pressing ESC, later this is discussed in detail.)
Within the outer loop the first thing is to poll for an event, which is done by function SDL_PollEvent.
SDL_PollEvent(event: PSDL_Event): SInt32
This function returns integer value 1 if one or more events are in the queue. The event data is allocated to the SDL_Event variable and deleted from the queue. If there are no events waiting, it returns 0 and the event variable is filled with nil instead of specific event data.
If there is an event waiting, its information are fed to “sdlEvent” and 1 is returned. The inner while loop is running until all events in the queue are treated. This will print out a text saying “Event detected” and, more important, check for the event type!
Don’t use an if-clause (instead of the inner while loop) to check for the events because that means we only check once a event each every cycle of the outer loop. By combining two while loops and check for all the events in the inner loop we can treat every event occured for this program loop cycle.
The type_ field
The event type can be read out from the field type_, hence we check for the type in sdlEvent^.type_ by a case-statement.
If _type is a SDL_KEYDOWN event a begin-end block is entered. There are several writeln output lines. Let’s discuss them one after another.
In the first line the SDL (virtual) key code is returned, which is represented by an integer value. For special keys (e.g. F-keys, Insert, …) these values often range beyond the scope of SmallInt (-32768 to 32767) or Word (0 to 65535) variables, which you should keep in mind if you intend to return these values to variables. The SDL virtual key code can be accessed by
sdlEvent^.key.keysym.sym.
Let’s try to break this down a little bit. The event is stored in sdlEvent which is of pointer type so to access the content we need sdlEvent^. In the keyboard event there is a field keysym which itself is a record. The SDL virtual key code is stored in the field sym of the keysym record. Don’t worry if this sounds kind of confusing, in the next part we treat these two records (SDL_KeyboardEvent record and keysym record) in detail.
Most of the key codes have a name, e.g. “Escape” for the escape key. In the next line of the code, the function SDL_GetKeyName is used to get the name of the key whose key code we found:
function SDL_GetKeyName(key: TSDL_ScanCode): PAnsiChar
The other way round it is also possible by this function:
function SDL_GetKeyFromName(const name: PAnsiChar): TSDL_KeyCode
This is not shown in the code though.
The scancode of a key is another representation. The details about the difference between key codes and scancodes are discussed a little bit later. Anyway, the scancode is stored in the scancode field of the keysym record. So it can be accessed by:
sdlEvent^.key.keysym.scancode
and its name can be read out by
function SDL_GetScancodeName(scancode: TSDL_ScanCode): PAnsiChar.
Also here you can do it the other way round by
function SDL_GetScancodeFromName(const name: PAnsiChar): TSDL_ScanCode.
For completion and your information, it is possible to get the key code from the scancode and vice versa. The functions to use are:
function SDL_GetKeyFromScancode(scancode: TSDL_ScanCode): TSDL_KeyCode
and
function SDL_GetScancodeFromKey(key: TSDL_KeyCode): TSDL_ScanCode.
You see, there is a strong relation between the two but they are not the same. Lets have a short example of what happens if you press a key:
Keycodes and Scancodes
The tutorial code returns these key- and scancodes for the “Q”-key, the escape key and the return key. As you can see the codes do not only differ between different keys but also for the same key, e.g. the key code for the escape key is 27 but the scanscode is 41. Anyway, the key names seems to be consistent. Later we will see an example where even the names differ.
The last line returns the code value of key modifiers. Key modifiers are keys which literally modify them. E.g., if you press a letter key while holding the shift key, usually you modify the letter to be the capital letter. Typical key modifiers are shift, ctrl and alt. The field which stores the key modifier value is called _mod. Later we will discuss this field more in detail.
case sdlEvent^.key.keysym.sym of
SDLK_ESCAPE: exitloop := true; // exit on pressing ESC key
//switching text input mode on/off
SDLK_F1: begin
if SDL_IsTextInputActive = SDL_True then SDL_StopTextInput
else SDL_StartTextInput;
writeln(' Text Input Mode switched' );
end;
end;
end;
We check for the value of the key code by a case-statement with sdlEvent^.key.keysym.sym. If we know the key code of a certain key, we may check for this key and react accordingly. First, we check if the event’s key code is SDLK_ESCAPE since this is the key code of the escape key (ESC). If so, the variable exitloop is set to true to stop the outer while loop and exiting the program.
Every key code is represented by the key code constant (e.g. SDLK_ESCAPE), a decimal value (27 here) and a hexadecimal value ($001B here). You may check the key codes in the following (official) SDL 2.0 key code lookup table:
In the fifth row the escape key key code is found.
In this table you find additionally to the decimal value, the hexadecimal value. You may try to use $001B instead of 27. This will do the trick either :-)! Also there is a character representation shown if possible.
If the the F1-key is pressed we would like to turn on or off the text input mode. This time we do not use a decimal key code to recognize the key but rather a constant representation (SDLK_F1). You could guess you have the choice and could use either the decimal value, the constant representation (what about SDLK_ESCAPE in the case before?) or the hexadecimal representation. – SDLK_ESCAPE even exists(!) but guess what, that doesn’t work. For some special keys this works like shown for the F1-key, for most of the other keys it doesn’t work (you will find that most keys are stored as string constants so the compiler will complain that they are of wrong type). So for most keys you have to look up the decimal representation and do as shown.
In the same way we check if the F1 key got pressed by checking for SDLK_F1. If F1 got pressed it is checked if the so called Text input mode is active by
function SDL_IsTextInputActive: TSDL_Bool.
If it is active, it gets deactivated by
procedure SDL_StopTextInput
and if it is not active, it gets activated by
procedure SDL_StartTextInput.
Finally a short text is returned which states that the Text input mode has been changed. More about the input mode later.
Last but not least, if _type is a SDL_KEYUP event, we know a key has been released, hence it physically moves up on the keyboard. We just print out “Key released”, we don’t care what exactly key is released here.
The keyboard events SDL_KEYDOWN and SDL_KEYUP
Let’s have a look at the structure of the keyboard event record and discuss the fields from top to bottom:
TSDL_KeyboardEvent = record
type_: UInt32; // SDL_KEYDOWN or SDL_KEYUP
timestamp: UInt32;
windowID: UInt32; // The window with keyboard focus, if any
state: UInt8; // SDL_PRESSED or SDL_RELEASED
_repeat: UInt8; // Non-zero if this is a key repeat
padding2: UInt8;
padding3: UInt8;
keysym: TSDL_KeySym; // The key that was pressed or released
end;
type_ is an unsigned 32 bit integer (hence UInt32) value which determines what exact type of event you have. As usual the integer values are represented by constants, here SDL_KEYDOWN (if pressed down) and SDL_KEYUP (if key has been released). They share the same overall event record structure (SDL_KeyboardEvent). As a sidenote, all the SDL 2.0 events have a type_ field for obvious reason.
The next field timestamp obviously contains a timestamp of integer type which is used internally by SDL 2.0 to resolute the sequence in which all the events were triggered. All SDL 2.0 events have the timestampfield.
The windowIDfield is necessary to distinguish between events raised from different SDL 2.0 windows if there is more than one. Let’s assume your program has two SDL 2.0 windows, window1 and window2. These two windows have certain constant id’s allocated by SDL 2.0. Now, if you have the focus on window1 (meaning the active window is window1) and press a key, the event’s windowID contains the specific id for window1. If the active SDL 2.0 window is window2 and you press a key, the specific id of window2 will be present in windowID. This way you can easily distinguish for which window the program should react to the keyboard event, e.g. a pressed key. Anyway, the capability to handle more than one window has been introduced by SDL 2.0, so you can imagine that for many programs and if you do not have more than one window in your program, you may ignore this field. The windowIDfield isn’t present (and necessary) for all the events SDL 2.0 provides.
The 8 bit integer state field may be read out to get the state of the key (pressed or release) which are encoded by SDL_PRESSED and SDL_RELEASED. You may be a little bit confused what the difference between the pair SDL_PRESSED and SDL_RELEASED and the pair SDL_KEYDOWN and SDL_KEYUP is. Essentially they have the same meaning for a key of a keyboard. Formally, the difference is that SDL_KEYDOWN and SDL_KEYUP are two different event types whereas SDL_PRESSED and SDL_RELEASED are two different key states. In the case of a pressed key on a keyboard the state is kind of redundant because if you get a SDL_KEYDOWN event you already know that a key was pressed and to read out the state (which will be SDL_PRESSED) is unnecessary. Anyway, the state field seems to be introduced for completeness, since for other event types (e.g. mouse events) there is a huge difference if you move the mouse and have mouse buttons pressed or released.
_repeat let’s you know if the corresponding key is in repeat mode. For most operation systems the repeat mode kicks in after a short delay when you keep a key pressed. You may try out to open up a simple text editor. If you press any key that has a letter (e.g. “a”-key), in the text editor you will see an “a”. If you keep the key pressed after a short delay the repeat mode kicks in and rapidly several further a’s are coming up. If you are in repeat mode for a certain key, repeat_ has a value different from 0 (most likely 1) and otherwise it will be 0. Especially for games, you may want to turn off the initial delay if you keep a key pressed and let the constant repeat mode kick in without delay. In SDL 1.2 I described here how to do it simply by using function called SDL_EnableKeyRepeat, this function is obsolete and does not exist in SDL 2.0 anymore!
A simple solution to the “repeat-delay”-problem: Instead of looking for the actual event being repeatedly triggered by an key event, use a switch which gets turned on if the key down event is occuring and which is turned off if the key up event is occuring. Example: Let’s assume you have a spaceship which should move left on pressing the “a”-key. Instead of changing it’s coordinates only once when the key down event is triggered, you start a switch (e.g. MoveSpaceshipLeft := true). The trigger is treated independently of the events treatment somewhere in the main game loop. As soon as the key up event is triggered for the “a”-key, the switch is turned off (e.g. MoveSpaceshipLeft := false).
I don’t know about meaning of the fields padding2 and padding3. Maybe they are kind of place holders for future developments or used internally.
Last but not least a very important field. The field keysym of SDL_KeySym type contains several information about the identity of the pressed key. In most cases we need to know which exact key has been pressed. Let’s have look into the SDL_KeySym record:
As you can see the first two fields contain keycode representations for identitifcation of the key pressed or released. Even though both fields seem to have again special records, namely SDL_ScanCode and SDL_KeyCode, they actually consist of only one field each, DWord (integer type) for SDL_ScanCode and SInt32 (integer type) for SDL_KeyCode.
The difference is that the scancode refers to a specific physical location of a key on the keyboard. The scancode is referenced to the typical US keyboard layout (QWERTY layout). The term “QWERTY” just refers to the the first six letters from left to right in the first row with letters on a typical US keyboard. For example: The German keyboard layout (QWERTZ layout) is similar to the US one (for most of the letter keys), though the “Y”-key and the “Z”-key have exactly opposite positions (hence QWERTZ for the German layout in contrast to QWERTY for the US layout). If I press the “Z” key on the German keyboard, the returned scancode will represent the “Y” key since the position of the key (independent of the layout) is equal to the position of the “Y” key on an US keyboard. Scancodes are layout-independent.
The key code refers to the virtual representation of the key according to the keyboard layout. Here you consider the layout, hence key codes are layout-dependent. As discussed before the scancode for the “Z”-key on a German keyboard will return that the “Y”-key has been pressed since the key has the location of the “Y”-key of an US keyboard. But the key code will not return it is the “Y”-key but it will correctly return that the “Z”-key has been pressed. The red marked output in the following image illustrates the result if the”Z” key is pressed on a German keyboard.
You may say, then I should always use keycodes to read out text input, right? – Wrong :-). In fact since SDL 2.0 it depends strongly on what you actually want to do. If you want to get a text or a single character from the user, you should neither use scancodes nor use key codes (anymore). (In former SDL 1.2 key codes or the unicode representation were indeed the preferable choice.) Treatment of real text input is not done this way anymore. We will discuss this case later. Anyway, this quote from the SDL 1.2 to SDL 2.0 migration guide sums it up excellently:
Use SDL_KEYDOWN to treat the keyboard like a 101-button joystick now. Text input comes from somewhere else.
Think of the famous T-shaped WASD key arrangement (arrangement of the four keys “W”, “A”, “S” and “D”) in the US layout, even if you keyboard without any latin letters, you may want to use these four keys to move a game character forward (“W”), left (“A”), backward (“S”) or right (“D”). The labeling of the keys doesn’t matter in that case and the keys are not used to input some text.
Again, keep in mind:
Never use key codes or scancodes to read out text input in SDL 2.0 (anymore)
The remaining _mod field is a 16 bit unsigned integer (corresponds to Pascal’s Word) and represents key modifiers (ctrl, alt, shift, num lock, caps lock, …). If one or more key modifiers are pressed the _mod value has a unique number for each key or the key combination. For example, the left shift key has the decimal value 1, the right shift key has the value 2, the left control (ctrl) key has the value 64, the right ctrl key has the value 128. If the left shift and ctrl key are pressed at the same time the _mod vlue will be 65 (1 + 64). Let’s assume you want to have your application to be quit by the user pressing the ctrl key and “Q”. So you read out the key code for “Q” and check if _mod is 64 or 128. Since there doesn’t seem to exist a table for key modifiers, here the most important ones:
Modifier key
UInt16 value
Left shift
1
Right shift
2
Left ctrl
64
Right ctrl
128
Left alt
256
Right alt
576 (64 + 512?)
Caps lock
8192
Num lock
4096
The unicode field is deprecated and will not be discussed here. By the way, also procedure SDL_EnableUnicode is gone, which turned on the unicode mode.
Text input in SDL 2.0
Let’s have a look in the next part of the code and learn about the correct text input in SDL 2.0.
With SDL 2.0 there is a new event type named SDL_TEXTINPUT. This one has been explicitly introduce to SDL 2.0 to make text input more flexible and easy.
Let’s have a look into the record structure of SDL_TextInputEvent.
TSDL_TextInputEvent = record
type_: UInt32; // SDL_TEXTINPUT
timestamp: UInt32;
windowID: UInt32; // The window with keyboard focus, if any
text: array[0..SDL_TEXTINPUTEVENT_TEXT_SIZE] of Char; // The input text
end;
In contrast to the event structure SDL_KeyBoardEvent where two event types (SDL_KEYDOWN and KEYUP) were available, at the moment for the event structure SDL_TextInputEvent only one event type, SDL_TEXTINPUT, is possible.
The timestamp and windowID field have been discussed earlier in great detail. They contain some general information about when this event was triggered and which application window had the focus when the event was triggered. You may scroll up to get more information about this.
Unique about this event structure is the field text which is an array of char elements. This array contains from 0 to SDL_TEXTINPUTEVENT_TEXT_SIZE char elements. SDL_TEXTINPUTEVENT_TEXT_SIZE has a size of 32 by default. Attention here, this doesn’t mean you can only have 32 characters in texts or something like this! It means that in the moment this event is triggered not more than 32 characters are submittable to the event. Keep in mind though, if you use a Western language with Latin characters you always only submit one chracter at a time by each key stroke.
So why there are 32 possible characters anyway then? – To understand this, it would be necessary to go deeper into the construction of non-Western languages and how words and sentences are constructed. The massive amount and complexity of the way words and sentences are constructed makes it so that these are constructed beforehand before they are submitted to the text field of SDL_TextInputEvent. These constructs are not simply created by just a single key stroke which could be read out. For these constructs there are 32 chars reserved. (I would be glad to give a more detailed explanation here, please feel free to contact me to improve this paragraph.)
Let’s go back to the code. If a SDL_TEXTINPUT event has been found, its record (SDL_TextInputEvent) can be accessed by text. Its text input information is stored in the text field which we have seen recently. That is why we can use
sdlEvent^.text.text
to access the text input information. In the first line of the code we simply print out the content of the text field. The string variable “text1” is used to store the found character and add to the characters which have been found before. This string is also printed out.
Note how special characters (e.g. currency symbols, French accent symbols, the German “ß” symbol, …) and capital letters are recognized correctly. Try this with key codes or scancodes (it is a pain).
Note also that function keys ( F1, Backspace, …) are not recognized as characters. It is your responsibility for them to work the desired way :-).