Episode 2: The Bloat of Computers
"Gus’s male" asks on Facebook: What makes Java such a hideously bloated resource hound? Seriously—Minecraft has all the graphical fidelity of an upscaled NES game—so apart from the number of objects it’s trying to load, what the hell causes it to want so many system resources just for a glorified game of Legos?
Not surprisingly, this is a complicated question. Yes, Minecraft does do a very good job of displaying a full 3D world in a manner reminiscent of NES-level graphics. But doing that, well, is harder than you think. And the level of system resources demanded by Java, Minecraft, and, truth be told, just about every computer program out there today, is a question of tradeoffs.
Computer programming has changed a lot since its infancy in the 1950s, and even since the 1990s. Moore’s Law is an observation, named after Gordon Moore, founder of Intel, which states that roughly every eighteen months to two years, the number of transistors on integrated circuits doubles. This more or less directly implies a proportional increase in computing power. (Incidentally, this observation is slowing down; since we’re hitting practical physical limits, here, we’re reaching a level where computing power only doubles once every three years.)
The space-time tradeoff goes back to Martin Hellman in 1980, who proposed it in relation to cryptographic analysis. In most applications and algorithms in computer science, you can greatly speed up computation if you’re willing to have the data you’re working with take up more space (memory, disk space, or whatever store you’re using). A good example is opening a file from a compressed archive, like a ZIP — it can be done, but first the data must be decompressed, which is slow. You end up losing time in the translation, but the space the data takes up is much smaller. But we’re talking games here, right?
.kkrieger is a simple first-person shooter game, with full graphics and sound, that is stored in a 95 kilobyte executable. Yes, kilobytes. How does it pull this off? The images, sounds, and just about everything else is generated via an algorithm. The code has a provision for how to generate the graphics, on the fly, from simple rules. So you can get marvelous images in very little space. I have played this game, and it’s not much of a game, frankly. What are you going to store in 95 KB? It’s also slow, because all of the assets have to be generated just in time to display them, the game locks each door as you walk through it, and sometimes it forgets which enemies are dead and which items you’ve picked up. But the concept is there; you can do amazing things with little space.
Having examined the kind of code that this and other games that generate levels programmatically require, I’ll tell you that it is an enormous pain to work like this. Humans generally don’t work well with the kind of bit twiddling required to develop a game in these constraints. If you’re wondering how an NES, which has two kilobytes of onboard RAM, can handle running games, and for that matter, why it took months and years to develop games that an amateur can build in his spare time in days now, it’s specifically that; to get a functional game to work in such a constrained environment, you need to be a freaking super-genius.
Not knowing much about the language in which NES games are programmed, I’ll delve in a direction I’m slightly more familiar: PC gaming. Most PC games — and in fact most software in general — have been written in C++, for decades at this point. C++, originally “C With Classes”, is an evolution on the old C programming language. You end up needing to interface with low-level libraries of code that directly manipulate the memory and features of your graphics card, system RAM, and so on. In fact, this wasn’t even enough early on; some code would need to be hand optimized in assembly language, to get the program to run even faster than the compiler could make it. (And compilers generally don’t mess around.)
Assembly language is one step removed from the literal ones-and-zeroes machine language that your processor uses to operate. Oh, and by the way, the assembly language of each type of processor architecture is different.
Punchline: This sucks. No normal person could program a game like Minecraft, solo, in any reasonable length of time, if they had to work in low-level languages. Which is why they don’t. Remember I mentioned “objects” and “game logic” here? Abstraction is the bread and butter of the modern software engineer.
Abstraction is a concept in computer science that means two different things: first, the abstraction of concepts into a data model, as we discussed earlier. Second, the abstraction of methods and functionality in a computer program. These two “kinds” of abstraction work in tandem to make a computer program, such as a game, much easier to keep in memory.
A “class”, in computer parlance, is a package of code, variables, and methods that represents some notional “object”, ideally a literal component of a data model. This takes advantage of one of the two types of abstraction in computer science: the abstraction of a data model.
In Goedel, Escher, Bach, Douglas Hofstadter illustrated six levels of abstraction for, say, a newspaper:
(1) a publication
(2) a newspaper
(3) The San Francisco Chronicle
(4) the May 18 edition of the The San Francisco Chronicle
(5) my copy of the May 18 edition of the The San Francisco Chronicle
(6) my copy of the May 18 edition of the The San Francisco Chronicle as it was when I first picked it up (as contrasted with my copy as it was a few days later: in my fireplace, burning)
In a similar vein, you might have a class called “Enemy”, which encapsulates code that enemy creatures in your game might need; “inheriting” from that, you might have the subtype “Walker”, and then individual enemies, like “Goomba”, or “Koopa Troopa”, and then descending still from that, individual instances of those enemies, with their positioning, health, and so forth.
Abstracting the functioning of the game code is a separate, but related matter. The “game loop”, as it’s called, creates instances of data model objects, as well as calling methods, packages of code which do further work. A real world example of this would be driving; “accelerate” turns into putting your foot on the gas, which turns into going through a series of linkages which make the fuel-to-air mixture going into the engine more rich, which turns the motor faster, which turns the wheels faster, which (usually) makes the car accelerate. Each of these levels is an abstraction, and the driver needs to worry about none of them, in the usual case, in order to drive successfully.
This abstraction, which is provided not just by the code we write, but also by the programming language and environment, makes programming easier, but also incurs a cost in terms of space and time. The good news? Moore’s law came into play, and we have loads of space and power to spare. And this also is why programs tend to be a lot more bloated these days. (For example, Word for Windows 2.0 required 4 MB of RAM and 8 MB of hard disk space. Word 2013, aka Word 15, requires 1 or 2 GB of RAM, depending on your system architecture, as well as 3 GB of hard disk space.)
Java, however, complicates the question further. Remember I mentioned compilers earlier? Java isn’t a compiled language. It’s run on a virtual machine — a virtual machine is a sort of intermediary between the Java code and the architecture of a machine. Theoretically, you have one Java virtual machine for each system, and then code written in Java will theoretically run on any architecture that supports a Java VM. The way this happens is that Java is a semi-interpreted language; it’s compiled down into “bytecode”, which is what you feed into the Java VM, which finishes the compilation process into the architecture you’re dealing with (Windows 32-bit, 64-bit, Unix, Mac, Android, whatever), and then you have a working program.
The good news is that, since the details of the architecture you’re working with are (mostly) abstracted away, writing programs is easier. (In fact, this leads into the Rule of Least Power: Given a program and performance requirements, you should use the least powerful language you need to get the job done. By “power”, here, we mean how low-level a language is, and therefore the faster it is, but by necessity, how hard it is to use. As an aside, this also means that it’s easier to make mistakes, because you’re not as far removed from the system.)
The bad news is that you pay a penalty in speed.
Minecraft just looks like an 8-bit game. Notch and the Mojang team have done a lot of work to get the game looking like a pop-up book rendition of the NES. It’s “programmer art” — trying to stay on the abstract [as opposed to photorealistic] side of the uncanny valley. The uncanny valley is that space where things look just real enough to be wrong somehow:
But the world of Minecraft is a lot of things that most NES worlds aren’t:
1) Fully 3D. Obvious.
2) Actually very-high resolution, if you examine the borders of objects. The blocky visuals are a stylistic choice.
3) Nearly 100% persistent. The state of blocks is preserved across the entire world, and a surprisingly high amount of the world is persisted in memory. (“Chunks” of the world that are sufficiently far away from your current position are unloaded and saved to disk.) But if you mine, say, a bunny out of the wall, walk sixteen miles away, and come back, the bunny will still be there. (Dropped items only persist for five minutes.) If you remember how, when you walked into a new area on an NES game and came back, only a handful of items were persisted? (And that’s if you were lucky — sometimes enemies would respawn if you walked a screen width away and came back, even in the same area!) Those were being explicitly saved in flags. Not so in Minecraft. (The original Deus Ex persisted the state of the area you were in, as well — that’s why its saves, at 10-20 MB, were unusually large for its era.)
There’s a lot going on. The choices that Notch, and subsequently his team, made, were designed to build a game that delivered on its promises, used the technical capability available, but still maintained a level of abstraction sufficient to make the game practical as a solo project. He might have made different choices if he knew the way the game was going to explode in popularity — but that’s another story.