diff --git a/allegro/allegro.zig b/allegro/allegro.zig index 63cb0f7..5fabd77 100644 --- a/allegro/allegro.zig +++ b/allegro/allegro.zig @@ -9,26 +9,34 @@ pub const Bitmap = struct { destroyBitmap(self); } - pub fn draw(self: Bitmap, x: f32, y: f32, flags: DrawFlags) void { - drawBitmap(self, x, y, flags); + pub fn draw(self: Bitmap, x: f32, y: f32) void { + drawBitmap(self, x, y, DrawFlags{}); } pub fn drawCentered(self: Bitmap, x: f32, y: f32) void { self.draw(x - 0.5 * @intToFloat(f32, self.width()), y - 0.5 * @intToFloat(f32, self.height()), DrawFlags{}); } - pub fn drawCenteredScaled(self: Bitmap, x: f32, y: f32, s: f32) void { + pub fn drawCenteredScaledUniform(self: Bitmap, x: f32, y: f32, s: f32) void { const w = s * @intToFloat(f32, self.width()); const h = s * @intToFloat(f32, self.height()); - self.drawScaled(x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + self.drawScaled(x - 0.5 * w, y - 0.5 * h, w, h); } - pub fn drawScaled(self: Bitmap, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { - drawScaledBitmap(self, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + pub fn drawScaled(self: Bitmap, x: f32, y: f32, w: f32, h: f32) void { + drawScaledBitmap(self, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, DrawFlags{}); } - pub fn drawTinted(self: Bitmap, tint: Color, x: f32, y: f32, flags: DrawFlags) void { - drawTintedBitmap(self, tint, x, y, flags); + pub fn drawScaledUniform(self: Bitmap, x: f32, y: f32, s: f32) void { + const sourceW = @intToFloat(f32, self.width()); + const sourceH = @intToFloat(f32, self.height()); + const scaledW = s * sourceW; + const scaledH = s * sourceH; + drawScaledBitmap(self, 0, 0, sourceW, sourceH, x, y, scaledW, scaledH, DrawFlags{}); + } + + pub fn drawTinted(self: Bitmap, tint: Color, x: f32, y: f32) void { + drawTintedBitmap(self, tint, x, y, DrawFlags{}); } pub fn drawTintedCentered(self: Bitmap, tint: Color, x: f32, y: f32) void { @@ -38,17 +46,21 @@ pub const Bitmap = struct { pub fn drawTintedCenteredScaled(self: Bitmap, tint: Color, x: f32, y: f32, s: f32) void { const w = s * @intToFloat(f32, self.width()); const h = s * @intToFloat(f32, self.height()); - self.drawTintedScaled(tint, x - 0.5 * w, y - 0.5 * h, w, h, DrawFlags{}); + self.drawTintedScaled(tint, x - 0.5 * w, y - 0.5 * h, w, h); } - pub fn drawTintedScaled(self: Bitmap, tint: Color, x: f32, y: f32, w: f32, h: f32, flags: DrawFlags) void { - drawTintedScaledBitmap(self, tint, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, flags); + pub fn drawTintedScaled(self: Bitmap, tint: Color, x: f32, y: f32, w: f32, h: f32) void { + drawTintedScaledBitmap(self, tint, 0, 0, @intToFloat(f32, self.width()), @intToFloat(f32, self.height()), x, y, w, h, DrawFlags{}); } pub fn height(self: Bitmap) i32 { return getBitmapHeight(self); } + pub fn sub(self: Bitmap, x: i32, y: i32, w: i32, h: i32) !Bitmap { + return createSubBitmap(self, x, y, w, h); + } + pub fn width(self: Bitmap) i32 { return getBitmapWidth(self); } @@ -396,6 +408,14 @@ pub fn createEventQueue() !EventQueue { return error.FailedToCreateEventQueue; } +pub fn createSubBitmap(bitmap: Bitmap, x: i32, y: i32, width: i32, height: i32) !Bitmap { + const sub = c.al_create_sub_bitmap(bitmap.native, x, y, width, height); + if (sub) |native| { + return Bitmap{ .native = native }; + } + return error.FailedToCreateSubBitmap; +} + pub fn createTimer(interval: f32) !Timer { const timer = c.al_create_timer(interval); if (timer) |native| { @@ -652,6 +672,14 @@ pub fn registerEventSource(queue: EventQueue, source: EventSource) void { c.al_register_event_source(queue.native, source.native); } +pub fn saveBitmap(path: [*:0]const u8, bitmap: Bitmap) bool { + return c.al_save_bitmap(path, bitmap.native); +} + +pub fn saveBitmapF(file: File, ident: []const u8, bitmap: Bitmap) bool { + return c.al_save_bitmap_f(file.native, &ident[0], bitmap.native); +} + pub fn setDisplayFlag(display: Display, flag: NewDisplayFlags, on: bool) bool { return c.al_set_display_flag(display.native, @bitCast(c_int, flag), on); } diff --git a/src/assets/fonts/Pixellari-OFL.txt b/src/assets/fonts/Pixellari-OFL.txt new file mode 100644 index 0000000..2c6ee48 --- /dev/null +++ b/src/assets/fonts/Pixellari-OFL.txt @@ -0,0 +1,91 @@ +SIL OPEN FONT LICENSE + +Version 1.1 - 26 February 2007 + +PREAMBLE + +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS + +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting - in part or in whole - any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION + +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/fonts/Pixellari.ttf b/src/assets/fonts/Pixellari.ttf new file mode 100644 index 0000000..5a3a3c2 Binary files /dev/null and b/src/assets/fonts/Pixellari.ttf differ diff --git a/src/assets/images/background_jungle.png b/src/assets/images/background_jungle.png new file mode 100644 index 0000000..36790c6 Binary files /dev/null and b/src/assets/images/background_jungle.png differ diff --git a/src/assets/images/character_lion_48.png b/src/assets/images/character_lion_48.png new file mode 100644 index 0000000..fb38584 Binary files /dev/null and b/src/assets/images/character_lion_48.png differ diff --git a/src/assets/images/item_star_32.png b/src/assets/images/item_star_32.png new file mode 100644 index 0000000..9360604 Binary files /dev/null and b/src/assets/images/item_star_32.png differ diff --git a/src/assets/images/text_balloons.png b/src/assets/images/text_balloons.png new file mode 100644 index 0000000..83e00b1 Binary files /dev/null and b/src/assets/images/text_balloons.png differ diff --git a/src/assets/images/tiles_dirt_32.png b/src/assets/images/tiles_dirt_32.png new file mode 100644 index 0000000..71b71f4 Binary files /dev/null and b/src/assets/images/tiles_dirt_32.png differ diff --git a/src/assets/images/tiles_grass_32.png b/src/assets/images/tiles_grass_32.png new file mode 100644 index 0000000..9ff3ce0 Binary files /dev/null and b/src/assets/images/tiles_grass_32.png differ diff --git a/src/assets/levels/level1.txt b/src/assets/levels/level1.txt new file mode 100644 index 0000000..7d09e4e --- /dev/null +++ b/src/assets/levels/level1.txt @@ -0,0 +1,21 @@ +x x +x x +x x +x S S x +x xxxxxx x +x x +x x +x x +x xxxxxx x +x x +x x +x xxxxxx x +x x +x x +x x +xxxxxxxxxx x +x SSSS x +x SSSSS x +x P S S S S x +xxxxxxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + \ No newline at end of file diff --git a/src/context.zig b/src/context.zig index 7d37823..34937e4 100644 --- a/src/context.zig +++ b/src/context.zig @@ -1,26 +1,27 @@ const std = @import("std"); const allegro = @import("allegro"); const engine = @import("engine.zig"); +const paths = @import("paths.zig"); const Palette = @import("palette.zig").Palette; const Scene = @import("scene.zig").Scene; +const Renderer = @import("renderer.zig").Renderer; pub const Context = struct { - const DefaultDisplayWidth = 1280; - const DefaultDisplayHeight = 720; + const DefaultDisplayWidth = 960; + const DefaultDisplayHeight = 540; allocator: std.mem.Allocator, palette: Palette = undefined, shouldQuit: bool = false, - showFPS: bool = false, + showFPS: bool = true, + showDebug: bool = true, scene: ?Scene = null, - display: allegro.Display, events: allegro.EventQueue, + renderer: Renderer, - viewport: engine.Viewport, fps: engine.FPS, - fonts: engine.Fonts, - textures: engine.Textures, + keys: engine.Keys(c_int), pub fn init(allocator: std.mem.Allocator) !Context { if (!allegro.init()) { @@ -41,29 +42,24 @@ pub const Context = struct { if (!allegro.initTtfAddon()) { return error.FailedToInitialize; } + const events = try allegro.createEventQueue(); allegro.setNewDisplayOption(allegro.NewDisplayOption.VSYNC, 1, allegro.OptionImportance.SUGGEST); allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLE_BUFFERS, 1, allegro.OptionImportance.REQUIRE); allegro.setNewDisplayOption(allegro.NewDisplayOption.SAMPLES, 4, allegro.OptionImportance.REQUIRE); allegro.setNewDisplayFlags(allegro.NewDisplayFlags{ .RESIZABLE = true }); - - const viewport = engine.Viewport.init(DefaultDisplayWidth, DefaultDisplayHeight); const display = try allegro.createDisplay(DefaultDisplayWidth, DefaultDisplayHeight); - const events = try allegro.createEventQueue(); - events.registerDisplay(display); events.registerKeyboard(); events.registerMouse(); return Context{ .allocator = allocator, - .display = display, .events = events, - .viewport = viewport, + .renderer = Renderer.init(allocator, display), .fps = engine.FPS{}, - .fonts = engine.Fonts.init(allocator), - .textures = engine.Textures.init(allocator), + .keys = engine.Keys(c_int).init(allocator), }; } @@ -76,21 +72,24 @@ pub const Context = struct { } pub fn deinit(self: *Context) void { - self.events.destroy(); - self.display.destroy(); self.exitScene(); - self.fonts.deinit(); - self.textures.deinit(); + self.renderer.deinit(); + self.events.destroy(); + } + + pub fn registerFonts(self: *Context) !void { + const viewport = self.renderer.viewport; + const fonts = &self.renderer.fonts; + try fonts.addFromFileTTF("default", paths.AssetsDir ++ "/fonts/Pixellari.ttf", viewport.scaledInteger(16)); + try fonts.addFromFileTTF("extra-small", paths.AssetsDir ++ "/fonts/Pixellari.ttf", viewport.scaledInteger(12)); + try fonts.addFromFileTTF("large", paths.AssetsDir ++ "/fonts/Pixellari.ttf", viewport.scaledInteger(32)); + try fonts.addFromFileTTF("debug", paths.AssetsDir ++ "/fonts/Cabin-Regular.ttf", viewport.scaledInteger(16)); } pub fn quit(self: *Context) void { self.shouldQuit = true; } - pub fn resized(self: *Context, width: i32, height: i32) void { - self.viewport.update(width, height); - } - pub fn switchToScene(self: *Context, comptime SceneType: type, build: ?*const fn (*SceneType) void) !void { self.exitScene(); @@ -98,10 +97,4 @@ pub const Context = struct { self.scene = scene; scene.enter(self); } - - pub fn toggleFullScreen(self: *Context) void { - var displayFlags = allegro.getDisplayFlags(self.display); - _ = allegro.setDisplayFlag(self.display, allegro.NewDisplayFlags{ .FULLSCREEN_WINDOW = true }, !displayFlags.FULLSCREEN_WINDOW); - self.viewport.update(self.display.width(), self.display.height()); - } }; diff --git a/src/engine.zig b/src/engine.zig index c2b3520..db51883 100644 --- a/src/engine.zig +++ b/src/engine.zig @@ -2,11 +2,14 @@ const assets = @import("engine/assets.zig"); pub const Fonts = assets.Fonts; pub const FPS = @import("engine/fps.zig").FPS; +pub const Keys = @import("engine/keys.zig").Keys; pub const OpaquePtr = @import("engine/opaque_ptr.zig").OpaquePtr; pub const Point = @import("engine/point.zig").Point; pub const PointF = @import("engine/point_f.zig").PointF; pub const Rectangle = @import("engine/rectangle.zig").Rectangle; pub const RectangleF = @import("engine/rectangle_f.zig").RectangleF; pub const Scene = @import("engine/scene.zig").Scene; +pub const Sprite = assets.Sprite; +pub const Sprites = assets.Sprites; pub const Textures = assets.Textures; pub const Viewport = @import("engine/viewport.zig").Viewport; diff --git a/src/engine/assets.zig b/src/engine/assets.zig index a5d31cf..b7863cf 100644 --- a/src/engine/assets.zig +++ b/src/engine/assets.zig @@ -52,6 +52,9 @@ pub const Fonts = struct { pub fn addFromFileTTF(self: *Fonts, name: []const u8, path: [*:0]const u8, size: i32) !void { const font = try allegro.loadTtfFont(path, size, allegro.LoadTtfFontFlags{}); + if (self.assets.get(name)) |f| { + f.destroy(); + } try self.assets.put(name, font); } @@ -95,3 +98,80 @@ pub const Textures = struct { return self.assets.get(name); } }; + +pub const Sprite = struct { + allocator: std.mem.Allocator, + frames: []allegro.Bitmap, + + pub fn init(allocator: std.mem.Allocator, bitmap: allegro.Bitmap, width: i32, height: i32) !Sprite { + const bitmapWidth = bitmap.width(); + const bitmapHeight = bitmap.height(); + + const horizontal = @divTrunc(bitmapWidth, width); + const vertical = @divTrunc(bitmapHeight, height); + const n = @intCast(usize, horizontal * vertical); + const frames = try allocator.alloc(allegro.Bitmap, n); + var i: i32 = 0; + while (i < n) : (i += 1) { + const left = @mod(i, horizontal) * width; + const top = @divTrunc(i, horizontal) * width; + frames[@intCast(usize, i)] = try bitmap.sub(left, top, width, height); + } + + return Sprite{ + .allocator = allocator, + .frames = frames, + }; + } + + pub fn deinit(self: Sprite) void { + for (self.frames) |f| { + f.destroy(); + } + self.allocator.free(self.frames); + } + + pub fn frame(self: Sprite, i: usize) allegro.Bitmap { + return self.frames[i]; + } +}; + +pub const Sprites = struct { + fn deinitAsset(sprite: Sprite) void { + sprite.deinit(); + } + const Map = Assets(Sprite, deinitAsset); + + allocator: std.mem.Allocator, + assets: Map, + + pub fn init(allocator: std.mem.Allocator) Sprites { + return Sprites{ + .allocator = allocator, + .assets = Map.init(allocator), + }; + } + + pub fn deinit(self: *Sprites) void { + self.assets.deinit(); + } + + pub fn add(self: *Sprites, name: []const u8, bitmap: allegro.Bitmap, width: i32, height: i32) !void { + try self.assets.put(name, try Sprite.init(self.allocator, bitmap, width, height)); + } + + pub fn addFromTextures(self: *Sprites, textures: Textures, name: []const u8, width: i32, height: i32) !void { + try self.add(name, textures.get(name).?, width, height); + } + + pub fn get(self: Sprites, name: []const u8) ?Sprite { + return self.assets.get(name); + } + + pub fn getFrame(self: Sprites, name: []const u8, i: usize) ?allegro.Bitmap { + if (self.assets.get(name)) |sprite| { + return sprite.frame(i); + } + return null; + } +}; diff --git a/src/engine/fps.zig b/src/engine/fps.zig index ece6f0e..8e5b5ef 100644 --- a/src/engine/fps.zig +++ b/src/engine/fps.zig @@ -20,6 +20,8 @@ pub const FPS = struct { } pub fn fps(self: FPS) usize { + if (self.framesCounted == 0) return 0; + return @floatToInt(usize, @intToFloat(f32, self.framesCounted) / self.total); } }; diff --git a/src/engine/keys.zig b/src/engine/keys.zig new file mode 100644 index 0000000..069eb3d --- /dev/null +++ b/src/engine/keys.zig @@ -0,0 +1,34 @@ +const std = @import("std"); + +pub fn Keys(comptime KeyType: type) type { + return struct { + const Self = @This(); + + pressed: std.AutoHashMap(KeyType, bool), + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .pressed = std.AutoHashMap(KeyType, bool).init(allocator), + }; + } + + pub fn deinit(self: *Self) void { + self.pressed.deinit(); + } + + pub fn isKeyPressed(self: Self, key: KeyType) bool { + if (self.pressed.get(key)) |pressed| { + return pressed; + } + return false; + } + + pub fn press(self: *Self, key: KeyType) !void { + try self.pressed.put(key, true); + } + + pub fn release(self: *Self, key: KeyType) !void { + try self.pressed.put(key, false); + } + }; +} diff --git a/src/engine/point.zig b/src/engine/point.zig index 11944af..6991f34 100644 --- a/src/engine/point.zig +++ b/src/engine/point.zig @@ -1,9 +1,12 @@ +const std = @import("std"); const PointF = @import("point_f.zig").PointF; pub const Point = struct { x: i64, y: i64, + pub const Zero = Point{ .x = 0, .y = 0 }; + pub fn init(x: i64, y: i64) Point { return Point{ .x = x, .y = y }; } @@ -16,6 +19,16 @@ pub const Point = struct { return Point{ .x = self.x + other.x, .y = self.y + other.y }; } + pub fn distance(self: Point, to: Point) i64 { + return std.math.sqrt(self.distance2(to)); + } + + pub fn distance2(self: Point, to: Point) i64 { + const dx = to.x - self.x; + const dy = to.y - self.y; + return dx * dx + dy * dy; + } + pub fn float(self: Point) PointF { return PointF{ .x = @intToFloat(f32, self.x), .y = @intToFloat(f32, self.y) }; } diff --git a/src/engine/point_f.zig b/src/engine/point_f.zig index 2746fdd..baf23a9 100644 --- a/src/engine/point_f.zig +++ b/src/engine/point_f.zig @@ -6,6 +6,8 @@ pub const PointF = struct { x: f32, y: f32, + pub const Zero = Point{ .x = 0, .y = 0 }; + pub fn init(x: f32, y: f32) PointF { return PointF{ .x = x, .y = y }; } @@ -18,6 +20,16 @@ pub const PointF = struct { return PointF{ .x = std.math.ceil(self.x), .y = std.math.ceil(self.y) }; } + pub fn distance(self: PointF, to: PointF) f32 { + return std.math.sqrt(self.distance2(to)); + } + + pub fn distance2(self: PointF, to: PointF) f32 { + const dx = to.x - self.x; + const dy = to.y - self.y; + return dx * dx + dy * dy; + } + pub fn floor(self: PointF) PointF { return PointF{ .x = std.math.floor(self.x), .y = std.math.floor(self.y) }; } @@ -30,6 +42,10 @@ pub const PointF = struct { return PointF{ .x = self.x * factor, .y = self.y * factor }; } + pub fn round(self: PointF) PointF { + return PointF{ .x = std.math.round(self.x), .y = std.math.round(self.y) }; + } + pub fn subtract(self: PointF, other: PointF) PointF { return PointF{ .x = self.x - other.x, .y = self.y - other.y }; } diff --git a/src/engine/rectangle.zig b/src/engine/rectangle.zig index ef38cee..60e5ae0 100644 --- a/src/engine/rectangle.zig +++ b/src/engine/rectangle.zig @@ -1,4 +1,5 @@ const Point = @import("point.zig").Point; +const RectangleF = @import("rectangle_f.zig").RectangleF; pub const Rectangle = struct { min: Point, @@ -65,6 +66,10 @@ pub const Rectangle = struct { return Point.init(@divTrunc(self.min.x + self.max.x, 2), @divTrunc(self.min.y + self.max.y, 2)); } + pub fn float(self: Rectangle) RectangleF { + return RectangleF{ .min = self.min.float(), .max = self.max.float() }; + } + pub fn height(self: Rectangle) i64 { return self.max.y - self.min.y; } diff --git a/src/engine/rectangle_f.zig b/src/engine/rectangle_f.zig index 6dd3abe..6ae4166 100644 --- a/src/engine/rectangle_f.zig +++ b/src/engine/rectangle_f.zig @@ -1,4 +1,5 @@ const PointF = @import("point_f.zig").PointF; +const Rectangle = @import("rectangle.zig").Rectangle; pub const RectangleF = struct { min: PointF, @@ -69,6 +70,10 @@ pub const RectangleF = struct { return self.max.y - self.min.y; } + pub fn integer(self: RectangleF) Rectangle { + return Rectangle{ .min = self.min.integer(), .max = self.max.integer() }; + } + pub fn width(self: RectangleF) f32 { return self.max.x - self.min.x; } diff --git a/src/engine/scene.zig b/src/engine/scene.zig index ea026e3..7dcb8b8 100644 --- a/src/engine/scene.zig +++ b/src/engine/scene.zig @@ -13,7 +13,7 @@ pub fn Scene(comptime Context: type, comptime Event: type) type { enter: *const fn (Self, *Context) void, exit: *const fn (Self, *Context) void, handle: *const fn (Self, *Context, Event) anyerror!void, - update: *const fn (Self, *Context, f32) void, + tick: *const fn (Self, *Context, f32, f32) void, render: *const fn (Self, *Context) void, }; @@ -29,14 +29,14 @@ pub fn Scene(comptime Context: type, comptime Event: type) type { try self.virtualTable.handle(self, ctx, event); } - pub fn update(self: Self, ctx: *Context, dt: f32) void { - self.virtualTable.update(self, ctx, dt); - } - pub fn render(self: Self, ctx: *Context) void { self.virtualTable.render(self, ctx); } + pub fn tick(self: Self, ctx: *Context, t: f32, dt: f32) void { + self.virtualTable.tick(self, ctx, t, dt); + } + pub fn makeOpaque(comptime SceneType: type, object: *SceneType) Self { return Self{ .object = @ptrToInt(object), .virtualTable = .{ .enter = struct { @@ -57,12 +57,12 @@ pub fn Scene(comptime Context: type, comptime Event: type) type { try scene.handle(ctx, event); } }.handle, - .update = struct { - fn update(self: Self, ctx: *Context, dt: f32) void { + .tick = struct { + fn tick(self: Self, ctx: *Context, t: f32, dt: f32) void { const scene = @intToPtr(*SceneType, self.object); - scene.update(ctx, dt); + scene.tick(ctx, t, dt); } - }.update, + }.tick, .render = struct { fn render(self: Self, ctx: *Context) void { const scene = @intToPtr(*SceneType, self.object); diff --git a/src/engine/viewport.zig b/src/engine/viewport.zig index c53e34d..cafda5a 100644 --- a/src/engine/viewport.zig +++ b/src/engine/viewport.zig @@ -1,3 +1,4 @@ +const std = @import("std"); const PointF = @import("point_f.zig").PointF; const RectangleF = @import("rectangle_f.zig").RectangleF; @@ -30,6 +31,20 @@ pub const Viewport = struct { return self.bounds.center(); } + pub fn scaledInteger(self: Viewport, i: i32) i32 { + return @floatToInt(i32, std.math.round(@intToFloat(f32, i) * self.scale)); + } + + pub fn viewToScreen(self: Viewport, x: f32, y: f32) PointF { + const screenX = self.bounds.min.x + x * self.bounds.width(); + const screenY = self.bounds.min.y + y * self.bounds.height(); + return PointF.init(screenX, screenY); + } + + pub fn viewToScreenP(self: Viewport, position: PointF) PointF { + return self.viewToScreen(position.x, position.y); + } + pub fn update(self: *Viewport, width: i32, height: i32) void { self.actualWidth = width; self.actualHeight = height; diff --git a/src/game.zig b/src/game.zig new file mode 100644 index 0000000..3557b9e --- /dev/null +++ b/src/game.zig @@ -0,0 +1,3 @@ +pub const Animation = @import("game/animation.zig").Animation; +pub const Game = @import("game/game.zig").Game; +pub const Level = @import("game/level.zig").Level; diff --git a/src/game/animation.zig b/src/game/animation.zig new file mode 100644 index 0000000..95f844a --- /dev/null +++ b/src/game/animation.zig @@ -0,0 +1,46 @@ +const allegro = @import("allegro"); +const engine = @import("../engine.zig"); + +pub const Animation = struct { + sprite: engine.Sprite, + current: usize, + begin: usize, + end: usize, + interval: f32, + lastUpdate: f32, + + pub fn init(sprite: engine.Sprite, interval: f32) Animation { + return Animation.initPartialLoop(sprite, interval, 0, sprite.frames.len); + } + + pub fn initPartialLoop(sprite: engine.Sprite, interval: f32, begin: usize, end: usize) Animation { + return Animation{ + .sprite = sprite, + .current = begin, + .begin = begin, + .end = end, + .interval = interval, + .lastUpdate = 0, + }; + } + + pub fn currentFrame(self: *Animation) allegro.Bitmap { + return self.sprite.frames[self.current]; + } + + pub fn reset(self: *Animation) void { + self.current = self.begin; + } + + pub fn tick(self: *Animation, t: f32) void { + const delta = t - self.lastUpdate; + + const skip = @floatToInt(usize, @divFloor(delta, self.interval)); + self.lastUpdate = t - @rem(delta, self.interval); + + self.current = (self.current + skip); + while (self.current >= self.end) { + self.current = self.begin; + } + } +}; diff --git a/src/game/game.zig b/src/game/game.zig new file mode 100644 index 0000000..331d93d --- /dev/null +++ b/src/game/game.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const engine = @import("../engine.zig"); +const Animation = @import("animation.zig").Animation; +const Renderer = @import("../renderer.zig").Renderer; +const Level = @import("level.zig").Level; + +pub const Game = struct { + pub const Direction = enum { + Left, + Right, + }; + + level: Level, + health: i64 = 4, + playerPosition: engine.PointF, + playerIdleAnimation: Animation, + playerWalkingAnimation: Animation, + playerDirection: Direction, + isPlayerWalking: bool, + + renderer: *Renderer, + // current viewport translated to tiles. + boundsInTiles: engine.RectangleF, + + pub fn init(level: Level, renderer: *Renderer) Game { + const playerPosition = level.character.float(); + return Game{ + .level = level, + .playerPosition = playerPosition, + .playerIdleAnimation = Animation.initPartialLoop(renderer.sprites.get("character_lion_48").?, 0.25, 0, 4), + .playerWalkingAnimation = Animation.initPartialLoop(renderer.sprites.get("character_lion_48").?, 0.125, 4, 8), + .playerDirection = Direction.Right, + .isPlayerWalking = false, + .renderer = renderer, + .boundsInTiles = Game.calculateBoundsInTiles(playerPosition), + }; + } + + fn calculateBoundsInTiles(position: engine.PointF) engine.RectangleF { + return engine.RectangleF.initRelative( + position.x - 15, + @min(position.y - 8.875, 4.125), + 30, + 16.875, + ); + } + + pub fn drawSpriteFrame(self: *Game, spriteName: []const u8, frame: usize, x: i64, y: i64) void { + self.drawSpriteFrameP(spriteName, frame, engine.Point.init(x, y).float()); + } + + pub fn drawSpriteFrameP(self: *Game, spriteName: []const u8, frame: usize, position: engine.PointF) void { + const view = self.tileP(position); + self.renderer.drawSpriteFrameV(spriteName, frame, view.x, view.y); + } + + pub fn moveCharacter(self: *Game, distance: f32) void { + self.playerPosition.x += distance; + self.isPlayerWalking = true; + self.playerDirection = if (distance > 0) Direction.Right else Direction.Left; + + self.boundsInTiles = Game.calculateBoundsInTiles(self.playerPosition); + } + + // return values are in view coordinates + pub fn tile(self: Game, x: f32, y: f32) engine.PointF { + return self.tileP(engine.PointF.init(x, y)); + } + + pub fn tileCenter(self: Game, x: i64, y: i64) engine.PointF { + return self.tileCenterP(engine.Point.init(x, y)); + } + + pub fn tileCenterP(self: Game, position: engine.Point) engine.PointF { + const center = engine.PointF.init(0.5, 0.5); + return self.tileP(position.float().add(center)); + } + + pub fn tileP(self: Game, position: engine.PointF) engine.PointF { + const relative = position.subtract(self.boundsInTiles.min); + return engine.PointF.init(relative.x / self.boundsInTiles.width(), relative.y / self.boundsInTiles.height()); + } + + pub fn tileTopLeft(self: Game, x: i64, y: i64) engine.PointF { + return self.tileTopLeftP(engine.Point.init(x, y)); + } + + pub fn tileTopLeftP(self: Game, p: engine.Point) engine.PointF { + return self.tileP(p.float()); + } + + pub fn tilesInView(self: Game) engine.Rectangle { + // tiles that are partially or completely in view. + return engine.RectangleF.initAbsolute( + std.math.floor(self.boundsInTiles.min.x), + std.math.floor(self.boundsInTiles.min.y), + std.math.floor(self.boundsInTiles.max.x) + 1, + std.math.floor(self.boundsInTiles.max.y) + 1, + ).integer(); + } + + pub fn tick(self: *Game, t: f32, dt: f32) void { + _ = dt; + self.playerAnimation.tick(t); + } +}; diff --git a/src/game/level.zig b/src/game/level.zig new file mode 100644 index 0000000..b6c688f --- /dev/null +++ b/src/game/level.zig @@ -0,0 +1,127 @@ +const std = @import("std"); +const engine = @import("../engine.zig"); + +pub const Level = struct { + pub fn Column(comptime Value: type) type { + return struct { + const Self = @This(); + + values: std.AutoHashMap(i64, Value), + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .values = std.AutoHashMap(i64, Value).init(allocator), + }; + } + + pub fn deinit(self: Self) void { + self.values.deinit(); + } + + pub fn get(self: Self, row: i64) ?Value { + return self.values.get(row); + } + + pub fn set(self: *Self, row: i64, value: Value) !void { + try self.values.put(row, value); + } + }; + } + + pub fn Components(comptime Value: type) type { + return struct { + const Self = @This(); + + allocator: std.mem.Allocator, + columns: std.ArrayList(Column(Value)), + + pub fn init(allocator: std.mem.Allocator) Self { + return Self{ + .allocator = allocator, + .columns = std.ArrayList(Column(Value)).init(allocator), + }; + } + + pub fn deinit(self: Self) void { + for (self.columns.items) |item| { + item.deinit(); + } + self.columns.deinit(); + } + + pub fn column(self: Self, i: i64) *Column(Value) { + return &self.columns.items[@intCast(usize, i)]; + } + + fn ensureColumns(self: *Self, n: usize) !void { + while (self.columns.items.len < n) { + try self.columns.append(Column(Value).init(self.allocator)); + } + } + + pub fn set(self: *Self, c: i64, r: i64, value: Value) !void { + try self.ensureColumns(@intCast(usize, c) + 1); + try self.column(c).set(r, value); + } + }; + } + + pub const Tile = enum { + Grass, + }; + + pub const Collectable = enum { + Star, + }; + + character: engine.Point, + tiles: Components(Tile), + collectables: Components(Collectable), + + pub fn load(allocator: std.mem.Allocator, path: []const u8) !Level { + const path_ = try std.fs.realpathAlloc(allocator, path); + defer allocator.free(path_); + + const file = try std.fs.openFileAbsolute(path_, .{ .mode = .read_only }); + defer file.close(); + + const data = try file.readToEndAlloc(allocator, 1024 * 1024); + defer allocator.free(data); + + return try Level.loadFromMemory(allocator, data); + } + + pub fn loadFromMemory(allocator: std.mem.Allocator, data: []const u8) !Level { + const n = std.mem.count(u8, data, "\n"); + _ = n; + + var character: ?engine.Point = null; + var tiles = Components(Tile).init(allocator); + var collectables = Components(Collectable).init(allocator); + + var lines = std.mem.split(u8, data, "\n"); + var y: i64 = 0; + while (lines.next()) |line| { + defer y += 1; + + try tiles.ensureColumns(line.len); + try collectables.ensureColumns(line.len); + + for (line, 0..) |tile, column| { + var x = @intCast(i64, column); + switch (tile) { + 'P' => character = engine.Point.init(x, y), + 'S' => try collectables.set(x, y, Collectable.Star), + 'x' => try tiles.set(x, y, Tile.Grass), + else => {}, + } + } + } + + return Level{ + .character = character.?, + .tiles = tiles, + .collectables = collectables, + }; + } +}; diff --git a/src/game_scene.zig b/src/game_scene.zig new file mode 100644 index 0000000..73255e0 --- /dev/null +++ b/src/game_scene.zig @@ -0,0 +1,117 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const game = @import("game.zig"); +const paths = @import("paths.zig"); +const Context = @import("context.zig").Context; +const Renderer = @import("renderer.zig").Renderer; + +pub const GameScene = struct { + game: game.Game = undefined, + + pub fn enter(self: *GameScene, ctx: *Context) void { + const level = game.Level.load(ctx.allocator, paths.AssetsDir ++ "/levels/level1.txt") catch unreachable; + self.game = game.Game.init(level, &ctx.renderer); + } + + pub fn exit(self: *GameScene, ctx: *Context) void { + _ = ctx; + _ = self; + } + + pub fn handle(self: *GameScene, ctx: *Context, event: *allegro.Event) !void { + _ = event; + _ = ctx; + _ = self; + } + + pub fn tick(self: *GameScene, ctx: *Context, t: f32, dt: f32) void { + const speed: f32 = 5; // tiles/s + + if (ctx.keys.isKeyPressed(allegro.c.ALLEGRO_KEY_LEFT)) { + self.game.moveCharacter(-speed * dt); + self.game.playerWalkingAnimation.tick(t); + } else if (ctx.keys.isKeyPressed(allegro.c.ALLEGRO_KEY_RIGHT)) { + self.game.moveCharacter(speed * dt); + self.game.playerWalkingAnimation.tick(t); + } else { + if (self.game.isPlayerWalking) { + self.game.playerWalkingAnimation.reset(); + } + self.game.isPlayerWalking = false; + self.game.playerIdleAnimation.tick(t); + } + } + + pub fn render(self: *GameScene, ctx: *Context) void { + allegro.clearToColor(ctx.palette.background.background); + + const renderer = ctx.renderer; + const viewport = renderer.viewport; + + const center = viewport.center(); + _ = center; + const bounds = viewport.bounds; + const scale = viewport.scale; + _ = scale; + const background = renderer.textures.get("background_jungle").?; + const backgroundDisplacementFactor = -0.01; + + const backgroundCenter = self.game.playerPosition.x * backgroundDisplacementFactor; + const backgroundLeft = @mod(backgroundCenter, 1.0) - 1; + + background.drawScaled(bounds.min.x + backgroundLeft * bounds.width(), bounds.min.y, bounds.width(), bounds.height()); + background.drawScaled(bounds.min.x + (backgroundLeft + 1) * bounds.width(), bounds.min.y, bounds.width(), bounds.height()); + + const tileBounds = self.game.tilesInView(); + + var x = tileBounds.min.x; + while (x < tileBounds.max.x) : (x += 1) { + self.game.drawSpriteFrame("tiles_dirt_32", 0, x, 19); + self.game.drawSpriteFrame("tiles_dirt_32", 3, x, 20); + + if (x < 0) continue; + + const tiles = self.game.level.tiles.column(x); + const collectables = self.game.level.collectables.column(x); + var y = tileBounds.min.y; + while (y < tileBounds.max.y) : (y += 1) { + if (tiles.get(y)) |tile| { + switch (tile) { + game.Level.Tile.Grass => { + self.game.drawSpriteFrame("tiles_grass_32", 1, x, y - 1); + self.game.drawSpriteFrame("tiles_grass_32", 5, x, y); + }, + // else => {}, + } + } + + if (collectables.get(y)) |collectable| { + switch (collectable) { + game.Level.Collectable.Star => { + const distanceToPlayer = engine.Point.init(x, y).float().distance(self.game.playerPosition); + self.game.drawSpriteFrame("item_star_32", @floatToInt(usize, @mod(distanceToPlayer * 0.54, 1) * 16), x, y); + }, + } + } + } + } + + const playerDirectionFrameOffset: usize = if (self.game.playerDirection == game.Game.Direction.Left) 0 else 8; + if (self.game.isPlayerWalking) { + self.game.drawSpriteFrameP("character_lion_48", self.game.playerWalkingAnimation.current + playerDirectionFrameOffset, self.game.playerPosition.add(engine.PointF.init(-0.25, -0.25))); + } else { + self.game.drawSpriteFrameP("character_lion_48", self.game.playerIdleAnimation.current + playerDirectionFrameOffset, self.game.playerPosition.add(engine.PointF.init(-0.25, -0.25))); + } + + if (ctx.showDebug) { + ctx.renderer.printTextV("debug", ctx.palette.background.text, 0.01, 0.1, Renderer.TextAlignment.Left, "Character: ({d}, {d})", .{ self.game.playerPosition.x, self.game.playerPosition.y }); + ctx.renderer.printTextV("debug", ctx.palette.background.text, 0.01, 0.15, Renderer.TextAlignment.Left, "Tiles: ({d}, {d}) -> ({d}, {d})", .{ tileBounds.min.x, tileBounds.min.y, tileBounds.max.x, tileBounds.max.y }); + + // const characterTopLeft = self.game.tileP(self.game.playerPosition); + // const characterTopLeftScreen = renderer.viewport.viewToScreenP(characterTopLeft); + // const tileWidthScreen = 32 * viewport.scale; + // renderer.textures.get("opaque").?.drawTintedScaled(allegro.mapRgba(127, 127, 127, 127), characterTopLeftScreen.x, characterTopLeftScreen.y, tileWidthScreen, tileWidthScreen); + } + } +}; diff --git a/src/main.zig b/src/main.zig index 54323f1..384f0ad 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,17 +1,14 @@ const std = @import("std"); const allegro = @import("allegro"); const engine = @import("engine.zig"); +const game = @import("game.zig"); +const paths = @import("paths.zig"); const Context = @import("context.zig").Context; +const GameScene = @import("game_scene.zig").GameScene; const Palette = @import("palette.zig").Palette; +const Renderer = @import("renderer.zig").Renderer; const TitleScene = @import("title_scene.zig").TitleScene; -fn currentFile() []const u8 { - return @src().file; -} - -const sourceDir = std.fs.path.dirname(currentFile()).?; -const assetsDir = sourceDir ++ "/assets"; - fn hexColor(hex: []const u8) allegro.Color { return allegro.Color.initFromHex(hex); } @@ -38,41 +35,69 @@ pub fn main() !void { .background = .{ .background = hexColor("#fffbff"), .text = hexColor("#2b0052") }, .outline = hexColor("#7c757e"), }; + const renderer = &context.renderer; allegro.setNewBitmapFlags(allegro.NewBitmapFlags{ .MIN_LINEAR = true, .MAG_LINEAR = true }); - try context.textures.addFromFile("opaque", assetsDir ++ "/images/opaque.png"); - try context.textures.addFromFile("title_untitled", assetsDir ++ "/images/title_untitled.png"); + try renderer.textures.addFromFile("opaque", paths.AssetsDir ++ "/images/opaque.png"); + try renderer.textures.addFromFile("title_untitled", paths.AssetsDir ++ "/images/title_untitled.png"); + try renderer.textures.addFromFile("background_jungle", paths.AssetsDir ++ "/images/background_jungle.png"); + try renderer.textures.addFromFile("character_lion_48", paths.AssetsDir ++ "/images/character_lion_48.png"); + try renderer.textures.addFromFile("item_star_32", paths.AssetsDir ++ "/images/item_star_32.png"); + try renderer.textures.addFromFile("text_balloons", paths.AssetsDir ++ "/images/text_balloons.png"); + try renderer.textures.addFromFile("tiles_dirt_32", paths.AssetsDir ++ "/images/tiles_dirt_32.png"); + try renderer.textures.addFromFile("tiles_grass_32", paths.AssetsDir ++ "/images/tiles_grass_32.png"); + + try renderer.sprites.addFromTextures(renderer.textures, "character_lion_48", 48, 48); + try renderer.sprites.addFromTextures(renderer.textures, "item_star_32", 32, 32); + try renderer.sprites.addFromTextures(renderer.textures, "tiles_dirt_32", 32, 32); + try renderer.sprites.addFromTextures(renderer.textures, "tiles_grass_32", 32, 32); + allegro.convertMemoryBitmaps(); - try context.fonts.addFromFileTTF("sub", assetsDir ++ "/fonts/Cabin-Regular.ttf", 16); - try context.fonts.addFromFileTTF("default", assetsDir ++ "/fonts/Cabin-Regular.ttf", 32); + try context.registerFonts(); - try context.switchToScene(TitleScene, null); + try context.switchToScene(GameScene, null); var t = allegro.getTime(); var fpsBuffer = [_]u8{0} ** 32; + _ = fpsBuffer; while (!context.shouldQuit) { const scene = if (context.scene) |scene| scene else { break; }; + const newT = allegro.getTime(); + const deltaT = @floatCast(f32, newT - t); + t = newT; + context.fps.update(deltaT); + scene.tick(&context, @floatCast(f32, t), deltaT); + while (!context.events.isEmpty()) { var event: allegro.Event = undefined; _ = context.events.get(&event); switch (event.type) { allegro.c.ALLEGRO_EVENT_DISPLAY_CLOSE => context.quit(), allegro.c.ALLEGRO_EVENT_DISPLAY_RESIZE => { - context.resized(event.display.width, event.display.height); + context.renderer.resized(event.display.width, event.display.height); + try context.registerFonts(); _ = allegro.acknowledgeResize(allegro.Display{ .native = event.display.source.? }); }, allegro.c.ALLEGRO_EVENT_KEY_CHAR => { switch (event.keyboard.keycode) { allegro.c.ALLEGRO_KEY_ESCAPE => context.quit(), allegro.c.ALLEGRO_KEY_F9 => context.showFPS = !context.showFPS, - allegro.c.ALLEGRO_KEY_F11 => context.toggleFullScreen(), + allegro.c.ALLEGRO_KEY_F10 => context.showDebug = !context.showDebug, + allegro.c.ALLEGRO_KEY_F11 => renderer.toggleFullScreen(), + allegro.c.ALLEGRO_KEY_F12 => try renderer.takeScreenshot(), else => {}, } }, + allegro.c.ALLEGRO_EVENT_KEY_DOWN => { + try context.keys.press(event.keyboard.keycode); + }, + allegro.c.ALLEGRO_EVENT_KEY_UP => { + try context.keys.release(event.keyboard.keycode); + }, else => {}, } if (context.shouldQuit) { @@ -81,18 +106,9 @@ pub fn main() !void { try scene.handle(&context, &event); } - const newT = allegro.getTime(); - const deltaT = @floatCast(f32, newT - t); - t = newT; - scene.update(&context, deltaT); - context.fps.update(deltaT); - scene.render(&context); if (context.showFPS) { - if (context.fonts.get("default")) |font| { - const fps = try std.fmt.bufPrintZ(fpsBuffer[0..], "FPS: {}", .{context.fps.fps()}); - allegro.drawText(font, context.palette.background.text, 10, 10, allegro.DrawTextFlags.ALIGN_LEFT, fps); - } + renderer.printTextV("debug", context.palette.background.text, 0.01, 0.01, Renderer.TextAlignment.Left, "FPS: {}", .{context.fps.fps()}); } allegro.flipDisplay(); diff --git a/src/main_menu_scene.zig b/src/main_menu_scene.zig index a126546..6b3e09e 100644 --- a/src/main_menu_scene.zig +++ b/src/main_menu_scene.zig @@ -1,4 +1,5 @@ const allegro = @import("allegro"); +const game = @import("game.zig"); const Context = @import("context.zig").Context; pub const MainMenuScene = struct { @@ -31,6 +32,7 @@ pub const MainMenuScene = struct { const center = ctx.viewport.center(); ctx.textures.get("title_untitled").?.drawTintedCenteredScaled(ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 100, 0.5 * ctx.viewport.scale); + ctx.renderer.drawTextV("default", ctx.palette.background.text, 0, 0.3, game.Renderer.TextAlignment.Center, "Play game"); allegro.drawText(ctx.fonts.get("default").?, ctx.palette.background.text, center.x, ctx.viewport.bounds.min.y + ctx.viewport.scale * 200, allegro.DrawTextFlags.ALIGN_CENTER, "Play game"); } }; diff --git a/src/paths.zig b/src/paths.zig new file mode 100644 index 0000000..3b75a57 --- /dev/null +++ b/src/paths.zig @@ -0,0 +1,8 @@ +const std = @import("std"); + +fn currentFile() []const u8 { + return @src().file; +} + +pub const SourceDir = std.fs.path.dirname(currentFile()).?; +pub const AssetsDir = SourceDir ++ "/assets"; diff --git a/src/renderer.zig b/src/renderer.zig new file mode 100644 index 0000000..9a1ecfe --- /dev/null +++ b/src/renderer.zig @@ -0,0 +1,96 @@ +const std = @import("std"); +const allegro = @import("allegro"); +const engine = @import("engine.zig"); +const Context = @import("context.zig").Context; + +pub const Renderer = struct { + textFormattingBuffer: [1024]u8 = [_]u8{0} ** 1024, + + display: allegro.Display, + viewport: engine.Viewport, + fonts: engine.Fonts, + textures: engine.Textures, + sprites: engine.Sprites, + + pub const TextAlignment = enum { + Center, + Left, + Right, + + pub fn toDrawTextFlags(self: TextAlignment) allegro.DrawTextFlags { + return switch (self) { + .Center => allegro.DrawTextFlags.ALIGN_CENTER, + .Left => allegro.DrawTextFlags.ALIGN_LEFT, + .Right => allegro.DrawTextFlags.ALIGN_RIGHT, + }; + } + }; + + pub fn init(allocator: std.mem.Allocator, display: allegro.Display) Renderer { + const viewport = engine.Viewport.init(display.width(), display.height()); + + return Renderer{ + .display = display, + .viewport = viewport, + .fonts = engine.Fonts.init(allocator), + .textures = engine.Textures.init(allocator), + .sprites = engine.Sprites.init(allocator), + }; + } + + pub fn deinit(self: *Renderer) void { + self.fonts.deinit(); + self.textures.deinit(); + self.sprites.deinit(); + self.display.destroy(); + } + + pub fn drawText(self: Renderer, fontName: []const u8, color: allegro.Color, x: f32, y: f32, alignment: TextAlignment, text: [*:0]const u8) void { + if (self.fonts.get(fontName)) |font| { + font.draw(color, x, y, alignment.toDrawTextFlags(), text); + } + } + + pub fn drawSpriteFrame(self: Renderer, spriteName: []const u8, frame: usize, x: f32, y: f32) void { + if (self.sprites.getFrame(spriteName, frame)) |sprite| { + sprite.drawScaledUniform(x, y, self.viewport.scale); + } + } + + pub fn drawSpriteFrameV(self: Renderer, spriteName: []const u8, frame: usize, x: f32, y: f32) void { + const screen = self.viewport.viewToScreen(x, y); + self.drawSpriteFrame(spriteName, frame, screen.x, screen.y); + } + + pub fn drawTextV(self: Renderer, fontName: []const u8, color: allegro.Color, x: f32, y: f32, alignment: TextAlignment, text: [*:0]const u8) void { + const screen = self.viewport.viewToScreen(x, y); + self.drawText(fontName, color, screen.x, screen.y, alignment, text); + } + + pub fn printText(self: *Renderer, font: []const u8, color: allegro.Color, x: f32, y: f32, alignment: TextAlignment, comptime fmt: []const u8, args: anytype) void { + const text = std.fmt.bufPrintZ(&self.textFormattingBuffer, fmt, args) catch { + return; + }; + self.drawText(font, color, x, y, alignment, text); + } + + pub fn printTextV(self: *Renderer, font: []const u8, color: allegro.Color, x: f32, y: f32, alignment: TextAlignment, comptime fmt: []const u8, args: anytype) void { + const screen = self.viewport.viewToScreen(x, y).round(); + self.printText(font, color, screen.x, screen.y, alignment, fmt, args); + } + + pub fn resized(self: *Renderer, width: i32, height: i32) void { + self.viewport.update(width, height); + } + + pub fn takeScreenshot(self: Renderer) !void { + const screen = try allegro.getBackbuffer(self.display); + _ = allegro.saveBitmap("screenshot.png", screen); + } + + pub fn toggleFullScreen(self: *Renderer) void { + var displayFlags = allegro.getDisplayFlags(self.display); + _ = allegro.setDisplayFlag(self.display, allegro.NewDisplayFlags{ .FULLSCREEN_WINDOW = true }, !displayFlags.FULLSCREEN_WINDOW); + self.viewport.update(self.display.width(), self.display.height()); + } +}; diff --git a/src/title_scene.zig b/src/title_scene.zig index a1fadda..8c7a7ff 100644 --- a/src/title_scene.zig +++ b/src/title_scene.zig @@ -28,7 +28,8 @@ pub const TitleScene = struct { _ = self; } - pub fn update(self: *TitleScene, ctx: *Context, dt: f32) void { + pub fn tick(self: *TitleScene, ctx: *Context, t: f32, dt: f32) void { + _ = t; _ = self; _ = dt; _ = ctx;