A downloadable asset pack

Download NowName your own price

Are you ready to delve into a world that defies conventional boundaries, where gravity pulls you in unexpected directions and adventures are found in 360 degrees? Welcome to our innovative tutorial series, where we'll bring to life a game environment unlike any other—a Circular World!

The thrill of the conventional 2D platformer is given a twist (quite literally!) as we wrap our world into a loop, with gravity holding everything together. Say goodbye to the 'left-right' monotony and embrace the freedom of circular exploration!

Together, we'll harness the robust power of GameMaker's built-in physics engine to create an immersive world that's not just a visual spectacle but also abides by the realistic laws of motion and gravity. You'll learn how to breathe life into objects, create forces, and simulate the mystical attraction of gravitational pull in a celestial context.

But what's a game without a little action? We'll top it all off by designing a player controller that can navigate this circular wonderland. You'll gain insights into scripting a character who can walk around our world, defying the traditional 'up and down', leaping from surface to surface, adhering to the pull of gravity.

So buckle up and brace yourself as we dive headfirst into this rollercoaster of a tutorial, breaking barriers and reshaping horizons. This journey promises to not only hone your game development skills but also change the way you perceive your game world. Whether you're a seasoned developer or a curious beginner, this series is your ticket to exploring new dimensions in game design!

Part 1: Creating a world object

We will start by creating a new object called obj_world. This will work best if we use a spherical sprite. In the sprite Texture settings make sure we check the box for "Separate Texture Page," this will be important later. Now add a create event and put the following code:

var _rad = sprite_width * 0.5;                /// base radius of shape
segments = 32;                                /// break shape into convex polygons
variation = 5;                                /// add variation to surface of shape
angle_dr = 360 / segments;                    /// angle per slice of convex poly
tex = sprite_get_texture(sprite_index, 0);    /// texture from sprite
points = [];                                /// all points on edge of shape
/// create positions on circle
for (var i = 0; i < segments; ++i) {
    var dist = _rad + irandom_range(-variation,variation);
    var x1 = lengthdir_x(dist, i * angle_dr);
    var y1 = lengthdir_y(dist, i * angle_dr);
    array_push(points, {x: x1, y: y1, r: dist});
}
/// create physics fixtures
var len =  array_length(points);
for (var i = 0; i < len; ++i) {
    // code here
    var fix, x1, y1, x2, y2;
    fix = physics_fixture_create();
    physics_fixture_set_density(fix, 0);
    physics_fixture_set_kinematic(fix);
    physics_fixture_set_friction(fix, 5);
    physics_fixture_set_restitution(fix, 0.0);
    
    x1 = points[i].x;
    y1 = points[i].y;
    x2 = points[(i+1)%len].x;
    y2 = points[(i+1)%len].y;
    
    
    physics_fixture_set_polygon_shape(fix);
    physics_fixture_add_point(fix, 0, 0);
    physics_fixture_add_point(fix, x2, y2);
    physics_fixture_add_point(fix, x1, y1);
    
    physics_fixture_bind(fix, id);
    physics_fixture_delete(fix);
}
gravity_radius = sprite_width * 0.5 + 128;

Let's break it down:

  1. First, it creates some variables. _rad represents the base radius of the shape, which is half of the sprite's width. segments specifies how many pieces the shape is split into. variation adds irregularity to the surface of the shape. angle_dr calculates the angle for each slice of the shape. tex is the texture of the sprite, and points will store the edge points of the shape.
  2. The first loop creates position points around the circle. For each segment, it calculates a random distance from the center of the circle using the base radius (_rad) and adding a random variation to it. It then uses lengthdir_x and lengthdir_y to get the x and y coordinates at this distance for the current segment's angle. These coordinates are stored as an object with fields x, y, and r (radius) in the points array.
  3. The second loop creates physics fixtures for each segment of the shape. For each point in the points array, it creates a physics fixture and sets its properties. The points used to define the polygon shape are the current point and the next point in the array (wrapping back to the start of the array for the last point). This essentially forms a triangle between the origin (0, 0) and two consecutive points on the shape's edge. The fixture is then bound to the object running the script (id) and finally deleted (since the fixture data is now stored in the physics world).
  4. gravity_radius is set to be slightly larger than the sprite's width. This could be used for gravity effects in a subsequent part of the code not shown here.

