Projectile based Weapons

From Valve Developer Community
Jump to navigation Jump to search
For general information on projectiles, see Projectiles and Authoring a weapon entity.

This article explains some of the possibilities for projectile weapons like the crossbow or RPG, instead of the standard guns that hit or miss instantly. All tweaks in this article are based on the crossbow code. The relevant code can all be found in the server side part of the crossbow code. This is found under hl -> source files -> HL2 DLL -> weapon_crossbow.cpp in the solution explorer

Adding spread

Spread can be added rather quickly. The GetAutoAimVector used in other weapons appears to do no good here, so another approach is needed. I basically use the aiming vector converted to angles to change these angles, and then convert it back to the aiming vector. The relevant code is found in the CWeaponCrossbow::FireBolt method, the lines that need to be changed are:

594 - 595

	QAngle angAiming;
	VectorAngles( vecAiming, angAiming );

these can be exchanged with:

	QAngle angAiming;
	VectorAngles( vecAiming, angAiming );

	angAiming.x += ((rand() % 250) / 100.0) * (rand() % 2 == 1 ? -1 : 1);
	angAiming.y += ((rand() % 250) / 100.0) * (rand() % 2 == 1 ? -1 : 1);
		
	AngleVectors(angAiming, &vecAiming);

This changes the x and y angles with up to 2.5 degrees in either direction. This actually results in a square spread instead of a cone, but is a good quick-and-dirty solution. To make a conical spread, take the cosine and sine of the x and y coordinates respectively. If anybody knows of a better approach, I would be happy to hear about it.


Here is an example of conical spread:

	QAngle angAiming;
	VectorAngles( vecAiming, angAiming );

	float RandomAngle = (rand() % 360);
	float RandMagnitudeX = ((rand() % 175) / 100.0);
	float RandMagnitudeY = ((rand() % 175) / 100.0);
	angAiming.x += (RandMagnitudeX)*cos(RandAngle);
	angAiming.y += (RandMagnitudeY)*sin(RandAngle);

	AngleVectors(angAiming, &vecAiming);

The RandMagnitude floats can have their limits changed to make the spread wider/narrower.
Giving X and Y different limits causes an ellipses shaped spread.


In order to simplify debugging, you may want to change line 608

	m_iClip1--;"

to

	//m_iClip1--;"

and line 640

	m_flNextPrimaryAttack = m_flNextSecondaryAttack	= gpGlobals->curtime + 0.75;

to

	m_flNextPrimaryAttack = m_flNextSecondaryAttack	= gpGlobals->curtime + 0.15;

This gives you unlimited ammo and a quicker "reload" time.

Obeying gravity

Crossbow bolts do not obey the sv_gravity server variable, which can be changed with the console, or I believe even on a per map basis by the mapper. In order to make this work properly, find the CCrossbowBolt::Spawn method and edit line no 163 (unmodified file):

	SetGravity( 0.05f );

change it to:

	SetGravity( sv_gravity.GetFloat() / 600);

Since the original value was 0.05, and gravity usually is set to 600 or 800, this means that the new gravity value will be larger, and the bolt will fly in a more pronounced arc. You will have to tweak the value as you see fit, but the bolt should now obey any server side changes in gravity.

In order to make this compile you will need to add

#include "movevars_shared.h"

to the includes in the top of the file, otherwise the sv_gravity variable is not accessible.

Firing multiple bolts (not working yet)

