Collision events when colliding with multiple things at once

ensomari

Member
It appears that when I create an object that collides with multiple objects at once, only one object is put through the collision event.

I have a FlameWheel attack the Player creates and follows so it's always centered on the Player.

I have a collision event set up in FlameWheel that should trigger on all Shootables. The Player's parent object is Shootable. The Bug's parent object is ShootableEnemy, and ShootableEnemy's parent is Shootable.

Early in that collision code, if it detects the FlameWheel is colliding with its owner, in this case the player, it "return"s. And for some reason, it never triggers on the enemy Bug.

If I move that collision code to trigger on all ShootableEnemies, it works fine and triggers on the Bug and vaporizes it. However, I want it to be more generic and work against the player if an enemy uses the FlameWheel.

But the exact same code doesn't work if it's set to trigger on collisions with Shootables. It triggers on the player and never any enemy...

Anybody have any clues why this is the case?


1611177282017.png
 
P

ParodyKnaveBob

Guest
Howdy, ensomari,
Early in that collision code, if it detects the FlameWheel is colliding with its owner, in this case the player, it "return"s. And for some reason, it never triggers on the enemy Bug.
This looks the most suspect. How about copy/pasting your collision event code into a GML code block here for us to see?

Regards,
 

ensomari

Member
Thanks, but collision_circle_list seems to fix it.

I just copied the whole collision code block into a for loop over all the objects in collision_circle_list, replaced the "other"s with "_other"s, and "return"s with "continue"s.

GML:
var radius = (bbox_right - bbox_left) / 2
var _list = ds_list_create();
var _num = collision_circle_list(x, y, radius, oShootable, false, true, _list, false);
if (_num > 0)
{
    for (var i=0; i<_num; ++i)
    {
        _other = _list[| i];

        if (_other == owner.id) // quick out for self because you're constantly doing that
            continue;
        ...
        ...
    }
}
ds_list_destroy(_list);
I do not see it written anywhere that collision events only process one collision event per frame, even if multiple things are colliding with an object at once!

I think I need to rewrite collision code for all of my explosions now.
 
P

ParodyKnaveBob

Guest
I do not see it written anywhere that collision events only process one collision event per frame, even if multiple things are colliding with an object at once!
That's because they don't. That's why I wanted to see your previous collision event code. It'd be nice to help you fix the real issue rather than "defactor" all your existing code.
 

ensomari

Member
Be careful what you wish for...


GML:
/// @description COPY FROM oBALL

//global.debugValue2++;

if (other == owner) // quick out for self because you're constantly doing that
    return;
    
global.debugValue1 = "computing...";
global.debugValue2++;

if (!visible)
{
    global.debugValue1 = "Inactive";
    return; // Don't collide if swallowed by a fish
}

if (nonColliding || other.nonColliding)
{
    global.debugValue1 = "Non-Colliding";
    return;
}

if (other.shotSide == shotSide)
{
    global.debugValue1 = "Same shot side";
    return; // Don't collide with friendlies
}

if (other.framesPhysicalInvuln > 0 && other.physicalInvulnTo == self) // DIFFERENT
{
    global.debugValue1 = "Physically invulnerable";
    return;
}

//if (framesPhysicalInvuln > 0) // No damage
//    return;

//if (framesPhysicalInvuln > 0) // No damage
//    return;
    
//if (dodging && dodgeAttack == 0) // Dodge
//    return;
    
// Grace
// -2/((x+1.7)^1.3) + 1
// At x=0 y=0; x=1 y=0.48; x=2 y=0.65; x=3 y=.73
//var graceChance = -2/(power(grace+1.7, 1.3)) + 1;
// (Didn't increase fast enough)
// -7/((x+2)^2.7) + 1
// (0,0), (1,0.64), (2,0.84), (3,0.93), (4,0.95), (5,0.97)
//var graceChance = -7/(power(grace+2, 2.7)) + 1;
//if (!dodging && random(1) < graceChance)
//{
//    return;
//}

//oVibrationManager.collision = true;

