Making an in-game level editor

hydroxy

Member
I have started work on an in game level editor. I always thought the creative process was slowed by having to exit a game, alter the levels and recompile the game between each iteration. With an in-game level editor this should streamline the entire process. I think I might release it as an asset in the future.

Are there any other level editors out there currently. I got my inspiration from Hyper Light Drifter's editor as seen here at about the 17 min mark onwards. Hoping to make one with a lot of the same functionality.

I was wondering what would be the best system for saving level data, JSON? Or should I use .csv?

Also is there a standard way to create small windows for use in-game? e.g. when I open the object editor I want a small window to open with buttons for changing object orientation, snap to grid properties, sprite index randomiser, overlap prevention and so on...

Here is a screenshot of the editor in action. Quite a lot of the buttons are just placeholders at the moment, though I don't think it'll take long to get everything sorted.

levedit.png
 

jucarave

Member
I don't know up to what point is possible, but maybe override the GM room file? also try not going nuts with all the features, just add the minimum you need for your game and expand on it in the future if you want
 

Joe Ellis

Member
I've been using my own in game editor for quite a while, it definitely has streamlined the process, being able to edit a small part and just press play and test that one part a few times till you've got it how you want. I made it just be stripping out all the 3d parts of my bigger 3d editor I'm working on. I can give you some advice on making the windows and saving\loading...
 
Are there any other level editors out there currently.
Welcome to the world of making games ;) hahaha. If you're making a game where very specific level editing is in any way important (i.e. not procedurally generated, which I love, but rarely needs level editors) and the game ranges from moderate to large in scope, then I think that making an easily accessible and modifiable custom level editor is almost a necessity. The default room system in GMS is loveable, but you really need something that conforms to your specific project and what better way to accomplish that than creating your own custom editor!

Not only will you be able to simplify specific problems that your project may face with the default room editor, but you might learn a few things that you neglected in your game programming knowledge up until that point, like data structures, or how to properly manipulate surfaces, or what buffers do exactly, or what a genuine GUI system entails, the amount of detail that you can drill down to by creating a custom level editor is almost endless (disclaimer: I've been spending the past few days creating my own custom editor for a small puzzle game I've been working on, you can see an early version of the level editor in action here). I think the only requirement is a willingness to learn and a loin girded by the steel of being able to rewrite a few pieces of code many, many times as you slowly start to understand the true scope of possibilities within your project.

(EDIT: Sorry if the above sounds condescending, I really didn't mean it that way. Just that there's a certain point where you really "get" what making games is, that goes beyond understanding code flow, or any specific code structure, just really getting how you make a game, and I think making a custom level editor is a great way of "levelling up" in game terms)
 
Last edited:

hydroxy

Member
Very neat level editor @RefresherTowel. No worries, that didn't sound condescending or anything, it is all founded in truth I believe. For me the need to create a level editor has arisen because I need greater control in the game world, it is more than speeding up the level design process, I want to be able to navigate and change the game world in real time, walk through it and make changes where I see fit.
 

Rob

Member
Making an editor for an isometric tactics battle game made sense too although it has far less features than yours lol
 

hydroxy

Member
Also, at a point in their video (sorry I canā€™t see the video, on mobile at work) the Hyper Light Drifter guys show using a little door map that can be used to link doors, how would this be achievable in GM?
 

Yal

šŸ§ *penguin noises*
GMC Elder
Also, at a point in their video (sorry I canā€™t see the video, on mobile at work) the Hyper Light Drifter guys show using a little door map that can be used to link doors, how would this be achievable in GM?
It's a really broad question, could you be a little more specific? draw_line is good enough for display purposes once you have the connection data, and that might be easiest configurable through a custom menu object that reads level data etc and lets you pick from a menu... stuff which your editor probably already has access to?

In my SoulsVania engine I pick the opposite route: level connections are defined FIRST, in the map editor:


(with a level's size on the map screen determining how big it is in the level editor) and then afterwards, in the level editor, placing a door near the edge of a screen will connect to whatever level is in the next map cell over. Doors specifically don't move you to a separate room; they move you one cell in the direction they're facing and then load whatever level's in there.
 

kraifpatrik

Member
Are there any other level editors out there currently.
I usually don't promote my stuff on other people's threads (or at least I hope so :D), but since you're asking, I've created a 2D/3D level editor. It was in GMS1.4, so nowadays it's not usable anymore, but maybe you could get inspired by it. Its full source code is available for free on GitHub: https://github.com/GameMakerDiscord/PushEd. A 3D-only version of it for GMS2 is currently WIP: https://github.com/blueburn-cz/Push3d. An example of a game using the editor is our team's shooter: https://forum.yoyogames.com/index.php?threads/bbp-pre-alpha-3d-online-fps.51950/.

I was wondering what would be the best system for saving level data, JSON? Or should I use .csv?
If you don't want the files to be human readable, then I would recommend you having a look into buffers/binary files. Those still can be reversed, but it takes a bit more work. Another benefit is that the resulting file size is smaller. Technically it should also be faster to read and write compared to JSON files.

Also is there a standard way to create small windows for use in-game?
Not really the standard way, but the GUI system behind the level editor was ported to GMS2 and it's also available on GitHub: https://github.com/blueburn-cz/Forms. If you're interested, you can PM and I can help you get started using it.
 
Last edited:

Joe Ellis

Member
Hi sorry for the late reply, I was trying to think what the best advice to give would be.

With saving, I use buffers cus they're fast, low memory and let you write the files exactly how you want, but text files are also just as versatile. I can't give an example of JSON or recommend it cus I've never used it.

Here is a simple(ish) example of saving and loading with buffers.
It does depend how your level editor works and whether it places the instances as actual inactive instances or it uses arrays or some kind of datastructure to store the positions and other info.
This example is for the level editor working with null\inactive instances(obj_inactive) cus this is how my editor works and I recommend doing it this way cus it makes variable handling alot easier and more versatile, but the example can be adjusted to fit either way of doing it:

GML:
///level_save(filename)

var
b = buffer_create(1, buffer_grow, 1),
i = -1,
l = global.instances,
ins;

//write num instances
buffer_write(b, buffer_u16, array_length1d(l))
repeat array_length1d(l)
{
ins = l[++i]
buffer_write(b, buffer_string, object_get_name(ins.object))
buffer_write(b, buffer_f32, ins.x)
buffer_write(b, buffer_f32, ins.y)
buffer_write(b, buffer_f32, ins.image_angle)
buffer_write(b, buffer_f32, ins.image_xscale)
buffer_write(b, buffer_f32, ins.image_yscale)
buffer_write(b, buffer_f32, ins.image_blend)
buffer_write(b, buffer_f32, ins.image_alpha)
}

//Trim the buffer
var b2 = buffer_create(buffer_tell(b), buffer_fixed, 1);
buffer_copy(b, 0, buffer_tell(b), b2, 0)
buffer_save(b2, argument0)
buffer_delete(b)
buffer_delete(b2)
GML:
///level_load(filename)

var
b = buffer_load(argument0)
i = -1,
l, ins,
num = buffer_read(b, buffer_u16);

l[num - 1] = 0
global.instances = l

repeat num
{
ins = instance_create_depth(0, 0, 0, obj_inactive)
ins.object = asset_get_index(buffer_read(b, buffer_string))
ins.x = buffer_read(b, buffer_f32)
ins.y = buffer_read(b, buffer_f32)
ins.image_angle = buffer_read(b, buffer_f32)
ins.image_xscale = buffer_read(b, buffer_f32)
ins.image_yscale = buffer_read(b, buffer_f32)
ins.image_blend = buffer_read(b, buffer_f32)
ins.image_alpha = buffer_read(b, buffer_f32)
l[++i] = ins
}

buffer_delete(b)
GML:
///level_play()

var l = global.instances, i = -1, ins;

repeat array_length1d(l)
{
with l[++i]
{instance_change(object, true)}
}

The only problem with this method is that if you decide to add more variables later down the line, or change the order that they're saved in, all the old level files will no longer be compatible with the new level_load script. So I recommend writing what each variable name is before saving the value, that way you can set default variable values before loading them, so if it's an old level file that wasn't saved with a certain variable, the instance will still have the default value so the level can load without any errors.

GML:
///level_save(filename)

var
b = buffer_create(1, buffer_grow, 1),
i = -1,
l = instances,
ins;
buffer_write(b, buffer_u16, array_length1d(l))

//Save variable names
buffer_write(b, buffer_u8, 7)//num variables
buffer_write(b, buffer_string, "x")
buffer_write(b, buffer_string, "y")
buffer_write(b, buffer_string, "image_angle")
buffer_write(b, buffer_string, "image_xscale")
buffer_write(b, buffer_string, "image_yscale")
buffer_write(b, buffer_string, "image_blend")
buffer_write(b, buffer_string, "image_alpha")

//Save instance's variable values
repeat array_length1d(l)
{
ins = l[++i]
buffer_write(b, buffer_string, object_get_name(ins.object))
buffer_write(b, buffer_f32, ins.x)
buffer_write(b, buffer_f32, ins.y)
buffer_write(b, buffer_f32, ins.image_angle)
buffer_write(b, buffer_f32, ins.image_xscale)
buffer_write(b, buffer_f32, ins.image_yscale)
buffer_write(b, buffer_f32, ins.image_blend)
buffer_write(b, buffer_f32, ins.image_alpha)
}

//Trim
var b2 = buffer_create(buffer_tell(b), buffer_fixed, 1);
buffer_copy(b, 0, buffer_tell(b), b2, 0)
buffer_save(b2, argument0)
buffer_delete(b)
buffer_delete(b2)
GML:
///level_load(filename)

var
b = buffer_load(argument0),
num_instances = buffer_read(b, buffer_u16),
num_vars = buffer_read(b, buffer_u8),
instances, var_names, ins, i = -1, j;

instances[num_instances - 1] = 0
global.instances = instances

var_names[num_vars] = 0
repeat num_vars
{var_names[++i] = buffer_read(b, buffer_string)}

i = -1
repeat num_instances
{
ins = instance_create_depth(0, 0, 0, obj_inactive)
ins.object = asset_get_index(buffer_read(b, buffer_string))
j = -1
repeat num_vars
{variable_instance_set(ins, var_names[++j], buffer_read(b, buffer_f32))}
instances[++i] = ins
}

buffer_delete(b)

Now about making windows, I use an object "obj_gui"
you have one master obj_gui instance which is the window and it has a list of sub instances for the buttons and things inside it,

then in the editor's step event it runs "mouse_get_gui" which cycles through each window,
if the mouse is in it's rectangle it then cycles through the window's sub instances and finds which one the mouse is over using point_in_rectangle(mouse_x - window.x, mouse_y - window.y, x, y, x + width, y + height)
Then if the mouse is over one of them it runs that sub instance's "step_script".

For the graphics, each window has\creates a surface with the width & height of the window and it draws the border and cycles through it's sub instances and runs their "draw_script"s.

I make it update\refresh the surface whenever the user interacts with one of the sub instances, but if you want to keep it simple you could just make each window runs it's draw_script in their draw events.

The last part required is actually creating these windows and the layouts of the sub instances.

I use scripts that create the window and the sub instances, and I made a bunch of scripts for creating certain types of gui objects.
Eg.

GML:
///frm_instance_settings()

with form_begin(x, y, width, height, caption, icon)
{

gui_button(x, y, width, height, caption (eg. "Close"), function(eg. form_close))

gui_numberbox(x, y, width, height, var_name, min_val, max_val, integer(true\false), wrap(true\false))

gui_checkbox(x, y, text, var_name)

return id
}
Here are all the scripts used in the system:

GML:
///form_begin(x, y, width, height, caption, icon)

var ins = instance_create(argument0, argument1, obj_gui);

ins.gui_instances[0] = 0 //initialize the form's list of sub instances
ins.num_gui_instances = 0
ins.surface = surface_create(ins.width, ins.height)

global.forms[@ ++global.num_forms] = ins //add the form to the global list

return ins
GML:
///gui_button(x, y, width, height, caption, function)

var ins = instance_create(argument0, argument1, obj_gui);
gui_instances[@ ++num_gui_instances] = ins //add the instance to the form's list of sub instances

ins.parent = id //"parent" is used for when the instance needs to interact with the form
ins.width = argument2
ins.height = argument3
ins.caption = argument4
ins.function = argument5
ins.surface = surface_create(ins.width, ins.height)
ins.step_script = gui_button_step
ins.draw_script = gui_button_draw
ins.pressed = false

return ins
GML:
///gui_button_draw(button)

var ins = argument0;

surface_set_target(ins.surface)

draw_clear(c_ltgray)

if pressed
{draw_pressed_border()}
else
{draw_normal_border()}

draw_set_h_align(1)
draw_text(ins.caption, round(ins.width * 0.5) + pressed, (round(ins.height * 0.5) - 8) + pressed)

surface_reset_target()
GML:
///gui_button_step(button)

var ins = argument0;

if !ins.pressed
{
if mouse_check_button_pressed(mb_left)
{
ins.pressed = true
gui_form_refresh(ins.parent)
}
}
else
{
if mouse_check_button_released(mb_left)
{
ins.pressed = false
script_execute(ins.function)
gui_form_refresh(ins.parent)
}
}
GML:
///gui_form_refresh(form)

var l = argument0.gui_instances, i = 0, ins;

surface_set_target(argument0.surface)

draw_clear(c_ltgray)

draw_form_border(argument0)

repeat argument0.num_gui_instances
{
ins = l[++i]
script_execute(ins.draw_script, ins)
}

surface_reset_target()
GML:
///mouse_get_gui()

var l = global.gui_forms, i = 0, ins, sub_ins, mx, my, sub_instances, j;

repeat global.num_gui_forms
{
ins = l[++i]
if point_in_rectangle(mouse_x, mouse_y, ins.x, ins.y, ins.x + ins.width, ins.y + ins.height)
{
//Get relative mouse coordinates
mx = mouse_x - ins.x
my = mouse_y - ins.y
sub_instances = ins.gui_instances
j = 0
repeat ins.num_gui_instances
{
sub_ins = sub_instances[++j]
if point_in_rectangle(mx, my, sub_ins.x, sub_ins.y, sub_ins.x + sub_ins.width, sub_ins.y + sub_ins.height)
{
script_execute(sub_ins.step_script, sub_ins)
break
}
}
break
}
}

I'll leave it at that for now, sorry if it seems really complicated, I tried to explain it as simply as I could, but if you have any questions or want to know more just let me know :)
 
Last edited:

hydroxy

Member
It's a really broad question, could you be a little more specific? draw_line is good enough for display purposes once you have the connection data, and that might be easiest configurable through a custom menu object that reads level data etc and lets you pick from a menu... stuff which your editor probably already has access to?

Ah, you are likely right with this I feel. I was reading that GM cant access room object information when out of the room, but if you create your own level editor and save the files to your own format you can read the data at will. Your SoulsVania engine is also an interesting way to go, defs worth considering doing it that way instead perhaps.

@kraifpatrik thanks for the information. I will have a look at your editor, I'm sure I will find it useful when developing my system also.

@Joe Ellis thanks for all the information also. I will have a look at your example codes, buffers defs looks like a good potential solution, thanks for the help.

updatedgui.png

Updated the GUI in my editor, makes a difference having nicer buttons, just need to actually get them working now.
 

chirpy

Member
Disclaimer: I had never implemented a full level editor ever
With that said
In theory, I'd look into an existing tile editor e.g. Tiled Map Editor and its available file formats. So you can test importing a map file before building your own map editor.
 
Bare with me as I've only done a level editor once and it's likely to not be the best; however! I'd recommend using a JSON file format as you can save them all into a folder not connected to the game itself which, in theory, should save a lot of loading time in relation to the mass amount of rooms. I figured out how to save instances to json file format from Shaun Spalding's tutorial on saving/loading, it's really good and can be adapted to include tile ids (I would put a link here but this is one of my first posts so I can't just add links :/).

