PDA

View Full Version : Spawnable Triggers



Mitch
4th December 2014, 17:46
Originally posted by general sniperfire on IWNation.
https://web.archive.org/web/20110107124933/http://iwnation.com/forums/forum/148-call-of-duty-4-scripting/

CoD4


/*
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////

Sniper[+]FIRE>>> Spawnable triggers script v1.0


To spawn a trigger:

1. Build an array of 6 values. The first set of three
defines one corner of the trigger. The second three
define the opposite corner.

2. Call spawnTrigger(), and pass in the array. an optional
parameter is type, which determines the type of trigger.
See extra settings. A new trigger entity is returned.

Ex:
trigger = spawnTrigger( volume_array, "multiple");
This would spawn a trigger_multiple.


To monitor a trigger:

Simply use:
trigger waittill("trigger", player);
This will only work on players.
The player variable is optional.

Also, you can check to see if an entity is touching it with:
if( player isTouchingTrigger(trigger) )


Extra settings:

Types of triggers supported:
|- multiple
|- use*
|- hurt

* - use triggers do not yet have icons that appear when able
to be pushed.

.delay - delay between triggering. Default: 0.05
.dmg - dmg to do for hurt triggers. Default: 5
._color - used in debug to change line color.


-==Debug==-

When developer_script is enabled and the "debugtriggers" dvar is set,
boxes will be drawn around triggers to help determine the position.
use ._color to change the color.


***Notes:

1. Only rectangular prism shaped triggers will be made, with no rotation.
2. Waittill("trigger") method only registers players. You can check to see
if other entities are touching the trigger by: if( entity isTouchingTrigger(trigger) )


////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
*/

spawnTrigger(volume, type)
{
trigger = spawnstruct();

trigger.volume = volume;

if(isdefined(type))
trigger.type = type;
else
trigger.type = "multiple";

if(type == "hurt")
trigger.dmg = 5;

trigger.delay = 0;

trigger thread watchTrigger();

return trigger;
}

watchTrigger()
{
for(;;)
{
for(i=0;i<level.players.size;i++)
{
player = level.players[i];
if(player.sessionstate != "playing")
continue;

if(player isInVolume(self.volume))
{
if(self.type == "hurt")
player maps\mp\gametypes\_callbacksetup::CodeCallback_Pla yerDamage(self, undefined, self.dmg, 0, "MOD_TRIGGER_HURT", "none", player.origin, (0,0,0), "none", 0);
else if(self.type == "use" && !player usebuttonPressed())
continue;

self notify("trigger", player);

if(self.delay > 0)
wait(self.delay);
}

}

wait(0.05);
debugTrigger(self.volume);
}
}

isTouchingTrigger(trigger)
{
return self isInVolume(trigger.volume);
}

isInVolume(volume)
{
max[0] = getVmax(volume, 0);
max[1] = getVmax(volume, 1);
max[2] = getVmax(volume, 2);
min[0] = getVmin(volume, 0);
min[1] = getVmin(volume, 1);
min[2] = getVmin(volume, 2);

for(axis=0;axis<3;axis++)
{
if(self.origin[axis] < min[axis] || self.origin[axis] > max[axis])
return false;
}
return true;
}

getVmax(points, axis)
{
max = undefined;

for(i=0;i<points.size;i+=3)
{
if(!isdefined(max) || points[i+axis] > max)
{
max = points[i+axis];
}
}

return max;
}

getVmin(points, axis)
{
min = undefined;

for(i=0;i<points.size;i+=3)
{
if(!isdefined(min) || points[i+axis] < min)
{
min = points[i+axis];
}
}

return min;
}

debugTrigger(volume)
{
/#
if( getdvar("debugTriggers") == "")
return;

if(!isdefined(self._color))
self._color = (1,1,1);

max[0] = getVmax(volume, 0);
max[1] = getVmax(volume, 1);
max[2] = getVmax(volume, 2);
min[0] = getVmin(volume, 0);
min[1] = getVmin(volume, 1);
min[2] = getVmin(volume, 2);

line( (max[0], max[1], max[2]), (min[0], max[1], max[2]), self._color);
line( (max[0], max[1], max[2]), (max[0], min[1], max[2]), self._color);
line( (max[0], max[1], max[2]), (max[0], max[1], min[2]), self._color);

line( (min[0], min[1], max[2]), (min[0], max[1], max[2]), self._color);
line( (min[0], min[1], max[2]), (max[0], min[1], max[2]), self._color);
line( (min[0], min[1], max[2]), (min[0], min[1], min[2]), self._color);

line( (max[0], min[1], min[2]), (max[0], max[1], min[2]), self._color);
line( (max[0], min[1], min[2]), (min[0], min[1], min[2]), self._color);
line( (max[0], min[1], min[2]), (max[0], min[1], max[2]), self._color);

line( (min[0], max[1], min[2]), (max[0], max[1], min[2]), self._color);
line( (min[0], max[1], min[2]), (min[0], max[1], max[2]), self._color);
line( (min[0], max[1], min[2]), (min[0], min[1], min[2]), self._color);
#/
}


