Custom Level Gap Tutorial

rodafy · 3 · 1235

Offline rodafy

  • Upload Approved
  • Newbie
  • ****
    • Posts: 14
    • View Profile
Gaps! Gotta love 'em. We don't currently have a tutorial on how to implement them into custom levels, so I figured I would make one in case anyone feels like putting gaps into their level. I will be using gaps from PS1 levels for reference.

Note: Simple gaps can be set up using the built-in scripts in the Blender plugin, however I prefer to use custom scripts as they offer more flexibility. If you don't know how to make custom scripts, then please read this:

There are a few types of gaps, and I will explain each one in as much detail as I can. These are simply the most common kinds of gaps, but there are others which I have not tested very well. If you're interested in implementing a gap that has not been described here, find me in the THPSX discord or respond to this thread and I'll figure something out for you.

Let's start off with the easiest kind of gap to implement, which is an air gap. As you probably know, an air gap happens when the player jumps from one kicker to another, one quarter pipe to another, over a big stair set, etc.


Firstly let's deal with one-way air gaps, like the "Oversized 8 Set" gap in Streets, which looks like this:

Simple enough, jump from the top to the bottom and you'll pass through both of these faces, known as 'gap polys'.

Firstly, select the first gap poly that you want the player to pass through, which you will use to start the gap. Enter Edit Mode, and select everything. Then mark the gap poly as both a 'Trigger' and 'Non-Collidable', and uncheck the box that says 'Export to Scene', like so:

(my settings look slightly different I think because I am using a different build of the plugin)

Now you are ready to write the 'start gap' script, which looks like this:

Code: [Select]
:i $StartGap$ $GapID$ = $Oversized8Set$ $FLAGS$ = :a{ $PURE_AIR$ :a}
Simple, right? Each gap has its own ID, and should have a StartGap and EndGap script associated with it. Make sure each gap has a different ID, even if the gaps themselves have the same name (I will explain this in more detail when we get to two-way air gaps). The flags are requirements for the gap, and there are various different flags which I will also explain in more detail later. For air gaps, the only flag you need is "PURE_AIR". This means the gap will only trigger if the player is in the air while doing it.

Give the script a name, for example I would name this script something like 'script_Gap_Oversized8Set_Start'.

Now, scroll to where it says Trigger Script, select 'Custom', and then select the script you just wrote.

The second gap poly, which ends the gap, must also be marked as a 'Trigger' and 'Non-Collidable', and not exported to scene.

The EndGap script should look like this:

