There is no spoon
Difference between revisions of "Legacy:KShip"
(Added an implmentation for KShip which should work out-of-the-box, and a sample vehicle (FlyingDog) based on it.) |
|||
Line 300: | Line 300: | ||
'''LiKeMiKeS:''' I've tried many times compiling this KShip script and have ended up with an error for the ShipState 'struct' & 'var' references obscuring each other and haven't been able to figure out a clean fix so it will compile - without creating another unneeded var instance or reference. I'm still learning the nuances of uscript, so it's difficult to debug atm. | '''LiKeMiKeS:''' I've tried many times compiling this KShip script and have ended up with an error for the ShipState 'struct' & 'var' references obscuring each other and haven't been able to figure out a clean fix so it will compile - without creating another unneeded var instance or reference. I'm still learning the nuances of uscript, so it's difficult to debug atm. | ||
+ | |||
+ | '''Brold9999:''' I tried the above code long ago and it didn't seem to work. I have gotten a lot of questions on my mod, UT Space Battle, as to how I implemented the vehicles. I did not use a Karma or PhysX based approach but that is what most of the people who have asked me about this seem to be looking for. I have taken the code above and hammered away at it until it works. I ripped out many unnecessary sections (such as afterburner) which only cluttered the salient concepts of implementing a 6DOF vehicle. The new version should compile and work out-of-the-box and has (mostly) working netcode. Some tweaks are still necessary to make a vehicle suitable for a mod but this one does the basics that seem to trip most people up when they try to implement a 6DOF vehicle. KShip is the base 6DOF vehicle, FlyingDog is a butchered Bulldog which is based on KShip. Code follows: | ||
+ | |||
+ | <uscript> | ||
+ | class KShip extends KVehicle; | ||
+ | |||
+ | // Used for Karma forces pileup | ||
+ | // Add to these variables to apply force and torque to the ship. | ||
+ | var vector extraForce, extraTorque; | ||
+ | |||
+ | // Amount to increment/decrement shipSteering.roll for every click of the mouse wheel. | ||
+ | var float rollUnit; | ||
+ | // Desired rotation torque is reduced each tick. This factor controls how rapidly old inputs decay. | ||
+ | var float turnDecayTime; | ||
+ | // Amount of rotation torque to apply for a given amount of mouse movement. | ||
+ | var float turnSensitivity; | ||
+ | // Maximum amount of rotation torque that can be accumulated. | ||
+ | var float maximumTurnRate; | ||
+ | // Maximum linear thrust the ship can apply. | ||
+ | var float maximumThrust; | ||
+ | |||
+ | // Desired torque on the ship - relative to it's current rotation. | ||
+ | var rotator shipSteering; | ||
+ | // Desired linear thrust to apply to the ship - not relative to it's current rotation. | ||
+ | var vector shipThrust; | ||
+ | |||
+ | // Ship replication vars and functions, thanks daid! | ||
+ | struct StructShipState | ||
+ | { | ||
+ | var KRBVec ChassisPosition; | ||
+ | var Quat ChassisQuaternion; | ||
+ | var KRBVec ChassisLinVel; | ||
+ | var KRBVec ChassisAngVel; | ||
+ | |||
+ | var rotator serverSteering; | ||
+ | var vector serverThrust; | ||
+ | |||
+ | var bool bNewState; // Set to true whenever a new state is received and should be processed | ||
+ | }; | ||
+ | |||
+ | var KRigidBodyState ChassisState; | ||
+ | var StructShipState ShipState; // This is replicated to the ship, and processed to update all the parts. | ||
+ | var bool bNewShipState; // Indicated there is new data processed, and chassis RBState should be updated. | ||
+ | |||
+ | var float NextNetUpdateTime; // Next time we should force an update of vehicles state. | ||
+ | var() float MaxNetUpdateInterval; | ||
+ | |||
+ | var int AVar;//Just for replication, else the ShipState doesn't get replicated | ||
+ | |||
+ | replication | ||
+ | { | ||
+ | unreliable if(Role == ROLE_Authority) | ||
+ | ShipState, AVar; | ||
+ | } | ||
+ | |||
+ | simulated event VehicleStateReceived() | ||
+ | { | ||
+ | if(!ShipState.bNewState) | ||
+ | return; | ||
+ | |||
+ | // Get root chassis info | ||
+ | ChassisState.Position = ShipState.ChassisPosition; | ||
+ | ChassisState.Quaternion = ShipState.ChassisQuaternion; | ||
+ | ChassisState.LinVel = ShipState.ChassisLinVel; | ||
+ | ChassisState.AngVel = ShipState.ChassisAngVel; | ||
+ | |||
+ | // Update control inputs | ||
+ | shipSteering = ShipState.serverSteering; | ||
+ | shipThrust = ShipState.serverThrust; | ||
+ | |||
+ | // Update flags | ||
+ | ShipState.bNewState = false; | ||
+ | bNewShipState = true; | ||
+ | } | ||
+ | |||
+ | simulated event bool KUpdateState(out KRigidBodyState newState) | ||
+ | { | ||
+ | // This should never get called on the server - but just in case! | ||
+ | if(Role == ROLE_Authority || !bNewShipState) | ||
+ | return false; | ||
+ | |||
+ | // Apply received data as new position of ship chassis. | ||
+ | newState = ChassisState; | ||
+ | bNewShipState = false; | ||
+ | |||
+ | return true; | ||
+ | } | ||
+ | |||
+ | function PackState() | ||
+ | { | ||
+ | local vector chassisPos, chassisLinVel, chassisAngVel; | ||
+ | local vector oldPos, oldLinVel; | ||
+ | local KRigidBodyState localChassisState; | ||
+ | |||
+ | // Get chassis state. | ||
+ | KGetRigidBodyState(localChassisState); | ||
+ | |||
+ | chassisPos = KRBVecToVector(localChassisState.Position); | ||
+ | chassisLinVel = KRBVecToVector(localChassisState.LinVel); | ||
+ | chassisAngVel = KRBVecToVector(localChassisState.AngVel); | ||
+ | |||
+ | // Last position we sent | ||
+ | oldPos = KRBVectoVector(ShipState.ChassisPosition); | ||
+ | oldLinVel = KRBVectoVector(ShipState.ChassisLinVel); | ||
+ | |||
+ | // See if state has changed enough, or enough time has passed, that we | ||
+ | // should send out another update by updating the state struct. | ||
+ | if( !KIsAwake() ) | ||
+ | { | ||
+ | return; // Never send updates if physics is at rest | ||
+ | } | ||
+ | |||
+ | if( VSize(oldPos - chassisPos) > 5 || | ||
+ | VSize(oldLinVel - chassisLinVel) > 1 || | ||
+ | abs(shipState.serverSteering.yaw - shipSteering.yaw) > 0.1 || | ||
+ | abs(shipState.serverSteering.pitch - shipSteering.pitch) > 0.1 || | ||
+ | abs(shipState.serverSteering.roll - shipSteering.roll) > 0.1 || | ||
+ | VSize(shipState.serverThrust - shipThrust) > 1 || | ||
+ | Level.TimeSeconds > NextNetUpdateTime ) | ||
+ | { | ||
+ | NextNetUpdateTime = Level.TimeSeconds + MaxNetUpdateInterval; | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | return; | ||
+ | } | ||
+ | |||
+ | ShipState.ChassisPosition = localChassisState.Position; | ||
+ | ShipState.ChassisQuaternion = localChassisState.Quaternion; | ||
+ | ShipState.ChassisLinVel = localChassisState.LinVel; | ||
+ | ShipState.ChassisAngVel = localChassisState.AngVel; | ||
+ | |||
+ | shipState.serverSteering = shipSteering; | ||
+ | shipState.serverThrust = shipThrust; | ||
+ | |||
+ | // This flag lets the client know this data is new. | ||
+ | ShipState.bNewState = true; | ||
+ | //Make sure ShipState gets replicated | ||
+ | AVar++; | ||
+ | if (AVar > 10) | ||
+ | AVar=0; | ||
+ | } | ||
+ | |||
+ | simulated function setInitialState() { | ||
+ | super.setInitialState(); | ||
+ | |||
+ | // don't disable my tick! | ||
+ | enable('tick'); | ||
+ | } | ||
+ | |||
+ | |||
+ | simulated event drivingStatusChanged() { | ||
+ | super.drivingStatusChanged(); | ||
+ | |||
+ | // don't disable my tick! | ||
+ | enable('tick'); | ||
+ | } | ||
+ | |||
+ | function KDriverEnter(Pawn P) { | ||
+ | super.KDriverEnter(p); | ||
+ | |||
+ | if (PlayerController(controller) != none) | ||
+ | controller.gotoState(landMovementState); | ||
+ | } | ||
+ | |||
+ | simulated function prevWeapon() { | ||
+ | shipSteering.roll = fclamp(shipSteering.roll - (rollUnit * turnSensitivity), -maximumTurnRate, maximumTurnRate); | ||
+ | } | ||
+ | |||
+ | simulated function nextWeapon() { | ||
+ | shipSteering.roll = fclamp(shipSteering.roll + (rollUnit * turnSensitivity), -maximumTurnRate, maximumTurnRate); | ||
+ | } | ||
+ | |||
+ | simulated function updateRocketAcceleration(float deltaTime, float yawChange, float pitchChange) { | ||
+ | if (deltaTime >= turnDecayTime) | ||
+ | shipSteering = rot(0,0,0); | ||
+ | else | ||
+ | shipSteering *= (turnDecayTime-deltaTime) / turnDecayTime; | ||
+ | |||
+ | // / 6000 because the aForward, etc. are 6000 by default when the appropriate button is pressed. | ||
+ | shipThrust = (((vect(1,0,0) * PlayerController(Controller).aForward) + (vect(0,1,0) * PlayerController(Controller).aStrafe) + (vect(0,0,1) * PlayerController(Controller).aUp)) / 6000) >> rotation; | ||
+ | if (vsize(shipThrust) > 1) | ||
+ | shipThrust = normal(shipThrust); | ||
+ | |||
+ | shipSteering.yaw = fclamp(shipSteering.yaw - (yawChange * turnSensitivity), -maximumTurnRate, maximumTurnRate); | ||
+ | shipSteering.pitch = fclamp(shipSteering.pitch + (pitchChange * turnSensitivity), -maximumTurnRate, maximumTurnRate); | ||
+ | } | ||
+ | |||
+ | simulated function tick(float delta) | ||
+ | { super.tick(delta); | ||
+ | |||
+ | if (controller == none) { | ||
+ | shipThrust = vect(0,0,0); | ||
+ | shipSteering = rot(0,0,0); | ||
+ | } | ||
+ | |||
+ | if(!KIsAwake() && controller != none) | ||
+ | KWake(); | ||
+ | |||
+ | if(Role == ROLE_Authority && Level.NetMode != NM_StandAlone) | ||
+ | PackState(); | ||
+ | |||
+ | updateExtraForce(delta); | ||
+ | } | ||
+ | |||
+ | simulated function updateExtraForce(float delta) | ||
+ | { | ||
+ | local vector worldForward, worldDown, worldLeft; | ||
+ | |||
+ | worldForward = vect(1, 0, 0) >> Rotation; | ||
+ | worldDown = vect(0, 0, -1) >> Rotation; | ||
+ | worldLeft = vect(0, -1, 0) >> Rotation; | ||
+ | |||
+ | ExtraForce = ExtraForce + shipThrust * maximumThrust * delta; // Speed | ||
+ | ExtraTorque = ExtraTorque + worldDown * shipSteering.yaw * delta; // Yaw | ||
+ | ExtraTorque = ExtraTorque - worldLeft * -shipSteering.pitch * delta; // Pitch | ||
+ | ExtraTorque = ExtraTorque + worldForward * -shipSteering.roll * delta; // Roll | ||
+ | } | ||
+ | |||
+ | simulated event KApplyForce(out vector Force, out vector Torque) | ||
+ | { | ||
+ | // This actually does the applying of the piled up force | ||
+ | Force = ExtraForce; | ||
+ | Torque = ExtraTorque; | ||
+ | ExtraForce = vect(0,0,0); | ||
+ | ExtraTorque = vect(0,0,0); | ||
+ | } | ||
+ | |||
+ | DefaultProperties | ||
+ | { | ||
+ | landMovementState=PlayerSpaceFlying | ||
+ | |||
+ | rollUnit=1024 | ||
+ | |||
+ | turnDecayTime=0.5 | ||
+ | turnSensitivity=0.5 | ||
+ | maximumTurnRate=5000 | ||
+ | maximumThrust=2500 | ||
+ | } | ||
+ | </uscript> | ||
+ | |||
+ | <uscript> | ||
+ | class FlyingDog extends KShip placeable CacheExempt; | ||
+ | |||
+ | // triggers used to get into the FlyingDog | ||
+ | var const vector FrontTriggerOffset; | ||
+ | var SVehicleTrigger FLTrigger, FRTrigger; | ||
+ | |||
+ | // Maximum speed at which you can get in the vehicle. | ||
+ | var (FlyingDog) float TriggerSpeedThresh; | ||
+ | var bool TriggerState; // true for on, false for off. | ||
+ | |||
+ | // Destroyed Buggy | ||
+ | var (FlyingDog) class<Actor> DestroyedEffect; | ||
+ | var (FlyingDog) sound DestroyedSound; | ||
+ | |||
+ | // Weapon | ||
+ | var float FireCountdown; | ||
+ | |||
+ | var (FlyingDog) float FireInterval; | ||
+ | var (FlyingDog) vector weaponFireOffset; | ||
+ | |||
+ | simulated function PostNetBeginPlay() | ||
+ | { | ||
+ | local vector RotX, RotY, RotZ; | ||
+ | |||
+ | Super.PostNetBeginPlay(); | ||
+ | |||
+ | GetAxes(Rotation,RotX,RotY,RotZ); | ||
+ | |||
+ | // Only have triggers on server | ||
+ | if(Level.NetMode != NM_Client) | ||
+ | { | ||
+ | // Create triggers for gettting into the FlyingDog | ||
+ | FLTrigger = spawn(class'SVehicleTrigger', self,, Location + FrontTriggerOffset.X * RotX + FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); | ||
+ | FLTrigger.SetBase(self); | ||
+ | FLTrigger.SetCollision(true, false, false); | ||
+ | |||
+ | FRTrigger = spawn(class'SVehicleTrigger', self,, Location + FrontTriggerOffset.X * RotX - FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); | ||
+ | FRTrigger.SetBase(self); | ||
+ | FRTrigger.SetCollision(true, false, false); | ||
+ | |||
+ | TriggerState = true; | ||
+ | } | ||
+ | |||
+ | // If this is not 'authority' version - don't destroy it if there is a problem. | ||
+ | // The network should sort things out. | ||
+ | if(Role != ROLE_Authority) { | ||
+ | KarmaParams(KParams).bDestroyOnSimError = False; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | simulated event Destroyed() | ||
+ | { | ||
+ | // Clean up random stuff attached to the car | ||
+ | if(Level.NetMode != NM_Client) | ||
+ | { | ||
+ | FLTrigger.Destroy(); | ||
+ | FRTrigger.Destroy(); | ||
+ | } | ||
+ | |||
+ | Super.Destroyed(); | ||
+ | |||
+ | // Trigger destroyed sound and effect | ||
+ | if(Level.NetMode != NM_DedicatedServer) | ||
+ | { | ||
+ | spawn(DestroyedEffect, self, , Location ); | ||
+ | PlaySound(DestroyedSound); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | function KDriverEnter(Pawn p) | ||
+ | { | ||
+ | Super.KDriverEnter(p); | ||
+ | |||
+ | p.bHidden = True; | ||
+ | ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 1); | ||
+ | } | ||
+ | |||
+ | function bool KDriverLeave(bool bForceLeave) | ||
+ | { | ||
+ | local Pawn OldDriver; | ||
+ | |||
+ | OldDriver = Driver; | ||
+ | |||
+ | // If we succesfully got out of the car, make driver visible again. | ||
+ | if( Super.KDriverLeave(bForceLeave) ) | ||
+ | { | ||
+ | OldDriver.bHidden = false; | ||
+ | AmbientSound = None; | ||
+ | return true; | ||
+ | } | ||
+ | else | ||
+ | return false; | ||
+ | } | ||
+ | |||
+ | function fireWeapons(bool bWasAltFire) | ||
+ | { | ||
+ | local vector FireLocation; | ||
+ | local PlayerController PC; | ||
+ | |||
+ | // Client can't do firing | ||
+ | if(Role != ROLE_Authority) | ||
+ | return; | ||
+ | |||
+ | FireLocation = Location + (weaponFireOffset >> Rotation); | ||
+ | |||
+ | while(FireCountdown <= 0) | ||
+ | { | ||
+ | if(!bWasAltFire) | ||
+ | { | ||
+ | spawn(class'PROJ_TurretSkaarjPlasma', self, , FireLocation, rotation); | ||
+ | |||
+ | // Play firing noise | ||
+ | PlaySound(Sound'ONSVehicleSounds-S.Laser02', SLOT_None,,,,, false); | ||
+ | PC = PlayerController(Controller); | ||
+ | if (PC != None && PC.bEnableWeaponForceFeedback) | ||
+ | PC.ClientPlayForceFeedback("RocketLauncherFire"); | ||
+ | } | ||
+ | else | ||
+ | { | ||
+ | spawn(class'PROJ_SpaceFighter_Rocket', self, , FireLocation, rotator(vector(rotation) + Vrand() * 0.05)); | ||
+ | |||
+ | // Play firing noise | ||
+ | PlaySound(Sound'AssaultSounds.HnShipFire01', SLOT_None,,,,, false); | ||
+ | PC = PlayerController(Controller); | ||
+ | if (PC != None && PC.bEnableWeaponForceFeedback) | ||
+ | PC.ClientPlayForceFeedback("RocketLauncherFire"); | ||
+ | } | ||
+ | |||
+ | FireCountdown += FireInterval; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Fire a rocket (if we've had time to reload!) | ||
+ | function VehicleFire(bool bWasAltFire) | ||
+ | { | ||
+ | Super.VehicleFire(bWasAltFire); | ||
+ | |||
+ | if(FireCountdown < 0) | ||
+ | { | ||
+ | FireCountdown = 0; | ||
+ | fireWeapons(bWasAltFire); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | simulated function Tick(float Delta) | ||
+ | { | ||
+ | Local float VMag; | ||
+ | |||
+ | Super.Tick(Delta); | ||
+ | |||
+ | // Weapons (run on server and replicated to client) | ||
+ | if(Role == ROLE_Authority) | ||
+ | { | ||
+ | // Countdown to next shot | ||
+ | FireCountdown -= Delta; | ||
+ | |||
+ | // This is for sustained barrages. | ||
+ | // Primary fire takes priority | ||
+ | if(bVehicleIsFiring) | ||
+ | fireWeapons(false); | ||
+ | else if(bVehicleIsAltFiring) | ||
+ | fireWeapons(true); | ||
+ | } | ||
+ | |||
+ | // Dont have triggers on network clients. | ||
+ | if(Level.NetMode != NM_Client) | ||
+ | { | ||
+ | // If vehicle is moving, disable collision for trigger. | ||
+ | VMag = VSize(Velocity); | ||
+ | |||
+ | if(VMag < TriggerSpeedThresh && TriggerState == false) | ||
+ | { | ||
+ | FLTrigger.SetCollision(true, false, false); | ||
+ | FRTrigger.SetCollision(true, false, false); | ||
+ | TriggerState = true; | ||
+ | } | ||
+ | else if(VMag > TriggerSpeedThresh && TriggerState == true) | ||
+ | { | ||
+ | FLTrigger.SetCollision(false, false, false); | ||
+ | FRTrigger.SetCollision(false, false, false); | ||
+ | TriggerState = false; | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Really simple at the moment! | ||
+ | function TakeDamage(int Damage, Pawn instigatedBy, Vector hitlocation, | ||
+ | Vector momentum, class<DamageType> damageType) | ||
+ | { | ||
+ | // Avoid damage healing the car! | ||
+ | if(Damage < 0) | ||
+ | return; | ||
+ | |||
+ | if(damageType == class'DamTypeSuperShockBeam') | ||
+ | Health -= 100; // Instagib doesn't work on vehicles | ||
+ | else | ||
+ | Health -= 0.5 * Damage; // Weapons do less damage | ||
+ | |||
+ | // The vehicle is dead! | ||
+ | if(Health <= 0) | ||
+ | { | ||
+ | if ( Controller != None ) | ||
+ | { | ||
+ | if( Controller.bIsPlayer ) | ||
+ | { | ||
+ | ClientKDriverLeave(PlayerController(Controller)); // Just to reset HUD etc. | ||
+ | Controller.PawnDied(self); // This should unpossess the controller and let the player respawn | ||
+ | } | ||
+ | else | ||
+ | Controller.Destroy(); | ||
+ | } | ||
+ | |||
+ | Destroy(); // Destroy the vehicle itself (see Destroyed below) | ||
+ | } | ||
+ | |||
+ | //KAddImpulse(momentum, hitlocation); | ||
+ | } | ||
+ | |||
+ | // AI Related code | ||
+ | function Actor GetBestEntry(Pawn P) | ||
+ | { | ||
+ | if ( VSize(P.Location - FLTrigger.Location) < VSize(P.Location - FRTrigger.Location) ) | ||
+ | return FLTrigger; | ||
+ | return FRTrigger; | ||
+ | } | ||
+ | |||
+ | defaultproperties | ||
+ | { | ||
+ | DrawType=DT_StaticMesh | ||
+ | StaticMesh=StaticMesh'BulldogMeshes.Simple.S_Chassis' | ||
+ | |||
+ | Begin Object Class=KarmaParamsRBFull Name=KParams0 | ||
+ | KActorGravScale=0 | ||
+ | KInertiaTensor(0)=20 | ||
+ | KInertiaTensor(1)=0 | ||
+ | KInertiaTensor(2)=0 | ||
+ | KInertiaTensor(3)=30 | ||
+ | KInertiaTensor(4)=0 | ||
+ | KInertiaTensor(5)=48 | ||
+ | KCOMOffset=(X=0.8,Y=0.0,Z=-0.7) | ||
+ | KStartEnabled=True | ||
+ | KFriction=1.6 | ||
+ | KLinearDamping=1 | ||
+ | KAngularDamping=10 | ||
+ | bKNonSphericalInertia=False | ||
+ | bHighDetailOnly=False | ||
+ | bClientOnly=False | ||
+ | bKDoubleTickRate=True | ||
+ | Name="KParams0" | ||
+ | End Object | ||
+ | KParams=KarmaParams'KParams0' | ||
+ | |||
+ | DrawScale=0.4 | ||
+ | drawScale3D=(x=-1,y=1,z=1) | ||
+ | |||
+ | DestroyedEffect=class'XEffects.RocketExplosion' | ||
+ | DestroyedSound=sound'WeaponSounds.P1RocketLauncherAltFire' | ||
+ | |||
+ | FrontTriggerOffset=(X=0,Y=165,Z=10) | ||
+ | |||
+ | TriggerSpeedThresh=40 | ||
+ | |||
+ | // Weaponry | ||
+ | FireInterval=0.1 | ||
+ | weaponFireOffset=(X=0,Y=0,Z=80) | ||
+ | |||
+ | // Driver positions | ||
+ | ExitPositions(0)=(X=0,Y=200,Z=100) | ||
+ | ExitPositions(1)=(X=0,Y=-200,Z=100) | ||
+ | ExitPositions(2)=(X=350,Y=0,Z=100) | ||
+ | ExitPositions(3)=(X=-350,Y=0,Z=100) | ||
+ | |||
+ | DrivePos=(X=-165,Y=0,Z=-100) | ||
+ | |||
+ | Health=800 | ||
+ | HealthMax=800 | ||
+ | |||
+ | SoundRadius=255 | ||
+ | } | ||
+ | </uscript> |
Latest revision as of 08:32, 17 April 2010
Well this is basically my code for a space ship, thanks a lot to Daid for lending me his replication code (which turned out to come from KCar but i thank him non the less, great guy he is :D)
I currently use a different, more client oriented replication system, but this one here is a lot better, i havent incorporated my ship to it yet because i have some other problems that come with it that i have to solve, but since the "core" ship here is simple, ill use the better replication for you guys :)
(Btw i deal with all the entering/leaving of the vehicle in a parent class, this is really irrelevant to physics and is easy to do, so i left it out just so there will be less and easier to read code)
var float Roll, CThrust, TempThrust, OldThrust; // Throttle and Steering are already declared in KVehicle, so all we need is roll, CThrust, TempThrust, OldThrust will be explained soon // BTW note that i use Throttle for Pitch alteration because it is already bound by other vehicles to the forward/backward key // ok i have also incorporated a system that allows for various thrust managments, //if you simply want to move forward full speed only while you press the the thrust button, you would use TempThrust. //when you release the thrust button you though you should always set TempThrust to -1 // if you want to have a constant speed, assign it to CThrust, so every time you release the thrust button (the one that changes TempThrust) // you would automatically start going at that speed (good for stuff like dogifghting and matching speeds) // and lastly i added an afterburner effect, ill include it here for you guys to see although it isnt hard to do. // also what i did with it was that when you afterburn and you run out of energy, while you still hold the afterburn key // you would keep going at your max possible speed, and once you release it you would start going at your constant wanted speed (CThrust) // Thats why i have 2 vars, bAfterburn is for after burning (holding key and having enough E), bBurn is for only holding the afterburn key with no regard to its depletion var bool bAfterburn, bBurn; // note that you should set both of those to true when you want to afterburn var() float ForwardTrust, TurnRate, AfterBurnThrust; // This is basically how fast our ship turns/moves var float CurRoll, CurSteering, CurThrottle, UnitAccRate; // This is used for acceleration // CThrust is used for ships that can assign a specific throttle value, like 75% of max speed or whatever... // UnitAccRate is how much each value increases per second var vector ExtraForce, ExtraTorque; // Used for Karama forces pileup // Ship replication vars and functions, thanks daid! struct StructShipState { var KRBVec ChassisPosition; var Quat ChassisQuaternion; var KRBVec ChassisLinVel; var KRBVec ChassisAngVel; var float ServerSteering; var float ServerThrottle; var float ServerRoll; var bool ServerbAfterburn; var bool ServerbBurn; var bool bNewState; // Set to true whenever a new state is received and should be processed }; var KRigidBodyState ChassisState; var StructShipState ShipState; // This is replicated to the ship, and processed to update all the parts. var bool bNewShipState; // Indicated there is new data processed, and chassis RBState should be updated. var float NextNetUpdateTime; // Next time we should force an update of vehicles state. var() float MaxNetUpdateInterval; var int AVar;//Just for replication, else the ShipState doesn't get replicated Replication { unreliable if(Role == ROLE_Authority) ShipState, AVar; } simulated event VehicleStateReceived() { if(!ShipState.bNewState) return; // Get root chassis info ChassisState.Position = ShipState.ChassisPosition; ChassisState.Quaternion = ShipState.ChassisQuaternion; ChassisState.LinVel = ShipState.ChassisLinVel; ChassisState.AngVel = ShipState.ChassisAngVel; // Update control inputs Steering = ShipState.ServerSteering; Throttle = ShipState.ServerThrottle; Roll = ShipState.ServerRoll; // Afterburner bAfterburn = ShipState.ServerbAfterburn; bBurn = ShipState.ServerbBurn; // Update flags ShipState.bNewState = false; bNewShipState = true; } simulated event bool KUpdateState(out KRigidBodyState newState) { // This should never get called on the server - but just in case! if(Role == ROLE_Authority || !bNewShipState) return false; // Apply received data as new position of ship chassis. newState = ChassisState; bNewShipState = false; return true; } function PackState() { local vector chassisPos, chassisLinVel, chassisAngVel; local vector oldPos, oldLinVel; local KRigidBodyState ChassisState; // Get chassis state. KGetRigidBodyState(ChassisState); chassisPos = KRBVecToVector(ChassisState.Position); chassisLinVel = KRBVecToVector(ChassisState.LinVel); chassisAngVel = KRBVecToVector(ChassisState.AngVel); // Last position we sent oldPos = KRBVectoVector(ShipState.ChassisPosition); oldLinVel = KRBVectoVector(ShipState.ChassisLinVel); // See if state has changed enough, or enough time has passed, that we // should send out another update by updating the state struct. if( !KIsAwake() ) { return; // Never send updates if physics is at rest } if( VSize(oldPos - chassisPos) > 5 || VSize(oldLinVel - chassisLinVel) > 1 || Abs(ShipState.ServerThrottle - Throttle) > 0.1 || Abs(ShipState.ServerSteering - Steering) > 0.1 || Abs(ShipState.ServerRoll - Roll) > 0.1 || bAfterburn != ShipState.ServerbAfterburn || bBurn != ShipState.ServerbBurn || Level.TimeSeconds > NextNetUpdateTime ) { NextNetUpdateTime = Level.TimeSeconds + MaxNetUpdateInterval; } else { return; } ShipState.ChassisPosition = ChassisState.Position; ShipState.ChassisQuaternion = ChassisState.Quaternion; ShipState.ChassisLinVel = ChassisState.LinVel; ShipState.ChassisAngVel = ChassisState.AngVel; // Player Input ShipState.ServerSteering = Steering; ShipState.ServerThrottle = Throttle; ShipState.ServerRoll = Roll; // AfterBurner ShipState.ServerbAfterburn = bAfterburn; ShipState.ServerbBurn = bBurn; // This flag lets the client know this data is new. ShipState.bNewState = true; //Make sure ShipState gets replicated AVar++; if (AVar > 10) AVar=0; } simulated function Tick(float DeltaTime) { Super.Tick(DeltaTime); if(!KIsAwake() && Controller!=None) KWake(); if(Role == ROLE_Authority && Level.NetMode != NM_StandAlone) PackState(); UpdateAcceleration(DeltaTime); UpdateExtraForce(DeltaTime); } simulated function UpdateAcceleration(float Delta) { CurSteering = CurSteering + Steering * UnitAccRate * Delta; CurThrottle = CurThrottle + Throttle * UnitAccRate * Delta; CurRoll = CurRoll + Roll * UnitAccRate * Delta; if(Steering==0) CurSteering=0; if(Throttle==0) CurThrottle=0; if(Roll==0) CurRoll=0; if(Abs(CurSteering)>Abs(Steering)) CurSteering=Steering; if(Abs(CurThrottle)>Abs(Throttle)) CurThrottle=Throttle; if(Abs(CurRoll)>Abs(Roll)) CurRoll=Roll; if(bAfterBurn) { ReduceAfterBurnEnergy(); // just made this up, put it here incase you have something like this if(GetAfterburnEnergy()<=0) // Another fictional function... { bAfterBurn=False; } else { CThrust=AfterBurnThrust; } } if(bBurn && !bAfterBurn && CThrust!=1) { CThrust=1; } else if(!bBurn && !bAfterBurn && CThrust!=OldThrust) { CThrust=OldThrust; } if(TempThrust!=-1 && !bAfterBurn) { if(Abs(CurThrust-TempThrust)<0.01) CurThrust=TempThrust; if(CurThrust>TempThrust) CurThrust = CurThrust - UnitAccRate * Delta / 4; // I made linear acceleration 4 times slower than rotation, you can do whatever you want though else if(CurThrust<TempThrust) CurThrust = CurThrust + UnitAccRate * Delta / 4; } else if(CurThrust!=CThrust && !bAfterBurn) { if(Abs(CurThrust-CThrust)<0.01) CurThrust=CThrust; else if(CurThrust>CThrust) CurThrust = CurThrust - UnitAccRate * Delta / 4; else if(CurThrust<CThrust) CurThrust = CurThrust + UnitAccRate * Delta / 4; } else if(CurThrust!=CThrust && bAfterBurn) { if(Abs(CurThrust-CThrust)<0.01) CurThrust=CThrust; else if(CurThrust>CThrust) CurThrust = CurThrust - UnitAccRate * Delta / 2.2; // Acceleration with afterburner a lot faster else if(CurThrust<CThrust) CurThrust = CurThrust + UnitAccRate * Delta / 2.2; } } simulated function UpdateExtraForce(float Delta) { local vector worldForward, worldDown, worldLeft; worldForward = vect(1, 0, 0) >> Rotation; worldDown = vect(0, 0, -1) >> Rotation; worldLeft = vect(0, -1, 0) >> Rotation; ExtraForce = ExtraForce + worldForward * ForwardThrust * Delta * CurThrust; // Speed ExtraTorque = ExtraTorque + worldDown * TurnRate * Delta * CurSteering; // Yaw ExtraTorque = ExtraTorque + worldLeft * TurnRate * Delta * CurThrottle; // Pitch ExtraTorque = ExtraTorque + worldForward * TurnRate * Delta * -CurRoll; // Roll } simulated event KApplyForce(out vector Force, out vector Torque) { // This actually does the applying of the piled up force Force = ExtraForce; Torque = ExtraTorque; ExtraForce = vect(0,0,0); ExtraTorque = vect(0,0,0); } DefaultProperties { // Create Karma collision Params for ourselves, you can change whatever you want here Begin Object Class=KarmaParamsRBFull Name=KParams0 KLinearDamping=2.000; KAngularDamping=2.000; KStartEnabled=True bHighDetailOnly=False bClientOnly=False bKDoubleTickRate=False KFriction=1.600000 KActorGravScale = 0.0; KMass=2; Name="KParams0" End Object KParams=KarmaParamsRBFull'<MyPackage>.<MyShipClass>.KParams0' // And just some other variables: TurnRate=8 ForwardThrust=5000 UnitAccRate=2.0 AfterBurnThrust=1.20 // basically means that afterburner goes at 120% of regular maximum speed }
and that should be it, i hope i didnt miss anything...
Spark: KParams=KarmaParamsRBFull'<MyPackage>.<MyShipClass>.KParams0' <– Is this supposed to be a trap? ;) Took me a while to figure out that my ship didn't fly because this wasn't set to KParams0 and thus wasn't enabled at all. :)
Sir_Brizz: Zep, did you end up getting your Quaternion working for the indicator? I have a few ideas that work if you want to know what they are.
ProjectX: I'm a n00b to coding, but is it possible to take this code, and change it to create a "hovering vehicles" class, that basicly, wont go a certan height above ground, but, from high jumps, etc., go lower to the ground? I also saw that there was 3 types of thrust, is it possible to bind a key that will cycle through the flying types (for more precise flying, eg. for dog matches and realism)?
NickR: Does anyone have a working example of a flying vehicle?
Foxpaw: I do, but my mod is still quite a ways from release. The code above allegedly is working code for a flying vehicle, though I've never tested it.
LiKeMiKeS: I've tried many times compiling this KShip script and have ended up with an error for the ShipState 'struct' & 'var' references obscuring each other and haven't been able to figure out a clean fix so it will compile - without creating another unneeded var instance or reference. I'm still learning the nuances of uscript, so it's difficult to debug atm.
Brold9999: I tried the above code long ago and it didn't seem to work. I have gotten a lot of questions on my mod, UT Space Battle, as to how I implemented the vehicles. I did not use a Karma or PhysX based approach but that is what most of the people who have asked me about this seem to be looking for. I have taken the code above and hammered away at it until it works. I ripped out many unnecessary sections (such as afterburner) which only cluttered the salient concepts of implementing a 6DOF vehicle. The new version should compile and work out-of-the-box and has (mostly) working netcode. Some tweaks are still necessary to make a vehicle suitable for a mod but this one does the basics that seem to trip most people up when they try to implement a 6DOF vehicle. KShip is the base 6DOF vehicle, FlyingDog is a butchered Bulldog which is based on KShip. Code follows:
class KShip extends KVehicle; // Used for Karma forces pileup // Add to these variables to apply force and torque to the ship. var vector extraForce, extraTorque; // Amount to increment/decrement shipSteering.roll for every click of the mouse wheel. var float rollUnit; // Desired rotation torque is reduced each tick. This factor controls how rapidly old inputs decay. var float turnDecayTime; // Amount of rotation torque to apply for a given amount of mouse movement. var float turnSensitivity; // Maximum amount of rotation torque that can be accumulated. var float maximumTurnRate; // Maximum linear thrust the ship can apply. var float maximumThrust; // Desired torque on the ship - relative to it's current rotation. var rotator shipSteering; // Desired linear thrust to apply to the ship - not relative to it's current rotation. var vector shipThrust; // Ship replication vars and functions, thanks daid! struct StructShipState { var KRBVec ChassisPosition; var Quat ChassisQuaternion; var KRBVec ChassisLinVel; var KRBVec ChassisAngVel; var rotator serverSteering; var vector serverThrust; var bool bNewState; // Set to true whenever a new state is received and should be processed }; var KRigidBodyState ChassisState; var StructShipState ShipState; // This is replicated to the ship, and processed to update all the parts. var bool bNewShipState; // Indicated there is new data processed, and chassis RBState should be updated. var float NextNetUpdateTime; // Next time we should force an update of vehicles state. var() float MaxNetUpdateInterval; var int AVar;//Just for replication, else the ShipState doesn't get replicated replication { unreliable if(Role == ROLE_Authority) ShipState, AVar; } simulated event VehicleStateReceived() { if(!ShipState.bNewState) return; // Get root chassis info ChassisState.Position = ShipState.ChassisPosition; ChassisState.Quaternion = ShipState.ChassisQuaternion; ChassisState.LinVel = ShipState.ChassisLinVel; ChassisState.AngVel = ShipState.ChassisAngVel; // Update control inputs shipSteering = ShipState.serverSteering; shipThrust = ShipState.serverThrust; // Update flags ShipState.bNewState = false; bNewShipState = true; } simulated event bool KUpdateState(out KRigidBodyState newState) { // This should never get called on the server - but just in case! if(Role == ROLE_Authority || !bNewShipState) return false; // Apply received data as new position of ship chassis. newState = ChassisState; bNewShipState = false; return true; } function PackState() { local vector chassisPos, chassisLinVel, chassisAngVel; local vector oldPos, oldLinVel; local KRigidBodyState localChassisState; // Get chassis state. KGetRigidBodyState(localChassisState); chassisPos = KRBVecToVector(localChassisState.Position); chassisLinVel = KRBVecToVector(localChassisState.LinVel); chassisAngVel = KRBVecToVector(localChassisState.AngVel); // Last position we sent oldPos = KRBVectoVector(ShipState.ChassisPosition); oldLinVel = KRBVectoVector(ShipState.ChassisLinVel); // See if state has changed enough, or enough time has passed, that we // should send out another update by updating the state struct. if( !KIsAwake() ) { return; // Never send updates if physics is at rest } if( VSize(oldPos - chassisPos) > 5 || VSize(oldLinVel - chassisLinVel) > 1 || abs(shipState.serverSteering.yaw - shipSteering.yaw) > 0.1 || abs(shipState.serverSteering.pitch - shipSteering.pitch) > 0.1 || abs(shipState.serverSteering.roll - shipSteering.roll) > 0.1 || VSize(shipState.serverThrust - shipThrust) > 1 || Level.TimeSeconds > NextNetUpdateTime ) { NextNetUpdateTime = Level.TimeSeconds + MaxNetUpdateInterval; } else { return; } ShipState.ChassisPosition = localChassisState.Position; ShipState.ChassisQuaternion = localChassisState.Quaternion; ShipState.ChassisLinVel = localChassisState.LinVel; ShipState.ChassisAngVel = localChassisState.AngVel; shipState.serverSteering = shipSteering; shipState.serverThrust = shipThrust; // This flag lets the client know this data is new. ShipState.bNewState = true; //Make sure ShipState gets replicated AVar++; if (AVar > 10) AVar=0; } simulated function setInitialState() { super.setInitialState(); // don't disable my tick! enable('tick'); } simulated event drivingStatusChanged() { super.drivingStatusChanged(); // don't disable my tick! enable('tick'); } function KDriverEnter(Pawn P) { super.KDriverEnter(p); if (PlayerController(controller) != none) controller.gotoState(landMovementState); } simulated function prevWeapon() { shipSteering.roll = fclamp(shipSteering.roll - (rollUnit * turnSensitivity), -maximumTurnRate, maximumTurnRate); } simulated function nextWeapon() { shipSteering.roll = fclamp(shipSteering.roll + (rollUnit * turnSensitivity), -maximumTurnRate, maximumTurnRate); } simulated function updateRocketAcceleration(float deltaTime, float yawChange, float pitchChange) { if (deltaTime >= turnDecayTime) shipSteering = rot(0,0,0); else shipSteering *= (turnDecayTime-deltaTime) / turnDecayTime; // / 6000 because the aForward, etc. are 6000 by default when the appropriate button is pressed. shipThrust = (((vect(1,0,0) * PlayerController(Controller).aForward) + (vect(0,1,0) * PlayerController(Controller).aStrafe) + (vect(0,0,1) * PlayerController(Controller).aUp)) / 6000) >> rotation; if (vsize(shipThrust) > 1) shipThrust = normal(shipThrust); shipSteering.yaw = fclamp(shipSteering.yaw - (yawChange * turnSensitivity), -maximumTurnRate, maximumTurnRate); shipSteering.pitch = fclamp(shipSteering.pitch + (pitchChange * turnSensitivity), -maximumTurnRate, maximumTurnRate); } simulated function tick(float delta) { super.tick(delta); if (controller == none) { shipThrust = vect(0,0,0); shipSteering = rot(0,0,0); } if(!KIsAwake() && controller != none) KWake(); if(Role == ROLE_Authority && Level.NetMode != NM_StandAlone) PackState(); updateExtraForce(delta); } simulated function updateExtraForce(float delta) { local vector worldForward, worldDown, worldLeft; worldForward = vect(1, 0, 0) >> Rotation; worldDown = vect(0, 0, -1) >> Rotation; worldLeft = vect(0, -1, 0) >> Rotation; ExtraForce = ExtraForce + shipThrust * maximumThrust * delta; // Speed ExtraTorque = ExtraTorque + worldDown * shipSteering.yaw * delta; // Yaw ExtraTorque = ExtraTorque - worldLeft * -shipSteering.pitch * delta; // Pitch ExtraTorque = ExtraTorque + worldForward * -shipSteering.roll * delta; // Roll } simulated event KApplyForce(out vector Force, out vector Torque) { // This actually does the applying of the piled up force Force = ExtraForce; Torque = ExtraTorque; ExtraForce = vect(0,0,0); ExtraTorque = vect(0,0,0); } DefaultProperties { landMovementState=PlayerSpaceFlying rollUnit=1024 turnDecayTime=0.5 turnSensitivity=0.5 maximumTurnRate=5000 maximumThrust=2500 }
class FlyingDog extends KShip placeable CacheExempt; // triggers used to get into the FlyingDog var const vector FrontTriggerOffset; var SVehicleTrigger FLTrigger, FRTrigger; // Maximum speed at which you can get in the vehicle. var (FlyingDog) float TriggerSpeedThresh; var bool TriggerState; // true for on, false for off. // Destroyed Buggy var (FlyingDog) class<Actor> DestroyedEffect; var (FlyingDog) sound DestroyedSound; // Weapon var float FireCountdown; var (FlyingDog) float FireInterval; var (FlyingDog) vector weaponFireOffset; simulated function PostNetBeginPlay() { local vector RotX, RotY, RotZ; Super.PostNetBeginPlay(); GetAxes(Rotation,RotX,RotY,RotZ); // Only have triggers on server if(Level.NetMode != NM_Client) { // Create triggers for gettting into the FlyingDog FLTrigger = spawn(class'SVehicleTrigger', self,, Location + FrontTriggerOffset.X * RotX + FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); FLTrigger.SetBase(self); FLTrigger.SetCollision(true, false, false); FRTrigger = spawn(class'SVehicleTrigger', self,, Location + FrontTriggerOffset.X * RotX - FrontTriggerOffset.Y * RotY + FrontTriggerOffset.Z * RotZ); FRTrigger.SetBase(self); FRTrigger.SetCollision(true, false, false); TriggerState = true; } // If this is not 'authority' version - don't destroy it if there is a problem. // The network should sort things out. if(Role != ROLE_Authority) { KarmaParams(KParams).bDestroyOnSimError = False; } } simulated event Destroyed() { // Clean up random stuff attached to the car if(Level.NetMode != NM_Client) { FLTrigger.Destroy(); FRTrigger.Destroy(); } Super.Destroyed(); // Trigger destroyed sound and effect if(Level.NetMode != NM_DedicatedServer) { spawn(DestroyedEffect, self, , Location ); PlaySound(DestroyedSound); } } function KDriverEnter(Pawn p) { Super.KDriverEnter(p); p.bHidden = True; ReceiveLocalizedMessage(class'Vehicles.BulldogMessage', 1); } function bool KDriverLeave(bool bForceLeave) { local Pawn OldDriver; OldDriver = Driver; // If we succesfully got out of the car, make driver visible again. if( Super.KDriverLeave(bForceLeave) ) { OldDriver.bHidden = false; AmbientSound = None; return true; } else return false; } function fireWeapons(bool bWasAltFire) { local vector FireLocation; local PlayerController PC; // Client can't do firing if(Role != ROLE_Authority) return; FireLocation = Location + (weaponFireOffset >> Rotation); while(FireCountdown <= 0) { if(!bWasAltFire) { spawn(class'PROJ_TurretSkaarjPlasma', self, , FireLocation, rotation); // Play firing noise PlaySound(Sound'ONSVehicleSounds-S.Laser02', SLOT_None,,,,, false); PC = PlayerController(Controller); if (PC != None && PC.bEnableWeaponForceFeedback) PC.ClientPlayForceFeedback("RocketLauncherFire"); } else { spawn(class'PROJ_SpaceFighter_Rocket', self, , FireLocation, rotator(vector(rotation) + Vrand() * 0.05)); // Play firing noise PlaySound(Sound'AssaultSounds.HnShipFire01', SLOT_None,,,,, false); PC = PlayerController(Controller); if (PC != None && PC.bEnableWeaponForceFeedback) PC.ClientPlayForceFeedback("RocketLauncherFire"); } FireCountdown += FireInterval; } } // Fire a rocket (if we've had time to reload!) function VehicleFire(bool bWasAltFire) { Super.VehicleFire(bWasAltFire); if(FireCountdown < 0) { FireCountdown = 0; fireWeapons(bWasAltFire); } } simulated function Tick(float Delta) { Local float VMag; Super.Tick(Delta); // Weapons (run on server and replicated to client) if(Role == ROLE_Authority) { // Countdown to next shot FireCountdown -= Delta; // This is for sustained barrages. // Primary fire takes priority if(bVehicleIsFiring) fireWeapons(false); else if(bVehicleIsAltFiring) fireWeapons(true); } // Dont have triggers on network clients. if(Level.NetMode != NM_Client) { // If vehicle is moving, disable collision for trigger. VMag = VSize(Velocity); if(VMag < TriggerSpeedThresh && TriggerState == false) { FLTrigger.SetCollision(true, false, false); FRTrigger.SetCollision(true, false, false); TriggerState = true; } else if(VMag > TriggerSpeedThresh && TriggerState == true) { FLTrigger.SetCollision(false, false, false); FRTrigger.SetCollision(false, false, false); TriggerState = false; } } } // Really simple at the moment! function TakeDamage(int Damage, Pawn instigatedBy, Vector hitlocation, Vector momentum, class<DamageType> damageType) { // Avoid damage healing the car! if(Damage < 0) return; if(damageType == class'DamTypeSuperShockBeam') Health -= 100; // Instagib doesn't work on vehicles else Health -= 0.5 * Damage; // Weapons do less damage // The vehicle is dead! if(Health <= 0) { if ( Controller != None ) { if( Controller.bIsPlayer ) { ClientKDriverLeave(PlayerController(Controller)); // Just to reset HUD etc. Controller.PawnDied(self); // This should unpossess the controller and let the player respawn } else Controller.Destroy(); } Destroy(); // Destroy the vehicle itself (see Destroyed below) } //KAddImpulse(momentum, hitlocation); } // AI Related code function Actor GetBestEntry(Pawn P) { if ( VSize(P.Location - FLTrigger.Location) < VSize(P.Location - FRTrigger.Location) ) return FLTrigger; return FRTrigger; } defaultproperties { DrawType=DT_StaticMesh StaticMesh=StaticMesh'BulldogMeshes.Simple.S_Chassis' Begin Object Class=KarmaParamsRBFull Name=KParams0 KActorGravScale=0 KInertiaTensor(0)=20 KInertiaTensor(1)=0 KInertiaTensor(2)=0 KInertiaTensor(3)=30 KInertiaTensor(4)=0 KInertiaTensor(5)=48 KCOMOffset=(X=0.8,Y=0.0,Z=-0.7) KStartEnabled=True KFriction=1.6 KLinearDamping=1 KAngularDamping=10 bKNonSphericalInertia=False bHighDetailOnly=False bClientOnly=False bKDoubleTickRate=True Name="KParams0" End Object KParams=KarmaParams'KParams0' DrawScale=0.4 drawScale3D=(x=-1,y=1,z=1) DestroyedEffect=class'XEffects.RocketExplosion' DestroyedSound=sound'WeaponSounds.P1RocketLauncherAltFire' FrontTriggerOffset=(X=0,Y=165,Z=10) TriggerSpeedThresh=40 // Weaponry FireInterval=0.1 weaponFireOffset=(X=0,Y=0,Z=80) // Driver positions ExitPositions(0)=(X=0,Y=200,Z=100) ExitPositions(1)=(X=0,Y=-200,Z=100) ExitPositions(2)=(X=350,Y=0,Z=100) ExitPositions(3)=(X=-350,Y=0,Z=100) DrivePos=(X=-165,Y=0,Z=-100) Health=800 HealthMax=800 SoundRadius=255 }