Gravity Express

In Gravity Express, you battle gravity while transporting cargo through abandoned mines.

To learn more about the game and be notified when the game is released (january 2023), click the 'Follow' button on Gravity Express by Nino [Gravity Express]

Below you will find the devlog, where I will go into technical detail of various aspects of developing this game.

Original topic start below

A port of Crazy Gravity Portable I made some 12 years ago in my stereotypical bedroom.
The original game was made for the PlayStation Portable, which had a lively homebrew scene. Only C++ was available as a toolchain, but my game ran in a Lua interpreter. It achieved some 20fps.

With the original system having a resolution of 480x272, porting is a matter of resizing the viewport, replacing all the game engine calls and converting the sprites to 1-bit. I also found out that not all Lua functions are available, but found a way to probably repackage and load level files faster than before. Thanks @Nic :slight_smile:

I used Canvas Dither to convert the colored sprite to 1-bit using atkinson. Still thinking about how to translate the color-coded gate keys to this platform

Project is available at GitHub - ninovanhooff/Crazy-Gravity-Playdate: Battle gravity while transporting cargo through abandoned mines. and completely unoptimized right now (full screen redraw every frame).

I would love to see this being played on hardware, plz post a video :slight_smile:
cgpd.pdx.zip (1.1 MB)

ezgif-1-52d0cc879d

18 Likes

The game runs a bit slow on the device (around 14fps) but all works pretty nicely. :+1:

1 Like

Great, thanks. There is ample room for optimization, so no worries. Worst case I'll aim for a solid 20fps, since that is the original speed. Somewhat surprised to not see the framerate go up when there is no sound playing, as the docs warn that's heavy on cpu.

This is a lovely surprise as I really enjoy this type of game.

I think current rendering can be optimised, but given that you are scrolling the whole screen partial redraws will not be as effective as you might hope. One of those things that need to be tried to see.

The way I would proceed would be to render the screen to a sprite to see if the SDK sprite system gives any quick gains. It will get better, but by how much is the question.

The best way would probably be to use the SDK tile map and sprites, but that sounds like a rather large rewrite of what you have already. it plays OK at 14fps, but it would play amazing at 30fps+.

Next I would aim to reduce the flickering (pixels that change every frame) by using careful sprite work to make sure shading and dithering align to an odd (or even) pixel grid - rather than random like Atkinson - and scrolling in multiples of 2 to align the scrolling with the dither pattern. This will reduce the amount of pixels changing every frame. Visually, it will look better on device. Whether it results in performance improvement I am not sure as the device sends dirty rows to the screen rather than dirty pixels. But, again, it cannot hurt. Preventing dither flashing/flickering on moving objects by snapping to even pixels

Sampler says this is where it is spending time:

  • 33% draw
  • 17% RenderGame
  • 8% pgeDraw

1 Like

@matt

I saw in another thread that you dabbled with optimizing the game. I always appreciate pull requests on github :slight_smile:

Even though it's silly to optimize without owning the hardware, I still liked it as a weekend challenge. My approach:

Game initialisation (expensive):

  • create two buffer images that are 1 tile wider/higher than the screen
  • full draw the camera view to one buffer image

Single frame render:

  • clear the screen (solid color) (cheap)

IF the camera moved over a full tile:

  • copy the active buffer to the inactive buffer, shifted in x and/or y direction by one tile (not sure how expensive)
  • fill the empty space (pretty cheap. Usually just a single row/column of tiles)
  • swap the active and inactive buffers

draw the active buffer as a single draw call to the screen (cheap)

These changes seem to be massive in the profiler on Mac. Would love to see another profiler screenshot and fps count from real hardware @matt :slight_smile:


cgpd.pdx.zip (1.1 MB)

1 Like

Added debug menu options for

  • fps
  • level select
  • dither pattern

cgpd.pdx.zip (1.1 MB)

I'm getting 30fps (with occasional dips to 24) from that build, which looks fantastic. I'm assuming collisions were turned off on purpose?

Yes indeed!

At refresh rate 20 we get locked 20 all the time.

At refresh rate 30 we get 24 (full speed diagonal) to 30 (static, not moving)

A refresh rate 0 (unlimited) there's a much more variable framerate: 24 (full speed diagonal) to 42 (static, not moving)

Building the latest source myself with a new fps of 25 gives a solid 25fps 98% of the time.

The optimisations I made were only math/Lua quick tricks, but because this was so bound by drawing they were not noticeable.

device sampling

Thanks, that's great news. So the 20fps target is achieved :tada: and the the 30fps may be in reach. Note that the level 03 that gets loaded by default might be the heaviest, along with 10.

todo:

  • profile impact of audio: disable as test; and compress to adpcm audio

  • DONE optimize the OptimizeLevel() function further so render code skips over empty spaces in both directions (currently only one direction). Really this should be baked into the level files, but doing it on level load is practical for experimentation for now.

  • replace all pgeDraw calls by sprite:draw calls

  • perhaps limit the maximum player or camera velocity in such a way that the player will only move 1 tile (8px) per frame. This rate-limits the amount that needs to be drawn.

It really doesn't make sense for me to do this work, since I cannot profile on my own. So if anyone wants a challenge: have at it and create a pull request :slight_smile:

Is there a way to drill down in the profiler, like only keep the samples for the 10% slowest frames or even better: the frames where the target was not achieved?

