gibdo.coffee |            |
|---|---|
|                                
  |                              |            
|                                Gibdo is a starting point for creating HTML5 Canvas games in a top-down 2D style. It is written in CoffeeScript and provides the following features, 
 Try it out here.  |                              |            
|                                Zepto.js is used for event handling  |                             $ = Zepto |            
|                                Create a new instance of the game and get it running.  |                             $ -> 
  game = new Game
  game.run() |            
                              GameThe game class handles top level game loop and initialisation.  |                             class Game |            
|                                Start the game in a default state and initiate the game loop. It attempts to run the loop every 1 millisecond but in reality the loop is just running as fast as it can.  |                               run: ->
    @setup()
    @reset()
    @then = Date.now()
    setInterval(@main, 1) |            
|                                Create a new game world and keyboard input handler  |                               setup: ->
    @world = new World
    @inputHandler = new InputHandler(@world) |            
|                                Nothing is reset at this level so just ask the world to reset itself.  |                               reset: -> @world.reset() |            
|                                The main game loop. Establish the time since the loop last ran in seconds and pass that through to the update method for recalculating sprite positions. After recalculation positions, render the sprites.  |                               main: =>
    now = Date.now()
    delta = now - @lastUpdate
    @lastUpdate = now
    @lastElapsed = delta
    @update(delta / 1000)
    @render() |            
|                                Updates are handled by the input handler.  |                               update: (modifier) -> @inputHandler.update(modifier) |            
|                                Tell the world to rerender itself.  |                               render: -> @world.render(@lastUpdate, @lastElapsed) |            
                              WorldThe World class manages the game world and what can be seen by the player.  |                             class World
  width: 512
  height: 480
  viewWidth: 400
  viewHeight: 300
  sprites: [] |            
|                                When the world is created it adds a canvas to the page and inserts all the sprites that are needed into the sprite array.  |                               constructor: ->
    @ctx = @createCanvas()
    @hero = new Hero(this)
    @column = new Column(this)
    @sprites.push(new Background(this))
    @sprites.push(new Monster(this))
    @sprites.push(@column)
    @sprites.push(@hero) |            
|                                Create an HTML5 canvas element and append it to the document  |                               createCanvas: ->
    canvas = document.createElement("canvas")
    canvas.width = @viewWidth
    canvas.height = @viewHeight
    $(".container .game").append(canvas)
    canvas.getContext("2d") |            
|                                Only the hero (player character) needs to be reset.  |                               reset: -> @hero.reset(@width, @height) |            
|                                The co-ordinates the hero occupies in the centre of the view.  |                               heroViewOffsetX: -> @hero.viewOffsetX(@viewWidth)
  heroViewOffsetY: -> @hero.viewOffsetY(@viewHeight) |            
|                                The maximum co-ordinates the view can scroll to.  |                               viewWidthLimit:  -> @width  - @viewWidth
  viewHeightLimit: -> @height - @viewHeight |            
|                                Check to see if the hero is at the limits of the world.  |                               atViewLimitLeft:   -> @hero.x < @heroViewOffsetX()
  atViewLimitTop:    -> @hero.y < @heroViewOffsetY()
  atViewLimitRight:  -> @hero.x > @viewWidthLimit() + @heroViewOffsetX()
  atViewLimitBottom: -> @hero.y > @viewHeightLimit() + @heroViewOffsetY() |            
|                                Tell all the sprites to render.  |                               render: (lastUpdate, lastElapsed) -> 
    sprite.draw() for sprite in @sprites
    @renderDebugOverlay(lastElapsed) |            
|                                Show the frames per second at the top of the view.  |                               renderDebugOverlay: (lastElapsed) ->
    @ctx.save()
    @ctx.fillStyle = "rgb(241, 241, 242)"
    @ctx.font = "Bold 20px Monospace"
    @ctx.fillText("#{Math.round(1e3 / lastElapsed)} FPS", 10, 20)
    @ctx.restore() |            
|                                Pass any keyboard events that come in from the input handler off to the hero.  |                               up:    (mod) -> @hero.up(mod)
  down:  (mod) -> @hero.down(mod, @height)
  left:  (mod) -> @hero.left(mod)
  right: (mod) -> @hero.right(mod, @width) |            
|                                Find the sprites that have collision detection enabled.  |                               collidableSprites: -> sprite for sprite in @sprites when sprite.collidable |            
                              InputHandlerResponsible for dealing with keyboard input.  |                             class InputHandler
  keysDown: {} |            
|                                Listen for keys being presses and being released. As this happens add and remove them from the key store.  |                               constructor: (@world) ->
    $("body").keydown (e) => @keysDown[e.keyCode] = true
    $("body").keyup (e)   => delete @keysDown[e.keyCode] |            
|                                Every time update is called from the game loop act on the currently pressed keys by passing the events on to the world.  |                               update: (modifier) ->
    @world.up(modifier)    if 38 of @keysDown
    @world.down(modifier)  if 40 of @keysDown
    @world.left(modifier)  if 37 of @keysDown
    @world.right(modifier) if 39 of @keysDown |            
                              SpriteImageWraps sprite loading.  |                             class SpriteImage
  ready: false
  url: "images/sheet.png" |            
|                                Create a new image based on the sprite file and set ready to true when loaded.  |                               constructor: ->
    image = new Image
    image.src = @url
    image.onload = => @ready = true
    @image = image |            
                              Sprite |                             class Sprite |            
