Intro
With this blog post I am starting a new series: "What not to do", that I will dedicate to describing various scenarios that were done by someone and would be highly advisable for anyone else to stay away from.
To be fair, I am starting from two bad examples of my own:
- Coding entire apps in the Assembly language. I will demonstrate it with my own 8-bit game that I coded in the late 90's.
- Importance of timely backups of your source code. Again, I will use my own bad example of losing the source code to the game I named above due to not having any backups of its source code.
Without further adieu, let's start from the first example.
Batty Game
I was studying the x86 Assembly language at the end of the 90's. And I was thinking of a project to test my newly acquired knowledge to gain some experience. At the time I was really enjoying playing 8-bit video games with a home console called ZX Spectrum, sold by the British company Sinclair. One of the games that seemed simple to make was the game, called Batty. It was a single-screen bat-and-ball game that had really colorful graphics and cool sounds. So I embarked on emulating it...
Working on it as a side project, as I was a college student, I was able to complete it within a year's time.
You can download the final playable game for Windows here. It doesn't require installation and can be played by extracting it somewhere to your local drive and by running the batty.exe
binary file.
The game starts by displaying its main menu, which is a simple Win32 dialog window:
In this window you can select the game controls and other options. The easiest way would be to play with a mouse (unless you have a trackpad.)
Then to start a new game click New Game
to begin. After a short level intro the game should begin:
Move the bat left and right to bounce the ball to keep it in the game, also trying to catch prizes that fall after you hit cells, and dodge bombs that are thrown by the aliens:
The goal of the game is to knock out all the cells.
To release the ball or to shoot, if you have a shooter bat, left-click the mouse, if you're playing with the mouse.
To pause the game, hit the Esc
key to get the game menu:
Alternatively, the game supports playing with two bats:
In this case you can play with both bats as a single player, or you can have two local players for each bat. You can select all these options in the original game menu dialog that I showed above.
Batty Game Issues
The Batty game turned out quite alright. Except maybe one "little" detail. I have coded it almost entirely in the x86 Assembly language. What looked like a great idea at that time, turned out to be quite a bad design choice in the long run.
Why Was It Bad To Code It In Assembly?
Here's why:
- By coding it in x86 Assembly, or a programming language specifically for 32-bit Intel CPUs, it was virtually impossible to port it to any other CPU architecture, even a 64-bit Intel one.
- The readability of the Assembly code is quite abysmal. Although I was including comments wherever possible, it's still really bad. To the point that now, 23 years later, the code that I wrote entirely by myself feels like someone else had done it. Even preparing for this blog post took me some time to study the code to describe it as I did below.
- Porting this game to some other platform, say making an iOS or an Android app is also virtually impossible, as the Assembly language is not very portable.
How I should have written it:
- An obvious choice in the 90's would be coding it in the C programming language. If it was written in C, I would have no problems porting it to C++ or Apple's Swift, since those languages support C out of the box. C is also easy to port to Java, for an Android app.
- C++ would be my second choice. It won't be as portable as C though.
- The Batty source code relies on Win32 APIs to create and manage the initial start windows and to play sounds, so that would require the most work if I needed to port it to another platform.
Modern game development is obviously outside of the scope of this simple blog post. But back in the 90's simple 8-bit games could have been coded by a single developer on pretty much any home computer.
Is Coding Anything In Assembly Bad?
No, of course not! But the scope of application for the Assembly language has shifted over the years. True to say, it has severely shrunk these days.
I would say that today, I'd be using a low-level Assembly language to code the following:
- Embedded systems with a very stringent resource constraints.
- Performance-critical parts of the kernel, such as synchronization locks or other areas of code subjected to a bottleneck.
- Performance-critical code where shaving off even a few CPU cycles could reap some benefits. There are not many applications that would require it these days though. One of such may be a stock-market/trading application.
- Game engines that require very high performance and down-to-the-bare-metal access to hardware.
- Writing patches for existing software. For instance, if you need to modify some binary that can no longer be re-compiled, making a software patch is one alternative. Those are usually written in Assembly, or some very low-level language.
- Writing a shellcode. I showed it here. But generally a shellcode is a small piece of code that is injected into another process, thus it has to be very versatile and robust, which excludes writing it entirely in C or in any other higher-level programming languages.
- Obfuscation engines. I would not recommend writing or employing these. But in case you want to create one, or to understand how one works, Assembly language is your go-to source.
- Malware. Again, I'm in no way advocating creation of malware. I am just listing a possible application for the Assembly language.
Note that the list above is not an exhaustive one. This is just what came to mind.
Batty Game Source Code
Since I brought up my 8-bit Batty game I need to explain a few things about how I coded it, don't I?
You can download a partial source code for the Batty game from my GitHub.Why only a partial source code? Read below to learn why.
Without any particular order, here are a few details about it.
Sine Optimization
At the time when I coded the Batty game consumer CPUs were not very powerful. (And coding it with a GPU was out of the scope of my test. Remember, I was learning the Intel x86 architecture.) Moreover, I wanted to make the Batty game as efficient as possible. But that required either cutting corners, or making some adjustments to the geometry of the game.
The physics of the game requires calculation of directions of the flight of the ball, as well as calculation of collisions between the objects and bounce angles. This needed computation of trigonometric sine and cosine functions. And even though both were implemented by the Intel x86 instruction set, they are very slow for the CPU to execute and required multiple CPU cycles, which was bad for my game.
Note that once you can efficiently calculate a sine, you can also get a cosine, as: cos θ = sin(π/2 - θ)
Thus I needed some optimization for my game engine.
The one I chose was to assume that an angle will be represented by a full number of degrees, or by an integer. In other words, if my angle was 10.3 degrees, I'd round it down to just 10 degrees and calculate the sine for that. This way I will be able to pre-compute all possible sine values for all 360 degrees before the game starts, and store them in a memory buffer, or in a lookup table. Then when the actual sine value is required during the game play, I should be able to look it up in my lookup table using the angle as an index. This has greatly improved the performance of my game engine.
In practical terms I needed a small overflow for my efficient calculation of the sine values. I went for 360 + 90 degrees for the lookup table, which made it 450 entries long, each containing a 4-byte float
.
The code to fill in the table was a part of the WinMain
(yes, at the time I was good at writing spaghetti code 🤦♂️):
mov eax,[FPU_CW]
and ax,0xf0c0
or ax,0x003f
mov [r],eax
fldcw [r] //set FPU settings
mov [r],180
mov ebx,[lpSin]
mov ecx,450 //filling out SIN(x) values
sub edx,edx
sinfn1:
mov [i],edx
fldpi
fmul dword ptr [i]
fdiv dword ptr [r]
fsin
fstp dword ptr [ebx]
inc edx
add ebx,4
loop sinfn1
The function loops through all 450 angle values, converts them to radians and computes a sine function for them using the slow fsin
Intel x86 instruction. The lookup table itself is stored in the global variable lpSin
.
It is later used in several places in the game logic itself.
For instance, during the calculation of trajectory of the bat pieces in the batexplode_obj
function when the player loses the ball:
mov eax,[lpSin]
fild dword ptr [esi+0x8]
fld st(0)
fmul dword ptr [eax+edx*4]
fchs
fistp dword ptr [ebx+0x18] //Ycoord=N*sin(EDX)
fmul dword ptr [eax+edx*4+90*4]
fistp dword ptr [ebx+0x14] //Xcoord=N*cos(EDX)
Note that the cosine is calculated by adding 90 degrees to the angle.
And also in the ball1
function that needs the sine and cosine values to calculate the ball flight trajectory:
mov edx,[eax+0x18] //alpha
or [SoundChosen],-1
shl edx,2
add edx,[lpSin]
fld qword ptr [eax+0x10] //R
fld st(0)
fmul dword ptr [edx]
fchs
fst [fYplus] //fYplus=-R*sin(alpha)
fadd qword ptr [eax+0x8]
fistp dword ptr [i]
fmul dword ptr [edx+90*4]
fst [fXplus] //fXplus=R*cos(alpha)
fadd qword ptr [eax]
fistp dword ptr [r]
The same inverse logic is used again to calculate the cosine.
Square Root Optimization
Another slow calculation that I needed to optimize was the calculation of a square root. I needed it to get the reflection of the ball from the bat, depending on the location of the impact. I used the lookup table technique similar to the one for the calculation of the sine, that I described above.
In this case though, the angle of reflection depended on the distance from the center of the bat, which is measured in full number of pixels, with 24 being the maximum. (This is the half-width of the extended bat.) So I created a lookup table with 24 entries as such (again in the WinMain
function):
//Set angles of BALL reflection from BAT
#define LOANGLE 25
#define HIANGLE 65
double N=(HIANGLE-LOANGLE)/sqart(24);
for(int a=0; a<24; a++) //right angles
{
RAngles[a]=(BYTE)(N*sqart(23-a)+(double)LOANGLE);
}
for(a=0; a<24; a++) //left angles
{
LAngles[a]=(BYTE)(180-(N*sqart(23-a)+(double)LOANGLE));
}
Where the sqart
function was using the Intel's fsqrt
CPU instruction for calculating the square root. It is also painfully slow:
double __fastcall sqart(double val)
{
__asm
{
fld [val]
ftst
fnstsw ax
sahf
jbe short sq1
fsqrt
sq1:
fstp [val]
}
return val;
}
This function has to be a little bit more careful not to throw a CPU exception for negative values.
Game Rendering
The rendering of the game screen in the Batty game is done entirely using CPU, or Win32 GDI functions. At the time I was writing it, GDI was all the rage.
The game uses a double-buffering technique, which involves creation of an internal (or hidden) device context with the CreateDIBSection
function. That internal context is used for all the drawing in the game play. Then that device context is moved to the screen, using either BitBlt
or StretchDIBits
functions, depending on the screen DPI.
The rendering device context is initialized in the initializeDIBsection
function.
An interesting nuance, that reflects the state of computers during the 90's, is that I was using a swap file, or a file-backed memory to store the screen pixels for the game. This is obvious by the use of theCreateFileMapping
function inside myinitializeDIBsection
. The reason I chose that technique was to ensure that the game runs on computers that may not have enough RAM to store the entire game screen. As you can see, we're talking about544
by384
pixels, or835,584
bytes for the whole screen (assuming that one pixel is represented by 4 bytes) which is less than 1 MB of memory.
The actual drawing of the game play objects is done in multiple functions. To name just a few: updatebackground
, objectdraw
, celldraw
, shadowdraw
, cellshadowdraw
, etc. Their names kinda reveal what they are used for.
Then the transfer of all the pixels from the internal (hidden) buffer into the main (visible) one is done using one of the two methods. For a faster, one-to-one copy with the BitBlt
system function:
push esi //SRCCOPY //1X1
mov edx,[edi+4] //y
mov ecx,[edi] //x
push edx
push ecx
mov ebp,[hDCMem]
mov eax,[edi+0xc] //dy
push ebp
push eax
mov ebp,[edi+8] //dx
mov eax,[hDC]
push ebp
push edx
push ecx
push eax
call dword ptr [BitBlt] //(hDC,x,y,dx,dy,hDCMem,xS,yS,SRCCOPY);
Or, for a slower method that presupposes stretching using the StretchDIBits
function:
push esi //dwROP
push DIB_RGB_COLORS //iUsage
mov edx,offset Gbmi
mov ecx,[lpGrx]
push edx //BITMAPINFO
push ecx //*lpBits
mov eax,[edi+0xc]
mov ebp,[edi+0x8]
push eax //hs
push ebp //ws
mov ecx,[edi+0x4]
mov edx,[edi]
push ecx //ys
push edx //xs
shl ecx,1
shl edx,1
shl eax,1
shl ebp,1
push eax //hd
push ebp //wd
push ecx //yd
mov eax,[hDC]
push edx //xd
push eax //hdc
call dword ptr [StretchDIBits] //(hdc,xd,yd,wd,hd,xs,ys,ws,hs,lpBits,BMinfo,iUsage,dwRop)
Main Loop
The main game logic loop is implemented in another long function, named mainjob
, that is called for each iteration of the game. It checks for special conditions and calls various logical and drawing functions that I mentioned earlier.
One interesting implementation is the calculation of the delay for the main loop of the game. As you can imagine just blindly calling main loop logic in an infinite loop would be a bad idea since the refresh rate (and thus the speed of the game play) will be dependent on the user's hardware. For some players, the speed would be OK, but for some, with more robust hardware, the game will be too fast, and thus unplayable.
The way to resolve this is to establish a slight adjustable delay between each iteration of the game's main loop. The tricky part though is how to compute such a delay?
I was working on the code to implement this with the use of the WaitForSingleObject
function in the main game loop (and that is the state you see the source code now.) But the logic for the delay remains unfinished (due to reasons that I explain later.)
Unfortunately because the main loop delay is not set correctly, the game looks quite choppy and maybe even slow at times.
The idea is to run the game and time how long it takes for each iteration of the game loop to complete, using some very precise timer. And then after having collected a few dozen of these time readings compute an average delay to be used in my WaitForSingleObject
function that is needed to ensure that each game loop runs within a certain time frame, say 100 ms, or so.
Game Sounds
Another interesting aspect that I want to show-case is dealing with game sounds. I could have implemented playing sounds in a separate thread, but at the time when I was writing Batty, spinning up a new thread could have been quite costly. Instead, I chose a different approach.
I used the Waveform Audio low-level system APIs to deal with the sounds. This is a legacy component now.
The sound logic runs in its own game loop, in a function called soundjob
. The idea was that I will break up each sound, or its wave-form, into small sound chunks (encoded with my CHUNKINFO
and PUMPINFO
structs) and then play them one after another in the sound loop, using the waveOutWrite
system API.
All game sounds will be placed into the sound queue during the game logic using the putsound
function, and are played sequentially in the soundjob
function.
This also involved doing my own sound mixing of the sound chunks in the play queue. That was done in the mixsound
function.
Unfortunately the definition of theputsound
andmixsound
functions did not survive. (Read below why.)
In a modern game though there is absolutely no need to complicate things the way I did in Batty. If I were to code it today I would play all the sounds in a separate worker thread.
Easter Eggs
And guess what, the Batty game also had some "Easter eggs". Some of them were designed to test the game performance, but some were just plain fun. For instance, there's one that makes the Alien bird throw turds instead of bombs, etc.
The logic for the Easter eggs is implemented in the WndProc
for the WM_KEYDOWN
message switch. That message processes key presses from the keyboard, and that is how Easter egg commands are supposed to be entered. You basically type a command and press the =
key to execute it. All of this has to be done during the game play though.
I'll let the reader discover all the Easter egg commands from the source code on their own though.
I remember that I also put some fun in the Easter egg logic. Try to type in an English swear word instead of the Easter egg command and press the =
key.
Regular Backups
Another example of what not to do that I want to share is the importance of timely backups (of your source code.) Or in my case, the danger of not doing any.
I was writing my Batty game at the end of the 90's, if I remember correctly, using the Windows ME operating system. And back then that OS would run pretty much any code that you gave it. Viruses were not Microsoft's worry at that time.
I was also pretty careless about what I would run on my system. I will probably never know where it came from, but somehow I got a malware in my computer. Malware was different at that time. It wasn't smart enough to encrypt my hard drive and ask for a ransom. There was no such thing as cryptocurrency back then. Plus the encryption was also at a very rudimentary stage.
The malware I contracted was very simple. It merely started deleting all files from my drive C:
, and I didn't notice it until I realized that something is amiss and started looking through the processes running via the Task Manager. Soon I realized that there was some weird process that was taking up CPU time and I killed it...
But by then it was too late. The malware managed to delete a good portion of files on my hard drive. (It wasn't obviously placing them in the Recycle Bin.) It was deleting them using the DeleteFile
function.
When I realized what happened, I immediately checked my folder with the source code. But I couldn't find it. It was gone!
I knew that deleting a file using regular DeleteFile
function doesn't actually delete the data, but instead marks the location of the file sectors on disk as free, or available. So there was a chance to recover my deleted files!
The obvious mistake in this case was that I did not have any backups of my source code files. You realize it pretty quickly when you notice that your entire folder with various source code is gone. You get this tingling sensation that shoots up your spine when you realize that you just lost a year's worth of work.
The second mistake that I made was that I started searching online, downloading & installing the software that could allow me to "un-delete", or recover deleted files. The mistake was that once a file is deleted, or its space on disk is marked as available, any consecutive creation of a new file may overwrite the deleted file sectors. So searching online and installing software obviously posed such a risk. Windows alone writes a ton of stuff on disk in the background. So every second after an accidental deletion counts!
The correct behavior in that case (after an accidental deletion of an important file if there was no backup) was to pull the power plug from the computer. Then to remove the hard drive, clone it sector-by-sector, and try to recover it on a separate computer having plugged it in as a secondary drive.
Low and behold, by the time I was able to install a software to recover my deleted files, and run it to recover my data, the operating system was able to clobber up some of my larger deleted source code files. So my old penchant of putting all code into one large file (i.e. "Spaghetti code") hit me in the ass again. This time because large source code files had their endings overwritten by some binary garbage. This meant that some of the C and ASM code in them was destroyed.
You can see an example of it at the end of my MAINLO~1.h
file.
By the way, the reason why my source code files have this weird 8.3 naming format is because I recovered them after deletion. The originals had longer NTFS file names.
I guess if I really wanted to, I could've recreated the endings of the large source code files. But the extra work required was too much for this toy project. Thus the source code for the Batty game was lost forever with the sequence of dumb mistakes that the author of this blog post had committed.
The only thing that remained was the unoptimized debug
binary build of the game that you can find in the Debug
folder on GitHub.
Correct Ways To Backup
There are countless articles and advice online on how to do proper backups. Many of you probably know all that. But just in case you are in my shoes (circa year 2000) and are waiting for the disaster to happen, here's a quick recap how you should backup your precious work.
First, what you should backup:
- Anything that you cannot re-create: your own photos, home videos, typed up documents, text files, source code.
- Your private information that is not easy to recover.
- Some rare software installers that are no longer supported, or unavailable online.
What you should NOT backup:
- Software, music, videos that are available for download from the original authors. (Unless you want to preserve these.)
- Parts of the operating system that can be reinstalled: system files, original Registry, etc.
- Movies that can be later streamed/downloaded, etc.
And finally how to backup:
- Ideally you need to have 3 backup destinations:
- On your local computer. Simply create a separate folder and just copy important files into it.
- On an external disk. Buy an external hard drive (those are relatively inexpensive these days) then connect it to your computer and copy your important files into it.
- Offsite backup. Ideally this should be some online (or cloud) resource. I would recommend the following free services: Google Drive, Microsoft OneDrive, mega.nz, etc. So compress your needed files into a
.zip
or.rar
archive and upload it to one or all of these services.Keep in mind that these services may not properly encrypt your uploaded files. I personally use WinRAR to compress and encrypt all my important files with a password before uploading.
- Make sure to do backups on a regular basis. How regular? This depends on how often you work on your stuff. But probably making all these backups at least once a month is the least that you can do. I personally try to backup my important source code files after each major release of the software in question. Or I do it weekly, if my work takes longer.
- There is some paid and free software that can automate the backups for you. If you use it make sure that you understand how to recover from a backup. And also test the recovery process at least once.
- Make sure to use the versioning type of backups, or the ones that keep previous copies of past backups for as long as possible instead of overwriting a previous backup with a new one.
Overall this blog post is not about backups. This advice is just the bare minimum that came to mind. Since I lost my source code for the Batty game, it was enough for me to learn my lesson the hard way. Since then I do backups quite diligently and have never had such a catastrophic loss of my work as I described here.
Conclusion
These were two bad examples from my past. Hopefully you will learn on my mistakes and won't repeat them.
By the way, do you have any faux pas of your own? Don't hesitate to share your cringy moments in the comment section below.
But in the future, let's try to avoid them.