chingu 0.9rc7 → 0.9rc8
Sign up to get free protection for your applications and to get access to all the features.
- data/examples/example15_trait_timer2.rb +1 -1
- data/examples/example7_gfx_helpers.rb +23 -11
- data/examples/game1.rb +299 -15
- data/examples/high_score_list.yml +22 -22
- data/examples/media/city3.png +0 -0
- data/examples/media/enemy_plane.png +0 -0
- data/lib/chingu.rb +1 -1
- data/lib/chingu/animation.rb +58 -38
- data/lib/chingu/game_state_manager.rb +11 -41
- data/lib/chingu/game_states/edit.rb +5 -2
- data/lib/chingu/game_states/enter_name.rb +3 -3
- data/lib/chingu/game_states/network_client.rb +88 -67
- data/lib/chingu/game_states/network_server.rb +66 -103
- data/lib/chingu/game_states/network_state.rb +71 -0
- data/lib/chingu/gosu_ext/sample.rb +79 -0
- data/lib/chingu/gosu_ext/song.rb +96 -0
- data/lib/chingu/helpers/game_object.rb +5 -1
- data/lib/chingu/helpers/game_state.rb +2 -0
- data/lib/chingu/simple_menu.rb +2 -2
- data/lib/chingu/text.rb +25 -10
- data/lib/chingu/traits/sprite.rb +3 -3
- data/lib/chingu/viewport.rb +18 -14
- data/lib/chingu/window.rb +54 -0
- data/spec/chingu/animation_spec.rb +41 -3
- data/spec/chingu/game_state_manager_spec.rb +50 -3
- data/spec/chingu/network_spec.rb +144 -11
- metadata +20 -15
@@ -22,18 +22,18 @@
|
|
22
22
|
module Chingu
|
23
23
|
module GameStates
|
24
24
|
#
|
25
|
-
# A game state that acts server in a
|
25
|
+
# A game state that acts server in a multi-player game, suitable for smaller/middle sized games.
|
26
26
|
# Used in combination with game state NetworkClient.
|
27
27
|
#
|
28
|
-
# Uses
|
28
|
+
# Uses non-blocking polling TCP and marshal to communicate.
|
29
29
|
# If your game state inherits from NetworkClient you'll have the following methods available:
|
30
30
|
#
|
31
|
-
# start(
|
32
|
-
# send_data(socket, data) # Send raw data on the network,
|
33
|
-
# send_msg(socket, whatever ruby data) # Will get
|
31
|
+
# start(address, port) # Start server listening on address:port
|
32
|
+
# send_data(socket, data) # Send raw data on the network, non-blocking
|
33
|
+
# send_msg(socket, whatever ruby data) # Will get Marshalled and sent to server
|
34
34
|
# broadcast_msg(whatever ruby data) # Send stuff to all connected clients, buffered and dispatched each gametick
|
35
|
-
# handle_incoming_connections #
|
36
|
-
# handle_incoming_data(max_size) #
|
35
|
+
# handle_incoming_connections # Non-blocking accept of incoming connections from clients
|
36
|
+
# handle_incoming_data(max_size) # Non-blocking read of incoming server data
|
37
37
|
|
38
38
|
#
|
39
39
|
# The following callbacks can be overwritten to add your game logic:
|
@@ -60,82 +60,36 @@ module Chingu
|
|
60
60
|
# end
|
61
61
|
# end
|
62
62
|
#
|
63
|
-
# push_game_state
|
63
|
+
# push_game_state NetworkServer.new(:address => "127.0.0.1", :port => 7778).start
|
64
64
|
#
|
65
65
|
# NetworkServer works mostly like NetworkClient with a few differences
|
66
66
|
# - since a server handles many sockets (1 for each connected client) all callbacks first argument is 'socket'
|
67
|
-
# - same with outgoing packets, send_data and
|
67
|
+
# - same with outgoing packets, #send_data and #send_msg, first argument is a socket.
|
68
68
|
#
|
69
69
|
# A good idea is to have a socket-ivar in your Player-model and a Player.find_by_socket(socket)
|
70
70
|
#
|
71
|
-
class NetworkServer <
|
72
|
-
|
73
|
-
PACKET_HEADER_FORMAT = "N"
|
74
|
-
DEFAULT_PORT = 7778
|
75
|
-
|
76
|
-
class PacketBuffer
|
77
|
-
def initialize
|
78
|
-
@data = '' # Buffered data.
|
79
|
-
@length = nil # Length of the next packet. nil if header not read yet.
|
80
|
-
end
|
81
|
-
|
82
|
-
# Add data string to the buffer.
|
83
|
-
def buffer_data(data)
|
84
|
-
@data << data
|
85
|
-
end
|
86
|
-
|
87
|
-
# Call after adding data with #buffer_data until there are no more packets left.
|
88
|
-
def next_packet
|
89
|
-
# Read the header to find out the length of the next packet.
|
90
|
-
unless @length
|
91
|
-
if @data.length >= PACKET_HEADER_LENGTH
|
92
|
-
@length = @data[0...PACKET_HEADER_LENGTH].unpack(PACKET_HEADER_FORMAT)[0]
|
93
|
-
@data[0...PACKET_HEADER_LENGTH] = ''
|
94
|
-
end
|
95
|
-
end
|
96
|
-
|
97
|
-
# If there is enough data after the header for the full packet, return it.
|
98
|
-
if @length and @length <= @data.length
|
99
|
-
begin
|
100
|
-
packet = @data[0...@length]
|
101
|
-
@data[0...@length] = ''
|
102
|
-
@length = nil
|
103
|
-
return packet
|
104
|
-
rescue TypeError => ex
|
105
|
-
puts "Bad data received:\n#{@data.inspect}"
|
106
|
-
raise ex
|
107
|
-
end
|
108
|
-
else
|
109
|
-
return nil
|
110
|
-
end
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
|
-
attr_reader :socket, :sockets, :ip, :port, :max_connections
|
115
|
-
alias_method :address, :ip
|
71
|
+
class NetworkServer < NetworkState
|
72
|
+
attr_reader :socket, :sockets, :max_connections
|
116
73
|
|
117
74
|
def initialize(options = {})
|
118
|
-
super
|
119
|
-
|
120
|
-
@ip = options[:ip] || "0.0.0.0"
|
121
|
-
@port = options[:port] || DEFAULT_PORT
|
122
|
-
@debug = options[:debug]
|
75
|
+
super(options)
|
76
|
+
|
123
77
|
@max_read_per_update = options[:max_read_per_update] || 20000
|
124
78
|
@max_connections = options[:max_connections] || 256
|
125
|
-
|
79
|
+
|
126
80
|
@socket = nil
|
127
81
|
@sockets = []
|
128
82
|
@packet_buffers = Hash.new
|
129
83
|
end
|
130
|
-
|
84
|
+
|
131
85
|
#
|
132
86
|
# Start server
|
133
87
|
#
|
134
|
-
def start(
|
135
|
-
@
|
88
|
+
def start(address = nil, port = nil)
|
89
|
+
@address = address if address
|
136
90
|
@port = port if port
|
137
91
|
begin
|
138
|
-
@socket = TCPServer.new(@
|
92
|
+
@socket = TCPServer.new(@address, @port)
|
139
93
|
on_start
|
140
94
|
rescue
|
141
95
|
on_start_error($!)
|
@@ -148,7 +102,7 @@ module Chingu
|
|
148
102
|
# Callback for when Socket listens correctly on given host/port
|
149
103
|
#
|
150
104
|
def on_start
|
151
|
-
puts "* Server listening on #{
|
105
|
+
puts "* Server listening on #{address}:#{port}" if @debug
|
152
106
|
end
|
153
107
|
|
154
108
|
#
|
@@ -156,12 +110,11 @@ module Chingu
|
|
156
110
|
#
|
157
111
|
def on_start_error(msg)
|
158
112
|
if @debug
|
159
|
-
puts "Can't start server on #{
|
113
|
+
puts "Can't start server on #{address}:#{port}:\n"
|
160
114
|
puts msg
|
161
115
|
end
|
162
116
|
end
|
163
|
-
|
164
|
-
|
117
|
+
|
165
118
|
|
166
119
|
#
|
167
120
|
# Default network loop:
|
@@ -196,16 +149,12 @@ module Chingu
|
|
196
149
|
|
197
150
|
def handle_incoming_connections
|
198
151
|
begin
|
199
|
-
while socket = @socket.accept_nonblock
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
on_connect(socket)
|
204
|
-
else
|
205
|
-
socket.close
|
206
|
-
end
|
152
|
+
while @sockets.size < @max_connections and @socket and socket = @socket.accept_nonblock
|
153
|
+
@sockets << socket
|
154
|
+
@packet_buffers[socket] = PacketBuffer.new
|
155
|
+
on_connect(socket)
|
207
156
|
end
|
208
|
-
rescue IO::WaitReadable, Errno::EINTR
|
157
|
+
rescue IO::WaitReadable, Errno::EAGAIN, Errno::EINTR
|
209
158
|
end
|
210
159
|
end
|
211
160
|
|
@@ -220,9 +169,7 @@ module Chingu
|
|
220
169
|
packet, sender = socket.recvfrom(max_size)
|
221
170
|
on_data(socket, packet)
|
222
171
|
rescue Errno::ECONNABORTED, Errno::ECONNRESET, IOError
|
223
|
-
|
224
|
-
|
225
|
-
on_disconnect(socket)
|
172
|
+
disconnect_client(socket)
|
226
173
|
end
|
227
174
|
end
|
228
175
|
end
|
@@ -236,38 +183,51 @@ module Chingu
|
|
236
183
|
|
237
184
|
buffer.buffer_data data
|
238
185
|
|
186
|
+
@bytes_received += data.length
|
187
|
+
|
239
188
|
while packet = buffer.next_packet
|
240
|
-
|
189
|
+
@packets_received += 1
|
190
|
+
begin
|
191
|
+
on_msg(socket, Marshal.load(packet))
|
192
|
+
rescue TypeError
|
193
|
+
disconnect_client(socket)
|
194
|
+
break
|
195
|
+
end
|
241
196
|
end
|
242
197
|
end
|
198
|
+
|
199
|
+
# Handler when message packets are received. Should be overriden in your code.
|
200
|
+
def on_msg(socket, packet)
|
201
|
+
# should be overridden.
|
202
|
+
end
|
243
203
|
|
244
204
|
#
|
245
|
-
# Broadcast 'msg' to all connected clients
|
246
|
-
#
|
247
|
-
#
|
205
|
+
# Broadcast 'msg' to all connected clients.
|
206
|
+
# Returns amount of data sent.
|
248
207
|
def broadcast_msg(msg)
|
249
208
|
data = Marshal.dump(msg)
|
250
|
-
@sockets.
|
209
|
+
@sockets.inject(0) {|tot, s| tot + send_data(s, data) }
|
251
210
|
end
|
252
|
-
|
253
|
-
#
|
254
|
-
# Send 'msg' to 'socket'.
|
255
|
-
# 'msg' must responds to #to_yaml
|
211
|
+
|
256
212
|
#
|
213
|
+
# Send 'msg' to a specific client 'socket'.
|
214
|
+
# Returns amount of data sent.
|
257
215
|
def send_msg(socket, msg)
|
258
216
|
send_data(socket, Marshal.dump(msg))
|
259
217
|
end
|
260
218
|
|
261
219
|
#
|
262
220
|
# Send raw 'data' to the 'socket'
|
263
|
-
#
|
221
|
+
# Returns amount of data sent, including headers.
|
264
222
|
def send_data(socket, data)
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
223
|
+
length = socket.write([data.length].pack(PACKET_HEADER_FORMAT))
|
224
|
+
length += socket.write(data)
|
225
|
+
@packets_sent += 1
|
226
|
+
@bytes_sent += length
|
227
|
+
length
|
228
|
+
rescue Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE, Errno::ENOTCONN
|
229
|
+
disconnect_client(socket)
|
230
|
+
0
|
271
231
|
end
|
272
232
|
|
273
233
|
#
|
@@ -275,10 +235,11 @@ module Chingu
|
|
275
235
|
#
|
276
236
|
def disconnect_client(socket)
|
277
237
|
socket.close
|
238
|
+
rescue Errno::ENOTCONN
|
239
|
+
ensure
|
278
240
|
@sockets.delete socket
|
279
241
|
@packet_buffers.delete socket
|
280
242
|
on_disconnect(socket)
|
281
|
-
rescue Errno::ENOTCONN
|
282
243
|
end
|
283
244
|
|
284
245
|
# Ensure that the buffer is cleared of data to write (call at the end of update or, at least after all sends).
|
@@ -296,14 +257,16 @@ module Chingu
|
|
296
257
|
# Stops server
|
297
258
|
#
|
298
259
|
def stop
|
299
|
-
|
300
|
-
|
301
|
-
@sockets.each {|socket| disconnect_client(socket) }
|
302
|
-
@sockets = []
|
303
|
-
@socket.close
|
304
|
-
@socket = nil
|
260
|
+
begin
|
261
|
+
@socket.close if @socket and not @socket.closed?
|
305
262
|
rescue Errno::ENOTCONN
|
263
|
+
end
|
264
|
+
|
265
|
+
@socket = nil
|
266
|
+
@sockets.each {|socket| disconnect_client(socket) }
|
267
|
+
@sockets = []
|
306
268
|
end
|
269
|
+
|
307
270
|
alias close stop
|
308
271
|
|
309
272
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Chingu
|
2
|
+
module GameStates
|
3
|
+
# Abstract state, parent of NetworkClient and NetworkServer.
|
4
|
+
class NetworkState < GameState
|
5
|
+
PACKET_HEADER_LENGTH = 4
|
6
|
+
PACKET_HEADER_FORMAT = "N"
|
7
|
+
DEFAULT_PORT = 7778
|
8
|
+
|
9
|
+
class PacketBuffer
|
10
|
+
def initialize
|
11
|
+
@data = '' # Buffered data.
|
12
|
+
@length = nil # Length of the next packet. nil if header not read yet.
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add data string to the buffer.
|
16
|
+
def buffer_data(data)
|
17
|
+
@data << data
|
18
|
+
end
|
19
|
+
|
20
|
+
# Call after adding data with #buffer_data until there are no more packets left.
|
21
|
+
def next_packet
|
22
|
+
# Read the header to find out the length of the next packet.
|
23
|
+
unless @length
|
24
|
+
if @data.length >= PACKET_HEADER_LENGTH
|
25
|
+
@length = @data[0...PACKET_HEADER_LENGTH].unpack(PACKET_HEADER_FORMAT)[0]
|
26
|
+
@data[0...PACKET_HEADER_LENGTH] = ''
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# If there is enough data after the header for the full packet, return it.
|
31
|
+
if @length and @length <= @data.length
|
32
|
+
begin
|
33
|
+
packet = @data[0...@length]
|
34
|
+
@data[0...@length] = ''
|
35
|
+
@length = nil
|
36
|
+
return packet
|
37
|
+
rescue TypeError => ex
|
38
|
+
puts "Bad data received:\n#{@data.inspect}"
|
39
|
+
raise ex
|
40
|
+
end
|
41
|
+
else
|
42
|
+
return nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
attr_reader :address, :port
|
48
|
+
attr_reader :bytes_sent, :bytes_received
|
49
|
+
attr_reader :packets_sent, :packets_received
|
50
|
+
|
51
|
+
def initialize(options = {})
|
52
|
+
raise "Can't instantiate abstract class" if self.class == NetworkState
|
53
|
+
|
54
|
+
super(options)
|
55
|
+
|
56
|
+
reset_counters
|
57
|
+
|
58
|
+
@address = options[:address] || "0.0.0.0"
|
59
|
+
@port = options[:port] || DEFAULT_PORT
|
60
|
+
@debug = options[:debug]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Resets #bytes_sent, #bytes_received, #packets_sent and #packets_received to zero.
|
64
|
+
def reset_counters
|
65
|
+
@bytes_sent = @bytes_received = 0
|
66
|
+
@packets_sent = @packets_received = 0
|
67
|
+
0
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Gosu
|
2
|
+
class Sample
|
3
|
+
DEFAULT_VOLUME = 1.0 # Default volume of new samples.
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# Volume of all Samples.
|
7
|
+
attr_reader :volume
|
8
|
+
|
9
|
+
public
|
10
|
+
# Volume of Samples, affected by Sample.volume and Window#volume and muting.
|
11
|
+
def effective_volume
|
12
|
+
@volume * $window.effective_volume
|
13
|
+
end
|
14
|
+
|
15
|
+
public
|
16
|
+
# Set the global volume of Samples.
|
17
|
+
def volume=(value)
|
18
|
+
raise "Bad volume setting" unless value.is_a? Numeric
|
19
|
+
|
20
|
+
@volume = [[value, 1.0].min, 0.0].max.to_f
|
21
|
+
end
|
22
|
+
|
23
|
+
public
|
24
|
+
def init_sound
|
25
|
+
@volume = DEFAULT_VOLUME
|
26
|
+
nil
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
init_sound
|
31
|
+
|
32
|
+
# Volume of this Sample. This is multiplied by the volume in #play.
|
33
|
+
attr_reader :volume
|
34
|
+
|
35
|
+
alias_method :old_initialize, :initialize
|
36
|
+
protected :old_initialize
|
37
|
+
public
|
38
|
+
# Accepts :volume (0.0..1.0) option, defaulting to 1.0.
|
39
|
+
def initialize(filename, options = {})
|
40
|
+
options = {
|
41
|
+
volume: DEFAULT_VOLUME,
|
42
|
+
}.merge! options
|
43
|
+
|
44
|
+
@volume = options[:volume]
|
45
|
+
|
46
|
+
old_initialize(filename)
|
47
|
+
end
|
48
|
+
|
49
|
+
public
|
50
|
+
# Set the volume of this Sample. This is multiplied by the volume in #play.
|
51
|
+
def volume=(value)
|
52
|
+
raise "Bad volume setting" unless value.is_a? Numeric
|
53
|
+
|
54
|
+
@volume = [[value, 1.0].min, 0.0].max.to_f
|
55
|
+
end
|
56
|
+
|
57
|
+
public
|
58
|
+
# Volume the Sample will actually be played at, affected by Sample.volume and Window#volume.
|
59
|
+
def effective_volume
|
60
|
+
@volume * self.class.effective_volume
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method :old_play, :play
|
64
|
+
protected :old_play
|
65
|
+
public
|
66
|
+
def play(volume = 1, speed = 1, looping = false)
|
67
|
+
volume *= effective_volume
|
68
|
+
old_play(volume, speed, looping) if volume > 0.0
|
69
|
+
end
|
70
|
+
|
71
|
+
alias_method :old_play_pan, :play_pan
|
72
|
+
protected :old_play_pan
|
73
|
+
public
|
74
|
+
def play_pan(pan = 0, volume = 1, speed = 1, looping = false)
|
75
|
+
volume *= effective_volume
|
76
|
+
old_play_pan(pan, volume, speed, looping) if volume > 0.0
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Gosu
|
2
|
+
class Song
|
3
|
+
DEFAULT_VOLUME = 1.0
|
4
|
+
|
5
|
+
class << self
|
6
|
+
attr_reader :resources
|
7
|
+
protected :resources
|
8
|
+
|
9
|
+
# Volume of Songs, not allowing for global volume settings.
|
10
|
+
attr_reader :volume
|
11
|
+
|
12
|
+
# Volume the song is played at, affected by Song.volume and Window#volume/muting.
|
13
|
+
def effective_volume
|
14
|
+
@volume * $window.effective_volume
|
15
|
+
end
|
16
|
+
|
17
|
+
# Volume of Songs, not allowing for global volume settings.
|
18
|
+
def volume=(value)
|
19
|
+
raise "Bad volume setting" unless value.is_a? Numeric
|
20
|
+
|
21
|
+
old_volume = @volume
|
22
|
+
@volume = [[value, 1.0].min, 0.0].max.to_f
|
23
|
+
|
24
|
+
recalculate_volumes(old_volume, @volume)
|
25
|
+
|
26
|
+
@volume
|
27
|
+
end
|
28
|
+
|
29
|
+
def init_sound
|
30
|
+
@volume = DEFAULT_VOLUME
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
# Recalculate all song volumes, after a global volume (Window#volume or Song.volume) has updated.
|
36
|
+
def recalculate_volumes(old_volume, new_volume)
|
37
|
+
# Avoid divide-by-zero when working out how much to alter the value by.
|
38
|
+
multiplier = if old_volume == 0.0
|
39
|
+
(new_volume > 0) ? 1.0 : 0.0
|
40
|
+
else
|
41
|
+
new_volume / old_volume
|
42
|
+
end
|
43
|
+
resources.each_value {|song| song.send(:effective_volume=, song.volume * effective_volume * multiplier) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
init_sound
|
48
|
+
|
49
|
+
alias_method :old_initialize, :initialize
|
50
|
+
protected :old_initialize
|
51
|
+
|
52
|
+
# Volume, as played.
|
53
|
+
alias_method :effective_volume, :volume
|
54
|
+
|
55
|
+
# Set the volume, as played.
|
56
|
+
alias_method :effective_volume=, :volume=
|
57
|
+
protected :effective_volume=
|
58
|
+
|
59
|
+
# Accepts :volume (0.0..1.0) option, defaulting to 1.0.
|
60
|
+
def initialize(filename, options = {})
|
61
|
+
options = {
|
62
|
+
volume: DEFAULT_VOLUME,
|
63
|
+
}.merge! options
|
64
|
+
|
65
|
+
old_initialize(filename)
|
66
|
+
|
67
|
+
@muted = false
|
68
|
+
self.volume = options[:volume]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Volume, not affected by Song volume or the Window volume/muted.
|
72
|
+
def volume
|
73
|
+
@true_volume
|
74
|
+
end
|
75
|
+
|
76
|
+
def volume=(value)
|
77
|
+
@true_volume = [[value, 0.0].max, 1.0].min
|
78
|
+
|
79
|
+
self.effective_volume = @true_volume * self.class.effective_volume unless @muted
|
80
|
+
|
81
|
+
volume
|
82
|
+
end
|
83
|
+
|
84
|
+
protected
|
85
|
+
def mute
|
86
|
+
self.effective_volume = 0.0
|
87
|
+
self
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
def unmute
|
92
|
+
self.effective_volume = @true_volume * self.class.effective_volume
|
93
|
+
self
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|