chingu 0.8.1 → 0.9rc1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +28 -0
- data/Rakefile +1 -1
- data/benchmarks/benchmark_ping_localhost.rb +63 -0
- data/chingu.gemspec +24 -6
- data/examples/example12_trait_timer.rb +5 -1
- data/examples/example28_networking.rb +75 -0
- data/examples/example29_asynchronous.rb +65 -0
- data/lib/chingu.rb +3 -1
- data/lib/chingu/async/basic_task.rb +46 -0
- data/lib/chingu/async/task_builder.rb +71 -0
- data/lib/chingu/async/task_list.rb +67 -0
- data/lib/chingu/async_tasks/call.rb +48 -0
- data/lib/chingu/async_tasks/exec.rb +48 -0
- data/lib/chingu/async_tasks/move.rb +45 -0
- data/lib/chingu/async_tasks/parallel.rb +63 -0
- data/lib/chingu/async_tasks/tween.rb +73 -0
- data/lib/chingu/async_tasks/wait.rb +54 -0
- data/lib/chingu/classic_game_object.rb +1 -1
- data/lib/chingu/core_ext/range.rb +13 -0
- data/lib/chingu/game_states/enter_name.rb +21 -11
- data/lib/chingu/game_states/network_client.rb +205 -0
- data/lib/chingu/game_states/network_server.rb +262 -0
- data/lib/chingu/simple_menu.rb +10 -2
- data/lib/chingu/traits/asynchronous.rb +84 -0
- data/lib/chingu/traits/simple_sprite.rb +1 -1
- data/lib/chingu/traits/sprite.rb +1 -1
- data/lib/chingu/window.rb +1 -1
- metadata +29 -10
@@ -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
|