In day 8 I added an enemy ghost, but one with no brain – it just bumbled randomly around the maze:
Time for some very simple AI.
In real Pacman the three ghosts have different chasing strategies, but I thought I’d go for something very simple, from first principles – a ghost that chases straight towards the player, wherever he/she is.
First, I had to rework something: my enemy ghost was making turn decisions too often, when they should, normally, only even consider changing direction at a junction or collision:
-- check for collision with wall on current path
if checkForOpenPath(mapRefX,mapRefY,v) == false then
enemyDirections[i] = chooseDirectionChange(i,v,mapRefX,mapRefY,true)
hasChanged = true
else
if checkForJunction(mapRefX,mapRefY,v) then
enemyDirections[i] = chooseDirectionChange(i,v,mapRefX,mapRefY,false)
end
end
The new checkForJunction function called there just tests if either of the paths at a 90 degree angle to the current trajectory is open.
The chooseDirectionChange function, though, is the one that has work to do. First we calculate the distance from the player on the X and Y axes:
-- calculate divergence from player sprite position
local divergenceX = ((playerSprite.x-topLeft)/16)+1 - mapRefX
local divergenceY = (playerSprite.y/16)+1 - mapRefY
.. them use those to make an ordered array* of directions, in preference order based on comparing the absolute values of both (eg, if divergenceX is 5 and divergenceY is -7, heading up should be the priority (7>5) and the array will look like up, right, left, down.
Finally, we iterate through that array, test to see if each direction in turn is possible, and if so, take it:
for i, v in ipairs(directionBestFirst) do
-- checking direction v
if v == currentDir and mandatory == true then
-- current direction, and turning due to collision, so ignore
else
-- is it possible to go that way?
if checkForOpenPath(mapRefX,mapRefY,v) then
-- yes this direction is open
return v
end
end
end
With this done, our enemy does actually head towards the player – and will quite successfully chase them, too. So – very basic enemy AI, done!
Two’s Company
When adding the first enemy I tried to make it extensible by defining the enemy as the first in an array. Time to put that to the test:
-- in the head, define two enemy starting directions:
local enemyDirections = {"l","r"}
-- and then in initEnemy:
function initEnemy(ghostTable)
-- Set up the enemy sprite
enemySprite = AnimatedSprite.new(ghostTable)
enemySprite:playAnimation()
enemySprite:setCenter(0, 0)
enemySprite:moveTo( (topLeft + (9*16)), (7*16) )
enemySprite:setCollideRect( 0, 0, 16, 16 )
enemySprite:add()
table.insert(enemySprites,enemySprite)
-- try another one as well?
enemySprite2 = AnimatedSprite.new(ghostTable)
enemySprite2:playAnimation()
enemySprite2:setCenter(0, 0)
enemySprite2:moveTo( (topLeft + (7*16)), (7*16) )
enemySprite2:setCollideRect( 0, 0, 16, 16 )
enemySprite2:add()
table.insert(enemySprites,enemySprite2)
end
Happily, this worked first time – up pops another ghost, with a slightly different starting position but the same laser-like focus in chasing down our player.
Also note in that code that I added a collision detection rectangle to each – each sprite needs one.
Game Over
And finally, what about when our enemy ghosts catch the player?
function checkCollisions()
-- v basic for now
local collisions = playerSprite:overlappingSprites()
if #collisions > 0 then
print ('died!')
return true
end
return false
end
This is called from the loop and, when it returns TRUE, the game (for now) ends. Here’s how it looks:
Next up:
- Change the player sprite, because with three ghosts in the maze this is all getting a bit silly?
- The “Ghost House” should be no-entry
- Collision detection needs a bit of work
- Lives, reset, game over states
Full code for this working demo here; my entire Playdate series is here.
* I know Lua calls them tables, but I just can’t. Sorry. Close enough!