Description | In this, the second part in the “Where is Poppy” modding tutorial series I take you through the steps for creating your own custom mesh loading entity |
Downloads | |
Version | 2017.1 |
Support | Forum Thread |
Software | Windows, Half-Life SDK, Microsoft Visual Studio [2010, 2012, 2013 +], Steam, Half-Life, Image Editor |
Learn how to create your first custom entity
Contents
This tutorial is the second part in the Where is Poppy Modding Series. Be sure to check out the previous tutorial here.
Entities in GoldSrc games are map place-able elements which serve a purpose such as a door, a Monster, AI Characters and elements we cannot see but perform actions during gameplay such as a player start position, path, multi-manager etc.
In this tutorial we will learn how to create our first custom Entity “wip_staticMesh”. “wip” in this case will stand for “where is poppy” and serves as an easy prefix filter when traversing the entity list in our level editor. The purpose of the entity is to place a static model on the map. Vanilla Half-Life did not make much use at all of static model files for props and chose rather to create most props with Constructive Solid Geometry (CSG). (hence the overly blocky style of the game)
I am not exactly sure why Valve chose to do this but I would guess that at the time it was both easier and cheaper to make props in the editor as CSG rather than as static models imported into the level. For the few models that were placed in the game generic entities were used such as monster_furniture or a cycler to place the mdl meshes.
Those of you have used the GoldSrc engine mod “Spirit of Half-Life” should be familiar with env_model which is used for placing static meshes in a level. We will attempt to recreate this functionality with wip_staticMesh.
What you will need
Text Editor (Notepad++, Notepad, Sublime etc.)Visual Studio ( 2010 – 2015* )Half-Life SDK from GitHub (* Use this Fork from Malortie for VS2015)Steam and a copy ofHalf-Life Hammer (Worldcraft),Jackhammer (Or a GoldSrc Level Editor of Choice)Perforce Server & Client (or any Version control system)
Overview
We will also load animations since meshes can still remain in one location and also animate. For instance, vegetation doesn’t move around due to its roots but it can sway in the wind so we want to provide an option for this possibility.
Preparation for Coding
My debugging options in hldll properties now look like this:
Be sure to set it again for all configurations and set the program you will be debugging through to hl.exe in your Half-Life directory. Then set the command to:
1 |
-game WhereIsPoppy_dev -console -dev -condebug -window -h 720 -w 1280 +map test_01 |
1 |
-game WhereIsPoppy_dev -console -dev –condebug -window -h 720 -w 1280 |
Header and CPP Files
For now, that will only cover a static model that doesn’t move (but can animate on the spot).

Right-Click on dlls and select Add -> New Item.

Select Header file and give your file a unique name, I call this file “wip_static_mesh.h” Now let’s add a .cpp file under hldll > Source Files > dlls.
Right-Click on dlls and Select Add -> New Item, this time select C++ File (.cpp) and name it the same way you called your header file with the exception of the extension which should be .cpp “wip_static_mesh.cpp”
Next let’s start preparing what will be the very minimum you will need to get a Static Mesh loading in your Mod.
Header
1 2 3 4 5 6 7 8 9 10 |
/****************************** Where is Poppy February, 2017 wip_static_mesh.h Static Mesh Loader for the mod Where is Poppy *******************************/ |
1 2 3 4 5 6 |
#ifndef WIP_STATIC_MESH_H #define WIP_STATIC_MESH_H #include "extdll.h" // Required for KeyValueData & Export Parts #include "util.h" // Required Consts & Macros #include "cbase.h" // Required for inheriting from CBaseAnimating |
The class will be called CStaticMesh and it inherits from CBaseAnimating so that we can use some cool functions from it later
1 2 3 4 5 6 7 |
class CStaticMesh : public CBaseAnimating { private: void Spawn(void); }; #endif |
Source CPP File
1 2 3 4 5 6 7 8 9 10 11 |
/****************************** Where is Poppy February, 2017 wip_custom_point_entities.cpp contains the source functions related to custom point entities for Where is Poppy *******************************/ |
1 |
#include "wip_static_mesh.h" |
1 |
LINK_ENTITY_TO_CLASS(wip_StaticMesh, CStaticMesh); |
If we don’t Precache the model the game will hang on start-up so it’s a necessary, step. If you remove the model file or rename it Half-Life will exit on load complaining that its can’t find the model. You can read a little more about precaching here.
Next we set the model itself using the SET_MODEL Macro/function
1 2 3 4 5 |
void CStaticMesh::Spawn(void) { PRECACHE_MODEL((char *)STRING(pev->model)); SET_MODEL(ENT(pev), STRING(pev->model)); } |
Save your Header and CPP file and compile. You should see that everything compiled correctly and a file was copied to your mod directory (if you are following on exactly from my previous tutorial)
Forge Game Data (.fgd)
This is what my wip.fgd file looks like.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 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" ] |
In this case we will only see a browse dialog for a Model File. We set some other settings such as color and size which correspond to the color and size of the initially placed entity prior to selecting a Model. More on this later.
Save your FGD and let’s fire up JackHammer.
Add our Custom Entity to the Level