Regardless of anything, externally saving your rooms/levels is just generally useful.
 

Kahrabaa

Member
Hello, Good luck on building your editor!

I've made level editors for some of my games, (none were actually finished unfortunately).
Its really fun and effective to notice were in your workflow things are slow and then just add a solution that simplifies for yourself.

Heres are pics of one of the more complex editor I made


Here are some random tips that came to my mind:

-Level transitions. Door objects have both an "ID" and "Destination" value. So every time you touch the door with ID 1 in the "fields" area you enter the "house" area and appear at door with ID 1 and the opposite. You then can have different doors that lead to same area but to different doors. Not exactly a visual way to connect areas, but easy to track using door no 1,2,3,4, etc.

-Don't allow multiple objects on the same spot obviously, either through spawning or movement. To be 100% mistake free you can add a button/function that loops through all objects and checks to see if an object is in collision with the same object that has the same values (angle,scale). And If they do, an indicator/highlight would be spawned so the you could scan the map and see were a duplicate is found.

-If you deactivate objects outside of screen, you need to reactivate everything before saving your map so you don't only save a snapshot of the level. If I remember correctly instance_activate_all doesn't activate all objects until next frame. So you cant save on the same frame.

-When saving, you might store the object_index for loading. Be careful If you delete objects in the resource tree indexes might change unintentionally making your save file have info that is not in sync with gamemaker's resource tree. So its better to save the object's name and then get the index from that name. Same with sprite_index.

