Using Crocotile3D with GameMaker

24 January 2021

This post is not sponsored in any way, I just really love Crocotile lol

As a GameMaker user building games in 3D, I replaced Blender with Crocotile a few months back and it has made the whole process quicker and much more enjoyable for me. Since then, I've found that I'm far from the only person using this combo of tools. Sometimes people will ask me questions about these tools or how I make my stuff, so I'm hoping this post can give some insight into how and why I use these programs together, as well as provide some ideas on how YOU can get started if you're interested.

Overview

Before we get into it, I want to emphasize that this post is not generally about doing 3D stuff in GameMaker, nor is it a guide on how to get started in 3D programming. It's really just me rambling about a handful of ways you can use Crocotile with GameMaker. I'll save the generalized discussion of 3D math and rendering in GameMaker for another time. There will also be some things here that are definitely doable with other programs like Blender, so if you don't have Crocotile but see something you wanna try out and think you can, then go for it!

In this post, I'm going to cover a few things:

For this, I'll be using GameMaker Studio 2.3.1 and Crocotile 1.5.8. In theory, you should be able to do any of this stuff as long as you're using a version of GM that lets you open external files and build vertex buffers.

Crocotile mascot as it appears in-program

The Crocotile3D mascot in-program

What is Crocotile?

Crocotile3D is a 3D modeling program where you use 2D tilesheets to build low-poly scenes and objects. I find it to be very fast, functional, and -- most important, in my opinion -- easy. Coming from a GameMaker-heavy background, the ability to just throw down tiles in a scene feels very familiar. It reminds me of my earlier days grabbing tiles from SMB3 tilesheets and making entire levels piece-by-piece. Even for those unfamiliar with GameMaker's Room/Tile editor, I think Crocotile is pretty straightforward and accessible.

Why Would You Use Crocotile With Gamemaker?

Crocotile is great for retro, low-poly visuals. If you're into visuals like those of Minecraft, the Playstation 1, or the Nintendo DS, Crocotile is fantastic for that. Coincidentally, since GameMaker offers only the barest built-in support for 3D rendering and logic -- and thus lacks many of the optimizations a built-for-3D engine would have -- using a low-poly style is also a great way to get into 3D without immediately tanking performance.

Crocotile can also be used for more than just making static models, which I'll talk about more later in the post.

gif of a character and scene I made in Crocotile

A character and scene I made in Crocotile, imported and rendered in GameMaker Studio 2

It's worth mentioning that Crocotile costs money, $25 (USD) to get the license. If you have the experience to utilize the tool well, or you don't mind dropping a little cash into a new hobby, then the price is more than worth it. While the free demo doesn't let you export your models, I strongly recommend trying it out first to see if the tool feels right to you.

