Class: Chingu::GameStates::NetworkClient

Inherits:
NetworkState show all
Defined in:
lib/chingu/game_states/network_client.rb

Overview

A game state for a client in a multiplayer game, suitable for smaller/middle sized games. Used in combination with game state NetworkServer.

Uses non-blocking polling TCP and marshal to communicate. If your game state inherits from NetworkClient you'll have the following methods available:

connect(address, port)          # Start a non-blocking connection. only connect() uses previosly given ip:port
send_data(data)                 # Send raw data on the network, non-blocking
send_msg(whatever ruby data)    # Will get marshalled and sent to server
handle_incoming_data(max_size)  # Non-blocking read of incoming server data
disconnect_from_server          # Shuts down all network connections

The following callbacks can be overwritten to add your game logic:

on_connect              # when the TCP connection to the server is opened
on_disconnect           # when server dies or disconnects you
on_data(data)           # when raw data arrives from server, if not overloaded this will unpack and call on_msg
on_msg(msg)             # an incoming msgs, could be a ruby hash or array or whatever datastructure you've chosen to send from server
on_timeout              # connection timed out
on_connection_refused   # server isn't listening on that port

Usage:

PlayState < Chingu::GameStates::NetworkClient
  def initialize(options = {})
    super   # this is always needed!
    connect(options[:address], options[:port])
  end

  def on_connect
    send_msg(:cmd => :hello)
  end

  def on_msg(msg)
    if msg[:cmd] == :ping
      send_msg(:cmd => :pong, :timestamp => msg[:timestamp])   # send back timestamp so server can calcuate lag
    end
  end
end

push_game_state PlayState.new(:ip => "127.0.0.1", :port => 7778))

So why not EventMachine? No doubt in my mind that EventMachine is a hell of a library Chingu rolls its own for 2 reasons:

AFAIK EventMachine can be hard to integrate with the classic game loop, event machine wants its own loop
Rubys non-blocking sockets work, so why not keep it simple

Constant Summary

Constants inherited from NetworkState

Chingu::GameStates::NetworkState::DEFAULT_PORT, Chingu::GameStates::NetworkState::PACKET_HEADER_FORMAT, Chingu::GameStates::NetworkState::PACKET_HEADER_LENGTH

Instance Attribute Summary collapse

Attributes inherited from NetworkState

#address, #bytes_received, #bytes_sent, #packets_received, #packets_sent, #port

Attributes inherited from Chingu::GameState

#game_objects, #game_state_manager, #options, #previous_game_state

Attributes included from Helpers::InputDispatcher

#input_clients

Attributes included from Helpers::GameObject

#game_objects

Instance Method Summary collapse

Methods inherited from NetworkState

#reset_counters

Methods inherited from Chingu::GameState

#button_down, #button_up, #close_game, #draw, #draw_trait, #filename, #setup, #setup_trait, #to_s, #to_sym, trait, #trait_options, traits, #update_trait

Methods included from Helpers::ClassInheritableAccessor

included

Methods included from Helpers::InputClient

#add_inputs, #holding?, #holding_all?, #holding_any?, #input, #input=, #on_input

Methods included from Helpers::InputDispatcher

#add_input_client, #dispatch_button_down, #dispatch_button_up, #dispatch_input_for, #remove_input_client

Methods included from Helpers::GameObject

#game_objects_of_class, #load_game_objects, #save_game_objects

Methods included from Helpers::GFX

#draw_arc, #draw_circle, #draw_rect, #fill, #fill_arc, #fill_circle, #fill_gradient, #fill_rect

Constructor Details

#initialize(options = {}) ⇒ NetworkClient



76
77
78
79
80
81
82
83
84
85
86
# File 'lib/chingu/game_states/network_client.rb', line 76

def initialize(options = {})
  super(options)

  @timeout = options[:timeout] || 4000

  @max_read_per_update = options[:max_read_per_update] || 50000
  
  @socket = nil
  @connected = false
  @packet_buffer = PacketBuffer.new
end

Instance Attribute Details

#socketObject (readonly)

Returns the value of attribute socket



72
73
74
# File 'lib/chingu/game_states/network_client.rb', line 72

def socket
  @socket
end

#timeoutObject (readonly)

Returns the value of attribute timeout



72
73
74
# File 'lib/chingu/game_states/network_client.rb', line 72

def timeout
  @timeout
end

Instance Method Details

#connect(options = {}) ⇒ Object

Connect to a given address:port (the server) Connect is done in a non-blocking manner. May pass :address and :port, which will overwrite any existing values.



129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/chingu/game_states/network_client.rb', line 129

def connect(options = {})
  options = {
    :address => @address,
    :port => @port,
    :reconnect => false, # Doesn't reset the timeout timer; used internally.
  }.merge! options
  
  return if @socket
  
  @address = options[:address]
  @port = options[:port]
    
  # Set up our @socket, update() will handle the actual nonblocking connection
  @socket = Socket.new(Socket::Constants::AF_INET, Socket::Constants::SOCK_STREAM, 0)
  @sockaddr = Socket.sockaddr_in(@port, @address)

  @connect_times_out_at = Gosu::milliseconds + @timeout unless options[:reconnect]
  
  return self
