HZ: Defining Sprites

HZ uses version 3.1 of the free Lua script language, developed at Tecgraf at the University PUC-RIO in Rio de Janeiro. All aspects of a sprite, from it's visual representation, to it's physics, user-control behavior, damage, and anything else one decides to throw in there, are all defined by writing script code in Lua. This simple documentation explains how you can define your own sprite and how to write some of your own sprite script code. At some point, you should read through the Lua 3.1 Programmming Manual to become comfortable with the Lua script language, but you should be able to make it through this sprite documentation before doing so.

If you want to play around with concepts as you learn them, you can skip to the bottom and follow the instructions for "Creating Your Own Sprite". You'll then have a test-bed object to work with as you read the docs.

0. Introduction

The are three main jobs of the object script code.
  1. Defining the visual representation of a sprite
  2. Handling the sprite's physics and other properties
  3. Handling the sprites AI (or user-control)
When the game is started, the lua init file init.lua is loaded and run. It in turn loads other files, using the dofile() function, until all lua files have been loaded and run. During the init process, all sprite types necessary for the game must register themselves. When they are registered, their script-code and visual representations are stored away for use later when sprites of that type are created and placed onto the map. As soon as the init process has completed and lua has returned control to the main game engine, the game begins.

1. Visual Representation

Inside every script object there must be a 'VisualRep' which defines what images the sprite will use and when. Inside a visual rep you can have an arbitrary tree of images. Those images are divided into nested imagesets. Below is an example VisualRep definition. It is for a bullet with four frames of animation. In this case the bullet images have no inherent direction. We merely want to animate the frames.

-- Double dash is used to make comments
-- Below is a VisualRep definition

VisualRep = { { "bullet01.bmp" },
              { "bullet02.bmp" },   
              { "bullet03.bmp" },    
              { "bullet04.bmp" };
              IndexedBy = "image_frame"
            }
This VisualRep only has one imageset. That image set contains four image filenames. You can see those four filenames listed first. Pay attention to the punctuation. Note that each image is surrounded in braces, and separated by a comma from the next image. Notice that the last image in the imageset has a semicolon, separating it from the next section.

NOTE: In the current source and binary releases, there are four zeros listed after each of the images in the visrep.lua tables. This is vistigial, has no purpose, and has thus been removed. With these older versions you can remove them without harm.

After the imageset, the line with the 'IndexedBy' keyword tells the game engine what object variable holds the index into the animation array. In this case, we've said that the name of the object property variable is "image_frame". Before this sprite is drawn, the game engine will read the value of the script object's "image_frame" property to figure out which image to be drawn. If there is no "image_frame" property, or the property has an invalid value, the first image in the imageset will be drawn. imagesets such as the one above are numerically indexed starting at 1. To get the last image, the "image_frame" property should be set to "4".

You can see an example which works just like this in HZ by looking at the 'Flag' in 'lua\obj_flg.lua' and 'lua\visrep.lua'. All of the game's VisualRep definisions are stored in visrep.lua. Other files merely reference these visualreps. In obj_flg.lua, you can see the code for the simple flag. The new() function is called each time a new flag is created in the game. The doTick() function is called every game frame. You can see that the property used to index this object's imageset is called flagimg.

Multi-level imagesets

Most objects are more complex that just four frames of animation. Fortunatly, the VisualRep structure allows an arbitrary set of nested imagesets. When nesting imagesets, the IndexedBy keyword tells the game engine which script object property to use to choose been two different imagesets, just as it tells the engine which property will choose between different images.

In the example below, we have a bullet which can be red or white.

VisualRep = { 
              Red = {  {"red_bullet0.bmp"},
                       {"red_bullet1.bmp"},
                       {"red_bullet2.bmp"},
                       {"red_bullet3.bmp"};
                       IndexedBy = "image_frame"
                    },
              White = { {"white_bullet0.bmp"},
                        {"white_bullet1.bmp"},
                        {"white_bullet2.bmp"}, 
                        {"white_bullet3.bmp"};
                        IndexedBy = "image_frame"
                    },
              IndexedBy = "Color"
    } 
Now, lets dissect this, the 'IndexedBy = "Color"' tells the engine that it should look for an object variable named 'Color' to figure out which of the two trees it should descend. If the Color script variable is equal to the string "Red" then it will use the top list, if it's equal to the string "White" then it'll use the bottom list. If it's not-present or neither of those, it will default to the top list (i.e. the red list).

A Bit About Punctuation

You may notice that there is a ";" at the end of the line before the first two IndexedBy, but not before the last one. This is because a semicolon is only used to separate un-named elements from named elements. When we list out the images in an imageset, we normally leave them without names, and they are assigned numbers starting with "1". In our example above "Red", "White", and "IndexedBy" are all of the top level index names. The imageset named "Red" lists no names for the images, and thus it is given 'implicit' numeric indicies starting at "1". Whenever you use unamed indicies, then they must be BEFORE named indicies, and the two sections must be separated by a semicolon.