-Each object will have variables that you want to edit through some window in the editor, like hp, chest content etc.. These variables need to be reset when you exit "Play mode". So you need to store the starting values.
Especially if you want to have alot if different variables for each objects.

This is one way to simplify the process.

1)Create a script for setting/getting preparing the custom variables
GML:
switch(setget){
case "set": //Initialise this variable and set its initial values

    _ev[ _evs, 0 ] = argument0; //Var name. To display in the editor
    _ev[ _evs, 1 ] = argument1; //Set the starting value of this var, this will be edited through the editor window
    _ev[ _evs, 2 ] = argument2;  //Var type, Is it a (0)number or a (1)string? Needed when storing buffers you must specify what type of variable you are saving
    _evs+=1; //Increase total editor variables for this object, these need to be set 0 before any _ev is added
    return argument0; //Return the starting value to set the object variable

break;
case "get": //Reset the object variable with the editor variable

    return _ev[ _evs, 0 ];

break;
}
2)Use any user event to declare your variables. For example event_user(7)
The values you set here in argument1 will be the default value each time you place this object in the editor
GML:
x= evar ( "x", 0, 0 );
y= evar ( "y", 0, 0 );
image_xscale= evar ( "image_xscale", 1, 0 );
image_yscale= evar ( "image_yscale", 1, 0 );
image_angle= evar ( "image_angle", 1, 0 );
turretDir= evar ( "Direction", 0, 0 ); //Set Direction to 0
hp = evar ( "HP", 9000 , 0  ); //Set HP to 9000
deathMsg= evar ( "Death message","Whyyy... me..?!?" , 1 );

