HaxeFlixel / flixel

Free, cross-platform 2D game engine powered by Haxe and OpenFL
https://haxeflixel.com/
MIT License
2k stars 444 forks source link

Make the plugin system more powerful #570

Closed Gama11 closed 11 years ago

Gama11 commented 11 years ago

Plugins are a great way to expand the engine without bloating the core or having to fiddle with it directly. We should be looking forward to ways to make this feature even more powerful!

I've already started this by introducing a new FlxPlugin class (35eed1aff1fd824202d2be63db62d91848b82b7b), which all plugins must extend instead of FlxBasic. It currently only offers two more features: They get notified of Event.RESIZE (via onResize()) and state switches via the empty onStateSwitch() methods that can be overriden.

@photonstorm was talking about improving the flixel plugin system in the v3.0 planning thread in the official flixel forums:

I think one of the biggest benefits flixel could have (beyond Molehill, which is a pretty vital move come 2012) would be to create a standard plugin system for it. So it can be easily extended by 3rd parties, and provide plenty of hooks deep into it, so they can really prod about and make it do things even you never thought of!

I share his thoughts. Too bad he didn't go into more detail what the exact implementation of this could look like.

@volvis has shown an interest in the flixel plugin system as well, by creating the FlixelPlugins repo very recently:

A hopefully growing collection of plugins and experiments for HaxeFlixel.

@volvic: Given your interest in the plugin system, are there any features you'd like to see for it?

Personally, I'd like to see a plugin system powerful enough to make the vcr a plugin. This requires some hooks deep into FlxGame, specifically we'd at least need the following notification functions in FlxPlugin:

I'm not sure it's a good idea to just keep on adding functions to FlxPlugin that get called for whatever event you could think of. It seems like that could create performance issues due to generating a lot of additional, unnecessary function calls.

Instead, it might be worth considering to create a flixel event system, similar to the flash events. You could add event listeners to a plugin like so:

plugin.addEventListener(FlxEvent.STEP, stepEventHandlerFunction);

That way, only the plugins that need a certain event get notified, which should improve the overall performance.

Phew, this has become a bit of a text wall.

As always, any input is greatly appreciated. :)
sergey-miryanov commented 11 years ago

As was discussed this is a proposal of typed signal system:

Some links: http://alecmce.com/as3/events-and-signals-performance-tests http://lab.polygonal.de/?p=2548 https://github.com/massiveinteractive/msignal https://github.com/zweimal/HxSignal

Gama11 commented 11 years ago

@Beeblerox suggested that it might be beneficial to have plugins that are persistent and ones that aren't (and are thus destroyed (and recreated?) when the state is changed). A persistent boolean in FlxPlugin should be able to handle that.

sergey-miryanov commented 11 years ago

if HaxeFlixel core will have stateChanged signal - each plugin can decide what to do - reset own internal state, ignore signal or do something else

Gama11 commented 11 years ago

@sergey-miryanov That is already the case, but this could be automated by FlxG.plugin.onStateSwitch(). It could check for persistence and then destroy the plugin if it's not persistent. If plugin.autoRevive == true, a new instance would be created automatically.

impaler commented 11 years ago

I think using signals are worth investigating, you could then use an interface for plugins instead. Have init(), enableSignals(), disableSignals(), destroy() as methods for the interface IFlxPlugin. When you add the plugin it calls init() and then enableSignals for where you would bind them to what the plugin needs. I use my own simple signals here https://github.com/impaler/Omni-Utils/blob/master/omni/utils/signals/OSignalType.hx. I suggest that the event dispatcher in Flash is not worth using and something like polygonals' work may perform best.

gamedevsam commented 11 years ago

If we're going to implement a Signal system, we should look at those that use fast data structures and rapid data access, as well as typed function call implementations (for signals that need params). Polygonal wrote a nice blog post about their signals lib which uses macros to create very fast way to identify which objects are listening to what events: http://lab.polygonal.de/?p=2548

In addition, we should also research hxs, the official haxe signaling library, which is fully typed and could provide some good insights into the needs of a signaling library: https://code.google.com/p/hxs/wiki/Overview

With a fast and optimized implementation, we could refactor the engine to have separate lists for active and visible objects, instead of brute force iterating through every object twice per frame (once per update, once per render) and checking if they exist and are active / visible.

// in set_active() call:
if(active)
    attach(update, FlxSignals.Step);
else
    detach(update, FlxSignals.Step);

// in set_visible() call:
if(visible)
    attach(draw, FlxSignals.Step);
else
    detach(draw, FlxSignals.Step);

// in plugin constructor that cares about resize events
attach(resize, FlxEvent.Resized);

This could open the door to more efficient multi-theading techniques, which are currently a problem due to the fact that we change FlxStates in the update loop (thus potentially causing a race condition in render thread if objects are removed while they're being rendered), and the fact that we store objects in a giant array inside FlxGroups (which could contain null entries, and can't be manipulated across threads). See this post here on StackOverflow for more info.

Ideally we would spin up as many threads as there are processors on the CPU, and execute a series of jobs in parallel (or sequentially for jobs that have dependencies), and have them all process and modify the same data. For example, we could thread updating sprites, checking if a sprite is on-screen, and if it isn't it would remove that sprite from the "renderable" list, or jobs to process animation frames, paths, or even tweens.

The most important thing is that everything is statically typed and very fast to process.

impaler commented 11 years ago

@crazysam Good point on the threading benefit of this the null issue is almost impossible to overcome otherwise. This almost makes the rendering system look like a composition pattern. Just to note hxs has not been updated in a while and was not intended for this use case. Obviously this is really a Haxeflixel 4.x task huge work indeed, for the better.

