chingu 0.8.1 → 0.9rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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