Articles Archive
Articles Search
Director Wiki
 

Building game displays

November 28, 2000
by Zac Belado

For all its other practical benefits, one of the more understated uses of Imaging Lingo is to compress your multi-sprite game and project data displays into a single sprite. For instance, take a typical gaming situation in which the player has an icon representing their player, a series of icons to represent the number of lives they have available, and the player's current score. Typically, creating this display would have entailed three or more sprites, but by using a few Imaging Lingo tricks you can assemble these different elements into a single sprite.

Bit by bit

Let's examine the steps needed to build a specific example.

This score system has three separate sections:

  1. the player's icon;
  2. the current number of lives; and
  3. the score.

Each part of this display system is relatively easy to assemble. The icon is simply a single bitmap; the graphic representing the number of lives is just the same icon repeated N times; and the score is the image of a Text member. Once these elements have been independently assembled they can be "stacked" above each other to create the vertical display above.

So, we will want to build in 4 basic functions:

  1. allow the user to select a player icon;
  2. display and increasing and decreasing number of lives;
  3. display an increasing and decreasing score; and
  4. allow the display to be grayed out to indicate that the player is inactive.

If you use multiple sprites for this type of display, you have several problems. The most vexing of these is that you need to co-ordinate the appearance of each of the elements. If you want to gray out the display, then you need to ensure that each sprite is grayed out. And once the player becomes active you need to repeat this process to ensure that the sprites are now "active". This complicates your movie and also adds a number of areas for possible bugs.

Putting things on top of other things.

The fundamental part of this process is the ability to "stack" images above other images. You need to take each element and add the next piece to the bottom, or right, of the previous image. So, the first thing you need to write is a small function to handle attaching these images to each other.

Being the completists that we all are (or at least pretend to be during employee evaluations), this function should be general enough to handle attaching images to all four of the basic directions. To be precise, it should handle appending image data to the top, bottom, left and right of another image.

Note that in this function (and in the rest of the article) the phrase "target image" refers to the base image, or the image that the new data will be added to; and the phrase "attach image" refers to the image data that will be added to the "target image".

Here is the complete function.

on os_attachImages target, attach, colorDepth, position

  if voidP(target) OR voidP(attach) then exit
  if target.ilk <> #image OR attach.ilk <> #image then exit
  if voidP(colorDepth) then colorDepth = 16
  if voidP(position) then position = #right

  case (position) of
      
    #left:
      
      w = target.width + attach.width
      h = max(target.height, attach.height)
      final = image(w, h, colorDepth)
      
      final.copyPixels(attach, attach.rect, attach.rect)
      
      destRect = offset(target.rect, attach.width, 0)
      final.copyPixels(target, destRect, target.rect)
      
    #right:
      
      w = target.width + attach.width
      h = max(target.height, attach.height)
      final = image(w, h, colorDepth)
      
      final.copyPixels(target, target.rect, target.rect)
      
      destRect = offset(attach.rect, target.width, 0)
      final.copyPixels(attach, destRect, attach.rect)
      
    #bottom:
      
      w = max (target.width, attach.width)
      h = target.height + attach.height
      final = image(w, h, colorDepth)
      
      final.copyPixels(target, target.rect, target.rect)
      
      destRect = offset(attach.rect, 0, target.height)
      final.copyPixels(attach, destRect, attach.rect)
      
    #top:
      
      w = max (target.width, attach.width)
      h = target.height + attach.height
      final = image(w, h, colorDepth)
      
      final.copyPixels(attach, attach.rect, attach.rect)
      
      destRect = offset(target.rect, 0, attach.height)
      final.copyPixels(target, destRect, target.rect)
      
  end case

  return final

end

(Thanks to Andy White for his help with this very handy function.)

The function has 4 basic steps:

  1. determine the width of the resulting image;
  2. determine the height of the resulting image;
  3. create a new image using these dimensions; and
  4. copy the target image and the image to attach to the new image.

The function takes four parameters: a target image; an image that the target will be attached to; the colorDepth of the new image; and the relative position that the target is going to be attached to, which will be #left, #right, #top or #bottom.

Determining the various parameters and order of execution for the function depends on the position at which the attach image will be added to the target image. For example, if the new image will be attached to the bottom of the target image, then:

  1. the width of the final image is either the width of the target or the attach image, depending on which is greater;
  2. the height of the resulting image is the sum of both heights;
  3. first the target image is copied; and
  4. the attach image is then copied.

This means that if you appended a 170 x 40 pixel image to a 70 x 70 pixel image, the final image combining the two would be 170 x 110 pixels.

This process would be different if the second image were being attached to the right of the target image. Using the same images as the example above would result in an image 240 pixels wide and 110 pixels high.

Piece by piece.

With that function in hand, you can now proceed to write some code that will handle displaying the player data as outlined above. To make the code more useful, you should probably incorporate all the code into a single behavior or object. It is entirely possible to build all the required handlers into a behavior that you could then drop onto a bitmap sprite, but the steps I outline below will use an object to do this.

There are two reasons for this. First, it allows you to create an instance of the player data object and address it (send it data or modify its properties) even if it isn't on the Stage. Second, using an object allows you to easily transfer the target sprite onto which the object is drawing more easily than if you had used a behavior.

