Tutorial: Brig Bug

This tutorial will cover most of the basics, and hopefully give you an idea of what Pfhortran coding is like. We will use the map, "Brig Bug" which is available here:

Download sample map: Brig Bug

The map has already been merged, but an unmerged version is also provided so you can open it in Forge. To complete this tutorial, you will need a plain text editor (I prefer BBEdit) and ResEdit, available on Apple's web site.

Open up the map in Aleph One and run around for a few minutes. You will notice that the map consists of a simple room with raised ledges on each side. The left ledge is accessible via a series of moving platforms, and has a terminal embedded in the ledge wall. The right ledge is not accessible, but we can see that two switches are set into the wall of the ledge. On the opposite end of the room we have windows looking out into space and a large closed door.

The first thing we want to do is give the player access to the buttons on the right side. To do this, we have decided to teleport the player to the top of the right ledge via the terminal on the left. Once the player reads the terminal, they will automatically be transported to the opposite side of the room. To accomplish this, open up your text editor and write the following:

_report_errors true


_procedure terminal_exit

    teleport_player 33
end



The first line turns on error reporting. If this were the final version of this map, we would want to turn it off. But, since we are in the middle of development, error reporting can help us debug our scripts. 33 happens to be the index of one of the polygons on the right side of the room, so teleport_player will teleport the player to that polygon. We have put this instruction in a terminal_exit procedure, which will execute every time the player leaves a terminal.

Now let's test this code. Copy the text from your editor, run ResEdit, and open the Brig Bug map file (make sure you open the merged version). Paste the text into the file (a "TEXT" resource should be created). Open up the "TEXT" resource and see that the text you just added is resource ID 128. Click on the resource and choose "Get Resource Info" in the Resource menu. Change the ID of this resource from 128 to 1000 and save. Now open Aleph One, choose Brig Bug as the map you want to use, and start the game. Make your way up to the terminal, read it, and watch in awe as you are teleported to the other side of the room automatically.

If it worked, congratulations. You have just completed your first Pfhortran script. If it did not work, go back into ResEdit and make sure the "TEXT" resource containing your script is ID 1000. Also make sure that the script reads exactly the same as above. If a file called "Pfhortran Error Report" was generated when you ran Aleph One, open it up and see what it says. It probably will tell you that there is a syntax error of some sort on a specific line.

Now that the player has access to the buttons, we should make them do something. The button to the left controls tag 2, which currently is linked to nothing. The button on the right controls tag 1, which will open the large door at the far end of the room. Try flicking the right button and watch the door open. Hmmm... it seems to open into complete space. Maybe we should simulate a vacuum when it is opened....

To do this, go back to your text editor and add the following:

_procedure init

    vacTime: define false
    playerOxy: define 0

end

_procedure tag_switch

    get_tag_state 1, vacTime

end

_procedure idle

    if_!= vacTime, true, skipIt
    get_oxygen playerOxy
    subtract playerOxy, 10
    set_oxygen playerOxy

skipIt: end



So what we have done here is create a loop that checks to see if the variable vacTime is true. If it is true (and it is set to true when tag 1 is set to true via a tag switch), we subtract 10 points of oxygen from the player using the playerOxy variable. Both the vacTime and playerOxy variables need to be initialized, so we have created an init procedure to do just that. If the vacTime variable does not equal true, the code in the idle procedure will jump to skipIt, a label that points at end. Since this is the idle procedure, ending the procedure actually makes it loop to the beginning the next frame.

Ok, so now we have a very slow oxygen drain when the bay door is open. We could increase the speed of the drain by increasing the amount subtracted from the oxygen level. Now we want to make this level a little spiffier... add some cinematic elements. This is going to require the use of some cameras.

First of all, let's make a static camera that shows us the bay door so we can make a cut scene when the first switch is thrown. As the Cameras and Paths section describes, each camera needs to have several values: an xyz position, a resident polygon, a yaw, and finally a pitch. Fortunately, there are easy ways to attain these values. While playing Aleph One, press the F10 key to bring up the value display. Now walk all the way to the end of the ledge on the left side of the room and look at the door. Note that the values in the corner of the screen change as you move around... these are the values we will end up using for our camera. Find a good view of the door and copy down the values. You can hit F9 to save a screen shot, if you wish. I got the following values:

X = -4.981
Y = 5.008
Z = 2.600
Polygon = 7
Yaw = -54.844
Pitch = -6.328



Now we need to take those values and use them in the script. Before that, we are going to have to initialize the camera we will use, so we'll have to make some additions to the init procedure.

