In this lesson, we are going to learn how to make our own types out of existing types. After that, we are ready to start using a graphical drawing library called Raylib.
D has two kinds of aggregate types -- struct
and class
. For now, we are going to focus on struct
types.
"struct" is short for "structure". Just like a variable, we need to first declare a struct
before we can use it. You will generally declare structs along-side any functions, though it is also possible to declare them inside functions.
The syntax to declare a struct
goes like this:
struct Point {
int x;
int y;
}
Inside the struct
you will see that we can declare variables just like we can in a function. We provide the type of the variable, followed by the identifier of the variable. Inside a struct, a variable declaration defines a field member.
A field of a struct tells the compiler that every time you create a variable of that struct type, it will need to reserve some memory for that field. We can say the struct contains the field.
Remember that a struct is a type, like int
or string
. So once we have defined a struct, we use it like we would those types.
Here is what it might look like to create variables of type Point
like we defined above:
Point pt1 = Point(1, 2);
This defines a variable named pt1
that is of type Point
. What this means is that the compiler has set aside enough memory to hold a Point
, including two integers, x
and y
. Notice that the assignment repeats the type name. Why is that? Because we need to create a value of type Point
to assign to pt1
. In this case, Point(1, 2)
gives us a Point
value with the x
field set to 1, and the y
field set to 2.
But how do we use those values? We access fields of a Point
by using a single dot or period after the variable name, followed by the name of the field we want. Using that syntax, we can read or write any of the fields:
writeln(pt1.x); // writes 1
pt1.y = 5; // change the y value to 5
writeln(pt1);
That last line actually prints the entire struct. writeln
is smart enough to know the type, and how to print all the fields inside it. Go ahead and try this out to see what it does!
Each time we create a variable of type Point
, it contains a different copy of the x
and y
fields. Just like we can declare more than one int
, we can declare more than one Point
and they are separate things. This is why we can't just write x
, we must write pt1.x
to let the compiler know which Point
we are talking about.
Point pt1; // defaults to (0, 0)
Point pt2 = Point(4, 5);
writeln(pt1.x, " ", pt2.x); // writes 0 4
pt1 = pt2; // assigns every field of pt1 to pt2
pt1.x = 1; // changes only pt1.x, pt2.x is not affected
writeln("pt1 = ", pt1); // Point(1, 5);
writeln("pt2 = ", pt2); // Point(4, 5);
You can even create an array of struct instances. Just like an array of int
you can index the array of Point
. If you want a specific x
out of the array, you first index, and then use the .x
syntax to access its x
field:
Point[] ptarr = [Point(1, 2), Point(3, 4)];
ptarr[1].x = 100;
writeln(ptarr); // [Point(1, 2), Point(100, 4)]
By default, structs support comparison for equality. This means you can check if two Point
s are equal (e.g. p1 == p2
) or not equal (e.g. p1 != p2
). There are ways to add support for the other kinds of comparisons, but for this class, we will not need this feature.
There are also ways to add math operations on structs. We will not be learning about how to add these in the introduction class, but you may notice that we will be using structs that do support these things.
The raylib library is a simple computer gaming library that we will be using to make simple 2 dimensional games for the class. You can follow the link to explore what raylib is capable of. Note that raylib can do full 3D graphics, but the math involved with 3D is much more advanced than 2D, and there is plenty to learn in 2D!
Of particular interest may be the "cheatsheet" link on the web page. That contains many of the functions that raylib supports, so you can get an idea of what things you can do with it.
Before we start using raylib, we will learn a little bit about how to add a library dependency in D. A dependency in D is used by the tool "dub". You may not have noticed this, but whenever we build in Visual Studio Code, it is using dub to build the project and run it.
To add a dub dependency for raylib, follow the instructions here. These are also linked from the home page so you can find them easier.
The first raylib application you will write is very similar to the "Hello World" application we wrote originally with D. Except this time, we will write it graphically to the screen!
In order to use any raylib functions, we must import the library using import raylib;
. After that, the main function should be copied exactly as below. I will explain every part so you can understand what the compiler is doing.
import raylib;
void main()
{
InitWindow(500, 500, "Hello!");
SetTargetFPS(60);
while(!WindowShouldClose())
{
BeginDrawing();
ClearBackground(Colors.WHITE);
DrawText("Hello Raylib!", 10, 10, 50, Colors.BLACK);
EndDrawing();
}
}
import raylib;
The import
statement tells the compiler to learn all the raylib functions and types, just like std.stdio
tells the compiler how to use writeln
.
InitWindow(500, 500, "Hello!");
The first line of the main
function does 2 things -- it sets up the raylib library, and creates a window to draw our program into. The InitWindow
function is defined like this inside raylib:
void InitWindow(int width, int height, const(char)* title)
You can find this on the cheatsheet. You can notice how the parameter names help you understand what the point of the parameters are. This is some of what I mean when I say that code is made for you, not the computer. The computer doesn't care what you name your parameters, because it doesn't understand English!
The first two parameters define the width and height of the window that will be created. The third parameter is a bit weird, and the type is something you haven't seen before. This is actually A C type for a string
. Since raylib is a C library, it must use C parameter types. Luckily, D will translate string literals into C string literals automatically. For that reason, we can just treat it as a string. The parameter tells raylib what text to use as the title of the window.
SetTargetFPS(60);
The next function call sets the target "frames per second" to 60. This is actually an important step, because otherwise, raylib will draw frames to the screen as fast possible. On my machine, that means about 1000 to 4000 FPS. Since my screen only refreshes 60 times a second, this is a huge waste of computing resources. Other processes on the computer will run slower, and we will have to do more calculations than necessary.
while(!WindowShouldClose())
{
BeginDrawing();
...
EndDrawing();
}
After this, we start a while
loop. In game programming, this is called the Game Event Loop of the program. It runs as long as we need to display the game to the screen. You will notice the stopping condition as !WindowShouldClose()
. From the cheatsheet, we can tell that the WindowShouldClose
function returns false
, until the user requests the game to end (usually by pressing the "X" button on the window, or presses the Escape key). The !
turns the false
into a true
, or a true
into a false
. Since the condition in which we want to run the event loop is normally false
, we need to change that to a true
for the loop condition.
Inside the loop, everything between the BeginDrawing
function call, and the EndDrawing
function call defines how one frame of the game will be drawn. Raylib uses these functions to help it run the timing for FPS, to do any kind of audio changes, or anything else it might need.
ClearBackground(Colors.WHITE);
The first drawing call clears the background of the window to a specific color (in this case WHITE
). This is important to always call first. If you don't do this, the background might be weird garbage (you can try omitting this if you want, and see what happens). If you call this later than the first drawing call, it will overwrite anything you already did.
DrawText("Hello Raylib!", 10, 10, 50, Colors.BLACK);
The second call draws text onto the screen. The DrawText
function looks like this:
void DrawText(const(char)* text, int posX, int posY, int fontSize, Color color)
There's a lot in here. Like before, the C const(char)*
is an odd type, but you just need to know it's a C string. This is the text that will be drawn to the screen. For now, we are passing a normal string literal, but we will examine in future lessons how to customize the text. The next 2 parameters give the X and Y coordinates of the top left of the text being drawn. Remember that X goes from the left side of the window to the right, and Y goes from the top to the bottom. So this will put the top-left corner of the text 10 pixels from the top, and 10 from the left.
The next parameter is the font size of the text, in pixels, in this case 50
. And finally the color to draw the text. Any time a Raylib function needs a Color
type, you can use the Colors
enumeration. This type has a lot of predefined colors that you can use without having to know how colors are created. In this case, we use the BLACK
color, to contrast with the WHITE
background.
When you run the program, it displays the text "Hello Raylib!" on a white background, 60 times a second. It's not incredibly interesting, but most "hello world" programs are not that interesting!
Note how the text is 10 pixels from the left edge of the window, and 10 pixels from the top edge. The bottom and right side of the text just land where they land.
Raylib has many different drawing functions, and we will be exploring a lot of these in the next few weeks!
For the exercise this time, we are going to make the text move!
Just like any value in a program, the x and y coordinates passed to the DrawText
function can be stored as variables. Instead of passing 10, 10, we will pass these variables. Then, each time through the event loop, we can modify these variables.
Let's use the Point
struct so you can get familiar with that syntax.
Here is the exact same code, but with the parameters changed to variables. Do something to the pt
variable while inside the loop to animate the text!
import raylib;
struct Point
{
int x;
int y;
}
void main()
{
InitWindow(500, 500, "Hello!");
SetTargetFPS(60);
// note we declare this outside the loop so it does not get reset for
// each loop iteration.
Point pt = Point(10, 10);
while(!WindowShouldClose())
{
// alter pt.x and pt.y here!
BeginDrawing();
ClearBackground(Colors.WHITE);
DrawText("Hello, Raylib!", pt.x, pt.y, 50, Colors.BLACK);
EndDrawing();
}
}
There is no right or wrong answer here, you can do whatever you like inside the loop to the pt.x
and pt.y
variables. But note how what you do to the variables affects the text! Remember that to change a variable you use the =
operator, like pt.x = 50
. To do math with a variable, you can use the appropriate operator, like pt.x + 5
.
Are there any other things that can be changed to a variable?
Return to home©2023 Steven Schveighoffer