Quick answer: ds_grid_create(w, h) creates indices 0 to w-1 and 0 to h-1. If you access index w or h, you are one past the end. Always loop to ds_grid_width(grid) - 1, add a bounds check helper, and free grids in the Destroy event.
You build a grid-based level system, spawn tiles into a ds_grid, and the game crashes the moment the player steps on the rightmost column. The error says “index out of bounds” and points at a perfectly innocent-looking grid access. The bug is a classic off-by-one: you are treating the grid’s width as a valid index when it is actually the count.
The Off-by-One
ds_grid_create(10, 8) creates a grid with 10 columns (x: 0–9) and 8 rows (y: 0–7). The maximum valid index is always width - 1 and height - 1. Accessing ds_grid_get(grid, 10, 0) is out of bounds.
The bug hides because most loops iterate for (var i = 0; i < width; i++) with a strict less-than, which is correct. The crash appears when you use <= or when you compute an index from game coordinates and forget to clamp.
// BAD: off-by-one
for (var xx = 0; xx <= ds_grid_width(grid); xx++) {
for (var yy = 0; yy <= ds_grid_height(grid); yy++) {
var val = ds_grid_get(grid, xx, yy); // crashes on last iteration
}
}
// GOOD: strict less-than
for (var xx = 0; xx < ds_grid_width(grid); xx++) {
for (var yy = 0; yy < ds_grid_height(grid); yy++) {
var val = ds_grid_get(grid, xx, yy);
}
}
World Coordinates to Grid Indices
The second common cause is converting world pixel coordinates to grid indices without clamping. A player at position (639, 480) in a 32-pixel tile grid maps to index (19, 15). If the grid was created with ds_grid_create(20, 15), x = 19 is valid but y = 15 is out of bounds (max is 14).
// Safe world-to-grid conversion
function grid_get_safe(grid, world_x, world_y, tile_size) {
var gx = clamp(floor(world_x / tile_size), 0, ds_grid_width(grid) - 1);
var gy = clamp(floor(world_y / tile_size), 0, ds_grid_height(grid) - 1);
return ds_grid_get(grid, gx, gy);
}
The clamp is cheap and prevents every possible out-of-bounds access from world coordinates.
ds_grid_resize Pitfalls
ds_grid_resize(grid, new_w, new_h) preserves existing data and initializes new cells to 0. But code that was written for the old dimensions still uses the old width and height. If you resize from 10×10 to 5×5, any loop that still runs to 10 crashes.
Always re-read ds_grid_width and ds_grid_height after a resize. Never cache them in variables that outlive the resize call.
Memory Leaks
Unlike arrays, ds_grid allocates on the heap and is not garbage-collected. Every ds_grid_create must be matched by a ds_grid_destroy, usually in the Destroy or Clean Up event of the object that owns it.
// Clean Up event
if (ds_exists(grid, ds_type_grid)) {
ds_grid_destroy(grid);
}
If you create a grid per level and forget to destroy the old one on room change, you leak one grid per level load. Over a long session, this adds up.
When to Use Arrays Instead
GameMaker 2.3+ supports 2D arrays natively (var arr = array_create(w) with each element being another array). Arrays are garbage-collected and do not need manual destruction. For simple tile data, arrays are simpler and safer. Use ds_grid when you need the built-in region functions (ds_grid_get_max, ds_grid_get_sum, ds_grid_sort) or when you need to pass the grid to functions that expect a ds_grid ID.
Verifying the Fix
Add a debug overlay that draws each grid cell as a colored rectangle. Red cells at the edges confirm you are accessing the full range. If the last row or column is missing, your loop is stopping one short. If the game crashes when the overlay runs, your loop goes one too far.
“Every ds_grid bug is an off-by-one or a missing destroy. Check both and you have fixed 95% of the reports you will ever see about grids.”
Related Issues
For broader GameMaker data lifecycle issues, see GameMaker instance variables resetting on room change. For save data that references grid indices, see GameMaker save best practices.
Wrap every ds_grid access in a bounds check during development. The check is one line and saves hours of debugging silent crashes on edge tiles.