Home Quake
News News
Befunge Befunge
Doom Doom
Inform Inform
Quake Quake
   - Levels
   - Mods
   - Miscellaneous

RISC OS RISC OS
Misc Misc
Links Links
Contact Contact

Trigger me timbers

Or something like that

I'll be telling you lot about a few improvements that every Quake mod should have. HL has quite a few of these, and so has much greater flexibility in its levels. The improvements are:

  • Trigger_changetarget, handy for lots of things (Have a look at some HL levels)
  • Trigger_random, which randomly chooses one of two objects to trigger (Based around a % chance)
  • Trigger_explode, which explodes when triggered
  • Trigger_lots, which triggers up to 10 other objects
  • Trigger_sight, which triggers its target when it loses sight of an entity
  • Trigger_setspeed, to set the speed value of an entity (e.g. trains and path_corners. Doors and plats may work)
  • Trigger_message, to send a message to all players
  • An improved path_corner which allows you to trigger other entities and set the trains speed
  • An improved func_train, so that it can now be paused in mid movement, and stops automatically when it reaches the end of the track
  • Improve trigger_hurt and trigger_push so they can be paused
  • Improve doors so they trigger objects when they've finished opening and closing
  • Trigger_toggle, which can be used to turn on or off doors (Open/close), lights, trains (Start/stop), trigger_hurts and trigger_pushes.
  • Trigger_if, which will trigger 1 target if its target is active, or another target if it is inactive