end

#connected?Boolean



74
# File 'lib/chingu/game_states/network_client.rb', line 74

def connected?; @connected; end

#disconnect_from_serverObject Also known as: close, stop

Shuts down all communication (closes socket) with server



251
252
253
254
255
256
257
258
259
# File 'lib/chingu/game_states/network_client.rb', line 251

def disconnect_from_server
  @socket.close if @socket and not @socket.closed?
rescue Errno::ENOTCONN
ensure
  @socket = nil
  was_connected = @connected
  @connected = false
  on_disconnect if was_connected
end

#flushObject

Ensure that the buffer is cleared of data to write (call at the end of update or, at least after all sends).



242
243
244
245
246
# File 'lib/chingu/game_states/network_client.rb', line 242

def flush
  @socket.flush if @socket
rescue IOError
  disconnect_from_server
end

#handle_incoming_data(amount = @max_read_per_update) ⇒ Object

Call this from your update() to read from socket. handle_incoming_data will call on_data(raw_data) when stuff comes on on the socket.



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/chingu/game_states/network_client.rb', line 183

def handle_incoming_data(amount = @max_read_per_update)
  return unless @socket and connected?
  
  if IO.select([@socket], nil, nil, 0.0)
    begin
      packet, sender = @socket.recvfrom(amount)
      on_data(packet)        
    rescue Errno::ECONNABORTED, Errno::ECONNRESET
      disconnect_from_server
    end
  end
end

#on_connectObject

on_connect will be called when client successfully makes a connection to server



168
169
170
# File 'lib/chingu/game_states/network_client.rb', line 168

def on_connect
  puts "[Connected to Server #{@address}:#{@port}]"  if @debug
end

#on_connection_refusedObject

Called when connect() fails with connection refused (closed port)



153
154
155
156
# File 'lib/chingu/game_states/network_client.rb', line 153

def on_connection_refused
  puts "[on_connection_refused() #{@address}:#{@port}]"  if @debug
  connect(:reconnect => true)
end

#on_data(data) ⇒ Object

on_data(data) will be called from handle_incoming_data() by default.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/chingu/game_states/network_client.rb', line 199

def on_data(data)
  @packet_buffer.buffer_data data

  @bytes_received += data.length

  while packet = @packet_buffer.next_packet
    @packets_received += 1
    begin
      on_msg(Marshal.load(packet))
    rescue TypeError
      disconnect_from_server
      break
    end
  end
end

#on_disconnectObject

on_disconnect will be called when server disconnects client for whatever reason



175
176
177
# File 'lib/chingu/game_states/network_client.rb', line 175

def on_disconnect
  puts "[Disconnected from Server]"             if @debug
end

#on_msg(packet) ⇒ Object

Handler when message packets are received. Should be overriden in your code.



216
217
218
# File 'lib/chingu/game_states/network_client.rb', line 216

def on_msg(packet)
  # should be overridden.
end

#on_timeoutObject

Called when connect() recieves no initial answer from server



161
162
163
# File 'lib/chingu/game_states/network_client.rb', line 161

def on_timeout
  puts "[on_timeout() #{@address}:#{@port}]"  if @debug
end

#send_data(data) ⇒ Object

Send whatever raw data to the server Returns amount of data sent, including header.



230
231
232
233
234
235
236
237
238
239
# File 'lib/chingu/game_states/network_client.rb', line 230

def send_data(data)
  length = @socket.write([data.length].pack(NetworkServer::PACKET_HEADER_FORMAT))
  length += @socket.write(data)
  @packets_sent += 1
  @bytes_sent += length
  length
rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN
  disconnect_from_server
  0
end

#send_msg(msg) ⇒ Object

Send a msg to the server



223
224
225
# File 'lib/chingu/game_states/network_client.rb', line 223

def send_msg(msg)
  send_data(Marshal.dump(msg))
end

#updateObject

Default network loop: 1) Try to complete outgoing connection if connect() has been called 2) read raw data from server with #handle_incoming_data 3) #handle_incoming_data call #on_data(data) 4) #on_data(data) will call #on_msgs(msg)



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/chingu/game_states/network_client.rb', line 95

def update

  if @socket and not @connected
    if Gosu::milliseconds >= @connect_times_out_at
      @socket = nil
      on_timeout
    else
      begin
        # Start/Check on our nonblocking tcp connection
        @socket.connect_nonblock(@sockaddr)
      rescue Errno::EINPROGRESS   #rescue IO::WaitWritable
      rescue Errno::EALREADY
      rescue Errno::EISCONN
        @connected = true
        on_connect
      rescue Errno::EHOSTUNREACH, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EINVAL
        @socket = nil
        on_connection_refused
      rescue Errno::ETIMEDOUT
        @socket = nil
        on_timeout
      end
    end
  end
  
  handle_incoming_data if @connected

  super
end