Authoring a Logical Entity
Logical entities are the simplest of entities because they have no visual component and only exist to service input from other entities. math_counter
for example stores a value that can be added to or subtracted from; other entities in the map can modify the data with inputs or receive information from it with an output.
In this tutorial we'll create a logical entity that performs the simple task of storing a value and incrementing that value every time it receives the appropriate input. Once the counter has reached a value we'll define, the entity will fire an output.
Contents
Create the source file
This tutorial assumes you are using Visual Studio or Visual C++ Express. See Compiler Choices.
Add a new folder ("filter") to the Server
project and call it whatever you like. Then add a new .cpp item there and call it sdk_logicalentity. This file will hold all of the code you will write in this tutorial.
Giving each entity its own .cpp reduces overhead, since it allows the compiler to separate your code into more discrete segments, and speeds compiling, since only .cpp files that have changed need to be re-compiled.
Includes
Every .cpp file starts out on its own, isolated from the rest of the project. To overcome this we can include "header files", which declare to the compiler what parts of the project we will be using. In the case of this very simple entity we only need to include one file:
#include "cbase.h"
cbase.h
provides access to Valve's basic set of commands for creating entities, and every .cpp you write for Source must include it. The letter "c" is a part of the naming pattern used throughout Source, and means that the code is server-side (client-side is generally "c_").
Declaring the class
This section will be quite long, but once you're finished with it you'll have a grasp of how C++ works.
Almost all processing is performed by "objects". Objects are created when a program is running from blueprints called "classes". Creating a new entity means creating a new class.
class CMyLogicalEntity : public CLogicalEntity
{
public:
DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity );
... // Don't actually type this line!
};
This is a "class declaration", in which we tell the C++ compiler what data we want objects of this class to store, and what functions we want it to perform.
In the first line of code, we start by saying that we're declaring a class with the class
command. We then give it a name, CMyLogicalEntity
, and then with the colon tell the compiler that it is "inheriting" from Valve's pre-existing CLogicalEntity
class (which we can reach thanks to our #include of cbase.h
earlier).
Inheritance means basing a class on an existing one instead of starting from scratch. CLogicalEntity
is itself quite simple, but there is a huge amount of code in the classes it inherits from in turn, going right back to CBaseEntity
— without being able to inherit all of that, anyone trying to create a new entity would probably go mad.
With the class command written down, we open a set of curly parentheses. Writing code inside parentheses means you are grouping it; in this case, the group is the current class. (Note how there isn't a line-ending semicolon here. We're still, technically, writing the same line of code.)
Next is "public:
", a command which causes the subsequent "members" of the class to be accessible by other classes (we'll switch back to private declarations later). You should only make members public when it is strictly necessary: this principle is called encapsulation and is by no means unique to C++.
The first command
Now we have the framework up and running and can start issuing commands. DECLARE_CLASS
is a 'macro' created by Valve to automate much of the book-keeping that needs to be done when declaring a new entity: when your code is compiled, the macro is replaced by various functions and variables.
The round parentheses is where we pass "arguments", or "parameters", to the macro. These give it the information it needs to do its job properly. You may already have noticed that we are sending it the same key information that's in the first line of the declaration, separated, or "delimited", with a comma.
The line's closing semicolon tells the compiler that this is the end of a command and a new one is about to start. This is needed because line breaks and whitespace are largely ignored - the compiler sees a continuous stream of characters.
Closing the declaration (ahead of time)
Don't type the ...
- it's only there to mark the point where the rest of the declaration code will go. We're going to skip ahead for a moment instead and end the class
command with };
. It's simple enough: the closing bracket counters the opening one we typed earlier, and the semicolon performs the same function as it did after DECLARE_CLASS
.
}
and ;
, as the former implies the latter. The class
command is a special case.Declaring a DATADESC, constructor and function
Now go back to where the ellipsis was and add these new lines:
DECLARE_DATADESC();
// Constructor
CMyLogicalEntity ()
{
m_nCounter = 0;
}
// Input function
void InputTick( inputdata_t &inputData );
The first line is another macro, this time that automates the process of declaring a DATADESC table we'll be adding later. Thanks to the //
the second is a commented line that is ignored by the compiler but helps people reading your code (including you, when you come back to it after a lengthy enough break) understand its meaning.
The third is the class' "constructor". It is a type of "function": a command or collection of commands between a {
and a }
that are "called", or "executed", in one batch. Because it has the same name as our class C++ knows to call it whenever it creates a new instance. In this example we are using it to "initialise" m_nCounter to zero. Variables (see the next subheading) do not have default values, and failing to give them one before their use can have weird and wacky results!
We've just "defined" the constructor in its entirety, but most functions (including constructors for larger classes) are too big to fit into a class declaration. So instead, we declare them and write their contents later on in the .cpp file.
Accordingly, the last line in this code snippet is a declaration of the function that will be called whenever the correct input is received and, if you remember back to our original goal, increment our stored value by 1. void
means that the function won't "return" a result to whatever called it, since the output we'll make goes via a different route (constructors are special cases that don't need a value type, since they are always void
).
We call this function InputTick
and between round brackets define the "parameters" it requires. In this case, one inputdata_t
value named &inputData
. This is information about input event that is automatically generated by the engine at run-time, including among other things its name, I/O arguments from the map (there won't be any), and the entity from which it originated.
The &
means that instead of pulling down all of the information about absolutely every detail of the input event, we instead record the location of that inputdata_t
instance in the system's memory. This "pointer" allows us to access the instance's data without having to copy it to our current function's location. It's similar to opening a file from a desktop shortcut instead of moving or copying it there.
There is no {}
pair for InputTick()
: they'll come when we write the actual contents of the function later. We need to use a semicolon in their place to mark the end of the command.
Private declarations
Now we are declaring private members. These won't be accessible anywhere outside this class unless we make them so in a public function (which we won't).
private:
int m_nThreshold; // Count at which to fire our output
int m_nCounter; // Internal counter
COutputEvent m_OnThreshold; // Output event when the counter reaches the threshold
The first two of these are "variables". They are buckets in the computer's physical memory that can be filled with a value, used in calculations, passed around, perhaps written to the disk, and emptied. They are "integer" or "int" variables which can be used to store whole numbers like -1, 0, 1, 2, 3, and so on. They can't store words or characters however, nor numbers with decimal points. C++ is very strict about these kinds of things!
m_OnThreshold
is an instance of COutputEvent
, a class that Valve have already written for you in cbase.h
. You'll use it in InputTick
to pass a value through the I/O system when our conditions are met. It's actually a variable like int
, but a custom one that doesn't come with the compiler. The only reason it isn't coloured blue as well is that Visual Studio doesn't recognise it.
A word about the names in this section. m_
means that the objects are members of the current class, while n
means that the value is numeric (i
for 'integer' could be used too). You don't have to follow these naming conventions, but they are very common.
Check back
That's the declaration finished. Check back over what you've written and make sure it's the same as the reference code. If it is, you're ready to move past that };
and into the body of the entity's code.
Linking the class to an entity name
We are now past the class declaration. In Visual Studio, you should be able to collapse that whole section by clicking on the second minus sign in the margin. Now, after the };
, enter this:
LINK_ENTITY_TO_CLASS( my_logical_entity, CMyLogicalEntity );
This is another macro (you can tell from the capitals), this time linking the C++ class CMyLogicalEntity
to the Source engine classname my_logical_entity. Classnames are used by the I/O system, by Hammer, and occasionally by programmers.
Data description table
The data description table is a series of macros that give Source metadata about the members of the class you mention in it. The comments in the code below should explain what each one does: DEFINE_FIELD
, for instance, ensures that the m_nCounter
value is stored in a saved game to prevent it from resetting to zero every time the game is reloaded. DEFINE_KEYFIELD
on the next line does the same job, and also opens the value up for editing in Hammer.
This code has examples of the string values mentioned in the last section. Unlike the other commands, these are simply data that the compiler accepts, stores, and blindly exposes to the engine. You can put anything you like between the quote marks and the code will compile, but when it comes to actually instancing the entity Source will be confused unless everything is correct.
// Start of our data description for the class
BEGIN_DATADESC( CMyLogicalEntity )
// For save/load
DEFINE_FIELD( m_nCounter, FIELD_INTEGER ),
// As above, and also links our member variable to a Hammer keyvalue
DEFINE_KEYFIELD( m_nThreshold, FIELD_INTEGER, "threshold" ),
// Links our input name from Hammer to our input member function
DEFINE_INPUTFUNC( FIELD_VOID, "Tick", InputTick ),
// Links our output member variable to the output name used by Hammer
DEFINE_OUTPUT( m_OnThreshold, "OnThreshold" ),
END_DATADESC()
You'll have noticed that while our input has a function, our output is merely the COutputEvent
variable we declared earlier. While an input requires processing when it arrives, and thus a function, an output will have been processed already by the time it leaves.
When writing your own datadesc tables, remember to include a comma after each DEFINE_*
command.
Creating the input function
Now we are going to "define" our input function, which will run when the entity receives the tick
input through the I/O system. Notice the addition of CMyLogicalEntity::
to the function name, which explicitly links the function definition to our class.
//-----------------------------------------------------------------------------
// Purpose: Handle a tick input from another entity
//-----------------------------------------------------------------------------
void CMyLogicalEntity::InputTick( inputdata_t &inputData )
{
// Increment our counter
m_nCounter++;
// See if we've met or crossed our threshold value
if ( m_nCounter >= m_nThreshold )
{
// Fire an output event
m_OnThreshold.FireOutput( inputData.pActivator, this );
// Reset our counter
m_nCounter = 0;
}
}
Here's what's going on:
m_nCounter++
- C++ shorthand for "increment by one". In full, it would be
m_nCounter = m_nCounter + 1;
. Another way of writing it ism_nCounter += 1;
. if ( m_nCounter >= m_nThreshold )
if
statements need a true/false equation to evaluate.>
means greater than, so combined with=
we have an "operator" that says "greater than or equal to". If the value of m_nCounter is greater than or equal to the value of m_nThreshold, execute whatever is inside this command.{ }
- This is a "nested" or "child" bracket pair, within the functions own "parent" bracket pair. It groups all of the commands the
if
statement will execute if it evaluates totrue
. There can be as many levels of nesting as is needed, but be sure to keep track of them all! m_OnThreshold.FireOutput
- You probably pasted this in. Delete it and type it out in full, and note what happens when you reach the dot. You are calling a public function of the
m_OnThreshold
instance of theCOutputEvent
class, and Visual Studio helps you by providing a list of the possible options. Keep typing to filter it, or use up/down and return to select an item. - If we were to call the function we're currently writing from another class, we would type
MyLogicalEntity1.InputTick(<arguments>)
, whereMyLogicalEntity1
is the name of the instance we were targeting. (Which means that we would need to have a pointer to an instance of the class - we can't just access the template!) ( inputData.pActivator, this )
- These are the arguments we are passing to the
COutputEvent
. They will be automatically generated by Source, and arrive at our function through the parameters we gave it between its()
s. - We're sending:
- The entity that started this whole chain off (in Source lingo the "activator").
- The entity that the call is being sent from, the "caller", which is "this" one.
- These are needed for targetname keywords.
m_nCounter = 0
- If we didn't reset our counter, the number would only keep on going up.
Congratulations, the entity is ready to be compiled - press F7 to start the process. If you receive any errors from Visual Studio, check the reference code!
We can't use this entity directly from the engine however - if you've ever made a map, you'll know there's a lot of I/O information that we haven't made room for in this code. Thankfully our DATADESC table allows Source to plug this entity into the wider I/O system automatically; so the only thing left to do now is tell Hammer about it.
Create the FGD entry
An FGD is a text file that defines both what entities your mod has available and what can be done with them. This isn't a perfect world and Hammer won't read from the mod code itself, so it's important to keep the FGD up to date.
If you don't already have an FGD, save an empty text file under your mod's folder as <modname>.fgd
. Then load Hammer and visit Tools > Options
. Making sure your mod is the active configuration, click the "Add" button and navigate to your FGD.
FGDs are a different topic, so this won't be covered in detail. Just paste the following code into your new file:
@include "base.fgd"
@PointClass base(Targetname) = my_logical_entity : "Tutorial logical entity."
[
threshold(integer) : "Threshold" : 1 : "Threshold value."
input Tick(void) : "Adds one tick to the entity's count."
output OnThreshold(void) : "Threshold was hit."
]
And there you have it! Your entity is now ready for use. See Hammer Entity Tool for adding it to a map (place it in one of Valve's sample maps if you're struggling to create your own one), and Inputs and Outputs for getting it doing things.
Using your newly created entity in Hammer
To use this entity in your mod, you will have to place it into a map using Hammer (unless you use/insert it programmatically elsewhere). This tutorial assumes you already have a map to work with and basic knowledge of Hammer; a very basic map made up of an empty room with an "info_player_start" in the middle works.
In this map, we'll place three items on the floor, along with a grenade. Each time the player grabs one of the items, our logical entity's counter will be incremented by 1. When it reaches a threshold of 3 (which we will set from Hammer), the grenade explodes. All we have to do is insert the objects/entities and define their relationships to each other. Simple enough, right?
Start by adding your entity from Hammer; select the Entity Tool (Shift+E), and on the right hand side, select "Entities" under "Categories" and "my_logical_entity" under "Objects".
Click any point in the map to choose the location of the entity to be created. Since this is a logical entity, it will be immaterial and invisible to the player, so its location is not that important; just put it where it won't bother you later.
Once the location has been pinpointed, right-click the selection and choose "Create Object"—your logical entity will be added to the world.
What you'll immediately want to do is name this logical entity; right-click it in any viewport and choose "Properties" (Remember to pick the Selection Tool (Shift+S) first).
This brings up the Properties menu. There, you can already see two fields:
Name - the name of that particular instance of "my_logical_entity" in the map
Threshold - property that refers to the member variable m_nThreshold from our C++ class, which we called "threshold" in the data description table and exported as "Threshold" in the .fgd file for Hammer to use. This is the count at which the entity will send out an "OnThreshold" signal/event.
So, let's call this entity "MyFirstCounter". And let's set the "Threshold" to 3. (Click the variable name in the list on the left, enter the desired value in the field on the right).
Once everything is set, apply and close the Properties dialog.
Next, we'll place our items on the map.
First, add the grenade. Select the Entity Tool, choose the Object "npc_grenade" and create that new entity somewhere in your map. Go into its properties and give it the name "MyBoobyTrap".
Now, add one item the user can pick up. For instance, "item_ammo_pistol." Go into its Properties dialog, and give it the name "MyPistolAmmo". Then go to the second tab in the Properties dialog, Outputs, and add a new Output. Select the appropriate items in the dropdown boxes: OnPlayerPickup is obviously the event we want to use as a trigger, the target is "MyFirstCounter," which will be incremented thanks to the "Tick" input function (to be selected in the "Via this Input dropdown". This will tell the game to order the counter to run its "Tick" function, which increments the counter by 1, when the player picks up the pistol ammo clip.
Once everything is set up, apply and close the Properties dialog. Copy the pistol ammo and paste two or more extra copies. All their properties and inputs/outputs will be copied over, and they will share the same name.
Now, go back to your logical entity's Properties, and modify its Outputs so that reaching the counter's Threshold value causes the grenade to "Ignite" (a predefined function of Valve's npc_grenade entity). Your output function will be "OnThreshold", the target will be "MyBoobyTrap" and the input you want is "Ignite". You can also set a time delay, if you'd like. This will cause the Valve-defined Ignite function of the grenade to run as soon as the counter hits its threshold (OnThreshold event generated).
Everything is now in place. Select File > Run Map or press F9 (make sure you've compiled the project first), and compile the map. In the map itself, you will be able to pick up the pistol ammo and watch your grenade explode as soon as you grab the third one (shoot some bullets if your gun is too full to pick up ammo).