Ads keep us online. Without them, we wouldn't exist. We don't have paywalls or sell mods - we never will. But every month we have large bills and running ads is our only way to cover them. Please consider unblocking us. Thank you from GameBanana <3

Better view bobbing - A Tutorial for Half-Life.

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.

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 something like 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?

Before we make our own view bobbing, we gotta understand the variables behind this stuff first.

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).

Now, this is all nice, but what if you want to bob it right and up, kinda like the view bob in Doom, Far Cry 1, HL2 etc.? In the form of an infinity shape, basically.

The plan is to have two waves, one for up, and the other one for right.
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, our viewmodel is moving diagonally now. :/
Let's fix this.

1.3. Separate bobs for each direction

We are still using only one wave, which is the bob variable. If we want to have different cycles for right and up, we need to somehow modify the V_CalcBob function, since that is the function that calculates the current value for the bob.

'bob' is the variable that changes over time, and it behaves like a wave. As it goes up'n'down over time, your viewmodel will go forward-backward.

If we have, say, bob_up and bob_right, planning for each to have a separate frequency, we could use a V_CalcBobUp and V_CalcBobRight, but this is a WRONG approach.
Right now, if we called the same V_CalcBob for them, the bobs themselves would be the same. This is why we are going to modify V_CalcBob, to allow us to handle multiple different cycles.

Notice the static variables inside V_CalcBob:
float V_CalcBob ( struct ref_params_s *pparams )
static double bobtime;
static float bob;
float cycle;
static float lasttime;
vec3_t vel;

This is pretty much the source of the problem. Since the variables are static, they will "remember" their values after the function ends, and when we call V_CalcBob again for another variable, it's gonna treat it like it's the same one. In other words, V_CalcBob was designed for only one cycle.

We will modify V_CalcBob in a way that we will send bobtime, bob and lasttime as parameters. The bob parameter will be an output parameter for our wave/cycle. Also, the function will no longer return a float. Instead, it'll be a void.

The function will start like this:
void V_CalcBob ( struct ref_params_s *pparams, float freqmod, calcBobMode_t mode, double &bobtime, float &bob, float &lasttime )
float cycle;
vec3_t vel;

Since V_CalcBob will now support cos, sin and their square variants, I added a calcBobMode_t variable as a parameter. Let's add an enum for that above the function:
enum calcBobMode_t

Now, on the end of V_CalcBob, write the following:
	bob = sqrt( vel[0] * vel[0] + vel[1] * vel[1] ) * cl_bob->value;

if ( mode == VB_SIN )
bob = bob * 0.3 + bob * 0.7 * sin( cycle );
else if ( mode == VB_COS )
bob = bob * 0.3 + bob * 0.7 * cos( cycle );
else if ( mode == VB_SIN2 )
bob = bob * 0.3 + bob * 0.7 * sin( cycle ) * sin( cycle );
else if ( mode == VB_COS2 )
bob = bob * 0.3 + bob * 0.7 * cos( cycle ) * cos( cycle );

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


Since the function is now a void, it can't return the bob. But, because bob is now a parameter by reference, we don't have to worry about returning anything.

You've also noticed the 'freqmod' parameter. This is what we will use to have different frequencies for the separated view bob variables.

The entire V_CalcBob function now looks like this:
void V_CalcBob ( struct ref_params_s *pparams, float freqmod, calcBobMode_t mode, double &bobtime, float &bob, float &lasttime )
float cycle;
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;
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 == VB_SIN )
bob = bob * 0.3 + bob * 0.7 * sin( cycle );
else if ( mode == VB_COS )
bob = bob * 0.3 + bob * 0.7 * cos( cycle );
else if ( mode == VB_SIN2 )
bob = bob * 0.3 + bob * 0.7 * sin( cycle ) * sin( cycle );
else if ( mode == VB_COS2 )
bob = bob * 0.3 + bob * 0.7 * cos( cycle ) * cos( cycle );

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

1.4. Using bobs with multiple directions

V_CalcNormalRefdef now must be changed.
Somewhere in that function, bob gets assigned to V_CalcBob, but since we changed V_CalcBob to a void, it will result in a compiler error.
Other than that, inside V_CalcNormalRefDef, we'll also have to declare 3 bobtimes and 3 lasttimes, since they used to be static variables in the old V_CalcBob.

Let's begin, at the start of V_CalcNormalRefdef:
void V_CalcNormalRefdef ( struct ref_params_s *pparams )
cl_entity_t *ent, *view;
int i;
vec3_t angles;
float bobRight = 0, bobUp = 0, bobForward = 0, waterOffset;
static viewinterp_t ViewInterp;

static float oldz = 0;
static float lasttime;

static double bobtimes[ 3 ] = { 0,0,0 };
static float lasttimes[ 3 ] = { 0,0,0 };

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

bobRight, bobUp and bobForward will be our bob variables, if that's not obvious.

Go to the line where you see bob = V_CalcBob(... and replace it with this:
// transform the view offset by the model's matrix to get the offset from
// model origin for the view
V_CalcBob( pparams, 0.75f, VB_SIN, bobtimes[0], bobRight, lasttimes[0] ); // right
V_CalcBob( pparams, 1.50f, VB_SIN, bobtimes[1], bobUp, lasttimes[1] ); // up
V_CalcBob( pparams, 1.00f, VB_SIN, bobtimes[2], bobForward, lasttimes[2] ); // forward

So now you can see how this is actually going to work. We have an array for bobtimes and lasttimes. We could have an array for bobs as well, but this works too.
When V_CalcBob does its thing, it's gonna update bobtimes, lasttimes and all 3 of the bobs separately, instead of treating them as one thing.

Let's update the for loop that applies the bobbing:
for ( i = 0; i < 3; i++ )
view->origin[i] += bobRight * 0.5 * pparams->right[i];
view->origin[i] += bobUp * 0.25 * pparams->up[i];
view->origin[i] += bobForward * 0.125 * pparams->forward[i];
And that's about it. The rest is tweaking these values.
So, we now got an idea how to manipulate the viewmodel angles. But what about the player's view camera?

1.5. 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;
Instead of bob, let's write bobRight, bobUp and bobRight respectively.
view->angles[YAW]   -= bobRight * 0.8;
view->angles[ROLL] -= bobUp * 0.8;
view->angles[PITCH] -= bobRight * 1.2;
However, this isn't actually the view camera. The variable view is actually used for the viewmodel.

To control our actual viewing camera, you can add this right under:
pparams->viewangles[ROLL]  += bobRight * 0.15;
pparams->viewangles[PITCH] += bobRight * 0.55;
In-game, you'll get a small up'n'down leaning when you move. It'd be like if you moved your mouse slowly back and forth.
I generally wouldn't recommend you to manipulate the view camera, unless you want to cause headaches and motion sickness for your players.

This is pretty much the end of the tutorial. If you weren't able to follow along, there's a GitHub repository containing all the source code of this tutorial. Alternatively, ask me in the comments if you need help.

Keep in mind, however, that you can do so much more than just this. We could add linear interpolation, 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 a small example of what you can do:

It is up to you, for now, to play around, and discover how some of that is done.
I'll very likely write a part 2 of the tutorial, where I'll talk more about the maths of view bobbing (nothing complicated), and how to achieve certain effects from that GIF up there.

All source code and compiled binaries can be found at the GitHub repo here.

Happy coding. :)
1-10 of 31
Go to page: