The GoldSrc Collison System

Now if you walk up to your entity hoping for it to present a realistic obstacle you will be sorely disappointed. We haven’t yet added code to utilize GoldSrc’s Collision system. The collision system in GoldSrc is based on the concept of AABB which stands for Axis Aligned Bounding Box. This means that all bounding boxes are locked to the orientation of the three axis of the world. Basically the bounding boxes cannot rotate in the same direction that the model is facing.



I assume this approach was used at the time for lack of a better solution and it is cheaper than other methods such as OOBB (Object oriented Bounding boxes). The following images demonstrate AABB better.

This shows a model which has no rotations on its local axis. The bounding box fits a little more naturally than below. For an Object Aligned Bounding Box system it would look the same since the model has no local rotation.

Here the Model has now been rotated about 45 degrees. The bounding box for an Axis Aligned system simply grows to encapsulate the whole model.

Here is what would happen on an OOBB system. The bounding box would follow the orientation of the model and would not stay locked to the world, and as a result it wouldn’t grow like it does in the AABB solution. This would be a nice system to have in GoldSrc but unfortunately it is not implemented and for the purposes of my mod it wouldn’t warrant an attempt to implement it. So for now we are stuck with AABB Collisions.



From what I have observed and learned of GoldSrc’s collision system is it depends greatly on how the entity that loads the model handles the size of the collision box. Either it will take the sizes set by the animation loaded by the model (if any) or you can set a size through code.



Rotations are going to cause an issue simply because the bounding volumes cannot be rotated in an AABB system. This means that if we want our model to be completely encapsulated in a bounding volume it will be a very inaccurate representation of the already inaccurate collision box. To combat that I suggest we set a manual bounding volume for any static meshes we place and that we reduce the size and position of this box to underlap the model itself. Some clipping will occur in some cases but it would provide a better collision volume. We will explore this further on in the tutorial using the Xen Tree as our example model.



In our case what we will do is provide the user with an option to load the bounding box from the models currently running animation sequence which it will get from the mdl file itself or the user can manually enter a static size in hammer.

The GoldSrc Collison System

Let’s start by simply setting a hard coded size in Visual Studio and viewing the result in-game. The Function that sets the Collision volume for a model in GoldSrc is:

UTIL_SetSize(Entity, Vec3 Mins, Vec3 Maxs)

It accepts a reference to the entity whose collision size you are setting, as well as two Vector 3 Objects for the Minimum XYZ Position and the Maximum XYZ Position of the Collision box. In our Spawn Function lets add the following:

pev->solid = SOLID_BBOX; UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 32)); // Mins X Y Z Maxs X Y Z

pev->solid must be set to SOLID_BBOX otherwise we will still be able to walk through it even with UTIL_SetSize() set. We will make this a selectable Flag in Hammer for those who want to clip static meshes.



Other Flags this can be set to include:

#define SOLID_NOT 0 // no interaction with other objects #define SOLID_TRIGGER 1 // touch on edge, but not blocking #define SOLID_BBOX 2 // touch on edge, block #define SOLID_SLIDEBOX 3 // touch on edge, but not an onground #define SOLID_BSP 4 // bsp clip, touch on edge, block

For the UTIL_SetSize() function note that you can set the mins to a negative value. Compile the Code and enter your test level once again. You should notice that when you try to walk through your mesh the player steps up slightly. That is because we made a square 64W * 64L * 32H at the models origin. We set the mins Z to 0 to avoid the volume clipping downwards through the worlds ground.



To briefly explain how the min and max values are used to create a bounding volume, see the following description.

UTIL_SetSize(pev, Vector(-32, -32, -32), Vector(32, 32, 32));

Bounding boxes for models in GoldSrc are bound to the local position of the model. So in the case above where we provide Mins of -32, -32, -32 that means -32 units on all axis from the models local 0,0,0 position, not the models world position. The Maxs are 32, 32, 32 on all axis which means +32 on all axis from the models local position.

UTIL_SetSize(pev, Vector(0, 0, 0), Vector(64, 64, 64));

