Buy Elder Scrolls Online: Greymoor for PC at Green Man Gaming

21Gorge behind the scenes: Why a control point map needs NPC entities

Discussion in 'Tutorials & Resources' started by Mikroscopic, Apr 3, 2020.

  1. Mikroscopic

    aa Mikroscopic

    Positive Ratings:
    For Startacker's 2020 April Fool's Day contest, I created a map named cp_21gorge. It's an edit of 5gorge but with 21 control points instead of 5. I actually came up with the idea for a control point map with an ungodly number of points several years ago, but was never able to make it due to TF2's hard-coded limit of 8 control point entities per map. This year I found myself without any new ideas, and almost considered sitting this year out. But I couldn't stand the thought of not submitting anything to an April Fool's Day contest, so at the beginning of March I finally decided to sit down and figure out how to make a map with more than 8 control points.

    Bypassing Engine Limits

    Most experienced mapmakers will be aware that the game will crash if you try to put more than 8 team_control_point entities in the same map. It simply isn't possible to have more than 8 points. So how did I make a map with 21?

    The answer is that there actually aren't more than 8 team_control_point entities. In fact, there are only 5 - the same as the original map! Most of the control points in the map are faked: the holograms are prop_dynamics parented to func_rotating entities, and the capture sounds are made using the open / close sounds of invisible doors located below each point.


    Each trigger_capture_area is in fact linked to a real control point - it's just that the point they're linked to changes each time a point is captured. At any time, only the two unlocked capture areas on the front lines are linked to the active points. When a point is captured, it is immediately set back to its previous owner using SetOwner, and the capture zones shift. Only when either team pushes all the way to the final capture area does the real, actual, final control point for the defending team become unlocked.

    Creating the HUD

    The next problem was to figure out how the hell to show a HUD for this. Obviously, I couldn't get the game to show an icon for all 21 points, since 16 of them don't even exist. Instead, I used env_screenoverlay to display a custom image on screen mimicking the control point HUD. Whenever a point is captured, it updates a math_counter keeping count of how many points Red owns, which sends that number to a logic_case that updates the overlay to the corresponding image.

    Of course, I still wanted players to be able to see the capture progress for the two active points, and I couldn't do that with static images. So I decided to still show the two real points on the real HUD below the overlay. I realized that trying to use inputs to change the "Hide Control Point on HUD" flag didn't work, so the next best solution I came up with was changing the HUD layout to put the three other points on a new row and scooting the whole thing down so they became hidden off the bottom of the screen. I used "Custom cap position Y" to change the vertical location of the HUD and "SetCapLayout" to change the custom layout shape. Which brings me to the next section...

    Changing the Cap Layout

    So, the HUD layout is controlled by the team_control_point_master entity, which has a keyvalue called "Cap Layout" that lets you specify a custom shape for the way control point icons are displayed on the HUD. This is formatted by using the index number for each control point (0 1 2 3 and 4) with commas to separate different rows. For example, a layout string of " 0 1, 2 3 4 " would display the cap icons in a pyramid with the first two points at the top and the rest below. This is exactly what I wanted: two icons on top, and the rest below them, to be shoved off screen. But I also needed to be able to change the cap layout based on which two real control points are currently unlocked.

    In total, there are 4 layouts I needed to switch between:

    Layout Use
    0 1, 2 3 4 Blu's final point contested
    1 2, 0 3 4 Somewhere in the middle, Red made the previous capture*
    2 3, 0 1 4 Somewhere in the middle, Blu made the previous capture*
    3 4, 0 1 2 Red's final point contested
    *Mid (point 2) is owned by whichever team has just captured.

    Luckily, team_control_point_master has an input called "SetCapLayout" which allows this string to be changed. However, there is one major oversight with this design:

    Commas are also used to separate parameters of an input in a vmf file.

    To understand why this is a big problem, consider this example: say we want to send an input to change the HUD layout so that it displays points 1 and 2 on top, with points 0, 3, and 4 on the bottom.

    My Output Target Entity Target Input Parameter
    OnTrigger team_control_point_master SetCapLayout 1 2, 0 3 4

    If you were to compile the map and fire this input, the HUD would display points 1 and 2, and then... scatter the rest of the point icons wherever it felt like. Sometimes overlapping each other. Why does this happen? Let's look at the map file in a text editor:

    "OnTrigger" "team_control_point_master,SetCapLayout,1 2, 0 3 4,0,-1"
    As you can see, the line that stores the input uses commas to separate the different fields of the input. This means that the only part of the layout string it is able to interpret is what comes before the comma - anything after that gets parsed as a different field, like delay time. Therefore, the layout string that gets passed to the team_control_point_master is "1 2" and it doesn't know what to do with the rest of the points.

    How did I manage to find a fix for this? Some amazing people in the TF2maps Discord (@Pdan4 @Sarexicus @Narpas @B!scuit @14bit) helped me figure out a solution.

    I couldn't put the cap layout string anywhere in an input, but I could put it in a keyvalue. All I would need is an entity that was capable of passing one of its keyvalues as the parameter for an input. A math_counter is such an entity, but it's only capable of passing numerical values, not strings. So what entity did I end up using? This is where things get a little bit weird.

    generic_actor is an entity that acts as a generic NPC for scripted sequences. You won't find this entity in TF2 hammer - you'll have to install the HL2 FGD to see it, but it does work in TF2. Why is it important? It has an output called "OnFoundEnemy" that gets fired whenever it establishes a line of sight with an enemy NPC. This output returns the targetname of the entity that it sees. And you know what? Targetnames can have commas in them!

    And so, here is my HUD logic:


    This is a secret chamber (I call it the dungeon) hidden below the map that has two rooms. In one room, there are four NPC entities (the tiny white balls). Each one is named after a different cap layout: '0 1, 2 3 4', '1 2, 0 3 4', '2 3, 0 1 4', and '3 4, 0 1 2'. In the other room is the watcher NPC (the skull). There is an ai_relationship entity that makes the watcher hate all of the layout NPCs. When I want to change the HUD layout, I teleport one of the layout NPCs into the watcher room. The watcher sees it, fires the OnFoundEnemy output and calls SetCapLayout on the team_control_point_master, passing the targetname of what it saw. Then I immediately teleport the layout NPC back into its room.

    The result is seamless and most players probably have no idea how much effort went into such a simple feature. Now you know!


    Oh, and if you see Jill, tell them to add escape characters for God's sake!
    • Like Like x 23
    • Thanks Thanks x 4
    • Agree Agree x 1
    Last edited: Apr 3, 2020
  2. Malachite Man

    Malachite Man L6: Sharp Member

    Positive Ratings:
    • Thanks Thanks x 1
    • Agree Agree x 1
  3. Pdan4

    Server Staff Pdan4

    Positive Ratings:
    I'm really amazed you actually did keep using the skull model. Fantastic.
    • Agree Agree x 2
  4. Kube

    aa Kube Soon it will be different

    Positive Ratings:
    any sufficiently advanced bullshittery is indistinguishable from logic
    • Agree Agree x 3
  5. Micnax

    aa Micnax Back from the dead (again)

    Positive Ratings:
    i'm amazed
  6. fubarFX

    aa fubarFX The "raw" in "nodraw"

    Positive Ratings:
  7. puxorb

    aa puxorb L69: Emoticon

    Positive Ratings:
    That was quite the entertaining read. I love it when mappers have to brute force the engine to get something to work, often in the most jank ways possible.
  8. MegapiemanPHD

    aa MegapiemanPHD Doctorate in Deliciousness

    Positive Ratings:
Buy Elder Scrolls Online: Greymoor for PC at Green Man Gaming