bevy 1.0.0

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.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/Cargo.lock +4279 -0
  3. data/Cargo.toml +36 -0
  4. data/README.md +226 -0
  5. data/crates/bevy/Cargo.toml +52 -0
  6. data/crates/bevy/src/app.rs +43 -0
  7. data/crates/bevy/src/component.rs +111 -0
  8. data/crates/bevy/src/entity.rs +30 -0
  9. data/crates/bevy/src/error.rs +32 -0
  10. data/crates/bevy/src/event.rs +190 -0
  11. data/crates/bevy/src/input_bridge.rs +300 -0
  12. data/crates/bevy/src/lib.rs +42 -0
  13. data/crates/bevy/src/mesh_renderer.rs +328 -0
  14. data/crates/bevy/src/query.rs +53 -0
  15. data/crates/bevy/src/render_app.rs +689 -0
  16. data/crates/bevy/src/resource.rs +28 -0
  17. data/crates/bevy/src/schedule.rs +186 -0
  18. data/crates/bevy/src/sprite_renderer.rs +355 -0
  19. data/crates/bevy/src/system.rs +44 -0
  20. data/crates/bevy/src/text_renderer.rs +258 -0
  21. data/crates/bevy/src/types/color.rs +114 -0
  22. data/crates/bevy/src/types/dynamic.rs +131 -0
  23. data/crates/bevy/src/types/math.rs +260 -0
  24. data/crates/bevy/src/types/mod.rs +9 -0
  25. data/crates/bevy/src/types/transform.rs +166 -0
  26. data/crates/bevy/src/world.rs +163 -0
  27. data/crates/bevy_ruby_render/Cargo.toml +22 -0
  28. data/crates/bevy_ruby_render/src/asset.rs +360 -0
  29. data/crates/bevy_ruby_render/src/audio.rs +511 -0
  30. data/crates/bevy_ruby_render/src/camera.rs +365 -0
  31. data/crates/bevy_ruby_render/src/gamepad.rs +398 -0
  32. data/crates/bevy_ruby_render/src/lib.rs +26 -0
  33. data/crates/bevy_ruby_render/src/material.rs +310 -0
  34. data/crates/bevy_ruby_render/src/mesh.rs +491 -0
  35. data/crates/bevy_ruby_render/src/sprite.rs +289 -0
  36. data/ext/bevy/Cargo.toml +20 -0
  37. data/ext/bevy/extconf.rb +6 -0
  38. data/ext/bevy/src/conversions.rs +137 -0
  39. data/ext/bevy/src/lib.rs +29 -0
  40. data/ext/bevy/src/ruby_app.rs +65 -0
  41. data/ext/bevy/src/ruby_color.rs +149 -0
  42. data/ext/bevy/src/ruby_component.rs +189 -0
  43. data/ext/bevy/src/ruby_entity.rs +33 -0
  44. data/ext/bevy/src/ruby_math.rs +384 -0
  45. data/ext/bevy/src/ruby_query.rs +64 -0
  46. data/ext/bevy/src/ruby_render_app.rs +779 -0
  47. data/ext/bevy/src/ruby_system.rs +122 -0
  48. data/ext/bevy/src/ruby_world.rs +107 -0
  49. data/lib/bevy/animation.rb +597 -0
  50. data/lib/bevy/app.rb +675 -0
  51. data/lib/bevy/asset.rb +613 -0
  52. data/lib/bevy/audio.rb +545 -0
  53. data/lib/bevy/audio_effects.rb +224 -0
  54. data/lib/bevy/camera.rb +412 -0
  55. data/lib/bevy/component.rb +91 -0
  56. data/lib/bevy/diagnostics.rb +227 -0
  57. data/lib/bevy/ecs_advanced.rb +296 -0
  58. data/lib/bevy/event.rb +199 -0
  59. data/lib/bevy/gizmos.rb +158 -0
  60. data/lib/bevy/gltf.rb +227 -0
  61. data/lib/bevy/hierarchy.rb +444 -0
  62. data/lib/bevy/input.rb +514 -0
  63. data/lib/bevy/lighting.rb +369 -0
  64. data/lib/bevy/material.rb +248 -0
  65. data/lib/bevy/mesh.rb +257 -0
  66. data/lib/bevy/navigation.rb +344 -0
  67. data/lib/bevy/networking.rb +335 -0
  68. data/lib/bevy/particle.rb +337 -0
  69. data/lib/bevy/physics.rb +396 -0
  70. data/lib/bevy/plugins/default_plugins.rb +34 -0
  71. data/lib/bevy/plugins/input_plugin.rb +49 -0
  72. data/lib/bevy/reflect.rb +361 -0
  73. data/lib/bevy/render_graph.rb +210 -0
  74. data/lib/bevy/resource.rb +185 -0
  75. data/lib/bevy/scene.rb +254 -0
  76. data/lib/bevy/shader.rb +319 -0
  77. data/lib/bevy/shape.rb +195 -0
  78. data/lib/bevy/skeletal.rb +248 -0
  79. data/lib/bevy/sprite.rb +152 -0
  80. data/lib/bevy/sprite_sheet.rb +444 -0
  81. data/lib/bevy/state.rb +277 -0
  82. data/lib/bevy/system.rb +206 -0
  83. data/lib/bevy/text.rb +99 -0
  84. data/lib/bevy/text_advanced.rb +455 -0
  85. data/lib/bevy/timer.rb +147 -0
  86. data/lib/bevy/transform.rb +158 -0
  87. data/lib/bevy/ui.rb +454 -0
  88. data/lib/bevy/ui_advanced.rb +568 -0
  89. data/lib/bevy/version.rb +5 -0
  90. data/lib/bevy/visibility.rb +250 -0
  91. data/lib/bevy/window.rb +302 -0
  92. data/lib/bevy.rb +390 -0
  93. metadata +150 -0
