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:
- the player's icon;
- the current number of lives; and
- 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:
- allow the user to select a player icon;
- display and increasing and decreasing number of lives;
- display an increasing and decreasing score; and
- 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:
- determine the width of the resulting image;
- determine the height of the resulting image;
- create a new image using these dimensions; and
- 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:
- the width of the final image is either the width of the target or the attach image, depending on which is greater;
- the height of the resulting image is the sum of both heights;
- first the target image is copied; and
- 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
- pScore
The current score - pLives
The current number of lives - pActive
A boolean variable to determine if the player data is drawn normally, or slightly opaque to indicate that the player is inactive - pPlayerName
The name of the player graphic that the user selected - pSpacer
A blank image used to easily space out the graphics as they are drawn
Methods
- new me
Initalise the object - selectPlayer me, playerName
Used to indicate to the object which player graphic was selected by the user - toggleState me
Toggle the pActive variable to allow toggling the state of the player - addScore me, aValue
Send a positive or negative number to increase or decrease the score - addLife me, aValue
Send a positive or negative number to increase or decrease the number of lives - draw me
Append the graphics together to draw the player game data
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:
- initialise the player object (this sets the default score and number of lives);
- the user selects a player icon;
- this name is sent to the player; and
- 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)
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.
Copyright 1997-2024, Director Online. Article content copyright by respective authors.