Here we set the Mins to 0, 0, 0 which is the same as the models local position and the Maxs to 64, 64, 64 along each axis. This in most cases (unless the model is offset) will give for an undesirable amount of collision coverage as only a quarter of the model (depending on the models shape) would be covered.



GoldSrc then takes the Mins and Maxs and constructs a Cube from the given value.

The GoldSrc Collison System

Currently we cannot see the bounding box but let’s change that by creating the UTIL_RenderBBox() function.



Add the following function to our Util.h file:

// Foo BBox Rendering void UTIL_RenderBBox(Vector origin, Vector mins, Vector maxs, int life, BYTE r = 0, BYTE b = 0, BYTE g = 0);

And then add the function to the end of util.cpp

void UTIL_RenderBBox(Vector origin, Vector mins, Vector maxs, int life, BYTE r, BYTE b, BYTE g) { //********************Render boundrybox************************** MESSAGE_BEGIN(MSG_BROADCAST, SVC_TEMPENTITY); WRITE_BYTE(TE_BOX); // coord, coord, coord boxmins WRITE_COORD(origin[0] + mins[0]); WRITE_COORD(origin[1] + mins[1]); WRITE_COORD(origin[2] + mins[2]); // coord, coord, coord boxmaxs WRITE_COORD(origin[0] + maxs[0]); WRITE_COORD(origin[1] + maxs[1]); WRITE_COORD(origin[2] + maxs[2]); WRITE_SHORT(life); // short life in 0.1 s (1min) WRITE_BYTE(r); // r, g, b WRITE_BYTE(g); // r, g, b WRITE_BYTE(b); // r, g, b MESSAGE_END(); // move PHS/PVS data sending into here (SEND_ALL, SEND_PVS, SEND_PHS) }

After that, we will need an update function which GoldSrc presents to us through the Think Function.



Note: If you want your model to update on a frame by frame (or a custom amount) basis you will need a Think function. We can set this function using the SetThink(&ReferenceToCustomThinkFunction). We will be setting Animate(void) as the Think Function for our Mesh Loader Class.



Add the Animate function to the header file.

class CStaticMesh : public CBaseAnimating { private: void Spawn(void); void EXPORT Animate(void); };

Note: If I remember correctly the EXPORT Macro is used to export the symbols to the DLL such that the game can query its state between save games. Functions that use this are SetThink, SetUse, SetTouch, SetBlocked. When we save a game in GoldSrc, the Engine is queried for the symbolic name for these functions, when we load the save game the Engine simply has to look up this symbolic name and restore the state.

Let’s add this Animate function to the source CPP file. Simply add it after void CStaticMesh::Spawn(void)

void CStaticMesh::Animate(void) { pev->nextthink = gpGlobals->time + 0.01; UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); }

pev->nextthink is basically a time in the future when you will call the Think Function again for this Class which in this case is Animate. We set pev->nextthink to the current time plus 0.01. Adding a larger number to the end would mean that the animate function would be re-called less frequently and the opposite for an even smaller number.



The function we added earlier will provide a visual representation of our bounding box.



UTIL_RenderBBox(Entity, Mins, Max’s, Lifetime, ColorR, ColorB, ColorG);



This function accepts a reference to our Entity, The Mins and Maxs of the box you want to render (we provide the same mins and maxs we gave the Bounding Box).



The Lifetime controls how long the representation renders for. (doesn’t seem to work as expected) The last three parameters control the RBG values of the visualization (yes that’s right RBG not RGB)



We have one further change to make to our Spawn() function in order to enable our Think function. Add the following at the end of the Spawn function:

SetThink(&CStaticMesh::Animate); pev->nextthink = gpGlobals->time + 0.01;

Here we set our local Animate function to be used as the Think Function by the Engine. Again we set the next think slightly into the future. In this case it is only called once as it’s the spawn function, The Animate function will henceforth handle all updates. Compile your code and run the game. You should see something like this:

Now if I was to change the Mins in the UTIL_SetSize to 0,0,0 like so:

UTIL_SetSize(pev, Vector(0, 0, 0), Vector(32, 32, 32));

Observe the affect it has on the Collision Volume:

