From a7ae25ec086117e133b2dfbbc1c5025aa2f29964 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Sun, 8 Sep 2024 19:49:47 -0400 Subject: [PATCH] Add example game using raylib --- examples/game/README.md | 6 ++++ examples/game/box.tm | 9 +++++ examples/game/color.tm | 22 ++++++++++++ examples/game/game.tm | 42 ++++++++++++++++++++++ examples/game/map.txt | 17 +++++++++ examples/game/player.tm | 31 ++++++++++++++++ examples/game/world.tm | 78 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 205 insertions(+) create mode 100644 examples/game/README.md create mode 100644 examples/game/box.tm create mode 100644 examples/game/color.tm create mode 100644 examples/game/game.tm create mode 100644 examples/game/map.txt create mode 100644 examples/game/player.tm create mode 100644 examples/game/world.tm diff --git a/examples/game/README.md b/examples/game/README.md new file mode 100644 index 0000000..c64b050 --- /dev/null +++ b/examples/game/README.md @@ -0,0 +1,6 @@ +# Example Game + +This is a simple example game that uses [raylib](https://www.raylib.com/) to +demonstrate a project that spans multiple files and showcases some game +programming concepts used in idiomatic Tomo code. It also showcases how to +interact with an external C library. diff --git a/examples/game/box.tm b/examples/game/box.tm new file mode 100644 index 0000000..8871315 --- /dev/null +++ b/examples/game/box.tm @@ -0,0 +1,9 @@ +# Defines a struct representing boxes on the terrain +use vectors + +use ./world.tm +use ./color.tm + +struct Box(pos:Vec2, size=Vec2(50, 50), color=Color.GRAY, blocking=yes): + func draw(b:&Box): + b.color:draw_rectangle(b.pos, b.size) diff --git a/examples/game/color.tm b/examples/game/color.tm new file mode 100644 index 0000000..79807a6 --- /dev/null +++ b/examples/game/color.tm @@ -0,0 +1,22 @@ +# Defines a struct used to represent colors using 64-bit floats (0.0 - 1.0), +# which can be used to draw colored rectangles in raylib +use +use vectors + +struct Color(r,g,b:Num32,a=1.0f32): + RED := Color(1,0,0) + GRAY := Color(.2f32,.2f32,.2f32) + LIGHT_GRAY := Color(.7f32,.7f32,.7f32) + + func draw_rectangle(c:Color, pos:Vec2, size:Vec2): + inline C { + DrawRectangle( + (int)($pos.$x), (int)($pos.$y), (int)($size.$x), (int)($size.$y), + ((Color){ + (int8_t)(uint8_t)(255.*$c.$r), + (int8_t)(uint8_t)(255.*$c.$g), + (int8_t)(uint8_t)(255.*$c.$b), + (int8_t)(uint8_t)(255.*$c.$a), + }) + ); + } diff --git a/examples/game/game.tm b/examples/game/game.tm new file mode 100644 index 0000000..368d17a --- /dev/null +++ b/examples/game/game.tm @@ -0,0 +1,42 @@ +# This game demo uses Raylib to present a simple +use libraylib.so +use +use +use file + +use ./world.tm + +func main(): + extern InitWindow:func(w:Int32, h:Int32, title:CString)->Void + InitWindow(1600, 900, "raylib [core] example - 2d camera") + + map := when read("map.txt") is Success(m): m + else: exit(code=1, "Could not find the game map!") + + World.CURRENT:load_map(map) + + extern SetTargetFPS:func(fps:Int32) + SetTargetFPS(60) + + extern WindowShouldClose:func()->Bool + + while not WindowShouldClose(): + extern GetFrameTime:func()->Num32 + dt := GetFrameTime() + World.CURRENT:update(Num(dt)) + + extern BeginDrawing:func() + BeginDrawing() + + inline C { + ClearBackground((Color){0xCC, 0xCC, 0xCC, 0xFF}); + } + + World.CURRENT:draw() + + extern EndDrawing:func() + EndDrawing() + + extern CloseWindow:func() + CloseWindow() + diff --git a/examples/game/map.txt b/examples/game/map.txt new file mode 100644 index 0000000..d0436a0 --- /dev/null +++ b/examples/game/map.txt @@ -0,0 +1,17 @@ +[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] [] +[] @@ [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[] [] [] +[][][][][][][][][][][][][][][][][][][][][][][][][][][][][][][] diff --git a/examples/game/player.tm b/examples/game/player.tm new file mode 100644 index 0000000..5aff9ea --- /dev/null +++ b/examples/game/player.tm @@ -0,0 +1,31 @@ +# Defines a struct representing the player, which is controlled by WASD keys +use libraylib.so +use +use + +use vectors +use ./world.tm + +struct Player(pos,prev_pos:Vec2): + WALK_SPEED := 500. + ACCEL := 0.3 + FRICTION := 0.99 + SIZE := Vec2(50, 50) + + func update(p:&Player): + target_x := inline C ( + (Num_t)((IsKeyDown(KEY_A) ? -1 : 0) + (IsKeyDown(KEY_D) ? 1 : 0)) + ) : Num + target_y := inline C ( + (Num_t)((IsKeyDown(KEY_W) ? -1 : 0) + (IsKeyDown(KEY_S) ? 1 : 0)) + ) : Num + target_vel := Vec2(target_x, target_y):norm() * WALK_SPEED + + vel := (p.pos - p.prev_pos)/World.DT + vel *= FRICTION + vel = vel:mix(target_vel, ACCEL) + + p.prev_pos, p.pos = p.pos, p.pos + World.DT*vel + + func draw(p:&Player): + Color.RED:draw_rectangle(p.pos, Player.SIZE) diff --git a/examples/game/world.tm b/examples/game/world.tm new file mode 100644 index 0000000..6af7cc7 --- /dev/null +++ b/examples/game/world.tm @@ -0,0 +1,78 @@ + +use vectors + +use ./player.tm +use ./color.tm +use ./box.tm + +# Return a displacement relative to `a` that will push it out of `b` +func solve_overlap(a_pos:Vec2, a_size:Vec2, b_pos:Vec2, b_size:Vec2)->Vec2: + a_left := a_pos.x + a_right := a_pos.x + a_size.x + a_top := a_pos.y + a_bottom := a_pos.y + a_size.y + + b_left := b_pos.x + b_right := b_pos.x + b_size.x + b_top := b_pos.y + b_bottom := b_pos.y + b_size.y + + # Calculate the overlap in each dimension + overlap_x := (a_right _min_ b_right) - (a_left _max_ b_left) + overlap_y := (a_bottom _min_ b_bottom) - (a_top _max_ b_top) + + # If either axis is not overlapping, then there is no collision: + if overlap_x <= 0 or overlap_y <= 0: + return Vec2(0, 0) + + if overlap_x < overlap_y: + if a_right > b_left and a_right < b_right: + return Vec2(-(overlap_x), 0) + else if a_left < b_right and a_left > b_left: + return Vec2(overlap_x, 0) + else: + if a_top < b_bottom and a_top > b_top: + return Vec2(0, overlap_y) + else if a_bottom > b_top and a_bottom < b_bottom: + return Vec2(0, -overlap_y) + + return Vec2(0, 0) + +struct World(player:@Player, boxes:[@Box], dt_accum=0.0): + DT := 1./60. + CURRENT := @World(@Player(Vec2(0,0), Vec2(0,0)), [:@Box]) + STIFFNESS := 0.3 + + func update(w:&World, dt:Num): + w.dt_accum += dt + while w.dt_accum > 0: + w:update_once() + w.dt_accum -= World.DT + + func update_once(w:&World): + w.player:update() + + # Resolve player overlapping with any boxes: + for i in 3: + for b in w.boxes: + w.player.pos += STIFFNESS * solve_overlap(w.player.pos, Player.SIZE, b.pos, b.size) + + func draw(w:&World): + for b in w.boxes: + b:draw() + w.player:draw() + + func load_map(w:&World, map:Text): + map = map:replace_all({$/[]/: "X", $/@{1..}/: "@", $/ /: " "}) + w.boxes = [:@Box] + box_size := Vec2(50., 50.) + for y,line in map:lines(): + for x,cell in line:split(): + if cell == "X": + pos := Vec2((Num(x)-1) * box_size.x, (Num(y)-1) * box_size.y) + box := @Box(pos, size=box_size, color=Color.GRAY) + (&w.boxes):insert(box) + else if cell == "@": + pos := Vec2((Num(x)-1) * box_size.x, (Num(y)-1) * box_size.y) + w.player = @Player(pos,pos) +