Torque prediction hack for hitscans
by Danni · 01/26/2008 (4:07 pm) · 3 comments
After a little playing with Torque I decided it is time to hunker down and make the engine a little more pleasant for my tastes. Being influenced by some of the most popular games out there I wanted true instant hitscans to create nice enjoyable gameplay and heck why not start with a weapon akin to the the Super Shot Gun from Doom II.
www.antihax.net/movies/ssg.wmv
First problem is obviously that weapons are not fired locally on the client and rather from a returned state from the server. Well that's quite nasty for a hitscan weapon so that needs to be fixed. There was actually very little stopping it, namely the weapon transitions were not ran during client prediction so a simple little modification and this starts happening just nicely.
Altering the setImageTriggerState to have a force value added to the end to ensure that when the client runs it also runs the appropriate script functions tied to a specific state. But ahha! we now run into problem two. There are no functions client side and much of the information from the datablock is missing as only select variables are passed.
After weighing all the options i dreamed the simplest was to create a new set of scripts essentially "shared" scripts that are loaded by both server and client and within these shared scripts, all the weapons, items and players are defined so now the clients and servers definitions are identical, albeit there is a little wasted memory as some things are allocated twice via the netcode, but we will get to that type of cleanup later.
Hmm... now we have another problem. I specifically want to create particles from the weapon effect client side only, transmitting the creation and data of twenty particles each time the shotgun is fired is way to much data for such a simple cosmetic effect. To do this i had to spawn the particles client side and the simplest way i concluded to do this was to create a impactParticleEmitter for all GameBase and SceneObject objects along with a default emitter if one is not specifically defined. Quite simply;
Huzzah this worked wonders... now to write our SSG logic. To do this is needed a method of dealing with random numbers in a manner that the client and server always generate the same numbers. Darn Torque doesn't appear to offer such a function, so lets write a simple method. Once again i was inspired by the simplicity and decided to use a simple static table for the random number generation, after all this is a simple effect and doesn't really need cryptography strength RNG.
Now I simply add this to my weaponImage and add a little networking code to send my pointer if the servers generator is dirty (picked a psudorandom number) so the clients is always synched. Little tricky to think about, the client hits its generator first, the server will also hit the generator and send back its value after, the clients should be in the same state at this point so really nothing happens similar to the prediction system only much simpler. On the off chance it fails, heck, it's only cosmetic and the server is still authoritive on the damage.
Now I appear to have all I need for Torque to perform my bidding... lets write one of our little shared scripts.
Now looking past all the fixed to float math there, we have a nice little shotgun spread almost identical to the real Doom II weapon with all its real time hitscan goodness. :D
www.antihax.net/movies/ssg.wmvFirst problem is obviously that weapons are not fired locally on the client and rather from a returned state from the server. Well that's quite nasty for a hitscan weapon so that needs to be fixed. There was actually very little stopping it, namely the weapon transitions were not ran during client prediction so a simple little modification and this starts happening just nicely.
void Player::processTick(const Move* move)
{
.......
Parent::processTick(move);
// Hack to run weapon states in prediction
if (mDamageState == Enabled && move)
{
setImageTriggerState(0,move->trigger[0], !isServerObject());
setImageTriggerState(1,move->trigger[1], !isServerObject());
}
// Warp to catch up to server
if (delta.warpTicks > 0) {
.......
}Altering the setImageTriggerState to have a force value added to the end to ensure that when the client runs it also runs the appropriate script functions tied to a specific state. But ahha! we now run into problem two. There are no functions client side and much of the information from the datablock is missing as only select variables are passed.
After weighing all the options i dreamed the simplest was to create a new set of scripts essentially "shared" scripts that are loaded by both server and client and within these shared scripts, all the weapons, items and players are defined so now the clients and servers definitions are identical, albeit there is a little wasted memory as some things are allocated twice via the netcode, but we will get to that type of cleanup later.
Hmm... now we have another problem. I specifically want to create particles from the weapon effect client side only, transmitting the creation and data of twenty particles each time the shotgun is fired is way to much data for such a simple cosmetic effect. To do this i had to spawn the particles client side and the simplest way i concluded to do this was to create a impactParticleEmitter for all GameBase and SceneObject objects along with a default emitter if one is not specifically defined. Quite simply;
static ParticleEmitterData* defaultBallisticImpactEmitter = NULL;
void GameBaseData::initPersistFields()
{
Parent::initPersistFields();
addField("category", TypeCaseString, Offset(category, GameBaseData));
addField("className", TypeString, Offset(className, GameBaseData));
addField("ballisticImpactEmitter", TypeParticleEmitterDataPtr, Offset(ballisticImpactEmitter, GameBaseData));
Con::addVariable("defaultBallisticImpactEmitter", TypeParticleEmitterDataPtr, &defaultBallisticImpactEmitter);
}
void GameBase::BallisticImpactEmitter(Point3F& position, Point3F& vector)
{
// Shut up scripts.
if (!mDataBlock->ballisticImpactEmitter)
mDataBlock->ballisticImpactEmitter = defaultBallisticImpactEmitter;
if (!mDataBlock->ballisticImpactEmitter)
return;
ParticleEmitter* pEmitter = new ParticleEmitter;
pEmitter->onNewDataBlock(mDataBlock->ballisticImpactEmitter);
if (pEmitter->registerObject() == false)
{
Con::warnf(ConsoleLogEntry::General, "Could not register particle emitter for particle of class: %s", getName());
delete pEmitter;
pEmitter = NULL;
}
else
{
pEmitter->emitParticles(position, position, Point3F(1,1,1), vector, TickMs*4);
pEmitter->deleteWhenEmpty();
}
}
ConsoleMethod(GameBase, BallisticImpactEmitter, void, 4,4, "")
{
Point3F p( 0.0f,0.0f,0.0f );
Point3F v( 0.0f,0.0f,0.0f );
dSscanf( argv[2], "%g %g %g", &p.x, &p.y, &p.z );
dSscanf( argv[3], "%g %g %g", &v.x, &v.y, &v.z );
object->BallisticImpactEmitter(p,v);
}Huzzah this worked wonders... now to write our SSG logic. To do this is needed a method of dealing with random numbers in a manner that the client and server always generate the same numbers. Darn Torque doesn't appear to offer such a function, so lets write a simple method. Once again i was inspired by the simplicity and decided to use a simple static table for the random number generation, after all this is a simple effect and doesn't really need cryptography strength RNG.
#ifndef __SYNCHEDRANDOM_HPP_
#define __SYNCHEDRANDOM_HPP_
const static U8 rndtable[256] = {
1, 8, 109, 220, 222, 241, 149, 107, 75, 248, 254, 140, 16, 66,
74, 21, 211, 47, 80, 242, 154, 27, 205, 128, 161, 89, 77, 36,
95, 110, 85, 48, 212, 140, 211, 249, 22, 79, 200, 50, 28, 188,
52, 140, 202, 120, 68, 146, 62, 70, 184, 190, 91, 197, 152, 224,
149, 104, 25, 178, 252, 182, 202, 182, 141, 197, 4, 81, 181, 242,
145, 42, 39, 227, 156, 198, 225, 193, 219, 93, 122, 175, 249, 0,
175, 143, 70, 239, 46, 246, 163, 53, 163, 109, 168, 135, 2, 235,
25, 92, 20, 145, 138, 77, 69, 166, 78, 176, 173, 212, 166, 113,
94, 161, 41, 50, 239, 49, 111, 164, 70, 60, 2, 37, 171, 75,
136, 156, 11, 56, 42, 147, 138, 229, 73, 146, 77, 61, 98, 196,
135, 106, 63, 197, 195, 86, 96, 203, 113, 101, 170, 247, 181, 113,
80, 250, 108, 7, 255, 237, 129, 226, 79, 107, 112, 166, 103, 241,
24, 223, 239, 120, 198, 58, 60, 82, 128, 3, 184, 66, 143, 224,
145, 224, 81, 206, 163, 45, 63, 90, 168, 114, 59, 33, 159, 95,
28, 139, 123, 98, 125, 196, 15, 70, 194, 253, 54, 14, 109, 226,
71, 17, 161, 93, 186, 87, 244, 138, 20, 52, 123, 251, 26, 36,
17, 46, 52, 231, 232, 76, 31, 221, 84, 37, 216, 165, 212, 106,
197, 242, 98, 43, 39, 175, 254, 145, 190, 84, 118, 222, 187, 136,
120, 163, 236, 249
};
class SynchedRandom
{
public:
inline SynchedRandom() { index = 0; dirty = false; };
inline void setPointer(U8 _index) { index = _index; };
inline U8 getPointer() { return index; };
inline U8 getRandom() { dirty = true;
return rndtable[++index]; };
inline bool isDirty() { return dirty; };
private:
U8 index;
bool dirty;
};
#endifNow I simply add this to my weaponImage and add a little networking code to send my pointer if the servers generator is dirty (picked a psudorandom number) so the clients is always synched. Little tricky to think about, the client hits its generator first, the server will also hit the generator and send back its value after, the clients should be in the same state at this point so really nothing happens similar to the prediction system only much simpler. On the off chance it fails, heck, it's only cosmetic and the server is still authoritive on the damage.
Now I appear to have all I need for Torque to perform my bidding... lets write one of our little shared scripts.
function ShotgunImage::onFire(%this, %obj, %slot)
function ShotgunImage::onFire(%this, %obj, %slot)
{
if (%obj.isServerObject())
{
%weapon = %obj.getMountedImage(%slot);
%searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::StaticObjectType | $TypeMasks::AtlasObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::WaterObjectType | $TypeMasks::VehicleObjectType | $TypeMasks::VehicleBlockerObjectType | $TypeMasks::CorpseObjectType | $TypeMasks::DebrisObjectType | $TypeMasks::AIObjectType;
for (%i = 0; %i < 20; %i++)
{
%damage = 5*(%obj.getRandom()%3+1);
%muzzzleVector = %obj.getMuzzleVector(%slot);
%muzzzleVector = VectorNormalize(%muzzzleVector);
%yaw = YawFromVector(%muzzzleVector);
%pitch = PitchFromVector(%muzzzleVector);
%yaw += mDegToRad(((((%obj.getRandom()-128) << 19) / 65536.0)/65536.0)*360);
%pitch += mDegToRad(((((%obj.getRandom()-128) << 18) / 65536.0)/65536.0)*360);
%muzzzleVector = VectorFromAngles(%yaw, %pitch);
%muzzzleVector = VectorScale(%muzzzleVector, %this.ballisticsRange);
%muzzlePoint = %obj.getMuzzlePoint(%slot);
%distance = VectorAdd(%muzzlePoint, %muzzzleVector);
// Do a "hitscan"
%scanTarg = ContainerRayCast(%muzzlePoint, %distance, %searchMasks, %obj);
if(%scanTarg)
{
%target = firstWord(%scanTarg);
%target.damage(%obj,%pos,%damage,%this.meansOfDeath);
}
}
}
else
{
%weapon = %obj.getMountedImage(%slot);
%searchMasks = $TypeMasks::PlayerObjectType | $TypeMasks::StaticObjectType | $TypeMasks::AtlasObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::WaterObjectType | $TypeMasks::VehicleObjectType | $TypeMasks::VehicleBlockerObjectType | $TypeMasks::CorpseObjectType | $TypeMasks::DebrisObjectType | $TypeMasks::AIObjectType;
for (%i = 0; %i < 20; %i++)
{
%damage = 5*(%obj.getRandom()%3+1);
%muzzzleVector = %obj.getMuzzleVector(%slot);
%muzzzleVector = VectorNormalize(%muzzzleVector);
%yaw = YawFromVector(%muzzzleVector);
%pitch = PitchFromVector(%muzzzleVector);
%yaw += mDegToRad(((((%obj.getRandom()-128) << 19) / 65536.0)/65536.0)*360);
%pitch += mDegToRad(((((%obj.getRandom()-128) << 18) / 65536.0)/65536.0)*360);
%muzzzleVector = VectorFromAngles(%yaw, %pitch);
%muzzzleVector = VectorScale(%muzzzleVector, %this.ballisticsRange);
%muzzlePoint = %obj.getMuzzlePoint(%slot);
%distance = VectorAdd(%muzzlePoint, %muzzzleVector);
// Do a "hitscan"
%scanTarg = ContainerRayCast(%muzzlePoint, %distance, %searchMasks, %obj);
if(%scanTarg)
{
%target = firstWord(%scanTarg);
%pos = getWords(%scanTarg, 1, 3);
%target.BallisticImpactEmitter(%pos, "1 1 1");
}
}
}
}Now looking past all the fixed to float math there, we have a nice little shotgun spread almost identical to the real Doom II weapon with all its real time hitscan goodness. :D
#2
Doing the animation when the trigger is down on the client is the way to go.
01/27/2008 (4:04 pm)
Didn't read all of it, but it's really nice to see someone typing this all down!Doing the animation when the trigger is down on the client is the way to go.
#3
01/28/2008 (11:35 am)
This is a fabulous example of someone just going out and finding a way to have Torque do something that they want. Too often people will just "squat" and complain that "Torque doesn't do this!" I hope people learn from this example. 
Torque Owner H.W. Kim
BTW, the only different line 'if (%obj.isServerObject())' and 'else' clause in the script function ShotgunImage::onFire() is added '%target.BallisticImpactEmitter(%pos, "1 1 1");' in the else clause. I guess where a reffactorying is needed place. =)
Neat way and nice sample movie which explains well what the changes are. I'll try it on my side.
Thank you for your efforts!