CoD2


/*
////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////

Sniper[+]FIRE>>> Spawnable triggers script v1.0


To spawn a trigger:

1. Build an array of 6 values. The first set of three
defines one corner of the trigger. The second three
define the opposite corner.

2. Call spawnTrigger(), and pass in the array. an optional
parameter is type, which determines the type of trigger.
See extra settings. A new trigger entity is returned.

Ex:
trigger = spawnTrigger( volume_array, "multiple");
This would spawn a trigger_multiple.


To monitor a trigger:

Simply use:
trigger waittill("trigger", player);
This will only work on players.
The player variable is optional.

Also, you can check to see if an entity is touching it with:
if( player isTouchingTrigger(trigger) )


Extra settings:

Types of triggers supported:
|- multiple
|- use*
|- hurt

* - use triggers do not yet have icons that appear when able
to be pushed.

.delay - delay between triggering. Default: 0.05
.dmg - dmg to do for hurt triggers. Default: 5
._color - used in debug to change line color.


-==Debug==-

When developer_script is enabled and the "debugtriggers" dvar is set,
boxes will be drawn around triggers to help determine the position.
use ._color to change the color.


***Notes:

1. Only rectangular prism shaped triggers will be made, with no rotation.
2. Waittill("trigger") method only registers players. You can check to see
if other entities are touching the trigger by: if( entity isTouchingTrigger(trigger) )


////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////
*/

spawnTrigger(volume, type)
{
trigger = spawnstruct();

trigger.volume = volume;

if(isdefined(type))
trigger.type = type;
else
trigger.type = "multiple";

if(type == "hurt")
trigger.dmg = 5;

trigger.delay = 0;

trigger thread watchTrigger();

return trigger;
}

watchTrigger()
{
for(;;)
{
players = getentarray("player", "classname");

for(i=0;i<players.size;i++)
{
player = players[i];

if(!isPlayer(player) || player.sessionstate != "playing")
continue;

if(player isInVolume(self.volume))
{
if(self.type == "hurt")
player maps\mp\gametypes\_callbacksetup::CodeCallback_Pla yerDamage(self, self, self.dmg, 0, "MOD_TRIGGER_HURT", "none", player.origin, (0,0,0), "none", 0);
else if(self.type == "use" && !player usebuttonPressed())
continue;

self notify("trigger", player);

if(self.delay > 0)
wait(self.delay);
}

}

wait(0.05);
debugTrigger(self.volume);
}
}

isTouchingTrigger(trigger)
{
return self isInVolume(trigger.volume);
}

isInVolume(volume)
{
max[0] = getVmax(volume, 0);
max[1] = getVmax(volume, 1);
max[2] = getVmax(volume, 2);
min[0] = getVmin(volume, 0);
min[1] = getVmin(volume, 1);
min[2] = getVmin(volume, 2);

for(axis=0;axis<3;axis++)
{
if(self.origin[axis] < min[axis] || self.origin[axis] > max[axis])
return false;
}
return true;
}

getVmax(points, axis)
{
max = undefined;

for(i=0;i<points.size;i+=3)
{
if(!isdefined(max) || points[i+axis] > max)
{
max = points[i+axis];
}
}

return max;
}

getVmin(points, axis)
{
min = undefined;

for(i=0;i<points.size;i+=3)
{
if(!isdefined(min) || points[i+axis] < min)
{
min = points[i+axis];
}
}

return min;
}

