Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Playing with Ruby

Playing with Ruby

A talk I gave at the RedDotRubyConf 2013 on how to create a simple online, real-time multi-player game using Gosu and Celluloid::IO.

Sau Sheong Chang

June 08, 2013
Tweet

More Decks by Sau Sheong Chang

Other Decks in Technology

Transcript

  1. Playing with Ruby
    How to write an online, real-time
    multi-player game with Ruby
    @sausheong
    Sunday, 9 June, 13

    View Slide

  2. About me
    Sunday, 9 June, 13

    View Slide

  3. About me
    Sunday, 9 June, 13

    View Slide

  4. About me
    Sunday, 9 June, 13

    View Slide

  5. About me
    Sunday, 9 June, 13

    View Slide

  6. About me
    Sunday, 9 June, 13

    View Slide

  7. About me
    Sunday, 9 June, 13

    View Slide

  8. About me
    Sunday, 9 June, 13

    View Slide

  9. About me
    Sunday, 9 June, 13

    View Slide

  10. About me
    Sunday, 9 June, 13

    View Slide

  11. About me
    Sunday, 9 June, 13

    View Slide

  12. Press start
    Sunday, 9 June, 13

    View Slide

  13. Sunday, 9 June, 13

    View Slide

  14. 2D game development library
    C++-based, with Ruby wrapper
    OS X, Windows and Linux
    Works with MRI, MacRuby, Rubinius
    (but not JRuby)
    Sunday, 9 June, 13

    View Slide

  15. Draw the game
    window
    Sunday, 9 June, 13

    View Slide

  16. require 'gosu'
    class GameWindow < Gosu::Window
    def initialize
    super 640, 480, false
    self.caption = "Tutorial 1"
    end
    def update
    end
    def draw
    end
    end
    window = GameWindow.new
    window.show
    Sunday, 9 June, 13

    View Slide

  17. Sunday, 9 June, 13

    View Slide

  18. update method
    called at every frame (60 frames per
    second)
    Contains game logic
    The main ‘controller’ of the game
    Sunday, 9 June, 13

    View Slide

  19. draw method
    Does the actual drawing of the game
    window
    Called after update
    Can also be called when necessary
    Sunday, 9 June, 13

    View Slide

  20. Add background
    Sunday, 9 June, 13

    View Slide

  21. require 'gosu'
    class GameWindow < Gosu::Window
    def initialize
    super 640, 480, false
    self.caption = "Tutorial 2"
    @background_image = Gosu::Image.new(self, "bg1.jpg", true)
    end
    def update
    end
    def draw
    @background_image.draw(0, 0, 0)
    end
    end
    window = GameWindow.new
    window.show
    x
    y
    z
    Sunday, 9 June, 13

    View Slide

  22. Sunday, 9 June, 13

    View Slide

  23. Add a player
    Sunday, 9 June, 13

    View Slide

  24. class Player
    def initialize(window)
    @image = Image.new window, "plane.png", false
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    end
    def warp(x, y)
    @x, @y = x, y
    end
    def turn_left
    @angle -= 5
    end
    def turn_right
    @angle += 5
    end
    def accelerate
    @vel_x += offset_x(@angle, 5)
    @vel_y += offset_y(@angle, 5)
    end
    def move
    @x += @vel_x
    @y += @vel_y
    @x %= 640
    @y %= 480
    @vel_x, @vel_y = 0, 0
    end
    def draw
    @image.draw_rot(@x, @y, 1, @angle)
    end
    end
    @angle
    5
    offset_y
    offset_x
    Sunday, 9 June, 13

    View Slide

  25. class GameWindow < Window
    def initialize
    super(640, 480, false)
    self.caption = "Tutorial 3"
    @background_image = Image.new(self, "bg1.jpg", true)
    @player = Player.new(self)
    @player.warp(320, 240)
    end
    def update
    @player.turn_left if button_down? KbLeft
    @player.turn_right if button_down? KbRight
    @player.accelerate if button_down? KbUp
    @player.move
    end
    def draw
    @player.draw
    @background_image.draw(0, 0, 0)
    end
    end
    Sunday, 9 June, 13

    View Slide

  26. class GameWindow < Window
    def initialize
    super(640, 480, false)
    self.caption = "Tutorial 3"
    @background_image = Image.new(self, "bg1.jpg", true)
    @player = Player.new(self)
    @player.warp(320, 240)
    end
    def update
    @player.turn_left if button_down? KbLeft
    @player.turn_right if button_down? KbRight
    @player.accelerate if button_down? KbUp
    @player.move
    end
    def draw
    @player.draw
    @background_image.draw(0, 0, 0)
    end
    end
    create player
    Sunday, 9 June, 13

    View Slide

  27. class GameWindow < Window
    def initialize
    super(640, 480, false)
    self.caption = "Tutorial 3"
    @background_image = Image.new(self, "bg1.jpg", true)
    @player = Player.new(self)
    @player.warp(320, 240)
    end
    def update
    @player.turn_left if button_down? KbLeft
    @player.turn_right if button_down? KbRight
    @player.accelerate if button_down? KbUp
    @player.move
    end
    def draw
    @player.draw
    @background_image.draw(0, 0, 0)
    end
    end
    place him in middle of screen
    Sunday, 9 June, 13

    View Slide

  28. class GameWindow < Window
    def initialize
    super(640, 480, false)
    self.caption = "Tutorial 3"
    @background_image = Image.new(self, "bg1.jpg", true)
    @player = Player.new(self)
    @player.warp(320, 240)
    end
    def update
    @player.turn_left if button_down? KbLeft
    @player.turn_right if button_down? KbRight
    @player.accelerate if button_down? KbUp
    @player.move
    end
    def draw
    @player.draw
    @background_image.draw(0, 0, 0)
    end
    end
    } move according to
    user input
    Sunday, 9 June, 13

    View Slide

  29. class GameWindow < Window
    def initialize
    super(640, 480, false)
    self.caption = "Tutorial 3"
    @background_image = Image.new(self, "bg1.jpg", true)
    @player = Player.new(self)
    @player.warp(320, 240)
    end
    def update
    @player.turn_left if button_down? KbLeft
    @player.turn_right if button_down? KbRight
    @player.accelerate if button_down? KbUp
    @player.move
    end
    def draw
    @player.draw
    @background_image.draw(0, 0, 0)
    end
    end
    draw the player
    Sunday, 9 June, 13

    View Slide

  30. Sunday, 9 June, 13

    View Slide

  31. Add sound
    Sunday, 9 June, 13

    View Slide

  32. def initialize(window)
    @image = Image.new window, "plane.png", false
    @sound = Sample.new window, "plane.wav"
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    end
    .
    .
    .
    def accelerate
    @sound.play
    @vel_x += offset_x(@angle, 5)
    @vel_y += offset_y(@angle, 5)
    end
    Sunday, 9 June, 13

    View Slide

  33. def initialize(window)
    @image = Image.new window, "plane.png", false
    @sound = Sample.new window, "plane.wav"
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    end
    .
    .
    .
    def accelerate
    @sound.play
    @vel_x += offset_x(@angle, 5)
    @vel_y += offset_y(@angle, 5)
    end
    Load the sound
    Sunday, 9 June, 13

    View Slide

  34. def initialize(window)
    @image = Image.new window, "plane.png", false
    @sound = Sample.new window, "plane.wav"
    @x = @y = @vel_x = @vel_y = @angle = 0.0
    end
    .
    .
    .
    def accelerate
    @sound.play
    @vel_x += offset_x(@angle, 5)
    @vel_y += offset_y(@angle, 5)
    end
    play the sound!
    Sunday, 9 June, 13

    View Slide

  35. Demo
    Sunday, 9 June, 13

    View Slide

  36. Level 1
    Complete!
    Sunday, 9 June, 13

    View Slide

  37. Use sprite sheets
    Sunday, 9 June, 13

    View Slide

  38. Sprites
    An image or animation that’s overlaid
    on the background
    Use single sprites (as before) or use
    sprite sheets
    Sprites normally represented by a
    square image
    Sunday, 9 June, 13

    View Slide

  39. Sprite sheet
    A bunch of images in a single file, used
    as sprites
    Often placed in sequence, image can be
    retrieved from knowing the location
    Reduces memory usage and increase
    drawing speed
    Sunday, 9 June, 13

    View Slide

  40. Sunday, 9 June, 13

    View Slide

  41. 59
    48
    25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
    24
    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
    0
    72
    100 101 102
    96
    Sunday, 9 June, 13

    View Slide

  42. module SpriteImage
    Grass = 102
    Earth = 101
    Gravel = 100
    Wall = 59
    Bullet= 28
    Tank = 39
    end
    Locate sprites in a
    sprite sheet
    Sunday, 9 June, 13

    View Slide

  43. Player:
    def initialize(window)
    @image = Image.new window, "plane.png", false
    end
    def draw
    @image.draw_rot(@x, @y, 1, @angle)
    end
    GameWindow:
    @spritesheet = Image.load_tiles(self, 'sprites.png', 32, 32,
    true)
    Player:
    @window.spritesheet[SpriteImage::Tank].draw_rot(@x, @y, 1,
    @angle)
    Sunday, 9 June, 13

    View Slide

  44. Create editable maps
    Sunday, 9 June, 13

    View Slide

  45. Editable maps
    Allows user to customize maps and
    backgrounds, using tiled sprites
    .....................
    .....................
    .....................
    .....................
    .....................
    ..##............##...
    ...#............#....
    ...#............#....
    ...#............#....
    ..##............##...
    .....................
    .....................
    .....................
    .....................
    .....................
    Sunday, 9 June, 13

    View Slide

  46. .....................
    .....................
    .....................
    .....................
    .....................
    ..##............##...
    ...#............#....
    ...#............#....
    ...#............#....
    ..##............##...
    .....................
    .....................
    .....................
    .....................
    .....................
    20 x 32 = 640
    15 x 32 = 480
    class Map
    def initialize(window, mapfile)
    lines = File.readlines(mapfile).map
    { |line| line.chomp }
    @window, @height, @width = window,
    lines.size, lines.first.size
    @tiles = Array.new(@width) do |x|
    Array.new(@height) do |y|
    case lines[y][x]
    when '.'
    SpriteImage::Earth
    when "#"
    SpriteImage::Wall
    when '"'
    SpriteImage::Grass
    end
    end
    end
    end
    def draw
    @height.times do |y|
    @width.times do |x|
    tile = @tiles[x][y]
    @window.spritesheet[tile].draw(x *
    32, y * 32, 1)
    end
    end
    Sunday, 9 June, 13

    View Slide

  47. Level 2
    Complete!
    Sunday, 9 June, 13

    View Slide

  48. Let’s play with
    others
    Sunday, 9 June, 13

    View Slide

  49. Sunday, 9 June, 13

    View Slide

  50. Design
    Real-time vs turn-based (immediate
    response)
    Latency (speed) is critical
    ‘Dead’ players can still observe the game
    Game spectators
    Sunday, 9 June, 13

    View Slide

  51. Design
    Client-server
    All artifacts are local
    Only messages sent back and forth the
    client-server
    Minimal size messages
    Messages sent from client -> server once
    every frame refresh
    Sunday, 9 June, 13

    View Slide

  52. Design
    Server should have minimal processing, all
    game logic should be in the client
    Server should only receive messages and
    broadcast to all clients
    Messages not compressed/encoded (takes
    time at the server)
    Don’t send useless messages
    Sunday, 9 June, 13

    View Slide

  53. Game flow
    Sunday, 9 June, 13

    View Slide

  54. Game server starts
    Game
    Server
    Sunday, 9 June, 13

    View Slide

  55. Tank 1 starts, sends
    message to server
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank 1 ignores
    messages that is
    about itself
    Sunday, 9 June, 13

    View Slide

  56. Server simply stores and broadcasts all
    messages sent to it to reduce processing
    Logic to process or ignore messages are
    in the client
    Sunday, 9 June, 13

    View Slide

  57. Tank 2 starts
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank2
    object:tank1
    object:tank2
    object:tank2
    Tank 1 receives messages
    from server about Tank 2,
    starts drawing Tank 2
    Sunday, 9 June, 13

    View Slide

  58. Tank 2 moves
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank2
    object:tank1
    object:tank2
    object:tank2
    When Tank 2 moves, its
    position is sent to the server
    and broadcast to everyone
    Sunday, 9 June, 13

    View Slide

  59. Tank 1 shoots
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank2
    object:tank1
    object:tank2
    object:tank2
    object:shot1
    object:shot1
    object:shot1
    Tank 1 creates a shot,
    message sent to server and
    broadcast to everyone
    Sunday, 9 June, 13

    View Slide

  60. Shot goes out of range
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank2
    object:tank1
    object:tank2
    object:tank2
    delete:shot1
    delete:shot1
    delete:shot1
    When the shot goes out of
    range, Tank 1 sends a delete
    message to the server,
    broadcasted to everyone
    Sunday, 9 June, 13

    View Slide

  61. Tank 1 shot hits Tank 2
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank2
    object:tank1
    object:tank2
    object:tank2
    object:shot1
    object:shot1
    object:shot1
    If Tank 1’s shot hits Tank 2,
    reduce hit points from Tank1
    Sunday, 9 June, 13

    View Slide

  62. Tank 2 destroyed
    Game
    Server
    Tank1
    object:tank1
    object:tank1
    Tank2
    object:tank1
    object:shot1
    object:shot1
    object:shot1
    When Tank 2’s hit points fall
    below 0 it is destroyed
    Sunday, 9 June, 13

    View Slide

  63. Message passing
    Sunday, 9 June, 13

    View Slide

  64. Messages are string delimited with
    vertical bar (|)
    Messages are accumulated till and sent
    only 1 time in a frame refresh
    Messages from client -> server :
    message type + sprite
    Message from server -> client : sprite
    only
    Sunday, 9 June, 13

    View Slide

  65. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    Sunday, 9 June, 13

    View Slide

  66. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    ‘obj’ or ‘del’
    Sunday, 9 June, 13

    View Slide

  67. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    a unique identifier
    for the sprite
    Sunday, 9 June, 13

    View Slide

  68. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    tank or shot
    Sunday, 9 June, 13

    View Slide

  69. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    player name
    Sunday, 9 June, 13

    View Slide

  70. "#{msg_type}|
    #{sprite.uuid}|
    #{sprite.type}|
    #{sprite.sprite_image}|
    #{sprite.player}|
    #{sprite.x}|
    #{sprite.y}|
    #{sprite.angle}|
    #{sprite.points}|
    #{sprite.color}"
    only valid
    for tanks
    Sunday, 9 June, 13

    View Slide

  71. Game Client
    Sunday, 9 June, 13

    View Slide

  72. Client sends
    messages to the
    server
    Sunday, 9 June, 13

    View Slide

  73. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    Sunday, 9 June, 13

    View Slide

  74. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    store my previous coordinates
    Sunday, 9 June, 13

    View Slide

  75. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    move!
    Sunday, 9 June, 13

    View Slide

  76. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    go back to previous
    coordinates if I hit the
    wall, go out or hit
    another tank
    Sunday, 9 June, 13

    View Slide

  77. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    add me to the list of
    messages to send to
    server
    Sunday, 9 June, 13

    View Slide

  78. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    check the other shots
    on screen to see if it
    hits me, if it does, tell
    the server I was hit
    Sunday, 9 June, 13

    View Slide

  79. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    move my shots, if it
    hits the wall or goes
    out, remove it
    Sunday, 9 June, 13

    View Slide

  80. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    if not, tell the server
    its new position
    Sunday, 9 June, 13

    View Slide

  81. def update
    begin
    move_tank
    px, py = @me.x, @me.y
    @me.move
    @me.warp_to(px, py) if @me.hit_wall? or @me.outside_battlefield?
    @other_tanks.each do |player, tank|
    @me.warp_to(px, py) if tank.alive? and @me.collide_with?(tank, 30)
    end
    add_to_message_queue('obj', @me)
    @other_shots.each_value do |shot|
    if @me.alive? and @me.collide_with?(shot, 16)
    @me.hit
    add_to_message_queue('obj', @me)
    end
    end
    @me_shots.each do |shot|
    shot.move # move the bullet
    if shot.hit_wall? or shot.outside_battlefield?
    @me_shots.delete shot
    add_to_message_queue('del', shot)
    else
    add_to_message_queue('obj', shot)
    end
    end
    @client.send_message @messages.join("\n")
    @messages.clear
    all my actions are
    processed, now to
    send messages to
    server
    Sunday, 9 June, 13

    View Slide

  82. "msg_type|uuid|type|sprite_image|player|x|y|angle|points|color"
    "msg_type|uuid|type|sprite_image|player|x|y|angle|points|color"
    "msg_type|uuid|type|sprite_image|player|x|y|angle|points|color"
    "msg_type|uuid|type|sprite_image|player|x|y|angle|points|color"
    "msg_type|uuid|type|sprite_image|player|x|y|angle|points|color"
    client message
    Sunday, 9 June, 13

    View Slide

  83. Client reads
    messages from the
    server
    Sunday, 9 June, 13

    View Slide

  84. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    Sunday, 9 June, 13

    View Slide

  85. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    read messages from the server
    Sunday, 9 June, 13

    View Slide

  86. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    parse the server messages into
    sprites
    Sunday, 9 June, 13

    View Slide

  87. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    for tank sprites other than me,
    set the properties and move it
    Sunday, 9 June, 13

    View Slide

  88. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    only time the server tells
    me about my changes is
    when I’m hit
    Sunday, 9 June, 13

    View Slide

  89. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    move the shot
    sprites
    Sunday, 9 June, 13

    View Slide

  90. if msg = @client.read_message
    @valid_sprites.clear
    data = msg.split("\n")
    data.each do |row|
    sprite = row.split("|")
    if sprite.size == 9
    player = sprite[3]
    @valid_sprites << sprite[0]
    case sprite[1]
    when 'tank'
    unless player == @player
    if @other_tanks[player]
    @other_tanks[player].points = sprite[7].to_i
    @other_tanks[player].warp_to(sprite[4], sprite[5], sprite[6])
    else
    @other_tanks[player] = Tank.from_sprite(self, sprite)
    end
    else
    @me.points = sprite[7].to_i
    end
    when 'shot'
    unless player == @player
    shot = Shot.from_sprite(self, sprite)
    @other_shots[shot.uuid] = shot
    shot.warp_to(sprite[4], sprite[5], sprite[6])
    end
    end
    end
    end
    Sunday, 9 June, 13

    View Slide

  91. "uuid|type|sprite_image|player|x|y|angle|points|color"
    server message
    "uuid|type|sprite_image|player|x|y|angle|points|color"
    "uuid|type|sprite_image|player|x|y|angle|points|color"
    "uuid|type|sprite_image|player|x|y|angle|points|color"
    "uuid|type|sprite_image|player|x|y|angle|points|color"
    Sunday, 9 June, 13

    View Slide

  92. @other_shots.delete_if do |uuid, shot|
    !@valid_sprites.include?(uuid)
    end
    @other_tanks.delete_if do |user, tank|
    !@valid_sprites.include?(tank.uuid)
    end
    end if shots and tanks (other than myself)
    weren’t broadcast from the server, this
    means they’ve been removed
    Sunday, 9 June, 13

    View Slide

  93. Level 3
    Complete!
    Sunday, 9 June, 13

    View Slide

  94. Game Server
    Sunday, 9 June, 13

    View Slide

  95. Sunday, 9 June, 13

    View Slide

  96. Sunday, 9 June, 13

    View Slide

  97. Event-driven IO library based on
    Celluloid
    Duck types Ruby IO classes (TCPSocket,
    TCPServer etc)
    Celluloid combines OO with concurrent
    programming, simplifies building
    multithreaded programs
    Sunday, 9 June, 13

    View Slide

  98. require 'celluloid/io'
    class Arena
    include Celluloid::IO
    finalizer :shutdown
    def initialize(host, port)
    puts "Starting Tanks Arena at #{host}:#{port}."
    @server = TCPServer.new(host, port)
    @sprites = Hash.new
    @players = Hash.new
    async.run
    end
    def shutdown
    @server.close if @server
    end
    def run
    loop { async.handle_connection @server.accept }
    end
    Sunday, 9 June, 13

    View Slide

  99. require 'celluloid/io'
    class Arena
    include Celluloid::IO
    finalizer :shutdown
    def initialize(host, port)
    puts "Starting Tanks Arena at #{host}:#{port}."
    @server = TCPServer.new(host, port)
    @sprites = Hash.new
    @players = Hash.new
    async.run
    end
    def shutdown
    @server.close if @server
    end
    def run
    loop { async.handle_connection @server.accept }
    end
    What to do when the
    server terminates
    Sunday, 9 June, 13

    View Slide

  100. require 'celluloid/io'
    class Arena
    include Celluloid::IO
    finalizer :shutdown
    def initialize(host, port)
    puts "Starting Tanks Arena at #{host}:#{port}."
    @server = TCPServer.new(host, port)
    @sprites = Hash.new
    @players = Hash.new
    async.run
    end
    def shutdown
    @server.close if @server
    end
    def run
    loop { async.handle_connection @server.accept }
    end
    Run the Arena object
    in a new thread
    Sunday, 9 June, 13

    View Slide

  101. require 'celluloid/io'
    class Arena
    include Celluloid::IO
    finalizer :shutdown
    def initialize(host, port)
    puts "Starting Tanks Arena at #{host}:#{port}."
    @server = TCPServer.new(host, port)
    @sprites = Hash.new
    @players = Hash.new
    async.run
    end
    def shutdown
    @server.close if @server
    end
    def run
    loop { async.handle_connection @server.accept }
    end
    When a client connects,
    handle the connection in
    a new thread
    Sunday, 9 June, 13

    View Slide

  102. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Sunday, 9 June, 13

    View Slide

  103. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Uniquely
    identifies a user
    Sunday, 9 June, 13

    View Slide

  104. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Get data from
    the client
    Sunday, 9 June, 13

    View Slide

  105. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Add to list of players
    if player is new
    Sunday, 9 June, 13

    View Slide

  106. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Add to list of
    sprites in
    this server
    Sunday, 9 June, 13

    View Slide

  107. def handle_connection(socket)
    _, port, host = socket.peeraddr
    user = "#{host}:#{port}"
    puts "#{user} has joined the arena."
    loop do
    data = socket.readpartial(4096)
    data_array = data.split("\n")
    if data_array and !data_array.empty?
    begin
    data_array.each do |row|
    message = row.split("|")
    if message.size == 10
    case message[0]
    when 'obj'
    @players[user] = message[1..9] unless @players[user]
    @sprites[message[1]] = message[1..9]
    when 'del'
    @sprites.delete message[1]
    end
    end
    .
    .
    .
    Remove sprite
    from this server
    Sunday, 9 June, 13

    View Slide

  108. .
    .
    .
    response = String.new
    @sprites.each_value do |sprite|
    (response << sprite.join("|") << "\n") if sprite
    end
    socket.write response
    end
    rescue Exception => exception
    puts exception.backtrace
    end
    end # end data
    end # end loop
    rescue EOFError => err
    player = @players[user]
    puts "#{player[3]} has left arena."
    @sprites.delete player[0]
    @players.delete user
    socket.close
    end
    end
    Sunday, 9 June, 13

    View Slide

  109. .
    .
    .
    response = String.new
    @sprites.each_value do |sprite|
    (response << sprite.join("|") << "\n") if sprite
    end
    socket.write response
    end
    rescue Exception => exception
    puts exception.backtrace
    end
    end # end data
    end # end loop
    rescue EOFError => err
    player = @players[user]
    puts "#{player[3]} has left arena."
    @sprites.delete player[0]
    @players.delete user
    socket.close
    end
    end
    Send list of sprites
    to the client
    Sunday, 9 June, 13

    View Slide

  110. .
    .
    .
    response = String.new
    @sprites.each_value do |sprite|
    (response << sprite.join("|") << "\n") if sprite
    end
    socket.write response
    end
    rescue Exception => exception
    puts exception.backtrace
    end
    end # end data
    end # end loop
    rescue EOFError => err
    player = @players[user]
    puts "#{player[3]} has left arena."
    @sprites.delete player[0]
    @players.delete user
    socket.close
    end
    end
    If client disconnects,
    remove the player
    and sprite
    Sunday, 9 June, 13

    View Slide

  111. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234
    supervisor = Arena.supervise(server, port.to_i)
    trap("INT") do
    supervisor.terminate
    exit
    end
    sleep
    Sunday, 9 June, 13

    View Slide

  112. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234
    supervisor = Arena.supervise(server, port.to_i)
    trap("INT") do
    supervisor.terminate
    exit
    end
    sleep
    Monitors and restarts
    the server if it crashes
    Sunday, 9 June, 13

    View Slide

  113. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234
    supervisor = Arena.supervise(server, port.to_i)
    trap("INT") do
    supervisor.terminate
    exit
    end
    sleep
    Nothing for the main
    thread to do so, sleep and
    let the other threads run
    Sunday, 9 June, 13

    View Slide

  114. Demo
    Sunday, 9 June, 13

    View Slide

  115. Level 4
    Complete!
    Sunday, 9 June, 13

    View Slide

  116. Advanced stuff
    (a bit more)
    Sunday, 9 June, 13

    View Slide

  117. Run more than 1 game server?
    Provide custom maps and sprites for
    every server?
    Manage and monitor game servers (not
    through a console)?
    Sunday, 9 June, 13

    View Slide

  118. Web-based game
    server console
    Sunday, 9 June, 13

    View Slide

  119. Sunday, 9 June, 13

    View Slide

  120. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Sunday, 9 June, 13

    View Slide

  121. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Start server at
    any of these ports
    Sunday, 9 June, 13

    View Slide

  122. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Registry of all arenas
    Sunday, 9 June, 13

    View Slide

  123. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Start arena
    Sunday, 9 June, 13

    View Slide

  124. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Register arena
    Sunday, 9 June, 13

    View Slide

  125. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Terminate arena
    Sunday, 9 June, 13

    View Slide

  126. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Let client know
    about the
    customizations
    Sunday, 9 June, 13

    View Slide

  127. configure do
    @@port_range = (10000..11000).to_a
    end
    get "/" do
    @arenas = Celluloid::Actor.all
    haml :arenas
    end
    post "/arena/start" do
    port = @@port_range.delete @@port_range.sample
    arena = Arena.new(request.host, port, request.port)
    arena.map_url = params[:map_url]
    arena.spritesheet_url = params[:spritesheet_url]
    arena.default_hp = params[:default_hp].to_i
    Celluloid::Actor[:"arena_#{port}"] = arena
    redirect "/"
    end
    get "/arena/stop/:name" do
    raise "No such arena" unless Celluloid::Actor[params[:name].to_sym]
    Celluloid::Actor[params[:name].to_sym].terminate
    redirect "/"
    end
    get "/config/:name" do
    arena = Celluloid::Actor[params[:name].to_sym]
    array = [arena.map_url, arena.spritesheet_url,
    arena.default_hp.to_s].join("|")
    end
    Sunday, 9 June, 13

    View Slide

  128. Demo
    Sunday, 9 June, 13

    View Slide

  129. Thank you for
    listening
    Sunday, 9 June, 13

    View Slide

  130. [email protected]
    @sausheong
    http:/
    /github.com/sausheong/tanks
    http:/
    /github.com/sausheong/tanksworld
    http:/
    /libgosu.org
    http:/
    /celluloid.io
    œœœ
    Sunday, 9 June, 13

    View Slide