Articles Archive
Director Forums
Director Wiki
Job Board
Search
 

Pacman-style games in Director

by Mika Tuupola

tile graphics by Kelta

A Pacman style game was one of the first thing I did when I started to learn Director. The version was 6.5. Soon Macromedia introduced Director 7 and I switched to it. Since Lingo with the new dot syntax looked more like a real programming language I rewrote a new version of the game for this article.

This article does not give you a finished game. The source is a simplified version of the game. That means all the fine tuning was left out (effects, powerups, score system etc.) so you have some brainwork to do yourself. I will try the explain all the theory and logic behind my approach. While you read this it is a good idea to open the source code of the example movie to find out how all of this was implemented. Code should be well commented.

When reading the code please note that all the globals start with a letter "g" and properties with letter "p". For example gfoo or pbar.

Download this demo for Mac or PC. This is a Director 7 movie.


Constructing the Maze

The usual way

When people do their first Pacman game in Director they tend to implement that by building the walls of maze from different sizes of square sprites. The player sprite is moved freely and checked for collisions against these walls. This kind of "free range" maze might first look like an easy solution but the truth is (in my opinion) different.

First of all it's quite hard to align the wall so that player is able to move only horizontally or vertically and not both in a hallway. The result is usually awkward movement. For example when a player presses up in a vertical hallway, moves one or two pixels up, collides with a wall and stops. This is not very playable. The enemy AI (artificial intelligence) would also be quite a pain to do.

My advice would be to forget freerange mazes.

Tile based approach

In many arcade games the graphics are still built from tiles. In fact in some systems (Nintendo Gameboy for example) everything is built from tiles. This is the approach I'll be discussing here.

The 2d table

On my approach all the logic is done on a 2d table and its coordinates. These coordinates are not to be confused with stage coordinates. The table can be virtually any size. Only the visible part is limited by the number of available sprite channels. The basis of the playfield is visualized by constructing a grid from a cast member called "emptytile". The tile corresponding to location x=1, y=1 on 2d table is in sprite channel 1; x=2, y=1 is in sprite channel 2 continuing to the last tile, x=10, y=10 in sprite channel 100.

The downside for this approach is that it needs one channel for each tile in the 2d table. The big advantage is the possibility of easily creating a level editor. This brings out many possibilities such as users creating their own levels which they can save on your server or send to their friends by email.

A primitive level editor is included in the example source. You can try it by dragging some tiles to the grid. The editor is dumb and allows you to build illegal combinations of tiles side by side (someone might call this a feature, since it enables oneway doors ;).

Gametable data is stored in global gtable. Global stores width and height of the table and all the tiles for the gametable row by row. After calling inittable() it looks like this:

put gtable
-- [#w: 10, #h: 10, #data: [
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"], 
  ["0", "0", "0", "0", "0", "0", "0", "0", "0", "0"] ] ]
  

An individual tile in gametable data can be addressed with and set with:

tile = gtable.data[y][x]
gtable.data[y][x] = tile

We obviously need to be able to convert table coordinates to spritechannels:

channel = (y-1) * gtable.w + x

And vice versa. These are a bit kludgish since first sprite channel is 1 and I used coordinates starting at (1,1) instead of (0,0). Please see channel2table() in the source:

x = channel mod gtable.w
y = channel / gtable.w + 1 

Every function dealing with the 2d gametable such as settable(), table2channel() or drawtile() are based on these formulas. Again, please see Game table in source.

The editor

The editor part is really simple. Tile builder and modified Drag Snap (which came with Director) behaviours are attached to the builder tiles. The tiles snap to all the basetiles from which the grid is built.

When builder tile is dragged over and the mouse button is released the behaviour checks which basetile it intersects (it intersects only one because Drag Snap aligns it with one tile only) and calculates basetiles gametable coordinates:

base = channel2table(chan)

Then it updates gametable data with the name of the dragged tile and draws the tile to stage:

tile = pspriteref.member.name		
settable(base.x, base.y, tile)
drawtile(base.x, base.y, tile)	

Editor can be as simple as that. The rest of the code is just cosmetics. Check Base tile and Tile builder behaviours for more.

The tiles

In this example the maze is built from 32x32 pixel tiles. I use this size because of an old habit. (Accessing tile sizes as 8x8, 16x16 or 32x32 from memory is fast in programming languages like C or Assembler. I'm pretty sure it doesn't make the Lingo code any faster though). There are 16 different tiles each having a unique combination of number and direction of possible exits from the tile. Tiles are named from letter a to letter p.

Next we construct a list which holds attributes for each direction (up, down, left, right) on each tile: 1 (or TRUE) if exit is possible and 0 (or FALSE) if the exit is blocked. These values are held in global gmoves. I chose to use rect as datatype because it just happened to have four entries. I of course have used a property list of my own. After calling initblockmoves() the list looks like this:

put gmoves
-- [ "a": rect(0, 1, 1, 1), "b": rect(1, 0, 1, 1), 
  "c": rect(1, 1, 0, 1), "d": rect(1, 1, 1, 0), 
  "e": rect(0, 0, 1, 1), "f": rect(1, 0, 0, 1), 
  "g": rect(1, 1, 0, 0), "h": rect(0, 1, 1, 0), 
  "i": rect(0, 0, 0, 1), "j": rect(1, 0, 0, 0), 
  "k": rect(0, 1, 0, 0), "l": rect(0, 0, 1, 0), 
  "m": rect(0, 1, 0, 1), "n": rect(1, 0, 1, 0), 
  "o": rect(1, 1, 1, 1), "p": rect(0, 0, 0, 0), 
  "x": rect(1, 1, 1, 1)]
  