If you want to seperate out the triggers, then trigger_changetarget, trigger_random, trigger_explode, trigger_lots, trigger_sight, trigger_setspeed, trigger_message, will all work on their own. The improved path_corner needs to go with the improved func_train in order to be any use (Unless you're only using it for monsters). Trigger_hurt and trigger_push improvements should also work fine by themselves, as should the door changes. Trigger_toggle and trigger_if are tricky though because they rely on some of the other improvements; the bits that work fine by themselves are marked in the code given below.

trig.zip (320,016 bytes) provides a sample implementation of these triggers, built around the QuakeC 1.01 source. There are also a couple of test maps, of which 'liftest' is likely to be the easiest to understand.

The New Triggers

First off, get a copy of the QC source which you want to make the changes to. Then, open a new file and enter the following (Copy & paste would be a good idea):

float TCT_ONCE = 1;
void() chtarg_use =
{
 local entity e;
 e = find(world,targetname,self.target);
 while (e != world)
 {
  e.target = self.netname;
  e = find(e,targetname,self.target);
 }
 if (self.spawnflags & TCT_ONCE)
 {
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
  self.use = SUB_Null;
 }
};
void() trigger_changetarget =
{
 // Change target's targetname to netname
 self.use = chtarg_use;
};
float TRND_ONCE = 1;
void() rnd_use =
{
 local float p;
 p = random()*100;
 if (floor(p) < self.currentammo)
 {
  self.target = self.noise;
  self.killtarget = self.noise2;
 }
 else
 {
  self.target = self.noise1;
  self.killtarget = self.noise3;
 }
 SUB_UseTargets();
 if (self.spawnflags & TRND_ONCE)
 {
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
  self.use = SUB_Null;
 }
};
void() trigger_random =
{
 // noise and noise1 are the two targs
 // noise2 and noise3 are the kill targs
 // currentammo is the prob (0 to 100) of noise/noise2
 self.use = rnd_use;
};
float TEXPL_ONCE = 1;
float TEXPL_NOSPRITE = 2;
void() expl_use =
{
 local entity e;
 T_RadiusDamage(self,activator,self.dmg,world);
 if (!(self.spawnflags & TEXPL_NOSPRITE))
 {
  e = self;
  self = spawn();
  setorigin(self,e.origin);
  BecomeExplosion();
  self = e;
 }
 if (self.spawnflags & TEXPL_ONCE)
 {
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
  self.use = SUB_Null;
 }
};
void() trigger_explode =
{
 // Simply blow up with force of self.dmg when triggered
 self.use = expl_use;
};
float TLOTS_TRIGONCE = 1;
void() tlots_use =
{
 self.target = self.noise;
 SUB_UseTargets();
 self.target = self.noise1;
 SUB_UseTargets();
 self.target = self.noise2;
 SUB_UseTargets();
 self.target = self.noise3;
 SUB_UseTargets();
 self.target = self.noise4;
 SUB_UseTargets();
 self.target = self.netname;
 SUB_UseTargets();
 self.target = self.wad;
 SUB_UseTargets();
 self.target = self.map;
 SUB_UseTargets();
 self.target = self.deathtype;
 SUB_UseTargets();
 self.target = self.mdl;
 SUB_UseTargets();
 if (self.spawnflags & TLOTS_TRIGONCE)
 {
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
  self.use = SUB_Null;
 }
};
void() trigger_lots =
{
 // Triggers noise->noise3, netname, wad, map, deathtype, mdl and noise4
 // But for some reason Quake overwrites wad, so use target
 self.wad = self.target;
 self.use = tlots_use;
};
float TSIGHT_NOMONSTERS = 1;
float TSIGHT_NOPLAYERS = 2;
float TSIGHT_NOITEMS = 4;
float TSIGHT_TRIGGERONCE = 8; // Could set killtarget to the entity
it's watching (e.g. if it's an info_notnull)
float TSIGHT_ONLYUSE = 16; // Don't check every 0.1s, only when
used by something else
void() tsight_think =
{
 local entity e;
 local float t;
 t = FALSE;
 e = find(world,targetname,self.netname);
 if (e == world)
  t = TRUE;
 else
 {
  if (self.spawnflags & TSIGHT_NOMONSTERS)
   traceline(self.origin,e.origin + (e.mins * 0.5 + e.maxs * 0.5),TRUE,e);
  else
   traceline(self.origin,e.origin + (e.mins * 0.5 + e.maxs * 0.5),FALSE,e);
  if (trace_fraction != 1)
   if ((trace_ent.classname != "player") || !(self.spawnflags & TSIGHT_NOPLAYERS))
    if (!(trace_ent.flags & FL_ITEM) || !(self.spawnflags & TSIGHT_NOITEMS))
     t = TRUE;
 }
 if (t == TRUE)
 {
  if (!(self.spawnflags & TSIGHT_ONLYUSE))
   activator = trace_ent;
  SUB_UseTargets();
  if (self.spawnflags & TSIGHT_TRIGGERONCE)
  {
   self.think = SUB_Remove;
   if (self.spawnflags & TSIGHT_ONLYUSE)
    self.use = SUB_Null;
  }
 }
 if (!(self.spawnflags & TSIGHT_ONLYUSE) || ((t == TRUE)
&& (self.spawnflags & TSIGHT_TRIGGERONCE)))
  self.nextthink = time + 0.1;
};
void() trigger_sight =
{
 // Tracks its netname
 // Then when it loses sight, it activates its target
 // Netname could be a monster, a train, a info_notnull, etc.
 if (self.spawnflags & TSIGHT_ONLYUSE)
  self.use = tsight_think;
 else
 {
  self.think = tsight_think;
  self.nextthink = time + 0.5; // Wait for objects to spawn
 }
};
float TSPEED_ONCE = 1;
void() setspeed_use =
{
 local entity e;
 e = find(world,targetname,self.target);
 while (e != world)
 {
  e.speed = self.speed;
  e = find(e,targetname,self.target);
 }
 if (self.spawnflags & TSPEED_ONCE)
 {
  self.use = SUB_Null;
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
 }
};
void() trigger_setspeed =
{
 // Simply sets the speed of the target to the speed of itself
 self.use = setspeed_use;
};
float TMESS_ONCE = 1;
void() tmessage_use =
{
 local entity e;
 sound (self, CHAN_BODY, "misc/talk.wav", 1, ATTN_NONE);
 e = find(world,classname,"player");
 while (e != world)
 {
  centerprint(e,self.message);
  e = find(e,classname,"player");
 }
 if (self.spawnflags & TMESS_ONCE)
 {
  self.use = SUB_Null;
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
 }
};
void() trigger_message =
{
 // Send a message to everyone
 precache_sound("misc/talk.wav");
 self.use = tmessage_use;
};
float TOGGLE_ONCE = 1;
float TOGGLE_OFF = 2; // Turn objects off
float TOGGLE_TOGGLE = 4; // Toggle between turning objects on or
off each time we are triggered
void() toggle_off;
void() toggle_on =
{
 // Turn on all recognized objects pointed to by target
 local entity oldself;
 oldself = self;
 self = find(world,targetname,oldself.target);
 while (self != world)
 {
  if (self.classname == "door") // ** Works fine on its own **
   if ((self.state != STATE_TOP) && (self.state != STATE_UP))
    door_go_up();
  if (self.use == light_use) // Quick hack of a light check, works fine on its own
   if (self.spawnflags & START_OFF) // Dark?
    light_use();
  if (self.classname == "func_train") // Needs the func_train improvements to work properly
   if (self.think == func_train_find) // Moving?
    train_next();
  if (self.classname == "trigger_hurt") // Needs the trigger_hurt improvements to work properly
   if (self.spawnflags & THURT_PAUSED)
    trigger_hurt_use();
  if (self.classname == "trigger_push") // Needs the trigger_push improvements to work properly
   if (self.spawnflags & PUSH_PAUSED)
    trigger_push_use();
  self = find(self,targetname,oldself.target);
 }
 self = oldself;
 if (self.spawnflags & TOGGLE_TOGGLE)
  self.use = toggle_off;
 if (self.spawnflags & TOGGLE_ONCE)
 {
  self.use = SUB_Null;
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
 }
};
// Requirements for the code in this function are the same as above
void() toggle_off =
{
 // Turn off all recognized objects pointed to by target
 local entity oldself;
 oldself = self;
 self = find(world,targetname,oldself.target);
 while (self != world)
 {
  if (self.classname == "door")
   if ((self.state != STATE_BOTTOM) && (self.state != STATE_DOWN))
    door_go_down();
  if (self.use == light_use) // Quick hack of a light check
   if (!(self.spawnflags & START_OFF)) // Light?
    light_use();
  if (self.classname == "func_train")
   if (self.think != func_train_find) // Moving?
   {
    if (self.netname != string_null)
     self.target = self.netname; // Bring back the old one
    if (self.count)
     self.speed = self.count; // Bring back the old speed too
    self.velocity = '0 0 0';
    self.think = func_train_find;
    self.nextthink = -1; // Stop it thinking
    sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
   }
  if (self.classname == "trigger_hurt")
   if (!(self.spawnflags & THURT_PAUSED))
    trigger_hurt_use();
  if (self.classname == "trigger_push")
   if (!(self.spawnflags & PUSH_PAUSED))
    trigger_push_use();
  self = find(self,targetname,oldself.target);
 }
 self = oldself;
 if (self.spawnflags & TOGGLE_TOGGLE)
  self.use = toggle_on;
 if (self.spawnflags & TOGGLE_ONCE)
 {
  self.use = SUB_Null;
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
 }
};
void() trigger_toggle =
{
 // Turns on/off a variety of triggers, funcs, etc.
 if (self.spawnflags & TOGGLE_OFF)
  self.use = toggle_off;
 else
  self.use = toggle_on;
};
float TRIGIF_ONCE = 1;
float TRIGIF_TOGGLE = 2;
// Same requirements as trigger_toggle
void() trigif_use =
{
 local entity e;
 e = find(world,targetname,self.noise3);
 self.target = self.noise2;
 if (e.classname == "door")
  if ((e.state != STATE_BOTTOM) && (e.state != STATE_DOWN))
   self.target = self.noise1;
 if (e.use == light_use)
  if (!(e.spawnflags & START_OFF))
   self.target = self.noise1;
 if (e.classname == "func_train")
  if (e.think != func_train_find)
   self.target = self.noise1;
 if (e.classname == "trigger_hurt")
  if (!(e.spawnflags & THURT_PAUSED))
   self.target = self.noise1;
 if (e.classname == "trigger_push")
  if (!(e.spawnflags & PUSH_PAUSED))
   self.target = self.noise1;
 SUB_UseTargets();
 if (self.spawnflags & TRIGIF_TOGGLE)
 {
  self.target = self.noise1;
  self.noise1 = self.noise2;
  self.noise2 = self.target;
 }
 if (self.spawnflags & TRIGIF_ONCE)
 {
  self.use = SUB_Null;
  self.think = SUB_Remove;
  self.nextthink = time + 0.1;
 }
};
void() trigger_if =
{
 // Activates 1 target (noise1) if the 1st ent it finds is 'on'
 // Activates 2nd target (noise2) if 1st ent is off
 // Looks for noise3
 self.use = trigif_use;
};

