//----------------------------------------------------------------------------- // 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. //----------------------------------------------------------------------------- //~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// // Arcane-FX for MIT Licensed Open Source version of Torque 3D from GarageGames // Copyright (C) 2015 Faust Logic, Inc. //~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~// #include "platform/platform.h" #include "T3D/projectile.h" #include "scene/sceneRenderState.h" #include "scene/sceneManager.h" #include "lighting/lightInfo.h" #include "lighting/lightManager.h" #include "console/consoleTypes.h" #include "console/typeValidators.h" #include "core/resourceManager.h" #include "core/stream/bitStream.h" #include "T3D/fx/explosion.h" #include "T3D/shapeBase.h" #include "ts/tsShapeInstance.h" #include "sfx/sfxTrack.h" #include "sfx/sfxSource.h" #include "sfx/sfxSystem.h" #include "sfx/sfxTypes.h" #include "math/mathUtils.h" #include "math/mathIO.h" #include "sim/netConnection.h" #include "T3D/fx/particleEmitter.h" #include "T3D/fx/splash.h" #include "T3D/physics/physicsPlugin.h" #include "T3D/physics/physicsWorld.h" #include "gfx/gfxTransformSaver.h" #include "T3D/containerQuery.h" #include "T3D/decal/decalManager.h" #include "T3D/decal/decalData.h" #include "T3D/lightDescription.h" #include "console/engineAPI.h" IMPLEMENT_CO_DATABLOCK_V1(ProjectileData); ConsoleDocClass( ProjectileData, "@brief Stores properties for an individual projectile type.\n" "@tsexample\n" "datablock ProjectileData(GrenadeLauncherProjectile)\n" "{\n" " projectileShapeName = \"art/shapes/weapons/SwarmGun/rocket.dts\";\n" "directDamage = 30;\n" "radiusDamage = 30;\n" "damageRadius = 5;\n" "areaImpulse = 2000;\n" "explosion = GrenadeLauncherExplosion;\n" "waterExplosion = GrenadeLauncherWaterExplosion;\n" "decal = ScorchRXDecal;\n" "splash = GrenadeSplash;\n" "particleEmitter = GrenadeProjSmokeTrailEmitter;\n" "particleWaterEmitter = GrenadeTrailWaterEmitter;\n" "muzzleVelocity = 30;\n" "velInheritFactor = 0.3;\n" "armingDelay = 2000;\n" "lifetime = 10000;\n" "fadeDelay = 4500;\n" "bounceElasticity = 0.4;\n" "bounceFriction = 0.3;\n" "isBallistic = true;\n" "gravityMod = 0.9;\n" "lightDesc = GrenadeLauncherLightDesc;\n" "damageType = \"GrenadeDamage\";\n" "};\n" "@endtsexample\n" "@ingroup gameObjects\n" ); IMPLEMENT_CO_NETOBJECT_V1(Projectile); ConsoleDocClass( Projectile, "@brief Base projectile class. Uses the ProjectileData class for properties of individual projectiles.\n" "@ingroup gameObjects\n" ); IMPLEMENT_CALLBACK( ProjectileData, onExplode, void, ( Projectile* proj, Point3F pos, F32 fade ), ( proj, pos, fade ), "@brief Called when a projectile explodes.\n\n" "This function is only called on server objects.\n" "@param proj The exploding projectile.\n" "@param pos The position of the explosion.\n" "@param fade The current fadeValue of the projectile, affects its visibility.\n\n" "@see Projectile\n" ); IMPLEMENT_CALLBACK( ProjectileData, onCollision, void, ( Projectile* proj, SceneObject* col, F32 fade, Point3F pos, Point3F normal ), ( proj, col, fade, pos, normal ), "@brief Called when a projectile collides with another object.\n\n" "This function is only called on server objects." "@param proj The projectile colliding with SceneObject col.\n" "@param col The SceneObject hit by the projectile.\n" "@param fade The current fadeValue of the projectile, affects its visibility.\n" "@param pos The position of the collision.\n" "@param normal The normal of the collision.\n" "@see Projectile\n" ); const U32 Projectile::csmStaticCollisionMask = TerrainObjectType | StaticShapeObjectType; const U32 Projectile::csmDynamicCollisionMask = PlayerObjectType | VehicleObjectType; const U32 Projectile::csmDamageableMask = Projectile::csmDynamicCollisionMask; U32 Projectile::smProjectileWarpTicks = 5; //-------------------------------------------------------------------------- // ProjectileData::ProjectileData() { INIT_ASSET(ProjectileShape); INIT_ASSET(ProjectileSound); explosion = NULL; explosionId = 0; waterExplosion = NULL; waterExplosionId = 0; //hasLight = false; //lightRadius = 1; //lightColor.set(1, 1, 1); lightDesc = NULL; faceViewer = false; scale.set( 1.0f, 1.0f, 1.0f ); isBallistic = false; velInheritFactor = 1.0f; muzzleVelocity = 50; impactForce = 0.0f; armingDelay = 0; fadeDelay = 20000 / 32; lifetime = 20000 / 32; activateSeq = -1; maintainSeq = -1; gravityMod = 1.0; bounceElasticity = 0.999f; bounceFriction = 0.3f; particleEmitter = NULL; particleEmitterId = 0; particleWaterEmitter = NULL; particleWaterEmitterId = 0; splash = NULL; splashId = 0; decal = NULL; decalId = 0; lightDesc = NULL; lightDescId = 0; } ProjectileData::ProjectileData(const ProjectileData& other, bool temp_clone) : GameBaseData(other, temp_clone) { faceViewer = other.faceViewer; // -- always set to false scale = other.scale; velInheritFactor = other.velInheritFactor; muzzleVelocity = other.muzzleVelocity; impactForce = other.impactForce; isBallistic = other.isBallistic; bounceElasticity = other.bounceElasticity; bounceFriction = other.bounceFriction; gravityMod = other.gravityMod; lifetime = other.lifetime; armingDelay = other.armingDelay; fadeDelay = other.fadeDelay; explosion = other.explosion; explosionId = other.explosionId; // -- for pack/unpack of explosion ptr waterExplosion = other.waterExplosion; waterExplosionId = other.waterExplosionId; // -- for pack/unpack of waterExplosion ptr splash = other.splash; splashId = other.splashId; // -- for pack/unpack of splash ptr decal = other.decal; decalId = other.decalId; // -- for pack/unpack of decal ptr CLONE_ASSET(ProjectileSound); lightDesc = other.lightDesc; lightDescId = other.lightDescId; // -- for pack/unpack of lightDesc ptr CLONE_ASSET(ProjectileShape);// -- TSShape loads using mProjectileShapeName activateSeq = other.activateSeq; // -- from projectileShape sequence "activate" maintainSeq = other.maintainSeq; // -- from projectileShape sequence "maintain" particleEmitter = other.particleEmitter; particleEmitterId = other.particleEmitterId; // -- for pack/unpack of particleEmitter ptr particleWaterEmitter = other.particleWaterEmitter; particleWaterEmitterId = other.particleWaterEmitterId; // -- for pack/unpack of particleWaterEmitter ptr } //-------------------------------------------------------------------------- void ProjectileData::initPersistFields() { docsURL; addGroup("Shapes"); addProtectedField("projectileShapeName", TypeShapeFilename, Offset(mProjectileShapeName, ProjectileData), &_setProjectileShapeData, &defaultProtectedGetFn, "@brief File path to the model of the projectile.\n\n", AbstractClassRep::FIELD_HideInInspectors); INITPERSISTFIELD_SHAPEASSET(ProjectileShape, ProjectileData, "@brief The model of the projectile.\n\n"); addField("scale", TypePoint3F, Offset(scale, ProjectileData), "@brief Scale to apply to the projectile's size.\n\n" "@note This is applied after SceneObject::scale\n"); endGroup("Shapes"); addGroup("Particle Effects"); addField("particleEmitter", TYPEID< ParticleEmitterData >(), Offset(particleEmitter, ProjectileData), "@brief Particle emitter datablock used to generate particles while the projectile is outside of water.\n\n" "@note If datablocks are defined for both particleEmitter and particleWaterEmitter, both effects will play " "as the projectile enters or leaves water.\n\n" "@see particleWaterEmitter\n"); addField("particleWaterEmitter", TYPEID< ParticleEmitterData >(), Offset(particleWaterEmitter, ProjectileData), "@brief Particle emitter datablock used to generate particles while the projectile is submerged in water.\n\n" "@note If datablocks are defined for both particleWaterEmitter and particleEmitter , both effects will play " "as the projectile enters or leaves water.\n\n" "@see particleEmitter\n"); addField("explosion", TYPEID< ExplosionData >(), Offset(explosion, ProjectileData), "@brief Explosion datablock used when the projectile explodes outside of water.\n\n"); addField("waterExplosion", TYPEID< ExplosionData >(), Offset(waterExplosion, ProjectileData), "@brief Explosion datablock used when the projectile explodes underwater.\n\n"); addField("splash", TYPEID< SplashData >(), Offset(splash, ProjectileData), "@brief Splash datablock used to create splash effects as the projectile enters or leaves water\n\n"); addField("decal", TYPEID< DecalData >(), Offset(decal, ProjectileData), "@brief Decal datablock used for decals placed at projectile explosion points.\n\n"); endGroup("Particle Effects"); addGroup("Sounds"); INITPERSISTFIELD_SOUNDASSET(ProjectileSound, ProjectileData, "The sound for the projectile."); endGroup("Sounds"); addGroup("Light Emitter"); addField("lightDesc", TYPEID< LightDescription >(), Offset(lightDesc, ProjectileData), "@brief LightDescription datablock used for lights attached to the projectile.\n\n"); endGroup("Light Emitter"); addGroup("Physics"); addProtectedField("lifetime", TypeS32, Offset(lifetime, ProjectileData), &setLifetime, &getScaledValue, "@brief Amount of time, in milliseconds, before the projectile is removed from the simulation.\n\n" "Used with fadeDelay to determine the transparency of the projectile at a given time. " "A projectile may exist up to a maximum of 131040ms (or 4095 ticks) as defined by Projectile::MaxLivingTicks in the source code." "@see fadeDelay"); addProtectedField("armingDelay", TypeS32, Offset(armingDelay, ProjectileData), &setArmingDelay, &getScaledValue, "@brief Amount of time, in milliseconds, before the projectile will cause damage or explode on impact.\n\n" "This value must be equal to or less than the projectile's lifetime.\n\n" "@see lifetime"); addProtectedField("fadeDelay", TypeS32, Offset(fadeDelay, ProjectileData), &setFadeDelay, &getScaledValue, "@brief Amount of time, in milliseconds, before the projectile begins to fade out.\n\n" "This value must be smaller than the projectile's lifetime to have an affect."); addField("isBallistic", TypeBool, Offset(isBallistic, ProjectileData), "@brief Detetmines if the projectile should be affected by gravity and whether or not " "it bounces before exploding.\n\n"); addField("velInheritFactor", TypeF32, Offset(velInheritFactor, ProjectileData), "@brief Amount of velocity the projectile recieves from the source that created it.\n\n" "Use an amount between 0 and 1 for the best effect. " "This value is never modified by the engine.\n" "@note This value by default is not transmitted between the server and the client."); addField("muzzleVelocity", TypeF32, Offset(muzzleVelocity, ProjectileData), "@brief Amount of velocity the projectile recieves from the \"muzzle\" of the gun.\n\n" "Used with velInheritFactor to determine the initial velocity of the projectile. " "This value is never modified by the engine.\n\n" "@note This value by default is not transmitted between the server and the client.\n\n" "@see velInheritFactor"); addField("impactForce", TypeF32, Offset(impactForce, ProjectileData)); addField("bounceElasticity", TypeF32, Offset(bounceElasticity, ProjectileData), "@brief Influences post-bounce velocity of a projectile that does not explode on contact.\n\n" "Scales the velocity from a bounce after friction is taken into account. " "A value of 1.0 will leave it's velocity unchanged while values greater than 1.0 will increase it.\n"); addField("bounceFriction", TypeF32, Offset(bounceFriction, ProjectileData), "@brief Factor to reduce post-bounce velocity of a projectile that does not explode on contact.\n\n" "Reduces bounce velocity by this factor and a multiple of the tangent to impact. " "Used to simulate surface friction.\n"); addField("gravityMod", TypeF32, Offset(gravityMod, ProjectileData), "@brief Scales the influence of gravity on the projectile.\n\n" "The larger this value is, the more that gravity will affect the projectile. " "A value of 1.0 will assume \"normal\" influence upon it.\n" "The magnitude of gravity is assumed to be 9.81 m/s/s\n\n" "@note ProjectileData::isBallistic must be true for this to have any affect."); endGroup("Physics"); Parent::initPersistFields(); // disallow some field substitutions onlyKeepClearSubstitutions("explosion"); onlyKeepClearSubstitutions("particleEmitter"); onlyKeepClearSubstitutions("particleWaterEmitter"); onlyKeepClearSubstitutions("sound"); onlyKeepClearSubstitutions("splash"); onlyKeepClearSubstitutions("waterExplosion"); } //-------------------------------------------------------------------------- bool ProjectileData::onAdd() { if(!Parent::onAdd()) return false; return true; } bool ProjectileData::preload(bool server, String &errorStr) { if (Parent::preload(server, errorStr) == false) return false; if( !server ) { if (!particleEmitter && particleEmitterId != 0) if (Sim::findObject(particleEmitterId, particleEmitter) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(particleEmitter): %d", particleEmitterId); if (!particleWaterEmitter && particleWaterEmitterId != 0) if (Sim::findObject(particleWaterEmitterId, particleWaterEmitter) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(particleWaterEmitter): %d", particleWaterEmitterId); if (!explosion && explosionId != 0) if (Sim::findObject(explosionId, explosion) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(explosion): %d", explosionId); if (!waterExplosion && waterExplosionId != 0) if (Sim::findObject(waterExplosionId, waterExplosion) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(waterExplosion): %d", waterExplosionId); if (!splash && splashId != 0) if (Sim::findObject(splashId, splash) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(splash): %d", splashId); if (!decal && decalId != 0) if (Sim::findObject(decalId, decal) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockId(decal): %d", decalId); _setProjectileSound(getProjectileSound()); if (getProjectileSound() != StringTable->EmptyString()) { if (!getProjectileSoundProfile()) Con::errorf(ConsoleLogEntry::General, "SplashData::preload: Cant get an sfxProfile for splash."); } if (!lightDesc && lightDescId != 0) if (Sim::findObject(lightDescId, lightDesc) == false) Con::errorf(ConsoleLogEntry::General, "ProjectileData::preload: Invalid packet, bad datablockid(lightDesc): %d", lightDescId); } if (mProjectileShapeAssetId != StringTable->EmptyString()) { //If we've got a shapeAsset assigned for our projectile, but we failed to load the shape data itself, report the error if (!mProjectileShape) { errorStr = String::ToString("ProjectileData::load: Couldn't load shape \"%s\"", mProjectileShapeAssetId); return false; } else { activateSeq = mProjectileShape->findSequence("activate"); maintainSeq = mProjectileShape->findSequence("maintain"); TSShapeInstance* pDummy = new TSShapeInstance(mProjectileShape, !server); delete pDummy; } } return true; } //-------------------------------------------------------------------------- void ProjectileData::packData(BitStream* stream) { Parent::packData(stream); PACKDATA_ASSET(ProjectileShape); stream->writeFlag(faceViewer); if(stream->writeFlag(scale.x != 1 || scale.y != 1 || scale.z != 1)) { stream->write(scale.x); stream->write(scale.y); stream->write(scale.z); } if (stream->writeFlag(particleEmitter != NULL)) stream->writeRangedU32(particleEmitter->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->writeFlag(particleWaterEmitter != NULL)) stream->writeRangedU32(particleWaterEmitter->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->writeFlag(explosion != NULL)) stream->writeRangedU32(explosion->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->writeFlag(waterExplosion != NULL)) stream->writeRangedU32(waterExplosion->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->writeFlag(splash != NULL)) stream->writeRangedU32(splash->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->writeFlag(decal != NULL)) stream->writeRangedU32(decal->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); PACKDATA_ASSET(ProjectileSound); if ( stream->writeFlag(lightDesc != NULL)) stream->writeRangedU32(lightDesc->getId(), DataBlockObjectIdFirst, DataBlockObjectIdLast); stream->write(impactForce); // stream->writeRangedU32(lifetime, 0, Projectile::MaxLivingTicks); // stream->writeRangedU32(armingDelay, 0, Projectile::MaxLivingTicks); // stream->writeRangedU32(fadeDelay, 0, Projectile::MaxLivingTicks); // [tom, 3/21/2007] Changing these to write all 32 bits as the previous // code limited these to a max value of 4095. stream->write(lifetime); stream->write(armingDelay); stream->write(fadeDelay); if(stream->writeFlag(isBallistic)) { stream->write(gravityMod); stream->write(bounceElasticity); stream->write(bounceFriction); } } void ProjectileData::unpackData(BitStream* stream) { Parent::unpackData(stream); UNPACKDATA_ASSET(ProjectileShape); faceViewer = stream->readFlag(); if(stream->readFlag()) { stream->read(&scale.x); stream->read(&scale.y); stream->read(&scale.z); } else scale.set(1,1,1); if (stream->readFlag()) particleEmitterId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->readFlag()) particleWaterEmitterId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->readFlag()) explosionId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->readFlag()) waterExplosionId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->readFlag()) splashId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); if (stream->readFlag()) decalId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); UNPACKDATA_ASSET(ProjectileSound); if (stream->readFlag()) lightDescId = stream->readRangedU32(DataBlockObjectIdFirst, DataBlockObjectIdLast); // [tom, 3/21/2007] See comment in packData() // lifetime = stream->readRangedU32(0, Projectile::MaxLivingTicks); // armingDelay = stream->readRangedU32(0, Projectile::MaxLivingTicks); // fadeDelay = stream->readRangedU32(0, Projectile::MaxLivingTicks); stream->read(&impactForce); stream->read(&lifetime); stream->read(&armingDelay); stream->read(&fadeDelay); isBallistic = stream->readFlag(); if(isBallistic) { stream->read(&gravityMod); stream->read(&bounceElasticity); stream->read(&bounceFriction); } } bool ProjectileData::setLifetime( void *obj, const char *index, const char *data ) { S32 value = dAtoi(data); value = scaleValue(value); ProjectileData *object = static_cast(obj); object->lifetime = value; return false; } bool ProjectileData::setArmingDelay( void *obj, const char *index, const char *data ) { S32 value = dAtoi(data); value = scaleValue(value); ProjectileData *object = static_cast(obj); object->armingDelay = value; return false; } bool ProjectileData::setFadeDelay( void *obj, const char *index, const char *data ) { S32 value = dAtoi(data); value = scaleValue(value); ProjectileData *object = static_cast(obj); object->fadeDelay = value; return false; } const char *ProjectileData::getScaledValue( void *obj, const char *data) { S32 value = dAtoi(data); value = scaleValue(value, false); String stringData = String::ToString(value); char *strBuffer = Con::getReturnBuffer(stringData.size()); dMemcpy( strBuffer, stringData, stringData.size() ); return strBuffer; } S32 ProjectileData::scaleValue( S32 value, bool down ) { S32 minV = 0; S32 maxV = Projectile::MaxLivingTicks; // scale down to ticks before we validate if( down ) value /= TickMs; if(value < minV || value > maxV) { Con::errorf("ProjectileData::scaleValue(S32 value = %d, bool down = %b) - Scaled value must be between %d and %d", value, down, minV, maxV); if(value < minV) value = minV; else if(value > maxV) value = maxV; } // scale up from ticks after we validate if( !down ) value *= TickMs; return value; } //-------------------------------------------------------------------------- //-------------------------------------- // Projectile::Projectile() : mPhysicsWorld( NULL ), mDataBlock( NULL ), mParticleEmitter( NULL ), mParticleWaterEmitter( NULL ), mSound( NULL ), mCurrPosition( 0, 0, 0 ), mCurrVelocity( 0, 0, 1 ), mSourceObjectId( -1 ), mSourceObjectSlot( -1 ), mCurrTick( 0 ), mProjectileShape( NULL ), mActivateThread( NULL ), mMaintainThread( NULL ), mHasExploded( false ), mFadeValue( 1.0f ) { // Todo: ScopeAlways? mNetFlags.set(Ghostable); mTypeMask |= ProjectileObjectType | LightObjectType | DynamicShapeObjectType; mLight = LightManager::createLightInfo(); mLight->setType( LightInfo::Point ); mLightState.clear(); mLightState.setLightInfo( mLight ); mDataBlock = 0; ignoreSourceTimeout = false; dynamicCollisionMask = csmDynamicCollisionMask; staticCollisionMask = csmStaticCollisionMask; } Projectile::~Projectile() { SAFE_DELETE(mLight); delete mProjectileShape; mProjectileShape = NULL; if (mDataBlock && mDataBlock->isTempClone()) { delete mDataBlock; mDataBlock = 0; } } //-------------------------------------------------------------------------- void Projectile::initPersistFields() { docsURL; addGroup("Physics"); addProtectedField("initialPosition", TypePoint3F, Offset(mInitialPosition, Projectile), &_setInitialPosition, &defaultProtectedGetFn, "@brief Starting position for the projectile.\n\n"); //addField("initialPosition", TypePoint3F, Offset(mCurrPosition, Projectile), // "@brief Starting position for the projectile.\n\n"); addProtectedField("initialVelocity", TypePoint3F, Offset(mInitialVelocity, Projectile), &_setInitialVelocity, &defaultProtectedGetFn, "@brief Starting velocity for the projectile.\n\n"); //addField("initialVelocity", TypePoint3F, Offset(mCurrVelocity, Projectile), // "@brief Starting velocity for the projectile.\n\n"); endGroup("Physics"); addGroup("Source"); addField("sourceObject", TypeS32, Offset(mSourceObjectId, Projectile), "@brief ID number of the object that fired the projectile.\n\n" "@note If the projectile was fired by a WeaponImage, sourceObject will be " "the object that owns the WeaponImage. This is usually the player."); addField("sourceSlot", TypeS32, Offset(mSourceObjectSlot, Projectile), "@brief The sourceObject's weapon slot that the projectile originates from.\n\n"); addField("ignoreSourceTimeout", TypeBool, Offset(ignoreSourceTimeout, Projectile)); endGroup("Source"); Parent::initPersistFields(); } bool Projectile::_setInitialPosition( void *object, const char *index, const char *data ) { Projectile* p = static_cast( object ); if ( p ) { Point3F pos; S32 count = dSscanf( data, "%f %f %f", &pos.x, &pos.y, &pos.z); if ( (count != 3) ) { Con::printf("Projectile: Failed to parse initial position \"px py pz\" from '%s'", data); return false; } p->setInitialPosition( pos ); } return false; } void Projectile::setInitialPosition( const Point3F& pos ) { mInitialPosition = pos; mCurrPosition = pos; } bool Projectile::_setInitialVelocity( void *object, const char *index, const char *data ) { Projectile* p = static_cast( object ); if ( p ) { Point3F vel; S32 count = dSscanf( data, "%f %f %f", &vel.x, &vel.y, &vel.z); if ( (count != 3) ) { Con::printf("Projectile: Failed to parse initial velocity \"vx vy vz\" from '%s'", data); return false; } p->setInitialVelocity( vel ); } return false; } void Projectile::setInitialVelocity( const Point3F& vel ) { mInitialVelocity = vel; mCurrVelocity = vel; } //-------------------------------------------------------------------------- bool Projectile::calculateImpact(float, Point3F& pointOfImpact, float& impactTime) { Con::warnf(ConsoleLogEntry::General, "Projectile::calculateImpact: Should never be called"); impactTime = 0; pointOfImpact.set(0, 0, 0); return false; } //-------------------------------------------------------------------------- F32 Projectile::getUpdatePriority(CameraScopeQuery *camInfo, U32 updateMask, S32 updateSkips) { F32 ret = Parent::getUpdatePriority(camInfo, updateMask, updateSkips); // if the camera "owns" this object, it should have a slightly higher priority if(mSourceObject == camInfo->camera) return ret + 0.2; return ret; } bool Projectile::onAdd() { if(!Parent::onAdd()) return false; if( !mDataBlock ) { Con::errorf("Projectile::onAdd - Fail - Not datablock"); return false; } if (isServerObject()) { ShapeBase* ptr; if (Sim::findObject(mSourceObjectId, ptr)) { mSourceObject = ptr; // Since we later do processAfter( mSourceObject ) we must clearProcessAfter // if it is deleted. SceneObject already handles this in onDeleteNotify so // all we need to do is register for the notification. deleteNotify( ptr ); } else { if (mSourceObjectId != -1) Con::errorf(ConsoleLogEntry::General, "Projectile::onAdd: mSourceObjectId is invalid"); mSourceObject = NULL; } // If we're on the server, we need to inherit some of our parent's velocity // mCurrTick = 0; scriptOnAdd(); } else { if (bool(mDataBlock->mProjectileShape)) { mProjectileShape = new TSShapeInstance(mDataBlock->mProjectileShape, isClientObject()); if (mDataBlock->activateSeq != -1) { mActivateThread = mProjectileShape->addThread(); mProjectileShape->setTimeScale(mActivateThread, 1); mProjectileShape->setSequence(mActivateThread, mDataBlock->activateSeq, 0); } } if (mDataBlock->particleEmitter != NULL) { ParticleEmitter* pEmitter = new ParticleEmitter; pEmitter->onNewDataBlock(mDataBlock->particleEmitter,false); if (pEmitter->registerObject() == false) { Con::warnf(ConsoleLogEntry::General, "Could not register particle emitter for particle of class: %s", mDataBlock->getName()); delete pEmitter; pEmitter = NULL; } mParticleEmitter = pEmitter; } if (mDataBlock->particleWaterEmitter != NULL) { ParticleEmitter* pEmitter = new ParticleEmitter; pEmitter->onNewDataBlock(mDataBlock->particleWaterEmitter,false); if (pEmitter->registerObject() == false) { Con::warnf(ConsoleLogEntry::General, "Could not register particle emitter for particle of class: %s", mDataBlock->getName()); delete pEmitter; pEmitter = NULL; } mParticleWaterEmitter = pEmitter; } } if (mSourceObject.isValid()) processAfter(mSourceObject); // Setup our bounding box if (bool(mDataBlock->mProjectileShape) == true) mObjBox = mDataBlock->mProjectileShape->mBounds; else mObjBox = Box3F(Point3F(0, 0, 0), Point3F(0, 0, 0)); MatrixF initialTransform( true ); initialTransform.setPosition( mCurrPosition ); setTransform( initialTransform ); // calls resetWorldBox addToScene(); if ( PHYSICSMGR ) mPhysicsWorld = PHYSICSMGR->getWorld( isServerObject() ? "server" : "client" ); return true; } void Projectile::onRemove() { if( !mParticleEmitter.isNull() ) { mParticleEmitter->deleteWhenEmpty(); mParticleEmitter = NULL; } if( !mParticleWaterEmitter.isNull() ) { mParticleWaterEmitter->deleteWhenEmpty(); mParticleWaterEmitter = NULL; } SFX_DELETE( mSound ); removeFromScene(); Parent::onRemove(); } bool Projectile::onNewDataBlock( GameBaseData *dptr, bool reload ) { mDataBlock = dynamic_cast( dptr ); if ( !mDataBlock || !Parent::onNewDataBlock( dptr, reload ) ) return false; if ( isGhost() ) { // Create the sound ahead of time. This reduces runtime // costs and makes the system easier to understand. SFX_DELETE( mSound ); if ( mDataBlock->getProjectileSound() ) mSound = SFX->createSource( mDataBlock->getProjectileSoundProfile() ); } return true; } void Projectile::submitLights( LightManager *lm, bool staticLighting ) { if ( staticLighting || mHasExploded || !mDataBlock->lightDesc ) return; mDataBlock->lightDesc->submitLight( &mLightState, getRenderTransform(), lm, this ); } bool Projectile::pointInWater(const Point3F &point) { // This is pretty much a hack so we can use the existing ContainerQueryInfo // and findObject router. // We only care if we intersect with water at all // so build a box at the point that has only 1 z extent. // And test if water coverage is anything other than zero. Box3F boundsBox( point, point ); boundsBox.maxExtents.z += 1.0f; ContainerQueryInfo info; info.box = boundsBox; info.mass = 0.0f; // Find and retreive physics info from intersecting WaterObject(s) if(mContainer != NULL) { mContainer->findObjects( boundsBox, WaterObjectType, findRouter, &info ); } else { // Handle special case where the projectile has exploded prior to having // called onAdd() on the client. This occurs when the projectile on the // server is created and then explodes in the same network update tick. // On the client end in NetConnection::ghostReadPacket() the ghost is // created and then Projectile::unpackUpdate() is called prior to the // projectile being registered. Within unpackUpdate() the explosion // is triggered, but without being registered onAdd() isn't called and // the container is not set. As all we're doing is checking if the // given explosion point is within water, we should be able to use the // global container here. We could likely always get away with this, // but using the actual defined container when possible is the right // thing to do. DAW AssertFatal(isClientObject(), "Server projectile has not been properly added"); gClientContainer.findObjects( boundsBox, WaterObjectType, findRouter, &info ); } return ( info.waterCoverage > 0.0f ); } //---------------------------------------------------------------------------- void Projectile::emitParticles(const Point3F& from, const Point3F& to, const Point3F& vel, const U32 ms) { if ( mHasExploded ) return; Point3F axis = -vel; if( axis.isZero() ) axis.set( 0.0, 0.0, 1.0 ); else axis.normalize(); bool fromWater = pointInWater(from); bool toWater = pointInWater(to); if (!fromWater && !toWater && bool(mParticleEmitter)) // not in water mParticleEmitter->emitParticles(from, to, axis, vel, ms); else if (fromWater && toWater && bool(mParticleWaterEmitter)) // in water mParticleWaterEmitter->emitParticles(from, to, axis, vel, ms); else if (!fromWater && toWater && mDataBlock->splash) // entering water { // cast the ray to get the surface point of the water RayInfo rInfo; if (gClientContainer.castRay(from, to, WaterObjectType, &rInfo)) { MatrixF trans = getTransform(); trans.setPosition(rInfo.point); Splash *splash = new Splash(); splash->onNewDataBlock(mDataBlock->splash, false); splash->setTransform(trans); splash->setInitialState(trans.getPosition(), Point3F(0.0, 0.0, 1.0)); if (!splash->registerObject()) { delete splash; splash = NULL; } // create an emitter for the particles out of water and the particles in water if (mParticleEmitter) mParticleEmitter->emitParticles(from, rInfo.point, axis, vel, ms); if (mParticleWaterEmitter) mParticleWaterEmitter->emitParticles(rInfo.point, to, axis, vel, ms); } } else if (fromWater && !toWater && mDataBlock->splash) // leaving water { // cast the ray in the opposite direction since that point is out of the water, otherwise // we hit water immediately and wont get the appropriate surface point RayInfo rInfo; if (gClientContainer.castRay(to, from, WaterObjectType, &rInfo)) { MatrixF trans = getTransform(); trans.setPosition(rInfo.point); Splash *splash = new Splash(); splash->onNewDataBlock(mDataBlock->splash,false); splash->setTransform(trans); splash->setInitialState(trans.getPosition(), Point3F(0.0, 0.0, 1.0)); if (!splash->registerObject()) { delete splash; splash = NULL; } // create an emitter for the particles out of water and the particles in water if (mParticleEmitter) mParticleEmitter->emitParticles(rInfo.point, to, axis, vel, ms); if (mParticleWaterEmitter) mParticleWaterEmitter->emitParticles(from, rInfo.point, axis, vel, ms); } } } void Projectile::explode( const Point3F &p, const Point3F &n, const U32 collideType ) { // Make sure we don't explode twice... if ( mHasExploded ) return; mHasExploded = true; // Move the explosion point slightly off the surface to avoid problems with radius damage Point3F explodePos = p + n * 0.001f; if ( isServerObject() ) { // Do what the server needs to do, damage the surrounding objects, etc. mExplosionPosition = explodePos; mExplosionNormal = n; mCollideHitType = collideType; mDataBlock->onExplode_callback( this, mExplosionPosition, mFadeValue ); setMaskBits(ExplosionMask); // Just wait till the timeout to self delete. This // gives server object time to get ghosted to the client. } else { // Client just plays the explosion at the right place... // Explosion* pExplosion = NULL; if (mDataBlock->waterExplosion && pointInWater(p)) { pExplosion = new Explosion; pExplosion->onNewDataBlock(mDataBlock->waterExplosion, false); } else if (mDataBlock->explosion) { pExplosion = new Explosion; pExplosion->onNewDataBlock(mDataBlock->explosion, false); } if( pExplosion ) { MatrixF xform(true); xform.setPosition(explodePos); pExplosion->setTransform(xform); pExplosion->setInitialState(explodePos, n); pExplosion->setCollideType( collideType ); if (pExplosion->registerObject() == false) { Con::errorf(ConsoleLogEntry::General, "Projectile(%s)::explode: couldn't register explosion", mDataBlock->getName() ); delete pExplosion; pExplosion = NULL; } } // Client (impact) decal. if ( mDataBlock->decal ) gDecalManager->addDecal(p, n, 0.0f, mDataBlock->decal); // Client object updateSound(); } /* // Client and Server both should apply forces to PhysicsWorld objects // within the explosion. if ( false && mPhysicsWorld ) { F32 force = 200.0f; mPhysicsWorld->explosion( p, 15.0f, force ); } */ } void Projectile::updateSound() { if (!mDataBlock->isProjectileSoundValid()) return; if ( mSound ) { if ( mHasExploded ) mSound->stop(); else { if ( !mSound->isPlaying() ) mSound->play(); mSound->setVelocity( getVelocity() ); mSound->setTransform( getRenderTransform() ); } } } void Projectile::processTick( const Move *move ) { Parent::processTick( move ); mCurrTick++; simulate( TickSec ); } void Projectile::simulate( F32 dt ) { if ( isServerObject() && mCurrTick >= mDataBlock->lifetime ) { deleteObject(); return; } if ( mHasExploded ) return; // ... otherwise, we have to do some simulation work. RayInfo rInfo; Point3F oldPosition; Point3F newPosition; oldPosition = mCurrPosition; if ( mDataBlock->isBallistic ) mCurrVelocity.z -= 9.81 * mDataBlock->gravityMod * dt; newPosition = oldPosition + mCurrVelocity * dt; // disable the source objects collision reponse for a short time while we // determine if the projectile is capable of moving from the old position // to the new position, otherwise we'll hit ourself bool disableSourceObjCollision = (mSourceObject.isValid() && (ignoreSourceTimeout || mCurrTick <= SourceIdTimeoutTicks)); if ( disableSourceObjCollision ) mSourceObject->disableCollision(); disableCollision(); // Determine if the projectile is going to hit any object between the previous // position and the new position. This code is executed both on the server // and on the client (for prediction purposes). It is possible that the server // will have registered a collision while the client prediction has not. If this // happens the client will be corrected in the next packet update. // Raycast the abstract PhysicsWorld if a PhysicsPlugin exists. bool hit = false; if ( mPhysicsWorld ) hit = mPhysicsWorld->castRay( oldPosition, newPosition, &rInfo, Point3F( newPosition - oldPosition) * mDataBlock->impactForce ); else hit = getContainer()->castRay(oldPosition, newPosition, dynamicCollisionMask | staticCollisionMask, &rInfo); if ( hit ) { // make sure the client knows to bounce if(isServerObject() && (rInfo.object->getTypeMask() & staticCollisionMask) == 0) setMaskBits( BounceMask ); MatrixF xform( true ); xform.setColumn( 3, rInfo.point ); setTransform( xform ); mCurrPosition = rInfo.point; // Get the object type before the onCollision call, in case // the object is destroyed. U32 objectType = rInfo.object->getTypeMask(); // re-enable the collision response on the source object since // we need to process the onCollision and explode calls if ( disableSourceObjCollision ) mSourceObject->enableCollision(); // Ok, here is how this works: // onCollision is called to notify the server scripts that a collision has occurred, then // a call to explode is made to start the explosion process. The call to explode is made // twice, once on the server and once on the client. // The server process is responsible for two things: // 1) setting the ExplosionMask network bit to guarantee that the client calls explode // 2) initiate the explosion process on the server scripts // The client process is responsible for only one thing: // 1) drawing the appropriate explosion // It is possible that during the processTick the server may have decided that a hit // has occurred while the client prediction has decided that a hit has not occurred. // In this particular scenario the client will have failed to call onCollision and // explode during the processTick. However, the explode function will be called // during the next packet update, due to the ExplosionMask network bit being set. // onCollision will remain uncalled on the client however, therefore no client // specific code should be placed inside the function! onCollision( rInfo.point, rInfo.normal, rInfo.object ); // Next order of business: do we explode on this hit? if ( mCurrTick > mDataBlock->armingDelay || mDataBlock->armingDelay == 0 ) { mCurrVelocity = Point3F::Zero; explode( rInfo.point, rInfo.normal, objectType ); } if ( mDataBlock->isBallistic ) { // Otherwise, this represents a bounce. First, reflect our velocity // around the normal... Point3F bounceVel = mCurrVelocity - rInfo.normal * (mDot( mCurrVelocity, rInfo.normal ) * 2.0); mCurrVelocity = bounceVel; // Add in surface friction... Point3F tangent = bounceVel - rInfo.normal * mDot(bounceVel, rInfo.normal); mCurrVelocity -= tangent * mDataBlock->bounceFriction; // Now, take elasticity into account for modulating the speed of the grenade mCurrVelocity *= mDataBlock->bounceElasticity; // Set the new position to the impact and the bounce // will apply on the next frame. //F32 timeLeft = 1.0f - rInfo.t; newPosition = oldPosition = rInfo.point + rInfo.normal * 0.05f; } else { mCurrVelocity = Point3F::Zero; } } // re-enable the collision response on the source object now // that we are done processing the ballistic movement if ( disableSourceObjCollision ) mSourceObject->enableCollision(); enableCollision(); if ( isClientObject() ) { emitParticles( mCurrPosition, newPosition, mCurrVelocity, U32( dt * 1000.0f ) ); updateSound(); } mCurrDeltaBase = newPosition; mCurrBackDelta = mCurrPosition - newPosition; mCurrPosition = newPosition; MatrixF xform( true ); xform.setColumn( 3, mCurrPosition ); setTransform( xform ); } void Projectile::advanceTime(F32 dt) { Parent::advanceTime(dt); if ( mHasExploded || dt == 0.0) return; if (mActivateThread && mProjectileShape->getDuration(mActivateThread) > mProjectileShape->getTime(mActivateThread) + dt) { mProjectileShape->advanceTime(dt, mActivateThread); } else { if (mMaintainThread) { mProjectileShape->advanceTime(dt, mMaintainThread); } else if (mActivateThread && mDataBlock->maintainSeq != -1) { mMaintainThread = mProjectileShape->addThread(); mProjectileShape->setTimeScale(mMaintainThread, 1); mProjectileShape->setSequence(mMaintainThread, mDataBlock->maintainSeq, 0); mProjectileShape->advanceTime(dt, mMaintainThread); } } } void Projectile::interpolateTick(F32 delta) { Parent::interpolateTick(delta); if( mHasExploded ) return; Point3F interpPos = mCurrDeltaBase + mCurrBackDelta * delta; Point3F dir = mCurrVelocity; if(dir.isZero()) dir.set(0,0,1); else dir.normalize(); MatrixF xform(true); xform = MathUtils::createOrientFromDir(dir); xform.setPosition(interpPos); setRenderTransform(xform); // fade out the projectile image S32 time = (S32)(mCurrTick - delta); if(time > mDataBlock->fadeDelay) { F32 fade = F32(time - mDataBlock->fadeDelay); mFadeValue = 1.0 - (fade / F32(mDataBlock->lifetime)); } else mFadeValue = 1.0; updateSound(); } //-------------------------------------------------------------------------- void Projectile::onCollision(const Point3F& hitPosition, const Point3F& hitNormal, SceneObject* hitObject) { // No client specific code should be placed or branched from this function if(isClientObject()) return; if (hitObject != NULL && isServerObject()) { mDataBlock->onCollision_callback( this, hitObject, mFadeValue, hitPosition, hitNormal ); } } //-------------------------------------------------------------------------- U32 Projectile::packUpdate( NetConnection *con, U32 mask, BitStream *stream ) { U32 retMask = Parent::packUpdate( con, mask, stream ); const bool isInitalUpdate = mask & GameBase::InitialUpdateMask; // InitialUpdateMask if ( stream->writeFlag( isInitalUpdate ) ) { stream->writeRangedU32( mCurrTick, 0, MaxLivingTicks ); if ( mSourceObject.isValid() ) { // Potentially have to write this to the client, let's make sure it has a // ghost on the other side... S32 ghostIndex = con->getGhostIndex( mSourceObject ); if ( stream->writeFlag( ghostIndex != -1 ) ) { stream->writeRangedU32( U32(ghostIndex), 0, NetConnection::MaxGhostCount ); stream->writeRangedU32( U32(mSourceObjectSlot), 0, ShapeBase::MaxMountedImages - 1 ); stream->writeFlag(ignoreSourceTimeout); } else // have not recieved the ghost for the source object yet, try again later retMask |= GameBase::InitialUpdateMask; } else stream->writeFlag( false ); } // ExplosionMask // // ExplosionMask will be set during the initial update but hidden is // only true if we have really exploded. if ( stream->writeFlag( ( mask & ExplosionMask ) && mHasExploded ) ) { mathWrite(*stream, mExplosionPosition); mathWrite(*stream, mExplosionNormal); stream->write(mCollideHitType); } // BounceMask if ( stream->writeFlag( mask & BounceMask ) ) { // Bounce against dynamic object mathWrite(*stream, mCurrPosition); mathWrite(*stream, mCurrVelocity); } return retMask; } void Projectile::unpackUpdate(NetConnection* con, BitStream* stream) { Parent::unpackUpdate(con, stream); if ( stream->readFlag() ) // InitialUpdateMask { mCurrTick = stream->readRangedU32( 0, MaxLivingTicks ); if ( stream->readFlag() ) { mSourceObjectId = stream->readRangedU32( 0, NetConnection::MaxGhostCount ); mSourceObjectSlot = stream->readRangedU32( 0, ShapeBase::MaxMountedImages - 1 ); ignoreSourceTimeout = stream->readFlag(); NetObject* pObject = con->resolveGhost( mSourceObjectId ); if ( pObject != NULL ) mSourceObject = dynamic_cast( pObject ); } else { mSourceObjectId = -1; mSourceObjectSlot = -1; mSourceObject = NULL; } } if ( stream->readFlag() ) // ExplosionMask { Point3F explodePoint; Point3F explodeNormal; mathRead( *stream, &explodePoint ); mathRead( *stream, &explodeNormal ); stream->read( &mCollideHitType ); // start the explosion visuals explode( explodePoint, explodeNormal, mCollideHitType ); } if ( stream->readFlag() ) // BounceMask { Point3F pos; mathRead( *stream, &pos ); mathRead( *stream, &mCurrVelocity ); mCurrDeltaBase = pos; mCurrBackDelta = mCurrPosition - pos; mCurrPosition = pos; setPosition( mCurrPosition ); } } //-------------------------------------------------------------------------- void Projectile::prepRenderImage( SceneRenderState* state ) { if (mHasExploded || mFadeValue <= (1.0/255.0)) return; if ( mDataBlock->lightDesc ) { mDataBlock->lightDesc->prepRender( state, &mLightState, getRenderTransform() ); } /* if ( mFlareData ) { mFlareState.fullBrightness = mDataBlock->lightDesc->mBrightness; mFlareState.scale = mFlareScale; mFlareState.lightInfo = mLight; mFlareState.lightMat = getTransform(); mFlareData->prepRender( state, &mFlareState ); } */ prepBatchRender( state ); } void Projectile::prepBatchRender( SceneRenderState *state ) { if ( !mProjectileShape ) return; GFXTransformSaver saver; // Set up our TS render state. 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 ); MatrixF mat = getRenderTransform(); mat.scale( mObjScale ); mat.scale( mDataBlock->scale ); GFX->setWorldMatrix( mat ); mProjectileShape->setDetailFromPosAndScale( state, mat.getPosition(), mObjScale ); mProjectileShape->animate(); mProjectileShape->render( rdata ); } DefineEngineMethod(Projectile, presimulate, void, (F32 seconds), (1.0f), "@brief Updates the projectile's positional and collision information.\n\n" "This function will first delete the projectile if it is a server object and is outside it's ProjectileData::lifetime. " "Also responsible for applying gravity, determining collisions, triggering explosions, " "emitting trail particles, and calculating bounces if necessary." "@param seconds Amount of time, in seconds since the simulation's start, to advance.\n" "@tsexample\n" "// Tell the projectile to process a simulation event, and provide the amount of time\n" "// that has passed since the simulation began.\n" "%seconds = 2.0;\n" "%projectile.presimulate(%seconds);\n" "@endtsexample\n" "@note This function is not called if the SimObject::hidden is true.") { object->simulate( seconds ); }