Create reusable plugins
Put components, systems, and settings in a plugin that your own game or another package can use. The game and reusable plugins use the same hooks.
Before you start
Use a Zig 0.17 game package that already calls eggy.addGame.
Install Eggy if you need one. A reusable plugin belongs in
src/root.zig as pub const Plugin; game-only behavior can stay in pub const Game
in src/main.zig.
Declare serializable components
List component types in the plugin's components declaration. This makes them available
to scene files and the Inspector:
pub const Spin = struct {
/// Turns per frame.
speed: f32 = 0.001,
turns: f32 = 0,
};
pub const Plugin = struct {
pub const components = .{Spin};
pub fn init(gpa: std.mem.Allocator, app: *eggy.App) !Plugin {
_ = gpa;
_ = try app.world.spawn(.{Spin{}});
return .{};
}
};
A field's /// comment shows when the pointer rests on its name in the Inspector.
Import std and eggy in the file containing this type. Place hooks inside the plugin,
not beside it. Omit hooks you do not need; fields need defaults when there is no init.
The hook reference lists signatures and dispatch order.
If a component requires another component, declare the dependency:
pub const Wheel = struct {
pub const dependencies = .{eggy.Transform};
radius: f32 = 0.5,
};
Adding Wheel through the Inspector or scene loading also adds Transform. The
Inspector prevents removing a component that another component still needs.
Use typed references for editable links to entities or files:
pub const Orbit = struct {
around: eggy.EntityRef(Body) = .{}, // an entity that has a Body
sound: eggy.AssetRef(.audio) = .{}, // an audio file
};
Here Body is your component type. Resolve entity references before use, as shown in
Meshes and cameras. Asset references
validate a path; they do not load or play a file.
Register systems in initialization
Register ECS systems through app.world in init. Eggy runs Init stages after all
enabled plugins start, then Update stages after each frame's plugin updates.
This independent counter example uses a different Spin definition from the
serializable component above. Put spin at file scope and init inside Plugin:
const ecs = eggy.ecs;
const Spin = struct { turns: u64 = 0 };
fn spin(spinners: ecs.Query(.{*Spin})) !void {
var iter = spinners;
while (iter.next()) |view| view.get(*Spin).turns += 1;
}
pub fn init(gpa: std.mem.Allocator, app: *eggy.App) !Plugin {
_ = gpa;
app.world.addSchedule(ecs.Schedule(.Update, .{spin}));
_ = try app.world.spawn(.{Spin{}});
return .{};
}
Keep owned allocations in plugin fields and release them in deinit. Do not store
pointers to the App, self, or component storage across reloads.
Read editable settings
Declare Preferences inside the plugin with defaults for every field:
pub const Plugin = struct {
pub const Preferences = struct {
volume: f32 = 0.8,
show_gizmos: bool = true,
difficulty: enum { easy, normal, hard } = .normal,
// Optional: a line under each field in the Settings window.
pub const descriptions = .{ .volume = "How loud the game is, 0 to 1." };
// Optional: which fields share a card; fields no group lists come last.
pub const groups = .{ .{ "volume", "difficulty" }, .{"show_gizmos"} };
};
pub fn update(self: *Plugin, app: *eggy.App) !void {
const prefs = app.preferences(Preferences);
if (prefs.show_gizmos) { ... }
}
};
Replace { ... } with your implementation and mark unused hook parameters with _.
Fields may be bools, integers, floats, enums, or strings ([]const u8, without quotes or
backslashes). Edit > Settings shows the controls: a switch, a number field (a name ending in
_seconds or _ms shows that unit), a text field, and for an enum, its choices side by side
when there are four or fewer.
Read settings where you use them to follow changes; values cached in init are refreshed
only when the instance restarts.
Project preference files are embedded in runtime builds. Their names and layering are listed in the configuration reference.
Publish and enable the plugin
In your package's build.zig, publish src/root.zig with addPlugin and pass the
returned module to addGame:
const std = @import("std");
const eggy = @import("eggy");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const plugin = eggy.addPlugin(b, .{
.name = "my_game",
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
eggy.addGame(b, .{
.name = "my_game",
.game = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
}),
.plugins = &.{plugin},
.target = target,
.optimize = optimize,
});
}
Both helpers request the same Eggy dependency arguments. Do not add divergent engine options: the game and plugin must share native engine and ECS types.
A plugin is an ordinary Zig package, so it can use other Zig packages. Fetch one into your
plugin's build.zig.zon, then import its module into the plugin's module. Projects that
install your plugin build it with your build.zig, so they get the dependency too:
zig fetch --save git+https://github.com/someone/noise
const noise = b.dependency("noise", .{ .target = target, .optimize = optimize });
plugin.addImport("noise", noise.module("noise"));
Don't fetch your own copy of Eggy this way: addPlugin already gives the plugin
@import("eggy"), and it must be the game's Eggy.
A library that also works without Eggy can pin its own Vitellus, Weeoui, Clutch or Eggenvector. Inside a game, every module the game and its plugins import gets Eggy's copy of these in place of its own, whatever version it pinned, so their types are the ones Eggy's API uses.
To use another package's published plugin, fetch it and add its module to .plugins.
Replace the example URL and module name with the package you want:
zig fetch --save git+https://github.com/someone/stats
.plugins = &.{ plugin, b.dependency("stats", .{}).module("stats") },
To help only while you make the game, pass a plugin in .dev_plugins instead: the
editor and zig build run-runtime run it, and zig build export leaves it out.
.dev_plugins = &.{b.dependency("level_tools", .{}).module("level_tools")},
Plugins run after the game in list order. A disabled plugin does not run; its component and preference declarations remain available.
Share the plugin
Other projects can install your plugin from the editor's Edit > Plugins... window
when its folder holds an eggy.zon with a .plugin section and, beside it, a README.md
that the window shows before anyone installs it:
.{
.plugin = .{
.title = "Stats",
.module = "stats", // the name you gave addPlugin
.version = "1.2.0", // semver
.description = "Frame times in a corner of the game.",
.repository = "https://github.com/someone/stats",
.fingerprint = 0x1234abcd5678ef90, // .fingerprint in your build.zig.zon
},
}
| Field | Default | Meaning |
|---|---|---|
title | Required | The plugin's name in the Plugins window |
module | Required | The module your package publishes with addPlugin |
version | Required | Semver; a higher version shows as an update |
repository | Required | The https address of the repository hosting the plugin |
fingerprint | Required | Your package's build.zig.zon fingerprint; installs check it |
description | "" | One line under the title |
dev | false | Leave the plugin out of exported games |
readme | "README.md" | The read-me, relative to this file |
package | Worked out | Where to fetch the package, as zig fetch takes it, inside repository |
libraries | .{} | Other modules the package publishes, which games that install the plugin can @import by name |
minimum_eggy_version | None | The oldest Eggy the plugin works with |
People install the plugin by its eggy.zon's raw address, for example
https://raw.githubusercontent.com/someone/stats/v1.2.0/eggy.zon, or by the GitHub page
of its folder. Without .package, the editor fetches your repository at the commit or
tag in that address (or at tag v<version> when the address isn't on GitHub). The
editor refuses an eggy.zon hosted outside repository, and a package whose fingerprint
doesn't match, so keep .fingerprint in step with build.zig.zon and never change it
between versions.
One repository can publish several plugins: give each its own folder with its own
eggy.zon and README.md, publish each module from the repository's build.zig, and
name the same repository and fingerprint in each. Projects fetch the package once and
build each installed plugin's module from it.
Verify
Build and open the project's editor with zig build run. Check that the plugin appears
in Edit > Plugins and that its declared components and settings appear in the
Inspector and Edit > Settings. Press Play to run gameplay initialization and systems;
entities spawned by init are gameplay entities, not saved scene content.
Change plugin code and refocus the editor. It should rebuild the game library and start new plugin state. See Ownership, Play, and reload before retaining IDs or allocating resources in a plugin.
Related reference
- Plugin hooks, Inspector field types, panel keys, and templates
- Project preference files
- Extend the editor for panels, commands, and undoable edits