3)Create a execution script:
GML:
setget = argument0; //What will we do? "set" the variables for the first time or "get" the values from the variables to reset the object
_evs = 0; //This is used to count which editor variables are working on.
event_user( 7 ); //Apply initialization or reset

The just use evar_execute in every object that will be placeable in the editor. In their Create event and whenever you Reset the level.
GML:
evar_execute( "set"); //This will set all the editor variables
GML:
evar_execute( "get");  //This will reset all variables to their default values

Saving and loading all values of any type of object is a piece of cake now.
GML:
//Remember writing order needs to be the same as reading order, I've written letters when writing/reading to clarify the connection, mess this up and everything will be written/read incorrectly.
var buff = buffer_create( 256, buffer_grow, 1);
var i;
//Save total objects, used for loading.
buffer_write( buff,buffer_f32, instance_number(objAllParent)); //(A)

with( objAllParent ){ //Loop through all active objects and store their information. Every object that will be saved need to have this object as its final parent
    buffer_write( buff,buffer_f32,object_get_name(object_index)); //(B)store the name of the object, used  to know what object to spawn when loading. (IMPORTANT to save the object's name and not the index. Because the index might change unintentionally spawning incorrect objects)
    buffer_write( buff,buffer_f32,_evs); //(C)Store the amount of custom variables
    i=0;
    repeat(_evs){ //Loop through values
        //Store each value depending on its type
        switch(_ev[i,2]){
        case 0: //Either store a number
        buffer_write( buff,buffer_f32,_ev[i,2]); //(D)Store variable type, used for loading, must be stored before the value so we know how to get it, as a string or as a number
        buffer_write( buff,buffer_f32,_ev[i,1]); //(E)Store variable value
        break;
        case 1: //Or store a string
        buffer_write( buff,buffer_f32,_ev[i,2]); //(D)Store variable type, used for loading, must be stored before the value so we know how to get it, as a string or as a number
        buffer_write( buff,buffer_f32,_ev[i,1]); //(E)Store variable value
        break;
        }
    i+=1;
    }
}
buffer_save(buff, "savefilename.sv");
buffer_delete(buff); //Dont forget to delete the buffer
GML:
//Remember writing order needs to be the same as reading order, ive written letters when writing/reading to clarify the connection, mess this up and everything will be written/read incorrectly.
var buff = buffer_load("savefilename.sv");
var i,loadtype,newobject,objectname,evs;
//Save total objects, used for loading.
var allobjects = buffer_read( buff,buffer_f32); //(A)

