Game Development Community

Advanced Weapon Recoil System - Weapon Bounce

by Matt "Mr. Pig" Razza · 07/03/2006 (11:37 am) · 51 comments

This resource was created because many people have attempted to create a recoil system and failed.

Please include Valde Games or Matt Razza in your Special Thanks page. :)

What this system does:
Each time a mounted object (a shapeBaseImage mounted to a shapeBase) enters the fire state (the state with stateFire set to true) the engine will move the camera according to settings set in the shapeBaseImage's datablock. There is also a bit of random fluctuation you can set and the recoil increases the longer you hold down the fire key (then decreases when you let go - forcing (or encouraging) the user to fire in bursts).

Let's begin, you will be editing the following files today:
shapeBase.cc
shapeImage.cc
shapeBase.h

You'll need to add the following includes at the bottom of the stack in shapeImage.cc:
#include "math/mRandom.h"
#include "game/fx/cameraFXMgr.h"

First we are going to add the required variables to the datablocks header file. In "shapeBase.h", around line 365 (and after "virtual void unpackData(BitStream* stream);") add the following:
/// Recoil related meathods
   /// Code (C) Valde Games
   F32 recoilFrequencyFactor;
   F32 recoilFrequencyRandomize;
   F32 recoilAmpFactor;
   F32 recoilAmpRandomize;
   F32 shotFactor; //Client only so server and client will be out of sync

   F32 calcRecoil(F32 recoilFactor, F32 recoilRandomize);
   F32 getShotFactor();

Now let's add the vars to the constructor. In "shapeImage.cc", around line 162, add:
recoilFrequencyFactor = 0;
   recoilFrequencyRandomize = 0;
   recoilAmpFactor = 0;
   recoilAmpRandomize = 0;
   shotFactor = 1;

Now that the vars will be set to the default setting we need to expose them to the datablock in script. To do so keep open "shapeImage.cc" and add the following around line 432:
//Recoil
   addNamedField(recoilFrequencyFactor, TypeF32, ShapeBaseImageData);
   addNamedField(recoilFrequencyRandomize, TypeF32, ShapeBaseImageData);
   addNamedField(recoilAmpFactor, TypeF32, ShapeBaseImageData);
   addNamedField(recoilAmpRandomize, TypeF32, ShapeBaseImageData);

Now we are going to add the functions that will calculate the amount of recoil to be applied based on the values set in the datablock. In "shapeImage.cc", around line 1485, add the following:
//===========================================================================
//Name: calcRecoil
//Description: Calculates the recoil to be applied
//Code by Valde Games (Matt Razza)
//---------------------------------------------------------------------------
F32 ShapeBaseImageData::calcRecoil(F32 recoilFactor, F32 recoilRandomize)
{
	MRandomLCG randomNumbers;
	F32 recoil; //Return var
	F32 factor = randomNumbers.randI(-1 * recoilRandomize, recoilRandomize); //Get random factor

	//Calc factor (if negitive)
	if (factor < 0)
	{
		factor = (-1 * factor) / recoilRandomize; //Fix recoil
	}
	else if (factor == 0)
	{
		factor = recoilRandomize; //Have some recoil
	}

	recoil = recoilFactor * factor; //Get recoil

	recoil = recoil * getShotFactor();

	return recoil;
}

//===========================================================================
//Name: getShotFactor
//Description: Returns the shot factor
//Code by Valde Games (Matt Razza)
//---------------------------------------------------------------------------
F32 ShapeBaseImageData::getShotFactor()
{
	return shotFactor;
}