In essence, this script is creating a irregular circle-like shape out of triangles and attaching it to the object's physics body. This allows for more complex collisions and physics interactions than just a simple circle or box collider would permit.

Part 2: Drawing the World

We are going to transform the circular shape of the sprite into the rough and rocky shape of the planet we defined in the create event. Add a draw event and place the following code

var _xx = phy_position_x;
var _yy = phy_position_y;
/// draw gravity field
draw_set_alpha(0.6);
draw_circle_color(_xx,_yy, gravity_radius, c_white, c_blue, false);
draw_set_alpha(1);
/// create physics fixture
draw_set_colour(c_white);
draw_primitive_begin_texture(pr_trianglelist, tex);
/*
    draw planetoid
*/
var len =  array_length(points);
for (var i = 0; i < len; ++i) {
    // code here
    var x1, y1, x2, y2;
    var dx1, dy1, dx2, dy2;
    
    dx1 = lengthdir_x(1, i * angle_dr);
    dy1 = lengthdir_y(1, i * angle_dr);
    dx2 = lengthdir_x(1, i * angle_dr + angle_dr);
    dy2 = lengthdir_y(1, i * angle_dr + angle_dr);
    
    x1 = _xx + dx1 * points[i].r;
    y1 = _yy + dy1 * points[i].r;
    x2 = _xx + dx2 * points[(i+1)%len].r;
    y2 = _yy + dy2 * points[(i+1)%len].r;
    
    
    draw_vertex_texture(_xx, _yy, 0.5, 0.5);
    draw_vertex_texture(x2, y2, dx2 * 0.49 + 0.5, dy2 * 0.49 + 0.5);
    draw_vertex_texture(x1, y1, dx1 * 0.49 + 0.5, dy1 * 0.49 + 0.5);
    draw_line(_xx,_yy,x2,y2);
    
}
draw_primitive_end();

The code is broken down into three main parts: drawing the gravity field, setting up the texture drawing, and then drawing the planetoid.

  1. Drawing the gravity field: The code first sets up the drawing of the gravity field around the object. This is represented as a circle centered at the object's current physics position (phy_position_x and phy_position_y) with radius gravity_radius, and it fades from white at the center to blue at the edge. The circle is filled (false in draw_circle_color).
  2. Setting up the texture drawing: Next, the drawing color is set to white, and the code begins a textured triangle list drawing using the texture tex.
  3. Drawing the planetoid: A loop is set up that will go through the points on the planetoid's surface. For each point, the code calculates direction vectors (dx1, dy1, dx2, dy2) for the current point and the next point (with wrapping). It then calculates the position of these two points (x1, y1, x2, y2) relative to the planetoid's position (_xx, _yy).

Each triangle to be drawn consists of the center of the planetoid and two adjacent points on the edge. The position of each vertex within the texture is also specified (as a coordinate between 0 and 1). These texture coordinates are calculated such that they form a similar irregular circle within the texture as the points do within the world. This allows the texture to wrap correctly around the irregular circular shape.

Finally, a line is drawn from the center of the planetoid to the second point. This will divide the object visually into segments. The draw_primitive_end call signifies the end of the triangle list.

In summary, this code is responsible for rendering the irregularly-shaped, textured planetoid and its gravity field.

Part 3: Creating physics objects

Before we add in the forces for gravity, lets create a parent object for all things affected by gravity. Create a obj_physics_parent (name is not important) and check the "Uses Physics" box. Next, open up obj_world and add a collision event with obj_physics_parent and type "exit" in the code block. Now lets create another object and call it obj_physics_static_parent and add it as a child to obj_physics_parent. The static parent will be the parent for all non player controlled physics objects. These should all have a physics collision group of 1.

