Chapter 1, Introduction
What are audio games?
Audio games are games specifically made for the blind, audio games use sound and text to speech as a medium to convey information, instead of using graphics.
Audio games are primarily targeted to the blind, as in, completely blind, or close to it, audio games can come in any shape or form, and are not limited to one type or genre, there are fps audio games, rpgs, racing games, and the list goes on.
The only difference is the fact that audio games convay information to the user differently compared to video games.
What is this book about?
After a lot of consideration and conversation with a lot of friends and people who are attempting to become audio game developers, I found out that the entry barrior is quite colossal and some of our friends in the audiogame development field are unfortunately simply unfriendly, don't know how to answer a question, or are simply way too arrogant for their own good.
So this came out, it will not be perfect, it probably won't look professional because my english isn't the best, but I could do it and no one did so I thought why not. I am aware there are some people that won't appreciate this, I've been told, but I also think that there are people that would, I would have per instance, and I kno people currently who would.
So, we would start first with basic programming flow, variables, etc, then we'll get to the juicy stuff, don't worry, I know a lot of us don't like the boring info dumps with nothing to show for it, so we'll try to do small things along the way to keep this entertaining. And by the end you'll hopefully understand how you can construct an audio game.
So, what programming language?
C sharp, it's the language I'm most comfortable with, and I honestly think it's a great language, it's my personal opinion though, so it's fine if we both disagree, but this tutorial will mostly use this, the ideas will likely apply to other languages, but you'll unfortunately have to do the translation yourself :()
So, hold on tight and lets go.
chapter 2: Installation.
Installing the C# compiler.
before we start you have to install the C# compiler and package manager. It's called dotnet
- Go to the dotnet download page.
- Under the latest version, at the time of this book's writing dotnet9, click on the "Download .NET SDK" Link.
- Launch the installer and follow the installation steps.
after the installation is successful, to confirm everything is running, first, open your system's terminal.
- On windows:
- press windows+r.
- type cmd and press enter.
- On macos:
- open spotlight, cmd+space.
- and type terminal, then press enter.
- On linux: press ctrl+alt+t.
after opening the terminal, type
dotnet --version
if you got something similar to
9.0.101
Note that numbers shouldn't exactly match, in jan 2025, dotnet9 is the latest version, this might not be the case for you, so it's fine if it was dotnet10.
if everything is well, then you are ready to continue on, otherwise, try a reinstall.
Chapter 3: Your first code.
Now that we'd got everything set up, it's time to start with the fun stuff.
first off, open your terminal, you should know how to do that by now.
afterward, create a new directory anywhere you like, let's say I want a directory called projects then I put my game under audiogame.
mkdir projects
cd projects
mkdir audiogame
cd audiogame
now you might be a bit overwhelmed if you have no experience with command line previously, don't worry it's simple.
- mkdir creates a directory.
- cd changes your current directory, by default, when you open your terminal, it goes to your home folder, for example, /users/mohamed.
now that we created the directories we need, let's create our C# project, a project is where all your source code is
dotnet new console
now, if you check the folder where we did put our project, projects/audiogame, or wherever you decided to, you'll find a few things.
- obj
- audiogame.csproj
- Program.cs
not much to be concerned about right? Yep, simple, in fact, it's less than that.
obj is a form of cache that the compiler and package manager generates, so ordirnarily we never touch it, so you can forget about it entirely.
audiogames.csproj is where our project configuration goes, and where we could define a phew things, for now, we don't need to touch this.
Program.cs is what we care about, open it, it should look like.
// See https://aka.ms/new-console-template for more information
Console.WriteLine("Hello, World!");
if you have prior experience with older versions of c# you might be taken a back because this is way less than you've expected, but the link says it all
The features that make the new program simpler are top-level statements, global using directives, and implicit using directives. The term top-level statements means the compiler generates the class and method elements for your main program. The compiler generated class and entry point method are declared in the global namespace. You can look at the code for the new application and imagine that it contains the statements inside the Main method generated by earlier templates, but in the global namespace. You can add more statements to the program, just like you can add more statements to your Main method in the traditional style. You can access args (command-line arguments), use await, and set the exit code. You can even add functions. They're created as local functions nested inside the generated entry point method. Local functions can't include any access modifiers (for example, public or protected).
too much blah blah to basically say that you can now put code in Program.cs and treat it like a Program class and main method exist. Similar to python, etc.
if you have no experience with C# and you're confused, you don't need to worry, this is unimportant, it's just a simple clarification, and it won't affect you.
the, //, in C# is a comment, comment is text in the middle of code that doesn't affect the program, it's useful for documentation, or it could be a todo, or a reminder, or anything, basically, having it or not having it doesn't affect the final program, and gets completely ignored by the compiler, but is useful because it's extra information for possibly confusing code, etc.
now, let's explain this line
Console.WriteLine("Hello, World!");
classic, hello world pretty much is a standard for a learning a language, but let's explain it step by step since there are people who have never coded before.
- Console is a class, think of a class as something that holds data and can perform operations. It's like a folder with text files and executable files that when clicked can do things.
- the . means that we're accessing something inside that class, again think like how you access what's inside a folder with /
- WriteLine is the name of a method, it's like a reusable command that performs a specific action, in this case, output text to the terminal.
- the ( (left paren) is the start of a method's argument list. Method arguments are like the details you tell the method, in this case, we tell it what text do we want to display.
- "Hello, World!", is a string, string is basically text, and this is the argument we give the method, as said above. A string is always wrapped between two " (quotation marks)
- the ) (right paren), We are done giving the method arguments, so we close the argument list.
- the ; (semicolon), is something we use when this line have ended. We already called the method from the Console class, given it our arguments, an closed it, now, we need to tell the compiler we are done with this line, so we put a semicolon.
and that's it, now, try running it.
$ dotnet run
you should get a message that says Hello, World!
That's it, you can go play around with the text displayed, or you can go add an extra WriteLine, or you could try removing something and see what happens.
Chapter 4: Variables.
Creating a variable and variable types.
one of the most important things in programming is variables, variables contain data, and in C#, variables are strongly typed, which basically means, when you declare it, you need to tell the compiler what will this variable contain, and once created, you can't put another type of data.
let's give an example.
int x = 5;
int y = 10;
int z = -10;
x and y are variables of the type int, integer, ints contain normal numbers, 1, 2, 3, we also have floats and doubles for numbers with fractions.
float x = 0.4f;
double y = 0.123456789;
floats and doubles are similar, but doubles are bigger, can hold more decimal point numbers, and are more reliable. If you're not sure which one to use, go with double.
we also have bool(boolean) That can store 2 values only, true, or false.
bool isEnemy = true;
bool isFriendly = false;
now, IF you remember in our last chapter, when we explained
Console.WriteLine("Hello, World!)");
we said that "Hello, World!" Is a string, so you would wonder, how would I write it like the ints floats and doubles above?
Well, the answer is
string x = "Hello, World!";
so as you can see from our examples above, a variable has 3 parts.
- Type: Type of the variable, string, int, float, double, bool, or any other type.
- Identifier: Basically the name of the variable, there are a few rules for this, name must either start with a character or an _ (underscore) it could contain numbers an characters, but the name can't begin with a number.
- The value, for example: 5, 0.4f, "Hello, World!".
Using and changing variables.
so now after we explained how to create variables, let's explain their uses.
since variables store data, we must be able to read them.
string greeting = "Hello, World!";
Console.WriteLine(greeting);
That's it, think of it this way, remember how I said that when you call the WriteLine method we give it an argument? The "Hello, World!" is a raw form of a value, but when we instead give it a variable, it knows that it's supposed to read it.
P.S. The raw form of a string is called a string literal in programming.
So, you would ask yourself, but if variables are, well, not variable and are instead constant and unchanging, what's the use out of them? In fact, that is not the case.
You can change a variable after creating one.
int x = 5;
x = 10; // We changed the value of x.
Console.WriteLine(x);
Ok, but that's still not practical, so let's do something more interesting, taking user input.
Console.ReadLine.
Some of you when hearing about the WriteLine method might have asked themselves, does a ReadLine method exist? Yes, in fact, it does, let's see how it works.
Console.WriteLine("What is your name?");
string name = Console.ReadLine();
Console.WriteLine($"Hello there {name}");
we have a few different things here.
- We called a method to give a string a value, this happens because methods can return values, and we can assign them to variables.
- In the string literal, we did $"Hello there {name}". This is a special type of string literal, see where we did {name}, that's basically slotting in the variable in the middle of a string. This can only be done with strings that are prefixed with $.
now, when you run the code, you should get a message that says what is your name, when you do, write anything in the console screen, then press enter.
then you should see your name being printed along with a greeting.
that's what ReadLine does, it takes the input from the user on the Console and when successful, it gives that value back to the caller.
Next, let's explore if, else if, and else.
Chapter 5: If, else if, and else.
In programming, you'll find yourself needing to perform some action if a variable has a value, or if greater than, or less than, or any check, these can be done through if statements.
an if basically evaluates to, check something true > do something.
Remember bools? You wondered what is the use of them before right? Well, if statements are all about booleans, they can get pretty huge, but the ultimate result basically checs if something is true.
let's look at an example.
int number = 15;
if (number == 15)
{
Console.WriteLine("Number is 15");
}
- if tells the language we want to check something here, the parens used are required and must wrap the condition.
- number == 15 is an expression, an expression is an action in programming that evaluates to give you a value, for example, number == 15 checks if number is equal to 15.
- The opening brace {, after the if is something that opens a block, blocks help the language know what code belongs to a statement, in our case, the if statement, so whatever comes between the braces belongs to the if statement, and it naturally can span multiple lines, like normal code.
- the WriteLine call here, wrapped in the braces after the if statement is the code that executes if number == 15 evaluated to be true.
- The closing brace, }, ends the block belonging to the if.
and it's not all.
what if, we wanted to also write something to the console if it wasn't 15?
int number = 15;
if (number == 15)
{
Console.WriteLine("Number is 15");
}
else
{
Console.WriteLine("Number is not 15");
}
the else statement executes only when the if statement before it fails, an else cannot exist without an if.
Ok, but I still want to handle more cases,let's say I want to check for more than one number.
int number = 15;
if (number == 15)
{
Console.WriteLine("Number is 15");
}
else if (number == 10)
{
Console.WriteLine("Number is 10");
}
else
{
Console.WriteLine("Number doesn't match 15 or 10.");
}
but we can write this a different way, for simpler conditions, something like this can also work.
int number = 10;
if (number == 15 || number == 10)
{
Console.WriteLine($"Number match: Number is {number}");
}
else
{
Console.WriteLine("Number does not match");
}
the || is called a logical or operator, it basically works when the first expression fails, then it tries the one after that, so if number is 10, it tries to check of it equals to 15, returns false, continues to check it's 10, returns true, then the code under the if executes, if both are false however, it goes to the else, if there is no else, it does nothing.
There's also something similar, called the logical and operator, &&
int number = 10;
if (number < 20 && number > 5)
{
Console.WriteLine("Number is more than 5 and less than 20");
}
the && operator is similar to the || or operator, but the difference is that instead of one exppression succeeding for it to continue to the code inside the if block, the && ensures that, while that condition is true this must also be true, if one succeeds and the other fails, both will fail, all must succeed for it to continue.
the < and > operators check if something is less than or greater than a number, respectively
there are also other operators that work similarly.
- <= less than or equal too, as an example, if we did if (number <= 10) above, this will be true, because it's either less or equal to 10, and it's equal to 10.
- >= works similarly to <=, but checks for greater than.
there are other operators for bitwise manipulation, but I decided not to cover them here and instead cover them in a later chapter because I consider them complex for beginners.
now, let's use if conditions for a practical use, it's time to guess the number.
a small guess the number game.
a common beginner game example that is simple but interesting to play with is guess the number, it is a game where the program gives you input and you are supposed to guess the number between 0 to 100.
let's implement this.
int number = 33;
Console.WriteLine("Enter your guess: between 1 and 100");
int guess = int.Parse(Console.ReadLine());
if (guess == number)
{
Console.WriteLine("Congratulations! You guessed right");
}
else if (guess < number)
{
Console.WriteLine("Too low");
}
else
{
Console.WriteLine("Too high.");
}
The int.Parse() line converts a string variable to an int. This is used because Console.ReadLine returns a string.
P.S. If you don't put a valid number there the program will crash, it can be handled, but I didn't want to overwhelm any beginner reading this with a wall of information.
Now, this is all fun and nice, but there's one tiny problem...
The number is always 33.
Well, let's do better, then.
Random random = new();
int number = random.Next(1, 101);
Console.WriteLine("Enter your guess: between 1 and 100");
int guess = int.Parse(Console.ReadLine());
if (guess == number)
{
Console.WriteLine("Congratulations! You guessed right");
}
else if (guess < number)
{
Console.WriteLine("Too low");
}
else
{
Console.WriteLine("Too high.");
}
there, much better.
the Random line creates an instance of a class, for now don't worry about it, because we'll cover classes in a later chapter.
Next, loops.
Chapter 6: Loops.
until now, all the programs we wrote performed an action and finished, but naturally, in a game we don't want that, instead, it should keep doing something until the user presses escape, alt f4, or something like that.
this is done through loops.
While loops.
the simplist form of loops are while loops.
While loops are exactly the same as an if condition, but the difference is that, the code inside the while block executes until the statement fails.
let's look at an example.
int x = 0;
while (x < 10)
{
Console.WriteLine(x);
x++;
}
exactly like an if condition, but the use is just different.
the loop above runs 10 times, because when it starts the first time, x is 0, then it increases the first time to become 1 until it reachs 9, it's 9 and not 10 because our condition is < 10, not <= 10
x++ increments x by one, so the first time x becomes 1, second 2, etc.
let's look at a practical example, let's say we modified our guess the number code
Random random = new();
int number = random.Next(1, 101);
while (true) // true for an infinite loop. Since the condition is always successful.
{
Console.WriteLine("Enter your guess between 1 and 100");
int guess = int.Parse(Console.ReadLine());
if (guess == number)
{
Console.WriteLine("Congratulations! You guessed right.");
break;
}
else if (guess < number)
{
Console.WriteLine("Too low");
}
else
{
Console.WriteLine("Too high");
}
}
This loop will run forever until the user answers the right question.
the break; statement is used to stop the execution of a loop and completely ignores what comes next, we did break;, so it will just completely disregard the else if and the else and not try to evaluate it, because break; tells the language we're done with this loop, close it.
there's also continue; which acts like break but instead of closing the entire loop, it just closes the current session of the loop, say it's our first loop, we do a continue, we still have some code below, it completely ignores it and starts over.
int x = 0;
while (x < 10)
{
if (x == 5)
{
Console.WriteLine("Continuing.");
continue;
}
Console.WriteLine(x);
x++;
}
see how after the console writing 4, there's Continuing. Instead of 5? Because we check if there's 5 and if so skip over this current loop and go to the next.
For loops.
for loops are similar to while loops but they're specifically used to track a variable, and change it every time the loop is run.
for (int i = 0; i < 10; i++)
{
if (i == 5)
{
Console.WriteLine("Continuing.");
continue; // continue and break also works in for loops.
}
Console.WriteLine(i);
}
- the first part is creating the variable that the for loop will use, int i = 0;
- The second is the condition we use, in this case, the condition is i should be less than 10.
- The last one is the step change, what should happen when every loop finishs, in this case we're doing i++,which increments i by 1.
next, let's explore classes.
Chapter 7: Classes.
sometimes, having simple variables to represent things can become, unmanageable or disorganized and we need something else.
as an example, imagine this.
int playerX = 0;
int playerY = 3;
int playerZ = 0;
that looks fine all things considered, but what if we have 2 players and not one.
int player1X = 0;
int player1Y = 3;
int player1Z = 0;
int player2X = 0;
int player2Y = 3;
int player1Z = 0;
like also sorta works, but imagine if we'll have 3, or 4, or who knows how many others. WE have the exact same variables for every player, we just need it to be stored differently.
this is where classes come in.
classes and members.
classes are custom types that you can create to store inside your custom variables. It's like how when a folder gets big enough you decide to split it into subfolders for it to be more manageable, classes are similar, and even more powerful than that.
let's look at an example of our player scenario.
public class Player
{
public int x;
public int y;
public int z;
}
as you can see, class is a new statement, and we give it the name player.
the public keyword is called an access modifier, it basically tells the language who can access this code, class in this case, most of the time you just use public because other modifiers such as protected or internal are only useful in a library context and not an application one, more about this later.
Note, create a different file than Program.cs to put this, from now on, classes go into different files. This is because Program.cs can't handle classes with access modifiers.
then inside that class we have variables, those variables, unlike what we'll normally do, have no assign values. This is intentional.
classes aren't directly used, instead, you can think of a class as a template to use in a factory for something, then you ask the factory to create one and then you use it, and it exactly matchs the specifications you wrote in the class declaration.
remember Random? That's an example of a class that you are supposed to create. We create an instance with new.
instance is the programming term for the created class, and there can be more than one instance, there can be any number of instances for something, which is why they're handy in our problem from above.
let's see how.
Write this in Program.cs, since it's the code we want to run when the program is launched.
Player player1 = new Player
{
x = 0,
y = 3,
z = 0
};
Player player2 = new Player
{
x = 0,
y = 5,
z = 0
};
Console.WriteLine($"Player1: {player1.x}, {player1.y}, {player1.z}");
Console.WriteLine($"Player2: {player2.x}, {player2.y}, {player2.z}");
new is a keyword used to create a new instance of a class.
focus on the code, you see between every member of Player we assign we put a comma after it, that tells the compiler that we're not done yet, we will assign something else too. Except for the last one, because we assign nothing after that, so no need for a comma.
one thing you might be confused about is why did we put a semicolon at the end of the new block, this has a reason actually. Assignment always ends with a semicolon, and we assigned to a variable.
As you can see we accessed the variables inside the class instances with ., the same way we did when using WriteLine or ReadLine from the Console class.
But wait a second, why is this thing so different compared to Random?
There's a reason, we did not use a constructor, so we manually instanciate the class, but we can do it a cleaner way.
public class Player
{
public int x;
public int y;
public int z;
public Player(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
}
the public Player() part is the constructor, it's a special form of method that gets called when we do new(), and we can give it our arguments.
As you can see, when we have multiple arguments, we separate them with commas (,)
this basically says let's use this instance of Player, ordinarily we don't use this, but remember, there's both x y z above and x y z from the function, and the compiler is confused which one we want, so we do this.x for assignment, and we do = x to use the one from the function arguments. You can avoid this if you named the arguments something else.
public class Player
{
public int x;
public int y;
public int z;
public Player(int newX, int newY, int newZ)
{
x = newX;
y = newY;
z = newZ;
}
}
we don't use this here because there's no name ambiguity for the variables.
ok, so let's change our first creation of instances above with this new constructor style.
Player player1 = new(0, 3, 0);
Player player2 = new(0, 5, 0);
Console.WriteLine($"Player1: {player1.x}, {player1.y}, {player1.z}");
Console.WriteLine($"Player2: {player2.x}, {player2.y}, {player2.z}");
There, much shorter and easier to both read and write.
Next, methods.
Chapter 8: Methods.
Through this book we've scene a lot of examples of methods, we used WriteLine and ReadLine from the Console class and Next from the Random class, and we in fact wrote one in the previous chapter, the constructor is a kind of method.
so, let's see how can we create methods.
using System;
public class Player
{
public int x;
public int y;
public int z;
publicPlayer(int x, int y, int z)
{
this.x = x;
this.y = y;
this.z = z;
}
public void SayPosition()
{
Console.WriteLine($"{x}, {y}, {z}");
}
}
methods contain 4 parts
- Return type, it can be an int, string, or any other type, if we won't return anything, we use the void type, which is nothing.
- Identifier, holds the same rules as variables, can contain numbers and characters and an underscore, but can't begin with a number.
- argument list, wrapped between 2 parens, can be empty if we have no arguments.
- The body, which is basically a block, this belongs to this method and when the method is called, that code gets executed.
the constructor is a special method because there's no need to give it a return type, because it automatically deals with that itself.
the using System; part pulls the System namespace, namespaces are like folders that contain classes, and Console is under the System namespace.
We don't do that in our Program.cs because Program.cs automatically pulls System, but since we're putting all our classes in a different place we manually need to pull it.
lets try using our class from above.
Player player = new(0, 2, 1);
player.SayPosition();
now if you read our code carefully you'll notice that, unlike Console.WriteLine and ReadLine, we must create an instance first of the Player class before being able to call SayPosition, this has a few reasons.
- The SayPosition is an instance method,not a static method, instance methods need to be called from an instance.
- Even if we made it static we need to change it first, because there won't be x y z because we haven't created any instance so x y z doesn't exist.
let's look at an example of how can we make static methods.
using System;
public class Player
{
public static SayPosition(int x, int y, int z)
{
Console.WriteLine($"{x}, {y}, {z}");
}
}
and we try it, again remember, classes go to separate files and code that we want to run goes to Program.cs.
Player.SayPosition(0, 3, 0);
you also can make variables static.
using System;
public class Player
{
public static int x =0;
public static int y = 4;
public static int z = 0;
public static SayPosition()
{
Console.WriteLine($"{x}, {y}, {z}");
}
}
The problem with static variables, though, is that they are like normal variables. You only get one copy, all instances of the class will use the same variable, which will be a problem.
using System;
public class Player
{
public static string name = "john";
public int x;
public int y;
public int z;
public Player(int x, int y, int z, string playerName)
{
this.x = x;
this.y = y;
this.z = z;
name = playerName;
}
public void Speak()
{
Console.WriteLine($"Hello, I'm {name} and I'm at {x}, {y}, {z}");
}
}
Now let's try using that.
Player player1 = new(0, 3, 0, "ahmad");
Player player2 = new(0, 6, 1, "oscar");
player1.Speak();
player2.Speak();
You'll notice that both show oscar. This is because name is static, there's only one piece of it, it has nothing to do with the instance, so even though you created one, it's still one variable, it's like the normal variables we create anywhere, but it's inside a class and doesn't need an instance.
now, there's one part of methods that we didn't cover befoe.
Remember with Console.ReadLine we got a string from it? We can also do that with our custom methods.
using System;
public class Player
{
public static string Hello()
{
return "Hello, World!";
}
}
return is like a break; in loops but it completely stops the function, use return anywhere, the function is finished.
and it's also used to give back values, like what we did, remember that we can only return a value of the type that we spesified in the method definition, if we had string, we can't do int, and vise versa.
lets try using our method.
Console.WriteLine(Player.Hello());
Next, inheritence.