Page 1 of 1

Managing multiple class objects at same time

Posted: Thu Dec 03, 2020 5:18 am
by Krumpet
What is the best way to structure a situation where a game has multiple instances of an object at the same time? I'm building a Space Invaders clone and have used two classes to manage the enemies: EnemyFormation and Enemy. The EnemyFormation class sets self.enemy property to a table of Enemy instances.

I would prefer to have these two classes be combined, but I'm stuck on the fact that such a class would need to hold an array of itself...which I'm unsure of how to accomplish. What is the preferred way to handle such a situation when working with classes?

I'm using Matthias Richter's Hump class library.

Thanks!

Here is my EnemyFormation class...

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
Here is my Enemy class...

Code: Select all

Enemy = Class{}

function Enemy:init(width, height, row, col, spacing)
  self.xOffset = (col - 1) * ENEMY_WIDTH * spacing
  self.yOffset = (row - 1) * ENEMY_HEIGHT * spacing
  self.width = width
  self.height = height
  self.isActive = true
  self.isShooter = false
end

function Enemy:render(formationX, formationY, stepFlag)
  if stepFlag then
    love.graphics.setColor( gColorPalette['yellow'], 1)
  else
    love.graphics.setColor( gColorPalette['lgreen'], 1)
  end
  love.graphics.rectangle("fill", formationX + self.xOffset, formationY + self.yOffset, self.width, self.height)
end
Edited for typos

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 2:51 am
by ReFreezed
Is there an issue with how you're currently handling the situation? As I understand it, EnemyFormation represents a group of enemies and thus is a different "thing" from what Enemy represents, i.e. an enemy. That separation into two classes seem logical. I'm not sure what you mean by combining these two things?

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 3:00 am
by Krumpet
The issue is I would prefer to manage the enemies with a single class. I worry that using two classes is amateur hour and that I'm not seeing an obvious way to accomplish the same results with one class.

It isn't that what I have doesn't work. It works. I'm trying to understand if my approach could be more "professional" in its design.

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 4:12 am
by ReFreezed
Well, you can always get rid of the Enemy class and just have the enemies be represented by "plain old tables" (i.e. basically move the Enemy methods/functionality to the EnemyFormation class, and think of the enemies as just data instead of individual "objects"). It's a good idea in general to keep things simple, and thinking about data is often easier than trying to combine both data and functionality (in classes).

In other words, remove the concept of an individual enemy (except inside the EnemyFormation class where needed) but keep the concept of multiple enemies.

There's no objectively "best" way of doing this though. The real answer is to just do what feels more intuitive to you (which is probably going to change as you get more experience).

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 4:58 am
by Krumpet
What you are suggesting sounds like what I did for my Shelter class. I wanted to have four shelters the player can hide behind. This put me in the exact same situation of having to manage multiple instances. As I mentioned, I wanted to avoid using two classes to do this. I ended up putting the x-, y-coordinates directly in a table in the Shelter class as follows:

Code: Select all

Shelter = Class{}

function Shelter:init()
  self.width = 35
  self.height = 15
  self.x = VIRTUAL_WIDTH / 5
  self.y = VIRTUAL_HEIGHT - 50
  self.shelters = {{x=self.x - self.width / 2, y=self.y},
                   {x=self.x * 2 - self.width / 2, y=self.y},
                   {x=self.x * 3 - self.width / 2, y=self.y},
                   {x=self.x * 4 - self.width / 2, y=self.y}}
end

function Shelter:render()
  love.graphics.setColor(gColorPalette['white'], 1)
  for key, shelter in ipairs(self.shelters) do
    love.graphics.rectangle("fill", shelter.x, shelter.y, self.width, self.height)
  end
end
For me to take this approach with the Enemy class, I would need to generate the table of Enemy instances in the EnemyFormation init function. Is placing logic in an init file ok? Frowned upon? I'm not really sure and it makes the init function sort of messy and that makes me nervous...