Open the Map we started working on from the previous tutorial “Test_01.map”. It should be available in Jackhammers, recent file list.
Select the Point Entity Tool

Then on the Right hand side of the editor under the Objects pane select wip_StaticMesh

Click anywhere on the ground to add our custom entity. Observe a green box protruding from the floor where you clicked.

Its green because in our FGD we set the color to (0 255 0) The size of this box should also be what you had set in the FGD.
Select the Selection Tool.

Either Right Click on the instance of the “wip_StaticMesh” entity, double click on it, or click on it and hit ALT + ENTER to bring up the properties page for your selection.
You should see something along these lines:

Let’s select a model for our entity. Since we have no models added to our mod yet we can grab one from Half-Life’s Valve/Model folder.
I select “tree.mdl” which is the Xen Tree from Half-Life
You should see the model update in jackhammer.
For example, if you loaded tree.mdl like I did you will notice that it is currently animated in the Editor but I know that it won’t animate in the game because I haven’t added code to my entity to handle that yet.
I will now play with the Yaw setting in the properties and set it to 180 so that the entity will face me.

The next step is to Compile the Map and Run it. This assumes that you have compiled your code to include your new entity.
If all went well you should see your custom entity loaded in the game.

Collision
The GoldSrc Collison System
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.
Hardcoded Bounding Volume
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:
1 2 3 |
pev->solid = SOLID_BBOX; UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 32)); // Mins X Y Z Maxs X Y Z |
Other Flags this can be set to include:
1 2 3 4 5 |
#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 |
To briefly explain how the min and max values are used to create a bounding volume, see the following description.

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

1 |
UTIL_SetSize(pev, Vector(0, 0, 0), Vector(64, 64, 64)); |
GoldSrc then takes the Mins and Maxs and constructs a Cube from the given value.
Visualizing the Collision Volume
Add the following function to our Util.h file:
1 2 |
// Foo BBox Rendering void UTIL_RenderBBox(Vector origin, Vector mins, Vector maxs, int life, BYTE r = 0, BYTE b = 0, BYTE g = 0); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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) } |
Add the Animate function to the header file.
1 2 3 4 5 6 |
class CStaticMesh : public CBaseAnimating { private: void Spawn(void); void EXPORT Animate(void); }; |
Let’s add this Animate function to the source CPP file. Simply add it after void CStaticMesh::Spawn(void)
1 2 3 4 5 |
void CStaticMesh::Animate(void) { pev->nextthink = gpGlobals->time + 0.01; UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); } |
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:
1 2 |
SetThink(&CStaticMesh::Animate); pev->nextthink = gpGlobals->time + 0.01; |
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:
1 |
UTIL_SetSize(pev, Vector(0, 0, 0), Vector(32, 32, 32)); |

