A Boojum Snark
- Nov 2, 2007
I don't believe he expected me to do it, or if he had any bit of expectation the statement was probably in jest. None the less, I deemed it possible and jumped to the challenge without telling him.Penguin: We need to do some more time constraint contests on you
Penguin: you finally produce stuff then
Penguin: Challenge: Make a racing game using Rexy's Chairman model. You have just 48 hours.
A Boojum Snark: but you realize, none of them were aimed at me.
Penguin: I know.
Penguin: But they should be.
and so, I give you...
>> ZIP | BZ2 <<
About the map:
There are four Chairman cars. You may race them, or just drive them down the road.
Car features include:
• 100% free-roaming control mode.
• In-car 360 degree first-person view.
• Third-person chase-cam toggle.
• Speed-sensitive pitch-shifted engine sound.
There is a starting signal you may use for racing with friends, though it does not restrict movement! Feel free to verbally abuse friends who begin prematurely.
The first car across the finish is also recognized with a celebratory trophy, celebratory confetti, and celebratory cheering from a disembodied crowd.
The cars are capable of passing through nearly every type of map object. Therefore travel has been restricted to the road by rigging up a kill mechanic if you drive into the trees. Please stay on the road for your own safety.
Player models were left visible on purpose, despite sticking through the floor and roof of the car.
Thanks to Rexy for making the excellent model of the Chairman.
IMPORTANT EDIT: Apparently if anyone is a car when waiting for players ends, it will crash. Seems to be a fourth player-exit scenario I did not think of because I always skip that timer. Use mp_waitingforplayers_cancel 1 to cancel it so you can play on your own.
Also, the map does not appear to function properly on dedicated servers, so don't try running it on one. I will investigate this issue later.
/end important edit
If you use the eject, suicide, or get killed while in the chase-cam view, you will NOT exit the camera view. This appears to be an issue with the way things are coded and I was unable to avoid it. The chase-cam was too neat of a feature to leave out due to this one thing, and the issue could be partially fixed by giving the camera a timeout, which I didn't opt to do since this is mostly a proof-of-concept and can't be taken seriously.
This does not occur if you crash into the trees and die.
Colliding with other cars only occurs because of contacting the other player. It may or may not cause a jam, but in my experience it usually does not. But do try to keep your distance for courtesy.
When inside the car, the camera will clip through the roof if you are a Heavy, Medic, Sniper or Spy. The reason is their camera positioning is higher than other classes, and I didn't want to make the others lower to fix the clipping.
The entity mechanics:
A few small notes:
• the game_ui entity
This is what allows us to interface with the commands the player sends. It has outputs such as OnPressedAttack and OnUnpressedMoveLeft. A fairly simple entity in concept, just send it an Activate and it goes to work.
• naming the player entity
When the player activates the game_ui, they are given an entity name via the AddOutput input with a parameter of targetname racer_#_player, where # is the number for that particular car. When they leave the car, this is "reset" by simply re-naming them "null". This is not a special name, just what I use. It could also be bob or uselessplayer or awlnignlis.
The Core: the free-roaming train
This is where the biggest piece of magic resides. Trains can only go on predefined paths, right? Not quite...
A path_track cannot be parented because, like several things, they cease to work when you do. However, nearly all such things can be pseudo-parented with the wonderful entity logic_measure_movement. This entity will track the relative motion of one entity and correspondingly move another. In this case, the func_brush in the image is being measured, and the "goal" path is being moved along with it, effectively parenting without destroying it.
Now a func_brush doesn't do much, so it is parented to the momentary_rot_button, which is in turn parented to the func_tracktrain (not shown). Once the train passes the starting paths, it will attempt to reach the goal path, but be unable to since it will always be moved further away. A wonderful carrot-on-a-stick mechanic.
All we have to do now is link the aforemention game_ui's OnPressedForward output to make the train move forward, and OnPressedMoveLeft/Right get linked to the m_r_b which will shift the goal path to the side, causing the train to turn in an effort to chase it. It is important to note the train responds very quickly to this, so in my setup the m_r_b only moves 2.5 degrees to the left or right, and the train still has a tight turning radius if moving slowly.
I did not wish to implement a move backward feature, but this could be accomplished by having another path between the two mentioned, which gets logic_measure_movement-linked only after the train has passed it, resulting in a double-ended carrot-on-a-stick.
The speed-sensitive pitch-shifted sound
Now, before you say it, yes, the func_tracktrain "move sound" can do this by itself. However the sound only plays while the train is in motion, which would be pretty dumb to have the engine sound starting and stopping every time you do! So I set out to do it manually...
Here we make use of the point_velocitysensor entity. The problem is it can only measure along one axis. However, if we set two p_vs to measure directly along the X and Y axes, there is a right angle between them we can use to solve for the real speed.
The good ol' Pythagorean Theorem says legA² + legB² = hypotenuse², so I have each p_vs first set it's value into a math_counter, then send the value again as a multiplication function, resulting in the square. Each of these counters then sends its value to a third where they get added, resulting in the square of the true speed.
Now we don't have the means to easily take the root of a number, but this isn't needed since we scale the number into the desired pitch range via a math_remap anyway. It ends up slightly inaccurate, but it's better than stop-and-start sound.
Additionally, I have the p_vs' disable themselves every time they send an output, to give the math a chance to compute, and then reenable themselves. This is because they are one of a handful of entities that spew outputs multiple times per second whenever they are active, which would cause the counter to get out of sync.
Because a func_tracktrain is used, there is very little that can impede it's progress as it has very little physical interaction with the world. After all, it's meant to stay on pre-defined routes. It will collide with physics-based objects, but such collisions are very "sticky" and not ideal for use when the player is driving the train, let alone racing.
I opted for a more malevolent method: simply kill the player. This is not as simple as one may expect though, the player has been parented to the train, and any parented objects lose all trigger interaction, meaning a trigger_hurt just will not do.
What we have here is a func_physbox which will follow the train around by use of another logic_measure_movement. The important part of this is the material used: toolsnpcclip. It is important because it is non-solid to pretty much everything (no NPCs in TF2), but it retains trigger-interaction, which a lot of non-solid things/materials do not.
Now we have an object on our train/car which can interact with boundary triggers, but how does that help us kill the player? This is where the rarely used but irreplaceable UserIO come into play. All the boundary triggers are given the output OnStartTouch > !activator > FireUser1, and then the physbox for each car is given an output of OnUser1 > racer_#_player > SetHealth > -10000. In this way we can use the physbox to "forward" the input along to the correct player, since the trigger does not know who it is.
The player exit fail-safes
There are three ways the player can disengage from the car system:
• Deactivating the game_ui by hitting jump (one of the game_ui flag options).
• Dying, either by suicide, another player, or out-of-bounds.
• Leaving the server.
Each of these presents unique situations that must be handled accordingly, and was in fact the most challenging part of this whole setup.
The first is the easiest as game_ui has the PlayerOff output we can use to disable all the various systems related to the car, be it lights or sounds or cameras.
Death presents an interesting case, because dying does not deactivate the game_ui you are linked to. To handle this there is a trigger_multiple over the player that is filtered via filter_activator_name to the corresponding racer_#_player. This has two OnEndTouchAll outputs.
It will trigger the relay shown there, which in turn tests a logic_branch. The relay is used as an intermediary because if the player leaves the trigger by directly deactivating the game_ui we don't want any of this to happen, so the game_ui disables the relay on PlayerOff.
The purpose of the logic_branch is to then determine whether it was a death or a disconnection. This is important because of a bug in the game_ui code: trying to deactivate an inactive one will cause a crash. As said previously, dying does not deactivate, but obviously leaving the server would.
How we make this determination is by actually using the player to send an output. When they first boarded the car, AddOutput was used to give them OnUser1 > racer_#_suicidebranch > SetValue > 1 (1 = true = suicide/death, not disconnect). Back to the trigger, you'll see that before it fires the relay it attempts to tell the player (represented here by an info_null, an entity which is removed during compile) to FireUser1. Therefore if the player disconnected and does not exist, the output will never be sent and the branch will remain false, causing it to only reset the car and not attempt to deactivate the game_ui r release the player.
The chase-cam, a minor snag
I found that the point_viewcontrol is yet another entity that ceases to function properly when parented. Once again I used a logic_measure_movement to link it to the car, except this time I used an info_teleport_destination as the tracking entity because it's a simple point entity but still has angle info, which is necessary for the camera to be aligned properly as the angles of something an l_m_m is moving do not matter, they get matched to the tracked entity's angles. The i_t_d is also parented to the m_r_b used for steering, to give a slight sway in the camera when turning.
There may still be a few obscure bugs lurking amidst this complicated system. I don't recommend running it on a heavily populated server since there are only 4 cars and no means to prevent havoc from ensuing. It is mostly as a proof of concept and having a little fun with friends.
Yes, I did manage it in under 48 hours I spent probably 6-7 hours hunting down the player-exit crashing bugs. This post was all written up after the 48, though.