Circular Require Woes

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.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Circular Require Woes

Post by airstruck »

r-hughes wrote:Obviously, this is a circular dependency.

There are a few ways to avoid this, but I'm not a big fan of any of them.
I'd wager if anyone comes up with anything substantially different from the workarounds you listed, you won't be a big fan of that either. I don't blame you; circular dependencies are ugly, and the workarounds just make them uglier.

For what it's worth, I'd probably consider the OOP solution to be a "legitimate" solution and not just a workaround. It's unfortunate that it doesn't suit your style of programming.
Previous explorations of this issue have used resulted in advice [...] to 'rethink' my architecture.
If you don't like workarounds, I'm not sure what other options you have. "Rethinking your architecture" could mean not limiting yourself to a strictly procedural design. Or it could mean that you might consider circular dependencies to be a sign that some dependencies are pointing in the wrong direction (see other comments). You could employ some strategy to remove the "scene" dependency from the "player" stuff, for example set up a simple events system, and when the player dies, broadcast a "player died" event, and listen for "player died" events someplace else, and destroy the scene there.
It'd be super helpful if you could give me any insight as to how to solve this problem. Thank you.
Rethink your...
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

Thanks. For clarity, I never suggested that changing my architecture was out of the question... only that it lead to unusual restructuring of the code (passing in modules of functions is the worst of these imo).

Other options have design implications. If I set a flag, and let the scene handle its own destruction (this is what I currently do), then all objects after the flag have been set still get updated. This is a slightly different behaviour.

I suppose there is no good solution.
User avatar
zorg
Party member
Posts: 3441
Joined: Thu Dec 13, 2012 2:55 pm
Location: Absurdistan, Hungary
Contact:

Re: Circular Require Woes

Post by zorg »

r-hughes wrote:(passing in modules of functions is the worst of these imo)
I really don't see the issue with this, since i use this method the most; but then again, it may simply not work for you.
Me and my stuff :3True Neutral Aspirant. Why, yes, i do indeed enjoy sarcastically correcting others when they make the most blatant of spelling mistakes. No bullying or trolling the innocent tho.
User avatar
4aiman
Party member
Posts: 262
Joined: Sat Jan 16, 2016 10:30 am

Re: Circular Require Woes

Post by 4aiman »

r-hughes wrote:If I set a flag, and let the scene handle its own destruction (this is what I currently do), then all objects after the flag have been set still get updated. This is a slightly different behaviour.
I thought it's your scene that is in charge of updating everything :o:
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

I thought it's your scene that is in charge of updating everything :o:
'in charge' is a very strange notion, since a scene is just a table holding some data like a C struct. I actually update all my game objects outside of the scene (I maintain a table of objects and if they have an active flag set to true, then I update and render them).
zorg wrote:
r-hughes wrote:(passing in modules of functions is the worst of these imo)
I really don't see the issue with this, since i use this method the most; but then again, it may simply not work for you.
After thinking about it, I might do just that. I'll give an example of what I have in mind for comment.

Code: Select all

-- game.lua

local game = {}

function game.new()
	-- create a new game instance
	local g = {}
	g.scenes = require("scenes")
	g.objects = require("objects")
end

function game.update(g)
    -- update the game
end

function game.quit(g)
    -- quit the game
end

return game

Code: Select all

-- objects.lua

local objects = {}

function objects.new(game)
    -- create a new player instance
end

function objects.update(o, game)
    -- update the player
end

function objects.destroy(o, game)
    game.scenes.destroy(game.active_scene, game)
end

return objects

Code: Select all

-- scenes.lua

local scenes = {}

function scenes.new(game)
    -- create a new scene instance here
end

function scenes.update(s, game)
    -- update a scene
end

function scenes.destroy(s, game)
    -- destroy a scene
end

return scenes

User avatar
4aiman
Party member
Posts: 262
Joined: Sat Jan 16, 2016 10:30 am

Re: Circular Require Woes

Post by 4aiman »

r-hughes wrote:'in charge' is a very strange notion, since a scene is just a table holding some data like a C struct.
Well, I've never seen any actual code you're using to manipulate anything, let alone a *.love files posted by you.
No wonder I got confused by the fact you've said nothing on samples like the one with

Code: Select all

function scene.destroy()
, where a scene is not just a struct, but rather an object with it's own methods. :P
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

In that case, 'scene' is just a module containing functions relevant to scenes.

I'm sorry for being misleading. Obviously, I'm very rubbish at this stuff, but trying to improve.

In my code, functions are put into modules. Any 'objects' are actually just tables with values, not functions.
User avatar
airstruck
Party member
Posts: 650
Joined: Thu Jun 04, 2015 7:11 pm
Location: Not being time thief.

Re: Circular Require Woes

Post by airstruck »

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

Code: Select all

currentScene:destroy()
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
Post Reply

Who is online

Users browsing this forum: Google [Bot] and 21 guests