Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

About me Sunday, 9 June, 13

Slide 3

Slide 3 text

About me Sunday, 9 June, 13

Slide 4

Slide 4 text

About me Sunday, 9 June, 13

Slide 5

Slide 5 text

About me Sunday, 9 June, 13

Slide 6

Slide 6 text

About me Sunday, 9 June, 13

Slide 7

Slide 7 text

About me Sunday, 9 June, 13

Slide 8

Slide 8 text

About me Sunday, 9 June, 13

Slide 9

Slide 9 text

About me Sunday, 9 June, 13

Slide 10

Slide 10 text

About me Sunday, 9 June, 13

Slide 11

Slide 11 text

About me Sunday, 9 June, 13

Slide 12

Slide 12 text

Press start Sunday, 9 June, 13

Slide 13

Slide 13 text

Sunday, 9 June, 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

Draw the game window Sunday, 9 June, 13

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

Sunday, 9 June, 13

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

Add background Sunday, 9 June, 13

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Sunday, 9 June, 13

Slide 23

Slide 23 text

Add a player Sunday, 9 June, 13

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Sunday, 9 June, 13

Slide 31

Slide 31 text

Add sound Sunday, 9 June, 13

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

Demo Sunday, 9 June, 13

Slide 36

Slide 36 text

Level 1 Complete! Sunday, 9 June, 13

Slide 37

Slide 37 text

Use sprite sheets Sunday, 9 June, 13

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Sunday, 9 June, 13

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

Create editable maps Sunday, 9 June, 13

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

..................... ..................... ..................... ..................... ..................... ..##............##... ...#............#.... ...#............#.... ...#............#.... ..##............##... ..................... ..................... ..................... ..................... ..................... 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

Slide 47

Slide 47 text

Level 2 Complete! Sunday, 9 June, 13

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Sunday, 9 June, 13

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

Game flow Sunday, 9 June, 13

Slide 54

Slide 54 text

Game server starts Game Server Sunday, 9 June, 13

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Message passing Sunday, 9 June, 13

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

"#{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

Slide 66

Slide 66 text

"#{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

Slide 67

Slide 67 text

"#{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

Slide 68

Slide 68 text

"#{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

Slide 69

Slide 69 text

"#{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

Slide 70

Slide 70 text

"#{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

Slide 71

Slide 71 text

Game Client Sunday, 9 June, 13

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

"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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

"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

Slide 92

Slide 92 text

@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

Slide 93

Slide 93 text

Level 3 Complete! Sunday, 9 June, 13

Slide 94

Slide 94 text

Game Server Sunday, 9 June, 13

Slide 95

Slide 95 text

Sunday, 9 June, 13

Slide 96

Slide 96 text

Sunday, 9 June, 13

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

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

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

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

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

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

Slide 108

Slide 108 text

. . . 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

Slide 109

Slide 109 text

. . . 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

Slide 110

Slide 110 text

. . . 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

Slide 111

Slide 111 text

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

Slide 112

Slide 112 text

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

Slide 113

Slide 113 text

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

Slide 114

Slide 114 text

Demo Sunday, 9 June, 13

Slide 115

Slide 115 text

Level 4 Complete! Sunday, 9 June, 13

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 text

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

Slide 119

Slide 119 text

Sunday, 9 June, 13

Slide 120

Slide 120 text

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

Slide 121

Slide 121 text

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

Slide 122

Slide 122 text

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

Slide 123

Slide 123 text

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

Slide 124

Slide 124 text

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

Slide 125

Slide 125 text

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

Slide 126

Slide 126 text

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

Slide 127

Slide 127 text

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

Slide 128

Slide 128 text

Demo Sunday, 9 June, 13

Slide 129

Slide 129 text

Thank you for listening Sunday, 9 June, 13

Slide 130

Slide 130 text

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