Let's define some of the functionality you'll need to add to this object.

Properties

Methods

Before we get into the particulars of creating this object and the supporting code it requires, why don't you take a second to view the code in action.

A sample Director 8 movie is available for download in Mac or Windows format.

All the steps that are fit to print

So, the process of selecting a player graphic and drawing the initial state of the player game data are as follows:

  1. initialise the player object (this sets the default score and number of lives);
  2. the user selects a player icon;
  3. this name is sent to the player; and
  4. draw the default graphic.

Step one is handled in the start movie or prepareMovie handler.

global gPlayer

on startMovie
  gPlayer = new (script "player object")
end

A global variable named gPlayer is used to store a reference to the object we create. This Lingo code calls the new method of the player object.

on new me

  pScore = 0
  pLives = 3
  pActive = TRUE
  canvas = VOID

  -- store an image to allow the behavior to add
  -- space between the graphics in the display
  pSpacer = image (100,8,8)

  -- reset the score
  member("score").text = string (pScore)

  return me

end

The new method sets the values for the default number of lives and score, and creates and stores the spacer image that will be used later. The score is actually stored in a Text member, and the new handler resets the value of the text in that member to the default score.

Step two is handled in a behavior that is attached to each of the player icons. When the sample movie starts, it displays six player icons from which the user can choose. Each of these icons has the following behavior attached to it.

global gPlayer
property pMyName

on beginSprite me
  pMyName = sprite(me.spriteNum).member.name
end


on mouseUp

  if rollover (the clickOn) then
    gPlayer.selectPlayer(pMyName)
  end if

end

As an expedient, the sample code simply takes the name of the player from the name of the sprite's member to which it is attached. Once the sprite has been clicked on, it sends this name to the player object via the selectPlayer method of the player object.

on selectPlayer me, playerName

  -- called by the behavior attached to the player icons
  if voidP(playerName) then exit

  -- store the name and then draw the base image
  pPlayerName = playerName
  me.draw()

  go frame "main"

end

The player object stores this name and then draws the default graphic. The game data is drawn in four steps.

First, the image of the player is stored:

on draw me

  -- draw the player
  canvas = member(pPlayerName).image

Next, the graphic depicting the current number of lives is created:

-- draw the lives
  livesImage = duplicate(member("extra life").image)
  baseImage = duplicate(livesImage)
  if pLives > 1 then
    
    repeat with index = 2 to pLives
      baseImage = os_attachImages (livesImage, baseImage, 8, #right)
    end repeat
    
  end if

This is done by taking the "extra life" graphic and, if there is more than one life, attaching it to itself (on the right side) once for each additional life.

-- add a spacer
  canvas = os_attachImages (canvas, pSpacer, 8, #bottom)

  -- now attach the lives
  canvas = os_attachImages (canvas, baseImage, 8, #bottom)
  baseImage = VOID
  livesImage = VOID

A spacer is appended to the bottom of the player graphic, and then the lives graphic is appended to the bottom of this new image.

Then the score is added by taking the image of the Text member and appending to the canvas image we initially created.

-- add a spacer
  canvas = os_attachImages (canvas, pSpacer, 8, #bottom)

  -- draw the score
  attachImage = member("score").image
  canvas = os_attachImages (canvas, attachImage, 8, #bottom)
  attachImage = VOID

Finally, if the player is inactive (pActive is FALSE), then the entire graphic is grayed out by creating a new white image and copying that image over the image we just created, but using the blendLevel property to make it opaque.

-- is this player inactive?
  if NOT pActive then
    
    -- make the fill image
    fillImage = image (canvas.width, canvas.height, 8)
    fillImage.fill(fillImage.rect, rgb(255,255,255))
    
    
    -- now copy it over at 65% opacity
    fillImage.copyPixels (canvas, canvas.rect, canvas.rect, [blendLevel:65])
    canvas = fillImage
    
    fillImage = VOID
    
  end if

Once that is done, this newly created image is assigned to the image property of the member on the Stage.

member("base").image = canvas
  member("base").regpoint = point(0,0)

end

A few details

The code in the os_attachImages function is externalized from the object for a few reasons. First, it is a very useful function that can be used in other projects. As well, keeping it external from the main code simplifies the player object. There are a few things that the function doesn't do. It always assumes that you want to align images to the top (in the case of #left and #right directions) or to the left (in the case of the #top and bottom directions). It would be more useful if the function allowed you to specify an alignment.

The function also doesn't handle any other directions other than #top, #bottom, #left and #right. It might be useful to be able to attach images on diagonals as well, so may be a feature to add in the future.

The player object also artificially controls the score and number of lives in order to ensure that the graphic it creates remains on the Stage. In an actual game, you would have to do some calculations to ensure that the display remains on screen.

Neither of these additions should be hard to add if the situation arises.

Zac Belado is a programmer, web developer and rehabilitated ex-designer based in Vancouver, British Columbia. He currently works as an Application Developer for a Vancouver software company. His primary focus is web applications built using ColdFusion. He has been involved in multimedia and web-based development, producing work for clients such as Levi Straus, Motorola and Adobe Systems. As well, he has written for the Macromedia Users Journal and been a featured speaker at the Macromedia Users Convention.

Copyright 1997-2024, Director Online. Article content copyright by respective authors.