_procedure init

    vacTime: define false
    playerOxy: define 0

    animTime: define false

    Init_Cameras 1

    Select_Camera 1
    Set_Camera_Poly 7
    Set_Camera_Pos -4.981, 5.008, 2.6
    Set_Camera_YP -54.844, -6.328

    use_camera default_camera

end



This will take care of our camera setup. We have initialized one camera and set it's values to the values we found in Aleph One. If there is any part of this you don't understand, go read the Cameras and Paths section.

However, we haven't actually activated the camera yet. What we want to do is turn it on for a few seconds while the door is opening, then turn it off again. We have created a variable called animTime to help us do that. When animTime becomes true, the camera we have made will be put to use. To set animTime to true at the right time, we need to make our tag_switch procedure a little smarter.

_procedure tag_switch

    set animTime, vacTime
    get_tag_state 1, vacTime
    if_= animTime, vacTime, noChange
    if_= vacTime, false, noChange
    set animTime, true

noChange: end



What we are doing here is checking to see if the vacTime variable has changed from true to false, indicating that the door switch has been thrown. To do this, we set animTime to vacTime's current state, then set vacTime to the current state of tag 1. If at this point vacTime and animTime are not equal, then we know that tag 1 has just changed. If vacTime does not equal false, we know that vacTime must be true, in which case the switch has just now been thrown. If that is the case, we set animTime to true so our cut scene will start.

Finally, we need to add a bit of code to the idle statement to watch for animTime and to execute the cut scene when it becomes true.

_procedure idle

    if_!= vacTime, true, skipIt
    if_= animTime, true, doAnim
    get_oxygen playerOxy
    subtract playerOxy, 10
    set_oxygen playerOxy
    jump skipIt

    doAnim: use_camera 1
    wait_ticks 300
    use_camera default_camera
    set animTime, false


skipIt: end



Basically, all we do is check to see if animTime is true, and if it is we jump strait to the doAnim label to execute the cut scene. If it is not true, we continue as normal, skipping the doAnim part by jumping to skipIt after oxygen levels have been affected. In the event that we do execute the cut scene, we set animTime to false when we are done so that the cut scene will only execute once.

You may notice there is a slight delay between flicking the switch and seeing the cut scene. This is because, normally, Pfhortran executes only one instruction per frame. There are about 8 lines of code between the start of the tag_switch procedure and the doAnim label that must be executed before the camera can become active. On my machine, A1 tops out at about 20 frames per second in software rendering. 8 frames means 40% of a second, or about 24 60ths of a second. This is definitely a perceptible pause. Fortunately, we can tell Pfhortran to be faster in place that it makes since by using block_start and block_end.

_procedure tag_switch

    block_start

    set animTime, vacTime
    get_tag_state 1, vacTime
    if_= animTime, vacTime, noChange
    if_= vacTime, false, noChange
    set animTime, true

    noChange: block_end

end



By placing all that stuff in a block, we instruct Pfhortran to execute it all in a single frame. This takes the total frames necessary to execute the cut scene from 8 to 4, a 50% speed increase. We could make it even faster by dropping block_start and block_end instructions into the idle loop as well, but I think what we have is good enough for now. For more info on using blocks, check out the Script Structure section.

Ok, the fun with cameras is just getting started. Let's put the button we haven't used yet to work and make it trigger a camera animation. This doesn't make much since in terms of a playable level, but hey, this is a tutorial.

The idea is that we want to make a moving camera animation with a path that executes when the second switch is thrown. Now a path is made up of several camera points, each having the same xyz/polygon/yaw/pitch values we used for the static camera. However, a camera following a path will attempt to create a smooth transition from one point to the next, so paths make it easy to create moving cameras. For this tutorial, we are going to construct a three point path that flies from outer space to just behind the player.

To do this, we need to find our three points by walking around in Aleph One with our positional values showing. Each point must have all that good info we copied down for our first camera, so you may need to get out a pencil and paper. The three points I choose are (1): outside the bay on the red space doc looking into the room, (2) inside the room, facing the ledge with the two buttons on it, and (3) the ledge just below where the second switch is, looking up at the switch. For these points I get the following values:

X = 2.269
Y = -12.196
Z = 0.600
Polygon = 17
Yaw = 84.375
Pitch = 0
X = 2.649
Y = 2.637
Z = 0.600
Polygon = 8
Yaw = -15.469
Pitch = 0
X = 9.626
Y = 1.275
Z = 2.1
Polygon = 4
Yaw = 8.438
Pitch = 7.734



You can get your own values or just use mine, it doesn't really matter. Now that we have our positional information for each point, we can initialize our path.

