Impact of Scripting Strategies on Game Performance

Summary

Show code and images as:

Introduction

Many developers will tell you to convert your whole game to C++, because it is more powerful and faster than LUA. Of course that's true, but in most cases, LUA is powerful enough, it just depends on how smart your code is. Script performance is closely related to your scripting strategies.

This guide will show you the strategies you can use to manage a big number of items in a list, from the worst to the best one, in order to have a loading time and update time which is not dependent to the items count. Here the list behaviour is manageed by JPPicker but it also works if you choose to manage the list by yourself. JPPicker provides you a function called helpMeUpdateMyItems which will greatly help you update your items in an optimized way, we'll talk about it starting from part 2.

The need to manage large lists in games is rather common, for instance in my Portfolio example "MySpher". JPPicker lets you work with big lists at minimal performance costs, as proven in Babel Rising Cataclysm where it was used to manage the list of scrolls and other lists in the game.

To illustrate what is described in this tutorial, I have created the sample named JPOptimizationSample, which uses each of the following strategies. In this sample, the picker contains 5000 items. You may think you'll never have such a big picker in your game, but do not forget the sample is executed by your computer and that the performances is a problem on mobile devices with far fewer items. The count of 5000 items is only for demonstration purposes, and is scaled match computers performances.

1. Basic implementation, no strategy

The common way to create and update the items is to create them all on startup and update their position each time they have to move. onInit will looks like that:

for i = 0, nItemCount - 1 do local hObject = scene.createRuntimeObject ( application.getCurrentUserScene ( ), "Box" ) scene.setObjectTag ( object.getScene ( hObject ), hObject, "object"..i ) end

onPickerPositionDidChange will be like that:

local nItemCount = application.getCurrentUserAIVariable ( "JPOptimizationSample", "nItemCount" ) for i = 0, nItemCount - 1 do local hObject = application.getCurrentUserSceneTaggedObject ( "object"..i ) object.setTranslation ( hObject, nPosition + i, 0, 0, object.kGlobalSpace ) end

It works well… unless you have a big number of items, because it is all dependent on the number of items you have to manage, and the more items you have, the more loop iterations you get.
As a result, your load time takes xx ms and the update time is xx ms, the first one resulting in a freeze on startup and the latter one resulting in poor performances when the items are moving.

2. Simple strategy

Okay, freeze on startup, poor framerate on update… the most important thing is to get a good framerate when moving the items. So the onInit part will not change, but I have to change what I do in my onPickerPositionDidChange handler. That's where the great JPPicker.helpMeUpdateMyItems function comes in. It will tell you which items really need to be updated, which ones don't need to be visible anymore, and which ones become visible.

local tToUpdate, tToDisable, tToEnable = JPPicker.helpMeUpdateMyItems ( sPickerID, nItemRange )

The first parameter is the ID of your picker, while the second parameter tells JPPicker how many items need to be displayed on each side of the center item of the picker.
This way, you can write a code like the following.

Hide the items that don't need to be updated anymore:

for i = 0, table.getSize ( tToDisable ) - 1 do local nIndex = table.getAt ( tToDisable, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) object.setVisible ( hObject, false ) end

Show the items that have become active (those which were hidden on the previous update):

for i = 0, table.getSize ( tToEnable ) - 1 do local nIndex = table.getAt ( tToEnable, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) object.setVisible ( hObject, true ) end

And update those which are visible:

for i = 0, table.getSize ( tToUpdate ) - 1 do local nIndex = table.getAt ( tToUpdate, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) object.setTranslation ( hObject, nPosition + nIndex, 0, 0, object.kGlobalSpace ) end

As a result, the update will cost almost no time at all, plus it is not dependent on the number of items you have.

3. Advanced strategy

Now that you have an update code which works well, can you reduce the amount of time that it takes to create all items? That's simple, JPPicker.helpMeUpdateMyItems tells you which items have become active and which ones are not active anymore, so why not just create or destroy items on the fly? By doing that, the onInit will be empty and thus cost nothing, while at the same time, the update time does not cost much performance with our new code from above.

Delete items that are not visible anymore:

for i = 0, table.getSize ( tToDisable ) - 1 do local nIndex = table.getAt ( tToDisable, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) scene.destroyRuntimeObject ( object.getScene ( hObject ), hObject ) end

Create items which have become visible:

for i = 0, table.getSize ( tToEnable ) - 1 do local nIndex = table.getAt ( tToEnable, i ) local hObject = scene.createRuntimeObject ( application.getCurrentUserScene ( ), "Box" ) scene.setObjectTag ( object.getScene ( hObject ), hObject, "object"..nIndex ) end

Update all of the visible items:

for i = 0, table.getSize ( tToUpdate ) - 1 do local nIndex = table.getAt ( tToUpdate, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) object.setTranslation ( hObject, nPosition + nIndex, 0, 0, object.kGlobalSpace ) end

This strategy works great, no loading time and a powerful update function.However there's another, even better strategy.

4. Best possible strategy

As you can see in part 3, items are created and destroyed on the fly. It works great on a powerful device, but it can cause some small freezes on devices that have a slow memory, because you create new items in memory and free memory all the time. The solution is to create a 'queue' of items. As soon as an item is not used anymore, you place it into the queue, and when you need a new item, you get it from the queue and update its properties. This way, there is far less memory movement involved. Of course this only works if all of the items are of the same kind, but it's not a problem either if you have 3 kinds of items - you'll just have 3 queues, one for each kind.

This is the onInit setup for the queue, which is an invisible item:

this.hQueue ( scene.createRuntimeObject ( application.getCurrentUserScene ( ), "" ) ) object.setVisible ( this.hQueue ( ), false ) object.forceInactive ( this.hQueue ( ), true, true )

I create a function named "queueReusableObject ( hObject )", which groups the object to the queue and removes its properties (scene tag, AIModels, …), as well as another function "dequeueReusableObject ( )", which will get an item in the queue and unparent it (or create a new item if none is available in the queue).
In my onPickerPositionDidChange handler, I have to replace scene.createRuntimeObject and scene.destroyRuntimeObject by the queue and dequeue functions:

for i = 0, table.getSize ( tToDisable ) - 1 do local nIndex = table.getAt ( tToDisable, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) this.queueReusableObject ( hObject ) end for i = 0, table.getSize ( tToEnable ) - 1 do local nIndex = table.getAt ( tToEnable, i ) local hObject = this.dequeueReusableObject ( ) scene.setObjectTag ( object.getScene ( hObject ), hObject, "object"..nIndex ) end

The update code remains unchanged:

for i = 0, table.getSize ( tToUpdate ) - 1 do local nIndex = table.getAt ( tToUpdate, i ) local hObject = application.getCurrentUserSceneTaggedObject ( "object"..nIndex ) object.setTranslation ( hObject, nPosition + nIndex, 0, 0, object.kGlobalSpace ) end

If we consider that 17 items are updated each frame, you'll have up to 17 item allocations for the whole lifetime of your picker, where you had 0-17 allocations each frame by using the strategy from part 3. The strategy used in part 4 is the best possible one.

Conclusion

During my life as a programmer for mobile devices, I always tried to find new optimization strategies on various subjects, in order to get the best possible framerate. Strategy #4 is the best one: no loading time, neglectable update times and no freeze at all. I am a ShiVa game scripter, but I also create native iOS applications, and when I became an iOS programmer, I realized that strategy #4 is also the one Apple suggests to its developers in order to manage items in UITableViews.

That's the end of this tutorial. I wanted to share this with all of you and tell you that the code performance is often more related to the scripting strategies than the language itself.