CollisionResponse for sprites in subclasses / Best practice

I‘ve come across a situation / issue with collision handling, specifically with the function CollisionResponse() with my current class setup.
I'm kind of new to OOP in the way Playdate is recommending, I used to handle everything with tables in my previous ventures.

Say I have a class Machine that extends sprite, and in it I create a couple of sprites that make up the machine. I'm doing this so I can handle movement and collision of individual parts of the machine seperately (is this a valid approach?) One part of the machine, the hose, has a collider which should collide with other objects. Here is the example code:

class('Machine').extends(gfx.sprite)

function Machine:init()

    Machine.super.init(self)
    
    self.x = 100
    self.y = 100
    
    --! Machine Body Sprite
    local machineBodyImage = gfx.image.new('example/machine-part1')
    self.machineBodySprite = gfx.sprite.new(machineBodyImage)
    self.machineBodySprite:moveTo(self.x,self.y)
    self.machineHoseSprite:setCollideRect( 0, 0, self.machineBodySprite:getSize() )
    self.machineBodySprite:add()
    
    --! Machine Flexible Hose Sprite
    local machineHoseImage = gfx.image.new('example/example-head')
    self.machineHoseSprite = gfx.sprite.new(machineHoseImage)
    self.machineHoseSprite:moveTo(self.x+2,self.y+10)
    self.machineHoseSprite:setCollideRect( 0, 0, self.machineHoseSprite:getSize() )
    self.machineHoseSprite:add()
    
end

function Machine:update()
    --! MOVEMENT
    self.machineHoseSprite:moveWithCollisions(self.x+2,self.y+10)
    
    
    --! COLLISIONS
    local collisions = self.machineHoseSprite:overlappingSprites()
    for i = 1, #collisions do
        local other = collisions[i]
        if (other:isa(Cargo)) then
            other:attach(self.machineHoseSprite)
        end
    end
end

function Machine:collisionResponse(other)
    if other:isa(Wall) then
            return 'freeze'
    elseif other:isa(Cargo) then
        return 'overlap'
    else
        return 'freeze'
    end
end

Now the collisions with other objects work fine this way, but CollisionResponse() doesn't seem to get called at all. If I try to print anything inside of it (like other.classname), nothing shows up.

Is this because the collision happens with self.machineHoseSprite which is of type sprite, and CollisionResponse() only gets called for the parent class (Machine)? What would be a better way for handling this?

I'm thinking of situations where I want multiple colliders that act like triggers (with overlap) on my Machine object, and a main collider for freeze collisions as well. I thought I could just create individual sprites for this within the class, but as I said they don't seem to be picked up my CollisionResponse().

Thank you for your help.

Edit
Ah, I realised I can set the collisionResponse directly on my sprite:

self.craneHookBlockSprite.collisionResponse = 'slide'

I guess that works for simple things, but I would still need to find a way to actually use the callback so I can define different behaviour for different object types my sprite is colliding with.

Hello,

Don't know if you found a work around already, but I've found using other.sprite:getZIndex() and having each sprite have a different zIndex (10001, 10002, etc) allows for getting the 'type' of object I'm colliding with. Required a bit of pre-planning but has worked well so far

local colX, colY, collisions, len = self.sprite:moveWithCollisions(self.newX, self.newY)
for n = 1, #collisions do
        local type = collisions[n].other:getZIndex()

Hi!

So, here's how I'm now handling everything and it works fine for me. Still don't know if this would be considered best practice, but it works.

I wrote a custom Collider class (which extends Sprite), so I can easily add multiple colliders into subclasses. The class looks like this:

class('Collider').extends(gfx.sprite)

function Collider:init(parent,xOffset,yOffset,w,h,groups,colGroups,isTrigger)

	Collider.super.init(self)
	
	self.isTrigger = isTrigger or false
	self.parent = parent
	self.xOffset = xOffset
	self.yOffset = yOffset
	if (self.parent ~= nil) then self:moveTo(self.parent.x+self.xOffset,self.parent.y+self.yOffset) end
	self:setSize(w, h)
	self:setCenter(0,0)
	self:setCollideRect( 0, 0, w,h )
	self:setGroups(groups)
	self:setCollidesWithGroups(colGroups)
	self:add()
