## Advice on getting two unrelated class objects to interact

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

### Advice on getting two unrelated class objects to interact

I'm building a Space Invaders clone to learn LOVE.

I have a Laser class that is used in the following two places in my game.
1. As a property of the Base class
2. As a property of the Enemy class
I believe I've set things up so the Base class and the Enemy class are encapsulated from one another (am I saying that right?). However, if an enemy laser collides with a player laser, they should destroy each other which means they need to be able to interact.

How can these two class properties communicate to do a proper collision check while remaining encapsulated?

One idea is to run the collision check from within PlayState which is a class and has self.base = Base() and self.enemyFormation = EnemyFormation() which has Enemy class as a property. That would work fine. However, I'm trying really hard to keep such logic out the PlayState.lua file. It seems reasonable to me to think the Base class laser instance should do the collision check on all enemy laser instances vs. pushing it up to the PlayState.

Thoughts on this? Mostly I'm interested in how I'm thinking about things. I can make the collision detection work easily enough, but my goal with this effort is to avoid hacking my way through the build. I want to learn how to develop games well.

Thanks!

PlayState.lua file

Code: Select all

PlayState = Class{includes = BaseState}

function PlayState:enter(params)
self.starfield = Starfield()
self.base = Base()
self.shelters = Shelters()
self.enemyFormation = EnemyFormation()
end

function PlayState:update(dt)
self.base:update(dt)
if self.base.laser.isVisible then
self.base.laser:update(dt)
end
self.enemyFormation:update(dt)

for key, enemy in ipairs(self.enemyFormation.enemy) do
if self.base.laser:collision(self.enemyFormation.x + enemy.xOffset, self.enemyFormation.x + enemy.xOffset + enemy.width, self.enemyFormation.y + enemy.yOffset, self.enemyFormation.y + enemy.yOffset + enemy.height) and enemy.isActive and self.base.laser.isVisible then
enemy.isActive = false
self.base.laser:resetLaser()
end
end

for key, laser in ipairs(self.enemyFormation.enemyLasers) do
if laser:collision(self.base.x, self.base.x + self.base.width, self.base.y, self.base.y + self.base.height) then
love.event.quit()
end

if laser:collision(self.base.laser.x, self.base.laser.x + self.base.laser.width, self.base.laser.y, self.base.laser.y + self.base.laser.height) then
love.event.quit()
end
end
end

function PlayState:render()
self.base:render()
self.enemyFormation:render()
self.starfield:render()
end

Laser.lua

Code: Select all

Laser = Class{}

function Laser:init(x, y, direction, dy)
self.width = 2
self.height = 10
self.x = x
self.y = y
self.dy = dy or 150
self.direction = direction
self.isVisible = false
end

function Laser:update(dt)
self.y = self.y + self.dy * dt * self.direction
if self.y < 0 then
self:resetLaser()
end
end

function Laser:render()
if self.isVisible then
love.graphics.rectangle("fill", self.x, self.y, self.width, self.height)
end
end

function Laser:collision(xMin, xMax, yMin, yMax)
if self.y + self.height < yMin then
return false
elseif self.y > yMax then
return false
elseif self.x + self.width < xMin then
return false
elseif self.x > xMax then
return false
end
return true
end

function Laser:fireLaser(x, y)
self.isVisible = true
self.x = x
self.y = y
end

function Laser:resetLaser()
self.isVisible = false
self.x = 0
self.y = 0
end

EnemyFormation.lua

Code: Select all

EnemyFormation = Class{}

function EnemyFormation:init()
self.x = 10
self.y = 10
self.rows = 4
self.cols = 8
self.xMin = 0
self.xMax = self.cols * 40 - 20
self.yMin = 0
self.yMax = self.rows * 30 - 10
self.dx = 30
self.spacing = 2
self.enemy = self:generateEnemyFormation(self.cols, self.rows, self.spacing)
self.timer = 0
self.stepTime = 1
self.accelerator = 0.9
self.xStep = (VIRTUAL_WIDTH - (ENEMY_WIDTH * (self.spacing * (self.cols - 1) + 1))) / 20
self.yStep = 10
self.edgeFlag = false
self.stepFlag = true
self.width = self.cols * 40 - 20
self.enemyLasers = {}
end

function EnemyFormation:update(dt)
self.timer = self.timer + dt

self.xMin = self:xMinCheck()
self.xMax = self:xMaxCheck()

-- Simple timer to "step" the enemyFormation across screen and toggle edgeFlag
if self.timer > self.stepTime then
if not self.edgeFlag then
self.x = self.x + self.xStep
elseif self.edgeFlag then
self.y = self.y + self.yStep
self.stepTime = self.stepTime * self.accelerator
self.edgeFlag = false
end

self:fireEnemyLaser()
self.timer = 0
self.stepFlag = not self.stepFlag
end