This is the changetarget, random, explode, lots, sight, setspeed, message, toggle and if code. They're simple point based triggers, which need something else to activate them. Hopefully you can see how they work. Make sure you include the QC file name in progs.h, below plats.qc (At the bottom should be fine). They all have spawnflags, even if it is as simple as 'trigger once'. Note that in the trigger once code, the triggers use code is set to SUB_Null to stop it getting re-triggered before it deletes itself.

Path_corner and func_train

Now we get on to the train improvements. This is in no particular order, so just do as I say and it will all come out in the wash.

Open plats.qc, and scroll down to train_use(). Replace:

if (self.think != func_train_find)
  return; // already activated

with:

 if ((self.think != func_train_find) && (self.classname != "misc_teleporttrain"))
 {
  if (self.netname != string_null)
   self.target = self.netname; // Bring back the old one
  if (self.count)
   self.speed = self.count; // Bring back the old speed too
  self.velocity = '0 0 0';
  self.think = func_train_find;
  self.nextthink = -1; // Stop it thinking
  sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
  return; // already activated
 }

This bit of code is called when a train is triggered by a button, and if the train is running then it brings back its old target and speed (Just because of the way the rest of the plat code was written) to make sure it doesn't skip out any corners. It then sets its velocity to 0 to stop movement, sets self.think to func_train_find to make sure it knows its been stopped (Otherwise this code would get called again), and sets nextthink to -1 to stop func_train_find actually being called. The sound() line causes it to play its 'I've just reached a path_corner' sound, so that it doesn't continously play the movement sound. It also safeguards against Shub's teleporttrain pausing each time you teleport through it.

