Very basic question here; how do i add image files like textures for SDL to my project?
A line from my gameproject main file:
SDL.IMG_Load("example_texture.png");
i can run this in the IDE (visual code) without problem, but if i build and go to the output folder, and run the exe, i get and error that the file cant be found.
I kind of understand this, since the path in the code above is relative to the /src/main.zig and that doesnt apply to the exe in the output folder.
So how do i tell the build system to include my image file?
follow up question; what do i do if the file is not in the gameproject folder?;
SDL.IMG_Load("../otherDir/example_texture.png");
i also want to point out that i really like zig, but i think its hard for a beginner coder to learn since there isnt a lot of newbie questions here on the forums.
i have looked in the new build system documentation, but couldnt figure out how to fix my problem.
Thanks in advance!
EDIT: i also looked at a few opensource zig game projects, and checked their build files but couldnt figure out how they load graphics and other files.
You probably want to add an install step which takes care of copying the data directory to the install prefix, have your executable’s install step depend on it, and the executable’s run step depend on the executable’s install step (this last one is there by default in the project skeleton set up by zig init-exe).
I won’t bother with all of SDL, but here’s an extremely simplified example with a plain text file:
## The barebones project structure
$ ls -R
.:
build.zig data/ src/
./data:
hello.txt
./src:
main.zig
## The data subtree (in this case with a single plain text file in it)
$ cat data/hello.txt
Hello, world!
## The demo source file which reads and prints back the contents of `data/hello.txt`
$ cat src/main.zig
const std = @import("std");
pub fn main() !void {
const f = try std.fs.cwd().openFile("data/hello.txt", .{});
defer f.close();
var data: [1024]u8 = undefined;
_ = try f.readAll(&data);
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(&data);
}
## The build file, where the magic happens
$ cat build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
b.installArtifact(exe);
// NOTE: this is the good part
const install = b.getInstallStep();
const install_data = b.addInstallDirectory(.{
.source_dir = .{ .path = "data" },
.install_dir = .{ .prefix = {} },
.install_subdir = "data",
});
install.dependOn(&install_data.step);
const run_cmd = b.addRunArtifact(exe);
// NOTE: this next line is there by default, I only updated it to use the `install` variable
run_cmd.step.dependOn(install);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
$ zig build run
Hello, world!
Do note though, this is a simplified example which works because I made Zig install the data/ subtree from the project root to the data/ subtree in the install prefix directory. When I launch the binary under zig build run, the file is relatively accessible via the same path under the current working directory.
In a real-life scenario I would feed the prefix path to the binary so that it can try reading the data files from there, instead of from std.fs.cwd().
$ cat build.zig
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// NOTE: this is new
const wf = b.addWriteFiles();
const config_source = wf.add(
"config.zig",
b.fmt(
\\pub const data_install_path = "{s}";
\\
, .{b.getInstallPath(.prefix, "data")}),
);
const exe = b.addExecutable(.{
.name = "demo",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// NOTE: next line is new
exe.addAnonymousModule("config", .{ .source_file = config_source });
b.installArtifact(exe);
const install = b.getInstallStep();
const install_data = b.addInstallDirectory(.{
.source_dir = .{ .path = "data" },
.install_dir = .{ .prefix = {} },
.install_subdir = "data",
});
install.dependOn(&install_data.step);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(install);
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);
}
$ cat src/main.zig
const std = @import("std");
// NOTE: importing the new module here
const config = @import("config");
pub fn main() !void {
// NOTE: opening the directory from here
// NOTE: std.fs.openDirAbsolute() could also be used
var data_dir = try std.fs.cwd().openDir(config.data_install_path, .{});
defer data_dir.close();
// NOTE: opening the file relative to data_dir this time
const f = try data_dir.openFile("hello.txt", .{});
defer f.close();
var data: [1024]u8 = undefined;
_ = try f.readAll(&data);
const stdout = std.io.getStdOut().writer();
try stdout.writeAll(&data);
}
Works just the same as before.
Here I’m constructing a config module during the build process which essentially materializes new Zig source file in the compiler cache and makes it available to the exe compile step under the config name (but doesn’t expose it publicly, you can use exe.addModule() instead of exe.addAnonymousModule() if you want it public).
Note that while a bit more flexible this is still essentially hardcoding the compile-time known value of the installation prefix, so that path can’t be changed afterwards. If you need your program to find the data path dynamically you can have it look for the data inside well-known directories (e.g. see the XDG specification, if you’re targeting Linux/BSD systems), or have it open a configuration file where the user can provide you with the path to the installed data on their system.
EDIT: ooh, I just noticed, the stdlib actually exposes std.fs.getAppDataDir() which helps with XDG and other system-specific variants. Neat!
If you don’t like having to deal with installing multiple files, another option to explore is using @embedFile to embed the image data directly into the final binary. This loses some flexibility (the user can’t easily replace the textures), but then all you need is the final binary file.
I haven’t tried it myself, but it looks like you can load an image from such static data in SDL using IMG_Load_RW and SDL_RWFromConstMem in this way, assuming the bindings you’re using support it:
const image_data = @embedFile("path/to/image.png"); // embeds the entire contents of the file into the binary
const image_rw = SDL.RWFromConstMem(image_data, image_data.len);
const image = SDL.IMG_Load_RW(image_rw, 1);
// NOTE: this is the good part
const install = b.getInstallStep();
const install_data = b.addInstallDirectory(.{
.source_dir = .{ .path = "data" },
.install_dir = .{ .prefix = {} },
.install_subdir = "data",
});
install.dependOn(&install_data.step);
If I understand this correctly, this copies everything in the “data” directory into the “data” directory of the output folder (so probably something like “zig-out/data”)? What if I only want certain files to get copied?