D is the programming language we are using for 2d games programming. This page is going to be populated with concepts that allow you to write your code and get it working. It does NOT cover how to install or build D programs, which is covered in Installing D.
This also is not going to cover every programming concept, it should be enough to get you started writing some D programs. There are far far more D concepts than what we will discuss here, and if you want to go further, you might find it useful to take the D Tour. We will start with some definitions for those who are truly beginners, and then introduce enough to get some things to happen!
Here are some terms you should know, and what they mean when reading about programming languages in general, and D in particular:
buildHouses
, then it might have a parameter which is the number of houses you want it to build.Note that the above is not a completely extensive list of all definitions for programming! But when we talk about how things look or are written in D, just like learning spoken languages, it helps to define what the terms mean.
In D, A declaration tells the compiler about something that you want to use later. Most declarations are types, variables, or functions. To declare a new variable, we first list the type, and then list the name of the variable. The name is very very important, as this is the only way you can access the variable later:
int var;
Let's look at this statement and learn what the parts mean. int
is D's basic type for an Integer. An integer in mathematics is a whole number (no fractional or decimal part) which goes from negative infinity to positive infinity. Computers can't conceptualize infinity really, and they have to store this number in memory, which is finite. So this number has a limit to how low or how high this number can go. In D, the int
type can be a value between roughly -2 Billion and 2 Billion. Why 2? That's a bit complicated, but has to do with how computers represent numbers (hint, it's stored as a Binary Number).
After the type, we see the word var
. This is where we list the name of the variable, which we will use later. In D, names are case sensitive, which means the name var
is different from the name Var
. A defined name can only consist of lower and upper case letters, an underscore, and digits (but only if the digit is not the first thing). You cannot create more than one variable with the same name in the same context.
After the name, we see a semicolon. This tells the compiler that the declaration is done. Think of it like a period at the end of a sentence. D is a programming language without significant whitespace. Whitespace is anything that is not letters and numbers (i.e. spaces, tabs, new lines). Not to say that whitespace isn't significant in D, as it separates many things (including the type and name above), but what kind and how much whitespace is not significant. In other words, the following statements are exactly the same as the one above in D (though I would not recommend writing your code this way):
int var ;
int
var
;
This allows you to write code in any kind of spacing you want. But part of the drawback is that you need to tell the compiler when you are done. You will find as you learn to program in D, you will make a lot of mistakes regarding semicolons. You will forget them, or you'll add extra ones, and the results usually end in the compiler having lots of errors. Without semicolons, the compiler is lost as to knowing what you want. As I said above, the language MUST be exact and unambiguous. The semicolons help define where things end and a new thing starts.
If I can translate this to English, the declaration above is saying
Compiler, I want you to make a new variable named 'var', and its type should be an integer.
Let's look at another declaration:
string var2 = "hello";
Here, we see most of the same things, except this time, we used a type called string
. In all programming languages, a "string" is text. Why is it called a string? Because it is a list of letters and/or digits "strung" together.
The name of the variable is var2
, which is now what we can use to refer to that variable. Remember, all variables have to have a name given to it.
Something new, however, is the code = "hello"
. This is assigning a value to the variable. All variables in D start out with an initial value. For int
it is the value 0
. For string
it is the empty string (no text). But in this case, we want the value to start out with the text "hello". The quotes here are very important. They tell the compiler where the data to store in the string stops and ends.
If written in English, this would say
Compiler, I want you to make a new variable named 'var2', its type should be a string, and it should hold the text 'hello' inside it.
So in D, the syntax for declaring a variable is one of:
type variable_name; type variable_name = initial_value;
Here is a list of some of the basic types that D has, so you can get an idea what a variable can hold. Here, I will list the type name, the initial value, the minimum and maximum values (for numbers), and an example declaration with an initial value:
Type name | What does it store? | Initial value | Range of values | Example |
---|---|---|---|---|
int |
An Integer number | 0 | -2,147,483,648 ~ 2,147,483,647 | int var = 5; |
string |
A string of text | Empty string (no data). | Not Applicable | string var = "Hi!"; |
byte |
A small integer number | 0 | -127 ~ 128 | byte x = -5; |
float |
A Decimal or "floating point" number | An invalid number | -3.40282 x 1038 ~ 3.40282 x 1038 | float pi = 3.14159; |
char |
A single letter or digit in text form. | An invalid character | Not Applicable | char c = 'h'; |
This is not the full list of basic types that D allows, but it has most of the important ones. There is one other type that means "has no type", and in D that type is named void
. We will learn uses for that later. Creating new types in D is a more advanced topic we will learn later, and is created by combining the basic types into one thing.
The most important declaration by far is the function declaration. Let's look at a simple function, and examine what each part means:
int doubleIt(int val) {
return val + val;
}
Ignoring the fact that we haven't talked about statements yet, let's look at the signature of the function. The signature is the first line that declares a function. In D, a function is denoted by the parentheses.
The first thing in the declaration is the word int
. This is the type of the value that will be produced when you execute it. Every function that produces a value must declare the type of the value it is producing. If it is not producing a value, then the type should be void
which represents "no value".
The second thing is the word doubleIt
. This is the name of the function we will use to execute it. Like variable names, this is restricted to letters, numbers and underscores, no spaces, and no initial number.
Now we see the opening parenthesis. The parentheses both signal that this is a function instead of a variable, and also provide the start of the parameters of the function. Inside these parentheses we can find almost an exact syntax for variable delcarations (without the semicolon). Each of these parameters becomes a variable ONLY inside the function body itself. More than one parameter would be separated by a comma.
And finally, after the close of the parentheses that denotes the parameters, we see an opening curly brace. This signifies to the compiler that the beginning of the function instructions is starting. Everything inside these braces will be considered part of the function.
The English translation would be:
Compiler, I want you to make a new function named 'doubleIt', which produces an 'int' and accepts a single 'int' as its parameter. Inside the function, I will refer to that parameter by the name 'val'.
So the syntax for declaring a function is:
type function_name(parameter0, parameter1, ... , parameterN) { function_code }
The function body (or code) can contain more declarations (including other functions!) and statements.
We already defined what a comment is, but how do you write a comment in D? D uses 3 forms of comment, one that comments everything until the end of the line, and two that allow comments between two markers. We will mostly use the end-of-line comments as it is simple to understand. Your editor you use to write D code should use colors to help you see which pieces are code and which pieces are comments (this web page does too).
The end-of-line comment syntax always starts with two forward slashes (//
). After that, everything until the end of the line is considered to be comment, and not part of the code.
int x; // This is an integer
The next comment style uses forward slash and asterisk to denote the start (/*
), and an asterisk followed by a forward slash to denote the end (*/
). Everything between these two makers is not considered to be part of the code.
/* The following initialization is commented out, so x will
have the value 0 unless you remove the comment markers */
int x /* = 5 */;
The third comment style uses slash and plus (/+
) to denote the start, and plus slash (+/
) to denote the end. These function exactly like the previous comment type except they can be nested (meaning you have a comment within a comment).
/+ I'm commenting out all this code, which includes a commented note
/+ Declare a variable +/
int x;
+/
Up until now, all we have told the compiler to do is arrange and name things. We were just declaring how to store variables, and what we will call them. We haven't actually written any executing code yet!
A statement is how we tell the compiler to do something. There are a lot (way more than I will tell you here) of statements that D supports. This section is to help you get started. Please see the Dlang Website for a complete list of supported statements.
I'm going to give a brief discussion of each statement. There are a lot to get through, so I don't want to bog this list down with a lot of details. If you need something explained further, let me know and I can add to it.
Before I discuss the statements, let's talk about expressions. An expression in D is syntax which yields a value. An example of a simple expression is 5 + 6
. This yields the value 11
. Math expressions are some of the most basic expressions you need to use. So here is a list of all the math expressions that you probably will need (there are more, but these are the most important):
Operation | Syntax |
---|---|
Addition | a + b |
Subtraction | a - b |
Multiplication | a * b |
Division | a / b |
Note that math operators follow the same operator precedence as real-life math.
When you call a function, you provide just the name, followed by parentheses, and you provide one expression for each parameter that the function accepts, separated by commas. The call hands execution over to the function, and when the function returns, the value produced by the function is used in place of the call.
int var = 5 * doubleIt(4);
Given our previous definition of doubleIt
, the function will add 4 to itself, and return the value 8. Therefore, the variable var
will contain the value 40.
Some things in D, such as variables and types, have properties which are like using a piece of something. If we could equate this to the real world, you can say that a person has a property named 'height' which would have the value of that person's height. Such properties are accessed using the syntax:
thing.prop
The dot in the middle tells the compiler you are looking for a property of thing
named prop
. As an example, there are several properties of basic types which tell you information about that type. Instead of knowing the exact number that is the minimum int, you can use int.min
. The maximum int is int.max
.
The simplest statement is to assign a value to a variable. It goes like this:
var = 5;
This looks almost the same as the declaration of a variable! But because we didn't give it a type to use, the compiler assumes this is an already existing variable, and we should assign the value 5
to it. If something looks fishy (like you didn't actually declare the variable var
before this statement), then the compiler will give you an error.
Probably the most important code construct is the branching, or if statement. An if statement checks to see if a condition is true or false. If it is true, the code inside one section is executed. Otherwise, another section might be executed.
Here is a simple if statement:
if(val == 5) {
val = 10;
}
This if statement checks a condition, and if it's true, sets val
to the value 10.
A condition is a boolean expression which can be true or false. If it's true, the code inside the curly braces following the if statement are executed. Otherwise, the code is skipped.
The condition expression is val == 5
. This reads val equal to 5. The double-equals is used for comparing two values for equality (remember, a single equal is used to assign a value). There are six comparison expressions, here are all of them:
a == b
- This is true if a
and b
are equal.a != b
- This is true if a
and b
are not equal.a < b
- This is true if a
is less than b
.a <= b
- This is true if a
is less than or equal to b
.a > b
- This is true if a
is greater than b
.a >= b
- This is true if a
is greater than or equal to b
.If the }
is followed by the word else
, then you can have a second block of code that is executed if the condition was false.
if(val == 5) {
val = 10;
} else {
val = val - 1;
}
If you have multiple conditions to check, then you can use boolean logic to combine them. For example, to execute a block of code if the variable a
is 5 and the variable b
is greater than 0, the condition will look like this:
if(a == 5 && b > 0) { ... }
The ...
represents code that would execute if the condition was true.
There are 3 boolean operations, and using those along with parentheses for precedence, we can express quite complex checks. The three operators are:
a && b
- true if both a
and b
are true.a || b
- true if either a
or b
is true.!a
- true if a is false. This is the "not" expression.Another very important statement is the foreach statement. Looping is what allows the computer to process all things in a list. The foreach statement looks like this:
foreach(int element; listOfInts) {
writeln(element);
}
In this statement, listOfInts
is a pre-existing list of int
.
element
is a new variable declaration, to tell the compiler what to call each thing it's looping over.
Inside the body of the loop, we are calling a function writeln
with the argument element
. This actually writes the value of element
to the screen. writeln
is a very important function that we will use quite often.
I'll talk about what this syntax means later, but just assume that this is how you would declare listOfInts
:
int[] listOfInts = [1, 2, 3, 4, 5];
in that case, the output from the program would be:
1 2 3 4 5
If you want to affect the data inside an array, you need to specify that the variable for the loop is ref
. I won't get into all the places that you can use ref
, but this is an important feature for loops.
To show how it works, here is an example:
int[] arr = [1, 2, 3, 4, 5];
foreach(i; arr) {
i = i + 1;
}
// this outputs [1, 2, 3, 4, 5], nothing has changed.
writeln(arr);
foreach(ref i; arr) {
i = i + 1;
}
// this outputs [2, 3, 4, 5, 6], the elements of the array were changed.
writeln(arr);
This is true for arrays of structs as well. If you call methods on the struct being looped, it will not affect the one in the array, unless you use the ref
designation.
A separate form of the foreach
loop can loop over a range of numbers. This can be handy if you want to run something a certain number of times, but you don't have a list of things to loop over. The syntax for this looks like:
foreach(i; 0 .. 5) {
writeln(i);
}
This code would output:
0 1 2 3 4
The exact syntax for this kind of loop is foreach(variable; start .. limit)
. The variable is the name of the loop variable which will count up. Even if you don't use this variable, you have to declare one. The start value tells the loop which value to assign to the variable first. The limit value tells it where to stop. Note that the variable stops before it reaches the limit, so you don't get a loop with the value set to the limit value. Notice the loop above only prints out 4
even though the limit is 5
A while loop is a loop that executes while a condition is true. Instead of needing a list of things to loop with, you can pick any arbitrary condition to determine when the loop should stop.
For instance, here is a program that waits for you to type the word "quit", and echoes back the word you typed:
string answer = strip(readln());
while(answer != "quit")
{
writeln("You wrote ", answer);
answer = strip(readln());
}
The code strip(readln())
reads a single line of text from the user, and removes all spaces from the beginning and end.
The while condition waits until you typed "quit", and then exits the loop.
The writeln
function call inside tells you what you wrote.
And finally, the answer is assigned to the next thing you type before looping (and checking to see if you wrote "quit" this time).
The break statement is used to exit a loop early. It immediately continues execution at the first statement past the loop, without running anything in between.
The continue statement is used to skip the rest of the current loop execution, and go to the next loop execution. For example, if you wanted to display only numbers that were less than 3, then the code might look like:
foreach(element; listOfInts) {
if(element >= 3) {
continue;
}
writeln(element);
}
Note that I omitted the type for element
. This is allowed in a foreach statement if the compiler knows what the type should be.
The return statement tells the function to exit, and also optionally provides a value to produce to the caller of the function. If the function returns void
, no return value is provided.
Here was our doubleIt
function again:
int doubleIt(int val) {
return val + val;
}
A return statement is required for any function that returns a type other than void
. If you forget a return statement in a function that needs one, the compiler will not build your program and produce an error.
D uses import
statements to tell the system where to find the definitions of other things such as functions (like writeln
) and other types. The import statement is straightforward, and the compiler might even suggest the correct import statement if you try to use functions that aren't defined.
import std.stdio;
Aside from the basic types in D, there is another built-in type that is important to understand. An array is a list of values, with an index used to retrieve each value. There are three types of arrays in D, we will mainly use just two of them.
A dynamic array is a list of values which is indexed using an integer number. The number of values inside the array is set or retrieved using the length
property. Each value in the array is accessed by using the index syntax. The first value is stored at index 0
, and the last item is stored at the index which is one less than the array's length.
An array is declared by putting [ ]
after a type, which indicates it is an array of that type of value. This is an array of ints named arr
:
int[] arr;
An array starts out with zero values. To add a value to the end of the array, we can use the append operator - ~=
.
int[] arr;
arr ~= 42; // add a 42 to the end
writeln(arr.length); // this should output 1
writeln(arr); // this should output [ 42 ].
To set or get a value from the array, we use the index operator, which is a pair of square brackets containing a numeric index.
int[] arr;
arr.length = 5; // set the length to 5, all the values are 0
arr[0] = 4; // set the first value to 4
arr[2] = 6; // set the third value to 6
writeln(arr); // should output [4, 0, 6, 0, 0]
An array's initial values can be set by enclosing a list of values by square brackets, with the values separated by commas.
int[] arr = [1, 2, 3, 4, 5];
Arrays are one of the most important types in programming, because we sometimes don't know how many of one thing we need to store.
An associative array stores values using any type of index, not just numeric, plus those indexes can be arbitrary. If we wanted to store just the index 0 and the index 1,000,000, a dynamic array would have to contain 1,000,001 values! This is very inefficient, because the computer must use up some of its memory to do this.
But using an associative array allows us to just store those two indexes. We can even use negative indexes! An associative array is declared very similarly to a dynamic array, except inside the square brackets, we put the type of the index:
int[string] arr;
The variable named arr
is now an associative array with string
indexes, and int
values. Setting a value involves using assignment on a specific index:
int[string] arr;
arr["hello"] = 42;
writeln(arr.length); // outputs 1
writeln(arr); // outputs ["hello": 42]
We can remove values by using the remove
method:
int[string] arr;
arr["hello"] = 42;
writeln(arr.length); // outputs 1
arr.remove("hello"); // remove the value using the index
writeln(arr.length); // outputs 0
An associative array's initial values can be set by enclosing a list of values by square brackets, with each index/value pair looking like index : value
, separated by commas.
int[string] arr = ["hello": 1, "student": 50];
Associative arrays are sometimes called Dictionaries because they allow you to put arbitrary values with arbitrary names or indexes, and they are efficient at retrieving the value stored at a given name, just like a dictionary.
A struct
is a way to group together related items, and also easily operate on them as a whole. Consider a 2-dimensional point, which has an x
component and a y
component. We can keep track of both of those things individually, but that means you need to pass them individually to other functions, and also that you have to keep track in your head which variables go together. In addition, keeping track of an array of points would mean keeping an array of x coordinates, and a separate array of y coordinates.
Using a struct
means we can group those two items together, and they always travel together no matter what we want to do with it. It is important to note that when you create a struct
, you are creating a new type. This means that once you declare it, in order to use it, you need to declare variables of that type.
Declaring the Point
struct with int
coordinates would look like this:
struct Point {
int x;
int y;
}
Now, we must declare a variable to use it. To access the x
and y
fields, we use the property expressions as above.
Point pt;
pt.x = 5;
pt.y = 7;
writeln(pt); // Point(5, 7)
The syntax for declaring a struct is like this:
struct Name { declarations... }
The declarations can be either variables (fields) or functions (methods) (and other stuff, but we won't get into that just yet).
A field is a variable that is declared inside the struct. Every variable of the struct's type will have all of its declared fields as members. A member is a piece of a struct. It is important to note that the field doesn't exist until you declare a variable of the struct's type.
A field can have any type, including the type of a struct! This way you can build large state types that help you organize your variables.
A field's default value can be set by declaring a value inside the struct itself, just like we would assign a default value to a normal variable.
A method is a specialized function inside a struct which has access to the struct fields when running. When you call a method, you specify the struct variable, followed by a period, and then the method name + arguments. A method call is a function call with the added piece that you have access to the struct data.
To demonstrate, the following two functions are exactly the same:
import std.math; // for sqrt
import std.stdio; // for writeln
struct Point {
int x;
int y;
float length() {
return sqrt(x * x + y * y);
}
}
float externalLength(Point p)
{
return sqrt(p.x * p.x + p.y * p.y);
}
void main() {
Point p = Point(3, 4);
writeln(p.length());
writeln(externalLength(p));
}
Note how when using the method, we call it by using p.length()
to tell it which Point
to call the length()
method on. Also note how inside the length
method, we do not have to identify where the x
and y
values come from. Because we are calling a method, it already knows which ones we mean.
To make a struct we can use a struct literal. This is done by naming the struct to make, and then setting the value of all the fields using a function call syntax. For example:
struct Point {
int x; // default value 0
int y = 5; // default value 5
}
void main()
{
Point p = Point(1, 2);
Point p2 = Point(1);
}
The variable p
now has value 1
for x
and value 2
for y
. You can just set the value for some of the member fields if you want by only passing in the values for the first fields. The variable p2
is a Point
with x = 1
and y = 5
.
©2022 Steven Schveighoffer