Code: [Select]
:i $EndGap$ $GapID$ = $Oversized8Set$ $text$ = %s(15,"Oversized 8 Set") $score$ = %i(500,000001f4)
There's the gap ID again! The 'text' is what shows up in the trick string. The number before it is just the length of the gap name in characters. The 'score' is, obviously, how many points the gap is worth. The first number is the gap's point value in decimal, and the second is that number in hexadecimal. (Here's a simple decimal-to-hexadecimal converter if you're as bad with hexadecimal as I am:

Now assign this script to the 2nd gap poly and your gap is good to go! Now when the player airs through both of these invisible faces, the gap will trigger, play that lovely camera click sound, and give them a nice little point bonus. Great!


Let's move on to two-way air gaps now. These are implemented in much the same way, but there is a little more to it.

Here's 'Over the Quarter' from Canada:

Jump from one kicker to the other and you'll hit the gap.

Now, both of the gap polys for this gap are double-sided. The player can start and end the gap in either direction, so both gap polys will need a StartGap and EndGap script, like so:

Code: [Select]
:i $StartGap$ $GapID$ = $OverTheQuarter01$ $FLAGS$ :a{ $PURE_AIR$ :a}
:i $EndGap$ $GapID$ = $OverTheQuarter02$ $text$ = %s(16,"Over the Quarter") $score$ = %i(250,000000fa)

(the other gap poly should have the exact same, except the StartGap ID should be 'OverTheQuarter02' and the EndGap ID should be 'OverTheQuarter01')

Here's where the gap ID becomes important. Although it is the same gap whichever direction you jump it, the IDs need to be different to prevent the gap from firing in unintended places, so each intended method of completing the gap has to have its own unique ID.

You can have the gaps be different if you wish, such as the gaps "Over the Lil' 4" and "Up the Lil' 4" in Marseille. This also uses two double-sided gap polys:

All you have to do is assign the gap poly at the top of the stairs to start the "Over the Lil' 4" gap and end the "Up the Lil' 4" gap, and vice-versa. Nifty!


So, aside from the hassle of creating your own gap polys, setting up air gaps is very easy. And now that you know how to do them, setting up other kinds of gaps is also very easy.

For example, a long grind uses the same principle - grind through two gap polys to complete the gap. Here's "Pipe Grind" from the Foundry.

It's done the same way, both gap polys should start and end gaps with different IDs, the only difference is you use different 'flags' on the StartGap.

You can use "PURE_RAIL" if you want the gap to trigger only if the player does one continuous grind through both gap polys. However, I prefer to give the player a little more leeway, so the script I would probably use would look something like this:

Code: [Select]
:i $StartGap$ $GapID$ = $PipeGrind01$ $FLAGS$ :a{ $REQUIRE_RAIL$ $CANCEL_GROUND$ $CANCEL_HANG$ $CANCEL_MANUAL$ :a}
:i $EndGap$ $GapID$ = $PipeGrind02$ $text$ = %s(10,"Pipe Grind") $score$ = %i(250,000000fa)

The "REQUIRE_RAIL" flag tells the game that the gap should only happen if the player has done a grind in the time between passing through the first gap poly and the second poly.. The various 'CANCEL' flags tell the game to cancel the gap if the conditions are met - CANCEL_GROUND cancels the gap if the player lands on the ground, CANCEL_HANG cancels it if the player hangs from the rail while off their board, etc. They are used to make sure the gap is done as intended.

Manual gaps work the same way, using the "PURE_MANUAL" flag, or a "REQUIRE_MANUAL" flag in combination with different "CANCEL" flags.

Wallride gaps also work this way ("PURE_WALL", "REQUIRE_WALL", etc.)


So far, we've dealt with gaps that only involve invisible gap polys. However, you can also assign scripts to rail nodes, which offers some more options for different kinds of gaps.
Please note that these can only be done with rail nodes, and cannot be done if you are selecting edges and simply marking them as rails.

Let's start with lip gaps as they are very simple - I'll also introduce a new argument, GetGap.

All you need to do is place a script like this on the rail node that's intended to trigger the lip gap.

Code: [Select]
:i $GetTriggerArray$$lip$$onto$
:i if $TriggerType$%GLOBAL%$trigger_array$
:i $GetGap$ $text$ = %s(11,"Bust Ya Lip") $score$ = %i(200,000000c8)
:i endif

There's some complex stuff there, but it just makes sure the gap only hits if the player does a lip trick on the specified rail node.

That's it! Gaps using GetGap scripts do not need a gap ID. GetGap scripts can also be used for Natas Spin gaps (change where it it says "$lip$$onto$" to "$spin$$onto$" for those).


Going back to our trusty StartGaps and EndGaps now...

Jumping from a ramp or a high platform, etc., to land in a grind can be done similarly, like the "Death Grind" in Downtown:

Jump off the kicker (through the gap poly) on the roof and grind the rail on the ground below.

Assign the gap poly as your StartGap:

Code: [Select]
:i $StartGap$ $GapID$ = $DeathGrind$ $FLAGS$ = :a{ $CANCEL_GROUND$ $CANCEL_MANUAL$ :a}
Then your rail node as your EndGap:

Code: [Select]
:i $GetTriggerArray$$grind$$onto$
:i if $TriggerType$%GLOBAL%$trigger_array$
:i $EndGap$ $GapID$ = $DeathGrind$ $text$ = %s(11,"Death Grind") $score$ = %i(2000,000007d0)
:i endif

There's the trigger array again, except this time it fires the EndGap if the player enters a grind on the rail node ("$grind$$onto$").

And there you have it! Easy.


Grind transfers are slightly different, and actually simpler in comparison, since they don't require trigger arrays, or invisible gap polys either - the scripts involved are applied only to the rail nodes.

Let's say I want to transfer from this rail to this rail:

The scripts involved are simple:

Code: [Select]
:i $StartGap$ $GapID$ = $GymRail2Rail01$ $FLAGS$ :a{ $PURE_AIR$ :a}
:i $EndGap$ $GapID$ = $GymRail2Rail02$ $text$ = %s(15,"Gym Rail 2 Rail") $score$ = %i(250,000000fa)

The reason we use PURE_AIR for this is that otherwise the StartGap would trigger on each new point of the rail node, and so the gap would show up multiple times in the trick string.

(Again, change the IDs accordingly for the script you would place on the other rail node.)


Longer grind transfers involving more than two rails can be set up by putting the StartGap on the first rail in the series, and the EndGap on the last one, but there's a more foolproof way of doing a gap like this, using $Continue$.

For example, these benches in Skate Heaven:

If you put the StartGap on the first bench and the EndGap on the last bench, the player could cheat it out by jumping and grinding the edge of the island and then jumping to grind the last bench.

Here's where $Continue$ comes in - and another concept known as "silent gaps". These are basically gaps that do not have 'text' or 'score' defined on the EndGap, so they don't show up in the trick string when completed. They are used for stuff like this to make sure a gap is completed in a specific way.

So, on the first bench in the series, you just need a simple StartGap script:

Code: [Select]
:i $StartGap$ $GapID$ = $Bench1_to_Bench2$ $FLAGS$ :a{ $PURE_AIR$ :a}
Now, the next four benches in the series will look different:

Code: [Select]
:i $EndGap$ :s{ $GapID$ = $Bench1_to_Bench2$ $Continue$ = :s{ $GapID$ = $Bench2_to_Bench3$ $FLAGS$ = :a{ $PURE_AIR$ :a} :s} :s}
There are some ":s{" in here, which go before both of the gap IDs. They have to be there, so don't forget them. Remember to close them at the end of the script too. Every continue script needs an ass at the end of it!

As you can see, the EndGap doesn't define a text or score, but it does define a "continue", which in this case behaves like a StartGap, except the player must have completed the gap defined by the EndGap first. Grinding from the first bench to the second bench then starts a gap between it and the third bench, and so on until the final bench, where we get back to more familiar territory:

Code: [Select]
:i $EndGap$ $GapID$ = $Bench5_to_Bench6$ $text$ = %s(13,"6 Bench Grind") $score$ = %i(600,00000258)
If you've done it right, now this gap will only trigger once the player has grinded all 6 benches in a row. If the gap can be done both ways, then you just need to add more scripts to your existing scripts to handle it (starting a "Bench 6 to Bench 5" gap on the last bench in this series, for example).



An EndGap can also have a "GapScript" argument defined. This just runs the specified script when the gap is completed.

The syntax is very simple, just add

Code: [Select]
$GapScript$ = $nameofscript$(where 'nameofscript' is the name of the script you want to run)

at the end of your EndGap.


Now for a small addendum - multiple gaps starting in the same place.

This is where the benefit of using custom scripts comes in.

For example, here is a series of gaps on the Chopper Drop:

The first gap poly starts 3 gaps:

Code: [Select]
:i $StartGap$ $GapID$ = $70ft$ $FLAGS$ = :a{ $PURE_AIR$ :a}
:i $StartGap$ $GapID$ = $80ft$ $FLAGS$ = :a{ $PURE_AIR$ :a}
:i $StartGap$ $GapID$ = $90ft$ $FLAGS$ = :a{ $PURE_AIR$ :a}

...and they are ended on their respective gap polys.

The same principle can be used for rails involved in multiple transfer gaps.

Let's say I have this:

...and I want to have gaps between each of the different tiers of rails. The middle rail would obviously be involved in a lot of different gaps. Here's what the script would look like for that:

Code: [Select]
:i $StartGap$ $GapID$ = $Middle2Bottom$ $FLAGS$ = :a{ $PURE_AIR$ :a}
:i $StartGap$ $GapID$ = $Middle2Top$ $FLAGS$ = :a{ $PURE_AIR$ :a}
:i $EndGap$ $GapID$ = $Bottom2Middle$ $text$ = %s(15,"Bottom 2 Middle") $score$ = %i(100,00000064)
:i $EndGap$ $GapID$ = $Top2Middle$ $text$ = %s(12,"Top 2 Middle") $score$ = %i(100,00000064)


I hope this tutorial was easy for you to understand. If it's not, you can find me in the Discord or you can reply to this thread, or send me a PM, whatever. I'll be happy to help you out with your gap-related woes.

I'm definitely far from being an expert so some of this stuff might be wrong! I'm always learning new stuff about making custom levels, scripting, and the like, so please don't hesitate to call me out! I'll look into the issues and figure out what's wrong.
« Last Edit: September 09, 2019, 09:24:44 PM by rodafy »

Offline ThAEm

  • Donor
  • Jr. Member
  • *
    • Posts: 82
  • It's pronounced "Th-aim".
    • View Profile
OK, this is actually a really good tutorial! you explained the basic stuff and even went in detail about two-way gaps and placing gap-triggers on rails, which is pretty neat.

I'm not sure if you know this, but there also exists an argument called $GetGap$. when triggered, it will just give the player the gap. this can be used if there is no definite starting point for your gap, like a Natas-Spin gap or Wallplant gap. a GetGap script is structured similar to an EndGap script, but you only define the score and gap name.

Code: [Select]
:i $GetGap$
    $score$ = %i(25,0)
    $text$ = %s(0,"Gap!")
"We underground pro, can't go gimmicky" -Frog One

Offline rodafy

  • Upload Approved
  • Newbie
  • ****
    • Posts: 14
    • View Profile
Oh, cool. I didn't know about that! It probably makes lip gaps easier too... I'll test it more soon. Thanks :D

I've added some information about $Continue$ arguments, and how to use the $GapScript$ argument.