Entity Programming - Writing New Entities Last edited 1 month ago2021-11-06 22:06:43 UTC

Half-Life Programming

The previous page outlined many features found in CBaseEntity that you can override and/or use to write entities. Here, you will learn how to write a standard map entity, as well as how to write entries in the FGD for the map editor.

To get the hang of entity writing, let's start with something that is extremely simple, yet still potentially useful for mappers. The idea is an entity that, when triggered, will print a custom message (set by the mapper) to the console. It can be used to debug some entity setups and sequences, or for subtle Easter eggs in the console.
If you haven't read the Overview page, it'd be a very good idea to do so, so you can familiarise yourself with some important methods that you can override. To be precise, read up on Use.

target_print - implementation

Assuming that you've already created a new .cpp file for the entity, in the dlls folder, we will start off by including the necessary headers:
#include "extdll.h"
#include "util.h"
#include "cbase.h"
They absolutely have to be in this order, otherwise there will be compiler errors.
Next, we declare the entity class itself:
class CTargetPrint : public CBaseEntity
{

};
We will also add LINK_ENTITY_TO_CLASS below:
LINK_ENTITY_TO_CLASS( target_print, CTargetPrint );
This will essentially "register" the entity, so that the engine can instantiate it whenever it reads "target_print" in the BSP. Under the hood, LINK_ENTITY_TO_CLASS declares a global, exported function, which the engine will look up. It's quite dirty and very insecure, but it worked for Valve back in the 90s.

Now, stop for a moment, and think about what the entity is supposed to do. This entity, when it spawns, will basically do nothing, so it doesn't really need a Spawn method. The only thing that will really happen is when the entity is triggered by another entity, whether it's a button or something else. So, we will override Use.
class CTargetPrint : public CBaseEntity
{
public:
   void Use( CBaseEntity* pActivator, CBaseEntity* pOther, USE_TYPE useType, float value ) override;
};

LINK_ENTITY_TO_CLASS( target_print, CTargetPrint );

void CTargetPrint::Use( CBaseEntity* pActivator, CBaseEntity* pOther, USE_TYPE useType, float value )
{

}
From here onward, it's quite simple. We just need to print our message:
ALERT( at_console, "Hello world\n" );
However, a question arises: how do we print a custom message? Surely we have to store it somewhere.
Generally, we can approach this in 2 ways, either: For the sake of simplicity, let's go with the first route. All entvars_t variables are automatically loaded from entity data in the BSP. By default, they're 0. So it's just a matter of choosing which variable we want to use. Let's use netname.

Since netname is a string_t, we will need to convert it to a C string:
ALERT( at_console, "%s\n", STRING( pev->netname ) );
This part is basically done. You could now build the DLL, go to your map editor, and create this entity by creating another entity, renaming its classname and adding keyvalues manually.

target_print - FGD

Of course, this isn't user-friendly. Mappers would rather want to use the entity tool to easily place the entity into the map, and have all relevant properties already listed to them. This is why we write FGDs.
FGDs actually don't have anything to do with game code. The engine doesn't parse the FGD, the game DLL doesn't parse the FGD, and especially not the client DLL. It is only relevant to the map editor, and the only thing the engine reads is entity data from the BSP.
You could take 2 approaches, yet again, either: Here, we will take the 1st route again, because you won't have to edit your game configuration, and the HL FGD provides many base classes for its entries.
The FGD format has its own, quite extensive syntax, so covering it whole is out of scope for this page. A dedicated article for FGD editing will be written at some point. In short, you write simple point entities the following way:
@ClassType base( B ) = entity_classname : "Description of the entity"
[
   keyvalue(datatype) : "Keyvalue title" : "Keyvalue default value"
]
Where:
  • ClassType can be BaseClass (to define a base), PointClass (to define point entities), SolidClass (to define brush entities)
  • B can be any base, such as Targetname
  • entity_classname is the classname that must match the one in LINK_ENTITY_TO_CLASS
  • datatype can be string, integer and so on
It is best to read entries in your FGD to get an idea for the syntax and other rules.
Our entity really only needs 2 keyvalues: targetname, so it can be triggered by other entities, and netname, to store the message.
@PointClass base( Targetname ) = target_print
[
   netname(string) : "Message text"
]

Conclusion

If everything went right, you should see your brand new entity in the map editor's entity list:
User posted image
And edit its properties nicely:
User posted image
Finally, in-game, you can see the message:
User posted image
Note: you need to turn on developer mode (developer 1) in order to actually see it in the console.

This entity still isn't entirely done. You can complete it and enhance it with the following: You can find the source code for this entity here:
https://github.com/Admer456/halflife-twhl-tutorials/tree/entprog/writing-new-ents

Comments

You must log in to post a comment. You can login or register a new account.