In order to create a shotgun-like effect, it may be desirable to fire multiple randomized bolts at once. The following is an example:

	for (int i = -10; i < 11; i += 2)
	{
		Vector	vecAiming	= pOwner->GetAutoaimVector( 0 );	
		Vector vecSrc		= pOwner->Weapon_ShootPosition();

		QAngle angAiming;

		VectorAngles( vecAiming, angAiming );

		angAiming.x += ((rand() % 250) / 100.0) * (rand() % 2 == 1 ? -1 : 1);
		vecSrc.x += i;
		angAiming.y += ((rand() % 250) / 100.0) * (rand() % 2 == 1 ? -1 : 1);
		vecSrc.y += i;
		/*angAiming.z += ((rand() % 350) / 100.0) * (rand() % 2 == 1 ? -1 : 1);
		vecSrc.z += i;*/
		
		AngleVectors(angAiming, &vecAiming);

		CCrossbowBolt *pBolt = CCrossbowBolt::BoltCreate( vecSrc, angAiming, pOwner );

		if ( pOwner->GetWaterLevel() == 3 )
		{
			pBolt->SetAbsVelocity(vecAiming * BOLT_WATER_VELOCITY );
		}
		else
		{
			pBolt->SetAbsVelocity(vecAiming * BOLT_AIR_VELOCITY );
		}
	}

Changing the vecSrc.x and vecSrc.y based on i results in a neat line of crossbow bolts in the instant that the trigger is pressed. However, just a moment later all but the last (or first?) in the line disappear, and only the remaining bolt continues on its trajectory, and it is only this bolt that glows. Changing the the vecSrc.x and vecSrc.y based on random numbers creates a more randomized spawn position for each bolt, and when moving around the crossbow actually shoots 3-5 bolts, but still not all 10 of them, and not all of the time. For now, the solution for this is unknown.

Note.pngNote:The reason the bolts don't appear is because you can't spawn an entity in the same space as another entity. Which is what is occurring in the above code. Working code for this will be posted shortly.

Modding the crossbow so that it has multiple spawn points may help with getting the code to function.

Modifying the RPG rocket to obey gravity

Note.pngNote:This tutorial will assume you are using OB code and are working with the HL2MP codebase

This section will show you how to make a rocket obey gravity.

This section also needs to add the code to turn off the RPG Guidance system but for now you may figure out how to do that using the Toggling RPG Guidance tutorial.

To start, open up weapon_rpg.cpp

In the CMissile::Spawn( void ) method,

void CMissile::Spawn( void )
{
    Precache();

    SetSolid( SOLID_BBOX );
    SetModel("models/weapons/w_missile_launch.mdl");
    UTIL_SetSize( this, -Vector(4,4,4), Vector(4,4,4) );

    SetTouch( &CMissile::MissileTouch );

    SetMoveType( MOVETYPE_FLYGRAVITY, MOVECOLLIDE_FLY_BOUNCE );
    SetThink( &CMissile::IgniteThink );
    
    SetNextThink( gpGlobals->curtime + 0.3f );
    SetDamage( EXPLOSION_DAMAGE );

    m_takedamage = DAMAGE_YES;
    m_iHealth = m_iMaxHealth = 100;
    m_bloodColor = DONT_BLEED;
    m_flGracePeriodEndsAt = 0;

    AddFlag( FL_OBJECT );
}

add these lines

    // Test out grav.
    // We should definately have the rocket be modified by gravity.
    SetGravity( 1.0f );

between

SetDamage( EXPLOSION_DAMAGE );

and

m_takedamage = DAMAGE_YES;


Note.pngNote:The SetGravity function parameter can be changed to whatever you want the gravity to be for the rocket.

Looking at this code, the rocket is spawned with MOVETYPE_FLYGRAVITY. And this is all well and good, but once the CMissile::IgniteThink( void ) method is called, the method changes the movetype to ignore gravity with MOVETYPE_FLY! (which is what you would want if you are still controlling the rocket with the laser).

So what we want to do is change the movetype back to MOVETYPE_FLYGRAVITY. Go ahead and change the CMissile::IgniteThink( void ) function to look like the one below.

Note.pngNote:The commented out lines are what the code was before the change. You can remove those comment lines if you want.

Also below you will notice the Think function has been changed out to use &CMissile::DumbThink. This function isn't currently declared in the header OR the DATADESC section (which it needs to be for the rest of the code to know it exists and use it). But for now just change out the code to make the next think method &CMissile::DumbThink and forget about it until we implement its declaration in a below section.

