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
Before you make a thread asking for help, read this.
Post Reply
User avatar
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

Advice on getting two unrelated class objects to interact

Post by Krumpet »

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
User avatar
ReFreezed
Party member
Posts: 612
Joined: Sun Oct 25, 2015 11:32 pm
Location: Sweden
Contact:

Re: Advice on getting two unrelated class objects to interact

Post by ReFreezed »

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, InputField, (more) Games: Momento Temporis
"If each mistake being made is a new one, then progress is being made."
User avatar
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

Re: Advice on getting two unrelated class objects to interact

Post by Krumpet »

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).
User avatar
ivan
Party member
Posts: 1911
Joined: Fri Mar 07, 2008 1:39 pm
Contact:

Re: Advice on getting two unrelated class objects to interact

Post by ivan »

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
User avatar
Krumpet
Prole
Posts: 12
Joined: Thu Dec 03, 2020 4:59 am

Re: Advice on getting two unrelated class objects to interact

Post by Krumpet »

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!
Post Reply

Who is online

Users browsing this forum: Ahrefs [Bot], Google [Bot] and 76 guests