implement asset loading from bundle
This commit is contained in:
parent
6b48af7ea9
commit
483daaf91e
@ -53,33 +53,60 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const engine_lib = b.createModule(.{
|
const engine_lib = b.createModule(.{
|
||||||
.root_source_file = b.path("src/lib/root.zig")
|
.root_source_file = b.path("src/game_lib/root.zig")
|
||||||
});
|
});
|
||||||
engine_lib.addOptions("build_options", build_options);
|
engine_lib.addOptions("build_options", build_options);
|
||||||
opts.root_module.addImport("engine", engine_lib);
|
|
||||||
|
|
||||||
const runtime_module = createRuntimeModule(b, .{
|
const dep_stb = b.dependency("stb", .{});
|
||||||
|
engine_lib.addImport("stb_image", dep_stb.module("stb_image"));
|
||||||
|
|
||||||
|
opts.root_module.addImport("engine_lib", engine_lib);
|
||||||
|
|
||||||
|
const engine_module = createEngineModule(b, .{
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.has_tracy = has_tracy,
|
.has_tracy = has_tracy,
|
||||||
.has_imgui = has_imgui
|
.has_imgui = has_imgui
|
||||||
});
|
});
|
||||||
runtime_module.root_module.addImport("lib", engine_lib);
|
engine_module.root_module.addImport("engine_lib", engine_lib);
|
||||||
if (statically_linked) {
|
if (statically_linked) {
|
||||||
runtime_module.root_module.addImport("game", opts.root_module);
|
engine_module.root_module.addImport("game", opts.root_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
const assset_bundler_tool = buildAssetBundler(
|
||||||
|
b, b.graph.host, .ReleaseSafe,
|
||||||
|
engine_module.root_module,
|
||||||
|
opts.root_module
|
||||||
|
);
|
||||||
|
|
||||||
|
const asset_bundler_step = b.addRunArtifact(assset_bundler_tool);
|
||||||
|
asset_bundler_step.addDirectoryArg(opts.asset_dir);
|
||||||
|
const assets_bundle_file = asset_bundler_step.addOutputFileArg("assets.bin");
|
||||||
|
|
||||||
|
const runtime_mod = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/runtime/main.zig"),
|
||||||
|
.target = opts.target,
|
||||||
|
.optimize = opts.optimize,
|
||||||
|
});
|
||||||
|
runtime_mod.addImport("engine", engine_module.root_module);
|
||||||
|
if (!load_assets_at_runtime) {
|
||||||
|
// TODO: Add compression on asset bundle
|
||||||
|
runtime_mod.addAnonymousImport("asset_bundle", .{
|
||||||
|
.root_source_file = assets_bundle_file
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
var run_cmd_step: *std.Build.Step = undefined;
|
var run_cmd_step: *std.Build.Step = undefined;
|
||||||
if (isWasm) {
|
if (isWasm) {
|
||||||
const build_step = buildWasm(b, .{
|
const build_step = buildWasm(b, .{
|
||||||
.name = "index",
|
.name = "index",
|
||||||
.root_module = runtime_module.root_module,
|
.root_module = runtime_mod,
|
||||||
.dep_sokol = runtime_module.dep_sokol,
|
.dep_sokol = engine_module.dep_sokol,
|
||||||
.outer_builder = outer_builder
|
.outer_builder = outer_builder
|
||||||
});
|
});
|
||||||
outer_builder.getInstallStep().dependOn(build_step);
|
outer_builder.getInstallStep().dependOn(build_step);
|
||||||
|
|
||||||
const dep_sokol = runtime_module.dep_sokol;
|
const dep_sokol = engine_module.dep_sokol;
|
||||||
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
||||||
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
|
const emrun_step = sokol.emRunStep(outer_builder, .{ .name = "index", .emsdk = dep_emsdk });
|
||||||
emrun_step.step.dependOn(build_step);
|
emrun_step.step.dependOn(build_step);
|
||||||
@ -87,10 +114,10 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
|
|
||||||
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
||||||
} else {
|
} else {
|
||||||
runtime_module.root_module.link_libc = true;
|
runtime_mod.link_libc = true;
|
||||||
const exe = buildNative(b, .{
|
const exe = buildNative(b, .{
|
||||||
.name = opts.name,
|
.name = opts.name,
|
||||||
.root_module = runtime_module.root_module,
|
.root_module = runtime_mod,
|
||||||
.win32_has_console = opts.win32_has_console,
|
.win32_has_console = opts.win32_has_console,
|
||||||
.win32_png_icon = opts.win32_png_icon
|
.win32_png_icon = opts.win32_png_icon
|
||||||
});
|
});
|
||||||
@ -123,9 +150,15 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void {
|
|||||||
|
|
||||||
const run_step = outer_builder.step("run", "Run game");
|
const run_step = outer_builder.step("run", "Run game");
|
||||||
run_step.dependOn(run_cmd_step);
|
run_step.dependOn(run_cmd_step);
|
||||||
|
|
||||||
|
{
|
||||||
|
const install_assets_bin = outer_builder.addInstallFileWithDir(assets_bundle_file, .bin, "assets.bin");
|
||||||
|
const assets_step = outer_builder.step("assets", "Build assets file");
|
||||||
|
assets_step.dependOn(&install_assets_bin.step);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const RuntimeModule = struct {
|
const EngineModule = struct {
|
||||||
root_module: *std.Build.Module,
|
root_module: *std.Build.Module,
|
||||||
dep_sokol: *std.Build.Dependency,
|
dep_sokol: *std.Build.Dependency,
|
||||||
|
|
||||||
@ -138,9 +171,9 @@ const RuntimeModule = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule {
|
fn createEngineModule(b: *std.Build, opts: EngineModule.Options) EngineModule {
|
||||||
const mod = b.createModule(.{
|
const mod = b.createModule(.{
|
||||||
.root_source_file = b.path("src/runtime/main.zig"),
|
.root_source_file = b.path("src/engine/root.zig"),
|
||||||
.target = opts.target,
|
.target = opts.target,
|
||||||
.optimize = opts.optimize,
|
.optimize = opts.optimize,
|
||||||
.link_libc = true
|
.link_libc = true
|
||||||
@ -199,7 +232,7 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule
|
|||||||
|
|
||||||
mod.addIncludePath(dep_sokol_c.path("util"));
|
mod.addIncludePath(dep_sokol_c.path("util"));
|
||||||
mod.addCSourceFile(.{
|
mod.addCSourceFile(.{
|
||||||
.file = b.path("src/runtime/fontstash/sokol_fontstash_impl.c"),
|
.file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"),
|
||||||
.flags = cflags.items
|
.flags = cflags.items
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -216,12 +249,36 @@ fn createRuntimeModule(b: *std.Build, opts: RuntimeModule.Options) RuntimeModule
|
|||||||
// // TODO: Define buid config for wasm
|
// // TODO: Define buid config for wasm
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return RuntimeModule{
|
return EngineModule{
|
||||||
.root_module = mod,
|
.root_module = mod,
|
||||||
.dep_sokol = dep_sokol
|
.dep_sokol = dep_sokol
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn buildAssetBundler(
|
||||||
|
b: *std.Build,
|
||||||
|
target: std.Build.ResolvedTarget,
|
||||||
|
optimize: std.builtin.OptimizeMode,
|
||||||
|
engine: *std.Build.Module,
|
||||||
|
game: *std.Build.Module
|
||||||
|
) *std.Build.Step.Compile {
|
||||||
|
const mod = b.createModule(.{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.root_source_file = b.path("tools/asset-bundler.zig"),
|
||||||
|
});
|
||||||
|
mod.addImport("engine", engine);
|
||||||
|
mod.addImport("game", game);
|
||||||
|
// mod.addImport("asset_bundle", b.createModule(.{
|
||||||
|
// .root_source_file = b.path("src/runtime/asset_bundle.zig")
|
||||||
|
// }));
|
||||||
|
|
||||||
|
return b.addExecutable(.{
|
||||||
|
.name = "asset-bundler",
|
||||||
|
.root_module = mod,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn buildPngToIconTool(
|
fn buildPngToIconTool(
|
||||||
b: *std.Build,
|
b: *std.Build,
|
||||||
target: std.Build.ResolvedTarget,
|
target: std.Build.ResolvedTarget,
|
||||||
@ -310,7 +367,7 @@ fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
|
|||||||
.use_webgl2 = true,
|
.use_webgl2 = true,
|
||||||
.use_emmalloc = true,
|
.use_emmalloc = true,
|
||||||
.use_filesystem = false,
|
.use_filesystem = false,
|
||||||
.shell_file_path = b.path("src/runtime/shell.html"),
|
.shell_file_path = b.path("src/engine/shell.html"),
|
||||||
}) catch unreachable;
|
}) catch unreachable;
|
||||||
|
|
||||||
return &link_step.step;
|
return &link_step.step;
|
||||||
|
|||||||
166
engine/src/engine/asset_bundle.zig
Normal file
166
engine/src/engine/asset_bundle.zig
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Lib = @import("engine_lib");
|
||||||
|
|
||||||
|
const AssetBundle = @This();
|
||||||
|
|
||||||
|
fonts: std.ArrayList(Font),
|
||||||
|
sounds: std.ArrayList(Sound),
|
||||||
|
textures: std.ArrayList(Texture),
|
||||||
|
data: std.ArrayList(u8),
|
||||||
|
|
||||||
|
const DataSegment = extern struct {
|
||||||
|
offset: u64,
|
||||||
|
len: u64,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Font = extern struct {
|
||||||
|
asset_id: Lib.Asset.Id,
|
||||||
|
data: DataSegment
|
||||||
|
};
|
||||||
|
|
||||||
|
const Sound = extern struct {
|
||||||
|
asset_id: Lib.Asset.Id,
|
||||||
|
data: DataSegment
|
||||||
|
};
|
||||||
|
|
||||||
|
const Texture = extern struct {
|
||||||
|
asset_id: Lib.Asset.Id,
|
||||||
|
info: Lib.Texture.Info,
|
||||||
|
data: DataSegment
|
||||||
|
};
|
||||||
|
|
||||||
|
const Header = extern struct {
|
||||||
|
magic: Magic,
|
||||||
|
fonts_len: u32,
|
||||||
|
sounds_len: u32,
|
||||||
|
textures_len: u32,
|
||||||
|
data_len: u32,
|
||||||
|
|
||||||
|
pub const Magic = enum(u32) {
|
||||||
|
v1 = 0x961C8C65,
|
||||||
|
_,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const endianness: std.builtin.Endian = .little;
|
||||||
|
|
||||||
|
pub fn init() AssetBundle {
|
||||||
|
return AssetBundle{
|
||||||
|
.fonts = .empty,
|
||||||
|
.sounds = .empty,
|
||||||
|
.textures = .empty,
|
||||||
|
.data = .empty,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *AssetBundle, gpa: std.mem.Allocator) void {
|
||||||
|
self.sounds.deinit(gpa);
|
||||||
|
self.textures.deinit(gpa);
|
||||||
|
self.fonts.deinit(gpa);
|
||||||
|
self.data.deinit(gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getData(self: AssetBundle, data: DataSegment) []const u8 {
|
||||||
|
return self.data.items[data.offset..][0..data.len];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insertData(
|
||||||
|
self: *AssetBundle,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
data: []const u8
|
||||||
|
) !DataSegment {
|
||||||
|
const data_offset = self.data.items.len;
|
||||||
|
try self.data.appendSlice(gpa, data);
|
||||||
|
|
||||||
|
return DataSegment{
|
||||||
|
.offset = data_offset,
|
||||||
|
.len = data.len
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appendSound(
|
||||||
|
self: *AssetBundle,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
id: Lib.Asset.Id,
|
||||||
|
data: []const u8
|
||||||
|
) !void {
|
||||||
|
try self.sounds.append(gpa, Sound{
|
||||||
|
.asset_id = id,
|
||||||
|
.data = try self.insertData(gpa, data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appendFont(
|
||||||
|
self: *AssetBundle,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
id: Lib.Asset.Id,
|
||||||
|
data: []const u8
|
||||||
|
) !void {
|
||||||
|
try self.fonts.append(gpa, Font{
|
||||||
|
.asset_id = id,
|
||||||
|
.data = try self.insertData(gpa, data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn appendTexture(
|
||||||
|
self: *AssetBundle,
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
id: Lib.Asset.Id,
|
||||||
|
data: []const u8,
|
||||||
|
info: Lib.Texture.Info
|
||||||
|
) !void {
|
||||||
|
try self.textures.append(gpa, Texture{
|
||||||
|
.asset_id = id,
|
||||||
|
.info = info,
|
||||||
|
.data = try self.insertData(gpa, data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write(self: AssetBundle, writer: *std.Io.Writer) !void {
|
||||||
|
try writer.writeStruct(Header{
|
||||||
|
.magic = .v1,
|
||||||
|
.fonts_len = @intCast(self.fonts.items.len),
|
||||||
|
.sounds_len = @intCast(self.sounds.items.len),
|
||||||
|
.textures_len = @intCast(self.textures.items.len),
|
||||||
|
.data_len = @intCast(self.data.items.len)
|
||||||
|
}, endianness);
|
||||||
|
|
||||||
|
try writer.writeSliceEndian(Font, self.fonts.items, endianness);
|
||||||
|
try writer.writeSliceEndian(Sound, self.sounds.items, endianness);
|
||||||
|
try writer.writeSliceEndian(Texture, self.textures.items, endianness);
|
||||||
|
try writer.writeAll(self.data.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(self: *AssetBundle, gpa: std.mem.Allocator, reader: *std.Io.Reader) !void {
|
||||||
|
const header = try reader.takeStruct(Header, endianness);
|
||||||
|
if (header.magic != .v1) {
|
||||||
|
return error.InvalidMagic;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.fonts.ensureTotalCapacity(gpa, header.fonts_len);
|
||||||
|
try self.sounds.ensureTotalCapacity(gpa, header.sounds_len);
|
||||||
|
try self.textures.ensureTotalCapacity(gpa, header.textures_len);
|
||||||
|
try self.data.ensureTotalCapacity(gpa, header.data_len);
|
||||||
|
|
||||||
|
self.fonts.clearRetainingCapacity();
|
||||||
|
self.sounds.clearRetainingCapacity();
|
||||||
|
self.textures.clearRetainingCapacity();
|
||||||
|
self.data.clearRetainingCapacity();
|
||||||
|
|
||||||
|
try reader.readSliceEndian(
|
||||||
|
Font,
|
||||||
|
self.fonts.addManyAsSliceAssumeCapacity(header.fonts_len),
|
||||||
|
endianness
|
||||||
|
);
|
||||||
|
try reader.readSliceEndian(
|
||||||
|
Sound,
|
||||||
|
self.sounds.addManyAsSliceAssumeCapacity(header.sounds_len),
|
||||||
|
endianness
|
||||||
|
);
|
||||||
|
try reader.readSliceEndian(
|
||||||
|
Texture,
|
||||||
|
self.textures.addManyAsSliceAssumeCapacity(header.textures_len),
|
||||||
|
endianness
|
||||||
|
);
|
||||||
|
try reader.readSliceAll(self.data.addManyAsSliceAssumeCapacity(header.data_len));
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ const assert = std.debug.assert;
|
|||||||
|
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
|
|
||||||
const Lib = @import("lib");
|
const Lib = @import("engine_lib");
|
||||||
const Math = Lib.Math;
|
const Math = Lib.Math;
|
||||||
const STBVorbis = @import("stb_vorbis");
|
const STBVorbis = @import("stb_vorbis");
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ const ThreadState = struct {
|
|||||||
temp_channel_buffer: []f32,
|
temp_channel_buffer: []f32,
|
||||||
|
|
||||||
pub const SoundInstance = struct {
|
pub const SoundInstance = struct {
|
||||||
sound_id: Lib.SoundId,
|
sound_id: Lib.Sound.Id,
|
||||||
volume: f32 = 0,
|
volume: f32 = 0,
|
||||||
cursor: u32 = 0,
|
cursor: u32 = 0,
|
||||||
};
|
};
|
||||||
@ -257,7 +257,7 @@ const SokolSetupOptions = struct {
|
|||||||
|
|
||||||
const SoundList = Lib.GenerationalArrayList(u8, u8, Sound);
|
const SoundList = Lib.GenerationalArrayList(u8, u8, Sound);
|
||||||
comptime {
|
comptime {
|
||||||
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.SoundId));
|
assert(@bitSizeOf(SoundList.Id) == @bitSizeOf(Lib.Sound.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
gpa: std.mem.Allocator,
|
||||||
@ -353,7 +353,7 @@ pub fn deinit(self: *AudioSystem) void {
|
|||||||
self.sounds.deinit(self.gpa);
|
self.sounds.deinit(self.gpa);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addSound(self: *AudioSystem) !Lib.SoundId {
|
pub fn addSound(self: *AudioSystem) !Lib.Sound.Id {
|
||||||
self.sounds_mutex.lock();
|
self.sounds_mutex.lock();
|
||||||
defer self.sounds_mutex.unlock();
|
defer self.sounds_mutex.unlock();
|
||||||
|
|
||||||
@ -363,7 +363,7 @@ pub fn addSound(self: *AudioSystem) !Lib.SoundId {
|
|||||||
return @enumFromInt(sound_id.asInt());
|
return @enumFromInt(sound_id.asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removeSound(self: *AudioSystem, id: Lib.SoundId) void {
|
pub fn removeSound(self: *AudioSystem, id: Lib.Sound.Id) void {
|
||||||
self.sounds_mutex.lock();
|
self.sounds_mutex.lock();
|
||||||
defer self.sounds_mutex.unlock();
|
defer self.sounds_mutex.unlock();
|
||||||
|
|
||||||
@ -388,7 +388,7 @@ pub const DataOptions = struct {
|
|||||||
playback_style: ?PlaybackStyle = null,
|
playback_style: ?PlaybackStyle = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !void {
|
pub fn setSoundData(self: *AudioSystem, id: Lib.Sound.Id, opts: DataOptions) !void {
|
||||||
const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
|
const sound = self.sounds.get(.fromInt(@intFromEnum(id))) orelse return error.SoundNotFound;
|
||||||
|
|
||||||
assert(opts.format == .vorbis);
|
assert(opts.format == .vorbis);
|
||||||
@ -441,10 +441,6 @@ pub fn setSoundData(self: *AudioSystem, id: Lib.SoundId, opts: DataOptions) !voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(self: *AudioSystem, id: Lib.SoundId) Sound {
|
|
||||||
return self.sounds.items[@intFromEnum(id)];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void {
|
fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channels: u32) void {
|
||||||
if (!self.running.load(.seq_cst)) {
|
if (!self.running.load(.seq_cst)) {
|
||||||
return;
|
return;
|
||||||
@ -482,7 +478,14 @@ fn sokolStream(self: *AudioSystem, buffer: [*c]f32, num_frames: u32, num_channel
|
|||||||
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
|
@memset(buffer[0..@intCast(num_frames * num_channels)], 0);
|
||||||
|
|
||||||
for (thread_state.instances.items) |*instance| {
|
for (thread_state.instances.items) |*instance| {
|
||||||
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse continue;
|
const sound = self.sounds.get(.fromInt(@intFromEnum(instance.sound_id))) orelse {
|
||||||
|
log.warn("Attempt to play that doesn't exist", .{});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if (sound.* == .nil) {
|
||||||
|
log.warn("Attempt to play .nil sound", .{});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
assert(sound.getSampleRate() == sample_rate); // TODO:
|
assert(sound.getSampleRate() == sample_rate); // TODO:
|
||||||
|
|
||||||
const sample_count = sound.streamChannels(.{
|
const sample_count = sound.streamChannels(.{
|
||||||
@ -6,7 +6,7 @@ const sapp = sokol.app;
|
|||||||
const simgui = sokol.imgui;
|
const simgui = sokol.imgui;
|
||||||
const sgl = sokol.gl;
|
const sgl = sokol.gl;
|
||||||
|
|
||||||
const Lib = @import("lib");
|
const Lib = @import("engine_lib");
|
||||||
const Math = Lib.Math;
|
const Math = Lib.Math;
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
const Vec4 = Math.Vec4;
|
const Vec4 = Math.Vec4;
|
||||||
@ -57,8 +57,13 @@ const Texture = struct {
|
|||||||
const Data = struct {
|
const Data = struct {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
rgba: [*]u8
|
rgba: [*]u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn destroy(self: *Texture) void {
|
||||||
|
sg.destroyView(self.view);
|
||||||
|
sg.destroyImage(self.image);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Font = struct {
|
const Font = struct {
|
||||||
@ -74,11 +79,19 @@ const Font = struct {
|
|||||||
|
|
||||||
const FontArray = Lib.GenerationalArrayList(u8, u8, Font);
|
const FontArray = Lib.GenerationalArrayList(u8, u8, Font);
|
||||||
comptime {
|
comptime {
|
||||||
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.FontId));
|
assert(@bitSizeOf(FontArray.Id) == @bitSizeOf(Lib.Font.Id));
|
||||||
|
assert(FontArray.Id.none.asInt() == @intFromEnum(Lib.Font.Id.nil));
|
||||||
|
}
|
||||||
|
|
||||||
|
const TexturesArray = Lib.GenerationalArrayList(u8, u8, Texture);
|
||||||
|
comptime {
|
||||||
|
// TODO: This is PITA, can this be refactored?
|
||||||
|
assert(@bitSizeOf(TexturesArray.Id) == @bitSizeOf(Lib.Texture.Id));
|
||||||
|
assert(TexturesArray.Id.none.asInt() == @intFromEnum(Lib.Texture.Id.nil));
|
||||||
}
|
}
|
||||||
|
|
||||||
gpa: std.mem.Allocator,
|
gpa: std.mem.Allocator,
|
||||||
textures: std.ArrayList(Texture) = .empty,
|
textures: TexturesArray = .empty,
|
||||||
fonts: FontArray = .empty,
|
fonts: FontArray = .empty,
|
||||||
|
|
||||||
scale_stack_buffer: [32]Vec2 = undefined,
|
scale_stack_buffer: [32]Vec2 = undefined,
|
||||||
@ -188,20 +201,24 @@ pub fn setupSokol(self: *Graphics) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
|
pub fn deinit(self: *Graphics, gpa: std.mem.Allocator) void {
|
||||||
if (self.sokol_state) |*sokol_state| {
|
|
||||||
imgui.shutdown();
|
|
||||||
sokol_state.font_context.deinit();
|
|
||||||
sgl.shutdown();
|
|
||||||
sg.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
var fonts_iter = self.fonts.iterator();
|
var fonts_iter = self.fonts.iterator();
|
||||||
while (fonts_iter.nextItem()) |font| {
|
while (fonts_iter.nextItem()) |font| {
|
||||||
font.deinit(self.gpa);
|
font.deinit(self.gpa);
|
||||||
}
|
}
|
||||||
self.fonts.deinit(gpa);
|
self.fonts.deinit(gpa);
|
||||||
|
|
||||||
|
var textures_iter = self.textures.iterator();
|
||||||
|
while (textures_iter.nextItem()) |texture| {
|
||||||
|
texture.destroy();
|
||||||
|
}
|
||||||
self.textures.deinit(gpa);
|
self.textures.deinit(gpa);
|
||||||
|
|
||||||
|
if (self.sokol_state) |*sokol_state| {
|
||||||
|
imgui.shutdown();
|
||||||
|
sokol_state.font_context.deinit();
|
||||||
|
sgl.shutdown();
|
||||||
|
sg.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawCommand(self: *Graphics, command: Command) void {
|
pub fn drawCommand(self: *Graphics, command: Command) void {
|
||||||
@ -293,17 +310,6 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refreshFonts(self: *Graphics) !void {
|
|
||||||
const sokol_state = self.sokol_state orelse return;
|
|
||||||
|
|
||||||
var fonts_iter = self.fonts.iterator();
|
|
||||||
while (fonts_iter.nextItem()) |font| {
|
|
||||||
if (font.fontstash_id == null and font.data != null) {
|
|
||||||
font.fontstash_id = try sokol_state.font_context.addFont("", font.data.?);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn beginFrame(self: *Graphics) !void {
|
pub fn beginFrame(self: *Graphics) !void {
|
||||||
assert(self.sokol_state != null);
|
assert(self.sokol_state != null);
|
||||||
const sokol_state = self.sokol_state.?;
|
const sokol_state = self.sokol_state.?;
|
||||||
@ -378,16 +384,24 @@ const Vertex = struct {
|
|||||||
uv: Vec2
|
uv: Vec2
|
||||||
};
|
};
|
||||||
|
|
||||||
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.TextureId) void {
|
fn drawQuad(self: *Graphics, quad: [4]Vertex, color: Vec4, texture_id: Lib.Texture.Id) void {
|
||||||
assert(self.sokol_state != null);
|
assert(self.sokol_state != null);
|
||||||
const sokol_state = self.sokol_state.?;
|
const sokol_state = self.sokol_state.?;
|
||||||
|
|
||||||
|
var view: ?sg.View = null;
|
||||||
|
if (self.textures.get(.fromInt(@intFromEnum(texture_id)))) |texture| {
|
||||||
|
view = texture.view;
|
||||||
|
} else {
|
||||||
|
log.warn("Attempt to use texture that doesn't exist, id={}", .{texture_id});
|
||||||
|
}
|
||||||
|
|
||||||
sgl.enableTexture();
|
sgl.enableTexture();
|
||||||
defer sgl.disableTexture();
|
defer sgl.disableTexture();
|
||||||
|
|
||||||
const view = self.textures.items[@intFromEnum(texture_id)].view;
|
|
||||||
// TODO: Make sampler configurable
|
// TODO: Make sampler configurable
|
||||||
sgl.texture(view, sokol_state.nearest_sampler);
|
if (view != null) {
|
||||||
|
sgl.texture(view.?, sokol_state.nearest_sampler);
|
||||||
|
}
|
||||||
|
|
||||||
sgl.beginQuads();
|
sgl.beginQuads();
|
||||||
defer sgl.end();
|
defer sgl.end();
|
||||||
@ -465,17 +479,9 @@ fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn makeView(image: sg.Image) !sg.View {
|
// ---------------------- Textures ----------------- //
|
||||||
const image_view = sg.makeView(.{
|
|
||||||
.texture = .{ .image = image }
|
|
||||||
});
|
|
||||||
if (image_view.id == sg.invalid_id) {
|
|
||||||
return error.InvalidView;
|
|
||||||
}
|
|
||||||
return image_view;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
fn initImageWithMipMaps(image: sg.Image, mipmaps: []const Texture.Data) !void {
|
||||||
if (mipmaps.len == 0) {
|
if (mipmaps.len == 0) {
|
||||||
return error.NoMipMaps;
|
return error.NoMipMaps;
|
||||||
}
|
}
|
||||||
@ -490,7 +496,7 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = sg.makeImage(.{
|
sg.initImage(image, .{
|
||||||
.width = @intCast(mipmaps[0].width),
|
.width = @intCast(mipmaps[0].width),
|
||||||
.height = @intCast(mipmaps[0].height),
|
.height = @intCast(mipmaps[0].height),
|
||||||
.pixel_format = .RGBA8,
|
.pixel_format = .RGBA8,
|
||||||
@ -500,35 +506,60 @@ fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
|||||||
.num_mipmaps = @intCast(mip_levels.items.len),
|
.num_mipmaps = @intCast(mip_levels.items.len),
|
||||||
.data = data
|
.data = data
|
||||||
});
|
});
|
||||||
if (image.id == sg.invalid_id) {
|
if (sg.queryImageState(image) != .VALID) {
|
||||||
return error.InvalidImage;
|
return error.InitImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn loadTexture(self: *Graphics, gpa: std.mem.Allocator, mipmaps: []const Texture.Data) !Lib.TextureId {
|
pub fn addTexture(self: *Graphics) !Lib.Texture.Id {
|
||||||
const image = try makeImageWithMipMaps(mipmaps);
|
const texture_id = try self.textures.insert(self.gpa, .{
|
||||||
errdefer sg.deallocImage(image);
|
.image = .{ .id = sg.invalid_id },
|
||||||
|
.view = .{ .id = sg.invalid_id }
|
||||||
const view = try makeView(image);
|
|
||||||
errdefer sg.deallocView(view);
|
|
||||||
|
|
||||||
assert(mipmaps.len > 0);
|
|
||||||
const index = self.textures.items.len;
|
|
||||||
try self.textures.append(gpa, .{
|
|
||||||
.image = image,
|
|
||||||
.view = view,
|
|
||||||
.info = .{
|
|
||||||
.width = mipmaps[0].width,
|
|
||||||
.height = mipmaps[0].height,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
return @enumFromInt(texture_id.asInt());
|
||||||
return @enumFromInt(index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn addFont(self: *Graphics) !Lib.FontId {
|
pub fn removeTexture(self: *Graphics, id: Lib.Texture.Id) void {
|
||||||
|
_ = self.textures.remove(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setTextureData(self: *Graphics, id: Lib.Texture.Id, data: Texture.Data) !void {
|
||||||
|
if (self.sokol_state == null) {
|
||||||
|
return error.SokolNotSetup;
|
||||||
|
}
|
||||||
|
|
||||||
|
const texture = self.textures.get(.fromInt(@intFromEnum(id))) orelse return;
|
||||||
|
|
||||||
|
if (texture.image.id == sg.invalid_id) {
|
||||||
|
texture.image = sg.allocImage();
|
||||||
|
if (texture.image.id == sg.invalid_id) {
|
||||||
|
return error.AllocImage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (texture.view.id == sg.invalid_id) {
|
||||||
|
texture.view = sg.allocView();
|
||||||
|
if (texture.view.id == sg.invalid_id) {
|
||||||
|
return error.AllocView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sg.uninitView(texture.view);
|
||||||
|
sg.uninitImage(texture.image);
|
||||||
|
|
||||||
|
try initImageWithMipMaps(texture.image, &.{ data });
|
||||||
|
|
||||||
|
sg.initView(texture.view, .{
|
||||||
|
.texture = .{ .image = texture.image }
|
||||||
|
});
|
||||||
|
if (sg.queryViewState(texture.view) != .VALID) {
|
||||||
|
return error.InitView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------- Fonts ----------------- //
|
||||||
|
|
||||||
|
pub fn addFont(self: *Graphics) !Lib.Font.Id {
|
||||||
const font_id = try self.fonts.insert(self.gpa, .{
|
const font_id = try self.fonts.insert(self.gpa, .{
|
||||||
.fontstash_id = null,
|
.fontstash_id = null,
|
||||||
.data = null
|
.data = null
|
||||||
@ -536,18 +567,23 @@ pub fn addFont(self: *Graphics) !Lib.FontId {
|
|||||||
return @enumFromInt(font_id.asInt());
|
return @enumFromInt(font_id.asInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn removeFont(self: *Graphics, id: Lib.FontId) void {
|
pub fn removeFont(self: *Graphics, id: Lib.Font.Id) void {
|
||||||
_ = self.fonts.remove(.fromInt(@intFromEnum(id)));
|
_ = self.fonts.remove(.fromInt(@intFromEnum(id)));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setFontData(self: *Graphics, id: Lib.FontId, data: []const u8) !void {
|
pub fn setFontData(self: *Graphics, id: Lib.Font.Id, data: []const u8) !void {
|
||||||
if (self.fonts.get(.fromInt(@intFromEnum(id)))) |font| {
|
const sokol_state = self.sokol_state orelse return error.SokolNotSetup;
|
||||||
|
|
||||||
|
const font = self.fonts.get(.fromInt(@intFromEnum(id))) orelse return;
|
||||||
|
|
||||||
const new_data = try self.gpa.dupe(u8, data);
|
const new_data = try self.gpa.dupe(u8, data);
|
||||||
|
errdefer self.gpa.free(new_data);
|
||||||
|
|
||||||
|
const new_fontstash_id = try sokol_state.font_context.addFont("", new_data);
|
||||||
|
|
||||||
if (font.data) |old_data| {
|
if (font.data) |old_data| {
|
||||||
self.gpa.free(old_data);
|
self.gpa.free(old_data);
|
||||||
}
|
}
|
||||||
font.data = new_data;
|
font.data = new_data;
|
||||||
font.fontstash_id = null;
|
font.fontstash_id = new_fontstash_id;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -9,7 +9,7 @@ const sokol = @import("sokol");
|
|||||||
const sapp = sokol.app;
|
const sapp = sokol.app;
|
||||||
const simgui = sokol.imgui;
|
const simgui = sokol.imgui;
|
||||||
|
|
||||||
const Lib = @import("lib");
|
const Lib = @import("engine_lib");
|
||||||
const build_options = Lib.build_options;
|
const build_options = Lib.build_options;
|
||||||
const enabled = build_options.has_imgui;
|
const enabled = build_options.has_imgui;
|
||||||
|
|
||||||
@ -2,7 +2,7 @@ const std = @import("std");
|
|||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
|
|
||||||
const lib = @import("lib");
|
const lib = @import("engine_lib");
|
||||||
const Frame = lib.Frame;
|
const Frame = lib.Frame;
|
||||||
const Nanoseconds = lib.Nanoseconds;
|
const Nanoseconds = lib.Nanoseconds;
|
||||||
const Vec2 = lib.Math.Vec2;
|
const Vec2 = lib.Math.Vec2;
|
||||||
@ -5,7 +5,7 @@ const assert = std.debug.assert;
|
|||||||
const sokol = @import("sokol");
|
const sokol = @import("sokol");
|
||||||
const sapp = sokol.app;
|
const sapp = sokol.app;
|
||||||
|
|
||||||
pub const Input = @import("./input.zig");
|
const Input = @import("./input.zig");
|
||||||
|
|
||||||
const ScreenScalar = @import("./screen_scaler.zig");
|
const ScreenScalar = @import("./screen_scaler.zig");
|
||||||
pub const imgui = @import("./imgui.zig");
|
pub const imgui = @import("./imgui.zig");
|
||||||
@ -17,7 +17,9 @@ const STBImage = @import("stb_image");
|
|||||||
|
|
||||||
const Gfx = Graphics;
|
const Gfx = Graphics;
|
||||||
|
|
||||||
const Lib = @import("lib");
|
pub const Lib = @import("engine_lib");
|
||||||
|
pub const AssetBundle = @import("./asset_bundle.zig");
|
||||||
|
|
||||||
const build_options = Lib.build_options;
|
const build_options = Lib.build_options;
|
||||||
const debug_keybinds = (builtin.mode == .Debug);
|
const debug_keybinds = (builtin.mode == .Debug);
|
||||||
const GameCallbacks = Lib.Callbacks;
|
const GameCallbacks = Lib.Callbacks;
|
||||||
@ -343,21 +345,22 @@ const GameState = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn, initial: Init, assets: []const Lib.Asset) void {
|
fn runGameInit(
|
||||||
|
self: *GameState,
|
||||||
|
init_fn: GameCallbacks.InitFn,
|
||||||
|
initial: Init,
|
||||||
|
asset_registry: Lib.Asset.Registry
|
||||||
|
) void {
|
||||||
assert(self.frame == null);
|
assert(self.frame == null);
|
||||||
const opts = Lib.Init{
|
const opts = Lib.Init{
|
||||||
.gpa = self.gpa,
|
.gpa = self.gpa,
|
||||||
.seed = initial.seed,
|
.seed = initial.seed,
|
||||||
.assets = .{
|
.assets = asset_registry
|
||||||
.items = assets
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
self.state = init_fn(&opts);
|
self.state = init_fn(&opts);
|
||||||
|
|
||||||
self.frame = .init(self.gpa, initial.screen_size);
|
self.frame = .init(self.gpa, initial.screen_size);
|
||||||
self.frame.?.assets = .{
|
self.frame.?.assets = asset_registry;
|
||||||
.items = assets
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void {
|
fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void {
|
||||||
@ -476,13 +479,157 @@ const Recording = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AssetManager = struct {
|
||||||
|
const LoadAsset = union(enum) {
|
||||||
|
font: struct {
|
||||||
|
id: Lib.Font.Id,
|
||||||
|
path: []const u8
|
||||||
|
},
|
||||||
|
sound: struct {
|
||||||
|
id: Lib.Sound.Id,
|
||||||
|
path: []const u8
|
||||||
|
},
|
||||||
|
texture: struct {
|
||||||
|
id: Lib.Texture.Id,
|
||||||
|
path: []const u8
|
||||||
|
},
|
||||||
|
|
||||||
|
pub fn clone(self: LoadAsset, arena: std.mem.Allocator) !LoadAsset {
|
||||||
|
return switch (self) {
|
||||||
|
.font => |opts| LoadAsset{
|
||||||
|
.font = .{
|
||||||
|
.id = opts.id,
|
||||||
|
.path = try arena.dupe(u8, opts.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.sound => |opts| LoadAsset{
|
||||||
|
.sound = .{
|
||||||
|
.id = opts.id,
|
||||||
|
.path = try arena.dupe(u8, opts.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.texture => |opts| LoadAsset{
|
||||||
|
.texture = .{
|
||||||
|
.id = opts.id,
|
||||||
|
.path = try arena.dupe(u8, opts.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
assets: std.ArrayList(Lib.Asset),
|
||||||
|
textures: std.ArrayList(Lib.Texture),
|
||||||
|
|
||||||
|
dir: ?std.fs.Dir,
|
||||||
|
queued_loads_arena: std.heap.ArenaAllocator,
|
||||||
|
queued_loads: std.ArrayList(LoadAsset),
|
||||||
|
|
||||||
|
pub fn init(gpa: std.mem.Allocator, dir: ?std.fs.Dir) AssetManager {
|
||||||
|
return AssetManager{
|
||||||
|
.gpa = gpa,
|
||||||
|
.assets = .empty,
|
||||||
|
.textures = .empty,
|
||||||
|
.dir = dir,
|
||||||
|
.queued_loads_arena = .init(gpa),
|
||||||
|
.queued_loads = .empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *AssetManager) void {
|
||||||
|
self.queued_loads_arena.deinit();
|
||||||
|
self.assets.deinit(self.gpa);
|
||||||
|
self.textures.deinit(self.gpa);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initRegistry(self: AssetManager) Lib.Asset.Registry {
|
||||||
|
return Lib.Asset.Registry{
|
||||||
|
.assets = self.assets.items,
|
||||||
|
.textures = self.textures.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enqueueLoad(self: *AssetManager, load: LoadAsset) !void {
|
||||||
|
const arena = self.queued_loads_arena.allocator();
|
||||||
|
|
||||||
|
try self.queued_loads.append(arena, try load.clone(arena));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn performQueuedLoads(self: *AssetManager, graphics: *Graphics, audio: *Audio) !void {
|
||||||
|
if (self.dir == null) {
|
||||||
|
if (self.queued_loads.items.len > 0) {
|
||||||
|
return error.MissingAssetDirectory;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dir = self.dir.?;
|
||||||
|
for (self.queued_loads.items) |queued_load| {
|
||||||
|
switch (queued_load) {
|
||||||
|
.font => |opts| {
|
||||||
|
const font_data = try readFileAll(self.gpa, dir, opts.path);
|
||||||
|
defer self.gpa.free(font_data);
|
||||||
|
try graphics.setFontData(opts.id, font_data);
|
||||||
|
},
|
||||||
|
.sound => |opts| {
|
||||||
|
const sound_data = try readFileAll(self.gpa, dir, opts.path);
|
||||||
|
defer self.gpa.free(sound_data);
|
||||||
|
try audio.setSoundData(opts.id, .{
|
||||||
|
.data = sound_data,
|
||||||
|
.format = .vorbis
|
||||||
|
});
|
||||||
|
},
|
||||||
|
.texture => |opts| {
|
||||||
|
const image_data = try readFileAll(self.gpa, dir, opts.path);
|
||||||
|
defer self.gpa.free(image_data);
|
||||||
|
|
||||||
|
const image = try STBImage.load(image_data);
|
||||||
|
defer image.deinit();
|
||||||
|
|
||||||
|
// TODO: Assert that loaded data matches precomputed texture info
|
||||||
|
try graphics.setTextureData(opts.id, .{
|
||||||
|
.width = image.width,
|
||||||
|
.height = image.height,
|
||||||
|
.rgba = image.rgba8_pixels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.queued_loads_arena.reset(.free_all);
|
||||||
|
self.queued_loads = .empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasTexture(self: AssetManager, texture_id: Lib.Texture.Id) bool {
|
||||||
|
for (self.textures.items) |texture| {
|
||||||
|
if (texture.id == texture_id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn assertAssetsLoaded(self: AssetManager) void {
|
||||||
|
for (self.assets.items) |asset| {
|
||||||
|
switch (asset.kind) {
|
||||||
|
.texture => |texture_id| {
|
||||||
|
assert(self.hasTexture(texture_id));
|
||||||
|
},
|
||||||
|
else => {} // TODO:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const Engine = @This();
|
const Engine = @This();
|
||||||
|
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
graphics: Graphics,
|
graphics: Graphics,
|
||||||
audio: Audio,
|
audio: Audio,
|
||||||
|
|
||||||
assets: std.ArrayList(Lib.Asset),
|
asset_bundle: ?AssetBundle = null,
|
||||||
|
asset_manager: AssetManager,
|
||||||
game_linking: GameLinking,
|
game_linking: GameLinking,
|
||||||
game_state: GameState,
|
game_state: GameState,
|
||||||
|
|
||||||
@ -501,6 +648,7 @@ recording_tick: u64,
|
|||||||
const RunOptions = struct {
|
const RunOptions = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: std.mem.Allocator,
|
||||||
game_library_path: ?[]const u8 = null,
|
game_library_path: ?[]const u8 = null,
|
||||||
|
asset_bundle_bytes: ?[]const u8 = null,
|
||||||
do_recording: bool = (builtin.mode == .Debug),
|
do_recording: bool = (builtin.mode == .Debug),
|
||||||
screen_width: u32 = 640,
|
screen_width: u32 = 640,
|
||||||
screen_height: u32 = 480,
|
screen_height: u32 = 480,
|
||||||
@ -514,11 +662,30 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
|
|||||||
game_linking = try .initStatic();
|
game_linking = try .initStatic();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var maybe_asset_bundle: ?AssetBundle = null;
|
||||||
|
if (opts.asset_bundle_bytes) |asset_bundle_bytes| {
|
||||||
|
var asset_bundle = AssetBundle.init();
|
||||||
|
errdefer asset_bundle.deinit(opts.allocator);
|
||||||
|
|
||||||
|
var reader = std.Io.Reader.fixed(asset_bundle_bytes);
|
||||||
|
asset_bundle.read(opts.allocator, &reader) catch |e| {
|
||||||
|
log.err("Failed to load asset bundle: {}", .{e});
|
||||||
|
return error.FailedToLoadAssetBundle;
|
||||||
|
};
|
||||||
|
|
||||||
|
maybe_asset_bundle = asset_bundle;
|
||||||
|
}
|
||||||
|
|
||||||
const initial = GameState.Init{
|
const initial = GameState.Init{
|
||||||
.seed = @bitCast(std.time.milliTimestamp()),
|
.seed = @bitCast(std.time.milliTimestamp()),
|
||||||
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height)
|
.screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var asset_dir: ?std.fs.Dir = null;
|
||||||
|
if (build_options.load_assets_at_runtime) {
|
||||||
|
asset_dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true });
|
||||||
|
}
|
||||||
|
|
||||||
self.* = Engine{
|
self.* = Engine{
|
||||||
.allocator = opts.allocator,
|
.allocator = opts.allocator,
|
||||||
.graphics = try .init(.{
|
.graphics = try .init(.{
|
||||||
@ -543,64 +710,100 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
|
|||||||
.play_recording = false,
|
.play_recording = false,
|
||||||
.recording_tick = 0,
|
.recording_tick = 0,
|
||||||
.seed = initial.seed,
|
.seed = initial.seed,
|
||||||
.assets = .empty
|
.asset_manager = .init(opts.allocator, asset_dir),
|
||||||
|
.asset_bundle = maybe_asset_bundle,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (build_options.load_assets_at_runtime) {
|
if (build_options.load_assets_at_runtime) {
|
||||||
var dir = try std.fs.cwd().openDir(build_options.asset_dir, .{ .iterate = true });
|
const dir = asset_dir.?;
|
||||||
defer dir.close();
|
|
||||||
var loader: Lib.Asset.Loader = .init(opts.allocator, dir);
|
var loader: Lib.Asset.Loader = .init(opts.allocator, dir);
|
||||||
defer loader.deinit();
|
defer loader.deinit();
|
||||||
game_linking.callbacks.loadAssets(&loader);
|
game_linking.callbacks.loadAssets(&loader);
|
||||||
for (loader.assets.items) |asset_desc| {
|
for (loader.assets.items) |asset_desc| {
|
||||||
switch (asset_desc.kind) {
|
switch (asset_desc.kind) {
|
||||||
.font => |path| {
|
.font => |path| {
|
||||||
const f = try dir.openFile(path, .{});
|
|
||||||
defer f.close();
|
|
||||||
|
|
||||||
var wa: std.Io.Writer.Allocating = .init(self.allocator);
|
|
||||||
defer wa.deinit();
|
|
||||||
|
|
||||||
var buffer: [4 * 4096]u8 = undefined;
|
|
||||||
var reader = f.reader(&buffer);
|
|
||||||
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
|
||||||
|
|
||||||
const font_id = try self.graphics.addFont();
|
const font_id = try self.graphics.addFont();
|
||||||
try self.graphics.setFontData(font_id, wa.written());
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
try self.assets.append(self.allocator, .{
|
|
||||||
.id = asset_desc.id,
|
.id = asset_desc.id,
|
||||||
.kind = .{ .font = font_id }
|
.kind = .{ .font = font_id }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try self.asset_manager.enqueueLoad(.{
|
||||||
|
.font = .{
|
||||||
|
.id = font_id,
|
||||||
|
.path = path
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
.texture => |path| {
|
.texture => |texture| {
|
||||||
_ = path; // autofix
|
const texture_id = try self.graphics.addTexture();
|
||||||
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
|
.id = asset_desc.id,
|
||||||
|
.kind = .{ .texture = texture_id }
|
||||||
|
});
|
||||||
|
|
||||||
|
try self.asset_manager.enqueueLoad(.{
|
||||||
|
.texture = .{
|
||||||
|
.id = texture_id,
|
||||||
|
.path = texture.path
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try self.asset_manager.textures.append(self.asset_manager.gpa, .{
|
||||||
|
.id = texture_id,
|
||||||
|
.info = texture.info
|
||||||
|
});
|
||||||
},
|
},
|
||||||
.sound => |path| {
|
.sound => |path| {
|
||||||
const f = try dir.openFile(path, .{});
|
|
||||||
defer f.close();
|
|
||||||
|
|
||||||
var wa: std.Io.Writer.Allocating = .init(self.allocator);
|
|
||||||
defer wa.deinit();
|
|
||||||
|
|
||||||
var buffer: [4 * 4096]u8 = undefined;
|
|
||||||
var reader = f.reader(&buffer);
|
|
||||||
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
|
||||||
|
|
||||||
const sound_id = try self.audio.addSound();
|
const sound_id = try self.audio.addSound();
|
||||||
try self.audio.setSoundData(sound_id, .{
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
.data = wa.written(),
|
|
||||||
.format = .vorbis
|
|
||||||
});
|
|
||||||
try self.assets.append(self.allocator, .{
|
|
||||||
.id = asset_desc.id,
|
.id = asset_desc.id,
|
||||||
.kind = .{ .sound = sound_id }
|
.kind = .{ .sound = sound_id }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
try self.asset_manager.enqueueLoad(.{
|
||||||
|
.sound = .{
|
||||||
|
.id = sound_id,
|
||||||
|
.path = path
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.game_state.runGameInit(game_linking.callbacks.init, initial, self.assets.items);
|
if (maybe_asset_bundle) |asset_bundle| {
|
||||||
|
for (asset_bundle.fonts.items) |font| {
|
||||||
|
const font_id = try self.graphics.addFont();
|
||||||
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
|
.id = font.asset_id,
|
||||||
|
.kind = .{ .font = font_id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (asset_bundle.textures.items) |texture| {
|
||||||
|
const texture_id = try self.graphics.addTexture();
|
||||||
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
|
.id = texture.asset_id,
|
||||||
|
.kind = .{ .texture = texture_id }
|
||||||
|
});
|
||||||
|
|
||||||
|
try self.asset_manager.textures.append(self.asset_manager.gpa, .{
|
||||||
|
.id = texture_id,
|
||||||
|
.info = texture.info
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (asset_bundle.sounds.items) |sound| {
|
||||||
|
const sound_id = try self.audio.addSound();
|
||||||
|
try self.asset_manager.assets.append(self.allocator, .{
|
||||||
|
.id = sound.asset_id,
|
||||||
|
.kind = .{ .sound = sound_id }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.game_state.runGameInit(game_linking.callbacks.init, initial, self.asset_manager.initRegistry());
|
||||||
|
|
||||||
if (opts.do_recording) {
|
if (opts.do_recording) {
|
||||||
self.recording = .init(opts.allocator, initial);
|
self.recording = .init(opts.allocator, initial);
|
||||||
@ -660,6 +863,20 @@ pub fn run(self: *Engine, opts: RunOptions) !void {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 {
|
||||||
|
const f = try dir.openFile(sub_path, .{});
|
||||||
|
defer f.close();
|
||||||
|
|
||||||
|
var wa: std.Io.Writer.Allocating = .init(gpa);
|
||||||
|
defer wa.deinit();
|
||||||
|
|
||||||
|
var buffer: [4 * 4096]u8 = undefined;
|
||||||
|
var reader = f.reader(&buffer);
|
||||||
|
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
||||||
|
|
||||||
|
return try wa.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
fn sokolInit(self: *Engine) !void {
|
fn sokolInit(self: *Engine) !void {
|
||||||
const zone = tracy.initZone(@src(), .{ });
|
const zone = tracy.initZone(@src(), .{ });
|
||||||
defer zone.deinit();
|
defer zone.deinit();
|
||||||
@ -667,7 +884,44 @@ fn sokolInit(self: *Engine) !void {
|
|||||||
try self.graphics.setupSokol();
|
try self.graphics.setupSokol();
|
||||||
self.audio.setupSokol();
|
self.audio.setupSokol();
|
||||||
|
|
||||||
try self.graphics.refreshFonts();
|
try self.asset_manager.performQueuedLoads(&self.graphics, &self.audio);
|
||||||
|
if (self.asset_bundle) |*asset_bundle| {
|
||||||
|
const registry = self.asset_manager.initRegistry();
|
||||||
|
|
||||||
|
for (asset_bundle.fonts.items) |font| {
|
||||||
|
const font_id = registry.getFont(font.asset_id);
|
||||||
|
assert(font_id != Lib.Font.Id.nil);
|
||||||
|
try self.graphics.setFontData(font_id, asset_bundle.getData(font.data));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (asset_bundle.textures.items) |texture| {
|
||||||
|
const texture_id = registry.getTexture(texture.asset_id);
|
||||||
|
assert(texture_id != Lib.Texture.Id.nil);
|
||||||
|
|
||||||
|
const image = try STBImage.load(asset_bundle.getData(texture.data));
|
||||||
|
defer image.deinit();
|
||||||
|
|
||||||
|
try self.graphics.setTextureData(texture_id, .{
|
||||||
|
.width = image.width,
|
||||||
|
.height = image.height,
|
||||||
|
.rgba = image.rgba8_pixels
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (asset_bundle.sounds.items) |sound| {
|
||||||
|
const sound_id = registry.getSound(sound.asset_id);
|
||||||
|
assert(sound_id != Lib.Sound.Id.nil);
|
||||||
|
try self.audio.setSoundData(sound_id, .{
|
||||||
|
.data = asset_bundle.getData(sound.data),
|
||||||
|
.format = .vorbis
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
asset_bundle.deinit(self.allocator);
|
||||||
|
self.asset_bundle = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.asset_manager.assertAssetsLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sokolCleanup(self: *Engine) void {
|
fn sokolCleanup(self: *Engine) void {
|
||||||
@ -684,7 +938,7 @@ fn sokolCleanup(self: *Engine) void {
|
|||||||
self.audio.deinit();
|
self.audio.deinit();
|
||||||
self.graphics.deinit(self.allocator);
|
self.graphics.deinit(self.allocator);
|
||||||
self.game_linking.deinit(self.allocator);
|
self.game_linking.deinit(self.allocator);
|
||||||
self.assets.deinit(self.allocator);
|
self.asset_manager.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sokolFrame(self: *Engine) !void {
|
fn sokolFrame(self: *Engine) !void {
|
||||||
@ -759,7 +1013,7 @@ fn sokolFrame(self: *Engine) !void {
|
|||||||
self.game_state.deinit();
|
self.game_state.deinit();
|
||||||
|
|
||||||
self.game_state = .init(self.allocator);
|
self.game_state = .init(self.allocator);
|
||||||
self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.assets.items);
|
self.game_state.runGameInit(self.game_linking.callbacks.init, initial, self.asset_manager.initRegistry());
|
||||||
|
|
||||||
if (self.recording) |recording| {
|
if (self.recording) |recording| {
|
||||||
recording.deinit();
|
recording.deinit();
|
||||||
@ -811,7 +1065,7 @@ fn showDebugWindow(self: *Engine) !void {
|
|||||||
self.game_state.deinit();
|
self.game_state.deinit();
|
||||||
|
|
||||||
self.game_state = .init(self.allocator);
|
self.game_state = .init(self.allocator);
|
||||||
self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.assets.items);
|
self.game_state.runGameInit(self.game_linking.callbacks.init, recording.initial, self.asset_manager.initRegistry());
|
||||||
|
|
||||||
self.play_recording = true;
|
self.play_recording = true;
|
||||||
self.recording_tick = 0;
|
self.recording_tick = 0;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
const Gfx = @import("./graphics.zig");
|
const Gfx = @import("./graphics.zig");
|
||||||
|
|
||||||
const Math = @import("lib").Math;
|
const Math = @import("engine_lib").Math;
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
const Vec4 = Math.Vec4;
|
const Vec4 = Math.Vec4;
|
||||||
const rgb = Math.rgb;
|
const rgb = Math.rgb;
|
||||||
|
|
||||||
const Lib = @import("lib");
|
const Lib = @import("engine_lib");
|
||||||
const Frame = Lib.Frame;
|
const Frame = Lib.Frame;
|
||||||
|
|
||||||
const ScreenScalar = @This();
|
const ScreenScalar = @This();
|
||||||
206
engine/src/game_lib/asset.zig
Normal file
206
engine/src/game_lib/asset.zig
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const log = std.log.scoped(.engine);
|
||||||
|
|
||||||
|
const STBImage = @import("stb_image");
|
||||||
|
|
||||||
|
const Font = @import("./font.zig");
|
||||||
|
const Sound = @import("./sound.zig");
|
||||||
|
const Texture = @import("./texture.zig");
|
||||||
|
|
||||||
|
const Asset = @This();
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
kind: union(Type) {
|
||||||
|
font: Font.Id,
|
||||||
|
sound: Sound.Id,
|
||||||
|
texture: Texture.Id,
|
||||||
|
},
|
||||||
|
|
||||||
|
pub const Id = u32;
|
||||||
|
pub const Type = enum {
|
||||||
|
font,
|
||||||
|
sound,
|
||||||
|
texture
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Loader = struct {
|
||||||
|
const Descriptor = struct {
|
||||||
|
const KindTexture = struct {
|
||||||
|
path: []const u8,
|
||||||
|
info: Texture.Info
|
||||||
|
};
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
kind: union(Type) {
|
||||||
|
font: []const u8,
|
||||||
|
sound: []const u8,
|
||||||
|
texture: KindTexture
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
dir: std.fs.Dir,
|
||||||
|
assets: std.ArrayList(Descriptor),
|
||||||
|
add_failed: bool,
|
||||||
|
|
||||||
|
pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader {
|
||||||
|
return Loader{
|
||||||
|
.arena = .init(gpa),
|
||||||
|
.dir = dir,
|
||||||
|
.assets = .empty,
|
||||||
|
.add_failed = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Loader) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readFile(self: *Loader, gpa: std.mem.Allocator, path: []const u8) ![]u8 {
|
||||||
|
const f = try self.dir.openFile(path, .{});
|
||||||
|
defer f.close();
|
||||||
|
|
||||||
|
var wa: std.Io.Writer.Allocating = .init(gpa);
|
||||||
|
defer wa.deinit();
|
||||||
|
|
||||||
|
var buffer: [4 * 4096]u8 = undefined;
|
||||||
|
var reader = f.reader(&buffer);
|
||||||
|
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
||||||
|
|
||||||
|
return try wa.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void {
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
const path_dupe = allocator.dupe(u8, path) catch |e| {
|
||||||
|
log.err("Failed to duplicate path: {}", .{e});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.assets.append(allocator, Descriptor{
|
||||||
|
.id = id,
|
||||||
|
.kind = .{ .font = path_dupe }
|
||||||
|
}) catch |e| {
|
||||||
|
log.err("Failed add sound: {}", .{e});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void {
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
const path_dupe = allocator.dupe(u8, path) catch |e| {
|
||||||
|
log.err("Failed to duplicate path: {}", .{e});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.assets.append(allocator, Descriptor{
|
||||||
|
.id = id,
|
||||||
|
.kind = .{ .sound = path_dupe }
|
||||||
|
}) catch |e| {
|
||||||
|
log.err("Failed add sound: {}", .{e});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addTextureFallible(self: *Loader, id: Asset.Id, path: []const u8) !void {
|
||||||
|
const allocator = self.arena.allocator();
|
||||||
|
|
||||||
|
var image_info: Texture.Info = undefined;
|
||||||
|
{
|
||||||
|
const file_contents = try self.readFile(allocator, path);
|
||||||
|
defer allocator.free(file_contents);
|
||||||
|
|
||||||
|
const image = try STBImage.load(file_contents);
|
||||||
|
defer image.deinit();
|
||||||
|
|
||||||
|
image_info = .{
|
||||||
|
.width = image.width,
|
||||||
|
.height = image.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const path_dupe = allocator.dupe(u8, path) catch |e| {
|
||||||
|
log.err("Failed to duplicate path: {}", .{e});
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const texture = Descriptor.KindTexture{
|
||||||
|
.path = path_dupe,
|
||||||
|
.info = image_info
|
||||||
|
};
|
||||||
|
|
||||||
|
self.assets.append(allocator, Descriptor{
|
||||||
|
.id = id,
|
||||||
|
.kind = .{ .texture = texture }
|
||||||
|
}) catch |e| {
|
||||||
|
log.err("Failed add sound: {}", .{e});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn addTexture(self: *Loader, id: Asset.Id, path: []const u8) void {
|
||||||
|
self.addTextureFallible(id, path) catch |e| {
|
||||||
|
log.err("Failed to add texture: {}", .{e});
|
||||||
|
self.add_failed = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Registry = struct {
|
||||||
|
assets: []const Asset,
|
||||||
|
textures: []const Texture,
|
||||||
|
|
||||||
|
pub const empty = Registry{
|
||||||
|
.assets = &.{},
|
||||||
|
.textures = &.{}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn getAsset(self: Registry, id: Asset.Id) ?Asset {
|
||||||
|
// TODO: Make this O(1)?
|
||||||
|
for (self.assets) |asset| {
|
||||||
|
if (asset.id == id) {
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getSound(self: Registry, id: Asset.Id) Sound.Id {
|
||||||
|
const asset = self.getAsset(id) orelse return Sound.Id.nil;
|
||||||
|
if (asset.kind != .sound) {
|
||||||
|
return Sound.Id.nil;
|
||||||
|
}
|
||||||
|
return asset.kind.sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getFont(self: Registry, id: Asset.Id) Font.Id {
|
||||||
|
const asset = self.getAsset(id) orelse return Font.Id.nil;
|
||||||
|
if (asset.kind != .font) {
|
||||||
|
return Font.Id.nil;
|
||||||
|
}
|
||||||
|
return asset.kind.font;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTexture(self: Registry, id: Asset.Id) Texture.Id {
|
||||||
|
const asset = self.getAsset(id) orelse return Texture.Id.nil;
|
||||||
|
if (asset.kind != .texture) {
|
||||||
|
return Texture.Id.nil;
|
||||||
|
}
|
||||||
|
return asset.kind.texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTextureInfo(self: Registry, id: Texture.Id) Texture.Info {
|
||||||
|
if (id == .nil) {
|
||||||
|
log.warn("Failed to access .nil texture info", .{});
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this O(1)?
|
||||||
|
for (self.textures) |texture| {
|
||||||
|
if (texture.id == id) {
|
||||||
|
return texture.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("Failed to find texture info, id={}", .{id});
|
||||||
|
return .nil;
|
||||||
|
}
|
||||||
|
};
|
||||||
9
engine/src/game_lib/font.zig
Normal file
9
engine/src/game_lib/font.zig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
|
||||||
|
pub const Id = enum(u16) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
||||||
|
};
|
||||||
@ -6,8 +6,9 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
/// This array provides:
|
/// This array provides:
|
||||||
/// - O(1) insertion
|
/// - O(1) insertion
|
||||||
/// - O(1) deletion
|
|
||||||
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
||||||
|
/// TODO: Use a stack of unused indexes to implement O(1) insertion
|
||||||
|
/// TODO: Implement a way to use a list *assumeCapacity() style of functions.
|
||||||
pub fn GenerationalArrayList(
|
pub fn GenerationalArrayList(
|
||||||
Index: type,
|
Index: type,
|
||||||
Generation: type,
|
Generation: type,
|
||||||
@ -367,6 +367,11 @@ pub const Rect = struct {
|
|||||||
.size = Vec2.zero
|
.size = Vec2.zero
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const unit = Rect{
|
||||||
|
.pos = Vec2.zero,
|
||||||
|
.size = Vec2.init(1, 1)
|
||||||
|
};
|
||||||
|
|
||||||
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
|
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
|
||||||
return Rect{
|
return Rect{
|
||||||
.pos = Vec2.init(x, y),
|
.pos = Vec2.init(x, y),
|
||||||
@ -5,6 +5,7 @@ pub const build_options = @import("build_options");
|
|||||||
pub const ImGui = @import("./imgui.zig");
|
pub const ImGui = @import("./imgui.zig");
|
||||||
pub const Math = @import("./math.zig");
|
pub const Math = @import("./math.zig");
|
||||||
pub const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
|
pub const GenerationalArrayList = @import("./generational_array_list.zig").GenerationalArrayList;
|
||||||
|
const STBImage = @import("stb_image");
|
||||||
|
|
||||||
const rgb = Math.rgb;
|
const rgb = Math.rgb;
|
||||||
const Rect = Math.Rect;
|
const Rect = Math.Rect;
|
||||||
@ -142,24 +143,13 @@ pub const MouseButton = enum {
|
|||||||
middle,
|
middle,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const TextureId = enum(u16) {
|
pub const Texture = @import("./texture.zig");
|
||||||
_,
|
pub const Font = @import("./font.zig");
|
||||||
};
|
pub const Sound = @import("./sound.zig");
|
||||||
|
pub const Asset = @import("./asset.zig");
|
||||||
pub const FontId = enum(u16) {
|
|
||||||
_,
|
|
||||||
|
|
||||||
pub const nil: FontId = @enumFromInt(std.math.maxInt(@typeInfo(FontId).@"enum".tag_type));
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const SoundId = enum(u16) {
|
|
||||||
_,
|
|
||||||
|
|
||||||
pub const nil: SoundId = @enumFromInt(std.math.maxInt(@typeInfo(SoundId).@"enum".tag_type));
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Sprite = struct {
|
pub const Sprite = struct {
|
||||||
texture: TextureId,
|
texture: Texture.Id,
|
||||||
uv: Rect
|
uv: Rect
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -221,7 +211,7 @@ pub const GraphicsCommand = union(enum) {
|
|||||||
pub const DrawText = struct {
|
pub const DrawText = struct {
|
||||||
pos: Vec2,
|
pos: Vec2,
|
||||||
text: []const u8,
|
text: []const u8,
|
||||||
font: FontId,
|
font: Font.Id,
|
||||||
size: f32,
|
size: f32,
|
||||||
color: Vec4,
|
color: Vec4,
|
||||||
};
|
};
|
||||||
@ -240,7 +230,7 @@ pub const GraphicsCommand = union(enum) {
|
|||||||
|
|
||||||
pub const AudioCommand = union(enum) {
|
pub const AudioCommand = union(enum) {
|
||||||
pub const Play = struct {
|
pub const Play = struct {
|
||||||
id: SoundId,
|
id: Sound.Id,
|
||||||
volume: f32 = 1
|
volume: f32 = 1
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -412,7 +402,7 @@ pub const Frame = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const DrawTextOptions = struct {
|
pub const DrawTextOptions = struct {
|
||||||
font: FontId,
|
font: Font.Id,
|
||||||
size: f32 = 16,
|
size: f32 = 16,
|
||||||
color: Vec4 = rgb(255, 255, 255),
|
color: Vec4 = rgb(255, 255, 255),
|
||||||
};
|
};
|
||||||
@ -491,121 +481,6 @@ pub const Init = struct {
|
|||||||
assets: Asset.Registry,
|
assets: Asset.Registry,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Asset = struct {
|
|
||||||
id: Id,
|
|
||||||
kind: union(Type) {
|
|
||||||
font: FontId,
|
|
||||||
sound: SoundId,
|
|
||||||
texture: TextureId,
|
|
||||||
},
|
|
||||||
|
|
||||||
pub const Id = u32;
|
|
||||||
pub const Type = enum {
|
|
||||||
font,
|
|
||||||
sound,
|
|
||||||
texture
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Loader = struct {
|
|
||||||
const Descriptor = struct {
|
|
||||||
id: Id,
|
|
||||||
kind: union(Type) {
|
|
||||||
font: []const u8,
|
|
||||||
sound: []const u8,
|
|
||||||
texture: []const u8,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
arena: std.heap.ArenaAllocator,
|
|
||||||
dir: std.fs.Dir,
|
|
||||||
assets: std.ArrayList(Descriptor),
|
|
||||||
|
|
||||||
pub fn init(gpa: std.mem.Allocator, dir: std.fs.Dir) Loader {
|
|
||||||
return Loader{
|
|
||||||
.arena = .init(gpa),
|
|
||||||
.dir = dir,
|
|
||||||
.assets = .empty,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Loader) void {
|
|
||||||
self.arena.deinit();
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addFont(self: *Loader, id: Asset.Id, path: []const u8) void {
|
|
||||||
const allocator = self.arena.allocator();
|
|
||||||
const path_dupe = allocator.dupe(u8, path) catch |e| {
|
|
||||||
log.err("Failed to duplicate path: {}", .{e});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.assets.append(allocator, Descriptor{
|
|
||||||
.id = id,
|
|
||||||
.kind = .{ .font = path_dupe }
|
|
||||||
}) catch |e| {
|
|
||||||
log.err("Failed add sound: {}", .{e});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn addSound(self: *Loader, id: Asset.Id, path: []const u8) void {
|
|
||||||
const allocator = self.arena.allocator();
|
|
||||||
const path_dupe = allocator.dupe(u8, path) catch |e| {
|
|
||||||
log.err("Failed to duplicate path: {}", .{e});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.assets.append(allocator, Descriptor{
|
|
||||||
.id = id,
|
|
||||||
.kind = .{ .sound = path_dupe }
|
|
||||||
}) catch |e| {
|
|
||||||
log.err("Failed add sound: {}", .{e});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Registry = struct {
|
|
||||||
items: []const Asset,
|
|
||||||
|
|
||||||
pub const empty = Registry{
|
|
||||||
.items = &.{}
|
|
||||||
};
|
|
||||||
|
|
||||||
pub fn get(self: Registry, id: Id) ?Asset {
|
|
||||||
for (self.items) |asset| {
|
|
||||||
if (asset.id == id) {
|
|
||||||
return asset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getSound(self: Registry, id: Id) SoundId {
|
|
||||||
const asset = self.get(id) orelse return SoundId.nil;
|
|
||||||
if (asset.kind != Type.sound) {
|
|
||||||
return SoundId.nil;
|
|
||||||
}
|
|
||||||
return asset.kind.sound;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getFont(self: Registry, id: Id) FontId {
|
|
||||||
const asset = self.get(id) orelse return FontId.nil;
|
|
||||||
if (asset.kind != Type.font) {
|
|
||||||
return FontId.nil;
|
|
||||||
}
|
|
||||||
return asset.kind.font;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn getTexture(self: Registry, id: Id) TextureId {
|
|
||||||
const asset = self.get(id) orelse return TextureId.nil;
|
|
||||||
if (asset.kind != Type.font) {
|
|
||||||
return TextureId.nil;
|
|
||||||
}
|
|
||||||
return asset.kind.font;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
pub const Callbacks = struct {
|
pub const Callbacks = struct {
|
||||||
pub const State = *anyopaque;
|
pub const State = *anyopaque;
|
||||||
|
|
||||||
9
engine/src/game_lib/sound.zig
Normal file
9
engine/src/game_lib/sound.zig
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
|
||||||
|
pub const Id = enum(u16) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
||||||
|
};
|
||||||
20
engine/src/game_lib/texture.zig
Normal file
20
engine/src/game_lib/texture.zig
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
id: Id,
|
||||||
|
info: Info,
|
||||||
|
|
||||||
|
pub const Info = extern struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
|
||||||
|
pub const nil = Info{
|
||||||
|
.width = 0,
|
||||||
|
.height = 0
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Id = enum(u16) {
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub const nil: Id = @enumFromInt(std.math.maxInt(@typeInfo(Id).@"enum".tag_type));
|
||||||
|
};
|
||||||
@ -1,6 +1,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const builtin = @import("builtin");
|
const builtin = @import("builtin");
|
||||||
const Engine = @import("./engine.zig");
|
const Engine = @import("engine");
|
||||||
|
const build_options = Engine.Lib.build_options;
|
||||||
|
|
||||||
var engine: Engine = undefined;
|
var engine: Engine = undefined;
|
||||||
|
|
||||||
@ -8,6 +9,11 @@ pub fn main() !void {
|
|||||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer _ = debug_allocator.deinit();
|
defer _ = debug_allocator.deinit();
|
||||||
|
|
||||||
|
var asset_bundle_bytes: ?[]const u8 = null;
|
||||||
|
if (!build_options.load_assets_at_runtime) {
|
||||||
|
asset_bundle_bytes = @embedFile("asset_bundle");
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Use tracy TracingAllocator
|
// TODO: Use tracy TracingAllocator
|
||||||
var allocator: std.mem.Allocator = undefined;
|
var allocator: std.mem.Allocator = undefined;
|
||||||
if (builtin.cpu.arch.isWasm()) {
|
if (builtin.cpu.arch.isWasm()) {
|
||||||
@ -28,7 +34,8 @@ pub fn main() !void {
|
|||||||
|
|
||||||
try engine.run(.{
|
try engine.run(.{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.game_library_path = game_library_path
|
.game_library_path = game_library_path,
|
||||||
|
.asset_bundle_bytes = asset_bundle_bytes
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
engine/tools/asset-bundler.zig
Normal file
79
engine/tools/asset-bundler.zig
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Game = @import("game");
|
||||||
|
const Engine = @import("engine");
|
||||||
|
const Lib = Engine.Lib;
|
||||||
|
const AssetBundle = Engine.AssetBundle;
|
||||||
|
|
||||||
|
fn readFileAll(gpa: std.mem.Allocator, dir: std.fs.Dir, sub_path: []const u8) ![]u8 {
|
||||||
|
const f = try dir.openFile(sub_path, .{});
|
||||||
|
defer f.close();
|
||||||
|
|
||||||
|
var wa: std.Io.Writer.Allocating = .init(gpa);
|
||||||
|
defer wa.deinit();
|
||||||
|
|
||||||
|
var buffer: [4 * 4096]u8 = undefined;
|
||||||
|
var reader = f.reader(&buffer);
|
||||||
|
_ = try reader.interface.stream(&wa.writer, .unlimited);
|
||||||
|
|
||||||
|
return try wa.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
defer _ = gpa.deinit();
|
||||||
|
|
||||||
|
const args = try std.process.argsAlloc(allocator);
|
||||||
|
defer std.process.argsFree(allocator, args);
|
||||||
|
|
||||||
|
if (args.len != 3) {
|
||||||
|
std.debug.print("Usage: ./asset-bundler <asset-dir> <output>", .{});
|
||||||
|
std.process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cwd = std.fs.cwd();
|
||||||
|
const asset_dir_path = args[1];
|
||||||
|
const output_path = args[2];
|
||||||
|
|
||||||
|
const asset_dir = try cwd.openDir(asset_dir_path, .{ .iterate = true });
|
||||||
|
|
||||||
|
var loader: Lib.Asset.Loader = .init(allocator, asset_dir);
|
||||||
|
defer loader.deinit();
|
||||||
|
Game.loadAssets(&loader);
|
||||||
|
|
||||||
|
var bundle = AssetBundle.init();
|
||||||
|
defer bundle.deinit(allocator);
|
||||||
|
|
||||||
|
for (loader.assets.items) |asset_desc| {
|
||||||
|
const id = asset_desc.id;
|
||||||
|
switch (asset_desc.kind) {
|
||||||
|
.font => |path| {
|
||||||
|
const font_data = try readFileAll(allocator, asset_dir, path);
|
||||||
|
defer allocator.free(font_data);
|
||||||
|
|
||||||
|
try bundle.appendFont(allocator, id, font_data);
|
||||||
|
},
|
||||||
|
.sound => |path| {
|
||||||
|
const sound_data = try readFileAll(allocator, asset_dir, path);
|
||||||
|
defer allocator.free(sound_data);
|
||||||
|
|
||||||
|
try bundle.appendSound(allocator, id, sound_data);
|
||||||
|
},
|
||||||
|
.texture => |opts| {
|
||||||
|
const texture_data = try readFileAll(allocator, asset_dir, opts.path);
|
||||||
|
defer allocator.free(texture_data);
|
||||||
|
|
||||||
|
try bundle.appendTexture(allocator, id, texture_data, opts.info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output_file = try std.fs.cwd().createFile(output_path, .{ });
|
||||||
|
defer output_file.close();
|
||||||
|
|
||||||
|
var buffer: [4096 * 4]u8 = undefined;
|
||||||
|
var writer = output_file.writer(&buffer);
|
||||||
|
|
||||||
|
try bundle.write(&writer.interface);
|
||||||
|
try writer.interface.flush();
|
||||||
|
}
|
||||||
16
src/game.zig
16
src/game.zig
@ -2,8 +2,8 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
const Engine = @import("engine");
|
const Engine = @import("engine_lib");
|
||||||
const FontId = Engine.FontId;
|
const FontId = Engine.Font.Id;
|
||||||
const Vec2 = Engine.Math.Vec2;
|
const Vec2 = Engine.Math.Vec2;
|
||||||
const rgb = Engine.Math.rgb;
|
const rgb = Engine.Math.rgb;
|
||||||
|
|
||||||
@ -20,6 +20,7 @@ const AssetId = enum {
|
|||||||
font_bold,
|
font_bold,
|
||||||
font_italic,
|
font_italic,
|
||||||
sound_wood,
|
sound_wood,
|
||||||
|
texture_icon,
|
||||||
};
|
};
|
||||||
|
|
||||||
const State = struct {
|
const State = struct {
|
||||||
@ -54,6 +55,8 @@ pub fn loadAssets(assets: *Engine.Asset.Loader) callconv(.c) void {
|
|||||||
assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf");
|
assets.addFont(@intFromEnum(AssetId.font_italic), "roboto-font/Roboto-Italic.ttf");
|
||||||
|
|
||||||
assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg");
|
assets.addSound(@intFromEnum(AssetId.sound_wood), "wood01.ogg");
|
||||||
|
|
||||||
|
assets.addTexture(@intFromEnum(AssetId.texture_icon), "icon.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
|
pub fn deinit(state_ptr: *anyopaque) callconv(.c) void {
|
||||||
@ -98,13 +101,20 @@ pub fn tick(state_ptr: *anyopaque, frame: *Engine.Frame) callconv(.c) void {
|
|||||||
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
|
.rect = .{ .pos = .init(0, 0), .size = canvas_size },
|
||||||
.color = rgb(20, 20, 20)
|
.color = rgb(20, 20, 20)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const icon = frame.assets.getTexture(@intFromEnum(AssetId.texture_icon));
|
||||||
|
|
||||||
const size = Vec2.init(20, 20);
|
const size = Vec2.init(20, 20);
|
||||||
frame.drawRectangle(.{
|
frame.drawRectangle(.{
|
||||||
.rect = .{
|
.rect = .{
|
||||||
.pos = state.player.sub(size.divideScalar(2)),
|
.pos = state.player.sub(size.divideScalar(2)),
|
||||||
.size = size
|
.size = size
|
||||||
},
|
},
|
||||||
.color = rgb(200, 2, 200)
|
.color = rgb(200, 2, 200),
|
||||||
|
.sprite = .{
|
||||||
|
.texture = icon,
|
||||||
|
.uv = .unit
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if (dir.x != 0 or dir.y != 0) {
|
if (dir.x != 0 or dir.y != 0) {
|
||||||
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);
|
frame.drawRectanglOutline(state.player.sub(size.divideScalar(2)), size, rgb(20, 20, 20), 3);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user