Yes, you read it right. This is the Scripting Docutorial. Hopefully it fulfills the needs of people looking for both scripting documentation and tutorials. Unlike the scripting reference, this document takes you through real-world applications of various aspects of GameDev scripting.
The Scrolling Game Development Kit supports customization in multiple ways. This document describes the use of VBscript to control the execution of a game. This document may skim over some of the specifics of VBScript syntax. In-depth VBScript documentation is available at Microsoft's website. It's important to note that, simply because this is a "tutorial" doesn't mean it's necessarily going to be easy. Scripting is a form of programming, and while I will try to cover a few of the basics of programming and VBScript in this document, I don't expect this to be a comprehensive manual for scripting in general (I intend to document GameDev, not VBScript). You will need to have some understanding of how to program or read VBScript before you can fully utilize this document. So I suggest referring to Microsoft's VBScript documentation if you're not already a programmer. This document is designed for those who have exhausted GameDev's built-in functionality and are hungry and ready for more advanced topics, so be prepared to invest a little time and effort into understanding how scripting works -- it might get complicated.
With that said, I would like this document to be as accessible as possible to those just entering the world of programming, so if there's something that's just a bit unclear and this document could be greatly improved by making some small adustment here or an addition there, let me know. I do try to keep up with all the questions people may have about this program, and I welcome input, so email me with any questions and suggestions you may have about GameDev scripting or GameDev in general: BlueMonkMN@email.com
The vast majority of GameDev scripts are based on a similar basic framework.
|
||
Figure 1: Basic Scripting Framework |
OnControllerMove represents one of many events that could be handled by the script. Let's step through each line of this framework script to familiarize ourselves with some terminology and basic concepts:
Event
GameDev communicates with VBScript code primarily by raising events. Every object (such as the player object) has a set of events that it can raise. The set of events for any particular type of object is constant and can be found in the scripting reference. The player object raises the most commonly used events. For example, this list is taken from the scripting reference:
Next we'll look at the first line of code, which traps the OnControllerMove event.
Sub Player_OnControllerMove(OldActions,
NewActions)
VBScript traps events by specifying subroutine names begining with an object name whose events will be trapped. on this line, we are trapping the OnControllerMove event of the player object. In order for this to work, the subroutine must accept the paramaters that are passed by the OnControllerMove event. If you don't exactly match the event's signature, VBScript will not properly trap the event. The word signature refers to the subroutine name and the number of paramaters. Also, it must be defined as a sub rather than a function.
'Your code here
The code within this sub will execute whenever the player object raises the OnControllerMove event. In this particular case, the code will execute whenever the player changes direction or presses a button on the input controller. The paramaters will indicate the old and new state of the input controller respectively. Different events will trigger different code to execute at different times as appropriate.
End Sub
This denotes the end of the subroutine. For any given event handler, execution of the event's code ends here and returns control to GameDev.
HostObj.SinkObjectEvents
ProjectObj.GamePlayer, "Player"
This line of code does not
occur within any event handler (it is global in scope). Since
this is the first such line of code in this script, it will be the first code
to execute. When GameDev initially loads the script, all subroutines are parsed
into memory and global code is executed on the spot. This particular line of
code associates GameDev's player object (ProjectObj.GamePlayer
)
with the name "Player". This determines the name that will be used for all
event handlers trapping events from the player object (ProjectObj.GamePlayer
).
HostObj.ConnectEventsNow()
This line of code indicates to GameDev that all subroutines for all objects have been processed and should now be connected. Any code after this cannot be used to trap events.
ProjectObj.GamePlayer.Play 16
Because GameDev has no hard-coded action when loading a game to play with script (the /p switch), the script has the responsibility of starting the game-playing process itself. (Occasionally, one might wish to write a script that simply performs some processing on a project without actually playing it.)
The scripting wizard can be used to easily create scripts that allow the player to shoot. However, closer examination of the generated script can be very instructive. So, in this chapter we will walk through the functionality of a generated shooting script. The following paramaters were passed to the scripting wizard (a program that can be run from GameDev's tools menu):
Figure 2: Wizard paramaters for a simple shooting script
This yields the following code:
|
||
Figure 3: A simple shooting script |
Dim nExpectCount
Dim nShot0Count
Dim arBtn0Shots(4)
The first line we will get to later. The second line declares a variable that will store the number of bullets currently maintained by the script. The third line declares an array where each element tracks one of the bullets. Variables in VBScript can be used for a variety of purposes. I use the prefix n to suggest a variable containing a number and ar to suggest an array. In this case, the array contains variables that refer to GameDev objects. These will make more sense as we get into the code that uses them.
Sub Player_OnControllerMove(OldActions, NewActions)
If (Not OldActions) And NewActions And ACTION_BUTTON1 Then DoFireButton0
End Sub
The parameters passed to the OnControllerMove event are bit maps. That means the individual bits in their numeric values are used to store seperate pieces of information. These are used frequently in GameDev. This is how they work:
Decimal value 11:
Remaining Decimal Value | Remainder after divide by 2 |
11 | 1 |
5 | 1 |
2 | 0 |
1 | 1 |
11/2 = 5 remainder 1
5/2 = 2 remainder 1
2/2 = 1 remainder 0
1/2 = 0 remainder 1
The last remainder represents the first bit in the binary result. The full binary value of eleven is 1011. Now let's convert it back to decimal.
Binary value 1011:
Bit | Place Value | Bit * Place Value | Running Sum |
1 | 1 | 1 | 1 |
1 | 2 | 2 | 3 |
0 | 4 | 0 | 3 |
1 | 8 | 8 | 11 |
Figure 5: Conversion from binary to decimal
1 * 1
+ 1 * 2
+ 0 * 4
+ 1 * 8
= 11
As you can see, eleven can be represented in four bits. A four-bit number can represent values up to decimal 15. Now let's understand how to use masking to extract a single bit. Recall that we used the and operator to do this. The definition of the and operator indicates that the output will only be true when both inputs are true. For instance, true and false will return false. When applied to numeric values, each bit is seperately calculated according to these rules. For example, binary 10 and 11 will return binary 10. In decimal, the expression 2 and 3 will return 2. Now let's assume we want to determine if the highest bit in a four-bit value is set to one.
Masking the high bit from a four-bit number:
Mask = 1000 (only
the high bit is set)
11 and 8 = 8
(1011 and 1000 = 1000)
As you can see, the high bit is set. If it were not set, the value resulting from the and operation would be 0.
The parameters passed to OnControllerMove are eight-bit values. Four bits are used to represent the four directions on the input device, and the other four bits represent the buttons being pressed. Instead of having to remember what order the bits are in, each mask has been given a name as described in the scripting reference. If you look under the eActionBits enumeration, you can see that the names for these masks are as follows:
So, the code in
OnControllerMove is using two variations of this technique to determine if the
NewActions
parameter indicates that the player is now
pressing button 1 and the OldActions parameter indicates that it was not being
pressed before. (Both NewActions
and OldActions
parameters are bit maps representing the bits from eActionsBits
enumeration.) If it determines that the button has just been pressed, it calles
a subroutine to handle the button press.
If you didn't understand the beginning of the bit masking section the first time through, go back and read it now for better understanding. You might understand it.
Sub RecountShots()
Dim nSIdx, nLIdx
In addition to the event handling subroutines, you can define your own subs that can be called from within other subs. This is in line with standard VBScript syntax and is used a number of times in the code generated by the scripting wizard. Variables declared within a sub are local. This means that they can only be accessed within the sub.
The RecountShots sub is used to update the number of shots currently active. A shooting script utilizes this to maintain the internal array of bullets after it sees that some have disappeared.
With ProjectObj.GamePlayer.PlayerSprite.rDef.rLayer
...
End With
GameDev has large and deep hierarchies of objects to support the complex structure of the games it maintains. The with statement is particularily useful here to avoid many long lines of code. This particular with statement locates the layer on which the player sprite resides and makes this object the current context. It is then possible to access the methods and properties of this layer object by simply preceeding the method or property name with a period.
nSIdx = 0
Do While nSIdx < nShot0Count
For nLIdx = 0 to .SpriteCount - 1
If arBtn0Shots(nSIdx) Is .Sprite(nLIdx) Then
Exit For
End If
Next
Scripts can maintain large
private stores of data by putting this data in array variables. In GameDev
scripting, arrays of objects can be particularily useful. The Scripting Wizard
keeps an array of all the bullet sprites so that it doesn't need to loop
through all active sprites when checking to see if a bullet should
disappear. It is important to realize when looping through an array that a For
loop will not adapt its upper bound if it changes. This is why the outer loop
in this code uses a While
loop instead of a For
loop.
This allows nShot0Count
to change within the loop, and
be certain that nSIdx
remains within appropriate
boundaries.
The Is
keyword is
used to compare variables to see if they refer to the same object. The code
above searches through the array of active sprites to see which bullets from
the internal array still exist as active sprites.
If nLIdx >= .SpriteCount Then
Set arBtn0Shots(nSIdx) = arBtn0Shots(nShot0Count - 1)
nShot0Count = nShot0Count - 1
Else
nSIdx = nSIdx + 1
End If
Loop
nExpectCount = .SpriteCount
In order to store objects in
script variables, the Set
statement is used to assign a reference.
Objects remain in existence as long as at least one reference exists. This
includes references by script code or by GameDev internal code. For instance,
clearing the entire contents of the arBtn0Shots
array
will only free those objects which are not still in use by GameDev. Also,
removing all the sprites from a layer and leaving the script's array intact
will leave sprite objects in memory. The above code removes from the script's
array any bullet sprites that are no longer part of GameDev's internal sprite
array for the player sprite's layer.
nExpectCount
is used to determine when the number of GameDev internal sprites has changed.
This also indicates when the script should check to see if the bullet sprites
have changed, that is, if any bullets have been removed. Rather than stepping
through each of the sprites for every frame, the script only checks for removed
bullets when the number of active sprites has changed.
Sub RemoveShot(Spr)
Dim nIdx
With ProjectObj.GamePlayer.PlayerSprite.rDef.rLayer
For nIdx = 0 to .SpriteCount - 1
If .Sprite(nIdx) Is Spr Then .RemoveSprite(nIdx) : Exit Sub
Next
End With
End Sub
There are two main aspects to
understand in scripting in GameDev. The VBScript syntax defines the basic
language features and is already documented by Microsoft at
their website. The objects, properties, and methods implemented by
GameDev are of more interest here. The quick reference provides a short
description of everything available in GameDev, but does not provide any
detailed documentation on how the pieces work together. The remainder of this
docutorial will attempt to focus on that aspect. In the code above, notice the
RemoveSprite
method call. This removes an active sprite from a layer
based on the sprite's index. It is used in the generated script to remove
bullets according to the conditions specified in the wizard. Because it is more
useful to remove a sprite based on the sprite object itself, this function is
created to search through the array of active sprites for the specified sprite
and then remove it based on its index.
Function GetStateDeltas(DX, DY)
With ProjectObj.GamePlayer.PlayerSprite
Select Case .rDef.Template.StateType
Case STATE_SINGLE
If Abs(DX) + Abs(DY) > 1 Then
DX = .DX / (Abs(.DX) + Abs(.DY))
DY = .DY / (Abs(.DX) + Abs(.DY))
Else
If Abs(DX) + Abs(DY) < 1 Then DX = 0 : DY = -1
End If
Case STATE_LEFT_RIGHT
DY = 0
If (.CurState Mod 2) = 0 Then DX = -1 Else DX = 1
Case STATE_8_DIRECTION
Select Case (.CurState Mod 8)
Case 0 : DX = 0 : DY = -1
Case 1 : DX = 1 : DY = -1
Case 2 : DX = 1 : DY = 0
Case 3 : DX = 1 : DY = 1
Case 4 : DX = 0 : DY = 1
Case 5 : DX = -1 : DY = 1
Case 6 : DX = -1 : DY = 0
Case 7 : DX = -1 : DY = -1
End Select
Case Else
DX = Cos((.CurState Mod 36) * 3.14159 / 18)
DY = -Sin((.CurState Mod 36) * 3.14159 / 18)
End Select
End With
End Function
The four intrinsic sprite types
in GameDev have various ways of tracking their direction and speed. The above
function is used to determine the direction of travel of a sprite based on its
state. This is used to determine which direction a bullet fired by the sprite
should travel (even when the sprite is motionless). Since a single-state sprite
has no directional information in its state, it must be based entirely on its
existing velocity. As you can see in the first case (STATE_SINGLE
)
if no current velocity exists, the function defaults to upward for a single
state sprite.
The STATE_LEFT_RIGHT
case returns a leftward velocity when the sprite is facing left, and a
rightward velocity otherwise. A left-right sprite never faces up, down, or
diagonally. Notice the connection between the .CurState
property
and the resulting velocity. A state of 0 for a left-right sprite is the
left-facing state and a state of 1 is the right-facing state.
This proceeds naturally to the
STATE_8_DIRECTION
case. Observe how each of the eight cases
represents a different direction for the eight-state sprite. 0 is straight up
(vertical velocity = -1), 1 is up and right, 2 is straight right, and so forth.
Finally, 36-state sprite has the most velocity information in its state. Its state effectively represents the direction that it is facing in 10-degree increments. The state number is converted in to degrees by multiplying by 10. However, since VBScript's trigonometric functions deal in radians, we use a slightly different formula to extract its angle. Multiplying by Pi and dividing by 18 (180 degrees / 10 = 18) gives us the result we want. If you understand trigonometry, you will also understand how sin and cos convert this in to a velocity/vector. Sin is negative because the computer's coordinate system is vertically reversed from the Cartesian coordinate system.
This function utilizes
VBScript's ability to pass variables by reference instead of by value (this is
the default). This allows DX
and DY
to be changed and affect the variables passed by the caller -- a handy way to
return multiple values.
Sub Player_OnAfterMoveSprites
Dim nIdx, VLeft, VTop, VRight, VBottom, VDat
With ProjectObj.GamePlayer
VLeft = .MapScrollX
VTop = .MapScrollY
VRight = VLeft + .rMap.ViewWidth
VBottom = VTop + .rMap.ViewHeight
End With
If nExpectCount <>
ProjectObj.GamePlayer.PlayerSprite.rDef.rLayer.SpriteCount Then RecountShots
End Sub
While the above code is not being applied in this sample script, it can be used to illustrate an important point. VBScript is an interpreted language, meaning it is processed at runtime and not compiled for the most efficient execution. As such, it's important to optimize code wherever possible so that it executes as efficiently as possible. This code is normally used to remove bullets that have left the view. Since our script had no such bullets defined, it's only calculating the edges of the view, and not using those values to check against bullet positions. Notice that these values are stored in variables rather than calculated for each test that would be performed. Reading a property of an object and performing arithmetic operations in addition to reading values from variables is, of course, slower than simply accessing a value in a variable. This is why the edges of the view are retrieved and calculated ahead of time. Then if there are a lot of bullets, the edges of the view don't have to be re-calculated for each bullet -- just use the stored values.
Notice that this event executes after each frame's MoveSprites stage. A further optimization could be made by removing this function altogether, since it's not needed. Also notice the use of "With". This kind of acts as another optimization because retrieving the GamePlayer property from the ProjectObj object repeatedly would be slower than simply re-using the object as returned by the GamePlayer object the first time. The With statement reuses the same object.
This secton of code also
demonstrates another small piece of GameDev's object model. The map's display
position and size are, of course, properties of the map object. The best way to
access the currently active map object is to go through the GamePlayer property
of the ProjectObj object representing the current project. GamePlayer is a
single object that describes the current state of gameplay. It stores
information such as the current player sprite, the scroll position of the map
and the currently active map. Here we are using it to get the currently scroll
position of the current map through the MapScrollX and MapScrollY properties.
We also use it to get the map (rMap) from which we then retrieve the view width
and height for that map. The scroll position is measured in pixels from the top
left corner of the map. Note that each layer may have a different scroll
position. To find out how for each layer is scrolled you must multiply the
map's scroll position by the layer's scroll rate.
Conveniently, yet another of the script's optimizations falls into this
function. The nExpectCount variable keeps track of how many sprites the script
expects to see on the layer. If it sees the same number of sprites as it
expects, it knows that it doesn't have to recount the sprites and possibly
remove some from its internal arrays of bullet sprites. As long as the number
of sprites remains the same on the layer, it figures it's pretty safe to assume
there's no need to spend time recounting the sprites to see which are still
active.
Sub DoFireButton0()
Dim NewSpr, DX, DY
If nShot0Count > = 5 Then Exit Sub
Set NewSpr = ProjectObj.GamePlayer.rMap.SpriteDefs("Bullet").MakeInstance
Set arBtn0Shots(nShot0Count) = NewSpr
nShot0Count = nShot0Count + 1
With ProjectObj.GamePlayer.PlayerSprite
.rDef.rLayer.AddSprite HostObj.AsObject(NewSpr)
nExpectCount = nExpectCount + 1
NewSpr.X = .X + (.Width - NewSpr.Width) / 2
NewSpr.Y = .Y + (.Height - NewSpr.Height) / 2
GetStateDeltas DX, DY
NewSpr.DX = DX * NewSpr.rDef.Template.MoveSpeed
NewSpr.DY = DY * NewSpr.rDef.Template.MoveSpeed
If NewSpr.rDef.Template.StateCount = 36 Then NewSpr.CurState =
RectToPolarState(DX, DY)
End With
End Sub
GameDev has a very large and
complicated object model. But with care, you can manipulate these objects,
creating new ones and adding them to the project, or deleting existing objects.
If done correctly, this gives you great control over what happens in the game.
You almost have as much power as you would if you were directly editing the
original source code. Knowing which operations are safe and which are unstable
simply takes some experience and experimentation. The simpler the effect, the
more likely it is to function as expected.
Not to describe exactly what operations are going on above that demonstrate
this. The DoFireButton0 function handles the response to the player pressing
the first button. First it checks to see if the maximum number of bullets of
this type are already active. This is a simple variable check. The number of
active button-0-sprites is maintained in nShot0Count. Then you will see that if
we have to spawn another bullet, we have a long path to go down to do it
properly -- these are the steps taken by the next single line:
The next line copies this sprite into an array of sprites. This array keeps
track of all the active "Bullet" sprites. That array is part of the script, not
the GameDev engine, so keep in mind we still have not added the sprite to tha
game. Before adding it, we also increase the count that tracks how many Bullet
sprites are active. Then inside the with we perform a number of operations with
the player sprite object as a starting point. First the sprite is added to the
player sprite's layer (makes sense that the bullet would reside on the same
layer as the player who shot it). Now the sprite is
part of the game because the layer is the part of the GameDev engine that holds
and utilizes sprite objects. In order to pass the sprite to GameDev we had to
use the "AsObject" method of the scripting host. This is necessary because
VBScript stores every variable as a variant, but the AddSprite method requires
a Sprite Object as a parameter. Since we are passing in a variable (which would
be a variant) we need to use AsObject to extract the object from the variable
before it gets passed to AddSprite. Note that there is only one copy of this
sprite object. The different variables that contain it are simply references or
pointers to the same object. So if you change anything about the NewSpr object,
that change is also seen in the arBtn0Shots array and in the layer's new sprite
object -- they all point to the same object.
This happens to be exactly what we do after adding 1 to the "nExpectCount" --
the properties of the newly added sprite are manipulated, which, as just
mentioned, affects the new sprite within the layer object. (nExpectCount is
incremented because we know we just added one to the array and don't have to do
a full recount if that is the only change that was made next time we check.) So
now that the sprite is on the layer we need to initialize its properties (this
could also have been done before adding it to the layer). The X and Y
coordinates are set so that the bullet is centered directly over the player
sprite, pased on the player sprite's X, Y, Width and Height properties. Then we
call that GetStateDeltas function to calculate the appropriate DX and DY values
(X and Y velocity) depending on the current state of the player sprite. The
will be returned in the form of velocities no larger than 1, so we then
multiply the velocities by the bullet sprite definition's speed setting so the
bullet goes in the same direction as the player sprite, but at its own speed.
In the case of a 36-state sprite, it's also necessary to set the current state
of the bullet sprite because this is the one sprite type whose state is
independent of its velocity (you can have a 36-state sprite that is pointing up
and left even while it's travelling down and right, unlike the other sprite
types).
So that's a good example of one specific set of interactions you can easily
perform with GameDev and VBScript. There are many other types of interactions
where you can manipulate a wide variety of GameDev's internal objects.
At this point, all we have is a sprite object in memory in that variable. It's
not part of the game yet because we haven't put it anywhere, just created it
and stored it in a variable.
Function RectToPolarState(X, Y)
Dim Angle, Pi
Pi = 3.14159
If X <> 0 Then
Angle = Atn(-Y / X)
Else
Angle = -(Pi / 2) * Sgn(Y)
End If
If X < 0 Then
Angle = Pi + Angle
ElseIf Y > 0 Then
Angle = Pi * 2 + Angle
End If
RectToPolarState = ((Angle * 18) / Pi) Mod 36
End Function
HostObj.SinkObjectEvents ProjectObj.GamePlayer, "Player"
HostObj.ConnectEventsNow()
ProjectObj.GamePlayer.Play 16
The RectToPolarState doesn't
introduce anything new. It just uses trigonometric functions to convert a
vector into an angle for use by the previous function which used it to
determine the state of a rotational sprite. I won't explain the math because it
has little to do with GameDev, but know that if you need to convert a vector
into an angle for a GameDev 36-state sprite, that's how to do it in VBScript.
The final three lines of code are the standard lines to connect the events and
start playing the game. To connect the script to a game you now need to execute
GameDev or GameDevPlayer with the "/p" command line switch. This can be done in
many ways. If you have version 1.3 or later, you can specify a script name in
the player settings dialog. Then when you right-click on that GDP (after it is
saved) and select play, it will automatically pick up that script file. Or, you
can use the Create Shortcut button in GameDev to create a shortcut that starts
GameDev using a certain GDP file and the VBS file. Or you can manually write
the command line yourself, like:
"C:\Program Files\GameDev\GDPlay.exe" "C:\Program
Files\GameDev\Projects\MyGame.GDP" /p MyScript.VBS
The project "GoldYoink" is a Lode Runner clone designed with the intention of demonstrating some more advanced scripting tasks. It is available on the GameDev projects page at http://gamedev.sf.net/Projects.html. In this chapter we will walk through the script implementation of many of the features in this project and describe what's being done and how the script accomplishes the task. An HTML representation of the entire script is in the frame below. The script can also be referred to as an independent document here. The content of this chapter will be grouped by task rather than by sequence in the script. All the pieces of script relavent to a task will be mentioned in association with that task (which may require jumping around in the script to see it all). This is presumed to be more helpful than going through the script sequentially and jumping around in describing what's going on. Also, please note that this section does not describe the entire functionality of the GoldYoink script. Rather it picks out some features in the script and uses pieces of the code to demonstrate how they are implemented with GameDev. Use these pieces as sample code and demonstraton of syntax, but don't expect to fully understand the whole script. For instance, I will not be documenting the enemies' search algorithm (at least not in this release) because it has more to do with basic programming and not specific GameDev functionality.
Sometimes it is much easier to define or copy GameDev objects within code than to try to manually define such objects within the development environment. This also allows you to change the rules more easily if you decide you don't want a particular set of objects to behave as they are throughout the game. Constructing objects dynamically within script can use pretty much the same process that is used within the GameDev source code itself because most of the methods used to construct these objects are exposed to VBScript. GoldYoink does a significant amount of dynamic object construction, which allows new maps to be added easily without having to re-define sprite templates and such on every map. Futhermore, it will create sprite definitions based on tiles in the map instead of requiring the designer to go through the trouble of defining paths and sprite definitions and so forth. You should be aware that dynamically constructed objects can also be saved as part of the project after they are created. Dynamically creating an object makes it part of the project just as if it had been created in the IDE. But such a task is better suited to a design-time script rather than a runtime script. This chapter will not go into design-time scripts.
First, let's look at
520Sub
Player_OnSpecialFunction(Func)
(Line 520 of the script). This
function traps all events raised by special functions in the game. This
is actually where all initialization occurs. Since initialization has to
take place on each map, special function events are used to initialize maps
instead of the one-time OnPlayInit event. The function that transports
the player from one map to the next raises an event and this code traps it and
initializes the next map so it's ready to be played. This is where all
the dynamic construction occurs, putting the necessary objects in place on that
map before it becomes active.
The first map (where the player simply runs down the ladder
to get to the first level) may seem like just a fun little intro, but it
actually allows the player to have a defined starting point that always
exists. That way we have some constant special function on the intro map
that can be trapped as it tranports the player to the first playable map.
Without this, the first playable map would have to be manually initialized
within the IDE or initialized specially within script, and there would be
problems setting the player sprite for the game because sprites are generated
at runtime. Notice the 3 lines of code that prepare for the initialization of
Map1 when the player is coming from the Intro
map:
527
Select Case
left(ProjectObj.GamePlayer.rMap.Name,3)
528
Case "Int"
529
Set
oNextMap = ProjectObj.Maps("Map1")
oNextMap is a local variable used to determine which map we need to initialize and where the player goes next. The special functions that transport the player from map to map are actually defined to just "raise an event" and not to do the actual transportation. The switching of the map is done within the script code because we have to ensure that the transportation occurs after the map is initialized, and special function events don't really fire until the special function has completed. The next few lines are for transporting the player from other maps to the following map based on the number in the map name. There's also a special case that allows the player to be transported from the last custom map to the final map of the game, which does not have a number because it must remain at the end of the sequence no matter how many maps there are.
On line 543 you can see that the map is actually initialized in a separate function. The transportation of the player takes place afterwards. The code that transports the player is similar to the code that implements a Switch Map function in the GameDev source code. InitMap starts on line 368 of the script. This is where all the dynamic creation takes place.
378For I = 0 To ProjectObj.Maps("Intro").SpriteTemplateCount -
1
This block of code re-creates the sprite templates required to
implement the sprites on each map. The templates are only defined on the
Intro map and then copied by this code to each map as it is initialized.
This allows the sprite templates to be defined only on one map even though they
are used from every map. After this block you can see that we loop
through the templates to locate the template specifically for the enemy
sprites. This will be required for creating the enemy sprites. The
search is performed using this loop, which allows the enemy template to be
named anything containing the word "Enemy", otherwise the template could have
simply been located by name using the SpriteTemplates property of the Project
object, as is the case when creating the player sprite.
379oMap.AddSpriteTemplate
ProjectObj.Maps("Intro").SpriteTemplates(I).Clone
380Next
On line 391 you will see some more dynamic creation. A new foreground layer is dynamically added to maps to allow the holes at the tops of ladders to actually appear partially in front of the player sprite. After creating the new layer, the function loops through all the tiles on the existing layer. Among other things, the loop will check for these ladder-hole tiles, and when it finds one, will change the tile on the new foreground layer to a tile that looks like the piece of the hole that should go in front of the player. That takes place on lines 419-420.
Other sections in that loop are processing the player and enemy tiles to create the enemies and player dynamically. The tiles are removed from the map and replaced with sprites. The first section (line 396) checks for any tile that represents a player and replaces it with tile 0. Since we want to be sure to only create one player sprite, only the position of the player is stored -- the sprite will be created after the loop is done. The next section, however, removes enemy tiles and actually handles the creation of the enemy sprites. First the path is created by creating an empty path and putting a single point on that path corresponding to the location of the tile. The path is associated with the main layer (presumed to be layer number 0 when it was acquired before the loop) by setting the LayerName property. Then the path itself is assigned a name (all named GameDev objects should be given non-empty unique names). Now the path object has been created, but a path must be added to a map before it can be used. So the next step adds the newly created path to the map being initialized. Notice the use of HostObj.AsObject. This is required because VBScript stores variables as variants. A variant is a data type that can contain any value (in this case, an object reference). But the AddPath method requires the type of the parameter to be exactly a path object, not a variant containing a path object. The AsObject function of the global HostObj object will return an object so that it can be passed directly to a function call without storing the object variable in a script variable (variant) first. It returns the object contained in the variable passed into it. If you don't do this you will get a type mismatch error.
Now we have the enemy template and a path for this specific enemy to start on. We need to create the sprite definition that makes an instance of an enemy sprite on the new path. NewSpriteDef, like NewPath, is a globally accessible method of the Engine object that will return a new object instance for us to work with -- in this case a new SpriteDef object. After we get the object, we want to set the flag to automatically have an instance of the sprite when the map starts. We also give the sprite definition a name based on how many enemy sprites there are ("Enemy1" then "Enemy2" etc).
After creating the sprite definition we are ready to connect all the dynamically created pieces together. It is important to remember that simply creating objects is not enough to make them part of the game. You need to connect them together appropriately and make sure the map/layer knows about these objects. The path has already been added to the map, but the sprite definition and template have not been added. First, the new sprite definition is associated with the newly created path. Then the sprite is given a reference to the layer on which it will reside. Then it is associated with the sprite template that defines what an "Enemy" sprite is. And finally, the map is updated to contain a reference to the sprite definition so it knows about this specific enemy definition on this path (using AddSpriteDef). Note the repeated use of AsObject. Once everything is connected, the tile representing the enemy is removed from the map by replacing the tile on the layer with an empty tile. Because the definition is set to automatically create an instance, GameDev will handle the creation of the sprite instance on the layer where the path resides. We don't have to do that part from script.
After the loop, all the pieces of the player sprite are dynamically created. First the path is created, defining where the player starts (the location that was found in the loop). The process proceeds similarly to the creation of the enemy sprites (see lines 425-436). The player is initialized in the falling state (using the PlayerFall template) because in this state, the script will autmatically transfer the player to the proper state (technically, it's not just a falling state, but a falling Sprite Definition since the player sprite switches to different sprite definitions as it changes "states").
438Set oWalkDef = NewSpriteDef
439oWalkDef.Name = "PlayerWalk"
440Set
oWalkDef.rPath = oNewPath
441Set oWalkDef.rLayer = oEditLayer
442Set
oWalkDef.Template = oMap.SpriteTemplates("PlayerWalk")
443oMap.AddSpriteDef HostObj.AsObject(oWalkDef)
Each sprite definition used for the player sprite is then dynamically constructed and associated with the same path and layer, and connected to the appropriate template (which was cloned from the intro map). First the walking definition is created, then 3 other player sprite definitions are similarly constructed so they will be available during gameplay when we need to switch states.
After the work in creating all the pieces of the sprites for a map, the function moves on to creating the special functions, which are used to switch the player sprite between the different states and to transport the player sprite to the next level. All the special functions are added using a separate script subroutine designed for adding special functions to maps in this game.
The AddSwitchFunc subroutine adds a Special Function that switches the "state" of the player sprite by activating another sprite definition. It takes four parameters
The implementation of the function starts on line 484. First it uses NewSpecialFunction to create a new Special Function object. The bRetainState flag is then used to appropriately set the flags for this functon that will be a "Switch to Sprite" type function. Enumerated constants (defined by GameDev) are used to set the flags and the type of the function. The function is placed on the main layer (presumed to be layer number 0) and assigned the name and sprite definition that were passed in. (This is the sprite definition to which the functon will switch when activated.) The position of the function is set at -1,-1,-1,-1 in the next 4 lines. This is where special functions go if they are not to be activated by the position of the player sprite. Functions positioned there are skipped when testing to see if the player is touching them (this is where a function goes if you click the "Add Function" button in the Maps Dialog instead of adding one in the map editor). Since we intend these Special Functions to be activated by the script, they are placed here off the edge of the map in this location where they will not be tested for collision with the player sprite. Finally the function adds the Special Function to the map using the AddSpecial method.
After adding the functions to switch the player sprite into all of its states, another function (AddEndFunc) is called to create the Special Function that handles transportation to the next level. This is similar to the other Special Function except that it is given a meaningful location at the top of the map. (The player climbs to the top of the map after acquiring all the gold to get to the next level.)
In order for script to be notified when two sprites collide, there must be some collision definition defined between the appropriate sprite classes. So even though the collision definition doesn't do anything (doesn't have any flags set or call any Special Function), we need to have a collision definition defined to detect when the player sprite touches an enemy sprite. To do this, use the NewCollisionDef function, following syntax similar to any object creation. In this case all we need to set on this object are the collision classes. Finally, adding the collision definition to the current map (AddCollDef) ensures that sprite collision events will be raised when the player hits an enemy while playing this map.
In GameDev 1.3.2, I found that I had neglected to support any "NewInteraction" method on the Engine that would allow one to create new tile interactions on the fly. So in this script, instead of creating a new interaction to add to each map, I simply added the same interaction from the intro map to every new map that was initialized. In GameDev version 1.4, expect a NewInteraction function that will return a new Interaction object that defines interaction between the player and the map tiles. This does, however, demonstrate that not all objects need to be created specifically for the map. Since all GoldYoink maps use the same TileSet, and the tile interaction doesn't depend on anything specific to the Intro map, we can use the exact same object on every map.
While the player sprite in GoldYoink is based off of several different standard sprite types, the enemy sprites are actually a type of their own. Each enemy is based on only one sprite template, and it doesn't switch from template to template like the player sprite. The enemy sprite is of a type that GameDev does not even define; it's totally defined in script. One of the lines of code that executes when handling the function that switches to a new map calls a function called "InitEnemies". This call takes place on line 569, and the function is implemented starting on line 574.
The first thing this function does is locate the template on which enemies are based. You may recall that all templates were copied during InitMap so there will be an enemy template on the current map. Pointers to the template and the TileSet are cached. Then the magic begins; a custom enemy sprite template is configured so enemy sprites can be created and associated with this template:
587oEnemyTemplate.StateType = 99 ' Some Non-Standard type
588oEnemyTemplate.StateCount = 7
589' 0 =
Walking Left
590' 1 = Walking Right
591' 2 = Climbing Up
592' 3 = Climbing
Down
593' 4 = Swinging Left
594' 5 = Swinging Right
595' 6 =
Falling
The StateType property determines what pre-defined type this sprite is. It is generally one of the values from the eStateType enumeration, like STATE_LEFT_RIGHT or STATE_36_DIRECTION. But here we are making up a new type and so we assign some arbitrary value to this property just to make sure that GameDev's internal processing doesn't make any assumptions about what kind of sprite this is. The custom sprite has 7 states, each described in the comment block. What is gained by defining separate states rather than making a single state and managing the switching of frames and states in script? Well, GameDev does do some work for us even on custom sprites. It may not have a clue what the different states mean, but is still allows us to add animation frames to each state and advance to the next frame within the state according to the defined animation speed.
So next, we need to define the frames for each state. First the frames are cleared out of each state, ensuring that we are starting from a blank slate (eliminate any frames from the template that may have been added at design time). This is done with the ClearState method. Then we specify for each state which tileset contains the graphics used for that state (each state can use its own tileset). All of our states are based on graphics in the same tileset. And finally we add the animation frames to each state by specifying which tile indexes represent animation frames in the various states. AppendStateFrame's first parameter specifies which state we want to add a frame to, and the second parameter is the index of the frame within the state's tileset. So state 0 (walking left state), as you can see, is represented by frames 22 and 23 from the tileset drawn in sequence (looping).
When the sprite has been fully defined, it is relatively easy to use it in script. Primarily, what you need to do is set the current state and velocity. For examples, look at certain lines of the ProcessEnemy subroutine that do this:
680oSprite.CurState = 6
681oSprite.DX = 0
682oSprite.DY = 1
683Exit Sub
...
689If oBarGroup.IsMember(nTile) Then
690oSprite.CurState = 5
691Else
692oSprite.CurState = 1
693End If
694oSprite.DY = 0
695oSprite.DX = 1
(There are many more examples in there too.) GameDev will handle all the simple movement, (moving the sprite based on its current velocity) and, sometimes, collision detecton and solidity checking for the sprite. Basically, it can handle the things that are defined in the template that don't care about the sprite's type/states. And since collisions only depend on the current position and current frame and size of the sprite, this can be handled by Gamedev. (Remember that many features are turned off, though, if you set your sprite controller to be "Simple/Scripted".) If you have defined the animations in each state, GameDev should also animate it for you based on the template's animation settings and the animation frames in the defined states.
Special functions can be used to do many things, including switch from map to map. Sometimes, however, it's nicer to not have to create a special function on every map to accomplish a particular task, or to accomplish the task slightly differently than the special function implementation accomplishes it. In GoldYoink, rather than using the switch map function to jump from map to map, we implement the map switching in code because it needs to happen after the event has been triggered (normally a switch map function would switch to a new map and then execute the script code).
On lines 544 through 556 is the code that manually switches the current map to a new map. To manually implement special functions it is sometimes helpful to look at the GameDev code itself (that implements the special function), especially since it's written in Visual Basic, which is somewhat similar to VBScript. So lets compare the script code to the original Visual Basic code. The code that implements special functions in GameDev is in the source code file called Player.cls in a function called ActivateFunction. The switch map code is in this section (taken from GameDev version 1.3.2):
Case SPECIAL_SWITCHMAP
PlayFunctionMedia SpecialObj
Set rOldMap = rMap
Set rMap.Disp = Nothing
TmpCtl = rMap.bDisablePlayerEdit
Set rMap = Prj.Maps(SpecialObj.Value)
Set rMap.Disp = Disp
rMap.LoadTiles
rMap.LoadTileAnims
rMap.InitSprites
InitPlayerSprite
If Len(SpecialObj.SpriteName) > 0 Then
For Idx = 0 To rMap.LayerCount - 1
With rMap.MapLayer(Idx)
For Idx2 = 0 To .SpriteCount - 1
If .Sprite(Idx2).rDef.Name = SpecialObj.SpriteName Then
Set PlayerSprite = .Sprite(Idx2)
Idx = rMap.LayerCount
Exit For
End If
Next
End With
Next
End If
rMap.bDisablePlayerEdit = TmpCtl
If SpecialObj.Flags And InteractionFlags.INTFL_OVERRIDEPOSITION Then
PlayerSprite.X = SpecialObj.DestX
PlayerSprite.Y = SpecialObj.DestY
End If
Prj.MediaMgr.ModalFadeOutAll
If Prj.MediaMgr.ClipExists(rMap.BackgroundMusic) Then
Prj.MediaMgr.Clip(rMap.BackgroundMusic).Play
End If
Disp.Cls rMap.BackgroundColor
Disp.Flip
Disp.Cls rMap.BackgroundColor
So that's the end of the script code that was needed to switch the maps.
The tiles on each layer of the map are stored in objects that are implemented in BMDXCtls.dll (the graphics engine). It is possible to access and manipulate the tiles in these objects directly from script code. For complete documentation on the objects contained in BMDXCtls.dll, download the source code package for BMDXCtls from the SourceForge project site and refer to BMDXCtls.hlp. The object ob interest is the BMDXTileMap object. Referring to the Scripting Wizard, we can see that BMDXTileMap is used only one place in GameDev, and that is on the Data property of the Layer object. (For any property whose type begins with "IBMDX", refer to the BMDXCtls.hlp file for details.) That means that the data property of the Layer object returns an interface to a BMDXTileMap object and we can use the properties and methods of BMDXTileMap on that object. Take lines 570 and 571 for example:
570DefaultMapData = oCurrentLayer.Data.MapData
571oCurrentLayer.Data.TileMapping(45)=0
Here we are accessing the layer data for the player's current layer. MapData (on line 570) accesses the MapData property of the BMDXTileMap object. Referring to BMDXCtls.hlp we can see that the MapData property returns the data for the entire BMDXTileMap object (which represents one layer in GameDev). What that line is doing is storing the state of the current level so if we need to restart the level, we can restore the map tiles to their initial state quickly and easily. Line 571 is setting the tile mapping for tile 45 to 0. That means every tile on the map whose tile index is 45 will be returned as tile 0 instead of 45. This is the same mechanism that is used for tile animation, but in this case it is used to hide the ladders that should only appear when the player is done with the level. Tile 45 is the tile that represents ladders that only appear at the end of the level (when the player has acquired all the gold).
One other, and possibly most important, property of the BMDXTileMap object is the TileValue property. This property is used to directly access or manipulate a single tile within a map. We can see that this was done during map initialization to convert player and enemy tiles into sprites and switch the tiles back to blank tiles. Lines 395 through 399 demonstrate this when they detect that a tile represents a player:
395Select Case oEditLayer.Data.TileValue(nX, nY)
396Case 1,2,3,4,5,6,7,8,9
397nPlayerX = nX
398nPlayerY = nY
399oEditLayer.Data.TileValue(nX, nY) = 0
Ladders are not built into GameDev, so if you want to design a game in which the player can climb ladders, you either have to cleverly design some sprite/map interactions or use script to switch the player to states where he or she can climb ladders as appropriate. GoldYoink, of course, opts for the scripting solution, but also makes use of some sprite templates and tile categories built into the project.
The first task is to detect when the player is touching a ladder. To do this, a tile category in the project called "Ladder" has been created. To test if a particular tile is a ladder tile, the IsMember method of the TileGroup object is used. A TileGroup is accessed by finding the named Category object in the project and then accessing the Group property. A category is simply a thin wrapper around the group object that allows a name and tileset to be assigned to a group of tile indexes. This group is used regularly to determine if the tile beneath any corner of the player sprite is a ladder.
Taking a look at the CheckHangAndLadder function on lines 295 through 309 we can see the code in use. Line 303 checks to see of the tile under the top-left, top-right, bottom-left or bottom-right corner of the player sprite corresponds to a ladder tile:
303ElseIf oLadderGroup.IsMember(nTLTile) Or oLadderGroup.IsMember(nTRTile) Or _
304oLadderGroup.IsMember(nBLTile) Or oLadderGroup.IsMember(nBRTile) Then
305ProjectObj.GamePlayer.ActivateFunction ProjectObj.GamePlayer.rMap.Specials("ClimbLadder")
306Set oPlayerSprite = ProjectObj.GamePlayer.PlayerSprite
307CheckHangAndLadder = True
308Exit Function
309End If
Variables nTLTile, nTRTile, nBLTile and nBRTile represent the tile indexes of the tiles beneath the four corners of the player sprite. They were set in the calling function, ProcessPlayerMovement. This is done to optimize performance so we don't have to read the map and do math every time we want to get these tile indexes. When one of these tiles is found to be a ladder, the special function is activated to switch the player sprite to a ladder climbing sprite.
Now let's follow this ladder climbing case from beginning to end because CheckHangAndLadder is only the lowest level of this feature, and only handles a specific frequently used test, not all cases where the player starts climbing a ladder. It starts when GameDev raises the Player_OnAfterMoveSprites event. This event is used to process the interaction with ladders because it is called for every frame and we want to check for ladders on every frame. In Player_OnAfterMoveSprites, the ProcessPlayerMovement function is called to handle all custom movement of the player (including ladders). ProcessPlayerMovement calculates the tile indexes beneath the 4 corners of the player sprite and stores them in the respective global variables on lines 182 through 191. It also sets some other tile indexes -- the tile that is beneath the center of the sprite goes into the nCTile variable and so forth. The ProcessPlayerMovement function later determines which state the player is in. Lets assume you've started in the walking state. The following condition will be selected as true:
239ElseIf oPlayerSprite.rDef is oWalkDef Then
Once it has determined that the player was walking, it determines if the player should switch to the climbing state. This should happen when the player's center is overlapping a ladder and when up is being pressed. This is done by checking the CtlActions property to see if up is being pressed and by checking if the nCTile index matches the index of a ladder tile:
253ElseIf ((ProjectObj.GamePlayer.CtlActions And ACTION_UP) > 0) And _
254oLadderGroup.IsMember(nCTile) Then
The key action then is to activate the function to switch to the climbing "state" (or switch to a climbing state however you like). After this is done we remember the new player sprite's sprite (since it was recreated as part of the switch process) and center the player over the ladder by adjusting the X property:
255ProjectObj.GamePlayer.ActivateFunction ProjectObj.GamePlayer.rMap.Specials("ClimbLadder")
256Set oPlayerSprite = ProjectObj.GamePlayer.PlayerSprite
257oPlayerSprite.X = ((oPlayerSprite.X+HalfTileSize) \ TileSize) * TileSize
Given that you have a function in the project to switch the player sprite into the climbing state, and have properly defined how a climbing sprite can move (none of that requires script) that is fundamentally all you need to do in order to properly switch to the climbing state at the appropriate time. So you can ignore the multitides of extra code for the time being. If you also want to be able to climb down ladders starting from a walking state, look into the block of code directly below it on lines 258 through 262 (nCBelow represents the tile index of the tile below the center of the player's feet).
Keep in mind that this script contains logic for a lot more than just climbing ladders. The script in the code being examined here has a lot of extra logic to account for overhead bars and specifically emulating Lode Runner ladders. As mentioned earlier, if you accept a certain set of simple rules, you can even implement ladder climbing without scripting at all. So remember that this script may be best as examples of syntax, and not necessarily as a whole drop-in code set.
Now that you have entered the climbing state, you will also want to be able to switch back to a non-climbing state when you get off the ladder. This case starts of with the ProcessPlayerMovement function realizing that the player sprite is already in the climbing state:
219If oPlayerSprite.rDef Is oLadderDef Then
It then determines if any corner of the player is still touching a ladder tile. If not, we need to switch to a falling state (if no solid ground is below) or a walking state (if there is solid ground below). The script first calls CheckHangAndLadder because we want to switch directly to a hanging state (instead of a falling state) if there is a bar near the player's head. But for the simple case, presume that CheckHangAndLadder returned False indicating that no hanging bars or ladders will prevent the player from falling. The script selects the Fall or Walk function to switch the player into the falling or walking state respectively, as appropriate. Note that nBLBTile represents the tile index of the tile below the bottom left corner of the player sprite and nBRBTile represents the tile below the bottom right corner of the player sprite.
220If Not (oLadderGroup.IsMember(nBRTile) Or oLadderGroup.IsMember(nBLTile) Or _
221oLadderGroup.IsMember(nTRTile) Or oLadderGroup.IsMember(nTLTile)) Then
222If Not CheckHangAndLadder Then
223If Not (oStopFallGroup.IsMember(nBLBTile) Or oStopFallGroup.IsMember(nBRBTile)) Then
224ProjectObj.GamePlayer.ActivateFunction ProjectObj.GamePlayer.rMap.Specials("Fall")
225Else
226ProjectObj.GamePlayer.ActivateFunction ProjectObj.GamePlayer.rMap.Specials("Walk")
227End If
228Set oPlayerSprite = ProjectObj.GamePlayer.PlayerSprite
229End If
As you see, we again have to re-cache the reference to the player sprite (line 228) after switching sprites because it will have been re-created. The remaining section will re-center the player over the ladder when moving up or down the ladder (by adjusting the X position) and also prevent diagonal climbing.
230Else
231If ((ProjectObj.GamePlayer.CtlActions And ACTION_UP) > 0) Or _
232((ProjectObj.GamePlayer.CtlActions And ACTION_DOWN) > 0) Then
233oPlayerSprite.X = ((oPlayerSprite.X+HalfTileSize) \ TileSize) * TileSize
234oPlayerSprite.DX = 0
235ElseIf oPlayerSprite.DX <> 0 Then
236oPlayerSprite.DY = 0
237End If
238End If
That's a quick overview of some of the most important aspects of writing a script to help the player climb ladders. There is one more aspect worth noting. In GoldYoink, we want the player to be able to walk past ladders when coming from the side, but also have the ladder support the player when the player walks accross the top. This is handled in two ways in GoldYoink, but really, only one needs to be used. One way is to have a special tile at the top of every ladder which is defined as solid. In GoldYoink there is a tile representing the ladder coming up through a hole in the ground. This tile is defined as solid. Putting this tile at the top of every ladder will allow the player to walk accross those tops, but allow the rest of the ladder to be non-solid so the player can pass through is when in the walking state. This top tile would be defined as non-solid, of course, for the player's climbing state, allowing the player to climb through the top of the ladder. The script then needs to switch the player to a climbing state when it detects that the player is standing on this tile pressing down:
258ElseIf (ProjectObj.GamePlayer.CtlActions And ACTION_DOWN) > 0 And _
259oLadderGroup.IsMember(nCBelow) Then
260ProjectObj.GamePlayer.ActivateFunction ProjectObj.GamePlayer.rMap.Specials("ClimbLadder")
261Set oPlayerSprite = ProjectObj.GamePlayer.PlayerSprite
262oPlayerSprite.X = ((oPlayerSprite.X+HalfTileSize) \ TileSize) * TileSize
The other method is to have the script specially handle walking accross the tops of ladders. This accounts for some of the complexity you see in the script code. It also contributes to the need for two separate categories to separate solid tiles from "StopFall" tiles, which aren't solid, but will stop the player from falling. The player is forced to remain on top of the ladder when walking, even though GameDev is trying to make the player fall through the non-solid tile:
263ElseIf oLadderGroup.IsMember(nBLBTile) Or oLadderGroup.IsMember(nBRBTile) Then
264oPlayerSprite.Y = nTopOfTile
265oPlayerSprite.DY = 0
There is much room for expansion and improvement in this document, but I have only so much energy before I must move on to something else. Hopefully those of you out there who have been hungering for some help and documentation on GameDev's scripting support will find this useful. I'm open to adding new sections to this document to make it more helpful, and to extend it in the future where appropriate. So keep an eye open for updates to this documentation. The latest version should always be available at http://gamedev.sf.net/Help/Docutorial.html.