Creating New Entities

A Tutorial for Half-Life

Members see zero ads. Signup for free

How to create a brand new entity! + writing the FGD

1. Creating New Entities

Last time, in Introduction to HL Coding, we made a func_wall print "Hello world!" into the console.


But what if you don't want that? What if you want a special entity that will print anything into the console?

Let's call it trigger_conprint, as in "console-printing trigger".

The first step would be to create a class that inherits from CBaseEntity, and then define a Use function that would print a locally-stored message. That message is set by the mapper, of course.

1.1. Creating a new source file

So, open up your IDE and load the solution. Open the hldll project.


Now, we have 2 options: either modify an existing CPP file such as triggers.cpp, or create a new source file. Let's do the latter.

Enter the "dlls" folder, and right-click it. Then go to Add and click New Item.


Make sure, for the sake of being tidy, to create the new .cpp file in [ProjectFolder]/dlls/, because Visual Studio will default it to [ProjectFolder]/projects/vs2017/. Let's name it consoleents.cpp, as in "Console entities".


2. Writing the new entity

Of course, the new source file is empty. Before we start defining the entity, we will have to include the required headers. And then we can base our class off CBaseEntity and do stuff. :)

2.1. Necessary includes

Usually, when writing entities, you will need at least these 3 headers:
extdll.h
util.h
cbase.h


extdll.h contains a bunch of other includes for: shared engine/DLL constants, the shared server-client header, shared engine interface, the vector class etc. Right now, you don't need to know what exactly these are, but, they are very important.

util.h contains a bunch of utilities and definitions. For example, the M_PI constant:
// In case this ever changes
#define M_PI 3.14159265358979323846
And many functions:


cbase.h is where CBaseEntity is at. 


So, let's actually include them:
#include "extdll.h"
#include "util.h"
#include "cbase.h"

2.2. The CTriggerConsolePrinter class

Creating an entity is generally in 3 or 4 parts:
1. Defining the class itself
2. Linking the class to an actual map entity
3. Defining the class' functions
4. Implementing a save-restore table if needed

2.2.1. Defining the class

So, let's define the class:
class CTriggerConsolePrinter : public CBaseEntity
{
But what is a class?
Think of it as a blueprint. It contains all characteristic variables and methods (functions) for the objects that will be built with this blueprint. Objects are then instances of these classes and they all get the same properties from the class. Each entity has its own class.

public CBaseEntity means that this will inherit CBaseEntity's public variables and functions. That means everything from the pev structure (origin, classname, targetname, velocity, angles etc.), and the functions for spawning, getting used/triggered by other entities etc.

We will only need three: Spawn, KeyValue and Use.

Spawn is what the entity will do as soon as it spawns. Ours won't do anything, so it'll be empty.
KeyValue is what actually reads the keyvalues from the map, and assigns it to the entity's variables. We'll see how to read strings soon enough.
Use is called when 1) the entity gets triggered by another entity in the map 2) the player uses the entity.

So, the class definition will look like this:
class CTriggerConsolePrinter : public CBaseEntity
{
public:
	void Spawn();
	void KeyValue(KeyValueData *pkvd);

	void Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value);
};
public: means that the class' variables and functions will be accessible easily. private: will mean that only the class itself can access these. We'll get to these later on in detail, in one of my future tutorials.

As you can see, all these functions are voids, meaning that they don't return anything. If you don't quite understand, look at this:
int AddTwoNumbers( int x, int y )
{
	return x + y;
}

int main()
{
	int a;
	a = AddTwoNumbers( 2, 3 );
	// etc.
}
AddTwoNumbers is a function that returns some number, so you can assign it to variables, in this case the variable a. Just like this, you can do the following:
CBaseEntity *pTarget = UTIL_FindEntityByClassname( /* some stuff */ );
The entity-finding function will return a pointer to another entity, so we can mess around with other entities remotely from one entity. This is how env_render works.

Now, we'll also need a local variable to store our message.
Add this right under the Use function:
private:
	string_t m_iszConMessage;
This is our console message that will get printed. :)
Keep in mind, however, that string_t in GoldSrc is not actually a string. It is actually a pointer to an index in GoldSrc's string table. Don't worry, it's nothing complicated. We'll cover this more in detail in one of the future tutorials.

Finally, the class definition should look like this:
class CTriggerConsolePrinter : public CBaseEntity
{
public:
	void Spawn();
	void KeyValue(KeyValueData *pkvd);

