Monday, January 30, 2023

January Focus: World Object Spawn Engine (sort of)

The month of January, where did it go?  I was able to get a ton of work done in December while on a very long vacation and it is unfortunate that I cannot devote as much time to the game as I did then.  But such is life, and my development efforts here are returning to normal levels.

The focus this month was the world object spawn engine.  I had several goals with this but did not realize just how much ground needed to be cleared before starting to build this important piece of the game. What is this thing I call the World Object Spawn Engine?  Simply put, it is the part of the game that allows me to manage the spawning of objects into the game world.  For example, chopping down a tree is supposed to yield one or more logs.  The world spawner needs to know what to spawn, when to spawn it and where.

As I started looking into this, I discovered a few issues with the map, viewport size, and the placement of trees and the player.  What I thought was fairly accurate turned out to be a bit messy when I actually wired up the ability to see the tile positions in relation to the player and the trees.  These obscure issues really led me down a rabbit hole.  First off, I created a tile selector.  It is a hollow square that follows the mouse around the map snapping itself to whatever tile it is hovering over.  As with many things in the Godot game engine, it was amazingly easy to implement.  

func _physics_process(delta):

var tile_pos = ground_tilemap.map_to_world(ground_tilemap.world_to_map(get_global_mouse_position()))
position.x = tile_pos.x
position.y = tile_pos.y 

This script is attached to the Selector object, which is an Area2D.  The Area2D has a Collisionshape2D child, and that has a child Sprite.  Voila!  Simple.  With this visual aid I was able to factually know the location of tile edges.  This helped me determine where the trunks of trees should be and where the player should be.

While my attention was focused on the tilemaps and aligning the player, I made a decision to remove the north-face and south-face player sprite animations.  This is by no means set in stone, but I felt that east and west facing sprites were completely sufficient for the purpose.  A few more tweaks and adjustments here and there and by mid-month my attention finally shifted to the object spawn engine.

There is a saying in software engineering, "Data is king."  It means the most important decisions we make will always revolve around the data we are wanting to manage.  I needed to think about what kinds of data this engine would manage.  Stewing on this made me realize I had to think beyond the spawn engine in order to develop it.  Game objects and systems are very tightly intertwined, and they should be.  Gathering world objects creates inventory items which are used as ingredients for crafting recipes to make other world objects.  They all tie-in together like a puzzle.  

Crafting requires recipes, which require ingredients and tools.  Recipes need to be stored in some kind of data structure that identifies a) the item to be created, b) the ingredients required, c) the quantity of each ingredient, d) any tools that may be required to craft the item, and e) how long it would normally take.  Crafted items are made from other items and it is typical to start the whole process with gathering.  You are probably well familiar with the stick, rock, and plant fiber paradigm especially in the context of sandbox survival games, which this hopes to become.  The player gathers raw resources that are in themselves usable crafting ingredients and sometimes tools.  From there, a + b = crafting something else, and so the tree grows from there.  Data!  We must be able to wrap all this around some kind of data.

Godot offers a few data structures that we can work with.  They are the Dictionary{} and the Array[].  The dictionary has a key and a value.  The key has to be unique.  The value can be anything.  I have already identified most of the data elements I am going to need to manage object spawning, gathering, and crafting.  What can we do with these structures in Godot to manage this data?  Let's do a simple recipe of two ingredients.

recipe_type =  { "flint_and_steel" : ["flint_stone", 1]}

The recipe_type is a dictionary.  The key is the name of the item to be crafted, "flint_and_steel".  The ingredients will be the dictionary value, an array[].  So far, we have an array which contains a string value of the name of the ingredient "flint_stone" and an integer value which is the quantity needed of this ingredient, 1.  But how do we expand this to more than one ingredient when we already have an array as the value of the dictionary?  Take a look at this.

 recipe_type =  { "flint_and_steel" : [["flint_stone", 1], ["fire_steel", 1]]}

We now have an array of arrays.  The inner array is [ingredient, quantity].  The outer array contains two inner arrays, forming another array.  How would we read such a data structure?  We would reference it like this:  array[0][0] would equal "flint_stone".  Array[0][1] would equal 1.  Array[1,0] would equal "fire_steel" and, you get the picture.  This is close to what we need, but we also need to store some other pieces of data.  Ingredients are what we call many-to-many relationships.  Many recipes can have many ingredients.  But what can we do about a one-to-one relationship?  For example, we need to store the base crafting time (how long it takes) to make the flint_and_steel recipe_type.  Let's introduce another string value and integer value to the array.

recipe_type =  { "flint_and_steel" : [["base_time", 20],["flint_stone", 1], ["fire_steel", 1]]}

I know that base_time is not an ingredient. Since every recipe needs a base_time value, I choose to place it at the beginning of the inner array and give it a special name, "base_time" and value x.  I placed it at the beginning of the outer array because I will extract that information always for every recipe from array[0][0], and [0][1].  I could search the array to find a match, but searching is typically an expensive operation in software development.  Better that I know where it is.  It is my data structure, after all!

Ok, so I know the first element is for base_time.  The rest (ever how many they are) will be ingredients.  This brings me to the last thing I need to manage, the tools.  Tools are interesting.  If a recipe calls for a cutting tool, do I just let the player pick any cutting tool, or do I dictate somehow which cutting tools would be appropriate?  I think the latter is more sensible, else the player might try to craft something requiring a knife by using a tomahawk!  This led me to another data structure that will help me manage these decisions.

tool_type = {"cutting_tool" : [["chert_chip", 15],["stone_hatchet", 25],["chert_knife", 30]]}

Similar to recipe_type, the tool_type dictionary will be an array of arrays.  The key will be the type of tool, in this example, cutting_tool.  Then, all the crafted items in the game that are considered able to cut something will be listed with an integer value representing how proficient they are at cutting.

Let's now suppose that I need to have a recipe which contains ingredients along with a cutting tool.  Back up in the recipe_type dictionary, I make another rule for myself.  If the Ingredient begins with an "*" it means it is a tool and not an ingredient.

recipe_type =  { "flint_and_steel" : [["base_time", 20],["flint_stone", 1], ["fire_steel", 1], ["*cutting_tool", 1]]}

The "*cutting_tool" will be processed as a tool type, not an ingredient.  I can take this reference and check to make sure the player chooses a tool that belongs to the "cutting_tool" category to satisfy the recipe for the item to craft.  Back in the tool_type dictionary, I can pull the cutting tool list from the key "cutting_tool" and obtain the success chance for any tool from that array.  

So then, I have two dictionaries, recipe_type and tool_type.  I can create recipes with any number of ingredients, any number of tool types, and I have all the information I need at this point in the development of my game crafting system.  I also know what kinds of data I need to manage.  And, I can built upon this system in any way I want. 

This is what I plan to do in February, to continue to design and implement a world object spawner, a recipe and crafting system, and the UI to make this all come together into something I hope will be thoroughly enjoyable.

Until next time...

No comments:

Post a Comment

Feel free to leave a comment. All comments are moderated.