tge/engine/game/vehicles/wheeledVehicle.cc
2017-04-17 06:17:10 -06:00

1618 lines
52 KiB
C++
Executable File

//-----------------------------------------------------------------------------
// Torque Game Engine
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------
#include "game/vehicles/wheeledVehicle.h"
#include "platform/platform.h"
#include "dgl/dgl.h"
#include "game/game.h"
#include "math/mMath.h"
#include "math/mathIO.h"
#include "console/simBase.h"
#include "console/console.h"
#include "console/consoleTypes.h"
#include "collision/clippedPolyList.h"
#include "collision/planeExtractor.h"
#include "game/moveManager.h"
#include "core/bitStream.h"
#include "core/dnet.h"
#include "game/gameConnection.h"
#include "ts/tsShapeInstance.h"
#include "game/fx/particleEngine.h"
#include "audio/audioDataBlock.h"
#include "sceneGraph/sceneGraph.h"
#include "sim/decalManager.h"
#include "dgl/materialPropertyMap.h"
#include "terrain/terrData.h"
#include "sceneGraph/detailManager.h"
//----------------------------------------------------------------------------
// Collision masks are used to determin what type of objects the
// wheeled vehicle will collide with.
static U32 sClientCollisionMask =
TerrainObjectType | InteriorObjectType |
PlayerObjectType | StaticShapeObjectType |
VehicleObjectType | VehicleBlockerObjectType |
StaticTSObjectType;
// Gravity constant
static F32 sWheeledVehicleGravity = -20;
// Misc. sound constants
static F32 sMinSquealVolume = 0.05;
static F32 sIdleEngineVolume = 0.2;
//----------------------------------------------------------------------------
// Vehicle Tire Data Block
//----------------------------------------------------------------------------
IMPLEMENT_CO_DATABLOCK_V1(WheeledVehicleTire);
WheeledVehicleTire::WheeledVehicleTire()
{
shape = 0;
shapeName = "";
staticFriction = 1;
kineticFriction = 0.5;
restitution = 1;
radius = 0.6;
lateralForce = 10;
lateralDamping = 1;
lateralRelaxation = 1;
longitudinalForce = 10;
longitudinalDamping = 1;
longitudinalRelaxation = 1;
}
bool WheeledVehicleTire::preload(bool server, char errorBuffer[256])
{
// 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 (shapeName && shapeName[0]) {
// Load up the shape resource
shape = ResourceManager->load(shapeName);
if (!bool(shape)) {
dSprintf(errorBuffer, 256, "WheeledVehicleTire: Couldn't load shape \"%s\"",shapeName);
return false;
}
// Determin 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 = shape->bounds.len_z() / 2;
}
return true;
}
void WheeledVehicleTire::initPersistFields()
{
Parent::initPersistFields();
addField("shapeFile",TypeFilename,Offset(shapeName,WheeledVehicleTire));
addField("mass", TypeF32, Offset(mass, WheeledVehicleTire));
addField("radius", TypeF32, Offset(radius, WheeledVehicleTire));
addField("staticFriction", TypeF32, Offset(staticFriction, WheeledVehicleTire));
addField("kineticFriction", TypeF32, Offset(kineticFriction, WheeledVehicleTire));
addField("restitution", TypeF32, Offset(restitution, WheeledVehicleTire));
addField("lateralForce", TypeF32, Offset(lateralForce, WheeledVehicleTire));
addField("lateralDamping", TypeF32, Offset(lateralDamping, WheeledVehicleTire));
addField("lateralRelaxation", TypeF32, Offset(lateralRelaxation, WheeledVehicleTire));
addField("longitudinalForce", TypeF32, Offset(longitudinalForce, WheeledVehicleTire));
addField("longitudinalDamping", TypeF32, Offset(longitudinalDamping, WheeledVehicleTire));
addField("logitudinalRelaxation", TypeF32, Offset(longitudinalRelaxation, WheeledVehicleTire));
}
void WheeledVehicleTire::packData(BitStream* stream)
{
Parent::packData(stream);
stream->writeString(shapeName);
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);
shapeName = stream->readSTString();
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);
WheeledVehicleSpring::WheeledVehicleSpring()
{
length = 1;
force = 10;
damping = 1;
antiSway = 1;
}
void WheeledVehicleSpring::initPersistFields()
{
Parent::initPersistFields();
addField("length", TypeF32, Offset(length, WheeledVehicleSpring));
addField("force", TypeF32, Offset(force, WheeledVehicleSpring));
addField("damping", TypeF32, Offset(damping, WheeledVehicleSpring));
addField("antiSwayForce", TypeF32, Offset(antiSway, WheeledVehicleSpring));
}
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);
WheeledVehicleData::WheeledVehicleData()
{
tireEmitter = 0;
maxWheelSpeed = 40;
engineTorque = 1;
engineBrake = 1;
brakeTorque = 1;
brakeLightSequence = -1;
for (S32 i = 0; i < MaxSounds; i++)
sound[i] = 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
breakLight Break light, time 0 = off, 1 = breaking
Wheel Nodes
hub# Wheel hub
The steering and animation sequences are optional.
*/
bool WheeledVehicleData::preload(bool server, char errorBuffer[256])
{
if (!Parent::preload(server, errorBuffer))
return false;
// A temporary shape instance is created so that we can
// animate the shape and extract wheel information.
TSShapeInstance* si = new TSShapeInstance(shape, false);
// Resolve objects transmitted from server
if (!server) {
for (S32 i = 0; i < MaxSounds; i++)
if (sound[i])
Sim::findObject(SimObjectId(sound[i]),sound[i]);
if (tireEmitter)
Sim::findObject(SimObjectId(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 = shape->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 = shape->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 = shape->findSequence("steering");
// Brakes
brakeLightSequence = shape->findSequence("brakelight");
// Extract collision planes from shape collision detail level
if (collisionDetails[0] != -1) {
MatrixF imat(1);
SphereF sphere;
sphere.center = shape->center;
sphere.radius = shape->radius;
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()
{
Parent::initPersistFields();
addField("jetSound", TypeAudioProfilePtr, Offset(sound[JetSound], WheeledVehicleData));
addField("engineSound", TypeAudioProfilePtr, Offset(sound[EngineSound], WheeledVehicleData));
addField("squealSound", TypeAudioProfilePtr, Offset(sound[SquealSound], WheeledVehicleData));
addField("WheelImpactSound", TypeAudioProfilePtr, Offset(sound[WheelImpactSound], WheeledVehicleData));
addField("tireEmitter",TypeParticleEmitterDataPtr, Offset(tireEmitter, WheeledVehicleData));
addField("maxWheelSpeed", TypeF32, Offset(maxWheelSpeed, WheeledVehicleData));
addField("engineTorque", TypeF32, Offset(engineTorque, WheeledVehicleData));
addField("engineBrake", TypeF32, Offset(engineBrake, WheeledVehicleData));
addField("brakeTorque", TypeF32, Offset(brakeTorque, WheeledVehicleData));
}
//----------------------------------------------------------------------------
void WheeledVehicleData::packData(BitStream* stream)
{
Parent::packData(stream);
if (stream->writeFlag(tireEmitter))
stream->writeRangedU32(packed? SimObjectId(tireEmitter):
tireEmitter->getId(),DataBlockObjectIdFirst,DataBlockObjectIdLast);
for (S32 i = 0; i < MaxSounds; i++)
if (stream->writeFlag(sound[i]))
stream->writeRangedU32(packed? SimObjectId(sound[i]):
sound[i]->getId(),DataBlockObjectIdFirst,DataBlockObjectIdLast);
stream->write(maxWheelSpeed);
stream->write(engineTorque);
stream->write(engineBrake);
stream->write(brakeTorque);
}
void WheeledVehicleData::unpackData(BitStream* stream)
{
Parent::unpackData(stream);
tireEmitter = stream->readFlag()?
(ParticleEmitterData*) stream->readRangedU32(DataBlockObjectIdFirst,
DataBlockObjectIdLast): 0;
for (S32 i = 0; i < MaxSounds; i++)
sound[i] = stream->readFlag()?
(AudioProfile*) stream->readRangedU32(DataBlockObjectIdFirst,
DataBlockObjectIdLast): 0;
stream->read(&maxWheelSpeed);
stream->read(&engineTorque);
stream->read(&engineBrake);
stream->read(&brakeTorque);
}
//----------------------------------------------------------------------------
// Wheeled Vehicle Class
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
IMPLEMENT_CO_NETOBJECT_V1(WheeledVehicle);
WheeledVehicle::WheeledVehicle()
{
mDataBlock = 0;
mBraking = false;
mJetSound = 0;
mEngineSound = 0;
mSquealSound = 0;
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()
{
Parent::initPersistFields();
}
//----------------------------------------------------------------------------
bool WheeledVehicle::onAdd()
{
if(!Parent::onAdd())
return false;
addToScene();
if (isServerObject())
scriptOnAdd();
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
if (mJetSound)
alxStop(mJetSound);
if (mEngineSound)
alxStop(mEngineSound);
if (mSquealSound)
alxStop(mSquealSound);
//
scriptOnRemove();
removeFromScene();
Parent::onRemove();
}
//----------------------------------------------------------------------------
bool WheeledVehicle::onNewDataBlock(GameBaseData* dptr)
{
// 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<WheeledVehicleData*>(dptr);
if (!mDataBlock || !Parent::onNewDataBlock(dptr))
return false;
F32 frontStatic = 0;
F32 backStatic = 0;
F32 fCount = 0;
F32 bCount = 0;
// 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.max - mObjBox.min);
// 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) {
wheel->emitter = new ParticleEmitter;
wheel->emitter->onNewDataBlock(mDataBlock->tireEmitter);
wheel->emitter->registerObject();
}
}
// Steering sequence
if (mDataBlock->steeringSequence != -1) {
mSteeringThread = mShapeInstance->addThread();
mShapeInstance->setSequence(mSteeringThread,mDataBlock->steeringSequence,0);
}
else
mSteeringThread = 0;
// Break light sequence
if (mDataBlock->brakeLightSequence != -1) {
mTailLightThread = mShapeInstance->addThread();
mShapeInstance->setSequence(mTailLightThread,mDataBlock->brakeLightSequence,0);
}
else
mTailLightThread = 0;
// Stop any existing sounds in case where switching datablocks
if (mJetSound) {
alxStop(mJetSound);
mJetSound = 0;
}
if (mEngineSound) {
alxStop(mEngineSound);
mEngineSound = 0;
}
if (mSquealSound) {
alxStop(mSquealSound);
mSquealSound = 0;
}
if (isGhost()) {
// Start the engine
if (mDataBlock->sound[WheeledVehicleData::EngineSound])
mEngineSound = alxPlay(mDataBlock->sound[WheeledVehicleData::EngineSound], &getTransform());
}
scriptOnNewDataBlock();
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::processTick(const Move* move)
{
Parent::processTick(move);
}
void WheeledVehicle::updateMove(const Move* move)
{
Parent::updateMove(move);
// Break 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: -1);
}
//----------------------------------------------------------------------------
void WheeledVehicle::advanceTime(F32 dt)
{
Parent::advanceTime(dt);
// Stick the wheels to the ground. This is purely so they look
// good while the vehicle is being interpolated.
extendWheels();
// 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)
{
extendWheels();
F32 oneOverSprungMass = 1 / (mMass * 0.8);
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 break.
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.force.set(0, 0, 0);
mRigid.torque.set(0, 0, 0);
// 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);
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 break 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;
// Container drag & buoyancy
mRigid.force += Point3F(0, 0, -mBuoyancy * sWheeledVehicleGravity * mRigid.mass);
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;
// Gravity
mRigid.force += Point3F(0, 0, sWheeledVehicleGravity * mRigid.mass);
// 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.force.set(0, 0, 0);
mRigid.torque.set(0, 0, 0);
// 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)
{
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;
}
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();
if (speed > 1.0f)
{
MaterialPropertyMap* matMap = static_cast<MaterialPropertyMap*>(Sim::findObject("MaterialPropertyMap"));
Point3F axis = vel;
axis.normalize();
// Only emit dust on the terrain
Wheel* wend = &mWheel[mDataBlock->wheelCount];
for (Wheel* wheel = mWheel; wheel < wend; wheel++)
{
if (wheel->tire && wheel->spring && !wheel->emitter.isNull() &&
wheel->surface.contact && wheel->surface.object &&
wheel->surface.object->getTypeMask() & TerrainObjectType)
{
TerrainBlock* tBlock = static_cast<TerrainBlock*>(wheel->surface.object);
S32 mapIndex = tBlock->mMPMIndex[0];
// Override the dust color with the material property
const MaterialPropertyMap::MapEntry* pEntry;
if (matMap && mapIndex != -1 &&
(pEntry = matMap->getMapEntryFromIndex(mapIndex)) != 0)
{
ColorF colorList[ParticleEngine::PC_COLOR_KEYS];
for (S32 x = 0; x < 2; ++x)
colorList[x].set(pEntry->puffColor[x].red,
pEntry->puffColor[x].green,
pEntry->puffColor[x].blue,
pEntry->puffColor[x].alpha);
for(S32 x = 2; x < ParticleEngine::PC_COLOR_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)(dt * (speed / mDataBlock->maxWheelSpeed) * 1000 * wheel->slip));
}
}
}
}
//----------------------------------------------------------------------------
/** Update engine sound
This method is only invoked by clients.
*/
void WheeledVehicle::updateEngineSound(F32 level)
{
if (mEngineSound)
{
alxSourceMatrixF(mEngineSound, &getTransform());
alxSourcef(mEngineSound, AL_GAIN_LINEAR, level);
// Creative Labs win32 OpenAL workaround
F32 pitch = ((level-sIdleEngineVolume) * 1.3);
if (pitch < 0.4)
pitch = 0.4;
// End workaround
alxSourcef(mEngineSound, AL_PITCH, pitch);
}
}
//----------------------------------------------------------------------------
/** Update wheel skid sound
This method is only invoked by clients.
*/
void WheeledVehicle::updateSquealSound(F32 level)
{
if (!mDataBlock->sound[WheeledVehicleData::SquealSound])
return;
// Allocate/Deallocate voice on demand.
if (level < sMinSquealVolume)
{
if (mSquealSound)
{
alxStop(mSquealSound);
mSquealSound = 0;
}
}
else
{
if (!mSquealSound)
mSquealSound = alxPlay(mDataBlock->sound[WheeledVehicleData::SquealSound], &getTransform());
alxSourceMatrixF(mSquealSound, &getTransform());
alxSourcef(mSquealSound, AL_GAIN_LINEAR, level);
}
}
//----------------------------------------------------------------------------
/** Update jet sound
This method is only invoked by clients.
*/
void WheeledVehicle::updateJetSound()
{
if (!mDataBlock->sound[WheeledVehicleData::JetSound])
return;
// Allocate/Deallocate voice on demand.
if (!mJetting)
{
if (mJetSound)
{
alxStop(mJetSound);
mJetSound = 0;
}
}
else
{
if (!mJetSound)
mJetSound = alxPlay(mDataBlock->sound[WheeledVehicleData::JetSound], &getTransform());
alxSourceMatrixF(mJetSound, &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(AbstractPolyList* polyList, const Box3F& box, const SphereF& sphere)
{
// Parent will take care of body collision.
Parent::buildPolyList(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.min.x = -(wbox.max.x = radius / 2);
wbox.min.y = -(wbox.max.y = radius);
wbox.min.z = -(wbox.max.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();
}
//----------------------------------------------------------------------------
bool WheeledVehicle::prepRenderImage(SceneState* state, const U32 stateKey,
const U32 startZone, const bool modifyBaseState)
{
AssertFatal(modifyBaseState == false, "Error, should never be called with this parameter set");
AssertFatal(startZone == 0xFFFFFFFF, "Error, startZone should indicate -1");
if (isLastState(state, stateKey))
return false;
setLastState(state, stateKey);
if( ( getDamageState() == Destroyed ) && ( !mDataBlock->renderWhenDestroyed ) )
return false;
// Select detail levels on mounted items
// but... always draw the control object's mounted images
// in high detail (I can't believe I'm commenting this hack :)
F32 saveError = TSShapeInstance::smScreenError;
GameConnection *con = GameConnection::getConnectionToServer();
bool fogExemption = false;
ShapeBase *co = NULL;
if(con && ( (co = con->getControlObject()) != NULL) )
{
if(co == this || co->getObjectMount() == this)
{
TSShapeInstance::smScreenError = 0.001;
fogExemption = true;
}
}
if (state->isObjectRendered(this))
{
mLastRenderFrame = sLastRenderFrame;
// get shape detail and fog information...we might not even need to be drawn
Point3F cameraOffset;
getRenderTransform().getColumn(3,&cameraOffset);
cameraOffset -= state->getCameraPosition();
F32 dist = cameraOffset.len();
if (dist < 0.01)
dist = 0.01;
F32 fogAmount = state->getHazeAndFog(dist,cameraOffset.z);
F32 invScale = (1.0f/getMax(getMax(mObjScale.x,mObjScale.y),mObjScale.z));
if (mShapeInstance)
DetailManager::selectPotentialDetails(mShapeInstance,dist,invScale);
Wheel* wend = &mWheel[mDataBlock->wheelCount];
for (Wheel* wheel = mWheel; wheel < wend; wheel++)
{
if (wheel->shapeInstance)
DetailManager::selectPotentialDetails(wheel->shapeInstance,dist,invScale);
}
if (mShapeInstance)
mShapeInstance->animate();
if ((fogAmount>0.99f && fogExemption == false) ||
(mShapeInstance && mShapeInstance->getCurrentDetail()<0) ||
(!mShapeInstance && !gShowBoundingBox)) {
// no, don't draw anything
return false;
}
for (U32 i = 0; i < MaxMountedImages; i++)
{
MountedImage& image = mMountedImageList[i];
if (image.dataBlock && image.shapeInstance)
{
DetailManager::selectPotentialDetails(image.shapeInstance,dist,invScale);
if (mCloakLevel == 0.0f && image.shapeInstance->hasSolid() && mFadeVal == 1.0f)
{
ShapeImageRenderImage* rimage = new ShapeImageRenderImage;
rimage->obj = this;
rimage->mSBase = this;
rimage->mIndex = i;
rimage->isTranslucent = false;
rimage->textureSortKey = (U32)(dsize_t)(image.dataBlock);
state->insertRenderImage(rimage);
}
if ((mCloakLevel != 0.0f || mFadeVal != 1.0f || mShapeInstance->hasTranslucency()) ||
(mMount.object == NULL))
{
ShapeImageRenderImage* rimage = new ShapeImageRenderImage;
rimage->obj = this;
rimage->mSBase = this;
rimage->mIndex = i;
rimage->isTranslucent = true;
rimage->sortType = SceneRenderImage::Point;
rimage->textureSortKey = (U32)(dsize_t)(image.dataBlock);
state->setImageRefPoint(this, rimage);
state->insertRenderImage(rimage);
}
}
}
TSShapeInstance::smScreenError = saveError;
if (mCloakLevel == 0.0f && mShapeInstance->hasSolid() && mFadeVal == 1.0f)
{
SceneRenderImage* image = new SceneRenderImage;
image->obj = this;
image->isTranslucent = false;
image->textureSortKey = mSkinHash ^ (U32)(dsize_t)(mDataBlock);
state->insertRenderImage(image);
}
if ((mCloakLevel != 0.0f || mFadeVal != 1.0f || mShapeInstance->hasTranslucency()) ||
(mMount.object == NULL))
{
SceneRenderImage* image = new SceneRenderImage;
image->obj = this;
image->isTranslucent = true;
image->sortType = SceneRenderImage::Point;
image->textureSortKey = mSkinHash ^ (U32)(dsize_t)(mDataBlock);
state->setImageRefPoint(this, image);
state->insertRenderImage(image);
}
calcClassRenderData();
}
return false;
}
//----------------------------------------------------------------------------
void WheeledVehicle::renderImage(SceneState* state, SceneRenderImage* image)
{
Parent::renderImage(state, image);
// Shape transform
glPushMatrix();
dglMultMatrix(&getRenderTransform());
Wheel* wend = &mWheel[mDataBlock->wheelCount];
for (Wheel* wheel = mWheel; wheel < wend; wheel++)
{
if (wheel->shapeInstance)
{
glPushMatrix();
// 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);
dglMultMatrix(&hub);
// Wheel rotation
MatrixF rot(EulerF(wheel->apos * M_2PI,0,0));
dglMultMatrix(&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));
dglMultMatrix(&wrot);
// Render it
wheel->shapeInstance->animate();
wheel->shapeInstance->render();
glPopMatrix();
}
}
glPopMatrix();
}
//----------------------------------------------------------------------------
void WheeledVehicle::writePacketData(GameConnection *connection, BitStream *stream)
{
Parent::writePacketData(connection, stream);
stream->writeFlag(mBraking);
Wheel* wend = &mWheel[mDataBlock->wheelCount];
for (Wheel* wheel = mWheel; wheel < wend; wheel++)
{
stream->write(wheel->avel);
stream->write(wheel->Dy);
stream->write(wheel->Dx);
stream->writeFlag(wheel->slipping);
}
}
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++)
{
stream->read(&wheel->avel);
stream->read(&wheel->Dy);
stream->read(&wheel->Dx);
wheel->slipping = stream->readFlag();
}
// Rigid state is transmitted by the parent...
setPosition(mRigid.linPosition,mRigid.angPosition);
mDelta.pos = mRigid.linPosition;
mDelta.rot[1] = mRigid.angPosition;
}
//----------------------------------------------------------------------------
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->shape.isNull())? 0 : new TSShapeInstance(wheel->tire->shape);
}
}
}
// 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
//----------------------------------------------------------------------------
ConsoleMethod(WheeledVehicle, setWheelSteering, bool, 4, 4, "obj.setWheelSteering(wheel#,float)")
{
S32 wheel = dAtoi(argv[2]);
if (wheel >= 0 && wheel < object->getWheelCount())
{
object->setWheelSteering(wheel,dAtof(argv[3]));
return true;
}
else
Con::warnf("setWheelSteering: wheel index out of bounds, vehicle has %d hubs",
argv[3],object->getWheelCount());
return false;
}
ConsoleMethod(WheeledVehicle, setWheelPowered, bool, 4, 4, "obj.setWheelPowered(wheel#,bool)")
{
S32 wheel = dAtoi(argv[2]);
if (wheel >= 0 && wheel < object->getWheelCount())
{
object->setWheelPowered(wheel,dAtob(argv[3]));
return true;
}
else
Con::warnf("setWheelPowered: wheel index out of bounds, vehicle has %d hubs",
argv[3],object->getWheelCount());
return false;
}
ConsoleMethod(WheeledVehicle, setWheelTire, bool, 4, 4, "obj.setWheelTire(wheel#,tire)")
{
WheeledVehicleTire* tire;
if (Sim::findObject(argv[3],tire))
{
S32 wheel = dAtoi(argv[2]);
if (wheel >= 0 && wheel < object->getWheelCount())
{
object->setWheelTire(wheel,tire);
return true;
}
else
Con::warnf("setWheelTire: wheel index out of bounds, vehicle has %d hubs",
argv[3],object->getWheelCount());
}
else
Con::warnf("setWheelTire: %s datablock does not exist (or is not a tire)",argv[3]);
return false;
}
ConsoleMethod(WheeledVehicle, setWheelSpring, bool, 4, 4, "obj.setWheelSpring(wheel#,spring)")
{
WheeledVehicleSpring* spring;
if (Sim::findObject(argv[3],spring))
{
S32 wheel = dAtoi(argv[2]);
if (wheel >= 0 && wheel < object->getWheelCount())
{
object->setWheelSpring(wheel,spring);
return true;
}
else
Con::warnf("setWheelSpring: wheel index out of bounds, vehicle has %d hubs",
argv[3],object->getWheelCount());
}
else
Con::warnf("setWheelSpring: %s datablock does not exist (or is not a spring)",argv[3]);
return false;
}
ConsoleMethod(WheeledVehicle, getWheelCount, S32, 2, 2, "obj.getWheelCount()")
{
return object->getWheelCount();
}