Sunday, November 27, 2022

A Seamless and Wrapping Tilemap Without Chunking

Back when I was working on this game in UNITY, I had developed a chunk engine for my tilemap data, and I was quite happy with it.  I generated map chunks that were stored in files and loaded them into memory as the player moved around the world - the typical map chunk approach.  It had its good points and not-so-good ones.  The game world could be massive because it only loaded chunks of map data.  But, the world did have edges and if the player reached the edge of the world, he would need to alter direction.  Also, there was always a noticeable lag spike when the engine decided it was time to load and unload chunk data.  Then too, there were so many tiles in memory at any given moment.  The chunks were 25x25 if I remember correctly, and there were 9 chunks possibly in memory.  That amounts to upwards of 5625 tiles.

When I switched to Godot, I wanted to do something different.  I didn't want the player to ever see the edge of the tilemap world.  Also, I didn't want to deal with chunk loading and lag spikes.  Finally, I wanted the map to be seamless so the tiles would blend at the edges.

A Wrapping Technique

In order to visualize tilemap wrapping, I did what software engineers do.  I threw together a program to simulate my goal.  I opened up Visual Studio and created a Winforms application.  I generated a grid of random numbers to represent tiles.  I then wrote an algorithm to determine which numbers needed to be populated based on the center target number, the player.  While the code turned out to be fairly straightforward, taking this approach allowed me to visually verify my code was working as intended.

The above example seems easy enough to figure out.  But the code needed to figure out which tiles would surround the player no matter where the player was on the map.  Consider the following example.

Since the player is on the east edge of the map, the western edge needed to be included in the surrounding tiles of the player.  Let's get a little more complicated and put the player on two edges of the map.

The above example needs to pull from all four corners of the map because the player "4" is against both the east and south edges.  The code in the WinForms app successfully pulled together all the numbers to display the correct area around the player and assured me that it worked.  Here is my Winforms C# code for populating the numbers on the right side of the screenshots, which represents the view of the map in the game viewport.

private void btnLocate_Click(object sender, EventArgs e)
        {
            tbView.Clear();
            var playerX = numX.Value;
            var playerY = numY.Value;            
            int start_X = decimal.ToInt32(playerX - (decimal)Math.Floor(viewSize * .5));
            int start_Y = decimal.ToInt32(playerY - (decimal)Math.Floor(viewSize * .5));
            for (int y = 0; y < viewSize; y++)
            {
                for (int x = 0; x < viewSize; x++)
                {
                    int pos_x = start_X + x;
                    int pos_y = start_Y + y;
                    int key_x;
                    if (pos_x >= mapSize)
                        key_x = pos_x - mapSize;                    
                    else if (pos_x < 0)
                        key_x = mapSize - Math.Abs(pos_x);
                    else key_x = pos_x;

                    int key_y;
                    if (pos_y >= mapSize)
                        key_y = pos_y - mapSize;
                    else if (pos_y < 0)
                        key_y = mapSize - Math.Abs(pos_y);
                    else key_y = pos_y;

                    tbView.AppendText(" " + mapData[key_y, key_x].ToString() + " ");
                    if (x == viewSize - 1)
                        tbView.AppendText("\r\n");
                }
            }
        }

The "key" is the key!  If position steps outside of the map size, the key compensates by pulling tile data from the opposite side of the map.  How did I implement this?  Well, first I need to explain my concept of a world without chunks.  When I generate my perlin noise tilemap, I store the tilemap data in a dictionary where the key to the dictionary is a Vector2 map position and the value is a tilemap tile type.  The dictionary is held in a singleton and gets saved and loaded along with all the rest of the game's save data.  I also store the current tile position of the player in this singleton.  The tilemap never gets loaded with the entire dictionary of tile data.  It only gets loaded with those tiles around the player's map position, the viewport plus a few tiles' worth of border.  Every time the player moves, I update the player position and then I clear the tilemap and re-populate the tilemap with the new tile content around the player.  This means every move of the player will clear the tilemap, load it with the new tiles, and render them to the viewport.  The code above handles the wrapping of the map so that I know which tiles to read from the dictionary.  The effect is that the player can move around the game world, no matter how large the dictionary world is, without chunking and without lag.  I have an established view size of 49x33 (which is very close to 16:9 ratio).  This configuration loads 1617 tiles and renders them in the tilemap at the end of every player move.  Does it studder?  Not even remotely!  There is zero lag, zero flicker, and the player never sees the edge of the tilemap.  Not only that, but my tilemap tile types are all AUTO_TILE types.  It is simply beautiful.  The map data for the entire map is in memory, not as tile nodes, but simply as dictionary entries of a tile type indexed by a Vector2, which is quite small in comparison.  This makes the tilemaps only ever 49x33 in size even though the dictionary may hold a million tile entries or more!

Seamless Perlin Noise

The wrapping tilemap would be of little benefit unless I was able to create seamless Perlin noise.  It took me quite a while of digging and searching to figure out how to do this using Godot's get_seamless_image() feature.  It is not well documented.  Fortunately, I found an implementation of it and it works well.  This is the start of my generate_world_data function().  The following code is GDScript:

    randomize()   
    var noise = OpenSimplexNoise.new()
    noise.seed = randi()
    noise.octaves = 2
    noise.period = 192
    noise.persistence = .75
    noise.lacunarity = 4
    var noise_image = noise.get_seamless_image(auto_global.map_size.x)   
    noise_image.lock()
    for x in auto_global.map_size.x:
        for y in auto_global.map_size.y:                       
            var pixel_map = noise_image.get_pixel(x,y)
            var noise_value = pixel_map[0]    #0 to 1           
            var map_tile_cord = Vector2(x,y)