repeat(allobjects){
    objectname = buffer_read( buff, buffer_f32); //(B) Get the name of the object we are about to create
    newobject = instance_create(0,0,asset_get_index( objectname )); //Get the index of the object name
    //Now the newly spawned object's create event will be executed here before the next line. So we dont have to worry about our variable setup, only to update the starting values

    evs = buffer_read( buff, buffer_f32); //(C) Get the number of custom vars, so we now how many times we loop
    i=0;
    repeat(evs){ //Loop through values
        //Load each value depending on its type
        loadtype=buffer_read( buff,buffer_f32,_ev[i,2]); //(D)Store variable type, used for loading, must be stored before the value so we know how to get it, as a string or as a number
        switch(loadtype){
        case 0: //Either load a number
        newobject._ev[i,1]=buffer_read( buff,buffer_f32,_ev[i,1]); //(E)Get variable value and set it to the current object we just created
        break;
        case 1: //Or load a string
        newobject._ev[i,1]=buffer_read( buff,buffer_f32); //(E)Get variable value and set it to the current object we just created
        break;
        }
    i+=1;
    }
}

This way will make the undo/redo buttons also easier to make since you can easily store every variable for each different object without having to make custom solutions for each object.

Hope any of this is useful.
I wrote the code here from memory, so sorry if there is a mistake.
Good luck and don't get lost in making the editor and forgetting about making the game like I've done a couple of times xD

Nice looking buttons btw!
 
Last edited:

hydroxy

Member
@Kahrabaa, excellent post. I've taken note of these suggestions, I think some of these would have taken some decent time for me to realise as well if you didn't say.

Yep, got to create the editor and then the game. Not going to lose sight I hope.

Nice looking buttons btw!
Thanks, the buttons are hand-drawn, they took a while but was a nice easy task to do before all the hard work started.
 
Top