1 Like

@kyle yes the collisions are disabled in debug mode. Compile from source to tweak the variables :slight_smile:

I don't think so, but it's a good feature request.

You could export the samples and do some analytics on them, but realtime view: currently no.

Another performance update (I hope).

  • Did some checking on whether bits are actually on screen before drawing
  • optimized the level format so that more empty areas can be ignored by render calls
  • Fixed out of memory by garbage collecting before loading a level
  • do not redraw the HUD when a lot of tiles were drawn in the current frame
  • disabled debug mode, so that more realistic scenarios are tested. (Flying full speed diagonal without crashing will be difficult)
  • pre-calculated some math (cos and sin)

Questions:

  • Let's say I have 20 8x8 pixel cannonballs to draw. Half of those are offscreen. Will it benefit performance to check their pixel coordinates before drawing, or can I draw them all and let the clipRect cull the offscreen draws?
  • Would love to know what the current fps stats are
  • Could you give me some gameplay feedback?
  • Can an out of memory situation be created by switching levels 10 times? Not on the 16MB simulator. (press menu button)
  • how long does loading a level take?

cgpd.pdx.zip (1.3 MB)

1 Like

First, there's a Lua crash when I run out of lives.

Some great optimisation here.

  • Runs at a steady 30fps. At unlimited setRefreshRate(0) it's at 34โ€“40fps! could be locked 35 easily. let's push for 40!
  • gameplay:
    • When crashing the scrolling just stops and the explosion animation plays. This feels at odds with such dynamic movement during the game. I'd expect the scrolling to continue and the craft to explode dramatically into pieces or particles or something?
    • feels unfair to be able to die when taking of by turning too soon.
  • No, I can't trigger out of memory by changing through all the levels.
  • Levels take ~1โ€“3 seconds to load depending on complexity/size. Did you consider saving their fully-loaded Lua state back to disk and loading that instead? I'll be doing that with my cars soon.

Thoughts

  • Do you know about using local aliases of Lua math functions for improved performance? See: https://www.lua.org/gems/sample.pdf
  • Please add a pdxinfo so at least the game name is shown in the launcher. Title card image would be even better!
  • What is "bob" dither type? I collect dither algorithms.

That makes me really happy @matt , thanks so much!

  • Runs at a steady 30fps. At unlimited setRefreshRate(0) it's at 34โ€“40fps! could be locked 35 easily. let's push for 40!

Don't tempt me! I spent a bit more time already, and if 40 is the max right now, a solid 40 would be very difficult. Also, the game physics is frame-based, not time based. So increasing fps would require re-tuning physics etc. Also, we humans are very accustomed to 30, 40 might feel weird. In addition, pushing the display past it's recommended fps might introduce ghosting or whatever.

I'd expect the scrolling to continue and the craft to explode dramatically into pieces or particles or something?

YES. I know the collision point, so could break up the sprite at that point and animate some parts to continue their momentum. This could be very cool.

  • feels unfair to be able to die when taking of by turning too soon.

Yeah, You'll get used to it, but I don't want to set a bad first impression. I'll revert the landing tolerance to the previous value

  • Levels take ~1โ€“3 seconds to load depending on complexity/size. Did you consider saving their fully-loaded Lua state back to disk and loading that instead? I'll be doing that with my cars soon.

Yikes, I already do this. See https://github.com/ninovanhooff/Crazy-Gravity-Playdate/blob/main/Source/levels/LEVEL01.lua

Which gets compiled into binary lua by pdc. Note that it's very inefficient: most numbers could have been 8-bit, but lua only has 32-bit numbers. level 3 takes about 7MB of ram. Im thinking about a feature request for a more efficient playdate-level-format which can be loaded with a progress update callback

I knew about it, will look into it further

  • Please add a pdxinfo so at least the game name is shown in the launcher. Title card image would be even better!

Weird, is this not picked up?

  • What is "bob" dither type? I collect dither algorithms.

Best of Both. Manual cut and paste of atkinson as base + the bits from others where atkinson looks bad

I think 30 is fine! Feels great. :slight_smile:

I run my game at 60 but it can go as high as 200 if you're not updating many rows.

Sounds good

It's not. Sorry for my assumption. I'll check if it's not a bug of some sort in the launcher.

According to the docs, pdxinfo is only read by the system, not displayed to the user in the launcher.
In the meantime, I did make a simple card though.

explosion_demo

Had a blast implementing a nice explosion effect, thanks for the inspiration @matt :slight_smile:
Hope the framerate is not too terrible, but maybe I could sell it as a slow-mo crash cam? :playdate_cry_laugh:

Also:

level loading should be quite a bit faster.
simple game card
converted some globals into locals for performance
changed some 2-bit images to 1-bit. Perhaps it'll improve performance

Up next
Game Over screen
Level Select screen

Get it here:

1 Like

Start Screen effect

Figured the plane looks like a cursor, and thought of this approach to a Start Screen, which gets the player familiar with the physics as well :slight_smile:

Button gets activated when it fills up completely

startScreen

2 Likes

This is giving me Sub-Terrania vibes and I love that. Also the thruster sounds like the jetpack in Club Penguin so you get bonus points for that.

Two thumbs up. Love it.

1 Like

Thanks @naltohq !. Do you maybe have suggestion for a new name for this game? Post them here: Name this game: futuristic rocket transport platformer