t may look like the bounding volume is rendering down the negative axis but in fact recall that we had rotated our model 180 degrees. The bounding volume is not at all affected by the rotation of the Mesh. So with that in mind notice that the Mins are positioned exactly on the pivot or root of this model. You will need to keep in mind that if you want your model encapsulated by your collision mesh set at least the X and Y values of the Mins Vector to the negative version of the Max’s X and Y Vector. A suitable Value I see for this particular Model is the following:

UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 190));

The next step is to make this currently static function a little more modular so that the user can set the Min and Max values in Hammer and have those values used on object spawn.

Manually set Collision Volume

Let’s add the following two lines to our Header:

void KeyValue(KeyValueData *pkvd); Vector mins, maxs;

The Header should now look like this:

class CStaticMesh : public CBaseAnimating { private: void Spawn(void); void EXPORT Animate(void); void KeyValue(KeyValueData *pkvd); Vector mins, maxs; };

KeyValue will be the function to read specific non standard elements from the compiled Map for use in our entities code. We also declare Vector variables for our min and max values so that we can modify and set them between functions in our class. Next we must modify our Spawn function once again.



We must replace our UTIL_SetSize() Function

UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 190));

with:

UTIL_SetSize(pev, maxs, mins);

The whole Spawn function should now look like this:

void CStaticMesh::Spawn(void) { PRECACHE_MODEL((char *)STRING(pev->model)); SET_MODEL(ENT(pev), STRING(pev->model)); pev->solid = SOLID_BBOX; UTIL_SetSize(pev, mins, maxs); SetThink(&CStaticMesh::Animate); pev->nextthink = gpGlobals->time + 0.01; }

Next up let’s make the KeyValue Function. You can add it anywhere in your CPP file.

void CStaticMesh::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "bbmins")) { UTIL_StringToVector(mins, pkvd->szValue); pkvd->fHandled = TRUE; } else if (FStrEq(pkvd->szKeyName, "bbmaxs")) { UTIL_StringToVector(maxs, pkvd->szValue); pkvd->fHandled = TRUE; } else CBaseEntity::KeyValue(pkvd); }

Basically this function is called before our spawn function to gather values set inside our map. We will need to make changes to our FGD and map shortly.



We are setting the min Vector to a string value which will be set on the “bbmins” FGD Key. The same will happen to the maxs Vector which will be set to the “bbmaxs” key value.



We use a very useful function to convert a string to a vector called UTIL_StringToVector(Vector, String) It turns the String “32 64 51” into the Vector(32, 64, 51) Add bbmins and bbmaxs to our FGD file with default Values.

// Where is Poppy Forge Game Data // Cathal McNally // sourcemodding@gmail.com // www.sourcemodding.com // February, 2017 // wip_StaticMesh @PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh" [ model(studio) : "Model" bbmins(string) : "Collision Volume Mins" : "-16 -16 -16" bbmaxs(string) : "Collision Volume Maxs" : "16 16 16" ]

Restart Hammer and open the properties for our wip_staticMesh Entity. Add 3 space separated Values to the Collision Volume Mins Parameter. “-32 -32 0”

Add 3 space separated Values to the Collision Volume Maxs Parameter. “32 32 190”

Compile the Map and the Code. Then run the Game to see your bounding box using the values you input through Hammer.

Solid Flag

Next we will add what is known as a flag which can also be set in Hammer. This is basically a condition which we will use in code to check if we should enable Collisions at all. Firstly, let’s add the following to our header:

#define WIP_IS_SOLID 1

This will be used as an identifier to check if the first flag set on the properties is true or false. If this was set to 2 we would be checking the second flag etc.



Next is to make some changes in our spawn function. We must wrap a condition around our pev->solid and UTIL_SetSize lines.



It will look like this:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; UTIL_SetSize(pev, mins, maxs); }

This checks the boolean state of the given Flag. In this case we are passing the first flag and only if it’s true will the entity be set to solid and a size set. The last thing we need to do to make this work is to add the spawnflags to the FGD

spawnflags(flags) = [ 1: "Solid?" : 1 ]

The “1” key above corresponds to the 1 we set WIP_IS_SOLID to. “Solid?” is what the flag will be called in hammer. The last “1” is the default value which in hammer will translate to true.