If we wanted to use all named indicies, we could have instead done:

VisualRep = { 
              Red = {  one =    {"red_bullet0.bmp"},
                       two =    {"red_bullet1.bmp"},
                       three =  {"red_bullet2.bmp"},
                       four =   {"red_bullet3.bmp"},
                       IndexedBy = "image_frame"
                    },
              White = { one =   {"white_bullet0.bmp"},
                        two =   {"white_bullet1.bmp"},
                        three = {"white_bullet2.bmp"}, 
                        four =  {"white_bullet3.bmp"},
                        IndexedBy = "image_frame"
                    },
              IndexedBy = "Color"
    } 
Notice that there are no more semicolons (";") in the array lists. This is because there is no need to separate the unnamed elements from the named elements. Because names are used, the script object property 'image_frame' should now contain the string values "one", "two", "three", or "four" to properly select the associated images.

Special Indicies: @Layer

There are special IndexedBy values which are not interpreted as script variable names. One of those special values is '@Layer'. This tells the sprite engine to render ALL layers listed, instead of choosing between them. For example, we could render both the red and white bullets over the top of eachother by changing the above to:

VisualRep = { 
              L1 = {   one =   {"red_bullet0.bmp"},
                       two =   {"red_bullet1.bmp"},
                       three = {"red_bullet2.bmp"},
                       four =  {"red_bullet3.bmp"},
                       IndexedBy = "image_frame"
                    },
              L2 = {    one =   {"white_bullet0.bmp"},
                        two =   {"white_bullet1.bmp"},
                        three = {"white_bullet2.bmp"}, 
                        four =  {"white_bullet3.bmp"},
                        IndexedBy = "image_frame"
                    },
              IndexedBy = "@Layer"
    } 
When you use @Layer, it is important that you label your layers appropriately. They must be named with "L" followed by a number. In the above case, there are two layers 'L1' and 'L2'. The layers will be rendered with the lowest numbered layer on top. So in the above case, the red bullet will be on top of the white bullet.

You can see an example like this in 'lua\visrep.lua' if you look at the helicopter visual rep. (VisualRepHeli)

2. Object Physics and other Properties

In order for objects to do anything useful, and choose between all those images you've stuck into the VisualRep, they are going to need some script logic. All sprite logic is written in the Lua programming language. Because Lua is an embedded script language, there are many parts of Lua which you can access interactively while the game is running. You may want to take a look at the Lua Game Console docs to get an idea of how to access your sprites once they are created.

There are three ways each script object is given a chance to run script code:

  1. every frame, your doTick(tick_diff) method is called
  2. your ge_collision(x,y,whoIhit) method is called for each object you are currently colliding with
  3. If your sprite is being controlled by the end-user, your keyUp(key_code), keyDown(key_code), and inputEvent(ascii_key) events will be called whenever keys are pressed or released.
doTick(tick_diff)

In between the rendering of each frame, the game gives each sprite a chance to execute some script code to react to the world. It does this by calling each sprite's doTick(tick_diff method. Inside this method, you should place code which handles the object physics, decides what images to show and sets the appropriate object properties, and anything else you want to have the object do automagically. You should return from your doTick() as soon as possible, because if you spend too long in there, you'll slow down the game. Because your VisualRep is looking at script object properties, you can make any properties you want, and make your code set those properties to any values, for any reasons. However, in order to make the game engine fast, there are certain things which must be handled by the C++ part of the game engine. The special C functions available to operate on objects are:

          C_obj_delete(objnum);
          C_obj_viewFollow(objnum);
  vx,vy = C_obj_getVelocity(objnum);
          C_obj_setVelocity(objnum,vx,vy);
  x,y   = C_obj_getPos(objnum);
          C_obj_setPos(objnum,x,y);
          C_obj_setLayer(objnum,layer_number);
Notice that they all take objnum as their first paramater. objnum is a special property of all script objects which identify them to the C++ game engine. Therefore, if you want to make your object's doTick() method set the object's position to (10,10), inside your object definition you would write:
    doTick = function(self,tick_diff)
      C_obj_setPos(self.objnum,10,10);
    end
NOTE: The use of these C_obj_* functions is a bit nasty. I am considering changing them to be methods on the object, in which case you would merely write: self:C_setPos(10,10). The "C_" is to remind you that these functions are part of the base game engine functionality.

Also notice that C_obj_getVelocity() and C_obj_getPos() each return two arguments. Convinently, Lua supports multiple return arguments from a function. To move yourself two map pixels to the right of your current position, you would write:

    doTick = function(self,tick_diff)
      local xpos,ypos;

      xpos,ypos = C_obj_getPos(self.objnum);
      xpos = xpos + 2;

      C_obj_setPos(self.objnum,xpos,ypos);
    end
   