//if (owner.spikes > 0 && framesSinceSpike >= spikeDelay)
//{
//    var totalSpikes = round(2 + owner.spikes);
    
//    var angleBetweenShots = 360 / totalSpikes;
//    var initialAngle = image_angle;
//    var spikeRotateDirection = choose(-1, 1);
//    var spikeWispType = OtherWispType(other.wispType); // Make it the color that will hurt whatever touched
    
//    for (var i=0; i<totalSpikes; i++)
//    {
//        var dirShoot = initialAngle + (angleBetweenShots * i);
//        if (dirShoot > 360)
//            dirShoot -= 360;
//        if (dirShoot < 0)
//            dirShoot += 360;
            
//        with (instance_create_layer(x, y, "Blasts", oSpike))
//        {
//            x += lengthdir_x(other.owner.spikes, dirShoot);
//            y += lengthdir_y(other.owner.spikes, dirShoot);
            
//            dmg = 1.618 * other.owner.gun.dmg * power(GRUPHALF, other.spikes); // 1.3
//            spd = 0;
//            hsp = 0;
//            vsp = 0;
//            image_angle = dirShoot;
//            turnTargetAngle = image_angle;
//            ttl = other.owner.gun.ttl * 3 * power(GRUPHALF, other.spikes); // 1.3
//            size = other.owner.gun.size * 1.4;
//            image_xscale = size * power(GRUPHALF, other.spikes); // 1.3 1.2
//            image_yscale = size * power(1.1, other.owner.spikes);
            
//            spikes = other.owner.spikes;
//            spikesRotate = other.owner.spikesRotate;
//            rotateDirection = spikeRotateDirection;
            
//            maxHits = max(1, other.spikes/2);
            
//            wispType = spikeWispType;
                
//            shotDrag = other.owner.gun.shotDrag;
//            poison = other.owner.gun.poison;
//            slow = other.owner.gun.slow;
//            //wave = other.gun.wave;
//            //waveUp = other.gun.waveUp;
//            //wavesAreRandom = other.gun.wavesAreRandom;
//            //penetration = other.gun.penetration;
//            //tracking = other.gun.tracking;
//            //blastPower = other.gun.blastPower;
//            //blastRadius = other.gun.blastRadius;
//            //blastCrackle = other.gun.blastCrackle;
//            //blastAttract = other.gun.blastAttract;
//            lightning = other.owner.gun.lightning;
//            lightningChains = other.owner.gun.lightningChains;
//            lightningExplodes = other.owner.gun.lightningExplodes;
//            //shatterChains = other.gun.shatterChains;
//            //shatterSplits = other.gun.shatterSplits;
//            //shatterRetention = other.gun.shatterRetention;
//            grow = other.owner.gun.grow; // Mines growing too much is OP - if Update, update grow in mine too
//            vampire = other.owner.gun.vampire;
//            vampireOwner = other;
            
//            shotSide = other.owner.gun.shotSide;
            
//        }
//    }
//}

/////////////
///////////////////
////// THIS IS DIFFERENT FROM oPLAYER
//////////////////////////////////////////////
var collisionDamage = baseDamage * power(1.2, owner.screw) * power(global.playerLevelUpDamage, global.level);

targetDamage = collisionDamage;

global.debugValue1 = "damaging: " + string(targetDamage);

other.hp -= targetDamage;
other.flashTimeToLive = global.standardFlashTime;
other.flashWispType = OtherWispType(other.wispType);
with (instance_create_layer(other.x + random_range(-5, 5), other.y + random_range(-5, 5), "Damage", oDamageNum))
{
    value = other.targetDamage;
    shotSide = ShotSide.Player;
}



//// TODO: change enemyPhysicalAttack to read from the shootable and customize for every enemy?
////var enemyPhysicalAttack = 15;
//var enemyPhysicalAttack = 30; // 40 // nov 30 made much stronger
//// playerDamage is damage that the player is going to receive
//playerDamage = enemyPhysicalAttack * power(global.enemyWaveScaling, global.wave) * global.enemyDamageScaling;
//targetDamage = physicalAttackDmg * power(global.playerLevelUpDamage, global.level);

