Introducción al Desarrollo de Videojuegos con GameMaker · Module 4: Controls, Movement, and Player Handling
Horizontal Movement and Speed
Lesson 17 of 60 · 60 min
Also available in: ES
From Key Presses to Physical Translation on Screen
In the previous lesson, you learned how to capture player intent using GameMaker's input functions, turning keyboard or gamepad presses into a normalized directional value between -1 and 1. However, knowing that the player wants to move left or right is only half the battle; the other half is translating that intent into a convincing, responsive kinematic motion tailored to the retro platformer we are building.
In 2D game development, translating an object along the X axis generally follows one of two core mechanical design philosophies: snappy, instant movement—where the entity reaches full speed the very frame a key is pressed and stops immediately when released—and gradual acceleration with inertia (friction), where speed builds up over time and smoothly bleeds off when input stops. Throughout this lesson, we will break down both approaches step by step, examining their mathematical implementation in GameMaker Language (GML Code), the role of dedicated variables, and their impact on game feel.
The GameMaker Coordinate System and the X Axis
Before writing a single line of movement code, it is essential to understand how GameMaker handles 2D space inside a room. Unlike the standard Cartesian plane taught in geometry classes, screen coordinates place the origin point (0, 0) at the top-left corner:
| Axis | Positive Direction (+) | Negative Direction (-) | Built-in Instance Property |
|---|---|---|---|
| Horizontal axis | To the right (adds to position) | To the left (subtracts from position) | x |
| Vertical axis | Downwards (adds to position) | Upwards (subtracts from position) | y |
Every instance automatically includes the built-in 'x' and 'y' variables, which store position coordinates in pixels with floating-point precision. Increasing 'x' shifts the instance to the right, while decreasing 'x' moves it to the left. However, directly modifying 'x' by adding a fixed constant (such as 'x += 4') without an intermediate speed variable prevents you from handling knockback, external forces, momentum, or the precise collision checks covered in the next module.
💡 Best Practice: While GameMaker features built-in movement variables such as 'hspeed', 'vspeed', 'speed', and 'direction', professional 2D platformer development favors custom variables (such as 'hsp' or 'vel_x'). Built-in variables update the instance's position automatically before you can execute custom, pixel-perfect collision checks.
Variable Architecture in the Create Event
To build a robust movement system for our player object ('obj_player'), we need to initialize a set of control variables in the 'Create' event. This event runs only once when the instance is spawned in the room, making it the ideal place to define the character's physical attributes.
Open your ongoing course project, select 'obj_player', open its 'Create' event, and add the horizontal speed variables we will use from now on:
/// @description Configuración inicial de movimiento horizontal
// obj_player - Evento Create
// Horizontal speed in pixels per frame
hsp = 0;
// Maximum horizontal speed
move_speed = 3.5;
// Acceleration rate (pixels per frame gained when pressing a key)
accel = 0.3;
// Friction or deceleration rate (pixels per frame lost when releasing)Let's break down the purpose of each variable: 1. 'hsp' (Horizontal Speed): Stores the calculated horizontal velocity for the current frame. It can be positive (moving right), negative (moving left), or zero (standing still). 2. 'move_speed': Defines the upper limit for standard movement. At 3.5, the character travels a maximum of 3.5 pixels per update step under a stable frame rate. 3. 'accel': Dictates how quickly the character builds momentum. A low value (e.g., 0.05) creates a slippery or heavy feel, while a value close to 'move_speed' makes acceleration virtually instantaneous. 4. 'fric': Determines ground braking friction. A moderate value such as 0.4 lets the character slide slightly before coming to a complete stop.
Approach 1: Instant Horizontal Movement (Digital/Arcade)
The simplest and most direct approach is to calculate velocity immediately by multiplying the input direction by the maximum speed. This style is iconic in classics like 'Mega Man' and high-precision action games where instant response takes precedence over physical inertia.
In the 'Step' event of 'obj_player', combine the input polling covered in the previous lesson with direct updates to 'hsp' and the 'x' coordinate:
/// @description Movimiento horizontal instantáneo
// obj_player - Evento Step
// 1. Get input vector (-1, 0, 1)
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _key_left = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _move = _key_right - _key_left;
// 2. Direct speed calculation
hsp = _move * move_speed;
// 3. Apply to horizontal position
x += hsp;The underlying math is clean and straightforward: if the player presses the right key, '_move' equals 1, and 'hsp' becomes 3.5 in a single frame; 'x' immediately advances by 3.5 pixels. If the player presses left, '_move' is -1 and 'hsp' becomes -3.5, moving the instance left. Releasing all keys sets '_move' to 0, which zeroes out 'hsp' on the spot and halts the character instantly.
Approach 2: Gradual Acceleration and Realistic Friction
While instant movement is exceptionally responsive, many platformers (such as 'Super Mario World' or 'Sonic the Hedgehog') use physics that simulate body weight and surface traction. To achieve this, we do not set velocity directly; instead, we accumulate speed step by step over time.
The logic in the 'Step' event splits into two branches: - When input is active (_move != 0): increment 'hsp' in the desired direction by 'accel', ensuring it does not exceed 'move_speed'. - When input is inactive (_move == 0): reduce 'hsp' toward 0 by 'fric', preventing it from overshooting zero and accidentally reversing direction.

