Playing With Ruby
-
Upload
sau-sheong-chang -
Category
Technology
-
view
329 -
download
2
description
Transcript of Playing With Ruby
Playing with RubyHow to write an online, real-time
multi-player game with Ruby
@sausheongSunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
About me
Sunday, 9 June, 13
Press start
Sunday, 9 June, 13
Sunday, 9 June, 13
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
Draw the game window
Sunday, 9 June, 13
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
Sunday, 9 June, 13
update method
called at every frame (60 frames per second)Contains game logicThe main ‘controller’ of the game
Sunday, 9 June, 13
draw method
Does the actual drawing of the game windowCalled after updateCan also be called when necessary
Sunday, 9 June, 13
Add background
Sunday, 9 June, 13
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
Sunday, 9 June, 13
Add a player
Sunday, 9 June, 13
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
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
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
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
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
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
Sunday, 9 June, 13
Add sound
Sunday, 9 June, 13
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
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
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
Demo
Sunday, 9 June, 13
Level 1
Complete!
Sunday, 9 June, 13
Use sprite sheets
Sunday, 9 June, 13
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
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
Sunday, 9 June, 13
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
module SpriteImage Grass = 102 Earth = 101 Gravel = 100 Wall = 59 Bullet= 28 Tank = 39end
Locate sprites in a sprite sheet
Sunday, 9 June, 13
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
Create editable maps
Sunday, 9 June, 13
Editable mapsAllows user to customize maps and backgrounds, using tiled sprites
.....................
.....................
.....................
.....................
.....................
..##............##...
...#............#....
...#............#....
...#............#....
..##............##...
.....................
.....................
.....................
.....................
.....................
Sunday, 9 June, 13
.....................
.....................
.....................
.....................
.....................
..##............##...
...#............#....
...#............#....
...#............#....
..##............##...
.....................
.....................
.....................
.....................
.....................
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
Level 2
Complete!
Sunday, 9 June, 13
Let’s play with others
Sunday, 9 June, 13
Sunday, 9 June, 13
DesignReal-time vs turn-based (immediate response)Latency (speed) is critical
‘Dead’ players can still observe the game
Game spectators
Sunday, 9 June, 13
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
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
Game flow
Sunday, 9 June, 13
Game server startsGame
Server
Sunday, 9 June, 13
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
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
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
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
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
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
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
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
Message passing
Sunday, 9 June, 13
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
"#{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
"#{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
"#{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
"#{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
"#{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
"#{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
Game Client
Sunday, 9 June, 13
Client sends messages to the
server
Sunday, 9 June, 13
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
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
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
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
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
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
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
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
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
"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
Client reads messages from the
server
Sunday, 9 June, 13
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
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
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
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
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
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
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
"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
@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
Level 3
Complete!
Sunday, 9 June, 13
Game Server
Sunday, 9 June, 13
Sunday, 9 June, 13
Sunday, 9 June, 13
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
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
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
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
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
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
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
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
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
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
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
.
.
.
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
.
.
.
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
.
.
.
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
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
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
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
Demo
Sunday, 9 June, 13
Level 4Complete!
Sunday, 9 June, 13
Advanced stuff (a bit more)
Sunday, 9 June, 13
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
Web-based game server console
Sunday, 9 June, 13
Sunday, 9 June, 13
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
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
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
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
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
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
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
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
Demo
Sunday, 9 June, 13
Thank you for
listening
Sunday, 9 June, 13
@sausheong
http://github.com/sausheong/tankshttp://github.com/sausheong/tanksworld
http://libgosu.org
http://celluloid.io
œœœ
Sunday, 9 June, 13