A downloadable asset pack

Download NowName your own price

In this tutorial series we are adding more collision code, submersion, and collisions with the tile layer.  This continues from Tutorial #3 Here. (Make sure to download the tutorial package if you want to save some time typing out code, or if you don't quite understand how it is implemented)

Part 1: Consolidating the position struct

To clean up the code, I am going to take a lot of the z-axis update code and put it into the position struct, and turn it into the Position class and set the methods to static so all of the objects use the same methods.

// Script assets have changed for v2.3.0 see
// https://help.yoyogames.com/hc/en-us/articles/360005277377 for more information
function Position(_height=16, _z=0) constructor{
    self.z = _z;                    /// position on z axis
    self.z_speed = 0;            /// speed on z axis
    self.z_gravity = 0.5;            /// acceleration on z axis
    self.z_ground = 0;            /// lowest point reachable
    self.z_height = _height;    /// height on z axis
    self.z_step = 4;            /// platform edge tolerance
    self.floor_obj = noone;        /// platform id underneath
    self.z_minimum = -16;        /// lowest point when no platform present
    
    /// calculate edge directions of rectangular bounding box
    var _cx = (other.bbox_left + other.bbox_right) * 0.5;
    var _cy = (other.bbox_top + other.bbox_bottom) * 0.5; 
    self.corners = [
        point_direction(_cx,_cy,other.bbox_right,other.bbox_top),
        point_direction(_cx,_cy,other.bbox_left,other.bbox_top),
        point_direction(_cx,_cy,other.bbox_left,other.bbox_bottom),
        point_direction(_cx,_cy,other.bbox_right,other.bbox_bottom)
    ];
    
    /// collision shape, use "circle" for circular collision mask
    self.shape = "rectangular";
    
    /// get highest point on the z-axis
    static z_top = function(){
        return self.z + self.z_height;    
    };
    
    /// return motion of floor_obj on x,y,z as an array
    static get_floor_vector = function(){
        var _floor_vec = [0,0,0];
        /// if on the ground and object is beneath
        if(self.floor_obj >= 0 && instance_exists(self.floor_obj)){
            /// additional check for overlapping objects
            var _floor_pos = self.floor_obj.position;
            /// platform is moving, we can check within the bounds of z_step for collision
            if(abs(self.z - _floor_pos.z_top()) <= self.z_step){
                /// [xspeed, ypseed, zspeed]
                _floor_vec = [self.floor_obj.hspeed, self.floor_obj.vspeed, self.floor_obj.position.z_speed];
            }
        }
        return _floor_vec;
    };
    
    /// get direction of collision for pushing objects out of bbox
    static collision_direction = function(_angle){
        if(self.shape == "circle") return _angle;
        if(_angle < self.corners[0]) return 0;
        if(_angle < self.corners[1]) return 90;
        if(_angle < self.corners[2]) return 180;
        if(_angle < self.corners[3]) return 270;
        return 0;
    };
    
    /// set z_ground to lowest possible position
    static reset_z_ground = function(){
        self.z_ground = self.z_minimum;
        self.floor_obj = noone;
    };
    
    static update = function(wall_parent){
        //------------------------------- Motion on Z-Axis -------------------------------------//
        /// always move when speed is applied
        self.z += self.z_speed;
        /// move in z direction, 
        /// Z_Axis is assumed to point in the -Y direction (+z)
        /// therefore, positive z_speed is up, -z_speed is down
        if(self.z > self.z_ground){
            self.z_speed -= self.z_gravity;
        } else {
            /// if below ground, adjust to ground position
            self.z = max(self.z, self.z_ground);
            /// if falling, stop falling
            /// get minimum speed [xspeed, yspeed, zspeed], use only zspeed
            var _floor_spd = self.get_floor_vector();
            self.z_speed = max(self.z_speed, _floor_spd[2]);
        }
        
        //------------------------- Collision Handling on Z-Axis --------------------------------//
        /// use parent object to check for collision on z-axis
        with(other){
            var _list = ds_list_create();
    
            /// default lowest point
            other.reset_z_ground();
    
            if(instance_place_list(x, y, wall_parent, _list, 0)){
    
                /// loop through all collision instances
                for(var _i = 0; _i < ds_list_size(_list); _i++){
                    var _collide = _list[| _i];
        
                    /// if collision occurs, we want to get on top of that object if possible
                    if(_collide != noone && variable_instance_exists(_collide, "position")){
                        var _pos2 = _collide.position;
                        /// if self is above the top of the other object, z_ground will now be at that height
                        if(other.z + other.z_step > _pos2.z_top()){
                            other.z_ground = max(other.z_ground, _pos2.z_top());
                            other.floor_obj = _collide;
                        } 
                    }
                }
            }
            /// remove list to clear memory
            ds_list_destroy(_list);
        }
        
        //------------------------------- Platform Movement --------------------------------------//
        var _platform_speed = self.get_floor_vector();
        /// add motion to parent object
        other.x += _platform_speed[0];
        other.y += _platform_speed[1];
    };
    
}

