Menue screen of the engine. (Image: With permission of the author.)
Well, the introduction for this amazing project over at the Kirinn’s (developer) website says it all!
SuperSakura is a free, open-source visual novel engine that can run quite a few old games, mostly published by JAST, one of the first developers in the field. In addition to well-known localised titles, Japanese companies produced lots of fairly good games in the 90’s that were never translated.
And this project is fully written in Free Pascal and uses SDL2. Amazing work! I was happy to hear, the author got started with Free Pascal and SDL2 right here, with these tutorials :-)!
In the next chapter we will see how so called events are processed to handle input of any kind (keyboard, mouse, joystick,…) Before that I’d like to introduce a simple, yet powerful way to handle keyboard input.
The keyboard state represents the state (pressed = 1 or unpressed = 0) of all the keyboard keys, hence the key states. By
function SDL_GetKeyboardState(numkeys: PInt): PUInt8
we have easy access to this array.
The red rectangle can be moved by the WASD keys by reading out their key states.
The following code example will draw a red rectangle which can be moved by the WASD keys. Therefore we read out their key states on every cycle of the program loop.
program SDL_KeyboardState;
uses SDL2;
var
sdlWindow1: PSDL_Window;
sdlRenderer: PSDL_Renderer;
sdlRectangle: TSDL_Rect;
sdlKeyboardState: PUInt8;
Running: Boolean = True;
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;
// prepare rectangle
sdlRectangle.x := 250;
sdlRectangle.y := 250;
sdlRectangle.w := 10;
sdlRectangle.h := 10;
// program loop
while Running = True do
begin
SDL_PumpEvents;
sdlKeyboardState := SDL_GetKeyboardState(nil);
// ESC pressed
if sdlKeyboardState[SDL_SCANCODE_ESCAPE] = 1 then
Running := False;
// WASD keys pressed
if sdlKeyboardState[SDL_SCANCODE_W] = 1 then
sdlRectangle.y := sdlRectangle.y-1;
if sdlKeyboardState[SDL_SCANCODE_A] = 1 then
sdlRectangle.x := sdlRectangle.x-1;
if sdlKeyboardState[SDL_SCANCODE_S] = 1 then
sdlRectangle.y := sdlRectangle.y+1;
if sdlKeyboardState[SDL_SCANCODE_D] = 1 then
sdlRectangle.x := sdlRectangle.x+1;
// black background
SDL_SetRenderDrawColor(sdlRenderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderClear(sdlRenderer);
// draw red rectangle
SDL_SetRenderDrawColor(sdlRenderer, 255, 0, 0, SDL_ALPHA_OPAQUE);
SDL_RenderDrawRect(sdlRenderer, @sdlRectangle);
SDL_RenderPresent(sdlRenderer);
SDL_Delay(20);
end;
// clear memory
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow1);
//closing SDL2
SDL_Quit;
end.
To get the keyboard state, we define a unsigned 8 bit pointer variable sdlKeyboardState in the var clause. It points to the array of key states.
// program loop
while Running = True do
begin
SDL_PumpEvents;
sdlKeyboardState := SDL_GetKeyboardState(nil);
After setting up a SDL2 window and and preparing a SDL2 rectangle, the program loop is entered. Here we need to update the event queue by procedure SDL_PumpEvents. After that we can grab the keyboard state by the former mentioned function SDL_GetKeyboardState. The argument should be nil. These actions have to performed on every cycle.
// ESC pressed
if sdlKeyboardState[SDL_SCANCODE_ESCAPE] = 1 then
Running := False;
// WASD keys pressed
if sdlKeyboardState[SDL_SCANCODE_W] = 1 then
sdlRectangle.y := sdlRectangle.y-1;
if sdlKeyboardState[SDL_SCANCODE_A] = 1 then
sdlRectangle.x := sdlRectangle.x-1;
if sdlKeyboardState[SDL_SCANCODE_S] = 1 then
sdlRectangle.y := sdlRectangle.y+1;
if sdlKeyboardState[SDL_SCANCODE_D] = 1 then
sdlRectangle.x := sdlRectangle.x+1;
We now can check for the of any key in the array by sdlKeyboardState[SDL_SCANCODE_…] using its scancode as a handle (e.g. SDL_SCANCODE_ESCAPE for the escape key) and react as desired, e.g. exit the program loop or change the x/y coordinates of the rectangle. The scancode represents the position of the related key state in the array. A detailed description of scancodes in the next chapter. A list of all the scancodes shows all possible scancodes.
Could you please give a short description of Savage Vessels for those who have never heard of it?
Within fields of asteroids and fragmented freighters you scavenge and combat against robot vessels. At the same time you have to keep away from the surrounding void. With your carrier you got into this threatening area. In order to escape you have to determine your location repeatedly: leave the carrier, visit some navpoints, land again and move on. But the robots won’t let you. So you have to arm against them by gathering and crafting.
The visuals are based on top down pixel-art and a dynamic field of view. Modern physics provide inertia and collision. Sound fx creates an eerie atmosphere. It’s a spiritual successor to Teleglitch.
Why did you decide to choose Pascal as a programing language and SDL/SDL2 as a library for this project?
Pascal is my mother tongue. It’s capable of everything I need and I’m feeling comfortable with it.
SDL is versatile, platform-agnostic and plain.
What do you think is the most interesting Pascal/SDL/SDL2 project out there (besides of your own, of course :-D)?
This project is a program to load GoldSrc BSP files. The GoldSrc BSP file format has been derived from the id’s Quake 2 file format by Valve Software for their Half-Life game series.
It has been realized with
Lazarus, Free Pascal
SDL2, OpenGL.
The BSP Loader powered by Lazarus.Loading a WAD file and displaying a selected texture from it.Textured rendering of a scene (estate). The blue colorkey is not interpreted to be transparent yet.Scene: Oilrig.Scene: Assault.
02/08/2018, v0.1 alpha
Capabilities
Load BSP files and show contents of data lumps (exception: VIS Lump)
Load WAD files and render contained textures
Load BSP file and all WAD files which are necessary to render the fully textured scene
Navigate by simple camera through scene
To-Do’s
lightmapping from lightmap data
VIS Lump: treat it at all
collision detection
face culling
have spaces between textures in atlas texture to prevent bleeding-effect (esp. in tiled textures recognizable)
make blue colorkey transparent
sky cube
release the source code (if beta stadium reached)
Important Sources
BSP and WAD File Formats
I cannot state how important these documents were in understanding the structure of the BSP and WAD file formats. Without them, this project wouldn’t have been possible.
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.
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.
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.
Pure translation of SDL 2.0 source files. The original, modular structure of the header files is preserved, so the SDL2.pas is composed of many include files. Translations of SDL2_mixer, SDL2_ttf and SDL2_image are available, SDL_net seems to be missing so far. It provides MacOS X support.
Pure translation of the SDL 2.0 source files. All the header files of the original SDL 2.0 source code are combined into one large SDL2.pas (similar to JEDI-SDL's SDL.pas for SDL). Translations of SDL2_mixer, SDL2_ttf, SDL2_image and SDL2_net are available. The MacOS X support is unproven.
Bare Game is a game library which is put on top of the SDL2 library. It also allows for easy combination of SDL2 with Lazarus, which is a great plus. [Official website (baregame.org) is down, is the project dead?]
Now we go for a detailed discussion of them.
Modified header translations
Well, the Bare Game Library is a great project and I like the idea to provide an easy-to-use game development library very much but it isn’t suitable to learn pure SDL 2.0. Many functions are wrapped by new function names, you would learn much about the usage of Bare Game, fewer about SDL 2.0 itself. Also, the ease of use is traded for flexibility, e.g. there is just support for Windows and Linux, no Mac support, and you are more or less forced to use Lazarus IDE (which is an excellent choice, no question!) but for some reason you might not want to use Lazarus. The usage of libraries always trades ease for flexibility. And finally you are dependent upon a second project. If SDL 2.0 is updated, will Bare Game have updates, too? Bare Game is a great project at all, but for learning SDL 2.0 and if you keep the downsides in mind, it is not the best choice here.
Imant’s Gulis units allow for dynamic loading of SDL 2.0, hence your application decides during run-time if SDL 2.0 has to be loaded. This led to heavily modified unit files compared to the original header files. Also it is expected to use Lazarus. Although there are numerous cases where dynamic loading can be a great plus, for the tutorial and a wide variety of applications this is not necessary.
Pure header translations
The unmodified header translations of the original SDL 2.0 headers is the best choice when it comes to learning SDL 2.0.
The beauty of p_daniel’s SDL 2.0 units is the fact that there are exactly five files you need. They contain all the translations for basic SDL 2.0 and the official extensions SDL2_mixer, SDL2_ttf, SDL2_image and SDL_net. Unfortunately, the original comments are cut out. This is a major drawback to my mind. Sometimes you need just a quick look at the implementation of a function in the source to get how it works or what is wrong. Also the MaxOS support is unproven.
In contrast Tim Blume’s SDL 2.0 units kept the comments. The unit names differ slightly from the original names (e.g. SDL_audio.h got sdlaudio.inc instead of SDL_audio.inc ), which I don’t like, but it is acceptable. This allows for a better understanding in how SDL 2.0 works. Also it allows for better flexibility with regards to later projects.
Conclusion
This said, to my mind, the best choice to start with SDL 2.0 and Free Pascal: Go for Tim Blume’s SDL 2.0 units. For other purposes, other units may be the better choice.
Since the rise of SmartPhones, many wonder if Free Pascal/SDL2 development is possible for them, too. The good news is, yes it is possible! The bad news is, it is kind of toilsome to set up a development environment for Android (one of the two major operating systems of many SmartPhones).
Imants Gulbis informed me that he set up a Lazarus package (LazSDL2Design) which makes development of SDL2 applications with Free Pascal for Android fairly simple and integrates with the Lazarus IDE. Check out the instructions to make Lazarus/SDL2 ready for Android. The LazSDL2Design package relies on an own translation of SDL2 headers (LazSDL2) by him.
Recently, I get more and more questions and requests regarding the development of Android applications with SDL2 and Free Pascal. Since I’m not planning to do a tutorial chapter on this in the near future, I would like to share some resources which may help you to set your system up. – Contact me, if you are interested in sharing a tutorial on how to set up a SDL2/Free Pascal/Android development environment (or if you know further resources which should be covered here).
This step by step tutorial describes very detailed the setup of a SDL2/Android development environment under Window, though it aims for C++ development rather than Pascal development:
A new Chapter 2 has been added. In contrast to the classical Chapter 2 which explains the installation of SDL2 and Free Pascal for the Windows operating system, the new Chapter 2 explains the installation and configuration of SDL2 and Free Pascal/Lazarus in Linux. Initially I was trying to check for some troubles which got mentioned. Finally I ended up with a short, new installation chapter. A few minor changes have been added to the other chapters which are basically hints for Linux users.