Quick answer: Collision is computed in room coordinates and is correct — the issue is visual. Snap the camera to whole pixels with floor() and use tilemap_get_at_pixel for collision tests.
Zoom the camera in to 4× for a closer view of the pixel art. The player visibly walks off the edge of a platform but doesn’t fall — or, worse, gets stopped by an invisible wall. At default 1× zoom, everything was fine.
What’s Actually Wrong
GameMaker collision logic runs in room coordinates and doesn’t know or care about camera zoom. A 4× zoom doesn’t magnify or distort the collision shapes — they remain pixel-accurate in the room. What zoom does is amplify two visual issues:
- Sub-pixel camera position: at 4× zoom, a camera at x=120.3 instead of x=120 shifts the visible world by 0.3 pixels of room space — which renders as 1.2 pixels of screen space. The player sprite appears one pixel off from where collision happens.
- Sub-pixel sprite position: a player at x=80.5 draws on either side of pixel 80 depending on the rendering engine’s rounding. Collision uses the exact 80.5 value, producing inconsistent visual results.
Fix 1: Snap the Camera
/// In your camera controller’s Step
var target_x = obj_player.x - camera_get_view_width(view_camera[0]) / 2;
var target_y = obj_player.y - camera_get_view_height(view_camera[0]) / 2;
// Smoothly approach, then snap
camera_x = lerp(camera_x, target_x, 0.1);
camera_y = lerp(camera_y, target_y, 0.1);
camera_set_view_pos(view_camera[0], floor(camera_x), floor(camera_y));
The lerp produces a smooth follow; the floor at the end ensures the camera ends up at integer coordinates each frame. Visual stability returns.
Fix 2: Snap Sprite Positions for Rendering
If sprite positions are fractional (very common when using a velocity vector), draw at the rounded position:
/// obj_player Draw
draw_sprite(sprite_index, image_index, floor(x), floor(y));
Collision still uses the precise x and y; only the visual is snapped. The player’s movement remains smooth in the underlying numbers, but renders to whole-pixel positions every frame.
Fix 3: Use tilemap_get_at_pixel
For custom tile-based collision (instead of place_meeting against a wall object), the canonical function is:
var tile_id = tilemap_get_at_pixel(tilemap_id, x + hsp, y);
if (tile_id != 0) {
// solid tile at that position
}
This handles the tilemap’s position offset internally. Rolling your own math — tile_x = (x - tilemap.x) div tile_w — is correct only if you remember to subtract the tilemap origin and account for negative coordinates.
Fix 4: Use Application Surface Scaling
Instead of camera zoom, render at native resolution to the application surface and scale the surface for display:
/// Room Start
surface_resize(application_surface, 320, 180);
display_set_gui_size(1280, 720); // 4x
The game renders to a small surface that’s upscaled by 4× for display. No fractional coordinates in the render path; pixel art stays crisp. Camera operates at native 320×180 and never zooms.
Verifying
Draw a debug rectangle at the player’s collision bbox using the precise coordinates, and another at the snapped render position. Move the player and watch the offset. After the fixes, both rectangles overlap perfectly each frame.
“Collision is correct. Visuals are lying. Snap camera and sprite to whole pixels.”
For pixel-art games, prefer surface upscaling over camera zoom — eliminates this entire class of bug.