data/lib/bevy/asset.rb ADDED
@@ -0,0 +1,613 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bevy
4
+ module AssetState
5
+ NOT_LOADED = 'NotLoaded'
6
+ LOADING = 'Loading'
7
+ LOADED = 'Loaded'
8
+ FAILED = 'Failed'
9
+ end
10
+
11
+ class Handle
12
+ attr_reader :id, :type_name, :path
13
+
14
+ def initialize(id:, type_name:, path: nil)
15
+ @id = id
16
+ @type_name = type_name
17
+ @path = path
18
+ end
19
+
20
+ def strong?
21
+ true
22
+ end
23
+
24
+ def weak?
25
+ false
26
+ end
27
+
28
+ def ==(other)
29
+ return false unless other.is_a?(Handle)
30
+
31
+ @id == other.id && @type_name == other.type_name
32
+ end
33
+
34
+ def eql?(other)
35
+ self == other
36
+ end
37
+
38
+ def hash
39
+ [@id, @type_name].hash
40
+ end
41
+
42
+ def to_s
43
+ "Handle<#{@type_name}>(#{@id})"
44
+ end
45
+
46
+ def inspect
47
+ "#<#{self.class} id=#{@id} type=#{@type_name} path=#{@path.inspect}>"
48
+ end
49
+ end
50
+
51
+ class AssetServer
52
+ def initialize
53
+ @next_id = 0
54
+ @assets = {}
55
+ @handles = {}
56
+ @states = {}
57
+ end
58
+
59
+ def load(path, type_name = nil)
60
+ type_name ||= infer_type(path)
61
+ return @handles[path] if @handles.key?(path)
62
+
63
+ id = generate_id
64
+ handle = Handle.new(id: id, type_name: type_name, path: path)
65
+ @handles[path] = handle
66
+ @states[id] = AssetState::NOT_LOADED
67
+ handle
68
+ end
69
+
70
+ def load_async(path, type_name = nil)
71
+ handle = load(path, type_name)
72
+ @states[handle.id] = AssetState::LOADING
73
+ handle
74
+ end
75
+
76
+ def get(handle)
77
+ @assets[handle.id]
78
+ end
79
+
80
+ def get_state(handle)
81
+ @states[handle.id] || AssetState::NOT_LOADED
82
+ end
83
+
84
+ def loaded?(handle)
85
+ get_state(handle) == AssetState::LOADED
86
+ end
87
+
88
+ def loading?(handle)
89
+ get_state(handle) == AssetState::LOADING
90
+ end
91
+
92
+ def failed?(handle)
93
+ get_state(handle) == AssetState::FAILED
94
+ end
95
+
96
+ def set_loaded(handle, asset)
97
+ @assets[handle.id] = asset
98
+ @states[handle.id] = AssetState::LOADED
99
+ end
100
+
101
+ def set_failed(handle)
102
+ @states[handle.id] = AssetState::FAILED
103
+ end
104
+
105
+ def get_handle(path)
106
+ @handles[path]
107
+ end
108
+
109
+ def all_handles
110
+ @handles.values
111
+ end
112
+
113
+ def loaded_handles
114
+ @handles.values.select { |h| loaded?(h) }
115
+ end
116
+
117
+ def pending_handles
118
+ @handles.values.select { |h| loading?(h) }
119
+ end
120
+
121
+ private
122
+
123
+ def generate_id
124
+ id = @next_id
125
+ @next_id += 1
126
+ id
127
+ end
128
+
129
+ def infer_type(path)
130
+ ext = File.extname(path).downcase
131
+ case ext
132
+ when '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'
133
+ 'Image'
134
+ when '.ogg', '.wav', '.mp3', '.flac'
135
+ 'AudioSource'
136
+ when '.ttf', '.otf'
137
+ 'Font'
138
+ when '.gltf', '.glb'
139
+ 'Gltf'
140
+ when '.json'
141
+ 'JsonAsset'
142
+ when '.ron'
143
+ 'RonAsset'
144
+ else
145
+ 'Unknown'
146
+ end
147
+ end
148
+ end
149
+
150
+ class Assets
151
+ def initialize(type_name)
152
+ @type_name = type_name
153
+ @assets = {}
154
+ end
155
+
156
+ def add(asset)
157
+ id = @assets.length
158
+ handle = Handle.new(id: id, type_name: @type_name)
159
+ @assets[id] = asset
160
+ handle
161
+ end
162
+
163
+ def get(handle)
164
+ return nil unless handle.type_name == @type_name
165
+
166
+ @assets[handle.id]
167
+ end
168
+
169
+ def get_mut(handle)
170
+ get(handle)
171
+ end
172
+
173
+ def remove(handle)
174
+ @assets.delete(handle.id)
175
+ end
176
+
177
+ def contains?(handle)
178
+ @assets.key?(handle.id)
179
+ end
180
+
181
+ def iter
182
+ @assets.map { |id, asset| [Handle.new(id: id, type_name: @type_name), asset] }
183
+ end
184
+
185
+ def each(&block)
186
+ iter.each(&block)
187
+ end
188
+
189
+ def len
190
+ @assets.length
191
+ end
192
+
193
+ def empty?
194
+ @assets.empty?
195
+ end
196
+ end
197
+
198
+ class AssetPath
199
+ attr_reader :path, :label
200
+
201
+ def initialize(path, label: nil)
202
+ @path = path
203
+ @label = label
204
+ end
205
+
206
+ def self.parse(asset_path)
207
+ if asset_path.include?('#')
208
+ path, label = asset_path.split('#', 2)
209
+ new(path, label: label)
210
+ else
211
+ new(asset_path)
212
+ end
213
+ end
214
+
215
+ def full_path
216
+ @label ? "#{@path}##{@label}" : @path
217
+ end
218
+
219
+ def extension
220
+ File.extname(@path).delete_prefix('.')
221
+ end
222
+
223
+ def file_name
224
+ File.basename(@path)
225
+ end
226
+
227
+ def parent
228
+ File.dirname(@path)
229
+ end
230
+
231
+ def ==(other)
232
+ return false unless other.is_a?(AssetPath)
233
+
234
+ @path == other.path && @label == other.label
235
+ end
236
+
237
+ def to_s
238
+ full_path
239
+ end
240
+ end
241
+
242
+ class AssetEvent
243
+ attr_reader :type, :handle
244
+
245
+ CREATED = :created
246
+ MODIFIED = :modified
247
+ REMOVED = :removed
248
+ LOADED = :loaded
249
+ FAILED = :failed
250
+
251
+ def initialize(type, handle)
252
+ @type = type
253
+ @handle = handle
254
+ end
255
+
256
+ def created?
257
+ @type == CREATED
258
+ end
259
+
260
+ def modified?
261
+ @type == MODIFIED
262
+ end
263
+
264
+ def removed?
265
+ @type == REMOVED
266
+ end
267
+
268
+ def loaded?
269
+ @type == LOADED
270
+ end
271
+
272
+ def failed?
273
+ @type == FAILED
274
+ end
275
+ end
276
+
277
+ class AssetLoader
278
+ attr_reader :extensions
279
+
280
+ def initialize(extensions: [])
281
+ @extensions = extensions
282
+ end
283
+
284
+ def load(path)
285
+ raise NotImplementedError, 'Subclasses must implement #load'
286
+ end
287
+
288
+ def can_load?(path)
289
+ ext = File.extname(path).delete_prefix('.').downcase
290
+ @extensions.include?(ext)
291
+ end
292
+ end
293
+
294
+ class ImageAsset
295
+ attr_reader :path, :width, :height, :data
296
+
297
+ def initialize(path:, width: 0, height: 0, data: nil)
298
+ @path = path
299
+ @width = width
300
+ @height = height
301
+ @data = data
302
+ end
303
+
304
+ def loaded?
305
+ !@data.nil?
306
+ end
307
+
308
+ def aspect_ratio
309
+ return 1.0 if @height.zero?
310
+
311
+ @width.to_f / @height.to_f
312
+ end
313
+ end
314
+
315
+ class FontAsset
316
+ attr_reader :path, :family
317
+
318
+ def initialize(path:, family: nil)
319
+ @path = path
320
+ @family = family || File.basename(path, '.*')
321
+ end
322
+ end
323
+
324
+ class AudioAsset
325
+ attr_reader :path, :duration
326
+
327
+ def initialize(path:, duration: 0.0)
328
+ @path = path
329
+ @duration = duration
330
+ end
331
+ end
332
+
333
+ class ImageLoader < AssetLoader
334
+ def initialize
335
+ super(extensions: %w[png jpg jpeg gif bmp webp])
336
+ end
337
+
338
+ def load(path)
339
+ return nil unless File.exist?(path)
340
+
341
+ ImageAsset.new(path: path)
342
+ end
343
+ end
344
+
345
+ class FontLoader < AssetLoader
346
+ def initialize
347
+ super(extensions: %w[ttf otf woff woff2])
348
+ end
349
+
350
+ def load(path)
351
+ return nil unless File.exist?(path)
352
+
353
+ FontAsset.new(path: path)
354
+ end
355
+ end
356
+
357
+ class AudioLoader < AssetLoader
358
+ def initialize
359
+ super(extensions: %w[ogg wav mp3 flac])
360
+ end
361
+
362
+ def load(path)
363
+ return nil unless File.exist?(path)
364
+
365
+ AudioAsset.new(path: path)
366
+ end
367
+ end
368
+
369
+ class AssetManager
370
+ def initialize
371
+ @asset_server = AssetServer.new
372
+ @loaders = {
373
+ 'Image' => ImageLoader.new,
374
+ 'Font' => FontLoader.new,
375
+ 'AudioSource' => AudioLoader.new
376
+ }
377
+ @hot_reload_enabled = false
378
+ @file_watcher = FileWatcher.new
379
+ @reload_callbacks = {}
380
+ @dependencies = {}
381
+ end
382
+
383
+ attr_reader :asset_server
384
+ attr_reader :hot_reload_enabled
385
+
386
+ def register_loader(type_name, loader)
387
+ @loaders[type_name] = loader
388
+ end
389
+
390
+ def enable_hot_reload
391
+ @hot_reload_enabled = true
392
+ end
393
+
394
+ def disable_hot_reload
395
+ @hot_reload_enabled = false
396
+ end
397
+
398
+ def on_reload(handle, &callback)
399
+ @reload_callbacks[handle.id] ||= []
400
+ @reload_callbacks[handle.id] << callback
401
+ end
402
+
403
+ def add_dependency(asset_handle, dependency_handle)
404
+ @dependencies[asset_handle.id] ||= []
405
+ @dependencies[asset_handle.id] << dependency_handle.id
406
+ end
407
+
408
+ def load(path)
409
+ handle = @asset_server.load(path)
410
+ type_name = handle.type_name
411
+
412
+ if @loaders.key?(type_name)
413
+ loader = @loaders[type_name]
414
+ asset = loader.load(path)
415
+
416
+ if asset
417
+ @asset_server.set_loaded(handle, asset)
418
+ @file_watcher.watch(path) if @hot_reload_enabled
419
+ else
420
+ @asset_server.set_failed(handle)
421
+ end
422
+ end
423
+
424
+ handle
425
+ end
426
+
427
+ def load_async(path, &callback)
428
+ handle = @asset_server.load_async(path)
429
+
430
+ Thread.new do
431
+ type_name = handle.type_name
432
+
433
+ if @loaders.key?(type_name)
434
+ loader = @loaders[type_name]
435
+ asset = loader.load(path)
436
+
437
+ if asset
438
+ @asset_server.set_loaded(handle, asset)
439
+ @file_watcher.watch(path) if @hot_reload_enabled
440
+ callback&.call(handle, :loaded)
441
+ else
442
+ @asset_server.set_failed(handle)
443
+ callback&.call(handle, :failed)
444
+ end
445
+ end
446
+ end
447
+
448
+ handle
449
+ end
450
+
451
+ def reload(handle)
452
+ path = handle.path
453
+ return false unless path
454
+
455
+ type_name = handle.type_name
456
+ return false unless @loaders.key?(type_name)
457
+
458
+ loader = @loaders[type_name]
459
+ asset = loader.load(path)
460
+
461
+ if asset
462
+ @asset_server.set_loaded(handle, asset)
463
+ notify_reload(handle)
464
+ reload_dependents(handle)
465
+ true
466
+ else
467
+ false
468
+ end
469
+ end
470
+
471
+ def check_for_changes
472
+ return [] unless @hot_reload_enabled
473
+
474
+ changes = @file_watcher.check_changes
475
+ reloaded = []
476
+
477
+ changes.each do |path, change_type|
478
+ handle = @asset_server.get_handle(path)
479
+ next unless handle
480
+
481
+ case change_type
482
+ when :modified
483
+ if reload(handle)
484
+ reloaded << AssetEvent.new(AssetEvent::MODIFIED, handle)
485
+ end
486
+ when :deleted
487
+ reloaded << AssetEvent.new(AssetEvent::REMOVED, handle)
488
+ end
489
+ end
490
+
491
+ reloaded
492
+ end
493
+
494
+ def get(handle)
495
+ @asset_server.get(handle)
496
+ end
497
+
498
+ def loaded?(handle)
499
+ @asset_server.loaded?(handle)
500
+ end
501
+
502
+ def loading?(handle)
503
+ @asset_server.loading?(handle)
504
+ end
505
+
506
+ def failed?(handle)
507
+ @asset_server.failed?(handle)
508
+ end
509
+
510
+ def get_state(handle)
511
+ @asset_server.get_state(handle)
512
+ end
513
+
514
+ def unload(handle)
515
+ @file_watcher.unwatch(handle.path) if handle.path
516
+ @reload_callbacks.delete(handle.id)
517
+ @dependencies.delete(handle.id)
518
+ end
519
+
520
+ private
521
+
522
+ def notify_reload(handle)
523
+ callbacks = @reload_callbacks[handle.id]
524
+ return unless callbacks
525
+
526
+ callbacks.each { |cb| cb.call(handle) }
527
+ end
528
+
529
+ def reload_dependents(handle)
530
+ @dependencies.each do |asset_id, deps|
531
+ next unless deps.include?(handle.id)
532
+
533
+ dependent_handle = find_handle_by_id(asset_id)
534
+ reload(dependent_handle) if dependent_handle
535
+ end
536
+ end
537
+
538
+ def find_handle_by_id(id)
539
+ @asset_server.all_handles.find { |h| h.id == id }
540
+ end
541
+ end
542
+
543
+ class FileWatcher
544
+ def initialize
545
+ @watched = {}
546
+ @poll_interval = 1.0
547
+ end
548
+
549
+ attr_accessor :poll_interval
550
+
551
+ def watch(path)
552
+ return unless File.exist?(path)
553
+
554
+ @watched[path] = File.mtime(path)
555
+ end
556
+
557
+ def unwatch(path)
558
+ @watched.delete(path)
559
+ end
560
+
561
+ def check_changes
562
+ changes = []
563
+ to_remove = []
564
+
565
+ @watched.each do |path, last_mtime|
566
+ if File.exist?(path)
567
+ current_mtime = File.mtime(path)
568
+ if current_mtime > last_mtime
569
+ changes << [path, :modified]
570
+ @watched[path] = current_mtime
571
+ end
572
+ else
573
+ changes << [path, :deleted]
574
+ to_remove << path
575
+ end
576
+ end
577
+
578
+ to_remove.each { |p| @watched.delete(p) }
579
+ changes
580
+ end
581
+
582
+ def watched_count
583
+ @watched.size
584
+ end
585
+
586
+ def watching?(path)
587
+ @watched.key?(path)
588
+ end
589
+ end
590
+
591
+ class HotReloadPlugin
592
+ def initialize(poll_interval: 1.0)
593
+ @poll_interval = poll_interval
594
+ @last_check = ::Time.now
595
+ end
596
+
597
+ def build(app)
598
+ app.asset_manager.enable_hot_reload
599
+ app.asset_manager.instance_variable_get(:@file_watcher).poll_interval = @poll_interval
600
+ end
601
+
602
+ def update(app, _delta)
603
+ now = ::Time.now
604
+ return if now - @last_check < @poll_interval
605
+
606
+ @last_check = now
607
+ events = app.asset_manager.check_for_changes
608
+ events.each do |event|
609
+ puts "[HotReload] #{event.type}: #{event.handle.path}" if ENV['BEVY_DEBUG']
610
+ end
611
+ end
612
+ end
613
+ end