debugTrigger(volume)
{
if( getcvar("debugTriggers") == "")
return;

if(!isdefined(self._color))
self._color = (1,1,1);

max[0] = getVmax(volume, 0);
max[1] = getVmax(volume, 1);
max[2] = getVmax(volume, 2);
min[0] = getVmin(volume, 0);
min[1] = getVmin(volume, 1);
min[2] = getVmin(volume, 2);

line( (max[0], max[1], max[2]), (min[0], max[1], max[2]), self._color);
line( (max[0], max[1], max[2]), (max[0], min[1], max[2]), self._color);
line( (max[0], max[1], max[2]), (max[0], max[1], min[2]), self._color);

line( (min[0], min[1], max[2]), (min[0], max[1], max[2]), self._color);
line( (min[0], min[1], max[2]), (max[0], min[1], max[2]), self._color);
line( (min[0], min[1], max[2]), (min[0], min[1], min[2]), self._color);

line( (max[0], min[1], min[2]), (max[0], max[1], min[2]), self._color);
line( (max[0], min[1], min[2]), (min[0], min[1], min[2]), self._color);
line( (max[0], min[1], min[2]), (max[0], min[1], max[2]), self._color);

line( (min[0], max[1], min[2]), (max[0], max[1], min[2]), self._color);
line( (min[0], max[1], min[2]), (min[0], max[1], max[2]), self._color);
line( (min[0], max[1], min[2]), (min[0], min[1], min[2]), self._color);
}


Example


volume = [];
volume[0] = 2080; // min (x)
volume[1] = 197; // min (y)
volume[2] = 336; // min (z)
volume[3] = 2672; // max (x)
volume[4] = 254; // max (y)
volume[5] = 436; // max (z)

trigger = sim\_sf_triggers::spawnTrigger( volume, "hurt");
trigger._color = (1,0,0);
trigger.dmg = 1000;


Result: http://screenshot.xfire.com/s/105753647-4.jpg

807

(The debug lines are only visible when 'dedicated' is 0 and 'developer', 'developer_script' and 'debugTriggers' are set to 1)

Ni3ls
4th December 2014, 18:04
What is so special about this? We already were able to spawn triggers with a corresponding action? Am I missing something?

IzNoGoD
4th December 2014, 18:20
This doesnt do much special stuff indeed, just a check for isinside(cube coordinates) would suffice for this to work...

Tally
4th December 2014, 18:42
What is so special about this? We already were able to spawn triggers with a corresponding action? Am I missing something?

No, you aren't missing anything. All it does is some less-than clever distance checks. You can do exactly the same with a simple 2D distance check between the player and a script_origin. If it returns true, you know the player is "touching" the entity; if it returns false, you know the player isn't touching. Lots of checks on origins is completely unnecessary.

EDIT - I remember when this was first posted, a lot of us expressed the view that it was not a very efficient way to do things, but SniperFire said he wanted something which would return isTouching(). I and others told him you could do that on a trigger_radius, and he replied that he didn't know about the isTouching() function on trigger_radius, so it looked like all this work was pretty redundant anyway.

kung foo man
5th December 2014, 02:05
Well, normal spawnable triggers are spheres only and this is box + debug lines, which are kinda helpful.

Tally
5th December 2014, 07:22
Well, normal spawnable triggers are spheres only and this is box + debug lines, which are kinda helpful.

But it's the efficiency of the code that is questionable. It can all be done so much simpler and just as effectively, without putting such a strain on the server. This is my universal trigger function for COD1, which doesn't natively support triggers which can be delimited by radius (there is no trigger_radius in COD1 - only trigger_multiple can be spawned dynamically):


spawnTrigger( origin, radius )
{
ent = spawn( "script_origin", origin );
ent.origin = origin;
ent.touchingPlayer = [];
ent.touchingRadius = radius;
ent thread flag_touching();

return( ent );
}

flag_touching()
{
level endon( "intermission" );

/#
showBoundedArea( self, (1,0,0) );
#/

for( ;; )
{
players = getEntArray( "player", "classname" );

for( i=0; i < players.size; i++ )
{
player = players[i];

if( isPlayer( player ) && isAlive( player ) && player.sessionstate == "playing" )
{
if( distance( self.origin, player.origin ) < self.touchingRadius )
{
self notify( "trigger", player );
}
}
}

wait( 0.05 );
}
}


