Rise, Darth Freeman
Single-Player Half-Life 2 Level || Source Engine, Half-Life 2 Hammer Editor || 8 Weeks || 250 Hours
The Hammer Editor's Visual Scripting system uses scripting Entities that the developer literally places throughout the world, connecting them together through reference lists to carry out logic sequences. These entities can do everything from comparing numbers to redirecting specific player inputs. However, because this version of the Hammer Editor was originally designed for older Source games, there are special restrictions scattered within the Entities' applications. Overcomming these restrictions was a major part of scripting the new mechanics.
The two mechanics I created with the Visual Scripting system were the Lightsaber Weapon and Force Powers. As part of the original Guildhall class, I also wrote a Wiki tutorial on how to add Force Powers to the default Half-Life 2 player, viewable here.
The Lightsaber replaces all of the players guns and melee weapons, desintegrating enemies in one swing but at extremely close range. It can also deflect bullets back at enemies, killing them and igniting explosive objects in the environemnt.
The BSP Blade
The Lightsaber's handle and laser blade are both constructed out of BSP, or basic shapes the Engine composes out of developer-defined points in 3D space. The Hammer Editor tools made composing the Lightsaber's physical appearance very easy, and the tricky part was finding and mapping appropriate textures.
The yellow arrows at the base of the handle are the func_rot_button entities, which create the blocking and swinging animations when the player uses the weapon. The Lightsaber itself, which is a single func_brush Entity, is re-parented to the correct rotator object depending on which action the player triggers.
The trigger boxes that surround the blade allow the weapon to kill enemies, apply push forces to hit physics Entities, and light certain objects on fire.
Saber Combat Scripting
To forge the Lightsaber from a pretty collection of colored polygons into an elegant weapon from a civilized age, it needed both swinging animations and damage triggers. There are two main script actions the Lightsaber uses to function,
Swinging and Blocking.
To get the Lightsaber kill people, I simply parented a Physics Trap trigger box to the blade that activates when the player clicks the left mouse button. However, this is useless unless the weapon actually swings.
To make the Lightsaber swing, I parented it to an invisible func_rot_button Entity that was parented to the Player. This button 'pushed in' whenever the player clicked, causing the entire Lightsaber to swing forward. The swing logic sequence swings the weapon, has a short delay, and then unswings it, all within about 0.4 seconds.
The toughest aspect was preventing the player from re-swinging before the last swing finished, as this accumulated a rotation offset and make the weapon useless over time. To fix this, the entire swing logic sequence begins with a special Logic Relay node that deactivates until the swing action has completed.
Similar to the Saber Swing, the Block action itself is created by parenting the Lightsaber to an invisible func_rot_button that moves the Lightsaber when the player holds down the left mouse button.
The Saber Block is, however, far more complex than the Saber Swing. Right off the bat, having an action occur when a button that does other things is held down was fairly difficult. Basically, when the player clicks the left mouse button, a large Lightsaber logic chain prepares itself to fire. After 0.3 seconds, a timer checks to see if the button has been released yet, stored in a seperate logic Entity. If the button has been released, the Swing logic sequence is triggered. If not, the Swing sequence is locked down until the button is released again, and the Block logic sequence is triggered. This logic chain rotates the Lightsaber model by 45 degrees and summons a special deflecting shield Entity in front of the player.
The Bullet Deflecting Shield is composed of several entities. There is a Bullet Shield trigger that detects when bullets hit it, which stops the projectiles from hitting the player and keeps track of changes in its health. When its health changes, meaning a bullet has tried to hit the player, it fires a new bullet back towards the center of the player's screen. Please note, in the Hammer Editor there is neither a method of redirecting an actual bullet nor manually firing visible bullets that can damage enemies.
To 'deflect' a bullet back towards the attacker, I needed two seperate Entities. One spawns a particle burst of bullets that visually implies bullets are being deflected. The other fires an invisible projectile to a point far in front of the player that can hit and damage specified enemy Entities. Combining these together forms an awesome effect where bullets stop and fire back towards aggressors.
The cherry on top of the Saber Block was a small twitch the Lightsaber makes when deflecting each bullet. Whenever the bullet shield stops a projectile, the script triggers a special rotator button to quickly press in and out within 0.15 seconds, creating a quick flicking motion that resembles how Jedis actually deflected incoming fire. All of these visual and damaging effects allow the Lightsaber to stop and deflect incoming fire in a responsive and sexy way.
The Force Powers
To make the Player a Jedi, they need Force Powers. I originally implented Push, Jump, Dash, and Lift, but Lift ended up being cut due to rare use. All of these mechanics were prototyped and impented during the first 2 weeks of the original project.
The Force Push mechanic was the main reason I wanted to turn the Player into a Jedi. Since Source uses the older, quirkier Havok Physics Engine, I wanted to let the player throw objects around the environment, giving them a sense of chaotic power that was slightly unpredictable in an amusing way.
The Force Push takes advantage of several funnel-shaped trigger_push Entities that are parented to the player. They are all by default inactive, but turn on individually when the player uses the ability, the specific active trigger determined by the charge level. Why use multiple triggers with different shapes, sizes, and push forces instead of a single one that is updated, you ask? That was one of the most annoying restrictions in the Hammer Editor's Scripting system. Entity variables cannot be updated at runtime. You need a new Entity for every different physics application.
Now, when the player presses down the right mouse button, they start a Timer that calls a sequence of logic_case Entities, setting up each specific trigger_push Entity to be activated at each charge interval. The moment the Player releases the button, a seperate logic chain fires off, using the currently enabled charge-level logic_case Entity to enable the right trigger_push, applying different levels of physics forces to dynamic objects and enemies in the environment.
There are also two special trigger_push Entities, designed specifically for massive objects in the environment that require a full charge to affect. These objects tended to have a much higher mass than standard physics-enabled clutter, and as such needed special trigger_push Entities with a much higher Push Force.
Each seperate charge level logic_case Entity also calls a specific env_shake and ambient_generic Entity, creating an increasingly instense rumbling sound and Camera Shake effect as the player charges up their Force Push. The charge level is indicated by a small set of bars in the HUD, found in the lower-right portion of the screen.
You'd immediately think, "Just increase the Player's Jump height", right? Unfortunately, the Hammer Editor did not make this Player modification easy. There is no way to update the Players default jump height, so to offer a higher jump, I spawn a special push_trigger, called the Jedi Jump Trigger, under their feet when they press the space bar.
Every time the player Jumps, they propel themselves upward a few feet, become fully affectable by physics forces, and, with my new scripts, summon a special Jedi Jump Trigger beneath them that quickly applies a large impulse upwards and dies. This new trigger allowed players to jump to much greater heights, and is immune to the lack of Player rotation tracking since their jump direction is always straight up. Since the trigger spawns under the Players feet regardless of where they are, this new mechanic evolved into a double-jump. When combined with the Force Dash, this mechanic gives Players much more potent traversal options in open areas.
The one major flaw with this method was its exploitability. There was no logic check to determine if the Player should be allowed to spawn the Jedi Jump Trigger, and so several playtesters would simply jump forever and fly over the entire level. However, there is no way to know if the player is grounded in the Hammer Editor. To make the Jedi Jump viable, I created a blanket of 'Ground Triggers' over the entire level on every jumpable surface, and scripted the Player Entity to use them as an indicator for when they were grounded or airborne. A few systems, including the Force Dash, backpacked off of this blanket of triggers.
The Force Dash is a small boost in the direction the player is currently facing. It lets them quickly move through the environment, and allows players to rapidly close the distance between themselves and the enemies. This mechanic combines well with the Force Push, as the player can boost right up to a group of enemies while charging the Push and immediately blow them away. The Dash has a 3 second cooldown that resets when the player jumps or lands, encouraging more vertical movement.
The Force Dash implementation is a little more crude than the Force Push mechanic. There exists no method of applying physics forces to the Player Entity that update their direction with the Player's rotation. As such, I parented a fun little Entity called phys_explosion to the Player, positioned right behind their back. Triggering the phys_explosion Entity emits a force in all directions that can be applied to specific Entities. Because this force is radial, as long as it was always behind the Player, the explosion would propel them 'forward'. This ended up working fairly well, but needed fine tuning to be a viable mechanic.
In Half-Life 2, the Player Entity is affected by physics forces differently in the air than on the ground. As such, before the updates, the phys_explosion would propel them a short distance on the ground, and send them to the moon in the air. To solve this, there are two seperate phys_explosion entities behind the Player that toggle off and on depending on if the Player is currently grounded. Determining if the player was grounded took a seperate logic system that I created for the Force Jump.
Other Jedi Powers
There were a few smaller scripted Jedi abilities that impacted gameplay. The Player has the ability to both heal over time and sprint indefinitely, and is also immune to all forms of falling damage.
Every 3 seconds, if the player is not blocking, they receive a 5 health boost. This sounds like a lot, but in later areas really helped players overcome the difficulty curve. This ability is never revealed to the player, giving them a subtle sense of invulnerability and rewarding smart gameplay. If players feel that their health is getting low, they typically run and hide, giving the Heal ability some time to fix them up. This ability is also balanced such that if shot by 3 or more soldiers within 10 feet of them without moving that much for more than 5 seconds, their health indicator will start to turn red. This means that players are by no means unstoppable and feel threatened, but have the ability to come back from the brink of death and keep playing. Even though there are almost no health pickups in the level, most players don't even notice that their health refills.
I created this ability by having a timer on the Player Entity that triggers every 3 seconds, checks to see if the player is blocking, and if not, spawns a small damage trigger around them that has a negative damage value (thus adding the health instead of subtracting) which dies after 0.5 seconds. Since the triggers update their overlaps every 0.5 seconds or so, this ensures that the Player always receives exactly the right health per trigger spawn.
This one was fairly easy, as the standard Half-Life sprint mechanic is based on the Players 'Stamina' value. Basically, I used a special Entity that executes system commands to give the player infinite stamina when the level starts.
Infinite Force Sprint
Fall Damage Immunity
This one had a simple solution that was tricky to find. I was tired of the Force Jump killing players since the new jump height was high enough to trick the player into thinking they had fallen from a high platform, and as such tried to make the Player invulnerable for the frame that they hit the ground. However, I discovered a special Entity that cancels out certain categories of damage, and one of the options was Fall-based. I attached it to the player, called the logic at Map Spawn, and the Player has never been hurt by a fall since.