(* A Simple Abstract Shmup Game Copyright (C) 2019 Florent Monnier This software is provided "AS-IS", without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software and associated elements for any purpose, including commercial applications, and to modify it and redistribute it freely. *) type point2d = int * int (* (x, y) *) type vector2d = int * int (* (x, y) *) module Vector2d : sig type t = vector2d (** (x, y) *) val add : t -> t -> t (** [a + b] *) val sub : t -> t -> t (** [a - b] *) val mul : t -> int -> t (** [v * k] *) val div : t -> int -> t (** [v / k] *) module Infix : sig val ( +. ) : t -> t -> t val ( -. ) : t -> t -> t val ( *. ) : t -> int -> t val ( /. ) : t -> int -> t end end = struct type t = vector2d let add (ax, ay) (bx, by) = (ax + bx, ay + by) let sub (ax, ay) (bx, by) = (ax - bx, ay - by) let mul (x, y) k = (x * k, y * k) let div (x, y) k = (x / k, y / k) module Infix = struct let ( +. ) = add ;; let ( -. ) = sub ;; let ( *. ) = mul ;; let ( /. ) = div ;; end end module QuadraticBezierCurves : sig val interval : int * int (** The interval for interpolation is [(0, 1000)] instead of [(0.0, 1.0)] for [floats]. *) val point_on_curve : point2d * point2d * point2d -> int -> point2d (** [point_on_curve (p1, p2, p3) t] returns a point on the quadratic bezier curve defined by p1, p2 and p3, with t in the interval predefined above *) end = struct let interval = (0, 1000) let point_on_curve (p1, p2, p3) t = let ti = 1000 - t in Vector2d.Infix.( ( p1 *. ((ti * ti) / 1000) +. p2 *. ((2 * ti * t) / 1000) +. p3 *. ((t * t) / 1000) ) /. 1000 ) end module Timeline : sig type time = int type ('a, 'b) animated = [ | `From of time * 'a (** [From (t, v)] after time [t] is reach (and before next timeline chunk) the returned value will be [v] *) | `Evol of time * time * (time -> time -> time -> 'b -> 'a) * 'b (** [Evol (t1, t2, f, d)] when [t] is between [t1] and [t2] the value is the result of [f t1 t2 t d] *) ] val val_at : time -> ('a, 'b) animated list -> 'a val finished : time -> ('a, 'b) animated list -> bool end = struct type time = int (* animating a value over time *) type ('a, 'b) animated = [ | `From of time * 'a | `Evol of time * time * (time -> time -> time -> 'b -> 'a) * 'b ] (* timeline function *) let rec val_at t = function | `From(t1, v) :: `From(t2,_) :: _ | `From(t1, v) :: `Evol(t2,_,_,_) :: _ when t1 <= t && t < t2 -> v | `From(t, v) :: [] -> v | `Evol(t1, t2, f, v) :: [] when t >= t2 -> f t1 t2 t2 v | `Evol(t1, t2, f, v) :: _ when t1 <= t && t <= t2 -> f t1 t2 t v | _ :: tl -> val_at t tl | [] -> invalid_arg "val_at" let rec finished t = function | `From _ :: [] -> true | `Evol(_, t2, _, _) :: [] -> t > t2 | `From(t2, _) :: tl -> if t < t2 then false else finished t tl | `Evol(_, t2, _, _) :: tl -> if t < t2 then false else finished t tl | _ -> false end module QBCurve = QuadraticBezierCurves type foe = { foe_pos: int * int; foe_anim: (point2d, point2d * point2d * point2d) Timeline.animated list; foe_last_shot: int; foe_shoot_freq: int; foe_sprite: int * int * int * int; foe_size: int * int; foe_tex: Canvas.image; foe_tex_loaded: bool ref; } type foe_bullet = { bullet_pos: int * int; bullet_line: (int * int) * (int * int); bullet_birth: int; } type player_dir = { left: bool; right: bool; up: bool; down: bool; } type player = { p_pos: int * int; p_size: int * int; p_last_shot: int; p_shoot_freq: int; p_shooting: bool; p_dir: player_dir; p_game_over: bool; p_texture: Canvas.image; mutable p_texture_ok: bool; } type game_state = { player: player; foes: foe list; f_bullets: foe_bullet list; (* foes bullets *) p_bullets: (int * int) list; (* player bullets *) } type game_data = { f_bullet_tex: Dom.element; p_bullet_tex: Dom.element; f_textures: (Canvas.image * bool ref * int ref * int ref) array; (* foe textures *) } let canvas = Canvas.getElementById Canvas.doc "my_canvas" let ctx = Canvas.getContext canvas "2d" let t0 = int_of_float (Js.Date.now ()) let width, height = (640, 480) let blue = (0, 0, 255) let green = (0, 255, 0) let yellow = (255, 255, 0) let alpha = 255 let shot = ref 0 let missed = ref 0 module Rect : sig type t = { x: int; y: int; w: int; h: int; } val make1 : int * int * int * int -> t val make2 : pos:int * int -> dims:int * int -> t val make4 : int -> int -> int -> int -> t val has_intersection : t -> t -> bool end = struct type t = { x: int; y: int; w: int; h: int; } let make1 (x, y, w, h) = { x; y; w; h } let make2 ~pos:(x, y) ~dims:(w, h) = { x; y; w; h } let make4 x y w h = { x; y; w; h } let has_intersection a b = (* (* Special cases for empty rects *) if (rect_empty(a) || rect_empty(b)) then (false) *) (* Horizontal intersection *) let a_min = a.x in let a_max = a_min + a.w in let b_min = b.x in let b_max = b_min + b.w in let a_min = if (b_min > a_min) then b_min else a_min in let a_max = if (b_max < a_max) then b_max else a_max in if (a_max <= a_min) then false else (* Vertical intersection *) let a_min = a.y in let a_max = a_min + a.h in let b_min = b.y in let b_max = b_min + b.h in let a_min = if (b_min > a_min) then b_min else a_min in let a_max = if (b_max < a_max) then b_max else a_max in if (a_max <= a_min) then (false) else (true) ;; end let _game_state = ref None let _game_data = ref None let get_ref r = match !r with Some v -> v | None -> assert false let set_ref r v = r := Some v let set_color color = let r, g, b = color in let color = Printf.sprintf "rgba(%d,%d,%d,%d)" r g b 255 in Canvas.setFillStyle ctx color; ;; let fill_rect40 color x y = set_color color; Canvas.fillRect ctx x y 40 40; ;; let fill_rect20 color x y = set_color color; Canvas.fillRect ctx x y 20 20; ;; let make_background () = Array.init 12 (fun y -> Array.init 16 (fun x -> let v = 60 + Random.int 20 in (v, v, v))) let background = Random.self_init (); make_background () let display_background playing = Array.iteri (fun y row -> Array.iteri (fun x rgb -> let _x = x * 40 and _y = y * 40 in if playing then fill_rect40 rgb _x _y else let r, g, b = rgb in fill_rect40 (r + 40, g / 2, b / 3) _x _y ) row ) background let display ~playing game_state game_data = display_background playing; Canvas.setFont ctx "bold 18px Arial"; Canvas.setTextBaseline ctx "top"; set_color green; let s = Printf.sprintf "shot: %d" !shot in Canvas.fillText ctx s 10 10; let s = Printf.sprintf "missed: %d" !missed in Canvas.fillText ctx s (width - 120) 10; let s = Printf.sprintf "score: %d" (!shot - !missed) in Canvas.fillText ctx s 10 (height - 25); List.iter (fun bullet -> let x, y = bullet.bullet_pos in let sprite = game_data.f_bullet_tex in Canvas.drawImage ctx sprite x y; ) game_state.f_bullets; List.iter (fun foe -> if !(foe.foe_tex_loaded) then begin let dx, dy = foe.foe_pos in let dw, dh = foe.foe_size in let sx, sy, sw, sh = foe.foe_sprite in let tex = foe.foe_tex in Canvas.drawImage8 ctx tex sx sy sw sh dx dy dw dh; end ) game_state.foes; List.iter (fun pos -> let x, y = pos in let sprite = game_data.p_bullet_tex in Canvas.drawImage ctx sprite x y; ) game_state.p_bullets; let player = game_state.player in if player.p_texture_ok then begin let dx, dy = player.p_pos in let sw, sh = player.p_size in let sx, sy = (0, 0) in let dw, dh = (sw / 3, sh / 3) in let sprite = player.p_texture in Canvas.drawImage8 ctx sprite sx sy sw sh dx dy dw dh; end; ;; type key_change = KeyDown | KeyUp let _ev_keychange key_change ev game_state = let player = game_state.player in let player = match key_change, ev.Canvas.keyCode, ev.Canvas.key with | KeyDown, 37, _ -> (* Left *) { player with p_dir = { player.p_dir with left = true } } | KeyDown, 39, _ -> (* Right *) { player with p_dir = { player.p_dir with right = true } } | KeyDown, 38, _ -> (* Up *) { player with p_dir = { player.p_dir with up = true } } | KeyDown, 40, _ -> (* Down *) { player with p_dir = { player.p_dir with down = true } } | KeyUp, 37, _ -> (* Left *) { player with p_dir = { player.p_dir with left = false } } | KeyUp, 39, _ -> (* Right *) { player with p_dir = { player.p_dir with right = false } } | KeyUp, 38, _ -> (* Up *) { player with p_dir = { player.p_dir with up = false } } | KeyUp, 40, _ -> (* Down *) { player with p_dir = { player.p_dir with down = false } } | KeyDown, _, "z" -> { player with p_shooting = true } | KeyUp, _, "z" -> { player with p_shooting = false } | KeyDown, _, "p" -> { player with p_game_over = false } | _ -> player in { game_state with player }; ;; let ev_keychange key_change ev = let game_state = get_ref _game_state in let game_state = _ev_keychange key_change ev game_state in set_ref _game_state game_state; ;; let _texture_of_pattern pattern ~color = let w, h = (5, 5) in (* width, height *) let imgData = Canvas.createImageData ctx (w * 4) (h * 4) in let pixels = imgData.Canvas.data in let len = Array.length pixels in let (r,g,b) = color in let width = 4 * 5 in let height = 4 * 5 in for y = 0 to height - 1 do for x = 0 to width - 1 do let _x = x / 4 in let _y = y / 4 in let v = pattern.(_y).(_x) in let (r,g,b,a) = if v = 1 then (r,g,b,255) else (0,0,0,0) (* transparent alpha *) in let pos = y * width + x in pixels.(pos*4 + 0) <- r; pixels.(pos*4 + 1) <- g; pixels.(pos*4 + 2) <- b; pixels.(pos*4 + 3) <- a; done; done; (imgData) let texture_of_pattern pattern ~color = let overlay_canvas = Canvas.createElement Canvas.doc "canvas" in Canvas.setWidth overlay_canvas (4*5); Canvas.setHeight overlay_canvas (4*5); let over_ctx = Canvas.getContext overlay_canvas "2d" in let imgData = _texture_of_pattern pattern ~color in Canvas.putImageData over_ctx imgData 0 0; (overlay_canvas) let f_bullet_inside bullet = let x, y = bullet.bullet_pos in (y < height) && (x < width) && (y > -20) && (x > -20) let point_on_line (p1, p2) i t = let ti = i - t in Vector2d.Infix.( ( (p1 *. ti) +. (p2 *. t) ) /. i ) let step_foes_bullets game_state t = let step_bullet bullet = let dt = t - bullet.bullet_birth in let p = point_on_line bullet.bullet_line 6000 dt in { bullet with bullet_pos = p } in let f_bullets = List.map step_bullet game_state.f_bullets in let f_bullets = List.filter f_bullet_inside f_bullets in { game_state with f_bullets } let inter1 t t1 t2 v1 v2 = ((v2 - v1) * (t - t1)) / (t2 - t1) + v1 let min_t, max_t = QBCurve.interval let fe t1 t2 t ps = let t = inter1 t t1 t2 min_t max_t in QBCurve.point_on_curve ps t let make_foe_anim foe_size t = let t1 = t and t2 = t + 6000 + Random.int 4000 in let dw, dh = foe_size in match Random.int 7 with | 0 -> (* left to right *) let p1, p2, p3 = (- dw, Random.int (height - dh)), (Random.int width, Random.int (height - dh)), (width, Random.int (height - dh)) in let ps = (p1, p2, p3) in [ `Evol (t1, t2, fe, ps) ] | 1 -> (* right to left *) let p1, p2, p3 = (width, Random.int (height - dh)), (Random.int width, Random.int (height - dh)), (- dw, Random.int (height - dh)) in let ps = (p1, p2, p3) in [ `Evol (t1, t2, fe, ps) ] | 2 | 3 | 4 -> (* top to bottom *) let p1, p2, p3 = (Random.int (width - dw), - dh), (Random.int (width - dw), Random.int (height - dh)), (Random.int (width - dw), height) in let ps = (p1, p2, p3) in [ `Evol (t1, t2, fe, ps) ] | 5 | 6 -> (* top to middle, pause, middle to bottom *) let t1 = t and t2 = t + 4000 + Random.int 3000 in let t3 = t2 + 2000 + Random.int 2000 in let t4 = t3 + 4000 + Random.int 3000 in let p1, p2, p3, p4, p5 = (Random.int (width - dw), - dh), (Random.int (width - dw), Random.int (height - dh)), (Random.int (width - dw), Random.int (height - dh)), (Random.int (width - dw), Random.int (height - dh)), (Random.int (width - dw), height) in let ps1 = (p1, p2, p3) in let ps2 = (p3, p4, p5) in [ `Evol (t1, t2, fe, ps1); `From (t2, p3); `Evol (t3, t4, fe, ps2); ] | _ -> assert false let rand_take arr = let n = Array.length arr in arr.(Random.int n) let rand_choose f_textures = let rec aux () = let (foe_tex, foe_tex_loaded, w, h) as foe_elems = rand_take f_textures in if !foe_tex_loaded then foe_elems else aux () in aux () let make_new_foe game_data t = let foe_tex, foe_tex_loaded, w, h = rand_choose game_data.f_textures in let foe_sprite, foe_size = let sx, sy = (0, 0) in let sw, sh = (!w, !h) in let dw, dh = (!w / 3, !h / 3) in (sx, sy, sw, sh), (dw, dh) in let foe_pos = (0, 0) in let foe_anim = make_foe_anim foe_size t in let foe_last_shot = t in let foe_shoot_freq = 1600 + Random.int 1800 in { foe_sprite; foe_pos; foe_size; foe_anim; foe_last_shot; foe_shoot_freq; foe_tex; foe_tex_loaded; } let new_foes_opt game_state game_data t = if Random.int 100 > 2 then game_state.foes else let new_foe = make_new_foe game_data t in (new_foe :: game_state.foes) let gun_new_f_bullets game_state foes t = let rec aux acc1 acc2 foes = match foes with | [] -> (acc1, acc2) | foe :: foes -> if t - foe.foe_last_shot < foe.foe_shoot_freq then aux acc1 (foe :: acc2) foes else let bullet_pos = let fx, fy = foe.foe_pos in let bw, bh = (10, 10) in (* half bullet size *) let w, h = foe.foe_size in (fx + w/2 - bw, fy + h - bh) in let updated_foe = { foe with foe_last_shot = t } in let bullet = { bullet_pos; bullet_line = (bullet_pos, game_state.player.p_pos); bullet_birth = t; } in aux (bullet :: acc1) (updated_foe :: acc2) foes in let new_f_bullets, foes = aux [] [] foes in let f_bullets = List.rev_append new_f_bullets game_state.f_bullets in let foes = List.rev foes in (f_bullets, foes) let foe_inside t foe = not (Timeline.finished t foe.foe_anim) let foe_touched p_bullets foe = let x, y = foe.foe_pos in let dw, dh = foe.foe_size in let foe_rect = Rect.make4 x y dw dh in List.exists (fun (x, y) -> let bullet_rect = Rect.make4 x y 20 20 in Rect.has_intersection foe_rect bullet_rect ) p_bullets let step_foes game_state game_data t = let step_foe foe = let new_pos = Timeline.val_at t foe.foe_anim in { foe with foe_pos = new_pos } in let foes = new_foes_opt game_state game_data t in let f_bullets, foes = gun_new_f_bullets game_state foes t in let foes = List.map step_foe foes in let foes = List.filter (fun foe -> if foe_inside t foe then true else (incr missed; false) ) foes in let foes = List.filter (fun foe -> if foe_touched game_state.p_bullets foe then (incr shot; false) else true ) foes in { game_state with foes; f_bullets } let player_touched game_state = let player = game_state.player in let x, y = player.p_pos in let sw, sh = player.p_size in let w, h = (sw / 3, sh / 3) in let player_rect = Rect.make4 x y w h in let p_game_over = List.exists (fun bullet -> let x, y = bullet.bullet_pos in let x, y = (x + 4, y + 4) in let bullet_rect = Rect.make4 x y 12 12 in Rect.has_intersection player_rect bullet_rect ) game_state.f_bullets in let player = { player with p_game_over } in { game_state with player } let player_moving player = let x, y = player.p_pos in let _x, _y = match player.p_dir with | { left = true; right = false; up = false; down = false } -> (x - 10, y) | { left = false; right = true; up = false; down = false } -> (x + 10, y) | { left = false; right = false; up = true; down = false } -> (x, y - 10) | { left = false; right = false; up = false; down = true } -> (x, y + 10) | { left = true; right = false; up = true; down = false } -> (x - 7, y - 7) | { left = true; right = false; up = false; down = true } -> (x - 7, y + 7) | { left = false; right = true; up = true; down = false } -> (x + 7, y - 7) | { left = false; right = true; up = false; down = true } -> (x + 7, y + 7) | _ -> (x, y) in let sw, sh = player.p_size in let w, h = (sw / 3, sh / 3) in let x = min (max _x 0) (width - w) and y = min (max _y 0) (height - h) in { player with p_pos = (x, y) } let step_player_bullets game_state = let p_bullets = game_state.p_bullets |> List.map (fun (x, y) -> (x, y - 8)) |> List.filter (fun (x, y) -> y > -20) in { game_state with p_bullets } let player_shooting player p_bullets t = if player.p_shooting && t - player.p_last_shot > player.p_shoot_freq then (* shoot *) let x, y = player.p_pos in let sw, sh = player.p_size in let w, h = (sw / 3, sh / 3) in let bx = x + ((w - 20) / 2) in let by = y + ((h - 20) / 2) in let bullet = (bx, by) in let player = { player with p_last_shot = t } in (player, bullet :: p_bullets) else (player, p_bullets) let step_player game_state t = let player = player_moving game_state.player in let player, p_bullets = player_shooting player game_state.p_bullets t in { game_state with player; p_bullets } let step_background background = Array.iteri (fun y row -> Array.iteri (fun x _ -> let v = 60 + Random.int 20 in if Random.int 3200 = 0 then background.(y).(x) <- (v, v, v) ) row ) background let update_game game_state game_data t = step_background background; let game_state = step_foes game_state game_data t in let game_state = step_foes_bullets game_state t in let game_state = step_player_bullets game_state in let game_state = step_player game_state t in (game_state) let init_game () = ctx.imageSmoothingEnabled <- false; let fb_pattern = [| [| 0; 0; 0; 0; 0 |]; [| 0; 1; 0; 1; 0 |]; [| 0; 0; 0; 0; 0 |]; [| 0; 1; 0; 1; 0 |]; [| 0; 0; 0; 0; 0 |]; |] in let pb_pattern = [| [| 1; 0; 0; 0; 1 |]; [| 1; 0; 0; 0; 1 |]; [| 0; 0; 0; 0; 0 |]; [| 1; 0; 0; 0; 1 |]; [| 1; 0; 0; 0; 1 |]; |] in let f_bullet_tex = texture_of_pattern fb_pattern ~color:yellow in let p_bullet_tex = texture_of_pattern pb_pattern ~color:green in let tex_path1 = "./assets/Commander/deep-space-ships/" in let tex_path2 = "./assets/Commander/Realistic_Ships/" in let tex_path3 = "./assets/Commander/spaceshipspack/" in let tex_path4 = "./assets/Commander/Space_Ships_pack3/" in let tex_path5 = "./assets/Commander/space_ships_pack4-1/" in let tex_path6 = "./assets/Commander/spaceships-sprites-super-pack/" in let player_texture = Canvas.newImage () in Canvas.setImgSrc player_texture (tex_path1 ^ "dipy16-rev.png"); let tex_w, tex_h = (134, 139) in let player = { p_pos = (width / 2 - (tex_w / 3) / 2, height - 90); p_last_shot = 0; p_shoot_freq = 220; p_shooting = false; p_game_over = false; p_size = (tex_w, tex_h); p_dir = { left = false; right = false; up = false; down = false; }; p_texture = player_texture; p_texture_ok = false; } in (* report when the sprite sheet is loaded *) Canvas.imgOnload player_texture (fun () -> player.p_texture_ok <- true; ); let foe_tex_files = [| tex_path1 ^ "dipy01b.png"; tex_path1 ^ "dipy02.png"; tex_path1 ^ "dipy03.png"; tex_path1 ^ "dipy04.png"; tex_path1 ^ "dipy05.png"; tex_path1 ^ "dipy5a.png"; tex_path1 ^ "dipy08.png"; tex_path1 ^ "dipy09.png"; tex_path1 ^ "dipy10.png"; tex_path1 ^ "dipy12.png"; tex_path1 ^ "dipy12a.png"; tex_path1 ^ "dipy14.png"; tex_path1 ^ "dipy15.png"; tex_path1 ^ "dipy17.png"; tex_path1 ^ "dipy18.png"; tex_path2 ^ "a2.png"; tex_path2 ^ "a3.png"; tex_path2 ^ "a4.png"; tex_path2 ^ "a6.png"; tex_path2 ^ "a7.png"; tex_path3 ^ "a01.png"; tex_path3 ^ "a05.png"; tex_path3 ^ "a09.png"; tex_path3 ^ "a14.png"; tex_path3 ^ "a15.png"; tex_path3 ^ "a18.png"; tex_path4 ^ "a16.png"; tex_path4 ^ "a20.png"; tex_path4 ^ "a21.png"; tex_path4 ^ "a24.png"; tex_path4 ^ "a28.png"; tex_path4 ^ "a31.png"; tex_path4 ^ "a32.png"; tex_path4 ^ "a35.png"; tex_path4 ^ "a38.png"; tex_path5 ^ "a1.png"; tex_path5 ^ "a2.png"; tex_path5 ^ "a3.png"; tex_path5 ^ "a23.png"; tex_path5 ^ "a45.png"; tex_path6 ^ "supership1.png"; tex_path6 ^ "supership2.png"; tex_path6 ^ "supership3.png"; tex_path6 ^ "supership4.png"; tex_path6 ^ "supership5.png"; tex_path6 ^ "supership5a.png"; tex_path6 ^ "supership7.png"; tex_path6 ^ "supership8.png"; tex_path6 ^ "supership9.png"; tex_path6 ^ "supership10.png"; tex_path6 ^ "supership11.png"; tex_path6 ^ "supership12a.png"; tex_path6 ^ "supership12.png"; tex_path6 ^ "supership14.png"; tex_path6 ^ "supership15.png"; tex_path6 ^ "supership16.png"; tex_path6 ^ "supership17.png"; tex_path6 ^ "supership18.png"; tex_path6 ^ "supership19.png"; tex_path6 ^ "supership20.png"; tex_path6 ^ "supership21.png"; tex_path6 ^ "supership22.png"; tex_path6 ^ "supership23.png"; tex_path6 ^ "supership24.png"; tex_path6 ^ "supership25.png"; tex_path6 ^ "supership26.png"; tex_path6 ^ "supership27.png"; tex_path6 ^ "supership28.png"; tex_path6 ^ "supership29.png"; |] in (* load foe textures *) let f_textures = Array.map (fun foe_tex_file -> let foe_texture = Canvas.newImage () in let foe_texture_loaded = ref false in let w, h = ref 0, ref 0 in Canvas.setImgSrc foe_texture foe_tex_file; Canvas.imgOnload foe_texture (fun () -> foe_texture_loaded := true; w := (Canvas.naturalWidth foe_texture); h := (Canvas.naturalHeight foe_texture); ); (foe_texture, foe_texture_loaded, w, h) ) foe_tex_files in let game_state = { player; foes = []; p_bullets = []; f_bullets = []; } in let game_data = { f_bullet_tex; p_bullet_tex; f_textures; } in (game_state, game_data) let animate () = let t = Js.Date.now () in let t = int_of_float t in let t = t - t0 in let game_state = get_ref _game_state in let game_data = get_ref _game_data in let game_state = player_touched game_state in let game_over = game_state.player.p_game_over in let game_state = if game_over then game_state else update_game game_state game_data t in set_ref _game_state game_state; let playing = if game_over then false else true in display ~playing game_state game_data; ;; let () = let game_state, game_data = init_game () in set_ref _game_state game_state; set_ref _game_data game_data; Canvas.addKeyEventListener Canvas.window "keydown" (ev_keychange KeyDown) true; Canvas.addKeyEventListener Canvas.window "keyup" (ev_keychange KeyUp) true; let loop = Canvas.setInterval animate (1000/25) in ignore(loop); () ;;