sergey-miryanov commented 11 years ago

hxs is bit outdated as I see. Polygonal approach seem very efficient from performance point of view, but it a bit difficult to use (I use polygonal events and msignal and msignal have very very simple interface). About multi-threading - update and render shouldn't overlaps in time, 'case it requires huge synchronization between them. I think in different threads should run different update task - physics updates, game state update. Also for large number of objects we can divide list of objects and update each part in different threads.

Also updating "renderable" list on each frame have memory cost, 'cause every list update will require memory allocation/deallocation.

gamedevsam commented 11 years ago

Also updating "renderable" list on each frame have memory cost, 'cause every list update will require memory allocation/deallocation.

That is only because we use arrays to store our game entities in our FlxGroups, it makes a system like this impossible. We would need to use a double linked list of singal handlers (FlxList would be a good place to start, but we need a new system, perhaps FlxEvents, or FlxSignals, these could be implemented as FlxPlugins). Double linked lists allow very fast addition / removal of objects). But it's not worth it unless we optimize updating / rendering in a smart way, by having one list of active objects and one list of visible objects.

For example, we could override set function for active and visible properties of objects: PSEUDO-CODE

// in FlxSprite.set_visible() call:
if(visible)
    FlxEvents.attach(draw, FlxEvents.draw);
else
    FlxEvents.detach(draw, FlxEvents.draw);

// in FlxBasic.set_active() call:
if(active)
    FlxEvents.attach(update, FlxEvents.update);
else
    FlxEvents.detach(update, FlxEvents.update);

// in set_exists() call:
if(exists)
{
    if(active)
        FlxEvents.attach(update, FlxEvents.update);
    if(visible)
        FlxEvents.attach(draw, FlxEvents.draw);
}
else
{
    if(active)
        FlxEvents.detach(update, FlxEvents.update);
    if(visible)
        FlxEvents.detach(draw, FlxEvents.draw);
}

// in FlxState init:
#if cpp
FlxJobs.initializeThreads(4); // initialize 4 threads to handle jobs requested
#end

// in FlxState update:
FlxJobs.dispatchEvent(FlxEvents.update); // dispatches the following event (on the first idle thread if multithreaded)

// in FlxState draw:
FlxJobs.dispatchEvent(FlxEvents.draw); // dispatches the following event (on the first idle thread if multithreaded)

We could refactor FlxGroups to use this Event system for entity management. This would complicate sorting somewhat, but the best approach to sorting linked lists is to sort elements into an array and reconstruct list from sorted elements. Sorting would only need to happen for objects that are existing, visible and on-camera, this would be a great optimization.

We can then easily thread individual function calls that can continuously get executed on every object (updating FlxBasic objects, doing occlusion culling check FlxSprite.onScreen, updating animation, updating FlxPlugins, processing input, and of course, drawing FlxState, even debugDrawing could be on a separate thread). All in separate threads, all operating on the same data (can't be on arrays).

The hard part is keeping every function call statically typed, but we're a smart bunch, we can figure it out.

sergey-miryanov commented 11 years ago

No, working on FlxLists - adding or removing elements requires memory allocation/deallocation for each list node. It will drive to more fragmented memory.

We can't easy have list of visible and active objects, 'cause visible object can stay invisible and should be removed from list.

If you run all tasks that you describe you should use huge amount of synchronization.

Also take a look on OpenMP - and you never will say that parallelization can't be on arrays ;)

impaler commented 11 years ago

@sergey-miryanov this post by polygonal shows where linked lists have strength, polygonal was writing this from back in as3 in 07 :) particular performance gain from remove, add from list vs array slice, hopefully this would translate to hxcpp with similar performance http://lab.polygonal.de/?p=206

gamedevsam commented 11 years ago

I understand what I'm proposing is a lot of work, but it's mostly under the hood optimizations that don't affect the user API, so we can commit to getting it done if it does indeed result in faster performance and more features for our users.

sergey-miryanov commented 11 years ago

Yes, I understand.

I just want to say that parallelization is very fragile ice ;)

impaler commented 11 years ago

@crazysam thanks for the proposal I think its worth the work. @sergey-miryanov yes crazy, slippery, sometimes sharp ice! ;)

volvis commented 11 years ago

Other people may be more knowleadge on the matter than I, but I had a pretty off beat idea on how plugins could behave. I started wondering if plugins could utilize macros to inject expressions where they want hooks. Essentially add lines of code to specified parts of HaxeFlixel codebase during compile time.

I managed to half get it to work, so it's still strictly hypothetical. I hit the wall trying to figure out a way to get a list of used plugin classes, but code injection seemed to work.

Downsides would be that macros add to the compile time (how much I don't know) and plugins get essentially unrestricted access to private variables since the code acts like it had always been there. On the positive side, it essentially eliminates the need for any kind of message passing and plugin looping system, since everything is injected to where they need to be.

But yeah, enough of this heresy. Messaging system sounds fine. I think the important bit is to have plugins relatively encapsulated, so that everything they need to work with is provided along with the message. Something like postRender submits the bitmapdata that had just been drawn, or postStateChange sends the current state. Should make the plugins pretty flexible to outside changes.

sergey-miryanov commented 11 years ago

Also discussion about signals, jfyi.

https://groups.google.com/d/msg/haxelang/1WpTConQvec/9e4e-TJCjIsJ

gamedevsam commented 11 years ago

I think the new plugin system is great.