Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
Building Your World - Your Way
In this article I will be discussing some of the reasons as to why and how I went about decoupling Game Makers Room Editor from the production of my game: ORB, and ended up creating my own in-game editor.
Building your world - Your way
A high level strategy for implementing your own in-game editor in Game Maker - Part 1
In this article I will be discussing some of my reasons as to why I went about de-coupling Game Makers Room Editor from the production of my game: ORB, and ended up creating my own In-game editor.
There is much to talk about when it comes to the implementation of your own editor, so much so that If I were to attempt to give you a detailed step by step guide on how I went about making mine, I’d be writing this document for ages - and as it stands - this article is already quite lengthy!
Instead I thought I would take a slightly different approach and present a few of my high level thought processes, and leave the implementation details and strategy up to you. Even though I do give two pseudo-examples of the save and load routines - this article is not a direct how to, and It most certainly is not a “quick win copy and paste solution”.
Instead it touches on some of the more crunchy points that I had to consider and figure out through trial and error - it will hopefully provide some food for thought and some insight should you wish to pursue your own.
I have left many smaller details out, however I will be more than happy to extend or dig deeper into certain areas should there be enough interest on this topic. Just drop a comment or send me a PM on one of my social outlets, which you can reach by visiting www.orbgame.net.
Why?
I guess the first place to start breaking down this mammoth topic, is to address the question: Why?
Well, personally for me, my top three reasons for going down this road are as follows:
1. The ability to allow the community to build and share levels with each other
Given that Game Makers room editor was required to actually create levels in the first place was a no-go for me. Can you imagine the marketing campaign? "Hey you can create your own levels in ORB, but you need Game Maker, and the source code..." What I needed was a way to externalize my level data, and allow it to be shared via in-game network or file systems.
2. Confidence in Game Makers object indices
Sometime during the very early development phase of ORB I lost confidence in the way Game Maker kept track of the object indices - I had a case where I was unable to delete the first object out of my solution without causing my entire project to break :/ There were also a number of times during my initial development phase where I would move objects around, and strange things would happen. I do not have an answer for the actual issues that I experienced - and in part, most of them were probably due to my lack of understanding the GM architecture, however given the direction I was taking - relying on GM’s object indices wouldn’t matter anyway.
3. Development turn around time
By far the biggest gain for me is the turnaround time between changing and testing a level through an easily accessible in-game editor. This somewhat outweighs the top two points quite drastically as having that power to change your levels on the spot without having to close the game, open up an external room editor, re-build the game and then finally run the game to test a minor change, is a major win for productivity.
I stand corrected here: However I believe that if you start externalizing your game object indices into your own custom level format, you run the risk of these objects changing indices inside your project, while your external level files will not be updated. For this reason I suggest the implementation of some form of mapping layer. If this is not the case - you can put your mind to rest over any mapping issues you may have between your level file and your internal game objects anyway.
So - how do you go about building your own editor, and de-coupling Game Makers room editor from your development cycle? Unfortunately, it will require a little bit of work and effort on your part - the first 6 months of the development of ORB was dedicated to implementing a stable, robust and extensible editor - a year and a half later and I am still making improvements and fixes (Granted, even though GM was still very new to me - my background as a software engineer meant that principles such as data structures, performance, memory management and general coding practices were not new, making it more of a syntax exercise instead of a foundation exercise). The question you need to answer is - Do I really need to build an in-game editor?
A Bird’s Eye View
The object layout and hierarchy
The first and most important thing you will need is a root or a base object, even if you are not creating your own editor, you should consider one anyway!.
Every single game object that you need to either persist or interact with in the world through your editor will require this object to be its parent.
There are other benefits of going down this road as well - it will assist you with building a state management system (checkpoints with persist-able objects) should you want one. It will aid in your performance management and it gives you a nice central repository to implement an effective pause system. Lastly, it will assist with easier and faster lookups of objects - should you need to do so.
What might one find on this root / base object?
This is obviously extremely dependent on the game you are making - however there are certain handles which you can start thinking about and implementing right away. For me these handles were among the most important:
Selected: This flag informs the editor whether this object is currently selected
Just_Selected: This flag informs the object whether it has just been selected this step.
Just_Deselected: This flag informs the object whether it has just been de-selected this step.
Editor_Object: This flag informs my editor whether this object belongs “on the Editor GUI” or in the game world. For me this is was very important as I did not want to have to replicate all my game objects (one for the world and one object for the editor). Rather I chose to have a flag which is set appropriately via scripts when you instantiate the object. For example, let’s say you have an object in the world that has a somewhat different draw routine from the standard sprite that you assign to it. If this object were to be placed into your editor, then it would draw itself like it would draw itself in the world. This may, or may not be desirable - however I have a few objects which emit particles, rotate sprites, scale them and so on. This would make the editor GUI a little too noisy and inconsistent to work with - not to mention difficult to understand and most probably unusable!. You’ll want a separate draw routine for those objects that appear on the editor GUI vs those objects that appear in your world. My typical object will have both a custom Draw and DrawGUI event which will draw the instance correctly, based on knowing where it lives.
Parent_Draw: This flag informs my engine whether it should even attempt to draw the parent sprites of the specialized object if the child does not have one. Useful for placeholder sprites and overriding the draw routines (while still keeping all necessary base draw calls in-tact). Note this is NOT the same as simply not invoking event_inherited(): When the editor is active I would preferably like to handle all the common drawing routines such as borders and resize handles in the base. If we omit the event_inherited() call we won’t even be given the chance to determine whether or not we need to draw these editor hooks.
There are obviously a few more properties on my base that are not mentioned here - properties which can control markers for scaling, light sources and the likes - however that falls outside the scope of this write up.
If you are concerned over the additional weight that your objects may carry, you can manage this by effectively utilizing scripts and by only declaring your editor’s base properties when you are in “development mode” or similar.
In my personal opinion, there are two schools of thought at play here when considering how to go about constructing your GM objects.:
I can create a specialized object with a couple of properties governing its behavior. I can then create child objects of this specialized object, and in the create event set the necessary properties to alter that instances behavior.
I can create a specialized object such as the one in (1). However instead of creating dozens of different child objects to govern each differing behavior - I create a parent object (example obj_animal) and save its configuration to file - I can then use this while loading the level to re-instantiate and configure that instance. In Game Maker terms, the equivalent of this would be to edit the creation code of an instance in your room.
There is nothing stopping you from incorporating both techniques, even in a custom save / load architecture. In-fact I regularly make use of creating a few different types of objects based on a single parent. If it makes sense to do so - then one should most probably consider it. Do keep in mind however that there are some trade-offs with each approach:
Going for (1) - you’ll run the risk of ending up with a HUGE amount of objects in your project! However you drastically simplify the mapping layer between your save file and your object instances as essentially you would not need to map object properties across. The issue here is that you lose the ability to configure the behavior for just that one, special instance, forcing you to create another object to carry the slightest behavioral or visual change.
Going for (2) means that you’ll have to ensure that each property on all your instances you want persisted are added to the mapping routine and saved / loaded correctly - And before you continue - please ALWAYS keep versioning in mind. Do your due diligence and fortify your code from properties that may not exist in either the level file, or the object!. Going for (2) obviously means more code, and more code = more potential bugs.
Unfortunately there is another trade off with going for (2), and that is that you will need to have a GUI which you can use to configure these instances - in other words, to effectively utilize this strategy, you will need to have an editor.
Moving on - Once we have defined a root object, our lives become a little simpler. We now know that every object that inherits from our base object is a candidate for being saved.
During my editors save event, I will grab all objects by their parent and iterate through each one, performing a mapping operation to a data structure. During my editors load event, i read in the serialized map of objects, de-serialize their configuration and instantiate them.
The save event
For both the save and the load events to function without interference or a dependency on Game Makers object indices, I suggest you consider implementing some sort of mapping layer. In ORB, I maintain a list of custom object type IDs which map to and from the actual game maker objects. For example, if I had an object by the name of obj_wall, I would assign obj_wall a type ID of 1. This type ID will then be written to the level file when saving. When loading this object, I read in the type ID, and in one glorious switch statement, I return the the actual instantiated GM object.
Going this route does mean that you have more code to maintain - but it does shield you somewhat from your object indices being shuffled and changed inside your project without your knowing. Another benefit I have discovered is that it becomes very easy to remove objects that you no longer need or want by simply removing them from the mapping switch statement - they will then just not load, and will subsequently be removed when saving.
Before moving into some of the more technical, pseudo code sections, you might be wondering at this stage - what data structures would I need in order to facilitate this process?
I make extensive use of the map and list data structures in ORB during my save and load processes. Unfortunately using these structures in GM are not as straightforward as one might think - especially when it comes to saving and loading them. If you are to save a map or a list within another map or list, you’ll have to do some footwork to ensure that your nested lists and maps are saved and loaded correctly. This means that you have to serialize your embedded lists and maps manually while saving, and de-serialize them manually upon load - I’ll show you how I go about this with a little pseudo-example of ORB's core save routine: (Note the methods in blue are GM's native methods while the methods in red will be discussed after this example):
var level_map = ds_map_create();
with(obj_base)
{
var my_object_map = ds_map_create();
var my_object_key = get_my_object_key();
var my_object_type_id = get_my_object_type_id();
// Map out common obj_base properties, keyed by property name
my_object_map[? “Key”] = my_object_key;
my_object_map[? “Object_Type_ID”] = my_object_type_id;
my_object_map[? “X”] = x;
my_object_map[? “Y”] = y;
...
...
// now specialized object properties
if ( i_am(obj_wall))
{
my_object_map[? “Wall_Type”] = wall_type;
my_object_map[? “Some_Other_Property” ] = some_other_property;
}
// Remember your parents! If you want to keep certain properties “private” to
// the specialized instance, and want to form a deeper hierarchy of
// inheritance - I always start with the parent object properties first,
// followed by a specific check for the specialized objects. For example,
// if obj_wall had a child, obj_wall_bouncy, then immediately after the check
// for the parent,check for the specialized instances, like so:
if (i_am(obj_wall_bouncy))
{
my_object_map[? “Bounciness”] = bounciness;
}
// This will then result in all properties (Wall_Type, Some_Other_Property,
// Bounciness as well as the base properties) being written to this specific
// object map - don’t repeat properties else you’ll end up with an
// unmaintainable chunk of code spread over thousands of lines.
if (i_am(...
...
}
// Right at the end of this object write, we want to persist a
// string version of this map to our level_map.
// Game Maker does not implicitly write out the content of embedded lists and
// maps, so we have to do this manually by serializing the map to a string
level_map[? My_object_Key] = ds_map_write(my_object_map);
// VERY IMPORTANT!
// Clean up as you go else you’ll end up eating and leaking through memory
// very quickly! All data structures MUST be manually destroyed when no
// longer needed!
ds_map_destroy(my_object_map);
}
// Now that we have all of our objects flattened inside a single map, we can
// serialize our map into a string and save this it to a text file. How you
// persist and load this string I'll leave up to you to decide.
var t_string = ds_map_write(level_map);
That pretty much sums up the core routine that you will need to bash out a custom save script for your game. You can also extend the level_map by serializing the level data into a string and storing it in the map by “Data” or a key of your choice. You can then add level specific configurations to this map as well, such as its name, environmental properties and so on.
For completeness sake I'll briefly list the three custom scripts: i_am(ind),
get_my_object_key(), and get_my_object_type_id():
script: i_am(ind)
///i_am(ind)
return object_index == argument0 || object_is_ancestor(object_index, argument0);
script: get_my_object_key()
When dealing with objects, ideally you want a way to uniquely identify the instance you are working with, or want to work with. You could potentially use GM's instance_id, however I chose to rather make the key a little more descriptive and include the x, y and the type ID along side the instance Id.
The primary reason for doing this is that you need to keep in mind that GM's instance_id is exactly what it says it is - an ID for that instance - and that ID will change upon subsequent loads.
In other words - don't persist the instance_id and expect to find the same object with the same instance_id the next time you load your level!
script: get_my_object_type_id()
In my architecture I have a mapping layer which essentially de-couples the Game Maker object indices from the indices I store inside the level file - essentially shielding me from any internal index changes that GM might perform without my knowing.
The Load Event
I've covered at a high level the routine to save your properties to a file, but what about loading them back again? Surprisingly, the process is not that much more complicated than the save event.
Remember we covered having a custom type ID for each of your objects? We’ll now use this type ID in our favor by switching on it, and returning an instance of the object requested.
Before I go any further with the load event, now might be a good time to point out that I got a little creative here with the object management - when I instantiate an object, I tell my engine whether or not it should “track” this object. By tracking, I mean “registering” the object in my object management controller - which I then use in my pause event to quickly activate or de-activate any game objects, without having to worry about deactivating objects that are essential for your game to keep on running. Furthermore should you wish to have persist-able state through level loads, then registering these objects in a central repository or controller will only make your life a little simpler.
Back to the Load Event:
// The first logical step is to load the data from the file that you saved into a
// variable. Once again how you read this string from disk I leave up to you.
var t_string = load_my_level_data_from_file(file_name);
var xlevel_map = ds_map_create();
// Deserialize the first depth. This will take your loaded t_string and populate
// the xlevel_map.
// Very important to remember that each entry inside this map are still
// stringified versions of the objects, and not the lists or maps that they
// represent!
ds_map_read(xlevel_map, t_string);
// Now that we have our first level map, we can start de-serializing the actual
// object data. First we need to iterate over the map. To do this we need to
// retrieve the first key from our structure,
// then traverse through the map requesting the next key and so on - until we are
// done with all keys
var map_size = ds_map_size(xlevel_map);
var key = ds_map_find_first(xlevel_map);
for(var i = 0; i < map_size; i++)
{
var mapped_obj = ds_map_create();
var mapped_string = xlevel_map[? key]; //grab the serialized object by key
ds_map_read(mapped_obj, mapped_string); //populate the mapped_obj structure
// start with the common base properties and create the parent object, we can
// do this by reading out our object type ID and instantiating the correct
// object
var my_object_type_id = mapped_obj[? “Object_Type_ID”];
var xx = mapped_obj[? “X”];
var yy = mapped_obj[? “Y”];
// insert method here to cater for instantiating the correct object
// I will write this example out in-line for a little more clarity:
var inst = noone;
switch(my_object_type_id)
{
case 1:
{
inst = instance_create(xx, yy, obj_wall);
}
break;
}
// once we have instantiated and we have a handle on our object, we can start
// with the specialized properties by performing the same “if i_am()” checks.
with(inst)
{
if (i_am(obj_wall_bouncy))
{
inst.bounciness = mapped_obj[? “Bounciness”];
}
if (i_am(...
...
}
}
// Once again please remember to clean up as you go. We have created a
// temporary map here to stream the data in - we need to free this memory as
// we no longer require the map
ds_map_destroy(mapped_obj);
}
// DON’T FORGET TO DESTROY YOUR DATA STRUCTURES!!
ds_map_destroy(xlevel_map);
And there we have it, a pseudo-example of my core load routine in ORB.
Where to from here?
Performing a manual save and load has many advantages - however one should not consider going this route unless you are absolutely comfortable with the additional complexity - and obviously only if your project requires it!
I have only scraped the surface of creating a custom editor here and briefly mentioned some of the advantages and disadvantages of going this route. Should this topic be of any interest and help, I will consider writing a few more technical articles covering how to fully utilize this strategy.
I do hope some that some find this useful, as I most certainly have in my game!
About the Author
You May Also Like