├── .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 | 
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 |
--------------------------------------------------------------------------------