├── .gitignore ├── hotreload.gif ├── TODO.md ├── game.ml ├── main.ml ├── README.md ├── LICENSE ├── raylib.ml ├── caml_raylib.c └── game_plug.ml /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.cmi 3 | *.cmo 4 | *.cmx 5 | game 6 | game.opt 7 | *.so 8 | -------------------------------------------------------------------------------- /hotreload.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/simple-ocaml-raylib-template/HEAD/hotreload.gif -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # Core Mechanics 2 | 3 | - [x] Tank rendering 4 | - [x] Tank movement 5 | - [x] Gun movement 6 | - [x] Shooting projectiles 7 | - [x] Projectiles Shockwave 8 | 9 | - [x] Infinite Space with Camera 10 | - [x] Platforms 11 | - [ ] Health (Collectable hearts/repair kits) 12 | 13 | - [ ] Destroyable platforms 14 | - [ ] Mines 15 | -------------------------------------------------------------------------------- /game.ml: -------------------------------------------------------------------------------- 1 | open Raylib 2 | 3 | module Projectile = struct 4 | type t = 5 | { pos: Vector2.t 6 | ; vel: Vector2.t 7 | ; lifetime: float 8 | } 9 | end 10 | 11 | module Explosion = struct 12 | type t = 13 | { pos: Vector2.t 14 | ; progress: float 15 | } 16 | end 17 | 18 | type t = 19 | { pos: Vector2.t 20 | ; vel: Vector2.t 21 | ; mov: Vector2.t 22 | ; projs: Projectile.t list 23 | ; explosions: Explosion.t list 24 | ; health: float 25 | } 26 | 27 | type plug_t = 28 | { mutable fresh: unit -> t 29 | ; mutable update: float -> t -> t 30 | } 31 | 32 | exception Plugin_not_loaded 33 | 34 | let plug = 35 | { fresh = (fun _ -> raise Plugin_not_loaded) 36 | ; update = (fun _ _ -> raise Plugin_not_loaded) 37 | } 38 | -------------------------------------------------------------------------------- /main.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | let open Raylib in 3 | #ifdef HOTRELOAD 4 | let plug_file_path = "game_plug.cmo" in 5 | Dynlink.loadfile_private plug_file_path; 6 | #endif 7 | let factor = 80 in 8 | let width = 16*factor in 9 | let height = 9*factor in 10 | let title = "Game" in 11 | init_window width height title; 12 | set_target_fps 60; 13 | let rec loop (game: Game.t) = 14 | match window_should_close () with 15 | | true -> () 16 | | false -> 17 | #ifdef HOTRELOAD 18 | if 'R' |> Char.code |> is_key_pressed then 19 | begin 20 | Dynlink.loadfile_private plug_file_path; 21 | Printf.printf "Loaded %s\n" plug_file_path; 22 | flush stdout 23 | end; 24 | #endif 25 | let dt = 0.016 in 26 | game |> Game.plug.update dt |> loop 27 | in Game.plug.fresh () |> loop; 28 | close_window() 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Raylib OCaml Template Project 2 | 3 | Intentionally simple Raylib OCaml Template. Not using [dune](https://github.com/ocaml/dune) or any other build systems. Just calling `ocamlc` and `clang` directly to demonstrate how all of this works under the hood for educational purposes. 4 | 5 | For more info on OCaml-C Interop, read this entire 73 pages documents (it's simple, you can do that in a couple of hours): https://v2.ocaml.org/manual/intfc.html 6 | 7 | ## Hot Reloading 8 | 9 | ![hotreload](./hotreload.gif) 10 | 11 | Hot Reloading mechanism is based upon [Dynlink](https://v2.ocaml.org/api/Dynlink.html) module from Stdlib. You can only Hot Reload [game_plug.ml](./game_plug.ml) module. To hot reload the module press R during the execution. 12 | 13 | ## Dependencies 14 | 15 | - [OCaml 5.0.0](https://ocaml.org/install) 16 | - [Raylib](https://www.raylib.com/) 17 | - [Clang](https://clang.llvm.org/) 18 | 19 | *Tested only on Linux* 20 | 21 | ## Quick Start 22 | 23 | ``` console 24 | $ ./build.sh 25 | $ ./game 26 | ``` 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Alexey Kutepov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /raylib.ml: -------------------------------------------------------------------------------- 1 | type color = 2 | { r: int 3 | ; g: int 4 | ; b: int 5 | ; a: int 6 | } 7 | 8 | let red = { r = 230 ; g = 41 ; b = 55 ; a = 255 } 9 | let green = { r = 0 ; g = 228 ; b = 48 ; a = 255 } 10 | let blue = { r = 0 ; g = 121 ; b = 241 ; a = 255 } 11 | 12 | module Vector2 = struct 13 | type t = { x: float; y: float } 14 | let ivec2 x y = { x = float_of_int x; y = float_of_int y } 15 | let vec2 x y = { x; y } 16 | let ( ~^ ) (a: t): t = 17 | { x = -.a.x 18 | ; y = -.a.y 19 | } 20 | let ( -^ ) (a: t) (b: t): t = 21 | { x = a.x -. b.x 22 | ; y = a.y -. b.y 23 | } 24 | let ( +^ ) (a: t) (b: t): t = 25 | { x = a.x +. b.x 26 | ; y = a.y +. b.y 27 | } 28 | let ( *^ ) (a: t) (b: t): t = 29 | { x = a.x *. b.x 30 | ; y = a.y *. b.y 31 | } 32 | let ( /^ ) (a: t) (b: t): t = 33 | { x = a.x /. b.x 34 | ; y = a.y /. b.y 35 | } 36 | let scalar (s: float): t = 37 | { x = s; y = s } 38 | let mag (v: t): float = 39 | sqrt (v.x*.v.x +. v.y*.v.y) 40 | let dir (v: t): float = 41 | atan2 v.y v.x 42 | end 43 | 44 | module Camera2D = struct 45 | type t = 46 | { offset: Vector2.t 47 | ; zoom: float 48 | } 49 | end 50 | 51 | module Rectangle = struct 52 | type t = { x: float; y: float; width: float; height: float } 53 | end 54 | 55 | external init_window: int -> int -> string -> unit = "caml_init_window" 56 | external set_target_fps: int -> unit = "caml_set_target_fps" 57 | external window_should_close: unit -> bool = "caml_window_should_close" 58 | external begin_drawing: unit -> unit = "caml_begin_drawing" 59 | external end_drawing: unit -> unit = "caml_end_drawing" 60 | external clear_background: color -> unit = "caml_clear_background" 61 | external close_window: unit -> unit = "caml_close_window" 62 | external draw_rectangle: int -> int -> int -> int -> color -> unit = "caml_draw_rectangle" 63 | external get_render_width: unit -> int = "caml_get_render_width" 64 | external get_render_height: unit -> int = "caml_get_render_height" 65 | external draw_circle : int -> int -> float -> color -> unit = "caml_draw_circle" 66 | external draw_rectangle_pro : Rectangle.t -> Vector2.t -> float -> color -> unit = "caml_draw_rectangle_pro" 67 | 68 | external is_key_pressed: int -> bool = "caml_is_key_pressed" 69 | external is_key_down: int -> bool = "caml_is_key_down" 70 | 71 | external get_mouse_x: unit -> int = "caml_get_mouse_x" 72 | external get_mouse_y: unit -> int = "caml_get_mouse_y" 73 | 74 | external is_mouse_button_pressed: int -> bool = "caml_is_mouse_button_pressed" 75 | 76 | external begin_mode_2d: Camera2D.t -> unit = "caml_begin_mode_2d" 77 | external end_mode_2d: unit -> unit = "caml_end_mode_2d" 78 | 79 | external get_screen_to_world2d: Vector2.t -> Camera2D.t -> Vector2.t = "caml_get_screen_to_world2d" 80 | -------------------------------------------------------------------------------- /caml_raylib.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include 7 | 8 | CAMLprim value caml_init_window(value width, value height, value title) 9 | { 10 | CAMLparam3(width, height, title); 11 | InitWindow(Int_val(width), Int_val(height), String_val(title)); 12 | CAMLreturn(Val_unit); 13 | } 14 | 15 | CAMLprim value caml_set_target_fps(value fps) 16 | { 17 | CAMLparam1(fps); 18 | SetTargetFPS(Int_val(fps)); 19 | CAMLreturn(Val_unit); 20 | } 21 | 22 | CAMLprim value caml_window_should_close(value unit) 23 | { 24 | CAMLparam1(unit); 25 | bool result = WindowShouldClose(); 26 | CAMLreturn(Val_bool(result)); 27 | } 28 | 29 | CAMLprim value caml_begin_drawing(value unit) 30 | { 31 | CAMLparam1(unit); 32 | BeginDrawing(); 33 | CAMLreturn(Val_unit); 34 | } 35 | 36 | CAMLprim value caml_end_drawing(value unit) 37 | { 38 | CAMLparam1(unit); 39 | EndDrawing(); 40 | CAMLreturn(Val_unit); 41 | } 42 | 43 | Color color_of_value(value v) 44 | { 45 | Color c = {0}; 46 | c.r = Int_val(Field(v, 0)); 47 | c.g = Int_val(Field(v, 1)); 48 | c.b = Int_val(Field(v, 2)); 49 | c.a = Int_val(Field(v, 3)); 50 | return c; 51 | } 52 | 53 | Vector2 vector2_of_value(value v) 54 | { 55 | Vector2 vec2 = {0}; 56 | vec2.x = Double_field(v, 0); 57 | vec2.y = Double_field(v, 1); 58 | return vec2; 59 | } 60 | 61 | Camera2D camera2d_of_value(value v) 62 | { 63 | Camera2D camera = { 0 }; 64 | camera.offset = vector2_of_value(Field(v, 0)); 65 | camera.zoom = Double_val(Field(v, 1)); 66 | return camera; 67 | } 68 | 69 | Rectangle rectangle_of_value(value v) 70 | { 71 | Rectangle rec = {0}; 72 | rec.x = Double_field(v, 0); 73 | rec.y = Double_field(v, 1); 74 | rec.width = Double_field(v, 2); 75 | rec.height = Double_field(v, 3); 76 | return rec; 77 | } 78 | 79 | CAMLprim value caml_clear_background(value color) 80 | { 81 | CAMLparam1(color); 82 | ClearBackground(color_of_value(color)); 83 | CAMLreturn(Val_unit); 84 | } 85 | 86 | CAMLprim value caml_close_window(value unit) 87 | { 88 | CAMLparam1(unit); 89 | CloseWindow(); 90 | CAMLreturn(Val_unit); 91 | } 92 | 93 | CAMLprim value caml_draw_rectangle(value x, value y, value w, value h, value color) 94 | { 95 | CAMLparam5(x, y, w, h, color); 96 | DrawRectangle(Int_val(x), Int_val(y), Int_val(w), Int_val(h), color_of_value(color)); 97 | CAMLreturn(Val_unit); 98 | } 99 | 100 | CAMLprim value caml_get_render_width(value unit) 101 | { 102 | CAMLparam1(unit); 103 | CAMLreturn(Val_int(GetRenderWidth())); 104 | CAMLreturn(Val_unit); 105 | } 106 | 107 | CAMLprim value caml_get_render_height(value unit) 108 | { 109 | CAMLparam1(unit); 110 | CAMLreturn(Val_int(GetRenderHeight())); 111 | CAMLreturn(Val_unit); 112 | } 113 | 114 | CAMLprim value caml_is_key_pressed(value key) 115 | { 116 | CAMLparam1(key); 117 | CAMLreturn(Val_bool(IsKeyPressed(Int_val(key)))); 118 | } 119 | 120 | CAMLprim value caml_is_key_down(value key) 121 | { 122 | CAMLparam1(key); 123 | CAMLreturn(Val_bool(IsKeyDown(Int_val(key)))); 124 | } 125 | 126 | CAMLprim value caml_draw_circle(value x, value y, value radius, value color) 127 | { 128 | CAMLparam4(x, y, radius, color); 129 | DrawCircle(Int_val(x), Int_val(y), Double_val(radius), color_of_value(color)); 130 | CAMLreturn(Val_unit); 131 | } 132 | 133 | CAMLprim value caml_draw_rectangle_pro(value rec, value origin, value rotation, value color) 134 | { 135 | CAMLparam4(rec, origin, rotation, color); 136 | DrawRectanglePro( 137 | rectangle_of_value(rec), 138 | vector2_of_value(origin), 139 | Double_val(rotation), 140 | color_of_value(color)); 141 | CAMLreturn(Val_unit); 142 | } 143 | 144 | CAMLprim value caml_get_mouse_x(value unit) 145 | { 146 | CAMLparam1(unit); 147 | CAMLreturn(Val_int(GetMouseX())); 148 | } 149 | 150 | CAMLprim value caml_get_mouse_y(value unit) 151 | { 152 | CAMLparam1(unit); 153 | CAMLreturn(Val_int(GetMouseY())); 154 | } 155 | 156 | CAMLprim value caml_is_mouse_button_pressed(value button) 157 | { 158 | CAMLparam1(button); 159 | CAMLreturn(Val_bool(IsMouseButtonPressed(Int_val(button)))); 160 | } 161 | 162 | CAMLprim value caml_begin_mode_2d(value camera) 163 | { 164 | CAMLparam1(camera); 165 | Camera2D c = camera2d_of_value(camera); 166 | BeginMode2D(c); 167 | CAMLreturn(Val_unit); 168 | } 169 | 170 | CAMLprim value caml_end_mode_2d(value unit) 171 | { 172 | CAMLparam1(unit); 173 | EndMode2D(); 174 | CAMLreturn(Val_unit); 175 | } 176 | 177 | CAMLprim value caml_get_screen_to_world2d(value position, value camera) 178 | { 179 | CAMLparam2(position, camera); 180 | CAMLlocal1(result); 181 | 182 | Vector2 position1 = GetScreenToWorld2D(vector2_of_value(position), camera2d_of_value(camera)); 183 | result = caml_alloc_float_array(2); 184 | Store_double_field(result, 0, position1.x); 185 | Store_double_field(result, 1, position1.y); 186 | CAMLreturn(result); 187 | } 188 | -------------------------------------------------------------------------------- /game_plug.ml: -------------------------------------------------------------------------------- 1 | open Raylib 2 | 3 | let gray = { r = 130; g = 130; b = 130; a = 255} 4 | 5 | let player_size = 100./.2. 6 | let player_color = red 7 | let gravity = Vector2.vec2 0. 2000. 8 | let dumpling = 0.75 9 | let friction = 0.9 10 | let jump_y = 1000.0 11 | let gun_length = 120. 12 | let gun_girth = 20. 13 | let tank_width = 150. 14 | let tank_height = 80. 15 | let dome_radius = 50. 16 | let move_speed = 250. 17 | let projectile_speed = 1000.0 18 | let projectile_radius = gun_girth/.2. 19 | let projectile_color = green 20 | let projectile_lifetime = 2. 21 | let shockwave_distance = 500. 22 | let shockwave_impact = 2000.0 23 | let shockwave_color = green 24 | let explosion_duration = 0.25 25 | let platform_color = blue 26 | let health_bar_padding = 50. 27 | let health_bar_girth = 20. 28 | 29 | let platforms: Rectangle.t list = 30 | let s = 100. in 31 | [ { x = 0.; y = 0.; width = s; height = s } 32 | ; { x = s +. s; y = -. s *. 4.; width = s *. 5.; height = s } 33 | ; { x = s *. 5.; y = -. s *. 8.; width = s *. 5.; height = s } 34 | ] 35 | 36 | let rectangle_sides (a: Rectangle.t): (float * float * float * float) = 37 | let l = a.x in 38 | let r = a.x +. a.width in 39 | let t = a.y in 40 | let b = a.y +. a.height in 41 | l, r, t, b 42 | 43 | let rectangle_overlap (a: Rectangle.t) (b: Rectangle.t): bool = 44 | let l1, r1, t1, b1 = rectangle_sides a in 45 | let l2, r2, t2, b2 = rectangle_sides b in 46 | r1 >= l2 && r2 >= l1 && b1 >= t2 && b2 >= t1 47 | 48 | let rectangle_contains (p: Vector2.t) (r: Rectangle.t): bool = 49 | let l, r, t, b = rectangle_sides r in 50 | l <= p.x && p.x <= r && t <= p.y && p.y <= b 51 | 52 | let fresh (): Game.t = 53 | let open Vector2 in 54 | { pos = vec2 0. (-.300.) 55 | ; vel = vec2 0. 100. 56 | ; mov = scalar 0. 57 | ; projs = [] 58 | ; explosions = [] 59 | ; health = 1. 60 | } 61 | 62 | let spawn_projectile (game: Game.t) (pos: Vector2.t) (vel: Vector2.t): Game.t = 63 | let new_proj: Game.Projectile.t = 64 | { pos 65 | ; vel 66 | ; lifetime = projectile_lifetime 67 | } 68 | in 69 | { game with projs = new_proj :: game.projs } 70 | 71 | let spawn_explosion (game: Game.t) (pos: Vector2.t): Game.t = 72 | let new_explosion: Game.Explosion.t = { pos; progress = 1. } in 73 | { game with explosions = new_explosion :: game.explosions } 74 | 75 | let render_gun (pivot: Vector2.t) (rotation: float): unit = 76 | let rectangle: Rectangle.t = 77 | { x = pivot.x 78 | ; y = pivot.y 79 | ; width = gun_length 80 | ; height = gun_girth 81 | } 82 | in 83 | let origin: Vector2.t = { x = 0.; y = gun_girth/.2. } in 84 | draw_rectangle_pro rectangle origin rotation red 85 | 86 | let render_tank (position: Vector2.t) (rotation: float): unit = 87 | draw_rectangle 88 | (int_of_float (position.x -. tank_width/.2.)) 89 | (int_of_float (position.y -. tank_height)) 90 | (int_of_float tank_width) 91 | (int_of_float tank_height) 92 | red; 93 | let dome_center: Vector2.t = 94 | { x = position.x 95 | ; y = position.y -. tank_height 96 | } 97 | in 98 | draw_circle 99 | (int_of_float dome_center.x) 100 | (int_of_float dome_center.y) 101 | dome_radius 102 | red; 103 | render_gun dome_center rotation 104 | 105 | let tank_hitbox_at (pos: Vector2.t): Rectangle.t = 106 | { x = pos.x -. tank_width/.2. 107 | ; y = pos.y -. tank_height 108 | ; width = tank_width 109 | ; height = tank_height 110 | } 111 | 112 | let update (dt: float) (game: Game.t): Game.t = 113 | let open Vector2 in 114 | 115 | (* Computable parameters of the game *) 116 | let res = ivec2 (get_render_width ()) (get_render_height ()) in 117 | 118 | let dome_center pos = 119 | pos -^ vec2 0. tank_height 120 | in 121 | 122 | let camera (pos: Vector2.t) : Camera2D.t = 123 | { offset = ~^ ((pos -^ res) *^ scalar 0.5) 124 | ; zoom = 0.5 125 | } 126 | in 127 | 128 | let mouse_screen = ivec2 (get_mouse_x ()) (get_mouse_y ()) in 129 | let mouse_world = get_screen_to_world2d mouse_screen (game.pos |> dome_center |> camera) in 130 | 131 | let gun_rotation_rads = dir (mouse_world -^ (dome_center game.pos)) in 132 | let gun_rotation_degrees = gun_rotation_rads/.Float.pi*.180.0 in 133 | 134 | (* Tank Controls *) 135 | let game = 136 | (* Reset the state of the game *) 137 | let game = 138 | if 'Q' |> Char.code |> is_key_pressed 139 | then fresh () 140 | else game 141 | in 142 | 143 | (* Shooting projectile *) 144 | let game = 145 | if is_mouse_button_pressed 0 then 146 | let gun_dir: Vector2.t = 147 | { x = cos gun_rotation_rads 148 | ; y = sin gun_rotation_rads 149 | } 150 | in 151 | let open Vector2 in 152 | let gun_tip: Vector2.t = 153 | gun_dir *^ scalar gun_length +^ (dome_center game.pos) 154 | in 155 | let proj_vel: Vector2.t = 156 | gun_dir *^ scalar projectile_speed 157 | in 158 | spawn_projectile game gun_tip proj_vel 159 | else game 160 | in 161 | 162 | (* Moving left *) 163 | let game = 164 | if 'A' |> Char.code |> is_key_down then 165 | { game with mov = vec2 (-.move_speed) 0. } 166 | else 167 | { game with mov = scalar 0. } 168 | in 169 | 170 | (* Moving right *) 171 | let game = 172 | if 'D' |> Char.code |> is_key_down then 173 | { game with mov = game.mov +^ vec2 move_speed 0. } 174 | else game 175 | in 176 | game 177 | in 178 | 179 | (* Tank Physics *) 180 | let game = 181 | let game = 182 | { game with vel = game.vel +^ gravity*^scalar dt } 183 | in 184 | 185 | let pos1 = game.pos +^ (game.vel +^ game.mov)*^scalar dt in 186 | 187 | let horz_collis = List.exists (rectangle_overlap (vec2 pos1.x game.pos.y |> tank_hitbox_at)) platforms in 188 | let game = 189 | if horz_collis then 190 | { game with vel = game.vel *^ vec2 0. friction } 191 | else 192 | { game with pos = { game.pos with x = pos1.x } } 193 | in 194 | 195 | let vert_collis = List.exists (rectangle_overlap (vec2 game.pos.x pos1.y |> tank_hitbox_at)) platforms in 196 | let game = 197 | if vert_collis then 198 | { game with vel = game.vel *^ vec2 friction 0. } 199 | else 200 | { game with pos = { game.pos with y = pos1.y } } 201 | in 202 | 203 | (* Bottom collision *) 204 | let game = 205 | if game.pos.y >= 0. 206 | then { game with pos = { game.pos with y = 0. } 207 | ; vel = game.vel *^ vec2 friction 0. 208 | } 209 | else game 210 | in 211 | 212 | game 213 | in 214 | 215 | let update_proj (proj: Game.Projectile.t): Game.Projectile.t = 216 | let open Vector2 in 217 | let proj = { proj with vel = proj.vel +^ gravity*^scalar dt } in 218 | let proj = { proj with pos = proj.pos +^ proj.vel*^scalar dt } in 219 | let proj = { proj with lifetime = if proj.pos.y >= 0. || (List.exists (rectangle_contains proj.pos) platforms) 220 | then 0. 221 | else proj.lifetime -. dt } 222 | in 223 | proj 224 | in 225 | let new_projs = game.projs |> List.map update_proj in 226 | let alive_projs = new_projs |> List.filter (fun (proj: Game.Projectile.t) -> proj.lifetime > 0.) in 227 | let dead_projs = new_projs |> List.filter (fun (proj: Game.Projectile.t) -> proj.lifetime <= 0.) in 228 | let game = { game with projs = alive_projs } in 229 | let exploded_projectile_force (pos: Vector2.t) (proj: Game.Projectile.t): Vector2.t = 230 | let open Vector2 in 231 | let direction = dome_center pos -^ proj.pos in 232 | let distance = mag direction in 233 | if distance > shockwave_distance then 234 | { x = 0.; y = 0.} 235 | else 236 | let s = 1. -. distance/.shockwave_distance in 237 | let eps = 0.001 in (* to prevent the division by distance == 0 *) 238 | direction /^ scalar (distance +. eps) *^ scalar (shockwave_impact *. s) 239 | in 240 | let force = 241 | let open Vector2 in 242 | dead_projs 243 | |> List.map (exploded_projectile_force game.pos) 244 | |> List.fold_left (+^) (scalar 0.) 245 | in 246 | let game = { game with vel = game.vel +^ force 247 | ; health = if (mag force) > 1. 248 | then max (game.health -. 0.1) 0. 249 | else game.health 250 | } 251 | in 252 | let game = 253 | let explosion_from_proj (proj: Game.Projectile.t): Game.Explosion.t = 254 | { pos = proj.pos 255 | ; progress = 1. 256 | } 257 | in 258 | let update_explosion (explosion: Game.Explosion.t): Game.Explosion.t = 259 | { explosion with progress = (explosion_duration*.explosion.progress -. dt)/.explosion_duration } 260 | in 261 | let is_explosion_alive (explosion: Game.Explosion.t): bool = 262 | explosion.progress > 0. 263 | in 264 | let new_explosions = dead_projs |> List.map explosion_from_proj in 265 | { game with explosions = 266 | game.explosions 267 | |> List.map update_explosion 268 | |> List.filter is_explosion_alive 269 | |> List.append new_explosions 270 | } 271 | in 272 | 273 | (* Rendering the state of the game *) 274 | begin_drawing (); 275 | let background = { r = 0x18; g = 0x18; b = 0x18; a = 0xFF } in 276 | clear_background background; 277 | begin_mode_2d (game.pos |> dome_center |> camera); 278 | game.projs 279 | |> List.iter (fun (proj: Game.Projectile.t) -> 280 | let x = proj.pos.x |> int_of_float in 281 | let y = proj.pos.y |> int_of_float in 282 | draw_circle x y projectile_radius projectile_color); 283 | game.explosions 284 | |> List.iter (fun (explosion: Game.Explosion.t) -> 285 | let x = explosion.pos.x |> int_of_float in 286 | let y = explosion.pos.y |> int_of_float in 287 | let r = shockwave_distance*.0.5*.explosion.progress in 288 | draw_circle x y r shockwave_color); 289 | render_tank game.pos gun_rotation_degrees; 290 | platforms 291 | |> List.iter (fun (platform: Rectangle.t) -> 292 | let x = platform.x |> int_of_float in 293 | let y = platform.y |> int_of_float in 294 | let w = platform.width |> int_of_float in 295 | let h = platform.height |> int_of_float in 296 | draw_rectangle x y w h platform_color); 297 | let () = 298 | let health_pos = 299 | (game.pos |> dome_center) 300 | -^ vec2 0. dome_radius 301 | -^ vec2 0. health_bar_padding 302 | -^ vec2 (tank_width/.2.) 0. 303 | -^ vec2 0. (health_bar_girth/.2.) 304 | in 305 | let () = 306 | let x = health_pos.x |> int_of_float in 307 | let y = health_pos.y |> int_of_float in 308 | let w = tank_width |> int_of_float in 309 | let h = health_bar_girth |> int_of_float in 310 | draw_rectangle x y w h gray 311 | in (); 312 | let () = 313 | let x = health_pos.x |> int_of_float in 314 | let y = health_pos.y |> int_of_float in 315 | let w = (tank_width *. game.health) |> int_of_float in 316 | let h = health_bar_girth |> int_of_float in 317 | draw_rectangle x y w h red 318 | in () 319 | in (); 320 | end_mode_2d (); 321 | end_drawing (); 322 | game 323 | 324 | let () = 325 | (* Registering hot reloaded functions *) 326 | Game.plug.fresh <- fresh; 327 | Game.plug.update <- update 328 | --------------------------------------------------------------------------------