Just above train_wait(), insert the following function:

void(entity e) trig_pc =
{
 local entity oself;
 oself = self;
 self = e;
 self.noise = self.target; // Temp space
 self.target = self.netname; // One to trig
 SUB_UseTargets();
 self.target = self.noise;
 self = oself;
};

This just triggers a path_corners target for it. The function will be used in a minute, but basically all it does is swap the paths 'next path' entry with 'netname' (The entity we want to be triggered), calls the generic trigger code, then swaps them back to make sure the path works ok.

Now go into train_wait() itself, and add at the start:

 local entity targ;
 targ = find(world,targetname,self.netname);
 if (targ.netname)
  trig_pc(targ); // Trig stuff; trigger path_corner just reached to activate its target

Train_wait is called just as the train reaches a path_corner, which is when we want it to trigger the path_corner's target (Not when we leave the path_corner, or are just approaching it).

Now in train_next(), just underneath

 local entity targ;

add the lines

 self.netname = self.target;

This sets up the 'old path' which the pausing code uses.

 SUB_CalcMove (targ.origin - self.mins, self.speed, train_wait);

you need to add

  self.count = self.speed; // Old speed for when you pause

the trains

 if (targ.speed)
  self.speed = targ.speed;
 if (self.velocity != '0 0 0')
 {
  self.worldtype = 1;
  sound (self, CHAN_VOICE, self.noise1, 1, ATTN_NORM);
 }
 else if (self.worldtype)
 {
  self.worldtype = 0;
  sound (self, CHAN_VOICE, self.noise, 1, ATTN_NORM);
 }

to control the speed changes. The speed changes come after SUB_CalcMove because we want the speed to change when we reach this new targ waypoint, not when we approach it. The code also handles the sounds, so make sure you remove the other

 sound (self, CHAN_VOICE, self.noise1, 1, ATTN_NORM);

The sound system just plays a sound if the train is moving, and plays the stop sound if it has just finished moving to stop it repeating endlessly.