Save the FGD and restart Hammer to load in the new FGD Values.



There should be a new flag on the Flags tab of the wip_staticMesh properties.

Set it to true, compile the map, compile the code and test your level. You should still be blocked by the collision box.



Set it to false and you should be able to pass clean through your model.

Sequence based Collision

Next up we add the ability for the user to decide if they want to use Sequence based Collisions or Manually input Values for Collision. Firstly, lets add this to our header.

unsigned short m_iCollisionMode;

Note: I made this a short because it is cheaper than a full integer type, and unsigned because it should never be a negative value.



It will be used for a multi choice selection within Hammer and then checked in our spawn function upon which we will use either a sequence based collision box or our previously added manual values for a collision box.



The function used to set our mins and maxs from a sequence is:

ExtractBbox(Sequence Number, mins, maxs)

This function will look up the local entity that owns the current instance of the class, grab the sequence that we set as an integer and populate two Vectors which in this case will be mins and maxs. In our Spawn function let us change this:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; UTIL_SetSize(pev, mins, maxs); }

to:

// Check if the Solid Flag is set and if so be sure to set it // solid with appropriate Collisions // If not we also do not set a Collision Box because for a static mesh // there is no reason to do so.. if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; if (m_iCollisionMode == 1) { UTIL_SetSize(pev, mins, maxs); } else if (m_iCollisionMode == 2) { ExtractBbox(0, mins, maxs); UTIL_SetSize(pev, mins, maxs); } }

Note that we are setting the sequence to 0 here, we will be providing a future option shortly for users to input what sequence they want their model to play.



Then we must modify our KeyValue function to read in a value for m_iCollisionMode



Change the following:

void CStaticMesh::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "bbmins")) { UTIL_StringToVector(mins, pkvd->szValue); pkvd->fHandled = TRUE; } else if (FStrEq(pkvd->szKeyName, "bbmaxs")) { UTIL_StringToVector(maxs, pkvd->szValue); pkvd->fHandled = TRUE; } else CBaseEntity::KeyValue(pkvd); }

to include it:

void CStaticMesh::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "collisionmode")) { m_iCollisionMode = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else if (FStrEq(pkvd->szKeyName, "bbmins")) { UTIL_StringToVector(mins, pkvd->szValue); pkvd->fHandled = TRUE; } else if (FStrEq(pkvd->szKeyName, "bbmaxs")) { UTIL_StringToVector(maxs, pkvd->szValue); pkvd->fHandled = TRUE; } else CBaseEntity::KeyValue(pkvd); }

Lastly we need to add our multi-option to our FGD.

collisionmode(choices) : "Collision Mode" : 2 = [ 0: "None" 1: "Manual Inputs" 2: "Sequence Based" ]

Save your FGD and restart hammer.



You should now see a multi-option choice box as part of the entities properties.

I then setup two entities in hammer one using Manual Inputs where I use Mins of (-32, -32, -32) and Maxs of (32, 32, 32) so our collision box should be a distinctive cube.



The other entity will use sequence based collision and as we have set the sequence to 0 in the code it will use this models first sequence as a collision box.



Note: It’s important to remember that if the Solid Flag is not set in the Flags tab, no collision values will work and no collision box will be set even if you set the values here. This is used to optimize spawn times for static meshes that do not require collision volumes.



This is how it looks.

The tree to the left uses the manually set collision while the tree to the right uses the models first sequence bounds as a collision volume.

The Issue of Rotation and Collisions with an AABB System

As you can see above the bounding Volume does not rotate with our model which has been rotated 180 on the z axis. The best solution to this would be to implement OOBB Collisions but that is far beyond the scope of this tutorial and is honestly not required.



I propose using manual inputs for bounding volumes on models of these type. Consider this:

Create a tall volume centred around the models pivot so that when the model rotates the main shaft of this particular model is covered. It’s not perfect but AABB is far from a perfect collision system, we work with what we have.



Consider a model that is longer in width or length than it is in height. I will rotate the tree model on its side to demonstrate this. You could not use the sequence based collisions at all, you can use the manual inputs for angles that are multiplies of 90 degrees.