//targetDamage *= power(1.2, bodyStrength);

//playerDamage /= power(1.2, bodyStrength);
//playerDamage /= power(1.2, damageResist);

//if (screwing)
//{
//    playerDamage = 0;
//    targetDamage /= 2;// 1.618; // 2; // 3
//    targetDamage *= power(1.2, screw);
//}







// NEW COLLISIONS CODE
// https://www.plasmaphysics.org.uk/programs/coll2d_cpp.htm
var m21 = other.mass / mass;
var x21 = other.x - x;
var y21 = other.y - y;
var vx21 = other.hsp - hsp;
var vy21 = other.vsp - vsp;

var vx_cm = (mass * hsp + other.mass * other.hsp) / (mass + other.mass);
var vy_cm = (mass * vsp + other.mass * other.vsp) / (mass + other.mass);

var fy21 = 0.000001 * abs(y21);
if (abs(x21) < fy21)
{
    var signVal = 1;
    if (x21 < 0)
        signVal = -1;
    x21 = fy21 * signVal;
}

// Update velocities
var a = y21/x21;
var dvx2 = -2*(vx21 + a * vy21)/((1 + a * a)*(1 + m21));
var vx2 = other.hsp + dvx2;
var vy2 = other.vsp + a * dvx2;
var vx1 = hsp - m21 * dvx2;
var vy1 = vsp - a * m21 * dvx2;

var R = 0.98; // (restitution coefficient)  between 0 and 1 (1=perfectly elastic collision)

//     ***  velocity correction for inelastic collisions ***
vx1=(vx1-vx_cm)*R + vx_cm;
vy1=(vy1-vy_cm)*R + vy_cm;
vx2=(vx2-vx_cm)*R + vx_cm;
vy2=(vy2-vy_cm)*R + vy_cm;



//var collisionDir = point_direction(other.x, other.y, x, y);
//var playerMoveMagnitude = 20; //5; //25;
//var enemyMoveMagnitude = playerMoveMagnitude;
//vx2 *= power(1.3, bodyStrength);
//vy2 *= power(1.3, bodyStrength);
//vx1 /= power(1.3, bodyStrength);
//vy1 /= power(1.3, bodyStrength);

//noControlFrames = 7 * power(0.7, bodyStrength);

// Move player (if not dodging)
//if (!dodging)
//{


// NOTE: This part is the only uncommented part in oBall
// But we don't want to move the storm
    //hsp = vx1;
    //vsp = vy1;
    
    
    
    //hsp += lengthdir_x(playerMoveMagnitude, collisionDir);
    //vsp += lengthdir_y(playerMoveMagnitude, collisionDir);
    //framesPhysicalInvuln = framesPhysicalInvulnAmount;
//}
// NOTE: The wrecking ball hits every frame if it can.

// Move target
other.framesPhysicalInvuln = other.framesPhysicalInvulnAmount;
other.physicalInvulnTo = self;
if (!other.unPushasble)
{
    //collisionDir = point_direction(x, y, other.x, other.y);
    //other.hsp += lengthdir_x(enemyMoveMagnitude, collisionDir);
    //other.vsp += lengthdir_y(enemyMoveMagnitude, collisionDir);
    other.hsp = vx2;
    other.vsp = vy2;
}


ScreenShake(4, 4); // ball collision screenshake
//PlaySound(Explosion_18, global.currentSfxVolume, 50, false);
audio_sound_pitch(dBodyHit1, random_range(0.93, 1.07));
PlaySound(dBodyHit1, global.currentSfxVolume, 60, false);


// Copied from oPlayer... (which was copied from oProjectile)
////////////////////////////////////////////////////
// Physical lightning, explosions, poison, ...
////////////////////////////////////////////////////