showBoundedArea( trigger, color )
{
player_height = 80.0;
center = trigger.origin;
forward = anglestoforward( trigger.angles );
right = anglestoright( trigger.angles );

forward = maps\mp\_utility::vectorScale( forward, trigger.touchingRadius );
right = maps\mp\_utility::vectorScale( right, trigger.touchingRadius );

a = center + forward - right;
b = center + forward + right;
c = center - forward + right;
d = center - forward - right;

thread lineUntilNotified( a, b, color, 0 );
thread lineUntilNotified( b, c, color, 0 );
thread lineUntilNotified( c, d, color, 0 );
thread lineUntilNotified( d, a, color, 0 );

thread lineUntilNotified( a, a + (0, 0, player_height), color, 0 );
thread lineUntilNotified( b, b + (0, 0, player_height), color, 0 );
thread lineUntilNotified( c, c + (0, 0, player_height), color, 0 );
thread lineUntilNotified( d, d + (0, 0, player_height), color, 0 );

a = a + ( 0, 0, player_height );
b = b + ( 0, 0, player_height );
c = c + ( 0, 0, player_height );
d = d + ( 0, 0, player_height );

thread lineUntilNotified( a, b, color, 0 );
thread lineUntilNotified( b, c, color, 0 );
thread lineUntilNotified( c, d, color, 0 );
thread lineUntilNotified( d, a, color, 0 );

}

lineUntilNotified( start, end, color, depthTest )
{
for( ;; )
{
line( start, end, color, depthTest );
wait( 0.05 );
}
}


And the developer mode produces this marked bounded area:

http://homepage.ntlworld.com/d.staveley/images/ctf_developer.jpg

In the gametype files, you code it as if it were a trigger_radius. This is my trigger think for CTF for COD1:


flag_think()
{
level endon( "intermission" );

objective_add( self.objective, "current", self.origin, self.compassflag );

for( ;; )
{
self waittill( "trigger", player );

if( isPlayer( player ) && isAlive( player ) && player.sessionstate == "playing" )
{
// PICKUP THE FLAG !!
if( player.pers["team"] != self.team )
{
if( self.atbase )
{
if( player.pers["team"] == "allies" )
player AnnouncetoAll( &"CTF_STOLE_AXIS", player.name );
else
player AnnouncetoAll( &"CTF_STOLE_ALLIES", player.name );

thread logEvent( "ctf_pickedup_flag", player );
}
else
{
if( player.pers["team"] == "allies" )
player AnnouncetoAll( &"CTF_TAKEN_BACK_AXIS", player.name );
else
player AnnouncetoAll( &"CTF_TAKEN_BACK_ALLIES", player.name );

thread logEvent( "ctf_pickedup_dropped_flag", player );
}

player pickupFlag( self );

friendlyAlias = "ctf_touchenemy";
enemyAlias = "ctf_enemy_touchenemy";

thread [[level.onPlaySoundOnPlayers]]( friendlyAlias, self.team );
thread [[level.onPlaySoundOnPlayers]]( enemyAlias, [[level.getOtherTeam]]( self.team ) );

thread [[level.onPlaySoundOnPlayers]]( game["flagTaken"][ player.pers["team"] ], player.pers["team"] );

}
else
{
// CAPTURED THE FLAG !!
if( self.atbase )
{
if( isdefined( player.flag ) )
{
player.flag returnFlag( undefined );
player detachFlag( player.flag );
player.flag = undefined;
player.statusicon = "";

player.score += 10;
teamscore = getTeamScore( player.pers["team"] );
teamscore += 1;
setTeamScore( player.pers["team"], teamscore );
level notify( "update_allhud_score");

if( player.pers["team"] == "allies" )
self AnnouncetoAll( &"CTF_CAPTURED_AXIS", player.name );
else
self AnnouncetoAll( &"CTF_CAPTURED_ALLIES", player.name );

friendlyAlias = "ctf_touchcapture";
enemyAlias = "ctf_enemy_touchcapture";

thread [[level.onPlaySoundOnPlayers]]( game["flagCapped"][ [[level.getOtherTeam]]( player.pers["team"] ) ] );

thread [[level.onPlaySoundOnPlayers]]( friendlyAlias, player.pers["team"] );
thread [[level.onPlaySoundOnPlayers]]( enemyAlias, [[level.getOtherTeam]]( player.pers["team"] ) );

thread logEvent( "ctf_captured_flag", player );

maps\mp\gametypes\_globallogic::checkScoreLimit();
}
}
else // RETURNED FLAG TO BASE !!
{
self returnFlag( true );

if( self.team == "allies" )
self AnnouncetoAll( &"CTF_RETURNED_ALLIES", player.name );
else
self AnnouncetoAll( &"CTF_RETURNED_AXIS", player.name );

thread [[level.onPlaySoundOnPlayers]]( "ctf_touchown", player.pers["team"] );

thread logEvent( "ctf_returned_own_flag", player );

player.score += 2;
level notify( "update_allhud_score" );
}
}
}

wait( 0.05 );
}
}