Now, lets create a script called Vector2 and fill it with the following code

function Vector2(vx = 0, vy = 0) constructor{
    self.x = vx;
    self.y = vy;
    static normalize = function(){
        var dis = self.length();
        if(dis == 0){
            return;    
        }
        self.x /= dis;
        self.y /= dis;
    };
    static length = function(){
        return(point_distance(0,0,self.x,self.y));
    };
    static set = function(vx, vy){
        self.x = vx;
        self.y = vy;
    };
    static add = function(v2){
        self.x += v2.x;
        self.y += v2.y;
    };
    static multiply = function(v2){
        self.x *= v2.x;
        self.y *= v2.y;
    };
    static dot = function(v2){
        return(dot_product(self.x,self.y,v2.x,v2.y));
    };
    static scale = function(value){
        self.x *= value;
        self.y *= value;
    };
    static angle = function(){
        return(point_direction(0,0,self.x,self.y));    
    };
    static lengthdir = function(length, angle){
        self.x = lengthdir_x(length, angle);
        self.y = lengthdir_y(length, angle);
    };
    static add_lengthdir = function(length, angle){
        self.x += lengthdir_x(length, angle);
        self.y += lengthdir_y(length, angle);
    };
    static set_length = function(length){
        self.normalize();
        self.scale(length);
    };
    static vector_from_points = function(x1,y1,x2,y2){
        var angle = point_direction(x1,y1,x2,y2);
        var dist = point_distance(x1,y1,x2,y2);
        self.add_lengthdir(dist, angle);
    };
    static to_array = function(){
        return [self.x, self.y];
    };
    static from_array = function(arr){
        self.x = arr[0];
        self.y = arr[1];
    };
}

The provided code is a constructor function for a Vector2 object in GameMaker. This object is designed to represent a vector in a 2D space with several useful functions (methods) for working with vectors. Here's a breakdown of what each part does:

  1. Vector2(vx = 0, vy = 0) constructor: This is the constructor for creating a new Vector2 object. If no arguments are provided, the vector will default to (0, 0).
  2. self.x and self.y: These are the x and y components of the vector.
  3. normalize: This method normalizes the vector, turning it into a unit vector pointing in the same direction. This is done by dividing each component by the vector's length.
  4. length: This method calculates the magnitude (length) of the vector.
  5. set: This method sets the x and y components of the vector to the provided values.
  6. add: This method adds another Vector2's components to this vector's components.
  7. multiply: This method multiplies this vector's components by another Vector2's components. Note that this is not a traditional vector multiplication (like the dot or cross product) but a component-wise multiplication.
  8. dot: This method calculates the dot product of this vector and another Vector2.
  9. scale: This method scales the vector by a scalar value.
  10. angle: This method returns the direction of the vector in degrees, relative to the origin.
  11. lengthdir: This method sets the vector's components based on a given length and angle.
  12. add_lengthdir: This method adds a vector to the current vector, where the vector to add is defined by a given length and direction.
  13. set_length: This method sets the vector's magnitude to a given length while maintaining its direction.
  14. vector_from_points: This method sets the vector's components such that it represents the direction and distance from one point to another.
  15. to_array: This method returns the vector's components as an array.
  16. from_array: This method sets the vector's components from an array.

In summary, this is a versatile Vector2 object that can be used in many 2D game scenarios for operations such as movement, physics calculations, AI direction finding, and much more. The self keyword is used to reference the instance of the Vector2 object on which the method is being called.

In obj_physics_parent put the following into the Create event and Begin Step event:

gravity_normal = new Vector2();

We will use this to calculate the normal direction, which is just a fancy way of saying the vector pointing in the opposite direction of the average gravity direction. (helps when you have multiple planets with overlapping gravity fields).

Part 4: Creating Player

Lets work on the player code, lets create a new object and call it obj_player. Add obj_physics_parent as the players parent. Now add a create event and put the following code:

