Progress over >1 day.

- Added debug overlay.
- Added game scene.
  * Rendering of background, tiles, items & player.
  * Added levels (reading from text file).
  * No collision detection yet.
- Automatic resizing of fonts.
- Added sprites (animate textures).
- Lots of utility methods everywhere....
This commit is contained in:
Sander Schobers 2023-06-04 17:05:20 +02:00
parent 0fb3650a9a
commit 7a89b05f8e
31 changed files with 902 additions and 74 deletions

View File

@ -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);
}

View File

@ -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.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -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

View File

@ -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());
}
};

View File

@ -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;

View File

@ -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;
}
};

View File

@ -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);
}
};

34
src/engine/keys.zig Normal file
View File

@ -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);
}
};
}

View File

@ -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) };
}

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

3
src/game.zig Normal file
View File

@ -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;

46
src/game/animation.zig Normal file
View File

@ -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;
}
}
};

106
src/game/game.zig Normal file
View File

@ -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);
}
};

127
src/game/level.zig Normal file
View File

@ -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,
};
}
};

117
src/game_scene.zig Normal file
View File

@ -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);
}
}
};

View File

@ -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();

View File

@ -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");
}
};

8
src/paths.zig Normal file
View File

@ -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";

96
src/renderer.zig Normal file
View File

@ -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());
}
};

View File

@ -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;