In this function we aggregate all of the update methods for the z-position into one method called "update." We then add in variables "shape" and "corners" so that we can treat rectangular bounding boxes different from circular bounding boxes. When we get pushed out of a rectangular shape we want to be pushed out at the normal direction, which is perpendicular to the surface. We then create a method called "collision_direction" which returns an angle that is in the direction of the normal.

We also add "z_minimum" for the lowest possible point the player can sit, this will replace our magic number, zero, and allow us to submerge the player.

Part 2: Update player code

Next, we can clean up the step event of the player, and add in code to manage the bobbing up and down of the player in the water, as well as change the depth so they appear underwater.

/// @description Update Movement
/// move player on z axis with gravity
update_zaxis(obj_block);
/// get tilemap under plalyer
var _tile_type = get_tile_index(x,y);
if(_tile_type <= 0){
    /// floating -- slowly dither the z minimum value
    position.z_minimum = wave(-12,-4,2.5,0);
} else {
    /// ground position - requires jumping to get above 0
    if(position.z >= 0){
        position.z_minimum = 0;
    }
}
/// sort y position when above water
if(position.z > -1){
    depth = -y;
    max_spd = 2;
    position.z_gravity = 0.5;
} else {
    depth = 50;
    max_spd = 1;
    position.z_gravity = 0.1;
    /// dampen
    position.z_speed = lerp(position.z_speed, 0, 0.1);
}
/// get movement axis
motion.x = keyboard_check(ord("D")) - keyboard_check(ord("A"));
motion.y = keyboard_check(ord("S")) - keyboard_check(ord("W"));
/// apply movement
if(motion.length() > 0){
    
    /// keeps movement smooth in all directions
    motion.normalize();    
    
    /// save motion vector so we can test it
    var move_vector = new Vector2(motion.x * max_spd, motion.y * max_spd);
    
    /// detect collision on x axis
    if(collision_check_zaxis(x+move_vector.x, y, obj_block)){
        move_vector.x = 0;
    }
    
    /// detect collision on y axis
    if(collision_check_zaxis(x, y+move_vector.y, obj_block)){
        move_vector.y = 0;
    }
    
    /// tilemap collision x
    if(position.z < 0 && get_tile_index(x + move_vector.x, y) > 0){
        move_vector.x = 0;
    }
    
    /// tilemap collision y
    if(position.z < 0 && get_tile_index(x, y + move_vector.y) > 0){
        move_vector.y = 0;
    }
    
    x += move_vector.x;
    y += move_vector.y;
}
/// jump
if(keyboard_check_pressed(vk_space)){
    var _jump_spd = position.z < 0 ? 4 : 6;
    if(abs(position.z - position.z_ground) < position.z_step){
        position.z_speed += _jump_spd;
    }
}

We have created a placeholder function called "get_tile_index" that will need to be defined. We also added some code in various places to change the jump, gravity, and movement behaviors depending on whether the player is below the water level threshold. We hard coded the water to be at 0, so if you choose to make the water level dynamic in the future you may want to create a variable called "water_level" and use it in place of 0. 

Lets now define "get_tile_index." Create a script called get_tile_index and paste the following.

function get_tile_index(_xto, _yto, _layer_name = "Floor"){
    var _lay_id = layer_get_id(_layer_name);
    var _map_id = layer_tilemap_get_id(_lay_id);
    return tilemap_get_at_pixel(_map_id, _xto, _yto);
}

This is really simple, it just returns the index of the tile placed in the layer named "Floor." If there is a tile placed, the index will be greater than 0. You could also make the "water" a tile, and check against that specific index.

Next we will want to update the "init_zaxis" method and "update_zaxis" method.

