A minimal 2d/3d game framework for zig.
Documentation: https://jack-ji.github.io/jok
TIPS: To eliminate console terminal on Windows platform, override exe.subsystem with .Windows in your build script.
Add jok as your project's dependency
Add jok dependency to your build.zig.zon, with following command:
zig fetch --save git+https://github.com/jack-ji/jok.git
For a tagged version:
zig fetch --save git+https://github.com/jack-ji/jok.git#0.16.0
Use jok's build script to add build step
In your build.zig, add:
const std = @import("std");
const jok = @import("jok");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{},
);
const install_cmd = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_cmd.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(&install_cmd.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&run_cmd.step);
}
If you want to add emscripten support for your project, the build script needs more care:
const std = @import("std");
const jok = @import("jok");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
if (!target.result.cpu.arch.isWasm()) {
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{},
);
const install_cmd = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_cmd.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(&install_cmd.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&run_cmd.step);
} else {
const webapp = jok.createWebApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{
.preload_path = "optional/relative/path/to/your/assets",
.shell_file_path = "optional/relative/path/to/your/shell",
},
);
b.getInstallStep().dependOn(&webapp.emlink.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&webapp.emrun.step);
}
}
(Optional) Using pre-built SDL3 libraries
By default, jok builds SDL3 from source. However, you can use pre-built SDL3 libraries, by passing build options:
zig build -Dsdl-lib-path=/path/to/sdl3/lib -Dsdl-include-path=/path/to/sdl3/include
Where:
sdl-lib-path: Directory containing the SDL3 library (e.g., libSDL3.so, libSDL3.a, SDL3.dll, or libSDL3.dylib)sdl-include-path: Directory containing SDL3 headers (should have SDL3/SDL.h inside)Using in your build.zig:
You can also pass SDL3 paths programmatically in your build script for desktop applications:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Get SDL paths from build options
const sdl_lib_path = b.option([]const u8, "sdl-lib-path", "Path to SDL3 lib");
const sdl_include_path = b.option([]const u8, "sdl-include-path", "Path to SDL3 include");
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{
.sdl_lib_path = sdl_lib_path,
.sdl_include_path = sdl_include_path,
},
);
// ... rest of build script
}
Note: Web builds (WASM) always use Emscripten's SDL3 port and do not support custom SDL3 paths.
Write some code!
You may import and use jok now, here's skeleton of your src/main.zig:
const std = @import("std");
const jok = @import("jok");
pub fn init(ctx: jok.Context) !void {
// your init code
}
pub fn event(ctx: jok.Context, e: jok.Event) !void {
// your event processing code
}
pub fn update(ctx: jok.Context) !void {
// your game state updating code
}
pub fn draw(ctx: jok.Context) !void {
// your drawing code
}
pub fn quit(ctx: jok.Context) void {
// your deinit code
}
Noticed yet? That's right, you don't need to write main function, jok got your back.
The game is deemed as a separate package to jok's runtime as a matter of fact. Your
only responsibility is to provide 5 public functions:
You can customize some setup settings (window width/height, fps, debug level etc), by
defining some public constants using predefined names (they're all prefixed with jok_).
Checkout src/config.zig.
Most of which are still modifiable at runtime.
Now, compile and run your game using command zig build run, have fun!
Please let me know if you have any issue or developed something interesting with this little framework.
Jok is short for joke, which is about how overly-complicated modern graphics programming has become. People are gradually forgetting lots of computing techniques used to deliver amazing games on simple machines. With so many tools, engines and computing resources at hand, however, gamedev is not as fun as it used to be. Jok is an effort trying to bring the joy back, it's being developed in the spirit of retro-machines of 1990s (especially PS1), which implies following limitations:
The limitations demand developers to be both creative and careful about game's design.
370 followers · starred Sep 2025
949 followers · starred Mar 2024
38 followers · starred Apr 2025
473 followers · starred Feb 2025
A minimal 2d/3d game framework for zig.
Documentation: https://jack-ji.github.io/jok
TIPS: To eliminate console terminal on Windows platform, override exe.subsystem with .Windows in your build script.
Add jok as your project's dependency
Add jok dependency to your build.zig.zon, with following command:
zig fetch --save git+https://github.com/jack-ji/jok.git
For a tagged version:
zig fetch --save git+https://github.com/jack-ji/jok.git#0.16.0
Use jok's build script to add build step
In your build.zig, add:
const std = @import("std");
const jok = @import("jok");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{},
);
const install_cmd = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_cmd.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(&install_cmd.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&run_cmd.step);
}
If you want to add emscripten support for your project, the build script needs more care:
const std = @import("std");
const jok = @import("jok");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
if (!target.result.cpu.arch.isWasm()) {
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{},
);
const install_cmd = b.addInstallArtifact(exe, .{});
b.getInstallStep().dependOn(&install_cmd.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(&install_cmd.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&run_cmd.step);
} else {
const webapp = jok.createWebApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{
.preload_path = "optional/relative/path/to/your/assets",
.shell_file_path = "optional/relative/path/to/your/shell",
},
);
b.getInstallStep().dependOn(&webapp.emlink.step);
const run_step = b.step("run", "Run game");
run_step.dependOn(&webapp.emrun.step);
}
}
(Optional) Using pre-built SDL3 libraries
By default, jok builds SDL3 from source. However, you can use pre-built SDL3 libraries, by passing build options:
zig build -Dsdl-lib-path=/path/to/sdl3/lib -Dsdl-include-path=/path/to/sdl3/include
Where:
sdl-lib-path: Directory containing the SDL3 library (e.g., libSDL3.so, libSDL3.a, SDL3.dll, or libSDL3.dylib)sdl-include-path: Directory containing SDL3 headers (should have SDL3/SDL.h inside)Using in your build.zig:
You can also pass SDL3 paths programmatically in your build script for desktop applications:
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Get SDL paths from build options
const sdl_lib_path = b.option([]const u8, "sdl-lib-path", "Path to SDL3 lib");
const sdl_include_path = b.option([]const u8, "sdl-include-path", "Path to SDL3 include");
const exe = jok.createDesktopApp(
b,
"mygame",
"src/main.zig",
target,
optimize,
.{
.sdl_lib_path = sdl_lib_path,
.sdl_include_path = sdl_include_path,
},
);
// ... rest of build script
}
Note: Web builds (WASM) always use Emscripten's SDL3 port and do not support custom SDL3 paths.
Write some code!
You may import and use jok now, here's skeleton of your src/main.zig:
const std = @import("std");
const jok = @import("jok");
pub fn init(ctx: jok.Context) !void {
// your init code
}
pub fn event(ctx: jok.Context, e: jok.Event) !void {
// your event processing code
}
pub fn update(ctx: jok.Context) !void {
// your game state updating code
}
pub fn draw(ctx: jok.Context) !void {
// your drawing code
}
pub fn quit(ctx: jok.Context) void {
// your deinit code
}
Noticed yet? That's right, you don't need to write main function, jok got your back.
The game is deemed as a separate package to jok's runtime as a matter of fact. Your
only responsibility is to provide 5 public functions:
You can customize some setup settings (window width/height, fps, debug level etc), by
defining some public constants using predefined names (they're all prefixed with jok_).
Checkout src/config.zig.
Most of which are still modifiable at runtime.
Now, compile and run your game using command zig build run, have fun!
Please let me know if you have any issue or developed something interesting with this little framework.
Jok is short for joke, which is about how overly-complicated modern graphics programming has become. People are gradually forgetting lots of computing techniques used to deliver amazing games on simple machines. With so many tools, engines and computing resources at hand, however, gamedev is not as fun as it used to be. Jok is an effort trying to bring the joy back, it's being developed in the spirit of retro-machines of 1990s (especially PS1), which implies following limitations:
The limitations demand developers to be both creative and careful about game's design.
370 followers · starred Sep 2025
949 followers · starred Mar 2024
38 followers · starred Apr 2025
473 followers · starred Feb 2025