chingu 0.8.1 → 0.9rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,205 @@
1
+ #--
2
+ #
3
+ # Chingu -- OpenGL accelerated 2D game framework for Ruby
4
+ # Copyright (C) 2009 ippa / ippa@rubylicio.us
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License as published by the Free Software Foundation; either
9
+ # version 2.1 of the License, or (at your option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with this library; if not, write to the Free Software
18
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
+ #
20
+ #++
21
+
22
+ module Chingu
23
+ module GameStates
24
+ #
25
+ # A game state for a client in a multiplayer game, suitable for smaller/middle sized games.
26
+ # Used in combination with game state NetworkServer.
27
+ #
28
+ # Uses nonblocking polling TCP and YAML to communicate.
29
+ # If your game state inherits from NetworkClient you'll have the following methods available:
30
+ #
31
+ # connect(ip, port) # Start a blocking connection period, updates in $window.caption
32
+ # send_data(data) # Send raw data on the network, nonblocking
33
+ # send_msg(whatever ruby data) # Will get YAML'd and sent to server
34
+ # handle_incoming_data(max_size) # Nonblocking read of incoming server data
35
+ # disconnect_from_server # Shuts down all network connections
36
+ #
37
+ # The following callbacks can be overwritten to add your game logic:
38
+ # on_connect # when the TCP connection to the server is opened
39
+ # on_disconnect # when server dies or disconnects you
40
+ # on_data(data) # when raw data arrives from server, if not overloaded this will unpack and call on_msg
41
+ # on_msg(msg) # an incoming msgs, could be a ruby hash or array or whatever datastructure you've chosen to send from server
42
+ # on_timeout # connection timed out
43
+ # on_connection_refused # server isn't listening on that port
44
+ #
45
+ # Usage:
46
+ # PlayState < Chingu::GameStates::NetworkClient
47
+ # def initialize(options = {})
48
+ # super # this is always needed!
49
+ # connect(options[:ip], options[:port])
50
+ # end
51
+ #
52
+ # def on_connect
53
+ # send_msg(:cmd => :hello)
54
+ # end
55
+ #
56
+ # def on_msg(msg)
57
+ # if msg[:cmd] == :ping
58
+ # send_msg(:cmd => :pong, :timestamp => msg[:timestamp]) # send back timestamp so server can calcuate lag
59
+ # end
60
+ # end
61
+ # end
62
+ #
63
+ # push_game_state PlayState.new(:ip => "127.0.0.1", :port => 7778))
64
+ #
65
+ #
66
+ # So why not EventMachine? No doubt in my mind that EventMachine is a hell of a library Chingu rolls its own for 2 reasons:
67
+ #
68
+ # AFAIK EventMachine can be hard to intergrate with the classic game loop, event machine wants its own loop
69
+ # Rubys nonblocking sockets work, so why not keep it simple
70
+ #
71
+ #
72
+ class NetworkClient < Chingu::GameState
73
+ attr_reader :latency, :socket, :packet_counter, :packet_buffer, :ip, :port
74
+
75
+ def initialize(options = {})
76
+ super
77
+ @timeout = options[:timeout] || 4
78
+ @debug = true
79
+
80
+ @socket = nil
81
+ @latency = 0
82
+ @packet_counter = 0
83
+ @packet_buffer = ""
84
+ end
85
+
86
+ #
87
+ # Default network loop:
88
+ # 1) read raw data from server with #handle_incoming_data
89
+ # 2) #handle_incoming_data call #on_data(data)
90
+ # 3) #on_data(data) will call #on_msgs(msg)
91
+ #
92
+ def update
93
+ super
94
+ handle_incoming_data
95
+ end
96
+
97
+ #
98
+ # Connect to a given ip:port (the server)
99
+ # Will timeout afte 4 seconds
100
+ #
101
+ def connect(ip, port = 7778)
102
+ return if @socket
103
+ @ip = ip
104
+ @port = port
105
+
106
+ begin
107
+ status = Timeout::timeout(@timeout) do
108
+ @socket = TCPSocket.new(ip, port)
109
+ @socket.setsockopt(Socket::IPPROTO_TCP,Socket::TCP_NODELAY,1)
110
+ on_connect
111
+ end
112
+ rescue Errno::ECONNREFUSED
113
+ on_connection_refused
114
+ rescue Timeout
115
+ on_timeout
116
+ end
117
+ end
118
+
119
+ def on_connection_refused
120
+ $window.caption = "Server: CONNECTION REFUSED"
121
+ connect(@ip, @port)
122
+ end
123
+
124
+ def on_timeout
125
+ $window.caption = "Server: CONNECTION TIMED OUT"
126
+ connect(@ip, @port)
127
+ end
128
+
129
+ #
130
+ # on_connect will be called when client successfully makes a connection to server
131
+ #
132
+ def on_connect
133
+ puts "[Connected to Server #{@ip}:#{@port}]" if @debug
134
+ end
135
+
136
+ #
137
+ # on_disconnect will be called when server disconnects client for whatever reason
138
+ #
139
+ def on_disconnect
140
+ puts "[Disconnected from Server]" if @debug
141
+ end
142
+
143
+ #
144
+ # Call this from your update() to read from socket.
145
+ # handle_incoming_data will call on_data(raw_data) when stuff comes on on the socket.
146
+ #
147
+ def handle_incoming_data(amount = 1000)
148
+ return unless @socket
149
+
150
+ if IO.select([@socket], nil, nil, 0.0)
151
+ begin
152
+ packet, sender = @socket.recvfrom(amount)
153
+ on_data(packet)
154
+ rescue Errno::ECONNABORTED
155
+ on_disconnect
156
+ end
157
+ end
158
+ end
159
+
160
+ #
161
+ # on_data(data) will be called from handle_incoming_data() by default.
162
+ #
163
+ def on_data(data)
164
+ begin
165
+ msgs = data.split("--- ")
166
+ if msgs.size > 1
167
+ @packet_buffer << msgs[0...-1].join("--- ")
168
+ YAML::load_documents(@packet_buffer) { |msg| on_msg(msg) if msg }
169
+ @packet_buffer = msgs.last
170
+ else
171
+ @packet_buffer << msgs.join
172
+ end
173
+ rescue ArgumentError
174
+ puts "Bad YAML recieved:\n#{data}"
175
+ end
176
+ end
177
+
178
+ #
179
+ # Send a msg to the server
180
+ # Can be whatever ruby-structure that responds to #to_yaml
181
+ #
182
+ def send_msg(msg)
183
+ # the "---" part is a little hack to make server understand the YAML is fully transmitted.
184
+ data = msg.to_yaml + "--- \n"
185
+ send_data(data)
186
+ end
187
+
188
+ #
189
+ # Send whatever raw data to the server
190
+ #
191
+ def send_data(data)
192
+ @socket.write(data)
193
+ @socket.flush
194
+ end
195
+
196
+ #
197
+ # Shuts down all communication (closes socket) with server
198
+ #
199
+ def disconnect_from_server
200
+ @socket.close
201
+ end
202
+
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,262 @@
1
+ #--
2
+ #
3
+ # Chingu -- OpenGL accelerated 2D game framework for Ruby
4
+ # Copyright (C) 2009 ippa / ippa@rubylicio.us
5
+ #
6
+ # This library is free software; you can redistribute it and/or
7
+ # modify it under the terms of the GNU Lesser General Public
8
+ # License as published by the Free Software Foundation; either
9
+ # version 2.1 of the License, or (at your option) any later version.
10
+ #
11
+ # This library is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14
+ # Lesser General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU Lesser General Public
17
+ # License along with this library; if not, write to the Free Software
18
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
+ #
20
+ #++
21
+
22
+ module Chingu
23
+ module GameStates
24
+ #
25
+ # A game state that acts server in a multiplayer game, suitable for smaller/middle sized games.
26
+ # Used in combination with game state NetworkClient.
27
+ #
28
+ # Uses nonblocking polling TCP and YAML to communicate.
29
+ # If your game state inherits from NetworkClient you'll have the following methods available:
30
+ #
31
+ # start(ip, port) # Start server listening on ip:port
32
+ # send_data(socket, data) # Send raw data on the network, nonblocking
33
+ # send_msg(socket, whatever ruby data) # Will get YAML'd and sent to server
34
+ # broadcast_msg(whatever ruby data) # Send stuff to all connected clients, buffered and dispatched each gametick
35
+ # handle_incoming_connections # Nonblocking accept of incoming connections from clients
36
+ # handle_incoming_data(max_size) # Nonblocking read of incoming server data
37
+
38
+ #
39
+ # The following callbacks can be overwritten to add your game logic:
40
+ #
41
+ # on_connect(socket) # when the TCP connection to the server is opened
42
+ # on_disconnect(socket) # when server dies or disconnects you
43
+ # on_data(socket, data) # when raw data arrives from server, if not overloaded this will unpack and call on_msg
44
+ # on_msg(socket, msg) # an incoming msgs, could be a ruby hash or array or whatever datastructure you've chosen to send from server
45
+ # on_start # called when socket is listening and ready
46
+ # on_start_error(msg) # callback for any error during server setup process
47
+ #
48
+ # Usage:
49
+ # ServerState < Chingu::GameStates::NetworkServer
50
+ # def initialize(options = {})
51
+ # super # this is always needed!
52
+ # connect_to_server(options[:ip], options[:port])
53
+ # end
54
+ #
55
+ # def on_connect(socket)
56
+ # send_msg(:cmd => :ping, :timestamp => Gosu::milliseconds)
57
+ # end
58
+ #
59
+ # def on_msg(socket, msg)
60
+ # if msg[:cmd] == :pong
61
+ # latency = Gosu::milliseconds - msg[:timestamp]
62
+ # puts "Server/Client roundtrip #{latency}ms"
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ # push_game_state ServerState.new(:ip => "127.0.0.1", :port => 7778))
68
+ #
69
+ # NetworkServer works mostly like NetworkClient with a few differences
70
+ # - since a server handles many sockets (1 for each connected client) all callbacks first argument is 'socket'
71
+ # - same with outgoing packets, send_data and send_msgs first argument is socket.
72
+ #
73
+ # A good idea is to have a socket-ivar in your Player-model and a Player.find_by_socket(socket)
74
+ #
75
+ class NetworkServer < Chingu::GameState
76
+ attr_reader :socket, :sockets, :packet_counter, :packet_counter, :ip, :port
77
+
78
+ def initialize(options = {})
79
+ super
80
+
81
+ @debug = true
82
+ @socket = nil
83
+ @sockets = []
84
+ @buffered_output = YAML::Stream.new
85
+
86
+ @packet_counter = 0
87
+ @packet_buffers = Hash.new
88
+ end
89
+
90
+ #
91
+ # Start server on ip 'ip' and port 'port'
92
+ #
93
+ def start(ip = '0.0.0.0', port = 7778)
94
+ @ip = ip
95
+ @port = port
96
+
97
+ begin
98
+ @socket = TCPServer.new(ip, port)
99
+ @socket.setsockopt(Socket::IPPROTO_TCP,Socket::TCP_NODELAY,1)
100
+ on_start
101
+
102
+ rescue
103
+ on_start_error($!)
104
+ end
105
+ end
106
+
107
+ #
108
+ # Callback for when Socket listens correctly on given host/port
109
+ #
110
+ def on_start
111
+ puts "* Server listening on #{ip}:#{port}" if @debug
112
+ end
113
+
114
+ #
115
+ # Callback for when something goes wrong with startup (when making TCP socket listen to a port)
116
+ #
117
+ def on_start_error(msg)
118
+ if @debug
119
+ puts "Can't start server on #{ip}:#{port}:\n"
120
+ puts msg
121
+ end
122
+ end
123
+
124
+
125
+
126
+ #
127
+ # Default network loop:
128
+ # 1) Save incoming connections with #handle_incoming_connections
129
+ # 2) read raw data from server with #handle_incoming_data
130
+ # 3) #handle_incoming_data call #on_data(data)
131
+ # 4) #on_data(data) will call #on_msgs(msg)
132
+ # 5) send all buffered broadcast data in one fell swoop
133
+ #
134
+ def update
135
+ super
136
+ if @socket && !@socket.closed?
137
+ handle_incoming_connections
138
+ handle_incoming_data
139
+ handle_outgoing_data
140
+ end
141
+ end
142
+
143
+ #
144
+ # on_connect will be called when client successfully makes a connection to server
145
+ #
146
+ def on_connect(socket)
147
+ puts "[Client Connected: #{socket}]" if @debug
148
+ end
149
+
150
+ #
151
+ # on_disconnect will be called when server disconnects client for whatever reason
152
+ #
153
+ def on_disconnect(socket)
154
+ puts "[Client Disconnected: #{socket}]" if @debug
155
+ end
156
+
157
+ def handle_incoming_connections
158
+ begin
159
+ socket = @socket.accept_nonblock
160
+ @sockets << socket
161
+ on_connect(socket)
162
+ @packet_buffers[socket] = ""
163
+ rescue IO::WaitReadable, Errno::EINTR
164
+ end
165
+ end
166
+
167
+ #
168
+ # Call this from your update() to read from socket.
169
+ # handle_incoming_data will call on_data(raw_data) when stuff comes on on the socket.
170
+ #
171
+ def handle_incoming_data(max_size = 1500)
172
+ @sockets.each do |socket|
173
+ if IO.select([socket], nil, nil, 0.0)
174
+ begin
175
+ packet, sender = socket.recvfrom(max_size)
176
+ on_data(socket, packet)
177
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET
178
+ @packet_buffers[socket] = nil
179
+ on_disconnect(socket)
180
+ end
181
+ end
182
+ end
183
+ end
184
+
185
+ #
186
+ # on_data(data) will be called from handle_incoming_data() by default.
187
+ #
188
+ def on_data(socket, data)
189
+ begin
190
+ msgs = data.split("--- ")
191
+ if msgs.size > 1
192
+ @packet_buffers[socket] << msgs[0...-1].join("--- ")
193
+ YAML::load_documents(@packet_buffers[socket]) { |msg| on_msg(socket, msg) if msg}
194
+ @packet_buffers[socket] = msgs.last
195
+ else
196
+ @packet_buffers[socket] << msgs.join
197
+ end
198
+ end
199
+ end
200
+
201
+ #
202
+ # Send all buffered outgoing data
203
+ #
204
+ def handle_outgoing_data
205
+ # the "---" part is a little hack to make server understand the YAML is fully transmitted.
206
+
207
+ data = @buffered_output.emit
208
+ if data.size > 0
209
+ @sockets.each { |socket| send_data(socket, data + "--- \n") }
210
+ @buffered_output = YAML::Stream.new
211
+ end
212
+ end
213
+
214
+ #
215
+ # Broadcast 'msg' to all connected clients
216
+ # Output is buffered and dispatched once each server-loop
217
+ #
218
+ def broadcast_msg(msg)
219
+ @buffered_output.add(msg)
220
+ end
221
+
222
+ #
223
+ # Send 'msg' to 'socket'.
224
+ # 'msg' must responds to #to_yaml
225
+ #
226
+ def send_msg(socket, msg)
227
+ # the "---" part is a little hack to make server understand the YAML is fully transmitted.
228
+ send_data(socket, msg.to_yaml + "--- \n")
229
+ end
230
+
231
+ #
232
+ # Send raw 'data' to the 'socket'
233
+ #
234
+ def send_data(socket, data)
235
+ begin
236
+ socket.write(data)
237
+ socket.flush
238
+ rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN
239
+ on_disconnect(socket)
240
+ end
241
+ end
242
+
243
+ #
244
+ # Shuts down all communication (closes socket) with a specific socket
245
+ #
246
+ def disconnect_client(socket)
247
+ socket.close
248
+ end
249
+
250
+ #
251
+ # Stops server
252
+ #
253
+ def stop
254
+ begin
255
+ @socket.close
256
+ rescue Errno::ENOTCONN
257
+ end
258
+ end
259
+
260
+ end
261
+ end
262
+ end