paralyzed by engine architecture problems

General discussion about LÖVE, Lua, game development, puns, and unicorns.
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

paralyzed by engine architecture problems

Post by r-hughes »

Three weeks ago I started working on my game and got really far. Development was a breeze. But I knew I had some architectural problems that I wanted to fix so I could reuse portions of the code in my next game. I haven't done anything in two weeks except stare at the same errors.

I'm trying to write my game in a very procedural style. I am specifically avoiding using any kind of OOP (and not interested in discussing its merits here).

The problem I've run into is that some modules in my code are highly mutually dependent and this leads to circular requires in lua.

e.g.

My event dispatcher will receive an event through a raise_event() call. It will then loop over its listeners and call an event handler function depending on what kind of listener it is (e.g. player_handle_event() or monster_handle_event()). Within these handlers, new events may be raised.

The problem is that in trying to separate my code into clean modules and keeping all the event handlers and event dispatcher in separate files... I can't figure out how to include them in the other without creating a circularity.

The dispatcher needs to know about the event handlers and the event handlers need to know about the dispatcher and I would prefer not jamming everything into one file.
User avatar
undef
Party member
Posts: 438
Joined: Mon Jun 10, 2013 3:09 pm
Location: Berlin
Contact:

Re: paralyzed by engine architecture problems

Post by undef »

Why do the event handlers need to know about the dispatcher?

Could you show a minimal example of your event system?
twitter | steam | indieDB

Check out quadrant on Steam!
User avatar
T-Bone
Inner party member
Posts: 1492
Joined: Thu Jun 09, 2011 9:03 am

Re: paralyzed by engine architecture problems

Post by T-Bone »

While this might not be the solution you're looking for, do you know that it's possible to reference variables in functions that don't exist when the function is created? So you could keep things as global variables, and have as many references back and forth as you like, and require them in any order you like, as long as you start calling functions after all the scripts have been required. Does that make any sense?

I would personally try to avoid this style, as it's a bit hard to maintain, but it could be worth it if it solves some bigger problem for you.
User avatar
s-ol
Party member
Posts: 1077
Joined: Mon Sep 15, 2014 7:41 pm
Location: Cologne, Germany
Contact:

Re: paralyzed by engine architecture problems

Post by s-ol »

Other ways might be passing the dependencies as arguments to a function of the module loaded first or having a module just for getting dependencies (loader = require "loader"; then use loader.getmodule1() or something). the latter is total overkill IMO.

I think there is no real one-fits-all solution here, if you show us your code I am sure we can come up with something that works well with it. In general its not as much an architectural issue as it seems, remember things only need to be ready when they are used and not when the module is loaded (like T-bone said).

s-ol.nu /blog  -  p.s-ol.be /st8.lua  -  g.s-ol.be /gtglg /curcur

Code: Select all

print( type(love) )
if false then
  baby:hurt(me)
end
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: paralyzed by engine architecture problems

Post by r-hughes »

undef wrote:Why do the event handlers need to know about the dispatcher?

Could you show a minimal example of your event system?
Because events can be raised within the event handlers.

Something like this (not exact but close enough to the idea)

Code: Select all

dispaptcher.lua

local event_handlers = require('event_handlers')

local dispatcher = {}

function dispatcher.dispatch_event(dispatcher, event, world)
    ... find listeners and call the appropriate event handler ...
    (in this case it will be mimic_event_handler())
end

function dispatcher.new_event(name, value)
	local e = {}
	e.name = name
	e.value = value
	return e
end

return dispatcher

Code: Select all

event_handlers.lua

local dispatcher = require('dispatcher')

local event_handlers = {}

function event_handlers.mimic_event_handler(object, event, world)
	if event.name == "PLAYER_SHOOT" then
		dispatcher.dispatch_event(world.dispatcher, dispatcher.new_event("MIMIC_SHOOT", object), world)
	end
end

return event_handlers
Last edited by r-hughes on Thu Mar 10, 2016 2:54 pm, edited 6 times in total.
User avatar
s-ol
Party member
Posts: 1077
Joined: Mon Sep 15, 2014 7:41 pm
Location: Cologne, Germany
Contact:

Re: paralyzed by engine architecture problems

Post by s-ol »

in your example neither file needs to require the other, because all functions live in the global environment and therefore do not need to exist at the time of being referenced in another function.

s-ol.nu /blog  -  p.s-ol.be /st8.lua  -  g.s-ol.be /gtglg /curcur

Code: Select all

print( type(love) )
if false then
  baby:hurt(me)
end
User avatar
Tanner
Party member
Posts: 166
Joined: Tue Apr 10, 2012 1:51 am

Re: paralyzed by engine architecture problems

Post by Tanner »

A system that makes adequate use to events to manage global state (and is very popular right now amongst web developers) is called Flux. But the reason it gets away with what you're trying to accomplish is mostly because of the declarative nature of the render tree (AKA React). Events are typically only created from within the context of those largely immutable views. And then those views are optimized to consume data modified by events and never consume the events themselves. Also, typically, this architecture does not run at 60 FPS. The constraints on the way events flow unidirectionally and the declarative nature of the render tree make it slow relative to more imperative methods that more closely match the way that OpenGL works.

