playing with ruby

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

Upload: sau-sheong-chang

Post on 21-Jun-2015

326 views

Category:

Technology


2 download

DESCRIPTION

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

TRANSCRIPT

Page 1: Playing With Ruby

Playing with RubyHow to write an online, real-time

multi-player game with Ruby

@sausheongSunday, 9 June, 13

Page 2: Playing With Ruby

About me

Sunday, 9 June, 13

Page 3: Playing With Ruby

About me

Sunday, 9 June, 13

Page 4: Playing With Ruby

About me

Sunday, 9 June, 13

Page 5: Playing With Ruby

About me

Sunday, 9 June, 13

Page 6: Playing With Ruby

About me

Sunday, 9 June, 13

Page 7: Playing With Ruby

About me

Sunday, 9 June, 13

Page 8: Playing With Ruby

About me

Sunday, 9 June, 13

Page 9: Playing With Ruby

About me

Sunday, 9 June, 13

Page 10: Playing With Ruby

About me

Sunday, 9 June, 13

Page 11: Playing With Ruby

About me

Sunday, 9 June, 13

Page 12: Playing With Ruby

Press start

Sunday, 9 June, 13

Page 13: Playing With Ruby

Sunday, 9 June, 13

Page 14: Playing With Ruby

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

Page 15: Playing With Ruby

Draw the game window

Sunday, 9 June, 13

Page 16: Playing With Ruby

require 'gosu'

class GameWindow < Gosu::Window def initialize super 640, 480, false self.caption = "Tutorial 1" end def update end def draw endend

window = GameWindow.newwindow.show

Sunday, 9 June, 13

Page 17: Playing With Ruby

Sunday, 9 June, 13

Page 18: Playing With Ruby

update method

called at every frame (60 frames per second)Contains game logicThe main ‘controller’ of the game

Sunday, 9 June, 13

Page 19: Playing With Ruby

draw method

Does the actual drawing of the game windowCalled after updateCan also be called when necessary

Sunday, 9 June, 13

Page 20: Playing With Ruby

Add background

Sunday, 9 June, 13

Page 21: Playing With Ruby

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) endend

window = GameWindow.newwindow.show x

y

z

Sunday, 9 June, 13

Page 22: Playing With Ruby

Sunday, 9 June, 13

Page 23: Playing With Ruby

Add a player

Sunday, 9 June, 13

Page 24: Playing With Ruby

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) endend

@angle5

offset_y

offset_x

Sunday, 9 June, 13

Page 25: Playing With Ruby

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) endend

Sunday, 9 June, 13

Page 26: Playing With Ruby

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) endend

create player

Sunday, 9 June, 13

Page 27: Playing With Ruby

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) endend

place him in middle of screen

Sunday, 9 June, 13

Page 28: Playing With Ruby

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) endend

} move according to user input

Sunday, 9 June, 13

Page 29: Playing With Ruby

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) endend

draw the player

Sunday, 9 June, 13

Page 30: Playing With Ruby

Sunday, 9 June, 13

Page 31: Playing With Ruby

Add sound

Sunday, 9 June, 13

Page 32: Playing With Ruby

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)end

Sunday, 9 June, 13

Page 33: Playing With Ruby

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)end

Load the sound

Sunday, 9 June, 13

Page 34: Playing With Ruby

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)end

play the sound!

Sunday, 9 June, 13

Page 35: Playing With Ruby

Demo

Sunday, 9 June, 13

Page 36: Playing With Ruby

Level 1

Complete!

Sunday, 9 June, 13

Page 37: Playing With Ruby

Use sprite sheets

Sunday, 9 June, 13

Page 38: Playing With Ruby

SpritesAn image or animation that’s overlaid on the backgroundUse single sprites (as before) or use sprite sheetsSprites normally represented by a square image

Sunday, 9 June, 13

Page 39: Playing With Ruby

Sprite sheetA bunch of images in a single file, used as spritesOften placed in sequence, image can be retrieved from knowing the locationReduces memory usage and increase drawing speed

Sunday, 9 June, 13

Page 40: Playing With Ruby

Sunday, 9 June, 13

Page 41: Playing With Ruby

5948

25 26 27 28 29 30 31 32 33 34 35 36 37 38 3924

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 230

72

100 101 10296

Sunday, 9 June, 13

Page 42: Playing With Ruby

