Authoring a Logical Entity

From Valve Developer Community
Revision as of 19:07, 16 January 2009 by Gparent (talk | contribs) (An int can store a character. A character IS an integer.)
Jump to navigation Jump to search

Logical entities are the simplest of entities because they have no position in the world, 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.

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

To get anywhere with our code we need to let the compiler know what "libraries" we are going to be calling on. Consider the commands we are going to be using to be books from these libraries. Insert this line of code:

#include "cbase.h"

cbase.h provides Valve's basic set of commands for creating entities. Since we are creating their very simplest form, we only need the one base library. In more complex cases there can be many, many different includes.

The opening "c" means that this is a server-side library, and the closing ".h" means that it's a header file instead of a full .cpp - but don't worry about that for now.

Declaring the class

This section will be quite long, but once you're finished with it you'll have a grasp on the outline of all C++ code.

In C++, instructions are handled by "objects". Think of them as the librarians of our ongoing analogy.

Objects are created from templates like the one we are about to define:

class CMyLogicalEntity : public CLogicalEntity
{
public:
	DECLARE_CLASS( CMyLogicalEntity, CLogicalEntity );
	...
};

This whole process is known as "declaring a class" (not just the DECLARE_CLASS command, despite its name!). Every version of your entity in the world is another "instance" of this template. The same is true of every entity: every Combine Soldier you've ever seen running around is an independent instance of the same C++ class.

In the first line of code, we start by saying that we're declaring 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 (that we can reach thanks to our include of cbase.h earlier).

Inheriting means that we're working on a copy of the original class, overriding and extending it instead of starting entirely from scratch or changing the original class itself. It's almost exactly like copying a file on your desktop: you get the contents of the original, and can edit it separately.

We then open a set of curly parentheses. This means that all of the instructions inside are members of this class. Note how there isn't a line-ending semicolon here. We're still, technically, writing the same line of code.

Next is "public:", which means that the commands coming up will need to interact with other .cpp files (We'll make some private commands in a moment). Each public command creates a little more overhead and increases the potential for nasty bugs, as well as adding another thing you have to think about when trying to interact with the class somewhere else. Only commands that really need to be public should be.

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 class.

The round parentheses mean that we are passing "arguments", or "parameters", to it: to continue our analogy, we're saying (in this case) what book we want filing and where it belongs. If you're being observant you will already have noticed that we are sending it the same key information that's in the first line, separated, or "delimited", with a comma.

This DECLARE_CLASS command means that when the class is initialised as your mod starts, somewhere else in the codebase some code written by Valve is performing lots of obscure and tedious legwork for you. The closing semicolon tells the compiler that this is the end of a command and a new one is about to start (line breaks and whitespace aren't taken into account - the compiler just sees a continuous stream of characters).

Closing the declaration (ahead of time)

Don't type the ellipsis (...) - it's only there to show you 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.

Note.pngNote:You don't normally need both } 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 directly from someone else's computer instead of first copying it to your own - only there's no network delay, since in this C++ case everything is on the same piece of Random Access Memory hardware.

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 objects. 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 however, nor floating point numbers. C++ is very strict about these kinds of things!

m_OnThreshhold 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 it is strongly recommended.

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) that is giving the class an "entity name". The programming code doesn't refer to the entity by the same name that the I/O system and Hammer does for reasons that you'll understand later.

Data description table

The data description table is a macro that gives Source metadata about whatever members of the class you mention in it. The comments should explain what each 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 brown "string" values. 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 it's correct.

// Start of our data description for the class
BEGIN_DATADESC( CMyLogicalEntity )
	
	// For save/load
	DEFINE_FIELD( m_nCounter, FIELD_INTEGER ),

	// Links our member variable to our keyvalue from Hammer
	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 is a function, our output is that COutputEvent variable we declared earlier. This is because, once generated, an output leaves the class it originates from immediately; any processing is done beforehand.

When you're writing your own datadesc tables, remember to include a comma after each DEFINE_* command.

Note.pngNote:There are no semicolons here. This is extremely unusual!

Creating the input function

Now we are going to "define" our input function. This is the last C++ step. Notice that the first line is identical to the one we entered in the class declaration, except for the addition of CMyLogicalEntity::. This ensures it is associated with the correct class. In this case there is only one class in the .cpp for it to be associated with, but there can be many.

//-----------------------------------------------------------------------------
// 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 within the function:

m_nCounter++
C++ shorthand for "increment by one". In full, it would be m_ncounter = m_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.
{ and }
This is a "nested" pair of square parentheses, within the "parent" set for the function. It groups all of the commands the if statement will execute if it evaluates to true. 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 the COutputEvent 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>), where MyLogicalEntity1 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:
  1. The entity that started this whole chain off (in Source lingo the "activator").
  2. 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

Hammer options dialogue

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 Also

Template:Otherlang:en Template:Otherlang:en:ru