Z-Axis Movement (3D/2.5D Game, Tutorial #5)
In this tutorial I will show you how to add wall jumping and ladder-climbing. This continues from Tutorial #4 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: State Machine
We need to manage the player behavior with a finite state machine so we can seamlessly switch between normal player movement and ladder climbing movement. The reason this is important is we will need to turn off the physics and collisions when climbing the ladder.
First we will have to initialize some variables in the create event to manage physics and wall jumping. Add the following code to the player create event.
/// wall jump variables
can_wall_jump = new Vector2();
air_motion = new Vector2();
/// state machine = normal player state
state = player_state_normal;
/// current ladder in use
ladder_object = noone;
/// timer for state machine
state_timer = 0;
The "can_wall_jump" variable will need to be set with the vector pointing away from the wall, but only when the player is able to jump. The "air_motion" vector keeps the player moving while in the air so you don't have to hold down the left/right movement keys.
Now, lets create a new script called "object_get_center" and put the following code in it:
function object_get_center(_obj){
var _center = new Vector2(
(_obj.bbox_left + _obj.bbox_right) * 0.5,
(_obj.bbox_top + _obj.bbox_bottom) * 0.5
);
return _center;
}
I will need to use this to get the center of the ladder on the ground, and having a separate script will help keep the code clean.
Next we are going to create the script for testing if the player is trying to climb a ladder, call it "player_ladder_test" and put the following code in it:
function player_ladder_test(_move_vec){
/// check if player is moving up/down only
if(abs(_move_vec.x) <= 0.2 && _move_vec.y != 0){
/// check for collision with ladder 6 pixels up or down
var _check_ladder = collision_check_zaxis(x, y + _move_vec.y * 6, obj_ladder, true);
/// if ladder exists, check to see if we are trying to climb it
if(_check_ladder >= 0 && instance_exists(_check_ladder)){
var _center = object_get_center(_check_ladder);
if(sign(_center.y - y) == sign(_move_vec.y)){
/// set ladder object and change the state
ladder_object = _check_ladder;
state = player_state_climb;
}
}
}
}
This script will detect when the player is trying to climb a ladder and switch the player to the ladder climbing state.
Part 2: Climbing Ladders
Now we will create the ladder climbing script, lets name it "player_state_climb" and put the following code into it:
function player_state_climb(){
/// no ladder, exit
if(ladder_object < 0 || !instance_exists(ladder_object)){
state = player_state_normal;
exit;
}
/// get off ladder
if((keyboard_check_pressed(vk_space) && state_timer <= 0)
|| (position.z > ladder_object.position.z_top())
|| position.z < ladder_object.position.z){
position.z = max(position.z, ladder_object.position.z);
state_timer = room_speed;
state = player_state_normal;
/// jump if above
if (position.z >= ladder_object.position.z_top()) {
position.z_speed = 4;
}
exit;
}
/// move up
if(keyboard_check(ord("W"))){
position.z += 2;
}
/// move down
if(keyboard_check(ord("S"))){
position.z -= 2;
}
/// move to center of ladder
var xmid = (ladder_object.bbox_left + ladder_object.bbox_right) * 0.5;
x = lerp(x, xmid, 0.25);
y = lerp(y, ladder_object.bbox_bottom+12, 0.25);
/// keep shadow at bottom
position.z_ground = ladder_object.position.z;
}
This script checks to see if the ladder exists, and if we are still climbing it. It will change the state back to "player_state_normal" when we get off the ladder. If we get off at the top, it makes the player jump slightly -- to help disengage from the ladder more effectively. The script also detects if the player presses the up or down key and moves the player along the z-axis. While the player is on the ladder we linearly interpolate the players current position to the position they need to be on the x/y axis to climb the ladder. We also set our z-ground to be at the bottom of the ladder so our shadow is drawn correctly, and there aren't any collision artefacts when jumping off.
Part 3: Integration
Now that we have the climbing state ready to go, we need to write "player_state_normal" and for that we grab all the code that is in the step event, cutting it out, and pasting it into a new function called "player_state_normal."
Then, at the bottom of the new function paste in the following code:
/// climb ladder
player_ladder_test(motion);
Lets also add in the code for wall jumping. This wont be too complicated. Right below where we set motion.x and motion.y in the "player_state_normal" script we will add the following code:
/// wall jump vector
can_wall_jump.x = 0;
can_wall_jump.y = 0;
if(position.on_ground()){
air_motion.x = 0;
air_motion.y = 0;
} else {
max_spd = 0.5;
}
This will initialize the wall jump vector every frame, and initialize the air motion vector when we are on the ground. The completed code for all the changes we will make to "player_state_normal" is below:
// Script assets have changed for v2.3.0 see
// https://help.yoyogames.com/hc/en-us/articles/360005277377 for more information
function player_state_normal(){
/// 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"));
/// wall jump vector
can_wall_jump.x = 0;
can_wall_jump.y = 0;
if(position.on_ground()){
air_motion.x = 0;
air_motion.y = 0;
} else {
max_spd = 0.5;
}
/// apply movement
var move_vector = new Vector2();
if(motion.length() > 0 || air_motion.length() > 0){
/// keeps movement smooth in all directions
motion.normalize();
/// save motion vector so we can test it
move_vector.x = motion.x * max_spd + air_motion.x;
move_vector.y = motion.y * max_spd + air_motion.y;
/// detect collision on x axis
if(collision_check_zaxis(x+move_vector.x, y, obj_block)){
if(!position.on_ground()){
can_wall_jump.x = -move_vector.x;
}
move_vector.x = 0;
}
/// detect collision on y axis
if(collision_check_zaxis(x, y+move_vector.y, obj_block)){
if(!position.on_ground()){
can_wall_jump.y = -move_vector.y;
}
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;
air_motion.x = move_vector.x + position.platform_motion.x;
air_motion.y = move_vector.y + position.platform_motion.y;
} else if(can_wall_jump.length() > 0){
air_motion.x = clamp(can_wall_jump.x * 2, -2, 2);
air_motion.y = clamp(can_wall_jump.y * 2, -2, 2);
position.z_speed = _jump_spd * 1.25;
}
}
/// climb ladder
player_ladder_test(motion);
}
For the part of the movement check that handles wall collision detection "if(collision_check_zaxis()..." we will need to add a check if the player is not on the ground, if the player is not on the ground then save the vector opposite of the movement vector into "can_wall_jump." When this value is set, we now know it is possible to wall jump along that vector. We want the opposite vector so we can jump away from the wall.
At the bottom of the "player_state_normal" function, where we handle jumping, we simply check to see if the "can_wall_jump" vector length is greater than 0. If we can wall jump, then we set the z_speed to 1.25 times the jump speed to give the player a more potent jump (it makes it easier to wall jump), and then we add the "can_wall_jump" vector to the "air_motion" vector -- and scale it by a factor of 2 (that also looks pretty nice).
We will also need to add the "air_motion" every frame to the player movement. In the part of the code where we are checking the (key-input) motion length, we can also check "air_motion" length. This will allow us to move the player while they are in the air, bypassing the key-input vector "motion". All forms of movement are simply added to the "move_vector".
The last thing we have to do for the player is run the state machine code. In the step event for the player this should be the only code running:
/// run player state machine
if(state_timer > 0) state_timer--;
state();
This code simply decrements the state timer, and runs the state machine.
Now we need to add a ladder object, go ahead and create an object called "obj_ladder" as a child of "obj_block" and place the following code in the create event:
/// @description set height
// Inherit the parent event
event_inherited();
position.z_height = sprite_height;
This doesn't require anything special to be a ladder, but you will need to make the bounding box the small rectangular area corresponding to the feet of the ladder for best results.
Conclusion:
We now have a state machine to handle ladder climbing and some wall jumping code. This last tutorial was very simple and really goes to show how powerful a state machine can be, and also how robust the z-axis code is -- allowing additional features to be added quite easily. I hope you have enjoyed this tutorial series!
Download the premium content bundle to get the full demo (includes day/night cycle lighting, and more!)
Status | Released |
Category | Assets |
Rating | Rated 5.0 out of 5 stars (1 total ratings) |
Author | frothzon |
Made with | GameMaker |
Code license | MIT License |
Asset license | Creative Commons Attribution_NoDerivatives v4.0 International |
Download
Click download now to get access to the following files:
Comments
Log in with itch.io to leave a comment.
Great tutorial series! I have a question:
Is there a reason to set up the depth/draw the way you did it with depth = -y and then in the draw_test script y -= position.z
as opposed to just having depth = -y - position.z? I haven't ever seen it setup the way you have so I'm just curious about that.
I didn't want the depth to change when the player jumps up, if there is a tree in front of them blocking them from view they would clip through it when jumping.