diff --git a/engine/app.odin b/engine/app.odin new file mode 100644 index 0000000..b4e8736 --- /dev/null +++ b/engine/app.odin @@ -0,0 +1,56 @@ +package engine + +import "core:fmt" +import sdl "vendor:sdl3" + +App :: struct { + window: ^sdl.Window, + renderer: ^sdl.Renderer, +} + +init :: proc(app: ^App, title: cstring, width, height: i32) -> bool { + if !sdl.Init({.VIDEO}) { + fmt.eprintfln("SDL_Init failed: %s", sdl.GetError()) + return false + } + + if !sdl.CreateWindowAndRenderer(title, width, height, {}, &app.window, &app.renderer) { + fmt.eprintfln("CreateWindowAndRenderer failed: %s", sdl.GetError()) + return false + } + + return true +} + +shutdown :: proc(app: ^App) { + if app.renderer != nil do sdl.DestroyRenderer(app.renderer) + if app.window != nil do sdl.DestroyWindow(app.window) + + sdl.Quit() + app^ = {} +} + +pump_events :: proc() -> bool { + event: sdl.Event + + for sdl.PollEvent(&event) { + if event.type == .QUIT { + return false + } + } + + return true +} + +begin_frame :: proc(app: ^App, r: u8 = 30, g: u8 = 30, b: u8 = 40, a: u8 = 255) { + sdl.SetRenderDrawColor(app.renderer, r, g, b, a) + sdl.RenderClear(app.renderer) +} + +end_frame :: proc(app: ^App) { + sdl.RenderPresent(app.renderer) +} + +renderer :: proc(app: ^App) -> ^sdl.Renderer { + return app.renderer +} diff --git a/engine/character_data.odin b/engine/character_data.odin new file mode 100644 index 0000000..4fc6d68 --- /dev/null +++ b/engine/character_data.odin @@ -0,0 +1,108 @@ +package engine + +import "core:encoding/json" +import "core:fmt" +import "core:os" +import "core:path/filepath" +import "core:strings" +import sdl "vendor:sdl3" + +Frame_Def :: struct { + rect: [4]int, + source_size: [2]int, + trim_offset: [2]int, +} + +Clip_Def :: struct { + loop: bool, + fps: f32, + frames: []Frame_Def, +} + +Char_Def :: struct { + version: int, + id: string, + atlas: string, + pixels_per_unit: f32, + pivot: [2]f32, + clips: map[string]Clip_Def, +} + +Character_Data :: struct { + def: Char_Def, + texture: ^sdl.Texture, +} + +load_character_data :: proc( + renderer: ^sdl.Renderer, + character_json_path: string, +) -> ( + Character_Data, + bool, +) { + file_data, read_err := os.read_entire_file(character_json_path, context.allocator) + if read_err != nil { + fmt.eprintfln("failed to read %s: %v", character_json_path, read_err) + return {}, false + } + + defer delete(file_data) + + out: Character_Data + if err := json.unmarshal(file_data, &out.def); err != nil { + fmt.eprintfln("json unmarshal failed: %v", err) + return {}, false + } + + // filepath.dir returns a substring of the path — do NOT delete it + dir := filepath.dir(character_json_path) + + atlas_path, join_err := filepath.join({dir, out.def.atlas}) + if join_err != nil { + fmt.eprintfln("atlas join failed: %v", join_err) + return {}, false + } + + defer delete(atlas_path) + + c_path := strings.clone_to_cstring(atlas_path) + defer delete(c_path) + + surface := sdl.LoadPNG(c_path) + if surface == nil { + fmt.eprintfln("LoadPNG failed: %s", sdl.GetError()) + return {}, false + } + defer sdl.DestroySurface(surface) + + out.texture = sdl.CreateTextureFromSurface(renderer, surface) + if out.texture == nil { + fmt.eprintfln("CreateTextureFromSurface failed: %s", sdl.GetError()) + return {}, false + } + sdl.SetTextureScaleMode(out.texture, .NEAREST) + return out, true +} + +destroy_character_data :: proc(data: ^Character_Data) { + if data.texture != nil { + sdl.DestroyTexture(data.texture) + } + data^ = {} +} + +character_frame_rect :: proc( + data: ^Character_Data, + clip_name: string, + frame_index: int, +) -> ( + rect: [4]int, + ok: bool, +) { + clip, found := data.def.clips[clip_name] + if !found || frame_index < 0 || frame_index >= len(clip.frames) { + return {}, false + } + + return clip.frames[frame_index].rect, true +} diff --git a/engine/sprite.odin b/engine/sprite.odin new file mode 100644 index 0000000..602f3cf --- /dev/null +++ b/engine/sprite.odin @@ -0,0 +1,51 @@ +package engine + +import sdl "vendor:sdl3" + +Vec2 :: [2]f32 + +Sprite :: struct { + data: ^Character_Data, + position: Vec2, + clip: string, + frame: int, +} + +spawn_sprite :: proc( + data: ^Character_Data, + position: Vec2, + clip: string = "idle", + frame: int = 0, +) -> Sprite { + return Sprite{data = data, position = position, clip = clip, frame = frame} +} + +// stubbed for later +update_sprite :: proc(sprite: ^Sprite, dt: f32) { + _ = sprite + _ = dt +} + +draw_sprite :: proc(renderer: ^sdl.Renderer, sprite: ^Sprite) { + assert(sprite.data != nil) + rect, ok := character_frame_rect(sprite.data, sprite.clip, sprite.frame) + if !ok { + return + } + + w := f32(rect[2]) + h := f32(rect[3]) + dst := sdl.FRect { + x = sprite.position.x - w * 0.5, + y = sprite.position.y - h, + w = w, + h = h, + } + src := sdl.FRect { + x = f32(rect[0]), + y = f32(rect[1]), + w = w, + h = h, + } + sdl.RenderTexture(renderer, sprite.data.texture, &src, &dst) +} diff --git a/examples/toad/main.odin b/examples/toad/main.odin index 3d0bb5e..f4e6ce6 100644 --- a/examples/toad/main.odin +++ b/examples/toad/main.odin @@ -1,117 +1,29 @@ package main -import "core:encoding/json" -import "core:fmt" -import "core:os" -import "core:text/regex/virtual_machine" -import sdl "vendor:sdl3" - -Frame_Def :: struct { - rect: [4]int, - source_size: [2]int, - trim_offset: [2]int, -} - -Clip_Def :: struct { - loop: bool, - fps: f32, - frames: []Frame_Def, -} - -Char_Def :: struct { - version: int, - id: string, - atlas: string, - pixels_per_unit: f32, - pivot: [2]f32, - clips: map[string]Clip_Def, -} +import eng "pkg:engine" main :: proc() { - if !sdl.Init({.VIDEO}) { - fmt.eprintfln("SDL_Init failed: %s", sdl.GetError()) + app: eng.App + if !eng.init(&app, "toad example", 800, 600) { return } - defer sdl.Quit() + defer eng.shutdown(&app) - window: ^sdl.Window - renderer: ^sdl.Renderer - - if !sdl.CreateWindowAndRenderer("toad", 800, 600, {}, &window, &renderer) { - fmt.eprintfln("CreateWindowAndRenderer failed: %s", sdl.GetError()) + data, ok := eng.load_character_data( + app.renderer, + "assets_baked/characters/toad/toad.char.json", + ) + if !ok { return } + defer eng.destroy_character_data(&data) - defer sdl.DestroyRenderer(renderer) - defer sdl.DestroyWindow(window) + toad := eng.spawn_sprite(&data, {400, 500}, "idle", 0) - surface := sdl.LoadPNG("assets_baked/characters/toad/toad.atlas.png") - if surface == nil { - fmt.eprintfln("LoadPNG failed: %s", sdl.GetError()) - return - } - defer sdl.DestroySurface(surface) - - texture := sdl.CreateTextureFromSurface(renderer, surface) - if texture == nil { - fmt.eprintfln("CreateTextureFromSurface failed: %s", sdl.GetError()) - return - } - defer sdl.DestroyTexture(texture) - - sdl.SetTextureScaleMode(texture, .NEAREST) - - char_path := "assets_baked/characters/toad/toad.char.json" - data, read_err := os.read_entire_file(char_path, context.allocator) - if read_err != nil { - fmt.eprintfln("failed to read %s: %v", char_path, read_err) - return - } - defer delete(data) - - char_def: Char_Def - if err := json.unmarshal(data, &char_def); err != nil { - fmt.eprintfln("json unmarshal failed: %v", err) - return - } - - idle, ok := char_def.clips["idle"] - if !ok || len(idle.frames) == 0 { - fmt.eprintfln("missing idle frames") - return - } - - // Atlas crop for idle frame 0 — used every frame as `src` - r := idle.frames[0].rect // [x, y, w, h] - src := sdl.FRect { - x = f32(r[0]), - y = f32(r[1]), - w = f32(r[2]), - h = f32(r[3]), - } - - running := true - - for running { - event: sdl.Event - for sdl.PollEvent(&event) { - if event.type == .QUIT { - running = false - } - } - - sdl.SetRenderDrawColor(renderer, 30, 30, 40, 255) - sdl.RenderClear(renderer) - - dst := sdl.FRect { - x = 400 - src.w * 0.5, - y = 500 - src.h, - w = src.w, - h = src.h, - } - - sdl.RenderTexture(renderer, texture, &src, &dst) - - sdl.RenderPresent(renderer) + for eng.pump_events() { + eng.begin_frame(&app) + eng.draw_sprite(app.renderer, &toad) + eng.end_frame(&app) } } + diff --git a/ols.json b/ols.json new file mode 100644 index 0000000..70c287b --- /dev/null +++ b/ols.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://raw.githubusercontent.com/DanielGavin/ols/master/misc/ols.schema.json", + "collections": [ + { "name": "pkg", "path": "." } + ], + "enable_document_symbols": true, + "enable_hover": true, + "enable_snippets": true, + "profiles": [ + { + "name": "default", + "checker_path": ["examples/toad", "engine", "assetbake"] + } + ] +}