for key, laser in pairs(self.enemyLasers) do
laser:update(dt)
end

-- Track position of bounding box to move Enemy instances as a group vs. individually
if self.x + self.xMax >= VIRTUAL_WIDTH then
self.edgeFlag = true
self.xStep = -self.xStep
self.x = VIRTUAL_WIDTH - self.xMax - 1
elseif self.x + self.xMin < 0 then
self.edgeFlag = true
self.xStep = -self.xStep
self.x = 1 - self.xMin
end
end

-- Pass x, y coordinates of the enemyFormation box for Enemy instances to use as a reference point.
function EnemyFormation:render()
for key, enemy in ipairs(self.enemy) do
if enemy.isActive then
enemy:render(self.x, self.y, self.stepFlag)
end
end

for key, laser in pairs(self.enemyLasers) do
if laser.y > VIRTUAL_HEIGHT then
table.remove(self.enemyLasers, key)
end
laser:render()
end
end

-- Function to generate all enemy instances within the formation
function EnemyFormation:generateEnemyFormation()
local enemies = {}
for row=1, self.rows do
for col=1, self.cols do
table.insert(enemies, Enemy(12, 12, row, col, self.spacing))
end
end
return enemies
end

-- Function to determine the leftmost x position of enemy formation
function EnemyFormation:xMinCheck()
for col=1, self.cols do
for row=1, self.rows do
if self.enemy[(col - 1) + (row - 1) * self.cols + 1].isActive then
return ((col - 1) * 40)
end
end
end
end

-- Function to determine the rightmost x position of enemy formation
function EnemyFormation:xMaxCheck()
for col=self.cols, 1, -1 do
for row=self.rows, 1, -1 do
if self.enemy[(col - 1) + (row - 1) * self.cols + 1].isActive then
return (col * 40) - 20
end
end
end
end

function EnemyFormation:fireEnemyLaser()
local shooter = self:selectShooter()
local laser = Laser(shooter.xOffset + shooter.width * 0.5 + self.x, shooter.yOffset + shooter.height + self.y, 1)
laser.isVisible = true
table.insert(self.enemyLasers, laser)
end

function EnemyFormation:selectShooter()
while true do
local col = math.random(self.cols)
for row = self.rows, 1, -1 do
if self.enemy[(row-1) * self.cols + col].isActive then
return self.enemy[(row-1) * self.cols + col]
end
end
end
end

Edited for clarity
ReFreezed
Party member
Posts: 217
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

### Re: Advice on getting two unrelated class objects to interact

I do think you're thinking too hard about encapsulation. For things to interact at all there has to be a connection somewhere, as you're indicating. Either the lasers have to know about the player, or the player has to know about the lasers, or the PlayState/some outside code has to either handle this logic itself or it needs to tell one object about the other objects. Just choose one place to put the code and then move things around later if needed. (I would personally try to put as much stuff as possible in as few places as possible, as long as the grouping of the code makes any sense.)

A thing about game development is that it's an iterative journey and you need to be able to add things, remove things and move things around over time. If things are a bit messy in the beginning, that's fine - you can always restructure the code better later when you know more about what you're actually doing and if it makes sense to do so (which it often doesn't because of time constraints/other priorities or it's fine enough as it is or whatever).

Also, I would like to say that there probably doesn't exists a game out there without some amount of "hacks". If you wanna develop games well you need experience - not 100% encapsulated code.
Tools: Hot Particles, LuaPreprocess, (more)
Games: Momento Temporis: Light from the Deep, Energize!
"If each mistake being made is a new one, then progress is being made."
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

### Re: Advice on getting two unrelated class objects to interact

Thanks so much for your perspective on this. It doesn't sound like there are any techniques I'm missing here due to my inexperience (or, if there are, they aren't critical to where I'm at and I'll get to them later).
ivan
Party member
Posts: 1719
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

### Re: Advice on getting two unrelated class objects to interact

Base class and the Enemy class are encapsulated from one another (am I saying that right?)
I think what you mean is "de-coupled" not "encapsulated".

Very briefly going over your code I would suggest moving the collisions outside to a separate "world" class.
This way one class doesn't need to access the internals of another.

Also, the following is not very good:

Code: Select all

if laser:collision(self.base.laser.x, self.base.laser.x + self.base.laser.width, self.base.laser.y, self.base.laser.y + self.base.laser.height) then
Just pass the actual collision object instead of a bunch of parameters:

Code: Select all

if laser:collision(hitmask) then
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

### Re: Advice on getting two unrelated class objects to interact

Agreed, passing the object vs. the parameters would be better. If I remember correctly, that is how I originally set it up, but I changed it for a reason I can't remember now. Thanks for the feedback!

### Who is online

Users browsing this forum: No registered users and 8 guests