Now that all the functions are added let's add the code that will trigger the recoil to be applied. Still in "shapeImage.cc", around line 1531, in the "void ShapeBase::setImageState(U32 imageSlot, U32 newState,bool force)" function. Add:
//If client is firing, do recoil
   if (!isServerObject() && newState == image.dataBlock->fireState && getControllingClientID() == dAtoi(Con::getVariable("$Client::gameConnection"))))
   {
	   //Gather info
	   F32 recoilFrequency = image.dataBlock->calcRecoil(image.dataBlock->recoilFrequencyFactor, image.dataBlock->recoilFrequencyRandomize);
	   F32 recoilAmp = image.dataBlock->calcRecoil(image.dataBlock->recoilAmpFactor, image.dataBlock->recoilAmpRandomize);

	   //Do recoil
	   CameraShake *camShake = new CameraShake;
           camShake->setDuration(0.2);
	   camShake->setFrequency(Point3F(recoilFrequency, recoilFrequency, recoilFrequency));
           VectorF shakeAmp = Point3F(recoilAmp, recoilAmp, recoilAmp);
           camShake->setAmplitude( shakeAmp );
           camShake->setFalloff( 10);
           camShake->init();
           gCamFXMgr.addFX( camShake );

	   //Add to shot factor
	   if (image.dataBlock->getShotFactor() < 1.2)
	   {
			image.dataBlock->shotFactor += 0.05;
	   }
   }

After:
if (!mMountedImageList[imageSlot].dataBlock)
      return;
   MountedImage& image = mMountedImageList[imageSlot];

Now the recoil will be applied each time the shapeBaseImage enters the onFire state. Also the recoil factor will increase after each shot. Now we need to have it decrease back to 1 over time. To do so, open "shapeBase.cc" and around line 969, after the "if (isServerObject())" code add:
else
   {
	   for (int x = 0; x < MaxMountedImages; x++) //Check each image
	   {
		   if (mMountedImageList[x].dataBlock != NULL) //Check if one is mounted
		   {
			   if (mMountedImageList[x].dataBlock->shotFactor > 1) //Check if we need to change it
			   {
					mMountedImageList[x].dataBlock->shotFactor -= 0.05; //Change shot factor
			   }
		   }
	   }
   }

Let's just add network support. At the bottom of the packData function in "shapeImage.cc", around line 545 add:
//Recoil data
	stream->write(recoilFrequencyFactor);
	stream->write(recoilFrequencyRandomize);
	stream->write(recoilAmpFactor);
	stream->write(recoilAmpRandomize);

Then at the bottom of the unpackData function, around line 669 add:
//Recoil data
	stream->read(&recoilFrequencyFactor);
	stream->read(&recoilFrequencyRandomize);
	stream->read(&recoilAmpFactor);
	stream->read(&recoilAmpRandomize);

Once network support for the shapeBase area has been added, we need to add network support for the rest. We will be adding clientID tracking the gameBase. Now ALL gameBase objects will have full support for client ID's. Please note that only the ID is being replicated, not the full gameConnection.

