//----------------------------------------------------------------------------- // Copyright (c) 2012 GarageGames, LLC // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to // deal in the Software without restriction, including without limitation the // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. //----------------------------------------------------------------------------- #include "platform/platform.h" #include "T3D/vehicles/wheeledVehicle.h" #include "math/mMath.h" #include "math/mathIO.h" #include "console/simBase.h" #include "console/console.h" #include "console/consoleTypes.h" #include "console/engineAPI.h" #include "collision/clippedPolyList.h" #include "collision/planeExtractor.h" #include "core/stream/bitStream.h" #include "core/dnet.h" #include "T3D/gameBase/gameConnection.h" #include "ts/tsShapeInstance.h" #include "T3D/fx/particleEmitter.h" #include "sfx/sfxSystem.h" #include "sfx/sfxTrack.h" #include "sfx/sfxSource.h" #include "sfx/sfxTypes.h" #include "scene/sceneManager.h" #include "core/resourceManager.h" #include "materials/materialDefinition.h" #include "materials/baseMatInstance.h" #include "lighting/lightQuery.h" // Collision masks are used to determine what type of objects the // wheeled vehicle will collide with. static U32 sClientCollisionMask = TerrainObjectType | PlayerObjectType | StaticShapeObjectType | VehicleObjectType | VehicleBlockerObjectType; // Misc. sound constants static F32 sMinSquealVolume = 0.05f; static F32 sIdleEngineVolume = 0.2f; //---------------------------------------------------------------------------- // Vehicle Tire Data Block //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1(WheeledVehicleTire); ConsoleDocClass( WheeledVehicleTire, "@brief Defines the properties of a WheeledVehicle tire.\n\n" "Tires act as springs and generate lateral and longitudinal forces to move " "the vehicle. These distortion/spring forces are what convert wheel angular " "velocity into forces that act on the rigid body.\n" "@ingroup Vehicles\n" ); WheeledVehicleTire::WheeledVehicleTire() { staticFriction = 1; kineticFriction = 0.5f; restitution = 1; radius = 0.6f; lateralForce = 10; lateralDamping = 1; lateralRelaxation = 1; longitudinalForce = 10; longitudinalDamping = 1; longitudinalRelaxation = 1; mass = 1.f; mShapeAsset.registerRefreshNotify(this); } bool WheeledVehicleTire::preload(bool server, String &errorStr) { // Load up the tire shape. ShapeBase has an option to force a // CRC check, this is left out here, but could be easily added. if (!getShape()) { errorStr = String::ToString("WheeledVehicleTire: Couldn't load shape \"%s\"", _getShapeAssetId()); return false; } else { // Determinw wheel radius from the shape's bounding box. // The tire should be built with it's hub axis along the // object's Y axis. radius = getShape()->mBounds.len_z() / 2; } return true; } void WheeledVehicleTire::initPersistFields() { docsURL; INITPERSISTFIELD_SHAPEASSET_REFACTOR(Shape, WheeledVehicleTire, "The shape to use for the wheel."); addFieldV( "mass", TypeRangedF32, Offset(mass, WheeledVehicleTire), &CommonValidators::PositiveFloat, "The mass of the wheel.\nCurrently unused." ); addFieldV( "radius", TypeRangedF32, Offset(radius, WheeledVehicleTire), &CommonValidators::PositiveFloat, "@brief The radius of the wheel.\n\n" "The radius is determined from the bounding box of the shape provided " "in the shapefile field, and does not need to be specified in script. " "The tire should be built with its hub axis along the object's Y-axis." ); addFieldV( "staticFriction", TypeRangedF32, Offset(staticFriction, WheeledVehicleTire), &CommonValidators::PositiveFloat, "Tire friction when the wheel is not slipping (has traction)." ); addFieldV( "kineticFriction", TypeRangedF32, Offset(kineticFriction, WheeledVehicleTire), &CommonValidators::PositiveFloat, "Tire friction when the wheel is slipping (no traction)." ); addFieldV( "restitution", TypeRangedF32, Offset(restitution, WheeledVehicleTire), &CommonValidators::PositiveFloat, "Tire restitution.\nCurrently unused." ); addFieldV( "lateralForce", TypeRangedF32, Offset(lateralForce, WheeledVehicleTire), &CommonValidators::PositiveFloat, "@brief Tire force perpendicular to the direction of movement.\n\n" "Lateral force can in simple terms be considered left/right steering " "force. WheeledVehicles are acted upon by forces generated by their tires " "and the lateralForce measures the magnitude of the force exerted on the " "vehicle when the tires are deformed along the x-axis. With real wheeled " "vehicles, tires are constantly being deformed and it is the interplay of " "deformation forces which determines how a vehicle moves. In Torque's " "simulation of vehicle physics, tire deformation obviously can't be handled " "with absolute realism, but the interplay of a vehicle's velocity, its " "engine's torque and braking forces, and its wheels' friction, lateral " "deformation, lateralDamping, lateralRelaxation, longitudinal deformation, " "longitudinalDamping, and longitudinalRelaxation forces, along with its " "wheels' angular velocity are combined to create a robust real-time " "physical simulation.\n\n" "For this field, the larger the value supplied for the lateralForce, the " "larger the effect steering maneuvers can have. In Torque tire forces are " "applied at a vehicle's wheel hubs." ); addFieldV( "lateralDamping", TypeRangedF32, Offset(lateralDamping, WheeledVehicleTire), &CommonValidators::PositiveFloat, "Damping force applied against lateral forces generated by the tire.\n\n" "@see lateralForce" ); addFieldV( "lateralRelaxation", TypeRangedF32, Offset(lateralRelaxation, WheeledVehicleTire), &CommonValidators::PositiveFloat, "@brief Relaxing force applied against lateral forces generated by the tire.\n\n" "The lateralRelaxation force measures how strongly the tire effectively " "un-deforms.\n\n@see lateralForce" ); addFieldV( "longitudinalForce", TypeRangedF32, Offset(longitudinalForce, WheeledVehicleTire), &CommonValidators::PositiveFloat, "@brief Tire force in the direction of movement.\n\n" "Longitudinal force can in simple terms be considered forward/backward " "movement force. WheeledVehicles are acted upon by forces generated by " "their tires and the longitudinalForce measures the magnitude of the " "force exerted on the vehicle when the tires are deformed along the y-axis.\n\n" "For this field, the larger the value, the larger the effect " "acceleration/deceleration inputs have.\n\n" "@see lateralForce" ); addFieldV( "longitudinalDamping", TypeRangedF32, Offset(longitudinalDamping, WheeledVehicleTire), &CommonValidators::PositiveFloat, "Damping force applied against longitudinal forces generated by the tire.\n\n" "@see longitudinalForce" ); addFieldV( "longitudinalRelaxation", TypeRangedF32, Offset(longitudinalRelaxation, WheeledVehicleTire), &CommonValidators::PositiveFloat, "@brief Relaxing force applied against longitudinal forces generated by the tire.\n\n" "The longitudinalRelaxation force measures how strongly the tire effectively " "un-deforms.\n\n" "@see longitudinalForce" ); Parent::initPersistFields(); } void WheeledVehicleTire::packData(BitStream* stream) { Parent::packData(stream); PACKDATA_ASSET_REFACTOR(Shape); stream->write(mass); stream->write(staticFriction); stream->write(kineticFriction); stream->write(restitution); stream->write(radius); stream->write(lateralForce); stream->write(lateralDamping); stream->write(lateralRelaxation); stream->write(longitudinalForce); stream->write(longitudinalDamping); stream->write(longitudinalRelaxation); } void WheeledVehicleTire::unpackData(BitStream* stream) { Parent::unpackData(stream); UNPACKDATA_ASSET_REFACTOR(Shape); stream->read(&mass); stream->read(&staticFriction); stream->read(&kineticFriction); stream->read(&restitution); stream->read(&radius); stream->read(&lateralForce); stream->read(&lateralDamping); stream->read(&lateralRelaxation); stream->read(&longitudinalForce); stream->read(&longitudinalDamping); stream->read(&longitudinalRelaxation); } //---------------------------------------------------------------------------- // Vehicle Spring Data Block //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1(WheeledVehicleSpring); ConsoleDocClass( WheeledVehicleSpring, "@brief Defines the properties of a WheeledVehicle spring.\n\n" "@ingroup Vehicles\n" ); WheeledVehicleSpring::WheeledVehicleSpring() { length = 1; force = 10; damping = 1; antiSway = 1; } void WheeledVehicleSpring::initPersistFields() { docsURL; addFieldV( "length", TypeRangedF32, Offset(length, WheeledVehicleSpring), &CommonValidators::PositiveFloat, "@brief Maximum spring length. ie. how far the wheel can extend from the " "root hub position.\n\n" "This should be set to the vertical (Z) distance the hub travels in the " "associated spring animation." ); addFieldV( "force", TypeRangedF32, Offset(force, WheeledVehicleSpring), &CommonValidators::PositiveFloat, "@brief Maximum spring force (when compressed to minimum length, 0).\n\n" "Increasing this will make the vehicle suspension ride higher (for a given " "vehicle mass), and also make the vehicle more bouncy when landing jumps." ); addFieldV( "damping", TypeRangedF32, Offset(damping, WheeledVehicleSpring), &CommonValidators::PositiveFloat, "@brief Force applied to slow changes to the extension of this spring.\n\n" "Increasing this makes the suspension stiffer which can help stabilise " "bouncy vehicles." ); addFieldV( "antiSwayForce", TypeRangedF32, Offset(antiSway, WheeledVehicleSpring), &CommonValidators::PositiveFloat, "@brief Force applied to equalize extension of the spring on the opposite " "wheel.\n\n" "This force helps to keep the suspension balanced when opposite wheels " "are at different heights." ); Parent::initPersistFields(); } void WheeledVehicleSpring::packData(BitStream* stream) { Parent::packData(stream); stream->write(length); stream->write(force); stream->write(damping); stream->write(antiSway); } void WheeledVehicleSpring::unpackData(BitStream* stream) { Parent::unpackData(stream); stream->read(&length); stream->read(&force); stream->read(&damping); stream->read(&antiSway); } //---------------------------------------------------------------------------- // Wheeled Vehicle Data Block //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- IMPLEMENT_CO_DATABLOCK_V1(WheeledVehicleData); ConsoleDocClass( WheeledVehicleData, "@brief Defines the properties of a WheeledVehicle.\n\n" "@ingroup Vehicles\n" ); typedef WheeledVehicleData::Sounds WheeledVehicleSoundsEnum; DefineEnumType(WheeledVehicleSoundsEnum); ImplementEnumType(WheeledVehicleSoundsEnum, "enum types.\n" "@ingroup WheeledVehicleData\n\n") { WheeledVehicleSoundsEnum::JetSound, "JetSound", "..." }, { WheeledVehicleSoundsEnum::EngineSound, "EngineSound", "..." }, { WheeledVehicleSoundsEnum::SquealSound, "SquealSound", "..." }, { WheeledVehicleSoundsEnum::WheelImpactSound, "WheelImpactSound", "..." }, EndImplementEnumType; WheeledVehicleData::WheeledVehicleData() { tireEmitter = 0; maxWheelSpeed = 40; engineTorque = 1; engineBrake = 1; brakeTorque = 1; brakeLightSequence = -1; steeringSequence = -1; wheelCount = 0; dMemset(&wheel, 0, sizeof(wheel)); for (S32 i = 0; i < MaxSounds; i++) INIT_SOUNDASSET_ARRAY(WheeledVehicleSounds, i); mDownForce = 0; } //---------------------------------------------------------------------------- /** Load the vehicle shape Loads and extracts information from the vehicle shape. Wheel Sequences spring# Wheel spring motion: time 0 = wheel fully extended, the hub must be displaced, but not directly animated as it will be rotated in code. Other Sequences steering Wheel steering: time 0 = full right, 0.5 = center brakeLight Brake light, time 0 = off, 1 = braking Wheel Nodes hub# Wheel hub The steering and animation sequences are optional. */ bool WheeledVehicleData::preload(bool server, String &errorStr) { if (!Parent::preload(server, errorStr)) return false; // A temporary shape instance is created so that we can // animate the shape and extract wheel information. TSShapeInstance* si = new TSShapeInstance(getShape(), false); // Resolve objects transmitted from server if (!server) { for (S32 i = 0; i < MaxSounds; i++) { if (!isWheeledVehicleSoundsValid(i)) { //return false; -TODO: trigger asset download } } if (tireEmitter) Sim::findObject(SimObjectId((uintptr_t)tireEmitter),tireEmitter); } // Extract wheel information from the shape TSThread* thread = si->addThread(); Wheel* wp = wheel; char buff[10]; for (S32 i = 0; i < MaxWheels; i++) { // The wheel must have a hub node to operate at all. dSprintf(buff,sizeof(buff),"hub%d",i); wp->springNode = getShape()->findNode(buff); if (wp->springNode != -1) { // Check for spring animation.. If there is none we just grab // the current position of the hub. Otherwise we'll animate // and get the position at time 0. dSprintf(buff,sizeof(buff),"spring%d",i); wp->springSequence = getShape()->findSequence(buff); if (wp->springSequence == -1) si->mNodeTransforms[wp->springNode].getColumn(3, &wp->pos); else { si->setSequence(thread,wp->springSequence,0); si->animate(); si->mNodeTransforms[wp->springNode].getColumn(3, &wp->pos); // Determin the length of the animation so we can scale it // according the actual wheel position. Point3F downPos; si->setSequence(thread,wp->springSequence,1); si->animate(); si->mNodeTransforms[wp->springNode].getColumn(3, &downPos); wp->springLength = wp->pos.z - downPos.z; if (!wp->springLength) wp->springSequence = -1; } // Match wheels that are mirrored along the Y axis. mirrorWheel(wp); wp++; } } wheelCount = wp - wheel; // Check for steering. Should think about normalizing the // steering animation the way the suspension is, but I don't // think it's as critical. steeringSequence = getShape()->findSequence("steering"); // Brakes brakeLightSequence = getShape()->findSequence("brakelight"); // Extract collision planes from shape collision detail level if (collisionDetails[0] != -1) { MatrixF imat(1); SphereF sphere; sphere.center = getShape()->center; sphere.radius = getShape()->mRadius; PlaneExtractorPolyList polyList; polyList.mPlaneList = &rigidBody.mPlaneList; polyList.setTransform(&imat, Point3F(1,1,1)); si->buildPolyList(&polyList,collisionDetails[0]); } delete si; return true; } //---------------------------------------------------------------------------- /** Find a matching lateral wheel Looks for a matching wheeling mirrored along the Y axis, within some tolerance (current 0.5m), if one is found, the two wheels are lined up. */ bool WheeledVehicleData::mirrorWheel(Wheel* we) { we->opposite = -1; for (Wheel* wp = wheel; wp != we; wp++) if (mFabs(wp->pos.y - we->pos.y) < 0.5) { we->pos.x = -wp->pos.x; we->pos.y = wp->pos.y; we->pos.z = wp->pos.z; we->opposite = wp - wheel; wp->opposite = we - wheel; return true; } return false; } //---------------------------------------------------------------------------- void WheeledVehicleData::initPersistFields() { docsURL; Parent::initPersistFields(); addGroup("Particle Effects"); addField("tireEmitter", TYPEID< ParticleEmitterData >(), Offset(tireEmitter, WheeledVehicleData), "ParticleEmitterData datablock used to generate particles from each wheel " "when the vehicle is moving and the wheel is in contact with the ground."); endGroup("Particle Effects"); addGroup("Sounds"); INITPERSISTFIELD_SOUNDASSET_ENUMED(WheeledVehicleSounds, WheeledVehicleSoundsEnum, MaxSounds, WheeledVehicleData, "Sounds related to wheeled vehicle."); endGroup("Sounds"); addGroup("Steering"); addFieldV("maxWheelSpeed", TypeRangedF32, Offset(maxWheelSpeed, WheeledVehicleData), &CommonValidators::PositiveFloat, "@brief Maximum linear velocity of each wheel.\n\n" "This caps the maximum speed of the vehicle." ); addFieldV("engineTorque", TypeRangedF32, Offset(engineTorque, WheeledVehicleData), &CommonValidators::PositiveFloat, "@brief Torque available from the engine at 100% throttle.\n\n" "This controls vehicle acceleration. ie. how fast it will reach maximum speed." ); addFieldV("engineBrake", TypeRangedF32, Offset(engineBrake, WheeledVehicleData), &CommonValidators::PositiveFloat, "@brief Braking torque applied by the engine when the throttle and brake " "are both 0.\n\n" "This controls how quickly the vehicle will coast to a stop." ); addFieldV("brakeTorque", TypeRangedF32, Offset(brakeTorque, WheeledVehicleData), &CommonValidators::PositiveFloat, "@brief Torque applied when braking.\n\n" "This controls how fast the vehicle will stop when the brakes are applied." ); addField("downforce", TypeF32, Offset(mDownForce, WheeledVehicleData), "downward force based on velocity."); endGroup("Steering"); } //---------------------------------------------------------------------------- void WheeledVehicleData::packData(BitStream* stream) { Parent::packData(stream); if (stream->writeFlag(tireEmitter)) stream->writeRangedU32(mPacked ? SimObjectId((uintptr_t)tireEmitter): tireEmitter->getId(),DataBlockObjectIdFirst,DataBlockObjectIdLast); for (S32 i = 0; i < MaxSounds; i++) { PACKDATA_SOUNDASSET_ARRAY(WheeledVehicleSounds, i); } stream->write(maxWheelSpeed); stream->write(engineTorque); stream->write(engineBrake); stream->write(brakeTorque); stream->write(mDownForce); } void WheeledVehicleData::unpackData(BitStream* stream) { Parent::unpackData(stream); tireEmitter = stream->readFlag()? (ParticleEmitterData*)(uintptr_t)stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast): 0; for (S32 i = 0; i < MaxSounds; i++) { UNPACKDATA_SOUNDASSET_ARRAY(WheeledVehicleSounds, i); } stream->read(&maxWheelSpeed); stream->read(&engineTorque); stream->read(&engineBrake); stream->read(&brakeTorque); stream->read(&mDownForce); } //---------------------------------------------------------------------------- // Wheeled Vehicle Class //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- IMPLEMENT_CO_NETOBJECT_V1(WheeledVehicle); ConsoleDocClass( WheeledVehicle, "@brief A wheeled vehicle.\n" "@ingroup Vehicles\n" ); WheeledVehicle::WheeledVehicle() { mDataBlock = 0; mBraking = false; mJetSound = NULL; mEngineSound = NULL; mSquealSound = NULL; mTailLightThread = 0; mSteeringThread = 0; for (S32 i = 0; i < WheeledVehicleData::MaxWheels; i++) { mWheel[i].springThread = 0; mWheel[i].Dy = mWheel[i].Dx = 0; mWheel[i].tire = 0; mWheel[i].spring = 0; mWheel[i].shapeInstance = 0; mWheel[i].steering = 0; mWheel[i].powered = true; mWheel[i].slipping = false; } } WheeledVehicle::~WheeledVehicle() { } void WheeledVehicle::initPersistFields() { docsURL; Parent::initPersistFields(); } //---------------------------------------------------------------------------- bool WheeledVehicle::onAdd() { if(!Parent::onAdd()) return false; addToScene(); return true; } void WheeledVehicle::onRemove() { // Delete the wheel resources if (mDataBlock != NULL) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (!wheel->emitter.isNull()) wheel->emitter->deleteWhenEmpty(); delete wheel->shapeInstance; } } // Stop the sounds SFX_DELETE( mJetSound ); SFX_DELETE( mEngineSound ); SFX_DELETE( mSquealSound ); // removeFromScene(); Parent::onRemove(); } //---------------------------------------------------------------------------- bool WheeledVehicle::onNewDataBlock(GameBaseData* dptr, bool reload) { // Delete any existing wheel resources if we're switching // datablocks. if (mDataBlock) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (!wheel->emitter.isNull()) { wheel->emitter->deleteWhenEmpty(); wheel->emitter = 0; } delete wheel->shapeInstance; wheel->shapeInstance = 0; } } // Load up the new datablock mDataBlock = dynamic_cast(dptr); if (!mDataBlock || !Parent::onNewDataBlock(dptr,reload)) return false; // Set inertial tensor, default for the vehicle is sphere if (mDataBlock->massBox.x > 0 && mDataBlock->massBox.y > 0 && mDataBlock->massBox.z > 0) mRigid.setObjectInertia(mDataBlock->massBox); else mRigid.setObjectInertia(mObjBox.maxExtents - mObjBox.minExtents); // Initialize the wheels... for (S32 i = 0; i < mDataBlock->wheelCount; i++) { Wheel* wheel = &mWheel[i]; wheel->data = &mDataBlock->wheel[i]; wheel->tire = 0; wheel->spring = 0; wheel->surface.contact = false; wheel->surface.object = NULL; wheel->avel = 0; wheel->apos = 0; wheel->extension = 1; wheel->slip = 0; wheel->springThread = 0; wheel->emitter = 0; // Steering on the front tires by default if (wheel->data->pos.y > 0) wheel->steering = 1; // Build wheel animation threads if (wheel->data->springSequence != -1) { wheel->springThread = mShapeInstance->addThread(); mShapeInstance->setSequence(wheel->springThread,wheel->data->springSequence,0); } // Each wheel get's it's own particle emitter if( mDataBlock->tireEmitter && isGhost() ) { wheel->emitter = new ParticleEmitter; wheel->emitter->onNewDataBlock( mDataBlock->tireEmitter, false ); wheel->emitter->registerObject(); } } // Steering sequence if (mDataBlock->steeringSequence != -1) { mSteeringThread = mShapeInstance->addThread(); mShapeInstance->setSequence(mSteeringThread,mDataBlock->steeringSequence,0); } else mSteeringThread = 0; // Brake light sequence if (mDataBlock->brakeLightSequence != -1) { mTailLightThread = mShapeInstance->addThread(); mShapeInstance->setSequence(mTailLightThread,mDataBlock->brakeLightSequence,0); } else mTailLightThread = 0; if (isGhost()) { // Create the sounds ahead of time. This reduces runtime // costs and makes the system easier to understand. SFX_DELETE( mEngineSound ); SFX_DELETE( mSquealSound ); SFX_DELETE( mJetSound ); if ( mDataBlock->getWheeledVehicleSounds(WheeledVehicleData::EngineSound) ) mEngineSound = SFX->createSource( mDataBlock->getWheeledVehicleSoundsProfile(WheeledVehicleData::EngineSound), &getTransform() ); if ( mDataBlock->getWheeledVehicleSounds(WheeledVehicleData::SquealSound) ) mSquealSound = SFX->createSource( mDataBlock->getWheeledVehicleSoundsProfile(WheeledVehicleData::SquealSound), &getTransform() ); if ( mDataBlock->getWheeledVehicleSounds(WheeledVehicleData::JetSound) ) mJetSound = SFX->createSource( mDataBlock->getWheeledVehicleSoundsProfile(WheeledVehicleData::JetSound), &getTransform() ); } scriptOnNewDataBlock(reload); return true; } //---------------------------------------------------------------------------- S32 WheeledVehicle::getWheelCount() { // Return # of hubs defined on the car body return mDataBlock? mDataBlock->wheelCount: 0; } void WheeledVehicle::setWheelSteering(S32 wheel,F32 steering) { AssertFatal(wheel >= 0 && wheel < WheeledVehicleData::MaxWheels,"Wheel index out of bounds"); mWheel[wheel].steering = mClampF(steering,-1,1); setMaskBits(WheelMask); } void WheeledVehicle::setWheelPowered(S32 wheel,bool powered) { AssertFatal(wheel >= 0 && wheel < WheeledVehicleData::MaxWheels,"Wheel index out of bounds"); mWheel[wheel].powered = powered; setMaskBits(WheelMask); } void WheeledVehicle::setWheelTire(S32 wheel,WheeledVehicleTire* tire) { AssertFatal(wheel >= 0 && wheel < WheeledVehicleData::MaxWheels,"Wheel index out of bounds"); mWheel[wheel].tire = tire; setMaskBits(WheelMask); } void WheeledVehicle::setWheelSpring(S32 wheel,WheeledVehicleSpring* spring) { AssertFatal(wheel >= 0 && wheel < WheeledVehicleData::MaxWheels,"Wheel index out of bounds"); mWheel[wheel].spring = spring; setMaskBits(WheelMask); } void WheeledVehicle::getWheelInstAndTransform( U32 index, TSShapeInstance** inst, MatrixF* xfrm ) const { AssertFatal( index < WheeledVehicleData::MaxWheels, "WheeledVehicle::getWheelInstAndTransform() - Bad wheel index!" ); const Wheel* wheel = &mWheel[index]; *inst = wheel->shapeInstance; if ( !xfrm || !wheel->shapeInstance ) return; MatrixF world = getRenderTransform(); world.scale( mObjScale ); // Steering & spring extension MatrixF hub(EulerF(0,0,mSteering.x * wheel->steering)); Point3F pos = wheel->data->pos; pos.z -= wheel->spring->length * wheel->extension; hub.setColumn(3,pos); world.mul(hub); // Wheel rotation MatrixF rot(EulerF(wheel->apos * M_2PI,0,0)); world.mul(rot); // Rotation the tire to face the right direction // (could pre-calculate this) MatrixF wrot(EulerF(0,0,(wheel->data->pos.x > 0)? M_PI/2: -M_PI/2)); world.mul(wrot); *xfrm = world; } //---------------------------------------------------------------------------- void WheeledVehicle::processTick(const Move* move) { Parent::processTick(move); } void WheeledVehicle::updateMove(const Move* move) { Parent::updateMove(move); // Brake on trigger mBraking = move->trigger[2]; // Set the tail brake light thread direction based on the brake state. if (mTailLightThread) mShapeInstance->setTimeScale(mTailLightThread, mBraking? 1.0f : -1.0f); // Update the steering animation: sequence time 0 is full right, // and time 0.5 is straight ahead. if (mSteeringThread) { F32 t = (mSteering.x * mFabs(mSteering.x)) / mDataBlock->maxSteeringAngle; mShapeInstance->setPos(mSteeringThread, 0.5 - t * 0.5); } } //---------------------------------------------------------------------------- void WheeledVehicle::advanceTime(F32 dt) { PROFILE_SCOPE( WheeledVehicle_AdvanceTime ); Parent::advanceTime(dt); // Stick the wheels to the ground. This is purely so they look // good while the vehicle is being interpolated. extendWheels(isClientObject()); // Update wheel angular position and slip, this is a client visual // feature only, it has no affect on the physics. F32 slipTotal = 0; F32 torqueTotal = 0; Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) if (wheel->tire && wheel->spring) { // Update angular position wheel->apos += (wheel->avel * dt) / M_2PI; wheel->apos -= mFloor(wheel->apos); if (wheel->apos < 0) wheel->apos = 1 - wheel->apos; // Keep track of largest slip slipTotal += wheel->slip; torqueTotal += wheel->torqueScale; } // Update the sounds based on wheel slip and torque output updateSquealSound(slipTotal / mDataBlock->wheelCount); updateEngineSound(sIdleEngineVolume + (1 - sIdleEngineVolume) * (1 - (torqueTotal / mDataBlock->wheelCount))); updateJetSound(); updateWheelThreads(); updateWheelParticles(dt); // Update the steering animation: sequence time 0 is full right, // and time 0.5 is straight ahead. if (mSteeringThread) { F32 t = (mSteering.x * mFabs(mSteering.x)) / mDataBlock->maxSteeringAngle; mShapeInstance->setPos(mSteeringThread,0.5 - t * 0.5); } // Animate the tail light. The direction of the thread is // set based on vehicle braking. if (mTailLightThread) mShapeInstance->advanceTime(dt,mTailLightThread); } //---------------------------------------------------------------------------- /** Update the rigid body forces on the vehicle This method calculates the forces acting on the body, including gravity, suspension & tire forces. */ void WheeledVehicle::updateForces(F32 dt) { PROFILE_SCOPE( WheeledVehicle_UpdateForces ); extendWheels(); if (mDisableMove) return; F32 aMomentum = mMass / mDataBlock->wheelCount; // Get the current matrix and extact vectors MatrixF currMatrix; mRigid.getTransform(&currMatrix); Point3F bx,by,bz; currMatrix.getColumn(0,&bx); currMatrix.getColumn(1,&by); currMatrix.getColumn(2,&bz); // Steering angles from current steering wheel position F32 quadraticSteering = -(mSteering.x * mFabs(mSteering.x)); F32 cosSteering,sinSteering; mSinCos(quadraticSteering, sinSteering, cosSteering); // Calculate Engine and brake torque values used later by in // wheel calculations. F32 engineTorque,brakeVel; if (mBraking) { brakeVel = (mDataBlock->brakeTorque / aMomentum) * dt; engineTorque = 0; } else { if (mThrottle) { engineTorque = mDataBlock->engineTorque * mThrottle; brakeVel = 0; // Double the engineTorque to help out the jets if (mThrottle > 0 && mJetting) engineTorque *= 2; } else { // Engine brake. brakeVel = (mDataBlock->engineBrake / aMomentum) * dt; engineTorque = 0; } } // Integrate forces, we'll do this ourselves here instead of // relying on the rigid class which does it during movement. Wheel* wend = &mWheel[mDataBlock->wheelCount]; mRigid.clearForces(); //calculate here so we can stiffen the springs a bit based on //the final amount of downforce //get the speed F32 downForce = mRigid.linVelocity.lenSquared() <= 1 ? 1 : mRigid.linVelocity.lenSquared(); //grab the datablock var downForce *= mDataBlock->mDownForce; //make it a smaller number so we can multiply gravity by it downForce = mSqrt(downForce); downForce *= TickSec; //ensure that it is not smaller then one, cause mulltiplying gravity by fractions is baaaad downForce = downForce < 1 ? 1 : downForce; // Calculate vertical load for friction. Divide up the spring // forces across all the wheels that are in contact with // the ground. U32 contactCount = 0; F32 verticalLoad = 0; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->tire && wheel->spring && wheel->surface.contact) { verticalLoad += wheel->spring->force * (1 - wheel->extension); contactCount++; } } if (contactCount) verticalLoad /= contactCount; // Sum up spring and wheel torque forces for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (!wheel->tire || !wheel->spring) continue; F32 Fy = 0; if (wheel->surface.contact) { // First, let's compute the wheel's position, and worldspace velocity Point3F pos, r, localVel; currMatrix.mulP(wheel->data->pos, &pos); mRigid.getOriginVector(pos,&r); mRigid.getVelocity(r, &localVel); // Spring force & damping F32 spring = wheel->spring->force * (1 - wheel->extension); spring += (spring * downForce); if (wheel->extension == 0) //spring fully compressed { // Apply impulses to the rigid body to keep it from // penetrating the surface. F32 n = -mDot(localVel,Point3F(0,0,1)); if (n >= 0) { // Collision impulse, straight forward force stuff. F32 d = mRigid.getZeroImpulse(r,Point3F(0,0,1)); F32 j = n * (1 + mRigid.restitution) * d; mRigid.force += Point3F(0,0,1) * j; } } F32 damping = wheel->spring->damping * -(mDot(bz, localVel) / wheel->spring->length); if (damping < 0) damping = 0; // Anti-sway force based on difference in suspension extension F32 antiSway = 0; if (wheel->data->opposite != -1) { Wheel* oppositeWheel = &mWheel[wheel->data->opposite]; if (oppositeWheel->surface.contact) antiSway = ((oppositeWheel->extension - wheel->extension) * wheel->spring->antiSway); if (antiSway < 0) antiSway = 0; } // Spring forces act straight up and are applied at the // spring's root position. Point3F t, forceVector = bz * (spring + damping + antiSway); mCross(r, forceVector, &t); mRigid.torque += t; mRigid.force += forceVector; // Tire direction vectors perpendicular to surface normal Point3F wheelXVec = bx * cosSteering; wheelXVec += by * sinSteering * wheel->steering; Point3F tireX, tireY; mCross(wheel->surface.normal, wheelXVec, &tireY); tireY.normalize(); mCross(tireY, wheel->surface.normal, &tireX); tireX.normalize(); // Velocity of tire at the surface contact Point3F wheelContact, wheelVelocity; mRigid.getOriginVector(wheel->surface.pos,&wheelContact); mRigid.getVelocity(wheelContact, &wheelVelocity); F32 xVelocity = mDot(tireX, wheelVelocity); F32 yVelocity = mDot(tireY, wheelVelocity); // Tires act as springs and generate lateral and longitudinal // forces to move the vehicle. These distortion/spring forces // are what convert wheel angular velocity into forces that // act on the rigid body. // Longitudinal tire deformation force F32 ddy = (wheel->avel * wheel->tire->radius - yVelocity) - wheel->tire->longitudinalRelaxation * mFabs(wheel->avel) * wheel->Dy; wheel->Dy += ddy * dt; Fy = (wheel->tire->longitudinalForce * wheel->Dy + wheel->tire->longitudinalDamping * ddy); // Lateral tire deformation force F32 ddx = xVelocity - wheel->tire->lateralRelaxation * mFabs(wheel->avel) * wheel->Dx; wheel->Dx += ddx * dt; F32 Fx = -(wheel->tire->lateralForce * wheel->Dx + wheel->tire->lateralDamping * ddx); // Vertical load on the tire verticalLoad = spring + damping + antiSway; if (verticalLoad < 0) verticalLoad = 0; // Adjust tire forces based on friction F32 surfaceFriction = 1; F32 mu = surfaceFriction * (wheel->slipping ? wheel->tire->kineticFriction : wheel->tire->staticFriction); F32 Fn = verticalLoad * mu; Fn *= Fn; F32 Fw = Fx * Fx + Fy * Fy; if (Fw > Fn) { F32 K = mSqrt(Fn / Fw); Fy *= K; Fx *= K; wheel->Dy *= K; wheel->Dx *= K; wheel->slip = 1 - K; wheel->slipping = true; } else { wheel->slipping = false; wheel->slip = 0; } // Tire forces act through the tire direction vectors parallel // to the surface and are applied at the wheel hub. forceVector = (tireX * Fx) + (tireY * Fy); pos -= bz * (wheel->spring->length * wheel->extension); mRigid.getOriginVector(pos,&r); mCross(r, forceVector, &t); mRigid.torque += t; mRigid.force += forceVector; } else { // Wheel not in contact with the ground wheel->torqueScale = 0; wheel->slip = 0; // Relax the tire deformation wheel->Dy += (-wheel->tire->longitudinalRelaxation * mFabs(wheel->avel) * wheel->Dy) * dt; wheel->Dx += (-wheel->tire->lateralRelaxation * mFabs(wheel->avel) * wheel->Dx) * dt; } // Adjust the wheel's angular velocity based on engine torque // and tire deformation forces. if (wheel->powered) { F32 maxAvel = mDataBlock->maxWheelSpeed / wheel->tire->radius; wheel->torqueScale = (mFabs(wheel->avel) > maxAvel) ? 0 : 1 - (mFabs(wheel->avel) / maxAvel); } else wheel->torqueScale = 0; wheel->avel += (((wheel->torqueScale * engineTorque) - Fy * wheel->tire->radius) / aMomentum) * dt; // Adjust the wheel's angular velocity based on brake torque. // This is done after avel update to make sure we come to a // complete stop. if (brakeVel > mFabs(wheel->avel)) wheel->avel = 0; else if (wheel->avel > 0) wheel->avel -= brakeVel; else wheel->avel += brakeVel; } // Jet Force if (mJetting) mRigid.force += by * mDataBlock->jetForce; // Add in force from physical zones... mRigid.force += mAppliedForce; // Container drag & buoyancy mRigid.force += Point3F(0, 0, mRigid.mass * mNetGravity * downForce); mRigid.force -= mRigid.linVelocity * mDrag; mRigid.torque -= mRigid.angMomentum * mDrag; // If we've added anything other than gravity, then we're no // longer at rest. Could test this a little more efficiently... if (mRigid.atRest && (mRigid.force.len() || mRigid.torque.len())) mRigid.atRest = false; // Integrate and update velocity mRigid.linMomentum += mRigid.force * dt; mRigid.angMomentum += mRigid.torque * dt; mRigid.updateVelocity(); // Since we've already done all the work, just need to clear this out. mRigid.clearForces(); // If we're still atRest, make sure we're not accumulating anything if (mRigid.atRest) mRigid.setAtRest(); } //---------------------------------------------------------------------------- /** Extend the wheels The wheels are extended until they contact a surface. The extension is instantaneous. The wheels are extended before force calculations and also on during client side interpolation (so that the wheels are glued to the ground). */ void WheeledVehicle::extendWheels(bool clientHack) { PROFILE_SCOPE( WheeledVehicle_ExtendWheels ); disableCollision(); MatrixF currMatrix; if(clientHack) currMatrix = getRenderTransform(); else mRigid.getTransform(&currMatrix); // Does a single ray cast down for now... this will have to be // changed to something a little more complicated to avoid getting // stuck in cracks. Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->tire && wheel->spring) { wheel->extension = 1; // The ray is cast from the spring mount point to the tip of // the tire. If there is a collision the spring extension is // adjust to remove the tire radius. Point3F sp,vec; currMatrix.mulP(wheel->data->pos,&sp); currMatrix.mulV(VectorF(0,0,-wheel->spring->length),&vec); F32 ts = wheel->tire->radius / wheel->spring->length; Point3F ep = sp + (vec * (1 + ts)); ts = ts / (1+ts); RayInfo rInfo; if (mContainer->castRay(sp, ep, sClientCollisionMask & ~PlayerObjectType, &rInfo)) { wheel->surface.contact = true; wheel->extension = (rInfo.t < ts)? 0: (rInfo.t - ts) / (1 - ts); wheel->surface.normal = rInfo.normal; wheel->surface.pos = rInfo.point; wheel->surface.material = rInfo.material; wheel->surface.object = rInfo.object; wheel->slipping = false; } else { wheel->surface.contact = false; wheel->slipping = true; } } } enableCollision(); } //---------------------------------------------------------------------------- /** Update wheel steering and suspension threads. These animations are purely cosmetic and this method is only invoked on the client. */ void WheeledVehicle::updateWheelThreads() { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->tire && wheel->spring && wheel->springThread) { // Scale the spring animation time to match the current // position of the wheel. We'll also check to make sure // the animation is long enough, if it isn't, just stick // it at the end. F32 pos = wheel->extension * wheel->spring->length; if (pos > wheel->data->springLength) pos = 1; else pos /= wheel->data->springLength; mShapeInstance->setPos(wheel->springThread,pos); } } } //---------------------------------------------------------------------------- /** Update wheel particles effects These animations are purely cosmetic and this method is only invoked on the client. Particles are emitted as long as the moving. */ void WheeledVehicle::updateWheelParticles(F32 dt) { // OMG l33t hax extendWheels(true); Point3F vel = Parent::getVelocity(); F32 speed = vel.len(); // Don't bother if we're not moving. if (speed > 1.0f) { Point3F axis = vel; axis.normalize(); Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { // Is this wheel in contact with the ground? if (wheel->tire && wheel->spring && !wheel->emitter.isNull() && wheel->surface.contact && wheel->surface.object ) { Material* material = ( wheel->surface.material ? dynamic_cast< Material* >( wheel->surface.material->getMaterial() ) : 0 ); if( material)//&& material->mShowDust ) { LinearColorF colorList[ ParticleData::PDC_NUM_KEYS ]; for( U32 x = 0; x < getMin( Material::NUM_EFFECT_COLOR_STAGES, ParticleData::PDC_NUM_KEYS ); ++ x ) colorList[ x ] = material->mEffectColor[ x ]; for( U32 x = Material::NUM_EFFECT_COLOR_STAGES; x < ParticleData::PDC_NUM_KEYS; ++ x ) colorList[ x ].set( 1.0, 1.0, 1.0, 0.0 ); wheel->emitter->setColors( colorList ); // Emit the dust, the density (time) is scaled by the // the vehicles velocity. wheel->emitter->emitParticles( wheel->surface.pos, true, axis, vel, (U32)(3/*dt * (speed / mDataBlock->maxWheelSpeed) * 1000 * wheel->slip*/)); } } } } } //---------------------------------------------------------------------------- /** Update engine sound This method is only invoked by clients. */ void WheeledVehicle::updateEngineSound(F32 level) { if ( !mEngineSound ) return; if ( !mEngineSound->isPlaying() ) mEngineSound->play(); mEngineSound->setTransform( getTransform() ); mEngineSound->setVelocity( getVelocity() ); //mEngineSound->setVolume( level ); // Adjust pitch F32 pitch = ((level-sIdleEngineVolume) * 1.3f); if (pitch < 0.4f) pitch = 0.4f; mEngineSound->setPitch( pitch ); } //---------------------------------------------------------------------------- /** Update wheel skid sound This method is only invoked by clients. */ void WheeledVehicle::updateSquealSound(F32 level) { if ( !mSquealSound ) return; if ( level < sMinSquealVolume ) { mSquealSound->stop(); return; } if ( !mSquealSound->isPlaying() ) mSquealSound->play(); mSquealSound->setTransform( getTransform() ); mSquealSound->setVolume( level ); } //---------------------------------------------------------------------------- /** Update jet sound This method is only invoked by clients. */ void WheeledVehicle::updateJetSound() { if ( !mJetSound ) return; if ( !mJetting ) { mJetSound->stop(); return; } if ( !mJetSound->isPlaying() ) mJetSound->play(); mJetSound->setTransform( getTransform() ); } //---------------------------------------------------------------------------- U32 WheeledVehicle::getCollisionMask() { return sClientCollisionMask; } //---------------------------------------------------------------------------- /** Build a collision polylist The polylist is filled with polygons representing the collision volume and the wheels. */ bool WheeledVehicle::buildPolyList(PolyListContext context, AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere) { PROFILE_SCOPE( WheeledVehicle_BuildPolyList ); // Parent will take care of body collision. Parent::buildPolyList(context, polyList,box,sphere); // Add wheels as boxes. Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->tire && wheel->spring) { Box3F wbox; F32 radius = wheel->tire->radius; wbox.minExtents.x = -(wbox.maxExtents.x = radius / 2); wbox.minExtents.y = -(wbox.maxExtents.y = radius); wbox.minExtents.z = -(wbox.maxExtents.z = radius); MatrixF mat = mObjToWorld; Point3F sp,vec; mObjToWorld.mulP(wheel->data->pos,&sp); mObjToWorld.mulV(VectorF(0,0,-wheel->spring->length),&vec); Point3F ep = sp + (vec * wheel->extension); mat.setColumn(3,ep); polyList->setTransform(&mat,Point3F(1,1,1)); polyList->addBox(wbox); } } return !polyList->isEmpty(); } void WheeledVehicle::prepBatchRender(SceneRenderState* state, S32 mountedImageIndex ) { Parent::prepBatchRender( state, mountedImageIndex ); if ( mountedImageIndex != -1 ) return; // Set up our render state *here*, // before the push world matrix, so // that wheel rendering will be correct. TSRenderState rdata; rdata.setSceneState( state ); // We might have some forward lit materials // so pass down a query to gather lights. LightQuery query; query.init( getWorldSphere() ); rdata.setLightQuery( &query ); // Shape transform GFX->pushWorldMatrix(); MatrixF mat = getRenderTransform(); mat.scale( mObjScale ); GFX->setWorldMatrix( mat ); Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->shapeInstance) { GFX->pushWorldMatrix(); // Steering & spring extension MatrixF hub(EulerF(0,0,mSteering.x * wheel->steering)); Point3F pos = wheel->data->pos; pos.z -= wheel->spring->length * wheel->extension; hub.setColumn(3,pos); GFX->multWorld(hub); // Wheel rotation MatrixF rot(EulerF(wheel->apos * M_2PI,0,0)); GFX->multWorld(rot); // Rotation the tire to face the right direction // (could pre-calculate this) MatrixF wrot(EulerF(0,0,(wheel->data->pos.x > 0)? M_PI/2: -M_PI/2)); GFX->multWorld(wrot); // Render! wheel->shapeInstance->animate(); wheel->shapeInstance->render( rdata ); if (mCloakLevel != 0.0f) wheel->shapeInstance->setAlphaAlways(1.0f - mCloakLevel); else wheel->shapeInstance->setAlphaAlways(1.0f); GFX->popWorldMatrix(); } } GFX->popWorldMatrix(); } //---------------------------------------------------------------------------- void WheeledVehicle::writePacketData(GameConnection *connection, BitStream *stream) { Parent::writePacketData(connection, stream); stream->writeFlag(mBraking); } void WheeledVehicle::readPacketData(GameConnection *connection, BitStream *stream) { Parent::readPacketData(connection, stream); mBraking = stream->readFlag(); Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (wheel->tire && wheel->spring) { // Update angular position wheel->apos += (wheel->avel) / M_2PI; wheel->apos -= mFloor(wheel->apos); if (wheel->apos < 0) wheel->apos = 1 - wheel->apos; } } // Rigid state is transmitted by the parent... setPosition(mRigid.linPosition,mRigid.angPosition); mDelta.pos = mRigid.linPosition; mDelta.rot[1] = mRigid.angPosition; // Stick the wheels to the ground. This is purely so they look // good while the vehicle is being interpolated. extendWheels(isClientObject()); updateWheelThreads(); } //---------------------------------------------------------------------------- U32 WheeledVehicle::packUpdate(NetConnection *con, U32 mask, BitStream *stream) { U32 retMask = Parent::packUpdate(con, mask, stream); // Update wheel datablock information if (stream->writeFlag(mask & WheelMask)) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (stream->writeFlag(wheel->tire && wheel->spring)) { stream->writeRangedU32(wheel->tire->getId(), DataBlockObjectIdFirst,DataBlockObjectIdLast); stream->writeRangedU32(wheel->spring->getId(), DataBlockObjectIdFirst,DataBlockObjectIdLast); stream->writeFlag(wheel->powered); // Steering must be sent with full precision as it's // used directly in state force calculations. stream->write(wheel->steering); } } } // The rest of the data is part of the control object packet update. // If we're controlled by this client, we don't need to send it. if (stream->writeFlag(getControllingClient() == con && !(mask & InitialUpdateMask))) return retMask; stream->writeFlag(mBraking); if (stream->writeFlag(mask & PositionMask)) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { stream->write(wheel->avel); stream->write(wheel->Dy); stream->write(wheel->Dx); } } return retMask; } void WheeledVehicle::unpackUpdate(NetConnection *con, BitStream *stream) { Parent::unpackUpdate(con,stream); // Update wheel datablock information if (stream->readFlag()) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { if (stream->readFlag()) { SimObjectId tid = stream->readRangedU32(DataBlockObjectIdFirst,DataBlockObjectIdLast); SimObjectId sid = stream->readRangedU32(DataBlockObjectIdFirst,DataBlockObjectIdLast); if (!Sim::findObject(tid,wheel->tire) || !Sim::findObject(sid,wheel->spring)) { con->setLastError("Invalid packet WheeledVehicle::unpackUpdate()"); return; } wheel->powered = stream->readFlag(); stream->read(&wheel->steering); // Create an instance of the tire for rendering delete wheel->shapeInstance; wheel->shapeInstance = (wheel->tire->getShape() == NULL) ? 0: new TSShapeInstance(wheel->tire->getShape()); } } } // After this is data that we only need if we're not the // controlling client. if (stream->readFlag()) return; mBraking = stream->readFlag(); if (stream->readFlag()) { Wheel* wend = &mWheel[mDataBlock->wheelCount]; for (Wheel* wheel = mWheel; wheel < wend; wheel++) { stream->read(&wheel->avel); stream->read(&wheel->Dy); stream->read(&wheel->Dx); } } } //---------------------------------------------------------------------------- // Console Methods //---------------------------------------------------------------------------- //---------------------------------------------------------------------------- DefineEngineMethod( WheeledVehicle, setWheelSteering, bool, ( S32 wheel, F32 steering ),, "@brief Set how much the wheel is affected by steering.\n\n" "The steering factor controls how much the wheel is rotated by the vehicle " "steering. For example, most cars would have their front wheels set to 1.0, " "and their rear wheels set to 0 since only the front wheels should turn.\n\n" "Negative values will turn the wheel in the opposite direction to the steering " "angle.\n" "@param wheel index of the wheel to set (hub node #)\n" "@param steering steering factor from -1 (full inverse) to 1 (full)\n" "@return true if successful, false if failed\n\n" ) { if ( wheel >= 0 && wheel < object->getWheelCount() ) { object->setWheelSteering( wheel, steering ); return true; } else Con::warnf("setWheelSteering: wheel index %d out of bounds, vehicle has %d hubs", wheel, object->getWheelCount()); return false; } DefineEngineMethod( WheeledVehicle, setWheelPowered, bool, ( S32 wheel, bool powered ),, "@brief Set whether the wheel is powered (has torque applied from the engine).\n\n" "A rear wheel drive car for example would set the front wheels to false, " "and the rear wheels to true.\n" "@param wheel index of the wheel to set (hub node #)\n" "@param powered flag indicating whether to power the wheel or not\n" "@return true if successful, false if failed\n\n" ) { if ( wheel >= 0 && wheel < object->getWheelCount() ) { object->setWheelPowered( wheel, powered ); return true; } else Con::warnf("setWheelPowered: wheel index %d out of bounds, vehicle has %d hubs", wheel, object->getWheelCount()); return false; } DefineEngineMethod( WheeledVehicle, setWheelTire, bool, ( S32 wheel, const char* tire ),, "@brief Set the WheeledVehicleTire datablock for this wheel.\n" "@param wheel index of the wheel to set (hub node #)\n" "@param tire WheeledVehicleTire datablock\n" "@return true if successful, false if failed\n\n" "@tsexample\n" "%obj.setWheelTire( 0, FrontTire );\n" "@endtsexample\n" ) { WheeledVehicleTire* tireObj = NULL; if (wheel < 0 || wheel > object->getWheelCount()) { Con::warnf("setWheelTire: invalid wheel index %d, vehicle has %d hubs", wheel, object->getWheelCount()); return false; } else if (!Sim::findObject(tire, tireObj)) { Con::warnf("setWheelSpring: invalid spring %s", tire); return false; } object->setWheelTire(wheel, tireObj); return true; } DefineEngineMethod( WheeledVehicle, setWheelSpring, bool, ( S32 wheel, const char* spring ),, "@brief Set the WheeledVehicleSpring datablock for this wheel.\n" "@param wheel index of the wheel to set (hub node #)\n" "@param spring WheeledVehicleSpring datablock\n" "@return true if successful, false if failed\n\n" "@tsexample\n" "%obj.setWheelSpring( 0, FrontSpring );\n" "@endtsexample\n" ) { WheeledVehicleSpring* springObj = NULL; if (wheel < 0 || wheel > object->getWheelCount()) { Con::warnf("setWheelSpring: invalid wheel index %d, vehicle has %d hubs", wheel, object->getWheelCount()); return false; } else if (!Sim::findObject(spring, springObj)) { Con::warnf("setWheelSpring: invalid spring %s", spring); return false; } object->setWheelSpring(wheel, springObj); return true; } DefineEngineMethod( WheeledVehicle, getWheelCount, S32, (),, "@brief Get the number of wheels on this vehicle.\n" "@return the number of wheels (equal to the number of hub nodes defined in the model)\n\n" ) { return object->getWheelCount(); }