end

function Collider:update()
	if (self.parent ~= nil) then 
		self:moveTo(self.parent.x+self.xOffset,self.parent.y+self.yOffset)
	end
end

function Collider:collisionResponse(other)
	if (self.isTrigger) then
		return 'overlap'
	else
		return 'freeze'
	end
end

The class takes a couple of parameters (might extend this in the future), e.g. x and y offset relative to the parent, and a 'trigger' flag which indicates the collision behaviour. Within any class I can then add a collider like so:

class('ExampleClass').extends(gfx.sprite)

function ExampleClass:init()

	ExampleClass.super.init(self)
 
   
    self.exampleCollider = Collider(self,10,10,20,20,2,1)
    self.exampleTrigger = Collider(self,20,20,10,10,2,1, true)

end

Notice how I'm passing the class as the parent, which comes in handy for finding out things about the collider later. Also I don't have to update the collider within the parent class, as the collider itself is added to the sprite list and is updated that way.

Checking for collision this way is very flexible. For example from within the class, if I want to check if a certain part of the object has collided with something. Here in the example you can see how I can also check for specific colliders on the other object:

local _, _, exampleTriggerCollisions, exampleTriggerCollisionsLength = self. exampleTrigger:checkCollisions(self.exampleTrigger.x, self.exampleTrigger.y)
	
for i = 1, exampleTriggerCollisionsLength do
		
	local collision = exampleTriggerCollisions[i]
    local other = collision.other
	
    if (other.isTrigger and other.parent:isa(AnotherExampleClass)) then
		-- handle collision events
	end
end

That's where having a reference to the parent of the collider comes in very handy, I can use the 'isa' function for example. In some cases I also simply gave the colliders names so I can just check for that. All works really well for me!
Obviously you still have to make sure to use moveWithCollisions to move the parent object, otherwise collisions won't get detected.

Ooh, that seems way cleaner than my current implementation. I've been assigning self to a sub value of the object's sprite in each object class in order to call object specific functions so collisions[n].other.object:objectsFunction() works when resolving collisions. Requires a quite extensive if elseif check though

function Spring:init(x, y, rotX, rotY)
     self.position = geo.vector2D.new(x, y)
     self.rotation = geo.vector2D.new(rotX, rotY)
     self.sprite = gfx.sprite.new()
     self.sprite.spring = self
end

function Spring:trigger()
    self.triggered = true
    return self.rotation:normalized()
end

Yes, collisionResponse affects only the sprite that is being moved, so if a collisionResponse function is defined it will only be called when you are moving the sprite it is defined on.

In your first code sample, the collisionResponse handler is a function on the Machine sprite, but the sprites it creates internally don’t know anything about that, their collisionResponse is left set to nil.

Some options: You could assign the same collision function all three sprites. Alternatively, you could have the inner sprites inherit from the Machine sprite, which would cause the collisionResponse implementation defined on your Machine sprite to be called by those subclassed sprites as well. However, this will lead to infinite recursion if you’re creating subclasses in the superclass’s init method, as the newly instantiated subclasses will also call their super’s init() method, which will create new instances of the subclasses, and so on and so on…

If you did want to allow creation of subclass sprites, you would need to define the new subclasses by doing something like:
class('Hose').extends(Machine)

Your second code example looks good! Tracking the parent that way makes sense (I would maybe use a different name so as not to confuse it with the object's parent? Not a big deal though).

The only other thing I was going to mention was about setting the collisionResponse directly (it’s better to do so when possible for performance reasons), but it looks like you discovered that already!

That's a neat idea! We actually added sprite:getTag() and sprite:setTag() to use as a little arbitrary number value for later identification, if you didn't want to overload the z-index that way.