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.
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Circular Require Woes

Post by r-hughes »

Hi, again.

It seems many moons ago I started a thread about problems I was having with circular requires. Well, they've struck again and I still don't have a good solution that doesn't require strange contortions of my architecture.

Let me explain the situation I keep running into.

I am trying to keep the code for my game in separate modules. For example, the code for creating, updating, and destroying players is in 'player.lua.'

I also have a module for handling creating, updating, and destroying scenes, 'scenes.lua.'

In 'scenes.lua,' I must require 'player.lua,' because many scenes will need player objects in them. However, in 'player.lua,' I would like to be able to call the function scenes.destroy() on the current scene, when the player dies. This means I have to require 'scenes.lua' inside player.

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.

1) Use globals.
2) Stuff functions in the tables and use them like OOP classes.
3) Separate everything into separate modules 'create_player.lua', 'destroy_player.lua', etc...
4) require() inside the functions

Previous explorations of this issue have used resulted in advice to either do one of those three things and, vaugely, to 'rethink' my architecture.

It'd be super helpful if you could give me any insight as to how to solve this problem. Thank you.
User avatar
DanielPower
Citizen
Posts: 50
Joined: Wed Apr 29, 2015 5:28 pm

Re: Circular Require Woes

Post by DanielPower »

One option would be to pass 'scene' to your player. In your scene.lua file:

Code: Select all

local player = require('player')
player.scene = scene
Then you should simply be able to access player.scene, or self.scene from your player.lua file.
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

That's definitely an option, but it seems a bit weird to pass in an entire module like that... I usually reserve table values for actual properties of that table, as though it were a struct, except in the case of tables acting as modules... in which case they simply store functions.
User avatar
DanielPower
Citizen
Posts: 50
Joined: Wed Apr 29, 2015 5:28 pm

Re: Circular Require Woes

Post by DanielPower »

If you don't want to use globals, you need to give 'player' a local variable that lets it find the scene that created it.
Also, in terms of performance, you're not really passing an entire module. Simply creating a link. 'player.scene' will be the exact same variable as 'scene' in scene's scope. So any modifications to 'scene' will be reflected in 'player.scene'.

Something I often do for these cases is prefix the variable name with an underscore to signify that it is a link to another library, and not part of this library.

player._scene = scene


Alternatively, you could have a parent module create and maintain a list of your modules. You could then access scene from player with 'self.parent.module['scene']. Although this is an uglier solution in my opinion.
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

I understand that it's just a reference, but it seems so *weird* to me.

Maybe this is a case where globals are the answer, despite everything telling me 'no!' Keeping all modules in the global scope might be alright.
alloyed
Citizen
Posts: 80
Joined: Thu May 28, 2015 8:45 pm
Contact:

Re: Circular Require Woes

Post by alloyed »

This is what I've done when I needed a quick and dirty solution to the problem

Code: Select all

--- module 'this'
function this.new()
  local that = require 'that'
  that.thing()
end

--- module 'that'
function that.new()
  local this = require 'this'
  self.add(this.new())
end

--- module 'main'
local this, that = require 'this', require 'that'
that.new()
So you can load the modules without any dependencies at all, and as soon as you've loaded the module, require() clocks out: it's done its job, and it can just give you the already loaded module next time you need it.

I do think it's worth thinking of a way to not have mutually recursive modules in the long run, though.
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: Circular Require Woes

Post by r-hughes »

I do think it's worth thinking of a way to not have mutually recursive modules in the long run, though.
This has been repeated as mantra, but never explained. How would I do that in this situation? I appreciate your post, but I've failed to understand what the solution is in terms of architectural design and nobody has explained it beyond saying 'you're doing it wrong.' :(

I don't think my design is completely unreasonable. I need to dispose of the scene when the player dies. The most straightforward way to do that is to call destroy_scene() in destroy_player(). Evidently, this is wrong. So let's think about ways to avoid the circular require completely that don't involve contortions of the codebase or the use of many globals to hold modules of functions.
alloyed
Citizen
Posts: 80
Joined: Thu May 28, 2015 8:45 pm
Contact:

Re: Circular Require Woes

Post by alloyed »

It sounds like you're using the function destroy_scene() to access some state, presumably doing something like

Code: Select all

local my_scene = { ... }

function scene.destroy()
   do_thing(my_scene)
end
My question is, why not you make that dependency explicit, either using an explicit scene object or passing in the whole module (which is semantically the same as an object) as your function argument?

Code: Select all

function player.destroy(child)
  child.destroy()
end
User avatar
Positive07
Party member
Posts: 1014
Joined: Sun Aug 12, 2012 4:34 pm
Location: Argentina

Re: Circular Require Woes

Post by Positive07 »

Callbacks?

In scene.lua

Code: Select all

player.destroyScene = function ()
   scene.destroy()
end
in player.lua

Code: Select all

function player:doSomething ()
   self.destroyScene() --Ugly but works
end
Another option is a function like this

in player.lua

Code: Select all

local scenedestroyed = false

function player:doSomething()
   scenedestroyed = true
end

function player.hasDestroyedScene()
   return scenedestroyed
end
But heck this is all ugly because your player is trying to do stuff it shouldn't!! You should have a game.lua module which handles your game logic, when the player performs some actions your game logic would destroy the scene, not your player!

The idea basically is that all your modules do one thing and one thing only, your player.lua module should only take care of your player, no collision detection, NPC interaction or moster fighting should be done here.

Same with monsters and everyting else

Your game logic is in a separate module which handles some of this stuff (you could split everything even more)

When a module depends on a parent module you are splitting stuff in a wrong way, probably that child is doing too much and should let the parent handle those situations, maybe even a higher parent. In this case maybe scene can take care of what the player was trying to do, if it's not his job either maybe a parent module should do it, as I proposed some kind of game.lua module.
for i, person in ipairs(everybody) do
[tab]if not person.obey then person:setObey(true) end
end
love.system.openURL(github.com/pablomayobre)
User avatar
4aiman
Party member
Posts: 262
Joined: Sat Jan 16, 2016 10:30 am

Re: Circular Require Woes

Post by 4aiman »

Boy, how it is easy to forget that "you should change your architecture" means next to nothing to the one who doesn't know "what?", "how?" or even "why do I have to?" to change it.

@r-hughes
My suggestion would be setting a simple field of a player to true. Like this

Code: Select all

player.scene_should_be_destroyed = true
Monitor that field in your scene module and destroy current scene in the scene module if that field was set to true.
You may also set that field to false immediately, so that your new scene won't be destroyed on the next update.

That's the simplest way in your case.
Post Reply

Who is online

Users browsing this forum: Bing [Bot], Google [Bot], slime and 5 guests