mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-01-19 20:24:49 +00:00
1664 lines
55 KiB
C++
1664 lines
55 KiB
C++
//-----------------------------------------------------------------------------
|
|
// 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;
|
|
|
|
// Gravity constant
|
|
static F32 sWheeledVehicleGravity = -20;
|
|
|
|
// 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()
|
|
{
|
|
shape = 0;
|
|
shapeName = "";
|
|
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;
|
|
}
|
|
|
|
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 (shapeName && shapeName[0])
|
|
{
|
|
|
|
// Load up the shape resource
|
|
shape = ResourceManager::get().load(shapeName);
|
|
if (!bool(shape))
|
|
{
|
|
errorStr = String::ToString("WheeledVehicleTire: Couldn't load shape \"%s\"",shapeName);
|
|
return false;
|
|
}
|
|
|
|
// 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 = shape->mBounds.len_z() / 2;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void WheeledVehicleTire::initPersistFields()
|
|
{
|
|
addField( "shapeFile",TypeShapeFilename,Offset(shapeName,WheeledVehicleTire),
|
|
"The path to the shape to use for the wheel." );
|
|
addField( "mass", TypeF32, Offset(mass, WheeledVehicleTire),
|
|
"The mass of the wheel.\nCurrently unused." );
|
|
addField( "radius", TypeF32, Offset(radius, WheeledVehicleTire),
|
|
"@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." );
|
|
addField( "staticFriction", TypeF32, Offset(staticFriction, WheeledVehicleTire),
|
|
"Tire friction when the wheel is not slipping (has traction)." );
|
|
addField( "kineticFriction", TypeF32, Offset(kineticFriction, WheeledVehicleTire),
|
|
"Tire friction when the wheel is slipping (no traction)." );
|
|
addField( "restitution", TypeF32, Offset(restitution, WheeledVehicleTire),
|
|
"Tire restitution.\nCurrently unused." );
|
|
addField( "lateralForce", TypeF32, Offset(lateralForce, WheeledVehicleTire),
|
|
"@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." );
|
|
addField( "lateralDamping", TypeF32, Offset(lateralDamping, WheeledVehicleTire),
|
|
"Damping force applied against lateral forces generated by the tire.\n\n"
|
|
"@see lateralForce" );
|
|
addField( "lateralRelaxation", TypeF32, Offset(lateralRelaxation, WheeledVehicleTire),
|
|
"@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" );
|
|
addField( "longitudinalForce", TypeF32, Offset(longitudinalForce, WheeledVehicleTire),
|
|
"@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" );
|
|
addField( "longitudinalDamping", TypeF32, Offset(longitudinalDamping, WheeledVehicleTire),
|
|
"Damping force applied against longitudinal forces generated by the tire.\n\n"
|
|
"@see longitudinalForce" );
|
|
addField( "longitudinalRelaxation", TypeF32, Offset(longitudinalRelaxation, WheeledVehicleTire),
|
|
"@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);
|
|
|
|
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);
|
|
|
|
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()
|
|
{
|
|
addField( "length", TypeF32, Offset(length, WheeledVehicleSpring),
|
|
"@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." );
|
|
addField( "force", TypeF32, Offset(force, WheeledVehicleSpring),
|
|
"@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." );
|
|
addField( "damping", TypeF32, Offset(damping, WheeledVehicleSpring),
|
|
"@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." );
|
|
addField( "antiSwayForce", TypeF32, Offset(antiSway, WheeledVehicleSpring),
|
|
"@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"
|
|
);
|
|
|
|
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++)
|
|
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
|
|
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(mShape, false);
|
|
|
|
// Resolve objects transmitted from server
|
|
if (!server) {
|
|
for (S32 i = 0; i < MaxSounds; i++)
|
|
if( !sfxResolve( &sound[ i ], errorStr ) )
|
|
return false;
|
|
|
|
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 = mShape->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 = mShape->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 = mShape->findSequence("steering");
|
|
|
|
// Brakes
|
|
brakeLightSequence = mShape->findSequence("brakelight");
|
|
|
|
// Extract collision planes from shape collision detail level
|
|
if (collisionDetails[0] != -1) {
|
|
MatrixF imat(1);
|
|
SphereF sphere;
|
|
sphere.center = mShape->center;
|
|
sphere.radius = mShape->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()
|
|
{
|
|
addField( "jetSound", TYPEID< SFXTrack >(), Offset(sound[JetSound], WheeledVehicleData),
|
|
"Looping sound played when the vehicle is jetting." );
|
|
addField( "engineSound", TYPEID< SFXTrack >(), Offset(sound[EngineSound], WheeledVehicleData),
|
|
"@brief Looping engine sound.\n\n"
|
|
"The pitch is dynamically adjusted based on the current engine RPM" );
|
|
addField("squealSound", TYPEID< SFXTrack >(), Offset(sound[SquealSound], WheeledVehicleData),
|
|
"@brief Looping sound played while any of the wheels is slipping.\n\n"
|
|
"The volume is dynamically adjusted based on how much the wheels are slipping." );
|
|
addField("WheelImpactSound", TYPEID< SFXTrack >(), Offset(sound[WheelImpactSound], WheeledVehicleData),
|
|
"Sound played when the wheels impact the ground.\nCurrently unused." );
|
|
|
|
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.");
|
|
addField("maxWheelSpeed", TypeF32, Offset(maxWheelSpeed, WheeledVehicleData),
|
|
"@brief Maximum linear velocity of each wheel.\n\n"
|
|
"This caps the maximum speed of the vehicle." );
|
|
addField("engineTorque", TypeF32, Offset(engineTorque, WheeledVehicleData),
|
|
"@brief Torque available from the engine at 100% throttle.\n\n"
|
|
"This controls vehicle acceleration. ie. how fast it will reach maximum speed." );
|
|
addField("engineBrake", TypeF32, Offset(engineBrake, WheeledVehicleData),
|
|
"@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." );
|
|
addField("brakeTorque", TypeF32, Offset(brakeTorque, WheeledVehicleData),
|
|
"@brief Torque applied when braking.\n\n"
|
|
"This controls how fast the vehicle will stop when the brakes are applied." );
|
|
|
|
Parent::initPersistFields();
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
|
|
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++)
|
|
sfxWrite( stream, sound[ i ] );
|
|
|
|
stream->write(maxWheelSpeed);
|
|
stream->write(engineTorque);
|
|
stream->write(engineBrake);
|
|
stream->write(brakeTorque);
|
|
}
|
|
|
|
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++)
|
|
sfxRead( stream, &sound[ i ] );
|
|
|
|
stream->read(&maxWheelSpeed);
|
|
stream->read(&engineTorque);
|
|
stream->read(&engineBrake);
|
|
stream->read(&brakeTorque);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
// 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()
|
|
{
|
|
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
|
|
SFX_DELETE( mJetSound );
|
|
SFX_DELETE( mEngineSound );
|
|
SFX_DELETE( mSquealSound );
|
|
|
|
//
|
|
scriptOnRemove();
|
|
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<WheeledVehicleData*>(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->sound[WheeledVehicleData::EngineSound] )
|
|
mEngineSound = SFX->createSource( mDataBlock->sound[WheeledVehicleData::EngineSound], &getTransform() );
|
|
|
|
if ( mDataBlock->sound[WheeledVehicleData::SquealSound] )
|
|
mSquealSound = SFX->createSource( mDataBlock->sound[WheeledVehicleData::SquealSound], &getTransform() );
|
|
|
|
if ( mDataBlock->sound[WheeledVehicleData::JetSound] )
|
|
mJetSound = SFX->createSource( mDataBlock->sound[WheeledVehicleData::JetSound], &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::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);
|
|
}
|
|
|
|
|
|
//----------------------------------------------------------------------------
|
|
|
|
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();
|
|
|
|
// 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();
|
|
|
|
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.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 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, -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)
|
|
{
|
|
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;
|
|
}
|
|
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);
|
|
|
|
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 == NULL) ? 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
|
|
//----------------------------------------------------------------------------
|
|
|
|
//----------------------------------------------------------------------------
|
|
|
|
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, WheeledVehicleTire* 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" )
|
|
{
|
|
if (wheel >= 0 && wheel < object->getWheelCount()) {
|
|
object->setWheelTire(wheel,tire);
|
|
return true;
|
|
}
|
|
else {
|
|
Con::warnf("setWheelTire: invalid tire datablock or wheel index, vehicle has %d hubs",
|
|
object->getWheelCount());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
DefineEngineMethod( WheeledVehicle, setWheelSpring, bool, ( S32 wheel, WheeledVehicleSpring* 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" )
|
|
{
|
|
if (spring && wheel >= 0 && wheel < object->getWheelCount()) {
|
|
object->setWheelSpring(wheel,spring);
|
|
return true;
|
|
}
|
|
else {
|
|
Con::warnf("setWheelSpring: invalid spring datablock or wheel index, vehicle has %d hubs",
|
|
object->getWheelCount());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|