Note.pngNote:Alternatively you might comment out the original SetMoveType( MOVETYPE_FLY ); without adding the SetMoveType( MOVETYPE_FLYGRAVITY ); since this is what it was set as in CMissile::Spawn( void ). But note this will leave MOVECOLLIDE_FLY_BOUNCE which will make the rocket bounce off of structures and objects!
void CMissile::IgniteThink( void )
{
    // If it's MOVETYPE_FLY then gravity won't affect it.
    // SetMoveType( MOVETYPE_FLY );
    SetMoveType( MOVETYPE_FLYGRAVITY );
    SetModel("models/weapons/w_missile.mdl");
    UTIL_SetSize( this, vec3_origin, vec3_origin );
    RemoveSolidFlags( FSOLID_NOT_SOLID );

    //TODO: Play opening sound

    Vector vecForward;

    EmitSound( "Missile.Ignite" );

    AngleVectors( GetLocalAngles(), &vecForward );
    SetAbsVelocity( vecForward * RPG_SPEED );

    // Changed out &CMissile::SeekThink for &CMissile::DumbThink.
    //SetThink( &CMissile::SeekThink );
    SetThink( &CMissile::DumbThink );
    SetNextThink( gpGlobals->curtime );

    if ( m_hOwner && m_hOwner->GetOwner() )
    {
        CBasePlayer *pPlayer = ToBasePlayer( m_hOwner->GetOwner() );

        if ( pPlayer )
        {
            color32 white = { 255,225,205,64 };
            UTIL_ScreenFade( pPlayer, white, 0.1f, 0.0f, FFADE_IN );
        }
    }

    CreateSmokeTrail();
}


Below is the new method CMissile::DumbThink( void ). This method is different than CMissile::DumbFire( void ) which you should not use if you want to make your rocket be affected by gravity.

The following function can be copied directly into the weapon_rpg.cpp class. A good place to put this would be after the CMissile::SeekThink( void ) method.

The first part of this Think function

    // If we have a grace period, go solid when it ends
    if ( m_flGracePeriodEndsAt )
    {
        if ( m_flGracePeriodEndsAt < gpGlobals->curtime )
        {
            RemoveSolidFlags( FSOLID_NOT_SOLID );
            m_flGracePeriodEndsAt = 0;
        }
    }

will check the Grace Period and remove the FSOLID_NOT_SOLID flags so the rocket may collide with surfaces and objects.

Note.pngNote:This code was copied from the beginning of CMissile::SeekThink( void )

The next part of the function

    // Correct the face of the rocket.
    QAngle angNewAngles;

    VectorAngles( GetAbsVelocity(), angNewAngles );
    SetAbsAngles( angNewAngles );

will correct the face in which the rocket is pointing. You might want to comment out this section of the code to see what the rocket would look like if you don't understand fully what this code does. (But make sure you finish the rest of the tutorial before you try this!)

Note.pngNote:The above code's section was also taken from a different part of the game from within the crossbolt code.

Finally,

    SetNextThink( gpGlobals->curtime + 0.1f );

will tell the code to execute this function again in 100 milliseconds. You need this as the new direction of the face needs to be input.

Here is the completed function.

//-----------------------------------------------------------------------------
// Purpose: To make the rocket follow gravity and set the face of it to what direction
//            it's pointing.
//-----------------------------------------------------------------------------
void CMissile::DumbThink( void )
{
    
    // If we have a grace period, go solid when it ends
    if ( m_flGracePeriodEndsAt )
    {
        if ( m_flGracePeriodEndsAt < gpGlobals->curtime )
        {
            RemoveSolidFlags( FSOLID_NOT_SOLID );
            m_flGracePeriodEndsAt = 0;
        }
    }

    // Correct the face of the rocket.
    QAngle angNewAngles;

    VectorAngles( GetAbsVelocity(), angNewAngles );
    SetAbsAngles( angNewAngles );

    SetNextThink( gpGlobals->curtime + 0.1f );
}