_procedure init

    vacTime: define false
    playerOxy: define 0

    animTime: define false
    pathTime: define false

    tempVal: define 0

    Init_Cameras 2
    Init_Paths 1

    New_Path 1, 3

    Select_Path 1
    Set_Path_Move_Speed 100
    Set_Path_Roll_Speed 100

    Select_Point 0
    Set_Point_Poly 17
    Set_Point_Pos 2.269, -12.196, 0.6
    Set_Point_YP 84.375, 0

    Select_Point 1

    Set_Point_Poly 8
    Set_Point_Pos 2.649, 2.637, 0.6
    Set_Point_YP -15.469, 0

    Select_Point 2

    Set_Point_Poly 4
    Set_Point_Pos 9.626, 1.275, 2.1
    Set_Point_YP 8.438, 7.734


    Select_Camera 1
    Set_Camera_Poly 7
    Set_Camera_Pos -4.981, 5.008, 2.6
    Set_Camera_YP -54.844, -6.328

    use_camera default_camera

end



The first addition to the init procedure is two new variables, pathTime and tempVal. pathTime will be used much like animTime to specify when the path animation should be executed. tempVal we'll get to later. The next change is to our Init_Cameras instruction. We have bumped up the number of custom cameras from one to two. This is because we will need one camera for the static cut scene we have already made and the second camera for the path we are about to build.

Speak of the devil, the next instruction, Init_Paths initializes 1 custom path for us to use. Once we have specified how many paths we will use, we must initialize each path separately and give the number of points in each path. The New_Path instruction does this, and here we initialize path 1 to have 3 points.

Now the path is initialized and ready to be edited. Just like cameras, we must select a path before we can work with it. Once we have done that, we need to tell the path how quickly to move and roll. Then we are ready to start working on individual points.

Points in a path start at zero, so our points for this path will be 0, 1, and 2. We set them up by selecting each individually and setting their positional values as we would for a static camera. These are just the values we found above, and the only difference between the static camera and the path instructions is the word "path" appears instead of "camera".

Once all three points are set up, our path is complete and ready to go. All we have to do is link it to a camera to make it go. However, before we do that, we have to set up the second button so that when it is flipped, pathTime becomes true. To do this, we'll have to make our tag_switch procedure a little smarter.

_procedure tag_switch

    block_start

    get_my_value tempVal

    if_= tempVal, 1, checkVac

    get_tag_state 2, pathTime
    jump noChange

    checkVac: set animTime, vacTime
    get_tag_state 1, vacTime
    if_= animTime, vacTime, noChange
    if_= vacTime, false, noChange
    set animTime, true

    noChange: block_end

end



As detailed in the Procedures section, most procedures have a specific variable which is set when they are called. This variable is called the "which" variable, and usually denotes which game element is being effected by the procedure. In the case of tag_switch, the "which" variable holds which tag has just changed. We can use this information to break the procedure up into two different parts: one for dealing with our cut scene (which is tied to tag 1) and one for triggering our path animation (which we want to tie to tag 2). To separate the two, we read the "which" variable (with get_my_value) and check if it is equal to one. If it is, then the switch controlling tag 1 has just been flipped and we need to handle the animTime and vacTime variables. We jump to checkVac, a label that points to that part of the code and continue execution. If the "which" variable is not equal to one, we set the pathTime variable equal to the state of tag two and then jump to noChange to skip the tag 1 related stuff. This way, tag 1's code and tag 2's code are separated into two manageable segments. Now pathTime will become true when the second switch is flipped.

Finally, all we need to do is check to see when pathTime becomes true in our idle loop:

_procedure idle

    if_!= vacTime, true, skipIt
    if_= animTime, true, doAnim
    if_= pathTime, true, doPath

    get_oxygen playerOxy
    subtract playerOxy, 10
    set_oxygen playerOxy
    jump skipIt

    doAnim: use_camera 1
    wait_ticks 300
    use_camera default_camera
    set animTime, false
    jump skipIt

    doPath: select_camera 2
    start_camera_on_path 1
    wait_for_path
    set pathTime, false

skipIt: end



The additions are simple enough: when pathTime becomes true execution moves to the doPath label, where the second camera is started on path 1. We wait for the animation to complete and then turn pathTime off. The result is that when we flip the switch (after opening the bay doors) our path animation plays out.

Go try it out! Check out how cool animated cameras look. If your path didn't turn out as you expected it to, please read the information at the end of the Cameras and Paths section about short integer round off errors.

That's about it for the tutorial for now, though I'll expand on it as Pfhortran's power grows. Pfhortran can already do a lot more than is covered here... the point of this tutorial is just to try and explain scripting in general terms. If you feel kind of lost even after doing all this stuff, try adding little features and changing little bits of code to get a feel for how things work. With a little practice, Pfhortran should make a lot more sense. Also, the complete code for Brig Bug is included with this tutorial, so you don't have to write everything out yourself.