Game Development Community

TGB onCollision(), how to call the method once in a time period

by Shaolin Dave · in Torque Game Builder · 06/02/2011 (10:17 pm) · 7 replies

function AttackAreaClass::onCollision(%srcObject, %dstObject, %srcRef, %dstRef, %time, %normal, %contacts, %points)
{
	if( player.getAnimationName() $= (player.Character @ "Slash") ||  player.getAnimationName() $= (player.Character @ "Chop") )
	{
		if (%dstObject.getName() $= target)
		{
			echo("successful attack");
		}
	}
}

so, here's the situation... i have a character who has a melee sword attack. the way i accomplish this is by mounting an invisible scene object, covering the area where the players sword will travel. when the player pushes the "attack" button, it plays the animation for the attack. if the invisible scene object overlaps an enemy at the same time the animation is playing, then damage is dealt to the enemy.

now, here's the problem, collision is detected several times per second. in the coded example above, the line "successful attack" is printed 10-15 times. I want it to occur once for each time the player hits the enemy.

I've already tried the following...

function AttackAreaClass::onCollision(%srcObject, %dstObject, %srcRef, %dstRef, %time, %normal, %contacts, %points)
{
	if( player.getAnimationName() $= (player.Character @ "Slash") ||  player.getAnimationName() $= (player.Character @ "Chop") )
	{
		if (%dstObject.getName() $= target)
		{
			%srcObject.setCollisionCallback(false);
			echo("successful attack");
			schedule(500, %srcObject, setCollisionCallback, true);
		}
	}
}

i've tried serveral values in place of the "500", but it doesn't appear to be relevant. In this case, I still get "successful attack" echoed twice (apparently this function is called twice before it even gets to the "%srcObject.setCollisionCallback(false);" line), and 500 miliseconds later i get two messages saying "setCollisionCallback: Unknown command."


#1
06/03/2011 (3:56 am)
You can probably use some kind of attack id, basically just a number that gets increased by one each time, so that there is a new number for each separate attack. When target is hit, just assign current attack id to one of it's fields, and check it back to filter out repetitive hits.
#2
06/03/2011 (11:22 am)
Thank you Rpahut.

One thing I tried eariler was creating a boolean variable that would become "true" at the beginning of every attack animation, and "false" as soon as the first hit was detected. I then coded an if statement so the effects of the hit would only play out if the variable was "true". For some reason, this didn't work at all. It was as if the variable was always "true". Maybe I had a typo.

On logic problem I noticed with that solution, the one I've coded above, and the one you just mentioned, is that even if they work, each attack will only be registered once. I know that's what I said I wanted in the first place, but I didn't take into consideration what would happen if multiple enemies where hit at the same time. If the attack is only registered once, only one enemy would be affected.

I guess what I really need is to create a takeDamage() function for each enemy class, and somehow limit how many times that function can be called (per object) in a given time period, say a quarter or half-second.

Does anyone remember "blinking time" from the old NES games, after you got hit you were invulnerable for a few moments? Basically what I'm looking for is a way to have that effect for fraction of a second for every player, enemy, and destructible item.
#3
06/03/2011 (6:38 pm)
I had a similar issue. The way I handled things was to have each thing that could be attacked have a variable called invulnerable. When they were hit with an attack, I would set invulnerable to true and then schedule a function to set invulnerable to false after a short time (the length of attack animation). So basically what you said, heh. It'd look something like this:

function enemyClass::takeDamage(%this, %dmg)
{
   if(%this.invulnerable == 0)
   {
      %this.invulnerable = 1;
      %this.schedule(300, "makeVulnerable");

      %this.health -= %dmg;

      //insert code to handle character dying here
   }
)

function enemyClass::makeVulnerable(%this)
{
   %this.invulnerable = 0;
}
#4
06/03/2011 (9:02 pm)
What would be nice is if t2d supported a "onCollisionEnter" and "onCollisionLeave". Then you could send damage on the "enter" only.

Otherwise, I'm doing what you thought of above, which is to mark a flag "false" until a certain amount of time has passed. It can work if you get the code debugged.

I think you are correct that you should switch to using the enemies collision routine if you want multiple enemies to take the hit. But there are some alternatives:

The attackers collision could do a "pickPoint" or other method of recognizing all items that are hit.

Perhaps another route altogether would be to avoid collisions and invisible scene objects. Instead, when your player attacks, do a "pickLine" the length of his sword and send damage to everything that is returned (that is an enemy).
#5
06/04/2011 (1:11 am)
@Charlie Patterson

I see a problem with "onCollisionEnter" and "onCollisionLeave", for this particular use anyway. What if the enemy enters the ranger of the attack and then the player attacks.. "onCollisionEnter" would have already been called before the attack.

Still, those functions could be useful elsewhere. Hopefully future versions will have it.

as for "pickLine", i was unfamiliar with it until now, I'm glad you brought it up.
pickLine(%x1, %y1, %x2, %y2, [%groupMask = MASK_ALL], [%layerMask = MASK_ALL], [%showInvisible = false])

so %x1 and %y1 would be the position of the player, %x2 and %y2 would be a few units in front of them... that much is simple... how would you code the function to call a "takeDamage" function for each detected object?
#6
06/04/2011 (3:23 pm)
@Dave You sure do not want to disable attack itself, except as for some special cases, and that wasn't what I was talking about. I suggested keeping last-attack-ID for each object that can be hit, not for the attack. Each object then either receives damage or not depending on attack id it is remembered. As simple as that.

Invulnerable timer may be good for player character, epecially in games where player gets hit from the contact with enemy, but it is unrelated to the problem of multiple hits detection, and you do not really want the same kind of protection for the enemies, or all hit-able objects in game.
#7
06/04/2011 (10:00 pm)
Hi Dave,

Quote:
I see a problem with "onCollisionEnter" and "onCollisionLeave", for this particular use anyway. What if the enemy enters the ranger of the attack and then the player attacks.. "onCollisionEnter" would have already been called before the attack.

Hmm. What I meant, and I *think* this would work, would be something like this psuedo-code:

onCollisionEnter(%enemy) {
   // we won't see any more collision calls for this enemy until we receive an onCollisionLeave
   // start "auto-attack" sequence.

   %this.attack(%enemy);
}

onCollisionLeave() {
   // stop the auto-attack
   cancel(%this.attackAgain);
}

attack(%enemy) {
   sendEnemyDamage(%enemy);
   %attackAgain = %this.schedule(%attackSpeed, attack, %enemy);
}

The onCollisionEnter would run once per enemy! I'm waving my hands a bit because you'd need a way to have a different schedule event ID for each enemy. Anyway...

Quote:
how would you code the function to call a "takeDamage" function for each detected object?

Something like this might help get you started:

// settings up the layer and group masks was the hardest for me to figure out
   %masks = %this.owner.getCollisionMasks();   
   %groupMask = getWord(%masks, 0);
   %layerMask = getWord(%masks, 1);
   %enemies = sceneWindow2D.getSceneGraph().pickLine(%start, %end, %groupMask, %layerMask);

   // got my enemies now. so send each one the pain
   %count = getWordCount(%enemies);
   for (%i = 0; %i < %count; %i++) {
      // ignore collisions with things that don't take damage
      %takesDamage = getWord(%enemies, %i).getBehavior("TakesDamageBehavior");
      if (!isObject(%takesDamage))
         continue;

      %takesDamage.takeDamage(%this.strength, %this);
   }

Then %enemies holds your prey. Just loop through them and send the pain!