Limiting Speed with clamp() and Checking Direction with sign()
To program acceleration and inertia without numeric glitches, GameMaker provides two fundamental math functions:
1. 'clamp(val, min, max)': Returns 'val' bounded strictly between a minimum and a maximum limit. If 'val' is below 'min', it returns 'min'; if it exceeds 'max', it returns 'max'. This prevents acceleration from increasing speed indefinitely. 2. 'sign(val)': Returns 1 if the number is positive, -1 if negative, and 0 if exactly zero. This lets you determine the current travel direction regardless of magnitude.
Here is how to implement full acceleration and friction logic inside the 'Step' event:
/// @description Movimiento horizontal con aceleración y fricción
// obj_player - Evento Step
// 1. Read input
var _key_right = keyboard_check(vk_right) || keyboard_check(ord("D"));
var _key_left = keyboard_check(vk_left) || keyboard_check(ord("A"));
var _move = _key_right - _key_left;
// 2. Manage acceleration and deceleration
if (_move != 0)
{
// Input detected: accelerate in that direction
hsp += _move * accel;
// Clamp velocity to stay within limits
hsp = clamp(hsp, -move_speed, move_speed);
}
else
{
// No keys pressed: apply friction to stop
if (abs(hsp) <= fric)
{
// If speed is less than or equal to friction, come to a complete stop
hsp = 0;
}
else
{
// Subtract friction against the current direction of travel
hsp -= sign(hsp) * fric;
}
}
// 3. Apply displacement to the X axis
x += hsp;Pay close attention to the condition 'if (abs(hsp) <= fric)'. The 'abs()' function returns the absolute value of a number (its unsigned magnitude). If a character is moving at 'hsp = 0.2' and friction is '0.4', simply subtracting friction would yield '0.2 - 0.4 = -0.2'. The player would start drifting left without any input, entering an endless jittering oscillation around zero. Checking whether the remaining speed is smaller than the friction step guarantees a clean stop at absolute zero.
The Alternative Method: Custom approach() Function
In modern GameMaker development, helper scripts are widely used to simplify smooth value transitions. Rather than writing long conditional blocks with 'abs()' and 'sign()', many developers write a reusable helper function called 'approach'.
To add this function to your project, open the Asset Browser, right-click your 'Scripts' folder (created in Module 3), and select 'Create' -> 'Script'. Name the script 'scr_math' and paste the following code:
/// @function approach(current, target, amount)
/// @description Shifts a current value toward a target by a given amount without overshooting.
/// @param {Real} _current Current value
/// @param {Real} _target Target value to reach
/// @param {Real} _amount Amount to adjust per step
function approach(_current, _target, _amount)
{
if (_current < _target)
{
return min(_current + _amount, _target);
}
else
{
return max(_current - _amount, _target);
}
}Using 'approach', the movement block in 'obj_player's 'Step' event becomes substantially more concise, readable, and elegant:
/// @description Movimiento limpio usando la función approach
// obj_player - Evento Step
var _move = (keyboard_check(vk_right) || keyboard_check(ord("D"))) -
(keyboard_check(vk_left) || keyboard_check(ord("A")));
var _target_speed = _move * move_speed;
var _rate = (_move != 0) ? accel : fric;
// Shift current horizontal speed toward target speed
hsp = approach(hsp, _target_speed, _rate);
// Move the player
x += hsp;This snippet sets a target speed ('_target_speed'). When holding right, the target is 3.5. When releasing keys, the target is 0. Next, it determines whether we are speeding up or slowing down to select the proper rate using the ternary operator '(_move != 0) ? accel : fric', and smoothly adjusts 'hsp' until it converges precisely on the target.
Sub-pixel Behavior and On-Screen Rendering
Adding floating-point values (such as 0.3 or 3.5) to the 'x' coordinate turns the instance's position into a fractional number (e.g., 'x = 104.7'). GameMaker manages fractional coordinates seamlessly under the hood and rounds the visual sprite position to whole pixels when drawing each frame.
However, in pixel-art games using low native display resolutions (such as 320x180 or 640x360), rendering sprites at sub-pixel coordinates can occasionally cause slight visual artifacts or pixel jitter (known as pixel shimmering). Later, in Module 6 and Module 10 when configuring camera viewports and render states, you will learn how to round draw positions in the Draw event without altering the underlying fractional coordinates of 'x' and 'y'.
Practical Activities and Hands-On Exercises
To solidify your grasp of horizontal kinematics, complete these three exercises directly in your GameMaker project.
Exercise 1: Sprint Mechanic
Objective: Implement a modifier key (such as the Shift key) that temporarily increases the player's top speed and acceleration while held.
Suggested steps: 1. In the 'Create' event of 'obj_player', declare two speed settings: 'walk_speed = 3.0;' and 'run_speed = 5.5;'. Set 'move_speed = walk_speed;'. 2. In the 'Step' event, check for the sprint key using 'keyboard_check(vk_shift)'. 3. If the key is held, set 'move_speed' to 'run_speed'. Otherwise, reset 'move_speed' to 'walk_speed'. 4. Run the project and confirm that the character reaches a noticeably higher top speed while sprinting and returns to normal walking speed upon release.
Exercise 2: Slippery Ground Simulation (Ice Surface)
Objective: Dynamically adjust kinematic parameters to simulate walking on low-traction terrain.
Suggested steps: 1. Declare a boolean variable 'on_ice = false;' in the 'Create' event. 2. In the 'Step' event, add a debug toggle: check if 'H' is pressed using 'keyboard_check_pressed(ord("H"))', and toggle 'on_ice = !on_ice;'. 3. Adjust acceleration and friction: if 'on_ice' is true, set 'accel = 0.05;' and 'fric = 0.03;'; if false, retain the default values ('accel = 0.3;' and 'fric = 0.4;'). 4. Test the room and observe how the character takes significantly longer to turn around and stop when ice physics are active.
Exercise 3: Real-Time Speed Detection and Debugging
Objective: Print the exact numeric value of 'hsp' to the Output Console to verify that deceleration reaches absolute zero without oscillating.
Suggested steps: 1. At the end of 'obj_player's 'Step' event, add: 'show_debug_message("Current HSP: " + string(hsp));'. 2. Run the game, move right, and then release all inputs. 3. Observe the 'Output' log panel at the bottom of the GameMaker workspace. 4. Verify that once keys are released, 'hsp' smoothly ramps down until reaching exactly 0.00, without alternating between positive and negative values.
Summary
- The horizontal (X) axis in GameMaker increases to the right and decreases to the left, originating from the room's top-left corner.
- Custom variables like 'hsp' should be used instead of built-in variables ('hspeed') to maintain explicit control over collisions and character physics.
- Instant movement provides snappy arcade responsiveness, while acceleration and friction create physical weight and momentum.
- The 'clamp()' function ensures that accumulated speed never exceeds established movement caps.
- To bring an instance to a clean stop without endless micro-jitter, check whether remaining speed is less than or equal to friction before subtracting.
- A custom 'approach()' helper function provides a reusable, clean way to linearly interpolate values toward a target without overshooting.
Practise what you learned
Exercise 1 · 1 point
Why is it recommended in professional 2D platformer development to use custom variables such as `hsp` instead of GameMaker's built-in variables like `hspeed`?
Exercise 2 · 1 point
Match each math function or variable used in the lesson with its corresponding role in horizontal movement:
- clamp(val, min, max)
- sign(val)
- abs(val)
- move_speed
Exercise 3 · 1 point
Complete the Step event code to implement instant horizontal movement by multiplying the direction by maximum speed and updating the appropriate coordinate: ```gml // 2. Direct speed calculation hsp = _move * ; // 3. Apply to horizontal position += hsp; ```
Sign in to save your progress — Sign in