	void Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value);

private:
	string_t m_iszMessage;
};

We're done with the 1st part.
Now, after the }; part, add the following:
LINK_ENTITY_TO_CLASS(trigger_conprint, CTriggerConsolePrinter);
This is the "magical glue" that connects the entity's name to the class. And yes, you can actually link multiple entity names (trigger_consoleprinter, trigger_cp, whatever) to the same class. But don't attempt to do it the other way around.

2.2.2. Defining the functions

Now, right after the entity-to-class linking, write the following:
void CTriggerConsolePrinter::Spawn()
{
	// Nothing
}
Quite simply, it'll do nothing when it spawns. It is a point entity, therefore it doesn't need to have a model, it doesn't have to precache anything like sounds, models etc., so it'll pretty much do nothing when it spawns.

void CTriggerConsolePrinter::KeyValue(KeyValueData *pkvd)
{
	if (FStrEq(pkvd->szKeyName, "conmessage"))
	{
		m_iszMessage = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}

	else
		CBaseEntity::KeyValue(pkvd);
}
This one is a bit more complex.

What happens is the following:
KeyValue will scan for every possible keyvalue. Initially, it will look if any given keyvalue name matches "conmessage". If it does, it will allocate a string in the string table, and return a pointer to m_iszMessage. Once that is done, it'll say that the key-value pair was understood (fHandled = TRUE).

If it doesn't find a matching keyvalue, it will look around for CBaseEntity's keyvalues, like origin, targetname etc.

FStrEq is actually a string comparison. It is a strcmp (string compare) in disguise:
inline BOOL FStrEq(const char*sz1, const char*sz2)
	{ return (strcmp(sz1, sz2) == 0); }
The original strcmp will return 0 if the strings match, so FStrEq just returns whether that number is 0 or not. And thus you won't need to write this:
if (!strcmp("a", "a")
{ /* Some stuff */ }
Beginners usually get confused when it returns 0 yet the two strings match, hence FStrEq, as it makes more sense. :)

Next up:
void CTriggerConsolePrinter::Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value)
{
	ALERT(at_console, "%s", STRING(m_iszMessage));
}
Here is our good old ALERT() again. :)
If you've used cout in C++, you know that you'd usually display a string like this:
std::string Name = "BANGA BANG BANGER";

cout << "My name is: " << Name << "\n";

However, ALERT() is actually just like printf(), except it has console alert types.
With printf(), you'd display the same message like this:
std::string Name = "Gordan Rocketcrotch";

printf( "My name is: %s", Name );
As you can see, it prints a formatted string. Think of %s as an indicator of what type of data to print.
%s is for strings, while %i and %d are for integers. Likewise, %f is for floats.
But, since we're only printing a single string, we'll need no more than just a %s.
printf( "%s", Name );
I agree it's a little bit inconvenient when you want to print just a single variable, but still, it's not a big deal.

So, the entire .cpp file will look something like this:
/* 
	==== CONSOLE ENTITIES ====
	They do stuff in the console.
*/

#include "extdll.h"
#include "util.h"
#include "cbase.h"

class CTriggerConsolePrinter : public CBaseEntity
{
public:
	void Spawn();
	void KeyValue(KeyValueData *pkvd);

	void Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value);

private:
	string_t m_iszMessage;
};

LINK_ENTITY_TO_CLASS(trigger_conprint, CTriggerConsolePrinter);

void CTriggerConsolePrinter::Spawn()
{
	// Nothing
}

void CTriggerConsolePrinter::KeyValue(KeyValueData *pkvd)
{
	if (FStrEq(pkvd->szKeyName, "conmessage"))
	{
		m_iszMessage = ALLOC_STRING(pkvd->szValue);
		pkvd->fHandled = TRUE;
	}

	else
		CBaseEntity::KeyValue(pkvd);
}

void CTriggerConsolePrinter::Use(CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value)
{
	ALERT(at_console, "%s", STRING(m_iszMessage));
}

2.3. Additions to the FGD file

Before your map editor can even know that this entity now exists, we have to modify the FGD file.
First copy the Half-Life FGD file (e.g. the one which came with J.A.C.K.), and rename it to something, and then open it.
//
// Half-Life game definition file (.fgd) 
// for Jackhammer 1.0 and above
//
// updated by Chris Bokitch
// autolycus@valvesoftware.com
// http://www.valve-erc.com/
// modified by XaeroX / support@hlfx.ru
//
You may add your own signature there if you wish. :)