The rest of this tutorial is to do some housekeeping so the code knows where to look for the CMissile::DumbThink( void ) method.

In the weapon_rpg.cpp, toward the top of the file is the DATADESC section. Modify the code to look as such. (You should be adding DEFINE_FUNCTION (DumbThink ), right after the code for SeekThink)

BEGIN_DATADESC( CMissile )

    DEFINE_FIELD( m_hOwner,                    FIELD_EHANDLE ),
    DEFINE_FIELD( m_hRocketTrail,            FIELD_EHANDLE ),
    DEFINE_FIELD( m_flAugerTime,            FIELD_TIME ),
    DEFINE_FIELD( m_flMarkDeadTime,            FIELD_TIME ),
    DEFINE_FIELD( m_flGracePeriodEndsAt,    FIELD_TIME ),
    DEFINE_FIELD( m_flDamage,                FIELD_FLOAT ),
    DEFINE_FIELD( m_bCreateDangerSounds,    FIELD_BOOLEAN ),
    
    // Function Pointers
    DEFINE_FUNCTION( MissileTouch ),
    DEFINE_FUNCTION( AccelerateThink ),
    DEFINE_FUNCTION( AugerThink ),
    DEFINE_FUNCTION( IgniteThink ),
    DEFINE_FUNCTION( SeekThink ),
    DEFINE_FUNCTION( DumbThink ), // Added dumb think method.

END_DATADESC()

Now you will need to go to the weapon_rpg.h file.

The below code in bold is the declaration of the DumbThink Method. This is needed so the definition of the method will know that this method is a part of the CMissile class.

Add the following code

    // Create a dumb think method. This should make it so the rpg obeys gravity.
    void    DumbThink( void );

before

    void    SeekThink( void );

Use this portion of the header as reference.

...
//###########################################################################
//    >> CMissile        (missile launcher class is below this one!)
//###########################################################################
class CMissile : public CBaseCombatCharacter
{
    DECLARE_CLASS( CMissile, CBaseCombatCharacter );

public:
    static const int EXPLOSION_RADIUS = 200;
    static const int EXPLOSION_DAMAGE = 200;
    CMissile();
    ~CMissile();

#ifdef HL1_DLL
    Class_T Classify( void ) { return CLASS_NONE; }
#else
    Class_T Classify( void ) { return CLASS_MISSILE; }
#endif
    
    void    Spawn( void );
    void    Precache( void );
    void    MissileTouch( CBaseEntity *pOther );
    void    Explode( void );
    void    ShotDown( void );
    void    AccelerateThink( void );
    void    AugerThink( void );
    void    IgniteThink( void );
    void    SeekThink( void );
   
    // Create a dumb think method. This should make it so the rpg obeys gravity.
    void    DumbThink( void );
    
    void    DumbFire( void );
    void    SetGracePeriod( float flGracePeriod );

    int        OnTakeDamage_Alive( const CTakeDamageInfo &info );
    void    Event_Killed( const CTakeDamageInfo &info );
    
    virtual float    GetDamage() { return m_flDamage; }
    virtual void    SetDamage(float flDamage) { m_flDamage = flDamage; }

    unsigned int PhysicsSolidMaskForEntity( void ) const;

    CHandle<CWeaponRPG>        m_hOwner;

    static CMissile *Create( const Vector &vecOrigin, const QAngle &vecAngles, edict_t *pentOwner );

    void CreateDangerSounds( bool bState ){ m_bCreateDangerSounds = bState; }


After you've completed this part, you should be all done! Compile the build and test the changes to your mod!

From this point, to get the exact formula you want for how affected the rocket is by gravity, you should modify the RPG_SPEED value and the float value that is passed into the SetGravity( float ) function in the CMissile::Spawn( void ) method.

Note.pngNote:You might have to do a complete rebuild for the compiler to effectively detect the changes you made to the DATADESC section!