I know many arcade games from the 80s were programmed in 68000 assembly. This carried on probably well into the 90s, even though Motorola C compilers existed in the 80s. Why then weren't C compilers used more frequently? Was it an issue with the limitations of ROM storage space and memory, and due to this the coder's need to optimize literally every line of code for speed/size?
I think the question I would ask is why would you program arcade gamers in C back in the 80's.
Firstly, C was not nearly as popular in the world of microprocessor programming as you might imagine back then. It was only one of a number of high level languages that were available (if it was available for your platform).
Secondly, arcade games need performance. For performance you needed assembly. Even C compilers didn't produce code as fast as an assembler in those days.
Thirdly, people tends to use what they know. When arcade machines were eight bit, everybody programmed them in assembler. When faced with a 16 bit machine, the natural thing would be to carry on as before - different assembler, but still assembler.
Many of the people who programmed arcade games got their start in an era where C compilers existed, but would have been expensive and not very convenient to use. Later in the 1980s, compilers reached the point where they would be practical, but experienced assembly language programmers could be more productive writing assembly code than they would be as neophyte C programmers.
Although today's compilers are much more sophisticated than those of the 1980s and 1990s, much of that sophistication wouldn't have been necessary or even useful on the kinds of processors people were using in that era. Today's processors can execute multiple instructions at once on each core, but certain combinations of instructions cannot execute simultaneously. In order to achieve good performance, it's often necessary that machine code perform operations in a different sequence from specified in the source code. The need to determine when reordering instructions will improve performance without breaking program semantics creates a need for a lot of compiler complexity which offers far less benefit and may even be counter-productive when targeting simpler processors.
[I see that @Mark supplied a substantially identical answer while I was typing this one]
One aspect that other answers do not seem to have touched upon is the issue of tightly controlled predictable and high-resolution timing for I/O devices. The simple in-order CPUs of the time allowed one to precisely determine the execution time for a particular piece of code by adding the cycle counts of the instructions being executed. This required a level of control over the machine code sequence executed that is simply no achievable with code compiled from an high-level language.
For example, various graphics effects required that particular code for manipulating the framebuffer could only run during the horizontal or vertical blanking intervals which have durations in the microsecond range. While I have no first-hand knowledge, I am under the impression that some sound effects, such as the playing of sound samples, likewise required very precise timing that was achieved via cycle counting. Some simple systems used cycle counting for implicit synchronization of video and sound until well into the 1990s.
Part of the reason is timing.
Early- and mid-80s arcade machines didn't have anywhere near the hardware capabilities of modern systems. You couldn't just load an MP3 of background music into RAM and tell the sound card to play it, or compose an image in an offscreen framebuffer and swap it in during the vertical-blanking period. Data had to be delivered to the output devices on a reasonably "just-in-time" basis, which was far easier to do in assembly.
Timing shows up again in getting the most out of the hardware's limited abilities. For example, you could change display parameters in between scanlines, or even mid-scanline, to do things like extra colors or perspective transforms that the hardware is theoretically incapable of doing. But this typically requires cycle-accurate timing, which you can't get out of anything but assembly language.
From my perspectives: in the 80's, a lot of hobbyists only had access to Basic and Assembly or machine code. They might have access to Microsoft Pascal or Turbo Pascal, but that was about it. In fact some people feel happy not needing to program in machine code but can use Assembly. There was no "C compiler" available to the general public. Machine code was generally free of charge and available to everybody (such as on the Apple II).
C didn't show up at least to me, until about 1989 in college, and I remember buying the K&R C book, and then about 1 or 1.5 years later, the ANSI C edition of the book came out, and I was asking the question "I just bought the book and now I have to buy the book again?"
Also, during those days, the 6502 was 1MHz, and 68000 at 8MHz, and compared to 4GHz nowadays, the 6502 processor had a speed 1 / 4,000 of a processor nowadays. The 68000 has a 1 / 500 of a processor today. And the storage? It may be 8kb ROM or 16kb ROM in the arcade machine to store the game (it was 2kb ROM x 6 on the Apple II), so we didn't have that much memory to work with. So if we use a C compiler and it was 2.5 times slower than the Assembly code or machine code, and takes up 2 or 3 times the memory space for the compiled code, we may run into problems. During those days, we directly program a processor, instead of writing something to be compiled to run it.
I also remember there was a somewhat steep learning curve to learn C, when we had to learn what is a pointer to a function that returns a pointer to a pointer to a character.
Probably as opinion-based as other answers but the reasons I see:
Back in the 70s, the aim of C was portability.
There was no need of portability in arcade games (well, they didn't care for future home conversions), so no instant benefit of C for that aspect.
On, the other hand the risk of switching from assembly to C was probably considered too high for very few benefits. The drawbacks were more apparent:
- more complex and slower toolchain
- possible compiler bugs
- suboptimal code generation leading to performance issues
- need for a symbolic debugger (whereas one could debug their asm code from raw disassembly on a simple low-level debugger if available)
Plus, once you have your base asm routines for a given processor, you're reusing them over and over and build over it for new games. Switching to another language forces you either to mix asm and C (others problems arise) or rewrite everything.
They were, later on in the decade. Marble Madness for example.
Using C just wasn't an option.
Whilst I wasn't a games developer in the 80s, some of my friends were, and I watched them do it.
At the time, the only tools available in the UK, to mere bedroom programmers, was hand crafted hex values (pen and paper to write assembly, then convert each line to hex, then POKE it in to memory), and later on, a disk based assembler (which took a text file, and compiled it into binary, and stored it back on the disk, not enough memory to store the assembler AND the code in RAM at the same time).
I vaguely remember reading about Andrew Braybrook using an AS/400 to cross compile assembly code and download via a serial port to a C64, but that cost lots of money that most developers didn't have.
One of the things I remember helping with, on a C64 game, was to add NOPs to some code that did video, to 'push' the border colour change so that it appeared 'off-screen'. You just can't do that in C.
You could code in a high level language (heck, I wrote two educational games in BASIC for Pete's sake) but you would get murdered on performance if you did.
That meant other game companies would write better games than you, and you'd lose market position.
All things being equal, C code will take more ROM space to store, use more RAM for the same data storage, and use more CPU cycles to execute- often audacious or highly varied cycles.
Generally most game programming was pushing the limits of the hardware. For instance, let's suppose I need to animate 60 sprites. However, the hardware only supports 8 sprites. What will I do? Reuse sprites. Scan line 47 is the bottom of the top-most sprite. (I order my sprites by their height on the screen). So on scan line 48, hardware sprite 1 won't be used again until the top of the next frame. So I change hardware sprite 1 to the values for (software) sprite 9. At scan line 51, I rewrite the registers for hardware sprite 2 to make it my sprite 10. And so on.
This kind of "beam riding", common to many games, just doesn't work with high level languages. There just aren't enough CPU cycles (as of the early 80s). You need the speed and predictable timing that comes from running in assembler.
Because, for instance, if you are "late" executing the housekeeping code and aren't there to move the sprite on scan line 48, then that sprite just disappears from the playfield that field, which means in practice, some of your sprites shimmer (absent every other field). See, FPS isn't an optimization like on modern games: FPS is 60 or your game dies.
That said, there is room for high level languages in the game administration and bookkeeping code. For instance if your graphic game is trying to use D&D/d20 rules with complex modeling of stats and combat, that's nice to do in a higher language as long as it doesn't take spuriously long to execute, e.g. stopping for a garbage collection.
As said, ROM and RAM are both precious, and you just can't afford the bloat that comes with compiled code. Keep in mind that code optimizers currently in use today simply did not exist then.
What I mean is, suppose you write
A = (computationally very expensive thing) B = (computationally expensive thing) C = (computationally cheap thing) C = 1 if A or B or C then print "Hello, world!\n"
(And A B C are never used again) ... a modern compiler will do a lot of optimization there. For instance if it understands the computational loads, it will reorder that to "if C or B or A" - which means it will compute in that order. Since line 3's computation of C is never used before it is replaced, it will delete line 3 altogether. Since 1 is always true, it won't ever compute B or A, so the top 2 lines disappear also. What remains reduces to "if 1", so they are eliminated too. My point is, those kinds of optimizations, which can make up for the bloat of compiled code, did not exist then. And even then, would not realize you were beam-riding, and would optimize for overall runtime, not being ready to go when each scan line starts.
The question seems fairly well answered already, but a couple of bonus considerations are: 1. There was little thought given to portability, since the chosen hardware platform was the only thing you’d ever want to run the program on, and 2. If you had a C compiler you’d need something you could run it on, and that probably wouldn’t be an arcade game; compile->burn EPROM->power up and test gives a slower cycle time than running a ROM-based assembler or hacking machine code by hand.
The biggest issue was that memory was really expensive, and its usage had to be tightly controlled. Game authors would use lots of tricks to save memory.
I don't know anyone who programmed arcade games, but I know some people who programmed dedicated chess computers with 8 bit CPUs (mostly the 6502, the Z80 in a couple of cases). They rarely had more than 4k of RAM to work with. With that, they needed to remember all the game moves and traverse the game tree of legal moves to select a good move. They also had to do a lot of other things that required memory. Every bit of memory was precious and a byte had to be used in multiple ways. They were all experts in bit shifting.