Now in func_train_find, just after

 self.target = targ.target;

you need to add the line

 self.netname = self.target;

I'm not entirely sure that this line is needed, but it does help to include it to make sure the train has an old wp to fall back on if it gets paused early on.

The trains now have controllable speed and a pause ability. Now we need to finish the path_corner trigger system, by adding some code to ai.qc to make sure that monsters trigger entities when they touch the paths.

In t_movetarget, go down to the line

 if (other.enemy)
  return; // fighting, not following a path

and add the following beneath it:

 if (self.netname)
 {
  self.noise = self.target; // Temp space
  self.target = self.netname; // One to trig
  SUB_UseTargets();
  self.target = self.noise;
 }

This, like trig_pc, calls the use target code. That's the basis of the system.

Trigger_hurt and trigger_push

Now on to trigger_hurt and trigger_push. Find the trigger_hurt section of triggers.qc, and add the lines

float THURT_PAUSABLE = 1;
float THURT_PAUSED = 2;

just above hurt_on(); Now go down below hurt_touch(), and add

void() trigger_hurt_use =
{
 if (self.spawnflags & THURT_PAUSED)
 {
  self.spawnflags = self.spawnflags - THURT_PAUSED;
  self.touch = hurt_touch;
 }
 else
 {
  self.spawnflags = self.spawnflags | THURT_PAUSED;
  self.touch = SUB_Null;
 }
};

Now change trigger_hurt itself to:

void() trigger_hurt =
{
 InitTrigger ();
 if (self.spawnflags & THURT_PAUSABLE)  //
  self.use = trigger_hurt_use;     // *** Trig stuff
 if (!(self.spawnflags & THURT_PAUSED)) //
  self.touch = hurt_touch;
 if (!self.dmg)
  self.dmg = 5;
};

That's trigger_hurt done. A spawnflag controls whether it can be paused, and another spawnflag controls whether it is paused at the moment.

The changes are virtually the same for trigger_push. Underneath

float PUSH_ONCE = 1;

add the lines

float PUSH_PAUSABLE = 2;
float PUSH_PAUSED = 4;

Then under trigger_push_touch add the function

void() trigger_push_use =
{
 if (self.spawnflags & PUSH_PAUSED)
 {
  self.spawnflags = self.spawnflags - PUSH_PAUSED;
  self.touch = trigger_push_touch;
 }
 else
 {
  self.spawnflags = self.spawnflags | PUSH_PAUSED;
  self.touch = SUB_Null;
 }
};

And then change trigger_push to:

void() trigger_push =
{
 InitTrigger ();
 precache_sound ("ambience/windfly.wav");
 if (self.spawnflags & PUSH_PAUSABLE)
  self.use = trigger_push_use;
 if (!(self.spawnflags & PUSH_PAUSED))
  self.touch = trigger_push_touch;
 if (!self.speed)
  self.speed = 1000;
};

That's that one done as well. You can now have lots of fun making spaceships which you can depressurise!

Func_doors

Here's one of the shorter ones - making doors trigger things when they open and close. Open doors.qc, and at the top of door_hit_top place:

 if (self.netname)
 {
  self.deathtype = self.target;
  self.target = self.netname;
  SUB_UseTargets();
  self.target = self.deathtype;
 }

And at the top of door_hit_bottom:

 if (self.map)
 {
  self.deathtype = self.target;
  self.target = self.map;
  SUB_UseTargets();
  self.target = self.deathtype;
 }

This should now work with all doors!

The changes from the mappers viewpoint

trigger_changetarget:

  • targetname is the name of the trigger
  • target is the object to change
  • netname is the new target for target
  • spawnflags 1 will set it to only trigger once

trigger_random:

  • targetname is the name of the trigger
  • currentammo is the chance (0 to 100) of triggering 'A'
  • noise is the name of target A
  • noise1 is the name of target B
  • noise2 is object to kill A
  • noise3 is object to kill B
  • spawnflags 1 will set it to only trigger once

trigger_explode:

  • targetname is the name of the trigger
  • dmg is how much damage to do when triggered
  • spawnflags 1 will set it to trigger once
  • spawnflags 2 will not produce the explosion sprite