It 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:
1 |
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
1 2 |
void KeyValue(KeyValueData *pkvd); Vector mins, maxs; |
1 2 3 4 5 6 7 8 |
class CStaticMesh : public CBaseAnimating { private: void Spawn(void); void EXPORT Animate(void); void KeyValue(KeyValueData *pkvd); Vector mins, maxs; }; |
We must replace our UTIL_SetSize() Function
1 |
UTIL_SetSize(pev, Vector(-32, -32, 0), Vector(32, 32, 190)); |
1 |
UTIL_SetSize(pev, maxs, mins); |
1 2 3 4 5 6 7 8 9 10 11 |
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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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); } |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 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" ] |
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
Firstly, let’s add the following to our header:
1 |
#define WIP_IS_SOLID 1 |
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:
1 2 3 4 5 |
if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; UTIL_SetSize(pev, mins, maxs); } |
The last thing we need to do to make this work is to add the spawnflags to the FGD
1 2 3 4 |
spawnflags(flags) = [ 1: "Solid?" : 1 ] |
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
Firstly, lets add this to our header.
1 |
unsigned short m_iCollisionMode; |
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:
1 2 3 4 5 |
if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; UTIL_SetSize(pev, mins, maxs); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 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); } } |
Then we must modify our KeyValue function to read in a value for m_iCollisionMode
Change the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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); } |
1 2 3 4 5 6 |
collisionmode(choices) : "Collision Mode" : 2 = [ 0: "None" 1: "Manual Inputs" 2: "Sequence Based" ] |
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.
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
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.
1 2 3 |
bool m_bDebugBB = false; #define WIP_DEBUG_BB 2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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 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:
1 2 3 4 5 |
void CStaticMesh::Animate(void) { pev->nextthink = gpGlobals->time + 0.01; UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); } |
1 2 3 4 5 6 7 |
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); } } |
1 2 3 4 5 |
spawnflags(flags) = [ 1: "Solid?" : 1 2: "Debug Bounding Box?" : 0 ] |

r_drawentities 5
- 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
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:
1 2 3 4 5 |
// Lets add bounding boxes to the OpenGL Renderer too! if (m_pCvarDrawEntities->value == 5) { IEngineStudio.StudioDrawAbsBBox(); } |