if (owner.gun.slowPhysical > 0)
{
    other.frameModToCheckSlow = (global.frame mod 60) + 1; // Start feeling the SLOW IMMEDIATELY if you don't have slow yet
    if (other.frameModToCheckSlow > 59)
        other.frameModToCheckSlow -= 60;
    var slowStack = instance_create_layer(0, 0, "Logic", dsStack);
    with (slowStack)
    {
        slowStack.startFrame = global.frame;
        slowStack.stacks = 1.5 * power(other.owner.gun.slowPhysical, 1.2);
    }
    ds_list_insert(other.slowStackList, 0, slowStack);
}
if (owner.gun.poisonPhysical > 0)
{
    //other.frameModToCheckPoison = (global.frame mod 60) + 30; // Start feeling the POISON SOON (+ half a second) if you don't have poison yet
    //if (other.frameModToCheckPoison > 59)
    //    other.frameModToCheckPoison -= 60;
    var poisonStack = instance_create_layer(0, 0, "Logic", dsStack);
    with (poisonStack)
    {
        poisonStack.startFrame = global.frame;
        poisonStack.stacks = 0.15 * other.targetDamage * power(other.owner.gun.poisonPhysical, 1.2); // Becomes accumulated poison damage
    }
    ds_list_insert(other.poisonStackList, 0, poisonStack);
}
//if (parasite > 0)
//{
//    var parasiteStack = instance_create_layer(0, 0, "Logic", dsStack);
//    with (parasiteStack)
//    {
//        parasiteStack.startFrame = global.frame;
//        parasiteStack.stacks = other.parasite;
//    }
//    ds_list_insert(other.parasiteStackList, 0, parasiteStack);
//}
if (owner.gun.vampirePhysical > 0)
{
    owner.vampireHp += targetDamage * 0.0618 * power(1.3, owner.gun.vampirePhysical-1);
}
// Blast
if (owner.gun.blastPhysical > 0 && owner.wispType != other.wispType)
{
    if (random(1) < 0.3 * power(1.3, owner.gun.blastPhysical) * power(1.3, owner.gun.blastRadius))
    {
        with (instance_create_layer(x, y, "Blasts", oBlast))
        {
            //dmg = (other.targetDamage * 0.2) * power(1.2, owner.gun.blastPhysical);
            //size = (other.owner.gun.size * 0.3) * power(1.3, other.blastRadius);
            dmg = other.targetDamage * 0.5 * power(1.2, other.owner.gun.blastPhysical);
            size = 0.25 * power(1.2, other.owner.gun.blastRadius) * power(1.1, other.owner.gun.blastPhysical);
            spd = 0;   
            hsp = 0;
            vsp = 0;
            wispType = other.owner.wispType;
            shotSide = other.owner.shotSide;
            blastPower = other.owner.gun.blastPower;
            blastRadius = other.owner.gun.blastRadius;
            blastCrackle = other.owner.gun.blastCrackle;
            //blastAttract = other.blastAttract; // not needed - only mines
        }
    }
}
/// NEW STUFF (not copied, but added)
// TODO: Move below, convert to only blow up on the opposite color
//if (explodes > 0)
//{
//    with (instance_create_layer(x, y, "Blasts", oBlast))
//    {
//        dmg = collisionDamage * 0.3 * power(1.2, other.owner.gun.blastPower) * power(1.2, other.explodes);
//        size = 0.25 * power(1.3, other.owner.gun.blastRadius) * power(1.2, other.explodes);
//        spd = 0;   
//        hsp = 0;
//        vsp = 0;
//        wispType = other.owner.wispType;
//        shotSide = other.owner.shotSide;
//        blastPower = other.owner.gun.blastPower;
//        blastRadius = other.owner.gun.blastRadius;
//        blastCrackle = other.owner.gun.blastCrackle;
//        //blastAttract = other.blastAttract; // not needed - only mines
//    }
//}
// Chain lightning
if (owner.gun.lightningPhysical > 0 && owner.wispType != other.wispType)
{
    dude💩💩💩💩[0] = other;
    lTarget = other;
    lChains = 0;
    var lStartX = other.x; // The starting point of the lightning,
    var lStartY = other.y; // the enemy getting hit.
    
    // While we can still make another chain...
    while (true)
    {
        if (lChains >= owner.gun.lightningChains)
        {
            //global.debugValue = "Too many chains, correct";
            break;
        }
        
        if (random(1) > 0.3 * power(1.2, owner.gun.lightningChains) * power(1.2, owner.gun.lightningPhysical))
        {
            // Failed the dice roll to keep chaining lightning
            break;
        }
    
        //lTarget = InstanceNthNearest(lStartX, lStartY, oShootableEnemy, 2);
        
        var maxAttemptsToTrack = 20;
        
        // Create a list of the 20 closest enemies
        maxAttemptsToTrack = min(max(1, maxAttemptsToTrack), instance_number(oShootableEnemy));
        var nearestList = ds_priority_create();
        //nearest = noone;
        // Make the priority queue and sort by distance
        with (oShootableEnemy) ds_priority_add(nearestList, id, distance_to_point(lStartX, lStartY));
        // Burn the first result, as it's the enemy getting hit by the projectile
        
        dude💩💩💩💩[0] = ds_priority_delete_min(nearestList);;
        
        var validTarget = false;
        for (var i=0; i<maxAttemptsToTrack-1; i++)
        {
            // Grab the next closest enemy from the priority queue
            lTarget = ds_priority_delete_min(nearestList);
            //lTarget = InstanceNthNearest(lStartX, lStartY, oShootableEnemy, 2+i); // Start 2 away so you don't target the same enemy right away
            
            if (lTarget == noone)
                break;
            
            if (distance_to_object(lTarget) > 250 * power(1.2, owner.gun.lightningPhysical))
                break;
            
            if (lTarget.shotSide == owner.shotSide)
                continue;
            
            if (lTarget.wispType == owner.wispType)
                continue;
                
            if (lTarget.nonColliding || !lTarget.visible || !lTarget.shootable)
                continue;
                
            if (lTarget.bbox_left > RES_W || lTarget.bbox_right < 0 || lTarget.bbox_bottom < 0 || lTarget.bbox_top > RES_H)
                continue;
                
            var alreadyHitThisGuy = false;
            for (var dh=0; dh<=lChains; dh++)
            {
                if (dude💩💩💩💩[dh] == lTarget)
                {
                    // We've already hit this guy
                    alreadyHitThisGuy = true;
                    break;
                }
            }
            if (alreadyHitThisGuy)
                continue;
            
            // If we make it here, we have a valid new target in lTarget
            validTarget = true;
            break;
        }
        ds_priority_destroy(nearestList);
        
        if (!validTarget)
        {
            break;
        }
        
        dude💩💩💩💩[lChains+1] = lTarget;
    
        with (instance_create_layer(lStartX, lStartY, "Lightning", oLightning))
        {
            target = other.lTarget;
            wispType = other.owner.wispType;
            shotSide = other.owner.shotSide;
            dmg = other.targetDamage * 0.4 * power(1.2, other.owner.gun.lightningPhysical) * power(0.8, other.lChains);
            
            var width = bbox_right - bbox_left;
            var dist = distance_to_point(target.x, target.y);   
            var ratio = dist * (1/width);
            image_xscale = ratio;
            
            // NOTE: there's a small bug in the lightning code.
            // Sometimes the image doesn't stretch all the way it needs to
            // Like image_xscale is limited to integers... or something...
            // Some of this code is trying to figure that out

                
            image_angle = point_direction(x, y, target.x, target.y);
            
            poison = other.owner.gun.poisonPhysical;
            slow = other.owner.gun.slowPhysical;
        }
        
        if (owner.gun.lightningExplodes > 0)
        {
            with (instance_create_layer(lTarget.x, lTarget.y, "Blasts", oBlast))
            {
                dmg = (other.targetDamage * 0.5) * power(1.2, other.owner.gun.blastPower) * power(1.2, other.owner.gun.lightningExplodes);
                size = (other.owner.gun.size * 0.6) * power(1.2, other.owner.gun.blastRadius) * power(1.2, other.owner.gun.lightningExplodes);
                spd = 0;   
                hsp = 0;
                vsp = 0;
                wispType = other.owner.wispType;
                shotSide = other.owner.shotSide;
                blastPower = other.owner.gun.blastPower;
                blastRadius = other.owner.gun.blastRadius;
                blastCrackle = other.owner.gun.blastCrackle;
                //blastAttract = other.blastAttract; // not needed - only mines
            }
        }
        
        lStartX = lTarget.x;
        lStartY = lTarget.y;
    
        lChains++;
    } // while
} // if