event_inherited();
phy_fixed_rotation = true;

This will inherit from the parent object and turn off physics rotations (so we can do it manually)

Now, create an event for End Step and put the following code in it:

/// @description update player
/// force rotation to match normal
var normal = -phy_rotation-270
if(gravity_normal.length() > 0){
    gravity_normal.normalize();        
    var normal = gravity_normal.angle();
    phy_rotation = -(normal+270);
}
/// move player
var move = keyboard_check(ord("D")) - keyboard_check(ord("A"));
var rot_ground = normal + 180;
var dx = phy_position_x + lengthdir_x(1, rot_ground);
var dy = phy_position_y + lengthdir_y(1, rot_ground);
var on_ground = physics_test_overlap(dx,dy,phy_rotation,obj_world) | physics_test_overlap(dx,dy,phy_rotation,_static_phys_obj)
//show_debug_message(on_ground);
if(move != 0){
    var acc = on_ground ? 600 : 300;
    var vs = new Vector2(lengthdir_x(acc, normal - 90) * sign(move), lengthdir_y(acc, normal - 90) * sign(move));
    var total = new Vector2(vs.x, vs.y);
    total.normalize();
    total.multiply(new Vector2(phy_speed_x, phy_speed_y));
    
    if(move > 0){
        image_xscale = 1;    
    } else {
        image_xscale = -1;    
    }
    
    /// move if speed is less than max
    if(total.length() < 2){
        physics_apply_force(
            phy_position_x,
            phy_position_y,
            vs.x,
            vs.y
        );
    }
}
/// jump
//show_debug_message(on_ground);
if(on_ground && keyboard_check_pressed(vk_space)){
    var _jumpforce = 100;
    physics_apply_impulse(phy_position_x, phy_position_y, gravity_normal.x * _jumpforce, gravity_normal.y * _jumpforce);
}

Let's break it down:

  1. Force rotation to match normal: The script sets the player's rotation to match the gravity normal (the direction of gravity). The gravity_normal vector is normalized (made into a unit vector), and the player's physics rotation (phy_rotation) is set to oppose the direction of the gravity vector. This would cause the player to always "stand" with their feet pointing to the direction of gravity.
  2. Move player: It captures input from the "A" and "D" keys to move the player. The result of keyboard_check(ord("D")) - keyboard_check(ord("A")) will be -1, 0, or 1, depending on whether "A", neither, or "D" is pressed.

    It then checks if the player is on the ground. This is done by moving a point just below the player (in the direction of gravity) and checking if this point overlaps with the world or a static physics object.

    If the player is trying to move (i.e., either "A" or "D" is pressed), the code creates a force vector pointing left or right relative to the direction of gravity. The magnitude of this force is greater if the player is on the ground (600 vs 300). This force is then applied to the player, but only if their current speed in the direction of movement is less than a maximum speed (2 in this case).

    The player's sprite is also flipped to face the direction of movement (right if "D" is pressed, left if "A" is pressed).

  3. Jump: If the player is on the ground and the space bar is pressed, an impulse (an instantaneous change in velocity) is applied to the player in the direction opposing gravity. This results in a jump action.

In summary, this script handles movement and jumping for a player character in a 2D physics game where gravity can point in arbitrary directions. The player will always "stand" perpendicular to gravity and can move and jump relative to the direction of gravity.

Part 5: Adding Gravity

Now it is time to add the force of gravity to the world, we will restrict it to the gravity_radius variable. If you wanted you could modify it to use the inverse square and be infinite but sometimes what is realistic isn't always fun. 

Create a step event for obj_world and add the following code:

