Better view bobbing

A Tutorial for Half-Life

Members see zero ads. Membership is 100% free

Bring back HL WON view bobbing + add your own!

1. Better view bobbing

If you're old enough to remember HL WON, you may remember that before version 1.1.x.x, there was a view bob that was more than just forward and backward.

NOTE:
Do NOT replace your vanilla Half-Life client.dll. I repeat, DO NOT do this. This tutorial is meant for custom mods. Thank you. :)


In this tutorial, we'll bring that back, and make it even better. 
So, let's get started!

Open Visual Studio or whatever IDE you use, and in cl_dll, find view.cpp.
Scroll down to line 490, at the function V_CalcNormalRefdef.

This function's mission is to do a couple of things.
  1. Grab player's view (position and angles)
  2. Grab player's viewmodel
  3. If it's in water, either raise or lower the view
  4. Calculate the view bob, the roll and similar
  5. Apply the calculated variables
  6. Smooth out the view, in multiplayer, when on trains and lifts
It does more than that, but for you, the 4th part is what's important.

In the end of the tutorial, we'll have this:


1.1. Enabling HL WON view bobbing

Firstly, we need to bring back the old view bobbing.
Go to line 664, and paste this:
// Enables old HL WON view bobbing
VectorCopy( view->angles, view->curstate.angles );

Compile the client DLL, and copy it to your mod. If you launch it, you'll see that our old view bobbing is back! However, it looks a bit cheesy, doesn't it?

We gotta understand the variables behind this stuff first, though.

Essentially, there's a cycle behind all of this. That cycle represents some angle, and the view bobbing variables are sines and cosines of that angle.

The variable "view" is actually our viewmodel clientside entity. So its angles and origin can be different than the player's 'camera'. I'll show you later how to manipulate the camera itself, but for now, let's stick to the viewmodel.

Let's take a look at view's most important variables.

Entity state 'curstate' -> angles, origin etc. received from the server
Vector 'origin' -> position
Vector 'angles' -> angles

These entity states are essentially structures that are used for entity packets sent to the client, by the server. Modifying these on the client side, once they are received, we can easily manipulate our view.
You may find it similar to the PEV structure, as it contains origin, angles, skin, solid, effects, and so on.

Changing curstate's values means changing the current view. So, let's do a little experiment.
Let's change some of the variables.

1.2. Messing with the view bob further

Add the following code right under line 658 (which is view->origin[2] += bob):
view->origin[1] += 2 * bob;

It looks very weird in-game, but, what we essentially did here was shifting the viewmodel's origin on the Y axis depending on the bob. Let's remove that line.

Instead, let's see right above:
for ( i = 0; i < 3; i++ )
{
view->origin[ i ] += bob * 0.4 * pparams->forward[ i ];
}

Let's change pparams->forward to pparams->right.

So far, we've moved our origin in world coordinates. However, pparams' vectors are local (up, forward, right).

The plan is to have two waves, one for up, and the other one for right, so we get a nice Doom-like bobbing.

So, replace that for loop with the following two:
for ( i = 0; i < 3; i++ )
{
view->origin[ i ] += bob * 0.4 * pparams->right[ i ];
view->origin[ i ] += bob * 0.4 * pparams->up[ i ];
}

However, now you may notice that our viewmodel is moving diagonally.

We are still using only one wave. For each new bob variable, we'll need another V_CalcBob function. This is because the internal variables of V_CalcBob are static, and they are 'remembered' after each function call. 

If we have, say, bob_up and bob_right, planning for each to have a separate frequency, we'd have to use a V_CalcBobUp and V_CalcBobRight. Otherwise, if we called the same V_CalcBob for them, the bobs themselves would be the same. Or even worse, the cycle would get messed up.

NOTE:
Do NOT copy-paste whole functions that basically do the same thing. This is one of those cases where you simply don't have another easy option. While other workarounds are available, they would prolong this tutorial and make it unnecessarily more complex - as the point of this tutorial is to give some insight into the mechanics of view bobbing itself.


So, we'll actually have 3 bobs. One for right, one for up and one for forward, in case you need it.

Go to V_CalcBob, and make some modifications to it.
float V_CalcBob ( struct ref_params_s *pparams, float freqmod = 1.0f, int mode = 1 )

freqmod = frequency multiplier/modifier
mode = sine or cosine, 1 means sine, everything else means cosine (we'll need cosines)

Inside of this function, make a few changes.
At this line:
bobtime += pparams->frametime;
Replace it with:
bobtime += pparams->frametime * freqmod;

At this line:
bob = bob * 0.3 + bob * 0.7 * sin(cycle);
Replace with:
if(mode)
bob = bob * 0.3 + bob * 0.7 * sin(cycle);
else
bob = bob * 0.3 + bob *0.7 * cos(cycle);

Now, select the entire V_CalcBob function. We'll make V_CalcBob2 and V_CalcBob3. All you gotta do is copy-paste V_CalcBob and rename the functions.

Then, back to V_CalcNormalRefdef, we go down to:
bob = V_CalcBob( pparams );

We'll replace it with the following:
bob = V_CalcBob( pparams, 0.75f, 0 );
bob2 = V_CalcBob( pparams, 1.50f, 1 );
bob3 = 0; // can just be V_CalcBob3( pparams );

Bob number 1 essentially cycles half as fast as bob number 2. Also, bob number 2 is phase-shifted 90° in front of bob number 1. What this means is that you'll get a movement path similar to the infinity symbol.

Now, back to the for loop:
for ( i = 0; i < 3; i++ )
{
view->origin[ i ] += bob  * 0.5  * pparams->right[ i ];
view->origin[ i ] += bob2 * 0.25  * pparams->up[ i ];
view->origin[ i ] += bob3 * 0.125 * pparams->forward[ i ];
}

1.3. All the code

And that's about it. The rest is tweaking these values. 
In case you missed something, here's all the code:
First half of V_CalcNormalRefdef:
void V_CalcNormalRefdef ( struct ref_params_s *pparams )
{
	cl_entity_t		*ent, *view;
	int				i;
	vec3_t			angles;
	float			bob, bob2, bob3, waterOffset;
	static viewinterp_t		ViewInterp;

	static float oldz = 0;
	static float lasttime;

	vec3_t camAngles, camForward, camRight, camUp;
	cl_entity_t *pwater;

	V_DriftPitch ( pparams );

	if ( gEngfuncs.IsSpectateOnly() )
	{
		ent = gEngfuncs.GetEntityByIndex( g_iUser2 );
	}
	else
	{
		// ent is the player model ( visible when out of body )
		ent = gEngfuncs.GetLocalPlayer();
	}
	
	// view is the weapon model (only visible from inside body )
	view = gEngfuncs.GetViewModel();

	// transform the view offset by the model's matrix to get the offset from
	// model origin for the view
	bob  = V_CalcBob ( pparams, 0.750f, 0 ); // right
	bob2 = V_CalcBob2( pparams, 1.500f, 1 ); // up
	bob3 = V_CalcBob3( pparams, 1.000f, 1 ); // forward

	// refresh position
	VectorCopy ( pparams->simorg, pparams->vieworg );
	pparams->vieworg[2] += ( bob );
	VectorAdd( pparams->vieworg, pparams->viewheight, pparams->vieworg );

	VectorCopy ( pparams->cl_viewangles, pparams->viewangles );

	gEngfuncs.V_CalcShake();
	gEngfuncs.V_ApplyShake( pparams->vieworg, pparams->viewangles, 1.0 );

	// never let view origin sit exactly on a node line, because a water plane can
	// dissapear when viewed with the eye exactly on it.
	// FIXME, we send origin at 1/128 now, change this?
	// the server protocol only specifies to 1/16 pixel, so add 1/32 in each axis
	
	pparams->vieworg[0] += 1.0/32;
	pparams->vieworg[1] += 1.0/32;
	pparams->vieworg[2] += 1.0/32;

	// Check for problems around water, move the viewer artificially if necessary 
	// -- this prevents drawing errors in GL due to waves

	waterOffset = 0;
	if ( pparams->waterlevel >= 2 )
	{
		int		i, contents, waterDist, waterEntity;
		vec3_t	point;
		waterDist = cl_waterdist->value;

		if ( pparams->hardware )
		{
			waterEntity = gEngfuncs.PM_WaterEntity( pparams->simorg );
			if ( waterEntity >= 0 && waterEntity < pparams->max_entities )
			{
				pwater = gEngfuncs.GetEntityByIndex( waterEntity );
				if ( pwater && ( pwater->model != NULL ) )
				{
					waterDist += ( pwater->curstate.scale * 16 );	// Add in wave height
				}
			}
		}
		else
		{
			waterEntity = 0;	// Don't need this in software
		}
		
		VectorCopy( pparams->vieworg, point );

		// Eyes are above water, make sure we're above the waves
		if ( pparams->waterlevel == 2 )	
		{
			point[2] -= waterDist;
			for ( i = 0; i < waterDist; i++ )
			{
				contents = gEngfuncs.PM_PointContents( point, NULL );
				if ( contents > CONTENTS_WATER )
					break;
				point[2] += 1;
			}
			waterOffset = (point[2] + waterDist) - pparams->vieworg[2];
		}
		else
		{
			// eyes are under water.  Make sure we're far enough under
			point[2] += waterDist;

			for ( i = 0; i < waterDist; i++ )
			{
				contents = gEngfuncs.PM_PointContents( point, NULL );
				if ( contents <= CONTENTS_WATER )
					break;
				point[2] -= 1;
			}
			waterOffset = (point[2] - waterDist) - pparams->vieworg[2];
		}
	}

	pparams->vieworg[2] += waterOffset;
	
	V_CalcViewRoll ( pparams );
	
	V_AddIdle ( pparams );

	// offsets
	VectorCopy( pparams->cl_viewangles, angles );

	AngleVectors ( angles, pparams->forward, pparams->right, pparams->up );

	// don't allow cheats in multiplayer
	if ( pparams->maxclients <= 1 )
	{
		for ( i=0 ; i<3 ; i++ )
		{
			pparams->vieworg[i] += scr_ofsx->value*pparams->forward[i] + scr_ofsy->value*pparams->right[i] + scr_ofsz->value*pparams->up[i];
		}
	}
	
	// Treating cam_ofs[2] as the distance
	if( CL_IsThirdPerson() )
	{
		vec3_t ofs;

		ofs[0] = ofs[1] = ofs[2] = 0.0;

		CL_CameraOffset( (float *)&ofs );

		VectorCopy( ofs, camAngles );
		camAngles[ ROLL ]	= 0;

		AngleVectors( camAngles, camForward, camRight, camUp );

		for ( i = 0; i < 3; i++ )
		{
			pparams->vieworg[ i ] += -ofs[2] * camForward[ i ];
		}
	}
	
	// Give gun our viewangles
	VectorCopy ( pparams->cl_viewangles, view->angles );
	
	// set up gun position
	V_CalcGunAngle ( pparams );

	// Use predicted origin as view origin.
	VectorCopy ( pparams->simorg, view->origin );      
	view->origin[2] += ( waterOffset );
	VectorAdd( view->origin, pparams->viewheight, view->origin );

	// Let the viewmodel shake at about 10% of the amplitude
	gEngfuncs.V_ApplyShake( view->origin, view->angles, 0.9 );

	for ( i = 0; i < 3; i++ )
	{
		view->origin[i] += bob  * 0.5 * pparams->right[i];
		view->origin[i] += bob2 * 0.25 * pparams->up[i];
//		view->origin[i] += bob3 * 0.125 * pparams->forward[i];
	}

	view->origin[2] += bob;

	// throw in a little tilt.
	view->angles[YAW]   -= bob * 0.8;
	view->angles[ROLL]  -= bob * 0.8;
	view->angles[PITCH] -= bob * 0.8;

	// Enables old HL WON view bobbing
	VectorCopy( view->angles, view->curstate.angles );
I'm not gonna paste the entire function, as nothing was changed after this last line.

V_CalcBob functions, since they're all the same, I'll paste V_CalcBob3:
// freqmod = frequency multiplier/modifier, mode: 1 = sin, 0 = cos
float V_CalcBob3(struct ref_params_s *pparams, float freqmod = 1.0f, int mode = 1 )
{
	static	double	bobtime;
	static float	bob;
	float	cycle;
	static float	lasttime;
	vec3_t	vel;


	if (pparams->onground == -1 ||
		pparams->time == lasttime)
	{
		// just use old value
		return bob;
	}

	lasttime = pparams->time;

	bobtime += pparams->frametime * freqmod;
	cycle = bobtime - (int)(bobtime / cl_bobcycle->value) * cl_bobcycle->value;
	cycle /= cl_bobcycle->value;

	if (cycle < cl_bobup->value)
	{
		cycle = M_PI * cycle / cl_bobup->value;
	}
	else
	{
		cycle = M_PI + M_PI * (cycle - cl_bobup->value) / (1.0 - cl_bobup->value);
	}

	// bob is proportional to simulated velocity in the xy plane
	// (don't count Z, or jumping messes it up)
	VectorCopy(pparams->simvel, vel);
	vel[2] = 0;

	bob = sqrt(vel[0] * vel[0] + vel[1] * vel[1]) * cl_bob->value;

	if (mode)
		bob = bob * 0.3 + bob * 0.7 * sin(cycle);
	else
		bob = bob * 0.3 + bob * 0.7 * cos(cycle);

	bob = V_min(bob, 4);
	bob = V_max(bob, -7);
	return bob;

}

So, we now got an idea how to manipulate the viewmodel angles. But what about the player's view camera?

1.4. View 'camera' swaying

The answer lies in the pparams structure. Precisely, pparams->viewangles.
So, let's go to this line:
// throw in a little tilt.
view->angles[YAW]   -= bob * 0.8;
view->angles[ROLL]  -= bob * 0.8;
view->angles[PITCH] -= bob * 0.8;
Let's add this right under:
pparams->viewangles[ROLL]  += bob * 0.15;
pparams->viewangles[PITCH] += bob * 0.55;
In-game, you'll get a small up'n'down leaning when you move.

This is pretty much the end of the tutorial. Keep in mind, however, that you can do so much more than just this. We could add lerping, we could push the weapon naturally up and down if we jumped. We could have a different view sway underwater, and we could make it sway quicker depending on the speed at which the player moves. Hell, you can even try to do what Move In! does, a free aim!

Here's what you can do:

It is up to you, for now, to play around, and discover how some of that is done.
If you want a part 2 with more features, write that to me. 

Happy coding. :)
1-10 of 13
1
Pages
  • 1
  • 2
Go to page:

Embed

menu
Share banner
Image URL
HTML embed code
BB embed code
Markdown embed code

Credits

Key Authors
Admer456 avatar
Admer456 username pic Joined 4y ago
Offline
22,449 points Ranked 224th
30 medals 1 legendary 5 rare
  • Submitted 30 Tutorials Medal icon
  • Submitted 15 Tutorials Medal icon
  • Returned 1000 times Medal icon
  • Reached 50 subscribers Medal icon
  • Received thanks 50 times Medal icon
  • 10 submissions featured Medal icon
Writer
Special Thanks
Shepard62700FR
Shepard62700FR Joined 11y ago
Offline
862 points Ranked 41491st
6 medals 1 rare
  • 6 years a member Medal icon
  • One month a member Medal icon
  • 6 months a member Medal icon
  • 1 year a member Medal icon
  • 2 years a member Medal icon
  • 4 years a member Medal icon
How to enable HL WON bobbing

Submitter

Admer456 avatar
Admer456 username pic Joined 4y ago
Gone. :3
22,449 points Ranked 224th
30 medals 1 legendary 5 rare
  • Submitted 30 Tutorials Medal icon
  • Submitted 15 Tutorials Medal icon
  • Returned 1000 times Medal icon
  • Reached 50 subscribers Medal icon
  • Received thanks 50 times Medal icon
  • 10 submissions featured Medal icon
Admer456 avatar
Admer456

Creator
Sign up to access this!
Sign up to access this!
Sign up to access this!

Game

Sign up to access this!

Category

Details

Difficulty Level
Beginner

Attributes

Share

  • Share on Reddit
  • Share on Twitter
  • Share on Facebook
  • Best of the Banana
    Featured on Jun 29 2019
  • Best of Yesterday
    Featured on Jun 28 2019
  • Today's Pick
    Featured on Jun 27 2019
  • favorite 26
  • remove_red_eye 2.1k
  • mode_comment 25
  • access_time 24d
  • access_time 11d

More from Submitter

menu

WiPs by Submitter

menu

More Programming Tutorials

bcp.crwdcntrl.net tracking pixel