Accessing Other Entities

From Valve Developer Community
Revision as of 10:01, 20 March 2009 by TomEdwards (talk | contribs) (New page: {{toc-right}} In Hammer, entities can only communicate through pre-defined inputs and outputs. The good news is that in C++ this limitation goes away. The bad news is ...)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

In Hammer, entities can only communicate through pre-defined inputs and outputs. The good news is that in C++ this limitation goes away. The bad news is that you have to work a bit harder to get off the ground.

An example

CBreakable *pWall = GetFuncBreakable();
if (!pWall) return;

pWall->Break(this);

In the above code we:

  1. Declare a pointer and assign an entity we want to it (a func_breakable)
  2. Confirm that the pointer was successfully assigned a value
  3. Access a member function of the target entity
Tip.pngTip:Prefixing 'p' to the names of pointers makes identifying them later on much easier, but isn't necessary.

Pointers

A pointer is the C++ equivalent of a desktop shortcut. The current target of a pointer is called the 'pointee'. They have five core rules:

  1. Pointers are declared like a variable, except for an asterisk before the name (e.g. CBaseEntity *pOther). Don't use the asterisk anywhere else - it's just for declarations.
    Tip.pngTip:The asterisk can go before or after the space. Before is more correct, but Valve tend to place it after.
  2. You must #include the header file of any class you want to create a pointer for.
  3. A pointer can only be assigned an object that matches its class. The compiler will throw an error otherwise.
  4. When accessing members of a pointee, you must use -> instead of the usual period character.
  5. Any attempt to access an unassigned ('null') pointer will immediately crash your mod. Welcome to C++!

These aside, the syntax surrounding a pointer is the same as any other member of the current class.

Casting

Pointees must be of the same class as the pointer. But what about touch functions, which provide the other entity involved as CBaseEntity*? There's no such thing as a pure CBaseEntity, right?

Indeed there isn't. The pointee isn't CBaseEntity, but the pointer has been converted — "cast" — to act as one. This is possible when one class inherits from another, and since every entity inherits from CBaseEntity every entity can be cast to CBaseEntity*. This is essential knowledge if you don't know what kind of entity your code will be dealing with!

CBaseEntity* isn't a problem if you want to access something defined in CBaseEntity. But if you need something defined elsewhere, the pointer must be cast to a class that has access to it. This is achieved with dynamic_cast.

CMyEntity::Touch( CBaseEntity *pOther )
{
	CBreakable *pWall = dynamic_cast<CBreakable*>(pOther);
	if (!pWall) return;

	pWall->Break(this);
}

We cast pOther to CBreakable*, storing the result in pWall.

It's doubly quadruply important to check that the pointer is assigned this time because the cast will be attempted whenever the entity touches something. If pOther isn't actually a CBreakable then the operation will fail, leading to a potentially dangerous null pointer.

Note.pngNote:Remember that casting a pointer does not affect the pointee itself.

Finding entities

There isn't much point in knowing how to cast between classes if you don't have an entity to work with in the first place. gEntList is the place to get one. It's a global object so you don't need to #include anything to get it.

gEntList offers various search functions, the names of which all start with Find or Next. Each will only return one result at a time however, so need manual iteration:

Icon-Bug.pngBug:See below for why this simple example is unsafe.  [todo tested in ?]
CAI_BaseNPC *pNPC = gEntList.FindEntityByClassname(NULL,"npc_*");
while (pNPC)
{
	pNPC->SetState(NPC_STATE_IDLE);
	pNPC = gEntList.FindEntityByClassname(pNPC,"npc_*");
}

Here we:

  1. Call gEntList.FindEntityByClassname() once to get the first entity with the Hammer classname "npc_*".
    Note.pngNote:The asterisk is a search wildcard this time.
  2. Enter a loop that will continue for as long as pNPC is valid.
  3. Perform our operation.
  4. Call FindEntityByClassname() and search for "npc_*" again - but this time starting at pNPC's position instead of the list's first item.
  5. Return to the start of the loop.

But there is a bug in this code! The loop will end when when FindEntityByClassname() runs out of matching entities to return...and also if it returns an entity that can't be cast to CAI_BaseNPC* like npc_maker. This will prevent anything listed after the npc_maker from being operated on, a nasty bug that can be difficult to spot from just its symptoms.

To avoid this either search with more specific terms, or store the current result in CBaseEntity* and attempt to cast it to CAI_BaseNPC* within the loop:

CBaseEntity *pResult = gEntList.FindEntityByClassname(NULL,"npc_*");
while (pResult)
{
	CAI_BaseNPC *pNPC = dynamic_cast<CAI_BaseNPC*>(pResult);
	if (pNPC)
		pNPC->SetState(NPC_STATE_IDLE);

	pResult = gEntList.FindEntityByClassname(pResult,"npc_*");
}

See also