|                                The base class from which all sprites get their draw function and default values from. Configure sane defaults for sprite positions and dimensions.  |                               sx: 0 # Source x position
  sy: 0 # Source y position
  sw: 0 # Source width
  sh: 0 # Source height
  dx: 0 # Destination x position
  dy: 0 # Destination y position
  dw: 0 # Destination width
  dh: 0 # Destination height
  x:  0 # Position x in the world
  y:  0 # Position y in the world
  image: new SpriteImage
  collidable: false
  constructor: (@world) -> |            
|                                If the image is loaded then draw the sprite on to the canvas.  |                               drawImage: (sx, sy, dx, dy) ->
    if @image.ready
      @world.ctx.drawImage(@image.image, sx, sy, @sw, @sh, dx, dy, @dw, @dh) |            
                              BackgroundThe sprite that represents the floor or level on which the other sprites walk around on.  |                             class Background extends Sprite |            
|                                As the background represents the entire world it's source image has the same dimensions.  |                               constructor: (world) ->
    @dw = world.viewWidth
    @dh = world.viewHeight
    @sw = world.viewWidth
    @sh = world.viewHeight
    super(world)
  draw: -> |            
|                                The background moves as the hero does.  |                                 x = @world.hero.x - @world.heroViewOffsetX()
    y = @world.hero.y - @world.heroViewOffsetY() |            
|                                Prevent the background from scrolling at the start of the world.  |                                 x = 0 if @world.atViewLimitLeft()
    y = 0 if @world.atViewLimitTop() |            
|                                Prevent the background from scrolling at the end of the world.  |                                 x = @world.viewWidthLimit() if @world.atViewLimitRight()
    y = @world.viewHeightLimit() if @world.atViewLimitBottom()
    @drawImage(x, y, @dx, @dy) |            
                              EntityEntities are non-background sprites that share a common draw function as they need to be offset from the player differently.  |                             class Entity extends Sprite
  draw: -> |            
|                                When the view is at the start of the world the sprites can be drawn at their full world co-ordinates.  |                                 @dx = @x if @world.atViewLimitLeft()
    @dy = @y if @world.atViewLimitTop() |            
|                                When the view is at the end of the world the sprites are drawn as an offset from the edge of the world.  |                                 @dx = @x - @world.viewWidthLimit() if @world.atViewLimitRight()
    @dy = @y - @world.viewHeightLimit() if @world.atViewLimitBottom()
    @drawImage(@sx, @sy, @dx, @dy) |            
                              MonsterAn example collidable stationary sprite.  |                             class Monster extends Entity
  x:  400
  y:  400
  sw: 30
  sh: 32
  dw: 30
  dh: 32
  sy: 480
  collidable: true |            
|                                Offset the view co-ordinates from the player.  |                               draw: -> 
    @dx = @x - @world.hero.x + @world.heroViewOffsetX()
    @dy = @y - @world.hero.y + @world.heroViewOffsetY()
    super |            
                              ColumnAnother example of a collidable stationary sprite.  |                             class Column extends Entity
  x:  300
  y:  300
  sw: 32
  sh: 32
  dw: 32
  dh: 32
  sy: 544
  collidable: true |            
|                                Offset the view co-ordinates from the player.  |                               draw: -> 
    @dx = @x - @world.hero.x + @world.heroViewOffsetX()
    @dy = @y - @world.hero.y + @world.heroViewOffsetY()
    super |            
                              Hero |                             class Hero extends Entity |            
|                                The sprite that represents the player and can be controlled and moved through the world.  |                               sw:    32
  sh:    30
  dw:    32
  dh:    30
  speed: 256
  sy:    513
  direction: 0
  draw: ->  |            
|                                By default the hero is drawn to the centre of the view.  |                                 @dx = @world.heroViewOffsetX()
    @dy = @world.heroViewOffsetY() |            
|                                Alternate sprite frames as the player's position changes to create an animation effect.  |                                 @sx = if Math.round(@x+@y)%64 < 32 then @direction else @direction + 32
    super |            
|                                The player's velocity is the default speed multiplied by the current time difference.  |                               velocity: (mod) -> @speed * mod |            
|                                Detect a collision between the proposed new player co-ordinates and the collidable objects in the world. If the player's co-ordinates fall within their bounds then it has collided.  |                               collision: (x, y) ->
    for o in @world.collidableSprites()
      return true if y > o.y - @dh and y < o.y + o.dh and x > o.x - @dw and x < o.x + o.dw
    false |            
|                                Handle keyboard input. By changing the  The player's position is modified in the direction of the key press if still inside the world and no collisions are detected.  |                               up: (mod) -> 
    @direction = 64
    y = @y - @velocity(mod)
    @y -= @velocity(mod) if y > 0 and !@collision(@x, y)
  down: (mod, height) -> 
    @direction = 0
    y = @y + @velocity(mod)
    @y += @velocity(mod) if y < height - @dh and !@collision(@x, y)
  left: (mod) -> 
    @direction = 128
    x = @x - @velocity(mod)
    @x -= @velocity(mod) if x > 0 and !@collision(x, @y)
  right: (mod, width) -> 
    @direction = 192
    x = @x + @velocity(mod)
    @x += @velocity(mod) if x < width - @dw and !@collision(x, @y) |            
|                                Helpers that the world uses to calculate the centre position of the hero.  |                               viewOffsetX: (width)  -> (width / 2)   - (@dw / 2)
  viewOffsetY: (height) -> (height / 2)  - (@dh / 2) |            
|                                The hero starts the game in the centre of the world.  |                               reset: (width, height) ->
    @x = @viewOffsetX(width)
    @y = @viewOffsetY(height)
 |