It's a bad idea to allow your event handlers to dispatch events. You're running into problems because this is a poor architectural decision (albeit a really common one). The issues with the circular dependency that you're running into now are nothing compared to what you will likely run into later when dispatching events at 60 frames per second. It will likely be the worst debugging experience you've ever had.

I want to leave you with a link to post written by John Carmack and shared by Johnathan Blow, two game developers for whom I have a lot of respect. It's about side-effect free functions and where Carmack thinks the payoffs for easy debugging and testability outweigh the performance gains of mutating global state. I hope you find it interesting. http://number-none.com/blow/john_carmac ... _code.html
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: paralyzed by engine architecture problems

Post by r-hughes »

S0lll0s wrote:in your example neither file needs to require the other, because all functions live in the global environment and therefore do not need to exist at the time of being referenced in another function.
Sorry. I was unclear. None of the functions exist in the global environment because they are all in packages. I have edited my post. I was just trying to illustrate the concept.
Tanner wrote:A system that makes adequate use to events to manage global state (and is very popular right now amongst web developers) is called Flux. But the reason it gets away with what you're trying to accomplish is mostly because of the declarative nature of the render tree (AKA React). Events are typically only created from within the context of those largely immutable views. And then those views are optimized to consume data modified by events and never consume the events themselves. Also, typically, this architecture does not run at 60 FPS. The constraints on the way events flow unidirectionally and the declarative nature of the render tree make it slow relative to more imperative methods that more closely match the way that OpenGL works.

It's a bad idea to allow your event handlers to dispatch events. You're running into problems because this is a poor architectural decision (albeit a really common one). The issues with the circular dependency that you're running into now are nothing compared to what you will likely run into later when dispatching events at 60 frames per second. It will likely be the worst debugging experience you've ever had.

I want to leave you with a link to post written by John Carmack and shared by Johnathan Blow, two game developers for whom I have a lot of respect. It's about side-effect free functions and where Carmack thinks the payoffs for easy debugging and testability outweigh the performance gains of mutating global state. I hope you find it interesting. http://number-none.com/blow/john_carmac ... _code.html
So then what is the solution? How do I avoid this design?

I suppose in the case I presented I could set a flag in the mimic or change its state in the event_handler, and then on the next update call it would act appropriately and fire off an event. To be clear, I am not modifying global state in any way.
Last edited by r-hughes on Wed Mar 09, 2016 6:08 pm, edited 1 time in total.
User avatar
undef
Party member
Posts: 438
Joined: Mon Jun 10, 2013 3:09 pm
Location: Berlin
Contact:

Re: paralyzed by engine architecture problems

Post by undef »

Think about which part of your program should be allowed to dispatch events and then expose it to an appropriate environment.
If you want to be able to dispatch events from anywhere that would be the global environment.

I find your code a little puzzling to be honest, but I think if you would do something like that you would get the results you want:

Code: Select all

-- main.lua or whatever your desired "upper hierarchy file" is

dispatcher = require"dispatcher
event_handlers = require"event_handlers"
This is a direct and simple way to approach it, and shouldn't be problematic for smaller projects.
If you're working on something bigger, you can think about placing state specific event handlers in tables and change the reference to event_handlers when you change the state.
Your dispatcher should always work the same and is therefore state independent.


One more thing you might not know about:
In Lua it is common to represent tuples as a key value pair in a table.
So your newEvent function could be implemented like this:

Code: Select all

function dispatcher.new_event(name, value)
   return { name = value } -- don't get what value (or object) is and what you need it for but ok
end
twitter | steam | indieDB

Check out quadrant on Steam!
r-hughes
Prole
Posts: 15
Joined: Wed Jan 20, 2016 1:25 am

Re: paralyzed by engine architecture problems

Post by r-hughes »

undef wrote:Think about which part of your program should be allowed to dispatch events and then expose it to an appropriate environment.
If you want to be able to dispatch events from anywhere that would be the global environment.

I find your code a little puzzling to be honest, but I think if you would do something like that you would get the results you want:

Code: Select all

-- main.lua or whatever your desired "upper hierarchy file" is

dispatcher = require"dispatcher
event_handlers = require"event_handlers"
This is a direct and simple way to approach it, and shouldn't be problematic for smaller projects.
If you're working on something bigger, you can think about placing state specific event handlers in tables and change the reference to event_handlers when you change the state.
Your dispatcher should always work the same and is therefore state independent.


One more thing you might not know about:
In Lua it is common to represent tuples as a key value pair in a table.
So your newEvent function could be implemented like this:

Code: Select all

function dispatcher.new_event(name, value)
   return { name = value } -- don't get what value (or object) is and what you need it for but ok
end
What part puzzles you? In my example, when the player shoots I want to raise an event for it. This is picked up by things like the audio system and decouples the player logic from the audio. I also want, in this case, to signal all mimic enemies to shoot at the player.

The object value in the event handler is necessary because the event handler IS NOT CONTAINED within the object itself. It is just a function that takes an object. The name of the event is just its name. The value of the event is a table with any additional info that might be necessary for the event (player location, which object spawned the event, et cetera). Sometimes this is an empty table if no value is needed.

I don't understand how your proposed code would work since dispatcher.lua requires event_handlers.lua requires dispatcher.lua... etc. It's still circular.

The issue is how to dispatch events from within the event handlers. Or if I even should be doing that.
Post Reply

Who is online

Users browsing this forum: No registered users and 177 guests