aboutsummaryrefslogtreecommitdiff
path: root/examples/game
diff options
context:
space:
mode:
authorBruce Hill <bruce@bruce-hill.com>2024-09-08 19:49:47 -0400
committerBruce Hill <bruce@bruce-hill.com>2024-09-08 19:49:47 -0400
commita7ae25ec086117e133b2dfbbc1c5025aa2f29964 (patch)
tree45bcab3728e0998205e280a0dd84a44bcb2925be /examples/game
parentf86cc6549ff6075c3963fce819391d8d8d6960dc (diff)
Add example game using raylib
Diffstat (limited to 'examples/game')
-rw-r--r--examples/game/README.md6
-rw-r--r--examples/game/box.tm9
-rw-r--r--examples/game/color.tm22
-rw-r--r--examples/game/game.tm42
-rw-r--r--examples/game/map.txt17
-rw-r--r--examples/game/player.tm31
-rw-r--r--examples/game/world.tm78
7 files changed, 205 insertions, 0 deletions
diff --git a/examples/game/README.md b/examples/game/README.md
new file mode 100644
index 00000000..c64b0508
--- /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 00000000..88713154
--- /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 00000000..79807a6d
--- /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 <raylib.h>
+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 00000000..368d17af
--- /dev/null
+++ b/examples/game/game.tm
@@ -0,0 +1,42 @@
+# This game demo uses Raylib to present a simple
+use libraylib.so
+use <raylib.h>
+use <raymath.h>
+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 00000000..d0436a0a
--- /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 00000000..5aff9ea5
--- /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 <raylib.h>
+use <raymath.h>
+
+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 00000000..6af7cc75
--- /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)
+