I’ve been working on a new personal project these past couple of weeks. It’s odd how quickly I bounce from project to project, but I think I often struggle because I try to limit myself to a very concise and contained game, and a few weeks after heavy development, I come to my senses and realize that I’m cutting out features left and right purely for the sake of making it more manageable. The problem is, sometimes I have a tendency to cut out the bits that make me go, “Oh woah, that’d be awesome!”, and the game starts melting down to nothing but a bare bones experience. This new title I’m working on is unlike anything I’ve ever tackled before, for two primary reasons:
First, it’s a genre totally beyond anything I’ve ever attempted before. I usually play it safe and stick to projects that fall within genres I’m comfortably familiar with – such as 2D ARPG’s and platformers. This time, I’m jumping outside of my comfort zone and tackling a 2D survival sim with procedurally generated worlds and online co-op. For real.
Secondly, the tone of the game isn’t cutesy or colorful – no big-headed characters with rosy cheeks and glistening eyes here! It’s a much darker, more serious tone, something I’ve never really given a serious attempt, but I’m beginning to find that this specific style has a demanding atmosphere of its own, a certain distinct and overwhelming characteristic that you can’t quite draw from the cuter 2D games. I like it.
So far, it’s given me a very welcomed opportunity to design aspects of gameplay I hadn’t ever run across before, such as working with guns, crafting and loot, procedurally generated worlds, etc. I’m learning a lot! So I’ve decided I’ll share with devlogs as I walk through this project. They’ll be short and concise, but I’d still like to share my approach and thought process!
Because I want the overworld to be EXTREMELY HUGE, I realized there simply isn’t any other way to approach it than dynamically loading portions of the overworld into memory and unloading when no longer needed. I have a basic system up and running, and it’s working quite fine so far! The entire world is divided into “chunks”. A chunk is a term I’m using just to describe a file filled with a bunch of 1’s and 0’s that explain the tiles and collision data for a specified area of the world, sort of like breaking the screen down into a grid. Each grid space/cell in the chunk occupies 16 bits (2 bytes) of chunk data, allowing me the opportunity to have up to 65,535 different types of blocks, and the size of a chunk, in bytes, would be the number of grid spaces we can fill in the chunk * 2 (since each block occupies 2 bytes – so if we’re dealing with a chunk with 256 grid spaces, it would consume 512 bytes of data – less than a kilobyte!). Each chunk is saved in the game directory with a very specific labeling indicating where in the overworld the chunk exists. When the player nears the chunk in the overworld, the chunk will be loaded into memory.
Player collisions with the environment actually aren’t handled by Gamemaker in any way whatsoever – imagine how long it would take to read through the chunk data and create all of the appropriate collision blocks every time we loaded a new chunk into memory. The more collision blocks/instances you have, the longer it takes to add a new one (so, in a sense, the time grows exponentially). This was my original approach – using gamemaker instances to handle collisions paired with gamemaker’s built in collision checking functions. As always, I ran some tests and crunched some numbers to see if this approach was feasible. Here are my results:
creating 22,500 block instances:
same room, while loop: 3017 ms.
same room, while loop, no levels_created: 2932 ms
same room, if statement: 3126 ms.
if loop + checking array :3298 ms.
To be entirely honest, I can’t quite remember the distinction between all of those (hey, don’t blame me! Once I realized the method wasn’t going to work, I stopped focusing on it :P). You can see though, filling a room with 22,500 blocks takes over 3 seconds.
Well, this is a shame. I can’t find where I wrote my notes. I jotted down some notes from my tests about accessing data from a buffer, how long it takes to save/load/create, etc. In either case, the results were blazingly fast!
I then realized that I had to program my own collision routines – rather than checking for a block instance in the player’s target position using place_meeting, collision_rectangle, etc., I decided to just read the data from the chunk I loaded into memory directly. The approach basically involves converting the player’s current x and y coordinates to a grid and comparing it against the chunk’s grid data, where a 0 would indicate a free location, and any other number would indicate a solid or passthrough block. An old school method I haven’t done for years! But the benefits are great, such as being able to have 100% total control over all collisions that occur, the ability to expand and customize to suit my needs, etc. The drawbacks? It’s a bit more tedious approach, so ALL the collision detection routines have to be programmed from scratch. Also, It’s a tile-based system, meaning that we can only create tiles snapped to a grid, so anything out of the grid’s subdivision level would simply be impossible. This isn’t to say we can’t have slopes – we certainly can! We’d just consider a slope a different type of block and do a bit of math to determine where to position the player on the slope based on the player’s current coordinates, where the slope began, and the slope’s angle.
Because the chunk system loads and unloads dynamically as you explore, we could hypothetically have an infinite terrain that expands forever with no hit on performance or memory! The only limitation? Hard drive space. Each chunk is saved as its own file and loaded into memory when needed. Currently, a chunk takes up about 7 kb of space just for the collision data – once we add in tiles in different layers (for parallax scrolling), it’ll up that size even higher. We’re still talking a relatively small filesize per chunk, but it is something to keep in mind that, although the world can be infinite, the player’s hard drive space isn’t!
The system is coming along quite nicely. Currently, when you enter a chunk that hasn’t yet been initialized, the program will create a brand new empty chunk (basically, a buffer filled with 0’s), immediately save it to your hd, and you can add and destroy collision blocks ingame which are updated and saved realtime to the chunk data file. Because accessing and saving buffers is super speedy, there’s no hit in performance for editing realtime! It was a bit tricky getting everything up and running, but Hexplorer is an absolute timesaver when you’re building custom file formats and writing data byte-by-byte – you can scroll through every byte of data, both with a Hex and ASCII view (and decimal info).
I’m intending on having the world seamlessly wrap around itself, and the procedurally generated terrain should extend for about 20 minutes of walking time (once you build some ingame vehicles, you’ll be able to traverse across the lands much quicker!).
Last thing I want to touch on – I keep tossing around “procedural generation”, but I want to mention that I haven’t attempted this portion yet. Because of the nature of this game, I’m already feeling pretty comfortable about how I’ll approach coding the algorithms, but I won’t know until I dive into the procedural stuff hardcore come January!
As always, I’ll keep you guys up to date on the progress of this project!