These values are used everytime player or enemy sprite tries to move away from a tile using canigo(). For example, to check out if it's possible to go down from location x=8, y=6 we would ask:

channel = table2channel(8,6)
possible = sprite(channel).canigo(#down)

The purpose of gmoves list, how and why it is used, is one of the most important things to understand when figuring out how this game works.

Movement in gametable

All the logic happens in the gametable. If you check the Player and Enemy AI behaviours you can find the following if statement:

if (pstatus = #there) then
  ...
end if

There are two possible pstatus values; #there and #moving. If pstatus is #moving, it means the sprite is still animating between two tiles. The animation is handled by a behaviour called Animation.

Behaviours Player and Enemy AI take care of what is the next tile where a sprite goes, nothing else. They work in gametable coordinates. The Animation behaviour however works in stage coordinates. It calculates what are the stage coordinates in the current gametable location of the sprite and what are the stage coordinates in the target location where it is going. It then animates the sprite between these stage coordinates. When the animation is done it gives the control back to Player or Enemy AI behaviour to decide the next target location again.

If you have trouble visualizing this, remove the Animation behaviour from player and enemy sprites. Drop the frame rate to 3 fps and attach No Animation behaviour to both sprites again. Now you can see how the logic works behind the scenes.

Navigating the Player in the Maze

Keypress is sent to player sprite in the usual way just by calling keydown(). You can find examples of this almost from every Director primer. To make the game more playable, the player sprite stores the last key pressed in property plastkey.

Always when it is finished animating between tiles:

if (pstatus = #there) then

... the behaviour checks if its possible to turn to direction of the last keypress:

channel = table2channel(pcurrent.x, pcurrent.y)
canturn = sprite(channel).canimove(plastkey)

... and updates all needed variables. If the turn was not possible behaviour checks if it can continue to the old direction:

possible = sprite(channel).canimove(pwannago)

And again, if it was possible to continue, it updates all the needed variables. If a special case where neither movement to plastkey nor pwannago was possible the sprite just sits in the corner / deadend and waits for user input.

Check the Player behaviour for full sourcecode.

Enemy AI in maze

Crash 'n turn algorithm

Even though this algorithm is not very effective it is perfect for this example because it is one of the easiest to implement and it is adequate for pacman style games since they themselves aren't too complicated (if comparing to real-time hex based strategy games or alike).

In the most basic form this algorithm goes like this:

  1. Calculate straight line from current location to target location and try to move to the next tile in that direction.
  2. If it collides with an obstacle turn right until free passage is found and then move to the tile that direction.
  3. Repeat 1 - 2 until we have reached the target location or we have collided with previous tile.
  4. If we collided with previous tile (the tile we came from) change the direction of turning and go through same 1 - 2 loop (this time just turning to another direction)

There are some concerns you should note in here. What happens if enemy walks into dead end? Yup. The algorithm locks since it is not allowed to go back to the tile it previously came from (there is a reason for this. If it was allowed to return to previous tile, there would be situations where enemy would start to run aimlessly back and forth between two tiles). So what we need to do is to modify step 3 a bit.

  1. Repeat 1 - 2 until we have reached the target location or we have collided with previous tile and we have not collided with previous tile before in this turn.

Note: Since the player sprite is not stationary we need to calculate new direction (step 1) everytime we move to the next tile.

The behaviour first finds the direction to the player using finddirection() function. I made a shortcut here and didn't implement any real linedrawing algorithm. Instead it compares difference between x and y coordinates between enemy and player (target) sprites:

xdiff = pcurrent.x - sprite(target).pcurrent.x
ydiff = pcurrent.y - sprite(target).pcurrent.y

Then, whichever is bigger, the function returns a direction depending whether the difference is positive or negative. For example if abs(ydiff) is bigger than abs(xdiff) and ydiff is negative (player y is bigger than enemy y) the direction to go is down.

The behaviour then tries to move where finddirection() told it to. If that was not possible:

if ((not possible) or (previous)) then
  ...
end if

It jumps into the mainloop:

repeat while ((not possible) or (previous))
  ...
end repeat

The code then turns where the enemy is heading. It also updates the needed variables and makes the possible / previous checks again. If it collides with the previous tile and it is the first time it collides with it, the behaviour just changes the direction of turning. If it collides with previous tile second time, the behaviour notices that it is in a dead end and forces the enemy to make a u-turn.

When the loop has exited and the new direction is found, pstatus is updated again to #moving and animation behaviour takes control.

Homework

As I said, this is not a finished game. However, it should be enough to help anyone finish their own Pacman game. A few things just need to be added: the eatable dots, powerups, more enemies etc.

The level editor can be improved quite a lot. Autojunctioning would be a nice feature. It also shouldn't allow impossible combinations of tiles.

You should also make different kind of behaviour models for the enemy AI to make the game more interesting. Here are some possible ideas:

  1. Waypoint patroling. Enemies just patrol between some predefined coordinates.
  2. Bloodhounds. Enemies just wonder aimlessly until they get a 'scent' (get close enough) to the player and then chase player at double speed.
  3. Roadblockers. They calculate location of the next junction in maze where player is heading and move there trying to 'block' the road and thus effectively trapping the player.
  4. Any combination of the above (1+2 is quite effective)

Have fun! If you use this, drop me an e-mail.


Originally coming from a Unix administration / programming background, Mika currently works as a technical designer for Taivas HEL Oy, located in Helsinki, Finland. HEL's clients include MTV Europe, Diesel s.p.a., Sprite and Radiolinja (a major Finnish telecom operator). They are also known for HEL13, their own design magazine.

(This article has been viewed 12040 times.)