Ada Looks Good, Now Program a Game Without Knowing Anything (FOSDEM 2022)
This article is a textual elaboration of my FOSDEM 2022 presentation "Ada Looks Good, Now Program a Game Without Knowing Anything".
I discovered Ada in early 2019, I can't remember exactly how, but I directly liked the language, unlike several other programming languages I've looked at or tried to learn in the past. Some of the things I liked were readable syntax, usable error messages, and that not any junk just compiled through.
So I started learning the language with the help of the tutorials from Wikibooks and Rosetta Code and after understanding some of the basics, of course I immediately started programming a game. The result is exactly on the level you would expect when someone programs an RPG with little knowledge of the programming language used or game development in general.
The time to really get more involved with Ada was at the beginning of 2020, when I also started developing a new game. By September 2020 it had taken the form of an early Civilization clone and after more than a year of development it's now almost a real game with its own ideas. As I live stream almost all of the development on YouTube and Twitch, I was "discovered" and asked to give a presentation at FOSDEM 2022.
But since I don't know anything, I just talked about some basic things and problems in Ada that I noticed during my game development. Because sometimes it seems as if Ada wasn't developed primarily for game programming.
First of all I would like to clarify that all statements refer to game development with Ada 2012 under a reasonably modern computer system and there will probably be significant differences if you develop for old or embedded systems (or you just know what you are doing). So let's start with a few basic recommendations.
Ada offers various additional validity checks (-gnatVx), which should all be activated. The same applies to the additional warnings, with the exception of -gnatw.y, which only provides information about why a package needs a body, which may be useful for very complex packages. You should also have all warnings treated as errors. All of this may sound a bit exaggerated at first, but once you understand what you have to do to avoid the messages that appear as a result, it hardly takes any more time and you can identify problems much earlier. Which then makes it possible to fix corresponding errors in five minutes of work and not drag it around for a while and then take three days to adjust everything. Which of course has never happened to me.
There are also various style checks that you should take a look at. Among other things, you should think about limiting the nesting depth, just because Ada allows things like nested packages doesn't mean you should overdo it. It should also be noted that Ada guarantees a length of 200 characters for names, this should also be used for a more understandable code, there is no advantage in limiting all names to three characters.
As with many programs, games need text, so string handling is an important thing. Luckily, Ada has a variety of different types of strings, but all but three variants can be practically ignored. These three types are Unbounded_Wide_Wide_String, Wide_Wide_String, and String.
Since the texts in games are often available in several languages and/or you want to enable modding by the players, the exact text length is unknown. Therefore, the simplest solution to this problem is an Unbounded_String. In addition, other languages often use characters outside of the ASCII character set, for example the German language contains the special characters ä, ö, ü and ß, a problem that can be easily solved by using the Wide_Wide_Version. Of course one could also use a different string variant and adjust the length of the string dynamically or use the UTF8 version of the standard string. Not only is this a significantly greater effort to save text, it also does not result in any significant advantages. Certainly, an Unbounded_Wide_Wide_String requires more memory than a normal string, but this is only theoretically relevant, since this consumption is completely lost in the background noise compared to other parts, for example the game world map. In addition, an Unbounded_String can also be used in records without further adjustments.
Occasionally, especially if you want to interact with the standard or external libraries, you need a string that is not Unbounded. In these cases, it is best to use Wide_Wide_String whenever possible. This way you don't have to worry about possible problems with texts containing special characters and by using Ada.Strings.Wide_Wide_Unbounded.To_Wide_Wide_String and Ada.Strings.Wide_Wide_Unbounded.To_Unbounded_Wide_Wide_String an easy conversion option is available. Again, the theoretically saveable memory is irrelevant and it just creates a bunch of extra work.
Finally there is the normal string, which is especially important when interacting with a library that only uses string. An example here is the standard library, which with Ada.Directories has offered the possibility of reading in directories and file names since Ada 2005, but only in a string.
Which brings us directly to one of the problems with the Ada standard library, Ada.Directories only uses String. Ada 2005 introduced Wide_Wide_ for complete UTF8 support and many packages have a corresponding Wide_Wide_ Variant, but not Ada.Directories. The revision in Ada 2012 hasn't changed that either. This must be taken into account when naming files and folders, and additional conversions must be built in.
Also for Ada.Float_Text_IO there is no Wide_Wide_Version and if you need one you have to create a derived package based on Ada.Wide_Wide_Text_IO yourself. Not an insurmountable obstacle, but sometimes quite inconsistent, especially since an Integer_Wide_Wide_Text_IO exists.
Another problem is that there are often no comments when naming different procedures or functions in the same way. Yes, Ada allows multiple procedures/functions with the same name in the same file, even if the transfer parameter names are the same, as long as the data types are different. However, creating 5000 procedures with the name Put and then distributing them in 300 files without any comments is not very user-friendly. When you're learning Ada and you're looking for a solution to a problem and you find the solution "just use Put for that", it's a bit difficult to find out which Put is meant. Of course, this does not only apply to Put, there are also plenty of Get, despite the possible name length of 200 characters in Ada.
Lastly, a few little things. The string that (Wide_Wide_)Image returns has a minus as the first character for negative numbers, and a space for positive numbers, possibly this can chop up the text positioning. (Wide_Wide_)Value allows you to convert a string to a number, but doesn't check if the string is all numbers. You have to check this yourself, because if the string contains a character that is not a number, a program error occurs. Converting float to a string puts it in scientific view. If you want it to be a normal decimal number, then you have to adjust it accordingly, but there is a put in the standard library for that, have fun finding the right one. Just kidding, what you are looking for is in Ada.Wide_Wide_Text_IO.Float_IO and is called:
(To : out Wide_Wide_String;
Item : Num;
Aft : Field := Default_Aft;
Exp : Field := Default_Exp);
Then there is the reading/writing of data. The simplest solutions are Text_IO for text and Stream_IO for everything else. This is easy and while other variants may use less disk space, it's not worth the extra work as the graphics, sounds and music will take up more space anyway.
That was roughly the summary of my FOSDEM contribution and I'm now going to continue working on my game. I still hope to be able to sell it successfully in the future, after all the number of commercial games written in Ada is far too low. In addition, you are of course welcome to my live streams, in addition to German I also speak English and if you still don't understand me then don't worry, I usually don't understand myself either.