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.
- 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
|