module SpriteImage Grass = 102 Earth = 101 Gravel = 100 Wall = 59 Bullet= 28 Tank = 39end

Locate sprites in a sprite sheet

Sunday, 9 June, 13

Page 43: Playing With Ruby

Player:

def initialize(window) @image = Image.new window, "plane.png", falseend

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

Page 44: Playing With Ruby

Create editable maps

Sunday, 9 June, 13

Page 45: Playing With Ruby

Editable mapsAllows user to customize maps and backgrounds, using tiled sprites

.....................

.....................

.....................

.....................

.....................

..##............##...

...#............#....

...#............#....

...#............#....

..##............##...

.....................

.....................

.....................

.....................

.....................

Sunday, 9 June, 13

Page 46: Playing With Ruby

.....................

.....................

.....................

.....................

.....................

..##............##...

...#............#....

...#............#....

...#............#....

..##............##...

.....................

.....................

.....................

.....................

.....................

20 x 32 = 64015

x 32

= 48

0class 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

Page 47: Playing With Ruby

Level 2

Complete!

Sunday, 9 June, 13

Page 48: Playing With Ruby

Let’s play with others

Sunday, 9 June, 13

Page 49: Playing With Ruby

Sunday, 9 June, 13

Page 50: Playing With Ruby

DesignReal-time vs turn-based (immediate response)Latency (speed) is critical

‘Dead’ players can still observe the game

Game spectators

Sunday, 9 June, 13

Page 51: Playing With Ruby

DesignClient-serverAll artifacts are localOnly messages sent back and forth the client-serverMinimal size messages Messages sent from client -> server once every frame refresh

Sunday, 9 June, 13

Page 52: Playing With Ruby

DesignServer should have minimal processing, all game logic should be in the clientServer should only receive messages and broadcast to all clientsMessages not compressed/encoded (takes time at the server)Don’t send useless messages

Sunday, 9 June, 13

Page 53: Playing With Ruby

Game flow

Sunday, 9 June, 13

Page 54: Playing With Ruby

Game server startsGame

Server

Sunday, 9 June, 13

Page 55: Playing With Ruby

Tank 1 starts, sends message to server

Game ServerTank1

object:tank1

object:tank1

Tank 1 ignores messages that is

about itself

Sunday, 9 June, 13

Page 56: Playing With Ruby

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

Page 57: Playing With Ruby

Tank 2 startsGame

ServerTank1object:tank1

object:tank1

Tank2ob

ject:t

ank2

objec

t:tan

k1

objec

t:tan

k2

object:tank2

Tank 1 receives messages from server about Tank 2,

starts drawing Tank 2

Sunday, 9 June, 13

Page 58: Playing With Ruby

Tank 2 movesGame

ServerTank1object:tank1

object:tank1

Tank2ob

ject:t

ank2

objec

t:tan

k1

objec

t:tan

k2

object:tank2

When Tank 2 moves, its position is sent to the server and broadcast to everyone

Sunday, 9 June, 13

Page 59: Playing With Ruby

Tank 1 shootsGame

ServerTank1object:tank1

object:tank1

Tank2ob

ject:t

ank2

objec

t:tan

k1

objec

t:tan

k2

object:tank2

object:shot1

object:shot1

objec

t:sho

t1

Tank 1 creates a shot, message sent to server and

broadcast to everyone

Sunday, 9 June, 13

Page 60: Playing With Ruby

Shot goes out of rangeGame

ServerTank1object:tank1

object:tank1

Tank2ob

ject:t

ank2

objec

t:tan

k1

objec

t:tan

k2

object:tank2

delete:shot1

delete:shot1

delet

e:sho

t1

When the shot goes out of range, Tank 1 sends a delete

message to the server, broadcasted to everyone

Sunday, 9 June, 13

Page 61: Playing With Ruby

Tank 1 shot hits Tank 2Game

ServerTank1object:tank1

object:tank1

Tank2ob

ject:t

ank2

objec

t:tan

k1

objec

t:tan

k2

object:tank2

object:shot1

object:shot1

objec

t:sho

t1

If Tank 1’s shot hits Tank 2, reduce hit points from Tank1

Sunday, 9 June, 13

Page 62: Playing With Ruby

Tank 2 destroyedGame

ServerTank1object:tank1

object:tank1

Tank2

objec

