mirror of
https://github.com/TorqueGameEngines/Torque3D.git
synced 2026-01-20 12:44:46 +00:00
use impactforce to applyimpulse for stock physics too add an optional explodeOnTmeout for nonballistic projectiles, track if they hit something before their armingdelay is up, and delete them next simulation cycle
1544 lines
53 KiB
C++
1544 lines
53 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.
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~//~~~~~~~~~~~~~~~~~~~~~//
|
|
// 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"
|
|
#include "T3D/rigidShape.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;
|
|
mExplodeOnTmeout = 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;
|
|
mExplodeOnTmeout = other.mExplodeOnTmeout;
|
|
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");
|
|
addProtectedFieldV("lifetime", TypeRangedS32, Offset(lifetime, ProjectileData), &setLifetime, &getScaledValue, &CommonValidators::NaturalNumber,
|
|
"@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");
|
|
addProtectedFieldV("armingDelay", TypeRangedS32, Offset(armingDelay, ProjectileData), &setArmingDelay, &getScaledValue, &CommonValidators::PositiveInt,
|
|
"@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");
|
|
addProtectedFieldV("fadeDelay", TypeRangedS32, Offset(fadeDelay, ProjectileData), &setFadeDelay, &getScaledValue, &CommonValidators::NaturalNumber,
|
|
"@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("explodeOnTmeout", TypeBool, Offset(mExplodeOnTmeout, ProjectileData),
|
|
"@brief Detetmines if the projectile should explode on timeout");
|
|
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");
|
|
addFieldV("velInheritFactor", TypeRangedF32, Offset(velInheritFactor, ProjectileData), &CommonValidators::F32Range,
|
|
"@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.");
|
|
addFieldV("muzzleVelocity", TypeRangedF32, Offset(muzzleVelocity, ProjectileData), &CommonValidators::PositiveFloat,
|
|
"@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");
|
|
addFieldV("impactForce", TypeRangedF32, Offset(impactForce, ProjectileData), &CommonValidators::PositiveFloat);
|
|
addFieldV("bounceElasticity", TypeRangedF32, Offset(bounceElasticity, ProjectileData), &CommonValidators::PositiveFloat,
|
|
"@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");
|
|
addFieldV("bounceFriction", TypeRangedF32, Offset(bounceFriction, ProjectileData), &CommonValidators::PositiveFloat,
|
|
"@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");
|
|
addFieldV("gravityMod", TypeRangedF32, Offset(gravityMod, ProjectileData), &CommonValidators::F32Range,
|
|
"@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);
|
|
|
|
if (!isProjectileSoundValid())
|
|
{
|
|
//return false; -TODO: trigger asset download
|
|
}
|
|
|
|
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);
|
|
|
|
stream->writeFlag(mExplodeOnTmeout);
|
|
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);
|
|
|
|
mExplodeOnTmeout = stream->readFlag();
|
|
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<ProjectileData*>(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<ProjectileData*>(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<ProjectileData*>(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 ),
|
|
mHasHit(false),
|
|
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<Projectile*>( 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<Projectile*>( 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<ProjectileData*>( 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() )
|
|
{
|
|
if (mCurrTick >= (mDataBlock->lifetime - TickMs))
|
|
{
|
|
if (mDataBlock->mExplodeOnTmeout)
|
|
explode(mCurrPosition, Point3F::UnitZ, VehicleObjectType);
|
|
}
|
|
if (mCurrTick >= mDataBlock->lifetime || (mHasHit && mCurrTick < mDataBlock->armingDelay))
|
|
{
|
|
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 && rInfo.object->getTypeMask() & VehicleObjectType)
|
|
{
|
|
RigidShape* aRigid = dynamic_cast<RigidShape*>(rInfo.object);
|
|
if (aRigid)
|
|
aRigid->applyImpulse(rInfo.point, Point3F(newPosition - oldPosition) * mDataBlock->impactForce);
|
|
}
|
|
}
|
|
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;
|
|
newPosition = oldPosition = rInfo.point + rInfo.normal * 0.05f;
|
|
mHasHit = true;
|
|
}
|
|
}
|
|
|
|
// 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<ShapeBase*>( 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 );
|
|
}
|