Let's analyse this a little bit.
@BaseClass = Targetname 
[ 
	targetname(target_source) : "Name" : : "Property used to identify entities."
]
@BaseClass = Target 
[ 
	target(target_destination) : "Target" : : "When an entity is activated, it triggers the entity with the name specified by Target."
]
As you can see, to avoid having to retype EVERY keyvalue over and over again, there are some base classes. Each class contains a keyvalue definition (or more of them), and entities include them like this:
@BaseClass base(Targetname,Global) = Breakable
This way, the Breakable class will include the keyvalues from the Targetname and Global classes. func_breakable then inherits from the Breakable class.

So, let's scroll down to the triggers.
//
// Triggers
//

@PointClass base(Targetx) iconsprite("sprites/trigger_auto.spr") = trigger_auto : "AutoTrigger"
[
	spawnflags(Flags) =
	[
		1 : "Remove On fire" : 0
	]
	globalstate(string) : "Global State to Read"
	triggerstate(choices) : "Trigger State" : 0 = 
	[
		0 : "Off"
		1 : "On"
		2 : "Toggle"
	]
]

@SolidClass base(Targetname) = trigger_autosave : "AutoSave Trigger"
[
	master(string) : "Master" 
]
It is a good idea to remain consistent to the alphabetical order.
So let's add trigger_conprint right between these two:
@PointClass base(Targetx, Targetname) iconsprite("sprites/trigger_changetarget.spr") = trigger_changetarget : "Trigger Change Target"
[
	m_iszNewTarget(string) : "New Target"
]

@SolidClass base(Trigger, Targetname) = trigger_counter : "Trigger counter" 
[
	spawnflags(flags) = 
	[ 
		1 : "No Message" : 0 
	]
	master(string) : "Master" 
	count(integer) : "Count before activation" : 2
]

As you can see, entities in FGDs begin with either a PointClass or SolidClass type.
Our entity is a point entity, so we'll use PointClass.
@PointClass base(Targetname) = trigger_conprint : "Trigger console printer"
[
	conmessage(string) : "Console message"
]
It is really simple. trigger_conprint gets defined as a point entity class, which inherits the "targetname" keyvalue, and it has a custom keyvalue called "conmessage".

2.4. Using the entity

Lastly, we can open the map editor, utilise the new FGD file, and spawn the entity:

Of course, you will likely use something like a button or an area trigger to activate the console printer.



Generally, this is how you create a really simple point entity. This console printer entity is actually really useful when you have a special entity setup, and you need debugging. 

Maybe sometime later, I could add a section in this tutorial about alert types for this entity, like this:


In the meantime...
Happy coding! :)

Embed

Share banner
Image URL
HTML embed code
BB embed code
Markdown embed code

Credits

Key Authors
Admer456 avatar
Admer456 username pic Joined 4y ago
Offline
18,684 points Ranked 269th
29 medals 1 legendary 5 rare
  • Submitted 30 Tutorials Medal icon
  • Submitted 15 Tutorials Medal icon
  • Returned 1000 times Medal icon
  • Reached 50 subscribers Medal icon
  • Received thanks 50 times Medal icon
  • 10 submissions featured Medal icon
Writer

Submitter

Admer456 avatar
Admer456 username pic Joined 4y ago
Gone. :3
18,684 points Ranked 269th
29 medals 1 legendary 5 rare
  • Submitted 30 Tutorials Medal icon
  • Submitted 15 Tutorials Medal icon
  • Returned 1000 times Medal icon
  • Reached 50 subscribers Medal icon
  • Received thanks 50 times Medal icon
  • 10 submissions featured Medal icon
Admer456 avatar
Admer456

Creator
Sign up to access this!
Sign up to access this!
Sign up to access this!

Game

Sign up to access this!

Category

Details

Difficulty Level
Beginner

Attributes

Share

  • Share on Reddit
  • Share on Twitter
  • Share on Facebook
  • Share on Google+
  • Today's Pick
    Featured on Feb 18 2019
  • 1
  • 361
  • 3
  • 1mo

More from Submitter

WiPs by Submitter

More Other/Misc Tutorials

bcp.crwdcntrl.net tracking pixel