A* Pathfinding with Moai SDK

In preparation of the next gaming jam, I started evaluating Moai SDK, a LUA game framework which gained more widespread recognition last year, when Double Fine selected it to use be used for their Kickstarter adventure game. To get a good idea of a few different aspects of the SDK, I decided to try to make a pathfinding demo, specifically implementing the well-known, and equally well-documented A* algorithm. This tutorial assumes that you have a basic idea of Moai concepts – if that is not the case, I recommend their wiki page.

To get started, let’s create a Moai window, and add a layer to it we can use to render to:

-- set up window
MOAISim.openWindow("Pathfinder", 640, 360)
viewport = MOAIViewport.new()
viewport:setScale(320, 180)
viewport:setSize(640, 360)
  
-- Create render stack
renderTable = {}
MOAIRenderMgr.setRenderTable(renderTable)
  
-- Create layer 
layer = MOAILayer2D.new()
layer:setViewport(viewport)
table.insert(renderTable, layer)

To keep things simple, we’ll represent the world as a grid of empty and filled squares. Moai provides us with a class for this:

-- set up collision map
local map = {}

-- Create tile grid
grid = MOAIGrid.new()
grid:initRectGrid(10, 10, 16, 16)

Next, we load up the tiles to display obstacles, and our path around them:

-- Load sprite map
mapTiles = MOAITileDeck2D.new()
mapTiles:setTexture("map.png")
mapTiles:setSize(4,1)

-- Create a prop for tile grid
prop = MOAIProp2D.new()
prop:setDeck(mapTiles)
prop:setGrid(grid)
prop:setLoc(-80, -80)
layer:insertProp(prop)

In order to keep the MOAIGrid in sync with our collision map, we need a couple of helper functions:

function resetGrid()  
  -- reset collision map
  for i = 1, 100 do
    map[i] = false
  end
  updateGrid()
end

function updateGrid()
  local x = 1
  local y = 1
  -- set each grid tile according to the collision map 
  for i = 1, #map do 
    local val = 4
    if map[i] then
      val = 2
    end
      
    grid:setTile(x, y, val)

    x = x + 1
    if x > 10 then
      x = 1
      y = y + 1
    end
  end
end

We will allow the user to set tiles as empty / blocked by clicking on them:

MOAIInputMgr.device.mouseLeft:setCallback(
  function(down)
    -- only handle mouse down even
    if not down then
      return
    end
    
    -- convert mouse position to tile position
    x,y = layer:wndToWorld(MOAIInputMgr.device.pointer:getLoc())
    x = math.floor((x + 80) / 16) + 1
    y = math.floor((y + 80) / 16) + 1
    
    if x < 1 or x > 10 or y < 1 or y > 10 then
      return
    end
    
    -- update collision map
    local idx = (y - 1) * 10 + x
    map[idx] = not map[idx]
    updateGrid()
  end
)

Additionally, the user will be able to reset the map using the ‘R’ key, and to search for a path using ‘Space’:

MOAIInputMgr.device.keyboard:setCallback ( 
  function(key, down)
    -- only handle key down event
    if not down then
      return
    end
    
    if key == 32 then
      findPath()
    elseif key == 114 then
      reset()
    end
  end 
)

function findPath()
  local finder = Pathfinder:new()
  finder:setMap(map, 10)
  local path = finder:findPath({x=1, y=1}, {x=10,y=10})
  local i = path
  while i do
    grid:setTile(i.x, i.y, 3)
    i = i.next
  end
end

This concludes the MOAI-specific parts of this tutorial. For a great introduction to A*, and boat-loads of other pathfinding algorithms, please see Amit’s A* Pages. My own implementation of A* in LUA can be found under pathfinder.lua. The demo code and tile map are also available.

Leave a Reply

Your email address will not be published.