First Custom Entity - Hammer Tutorial/Half-Life Modding
First Custom Entity - Hammer Tutorial/Half-Life Modding
Cathal McNally
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.
Note: This tutorial will not cover the absolute basics of programming, there is a wealth of other tutorials and books
freely available on net for this exact purpose. This tutorial is for those who are familiar with the concepts of
programming in C/C++ but are interested in applying that to Half-Life/GoldSrc
Optional
• Perforce Server & Client (or any Version control system)
Overview
Basically what we want to achieve is the loading of a mesh/model (.mdl) into our Mod. As it’s a static mesh we do
not have to worry about movement code and simply need to ensure its placed correctly and that it has a physical
presence (collision volume) preventing the player from walking through it.
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.
Visual Studio:
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:
-game WhereIsPoppy_dev -console -dev -condebug -window -h 720 -w 1280 +map test_01
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, 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.
I typically always start with a block comment at the top which looks something like this.
/******************************
Where is Poppy
February, 2017
wip_static_mesh.h
*******************************/
I then define the Header file and include any dependencies the class will require.
#ifndef WIP_STATIC_MESH_H
#define WIP_STATIC_MESH_H
Next we will create the Class itself and give it a unique name from anything else that has been created in the
project so as to avoid conflicts in naming and reference etc.
The class will be called CStaticMesh and it inherits from CBaseAnimating so that we can use some cool functions
from it later
#endif
We declare our functions which will be used in the cpp file later.
Spawn: which pretty much spawns our entity in the world when called upon either by other code or a level
including it.
/******************************
Where is Poppy
February, 2017
wip_custom_point_entities.cpp
*******************************/
Next we include a reference to our header file we created earlier. Since we include all other references in the
header file there is no need to #include anything else here.
#include "wip_static_mesh.h"
Then we must link our class CStaticMesh to what hammer will recognise it as: “wip_StaticMesh” (this is what we
will add to our custom FGD shortly)
LINK_ENTITY_TO_CLASS(wip_StaticMesh, CStaticMesh);
Next up is the Spawn function which will take control of adding our mesh to the world.
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.
void CStaticMesh::Spawn(void)
{
PRECACHE_MODEL((char *)STRING(pev->model));
SET_MODEL(ENT(pev), STRING(pev->model));
}
That’s all we need on the programming side to get a static mesh loaded into the Game.
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)
Let’s create in your text editor of choice a new file called after your mod. In my case I will call it wip.fgd and I will
save it to my “WhereIsPoppy_dev” Mod Directory inside the Half-Life Directory.
// February, 2017
// wip_StaticMesh
Basically an FGD is a descriptor for Hammer to interface with classes you have created in code. This represents
your CStaticMesh Class inside Hammer. In the entity list when we add this FGD to our mod we will see a new
entry called “wip_StaticMesh” which reads this Key : Value Data to figure out what options to give the user within
hammer.
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.
Open the Map we started working on from the previous tutorial “Test_01.map”. It should be available in
Jackhammers, recent file list.
Click anywhere on the ground to add our custom entity. Observe a green box protruding from the floor where you
clicked.
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.
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.
Note: There is no WYSIWYG Editor for GoldSrc or Source Games simply because the levels still need to be
compiled and are only then loaded by the Engine. Recent Game Engine adaptations such as UE4, CryEngine &
Unity use an instance of the Engine as the Level Editor so you have a clear idea of that the end result will be.
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.
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.
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.
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.
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:
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.
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.
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.
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.
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.
Currently we cannot see the bounding box but let’s change that by creating the UTIL_RenderBBox() function.
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]);
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.
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
The function we added earlier will provide a visual representation of our bounding box.
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:
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.
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.
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.
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.
// February, 2017
// wip_StaticMesh
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”
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.
#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.
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
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.
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.
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:
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.
if (FBitSet(pev->spawnflags, WIP_IS_SOLID))
{
pev->solid = SOLID_BBOX;
UTIL_SetSize(pev, mins, maxs);
}
To:
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.
To include it:
You should now see a multi-option choice box as part of the entities properties.
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.
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.
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 then set the Mins to -190 -28 -28 and the Maxs to 0 28 28 and the result can be seen below.
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.
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.
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.
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.
#define WIP_DEBUG_BB 2
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 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.
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
]
Those of you familiar with the r_drawentities Console variable know that it provides 4 rendering modes for entities
in game.
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
You can see that I borrowed it directly from the StudioRenderFinal_Software function. As to why this was
exclusive to the Software Renderer I do not know but I suspect it was simply not supported by OpenGL in the late
90’s. You can compile the code and enter r_drawentities 5 to see the transparent blue representation of the
currently selected sequence Bounding Box (Remember that we hardcoded it to 0 earlier as part of our
ExtractBbox function)
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.
Next let’s add to the KeyValue Function so that it reads in the correct value for the animation sequence.
if (FStrEq(pkvd->szKeyName, "animate"))
{
m_iSequence = atoi(pkvd->szValue);
pkvd->fHandled = TRUE;
}
We must then set the sequence based on what the user input through hammer. We do that in the Spawn function
near the end. We must add the following:
pev->sequence = m_iSequence;
To:
This makes sure that the correct sequence Collision box will be used if the user selects to use that collision mode.
We then need to add the following to our Animate function so that the animation can play.
For those unfamiliar with the above line it is known as a ternary operation, basically an inline if else statement. Its
written like this simply as an arguably easier alternative which is a little faster to write.
Basically we are just making sure that the frames are incrementing with each update and if we reach the max of
255 reset to zero and start incrementing again.
We use the following to set a sequence that we can visualize in the editor, for some reason this does not work for
actually setting the value we would expect to see in game and that’s why we have a separate animate key. I
believe that the word “sequence” is reserved much like the keys “mins” and “maxs”. I could not for the life of me
get them to work.
And finally we use the following to set what sequence will actually be used in game
Save the FGD, compile the code, restart Hammer and you should see 2 new entries in the properties of our
entity.
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.
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.
To:
#define WIP_ANIMATE 4
pev->sequence = m_iSequence;
To:
To:
if (m_bAnimate){
pev->frame > 255 ? pev->frame = 0 : pev->frame++;
}
Again this is cheaper to check a Boolean in the update loop as opposed to checking the state of the Flag which
involves calling further functions.
4: "Animate?" : 1
Note: that I skip the number 3 here and in the header which seems to be a feature/bug/limit in how flags work or
are sent between the FGD and the game or between the Map and the game. Flags are stored as a power of 2. So
the series to set them goes like this 1, 2, 4, 8, 16, 32, 64, 128, 256 etc.
All the spawn flags together should now look like this:
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.
Then add it to our KeyValue Function to read in its data. We use atof() not atoi()
if (FStrEq(pkvd->szKeyName, "animationspeed"))
{
m_flAnimationSpeed = atof(pkvd->szValue);
pkvd->fHandled = TRUE;
}
Let’s add it to our animate function where it will be used to set the animation speed.
Note: 1.0 = normal speed, Greater than 1.0 = faster animation, we will use a negative number to change the
direction of the animation, which involves a little extra coding.
Let’s change:
if (m_bAnimate){
pev->frame > 255 ? pev->frame = 0 : pev->frame++
}
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;
}
}
Basically what I am doing here is checking if we are animating, then I am checking our speed variable for what
was input in the map,
If it’s a positive number, we increment using the speed as the increment value
If it’s a negative number, we decrement using the speed as the decrement value effectively reversing the
animation.
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.
Restart hammer, check the new property and test out both positive and negative values.
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.
In the function:
Basically this checks for a change in the models scale. The scale setting by default is 0 since it was unused prior
to this. If its zero technically we shouldn’t see it so let’s not modify it if its zero.
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.
Then we must also consider that we have to modify the collision boxes. We scale them by the same amount we
scale the visible mesh.
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;
}
}
To:
if (FBitSet(pev->spawnflags, WIP_IS_SOLID))
{
pev->solid = SOLID_BBOX;
if (m_iCollisionMode == 1)
{
mins = mins * m_fModelScale;
maxs = maxs * m_fModelScale;
if (FBitSet(pev->spawnflags, WIP_DEBUG_BB)){
m_bDebugBB = true;
}
}
pev->scale = m_fModelScale;
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)"
Again the word “scale” seems to be reserved and won’t pass values to the game engine so we use “scale” for the
editor and “modelscale” for ingame scale changes.
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.
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.
Change:
PRECACHE_MODEL((char *)STRING(pev->model));
SET_MODEL(ENT(pev), STRING(pev->model));
To:
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");
}
So now even if the user removes the defaults from the entity to force the error we will always load the null.mdl
(provided it hasn’t been removed) to visualize that a model is missing.
Header
/******************************
Where is Poppy
29.8.2016
wip_static_mesh.h
*******************************/
#ifndef WIP_STATIC_MESH_H
#define WIP_STATIC_MESH_H
Vector mins = { 0, 0, 0 },
maxs = { 0, 0, 0 };
#define WIP_IS_SOLID 1
#define WIP_DEBUG_BB 2
#define WIP_ANIMATE 4
};
#endif
/******************************
Where is Poppy
February, 2017
wip_static_mesh.cpp
*******************************/
#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");
}
///////////////////////////////
// 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;
}
///////////////////////////////
// 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;
}
}
In case you hadn’t noticed FGDs support a default value as well as the option to include a short description of
what the function does. For Example:
I cleaned up the FGD to include descriptions which can be shown in the help section of the Entity in Hammer.
// 29.8.2016
// wip_StaticMesh
Further Reading
Custom Model Entity
Automatically set entity collision box by model
Custom AABB collision boxes for an entity
A very technical explanation regarding the engine
Special Thanks
Sam Vanheer aka Solokiller, for his insight into engine features and his ever eager nature to help me.
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.
Kind Regards
Cathal McNally
www.sourcemodding.com
All trademarks are property of their respective owners in the US and other countries.