Animation
Let’s add to our header file an integer to hold the id for what animation our currently loaded model should be playing. I add it to the end of our other unsigned short for ease of use and reuse of code.
1 |
unsigned short m_iCollisionMode, m_iSequence; |
1 2 3 4 5 |
if (FStrEq(pkvd->szKeyName, "animate")) { m_iSequence = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void CStaticMesh::KeyValue(KeyValueData *pkvd) { if (FStrEq(pkvd->szKeyName, "animate")) { m_iSequence = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } else 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); } |
1 |
pev->sequence = m_iSequence; |
1 |
ExtractBbox(0, mins, maxs); |
1 |
ExtractBbox(m_iSequence, mins, maxs); |
We then need to add the following to our Animate function so that the animation can play.
1 |
pev->frame > 255 ? pev->frame = 0 : pev->frame++; |
The equivalent as an if else would be:
1 2 3 4 5 6 |
if (pev->frame > 255){ pev->frame = 0; } else{ pev->frame++ } |
pev->frame controls the individual frames in a sequence.
We must change our FGD from:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@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" spawnflags(flags) = [ 1: "Solid?" : 1 2: "Debug Bounding Box?" : 0 ] collisionmode(choices) : "Collision Mode" : 2 = [ 0: "None" 1: "Manual Inputs" 2: "Sequence Based" ] ] |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh" [ sequence(integer) : "Animation Sequence (Editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay." animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model" model(studio) : "Model" bbmins(string) : "Collision Volume Mins" : "-16 -16 -16" bbmaxs(string) : "Collision Volume Maxs" : "16 16 16" spawnflags(flags) = [ 1: "Solid?" : 1 2: "Debug Bounding Box?" : 0 ] collisionmode(choices) : "Collision Mode" : 2 = [ 0: "None" 1: "Manual Inputs" 2: "Sequence Based" ] ] |
1 |
sequence(integer) : "Animation Sequence (editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay." |
1 |
animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model" |

The Editor Entry changes it for the editor and the Game Entry changes it in game so be sure to set the in game entry correctly if you intend to use animations.
Try the editor version and watch as your model plays the different animations you switch to. I highlight this option as an easy way to preview what sequence you play and its totally optional, rip it out if you don’t need it.
If you load tree.mdl into JHLMV note that you have two animations to choose from. A sequence number is given to be used as an ID/Index, this is the number you provide in hammer to select a Sequence.

This current implementation does not provide for people who do not want to play animations. So let’s prepare a Flag and a condition in the code to cater for this.
Change the following to our header:
1 |
bool m_bDebugBB = false; |
1 |
bool m_bDebugBB = false, m_bAnimate = false; |
I explain why I skip 3 below.
1 |
#define WIP_ANIMATE 4 |
1 |
pev->sequence = m_iSequence; |
1 2 3 4 5 6 |
// Check if the User wants to animate if (FBitSet(pev->spawnflags, WIP_ANIMATE)) { m_bAnimate = true; pev->sequence = m_iSequence; } |
1 |
pev->frame > 255 ? pev->frame = 0 : pev->frame++; |
1 2 3 |
if (m_bAnimate){ pev->frame > 255 ? pev->frame = 0 : pev->frame++; } |
Add the following to our flags section in the FGD
1 |
4: "Animate?" : 1 |
All the spawn flags together should now look like this:
1 2 3 4 5 6 |
spawnflags(flags) = [ 1: "Solid?" : 1 2: "Debug Bounding Box?" : 0 4: "Animate?" : 1 ] |

Test your changes with the flag enabled and observe the inanimate model within our seen.
Let’s also add a speed variable to our entity to control how fast the model’s sequence plays back in game.
Add the following float to our header
1 |
float m_flAnimationSpeed = 0.0f; |
1 2 3 4 5 |
if (FStrEq(pkvd->szKeyName, "animationspeed")) { m_flAnimationSpeed = atof(pkvd->szValue); pkvd->fHandled = TRUE; } |
Let’s change:
1 2 3 |
if (m_bAnimate){ pev->frame > 255 ? pev->frame = 0 : pev->frame++ } |
1 2 3 4 5 6 7 8 |
if (m_bAnimate){ if (m_flAnimationSpeed >= 0.0){ pev->frame > 255 ? pev->frame = 0 : pev->frame += m_flAnimationSpeed; } else{ pev->frame < 0 ? pev->frame = 255 : pev->frame += m_flAnimationSpeed; } } |
1.0 = normal forward animation speed, default
>1.0 = faster than normal speed, dependant on what you input
0.0 = No animation
<1.0 && > 0.0 = slower animation speed
<= -1 = reversed animation and a lower negative value gives faster reverse playback
You could argue against my animate flag here (and use the animation speed condition instead) since a value of 0.0 for the animation speed means the model wont animate either but I wanted to also show you that flag id’s were powers of 2.
Add our latest addition to the FGD:
1 |
animationspeed(string) : "Animation Speed" : "1.0" |
Scaling our Mesh
This does not work out of the box for meshes in GoldSrc as the support was only built in for sprites. However, we can make a small change to our client project which would enable pev->scale for meshes.
Locate in the hl_cdll project the file StudioModelRenderer.cpp.
In the function:
1 |
CStudioModelRenderer::StudioSetUpTransform (int trivial_accept) |
1 2 3 4 5 6 7 8 9 10 |
if (m_pCurrentEntity->curstate.scale != 0 && m_pCurrentEntity->curstate.scale != 1.0) { for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { (*m_protationmatrix)[i][j] *= m_pCurrentEntity->curstate.scale; } } } |
A scale of 1 would also mean no change and the model would be its original scale baked into the mdl file.
Anything between those numbers require that the rotation matrix be modified by multiplying each value in the matrix by the scale input by the user through the hammer level.
Let’s tie it all together by returning to wip_static_mesh.h
Add the following float:
1 |
float m_fModelScale = 1.0f; |
Change the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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(m_iSequence, mins, maxs); UTIL_SetSize(pev, mins, maxs); } if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){ m_bDebugBB = true; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
if (FBitSet(pev->spawnflags, WIP_IS_SOLID)) { pev->solid = SOLID_BBOX; if (m_iCollisionMode == 1) { mins = mins * m_fModelScale; maxs = maxs * m_fModelScale; UTIL_SetSize(pev, mins, maxs); } else if (m_iCollisionMode == 2) { ExtractBbox(m_iSequence, mins, maxs); mins = mins * m_fModelScale; maxs = maxs * m_fModelScale; UTIL_SetSize(pev, mins, maxs); } if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){ m_bDebugBB = true; } } pev->scale = m_fModelScale; |
Add the following:
1 2 3 4 5 |
else if (FStrEq(pkvd->szKeyName, "modelscale")) { m_fModelScale = atof(pkvd->szValue); pkvd->fHandled = TRUE; } |
1 2 |
modelscale(string) : "Model Scale (Game)" : "1.0" : "Set the Model Scale (0.0 - 1.0)" scale(string) : "Model Scale (Editor)" : "1.0" : "Set the Model Scale (0.0 - 1.0)" |
Save the FGD, Compile the Code, make scale changes in Hammer and load the game to see a scaled Mesh. If you enable the bounding box visualizer and set the collision mode to sequence you can see the collision box scale perfectly with your model.
Here is a xen tree scaled by 0.2:

