chingu 0.9rc7 → 0.9rc8
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/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
|