Auto_global is a singleton where I store the size of the game world and the tilemap dictionary data.  The noise_value uses the first value from the pixel data.  This value ranges from 0 to 1, unlike noise.get_noise_2d(x, y) which contains values ranging from -1 to 1.  Taking this along with the logic from the code above, it is very easy to take a flat 2d tilemap world and turn it into a circular ball of sorts.  The result is a seamless, wrapping tilemap with no chunking required.

Feel free to leave comments or ask questions.  Thanks for visiting and reading along on my game-dev journey.

Until next time...

Saturday, November 19, 2022

The Inventory System

My Godot achievements are growing, thanks to just how easy it is to do stuff in the Godot game engine.  In just a month's time, I have been able to create a robust inventory system for the game.

Eyesgood's Inventory System

The inventory system is a resource-based system that manages containers and items. It was very important to me that my inventory system could separate mutable properties from immutable ones.  What does that mean?  It simply means the definition resource classes hold all the properties that do not change for the containers and items.  For example, name, description, texture (display image), slot size, max stack size, number of small, medium, and large slots (for containers), weight, length, width, height are all properties that do not change.  They are immutable.  That means I don't have to save these properties in the save game file.  They are static and can reside in a resource file.  However, when I create instances of containers and items, they have properties that do change.  For example, id, parent id, parent slot id, stack amount, UI position (where a container is when locked on the screen), etc.  By taking this approach, the save game file only contains the mutable (changeable) data for each instance of each container and item in the game.  This will reduce loading time, which is something I need when I contemplate a game world of millions of tiles - yes millions!

The UI consists of a single dynamic inventory window that is constructed based on the container's definition.  In the above screenshot, all three windows are the same scene, only they are constructed at runtime from the container definition, which states how many small, medium, and large slots are defined as well as how many columns should be drawn for each slot size.  Since everything is data-driven, I can create an inventory container for every container in the game in any configuration I can imagine.

The Player Inventory window is special in that it has no parent.  But it is a container and therefore has a container definition of two large slots, four medium slots, and twelve small slots.  The backpack fits into a large slot in the player inventory.  Opening it up, it is also a container and is defined containing zero large slots, one medium slot, and six small slots.  Finally, the leather pouch fits into a small slot. But opening it up reveals four small slots.

I wanted to make the inventory system robust but also logical.  A Struggle to Survive is all about working with what you have - which is not very much.  It isn't an MMO where you can hold 50k items in your inventory and run around at full speed.  That's not what I am trying to design.  I want pseudo-realistic design that really forces the player to make decisions just like in a real-life survival scenario. Take the small pouch as an example.  It takes up one small slot in the player's inventory.  Opening it up reveals that it has four small slots for storing small-slot items.  But looking at the player inventory window, we see that a canteen is also a small-slot item.  So is a bear trap.  Does it make sense to put four canteens or four bear traps inside a small leather pouch?  Certainly not!  Therefore, I had to come up with a way to create limitations to the containers that make sense from a pseudo-real-world perspective.  Furthermore, I also wanted to limit just how much a player can carry around.  The solution to these two problems was solved with the concepts of Volume and Weight.

Taking the simple formula of volume = length x width x height, I assigned a volume value to each container.  I also assigned a volume for each item.  The leather pouch has a volume of fifty (5 inches x 2 inches x 5 inches).  When an item is dropped, the volume of the item is compared to the remaining volume of the container.  If there is room, the item will be placed in the container.  If there is not enough room, the item will be rejected and the player will be told the item will not fit inside the container.  Volume allows me to prevent a canteen from being placed inside a pouch.  But it will fit nicely inside a backpack (18 inches x 6 inches x 18 inches).  Since the player inventory is itself a container in this system, I simply set the player inventory to 100 inches x 100 inches x 100 inches, which provides a volume of 1 million - more than enough to handle the slots and volumes of items placed inside.

Weight is currently just used to make sure the player can actually carry what is in the inventory.  The player's strength coupled with a few other attributes will determine how much the player can carry before being hindered from running or even walking.

I am extremely excited to have been able to complete an inventory system for my game.  This is something I struggled with when I was coding the game in UNITY.  But with Godot it was a breeze!  And, having so much community exchanges of ideas and the documentation was extremely helpful.  Permit me to share with you some of the features of this inventory system.

  • Resource-based with game saves only storing mutable data
  • Three slot sizes: small, medium, and large
  • A dictionary-based list of containers and items
  • A single scene for the inventory window, created at runtime based on the container definition
  • Definable slot configurations with column settings
  • Drag-and-drop based on slot size recognition
  • Transfer of items across open containers
  • Item stacking with defined maximum stack limits per item
  • Container volume calculations with checks for remaining volume based on existing container items
  • Recursive volume and weight calculators to get total weight and volume regardless of how many containers are stacked inside other containers
  • Container windows spawn with a tween grow from nothing to max size when opened and reversed when closed, providing a very smooth animation
  • Containers can be positioned anywhere on the screen and locked to prevent accidental movement
  • Container windows can be stacked and will jump to front when clicked
  • Weight can be shown in metric or standard designations to two decimal places
  • Container total weight is shown on each container and includes child container content weights
  • Tool-tips provide instant visibility of container and item name, weight, and volume stats

I haven't decided yet whether I will attempt to create a series of videos showing how to create this system.  It isn't that I want to keep its design to myself.  I am happy to share it.  It is more that creating videos often takes as much or even more time to do than coding the feature the video is about.  We shall see.  In the mean time, thanks for reading along.

Until next time...