concatenations of concatenations
It all begins with an idea.
prerendered node level streaming in pipmak
You haven't heard of Pipmak. That makes sense, because it's been 17 years since it was updated and it still lurks at version 0.27. Nobody has made a full game with Pipmak. I am. As one might imagine, there are a few small problems to overcome with this approach. Here is how I dealt with one. A prerendered adventure game is composed of hundreds or thousands of static images. In the early 2000s, there were practical limitations on the dimensions of these images. Myst III: Exile was composed of 640x640 images, at the most. In the over two decades since, the ability to render and display images much, much larger is commonplace. Intrinsic, my project, is composed of 1536x1536 images. You might think that increase only represents about a 2.5x difference in engine loading times, which ought be be balanced out by the much faster hardware in modern computers. But, the difference is actually much greater. A 640x640 image is composed of 409,600 pixels. A 1536x1536 image has 2,359,296 pixels. A typical node in Intrinsic is composed of at least six of these images, often more. The bottleneck is quickly apparent, especially due to the nature of Pipmak's image drawing: isolated to a single thread on the CPU. CPU clock speeds have increased since the early 2000s, but the real performance boost comes from the number of threads or cores, which have multiplied immensely. In Pipmak's case, this limitation makes sense, because in those days, the Pentium and AMD Athlon ruled supreme. There was essentially no parallel desktop computing. But this was fine at the time, because the number of raw pixels involved wasn't as significant. The other side of this(at the time) unavoidable limitation was memory. Desktop computers didn't have hundreds or thousands of megabytes free to install a game. So, they were streamed from CD-ROM. We don't need to do that anymore. So, how do we make a single-thread, 2D game engine from the 2000s load nodes and entire levels ahead of time? The first answer I came up with: loading screens. Essentially all modern realtime 3D games feature some form of loading screen between levels. So, I established a naming convention for all node images, which went something like this: the first two digits of a given node would be its level, and the second two digits the node ID, followed by a specific letter representing the type of image and its number(the most common images being cubic panorama images, a typical filename could be "1103_c1.jpg". So, up to 99 levels and 99 nodes per level, more than enough. I defined a Lua function to load a given node, placing each image within a global table, using a key reproducible with concatenation and basic arithmetic. Each node.lua file recieved a local variable to define the node ID, and each node was given portable concatenations referring to the given global table within which all images have been loaded. So far, so good. Another series of tables was created in a centralized location in order to advise the node loading function of how many of a certain image existed within a given node and therefore needed to be loaded(for example, flickering flourescent lights or rippling water). I then implemented a function in Lua to, given a single argument, load an entire level. For example, loadLevel(11) loads the entirety of level 11, complete with a scrolling loading bar. However, again, given the single thread nature of Pipmak, this takes an uncomfortably long time, especially as a typical level contains several dozen nodes, this meant that a level could contain up to one billion pixels. I came to realize that while today's gamers are accustomed to loading screens, it might not be reasonable to expect them to sit through 15 seconds of loading screen, especially given the retro nature of the navigation mechanics, without having a fully realized realtime 3D space as a "justification" for that loading screen. So, what next? I experimented with an on-the-fly node streaming approach. Whenever the player is in a given node, a function loads the neighboring four nodes. But, performance was choppy with this approach. And there was always the risk that the player would zip through the level too quickly, outpacing the loading functions. And, there was a degree of unwanted redundancy -- regardless of the direction of player travel, the two nodes more or less in front of the player, and the two nodes more or less behind, were loaded. The solution turned out to be in combining the two approaches and refining it with a primitive directional intelligence. A brief level loading screen(about 4 seconds long) loads an initial spread of six nodes. Once the player enters the first of those nodes, a function determines the current panning direction in degrees of azimuth and converts those into compass directions. An improved level loading function then loads a single node, strategically chosen from a table, estimated to be likely to be shortly encountered by the player. Each node has this table with different keys based on a given compass direction. If the player does end up outpacing even this system, with the generous buffer provided by the short loading screen, a "safety" function checks each node transition to ensure the target node has been loaded, and if not, it cancels all other activity and loads that node immediately, resulting in a barely noticeable pause in the worst of circumstances.