Today we’re going to create a pair of entity classes; one of which will work like a brush that only collides against players, and its opposite which collides with everything but players. However, these entities work best when they are comprised of clipping brushes, and there’s a quirk with clip in brush entities we must work around first.
If you create a func_wall and texture it all over with clip, you would be disappointed to find it doesn’t work in standard engines – you never collide with it. The issue is that clipping brushes do not give an entity its dimensions, and our entity contains nothing else, so it ends up zero-sized and gets ignored. Our workaround today is to sandwich a clipping brush between two thin conventional brushes. These outer brushes give the entity its dimensions, and the clipping brushes within the bounds start colliding (see below for illustration). You can bury the regular brushes in the world geometry.
On to the code! We start with a set of functions which make things solid and non-solid respectively to add to client.qc.
void(string target_class) MakeClassSolid =
{
entity e;
e = world;
while( (e = find(e, classname, target_class)) )
{
e.solid = SOLID_BSP;
e.movetype = MOVETYPE_PUSH;
setmodel(e, e.mdl);
e.model = string_null;
}
};
void(string target_class) MakeClassNonSolid =
{
entity e;
e = world;
while( (e = find(e, classname, target_class)) )
{
e.solid = SOLID_NOT;
e.movetype = MOVETYPE_NONE;
setmodel(e, "");
}
};
The highlighted line above ensures that the entity is invisible while solid. We want to call these functions using the PlayerPreThink and PlayerPostThink functions to make func_player_clip solid during the player’s physics then non-solid afterwards, and vice-versa for func_nonplayer_clip.
Add the following to the very top of PlayerPreThink
MakeClassSolid("func_player_clip");
MakeClassNonSolid("func_nonplayer_clip");
Add this one to the very top of PlayerPostThink
MakeClassNonSolid("func_player_clip");
MakeClassSolid("func_nonplayer_clip");
All which remains is to add the spawn functions for our new entities. They are essentially just initialisation; the entities themselves don’t generate any think or touch events to function, as they are driven entirely by the code in the player functions.
void() func_nonplayer_clip =
{
self.mdl = self.model;
self.solid = SOLID_BSP;
self.movetype = MOVETYPE_PUSH;
setmodel(self, self.model);
self.modelindex = 0;
};
void() func_player_clip =
{
self.mdl = self.model;
};
Aside: Zero Player Game
Why do we make the func_nonplayer_clip entity solid, but not its opposite number? Well, imagine that we run a co-op server but no players have connected yet. Then PlayerPostThink never runs, but we still want the func_nonplayer_clip to be solid for the sake of patrolling monsters etc. It’s a good question to pose of any code you write relating to the player – what happens when there are no players? It forces you to avoid the assumption that The Player is a single entity, and so benefits co-op in general.
And now we’re done. Download clip.zip for a simple test map. In the first room you will encounter knights, and the blue tiles on the floor mark where the func_nonplayer_clip pillars are placed. One set of blue tiles are different; this pillar is made of regular brushes instead of clip brushes, can you tell the difference it makes? In the following room, you must navigate a maze of func_player_clip to reach your prize, but beware the zombies who can pass through the walls.
Important message: This article originally had fancier code with optimisation. Spike showed me that these optimisations were occasionally dangerous in every engine, rising to frequently incorrect in certain engines. The post has been edited to remove these optimisations.
