One script or multiple? Impact on performance


#1

Does anyone know whats the recommended approach to programming with lua in defold with regards to the number of scripts?
So, for instance, is having one mega script with all the functionality of an object going to be better, then doing it the JS way and having all functionality be modular, and therefore even potentially reused in other objects?

Just to be clear I’m not talking about multiple GO necessarily, but rather about multiple scripts, passing messages around, vs one monolith script.
I’m sure it should have some overhead, but maybe it’s very minimal?

Thanks for any feedback.


#2

More scripts are fine it’s going to be fast as long as you don’t do anything cpu expensive or OS locking… but for gameplay stuff I usually have one script per GO, and then have those scripts import several Lua modules related to different gameplay systems.


#3

Thanks, maybe the “lua modules” alternative is what i was looking for. :wink:
The objective was mostly organization.


#4

#5

Other’s have already pointed out the benefit of using Lua modules, but I’d like to emphasize a couple of things:

  • There is a limit of 1024 scripts in a project
  • Putting logic in Lua modules instead of scripts will make it harder to work with hot-reload since modules can’t be hot reloaded. You can however hot-reload the script requiring the module and then unload the module in on_reload() and require it again, but that might have side effects if the module maintains state and is required from multiple scripts.
  • At all times keep the number of update() functions to a minimal. Each update function will run 60 times per second and there’s an overhead in going from engine->Lua.
    • Because of this it is preferred to hand over as much to the engine as possible. Animate using go.animate() if you can instead of in update()
    • In the case of multiple identical game objects that should be controlled the same way, for instance bullets or simple enemies, my recommendation is to avoid scripts on each go if possible and animate and handle logic in a manager script that keeps a list of ids to the individual game objects that need to be manipulated. This is not always possible to do, but when you can you’ll get better performance this way

#6

There are other very positive effects of using the module way. I wrote a simple lib to handle modules just like scripts (with all functions that scripts provide) but in addition you can have:
Shared self between modules
Private self within the specific module.
Full control of order - You can decide in which order update/input/on_message is fired between the modules.

I personally tend to design my games in composite-like patterns which means I do like the approach of having many small functional “scripts” on one object (eg in a shooter you could have health, steering, weapon, ai, visuals, movement patterns etc as separate scripts to combine for different units/enemies). This could be a good case for using modules instead of many scripts.


#7

You should share this!


#8

Yes, I hope to do that, problem right now is that it is integrated with many other libs like our own messaging proxy.
I will try to clean it up and make it standalone


#9

About the last point, so essentially what you are saying is that in Defold a singleton approach will always be more performant?

Perhaps then, the best way is to have a manager GO(with script), that uses modules if needed and talks to a bunch of other GOs, utilizing different logic from different modules as needed.


#10

Yes, if we’re talking about many game objects. And what constitutes “many” depends on the target platform and the purpose of the game objects. A very generic benchmark that clearly shows the difference in performance between the different solutions can be seen in this bunnymark test: https://github.com/britzl/defold-bunnymark


#11

Very interesting. Just to clarify the 1024 limit is effectively per project, not just active at run time or whatnot.(which would increase the possible effective limit)


#12

Correct. As with most other things in Defold things are allocated up front based on various buffer sizes (max sprites, max instances, max sounds etc). The same goes for max number of scripts, although this value isn’t configurable, and rightly so. In my opinion 1024 scripts or anything even remotely near that number indicates some serious design issues in a project.


#13

I just reached that 1024 scripts limit, doing a simple benchmark test I run on every game engine I try.
I got “ERROR:GAMEOBJECT: Could not create script component, out of resources.”.
Most games may not need so many game entities and sprites but I have two reasons to do this test, 1) is fun 2) I actually have a game sim that need more than 3,000 entities doing stuff.

Now, after reading this thread I understand that I need to put the logic outside of the Game Objects, but my question is: Why the 1024 limit? If I actually have a game project that need to spawns dynamically more than 3000 entities having their own behaviors and interacting each other. Is it a “design issue” if I want to use the engine how it is supposed to be used? Putting game object logic inside the game object? While other game engines simply don’t put a limit on entities and scripts I can spawn? Why not let the hardware set our limits?


#14

BTW, the engine is doing good, disabling the script does nice FPS for 5,000 game objects, and stable 60fps with 1024 game objects with the script enabled. The only thing blocking the test is the fixed 1024 engine limit.
Going to implement the movement of the G.O. in the main script to see what happens.


#15

1024 is a pretty arbitrary but high limit. It could have been 1000 or 1500 and maybe we could make it configurable from game .project, but I think the main thing here is to in some sense help you as a developer to design a performant game.

If all of your 1024 scripts have code running in update() and maybe also in on_message() then that’s a lot of crossing the C/engine to Lua/game logic border every frame and that will have a not so insignificant impact, at least on mobile devices.

Some alternative solutions:

  • For movement:
    • Use go.animate() if they move in a straight line
    • Use a controller script that keeps a list of all of the game objects and make per frame movement of all objects from that single script
  • For collision detection:
    • Use ray casting (although now that I think about it there might be a ray cast limit)
    • Detect collisions on the targets instead of the bullets in case of a game with many bullets (think bullet hell shooter)
    • Skip Box2D collisions and resolve in pure Lua instead
  • For per object state:
    • Use a controller script with a Lua table with a lookup between game object id and game object state

#16

I agree with the altrenative solutions, I just completed the test taking the Game Objects as just something that keeps the sprite and all the code to move them resides in a single main lua script, storing IDs and so on.

Still think 1024 is a very low limit that should be optional, like sprite limit, and instances limit configurable in game settings. Is high for mobiles but Defold is also targeting Desktop PCs.

I finally got:
30FPS for 4,500 moving sprites on Intel HD graphics 4000, with i5 4690k cpu.
16FPS for 2,000 moving sprites on Samsung Galaxy j5.

thanks for the quick answers! :+1:


#17

Would it be a box2d limit somewhere? Max raycast limit can be increased in the game.project file. There is a limit at number of IDs though at 256 so if you want more than that you have to have multiple manager scripts each managing 256 IDs.

I agree with being able to increase max scripts as an option. Although a good design would have less. In some situations you may still want that many in a simulation such as with a 3d project of some kind where every physics entity has its own script for convenience. In case of 3D though you would want to disable/unload areas of the world where the player is far away from.


#18

There might be additional optimisations to make. Take a look at my bunnymark sprite benchmark:

Key takeaway to get those additional milliseconds: Don’t create lots of short lived vector3’s.


#19

Nice bunnies, I think I’ll download it and replace with my sprite and counts…

My update is simple, shaking the sprites and I even declare the local vector outside the loop:



function update(self, dt)
	-- Add update code here	
	local newpos = vmath.vector3(0,0,0)
	for i,isa in ipairs(self.isas) do
		newpos.x = isa.posini.x + math.random(4)-2
		newpos.y = isa.posini.y + math.random(4)-2
		go.set_position(newpos, isa.id)
	end
end


my sprite:
isabella


#20

Your bunnies are doing well at 60FPS ( 16ms), an impressive 10,000 objects.

Replacing with my sprite (Isabella) then we have similar results (now slightly lower): 2500 Isabellas at 30FPS, with the single update code.

Both Isabellas and Bunnies give me good 60FPS with 1024 instances with update per object. How many more we can reach at 60FPS this way will keep a mistery due to the 1024 limit imposed by the engine design :wink: