Ditching the import statement to improve boot times
As per the docs, the import
statement is a function available in the Playdate SDK that is not part of standard Lua. Conversely, the require
statement is in every Lua distribution except for Playdate. The import statements are not run by the playdate, but instead by the pdc
compiler. import "otherfile"
basically means: read otherfile.lua and paste everything you find there at the current position in the current file. This way, you as developer can keep your projects organized, while the playdate only has to run a single file to load your entire game.
I can't remember whether the rationale is explained somewhere, but there are some apparent advantages to import
: we would rather wait a second for the game to load and then have a smooth experience, rather than have stutters during gameplay when gunshot.wav
is loaded for the first time when you press the A button. That's the wrong kind of bullet time. File access is considered slow on playdate, so we save on the overhead of loading 50 tiny lua files when we just load the main.lua. Note: pdc
gives compiled lua files the extension pdz
. The playdate actually never sees any lua file, just main.pdz
.
This works well for a sample games like Level1-1, which is so small that the game can pull off a seamless transition between the launch card animation and the game's start screen. Level 1-1's main.pdz weighs in at 28 kB.
Gravity Express's total compiled code is 475% the size of Level 1-1's: or 133 KB. It's not just code that gets loaded though. When running the code, images, sounds and other assets will have to be fetched from disk.
Let's have a look at the screen recording using mirror:
Here is some timing debug logging from a playdate without Mirror running (Mirror puts extra strain on the cpu, so should be eliminated from the equation)
Before
0.8010885 hoi *
1.4860770 Start rendered
* "hoi" is "hi" in Dutch.
I wrote this line some 15 years ago. Remember that this started out as a homebrew game for the PlayStation Portable. I'm keeping it in there for nostalgic reasons
The "hoi" line is a benchmark for the earliest moment after the game is started that code is run. It is at the top of my main.lua.
We see that loading the pdz
file takes 0.8 seconds, and then more loading is done until at time t=1.49 seconds, our first frame of the start screen is rendered.
Most players would find this acceptable, I think. Many Season games take a comparable time to load, albeit while showing a "loading" message on screen. What bothers me about it though, is that it breaks up the swish (launch animation) -> swoosh (start screen appears) transition. 1.4s is not long enough to get worried that the device has crashed, but it is long enough that you don't see the swish-swoosh as a single transition.
Let's also have a look at file size
main.pdz: 188 KB
other pdz's: 1.6 MB
total: 1.8 MB
That is an enormous amount of data, for compiled code. Remember that Level 1-1 only had a main.pdz of 28KB.
What is happening here is that the sdk's promise of importing lua files only once is not kept when your project structure contains subdirectories and you use import like this:
import "../credits/CreditsScreen.lua"
The lua
extension is never used in the samples, but without it, pdc can't find the file in my case. The result is that code gets combined and duplicated all over the place. (we might import startScreen in main.lua
and then again in the credits screen so that we can show the start screen again when exiting the credits. We now have 2 copies of the startScreen code in our pdx.)
I filed a bug for that here. I don't believe it has been addressed yet. Luckily, we can replace import
by something much better and more suitable for the project, using only 8 lines of code.
I now have two reasons to look into this issue:
- decreasing pdx file size
- improving startup speed
After
Allright, let's look at that new code. I cheeckily called the function require
because the name is still available and gets the proper syntax highlighting in a code editor:
-- must be placed in the root of the source directory, eg. at the top of main.lua
local run <const> = playdate.file.run
--- All paths that have already been required
local requiredPaths <const> = {}
--- @param sourcePath path relative to source directory
function require(sourcePath)
if requiredPaths[sourcePath] then
print("SKIP: Already required", sourcePath)
return
end
print("RUN " .. sourcePath)
requiredPaths[sourcePath] = true
return run(sourcePath)
end
Here is a sample usage, that only loads the settings screen when the player selects an option from the menu:
menu:addMenuItem("Settings",
function()
-- this code is only executed when The Settings menu item is selected
require "lua/settings/SettingsScreen"
-- pushScreen is part of my Scene management system
pushScreen(SettingsScreen())
end
)
Don't get me wrong, I still use inport
to combine multiple files into one. The Startscreen import
s StartView and StartViewModel, for example. From main, we require
Startscreen. Main has no reference to LevelSelectScreen though, and that is the reason why main, and thus our boot time, can remain small.
The StartViewModel contains code to require
LevelSelectScreen, but it is only executed when the user selects the level select option in the startscreen.
What we expect to find in our pdx file now are a main.pdx
and several *Screen.pdz
files. This is indeed the case. But the LevelSelectViewModel.pdz
and -View are there as well. That's strange.
I think the reason is that pdc will compile all Lua files that are not imported as pdz in the pdx. The unexpected files are indeed imported, but there is no import chain going back to main.
I manually deleted the unexpected files and indeed the game still runs. So, I created a bash script to find and delete redundant files from the pdx on every build. And finally arrived at the expected numbers above:
0.3018594 hoi
0.4702041 RUN lua/start/startScreen
0.5456690 Start rendered
main.pdz: 55 KB
other pdzs: 76 KB
total: 133 KB
Avery nice result that saves 1.5 MB in file size and almost one second of boot time!
I feel it is just fast enough now to make the swish-swoosh feel like a connected animation. Judge for yourself by watching the video above. Again, remeber that screen capturing imposes an extra slowdown on the device