  A few days ago in the Sauerworld Discord (join here), we talked about the chaingun (aka minigun, aka machine gun) and shotgun damage and how their rays spread around your crosshair. Games like Counter-Strike aim for realistic projectile spread induced by the weapon’s recoil: your crosshair (and with it, projectile vectors) tend to move upwards the longer you burst-fire, and players learn to correct for it by moving the crosshair down. Some weapons in some games also have projectiles spreading outwards around your crosshair, with the spread usually increasing the longer you hold the trigger.

Sauerbraten, in contrast to most other shooter games you may know, has pretty basic projectile spread mechanics. And since Sauer is open-source, we can take a look behind the scenes to understand the intricacies of those mechanics! The following C++ code is taken directly from Sauerbraten’s source code (`src/fpsgame/weapon.cpp`):

``````void offsetray(const vec &from, const vec &to, int spread, float range, vec &dest)
{
vec offset;
do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f);
while(offset.squaredlen() > 0.5f*0.5f);
offset.z /= 2;
if(dest != from)
{
vec dir = vec(dest).sub(from).normalize();
raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY);
}
}``````

Some things to note before we dive into this function:

• the function calculates the offset for a single ray (from the straight line, which would be used for example a rifle projectile)
• it’s the only function to calculate ray offset (= projectile spread) in the source code, which means the overall spread calculation is the same for all weapons that have spreading projectiles (currently, chaingun, shotgun and pistol)
• however, it takes a `spread` argument, so the output is not neccessarily the same for all weapons
• the function is called with the same `spread` argument for every ray of a weapon, meaning chaingun spread does not increase with time

So what does the code do, exactly? Let’s begin by looking at the function’s input (its arguments) and output:

The first two arguments tell the function `from` where `to` where in the map the player is shooting. Then there are the `spread` and `range` arguments, which are taken from the weapon’s defined settings. (These weapon settings are set in the source code, and can’t be changed in-game. Other weapon settings would be how much damage a ray deals and how long it takes to reload.) The last argument, `dest`, is a variable that will hold the destination of the offset ray after the function ran and is actually the output of the function. (In other programming languages, this would be the function’s return value.)

From a high-level point of view, the function calculates a single ray’s destination vector by preparing an offset vector which it adds to the vector pointing from the player to the target, i.e. offsetting the ray from the vector connecting `from` and `to`. It returns the vector pointing from `from` to the offset target as the `dest` vector.

Let’s go through the function step-by-step:

``````vec offset;
do offset = vec(rndscale(1), rndscale(1), rndscale(1)).sub(0.5f);
while(offset.squaredlen() > 0.5f*0.5f);``````

The first three lines prepare a vector variable with three coordinates (x, y and z) and try random values between -0.5 and 0.5 for each coordinate, until it finds a vector where x2 + y2 + z2 is greater than 0.25. This basically means the offset vector can point in any direction, but its magnitude is limited to a sphere of radius 0.5. Although this explicitly prevents the case that x2 + y2 + z2 = 0, it does not mean that this function will never produce a ray that goes exactly straight: the offset vector might point parallel to the direction of the shot, so offsetting the ray will only make it point behind or in front of the original target! You might get lucky and get a straight shot even with your chaingun!

``offset.mul((to.dist(from)/1024)*spread);``

The next line makes it so that long range shots are offset more than short range ones. It uses the shot distance (`to.dist(from)`), scales it by a magic factor of 1/1024, and then scales it again by the weapon’s spread setting (currently 100 for chaingun, 400 for shotgun, 50 for pistol). The entire offset vector is then scaled by the result of all that scaling of the shot distance.

``offset.z /= 2;``

This line is very interesting: `z` is the up-down axis in Sauer (if you jump, your z coordinate increases, if you fall down like a noob on reissen, it decreases). The `/= 2` bit means the z component is halved. We will get back to what this means for us later!

``dest = vec(offset).add(to);``

This part simply defines `dest` as the position where the offset ray ends (for now), by adding the offset vector to the position vector of the target of the shot.

``````if(dest != from)
{
...
}``````

The next bit ensures that the calculated ray doesn’t end where it starts, for reasons I am not sure why. It might have to do with Sauer’s spawn kill protection, but it’s really just my best guess here. For simplicity, let’s assume that will never be the case, so the code inside the braces will be executed next.

``````vec dir = vec(dest).sub(from).normalize();
raycubepos(from, dir, dest, range, RAY_CLIPMAT|RAY_ALPHAPOLY);``````

The last two lines of code move the destination (= end) point of the ray along the ray until it collides with something in the world, for example the wall or (ideally) an enemy’s player model. This is done by calculating a normalized vector `dir` of the ray’s direction from its start (`from`) and end (`dest`) vectors, and then relying on the engine to set `dest` to the point where this vector `dir`, starting at `from` intersects with something that would stop a projectile. Essentially, this makes sure the ray doesn’t end in front of the player or goes through her model without hitting or has the ray end somewhere beside the player in the middle of the air.

Now back to why `offset.z /= 2` is so interesting here: For you as a player, this line means shots are more accurate when you are at the same height as your target!

If you’re not sure why, think about the sphere of possible offset vectors around the target: when the offset vector’s z component is reduced by the `/= 2` operation, the height of the sphere of possible offsets around the target is reduced, so it’s no longer a sphere, really, but more of a pumpkin! At the very end, what matters is the 2D projection of this pumpkin towards the players camera (since the depth component of the offset vector in relation to the player’s camera is irrelevant [the end point of the ray is recalculated after offsetting the shot]). Seen from eye level (that is, perfectly horizontal), the surface area of the sphere of possible offset vectors got smaller by compressing it along the z-axis, but seen from above, it’s still the same size! So the more “from above” a player’s perspective onto the target is, the less they benefit from this height compression of the possible offset vectors. The greater the difference in z-height between the start and end of the shot (i.e. the player and their target), the less likely a ray is to be close enough in the center to count as a hit!

## 1 Comment

1. Forsty

“You might get lucky and get a straight shot even with your chaingun!” :D

I hadn’t ever thought of how spread+hitscan weps could be more accurate based on the height discrepancy, although I wonder how significant it would really be in applicable game situations.

Nice article