Authoring a Brush Entity

From Valve Developer Community
Jump to navigation Jump to search
English (en)Русский (ru)Translate (Translate)


This tutorial assumes you have completed and understood Authoring a Logical Entity and, ideally, Authoring a Model Entity.

Our last example dealt with giving entities a model. Here we'll use part of the world's BSP architecture (a "brush") to represent our entity and to decide how it collides and moves around the world. We'll also look at the touch function, available to all entities, which we will use to make the entity move when touched.

Declaration and DATADESC

#include "cbase.h"
#include "triggers.h"

class CMyBrushEntity : public CBaseTrigger
{
public:
	DECLARE_CLASS( CMyBrushEntity, CBaseTrigger );
	DECLARE_DATADESC();

	void Spawn();

	void BrushTouch( CBaseEntity *pOther );
};

LINK_ENTITY_TO_CLASS( my_brush_entity, CMyBrushEntity );

BEGIN_DATADESC( CMyBrushEntity )
	
	// Declare this function as being the touch function
	DEFINE_ENTITYFUNC( BrushTouch ),

END_DATADESC()

A brush entity can inherit from CBaseEntity if it really wants to, but in our example we'll take advantage of some code that's already written for brush entities in CBaseTrigger. To access it we need a second include: triggers.h.

DEFINE_ENTITYFUNC is a badly-named command to declare what function should be executed OnTouch - i.e. when the entity touches or is touched by another. Aside from our intended use, it's good for explode-on-contact ordnance, flying entities colliding with things, picking up health etc. (on foot or in a vehicle), triggers, and so on.

There is no constructor for this entity as there are no variables to initialise.

Spawn()

void CMyBrushEntity::Spawn()
{
	BaseClass::Spawn();

	// We want to capture touches from other entities
	SetTouch( &CMyBrushEntity::BrushTouch );

	// We should collide with physics
	SetSolid( SOLID_VPHYSICS );
	
	// Use our brushmodel
	SetModel( STRING( GetModelName() ) );

	// We push things out of our way
	SetMoveType( MOVETYPE_PUSH );

	// Create our physics hull information
	VPhysicsInitShadow( false, false );
}

SetTouch() behaves like SetThink(). The default touch function is StartTouch() and we're only making one with a different name as a demonstration.

We'll be simulating collisions with VPhysics. Since this entity will never rotate you may think it more sensible to use SOLID_BBOX, like we did with the model entity, but bear in mind that the brush or brushes tied to this entity could easily be forming a complex shape. We shouldn't use SOLID_BSP either, as that only works for non-interactive world brushes. Which leave two options: either SOLID_VPHYSICS or SOLID_NONE. Obviously, to achieve our goals, we want SOLID_VPHYSICS.

A brush entity never has a pre-defined model. We always call SetModel( STRING( GetModelName() ) ) to use whatever the map has given us. Note how we call three functions within each other here: One to return the model name, another to convert it to a string, and the third to act on the returned data. We are able to do this because these, unlike our void functions, STRING() and GetModelName() return values.

MOVETYPE_PUSH is the behaviour you'll be used to from most brush entities; moving platforms, buttons, doors, and so on. It simply pushes whatever is in its way out of the way, stopping and potentially applying crush damage to obstructions when it can move no further. The only caveat for this behaviour is that it won't collide with world brushes, as it is assumed that the mapper has compensated for this by limiting the entity's freedom of movement. (Limiting our entity's movement won't be possible through any means, but you could easily make it so with some variables.)

Finally, we initialise our VPhysics collisions. VPhysicsInitShadow(), as opposed to VPhysicsInitNormal() and VPhysicsInitStatic(), creates a physics object that collides with other physics objects without being affected by them itself. It follows the entity rather than leading it. Hover your mouse over the function's name and you'll see the arguments it takes - MOVETYPE_PUSH overrides both, but keep them false anyway to avoid any unnecessary calculations.

BrushTouch()

void CMyBrushEntity::BrushTouch( CBaseEntity *pOther )
{
	// Get the collision information
	const trace_t &tr = GetTouchTrace();

	// We want to move away from the impact point along our surface
	Vector	vecPushDir = tr.plane.normal;
	vecPushDir.Negate();
	vecPushDir.z = 0.0f;

	// Uncomment this line to print plane information to the console in developer mode
	//DevMsg ( "%s (%s) touch plane's normal: [%f %f]\n", GetClassname(), GetDebugName(),tr.plane.normal.x, tr.plane.normal.y );

	// Move slowly in that direction
	LinearMove( GetAbsOrigin() + ( vecPushDir * 64.0f ), 32.0f );
}

This function uses a trace_t, or "traceline". This is a special type of vector that is mainly used either to decide what something has hit, be that a bullet, an NPC's line of sight, or a debug command, or to work out where something that has hit an entity has come from. We are using it in the latter sense.

A bit of optimisation: we declare tr as a const ("constant") because we will never need to change it. This reduces overhead slightly when compared to a normal, dynamic variable declaration.

With our traceline in place we extract the "normal" of the "plane" (i.e. face) on which our brush was hit. This returns the vector of the face struck without considering the angle it was struck at. For a cuboid brush this would give axes of either 0 or 1, since its sides are all at right angles. The easiest way of understanding this principle is by uncommenting the DevMsg command, enabling developer mode, and experimenting with brushes of different shapes yourself.

We then flip the vector around, so that it points away from the impact rather than toward, remove any z-axis (up/down) movement it might have (in order to stop it from flying into the sky where the player can't easily get at it), and call LinearMove(), a movement function we've inherited from CBaseTrigger.

We multiply vecPushDir for LinearMove(). This is because its length is always 1, and multiplication by a number (float or int) does not affect any of a vector's axes.

To sum up this final line of code: move 64 units away from your current position in this direction, at 32 units per second. Vectors can be tricky to wrap your head around, so don't feel left behind if you don't understand them yet. Keep experimenting, and check Wikipedia for further explanation.

FGD entry

@include "base.fgd"

@SolidClass base(Targetname) = my_brush_entity : "Tutorial brush entity."
[
	
]

The working entity

You can't spawn brush entities from the console, so you'll have go into Hammer and tie a (preferably smallish) world brush to my_brush_entity. There's a guide to doing this here. Compile the map and you'll be able to start playing around.

It won't stop at the edges of my map
As discussed earlier, we are using a type of movement that assumes constraints. We don't have any, except in the z-axis.
It will crush crates and boxes, but not NPCs
The crushing is a function of the crates' and boxes' VPhysics simulation, and not related to anything we're doing. NPCs don't get crushed because, with a few exceptions, they aren't physically simulated.
Sometimes it moves towards the impact, not away from it!
There is a momentary delay in BrushTouch() being called that allows for the touching entity to begin bouncing away before a measurement is made. This inverts tr.plane.normal's axes. Find the dot product of the direction vector, and the surface normal of the brush face. If it's positive, it's pointing away from the face. Negative, it's pointing into the face.

See also