Not satisfied with new art? Add new behavior!
Other hacks demonstrate how to model a 1969
Plymouth Barracuda (
[Hack #82]
)
and how to turn it into a playable Actor in Unreal
Tournament 2004 (
[Hack #83]
). This
hack builds on that to add nitrous-style speed boosts to the
‘Cuda using UnrealScript.
While it isn’t required, you’ll have better luck understanding this hack if you understand high-level object-oriented languages such as Java or C++. Being familiar with vector math and basic physics will help, and a working knowledge of UnrealScript and how “replication” (the Unreal networking idiom) works is also beneficial.
Download the source code mentioned in this hack as well as the art packages necessary to use this example from http://www.demiurgestudios.com/CudaExample/.
Nitrous is short for nitrous oxide, a gas that produces a significant horsepower boost when injected into a gasoline engine. Street and drag racers often use nitrous to add speed boosts at key moments. In general, you cannot carry enough nitrous oxide to use it at all times. For more information on how nitrous oxide works, see its How Stuff Works page at http://auto.howstuffworks.com/question259.htm.
In this example, you’ll simulate the use of nitrous for the ‘Cuda. You’ll add a short speed boost when you click the left mouse button (by default, the AltFire key). During the boost, the ‘Cuda will accelerate rapidly, and flames will shoot out of the tailpipes. Despite the fact that the amount of nitrous a car can practically store is relatively small, the ‘Cuda in this example will have 50 nitrous hits just because it is more fun!
To simulate nitrous, we will use a very
simple approach: apply a large force to the back of the car when the
nitrous is on. Of course, you should apply the force only when the
car is on the ground, or else the car could fly. You accomplish this
by applying the force only if bVehicleOnGround is
true.
The next consideration is that the force of the nitrous should depend
on the force with which the tires press against the ground. If you
don’t account for this, the ‘Cuda
will be able to drive straight up walls;
bVehicleOnGround is true if its wheels touch
anything! To deal with the force on the wheels,
we average the TireLoad of all the wheels. Then
cap the average tire force to keep the nitrous force under control if
there is a lot of downforce on the tires, as there is after a jump.
Once you’ve calculated the magnitude of the force,
apply it in the direction the car is pointing.
All this code lives in the KApplyForce function.
Unreal calls this function to give Actors a chance
to adjust the forces applied to them. Note that you never assign a
value to Force; you just add to it. If you assign
a value to Force, that value overrides all the
other forces the engine applies to the car. Here is the function for
adding the nitrous force as well as some of the variables used by the
nitrous system:
// Nitrous
// Set to true while pending and during a nitrous boost
var bool bClientDoNitrous;
// How much force to apply per-tick
var float NitrousForce;
simulated event KApplyForce(out vector Force, out vector Torque)
{
local int i;
local float avgLoad;
Super.KApplyForce(Force, Torque); // apply other forces first
// If the car is nitrousing and vehicle is on the ground
if (bClientDoNitrous && bVehicleOnGround)
{
// apply the nitrous force as a function of how much grip
// each wheel has
avgLoad = 0;
for(i=0; i<Wheels.Length; i++)
{
avgLoad += Wheels[i].TireLoad;
}
avgLoad = avgLoad / Wheels.Length;
// cap avgLoad with experimentally determined value
avgLoad = FMin(avgLoad, 20.0);
// normalize avgLoad factor with respect to cap
// it can only reduce the nitrous force, never increase
// it beyond 100%
avgLoad = avgLoad / 20.0;
// add forces to any existing forces already being applied
// DO NOT OVERWRITE PREVIOUS FORCE VALUES
// get direction of hot rod and apply force in that direction
Force += vector(Rotation);
Force += Normal(Force) * NitrousForce * avgLoad;
}
}
defaultproperties
{
...
// Nitrous
NitrousForce=250.000000
...
}The code that follows, working with the code described earlier, does the triggering and counting of nitrous. The triggering mechanism is slightly confusing because it has to work in a multiplayer game.
When the server detects an AltFire press (done in
VehicleFire), it calls the
Nitrous function. This function checks to see if
you’re not using nitrous and if you have any nitrous
left. If this check passes, it plays the nitrous sound and decrements
the number of nitrous hits remaining. The most important part is that
it sets bClientDoNitrous to
true. The server replicates
bClientDoNitrous to each client so that everyone
in the game knows that a particular car has fired its nitrous.
The KApplyForce function, shown previously, uses
the bClientDoNitrous value to determine if it
should apply the nitrous force. Also, Tick, which
the game calls every frame, checks the value of
bClientDoNitrous and sets a timer to time how long
the nitrous should last (DoNitrousTime). Once the
designated amount of time has passed, the Timer
event fires, turning off the nitrous by setting
bClientDoNitrous to false.
// Nitrous
var float DoNitrousTime; // How long to boost for
var int NitrousRemaining; // How many nitrous shots left in this car.
var ( ) sound NitrousSound; // Sound when nitrous is fired
replication
{
reliable if(bNetDirty && Role= =ROLE_Authority)
bClientDoNitrous, NitrousRemaining;
}
function VehicleFire(bool bWasAltFire)
{
if(bWasAltFire)
{
Nitrous( );
}
else
Super.VehicleFire(bWasAltFire);
}
function Nitrous( )
{
// If we have any left and we're not currently using it
if(NitrousRemaining > 0 && !bClientDoNitrous)
{
PlaySound(NitrousSound, SLOT_Misc, 1);
bClientDoNitrous = true;
NitrousRemaining--;
}
}
simulated event Tick(float DeltaTime)
{
Super.Tick(DeltaTime);
// If bClientDoNitrous and pipe fire don't agree
if(bClientDoNitrous != bPipeFlameOn)
{
// it means we need to change the state of the car (bPipeFlameOn)
// to match the desired state (bClientDoNitrous)
EnablePipeFire(bClientDoNitrous); // show/hide flames
// if we just enabled pipe flames, set the timer
// to turn them off after nitrous time has expired
if(bClientDoNitrous)
{
SetTimer(DoNitrousTime, false);
}
}
}
simulated event Timer( )
{
// when nitrous exceeds time limit, turn it off
bClientDoNitrous = false;
}
defaultproperties
{
...
// Nitrous
DoNitrousTime=2.000000
NitrousRemaining=50
NitrousSound=Sound'WeaponSounds.Misc.redeemer_shoot'
...
}The final bit of polish to add to the nitrous
system is tailpipe flames. I’ve created a tailpipe
fire particle system found in the class
CudaPipeFire.uc, as found with the rest of
source described here. Unfortunately, talking about how to create
particle systems in Unreal is outside the scope of this hack.
The first step is to create the tailpipe flames and stick them on the
tailpipes. The PostBeginPlay event does this. The
server calls this function after creating the ‘Cuda.
Use the spawn function, which makes new objects in
the world, to create the flames. Next, attach the flames and then
rotate them correctly. Once this is done,
EnablePipeFire(false) disables all the emitters so
the tailpipe flames look like they are off.
It works better to just turn the flames on and off as opposed to
creating and destroying the flames each time they are needed. The
bClientDoNitrous variable in the
Tick event controls their visibility. When the
CudaCar is destroyed, the server calls the
Destroyed event, which destroys the flames so that
you’ll never have nitrous flames left after a
‘Cuda blows up.
The following code is specific to tailpipe flames. Keep in mind that some of code presented earlier deals with tailpipe flames as well.
// Fire
var ( ) class<Emitter> TailPipeFireClass;
var Emitter TailPipeFire[2];
var ( ) Vector TailPipeFireOffset[2];
var ( ) Rotator TailPipeFireRotOffset[2];
var float PotentialFireTime;
var bool bPipeFlameOn;
var ( ) Sound TailPipeFireSound;
simulated event PostBeginPlay( )
{
Super.PostBeginPlay( );
// Dont bother making emitters etc. on dedicated server
if(Level.NetMode != NM_DedicatedServer)
{
// Create tail pipe fire emitters.
TailPipeFire[0] = spawn(TailPipeFireClass, self,, Location +
(TailPipeFireOffset[0] >> Rotation) );
TailPipeFire[0].SetBase(self);
TailPipeFire[0].SetRelativeRotation(TailPipeFireRotOffset[0]);
TailPipeFire[1] = spawn(TailPipeFireClass, self,, Location +
(TailPipeFireOffset[1] >> Rotation) );
TailPipeFire[1].SetBase(self);
TailPipeFire[1].SetRelativeRotation(TailPipeFireRotOffset[1]);
EnablePipeFire(false);
}
}
// Enable/disable pipe fire effects
// via passed bool Enable
simulated function EnablePipeFire(bool bEnable)
{
local int i,j;
// enable/disable emitters
if(Level.NetMode != NM_DedicatedServer)
{
for(i = 0; i < 2; i++)
{
for(j = 0; j < TailPipeFire[i].Emitters.Length; j++)
{
TailPipeFire[i].Emitters[j].Disabled = !bEnable;
}
}
}
bPipeFlameOn = bEnable; // update state of pipe flames
}
simulated event Destroyed( )
{
if(Level.NetMode != NM_DedicatedServer)
{
TailPipeFire[0].Destroy( );
TailPipeFire[1].Destroy( );
}
Super.Destroyed( );
}
defaultproperties
{
...
// TailPipeFire
TailPipeFireClass=Class'CudaPipeFire'
TailPipeFireOffset(0)=(X=-140.000000,Y=20.000000,Z=-16.000000)
TailPipeFireOffset(1)=(X=-140.000000,Y=-20.000000,Z=-16.000000)
TailPipeFireRotOffset(0)=(Yaw=32768)
TailPipeFireRotOffset(1)=(Yaw=32768)
...
}The ‘Cuda is now ready to go. The example download
provides the map ONS-CudaExample. You can play in
single player or multiplayer.
Once you are accustomed to how the CudaCar plays,
consider adding or changing parts. For example, try adding
bAllowAirControl=true to
defaultproperties to allow you to control the
orientation of the car in the air. When in the air, strafe left and
strafe right to rotate left and right and use the jump and crouch
controls to change the pitch up and down. You can also add
bEjectPassengersWhenFlipped=false so you
aren’t ejected from the vehicle when it flips over;
you have to press the Use key to escape. You can also increase or
decrease the force of nitrous or maybe even change the direction so
the car can hover and fly. Have fun!