diff --git a/src/assets.zig b/src/assets.zig index 9d07621..e53a2e8 100644 --- a/src/assets.zig +++ b/src/assets.zig @@ -7,6 +7,8 @@ const Engine = @import("./engine/root.zig"); const STBImage = @import("stb_image"); const Gfx = Engine.Graphics; const Audio = Engine.Audio; +const Vec2 = Engine.Vec2; +const Rect = Engine.Math.Rect; const Assets = @This(); @@ -18,18 +20,34 @@ const FontName = enum { const EnumArray = std.EnumArray(FontName, Gfx.Font.Id); }; +pub const Tilemap = struct { + texture: Gfx.TextureId, + tile_size: Engine.Vec2, + + + pub fn getTileUV(self: Tilemap, tile_x: f32, tile_y: f32) Rect { + const texture_info = Engine.Graphics.getTextureInfo(self.texture); + const tilemap_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height); + + return .{ + .pos = Vec2.init(tile_x, tile_y).multiply(self.tile_size).divide(tilemap_size), + .size = self.tile_size.divide(tilemap_size), + }; + } +}; + arena: std.heap.ArenaAllocator, font_id: FontName.EnumArray, wood01: Audio.Data.Id, map: tiled.Tilemap, tilesets: tiled.Tileset.List, -tileset_texture: Gfx.TextureId, -players_texture: Gfx.TextureId, -tile_size: Engine.Vec2, -player_size: Engine.Vec2, move_sound: []Audio.Data.Id, +terrain_tilemap: Tilemap, +players_tilemap: Tilemap, +weapons_tilemap: Tilemap, + pub fn init(gpa: std.mem.Allocator) !Assets { var arena = std.heap.ArenaAllocator.init(gpa); errdefer arena.deinit(); @@ -88,6 +106,16 @@ pub fn init(gpa: std.mem.Allocator) !Assets { } }); + const weapons_tileset = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Weapons/tilemap_packed.png")); + defer weapons_tileset.deinit(); + const weapons_texture = try Gfx.addTexture(&.{ + .{ + .width = weapons_tileset.width, + .height = weapons_tileset.height, + .rgba = weapons_tileset.rgba8_pixels + } + }); + const move_c = try Audio.load(.{ .data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg"), .format = .vorbis, @@ -104,11 +132,19 @@ pub fn init(gpa: std.mem.Allocator) !Assets { .wood01 = wood01, .map = map, .tilesets = tilesets, - .tileset_texture = tileset_texture, - .tile_size = .init(16, 16), - .players_texture = players_texture, - .player_size = .init(24, 24), - .move_sound = move_sound + .move_sound = move_sound, + .terrain_tilemap = .{ + .texture = tileset_texture, + .tile_size = .initFromInt(u32, tileset.tile_width, tileset.tile_height) + }, + .players_tilemap = .{ + .texture = players_texture, + .tile_size = .init(24, 24) + }, + .weapons_tilemap = .{ + .texture = weapons_texture, + .tile_size = .init(24, 24) + }, }; } diff --git a/src/engine/frame.zig b/src/engine/frame.zig index 7b2e0bc..59a83bd 100644 --- a/src/engine/frame.zig +++ b/src/engine/frame.zig @@ -14,6 +14,7 @@ const GraphicsSystem = @import("./graphics.zig"); const TextureId = GraphicsSystem.TextureId; const GraphicsCommand = GraphicsSystem.Command; const Font = GraphicsSystem.Font; +const Sprite = GraphicsSystem.Sprite; const Math = @import("./math.zig"); const Rect = Math.Rect; @@ -110,6 +111,7 @@ audio: Audio, graphics: Graphics, show_debug: bool, +hide_cursor: bool, pub fn init(self: *Frame, gpa: std.mem.Allocator) void { self.* = Frame{ @@ -119,7 +121,8 @@ pub fn init(self: *Frame, gpa: std.mem.Allocator) void { .input = .empty, .audio = .empty, .graphics = .empty, - .show_debug = false + .show_debug = false, + .hide_cursor = false }; } @@ -131,6 +134,10 @@ pub fn deltaTime(self: Frame) f32 { return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s; } +pub fn time(self: Frame) f64 { + return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s; +} + pub fn isKeyDown(self: Frame, key_code: KeyCode) bool { return self.input.down_keys.contains(key_code); } @@ -214,15 +221,6 @@ pub fn popScissor(self: *Frame) void { }); } -const DrawRectangleOptions = struct { - rect: Rect, - color: Vec4, - texture: ?struct { - id: TextureId, - uv: Rect, - } = null -}; - pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void { self.pushGraphicsCommand(.{ .draw_rectangle = opts }); } diff --git a/src/engine/graphics.zig b/src/engine/graphics.zig index ff070bc..2ea0894 100644 --- a/src/engine/graphics.zig +++ b/src/engine/graphics.zig @@ -45,10 +45,9 @@ pub const Command = union(enum) { pub const DrawRectangle = struct { rect: Rect, color: Vec4, - texture: ?struct { - id: TextureId, - uv: Rect, - } = null + sprite: ?Sprite = null, + rotation: f32 = 0, + origin: Vec2 = .init(0, 0), }; set_scissor: Rect, @@ -93,6 +92,11 @@ const Texture = struct { pub const TextureId = Texture.Id; pub const TextureInfo = Texture.Info; +pub const Sprite = struct { + texture: TextureId, + uv: Rect, +}; + var gpa: std.mem.Allocator = undefined; var main_pipeline: sgl.Pipeline = .{}; @@ -352,38 +356,38 @@ fn drawRectangle(opts: Command.DrawRectangle) void { const pos = opts.rect.pos; const size = opts.rect.size; - const top_left = pos; - const top_right = pos.add(.{ .x = size.x, .y = 0 }); - const bottom_right = pos.add(size); - const bottom_left = pos.add(.{ .x = 0, .y = size.y }); + const top_left = Vec2.init(0, 0).rotateAround(opts.rotation, opts.origin); + const top_right = Vec2.init(size.x, 0).rotateAround(opts.rotation, opts.origin); + const bottom_right = size.rotateAround(opts.rotation, opts.origin); + const bottom_left = Vec2.init(0, size.y).rotateAround(opts.rotation, opts.origin); - if (opts.texture) |texture| { - const uv = texture.uv; + if (opts.sprite) |sprite| { + const uv = sprite.uv; const quad = [4]Vertex{ .{ - .pos = top_left, + .pos = pos.add(top_left), .uv = .init(uv.left(), uv.top()) }, .{ - .pos = top_right, + .pos = pos.add(top_right), .uv = .init(uv.right(), uv.top()) }, .{ - .pos = bottom_right, + .pos = pos.add(bottom_right), .uv = .init(uv.right(), uv.bottom()) }, .{ - .pos = bottom_left, + .pos = pos.add(bottom_left), .uv = .init(uv.left(), uv.bottom()) } }; - drawQuad(quad, opts.color, texture.id); + drawQuad(quad, opts.color, sprite.texture); } else { const quad = .{ - top_left, - top_right, - bottom_right, - bottom_left + pos.add(top_left), + pos.add(top_right), + pos.add(bottom_right), + pos.add(bottom_left) }; drawQuadNoUVs(quad, opts.color); } diff --git a/src/engine/math.zig b/src/engine/math.zig index 10b1116..301b334 100644 --- a/src/engine/math.zig +++ b/src/engine/math.zig @@ -63,6 +63,21 @@ pub const Vec2 = extern struct { return Vec2.init(-self.y, self.x); } + pub fn rotate(self: Vec2, angle: f32) Vec2 { + return init( + @cos(angle) * self.x - @sin(angle) * self.y, + @sin(angle) * self.x + @cos(angle) * self.y, + ); + } + + pub fn rotateAround(self: Vec2, angle: f32, origin: Vec2) Vec2 { + return self.sub(origin).rotate(angle).add(origin); + } + + pub fn getAngle(self: Vec2) f32 { + return std.math.atan2(self.y, self.x); + } + pub fn flip(self: Vec2) Vec2 { return Vec2.init(-self.x, -self.y); } @@ -120,7 +135,10 @@ pub const Vec2 = extern struct { pub fn limitLength(self: Vec2, max_length: f32) Vec2 { const self_length = self.length(); if (self_length > max_length) { - return Vec2.init(self.x / self_length * max_length, self.y / self_length * max_length); + if (self_length == 0) { + return Vec2.init(0, 0); + } + return self.divideScalar(self_length / max_length); } else { return self; } @@ -131,7 +149,7 @@ pub const Vec2 = extern struct { if (self_length == 0) { return Vec2.init(0, 0); } - return Vec2.init(self.x / self_length, self.y / self_length); + return self.divideScalar(self_length); } pub fn initScalar(value: f32) Vec2 { diff --git a/src/engine/root.zig b/src/engine/root.zig index b944641..162edf9 100644 --- a/src/engine/root.zig +++ b/src/engine/root.zig @@ -39,6 +39,8 @@ game: Game, assets: Assets, frame: Frame, +canvas_size: ?Vec2 = null, + const RunOptions = struct { window_title: [*:0]const u8 = "Game", window_width: u31 = 640, @@ -161,6 +163,16 @@ fn sokolFrame(self: *Engine) !void { const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); + var revert_mouse_position: ?Vec2 = null; + if (self.canvas_size) |canvas_size| { + if (self.frame.input.mouse.position) |mouse| { + const transform = ScreenScalar.init(screen_size, canvas_size); + + revert_mouse_position = mouse; + self.frame.input.mouse.position = mouse.sub(transform.translation).divideScalar(transform.scale); + } + } + { _ = frame.arena.reset(.retain_capacity); const arena = frame.arena.allocator(); @@ -179,6 +191,7 @@ fn sokolFrame(self: *Engine) !void { frame.time_ns = time_passed; frame.dt_ns = time_passed - self.last_frame_at; + frame.hide_cursor = false; try self.game.tick(&self.frame); @@ -186,15 +199,24 @@ fn sokolFrame(self: *Engine) !void { frame.input.released_keys = .initEmpty(); } - if (self.frame.graphics.canvas_size) |canvas_size| { - _ = ScreenScalar.apply( - &self.frame, + if (self.canvas_size) |canvas_size| { + const transform = ScreenScalar.init( screen_size, - canvas_size, - rgb(0, 0, 0), + canvas_size + ); + transform.apply( + screen_size, + &self.frame, + rgb(0, 0, 0) ); } + sapp.showMouse(!self.frame.hide_cursor); + + // Canvas size modification must always be applied a frame later. + // So that mouse coordinate transformations are consistent. + self.canvas_size = self.frame.graphics.canvas_size; + { Gfx.beginFrame(); defer Gfx.endFrame(frame.graphics.clear_color); @@ -210,6 +232,10 @@ fn sokolFrame(self: *Engine) !void { for (frame.audio.commands.items) |command| { try Audio.mixer.commands.push(command); } + + if (revert_mouse_position) |pos| { + self.frame.input.mouse.position = pos; + } } fn showDebugWindow(frame: *Frame) !void { diff --git a/src/engine/screen_scaler.zig b/src/engine/screen_scaler.zig index 405b990..08a57ea 100644 --- a/src/engine/screen_scaler.zig +++ b/src/engine/screen_scaler.zig @@ -16,12 +16,7 @@ const ScreenScalar = @This(); translation: Vec2, scale: f32, -pub fn apply( - frame: *Frame, - window_size: Vec2, - canvas_size: Vec2, - color: Vec4 -) ScreenScalar { +pub fn init(window_size: Vec2, canvas_size: Vec2) ScreenScalar { // TODO: Render to a lower resolution instead of scaling. // To avoid pixel bleeding in spritesheet artifacts const scale = @floor(@min( @@ -33,6 +28,16 @@ pub fn apply( translation.x = @round(translation.x); translation.y = @round(translation.y); + return ScreenScalar{ + .translation = translation, + .scale = scale + }; +} + +pub fn apply(self: ScreenScalar, window_size: Vec2, frame: *Frame, color: Vec4) void { + const scale = self.scale; + const translation = self.translation; + frame.prependGraphicsCommand(.{ .push_transformation = .{ .translation = translation, @@ -72,9 +77,4 @@ pub fn apply( }, .color = color }); - - return ScreenScalar{ - .translation = translation, - .scale = scale - }; } diff --git a/src/game.zig b/src/game.zig index c169ac9..d55d6b4 100644 --- a/src/game.zig +++ b/src/game.zig @@ -1,15 +1,19 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const clamp = std.math.clamp; const Assets = @import("./assets.zig"); +const Tilemap = Assets.Tilemap; const Engine = @import("./engine/root.zig"); const Nanoseconds = Engine.Nanoseconds; const imgui = Engine.imgui; const Vec2 = Engine.Vec2; +const Vec4 = Engine.Math.Vec4; const Rect = Engine.Math.Rect; const rgb = Engine.Math.rgb; +const rgba = Engine.Math.rgba; const Range = Engine.Math.Range; const TextureId = Engine.Graphics.TextureId; const AudioId = Engine.Audio.Data.Id; @@ -99,6 +103,7 @@ player: Vec2, player_anim_state: Animation.State = .default, last_faced_left: bool = false, player_walk_sound: AudioBundle = .empty, +hand_offset: Vec2 = .zero, player_anim: Animation, @@ -106,18 +111,15 @@ pub fn init(gpa: Allocator, seed: u64, assets: *Assets) !Game { var arena = std.heap.ArenaAllocator.init(gpa); errdefer arena.deinit(); - const texture_info = Engine.Graphics.getTextureInfo(assets.players_texture); - const tilemap_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height); - const tile_size = assets.player_size; const player_anim = Animation{ - .texture = assets.players_texture, + .texture = assets.players_tilemap.texture, .frames = try arena.allocator().dupe(Animation.Frame, &.{ .{ - .uv = getUVFromTilemap(tilemap_size, tile_size, 0, 0), + .uv = assets.players_tilemap.getTileUV(0, 0), .duration = 0.1, }, .{ - .uv = getUVFromTilemap(tilemap_size, tile_size, 1, 0), + .uv = assets.players_tilemap.getTileUV(1, 0), .duration = 0.2, } }), @@ -157,24 +159,37 @@ fn findSpawnpoint(assets: *Assets) ?Vec2 { return null; } -fn getUVFromTilemap(tilemap_size: Vec2, tile_size: Vec2, tile_x: f32, tile_y: f32) Rect { - return .{ - .pos = Vec2.init(tile_x, tile_y).multiply(tile_size).divide(tilemap_size), - .size = tile_size.divide(tilemap_size), - }; -} +const DrawTileOptions = struct { + pos: Vec2, + scale: Vec2 = .init(1, 1), + color: Vec4 = rgb(255, 255, 255), + rotation: f32 = 0, + origin: Vec2 = .init(0, 0), -fn getUVFromTilemapByID(tilemap_size: Vec2, tile_size: Vec2, tile_id: u32) Rect { - const tile_id_f32: f32 = @floatFromInt(tile_id); - const width_in_tiles = tilemap_size.x / tile_size.x; - const tile_x = @rem(tile_id_f32, width_in_tiles); - const tile_y = @divFloor(tile_id_f32, width_in_tiles); - return getUVFromTilemap(tilemap_size, tile_size, tile_x, tile_y); + tilemap: Tilemap, + tile: Vec2 +}; + +fn drawTile(frame: *Engine.Frame, opts: DrawTileOptions) void { + frame.drawRectangle(.{ + .rect = .{ + .pos = opts.pos, + .size = opts.tilemap.tile_size.multiply(opts.scale) + }, + .color = opts.color, + .rotation = opts.rotation, + .origin = opts.origin, + .sprite = .{ + .texture = opts.tilemap.texture, + .uv = opts.tilemap.getTileUV(opts.tile.x, opts.tile.y) + } + }); } fn drawTilemap(self: *Game, frame: *Engine.Frame) void { - const texture = self.assets.tileset_texture; - const texture_info = Engine.Graphics.getTextureInfo(texture); + const tilemap = self.assets.terrain_tilemap; + const texture_info = Engine.Graphics.getTextureInfo(tilemap.texture); + const tilemap_size = Vec2.initFromInt(u32,texture_info.width, texture_info.height); const map = self.assets.map; @@ -193,19 +208,15 @@ fn drawTilemap(self: *Game, frame: *Engine.Frame) void { const tile_gid = tile_layer.get(x, y) orelse continue; const tile = map.getTile(self.assets.tilesets, tile_gid) orelse continue; - const tilemap_size = Vec2.initFromInt(u32,texture_info.width, texture_info.height); - const tile_size = Vec2.initFromInt(u32, tile.tileset.tile_width, tile.tileset.tile_height); + const tile_id_f32: f32 = @floatFromInt(tile.id); + const width_in_tiles = tilemap_size.x / tilemap.tile_size.x; + const tile_x = @rem(tile_id_f32, width_in_tiles); + const tile_y = @divFloor(tile_id_f32, width_in_tiles); - frame.drawRectangle(.{ - .rect = Rect{ - .pos = Vec2.initFromInt(i32, x, y).multiply(tile_size), - .size = tile_size - }, - .color = rgb(255, 255, 255), - .texture = .{ - .id = self.assets.tileset_texture, - .uv = getUVFromTilemapByID(tilemap_size, tile_size, tile.id) - } + drawTile(frame, .{ + .pos = Vec2.initFromInt(i32, x, y).multiply(tilemap.tile_size), + .tilemap = tilemap, + .tile = .init(tile_x, tile_y) }); } } @@ -222,10 +233,13 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { frame.show_debug = !frame.show_debug; } - frame.pushTransform( - canvas_size.divideScalar(2).sub(self.player), - .init(1, 1) - ); + frame.drawRectangle(.{ + .rect = .init(0, 0, canvas_size.x, canvas_size.y), + .color = rgb(20, 20, 20) + }); + + const camera_offset = canvas_size.divideScalar(2).sub(self.player); + frame.pushTransform(camera_offset, .init(1, 1)); defer frame.popTransform(); var dir = Vec2.init(0, 0); @@ -261,18 +275,13 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { self.drawTilemap(frame); - frame.drawRectangle(.{ - .rect = .init(0, 0, canvas_size.x, canvas_size.y), - .color = rgb(20, 20, 20) - }); - if (dir.x < 0) { self.last_faced_left = true; } else if (dir.x > 0) { self.last_faced_left = false; } - var size = self.assets.player_size; + var size = self.assets.players_tilemap.tile_size; if (self.last_faced_left) { size.x *= -1; } @@ -282,15 +291,47 @@ pub fn tick(self: *Game, frame: *Engine.Frame) !void { .size = size, }, .color = rgb(255, 255, 255), - .texture = .{ - .id = self.player_anim.texture, + .sprite = .{ + .texture = self.player_anim.texture, .uv = self.player_anim.frames[self.player_anim_state.frame_index].uv } }); + const max_hand_length = 32; + if (frame.input.mouse.position) |mouse_screen| { + const mouse = mouse_screen.sub(camera_offset); + const player_to_mouse = mouse.sub(self.player); + self.hand_offset = mouse.sub(self.player).limitLength(max_hand_length); + + const opacity = clamp((player_to_mouse.length() - max_hand_length) / 16, 0, 1); + drawTile(frame, .{ + .pos = mouse.sub(self.assets.weapons_tilemap.tile_size.multiplyScalar(0.5)), + .tilemap = self.assets.weapons_tilemap, + .color = rgba(255, 255, 255, opacity), + .tile = .init(4, 2) + }); + frame.hide_cursor = true; + } + + const hand = self.player.add(self.hand_offset); + var hand_flip_x: f32 = 1; + if (self.hand_offset.x < 0) { + hand_flip_x *= -1; + } + const hand_scale = Vec2.init(1, hand_flip_x); + const weapon_size = self.assets.weapons_tilemap.tile_size; + drawTile(frame, .{ + .pos = hand.add(weapon_size.multiplyScalar(-0.5).multiply(hand_scale)), + .scale = hand_scale, + .tilemap = self.assets.weapons_tilemap, + .tile = .init(0, 0), + .origin = weapon_size.multiplyScalar(0.5).multiply(hand_scale), + .rotation = self.hand_offset.getAngle() + }); + frame.drawText(self.player, "Player", .{ .font = regular_font, - .size = 1 + .size = 16 }); }