Ockham's Razor: All things being equal, the SIMPLEST solution is the right one. And my method - the method used by modders before trigger_radius was introduced in COD2 - is to use a distance test of a script_origin against all players. Such a method is less server CPU heavy than SniperFire's method.

IzNoGoD
5th December 2014, 08:15
1. You are using an implementation that emulates a trigger_radius, yet your screenshot shows a cube-like area, shouldnt it be a spherical area?
2. Why not use distancesquared instead, this saves you a squareroot operation per frame per player, and squareroot operations are VERY expensive

Tally
5th December 2014, 09:08
1. You are using an implementation that emulates a trigger_radius, yet your screenshot shows a cube-like area, shouldnt it be a spherical area?
2. Why not use distancesquared instead, this saves you a squareroot operation per frame per player, and squareroot operations are VERY expensive

1. As you rightly say, I am EMULATING a trigger radius. Hence it can be square, rectangle, oblong, or bannana shaped, it doesn't matter. As long as the functionality is the same, it doesn't matter what shape it is.

2. distanceSquared is not a built-in function in COD1, and I didn't emulate it or distance2d() (which I actually have in my utility files) because you actually need the height check in CTF as you move the trigger below the trigger radius in order to stop it triggering (cf. maps\mp\_utility::triggerOff()).

For the record, and so I don't have to keep saying it, this is a list of things which AREN'T in COD1:

no file functions. You cannot write to a file in any manner other than a log file. The only file formats you can read from are .GSC and .CFG;
no including files (i.e. #include <directory><filename>), you have to either manually thread to them, or build function pointers;
no trigger_radius;
no isSubStr();
no getSubStr();
no strTok();
no openscriptmenu command;
no updated menu language at all, just pure Quake 3 functions and syntax;
no closeingamemenu();
no playerADS();
only 2 channels for sound, so when you have 2 sounds playing simultaneously, if you play a 3rd, the sound of 1 of the others cuts out (the Quake 3 Team Arena engine natively has only 1 sound thread. It took other companies to implement sound functions such as 5.1 and EAX)
no getTagOrigin();
you cannot color light in COD1. It is just yellow, and the only way to color it is pass yellow light through a colored shader. In the COD2 engine, you can manipulate light directly, using 6 different parameters.
no physicstrace();
no fragbuttonpressed();
no GetOffhandSecondaryClass() (because grenades go into their own weapon slots - the slots are "primary", "primaryb", "pistol", "grenade", "smokegrenade". You can put a grenade in the pistol slot and it will work, and vice versa, as long as you change the slot name);
no "cg_fovscale" dvar;
no "cg_fovmin" dvar (so you can't zoom in with a sniper scope like you can in COD2/COD4);
no getPlayerAngles();

That list is by no means exhaustive. It is just the things I have come across while building my current mod for COD1. the engine is seriously limited. I have reproduced the basic functions we all take for granted because I needed them, but there are things I want but can't have because they have to be built into the engine. Here are some of the basics I have built:


strTok( string, token )
{
j = 0;
temparr[j] = "";

for( i = 0; i < string.size; i++ )
{
if( string[i] == token )
{
j++;
temparr[j] = "";
}
else
temparr[j] += string[i];
}

return( temparr );
}

getSubStr( string, start, end )
{
temp = "";
count = 0;
if( start > 0 )
{
for( i=start; i < string.size; i++ )
{
if( isDefined( end ) && count >= end )
break;

temp = temp + string[i];
count++;
}
}
else
{
for( i=0; i < string.size; i++ )
{
if( isDefined( end ) && i >= end )
break;

temp = temp + string[i];
}
}

return( temp );
}

isSubStr( str1, str2 )
{
// compare string1 with string2
for( i=0; i < str2.size - str1.size; i++ )
{
temp = "";
for( j=0; j < str1.size; j++ )
{
temp = temp + str2[ i + j ];
}

if( str1 == temp )
return( true );
}

// if 1st comparision fails compare string2 with string1
for( i=0; i < str1.size - str2.size; i++ )
{
temp = "";
for( j=0; j < str2.size; j++ )
{
temp = temp + str1[ i + j ];
}

if( str2 == temp )
return( true );
}

// all comparisons failed
return( false );
}

distance2d( a, b )
{
return( distance( (a[0],a[1],0), (b[0],b[1],0) ) );
}