From 5da084391ec649ecc42ca5ba4b57fbb725b7dc81 Mon Sep 17 00:00:00 2001 From: Jason Hilder Date: Thu, 23 Jul 2026 08:34:46 +0200 Subject: [PATCH] Initial enemy setup, with collisions and basic spawning. Modified-by: Jason Hilder --- assets/textures/enemy/player_idle.png | Bin 0 -> 1054 bytes assets/textures/enemy/player_walk.png | Bin 0 -> 1409 bytes src/asset_manager.odin | 20 ++++- src/debug.odin | 15 ++++ src/enemy.odin | 109 +++++++++++++++++++++++--- src/main.odin | 7 +- src/map.odin | 15 ++++ 7 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 assets/textures/enemy/player_idle.png create mode 100644 assets/textures/enemy/player_walk.png diff --git a/assets/textures/enemy/player_idle.png b/assets/textures/enemy/player_idle.png new file mode 100644 index 0000000000000000000000000000000000000000..d9be6afc70ca236476f6775de47083a9e4e3a96e GIT binary patch literal 1054 zcmV+(1mXLMP)Px&*hxe|RCt{2nonpPRUF4Ziu9tQh*@?=$PSo84j$JS$VFFj2ud$e5xa+=EGq;n zQgdptM%qiuiXfqE7Ge)6J=CQ}3aJFvf&|i%gQiUgi!IA~$hrX`e-5$=KM(G{ZluWU zW+rd{{5~*bnM~&M%lpoo_x8COZpy!v4q!U~n|}HDi}ZmV(H9~B#1oSNYTL$7 zx8>h#2e2K0Z9f3_);HL=Ut;5a34p8DZ;+p#=>@;-_V?Z2(iEr%F97pJYiDoY)Nu9s z4SrhvB}i|(_z%!c`9Ij(W=mTY^7AvS{rML!Kld!(eU@UkuolE!(6*l*&;Q?{_x7#4L>))siAl2o zA|mpBQ`0grH7(iPqWKxn3W!a=?e-7c?`57S3v;uaJo7mx-*EWj z_Y&v7ub2lABf~>1uiRl|c!=cqnAdYQgKa;6q?0k@PPr~Ot5se*kuo(T$Hz#yE}3F6 zh#GAA548WO>?%B5cR%v{{q}o3xR);gQv*QCbpgl}i@feU$-uEb00zThK3Q3*AKPty zNhf2R%B~u}jr@b~H&bXYUjWpm9gGYQaqgoJ7+{BiV|@&U!vJiS%O;e|7taP!L)-a* z1MUA+snl`)rk(>#OidF@U*cx9YA(LPaG1exn9XvTsN)3sG|;vmV0q<^`TT2jo6T~W z&2l*?U0eJA+y0&CZ)yan&%1uSk|7cM8-R!t1>nVjm-z6@9|FGvSoBlxSf`JN%#KF? z*r(srcL()qvig1YH(RLC@YuOO65_Wj7XWzo>u>8{LRvX*r}(MAh4?@Ipx?_^zfJ%6 z=%@gaa$QNeu34IT({Til+V9n`{*Lf_eHDU7oj6l0GCn#=ZfWU}$L5xneCv(sSAP(G zPX!*PSNqWj>Q{eIe$U0VQbf>q0QIZ?u>PhC^iqhQ|G4_q-%fr-Q4~c{6h%=KMd?`n Y0oc!oyE&s*V*mgE07*qoM6N<$f-U3=y#N3J literal 0 HcmV?d00001 diff --git a/assets/textures/enemy/player_walk.png b/assets/textures/enemy/player_walk.png new file mode 100644 index 0000000000000000000000000000000000000000..105f2fa3799fc96071c819ef4c1080ce0038bb50 GIT binary patch literal 1409 zcmV-{1%CR8P)Px)J4r-ARCt{2oLxv9R~UvLi_r`FM@eLzfNrT!FPf4In+myUR)m&HDcFS4?1i+* zwrrbHf>$lImi8uMTPPA#uwE#+u!~g$Qx-vjD~P&kIynRgzv_1+e4r zHHLsUAl>-~T^}#}FSG&ZEI`qpm;|8h^lMaCRnniBWU#Bl*BAocfOO{{bbY+?zmNu? zvjD~WpLS=xP;<#o>5#-@zn!srA)&iGd0vB zJiaZcGyfgTFQl@5w@*OF^`O`2&Ohk-S$OGxJ~IGWfNU4p+QueV7jJNTq*oRpjso;> z8Y;Cb>{DkLDvjOcm6@wN2mV+5SK4a zi5klik!W0QfDl56H>(bc^~E`{zBnhgS0=r@EhV+b2KwYJj~dGoQ~l?qu5(jhzRvu! zVz7_j+TJN5(YQELUMh~1mr6}%Y+h^Y@;~7EX!c)w0Ro7zfj&`VS)#kGUVQjUr2s^0 zd#7k^?-c!sN%>QScEB5u_WXmcUjz+EYXQm_oZ7K)ct;G**S*#ba08+U{139ea{u#P zC==an_5AUBn$O2qiG{;l+}h&A>3qWWk z|3TJQ>c5*03xHTS3_$PF5{*`cUj-;*uoM4bxR)2p%BTy40JyfYB25|W>QE6wQ8gf4 z4OpZ957hrEomTF@CVgf8yLt|wwY`)2?(7xebs2xSLVO#2Z8L9zoEXu?*f!WqH%tD^d!GM9cOer#prm7U!Oe1 z%}hpB_qWm6fV3ANMd1Ja^nLJOtG){V-3BUfJatrERmrX4a{#>l^|$slNFm2{i)gki z046dS$eEfc))$)c&$|4H*53i3%Blh2soH0F`-|^YPJt-`|MSsT>b*w&V(=eYnwsQ_ zyS?h(Y*})Ja{GN{v@|u@8dA?0lght-IP!OH&aqIOf7bop*7i=(XjOd*!T3KR1pA2_WI=NpAUdNH^f6k28 z{)}t=yaL=ySMy>JjDE27?-hexfV(w6)!Kf>pmO{Ro&k6v?gpbDT>WBgKz?bf)OQ_V zK7pv1f*Ji_>+3E+H*xHz@i+fT*RZe9U4RCI!C){L3 Asset_Manager { assets := Asset_Manager{} - init_player_assets(&assets.player.clips); + init_player_assets(&assets.player.clips) + init_enemy_assets(&assets.enemy.clips) return assets } @@ -35,6 +41,18 @@ init_player_assets :: proc(clip_array: ^[Player_State]AnimationClip) { } } +init_enemy_assets :: proc(clip_array: ^[Enemy_State]AnimationClip) { + idle_tex := rl.LoadTexture("assets/textures/enemy/player_idle.png") + walk_tex := rl.LoadTexture("assets/textures/enemy/player_walk.png") + + clip_array[.IDLE] = AnimationClip { + texture = idle_tex, frame_count = 6, frame_width = 32, frame_height = 32, frame_time = 0.14, looping = true, + } + clip_array[.WALK] = AnimationClip { + texture = walk_tex, frame_count = 8, frame_width = 32, frame_height = 32, frame_time = 0.14, looping = true, + } +} + asset_manager_destroy :: proc(assets: ^Asset_Manager) { for clip in assets.player.clips { rl.UnloadTexture(clip.texture) diff --git a/src/debug.odin b/src/debug.odin index f01a9db..b20d809 100644 --- a/src/debug.odin +++ b/src/debug.odin @@ -22,7 +22,15 @@ debug_init :: proc(game: ^Game) { debug_fps :: proc() { when DEBUG_MODE { rl.DrawFPS(20, 20) + for i in i32(0)..<4 { + if rl.IsGamepadAvailable(i) { + rl.DrawText(rl.TextFormat("GP%d: %s", i, rl.GetGamepadName(i)), 20, 45 + i*20, 20, rl.BLACK) + } else { + rl.DrawText(rl.TextFormat("GP%d: NOT DETECTED", i), 20, 45 + i*20, 20, rl.GRAY) + } + } } + } // DEBUG CAMERA @@ -82,6 +90,13 @@ debug_bounds :: proc(game: ^Game) { player_collider := get_player_collider(&game.player, game.player.position) rl.DrawRectangleLines( i32(player.position.x), i32(player.position.y), TILE_SIZE, TILE_SIZE, rl.GREEN) rl.DrawRectangleLinesEx(player_collider, 1, rl.BLUE) + + // enemies + for &e in game.enemies { + enemy_collider := get_enemy_collider(e, e.position) + rl.DrawRectangleLinesEx(enemy_collider, 1, rl.BLACK) + rl.DrawRectangleLines( i32(e.position.x), i32(e.position.y), TILE_SIZE, TILE_SIZE, rl.MAROON) + } } } } diff --git a/src/enemy.odin b/src/enemy.odin index 17e1ba0..b4b26ec 100644 --- a/src/enemy.odin +++ b/src/enemy.odin @@ -1,24 +1,115 @@ package game +import "core:math/rand" import rl "vendor:raylib" -Enemy :: struct { - position: rl.Vector2, +Enemy_State :: enum { IDLE, WALK, ATTACK } +Enemy_Type :: enum { SKELETON, ZOMBIE, GHOST } +Enemy :: struct { + // info + type: Enemy_Type, + position: rl.Vector2, + state: Enemy_State, + animation: AnimationState, + flipped: bool, + + // movement velocity: rl.Vector2, - speed: f32, // max move speed + speed: f32, + + // collision + collider_offset: rl.Vector2, + collider_w: f32, + collider_h: f32 } -enemy_init :: proc() -> Enemy { - enemy := Enemy{} - enemy.position = {300, 300} +enemy_init :: proc(assets: ^Enemy_Assets, pos: [2]f32) -> Enemy { + enemy := Enemy{} + enemy.position = {pos.x, pos.y} + enemy.state = .IDLE + enemy.collider_offset = rl.Vector2{8, 15} + enemy.collider_w = 16 + enemy.collider_h = 10 + animation_play(&enemy.animation, &assets.clips[.IDLE]) return enemy } -update_enemy :: proc() { } +get_enemy_collider :: proc(enemy: Enemy, pos: rl.Vector2) -> rl.Rectangle { + return rl.Rectangle{ + (pos.x + enemy.collider_offset.x), + pos.y + enemy.collider_offset.y, + enemy.collider_w, + enemy.collider_h, + } +} -draw_enemy :: proc(enemy: Enemy) { - rl.DrawCircle(i32(enemy.position.x), i32(enemy.position.y), 10, rl.RED) +get_spawn_rect :: proc(max_x, max_y: f32) -> rl.Rectangle { + // Get random x and y pos + x_pos := rand.float32_range(TILE_SIZE, (max_x - TILE_SIZE)) + y_pos := rand.float32_range(TILE_SIZE, (max_y - TILE_SIZE)) + + return rl.Rectangle{x_pos, y_pos, TILE_SIZE, TILE_SIZE} +} + +// @TODO consider a self check, ie pass the enemy in question index and make sure to skip that +spawn_enemy :: proc(enemy_array: ^[MAX_ENEMIES]Enemy, assets: ^Enemy_Assets, enemy_count: ^int, game_map: Game_Map) { + max_x := game_map.width_px + max_y := game_map.height_px + cnt := enemy_count^ + + // Can only have Max amount of enemies + if cnt < MAX_ENEMIES { + + MAX_ATTEMPTS :: 30 + enemy_rect := rl.Rectangle{} + valid_pos := false + + for attempt := 0; !valid_pos && attempt < MAX_ATTEMPTS; attempt += 1 { + enemy_rect = get_spawn_rect(max_x, max_y) + valid_pos = !check_map_collision(enemy_rect, game_map) && !check_enemy_collision(enemy_rect, cnt, enemy_array) + } + if !valid_pos { + return // couldn't find a spot this call, skip spawning + } + + // init the enemy there in .idle state + enemy_array[cnt] = enemy_init(assets, {enemy_rect.x, enemy_rect.y}) + cnt += 1 + enemy_count^ = cnt + } +} + + + +update_enemies :: proc(enemy_array: ^[MAX_ENEMIES]Enemy, assets: ^Enemy_Assets) { + for &enemy in enemy_array { + moving := false + dt := rl.GetFrameTime() + + + enemy.state = moving ? .WALK : .IDLE + animation_play(&enemy.animation, &assets.clips[enemy.state]) + animation_update(&enemy.animation, dt) + + } +} + + +draw_enemies :: proc(enemy_array: ^[MAX_ENEMIES]Enemy) { + for &enemy in enemy_array { + clip := enemy.animation.clip + src_rect := animation_frame_rect(&enemy.animation) + + dst_rect := rl.Rectangle{ + x = enemy.position.x, + y = enemy.position.y, + width = f32(clip.frame_width), + height = f32(clip.frame_height), + } + + rl.DrawTexturePro(clip.texture, src_rect, dst_rect, rl.Vector2{}, 0, rl.WHITE) + } } diff --git a/src/main.odin b/src/main.odin index 4c4d372..aac5f17 100644 --- a/src/main.odin +++ b/src/main.odin @@ -7,14 +7,15 @@ import "core:mem" // ----------------------------------------------------------------------------- DEBUG_MODE :: true +WINDOW_TITLE :: "Graveyard" WINDOW_WIDTH :: 1920 WINDOW_HEIGHT :: 1080 GAME_WIDTH :: 480 GAME_HEIGHT :: 270 - -WINDOW_TITLE :: "Graveyard" -PLAYER_SPEED :: 180 +MAX_ENEMIES :: 140 +MAX_PROPS :: 64 TILE_SIZE :: 32 + // ----------------------------------------------------------------------------- main :: proc() { diff --git a/src/map.odin b/src/map.odin index 50f41c5..c9964b6 100644 --- a/src/map.odin +++ b/src/map.odin @@ -171,6 +171,21 @@ check_map_collision :: proc(rect: rl.Rectangle, game_map: Game_Map) -> bool { return false } +// true when collision found +check_enemy_collision :: proc(rect_a: rl.Rectangle, active_enemies: int, enemies: ^[MAX_ENEMIES]Enemy) -> bool { + + for idx in 0..