download pixbet apk
Joyrider: Run-time Level Graphics Generation Kasper Mol · Follow Published in Poki · 11
min read · Nov 11, 2024 -- Listen Share
Gameplay from a Joyrider level
Hi all! Next to
my job as a Tech Lead at Poki, I like to spend some of my free time chasing my passion:
game development. I’ve been at Poki for a long time, and next to the awesome team, a
big reason for that is because I believe in web as a great destination for games.
So
far I’ve created two games with my one-man studio kokos.games: Supernova and the
freshly released Joyrider.
This post is a deep-dive into how levels (and especially the
graphics) are created for Joyrider, but before we get there, a little bit of
context:
One project leads to another
Supernova, my first project, released in early
2024
Supernova: small size, simple experience
Good conversion to play, or an as small
as possible loading drop-off, is one of the main pillars of creating a successful web
game. This is achieved by multiple things, the most important one being a small initial
file-size.
My goal when creating Supernova was centered around this idea. Basically, I
wanted to create the smallest possible game, while still providing a polished (though
simple) experience.
I would say that was a success, by using Three.js and relying on
mostly procedural graphics, the game ended up at around 200kb. And I’m quite happy with
how far I managed to push the geometric psychedelic style within this budget. The users
seem happy too, the game has a great rating of 4.5 out of 5.
Joyrider: more goals, more
better (😅)
For Joyrider, my goals were not as straightforward. The project actually
started out as an exploration of building my own ECS framework with RxJS. Originally I
intended to make a simple game again so I could focus on this framework, but somewhere
along the way I realised that I wanted to make a game that forced me to explore more
sides of the game development process such as 2D art, level design, sound design and
others.
My first prototype in the RxJS-based ECS framework
In the end I landed on the
idea of creating a bike trials game, very much inspired by a classic series: RedLynx
Trials, which I played and loved in high-school. My aim was for it to be a slightly
more casual-friendly experience while still keeping the trials vibe of controlling your
character’s weight alongside the bike’s acceleration.
The project’s scope ended up a
lot bigger than I initially intended, but since these games are personal projects, I
don’t have any deadlines to work with. As long as I enjoyed the process, I’d get there
eventually. The project took about two to three times longer to complete compared to
Supernova, and was much more challenging at times, but eventually I got it done. Don’t
be afraid to challenge yourself!
Early prototype of the game’s bike and player
During
the entire process of designing and developing the game, the conversion to play was
always in the back of my mind. This started with the choice of tech by creating my own
framework (as opposed to using an existing engine with more overhead), with PixiJS for
graphics, Planck.js (a Box2D implementation in JS) for physics and howler.js for
audio.
Run-time level graphics generation
It was important to me for Joyrider to have
some basic sense of world-building. I really wanted to have a Super Mario Bros 3 style
overworld through which the game levels and shops were accessed, and the game would
need to support various biomes. I wanted to avoid having to download too many sprites
for all these biomes and levels and keep the file-size as small as possible, but I
still wanted to have great control over the level geometry as this would be a physics
game where small changes in geometry could have a big impact.
The solution? Designing
levels with polygonal geometry, and drawing the graphics at run-time. I realised that
this could prove to be a challenge especially for mobile devices, but I had faith and
started the work.
Comparison of in-editor vs. in-game level in the final build
Step 1:
Designing the levels
Before I got to worry about generating the graphics, I first
needed level geometry. Initially I intended to have my own level editor, but quickly
decided to drop that idea due to the insane scope it would add to an already big
project. While I was researching techniques of building good physics simulations, I
landed on the amazing Youtube channel of iforce2d, who not only does some insane things
with Box2D, but has also built his own editor called R.U.B.E. (Really Useful Box2D
Editor).
The editor is super powerful, fairly priced, and the creator provides a ton of
tutorials on his Youtube channel. I ended up using only a fraction of the features the
tool provides, but it gave me a great foundation to build my levels. I started with a
trial and quickly decided to purchase and use the editor for the project, and I’m still
very happy with it.
A screenshot of Joyrider’s first level in R.U.B.E.
A Joyrider level
in R.U.B.E. consists of the following:
Static bodies which contain the main geometry
for the level Dynamic/kinematic bodies and joints for things like rope bridges or
moving platforms Static sensors that get translated to things like kill, finish and
camera volumes Image objects which get translated into simple or complex entities in
the game engine, for things like warning signs and checkpoints
Once a level is
complete, it is directly exported from the editor to a json file. This file contains
all the information necessary for a Box2D engine to run the simulation. On top of that,
the custom attributes I’ve specified in the editor allow the passing of more
information to the game engine about things like ground material or camera volume
configuration.
Custom attributes on a body
Step 2: Getting the level ready for the
engine
Read along with the raw export of the first level here. The file that gets
exported from R.U.B.E. has a lot of info, but it’s not fully ready yet.
Translating to
game coordinates and reducing decimals
First I need to translate the coordinates to
game world space. This is a simple piece of logic that just inverts every Y coordinate,
and multiplies all X and Y coordinates by our WORLD_SPACE constant (which is
100).
While I’m doing this, I also round up all coordinates as there’s really no need
for 10 decimal places of detail. This reduces the size of the level file, leading to
savings in download time, and has the added benefit of making the merging of polygons a
bit easier in the next step.
Re-merging polygons
A limitation of Box2D is that it does
not support concave polygons. A great feature of R.U.B.E. is that it handles this issue
elegantly, and upon exporting automatically splits up your concave polygons into
multiple convex polygons. However, this makes it hard to create nice-looking graphics
on top, as those require one continuous polygon.
A concave polygon, split by R.U.B.E.
into two convex ones as you can tell from the separator in the center.
To solve this, I
have to do a pass on the exported data to re-merge the polygons where possible within a
body. To achieve this, I implemented Turf: “a JavaScript library for spatial analysis”.
This library has loads of features, but I only used one: @turf/union.
My implementation
basically comes down to brute-forcing all the polygon pairs until I’ve tried
everything. It’s not the fastest, but since this is all part of a build step it doesn’t
need to be fast, it just needs to work. You can see the relevant code here.
During this
step I also hash the resulting polygons, and give all of them a unique identifier based
on this hash. This helps later on by not having to generate graphics multiple times for
identical polygons.
In the end there’s two sets of polygons per body. One for Box2D to
handle the physics, and one for the generation of sprites.
Result
You can view the
resulting level file after these changes here. The final minified level json weighs in
at 38KB before getting Brotli compressed on the Poki game CDN. I decided to ship the
levels as part of the main game bundle as they are very suitable for compression and
thus barely impact the game’s total file-size.
Step 3: Generating the world
Now that
all the level data is prepared and ready for the engine, it’s time to start worrying
about drawing the world.
Drawing the sprites
Drawing the sprites happens as soon as
somebody selects a level.
The moment that levels are generated
As a last thing before
drawing the sprites, I check the capabilities of the user’s device. Two things are
important:
Does the device support workers?
Hopefully yes! This allows doing the sprite
generation off the main thread, and helps performance greatly. But if not, I just fall
back to doing this on the main thread. Are we dealing with a mobile device, and more
specifically, an iOS device?
For mobile devices I make the general assumption that
there’s less power to work with. For these devices, I lower the resolution of the to-be
generated sprite to 50%. For iOS, I lower it even further to 25%, as those devices were
especially prone to crashing. Lower resolution means smaller sprites means a lower
memory footprint, means happy devices.
Now we’re all set!
The drawing logic itself is
quite straightforward. I first create an OffScreenCanvas or Canvas Element (depending
on whether we’re in a worker) at the size of the polygon. Then I use the Canvas API to
draw this polygon onto the 2D context. The sprites are nothing more than a nice filled
pattern surrounded by a couple of strokes.
The ground patterns used
One ground polygon
in-editor and after generation
Easy does it! However there is one more thing to deal
with, on most mobile GPU’s, the maximum size of an image is reasonably low. To be safe,
I assume we can have no
larger than 2048x2048. Although most of the level
polygons are below this limit, not all of them are.
To fix this, I split up the image
into chunks of 2048x2048. I extract these chunks by using context.getImageData. Then I
use createImageBitmap (polyfilled as not all browsers support it) to turn this
ImageData back into a format that can be used directly in PixiJS.
An example of what
these chunks look like (for illustrative purposes I’ve set the chunk size to
256x256)
All these chunks are sent back over to the main thread, alongside the
necessary data for stitching them back together.
The resulting
are saved to
cache, keyed on the hash that was generated during the build step. In some cases same
polygon is re-used for multiple bodies and it would be a waste to do this process again
when I can just re-use the same sprite. This also improves run-time performance as the
GPU can re-use the same image as well.
Loading everything into the game
Finally, the
game entities are generated. All the chunks are combined together into a single PixiJS
container so it looks like a whole again. Next to the polygons, there’s also some other
entities to generate (such as checkpoints).
The entities are split into two types,
static and dynamic. I do this so I can destroy and re-generate the dynamic entities
once a player crashes / restarts, while keeping the static ones alive during the entire
level duration.
And that’s it, we now have a level :)
{nl}blaze cassino download
apostas on line da sportsnetloteria da pascoa
criar roleta digitaljogo da bet365