Playing With Ruby

626 views

Published on

This is a talk from RedDotRubyConf 2013 where I described how I created a simple online, real-time, multi-player game with Gosu

Published in: Technology
0 Comments
2 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
626
On SlideShare
0
From Embeds
0
Number of Embeds
3
Actions
Shares
0
Downloads
12
Comments
0
Likes
2
Embeds 0
No embeds

No notes for slide

Playing With Ruby

  1. 1. Playing with RubyHow to write an online, real-timemulti-player game with Ruby@sausheongSunday, 9 June, 13
  2. 2. About meSunday, 9 June, 13
  3. 3. About meSunday, 9 June, 13
  4. 4. About meSunday, 9 June, 13
  5. 5. About meSunday, 9 June, 13
  6. 6. About meSunday, 9 June, 13
  7. 7. About meSunday, 9 June, 13
  8. 8. About meSunday, 9 June, 13
  9. 9. About meSunday, 9 June, 13
  10. 10. About meSunday, 9 June, 13
  11. 11. About meSunday, 9 June, 13
  12. 12. Press startSunday, 9 June, 13
  13. 13. Sunday, 9 June, 13
  14. 14. 2D game development libraryC++-based, with Ruby wrapperOS X, Windows and LinuxWorks with MRI, MacRuby, Rubinius(but not JRuby)Sunday, 9 June, 13
  15. 15. Draw the gamewindowSunday, 9 June, 13
  16. 16. require gosuclass GameWindow < Gosu::Windowdef initializesuper 640, 480, falseself.caption = "Tutorial 1"enddef updateenddef drawendendwindow = GameWindow.newwindow.showSunday, 9 June, 13
  17. 17. Sunday, 9 June, 13
  18. 18. update methodcalled at every frame (60 frames persecond)Contains game logicThe main ‘controller’ of the gameSunday, 9 June, 13
  19. 19. draw methodDoes the actual drawing of the gamewindowCalled after updateCan also be called when necessarySunday, 9 June, 13
  20. 20. Add backgroundSunday, 9 June, 13
  21. 21. require gosuclass GameWindow < Gosu::Windowdef initializesuper 640, 480, falseself.caption = "Tutorial 2"@background_image = Gosu::Image.new(self, "bg1.jpg", true)enddef updateenddef draw@background_image.draw(0, 0, 0)endendwindow = GameWindow.newwindow.showxyzSunday, 9 June, 13
  22. 22. Sunday, 9 June, 13
  23. 23. Add a playerSunday, 9 June, 13
  24. 24. class Playerdef initialize(window)@image = Image.new window, "plane.png", false@x = @y = @vel_x = @vel_y = @angle = 0.0enddef warp(x, y)@x, @y = x, yenddef turn_left@angle -= 5enddef turn_right@angle += 5enddef accelerate@vel_x += offset_x(@angle, 5)@vel_y += offset_y(@angle, 5)enddef move@x += @vel_x@y += @vel_y@x %= 640@y %= 480@vel_x, @vel_y = 0, 0enddef draw@image.draw_rot(@x, @y, 1, @angle)endend@angle5offset_yoffset_xSunday, 9 June, 13
  25. 25. class GameWindow < Windowdef initializesuper(640, 480, false)self.caption = "Tutorial 3"@background_image = Image.new(self, "bg1.jpg", true)@player = Player.new(self)@player.warp(320, 240)enddef update@player.turn_left if button_down? KbLeft@player.turn_right if button_down? KbRight@player.accelerate if button_down? KbUp@player.moveenddef draw@player.draw@background_image.draw(0, 0, 0)endendSunday, 9 June, 13
  26. 26. class GameWindow < Windowdef initializesuper(640, 480, false)self.caption = "Tutorial 3"@background_image = Image.new(self, "bg1.jpg", true)@player = Player.new(self)@player.warp(320, 240)enddef update@player.turn_left if button_down? KbLeft@player.turn_right if button_down? KbRight@player.accelerate if button_down? KbUp@player.moveenddef draw@player.draw@background_image.draw(0, 0, 0)endendcreate playerSunday, 9 June, 13
  27. 27. class GameWindow < Windowdef initializesuper(640, 480, false)self.caption = "Tutorial 3"@background_image = Image.new(self, "bg1.jpg", true)@player = Player.new(self)@player.warp(320, 240)enddef update@player.turn_left if button_down? KbLeft@player.turn_right if button_down? KbRight@player.accelerate if button_down? KbUp@player.moveenddef draw@player.draw@background_image.draw(0, 0, 0)endendplace him in middle of screenSunday, 9 June, 13
  28. 28. class GameWindow < Windowdef initializesuper(640, 480, false)self.caption = "Tutorial 3"@background_image = Image.new(self, "bg1.jpg", true)@player = Player.new(self)@player.warp(320, 240)enddef update@player.turn_left if button_down? KbLeft@player.turn_right if button_down? KbRight@player.accelerate if button_down? KbUp@player.moveenddef draw@player.draw@background_image.draw(0, 0, 0)endend} move according touser inputSunday, 9 June, 13
  29. 29. class GameWindow < Windowdef initializesuper(640, 480, false)self.caption = "Tutorial 3"@background_image = Image.new(self, "bg1.jpg", true)@player = Player.new(self)@player.warp(320, 240)enddef update@player.turn_left if button_down? KbLeft@player.turn_right if button_down? KbRight@player.accelerate if button_down? KbUp@player.moveenddef draw@player.draw@background_image.draw(0, 0, 0)endenddraw the playerSunday, 9 June, 13
  30. 30. Sunday, 9 June, 13
  31. 31. Add soundSunday, 9 June, 13
  32. 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.0end...def accelerate@sound.play@vel_x += offset_x(@angle, 5)@vel_y += offset_y(@angle, 5)endSunday, 9 June, 13
  33. 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.0end...def accelerate@sound.play@vel_x += offset_x(@angle, 5)@vel_y += offset_y(@angle, 5)endLoad the soundSunday, 9 June, 13
  34. 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.0end...def accelerate@sound.play@vel_x += offset_x(@angle, 5)@vel_y += offset_y(@angle, 5)endplay the sound!Sunday, 9 June, 13
  35. 35. DemoSunday, 9 June, 13
  36. 36. Level 1Complete!Sunday, 9 June, 13
  37. 37. Use sprite sheetsSunday, 9 June, 13
  38. 38. SpritesAn image or animation that’s overlaidon the backgroundUse single sprites (as before) or usesprite sheetsSprites normally represented by asquare imageSunday, 9 June, 13
  39. 39. Sprite sheetA bunch of images in a single file, usedas spritesOften placed in sequence, image can beretrieved from knowing the locationReduces memory usage and increasedrawing speedSunday, 9 June, 13
  40. 40. Sunday, 9 June, 13
  41. 41. 594825 26 27 28 29 30 31 32 33 34 35 36 37 38 39241 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23072100 101 10296Sunday, 9 June, 13
  42. 42. module SpriteImageGrass = 102Earth = 101Gravel = 100Wall = 59Bullet= 28Tank = 39endLocate sprites in asprite sheetSunday, 9 June, 13
  43. 43. Player:def initialize(window)@image = Image.new window, "plane.png", falseenddef draw@image.draw_rot(@x, @y, 1, @angle)endGameWindow:@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
  44. 44. Create editable mapsSunday, 9 June, 13
  45. 45. Editable mapsAllows user to customize maps andbackgrounds, using tiled sprites...........................................................................................................##............##......#............#.......#............#.......#............#......##............##............................................................................................................Sunday, 9 June, 13
  46. 46. ...........................................................................................................##............##......#............#.......#............#.......#............#......##............##............................................................................................................20 x 32 = 64015x32=480 class Mapdef 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::Earthwhen "#"SpriteImage::Wallwhen "SpriteImage::Grassendendendenddef draw@height.times do |y|@width.times do |x|tile = @tiles[x][y]@window.spritesheet[tile].draw(x *32, y * 32, 1)endendSunday, 9 June, 13
  47. 47. Level 2Complete!Sunday, 9 June, 13
  48. 48. Let’s play withothersSunday, 9 June, 13
  49. 49. Sunday, 9 June, 13
  50. 50. DesignReal-time vs turn-based (immediateresponse)Latency (speed) is critical‘Dead’ players can still observe the gameGame spectatorsSunday, 9 June, 13
  51. 51. DesignClient-serverAll artifacts are localOnly messages sent back and forth theclient-serverMinimal size messagesMessages sent from client -> server onceevery frame refreshSunday, 9 June, 13
  52. 52. DesignServer should have minimal processing, allgame logic should be in the clientServer should only receive messages andbroadcast to all clientsMessages not compressed/encoded (takestime at the server)Don’t send useless messagesSunday, 9 June, 13
  53. 53. Game flowSunday, 9 June, 13
  54. 54. Game server startsGameServerSunday, 9 June, 13
  55. 55. Tank 1 starts, sendsmessage to serverGameServerTank1object:tank1object:tank1Tank 1 ignoresmessages that isabout itselfSunday, 9 June, 13
  56. 56. Server simply stores and broadcasts allmessages sent to it to reduce processingLogic to process or ignore messages arein the clientSunday, 9 June, 13
  57. 57. Tank 2 startsGameServerTank1object:tank1object:tank1Tank2object:tank2object:tank1object:tank2object:tank2Tank 1 receives messagesfrom server about Tank 2,starts drawing Tank 2Sunday, 9 June, 13
  58. 58. Tank 2 movesGameServerTank1object:tank1object:tank1Tank2object:tank2object:tank1object:tank2object:tank2When Tank 2 moves, itsposition is sent to the serverand broadcast to everyoneSunday, 9 June, 13
  59. 59. Tank 1 shootsGameServerTank1object:tank1object:tank1Tank2object:tank2object:tank1object:tank2object:tank2object:shot1object:shot1object:shot1Tank 1 creates a shot,message sent to server andbroadcast to everyoneSunday, 9 June, 13
  60. 60. Shot goes out of rangeGameServerTank1object:tank1object:tank1Tank2object:tank2object:tank1object:tank2object:tank2delete:shot1delete:shot1delete:shot1When the shot goes out ofrange, Tank 1 sends a deletemessage to the server,broadcasted to everyoneSunday, 9 June, 13
  61. 61. Tank 1 shot hits Tank 2GameServerTank1object:tank1object:tank1Tank2object:tank2object:tank1object:tank2object:tank2object:shot1object:shot1object:shot1If Tank 1’s shot hits Tank 2,reduce hit points from Tank1Sunday, 9 June, 13
  62. 62. Tank 2 destroyedGameServerTank1object:tank1object:tank1Tank2object:tank1object:shot1object:shot1object:shot1When Tank 2’s hit points fallbelow 0 it is destroyedSunday, 9 June, 13
  63. 63. Message passingSunday, 9 June, 13
  64. 64. Messages are string delimited withvertical bar (|)Messages are accumulated till and sentonly 1 time in a frame refreshMessages from client -> server :message type + spriteMessage from server -> client : spriteonlySunday, 9 June, 13
  65. 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
  66. 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
  67. 67. "#{msg_type}|#{sprite.uuid}|#{sprite.type}|#{sprite.sprite_image}|#{sprite.player}|#{sprite.x}|#{sprite.y}|#{sprite.angle}|#{sprite.points}|#{sprite.color}"a unique identifierfor the spriteSunday, 9 June, 13
  68. 68. "#{msg_type}|#{sprite.uuid}|#{sprite.type}|#{sprite.sprite_image}|#{sprite.player}|#{sprite.x}|#{sprite.y}|#{sprite.angle}|#{sprite.points}|#{sprite.color}"tank or shotSunday, 9 June, 13
  69. 69. "#{msg_type}|#{sprite.uuid}|#{sprite.type}|#{sprite.sprite_image}|#{sprite.player}|#{sprite.x}|#{sprite.y}|#{sprite.angle}|#{sprite.points}|#{sprite.color}"player nameSunday, 9 June, 13
  70. 70. "#{msg_type}|#{sprite.uuid}|#{sprite.type}|#{sprite.sprite_image}|#{sprite.player}|#{sprite.x}|#{sprite.y}|#{sprite.angle}|#{sprite.points}|#{sprite.color}"only validfor tanksSunday, 9 June, 13
  71. 71. Game ClientSunday, 9 June, 13
  72. 72. Client sendsmessages to theserverSunday, 9 June, 13
  73. 73. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearSunday, 9 June, 13
  74. 74. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearstore my previous coordinatesSunday, 9 June, 13
  75. 75. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearmove!Sunday, 9 June, 13
  76. 76. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.cleargo back to previouscoordinates if I hit thewall, go out or hitanother tankSunday, 9 June, 13
  77. 77. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearadd me to the list ofmessages to send toserverSunday, 9 June, 13
  78. 78. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearcheck the other shotson screen to see if ithits me, if it does, tellthe server I was hitSunday, 9 June, 13
  79. 79. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearmove my shots, if ithits the wall or goesout, remove itSunday, 9 June, 13
  80. 80. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearif not, tell the serverits new positionSunday, 9 June, 13
  81. 81. def updatebeginmove_tankpx, 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)endadd_to_message_queue(obj, @me)@other_shots.each_value do |shot|if @me.alive? and @me.collide_with?(shot, 16)@me.hitadd_to_message_queue(obj, @me)endend@me_shots.each do |shot|shot.move # move the bulletif shot.hit_wall? or shot.outside_battlefield?@me_shots.delete shotadd_to_message_queue(del, shot)elseadd_to_message_queue(obj, shot)endend@client.send_message @messages.join("n")@messages.clearall my actions areprocessed, now tosend messages toserverSunday, 9 June, 13
  82. 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"clientmessageSunday, 9 June, 13
  83. 83. Client readsmessages from theserverSunday, 9 June, 13
  84. 84. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendSunday, 9 June, 13
  85. 85. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendread messages from the serverSunday, 9 June, 13
  86. 86. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendparse the server messages intospritesSunday, 9 June, 13
  87. 87. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendfor tank sprites other than me,set the properties and move itSunday, 9 June, 13
  88. 88. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendonly time the server tellsme about my changes iswhen I’m hitSunday, 9 June, 13
  89. 89. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendmove the shotspritesSunday, 9 June, 13
  90. 90. if msg = @client.read_message@valid_sprites.cleardata = msg.split("n")data.each do |row|sprite = row.split("|")if sprite.size == 9player = sprite[3]@valid_sprites << sprite[0]case sprite[1]when tankunless player == @playerif @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)endelse@me.points = sprite[7].to_iendwhen shotunless player == @playershot = Shot.from_sprite(self, sprite)@other_shots[shot.uuid] = shotshot.warp_to(sprite[4], sprite[5], sprite[6])endendendendSunday, 9 June, 13
  91. 91. "uuid|type|sprite_image|player|x|y|angle|points|color"servermessage"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
  92. 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)endendif shots and tanks (other than myself)weren’t broadcast from the server, thismeans they’ve been removedSunday, 9 June, 13
  93. 93. Level 3Complete!Sunday, 9 June, 13
  94. 94. Game ServerSunday, 9 June, 13
  95. 95. Sunday, 9 June, 13
  96. 96. Sunday, 9 June, 13
  97. 97. Event-driven IO library based onCelluloidDuck types Ruby IO classes (TCPSocket,TCPServer etc)Celluloid combines OO with concurrentprogramming, simplifies buildingmultithreaded programsSunday, 9 June, 13
  98. 98. require celluloid/ioclass Arenainclude Celluloid::IOfinalizer :shutdowndef initialize(host, port)puts "Starting Tanks Arena at #{host}:#{port}."@server = TCPServer.new(host, port)@sprites = Hash.new@players = Hash.newasync.runenddef shutdown@server.close if @serverenddef runloop { async.handle_connection @server.accept }endSunday, 9 June, 13
  99. 99. require celluloid/ioclass Arenainclude Celluloid::IOfinalizer :shutdowndef initialize(host, port)puts "Starting Tanks Arena at #{host}:#{port}."@server = TCPServer.new(host, port)@sprites = Hash.new@players = Hash.newasync.runenddef shutdown@server.close if @serverenddef runloop { async.handle_connection @server.accept }endWhat to do when theserver terminatesSunday, 9 June, 13
  100. 100. require celluloid/ioclass Arenainclude Celluloid::IOfinalizer :shutdowndef initialize(host, port)puts "Starting Tanks Arena at #{host}:#{port}."@server = TCPServer.new(host, port)@sprites = Hash.new@players = Hash.newasync.runenddef shutdown@server.close if @serverenddef runloop { async.handle_connection @server.accept }endRun the Arena objectin a new threadSunday, 9 June, 13
  101. 101. require celluloid/ioclass Arenainclude Celluloid::IOfinalizer :shutdowndef initialize(host, port)puts "Starting Tanks Arena at #{host}:#{port}."@server = TCPServer.new(host, port)@sprites = Hash.new@players = Hash.newasync.runenddef shutdown@server.close if @serverenddef runloop { async.handle_connection @server.accept }endWhen a client connects,handle the connection ina new threadSunday, 9 June, 13
  102. 102. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Sunday, 9 June, 13
  103. 103. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Uniquelyidentifies a userSunday, 9 June, 13
  104. 104. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Get data fromthe clientSunday, 9 June, 13
  105. 105. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Add to list of playersif player is newSunday, 9 June, 13
  106. 106. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Add to list ofsprites inthis serverSunday, 9 June, 13
  107. 107. def handle_connection(socket)_, port, host = socket.peeraddruser = "#{host}:#{port}"puts "#{user} has joined the arena."loop dodata = socket.readpartial(4096)data_array = data.split("n")if data_array and !data_array.empty?begindata_array.each do |row|message = row.split("|")if message.size == 10case message[0]when obj@players[user] = message[1..9] unless @players[user]@sprites[message[1]] = message[1..9]when del@sprites.delete message[1]endend...Remove spritefrom this serverSunday, 9 June, 13
  108. 108. ...response = String.new@sprites.each_value do |sprite|(response << sprite.join("|") << "n") if spriteendsocket.write responseendrescue Exception => exceptionputs exception.backtraceendend # end dataend # end looprescue EOFError => errplayer = @players[user]puts "#{player[3]} has left arena."@sprites.delete player[0]@players.delete usersocket.closeendendSunday, 9 June, 13
  109. 109. ...response = String.new@sprites.each_value do |sprite|(response << sprite.join("|") << "n") if spriteendsocket.write responseendrescue Exception => exceptionputs exception.backtraceendend # end dataend # end looprescue EOFError => errplayer = @players[user]puts "#{player[3]} has left arena."@sprites.delete player[0]@players.delete usersocket.closeendendSend list of spritesto the clientSunday, 9 June, 13
  110. 110. ...response = String.new@sprites.each_value do |sprite|(response << sprite.join("|") << "n") if spriteendsocket.write responseendrescue Exception => exceptionputs exception.backtraceendend # end dataend # end looprescue EOFError => errplayer = @players[user]puts "#{player[3]} has left arena."@sprites.delete player[0]@players.delete usersocket.closeendendIf client disconnects,remove the playerand spriteSunday, 9 June, 13
  111. 111. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") dosupervisor.terminateexitendsleepSunday, 9 June, 13
  112. 112. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") dosupervisor.terminateexitendsleepMonitors and restartsthe server if it crashesSunday, 9 June, 13
  113. 113. server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") dosupervisor.terminateexitendsleepNothing for the mainthread to do so, sleep andlet the other threads runSunday, 9 June, 13
  114. 114. DemoSunday, 9 June, 13
  115. 115. Level 4Complete!Sunday, 9 June, 13
  116. 116. Advanced stuff(a bit more)Sunday, 9 June, 13
  117. 117. Run more than 1 game server?Provide custom maps and sprites forevery server?Manage and monitor game servers (notthrough a console)?Sunday, 9 June, 13
  118. 118. Web-based gameserver consoleSunday, 9 June, 13
  119. 119. Sunday, 9 June, 13
  120. 120. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endSunday, 9 June, 13
  121. 121. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endStart server atany of these portsSunday, 9 June, 13
  122. 122. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endRegistry of all arenasSunday, 9 June, 13
  123. 123. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endStart arenaSunday, 9 June, 13
  124. 124. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endRegister arenaSunday, 9 June, 13
  125. 125. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endTerminate arenaSunday, 9 June, 13
  126. 126. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endLet client knowabout thecustomizationsSunday, 9 June, 13
  127. 127. configure do@@port_range = (10000..11000).to_aendget "/" do@arenas = Celluloid::Actor.allhaml :arenasendpost "/arena/start" doport = @@port_range.delete @@port_range.samplearena = 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_iCelluloid::Actor[:"arena_#{port}"] = arenaredirect "/"endget "/arena/stop/:name" doraise "No such arena" unless Celluloid::Actor[params[:name].to_sym]Celluloid::Actor[params[:name].to_sym].terminateredirect "/"endget "/config/:name" doarena = Celluloid::Actor[params[:name].to_sym]array = [arena.map_url, arena.spritesheet_url,arena.default_hp.to_s].join("|")endSunday, 9 June, 13
  128. 128. DemoSunday, 9 June, 13
  129. 129. Thank you forlisteningSunday, 9 June, 13
  130. 130. sausheong@gmail.com@sausheonghttp://github.com/sausheong/tankshttp://github.com/sausheong/tanksworldhttp://libgosu.orghttp://celluloid.ioœœœSunday, 9 June, 13

×