trigger_lots:

  • targetname is the name of the trigger
  • noise, noise1, noise2, noise3, noise4, netname, wad, target, deathtype, and mdl are the names of the objects to trigger
  • spawnflags 1 will set it to trigger once

trigger_sight:

  • targetname is the name of the trigger (Not always used)
  • target is the object to trigger when it loses its sight
  • netname is the name of the object to track
  • spawnflags 1 will ignore monsters
  • spawnflags 2 will ignore players
  • spawnflags 4 will ignore items
  • spawnflags 8 will set it to trigger once
  • spawnflags 16 will make it only check when it gets triggered (via targetname), instead of checking all the time.

trigger_setspeed:

  • targetname is the name of the trigger
  • target is the object to change
  • speed is the speed value to give it
  • spawnflags 1 will set it to trigger once

trigger_message:

  • targetname is the name of the trigger
  • message is the message to print
  • spawnflags 1 will set it to trigger once

path_corner:

  • speed is the speed for the train to run at (When it reaches this path)
  • netname is the object to trigger when a train/monster reaches the path
  • By linking a path_corner to itself, the train will now stop without making any continuos noises.

func_train:

  • Can now be stopped/restarted by triggering it again

trigger_hurt:

  • targetname is the name of the trigger (Only used if set to be pausable)
  • spawnflags 1 controls whether it is pausable
  • spawnflags 2 sets it to be paused upon spawn

trigger_push:

  • targetname is the name of the trigger (Only used if set to be pausable)
  • spawnflags 2 controls whether it is pausable
  • spawnflags 4 sets it to be paused upon spawn

func_door (And other doors):

  • netname will now be triggered once the door has finished opening
  • map will be triggered when the door has finished closing

trigger_toggle:

  • targetname is the name of the trigger
  • target is the name of the doors, lights, trains and triggers to start/stop when this ent is triggered
  • spawnflags 1 will set it to only trigger once
  • spawnflags 2 will set it to turn off objects, e.g. doors close, lights go off, trains stop and triggers deactivate. Not setting this flag causes it to turn the objects on, e.g. doors open, lights go on, trains move, and triggers hurt/push.
  • spawnflags 4 will cause it to effectively toggle spawnflag 2 on and off each time the trigger_toggle is triggered

trigger_if:

  • targetname is the name of the trigger
  • noise1 is the object to trigger if the target is on (Open door, light on, train moving, triggers active)
  • noise2 is the object to trigger if the target is off (Closed door, light off, train stopped, triggers inactive)
  • noise3 is the targetname of the object to check
  • spawnflags 1 will set it to only trigger once
  • spawnflags 2 will cause it to swap noise1 and noise2 each time it is triggered

A few ways to use these features

  • Create button operated lifts. When you press a button to call the lift, all the path_corners for the lift are redirected to the floor you wanted (Via trigger_changetarget). With enough work you can create lifts with external doors, and internal ones (func_trains) which follow the main lift.
  • Create random levels - use noise2 and noise3 in trigger_randoms to remove func_walls blocking off different bits of the level. Stick a trigger_once over the player spawn area to set the system off.
  • Improved level logic - use trigger_changetargets to activate/deactivate a series of trigger_relays which control some action
  • Use trigger_explode to rig up explosions
  • Use trigger_lots instead of lots of trigger_relays, or naming lots of entities the same
  • Use trigger_sight with an info_notnull to provide security systems, or with, erm, something else. Could use it to get monsters to trigger actions.
  • Create a space ship level, where opening airlock(s) will activate trigger_pushes and trigger_hurts to kill the people nearby. Link it with a few bulkheads around the level to be able to depressurise the entire ship!
  • Create another space ship level with a self destruct sequence

Improvements

  • Make all the new triggers have the option of being brush based instead of just point based

Disclaimer

Don't expect any of this to work under all conditions and with all mods/versions of the QC source. In fact, it's best if you don't expect it to work at all. Also I'm not taking any responsibility for any of it. Except that I want my name in the credits of any mod that uses these :-)

Page last modified 03/03/2005