Normally, it's good to make use of the velocity functions. This allows the C++ game engine to do your range checking math and apply the velocity to your position automatically. However, there are times where you want to move a sprite to a specific position on the map. You will see an example like this in the next section.

ge_collision(x,y,whoIhit)

Whenever your sprite has been found to collide with another sprite, you're sprite's ge_collision() method will be called. This is used to handle bullet's hitting objects and doing damage, figuring out when you are flying over another object, or just about anything which involves two objects touching eachother.

The exact point where the collision occured is provided in the x and y paramaters. Sometimes collisions will occur at more than one point, if that's the case, it will be the center of the collisions. In the case of a bullet striking another object, this position is probably where you want to put the explosion.

The object you hit is provided in the whoIhit paramater. This is the Lua script object. You can call any method on that object which you know to be available. You can read or write that object's properties like they are your own. You can store the pointer to this object in a property in your own object for later use. You can even call C api functions by providing whoIhit.objnum as the object to act on instead of your own objnum. Here is an example method which moves any object you collide with five pixels to the right:

    ge_collision = function(self,x,y,whoIhit)
      local xpos,ypos;

      xpos,ypos = C_obj_getPos(whoIhit.objnum);
      xpos = xpos + 5;

      C_obj_setPos(whoIhit.objnum,xpos,ypos);
    end

keyUp(key_code)
keyDown(key_code)
inputEvent(ascii_key)

These methods are used to accept key presses from the user. They are only called when the current sprite is the one being followed by the main view. You can set which object is followed by the main view (and which gets keyboard events) by using the C_obj_viewFollow(objnum) game api function.

keyDown() and keyUp() provide raw access to the keyboard. Using these event methods you can track which keys are currently being pressed, and thus know exactly which keys are being held down. The key information is currently provided in the native keymap format. [[ unfortunatly, this differs from platform to platform. We need to make the game engine code map into a standard keyboard map, and make symbols for that keyboard map available to Lua... ]]

inputEvent(ascii_key) is a convinence function provided to give you post-processed keyboard information. The ascii_key code of the key is provided. Because this is a post-processed key, it also delivers on events such as key-repeat.

For most game-oriented control, you will use keyUp/keyDown and ignore inputEvent completely. If you wanted to receive a user's typing, such as for a chat window, you would use inputEvent. As an example of how these events are delivered, if a user presses and holds the "r" key for two seconds, depending on his key repeat rate, the following events would be called:

    keyDown(r_key_code);
    inputEvent("r");
    -- pause for key-repeat to kick in
    inputEvent("r");
    inputEvent("r"); 
    inputEvent("r");
    inputEvent("r");
    keyUp(r_key_code);

3. Object AI and User-Control

By combinging the above events, and by storing properties inside your object, you can have your object respond to user control, or make complex decisions about how it should act on it's own. [[TODO: we need to make API calls for sprites to: (a) access tile objects, (b) get a list of objects near them on the map, (c) compute shortest paths between two points, avoiding obstacles ]]

Tutorial A. Creating Your Own Sprite

There are a few other things you need to do to create your own sprite and see it in the world, so here I'll walk you through the steps.
  1. Copy the obj_flg.lua file to obj_mysprite.lua
  2. Open obj_mysprite.lua.
  3. Change the "flag" paramater of the hz_register_objtype() call to "mysprite" instead.
  4. Change the line VisualRep = VisualReps.Flag to say VisualRep = mysprite_visualrep
  5. above the call to hz_register_objtype(), add the following VisualRep to get you started:
       mysprite_visualrep = { { "img/redflag0.bmp" },
                              { "img/redflag1.bmp" };
                              IndexedBy = "flagimg" };
         
    Notice that I named it mysprite_visualrep because that's the variable name your object is putting into it's VisualRep property.
  6. Open objects.lua
  7. Right below the line which says dofile("obj_flg.lua");, add your own line which says dofile("obj_mysprite.lua");.
  8. Just below that inside teh setupGameSprites() function, you will see several calls to C_addsprite(). Add a call at the bottom which looks like: C_addsprite("mysprite",200,200);
  9. Run the game!
You should see a flag at x=200,y=200 which does not wave exactly right. That's because the script code is setting flagimg to the values 1-4, but there are only two images. Remember that when a script object property is set to an invalid value, it draws the first image in the imageset. You should be able to use what you know about VisualReps to include your own images into this VisualRep.

For more information about how to program in the script language Lua, you should consult the online Lua v3.1 documentation


David Jeske (jeske@chat.net) -- HZ Game Engine Documentation
Copyright (C) 1998 by David W. Jeske