var _list = ds_list_create();
var _count = collision_circle_list(phy_position_x, phy_position_y, gravity_radius, obj_physics_parent, 0, 1, _list, 0);
var _gravity = 45;
/// apply gravity
for (var i = 0; i < _count; ++i) {
    // get every object in the gravity well and apply force proportional to their masses
    var _obj = _list[| i];
    with(_obj){
        var _force = phy_mass * _gravity;
        var _direction = point_direction(phy_position_x, phy_position_y, other.phy_position_x, other.phy_position_y);
        var _fx = lengthdir_x(_force, _direction);
        var _fy = lengthdir_y(_force, _direction);
        physics_apply_force(phy_position_x, phy_position_y, _fx, _fy);
        /// add each gravity vector
        var move_vec = new Vector2(-_fx, -_fy);
        gravity_normal.add(move_vec);
    }
}

This script is used to simulate the effect of a gravitational field around an object. Here's the breakdown of the script:

  1. A ds_list (dynamic data structure that behaves like an array) is created, and all the objects within a certain radius of the current instance (assumed to be the source of gravity) are found using the collision_circle_list function. These objects are stored in the ds_list, and the total count is stored in _count.
  2. The gravity constant is set as 45 (this is a game-specific value that represents the strength of the gravitational pull).
  3. A for loop is used to iterate through all the objects found within the gravitational field.

    For each object:

    • The direction from the object to the source of gravity is calculated using point_direction.
    • The gravitational force is calculated by multiplying the mass of the object (phy_mass) by the gravity constant.
    • The force is then decomposed into its x and y components using lengthdir_x and lengthdir_y.
    • The force is applied to the object using physics_apply_force, which effectively pulls the object towards the gravity source.
    • A new Vector2 object is created, representing the force vector but pointing in the opposite direction (the negative x and y components of the force).
    • This vector is then added to gravity_normal, which representing the sum of all gravitational forces acting on it. We defined this earlier in the code to determine the overall direction and strength of gravity on the object.

In summary, this script creates a gravitational effect where objects within a certain radius are attracted towards the center. The strength of this attraction is proportional to the mass of the object, and multiple sources of gravity are added together. It's a simplification of how gravity works in the real world and is commonly used in game physics systems.

Conclusion:

This tutorial provides a comprehensive understanding of how to create and manipulate a physics-based environment in GameMaker, specifically focusing on character movement around circular objects. Throughout the tutorial, we've explored different scripts and their functionality, helping us understand how to simulate a character moving, jumping, and responding to a gravitational field.

We started with the setup script, which creates the physics fixtures for our convex polygon object, essentially forming the "planet" our character can traverse. This setup is crucial for creating a base structure in which our characters can interact with.

The second script concentrated on rendering these objects. We saw how to draw the character and the gravitational field around the convex polygon objects. This is crucial for player feedback, giving visual representations to the physics we've implemented.

Next, we studied a Vector2 script which provided us with a variety of vector operations, which are fundamental for physics calculations. This script is a toolset that was used extensively in the following scripts, highlighting its importance.

In the update script, we saw how to use these vector operations to handle player input and manipulate the character's position and velocity. This script forms the core of our character's movement and interactions with the physics world.

Lastly, we reviewed a script that applied gravitational forces to the player and other objects within a specified range of the polygon object, simulating a gravity effect similar to that experienced by celestial bodies in space.

By combining these scripts, we achieved a physics-based character movement system where a character can walk and jump around different "planets", while the force of gravity keeps the character grounded to the surface. 

This tutorial highlights the power and flexibility of GameMaker's physics engine and the potential for creating unique and interesting gameplay mechanics. We hope you found this tutorial helpful and are now inspired to experiment further with physics in your own games. Happy Game Making!

StatusReleased
CategoryAssets
Rating
Rated 5.0 out of 5 stars
(2 total ratings)
Authorfrothzon
GenrePlatformer
Tags2D, circular, GameMaker, Gravity, mario, Physics
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:

circular_world_demo.yymps 78 kB

Comments

Log in with itch.io to leave a comment.

(1 edit)

Are you planning tutorials about procedural generation and generative art?

(+1)

Yes

What is yymps?

Game Maker Studio 2 asset package. Drag it into the game maker to use.