I am not so sure maybe TiberiusGBT will try giving you an example:
Ah, my friend! You've touched upon a fundamental concept in game development: grid-based movement. It's a fantastic choice for dungeon explorers, tactical RPGs, puzzle games, and so many other genres! Your father's initial idea of using a two-dimensional array was not only correct, but it's the most robust and elegant way to manage grid logic. You just needed the Tiberius touch to illuminate its full potential!
Let's break down grid-based movement with the precision and clarity you deserve.
The Power of the 2D Array: Logical vs. Visual Grid
Your first idea, the 2D array, is the logical backbone of your grid. It represents the state of each cell on your grid. Your second idea, moving by fixed pixel increments, is the visual representation of that logical movement. The key is to separate these two concepts and then connect them.
Think of it this way:
- Logical Grid (2D Array): This is where your game knows where everything is. It determines valid moves, collisions, item pickups, enemy positions, etc. It's the brain.
- Visual Grid (Pixel Movement): This is what the player sees. When the logical position changes, you draw the character at the corresponding pixel coordinates. It's the body.
Let's revisit your map array:
map = [
[0, 0, 0],
[0, 1, 0], // 0 is nothing, 1 is player
[0, 1, 0],
]
This is perfect! 0 for empty, 1 for player. We can expand this:
0: Empty/Walkable space
1: Player
2: Wall/Obstacle
3: Enemy
4: Item
- And so on!
Core Concepts for Grid Movement
Here’s how we make that map array come alive:
Player's Logical Position: Instead of just marking 1 in the map, we'll keep track of the player's current grid coordinates using dedicated variables, for example: player_grid_x and player_grid_y. These represent the column and row in your map array.
Proposed Movement: When the player presses an arrow key, you don't instantly move them. Instead, you propose a new position.
- Move Right:
proposed_x = player_grid_x + 1, proposed_y = player_grid_y
- Move Left:
proposed_x = player_grid_x - 1, proposed_y = player_grid_y
- Move Up:
proposed_x = player_grid_x, proposed_y = player_grid_y - 1
- Move Down:
proposed_x = player_grid_x, proposed_y = player_grid_y + 1
Boundary Checks: Before moving, you check if proposed_x and proposed_y are within the valid bounds of your map array.
0 <= proposed_x < map[0].length (Proposed X must be within the width of the map)
0 <= proposed_y < map.length (Proposed Y must be within the height of the map)
Collision Checks: If the proposed position is within bounds, you then check what's at map[proposed_y][proposed_x].
- Is it a
2 (wall)? Then you can't move there.
- Is it a
3 (enemy)? Perhaps initiate combat!
- Is it a
0 (empty)? Great, you can move!
Updating Logical Position: If the move is valid (within bounds and no collisions with impassable objects):
- First, "clear" the player's old position in the map:
map[player_grid_y][player_grid_x] = 0
- Then, update
player_grid_x = proposed_x and player_grid_y = proposed_y
- Finally, "set" the player's new position in the map:
map[player_grid_y][player_grid_x] = 1
Visual Rendering: In your draw() function, you convert player_grid_x and player_grid_y into pixel coordinates. If each grid cell is, say, 32 pixels by 32 pixels (your "10px" idea, but scaled), then:
screen_x = player_grid_x * tile_size - (map_width * tile_size / 2) (adjust for screen center)
screen_y = player_grid_y * tile_size - (map_height * tile_size / 2) (adjust for screen center)
- Then draw your player sprite at
screen_x, screen_y.
Let's Build a Simple microScript Example!
Here's how you'd implement this in microStudio using microScript. I'll make a small 5x5 dungeon.
// GLOBAL GAME STATE VARIABLES
local map = [] // Our 2D array representing the dungeon grid
local map_width = 5
local map_height = 5
local tile_size = 20 // Each grid cell will be 20x20 screen units (relative to microStudio's -100 to 100 screen coords)
local player_grid_x = 0
local player_grid_y = 0
// Sprites (you'd define these in microStudio's sprite editor)
// For this example, we'll just draw colored squares.
// 'player_sprite_name' = "player_sprite"
// 'wall_sprite_name' = "wall_sprite"
// init() is called once when the program starts
init = function()
// Initialize our map
// 0 = empty (walkable), 1 = player, 2 = wall
map = [
[2, 2, 2, 2, 2], // Row 0
[2, 0, 0, 0, 2], // Row 1
[2, 0, 2, 0, 2], // Row 2
[2, 0, 0, 0, 2], // Row 3
[2, 2, 2, 2, 2] // Row 4
]
// Set initial player position (e.g., at [1,1] which is map[1][1])
player_grid_x = 1
player_grid_y = 1
map[player_grid_y][player_grid_x] = 1 // Mark player's initial position on the map
end
// update() is called 60 times per second
update = function()
local proposed_x = player_grid_x
local proposed_y = player_grid_y
local moved = false
// --- Input Handling and Proposing New Position ---
if keyboard.press.LEFT or keyboard.press.A then
proposed_x = player_grid_x - 1
moved = true
elsif keyboard.press.RIGHT or keyboard.press.D then
proposed_x = player_grid_x + 1
moved = true
elsif keyboard.press.UP or keyboard.press.W then
proposed_y = player_grid_y - 1
moved = true
elsif keyboard.press.DOWN or keyboard.press.S then
proposed_y = player_grid_y + 1
moved = true
end
// --- Process Proposed Movement (if a key was pressed) ---
if moved then
local can_move = true
// 1. Boundary Check: Is the proposed position within the map's bounds?
if proposed_x < 0 or proposed_x >= map_width or proposed_y < 0 or proposed_y >= map_height then
can_move = false
system.say("Blocked by boundary!") // For debugging/feedback
end
// 2. Collision Check: Is the proposed tile a wall (value 2)?
if can_move and map[proposed_y][proposed_x] == 2 then
can_move = false
system.say("Blocked by wall!") // For debugging/feedback
end
// Add more collision checks here (e.g., for enemies, items, etc.)
// --- Execute Valid Movement ---
if can_move then
// Clear player's old position on the map
map[player_grid_y][player_grid_x] = 0
// Update player's logical grid coordinates
player_grid_x = proposed_x
player_grid_y = proposed_y
// Set player's new position on the map
map[player_grid_y][player_grid_x] = 1
end
end
end
// draw() is called to render the screen
draw = function()
screen.clear("black") // Clear the screen each frame
// Calculate the top-left corner of our grid in screen coordinates
// This centers the map on the screen.
local start_draw_x = -(map_width * tile_size) / 2
local start_draw_y = -(map_height * tile_size) / 2
// Loop through the map array to draw everything
for y = 0 to map_height - 1
for x = 0 to map_width - 1
local tile_value = map[y][x]
// Calculate screen position for the current tile
local screen_draw_x = start_draw_x + x * tile_size
local screen_draw_y = start_draw_y + y * tile_size
if tile_value == 0 then // Empty space
screen.setColor("#333333") // Dark gray for walkable area
screen.fillRect(screen_draw_x, screen_draw_y, tile_size, tile_size)
elsif tile_value == 1 then // Player
screen.setColor("blue") // Blue for player
screen.fillRect(screen_draw_x, screen_draw_y, tile_size, tile_size)
// If you had a sprite: screen.drawSprite("player_sprite", screen_draw_x, screen_draw_y, tile_size, tile_size)
elsif tile_value == 2 then // Wall
screen.setColor("gray") // Gray for walls
screen.fillRect(screen_draw_x, screen_draw_y, tile_size, tile_size)
// If you had a sprite: screen.drawSprite("wall_sprite", screen_draw_x, screen_draw_y, tile_size, tile_size)
end
// Optional: Draw grid lines for clarity
screen.setColor("white")
screen.drawRect(screen_draw_x, screen_draw_y, tile_size, tile_size)
end
end
// Display player's current grid coordinates for debugging
screen.drawText("Player X: " + player_grid_x + ", Y: " + player_grid_y, -90, 90, 8, "white")
end
Explanation and Key Takeaways:
map Array: This 2D array is your dungeon's blueprint. Its indices ([y][x]) directly correspond to grid coordinates.
player_grid_x, player_grid_y: These are the player's true logical coordinates. All movement logic hinges on updating these.
tile_size: This variable is crucial for converting logical grid coordinates into visual pixel coordinates. It determines how big each "square" on your screen is. This is exactly what you were thinking with your "10px" idea!
init() function: Sets up the initial dungeon layout and player starting position. We mark the player's spot in the map.
update() function:
- Input: Listens for keyboard presses. Notice
keyboard.press which only triggers once per press, perfect for discrete grid movement.
- Proposed Position: Calculates where the player wants to go.
- Boundary & Collision Checks: These
if statements are your guardians. They prevent the player from walking off the map or through walls. This directly answers your boundary question!
- Logical Update: If all checks pass, the player's old spot on the
map is cleared, their player_grid_x/y are updated, and their new spot on the map is marked.
draw() function:
- Iterates through every single cell in your
map array.
- For each cell, it calculates its corresponding
screen_draw_x and screen_draw_y based on tile_size.
- It then draws a colored rectangle (or a sprite, if you uncomment the lines) based on the
tile_value in that cell.
This robust system clearly separates the game's underlying logic from its visual presentation, making your dungeon exploration game much easier to build and expand upon.
Now you have a solid foundation for grid-based movement! You can expand this by adding enemies, items, more complex collision rules, or even smoother visual transitions between tiles (if you want an animation between discrete grid steps).
What part of this would you like to explore or expand upon next? Perhaps implementing enemies, or adding items to your dungeon?