function init_zaxis(_height=16, _z=0){
    position = new Position(_height, _z);
}
function update_zaxis(_wall_parent){
   position.update(_wall_parent);
}

Finally, we want to add into the end_step event the following placeholder function:

zaxis_collision_handler(obj_block);

Part 3: Collision Handling

The last part of this tutorial is going to be very important when you have moving platforms. When a platform lands on the player, it pins them in place. We don't want the player to get pinned in place, so we need a collision handling function called "zaxis_collision_handler" which will take an object parameter for the parent object currently named obj_block. Here is the code we will use:


function zaxis_collision_handler(_wall_parent){
    
    /// find any object colliding with this object
    var _stuck = collision_check_zaxis(x,y,_wall_parent,true);
    /// if this object is real, perform push-out manuever
    if(_stuck != noone && instance_exists(_stuck)){
        /// if player is moving upward, stop moving upward
        if(position.z_speed > 0){
            position.z_speed = 0;
        }
        
        /// if player is on the ground, push out of object
        if(position.z <= position.z_ground){
            var _cx = (_stuck.bbox_left + _stuck.bbox_right) * 0.5;
            var _cy = (_stuck.bbox_top + _stuck.bbox_bottom) * 0.5;
            var _dsin = point_direction(_cx,_cy,x,y);
            var _ds = _stuck.position.collision_direction(_dsin);
            var _dx = lengthdir_x(1, _ds);
            var _dy = lengthdir_y(1, _ds);
            var _try_count = 4;
            while(collision_check_zaxis(x,y,_wall_parent,true) != noone){
                x += _dx;
                y += _dy;
                /// ran out of attempts, leave while loop
                if(_try_count-- <= 0){
                    break;    
                }
            }
        }
    }
}

This function checks to see if we are inside of another object, and if so it finds the vector pointing away from the player (using the surface normal function we created) and pushes the player 4 pixels per frame out of the object until there are no collision conflicts.

Conclusion:

The implementation of collisions and swimming capabilities was a relatively straightforward process, with the majority of the modifications being made to the position struct and its corresponding update functions. With these changes in place, we now possess a comprehensive z-axis solution that can be readily adapted to enhance the gameplay experience of any 2.5D or 3D game. Overall, our efforts have resulted in a polished and versatile solution that can be easily incorporated into a range of game development projects. (Click hereto view tutorial #5)

StatusReleased
CategoryAssets
Rating
Rated 5.0 out of 5 stars
(2 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:

z-axis-tutorial_partD.zip 30 kB

Comments

Log in with itch.io to leave a comment.

Thank you very much for this tutorial series!!!

I have added some code to the player so that you can increase/decrease your max speed from 0 to 12 by clicking the left/right mouse buttons.

I have made some ramps and placed blocks next to them. At a slow speed the player can go up and down the ramps and continue along the blocks fine.

However when the player goes up the ramp in a faster speed, the player will collide/bump with the next block and stop, before continuing again. The work around I found to stop the collision/bump, was to increase the z_step.

"
if instance_place(x,y,o_ramp_LR) or instance_place(x,y,o_ramp_RL){
position.z_step = 16;}else{position.z_step = 4;}
"
without posting a video or the full code/project here.

Is this correct?

(1 edit)

You might want to make z step equal to the absolute value of  speed + 1 (speed on x/y axis only). Then, the further (i.e. faster) you step into the slope, the higher the step the player can take. Keep in mind that this could allow the player to climb on anything if their speed exceeds the height of the object.

Thank you for your reply.
I have created a hierarchy o_blocks_parent with, o_blocks and o_ramps as the children. And updated all the collision code with o_blocks_parent. And cleaned up my previous code in the above comment with; if instance_place(x,y,o_ramps){ position.z_step = 4 + abs(speed+1)}else{position.z_step = 4} this way i cant "climb" on anything and it works buttery smooth.

You will be named in the credits.

I just donated $15 to this project. Between 1-4, slopes and the water I’ve been trying to figure this out for a year. 


at this point I could probably add stuff like ladders and wall jumps, myself, but ideas to add for version 5

Thanks, I appreciate it. When I have some time I will add tutorial #5.

This is really awesome stuff. Do you ever do paid work in gml? 

I am unable to do most paid work since I don't have a lot of time to get involved in projects that will require copious amounts of development. My work as an Embedded Systems Programmer is a demanding task, so this is more of a hobby for me at this point.

Deleted 1 year ago