To do so open "gameBase.h" and add the following in the gameBase class under "private:" (around line 175:
S32 mControllingClientID;

Then add the following under "public:" (around line 341):
S32 getControllingClientID() { return mControllingClientID; }

Now open "gameBase.cc" and add the following to the "packUpdate" function, around line 459:
if (stream->writeFlag((mask & ControlMask) && mControllingClient != 0))
   {
		stream->write(dAtoi(mControllingClient->getIdString()));
   }

Lastly we need to add the required "unpackUpdate" code, add the following around line 484:
if (stream->readFlag())
   {
		stream->read(&mControllingClientID);
   }

Now we are done with the engine. Open each of your weapon's datablocks (the "datablock ShapeBaseImageData()" thing) and add the following:
//Weapon Recoil Hooks
	recoilFrequencyFactor = 1.5; //Pitch recoil
	recoilFrequencyRandomize = 2; //Range to randomize (-/+)
	recoilAmpFactor = 0.5; //Yaw recoil
	recoilAmpRandomize = 2; //Range to randomize (-/+)

Finally we need to track what the server thinks our client ID is to do full perdictions. In script (any file) add the following function:
function clientCmdsetID(%id)
{
	$Client::gameConnection = %id;
}

Then add the following line in the function "gameConnection::onConnect" in clientConnection.cs which should be located in the common directory:
commandToClient(%client, 'setID', %client); //Tell the client it's ID

Feel free to change the values - I have not tested the current values but am guessing they are ok.

That's it! I have not tested this code - it is based off of my other resource (an the camera shake code off of one by Tim Heldna). Let me know if there is a problem.

Resources:
www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=9847 - Tims
www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=10241 - Mine
Page «Previous 1 2 3 Last »
#1
04/12/2006 (6:39 pm)
Now it should work online.
#2
04/12/2006 (6:55 pm)
Edit - Added include needed.
#3
04/12/2006 (7:00 pm)
Works great, but uh, it recoils with more amplitude/shake power with a longer firing sequence doesn't it? Which value is it that determines the maximum/minimum shake?
#4
04/12/2006 (7:17 pm)
It's done by a factor. So 1 * current shake to begin with then 1.2 * current shake after u hold fire. The max that the shake factor can be (currently 1.2 because anything larger than that in my old resource would just be stupid) is the
//Add to shot factor
if (image.dataBlock->getShotFactor() < 1.2)
line. In "shapeImage.cc", around line 1531, in the "void ShapeBase::setImageState(U32 imageSlot, U32 newState,bool force)" function.

The increase per shot is right inside that if statment.
#5
04/16/2006 (9:59 am)
Tested, camera bounce is transmitted through all players, server or client, if a player fires, all players in the game get the bounce. Needs a fix :P
#6
04/16/2006 (5:35 pm)
ROFLMFAO. Nice man - I havn't tested it with more then one player. Should be an easy fix though. :P

Keep de-bugging it. :) I'll fix it tom (or later tonight).
#7
04/16/2006 (5:54 pm)
Maybe if we just get rid of the stream read and writes? (random non-programmer taking a stab)
#8
04/16/2006 (7:20 pm)
No. You need those. The problem is that apperently that function is called for every client that changes state on every client (which makes sense now that I think about it - torque uses the same third person rendering as first person so updating everything for the on-fire state for each client is required (you can't just make a projectile)) so all I need to do is make sure the client changing state is you. :P

Just give me a few min. (working on it now)

[Edit]
Now - if only I knew how to find the GameConnection object that we are (as in - the object that the client YOU are using is - not the client that the image is being controled by - I know that).
#9
04/22/2006 (6:37 pm)
In Milestone 3, upon compile you get many errors about all the "recoilFrequencyFactor, calcRecoil" etc saying it's got an "undeclared identifier", pointing to all the stream read/writes in shapeBase.cpp. Good old TGE 1.4 code merge.

Edit: Screw it. I'm not liking MS3, everything breaks.
#10
04/23/2006 (10:39 am)
Good work on this Mr. Pig! I've altered my resource to include a link to this resource, please post here when you get it working properly across a network.

If you wanted to make some gold you might want to look at creating a C++ based complete weapon management system / package which caters for recoil, damage, cycling, reloading and everything else pertinent to weapons. If it's cleanly written, well commented, easy to customise, and runs efficiently (esp across a network) you could easily flog it off for $10 - $15 US. I know many people that would be interested in such a thing, just a thought.

Quote:
Screw it. I'm not liking MS3, everything breaks.
Responding with a quote whose origin skips my mind
The more things change the more they stay the same...
#11
04/23/2006 (12:17 pm)
Alright. If you have any idea on how to find the clients ID. (Not the client that owns the shape - the client your playing on) That would be great.
#12
04/23/2006 (10:26 pm)
%client = ClientGroup.getObject(%i);
#13
04/24/2006 (12:00 pm)
In the engine...
#14
04/27/2006 (2:04 pm)
Fixed online.
#16
04/30/2006 (6:05 am)
Note - I have yet to test it online because my team members are away. But it seems it should work.
#17
05/03/2006 (7:15 pm)
This goes script side everywhere instead of %conn = new GameConnection(ServerConnection); etc.?
%conn = $Client::gameConnection;
#18
05/04/2006 (10:14 am)
Quote:
Fixed online.
Quote:
Note - I have yet to test it online because my team members are away. But it seems it should work.
Lol, nice one Matt ;)
#19
05/04/2006 (12:49 pm)
No, it goes after that code. It does not replace it.
#20
05/31/2006 (3:44 pm)
where exactly do I place %conn = $Client::gameConnection;?
Page «Previous 1 2 3 Last »