We keep information about Mario in a single record type with fields that
represent the current x and y coordinates (x
and y
), current velocity
(vx
and vy
) and the current direction dir
. The direction is used to
pick the correct Mario image when rendering:
1:
2:
3:
4:
|
type Mario =
{ x:float; y:float;
vx:float; vy:float;
dir:string }
|
The step function of the game takes previvous Mario
value and returns a new
one. It is composed from 4 functions that represent different aspects of the game.
The functions that depend on keyboard take the current keyboard state as the
first argument. This is represented as a tuple int*int
consisting of x and y
directions. For example, when the left key is pressed, the value is (-1, 0)
.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
|
// If the Up key is pressed (y > 0) and Mario is on the ground,
// then create Mario with the y velocity 'vy' set to 5
let jump (_,y) m =
if y > 0 && m.y = 0. then { m with vy = 5. } else m
// If Mario is in the air, then his "up" velocity is decreasing
let gravity m =
if m.y > 0. then { m with vy = m.vy - 0.1 } else m
// Apply physics - move Mario according to the current velocities
let physics m =
{ m with x = m.x + m.vx; y = max 0. (m.y + m.vy) }
// When Left/Right keys are pressed, change 'vx' and direction
let walk (x,_) m =
let dir = if x < 0 then "left" elif x > 0 then "right" else m.dir
{ m with vx = float x; dir = dir }
|
The step
function takes a dir
parameter representing the keyboard status
and a current Mario
state. It simply runs 4 components in a pipeline:
1:
2:
|
let step dir mario =
mario |> physics |> walk dir |> gravity |> jump dir
|
Now we're ready to render Mario using HTML 5 canvas! To do that, we need the
width and height of the canvas and the current state of Mario. The following
function fills the bottom half of the canvas with green, upper half with blue
and then chooses the right Mario image. It uses helpers from the Win
module, which are discussed below:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
|
/// Render mario on canvas
let render (w,h) (mario:Mario) =
// Render background
(0.,0.,w,h) |> Win.filled (Win.rgb 174 238 238)
(0.,h-50.,w,50.) |> Win.filled (Win.rgb 74 163 41)
// Select and position Mario
// (walking is represented as an animated gif)
let verb =
if mario.y > 0. then "jump"
elif mario.vx <> 0. then "walk"
else "stand"
"images/mario" + verb + mario.dir + ".gif"
|> Win.image
|> Win.position (w/2.-16.+mario.x, h-50.-31.-mario.y)
|
The last thing that needs to be done is to write the main
function that drives
the game. The function does some initialization and then starts a recursive update
function that calculates a new game state using step
and renders it in a loop.
The Keyboard
helper module is discussed below.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
|
// Some initialization
Keyboard.init()
let w,h = Win.dimensions()
// Recursive function that updates the state & renders it
let rec update mario () =
let mario = mario |> step (Keyboard.arrows())
render (w,h) mario
window.setTimeout(update mario, 1000. / 60.) |> ignore
// Start the game with Mario in the center
let mario = { x=0.; y=0.; vx=0.; vy=0.; dir="right" }
update mario ()
|
The Keyboard
module handles keydown and keyup events of the window and
exposes them using the arrows
property (which is a tuple int*int with -1 if the
left/up key is pressed, 1 if right/down key is pressed and 0 otherwise).
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
|
module Keyboard =
/// Set of currently pressed keys
let mutable keysPressed = Set.empty
/// Returns 1 if key with given code is pressed
let code x =
if keysPressed.Contains(x) then 1 else 0
/// Update the state of the set for given key event
let update (e : KeyboardEvent, pressed) =
let keyCode = int e.keyCode
let op = if pressed then Set.add else Set.remove
keysPressed <- op keyCode keysPressed
null
/// Returns pair with -1 for left or down and +1
/// for right or up (0 if no or both keys are pressed)
let arrows () =
(code 39 - code 37, code 38 - code 40)
let init () =
document.addEventListener_keydown(fun e -> update(e, true))
document.addEventListener_keyup(fun e -> update(e, false))
|
The Window
module contains basic functionality for creating and rendering
window using HTML5 canvas and moving images around. The functionsbelow
fill the window, set position of image and create image.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
|
module Win =
// Get the canvas context for drawing
let canvas = document.getElementsByTagName_canvas().[0.]
let context = canvas.getContext_2d()
// Format RGB color as "rgb(r,g,b)"
let ($) s n = s + n.ToString()
let rgb r g b = "rgb(" $ r $ "," $ g $ "," $ b $ ")"
/// Fill rectangle with given color
let filled color rect =
let ctx = context
ctx.fillStyle <- U3.Case1 color
ctx.fillRect rect
/// Move element to a specified X Y position
let position (x,y) (img : HTMLImageElement) =
img.style.left <- x.ToString() + "px"
img.style.top <- (canvas.offsetTop + y).ToString() + "px"
let dimensions () =
canvas.width, canvas.height
/// Get the first <img /> element and set `src` (do
/// nothing if it is the right one to keep animation)
let image (src:string) =
let image = document.getElementsByTagName_img().[0.]
if image.src.IndexOf(src) = -1 then image.src <- src
image
|
namespace Fable
namespace Fable.Core
namespace Fable.Import
module Browser
from Fable.Import
val max : a:'a -> b:'a -> 'a (requires comparison)
Full name: Mario.max
val a : 'a (requires comparison)
val b : 'a (requires comparison)
val mutable keysPressed : Set<int>
Full name: Mario.Keyboard.keysPressed
Set of currently pressed keys
Multiple items
module Set
from Microsoft.FSharp.Collections
--------------------
type Set<'T (requires comparison)> =
interface IComparable
interface IEnumerable
interface IEnumerable<'T>
interface ICollection<'T>
new : elements:seq<'T> -> Set<'T>
member Add : value:'T -> Set<'T>
member Contains : value:'T -> bool
override Equals : obj -> bool
member IsProperSubsetOf : otherSet:Set<'T> -> bool
member IsProperSupersetOf : otherSet:Set<'T> -> bool
...
Full name: Microsoft.FSharp.Collections.Set<_>
--------------------
new : elements:seq<'T> -> Set<'T>
val empty<'T (requires comparison)> : Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Collections.Set.empty
val code : x:int -> int
Full name: Mario.Keyboard.code
Returns 1 if key with given code is pressed
val x : int
member Set.Contains : value:'T -> bool
val update : e:KeyboardEvent * pressed:bool -> 'a (requires 'a : null)
Full name: Mario.Keyboard.update
Update the state of the set for given key event
val e : KeyboardEvent
Multiple items
val KeyboardEvent : KeyboardEventType
Full name: Fable.Import.Browser.KeyboardEvent
--------------------
type KeyboardEvent =
interface
inherit UIEvent
abstract member getModifierState : keyArg:string -> bool
abstract member DOM_KEY_LOCATION_JOYSTICK : float
abstract member DOM_KEY_LOCATION_LEFT : float
abstract member DOM_KEY_LOCATION_MOBILE : float
abstract member DOM_KEY_LOCATION_NUMPAD : float
abstract member DOM_KEY_LOCATION_RIGHT : float
abstract member DOM_KEY_LOCATION_STANDARD : float
abstract member altKey : bool
abstract member char : string
...
end
Full name: Fable.Import.Browser.KeyboardEvent
val pressed : bool
val keyCode : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
property KeyboardEvent.keyCode: float
val op : (int -> Set<int> -> Set<int>)
val add : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Collections.Set.add
val remove : value:'T -> set:Set<'T> -> Set<'T> (requires comparison)
Full name: Microsoft.FSharp.Collections.Set.remove
val arrows : unit -> int * int
Full name: Mario.Keyboard.arrows
Returns pair with -1 for left or down and +1
for right or up (0 if no or both keys are pressed)
val init : unit -> unit
Full name: Mario.Keyboard.init
val document : Document
Full name: Fable.Import.Browser.document
abstract member Document.addEventListener_keydown : listener:System.Func<KeyboardEvent,obj> * ?useCapture:bool -> unit
abstract member Document.addEventListener_keyup : listener:System.Func<KeyboardEvent,obj> * ?useCapture:bool -> unit
val canvas : HTMLCanvasElement
Full name: Mario.Win.canvas
abstract member Document.getElementsByTagName_canvas : unit -> NodeListOf<HTMLCanvasElement>
val context : CanvasRenderingContext2D
Full name: Mario.Win.context
abstract member HTMLCanvasElement.getContext_2d : unit -> CanvasRenderingContext2D
val s : string
val n : 'a
System.Object.ToString() : string
val rgb : r:'a -> g:'b -> b:'c -> string
Full name: Mario.Win.rgb
val r : 'a
val g : 'b
val b : 'c
val filled : color:string -> float * float * float * float -> unit
Full name: Mario.Win.filled
Fill rectangle with given color
val color : string
val rect : float * float * float * float
val ctx : CanvasRenderingContext2D
property CanvasRenderingContext2D.fillStyle: U3<string,CanvasGradient,CanvasPattern>
type U3<'a,'b,'c> =
| Case1 of 'a
| Case2 of 'b
| Case3 of 'c
Full name: Fable.Core.U3<_,_,_>
union case U3.Case1: 'a -> U3<'a,'b,'c>
abstract member CanvasRenderingContext2D.fillRect : x:float * y:float * w:float * h:float -> unit
val position : x:'a * y:float -> img:HTMLImageElement -> unit
Full name: Mario.Win.position
Move element to a specified X Y position
val x : 'a
val y : float
val img : HTMLImageElement
Multiple items
val HTMLImageElement : HTMLImageElementType
Full name: Fable.Import.Browser.HTMLImageElement
--------------------
type HTMLImageElement =
interface
inherit HTMLElement
abstract member align : string
abstract member alt : string
abstract member border : string
abstract member complete : bool
abstract member crossOrigin : string
abstract member currentSrc : string
abstract member height : float
abstract member hspace : float
abstract member isMap : bool
...
end
Full name: Fable.Import.Browser.HTMLImageElement
property HTMLElement.style: CSSStyleDeclaration
property CSSStyleDeclaration.left: string
property CSSStyleDeclaration.top: string
property HTMLElement.offsetTop: float
val dimensions : unit -> float * float
Full name: Mario.Win.dimensions
property HTMLCanvasElement.width: float
property HTMLCanvasElement.height: float
val image : src:string -> HTMLImageElement
Full name: Mario.Win.image
Get the first <img /> element and set `src` (do
nothing if it is the right one to keep animation)
val src : string
Multiple items
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
val image : HTMLImageElement
abstract member Document.getElementsByTagName_img : unit -> NodeListOf<HTMLImageElement>
property HTMLImageElement.src: string
System.String.IndexOf(value: string) : int
System.String.IndexOf(value: char) : int
System.String.IndexOf(value: string, comparisonType: System.StringComparison) : int
System.String.IndexOf(value: string, startIndex: int) : int
System.String.IndexOf(value: char, startIndex: int) : int
System.String.IndexOf(value: string, startIndex: int, comparisonType: System.StringComparison) : int
System.String.IndexOf(value: string, startIndex: int, count: int) : int
System.String.IndexOf(value: char, startIndex: int, count: int) : int
System.String.IndexOf(value: string, startIndex: int, count: int, comparisonType: System.StringComparison) : int
type Mario =
{x: float;
y: float;
vx: float;
vy: float;
dir: string;}
Full name: Mario.Mario
Mario.x: float
Multiple items
val float : value:'T -> float (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.float
--------------------
type float = System.Double
Full name: Microsoft.FSharp.Core.float
--------------------
type float<'Measure> = float
Full name: Microsoft.FSharp.Core.float<_>
Mario.y: float
Mario.vx: float
Mario.vy: float
Mario.dir: string
val jump : 'a * y:int -> m:Mario -> Mario
Full name: Mario.jump
val y : int
val m : Mario
val gravity : m:Mario -> Mario
Full name: Mario.gravity
val physics : m:Mario -> Mario
Full name: Mario.physics
val walk : x:int * 'a -> m:Mario -> Mario
Full name: Mario.walk
val dir : string
val step : int * int -> mario:Mario -> Mario
Full name: Mario.step
val dir : int * int
val mario : Mario
val render : w:float * h:float -> mario:Mario -> unit
Full name: Mario.render
Render mario on canvas
val w : float
val h : float
module Win
from Mario
val verb : string
module Keyboard
from Mario
val w : float
Full name: Mario.w
val h : float
Full name: Mario.h
val update : mario:Mario -> unit -> unit
Full name: Mario.update
val window : Window
Full name: Fable.Import.Browser.window
abstract member WindowTimers.setTimeout : handler:obj * ?timeout:obj * [<System.ParamArray>] args:obj [] -> float
val ignore : value:'T -> unit
Full name: Microsoft.FSharp.Core.Operators.ignore
val mario : Mario
Full name: Mario.mario