A downloadable asset pack

Download NowName your own price

Cylindrical Realms: Crafting an Animal Crossing-Inspired World in Game Maker - Tutorial #2

(Disclaimer: the following art is not mine. The tree asset comes from here, and the player asset is from here

Welcome back to the second installment of our tutorial series, "Cylindrical Realms," where we aim to create an Animal Crossing-inspired world using Game Maker. In our last session, we laid the groundwork for our world by setting up the basic environment, creating a player object, and populating the realm with trees. If you haven't gone through the first tutorial yet, I highly recommend doing so before proceeding, as it lays the foundation for what we'll be building on today.

In this tutorial, we'll be diving into one of the most intriguing aspects of the Animal Crossing world—the cylindrical perspective. Ever wonder how you can walk in a straight line and yet end up where you started, as if the world wraps around? Well, that's the magic of cylindrical mapping, and today we're going to implement that feature into our game world.

(Tutorial 1 can be found here)

Specifically, we'll learn:

1. How to apply transformations to the player and tree objects so that they appear to be rotating around the world's central axis.

2. How to handle translations to make sure the objects reappear on the other side of the screen, giving the illusion of a never-ending, looping world.

3. Some tips and tricks for making the rotations and translations seamless, thereby enhancing the gaming experience.

By the end of this tutorial, your flat 2D world will transform into a dynamic, rotating cylindrical realm, providing the unique Animal Crossing feel that we all know and love.

So grab a cup of your favorite beverage, open up Game Maker, and let's get coding!

In the next sections, we'll delve into the actual code and break down each component to understand its role in crafting our cylindrical world. 

(It is recommended that you download the example project to follow along with)

Part 1: Resolving Initial Hurdles and Adjusting Cylinder Geometry

Introduction

Before we dive into the nitty-gritty of transforming our flat 2D world into a rotating cylindrical realm, it's crucial to resolve a couple of issues that may hinder our progress. This section aims to clarify these issues and provide solutions to set you on the right track.

Issue 1: tilemap_get_tileset() Returning Undefined Value

In Game Maker, the order in which layers are instantiated in a room can affect the way objects interact with them. You might find that the function tilemap_get_tileset() returns an "undefined" value. This happens because, during the room's initialization, our camera object (obj_camera) tries to access the tileset before it has been fully loaded.

Solution

To rectify this, simply move the tile layer to the very top of the room layer inspector. By giving it a higher depth, you ensure that the tile layer is initialized before the obj_camera starts to use it for building our cylindrical world.

Issue 2: Orientation Issues in tilemap_get_vertex_buffer_cylinder()

If you remember, we initially set up a function called tilemap_get_vertex_buffer_cylinder(...) to create our cylindrical world. A slight mistake in the way the y/z position of the cylinder was calculated led to incorrectly oriented textures—they appeared upside down.

Solution

To fix this, let's revisit and modify the lines of code responsible for calculating the y/z position of the cylinder. Here's the corrected code:


var dy1 = lengthdir_x(radius, angle1);
var dz1 = -lengthdir_y(radius, angle1);
var dy2 = lengthdir_x(radius, angle2);
var dz2 = -lengthdir_y(radius, angle2);

The key change here is flipping the sign of dz1 and dz2. This simple adjustment corrects the orientation of the textures drawn to the cylinder.

With these changes, we've cleared the path to delve into the magical world of cylindrical mapping. In the following sections, we'll finally get to apply transformations and translations to bring our Animal Crossing-inspired world to life. 

Part 2: Implementing Object Sorting for Seamless Cylindrical Movement

Introduction

Now that we've overcome our initial hurdles, it's time to turn our focus toward transforming and translating the player and tree objects to make them appear as if they are rotating around our cylindrical world. One of the challenges in creating this illusion is ensuring that objects are drawn in the correct order as they move around the cylinder. This part will teach you how to effectively manage the drawing order of these objects by implementing a sorting mechanism.

Step 1: Creating a Parent Object

Before diving into the sorting function, let's first create a parent object that will encompass both the player and the tree objects. This will make it easier to manage them collectively. In your Game Maker environment:

  1. Create a new object and name it obj_sort_parent.
  2. Set the player and tree objects to be child objects of obj_sort_parent.

Doing this allows us to loop through all obj_sort_parent instances to easily manage both player and tree objects without writing redundant code.

Step 2: The Sorting Function

To manage the sorting, we'll use a function called camera_sort_objects(). This function uses an array, sortable_objects, to store the IDs of instances and sorts them based on their zdraw positions. Then, it updates their depth to ensure they are drawn in the correct order.

Here's the function:


function camera_sort_objects(){
    
    sortable_objects = [];
    
    with(obj_sort_parent){
        var len = array_length(other.sortable_objects);
        var ind = 0;
        for(var i = 0; i < len; i++){
            var obj = other.sortable_objects[i];
            if(obj.position.zdraw < position.zdraw){
                ind = i;
            }
        }
        array_insert(other.sortable_objects, ind+1, id);
    }
    /// set depth
    var len = array_length(sortable_objects);
    for(var i = 0; i < len; i++){
        var obj = sortable_objects[i];
        obj.depth = i;
    }
}

Explanation:

  1. sortable_objects = [];: Initializes an empty array to store object IDs for sorting.
  2. with (obj_sort_parent) {...}: Loops through all instances of obj_sort_parent (which includes child objects like the player and the trees).
  3. array_insert(other.sortable_objects, ind + 1, id);: Inserts the object ID (id) into the sortable_objects array at the correct position (ind+1), based on the zdraw value. (The position we will draw it on the z axis -- defined later)
  4. Finally, the depth of each object is set based on its position in the sortable_objects array, ensuring they are drawn in the correct order.

And there you have it! With the parent object and sorting function in place, we've set the stage for creating our rotating, Animal Crossing-inspired world. In the next part, we'll look at how to apply transformations and translations to these objects to make the rotation look seamless and realistic.

Part 3: Setting Up Camera Functions for Dynamic Cylindrical World Rendering

Introduction

In this section, we'll focus on the camera functions essential for rendering our dynamic cylindrical world. Since the camera plays a critical role in how the player perceives the game, setting it up correctly is crucial. We'll set up three different functions: one for initializing the camera (setup_camera), one for updating it (update_camera), and one for drawing with it (draw_camera).

Step 1: setup_camera Function

The setup_camera function initializes various variables that help in setting up the camera and the cylindrical world.

Here is the code:


function setup_camera(){
    /// @description setup
    world_radius = room_height * 0.25;
    world_angle = 0;
    world_xpos = 0;
    world_ypos = 0;
    world_zpos = 0;
    target = noone;
    alarm[0] = 1;
    tbuffer = tilemap_get_vertex_buffer_cylinder("Tiled_Cylinder", world_radius);
    depth = 100;
    sortable_objects = [];
}

Explanation:

  • world_radius: Sets the radius of our cylindrical world. It's set to 25% of the room's height.
  • world_angle: Initializes the angle of the world to 0.
  • world_xpos, world_ypos, world_zpos: Initialize the world's x, y, and z positions to 0.
  • target: Initializes the camera target to noone.
  • alarm[0] = 1;: Sets an alarm to trigger.
  • tbuffer: Obtains the vertex buffer for the tilemap cylinder.
  • depth = 100;: Sets the depth of the camera.
  • sortable_objects = [];: Initializes the sortable_objects array.

Step 2: update_camera Function

This function updates the camera's position and sorts the objects.


function update_camera(){
    if(instance_exists(target)){
        var cam_w = camera_get_view_width(view_camera[0]);
        var cam_h = camera_get_view_height(view_camera[0]);
        var cam_x = clamp(target.x - cam_w * 0.5, 0, room_width - cam_w);
        camera_set_view_pos(view_camera[0], cam_x, 0);
    }
    
    /// sort in place
    camera_sort_objects();
}

Explanation:

  • instance_exists(target): Checks if the target instance exists.
  • camera_get_view_width/view_height: Gets the camera's view dimensions.
  • clamp: Clamps the camera x-position to stay within the room.
  • camera_set_view_pos: Sets the camera's position.
  • camera_sort_objects(): Calls the sorting function.

Step 3: draw_camera Function

Finally, this function draws the elements on the screen, accounting for the cylindrical world.


function draw_camera(){
    
    var hh = camera_get_view_height(view_camera[0]);
    world_ypos = hh;
    gpu_set_ztestenable(1);
    gpu_set_zwriteenable(1);
    draw_tilemap_buffer(tbuffer, ts_cylder_tex2, 0, hh, 0, world_angle);
}

Explanation:

  • camera_get_view_height: Gets the camera's view height.
  • gpu_set_ztestenable(1); gpu_set_zwriteenable(1);: Enables the Z-test and Z-write functionalities.
  • draw_tilemap_buffer: Draws the tilemap buffer, thereby rendering the cylindrical world.

By the end of this section, you will have set up a robust camera system capable of rendering your dynamic, cylindrical Animal Crossing-inspired world. These camera functions play a key role in enabling the transformations and translations that make the world loop seamlessly. 

Part 4: Managing Animations and Drawing in the Cylindrical World

Introduction

Up until this point, we have focused primarily on setting up the core mechanics for our cylindrical world, including sorting objects and implementing camera functionalities. Now, we're going to dig into the details of how game objects are positioned, animated, and drawn within this unique environment. We'll introduce four new functions that manage these aspects: setup_position, update_position, set_animation_direction, and draw_self_on_cylinder.

Step 1: setup_position Function

Let's start by initializing the object's position and movement variables.

function setup_position(){
    position = {
        xdraw: x,
        ydraw: y,
        zdraw: 0,
        angle: 0,
        height: 24
    };
    
    depth = 0;
    movement = {
        x: 0,
        y: 0,
        max_speed: 1.5
    }
}

Explanation:

  • position: A struct that contains the drawn positions xdraw, ydraw, zdraw, the angle, and height of the object.
  • depth: Sets the object's depth to 0.
  • movement: A struct that contains movement directions x, y, and a max_speed.

Step 2: update_position Function

This function calculates the positions where the objects should be drawn.

function update_position(angle_override = undefined, offset=0){
    var radius = obj_camera.world_radius;
    var height = position.height;
    
    position.angle = y / room_height * 360;
    
    var rotate = angle_override == undefined ? position.angle + offset : angle_override;
    
    position.xdraw = obj_camera.world_xpos + x;
    position.ydraw = obj_camera.world_ypos - lengthdir_x(radius + height, rotate);
    position.zdraw = obj_camera.world_zpos + lengthdir_y(radius + height + sprite_height, rotate);
    
    /// scaling
    var ratio = 0.5 + 0.5 * lengthdir_y(1, rotate);
    image_xscale = lerp(1, 0.6, ratio);
    image_yscale = image_xscale;
    
    image_alpha = clamp(1 - lengthdir_y(1, rotate), 0, 1);
}

Explanation:

  • angle_override and offset: Optional parameters to override the angle.
  • position.angle: Calculates the angle based on the object's y-position.
  • rotate: Determines the angle to use for calculating x, y, and z draw positions.
  • image_xscale, image_yscale, and image_alpha: Sets scaling and transparency based on the rotation.

Step 3: set_animation_direction Function

This function sets the appropriate sprite based on the movement vector.

function set_animation_direction(move_vec, spr_up, spr_down, spr_left, spr_right){
    var spr = -1;
    if(abs(move_vec.y) > 0){
        if(move_vec.y > 0){
            spr = spr_down;    
        } else if(move_vec.y < 0){
            spr = spr_up;    
        }
    } else {
        if(move_vec.x > 0){
            spr = spr_right;    
        } else if(move_vec.x < 0){
            spr = spr_left;    
        }
    }
    return spr;
}

Explanation:

  • Determines the direction of movement and sets the appropriate sprite (spr_up, spr_down, spr_left, spr_right).

Step 4: draw_self_on_cylinder Function

This function handles drawing the object on the cylinder.

function draw_self_on_cylinder(){
    
    /// transform position
    matrix_set(matrix_world, setup_matrix(
        position.xdraw,
        position.ydraw,
        position.zdraw,
        0,0,0,image_xscale,image_yscale,1)
    );
    
    draw_sprite_ext(sprite_index, image_index, 0, 0, 1, 1, image_angle, image_blend, image_alpha);
    
    /// reset matrix
    matrix_set(matrix_world, matrix_build_identity());
}

Explanation:

  • matrix_set(matrix_world, setup_matrix(...)): Transforms the object's position for drawing.
  • draw_sprite_ext: Draws the sprite with various parameters like scaling, angle, and alpha.
  • matrix_set(matrix_world, matrix_build_identity()): Resets the matrix.

By the end of this part, you should have a clearer understanding of how game objects are managed in the cylindrical world. The four functions combined offer a comprehensive approach to dealing with object positioning, drawing, and animation within the unique game environment.

Part 5: Implementing the Player Object in Our Cylindrical World

Introduction

With the framework set up to handle the cylindrical environment and its objects, it's time to introduce the player into our world. We will implement a player object (obj_player) and program its movement, interaction with the camera, and drawing behavior in the cylindrical world. We will remove the obj_camera from the room and placing the creation code for it in the player (since the player will have a lot of code references to the camera, this ensures there are no creation order related issues)

Now, lets create obj_player and start adding the events and code below:

Create Event: Initialize Player and Camera

instance_create_depth(0,0,0,obj_camera);
setup_position();
/// change camera target
obj_camera.target = id;

Explanation:

  • instance_create_depth(0,0,0,obj_camera);: Creates a camera object in the room.
  • setup_position();: Calls the previously defined setup_position function to initialize the player's position.
  • obj_camera.target = id;: Sets the camera's target to follow this player instance.

Step Event: Update Player and World Movement


/// update world to rotate instead of player
var fixed_angle = 60;
obj_camera.world_angle = y / room_height * 360 - fixed_angle + 4;
/// get move
movement.y = keyboard_check(ord("S")) - keyboard_check(ord("W"));
movement.x = keyboard_check(ord("D")) - keyboard_check(ord("A"));
/// normalize
var len = point_distance(0,0,movement.x,movement.y);
if(len > 1){
    movement.x /= len;
    movement.y /= len;
}
if(len > 0){
    /// select sprite
    image_speed = 1;
    var spr_next = set_animation_direction(movement, spr_char_up, spr_char_down, spr_char_left, spr_char_right);
    if(spr_next != -1){
        sprite_index = spr_next;
    }
} else {
    image_speed = 0;    
}
/// move player
x += movement.x * movement.max_speed;
y += movement.y * movement.max_speed;
/// keep player in room
x = clamp(x, 0, room_width);
if(y < 0) y = room_height;
if(y > room_height) y = 0;
/// update cylindrical position (3d space)
update_position(fixed_angle);

Explanation:

  • obj_camera.world_angle: Updates the angle of the world based on the player's y-position.
  • keyboard_check(...): Captures keyboard inputs to control player movement.
  • point_distance(...): Normalizes the movement vectors.
  • set_animation_direction(...): Sets the sprite to use based on movement direction.
  • x += movement.x * movement.max_speed;: Moves the player horizontally.
  • y += movement.y * movement.max_speed;: Moves the player vertically.
  • clamp(x, 0, room_width);: Keeps the player within room boundaries.
  • update_position(fixed_angle);: Calls update_position to update player's cylindrical coordinates.

Draw Event: Render Player on Cylinder

draw_self_on_cylinder();

Explanation:

  • draw_self_on_cylinder();: Uses the previously defined function to draw the player object on the cylindrical world.

In this part, we have successfully integrated a player object into our cylindrical Animal Crossing-inspired world. We've also managed to handle player movement and the associated animation changes. 

Part 6: Adding Trees and Object Sorting in Our Cylindrical World

Introduction

So far, we have created a player object and a camera to give a cylindrical perspective to our Animal Crossing-inspired world. Now, it's time to populate the world with objects—starting with a tree. We'll also explore how to make objects aware of their order in the world, ensuring they draw correctly with respect to each other.

Create Event: Initialize Tree Position

setup_position();
position.height = 0;

Explanation:

  • setup_position();: This calls the setup_position function we previously defined, which initializes the tree's position in the world.
  • position.height = 0;: Sets the tree's height to zero, placing it on the "ground" of our cylindrical world.

Step Event: Update Tree Position

update_position(undefined, -obj_camera.world_angle);</code>

Explanation:

  • update_position(undefined, -obj_camera.world_angle);: Updates the tree's position. The undefined parameter means that the tree's default angle will be used, while the -obj_camera.world_angle ensures that the tree rotates in sync with the camera.

Draw Event: Render Tree on Cylinder

draw_self_on_cylinder();

Explanation:

  • draw_self_on_cylinder();: Calls the function we defined earlier to draw the tree in its correct place on the cylinder.

Inheritance and Sorting: Making obj_player and obj_tree Children of obj_sort_parent

To make sure the player and tree are drawn in the correct order, make both obj_player and obj_tree children of obj_sort_parent.

Explanation:

By making obj_player and obj_tree children of obj_sort_parent, they inherit the code to sort themselves in the cylindrical world. This ensures that objects are drawn correctly in relation to each other, thanks to the camera_sort_objects() function in obj_sort_parent.

Conclusion: Wrapping Up Your Animal Crossing-Inspired Cylindrical World

Recap

Over the course of this tutorial series, we've delved deep into the process of creating a unique and immersive game world inspired by Animal Crossing, using Game Maker. Starting from the ground up, we built a cylinder-based world, added a player character, and populated it with trees. Not only did we create these elements, but we also went behind the scenes to understand the code driving them—making sure objects are sorted and displayed in the correct order and perspective.

What You've Learned

  • How to set up a cylindrical world.
  • Managing camera perspectives.
  • Creating and manipulating player and tree objects.
  • Object inheritance and sorting for a layered world.
  • Animation and movement in a looped environment.

What's Next?

This tutorial series serves as a foundational guide. From here, you can expand in many directions, adding more objects, introducing interactions, and maybe even implementing seasons or other dynamic environmental changes.

I hope you've enjoyed this journey as much as I have. With your newfound knowledge and skills, you're well on your way to creating even more amazing game worlds. Keep coding, keep learning, and most importantly, have fun doing it!

Happy Game Making! 🎮

StatusReleased
CategoryAssets
Rating
Rated 5.0 out of 5 stars
(3 total ratings)
Authorfrothzon
Code licenseMIT License
Asset licenseCreative Commons Attribution v4.0 International

Download

Download NowName your own price

Click download now to get access to the following files:

cylinder_world_tutorial2.yyz 135 kB

Leave a comment

Log in with itch.io to leave a comment.