rgame 0.2.0 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +135 -1
- data/README.md +67 -65
- data/docs/api/README.md +86 -82
- data/docs/api/app.md +125 -68
- data/docs/api/assets.md +203 -165
- data/docs/api/audio.md +130 -89
- data/docs/api/cli.md +259 -0
- data/docs/api/components.md +1032 -265
- data/docs/api/drawing.md +160 -160
- data/docs/api/examples.md +263 -0
- data/docs/api/game.md +149 -50
- data/docs/api/images.md +39 -39
- data/docs/api/input.md +226 -148
- data/docs/api/internals.md +241 -62
- data/docs/api/localization.md +285 -0
- data/docs/api/scene_graph.md +397 -244
- data/docs/api/signals.md +81 -76
- data/docs/api/systems.md +186 -73
- data/docs/api/text.md +58 -41
- data/docs/api/tile_maps.md +237 -0
- data/docs/api/toolbox.md +395 -146
- data/docs/api/ui.md +883 -54
- data/docs/api/values.md +216 -33
- data/examples/assets/README.md +322 -0
- data/examples/assets/blip.ogg +0 -0
- data/examples/assets/glyphs.json +5 -0
- data/examples/assets/glyphs.png +0 -0
- data/examples/assets/hero.json +12 -0
- data/examples/assets/hero.png +0 -0
- data/examples/assets/icons.json +13 -0
- data/examples/assets/icons.png +0 -0
- data/examples/assets/music.ogg +0 -0
- data/examples/assets/skills.json +10 -0
- data/examples/assets/skills.png +0 -0
- data/examples/assets/tileset.png +0 -0
- data/examples/assets/tileset.tsx +65 -0
- data/examples/assets/town.tmx +26 -0
- data/examples/assets/ui.json +11 -0
- data/examples/assets/ui.png +0 -0
- data/examples/collision/locales/en.yml +8 -0
- data/examples/collision/main.rb +316 -0
- data/examples/collision_tiles/locales/en.yml +9 -0
- data/examples/collision_tiles/main.rb +274 -0
- data/examples/fullscreen/locales/en.yml +10 -0
- data/examples/fullscreen/main.rb +216 -0
- data/examples/game_menu/locales/en.yml +8 -0
- data/examples/game_menu/main.rb +170 -0
- data/examples/input_glyphs/locales/en.yml +14 -0
- data/examples/input_glyphs/main.rb +213 -0
- data/examples/jump_topdown/locales/en.yml +9 -0
- data/examples/jump_topdown/main.rb +178 -0
- data/examples/localization/locales/de.yml +12 -0
- data/examples/localization/locales/en.yml +13 -0
- data/examples/localization/main.rb +158 -0
- data/examples/menu_navigation/locales/en.yml +23 -0
- data/examples/menu_navigation/main.rb +365 -0
- data/examples/music/locales/en.yml +7 -0
- data/examples/music/main.rb +134 -0
- data/examples/pathfinding/locales/en.yml +17 -0
- data/examples/pathfinding/main.rb +298 -0
- data/examples/pooling/locales/en.yml +7 -0
- data/examples/pooling/main.rb +259 -0
- data/examples/quick_wheel/locales/en.yml +16 -0
- data/examples/quick_wheel/main.rb +184 -0
- data/examples/radial_menu/locales/en.yml +16 -0
- data/examples/radial_menu/main.rb +184 -0
- data/examples/save_load/locales/en.yml +11 -0
- data/examples/save_load/main.rb +207 -0
- data/examples/save_load_ids/locales/en.yml +11 -0
- data/examples/save_load_ids/main.rb +322 -0
- data/examples/scroll_map/locales/en.yml +4 -0
- data/examples/scroll_map/main.rb +140 -0
- data/examples/signals/locales/en.yml +6 -0
- data/examples/signals/main.rb +278 -0
- data/examples/skill_bar/locales/en.yml +14 -0
- data/examples/skill_bar/main.rb +159 -0
- data/examples/sound/locales/en.yml +6 -0
- data/examples/sound/main.rb +122 -0
- data/examples/split_screen/locales/en.yml +9 -0
- data/examples/split_screen/main.rb +304 -0
- data/examples/sprite/locales/en.yml +8 -0
- data/examples/sprite/main.rb +180 -0
- data/examples/timer/locales/en.yml +12 -0
- data/examples/timer/main.rb +273 -0
- data/examples/velocity/locales/en.yml +6 -0
- data/examples/velocity/main.rb +196 -0
- data/examples/walk/locales/en.yml +4 -0
- data/examples/walk/main.rb +99 -0
- data/exe/rgame +9 -0
- data/ext/rgame_core/app/app.c +33 -3
- data/ext/rgame_core/app/locale.c +67 -0
- data/ext/rgame_core/app/locale.h +28 -0
- data/ext/rgame_core/audio/audio.c +39 -2
- data/ext/rgame_core/example.rb +0 -49
- data/ext/rgame_core/extconf.rb +0 -125
- data/ext/rgame_core/include/rgame/core.h +38 -2
- data/ext/rgame_core/ruby/audio_ext.c +10 -5
- data/ext/rgame_core/ruby/core_ext.c +30 -7
- data/ext/rgame_core/ruby/core_ext.h +3 -0
- data/ext/rgame_core/ruby/locale_ext.c +44 -0
- data/ext/rgame_core/ruby/recording_ext.c +1 -1
- data/ext/rgame_core/ruby/renderer_ext.c +20 -20
- data/ext/rgame_util/extconf.rb +2 -20
- data/ext/rgame_util/route_search.c +305 -0
- data/ext/rgame_util/route_search.h +86 -0
- data/ext/rgame_util/route_search_ext.c +150 -0
- data/ext/rgame_util/solid_grid.c +58 -0
- data/ext/rgame_util/solid_grid.h +49 -0
- data/ext/rgame_util/solid_grid_ext.c +161 -0
- data/ext/rgame_util/tile_sweep.c +164 -0
- data/ext/rgame_util/tile_sweep.h +62 -0
- data/ext/rgame_util/tile_sweep_ext.c +155 -0
- data/ext/rgame_util/util_ext.c +3 -0
- data/ext/rgame_util/util_ext.h +15 -0
- data/lib/rgame/boot.rb +0 -10
- data/lib/rgame/cli/new_project.rb +139 -0
- data/lib/rgame/cli/templates/Gemfile.tt +23 -0
- data/lib/rgame/cli/templates/README.md.tt +93 -0
- data/lib/rgame/cli/templates/Rakefile.tt +9 -0
- data/lib/rgame/cli/templates/assets/locales/en.yml.tt +10 -0
- data/lib/rgame/cli/templates/game.rb.tt +23 -0
- data/lib/rgame/cli/templates/gitignore.tt +12 -0
- data/lib/rgame/cli/templates/main.rb.tt +11 -0
- data/lib/rgame/cli/templates/nodes/root.rb.tt +24 -0
- data/lib/rgame/cli/templates/rspec.tt +2 -0
- data/lib/rgame/cli/templates/rubocop.yml.tt +75 -0
- data/lib/rgame/cli/templates/ruby-version.tt +1 -0
- data/lib/rgame/cli/templates/spec/locales_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/nodes/root_spec.rb.tt +18 -0
- data/lib/rgame/cli/templates/spec/spec_helper.rb.tt +37 -0
- data/lib/rgame/cli.rb +66 -0
- data/lib/rgame/core/app.rb +6 -44
- data/lib/rgame/core/asset_manager.rb +13 -31
- data/lib/rgame/core/audio.rb +37 -16
- data/lib/rgame/core/font.rb +0 -3
- data/lib/rgame/core/locale.rb +22 -0
- data/lib/rgame/core/nine_slice.rb +0 -21
- data/lib/rgame/core/renderer.rb +6 -63
- data/lib/rgame/core/sprite_sheet.rb +0 -3
- data/lib/rgame/core/tile_map_renderer.rb +2 -19
- data/lib/rgame/core/ui_atlas.rb +28 -13
- data/lib/rgame/core.rb +1 -8
- data/lib/rgame/engine/actor_blockers.rb +131 -0
- data/lib/rgame/engine/animation_set.rb +1 -0
- data/lib/rgame/engine/audio_director.rb +36 -6
- data/lib/rgame/engine/bounds_blockers.rb +74 -0
- data/lib/rgame/engine/camera.rb +3 -3
- data/lib/rgame/engine/circle_collider.rb +4 -2
- data/lib/rgame/engine/collision_box.rb +26 -1
- data/lib/rgame/engine/collision_system.rb +110 -22
- data/lib/rgame/engine/component.rb +34 -10
- data/lib/rgame/engine/components/action_trigger.rb +0 -1
- data/lib/rgame/engine/components/animated_sprite.rb +27 -25
- data/lib/rgame/engine/components/box_collider.rb +99 -0
- data/lib/rgame/engine/components/camera_follow.rb +6 -5
- data/lib/rgame/engine/components/character_body.rb +20 -61
- data/lib/rgame/engine/components/circle_collider.rb +47 -11
- data/lib/rgame/engine/components/collision_world.rb +159 -31
- data/lib/rgame/engine/components/despawn_offscreen.rb +24 -8
- data/lib/rgame/engine/components/feet_collider.rb +61 -0
- data/lib/rgame/engine/components/hop.rb +76 -0
- data/lib/rgame/engine/components/identity.rb +73 -0
- data/lib/rgame/engine/components/mover.rb +285 -0
- data/lib/rgame/engine/components/navigator.rb +145 -0
- data/lib/rgame/engine/components/path_follow.rb +123 -31
- data/lib/rgame/engine/components/player_controller.rb +5 -2
- data/lib/rgame/engine/components/pool.rb +1 -1
- data/lib/rgame/engine/components/screen_wrap.rb +33 -11
- data/lib/rgame/engine/components/sprite.rb +12 -6
- data/lib/rgame/engine/components/targeting.rb +9 -11
- data/lib/rgame/engine/components/thrust_controller.rb +1 -1
- data/lib/rgame/engine/components/tile_world.rb +41 -13
- data/lib/rgame/engine/components/timer.rb +1 -1
- data/lib/rgame/engine/components/velocity.rb +23 -7
- data/lib/rgame/engine/components/wander_controller.rb +6 -2
- data/lib/rgame/engine/components/world.rb +133 -0
- data/lib/rgame/engine/contact_set.rb +74 -0
- data/lib/rgame/engine/culling.rb +4 -6
- data/lib/rgame/engine/debug_overlay.rb +5 -11
- data/lib/rgame/engine/i18n/plural.rb +45 -0
- data/lib/rgame/engine/i18n/plural_rules.rb +82 -0
- data/lib/rgame/engine/i18n/template.rb +59 -0
- data/lib/rgame/engine/i18n.rb +276 -51
- data/lib/rgame/engine/input/action_mapper.rb +0 -26
- data/lib/rgame/engine/input/actions.rb +2 -8
- data/lib/rgame/engine/input/input_map.rb +39 -21
- data/lib/rgame/engine/nav_grid.rb +87 -0
- data/lib/rgame/engine/node2d.rb +248 -133
- data/lib/rgame/engine/path.rb +4 -6
- data/lib/rgame/engine/players.rb +6 -13
- data/lib/rgame/engine/presentation.rb +171 -0
- data/lib/rgame/engine/scene/scene_stack.rb +4 -4
- data/lib/rgame/engine/sealed_privates.rb +54 -0
- data/lib/rgame/engine/spatial_hash.rb +51 -19
- data/lib/rgame/engine/text.rb +194 -0
- data/lib/rgame/engine/tile_blockers.rb +63 -0
- data/lib/rgame/engine/tile_map.rb +2 -3
- data/lib/rgame/engine/tile_map_layer.rb +0 -2
- data/lib/rgame/engine/tileset.rb +2 -4
- data/lib/rgame/engine/timer.rb +2 -2
- data/lib/rgame/engine/ui/button.rb +248 -0
- data/lib/rgame/engine/ui/column.rb +20 -0
- data/lib/rgame/engine/ui/icon_button.rb +93 -0
- data/lib/rgame/engine/ui/menu.rb +246 -71
- data/lib/rgame/engine/ui/navigation.rb +57 -0
- data/lib/rgame/engine/ui/nine_slice_style.rb +50 -0
- data/lib/rgame/engine/ui/option_button.rb +163 -0
- data/lib/rgame/engine/ui/panel_button.rb +32 -0
- data/lib/rgame/engine/ui/panel_menu.rb +36 -0
- data/lib/rgame/engine/ui/pointing.rb +146 -0
- data/lib/rgame/engine/ui/radial_menu.rb +85 -0
- data/lib/rgame/engine/ui/ring.rb +55 -0
- data/lib/rgame/engine/ui/row.rb +21 -0
- data/lib/rgame/engine/ui/shape_style.rb +102 -0
- data/lib/rgame/engine/ui/stack.rb +58 -0
- data/lib/rgame/engine/ui/stepping.rb +93 -0
- data/lib/rgame/engine/ui/text_button.rb +59 -0
- data/lib/rgame/engine/viewports.rb +2 -5
- data/lib/rgame/engine/world_view.rb +5 -4
- data/lib/rgame/engine.rb +31 -25
- data/lib/rgame/game.rb +99 -27
- data/lib/rgame/rubocop/cop/game/draw_in_local_space.rb +103 -0
- data/lib/rgame/rubocop/cop/game/hot_path.rb +36 -0
- data/lib/rgame/rubocop/cop/game/layer_boundary.rb +43 -0
- data/lib/rgame/rubocop/cop/game/no_core_in_engine_layer.rb +100 -0
- data/lib/rgame/rubocop/cop/game/no_engine_in_core_layer.rb +84 -0
- data/lib/rgame/rubocop/cop/game/no_interpolation_in_hot_path.rb +50 -0
- data/lib/rgame/rubocop/cop/game/no_literal_text.rb +41 -0
- data/lib/rgame/rubocop/cop/game/no_needless_allocation.rb +112 -0
- data/lib/rgame/rubocop/default.yml +39 -0
- data/lib/rgame/rubocop/plugin.rb +45 -0
- data/lib/rgame/rubocop.rb +11 -0
- data/lib/rgame/util/color.rb +20 -24
- data/lib/rgame/util/controls.rb +14 -28
- data/lib/rgame/util/route_search.rb +27 -0
- data/lib/rgame/util/save_file.rb +107 -0
- data/lib/rgame/util/solid_grid.rb +37 -0
- data/lib/rgame/util/tensor.rb +0 -9
- data/lib/rgame/util/tile_sweep.rb +36 -0
- data/lib/rgame/util/z.rb +4 -14
- data/lib/rgame/util.rb +4 -3
- data/lib/rgame/version.rb +1 -1
- data/lib/rgame.rb +0 -15
- metadata +152 -11
- data/lib/rgame/engine/actor.rb +0 -53
- data/lib/rgame/engine/body.rb +0 -49
- data/lib/rgame/engine/cached_label.rb +0 -33
- data/lib/rgame/engine/input/player_controller.rb +0 -14
- data/lib/rgame/engine/matrix.rb +0 -32
- data/lib/rgame/engine/resettable.rb +0 -67
- data/lib/rgame/engine/tile_collision.rb +0 -78
- data/lib/rgame/engine/ui/menu_item.rb +0 -84
data/lib/rgame/boot.rb
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Turns on YJIT, Ruby's JIT compiler, for a meaningful speedup in the game loop.
|
|
4
|
-
#
|
|
5
|
-
# Required by RGame::Game rather than left to each game to remember, because a
|
|
6
|
-
# game loop is the case JIT was built for and forgetting costs frames silently.
|
|
7
|
-
# It is deliberately *not* required by `rgame` or `rgame/core`: enabling a JIT
|
|
8
|
-
# is a decision about the whole process, and a library has no business making
|
|
9
|
-
# it for a host that only wanted a Color.
|
|
10
|
-
#
|
|
11
|
-
# Guarded, so the project still runs on a Ruby built without YJIT — it just runs
|
|
12
|
-
# unaccelerated instead of crashing.
|
|
13
3
|
RubyVM::YJIT.enable if defined?(RubyVM::YJIT) && RubyVM::YJIT.respond_to?(:enable)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rubygems/version'
|
|
6
|
+
|
|
7
|
+
require_relative '../version'
|
|
8
|
+
|
|
9
|
+
module RGame
|
|
10
|
+
module CLI
|
|
11
|
+
# Writes a new game project: `rgame new tictactoe`.
|
|
12
|
+
#
|
|
13
|
+
# The layout it produces is the project's own architecture in miniature, and
|
|
14
|
+
# the generated comments say so at each file:
|
|
15
|
+
#
|
|
16
|
+
# - `game.rb` is the only file that requires `rgame/game`, so it is the
|
|
17
|
+
# only one that loads SDL and OpenGL. It is the game's glue class, the
|
|
18
|
+
# local counterpart of RGame::Game.
|
|
19
|
+
# - `nodes/` requires `rgame` and names only RGame::Engine, so every node
|
|
20
|
+
# runs with no window.
|
|
21
|
+
# - `spec/` therefore loads `rgame` too, and the whole suite runs headless.
|
|
22
|
+
#
|
|
23
|
+
# Getting a newcomer to that shape by default is most of the point: it is
|
|
24
|
+
# the arrangement that keeps game logic spec-able, and it is not one anybody
|
|
25
|
+
# would arrive at by guessing.
|
|
26
|
+
#
|
|
27
|
+
# @api private
|
|
28
|
+
class NewProject
|
|
29
|
+
# Anything the caller did wrong: a bad name, a directory in the way.
|
|
30
|
+
# RGame::CLI turns it into a message and a non-zero status.
|
|
31
|
+
class Error < StandardError; end
|
|
32
|
+
|
|
33
|
+
TEMPLATE_ROOT = File.expand_path('templates', __dir__)
|
|
34
|
+
|
|
35
|
+
DOTFILES = {
|
|
36
|
+
'gitignore' => '.gitignore',
|
|
37
|
+
'rspec' => '.rspec',
|
|
38
|
+
'ruby-version' => '.ruby-version',
|
|
39
|
+
'rubocop.yml' => '.rubocop.yml'
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_-]*\z/i
|
|
43
|
+
|
|
44
|
+
def initialize(name, out: $stdout, root: Dir.pwd)
|
|
45
|
+
@name = name
|
|
46
|
+
@out = out
|
|
47
|
+
@target = File.expand_path(name, root)
|
|
48
|
+
|
|
49
|
+
validate_name!
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def generate
|
|
53
|
+
check_target!
|
|
54
|
+
|
|
55
|
+
templates.each { |source, destination| write(destination, render(source)) }
|
|
56
|
+
|
|
57
|
+
report_next_steps
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# `tic_tac_toe` and `tic-tac-toe` both give `TicTacToe`. Used for the
|
|
61
|
+
# window caption and, with `Game` appended, for the game class.
|
|
62
|
+
def caption = @name.split(/[_-]+/).map(&:capitalize).join
|
|
63
|
+
|
|
64
|
+
def game_class = "#{caption}Game"
|
|
65
|
+
|
|
66
|
+
def app_name = @name
|
|
67
|
+
|
|
68
|
+
# "~> 0.2" for a 0.2.0 generator, so a project tracks whatever version
|
|
69
|
+
# created it rather than a number frozen into a template.
|
|
70
|
+
def rgame_requirement = Gem::Version.new(RGame::VERSION).approximate_recommendation
|
|
71
|
+
|
|
72
|
+
# The Ruby running `rgame new`, written to .ruby-version and pointed at
|
|
73
|
+
# from the Gemfile. Whichever Ruby generated the project is the one it was
|
|
74
|
+
# known to work on, which is the only version this can honestly claim.
|
|
75
|
+
#
|
|
76
|
+
# The bare `4.0.5` form rather than mise's `ruby 4.0.5`: every version
|
|
77
|
+
# manager reads it, and so does Bundler's `ruby file:`.
|
|
78
|
+
def ruby_version = RUBY_VERSION
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def templates
|
|
83
|
+
Dir.glob('**/*.tt', base: TEMPLATE_ROOT).sort.map do |source|
|
|
84
|
+
[source, destination_for(source)]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def destination_for(source)
|
|
89
|
+
dir = File.dirname(source)
|
|
90
|
+
base = File.basename(source, '.tt')
|
|
91
|
+
base = DOTFILES.fetch(base, base)
|
|
92
|
+
|
|
93
|
+
dir == '.' ? base : File.join(dir, base)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render(source)
|
|
97
|
+
erb = ERB.new(File.read(File.join(TEMPLATE_ROOT, source)), trim_mode: '-')
|
|
98
|
+
erb.filename = source
|
|
99
|
+
erb.result(binding)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def write(destination, content)
|
|
103
|
+
path = File.join(@target, destination)
|
|
104
|
+
|
|
105
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
106
|
+
File.write(path, content)
|
|
107
|
+
|
|
108
|
+
@out.puts(" create #{File.join(@name, destination)}")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def validate_name!
|
|
112
|
+
return if @name.is_a?(String) && @name.match?(NAME_PATTERN)
|
|
113
|
+
|
|
114
|
+
raise Error, "#{@name.inspect} is not a valid project name — use letters, digits, " \
|
|
115
|
+
'underscores and dashes, starting with a letter'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def check_target!
|
|
119
|
+
return unless File.exist?(@target)
|
|
120
|
+
raise Error, "#{@name} exists and is not a directory" unless File.directory?(@target)
|
|
121
|
+
return if (Dir.children(@target) - ['.', '..']).empty?
|
|
122
|
+
|
|
123
|
+
raise Error, "#{@name} already exists and is not empty"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def report_next_steps
|
|
127
|
+
@out.puts(<<~NEXT)
|
|
128
|
+
|
|
129
|
+
Created #{@name}. Next:
|
|
130
|
+
|
|
131
|
+
cd #{@name}
|
|
132
|
+
bundle install
|
|
133
|
+
bundle exec rspec # the game logic, headless — no window needed
|
|
134
|
+
ruby main.rb # the game itself
|
|
135
|
+
NEXT
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
|
+
|
|
5
|
+
# The Ruby this project was generated with, kept in one place so bundler and
|
|
6
|
+
# your version manager cannot disagree about it. Bundler reads .ruby-version as
|
|
7
|
+
# an exact requirement and refuses to install on anything else; for a range,
|
|
8
|
+
# replace this line with something like `ruby '~> <%= ruby_version.split('.').first(2).join('.') %>'`.
|
|
9
|
+
ruby file: '.ruby-version'
|
|
10
|
+
|
|
11
|
+
# The engine. It has no runtime dependencies of its own — SDL2 and OpenGL are
|
|
12
|
+
# system libraries rather than gems — but installing it compiles two C
|
|
13
|
+
# extensions, so a C compiler, `make`, `pkg-config` and the SDL2/OpenGL
|
|
14
|
+
# development headers have to be present. See the rgame README.
|
|
15
|
+
gem 'rgame', '<%= rgame_requirement %>'
|
|
16
|
+
|
|
17
|
+
group :development, :test do
|
|
18
|
+
gem 'rake', require: false
|
|
19
|
+
gem 'rspec'
|
|
20
|
+
gem 'rubocop', require: false
|
|
21
|
+
gem 'rubocop-performance', require: false
|
|
22
|
+
gem 'rubocop-rspec', require: false
|
|
23
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# <%= caption %>
|
|
2
|
+
|
|
3
|
+
A game built with [rgame](https://github.com/psuessenb/rgame).
|
|
4
|
+
|
|
5
|
+
## Running it
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
bundle install
|
|
9
|
+
ruby main.rb
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`F1` toggles the debug overlay, `F2` closes the window.
|
|
13
|
+
|
|
14
|
+
This project was generated on Ruby <%= ruby_version %>, which is what
|
|
15
|
+
`.ruby-version` holds and what the `Gemfile` points at. Bundler treats that as
|
|
16
|
+
an exact requirement; see the note in the `Gemfile` to loosen it.
|
|
17
|
+
|
|
18
|
+
## Testing it
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
bundle exec rspec
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The suite runs **headless** — no window, no GPU, no clock. That is not an
|
|
25
|
+
accident of this project being small; it is the arrangement the layout below is
|
|
26
|
+
for, and it holds however large the game gets.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
bundle exec rubocop
|
|
30
|
+
bundle exec rake # both of the above
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Layout
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
main.rb boots the game and nothing else
|
|
37
|
+
game.rb the window, its size, its caption — and the ONLY file that
|
|
38
|
+
loads SDL and OpenGL
|
|
39
|
+
nodes/ the scene tree: game logic, graphics-free
|
|
40
|
+
root.rb the root node, drawn every frame
|
|
41
|
+
assets/ images, sounds, maps — RGame::Game's media_root
|
|
42
|
+
locales/ translation tables, one file per language
|
|
43
|
+
spec/ specs for nodes/, run with no display
|
|
44
|
+
locales_spec.rb every language has every key en.yml has
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Why `game.rb` and `nodes/` are separate
|
|
48
|
+
|
|
49
|
+
rgame comes in two halves, and they are worth keeping apart.
|
|
50
|
+
|
|
51
|
+
`RGame::Core` owns the window, the GPU textures and the sound device — real
|
|
52
|
+
operating-system handles. `RGame::Engine` is the scene graph: nodes, components,
|
|
53
|
+
signals, sprites, tile maps. It holds no handles at all, and it never names
|
|
54
|
+
`RGame::Core`. A node is *handed* a renderer at draw time and calls methods on
|
|
55
|
+
it by name.
|
|
56
|
+
|
|
57
|
+
So `require 'rgame'` loads the engine and the value types with no graphics
|
|
58
|
+
library in the process, while `require 'rgame/game'` loads everything. `game.rb`
|
|
59
|
+
does the latter, `nodes/*.rb` and `spec/*` do the former — and that one
|
|
60
|
+
difference is what lets the entire game logic be tested in milliseconds, with a
|
|
61
|
+
spy standing in for the renderer, on a machine with no display.
|
|
62
|
+
|
|
63
|
+
Keep new game logic under `nodes/`. When something there needs to draw, give it
|
|
64
|
+
an `on_draw(renderer, view)` and call the renderer by name.
|
|
65
|
+
|
|
66
|
+
## Text on screen
|
|
67
|
+
|
|
68
|
+
Text the game draws lives in `assets/locales/en.yml`, not in the code. A node
|
|
69
|
+
builds an `RGame::Engine::Text` from a key once and draws it every frame:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
@greeting = RGame::Engine::Text.new('root.greeting') # in initialize
|
|
73
|
+
renderer.text(@greeting, 20, 20) # in on_draw
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **To add text,** add a key to `en.yml`.
|
|
77
|
+
- **To add a language,** add a file beside it, such as `de.yml` starting with
|
|
78
|
+
`de:`, holding the same keys. The game loads every file in `assets/locales/`
|
|
79
|
+
and picks the player's language from their operating system.
|
|
80
|
+
- **`bundle exec rspec` fails** when a language lacks a key `en.yml` has, and
|
|
81
|
+
when a spec draws a key no table has. In the game, a missing key shows the
|
|
82
|
+
English text instead.
|
|
83
|
+
|
|
84
|
+
`spec/spec_helper.rb` loads the tables from `assets/locales/` itself, since specs
|
|
85
|
+
cannot load `game.rb`. Move that directory and update both.
|
|
86
|
+
|
|
87
|
+
The [localization guide](https://github.com/psuessenb/rgame/blob/main/docs/api/localization.md)
|
|
88
|
+
covers plurals, variables and the fallback between languages.
|
|
89
|
+
|
|
90
|
+
## Where to look next
|
|
91
|
+
|
|
92
|
+
The [rgame API guide](https://github.com/psuessenb/rgame/blob/main/docs/api/README.md)
|
|
93
|
+
covers the scene graph, components, input, drawing, audio and the asset layer.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# The English table: every piece of text the game draws, by key. Rails' format,
|
|
2
|
+
# so the top-level key is the locale and everything below it nests.
|
|
3
|
+
#
|
|
4
|
+
# To add a language, add a file beside this one (de.yml, starting `de:`) with
|
|
5
|
+
# the same keys. RGame::Game loads every .yml in this directory and picks the
|
|
6
|
+
# player's language from their operating system; spec/locales_spec.rb fails
|
|
7
|
+
# while a language lacks a key this file has.
|
|
8
|
+
en:
|
|
9
|
+
root:
|
|
10
|
+
greeting: "Hello from <%= app_name %>!"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame/game'
|
|
4
|
+
|
|
5
|
+
require_relative 'nodes/root'
|
|
6
|
+
|
|
7
|
+
class <%= game_class %> < RGame::Game
|
|
8
|
+
WIDTH = 640
|
|
9
|
+
HEIGHT = 480
|
|
10
|
+
|
|
11
|
+
# The bare `**` forwards every keyword straight through to RGame::Game, so
|
|
12
|
+
# anything it accepts still works here — `players: 2` for split-screen, or
|
|
13
|
+
# `input:` to hand the game a scripted input backend and drive it from a test
|
|
14
|
+
# harness with no hardware attached.
|
|
15
|
+
def initialize(**)
|
|
16
|
+
super(root: Root.new,
|
|
17
|
+
caption: '<%= caption %>',
|
|
18
|
+
width: WIDTH,
|
|
19
|
+
height: HEIGHT,
|
|
20
|
+
media_root: File.join(__dir__, 'assets'),
|
|
21
|
+
**)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Entry point: `ruby main.rb`.
|
|
4
|
+
#
|
|
5
|
+
# Deliberately nothing but a boot. Everything the game *is* lives in game.rb and
|
|
6
|
+
# nodes/, which is what lets the spec suite load the interesting half without
|
|
7
|
+
# also starting a window.
|
|
8
|
+
|
|
9
|
+
require_relative 'game'
|
|
10
|
+
|
|
11
|
+
<%= game_class %>.new.start
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rgame'
|
|
4
|
+
|
|
5
|
+
# The root of the scene tree. Everything the game draws hangs below it.
|
|
6
|
+
#
|
|
7
|
+
# The three hooks to override are `on_control(actions)`, `on_update(dt)` and
|
|
8
|
+
# `on_draw(renderer, view)`. Override those rather than `control`, `update` or
|
|
9
|
+
# `draw`: the engine does its own bookkeeping in the outer methods and calls
|
|
10
|
+
# these, so there is no `super` to forget.
|
|
11
|
+
#
|
|
12
|
+
# Text on screen is a key into assets/locales/, not a String in the code. A
|
|
13
|
+
# `Text` is built once and read every frame: it looks the key up again only when
|
|
14
|
+
# the language changes, so drawing it costs nothing.
|
|
15
|
+
class Root < RGame::Engine::Node2D
|
|
16
|
+
def initialize
|
|
17
|
+
super
|
|
18
|
+
@greeting = RGame::Engine::Text.new('root.greeting')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def on_draw(renderer, _view)
|
|
22
|
+
renderer.text(@greeting, 20, 20)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# rgame's own cops come with the gem. They catch what a game gets wrong without
|
|
2
|
+
# noticing: a String built every frame, a node drawn at its own position twice,
|
|
3
|
+
# text a translation cannot reach. Each explains itself in its message.
|
|
4
|
+
plugins:
|
|
5
|
+
- rubocop-performance
|
|
6
|
+
- rubocop-rspec
|
|
7
|
+
- rgame/rubocop:
|
|
8
|
+
plugin_class_name: RuboCop::Game::Plugin
|
|
9
|
+
|
|
10
|
+
AllCops:
|
|
11
|
+
NewCops: enable
|
|
12
|
+
TargetRubyVersion: 4.0
|
|
13
|
+
|
|
14
|
+
# This is a game, not a web app: update/draw methods naturally run long, and
|
|
15
|
+
# short coordinate and loop variable names (x, y, dt, id) are idiomatic in the
|
|
16
|
+
# maths they contain. Relax the size and naming cops accordingly.
|
|
17
|
+
Metrics/AbcSize:
|
|
18
|
+
Enabled: false
|
|
19
|
+
Metrics/BlockLength:
|
|
20
|
+
Enabled: false
|
|
21
|
+
Metrics/ClassLength:
|
|
22
|
+
Enabled: false
|
|
23
|
+
Metrics/CyclomaticComplexity:
|
|
24
|
+
Enabled: false
|
|
25
|
+
Metrics/MethodLength:
|
|
26
|
+
Enabled: false
|
|
27
|
+
Metrics/ModuleLength:
|
|
28
|
+
Enabled: false
|
|
29
|
+
Metrics/ParameterLists:
|
|
30
|
+
Enabled: false
|
|
31
|
+
Metrics/PerceivedComplexity:
|
|
32
|
+
Enabled: false
|
|
33
|
+
Naming/MethodParameterName:
|
|
34
|
+
Enabled: false
|
|
35
|
+
|
|
36
|
+
# Maths-heavy code reads fine on standard operator precedence
|
|
37
|
+
# (row * cols + col, x + w / 2); requiring parentheses everywhere is just noise.
|
|
38
|
+
Lint/AmbiguousOperatorPrecedence:
|
|
39
|
+
Enabled: false
|
|
40
|
+
|
|
41
|
+
# nodes/ and spec/ run with no window, and only game.rb loads one. This keeps it
|
|
42
|
+
# so: naming RGame::Core, or requiring rgame/core or rgame/game, fails the lint
|
|
43
|
+
# rather than the spec run on a machine without a display.
|
|
44
|
+
Game/NoCoreInEngineLayer:
|
|
45
|
+
Enabled: true
|
|
46
|
+
Include:
|
|
47
|
+
- 'nodes/**/*.rb'
|
|
48
|
+
- 'spec/**/*.rb'
|
|
49
|
+
|
|
50
|
+
Style/Documentation:
|
|
51
|
+
Enabled: false
|
|
52
|
+
|
|
53
|
+
# Integration-flavoured specs in a game naturally run longer than five lines and
|
|
54
|
+
# assert more than one thing about the frame they drove.
|
|
55
|
+
RSpec/ExampleLength:
|
|
56
|
+
Enabled: false
|
|
57
|
+
RSpec/MultipleExpectations:
|
|
58
|
+
Enabled: false
|
|
59
|
+
|
|
60
|
+
# A node is handed a renderer and calls it by name — it never learns what class
|
|
61
|
+
# answered, which is exactly what lets the spec suite run with no window.
|
|
62
|
+
# Verifying a double against RGame::Core::Renderer would mean loading SDL and
|
|
63
|
+
# OpenGL into the spec process, so these doubles are deliberately unverified.
|
|
64
|
+
RSpec/VerifiedDoubles:
|
|
65
|
+
Enabled: false
|
|
66
|
+
|
|
67
|
+
# Spec paths mirror the source tree, and a class with a trailing dimension maps
|
|
68
|
+
# to node2d_spec.rb rather than the cop's default node2_d_spec.rb.
|
|
69
|
+
RSpec/SpecFilePathFormat:
|
|
70
|
+
CustomTransform:
|
|
71
|
+
Node2D: node2d
|
|
72
|
+
# It checks the tables in assets/locales/, and describes I18n only because
|
|
73
|
+
# that is what it asks, so there is no source file for its path to mirror.
|
|
74
|
+
Exclude:
|
|
75
|
+
- spec/locales_spec.rb
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<%= ruby_version %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The translation tables in assets/locales/, checked against each other. A key
|
|
4
|
+
# added to en.yml and forgotten in another language fails here, rather than
|
|
5
|
+
# showing a player English text in the middle of their own language.
|
|
6
|
+
RSpec.describe RGame::Engine::I18n do
|
|
7
|
+
# Without this, deleting en.yml would leave nothing to compare against, and
|
|
8
|
+
# the example below would pass for every language.
|
|
9
|
+
it 'has a table for the default locale' do
|
|
10
|
+
expect(described_class.available).to include(described_class.default)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'gives every locale every key the default locale has' do
|
|
14
|
+
missing = described_class.available.to_h { |locale| [locale, described_class.missing_keys(locale)] }
|
|
15
|
+
|
|
16
|
+
expect(missing.reject { |_locale, keys| keys.empty? }).to be_empty
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# What a node spec looks like: build the node, drive one of its hooks, assert.
|
|
4
|
+
# No window is opened, and none is needed — see spec/spec_helper.rb for why.
|
|
5
|
+
RSpec.describe Root do
|
|
6
|
+
describe '#on_draw' do
|
|
7
|
+
it 'draws its greeting from the English table' do
|
|
8
|
+
# A node calls a renderer by name and never asks what class it is, so a
|
|
9
|
+
# plain spy stands in for one. The real RGame::Core::Renderer lives on the
|
|
10
|
+
# other side of a line this suite deliberately does not cross.
|
|
11
|
+
renderer = spy('renderer')
|
|
12
|
+
|
|
13
|
+
described_class.new.on_draw(renderer, nil)
|
|
14
|
+
|
|
15
|
+
expect(renderer).to have_received(:text).with('Hello from <%= app_name %>!', 20, 20)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# `rgame` and never `rgame/game`. It loads RGame::Util and RGame::Engine —
|
|
4
|
+
# everything that runs without a window — so no SDL and no OpenGL enter this
|
|
5
|
+
# process, and the whole suite runs with no display: in CI, over SSH, anywhere.
|
|
6
|
+
#
|
|
7
|
+
# It also means `RGame::Core` is an undefined constant here. A spec that reached
|
|
8
|
+
# for it would fail loudly rather than quietly opening a window, which is the
|
|
9
|
+
# point: game logic that can only be tested with a GPU attached is game logic
|
|
10
|
+
# that stops being tested.
|
|
11
|
+
require 'rgame'
|
|
12
|
+
|
|
13
|
+
# The project root, so `require 'nodes/root'` works from anywhere in the suite.
|
|
14
|
+
$LOAD_PATH.unshift File.expand_path('..', __dir__)
|
|
15
|
+
|
|
16
|
+
Dir[File.expand_path('../nodes/**/*.rb', __dir__)].each { |file| require file }
|
|
17
|
+
|
|
18
|
+
# The translation tables, read from disk once. This is the directory RGame::Game
|
|
19
|
+
# loads in the game: `locales` under game.rb's `media_root`. Move one and move
|
|
20
|
+
# the other, or every key a spec draws raises.
|
|
21
|
+
LOCALES = Dir[File.expand_path('../assets/locales/**/*.yml', __dir__)]
|
|
22
|
+
.to_h { |path| [path, File.read(path)] }.freeze
|
|
23
|
+
|
|
24
|
+
RSpec.configure do |config|
|
|
25
|
+
config.expect_with(:rspec) { |c| c.syntax = :expect }
|
|
26
|
+
config.disable_monkey_patching!
|
|
27
|
+
|
|
28
|
+
# RGame::Engine::I18n is global, so every example starts from the game's own
|
|
29
|
+
# tables in the default language, whatever the example before it loaded or
|
|
30
|
+
# switched to. A key no table has raises instead of drawing itself: found by
|
|
31
|
+
# `rake` rather than by a player.
|
|
32
|
+
config.before do
|
|
33
|
+
RGame::Engine::I18n.reset
|
|
34
|
+
LOCALES.each { |path, yaml| RGame::Engine::I18n.load(yaml, source: path) }
|
|
35
|
+
RGame::Engine::I18n.missing = :raise
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/rgame/cli.rb
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'cli/new_project'
|
|
5
|
+
|
|
6
|
+
module RGame
|
|
7
|
+
# Argument parsing and dispatch. The work is in RGame::CLI::NewProject.
|
|
8
|
+
module CLI
|
|
9
|
+
USAGE = <<~USAGE
|
|
10
|
+
Usage: rgame COMMAND [ARGS]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
new NAME Create a new game project in the directory NAME
|
|
14
|
+
version Print the rgame version
|
|
15
|
+
help Print this message
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
rgame new tictactoe
|
|
19
|
+
USAGE
|
|
20
|
+
|
|
21
|
+
# Runs one command and returns the process exit status, rather than calling
|
|
22
|
+
# `exit` itself — which is what lets a spec drive it in-process and assert on
|
|
23
|
+
# what it wrote and what it printed.
|
|
24
|
+
#
|
|
25
|
+
# `out` and `err` are injectable for the same reason.
|
|
26
|
+
def self.run(argv, out: $stdout, err: $stderr)
|
|
27
|
+
command, *rest = argv
|
|
28
|
+
|
|
29
|
+
case command
|
|
30
|
+
when 'new' then new_project(rest, out: out, err: err)
|
|
31
|
+
when 'version', '--version', '-v' then version(out: out)
|
|
32
|
+
when 'help', '--help', '-h', nil then help(out: out)
|
|
33
|
+
else
|
|
34
|
+
err.puts("rgame: unknown command #{command.inspect}", '', USAGE)
|
|
35
|
+
1
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.help(out:)
|
|
40
|
+
out.puts(USAGE)
|
|
41
|
+
0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.version(out:)
|
|
45
|
+
out.puts("rgame #{RGame::VERSION}")
|
|
46
|
+
0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.new_project(args, out:, err:)
|
|
50
|
+
name = args.first
|
|
51
|
+
|
|
52
|
+
if name.nil? || name.start_with?('-')
|
|
53
|
+
err.puts('rgame new: expected a project name', '', USAGE)
|
|
54
|
+
return 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
NewProject.new(name, out: out).generate
|
|
58
|
+
0
|
|
59
|
+
rescue NewProject::Error => e
|
|
60
|
+
err.puts("rgame new: #{e.message}")
|
|
61
|
+
1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private_class_method :help, :version, :new_project
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/rgame/core/app.rb
CHANGED
|
@@ -1,48 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# RGame::Core::App is implemented in C — see ext/rgame_core/. It wraps the
|
|
4
|
-
# engine's public API (ext/rgame_core/include/rgame/core.h): an SDL window plus
|
|
5
|
-
# OpenGL context, and a fixed-timestep main loop that drives callbacks.
|
|
6
|
-
#
|
|
7
|
-
# App is meant to be subclassed. The engine calls back into methods on the
|
|
8
|
-
# object itself, so a game overrides the hooks it needs and inherits no-ops for
|
|
9
|
-
# the rest:
|
|
10
|
-
#
|
|
11
|
-
# class MyGame < RGame::Core::App
|
|
12
|
-
# def initialize = super(width: 800, height: 600, caption: 'my game')
|
|
13
|
-
#
|
|
14
|
-
# def frame_begin; end # once per frame, before that frame's ticks
|
|
15
|
-
# def update(dt); end # one fixed simulation tick; dt is always the
|
|
16
|
-
# # fixed step, never wall-clock frame time
|
|
17
|
-
# def needs_redraw?; end # false skips the draw (simulation still runs)
|
|
18
|
-
# def draw; end # render one frame
|
|
19
|
-
# def frame_end; end # after draw is submitted, before the swap —
|
|
20
|
-
# # rarely needed outside a test that reads pixels
|
|
21
|
-
# def button_down(id); end # discrete key press (no repeats)
|
|
22
|
-
# def button_up(id); end
|
|
23
|
-
# def resize(w, h); end
|
|
24
|
-
# end
|
|
25
|
-
#
|
|
26
|
-
# MyGame.new.run
|
|
27
|
-
#
|
|
28
|
-
# Also inherited: #close (stops the loop; safe from inside a callback), #width,
|
|
29
|
-
# #height, #caption, #caption=, #ticks_ms (monotonic ms since startup) and #fps.
|
|
30
|
-
#
|
|
31
|
-
# The loop owns the fixed-timestep accumulator, so #update is called once per
|
|
32
|
-
# whole tick and may run zero or several times per rendered frame. #frame_begin
|
|
33
|
-
# is the place to sample input once and reuse it across every tick of that
|
|
34
|
-
# frame.
|
|
35
|
-
#
|
|
36
|
-
# If a callback raises, the exception is carried out of #run with its original
|
|
37
|
-
# class, message and backtrace, and the loop shuts down cleanly first. A
|
|
38
|
-
# non-local exit (throw/break/return) out of a callback cannot be carried
|
|
39
|
-
# across the C loop and is reported as a RuntimeError instead — use #close to
|
|
40
|
-
# stop the loop.
|
|
41
|
-
#
|
|
42
|
-
# Loading this .so pulls SDL2 and OpenGL into the process — see
|
|
43
|
-
# lib/rgame/core.rb for why that's kept off the default `require "rgame"`.
|
|
44
|
-
# It loads from lib/rgame/core_ext.so, which the build (`make ext-core`)
|
|
45
|
-
# copies out of ext/rgame_core/.
|
|
46
3
|
require 'rgame/core_ext'
|
|
47
4
|
|
|
48
5
|
module RGame
|
|
@@ -73,7 +30,12 @@ module RGame
|
|
|
73
30
|
# This app's sound device. Not tied to the window in any way — audio has
|
|
74
31
|
# no GL context and survives one being recreated — it lives here because
|
|
75
32
|
# a game wants exactly one, the same way it wants one asset manager.
|
|
76
|
-
|
|
33
|
+
# It is handed this app's asset manager, so `audio.play_sound('hurt.ogg')`
|
|
34
|
+
# resolves a path the same way `renderer.sprite('hero.json', …)` does.
|
|
35
|
+
# Building the manager does not touch audio — its loaders are lambdas,
|
|
36
|
+
# called at load time — so there is no cycle here despite the manager
|
|
37
|
+
# reaching back through `app.audio` to make a sound.
|
|
38
|
+
def audio = @audio ||= Audio.new(assets: assets)
|
|
77
39
|
|
|
78
40
|
# Where #assets resolves relative paths from. Set once, as a keyword to
|
|
79
41
|
# the constructor; there is deliberately no writer, because changing it
|