////////////////////////////////////////////////////
// (end) Physical lightning, explosions, poison, ...
////////////////////////////////////////////////////
 
P

ParodyKnaveBob

Guest
Yep, this is what I suspected:
GML:
if (other == owner) // quick out for self because you're constantly doing that
    return;
As briefly mentioned in the manual, the object/instance keywords like other are actually just negative numbers. I'm guessing owner is the creating instance's id, which of course is a high positive number. You can see how these two things will never equal each other. Try this to see if it fixes everything else (over which I only gave a cursory glance since I so quickly found what I said in post 3 was suspect).
GML:
if (other.id == owner) // quick out for self because you're constantly doing that
    return;
In the future, when things seem weird to you that should work but don't, try dropping in show_debug_message() lines and/or watch things in the debugger, stepping through code line by line if need be.

I hope this helps, $:^ J
 

ensomari

Member
You can see how these two things will never equal each other.
It's not that they never equal each other, they were always equaling each other.

and/or watch things in the debugger, stepping through code line by line if need be
I do, and other and owner seem to be objects (when I hover over them, a lot of detail is shown), rather than numbers.

When I do other.id, I get a number like 100034 comparing to an object and it doesn't equal so it goes on, until it gets to the following, where it breaks out, and still no other bugs ever seem to trip this collision function.
GML:
if (other.shotSide == shotSide)
{
    global.debugValue1 = "Same shot side";
    return; // Don't collide with friendlies
}
I appreciate you looking at this, but I don't feel like it's worth your time. I got it working in the step event, which is better overall as sometimes GMS refuses to let me step through code in a collision event.