Error Prevention
They both relate to loading the mesh itself.
If the model you are trying to load does not exist, the game will throw a precaching error and tell you which model is missing.

That’s useful enough in this case because you know what model is missing so you can simply locate and fix the issue.
The other issue causes a crash with no error and occurs if you create an instance of our entity but do not apply a model file to it.
To avoid this crash, I have made a small null.mdl model which says no model loaded and I use the defaults parameter in the FGD to set this model.
That way when you set the model in hammer you will see this null.mdl instead of a solid box.
1 |
model(studio) : "Model" : "models/null.mdl" : "Set a Mesh to load into the Game" |
Its looks like this:

Without our custom null.mdl set it would look like this:

And would cause a crash.
To avoid the crash let’s make a small change to the following code:
Change:
1 2 |
PRECACHE_MODEL((char *)STRING(pev->model)); SET_MODEL(ENT(pev), STRING(pev->model)); |
1 2 3 4 5 6 7 8 9 10 |
if (pev->model != 0){ PRECACHE_MODEL((char *)STRING(pev->model)); SET_MODEL(ENT(pev), STRING(pev->model)); } else{ ALERT(at_console, "[wip_staticMesh] Error, Model Failed to load!\n"); ALERT(at_console, "[wip_staticMesh] Setting model/null.mdl in its place!\n"); PRECACHE_MODEL("models/null.mdl"); SET_MODEL(ENT(pev), "models/null.mdl"); } |

