JamUtil is a very simple little framework meant to be used on top of Vulkan2D with the purpose being use in game jams. These tools are not the best in class, they are just easy to use on top of VK2D since game jams have very limited time.
- Asset loader to easily load and free many resources at once
- Simple bitmap fonts, monospaced and automatically generated
- Sound support (Windows, Mac, and possibly Linux only)
- Easily save and load many different types of data
- Save/load buffers to/from files easily
- Simplified keyboard controls
- Basic collisions
- Simple-to-use job system
- Entity component system (ECS)
- Basic clock
This is meant to be used the same way Vulkan2D is, in that you just add it as a submodule
and include the few files in your CMake manually. On Windows you may need to link dsound
as well depending on your build environment.
Make sure to call juInit
after SDL/VK2D and juQuit
before you clean them up. Also call
juUpdate
before the SDL events loop each frame or certain systems won't work. There is also
juDelta
which will simply return the amount of time in seconds the last frame took to process.
The asset loader is used to load many different assets with very little code. The general
idea is you make a list of assets you want to load and pass it to the loader and it will
attempt to load them all based on the file extension. If the extension is not recognized,
it will be loaded as a JUBuffer
(see buffers below).
JULoadedAsset FILES[] = {{...}};
const int FILE_COUNT = 10;
JULoader loader = juLoaderCreate(FILES, FILE_COUNT);
...
juLoaderGetTexture(loader, "assets/myasset.png");
...
juLoaderFree(loader);
You grab assets from the loader by filename. Internally a hash map is used, so it is a quick process to grab assets, but it is still recommended that you store the assets locally if you plan on grabbing a bunch every frame.
Supported loader file extensions are
.png
,.jpg
,.jpeg
,.bmp
will create a VK2D texture.jufnt
will create a font (see fonts below).wav
will create a sound (see sounds below)- all else will be loaded as a buffer
The data you pass to the loader to load files is usually just the path, but it must be wrapped
in a JULoadedAsset
struct, which allows for the loading of sprite sheets and possibly other
things that would require parameters to be created with.
JULoadedAsset FILES[] = {
{"myspritesheet.png", 10, 10, 20, 20, 0.2, 5},
{"myaudioclip.wav"},
{"myfont.jufnt"},
{"somebinaryfile.bin"},
};
Of course check JamUtil.h
for the documentation on JULoadedAsset
.
You may also use GenHeader.py
to automatically generate a header that will already
have the necessary code written for a folder of assets. Essentially, you can fill a
folder with assets (for example, game/assets/
) then call GenHeader.py
with the
required arguments to generate a header file. For example,
python GenHeader.py "-dir=game/assets" -var=ASSETS -struct=GameAssets "-o=test.h"
Will generate a file called test.h
that will be structured as follows
// This code was automatically generated by GenHeader.py
#pragma once
#include "JamUtil.h"
JULoadedAsset ASSETS[] = {
...
};
typedef struct GameAssets {
...
} GameAssets;
#ifdef GAMEASSETS_IMPLEMENTATION
GameAssets *buildGameAssets() {
...
}
void destroyGameAssets(GameAssets *s) {
...
}
#endif
From there you may call buildGameAssets
to get all of your assets.
Sprites are pretty much what you would expect, you can load sprite sheets and draw
animations. They also store some metadata like origin and rotation that you can
change whenever. They are simple to load from sprite sheets, just specify the file
and give some parameters and JamUtil will figure out how to draw it (see main.c
for an example using a weird sprite sheet). You can also copy sprites so you can have
multiple sprites using the same texture/spritesheet and have them in different points
of their own animations.
There are two ways you can use fonts, you can either use .jufnt
files which automate loading
fonts, or manually load a bitmap font. You can create .jufnt
files by calling GenFont.py
with a path to a ttf font and the size, which will put the newly generated font in the same
folder as the ttf (with the same name but proper extension). You can then load these with
juFontLoad
(or just use the loader). If you want to use an image, you just supply the image's
filename and some other data, check the documentation in the header for more info.
juFontDraw(font, 50, 50, "The quick brown fox jumps over the lazy dog.");
It should be noted that .jufnt
s are strongly recommended because it is a tiresome process
to manually create bitmap fonts, and .jufnt
s allow for non-mono-spaced bitmap fonts. So
here is a basic process for it
# In your command prompt of choice
python GenFont.py myfont.ttf 15
# In your code (or just load with the loader)
JUFont font = juFontLoad("myfont.jufnt");
...
juFontFree(font);
Sounds are quite simple. You load them and play them. You can also update their volume as they play. Nothing too fancy, but gets the job done for simple games. Just check the in-header documentation for sounds because it pretty much operates exactly how you would want it to.
These are a handy tool to easily save and load many different types of data. They are basically just hash tables that support multiple different types. They save as binary files so they are not easily edited by the user (but obviously this is open source and the specification for the save format is below and its not at all encrypted).
// This will create an empty save if the file doesn't exist
JUSave save = juSaveLoad("thing.jusav");
...
juSaveSetString(save, "some key", "some string");
const char *myString = juSaveGetString(save, "some key");
...
juSaveStore(save, "thing.jusav");
juSaveFree(save);
Supported types to save and load are
- Floats
- Doubles
- 64-bit signed ints
- 64-bit unsigned ints
- Strings
- Void pointers (it saves the size and data itself)
Buffers are the simplest piece of JamUtil, they mostly exist to help the loader. It can
- Copy files into buffers
- Save buffers to files
- Save raw
void*
pointers to file
Very simple, just exists to make some tasks simple.
The collision/math "subsystem" includes several functions that are all very simple to understand and use, so here they are
- Check for collisions between two rectangles
- Check for collisions between two circles
- Check for a point in a rectangle
- Check for a point in a circle
- Get the distance between two points
- Get the angle between two points
- Linear interpolation (lerp)
- Sin interpolation (same as linear interpolation but with a sin graph for a smooth start and stop)
- Rotate a point about an origin
Again, for specifics, just check the header. Everything is documented.
You may utilize a job system by specifying a number of channels above 0 when initializing JamUtil. With an active job system you may queue jobs and wait for channels to finish. JamUtil will create one worker thread per CPU core minus one to account for the main thread, and when you queue a job the next available worker thread will pull the job and complete it. Waiting for a channel waits until all jobs on that channel are complete, and for that reason it is not recommended to queue jobs from a job on the same channel.
Entity component system (ECS) is a way of organizing and processing entities in a game based around components and systems. It consists of three parts in this library:
- Component - A piece of data, effectively just a struct
- System - Function that is run over entities that have the specified component(s)
- Entity - Collection of components, exposed to the user as an integer ID (
JUEntityID
)
To set up the ECS in your project you need to give the ECS two things: a list of all your systems and
a list of the size of each component. The system does not care what the content of your component are
but it needs to know their size. Each system is comprised of two things: a list of required components
and a function pointer to a system function. That system function will be ran for each entity that has
all the required components, passing that entity's ID to the function. See main.c
for an example of it.
Each system in the ECS is ran as a separate job, meaning they are likely all going to be run on their own
threads. Because of this, there are two copies of every component in the ECS: the current frame and previous
frame's components. The previous frame is read-only and as such every system may read from it without
worrying about data races, but only one system may write to the current frame's components at a time. If
multiple systems require current-frame write access for a component you may use JUECSLock
s to synchronize
the order of which the systems may access them.
The following is a very simple example of running the ECS
while (running) {
...
vk2dRendererStartFrame(colour);
juECSRunSystems();
juECSCopyState();
vk2dRendererEndFrame();
}
Both the juECSRunSystems
and juECSCopyState
functions do their actual work in jobs and very little happens
in those functions. There is also some synchronization work going on there
juECSRunSystems
waits until the job(s) queued byjuECSCopyState
before queueing its own jobs, but this usually is a non-issue becausejuECSCopyState
will be running in another thread while VK2D is finishing processing the frame and starting the next framejuECSCopyState
waits untiljuECSRunSystems
is done running, so if you have any processing work to do outside of the ECS, betweenjuECSRunSystems
andjuECSCopyState
is a good place to do it.
And finally some more general synchronization notes for ECS:
- Adding entities to the ECS and searching through entities (
juECSAddEntity
andjuECSEntityIter*
functions) will wait untiljuECSCopyState
job(s) are finished and only one of those functions may be used at a time. So if multiple systems are all trying to access one of the aforementioned functions they will have to wait until said functions are no longer being used in another thread - Systems are guaranteed to be run on the same thread. Given a system
s
, that system will be entirely processed by one thread and the function associated withs
will never be running on multiple threads at the same time - VK2D is not thread safe and you must synchronize access to VK2D functions yourself - but because of the previous
point if you only have one system that calls VK2D you need only synchronize VK2D calls between that system and
the main thread (see
juECSWaitSystemFinished
)
It can be a bit complicated to understand and add both libraries (Vulkan2D and JamUtil) so here is a "working" (I made one that worked then put it here to explain) CMakeLists.txt so you can mostly just copy/paste this to start.
project(YOUR_GAME)
...
# At the top we find the two required packages: Vulkan and SDL2. You may need to link SDL2 manually on Windows or use FindSDL2.cmake
find_package(Vulkan)
find_package(SDL2 REQUIRED)
# For the sake of readability all of the needed files are put into variables, this should be a straight copy/paste
set(VMA_FILES Vulkan2D/VulkanMemoryAllocator/src/vk_mem_alloc.h Vulkan2D/VulkanMemoryAllocator/src/VmaUsage.cpp)
file(GLOB VK2D_FILES Vulkan2D/VK2D/*.c)
set(JAMUTIL_FILES JamUtil/JamUtil.c)
# Obviously include/link whatever else you need here, but this is the minimum required stuff
include_directories(Vulkan2D/ JamUtil/ ${SDL2_INCLUDE_DIR} ${Vulkan_INCLUDE_DIRS})
add_executable(${PROJECT_NAME} main.c ${JAMUTIL_FILES} ${VK2D_FILES} ${VMA_FILES})
# You also may or may not need to link "dsound" depending on your compiler/environment
target_link_libraries(${PROJECT_NAME} m ${SDL2_LIBRARIES} ${Vulkan_LIBRARIES})
And from there you can use VK2D and JamUtil to your heart's content. See main.c
if you want a
quick setup example (be sure to change the assets directory or remove them).
In order with no padding, this is the binary file format .jufnt
.
- 5 bytes at the top that just say
JUFNT
- 4 bytes representing the size in bytes of the png at the bottom
- 4 bytes for the number of characters in the font
- 4 bytes * number of characters denoting each character's size in pixels (2 bytes for width, 2 bytes for height)
- X bytes for the png stored at the end of the file
This is not at all important for the average user to know, but for my own sake and those curious, the save functionality uses the following format to save data
- 5 bytes at the top that read
JUSAV
- 4 bytes stating the number of "datas" stored in the file
- From here on out its just data, so for each piece of data
- 4 bytes for the size of the key string
- X bytes for the key string
- 4 byte representing the type of data it is
- If it is a fixed width piece of data (floats, ints), X bytes for that data
- If it is a variable length thing (strings, void bytestreams)
- 4 bytes for the size of the data
- X bytes for the data itself