-
Notifications
You must be signed in to change notification settings - Fork 1
Adding New Objects to Systems
This page details how to add new objects to existing systems. For instance, it will explain how to add a new type of obj_item, a new variation to obj_node, and so on. These are in no particular order, and you can jump around as needed.
- Adding a new OBJ_ITEM
- Adding a new OBJ_NODE_PARENT
- Adding A New OBJ_MOB_PARENT
- Adding a new Type of OBJ_STRUCTURE_PARENT
- Adding a new OBJ_CRAFTING_BUTTON
- Adding a New Minor Relic
TODO:
- obj_tool
- New Region
- Add the name of the new item object to the item enumerator found in the create event of the backstage GUI object obj_inventory. Don't forget to update the value of height.
- Add the 32x32px sprite to spr_inventory_items and the 16x16px version to spr_inventory_items_small.Both of these can be found in sprites/GUI (for some reason?). The slot to add the sprites to corresponds with the item's value in the enumerator, counting left to right, top to bottom, starting at zero.
- You will also need to add the proper name of the item to the 'name_array' in obj_inventory. if it is a special case where its enum value does not match its index in the name_array (like how renown is -2 in the enum), you will need to add an exception to the scr_mat_array_to_text script to account for that.
- Once you've filled in the info, you can refer to it in code using instance_create functions. You will need to set the new instances item_num, x_frame, and y_frame variables. This will look roughly like this:
var inst = instance_create_layer(x, y, "Active", obj_item);
with (inst) {
item_num = resource_mat_number_to_spawn;
x_frame = item_num mod (spr_width/cell_size);
y_frame = item_num div (spr_width/cell_size);
}
// Note:spr_width and cell_size are controlled by obj_item
- Create a sprite to represent the node. It should be named "spr_name_node" and placed in the Sprites/Resources/Nodes folder. If it is a variation on a common type of node it may warrant its own subfolder. There aren't strict requirements for the size of the sprite, but there are a few rules of thumb:
- The sprite should have a width of 32px
- The height should be either 32px or 64px
- if it is a "double-tall" sprite (64px), its collision box should not extend past the 32px mark. If it does, there had better be a really good reason
- The center of the sprite will lie up with the center of the hex it spawns on, so keep that in mind when making the sprite
- Give it a face!!!
- Add the node to enum node_types found in the create event of the GUI object obj_NodeController.
- To add a new type of node, you need to know its region(s) and its relative percentage chance of spawning.
- Navigate to the scr_create_region_node script in the Scripts/Nodes folder.
- In the switch (_region) section find the case region_list.region for where you want the node to spawn and add it to the appropriate rarity tier. For instance, let's say I want to add an uncommon chance of an iron rock spawning in the mountain region, and before I start, the case looks like this:
case region_list.mountian:
common = [node_types.rock];
uncommon = [];
rare = [node_types.tree];
ultrarare = [];
break;
after adding it, it should look something like this:
case region_list.mountian:
common = [node_types.rock];
uncommon = [node_types.rock_iron];
rare = [node_types.tree];
ultrarare = [];
break;
- If this is a new type of node, you will also need to add it to the switch node_type section. This is a pretty straightforward process, and the easiest way is to just copy/paste an existing node type and update its specific instance vars. It looks like this:
case node_types.rock:
var node = instance_create_layer(x + 0, y + 0, "Active", obj_node_parent);
node.node_health = 2;
node.spr_to_draw = spr_rock_node;
node.tool_to_use = tools.pickaxe;
node.spawn_resource = item.rock;
node.base_rate = 10;
obj_NodeController.nodes_spawned += 1;
break;
- Create a sprite to represent the structure. It should be named "spr_struct_name" and placed in the Sprites/Structures folder. There aren't strict requirements for the size of the sprite, but there are a few rules of thumb:
- The sprite should have a width of 32px
- The height should be either 32px or 64px
- if it is a "double-tall" sprite (64px), its collision box should not extend past the 32px mark. If it does, there had better be a really good reason
- The center of the sprite will lie up with the center of the hex it spawns on, so keep that in mind when making the sprite
- Add the name of the structure to the struct enumerator in the create event of the Backstage GUI object obj_structure_menu and update the height of the enumerator
- In the step event of obj_struct_spawner, add a case to the selected_struct switch statement. It should look something like this:
case struct.name:
active_sprite = spr_struct_name;
active_object = struct.name;
required_mats = [item.rock, 10];
col_width = 25; //the width of the collision box of the sprite
col_height = 25; //the height of the collision box of the sprite
break;
- Go to the scr_create_struct script, and add a new case to the struct_type switch statement. The case should reference the position in the struct enumerator and you will need to set the draw_spr and draw_menu functions. It should look like this:
switch struct_type {
case struct.name:
draw_spr = spr_struct_name;
draw_menu = menu_type.name;
break;
Adding a new type of mob can be thought of as a three-stage process. First, you need to design the mob, then you need to animate it, and finally, you need to implement it. Designing it is a relatively straightforward process, you simply need to know what states it can occupy, what it can do in those states, and how it will transition to other states. After all, artificial intelligence is just a sufficiently large Finite State Machine (at least for the scope of this project). Animating a mob means creating animations to match the states outlined in the design, and implementing it is putting all of that into code.
While it is possible that some mobs will differ from the standard variation, I'm going to walk through this process as though you are designing a mob similar in function to the slime and explaining how you would change it if that wasn't the case. For basic mobs, there are four states you need to cover: Idling, Roaming, Following, and Attacking.
- Idling is the idle state, it's what the mob does when it doesn't have anything to interact with. All this state needs is an Idle animation and a couple of triggering conditionals so it can move to other states
- Roaming is very similar to the idle state, but in this situation, the mob is moving about on the map with no direction/goal. For this state, you need a moving animation and some triggering conditionals, very similar to the idle state conceptually
- Following is the state where the mob moves about on the map to a specified position, typically this can be thought of as following the player. For this state you also need a moving animation ( you can probably use the same one from roaming state) transitioning conditionals to move to different states and some fundamental logic for what you want the mob to be moving towards
- Attacking is the state in which the mob is interacting with another object to cause harm/an effect. Again, usually, this will be to the player, but under certain conditions, it could be towards other mobs or placed objects in the game world When designing these various states I find it helpful to sketch it out on a piece of paper so I can better conceptually understand it, and I also find this video from friendlycosmonaut on YouTube very helpful
I'm not very good when it comes to graphical work, so I don't have a lot to say on that aspect of mob animation, so this section is more than about putting the animation into the game engine. For the rest of this step, I'm going to assume you have all of the frames for your animation in a folder ready to be imported.
- All animations for a given mob need to be grouped together and placed in the appropriate folder in the asset browser. Mob sprites are stored in
//Sprites/Resources/Mobs/GroupFolder
. If I was creating a slime group, would go into//Sprites/Resources/Mobs/Slimes
whereas if I was creating a skeleton it would go into//Sprites/Resources/Mobs/Skeletons
. If there are multiple variations of the same type of mob, it might be helpful to create a subgroup to contain the animations for that specific variation, see theSlimes
folder for examples - The Sprite naming convention is very specific and takes the form:
spr_mob_ MobType_StateName_MobSubType
. For instance, if I was creating the Attack animation of an Arctic Slime, the sprite for this would be calledspr_mob_slime_attack_artic
- To actually import the Sprite, I find it easiest to have all of my frames named out in a workspace folder. To create a new sprite using the naming scheme in Step 2, use the import future to pull them all in at one time. If for some reason you need to import things frame by frame within the image editor, the Ctrl+Shift+A hotkey is a lifesaver. Once you have your frames imported make sure that the speed of the animation is set correctly (remember here that frames per second is the speed of your animation not the speed of the game).
- The origin point for sprites will usually be bottom center, but if that doesn't make sense with the specific sprite being used, the origin point needs to be as close to the middle of what part of the sprite would be “touching the ground.”
- The collision hitbox for the sprite should be the same throughout all of its animations. even if the mob sprite makes some sort of long-reaching/projectile attack. If that's the case, make a note of it, but that would be covered in the implementation step (in short, it will create a child object to serve as a projectile for the purposes of collision checking)
The first thing you need to do is add your new mob to the node tracking array. I know it might seem weird to think of a creature the same way the game handles rocks and trees, but for the purposes of spawning and resource tracking, they really aren't that different. Specifically, create a new entry in the node_types
enumerator which can be found in the Create Event of obj_NodeController
under //Objects/Resources.
Additionally, if the mob is going to be using new or different states then those already established, you'll need to add them to the mob_states enumerator located in the same place and make a note of the value of the new state.
Then you need to go to the Create Region Node script at //Scripts/Nodes/scr_crate_region_node
and define the mob in the Mobs section of the node_type
switch statement with the following values: mob_type, _health, attack_power, tool_to_use, and drop_array. You will also need to set the _successful_spawn flag to true. Once you've done that, go up to the _region
switch case, and put the mob in whatever regions and rarities that are appropriate for it.
Now for the really complicated part. For this section, I'm going to assume you're using the four basic states, but the process will be the same regardless of what states you're using. For each of the states you want to work with, you need to navigate to the location of their script file which can be found in //Scripts/Nodes/Mob State Management
. Once there, simply add the region and cases for the particular mob you are working with under the mob_type
switch statement. Here you will be working at instance scope level of the obj_mob_parent
object, so you have access to any of the instance variables from the parent object that you need. If the mob you’re working with does not use one of these specific states, you can leave it blank just make sure you don't ever transition to that state otherwise you're going to have a fatal error.
If you're using a mob state outside of the four basic states there are a couple of different things you need to do. First, you need to double-check that you have added state to the state tracking array, and assuming you have you need to navigate back to scr_region_create_node
so you can change the obj_mob_parent.states_array variable
. All you need to do is make sure that the index of the script you want to call for a specific state matches its corresponding value in the mob_states
enumerator. If this results in specific indexes of state_array
having nonsense information, that's fine. Ultimately all you're doing is making sure that this information matches up with what's in the step events of the mob parent object. If you need to create a new state management script, the naming scheme is scr_enemy_StateName
, So if I wanted to create a new state called teleport the script would be called scr_enemy_teleport
.
- Ultimately what you need to do is create a new case under the menu_to_draw switch statement in the Draw GUI event of obj_MenuBuilder. Once you've done that you'll be able to follow the instructions for adding to an existing structure menu below.
- To add a new case first you need to add the name of the structure to the menu_type enumerator in the create event of obj_MenuBuilder. Assuming you implemented the structure correctly, you won't have to specify the menu type to draw as it will pull that information from the structure object.
- The code for a new structure crafting menu should look something like this:
switch (menu_to_draw) {
case menu_type.pebble_refiner:
if !(menu_drawn) {
Recipes = [
// [Recipe Name, [Input Array], [Output Array], Base Crafting Time,
// "Button Display Text", "Flavor Text", Dynamic]
//Level Up
["Level Up", [item.shiny_rock, floor(power(1.1, struct_refrence.structure_level))], ["Level up", ""], 0,
"Level Up", "", true],
]
//Button Formating
var _button_count = array_length(Recipes);
var _width = 256;
var _height = 32;
var _h_space = 8;
var _i = 0;
// Background
var _background = scr_static_background_button(spr_brown_button_base, (screen_width / 2) - (.6 * _width), 70,
(1.2 * _width), (_button_count * (_height + _h_space) + 150))
ds_list_add(button_ref_list, _background)
//Buttons
var _button;
repeat(_button_count - 1) {
_button = scr_create_crafting_button(screen_width/2 - _width/2, 100 + (_height + _h_space) * _i, _width, _height,
Recipes[_i], struct_refrence);
ds_list_add(button_ref_list, _button)
_i++;
}
//Level Up Button
_button = scr_create_crafting_button(screen_width/2 - _width/2, 100 + (_height + _h_space) * _i, _width, _height,
Recipes[array_length(Recipes) - 1], struct_refrence);
ds_list_add(button_ref_list, _button)
ds_list_add(button_update_ref_list, _button)
menu_drawn = true;
}
//Update Dynamic Inputs
//Level Up Button
//Cost
Recipes[@ array_length(Recipes) - 1][@ 1][@ 1] = floor(power(1.1, struct_refrence.structure_level))
button_update_ref_list[| 0].input = Recipes[@ 0][@ 1];
//Time
Recipes[@ array_length(Recipes) - 1][@ 3] = 60 * struct_refrence.structure_level;
button_update_ref_list[| 0].crafting_time = Recipes[@ 0][@ 3];
break;
The section as soon as you've already implement the proper code for creating the recipe menu structure, if not see the section above on creating new types of menus.
The information you need to create a new button/recipe and fill out its recipe array is:
- “Recipe Name” - This text is The information is displayed in the crafting section after you've clicked on the recipe select button
- [Input Array] - an array that specifies the input for the recipe see, for example if I wanted a recipe that took three rocks and two coal to craft, if you put away would be [items.rock, 3, items.coal, 1]
- [Output Array] - This follows the same format the end footer a, but it represents the output of the recipe, If you wanted the recipe output one iron bar and 1 rock, the output array would be [item.iron_ingot, 1, item.rock, 1] Base Crafting Time - This is time the recipe takes to complete as measured in tics, 1 second is 60 tics
- “Button Display Text" - A string that specifies the recipes named, this is what is displayed In the recipe selection section of the crafting menu
- "Flavor Text" - Currently this isn't used, but the plan is to have the crafting menu section display this as a little blurb about the recipe being created
- Dynamic - This is a Boolean value that Flags whether or not the recipe has a dynamic input and dust needs to be added to button_update_ref_list. once the button is put into that list it will allow you to update its input
What you have that information you also need to determine the class of the button. The button classes are:
- Standard - Standard recipes are static and scale only with the level of the structure creating them. Once their input and output sir to find in the initial array they will not change
- Relic - Relic recipes have both a dynamic input and a dynamic output. because they are used to track various Powers the player has in different areas of the game these are the most complicated type of button to create
- Level Up - The level up button is a special case button. It has a dynamic input but a static output stop. It's input scales exponentially with the level of the structure by initiating the level function, but it will only ever increments the level of the structure by a set amount (Typically one, but it's possible that I create a relic that changes this)
If you are creating a standard recipe, simply add it to the recipe array with the above information and to make sure that it is not the last entry in the array (as the last entry is reserved for the level up button). If the structure does not have a level up button you can have your recipe be the very last entry but you will need to go down to the level up button section and disable it. An example recipe looks like this:
Recipes = [
// [Recipe Name, [Input Array], [Output Array], Base Crafting Time,
// "Button Display Text", "Flavor Text", Dynamic]
//3x Rock -> 1x Shiny rock, 60 tics
["Shiny Rock", [item.rock, 3], [item.shiny_rock, 1], 60,
"Make Shiny", "Its a rock, that's shiny", false],
Drawing the buttons is better covered above, but for reference, the code for drawing these recipes is:
//Button Formating
var _button_count = array_length(Recipes);
var _width = 256;
var _height = 32;
var _h_space = 8;
var _i = 0;
// Background
var _background = scr_static_background_button(spr_brown_button_base, (screen_width / 2) - (.6 * _width), 70,
(1.2 * _width), (_button_count * (_height + _h_space) + 150))
ds_list_add(button_ref_list, _background)
//Buttons
var _button;
repeat(_button_count - 1) {
_button = scr_create_crafting_button(screen_width/2 - _width/2, 100 + (_height + _h_space) * _i, _width, _height,
Recipes[_i], struct_refrence);
ds_list_add(button_ref_list, _button)
_i++;
}
If you are adding the Level Up button to a structure, make sure that the recipe is the last entry in the Recipes array, make sure that you mark dynamic as true so the button will be properly added to the button update reference array, and make sure that the 0th element of the Output Array is exactly “Level up” and that the 1st element of the array is non-empty. This should look something like this:
Recipes = [
//Any other recipes
//Level Up Button
["Level Up", [item.shiny_rock, floor(power(1.1, struct_refrence.structure_level))], ["Level up", ""], 60,
"Level Up", "", true],
];
Once you have that info, you need to include the draw code for the Level Up button. This code should come after the draw code for any standard or relic recipe buttons and is:
//Level Up Button
_button = scr_create_crafting_button(screen_width/2 - _width/2, 100 + (_height + _h_space) * _i, _width, _height,
Recipes[array_length(Recipes) - 1], struct_refrence);
ds_list_add(button_ref_list, _button)
ds_list_add(button_update_ref_list, _button)
Then after the button draw code make sure to include the following snippet (note this snippet assumes that the only Dynamic button you have is the level up button if that's not the case you need to change the button_update_ref_list index):
// Code here
Adding a new Relic button is a bit more complicated because it needs to be both Dynamic and reference a specific Relic. You'll still need the information from section one but there are a few specific things you need to do. You need to make sure that the output array's 0th element is exactly “Renown” and that its 1st element is the reference to a specific relic in the minor_relics enumerator. Relic recipes follow the same ordering rule as standard recipes, which is to say that it generally should not be the final element in the recipes array And if it is you need to remove the drop code for the level up button. The Recipe array should look like this:
Recipes = [
//Renown -> Pick Power, 600 tics
["Pick Power", [item.renown_ref, floor(power(1.1, obj_relics_menu.pick_power))], ["Renown", minor_relics.pick_power], 600,
"Level Up Relic", "", true],
];
Once you've done that you will also need to add any necessary dynamic update information to the Update Dynamic Inputs section. Again using an example of the pick_power relic and come after the if !(menu_drawn)
conditional, that would look something like this:
//Update Dynamic Inputs
//Pick Power
//Cost
Recipes[@ 0][@ 1][@ 1] = floor(power(1.1, obj_relics_menu.pick_power));
button_update_ref_list[| 0].input = Recipes[@ 0][@ 1];
//Time
Recipes[@ 0][@ 3] = 300 * obj_relics_menu.pick_power;
button_update_ref_list[| 0].crafting_time = Recipes[@ 0][@ 3];
Of course this still isn't the end, because once you've done this you need to add a special case exception to the Renown Shop and Relics
section of scr_show_and_craft script. Hypothetically if I wanted to add the pic power relic to this section it would start like this:
//Renown Shop and Relics
} else if (struct_id.output[@ 0] == "Renown") {
switch (struct_id.output[@ 0]) {
case (minor_relics.example):
obj_relics_menu.example += 1;
break;
}
and after adding it, look like this:
//Renown Shop and Relics
} else if (struct_id.output[@ 0] == "Renown") {
switch (struct_id.output[@ 0]) {
case (minor_relics.example):
obj_relics_menu.example += 1;
break;
case (minor_relics.pick_power):
obj_relics_menu.pick_power += 1;
break;
}
This process is a bit more complicated than anything covered on this page yet. Setting up the tracking information for minor Alex is a straightforward and simple process, however, implementing their effects correctly is going to vary wildly depending on these specific relic types you're trying to implement so covering how to implement it is not really something that tutorial can be written for.
- There's currently not a process for implementing any visual aspects to the relics. That is currently being tracked with issue #60 and once and when I did I will update this section accordingly.
- Add the name of the minor relic to the minor relics enumerator found in the create event of the Backstage GUI Object obj_relics_menu. Update the height variable.
- Create any specific tracking variables you will need for that minor relic in the same event below the enumerator and make sure to label that section with a comment so that it will be easier to navigate in the future.
- Once you've done that, you just go in and create a way for the player to obtain this minor relic, I imagine the most common way we'll be by creating a crafting button in the points shop structure. And then going throughout the codebase and using its tracking variable in places where its calculation would be important. For instance, using the pick_power relic, I have a pick_power tracking variable that is used in the node left-click script to increase the amount of health subtracted from each node on each click.