Thanks!

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 8:51 pm
by ReFreezed
Yeah, that Shelter situation does look similar, though I have to ask why a Shelter consists of multiple actual shelters? If there's no relation between the individual shelters then just have each Shelter represent one thing (i.e. have four separate Shelter objects instead of that self.shelters array). Right now, both EnemyFormation and Shelter are like "manager" classes, but the former may have something like an AI that logically connects the "managed objects" while the latter does not need anything like that (unless your game actually does).

Anyway, all logic has to happen at some point, whether it's in the "init" function or later. Putting things in init() doesn't necessarily mean it's messy - it depends on how your game works, or rather, what rules you've put in place for your own code. For example, if your objects are trying to access other objects at init time then maybe it makes sense to put that code in a secondary init function that you call after all objects have been created and are ready to be accessed by others. You can think of it as there being multiple phases when you load a level, for example an "init" phase (where "simple" initialization happen) and an "objects are ready" phase (where more involved things happen).

Similarly, there can also be multiple phases that happen during an update, like a "normal update" phase (where maybe you update velocities/positions and AI etc.), a "physics update" (where physics simulation, including collisions, happens) and an "animation update" (before things are rendered to the screen). If you identify what "phases" your game needs to work then you'll know better where to put code and thus things will be less messy.

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 9:05 pm
by Krumpet
I think what you're saying is either approach (a single class or a class with a manager class) is acceptable.

The issue I noticed with this Shelter approach is updating the Shelters (for example when they take damage from the Enemy objects) will be much harder when they aren't individual instances.

I did consider creating four separate shelters in this class, but the idea of hard coding each shelter (Shelter1, Shelter2, etc) didn't sit well with me. Also, I don't know of any way to create a Shelter instance inside the Shelter class. Pretty sure that isn't possible...hence the need for a manager class. All that being said, the manner in which I ended up doing it isn't any better. Where I want to land is if I decide in the future to have five shelters, I want to be able to edit one line of code and have the rest of the game adjust accordingly. Perhaps I'll weave that into the player control options. They can decide whether they want 2, 4, or 6 shelters, for example.

Edited for clarity

Re: Managing multiple class objects at same time

Posted: Sun Dec 06, 2020 10:27 pm
by ReFreezed
I would advice minimizing the usage of manager classes as I think they're usually not very useful - they often just spread out the code. But the "state of things" has to be controlled somewhere, whether it's in a manager class or e.g. directly in PlayState (in reference to your other thread).

I'm not sure I explained very well regarding the shelters. Instead of having each Shelter object contain an array of "actual" shelters, have an array outside the class containing a list of Shelter instances, like this:

Code: Select all

function PlayState:enter(params)
	self.shelters = {}

	for i = 1, 4 do -- 4 here can be a variable!
		local shelter = Shelter()
		shelter.x     = something
		shelter.y     = something
		table.insert(self.shelters, shelter)
	end
end
Aiming for a "flatter" game structure usually makes things simpler than having a deeper one (where one thing contains other things that contains other things... etc).

You could do the same with the enemies - have an array of all Enemy instances (from all formations) stored in the PlayState and have each EnemyFormation "reach out" to the PlayState, look for the related Enemy objects and do stuff to them. That does sound a bit messy though and maybe isn't good in this case.

In my games I always have an object representing the world or the level, and then I store all "world objects" (like the player, enemies, projectiles, sound emitters, purely graphical objects etc.) in that object. In your code, PlayState seem to be the equivalent object.

Re: Managing multiple class objects at same time

Posted: Mon Dec 07, 2020 1:34 am
by Krumpet
Funny! I was aiming to trim my PlayState file down to the absolute bare minimum in favor of everything living in deeper object relationships. In my head, I'm drawn to such relationships. They seem simpler and more logical to me. However, as I've gone further down this path, I've begun to run into the issues I've mentioned. Tops on the list is this approach leading to decoupled classes that need a way to communicate.

Just like you suggested, I've seen others propose the idea of a "World" object to use as the unifying location for everything. I'm thinking there are (at least) two approaches here. I'll give that some deeper thought.

Thanks for taking the time to weigh in here. Greatly appreciate your time!

Re: Managing multiple class objects at same time

Posted: Mon Dec 07, 2020 5:57 am
by ReFreezed
Sure thing! :)