t:tan

k1

object:shot1

object:shot1

objec

t:sho

t1

When Tank 2’s hit points fall below 0 it is destroyed

Sunday, 9 June, 13

Page 63: Playing With Ruby

Message passing

Sunday, 9 June, 13

Page 64: Playing With Ruby

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

Page 65: Playing With Ruby

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

Page 66: Playing With Ruby

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

Page 67: Playing With Ruby

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

Page 68: Playing With Ruby

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

Page 69: Playing With Ruby

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

Page 70: Playing With Ruby

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

Page 71: Playing With Ruby

Game Client

Sunday, 9 June, 13

Page 72: Playing With Ruby

Client sends messages to the

server

Sunday, 9 June, 13

Page 73: Playing With Ruby

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

Page 74: Playing With Ruby

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

Page 75: Playing With Ruby

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

Page 76: Playing With Ruby

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

Page 77: Playing With Ruby

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

Page 78: Playing With Ruby

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

Page 79: Playing With Ruby

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

Page 80: Playing With Ruby

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

Page 81: Playing With Ruby

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

Page 82: Playing With Ruby

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

clien

t mes

sage

Sunday, 9 June, 13

Page 83: Playing With Ruby

Client reads messages from the

server

Sunday, 9 June, 13

Page 84: Playing With Ruby

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

Page 85: Playing With Ruby

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

Page 86: Playing With Ruby

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

Page 87: Playing With Ruby

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

Page 88: Playing With Ruby

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

Page 89: Playing With Ruby

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

Page 90: Playing With Ruby

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

Page 91: Playing With Ruby

"uuid|type|sprite_image|player|x|y|angle|points|color"

serv

er m

essa

ge

"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

Page 92: Playing With Ruby

@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

Page 93: Playing With Ruby

Level 3

Complete!

Sunday, 9 June, 13

Page 94: Playing With Ruby

Game Server

Sunday, 9 June, 13

Page 95: Playing With Ruby

Sunday, 9 June, 13

Page 96: Playing With Ruby

Sunday, 9 June, 13

Page 97: Playing With Ruby

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

Page 98: Playing With Ruby

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

Page 99: Playing With Ruby

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

Page 100: Playing With Ruby

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

Page 101: Playing With Ruby

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

Page 102: Playing With Ruby

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

Page 103: Playing With Ruby

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

Page 104: Playing With Ruby

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

Page 105: Playing With Ruby

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

Page 106: Playing With Ruby

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

Page 107: Playing With Ruby

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

Page 108: Playing With Ruby

.

.

.

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 endend

Sunday, 9 June, 13

Page 109: Playing With Ruby

.

.

.

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 endend

Send list of sprites to the client

Sunday, 9 June, 13

Page 110: Playing With Ruby

.

.

.

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 endend

If client disconnects, remove the player

and sprite

Sunday, 9 June, 13

Page 111: Playing With Ruby

server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") do supervisor.terminate exitend

sleep

Sunday, 9 June, 13

Page 112: Playing With Ruby

server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") do supervisor.terminate exitend

sleep

Monitors and restarts the server if it crashes

Sunday, 9 June, 13

Page 113: Playing With Ruby

server, port = ARGV[0] || "0.0.0.0", ARGV[1] || 1234supervisor = Arena.supervise(server, port.to_i)trap("INT") do supervisor.terminate exitend

sleep

Nothing for the main thread to do so, sleep and let the other threads run

Sunday, 9 June, 13

Page 114: Playing With Ruby

Demo

Sunday, 9 June, 13

Page 115: Playing With Ruby

Level 4Complete!

Sunday, 9 June, 13

Page 116: Playing With Ruby

Advanced stuff (a bit more)

Sunday, 9 June, 13

Page 117: Playing With Ruby

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

Page 118: Playing With Ruby

Web-based game server console

Sunday, 9 June, 13

Page 119: Playing With Ruby

Sunday, 9 June, 13

Page 120: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 121: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 122: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 123: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 124: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 125: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 126: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 127: Playing With Ruby

configure do @@port_range = (10000..11000).to_aend

get "/" do @arenas = Celluloid::Actor.all haml :arenasend

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

Page 128: Playing With Ruby

Demo

Sunday, 9 June, 13

Page 129: Playing With Ruby

Thank you for

listening

Sunday, 9 June, 13