( If you decide to buy Crocotile, I urge you do so directly through the developer's site. Steam takes a cut of all sales. )

How Can You Use Crocotile With GameMaker?

Here, I'll share some ideas and advice on how you can use Crocotile and GameMaker together that plays to both programs strengths. This is just a handful of ways to use the tools together, and I encourage you to get creative/experimental with it in your own projects!

1. Add A 3D Object To A 2D Game

This is one of the simplest ways you could use a 3D model in your GameMaker game. It's purely visual and works almost entirely off of GameMaker-provided functions. Crocotile's 1 unit == 16 pixels design + GameMaker's default orthographic camera means that it's quite easy to maintain a 'pixel perfect' appearance when your 3D model is thrown into the 2D scene. If your player sprite is 16x16 pixels, you can expect a 16x16 tile in your Crocotile model to be the same size*.

*When exporting an OBJ from Crocotile, you'll want to set the scale to x16 since Crocotile works by 1 unit == 16 pixels but GameMaker works by 1 unit == 1 pixel.

Mushroom model image

A legally-distinct mushroom modeled real quick in Crocotile

Keeping things simple with 3D in GameMaker means you really only need a handful of functions and variables to maintain everything.

Create Event
[ + / - ]
/* load_obj is a custom function. Assume it imports the OBJ as-is into a vertex buffer */
mushroom_buffer = load_obj( working_directory + "obj_mushroom.obj" );
rotator = 0;
    
/* need this so 'closer' faces overwrite 'further' faces */
gpu_set_ztestenable( true );

Draw Event
[ + / - ]
/* To manipulate the position of vertex buffers, modify the world matrix */
rotator += 2;
matrix_set( matrix_world, matrix_build(
    32, 32, 0,
    rotator * -0.25, rotator, rotator * 0.5,
    1, 1, 1
));
vertex_submit( 
    mushroom_buffer,
    pr_trianglelist,
    sprite_get_texture( tex_mushroom, 0 )
);
    
/* Reset the world matrix afterwards for safety */
matrix_set(matrix_world, matrix_build_identity() );

Mushroom model image

The mushroom OBJ, displayed in gamemaker via the code above

That's all there is to it. This is a quick and effective way to create an element that pops out, or to animate something that would be impractical to animate by hand.

2. Design Maps

Using a 3D map will quite literally add depth to your scene even if the logic all happens at a 2D level. You can make a retro FPS like Gun Godz, or a top-down game with 3D map elements like Pokemon Diamond/Pearl, all while mostly working with the functions that GameMaker provides for you.

Let's start with the following assumptions:

An image of the level model in Crocotile

A level quickly thrown together. My example is blocky like Minecraft, but you can style it however you want

I have multiple objects in my scene, and one of the objects is called COLLISION. For this object, I have a separate tileset loaded with checkerboard tiles, and I'm using the orthographic perspective so I can lay down tiles where I want the player to be stopped while keeping some physical distance between the visual model and the collision model.

A gif showing the collision object toggling

Using the orthographic camera makes it easy to align tiles at different heights since there's no distortion/scaling like this is with a perspective camera

When exporting an OBJ from Crocotile, all of the objects you include in the export will be separated by g {OBJECT NAME IN CROCOTILE}. Because of this, when we parse the file in GameMaker, we can handle the faces under g COLLISION differently from the other polygons.

With the two assumptions listed at the start of this section, we get to do a few things that make this really easy to use:

Here's some code that:

Create Event
[ + / - ]
/*
    load_obj is a custom function. As opposed to the first example, it now returns an
    array where [0] is the vertex buffer, and [1] is the array of triangles under
    'g COLLISION' in the OBJ file
*/
var _obj_info = load_obj( working_directory + "obj_grasslands.obj" );
grasslands_buffer   = _obj_info[ 0 ];
collision_array     = _obj_info[ 1 ];
    
rotator = 0;
    
player_pos = [0,0];
target_pos = [0,0];
    
last_update_time = 0;

Step Event
[ + / - ]
/* Update the player position randomly every little bit */
if (current_time - last_update_time > 1000) {
    target_pos[0] = random_range( -128, 128 );
    target_pos[1] = random_range( -128, 128 );
    last_update_time = current_time;
}
var _new_pos = [ 0, 0 ];
var _dir_to_target = point_direction(
    player_pos[0], player_pos[1],
    target_pos[0], target_pos[1],
);
_new_pos[0] = player_pos[0] + 1*dcos(_dir_to_target);
_new_pos[1] = player_pos[1] - 1*dsin(_dir_to_target);
    
/* Check if the desired position would cause a collision against the triangles */
var _tri, _a, _b, _c;
var _hit = false;
var _arrlen = array_length( collision_array );
for(var _index=0; _index < _arrlen; ++_index) {
    _tri = collision_array[ _index ];
    _a = _tri[ 0 ];
    _b = _tri[ 1 ];
    _c = _tri[ 2 ];
    _hit = point_in_triangle(
        _new_pos[0], _new_pos[1],
        _a[0], _a[1],
        _b[0], _b[1],
        _c[0], _c[1]
    );
    if (_hit == true) {
        break;  
    }
}
    
/* Only update position if there were no predicted hits */
if (_hit == false) {
    player_pos = _new_pos;  
}

Draw Event
[ + / - ]
/* Set the camera in place with an orthographic perspective*/
rotator += 2;
var _dist = 100;
var _view_angle = 45 + 10*dsin(rotator);
var _cx = dcos(_view_angle) * _dist;
var _cy = -dsin(_view_angle) * _dist;
var _cz = -_dist;
camera_set_proj_mat( 0, matrix_build_projection_ortho( 192, 192, 1, 32000 ) );
camera_set_view_mat( 0, matrix_build_lookat( _cx, _cy, _cz, 0, 0, 0, 0, 0, 1 ) );
camera_apply( 0 );
    
/* Draw the level */
vertex_submit( 
    grasslands_buffer,
    pr_trianglelist,
    sprite_get_texture( tex_grasslands, 0 )
);
    
/* Momentarily turn this on to force the lines to draw over the model */
gpu_set_zfunc( cmpfunc_always );
    
/* Draw the collision triangles */
draw_set_color(c_white);
var _tri, _a, _b, _c;
var _arrlen = array_length( collision_array );
for(var _index=0; _index < _arrlen; ++_index) {
    _tri = collision_array[ _index ];
    _a = _tri[ 0 ];
    _b = _tri[ 1 ];
    _c = _tri[ 2 ];
    draw_line( _a[0], _a[1], _b[0], _b[1] );
    draw_line( _b[0], _b[1], _c[0], _c[1] );
    draw_line( _c[0], _c[1], _a[0], _a[1] );
}
    
/* Draw the 'player' */
draw_set_color(c_yellow);
draw_circle( player_pos[0], player_pos[1], 2, false );
    
/* Draw the target position */
draw_set_color(c_fuchsia);
draw_circle( target_pos[0], target_pos[1], 10, true );
    
gpu_set_zfunc( cmpfunc_lessequal ); // the default cmpfunc

Side-by-side of the grasslands rendering and collision tris

The player is signified by the yellow circle, and the desired position is signified by the pink ring. The player gets stopped whenever a triangle is in the way of movement.

Yeah... it's not particularly efficient, but it does exactly what I need it to do for this example!

3. Build Navigable 3D Environments

An old gif of something I made

An old gif of some 3D stuff I made in GameMaker Studio 2.3, with collision meshes, raycasting, skeletal animation, etc... it took a lot of time to be able to make something that looks and works like this in GameMaker.

I won't share any examples here, just some advice. For most people, if you're gonna need full 3D functionality -- collision, physics, all that stuff -- I would recommend you go with another engine like Unity. Doing 3D in GameMaker can be really fun, but it's far from easy and it takes a lot of time both to learn the concepts and implement them in a useful way. If you are really set on doing 'advanced' 3D work in GameMaker (or you already have, and you just wanna know how Crocotile can fit into it) then here are some basic tips for using Crocotile to help you out:

Some Things To Know When Using Crocotile with GameMaker

This last part is list of little things I've learned while using these 2 programs together. I may come back later and expand this list with any little details I learn.

How the model looks with non-separated Texture Pages

Note how the mushroom texture + some blank texturing now also appear on the model. This is because the model expects to find it's texture occupying 100% of the texture page, but without 'Separate Texture Page' enabled, the intended texture only occupies a portion of it's page, alongside any other textures not marked for separation.

Closing

I hope that this post was either helpful, inspiring, or entertaining for you. I believe in fun and creativity above all else, and I would like to emphasize again that this is not the end-all-be-all guide on using Crocotile with GameMaker -- it's just bunch of anecdotal advice and gifs.

Here's a list of relevant links:

If there's anything about the GameMaker + Crocotile combo you'd like to know that I haven't talked about here, you can find me on Twitter! That would also be the place to let me know if any of the information here is objectively wrong, just don't be rude about it cuz I probably won't respond in that case :)