Final Reference Code
Header
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
/****************************** Where is Poppy 29.8.2016 wip_static_mesh.h Static Mesh Loader for the mod Where is Poppy *******************************/ #ifndef WIP_STATIC_MESH_H #define WIP_STATIC_MESH_H #include "extdll.h" // Required for KeyValueData #include "util.h" // Required Consts & Macros #include "cbase.h" // Required for CPointEntity class CStaticMesh : public CBaseAnimating { private: void Spawn(void); void EXPORT Animate(void); void KeyValue(KeyValueData *pkvd); Vector mins = { 0, 0, 0 }, maxs = { 0, 0, 0 }; unsigned short m_iCollisionMode = 0, m_iSequence = 0; bool m_bDebugBB = false, m_bAnimate = false; float m_flAnimationSpeed = 1.0f, m_fModelScale = 1.0f; #define WIP_IS_SOLID 1 #define WIP_DEBUG_BB 2 #define WIP_ANIMATE 4 }; #endif |
Source CPP
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
/****************************** Where is Poppy February, 2017 wip_static_mesh.cpp Static Mesh Loader for the mod Where is Poppy *******************************/ #include "wip_static_mesh.h" // Need to Link our class to the name (wip_StaticMesh) that hammer will read from the FGD. // This will be linked directly to the level as well so that the engine can link to it. LINK_ENTITY_TO_CLASS(wip_StaticMesh, CStaticMesh); /////////////////////////////// // Spawn(void) // // The Spawn function handles the creation and intialization of our entitty // It is the second function to run in this Class //////////////////////////////// void CStaticMesh::Spawn(void) { // Precache and Load the model if (pev->model != 0){ PRECACHE_MODEL((char *)STRING(pev->model)); SET_MODEL(ENT(pev), STRING(pev->model)); } // If the Model doesnt exist, print an error and set a default null.mdl as the model else{ ALERT(at_console, "[wip_staticMesh] Error, Model Failed to load!\n"); ALERT(at_console, "[wip_staticMesh] Setting model/null.mdl in its place!\n"); PRECACHE_MODEL("models/null.mdl"); SET_MODEL(ENT(pev), "models/null.mdl"); } // 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)) { // Set Model solid pev->solid = SOLID_BBOX; // Check the collision mode 0 = None, 1 = Manual, 2 = Sequence based if (m_iCollisionMode == 1) { // Scale the collision box mins = mins * m_fModelScale; maxs = maxs * m_fModelScale; // Set Collision box Size UTIL_SetSize(pev, mins, maxs); } else if (m_iCollisionMode == 2) { // Grab Bounding box size from current sequence ExtractBbox(m_iSequence, mins, maxs); // Sacle the collision box mins = mins * m_fModelScale; maxs = maxs * m_fModelScale; // Set Collision box Size UTIL_SetSize(pev, mins, maxs); } // Check if the bounding box Visualizer flag is set if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){ m_bDebugBB = true; // If so set this value to true, we using it in Animate() } } // Set the visible meshes scale pev->scale = m_fModelScale; // Check if the User wants to animate if (FBitSet(pev->spawnflags, WIP_ANIMATE)) { m_bAnimate = true; // Used in Animate() pev->sequence = m_iSequence; // Set the animation based on what the user set in the level } // Set our update/think function to Animate() SetThink(&CStaticMesh::Animate); // Set when in the future to update next pev->nextthink = gpGlobals->time + 0.01; } /////////////////////////////// // KeyValue(KeyValueData *pkvd) // // The KeyValue function imports values set by our level editor in our map // These Keys are created in our FGD // We set local variables to the values that the map returns when requested // It is the first function to run in this Class //////////////////////////////// void CStaticMesh::KeyValue(KeyValueData *pkvd) { // Grab the speed our animation plays at // 0.0 here also stops the animation // A netagive value plays the animation in reverse // A higher value speeds up the animation if (FStrEq(pkvd->szKeyName, "animationspeed")) { m_flAnimationSpeed = atof(pkvd->szValue); pkvd->fHandled = TRUE; } // In-Game version of editor only variable "sequence" // Set an integer to what sequence you want this model to play ingame else if (FStrEq(pkvd->szKeyName, "animate")) { m_iSequence = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } // Set a mode for Collision // 0 = No Collision // 1 = Manual Mins & Maxs // 2 = Sequence Based Collision else if (FStrEq(pkvd->szKeyName, "collisionmode")) { m_iCollisionMode = atoi(pkvd->szValue); pkvd->fHandled = TRUE; } // Minimum Bounding box position else if (FStrEq(pkvd->szKeyName, "bbmins")) { UTIL_StringToVector(mins, pkvd->szValue); pkvd->fHandled = TRUE; } // Maximum Bounding box position else if (FStrEq(pkvd->szKeyName, "bbmaxs")) { UTIL_StringToVector(maxs, pkvd->szValue); pkvd->fHandled = TRUE; } // Set the scale of our model and collision boxes // In-Game version of the editor only "scale" keyword else if (FStrEq(pkvd->szKeyName, "modelscale")) { m_fModelScale = atof(pkvd->szValue); pkvd->fHandled = TRUE; } // defaults else CBaseEntity::KeyValue(pkvd); } /////////////////////////////// // Animate(void) // // The Animate function is basically the Update function of this Entitiy // You add thinks here that you want to change on a frame by frame basis // Things like animations // Position changes // Interactive code //////////////////////////////// void CStaticMesh::Animate(void) { // Set when in the future to next run the animate function pev->nextthink = gpGlobals->time + 0.01; // If animation is allowed if (m_bAnimate) { if (m_flAnimationSpeed >= 0.0) { // Ternary condition to update the models normal animation + any extra speed the user adds pev->frame > 255 ? pev->frame = 0 : pev->frame += m_flAnimationSpeed; } else { // Ternary condition to update the models reverse animation + any extra speed the user adds pev->frame < 0 ? pev->frame = 255 : pev->frame += m_flAnimationSpeed; } } // Visualize the Collision Volume around the model if (m_bDebugBB){ UTIL_RenderBBox(pev->origin, pev->mins, pev->maxs, 1, 0, 255, 0); } } |
FGD
1 2 3 4 5 |
// Key(type) : Name : Default Value : Description of function model(studio) : "Model" : "path/to/model" : "Select a Model to Load" sequence(integer) : "Animation Sequence (editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay." |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// Where is Poppy Forge Game Data // Cathal McNally // sourcemodding@gmail.com // www.sourcemodding.com // 29.8.2016 // wip_StaticMesh @PointClass color(0 255 0) size(-20 -20 -20, 20 20 20) studio() = wip_StaticMesh : "wip_StaticMesh" [ animate(integer) : "Animation Sequence (Game)" : 0 : "Setting an in game Animation Sequence for the selected model" sequence(integer) : "Animation Sequence (Editor)" : 0 : "Sequence to display in Jackhammer. This does not affect gameplay." animationspeed(string) : "Animation Speed" : "1.0" : "Set the Speed of your animation. 1.0 = normal, 0.0 - 1.0 is slower, greater than 1 is faster and less or equal to -1 reverses the animation" modelscale(string) : "Model Scale (Game)" : "1.0" : "Set the Model Scale (0.0 - 1.0)" scale(string) : "Model Scale (Editor)" : "1.0" : "Set the Model Scale (0.0 - 1.0)" model(studio) : "Model" : "models/null.mdl" : "Set a Mesh to load into the Game" bbmins(string) : "Collision Volume Mins" : "-16 -16 -16" : "Set the Minuimum Collision position for our Manually set Bounding Volume" bbmaxs(string) : "Collision Volume Maxs" : "16 16 16" : "Set the Maximum Collision position for our Manually set Bounding Volume" spawnflags(flags) = [ 1: "Solid?" : 1 : "Enable Collisions?" 2: "Debug Bounding Box?" : 0 : "Show a visual representation of the bounding box?" 4: "Animate?" : 1 : "Animate the Model?" ] collisionmode(choices) : "Collision Mode" : 2 = [ 0: "None" : "No Collisions" 1: "Manual Inputs" : "Enter Manual Min and Max values for a Custom Bounding Volume?" 2: "Sequence Based" : "Take the Bounding volume from the selected Animation Sequence?" ] ] |

Further Reading
Automatically set entity collision box by model
Custom AABB collision boxes for an entity
A very technical explanation regarding the engine
Special Thanks
Elias Ringhauge aka eliasr, for his tutorial on the collision system on GoldSrc and for taking the time to help me understand it better, especially the visualizer for the Collision box.
I hope this tutorial helps you get to grips with coding your own entities in GoldSrc. If you find any issues or if you know of anything this document should include please feel free to send a mail onto me concerning it.
The support thread for this tutorial can be found over on the forums.
You can download the Project Source Code, Assets and Release through Github
Cathal McNally