That being said, I would be interested in knowing why it doesn't work in Collision code but it works in Step. For now, I still think GMS doesn't run through all instances that are colliding in Collision events, only one.
 

ensomari

Member
I put a breakpoint at the top, looking for anything else that's not with id 100034, but it never triggered.

Code:
global.debugValue1 = string(other.id);

if (other.id != 100034)
{
    var a = 0;
    var b = a;   // breakpoint here (never hits)
}

if (other.id == owner.id) // quick out for self because you're constantly doing that
    return;
    
...
 
P

ParodyKnaveBob

Guest
I made the mistake of saying never matching instead of always matching, yes. I'd be curious exactly what GML you use to set owner then.

That said, I was hoping someone else would have swooped in by now...
 

ensomari

Member
Yeah, don't worry about it. You're under no obligation, of course. I appreciate you trying though.

But to answer you, from oPlayer...
GML:
screwFlame = instance_create_layer(x, y, "Blasts", oScrewFlame);
screwFlame.owner = self;
 
P

ParodyKnaveBob

Guest
Ah, there's the other half of the problem, then, thank you:
Yeah, don't worry about it. You're under no obligation, of course. I appreciate you trying though.

But to answer you, from oPlayer...
GML:
screwFlame = instance_create_layer(x, y, "Blasts", oScrewFlame);
screwFlame.owner = self;
Your self reference has the same issue as the other reference. I recommend the following two tiny changes then:
GML:
screwFlame.owner = id;
GML:
if (other.id == owner)
That way, owner will be assigned and compared to an instance id variable instead of the more contextual keywords. (Skimming through your code a bit more attentively this time, I see some more places in the collision event where self gets assigned instead of id; clearly, if my advice works, then those would need to change, too.)

Hm? $:^ ]
 
Last edited by a moderator:
Top