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.

Ee191858f0d96ad93098694537f71998?s=128

Sau Sheong Chang

June 08, 2013
Tweet

Transcript

  1. 1.

    Playing with Ruby How to write an online, real-time multi-player

    game with Ruby @sausheong Sunday, 9 June, 13
  2. 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
  3. 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
  4. 18.

    update method called at every frame (60 frames per second)

    Contains game logic The main ‘controller’ of the game Sunday, 9 June, 13
  5. 19.

    draw method Does the actual drawing of the game window

    Called after update Can also be called when necessary Sunday, 9 June, 13
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 45.

    Editable maps Allows user to customize maps and backgrounds, using

    tiled sprites ..................... ..................... ..................... ..................... ..................... ..##............##... ...#............#.... ...#............#.... ...#............#.... ..##............##... ..................... ..................... ..................... ..................... ..................... Sunday, 9 June, 13
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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
  31. 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
  32. 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
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. 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
  39. 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
  40. 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
  41. 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
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. 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
  55. 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
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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
  66. 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
  67. 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
  68. 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
  69. 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
  70. 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
  71. 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
  72. 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
  73. 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
  74. 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
  75. 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
  76. 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
  77. 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