1 Continue Left
Don't
know how old you are, but when I was your age, we used to play games with our
bare hands. No health recharge, no "switch-to-easy?" after dying 3
times, no IDDQD, and no AutoSaves. Hell, no saves at all!
Hard to
imagine, but indeed some of the old PC, NES or even later SNES games I played,
didn't give you the opportunity to save, switch off the system, relax, and go
play outside. No sir. Finish what you started, dammit. Countless times being in
the very last level, finally, nerves up to the throat, and then mom yelling
"bedtime! NOW!". Jesus, now what?! Restarting was not an option, so
we had to turn off the TV but leave the system on, sleep, go to school, and hope
mom didn't turn off the system (and hope the Nintendo wouldn't catch fire). And
of course, next day, 16 hours later, we died over some ridiculous jump or
impossible end-boss anyhow. Some games showed some mercy and would let you
restart the level, but mostly you just had to replay the whole fucking game
once the “Continues” depleted. Developers were soulless beings. Or maybe they
were just too stupid to make a SaveState back then...
Well, as
a cold comfort, most games weren't that long those days. If you include all the
dying, restarts, flying gamepads, swearing and berserk kids, a game could
easily take days or weeks to finish. But once you knew the trick, the average
game could be finished within a few hours or less. And the somewhat better
games usually had a Password system though. Or at the very least, gave you some
cheats to skip sections, like the Warpzones or Magic Flutes in Super Mario
Bros.
Axe, Axe, Heart, Turd, Heart to head on Dracula's castle.
The Hard-coded magical Flute
But that
was then, this is now. Glad as I am that the Tower22 playable demo finally has
enough content/length that saving progress would be a nice thing, I certainly
don't want to play a goddamn flute to skip parts.
“Saving
& Loading” is probably not the first article you will find when learning
about "Game Programming". It's one of those "yeah, whatever.
Some day later I'll bother." features. Usually I just hard-coded some
cheats or hacks to start elsewhere in the game, magically unlock doors, or give
me item-X. That may work fairly well for level based games, but Tower22 is just
one big world. And to make things worse, it's sort of a puzzle game. So a lot
of conditions and events depend on stuff that has been collected or triggered
earlier on. Complex "chains" like these quickly get tedious to overrule
with hard-code, so basically I just had to replay the whole game to test if
Script-X at the end of the demo works... which didn't because of 1 typo in the
LUA script. 0 ups, game-over restart. Just like the good old days.
Yes,
about time for a Save/Load system. But honestly, I never wrote a Save/Load
before. Well, I did for other programs, but not games specifically. Usually the
games I wrote weren't really playable, or had less than one level content ;) So
what I'm about to write might not be the most solid solution, then again I'm guessing
there is no universal standard answer on this topic anyway.
Thankfully,
I sort of anticipated on this early on, so the engine had some tricks and
mechanisms prepared. Which sure wasn't a bad idea, because I think for a more
complicated (puzzle)game like this, where a lot of various stuff has to be
saved in order to pick-up, it's actually quite a fundamental part of the engine
entity systems, filestreams, and so on.
Save – Just a little bit
Correct
me if I forget something brilliant, but I'd say there are three ways to deal
with saving to begin with. Option1 is pretty simple: just save some numbers
like the current level, and remaining lives (called "ups" back in the
Good 'ol days). It's enough information to get us started in level-X. But what
if your levels are huge, or if you don't have levels at all?
In that
case you probably want to save a bit more information. Option2 does the same
thing, but in more detail. You would save details like the actual player
position, unlocked doors, his inventory, whether collectable items were picked
yes/no, and maybe the status of your enemies (if & where they got(killed)).
Now you can resume without having to restart, re-collect, or re-kill all your
opponents.
Option2
is still limited though, and requires a good portion of very specific code.
What needs to be saved, and what doesn't? And I'm talking about killing... but
maybe you were making fishing game or something. When making engines, you don't
(shouldn't) know such details, meaning you'll have to offer a more abstract solution.
Save EVERYTHING
Option3
is pretty hard-core: you literally dump the entire state of your world/level
and every entity inside. All exact positions, bullet holes, destructed
environment, caught fish, and so on. Works pretty well if your levels aren't
too big (or if there is just very little you can alter), and is often used for shooters
and such. And talking about old shitty games that couldn’t be saved, an Emulator
can! It dumps the exact memory(“RAM”) state. However, SNES RAM was about 128
kb, thus resulting in tiny files as well. But don’t try this at home with a
modern game, that may eat 3+ GB of RAM!
Oh, and
I forgot option4: just don't save at all. Easy Peasy. But seriously, in case
you are still baffled the devs didn't give proper "Save" options back
then; old systems like the Gameboy or NES gave you a bunch of (kilo)bytes on a cartridge,
go figure.
What the hell is that?! That, my grandson, is a SNES game. Didn't have Steam to download games, even CD-ROMs were yet to be born. Games looked more like computers back then, having a ROM chip and their own (battery backed) SRAM to store savegames. I always wondered why some games were almost 30 dollars more expensive than others. Well, as you can see, some games actually contained more physical hardware, like extra RAM memory or a SuperFX chip to render 3D stuff. So if your game didn't let you save anything at all, it was probably because those cheap assholes saved on a SRAM chip.
Save - anything Dynamic
I think
modern games use option2 in many cases. But instead of saving a bunch of
special numbers, it saves the attributes (position, state, special properties, …)
of anything Dynamic – stuff that can change. Static stuff – stuff you can
reload from its original source, such as sounds, textures or the World Geometry
(unless destructible) can be ruled out.
The cool
thing is that you can do this in the same manner as your Map-Editors would save
the files… but preferably without the map geometry itself. Why? Well, if your
mapfile is 500 MB of "level data", a SaveGame file would be equally
big. Now storage space isn't our biggest enemy anymore, but it takes quite a
while to load all that, and these days you may want to store these files in a
Cloud as well, which still makes them (too) big. And don’t forget, a modern
game saves your progress every 30 seconds or so. Don’t want to send those poor
kids more than a minute back in time, if they gang-raped by a horde of demons
again!
Yellow stuff = static data and/or initial states of dynamic data. Blue stuff = computed on the go, green stuff = data that can potentially change, and therefore has to be stored in a SaveGame file.
The Cleaning Crew
Another
less technical issue then. What IF everything would be stored "as it is"
in a game like GTA? The whole city would turn into a gigantic wrecking yard!
Bumped lampposts, burned cars, crashed helicopters, busted hoes. Sounds pretty
cool, but what if a trainwreck blocks your way into the building your next
mission needs you to be? Exactly, like in the Good 'ol days, you can suck it
and restart the whole game. And no, unlike SNES platformers, GTA doesn't take 2
hours to complete.
Another
example, just for the fun of it, what IF you would shoot all space-crabs,
flying lava dragons, and other insect-scum in a Metroid-like Exploration game?
The game would become pretty boring, and in case of Metroid, even more lonesome
once you killed all life on the planet!
No, that
magical cleaning crew that repairs the lampposts, cleans the streets, replaces
the cars and clears homicide scenes are actually quite welcome. What really
happens though, you walk/drive/fly far enough from a certain spot, so it
becomes "unloaded". Then when returning, the original scene -as
originally created in the MapEditor- gets reloaded again. Clear from mistakes,
forgiving your sins.
Please clean-up the Millenium Falcon on Isle Five.
Local versus Global entities
Tower22
does the same. You can run over a porcelain vase, but the cleaning ladies are
kind enough to clean up the mess and place a new one - if you are not looking,
they're shy. However, some entities should stay as they were. Unlocked doors
should remain unlocked typically, collected items should stay gone, solved
puzzles should stay solved. And Bosses should stay dead, definitely that.
In
Engine22, when making maps in the editor, every entity (furniture, walls,
items, monsters, lights, particles, and so on) can be marked as
"Global" or "Local". Local entities are bound to the
Sector they’re placed in, which is typically the room or corridor it stands in.
When (re)loading that piece of the map, it will also load all of its local
entities back in their original state (and yeah, a script can still move/hide/alter
them afterwards, if needed).
Global
entities on the
other hand aren't part of a specific Sector. Global entities can eventually
move throughout the world, for example, being carried as an item by the player.
Global entities can also remain stationary, but store a certain state or other
variables. Under the hood, each entity can have a property-list, or
"Blackboard" as we call them now in BehaviorTree terms. This
Blackboard is just a bunch of keys + values, like "doorLocked := false", "fuelLevel := 45", or "puzzleCompleted
:= true". Local entities can also have a blackboard, but as said, its
properties will reset back to default once being reloaded.
Global
entities are not saved in Sector(map-piece) files, but in 1 global file...
which is pretty much the "SaveGame". When starting a new game, you
actually also load a game, but filled with initial states. Then when moving on,
you alter that data, and save it under a different name: your SaveGame, Slot,
or whatever you would like to call it.
SaveGame file content
So
résumé, how does the "Save" file actually looks? In my case it’s a
binary file, with all global entities stored. This is done in the exact same
way as the MapEditor would do. Entity classes have their own "readFromStream"
/ "writeToStream" functions and may differ in detail, but in general
they would store a matrix, references like which material, sound or objectData
has been assigned, its current state, eventually defined by a Blackboard
(property-list). Speaking of which, there is also a Global Blackboard stored,
which contains all global "game variables", that can be shared
amongst all entities. Also handy, entities can also refer to each other. For
example, entities like the Player or a storage trunk can have an "Inventory",
which is basically a collection of items, thus pointers to other entities.
Last but
not least, the engine uses a callback to let you -the Game- write custom data.
In the case of Tower22 that would be the clock/calendar (which day are we, how
late?) and additional non-engine attributes related to the player.
Binaries
It
works, though I must warn about the snakes and scorpions in this method. First
of all, it’s a binary file. The good part is that ordinary teenage pukes can't
mess around with those files that easily, and binary files are relative small compared
to text-based files such as XML. They also load a whole lot faster. You’re not
parsing stuff, you just suck stuff straight into RAM objects / arrays.
Really,
I always wonder what the hell games or other programs are waiting for when
hitting "START". When I click the RUN arrow in Delphi, I'm playing
Tower22 4 seconds later. And yes, hundreds of (texture, 3D, material, audio,
world geometry, ...) files have been loaded then. I dare to say modern software
has become extremely lazy when it comes to RAM usage, file-loading, and so on.
My laptop has 16 times more memory and CPU than ten years ago... but Apps also
have become 16 times shittier in terms of resource management. Conclusion, everything
still sucks.
So yes,
I'm a binary fan. But there is one BIG BUT; they break easily. Just 1 wonky bit
is enough to screw up the whole loading process, and giving you a headache
trying to fix the file. Now if your binary format is final, and if you can guarantee
the save-process doesn't make mistakes, binaries are your friend. Otherwise,
expect trouble. Here a possible scenario. You finished your Minecraft game and
sold 800 billion copies. Fine and dandy, but now the community cries for a new game
element: an attribute that describes if a cube stinks or not. Being binary, you
can't just add another boolean in your per-cube properties. Older versions wouldn't
understand this file-format and basically read too little bytes per cube, newer
versions won't understand older files, and basically read too many bytes per cube.
Making the program crash.
You forgot
to add some sort of "version" identifier prior to reading your cube-data,
with as a result hundred millions of enraged teenagers. Kids lost their
8th-world-Minecraft-wonders, jumped in front of trains. You lost all your
money, your wife ran off with that big black dude, and your mother hates you
too.
That's
one possible scenario. Now you could have avoided this by adding a version or
something, so that your program can switch between 2 reading strategies, but
what if you didn't have version numbers at all in your original files? Use
them. Always. In my case, every (sub)instance that contributes to a FileStream,
first writes a version byte. It could be that 99,9% of the contributors didn't
change, but one specific entity type did, so I only have to alter its
write/read procedure based on its own subversion.
Still,
more structural adjustments can be a pain in the ass, and if you have a bug in
your filewriter, it’s very hard to fix corrupted files. Think about those raged
teenagers.
This is a higher level loading procedure; a Sector (map part) reading all its sub-entities (walls, floors, lights, eplosive barrels, sprites, ...). Note it first reads a version byte, which tells me how to anticipate on what follows. If something would ever change, I can "if ( version < 10 ) then ...". Entities on their turn do the same thing, inside their "ent.loadFromStream" procedures.
Incompatible Save states
Speaking
of versions. Another problem my system has, are possible differences between
dated SaveGames and newer versions of the game. In the playable demo, you have
to collect trash at some point (not kidding). But I forgot to mark that trash as
"Global" entities. So if you reload the game, all the trash would be
back, even if you already collected them. Which was actually a nice bug, as you
could cheat and reach the "18 items-collected" target easily. So, I
fixed that by marking the cola cans, banana peels and blood-vomit pools as
Global in the MapEditor. But, now all trash was suddenly gone. It wasn't stored
in my local sector files anymore obviously, but neither in my SaveGame. Result?
Couldn't collect all trash & finish the puzzle. The only fix was to restart
the whole fucking game - like in the Good 'ol days. Doh.
I don't
really know how to fix that -easily, without writing crazy hard-coded patches
in the load procedure. One more sophisticated method might be not to simply
dump all Global Entities into a file, but to write their "Mutations".
So you would always load the initial "original state" file first,
then go through a list of alterations. EntityX moved from A to B, Trash entity
#123 deleted, Poor Monster's health changed from 100 to -1, and so on. That
would at least respawn the trash. Heck, you could actually rewind all your
actions like an UNDO function!
Yet,
there is still no guarantee that this works. The initial state might be
different in a newer version of the game, leading to certain events that weren’t
covered in your dated file. You can’t reproduce the same state if certain
actors were missing in your “flight-record” data. Plus I'm guessing this mutation-list
gets veeery long, veeery quick. Basically you can’t just change the puzzles,
then expect your SaveGame still works well.
Slept well Samus? Metroid would save your inventory, which bosses are dead, and which passages have been revealed, I think. But you could get locked if the map or some of those passages would be changed in a game-update... Good thing there were no patches back in 1994. Devs had only one shot to make their game work well. Good 'ol days.
The thin red line
Probably
a better idea is not letting your game care about every single detail, and
having a fall-back plan. In Tower22, you can't save while playing really. You
save at special spots - in bed, when sleeping, to be more precise. So a puzzle
like this trash-collecting-nonsense is either finished or not, don't care about
the details. Tower22 doesn't have levels, but storing the current
"Day" may reveal enough information to estimate what has been
completed so far. A game like GTA doesn't save mission-details either, it just
saves whether missionX has been done or not. Based on that, the game can decide
what your next assignments and phone-calls should be, rolling on.
So in
other words, it's probably a good idea to have a "Life Line" stored,
telling the global game progress. Like "Chapter 6". In the very worst
case, you can help your players by letting them return to a fixed state based
on that, not having to replay the whole game - like in the Good 'ol days.
No comments:
Post a Comment