Honestly what you have looks like a perfect candidate for the OOP approach. You have a handle to the scene instance, but can't get a handle to the functions that are supposed to operate on that instance in a clean way. You could address it by adding two very short lines to scenes.lua:
Code: Select all
-- scenes.lua
local scenes = {}
local meta = { __index = scenes } -- metatable for all instances
function scenes.new()
-- create a new scene instance
local s = {}
-- your initialization stuff here...
return setmetatable(s, meta) -- set the metatable and return the instance
end
function scenes.update(s)
-- update the player
end
function scenes.destroy(s)
-- update the player
end
return scenes
Scenes will still work exactly as they did, and you can keep using them in a procedural way. But scene instances will also now have all the functions exposed by scenes.lua as methods, so you can also use them as objects. Now when you handle player death you can do
instead of
Code: Select all
local scenes = require 'scenes'
scenes.destroy(currentScene)
So you add two lines of code, you can keep everything else as you had it, and you can also use your instance tables as objects now. Wouldn't this be cleaner than the proposed solution that gives your entity manager the power to destroy scenes?
In other words, the problem with players.lua requiring scenes.lua is that scenes.lua contains a specific implementation for handling scenes, which the player manager should not care about. Imagine you add cutscenes, another type of scene, and you want a different scene manager for it (cutscenes.lua) with the same API as scenes.lua. If you want to use players in regular scenes or cutscenes, players.lua now needs to choose
which scene manager to use to operate on the current scene.
With scenes as objects, you get the benefits of polymorphism. The player entity manager can ask an individual scene to do something, and it doesn't have to worry about implementation (which scene manager to use), only contract (method signatures shared by all scene types). The circular dependency goes away because the player manager no longer relies on any particular scene manager, only a notion of how scene managers work.
If you do the same thing in players.lua and elsewhere, things will mostly be bound by contract rather than to particular implementations of one another. You'd still need to use specific implementations to create new instances (module.new), unless you use something like DI to address that (pretty straightforward with first-class modules).
Of course you still have the stylistic question about whether player managers should have any knowledge at all of how scene managers work. You might find over time that you need to do stuff with lots of modules when a player dies (highscore, goals, playback), and think you'd rather have the player broadcast a "died" message, and let other things consume it, decoupling player managers from the other modules. But it's just a stylistic question now, the circular dependency issue is solved (and you probably won't see it again if you stick to this pattern, and if you do, it might be time for DI).
I do like the idea of using "game" as a sort of resource locater, but you might consider internalizing it as a field of the entity/scene instances so you don't have to pass it to all those functions (the associated game probably won't change over the object's lifetime).
Code: Select all
function scenes.new(game)
local s = { game = game }
-- ...
end