I rotate the model in hammer with the following values.

It then looks like this:

I then set the Mins to -190 -28 -28 and the Maxs to 0 28 28 and the result can be seen below.

If I rotate the model 90 degrees, I would need to update the mins and maxs to cover it again. So long as the models local orientation looks directly down one of the world’s axis you will be able to make some sort of useable collision volume for it.



However, when the model is rotated anywhere between 90 degree steps you have the following issue when you update the mins and maxs to cover the model.



The mins and maxs used to get this are: Mins -130 -145 -28 Maxs 0 28 28



And as you can see it is a woefully inaccurate collision box.

To work around this, I propose you disable collision on a model with these rotations and orientation and use invisible BSP geometry (CLIP Brush) to build smaller colliders along the model which is assumed to be static. They would look something like this:

The CLIP Brush is basically a static block with the CLIP Material applied to it. It renders the geometry with the Material applied to it as invisible but prevents the user from walking through it, similar to “player clip” in Source.



It’s obviously far from a perfect solution but it’s a decent workaround.



Keep in mind that you could always use a CLIP brush instead of manually entering Mins and Maxs for the models own collision model.

Bounding Box Visualization Aid

The last change I want to make regarding collision is giving the user the choice whether they want to render the bounding box visualization around their model or not. Let’s first add another Flag to our header and set it to 2 (The second flag in the Flags Tab) and a Boolean that we will use in our animate function to enable or disable the bounding box visualizer.

bool m_bDebugBB = false; #define WIP_DEBUG_BB 2

Next change the following in our spawn function:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; if (m_iCollisionMode == 1) { UTIL_SetSize(pev, mins, maxs); } else if (m_iCollisionMode == 2) { ExtractBbox(0, mins, maxs); UTIL_SetSize(pev, mins, maxs); } }

to this:

if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; if (m_iCollisionMode == 1) { UTIL_SetSize(pev, mins, maxs); } else if (m_iCollisionMode == 2) { ExtractBbox(0, mins, maxs); UTIL_SetSize(pev, mins, maxs); } if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){ m_bDebugBB = true; } }

We do the WIP_DEBUG_BB check within the WIP_IS_SOLID check because we don’t need to visualize the bounding box when our model won’t be solid.



We carry out the flag check in the spawn function and set the Boolean m_bDebugBB to true. We do this because it’s cheaper than doing an FBitSet check in our animate function every frame. This way we will only have to check the Boolean value every time the animate function is called.



Change the Animate function from:

void CStaticMesh::Animate(void) { pev->nextthink = gpGlobals->time + 0.01; UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); }

to:

void CStaticMesh::Animate(void) { pev->nextthink = gpGlobals->time + 0.01; if (m_bDebugBB){ UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); } }

Then in the FGD add the “Debug Bounding Box?” flag to our spawnflags:

spawnflags(flags) = [ 1: "Solid?" : 1 2: "Debug Bounding Box?" : 0 ]

Note: The UTIL_RenderBBox function seems to be limited in how many lines it can render, Probably an engine limit. See the following screen shot to explain what I mean; I have multiple entities loaded in the map with the WIP_DEBUG_BB Flag set to true. The last model (rightmost) I added does not render any Bounding Box Visualization and the second model (middle) only renders some of the lines of the Bounding volume.

r_drawentities 5

Those of you familiar with the r_drawentities Console variable know that it provides 4 rendering modes for entities in game. Don’t render any entities Default, Render an Entity in its normal state Render an Entities Skeleton if it has one Render an Entities HitBoxes Render an Entities Hitboxes translucent with the Model underneath In GoldSrc’s Software rendering mode there was a 5th option which quite helpfully renders the bounding volume based on the Models currently running sequence only. It will not render a bounding volume of a custom set Size so for that the UTIL_RenderBBox will provide an accurate representation in that case.



To enable this feature in the OpenGL Renderer we have a small change to make to StudioModelRenderer.cpp



Locate the StudioRenderFinal_Hardware Function and add the following condition:

// Lets add bounding boxes to the OpenGL Renderer too! if (m_pCvarDrawEntities->value == 5) { IEngineStudio.StudioDrawAbsBBox(); }