rgame 0.2.0 → 0.3.1
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 +144 -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 +3 -5
- 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 +167 -12
- 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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"image": "icons.png",
|
|
3
|
+
"images": {
|
|
4
|
+
"home": { "x": 0, "y": 0, "w": 50, "h": 50 },
|
|
5
|
+
"gear": { "x": 50, "y": 0, "w": 50, "h": 50 },
|
|
6
|
+
"save": { "x": 100, "y": 0, "w": 50, "h": 50 },
|
|
7
|
+
"star": { "x": 150, "y": 0, "w": 50, "h": 50 },
|
|
8
|
+
"trophy": { "x": 200, "y": 0, "w": 50, "h": 50 },
|
|
9
|
+
"audio_on": { "x": 250, "y": 0, "w": 50, "h": 50 },
|
|
10
|
+
"music_on": { "x": 300, "y": 0, "w": 50, "h": 50 },
|
|
11
|
+
"locked": { "x": 350, "y": 0, "w": 50, "h": 50 }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"image": "skills.png",
|
|
3
|
+
"images": {
|
|
4
|
+
"wand": { "x": 0, "y": 0, "w": 64, "h": 64 },
|
|
5
|
+
"wrench": { "x": 64, "y": 0, "w": 64, "h": 64 },
|
|
6
|
+
"torch": { "x": 128, "y": 0, "w": 64, "h": 64 },
|
|
7
|
+
"hammer": { "x": 192, "y": 0, "w": 64, "h": 64 },
|
|
8
|
+
"watering_can": { "x": 256, "y": 0, "w": 64, "h": 64 }
|
|
9
|
+
}
|
|
10
|
+
}
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!-- Kenney "Tiny Town" (CC0) sliced as a Tiled tileset. See README.md.
|
|
3
|
+
|
|
4
|
+
A tile is solid to RGame::Engine::Tileset because it carries a collision
|
|
5
|
+
shape here — an <objectgroup> with at least one object — not because of
|
|
6
|
+
anything in code. The trees and fences below are the obstacles the examples
|
|
7
|
+
use; every other tile is walkable. -->
|
|
8
|
+
<tileset version="1.10" tiledversion="1.10.2" name="tiny_town" tilewidth="16" tileheight="16" tilecount="132" columns="12">
|
|
9
|
+
<image source="tileset.png" width="192" height="176"/>
|
|
10
|
+
<tile id="3">
|
|
11
|
+
<objectgroup draworder="index">
|
|
12
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
13
|
+
</objectgroup>
|
|
14
|
+
</tile>
|
|
15
|
+
<tile id="4">
|
|
16
|
+
<objectgroup draworder="index">
|
|
17
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
18
|
+
</objectgroup>
|
|
19
|
+
</tile>
|
|
20
|
+
<tile id="5">
|
|
21
|
+
<objectgroup draworder="index">
|
|
22
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
23
|
+
</objectgroup>
|
|
24
|
+
</tile>
|
|
25
|
+
<tile id="15">
|
|
26
|
+
<objectgroup draworder="index">
|
|
27
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
28
|
+
</objectgroup>
|
|
29
|
+
</tile>
|
|
30
|
+
<tile id="16">
|
|
31
|
+
<objectgroup draworder="index">
|
|
32
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
33
|
+
</objectgroup>
|
|
34
|
+
</tile>
|
|
35
|
+
<tile id="27">
|
|
36
|
+
<objectgroup draworder="index">
|
|
37
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
38
|
+
</objectgroup>
|
|
39
|
+
</tile>
|
|
40
|
+
<tile id="28">
|
|
41
|
+
<objectgroup draworder="index">
|
|
42
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
43
|
+
</objectgroup>
|
|
44
|
+
</tile>
|
|
45
|
+
<tile id="44">
|
|
46
|
+
<objectgroup draworder="index">
|
|
47
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
48
|
+
</objectgroup>
|
|
49
|
+
</tile>
|
|
50
|
+
<tile id="45">
|
|
51
|
+
<objectgroup draworder="index">
|
|
52
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
53
|
+
</objectgroup>
|
|
54
|
+
</tile>
|
|
55
|
+
<tile id="46">
|
|
56
|
+
<objectgroup draworder="index">
|
|
57
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
58
|
+
</objectgroup>
|
|
59
|
+
</tile>
|
|
60
|
+
<tile id="47">
|
|
61
|
+
<objectgroup draworder="index">
|
|
62
|
+
<object id="1" x="0" y="0" width="16" height="16"/>
|
|
63
|
+
</objectgroup>
|
|
64
|
+
</tile>
|
|
65
|
+
</tileset>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!-- Authored for the examples; edit it in Tiled. See README.md.
|
|
3
|
+
|
|
4
|
+
60x40 tiles at 16px = 960x640 pixels, larger than the 640x480 window on
|
|
5
|
+
both axes so the camera has somewhere to scroll. Two layers: the ground is
|
|
6
|
+
entirely walkable, and everything solid lives in "obstacles" - a tree
|
|
7
|
+
border, a fence right across the middle, and scattered trees.
|
|
8
|
+
|
|
9
|
+
The fence has exactly one gap, at x=12..14, and it is deliberately far
|
|
10
|
+
west of both clearings. A gap sitting between them would be no obstacle at
|
|
11
|
+
all: the shortest route through it would cost exactly the straight-line
|
|
12
|
+
distance, and a pathfinder would have nothing to show. Placed out here, a
|
|
13
|
+
walker starting in the north clearing has to head away from the south one
|
|
14
|
+
to get through - a route you can watch being taken. The fence runs the full
|
|
15
|
+
interior width for the same reason: stopping it a tile short of the border
|
|
16
|
+
leaves a second gap nobody planned, and the route quietly uses that
|
|
17
|
+
instead. -->
|
|
18
|
+
<map version="1.10" tiledversion="1.10.2" orientation="orthogonal" renderorder="right-down" width="60" height="40" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
|
|
19
|
+
<tileset firstgid="1" source="tileset.tsx"/>
|
|
20
|
+
<layer id="1" name="ground" width="60" height="40">
|
|
21
|
+
<data encoding="base64" compression="zlib">eJztWU1vwzAIzexzt3O7czdNyv//fU0PlqwM8/EAJ5t6eFIbJ5gHGGNclmV5Y1CIZ6vwjQaU3Jokl3qmBaXTyoxpwdmwjXn0fgGLg9E7lRkrDtmF+Z6bcx8viN57GVabcfF/2fAO4EOpr1aPWbhuuAH4TNInK283W0fzPSLXVcP8mf7N4u6R6+UbEWvZHGf5F9VTY8MCzpG1fnsdpLys3TettqTGI/hWwid18Dubj4T7hi8A34QsjrMXlhyM2jRrL7TKzOKXFXdRep/hHOOpz/v/Px3655wPPHaOsB13Nmjjo3dGfDm9VuV7lDxtLGfFVM+XyyGe+bm1O/ssM/LvX4WU91G+M/apjDkovmfIx1mIimdN7WqV9/xGqhGs4/9t/SL+1dZynE/X5XevLGIv0vbfrPpK83LjSJ1K5VXN2VPq82flVUkHT3/Sq+/IJl5bUH5ttZ3n/JtVk2TJ9fJFfWj1XWS9P6Nfx9XAkXdsfX4ZyT0inr35Sbpj4XxwxvU7sgvFw9pviV6/1rpK+h8dG0feH6F38Uj90ezo6U/u90qvniiQHqLmm6N7ICg3DaJykaYGReKaGkd7PxFcuT0jIs9KXK2cZ8VDlD57e0XJ1fJ6AGJxLzQ=</data>
|
|
22
|
+
</layer>
|
|
23
|
+
<layer id="2" name="obstacles" width="60" height="40">
|
|
24
|
+
<data encoding="base64" compression="zlib">eJztmU0OAiEMhdnofg7gEdT7385xQVIJP4X2wZvgl5DZOGD7WiidI4RwO8ex0XMXos278Ah1fe/KOXrfXxVXUt+abTWbWjDFT0lfja5XhDF/kb5u5a830pbV+YuanwmEvmw2SqS+KJ1lDFv2eQ9SfVt7xehe0vLlDF9/8crflm6rdY2g92e2s857f/bUERF3JX2tazHomrMr1ddiu0f9jfC9XL9WP/fGpsUmy3w9v7fmr1wjnccyb+2MtNTXs+vn1UR9n+d4KYdE+w7DeAc/fa9yX2a8/0ZG/pemztsxf5lA6RqfnvquugflyPlAo++M+ysqp7T1c0rJJ4h+FPLuyJi/SKS+I98JUnK+Y9r/U31z8R7xjAPp45m1isf3oxFW5dCs/hULyPqK0QdafUd9wta3RPWvWPmfv796e/ZoJJ49mh521Xen5wfjJRxI</data>
|
|
25
|
+
</layer>
|
|
26
|
+
</map>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"image": "ui.png",
|
|
3
|
+
"scale": 1,
|
|
4
|
+
"nine_slices": {
|
|
5
|
+
"panel": { "x": 0, "y": 0, "w": 32, "h": 32, "border": 8 },
|
|
6
|
+
"button_idle": { "x": 32, "y": 0, "w": 32, "h": 32, "border": 8 },
|
|
7
|
+
"button_focus": { "x": 64, "y": 0, "w": 32, "h": 32, "border": 8 },
|
|
8
|
+
"button_pressed": { "x": 96, "y": 0, "w": 32, "h": 32, "border": 8 },
|
|
9
|
+
"button_disabled": { "x": 128, "y": 0, "w": 32, "h": 32, "border": 8 }
|
|
10
|
+
}
|
|
11
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
walk: Arrows / WASD walk the pale circle — the other two need nobody
|
|
5
|
+
crate: "Enter a crate: it blinks once and counts you, you stay lit inside"
|
|
6
|
+
circles: Circle into circle does nothing — same layer, one line ignoring it
|
|
7
|
+
hud:
|
|
8
|
+
visits: "visits: %{count}"
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Collision — two shapes touching.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/collision/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys or WASD walk the pale circle. The other circles need nobody. Drive
|
|
10
|
+
# into a crate and both of you light up; drive into another circle and neither
|
|
11
|
+
# of you does. It exercises:
|
|
12
|
+
# - Components::CollisionWorld — the scene-scoped system that pairs shapes up
|
|
13
|
+
# each step and tells them when they start and stop overlapping;
|
|
14
|
+
# - Components::CircleCollider — a round shape on a node;
|
|
15
|
+
# - Components::BoxCollider — the rectangular one, and the two mix freely;
|
|
16
|
+
# - Components::Velocity — from `examples/velocity`, so that things move into
|
|
17
|
+
# each other without anybody pressing a key;
|
|
18
|
+
# - Engine::Text — a crate's counter, a key with a variable, rendered again
|
|
19
|
+
# only on a contact.
|
|
20
|
+
#
|
|
21
|
+
# The pale circle stays lit for as long as it is inside a crate, and the crate
|
|
22
|
+
# blinks once and adds one to its counter. Two readings of the same pair of
|
|
23
|
+
# signals: one brackets a contact, the other marks the moment it began.
|
|
24
|
+
#
|
|
25
|
+
# ## The system is on the scene, the shapes are on the nodes
|
|
26
|
+
#
|
|
27
|
+
# `CollisionWorld` is a component mounted on the scene node, so it is born with
|
|
28
|
+
# the scene and torn down with it, and it rides the ordinary update traversal
|
|
29
|
+
# rather than needing a step of its own. The shapes are components on the things
|
|
30
|
+
# that have shapes.
|
|
31
|
+
#
|
|
32
|
+
# Nothing connects the two by hand. A collider registers with
|
|
33
|
+
# `node.system(CollisionWorld)` when its node enters the tree and unregisters
|
|
34
|
+
# when it leaves, and the engine fires both hooks — so a spawned or despawned
|
|
35
|
+
# entity cannot leak a registration, and no game code is asked to remember.
|
|
36
|
+
#
|
|
37
|
+
# That is also why every component here is added in `initialize` rather than in
|
|
38
|
+
# `on_add`: a node assembled outside the tree collects all of its components
|
|
39
|
+
# before any of them attaches, so the order they go on in never matters.
|
|
40
|
+
#
|
|
41
|
+
# ## A contact is reported to both sides, and neither is asked what it means
|
|
42
|
+
#
|
|
43
|
+
# Each step, the system buckets every collider, finds the overlapping pairs, and
|
|
44
|
+
# tells both colliders of each pair about the other. It stops there. It does not
|
|
45
|
+
# know what a crate is, and it never looks at `layer`.
|
|
46
|
+
#
|
|
47
|
+
# `layer` is an opaque tag, and the whole of the rule in this example is one
|
|
48
|
+
# line in `Mover`:
|
|
49
|
+
#
|
|
50
|
+
# collider.on_hit { |other| @touching += 1 unless other.layer == :mover }
|
|
51
|
+
#
|
|
52
|
+
# The two circles that drift through each other *are* reported, and that line is
|
|
53
|
+
# what makes it look like nothing happened. Pushing the test up into the system
|
|
54
|
+
# would mean the system deciding that same-layer things never interact — which is
|
|
55
|
+
# wrong the moment one kind of thing has to hurt another of its own kind.
|
|
56
|
+
#
|
|
57
|
+
# ## A contact is two edges, not a state
|
|
58
|
+
#
|
|
59
|
+
# `on_hit` fires on the step a pair starts overlapping. `on_separated` fires on
|
|
60
|
+
# the step it stops. Nothing fires on the steps in between, however long the two
|
|
61
|
+
# sit inside each other, and a pair is reported once however many broadphase
|
|
62
|
+
# cells it happens to span.
|
|
63
|
+
#
|
|
64
|
+
# That is what lets a handler do something that must happen exactly once —
|
|
65
|
+
# `@score += 100`, play a sound, spend a life — with nothing to remember and
|
|
66
|
+
# nothing to guard. `Crate` below counts arrivals in one line because of it.
|
|
67
|
+
#
|
|
68
|
+
# The two signals also come in balanced pairs, which is the other thing to see on
|
|
69
|
+
# screen. `Mover` keeps a count: up on `on_hit`, down on `on_separated`, lit
|
|
70
|
+
# while it is above zero. That count is right no matter how many crates the
|
|
71
|
+
# circle is inside at once, because the edges are **per pair** — a second crate
|
|
72
|
+
# is a second `on_hit`, not a louder version of the first.
|
|
73
|
+
#
|
|
74
|
+
# A contact also ends when the other side is destroyed or leaves the tree, and
|
|
75
|
+
# `on_separated` fires then too. Nothing here dies, but a shooter whose bullets
|
|
76
|
+
# vanish on impact depends on it: without it a ship could stay "in contact" with
|
|
77
|
+
# a rock that no longer exists.
|
|
78
|
+
#
|
|
79
|
+
# ## A box and a circle collide with each other
|
|
80
|
+
#
|
|
81
|
+
# `CircleCollider` and `BoxCollider` are not two separate worlds. Both answer the
|
|
82
|
+
# same broadphase question (the bounding box to bucket me in) and the same
|
|
83
|
+
# narrowphase one (`overlap?`), and the pair settles the actual test between
|
|
84
|
+
# themselves by handing each other their numbers. So one world holds both, and
|
|
85
|
+
# every circle here collides with every crate.
|
|
86
|
+
#
|
|
87
|
+
# ## An AABB does not turn with its node
|
|
88
|
+
#
|
|
89
|
+
# The drifting circles spin — the spoke is drawn so you can see it — and their
|
|
90
|
+
# collision shape is unaffected, because a circle is the same circle at every
|
|
91
|
+
# angle. The crates carry an axis-aligned box and do not spin, and that is a
|
|
92
|
+
# choice rather than a preference: a box that turned with its node would have to
|
|
93
|
+
# be recomputed into a wider box every frame, and the wider box is wrong in a
|
|
94
|
+
# different way. A thing that spins wants a circle.
|
|
95
|
+
#
|
|
96
|
+
# ## `cell_size` is the one number this system asks a game to pick
|
|
97
|
+
#
|
|
98
|
+
# The broadphase is a spatial hash: a lattice of square cells, each collider
|
|
99
|
+
# dropped into the cells its bounding box covers, and only colliders sharing a
|
|
100
|
+
# cell ever tested against each other. The faint grid on the backdrop is drawn at
|
|
101
|
+
# that size, so the number is visible rather than a constant at the top of a
|
|
102
|
+
# file.
|
|
103
|
+
#
|
|
104
|
+
# Pick it at about the size of the things being bucketed. Much smaller and one
|
|
105
|
+
# collider spans a block of cells and is tested from every one of them; much
|
|
106
|
+
# larger and everything lands in the same cell and the hash has quietly become
|
|
107
|
+
# the loop over all pairs it exists to avoid.
|
|
108
|
+
#
|
|
109
|
+
# ## What it does not solve
|
|
110
|
+
#
|
|
111
|
+
# Response. Nothing here is pushed out of anything: a contact is a notification,
|
|
112
|
+
# and these shapes overlap and carry on. Stopping a body at a wall is a different
|
|
113
|
+
# problem with different machinery, and `examples/collision_tiles` is where it
|
|
114
|
+
# lives.
|
|
115
|
+
|
|
116
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
117
|
+
require 'rgame/game'
|
|
118
|
+
|
|
119
|
+
WIDTH = 640
|
|
120
|
+
HEIGHT = 480
|
|
121
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
122
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
123
|
+
|
|
124
|
+
# The broadphase cell, and the grid drawn on the backdrop. Everything bucketed
|
|
125
|
+
# here is 32 to 96 pixels across, so the cell is one of those.
|
|
126
|
+
CELL_SIZE = 64
|
|
127
|
+
|
|
128
|
+
WALK_SPEED = 150.0
|
|
129
|
+
|
|
130
|
+
# How long a crate stays lit after a circle arrives. Long enough to see at a
|
|
131
|
+
# glance, short enough that two arrivals read as two.
|
|
132
|
+
FLASH_TIME = 0.3
|
|
133
|
+
|
|
134
|
+
# A round thing that lights up while it is inside a crate. Its two subclasses
|
|
135
|
+
# differ only in what moves them — the shape, the layer and the rule are the same
|
|
136
|
+
# for both.
|
|
137
|
+
#
|
|
138
|
+
# `@touching` is the balanced pair of edges, kept as a count rather than a flag
|
|
139
|
+
# because a circle can be inside two crates at once: the two in the middle of the
|
|
140
|
+
# field overlap, and driving into the pair is two `on_hit` calls before either
|
|
141
|
+
# `on_separated` arrives. A flag would go dark on leaving the first of them.
|
|
142
|
+
class Mover < RGame::Engine::Node2D
|
|
143
|
+
RADIUS = 16
|
|
144
|
+
BODY = RGame::Util::Color.new(236, 233, 220)
|
|
145
|
+
HIT = RGame::Util::Color.new(255, 214, 92)
|
|
146
|
+
SPOKE = RGame::Util::Color.new(60, 66, 82)
|
|
147
|
+
|
|
148
|
+
def initialize(**)
|
|
149
|
+
super(width: RADIUS * 2, height: RADIUS * 2, **)
|
|
150
|
+
@touching = 0
|
|
151
|
+
collider = add_component(RGame::Engine::Components::CircleCollider.new(radius: RADIUS, layer: :mover))
|
|
152
|
+
# Both edges are reported to both colliders, each handed the other, so this is
|
|
153
|
+
# written entirely from this node's side. The guard is the example's subject:
|
|
154
|
+
# same-layer pairs are reported and these lines drop them. It has to be the
|
|
155
|
+
# same guard on both, or the count would not come back to zero.
|
|
156
|
+
collider.on_hit { |other| @touching += 1 unless other.layer == :mover }
|
|
157
|
+
collider.on_separated { |other| @touching -= 1 unless other.layer == :mover }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# The centre is where the traversal has already put the renderer, and the
|
|
161
|
+
# circle's collision centre is the node's origin — the same point.
|
|
162
|
+
def on_draw(renderer, _view)
|
|
163
|
+
renderer.circle(0, 0, RADIUS, color: @touching.positive? ? HIT : BODY)
|
|
164
|
+
# Drawn at the node's own angle, which is what makes the spin visible at all:
|
|
165
|
+
# a spinning circle looks like a still one without a mark on it.
|
|
166
|
+
renderer.line(0, 0, RADIUS, 0, thickness: 3, color: SPOKE)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Moves because it was given a velocity, and for no other reason.
|
|
171
|
+
class Drifter < Mover
|
|
172
|
+
def initialize(vx:, vy:, spin: 0.0, **)
|
|
173
|
+
super(**)
|
|
174
|
+
add_component(RGame::Engine::Components::Velocity.new(vx: vx, vy: vy, spin: spin))
|
|
175
|
+
add_component(RGame::Engine::Components::ScreenWrap.new(margin: RADIUS))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The one with a hand on it. Same shape, same layer, same rule.
|
|
180
|
+
class Walker < Mover
|
|
181
|
+
def initialize(**)
|
|
182
|
+
super
|
|
183
|
+
add_component(RGame::Engine::Components::CharacterBody.new(speed: WALK_SPEED))
|
|
184
|
+
add_component(RGame::Engine::Components::PlayerController.new)
|
|
185
|
+
add_component(RGame::Engine::Components::ScreenWrap.new(margin: RADIUS))
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# A rectangle that never moves and counts the circles that have arrived.
|
|
190
|
+
#
|
|
191
|
+
# Two of them overlap on purpose, in the middle of the field. That pair is in
|
|
192
|
+
# contact from the first step to the last, the system reports it exactly once,
|
|
193
|
+
# and neither counter moves — which is the same one-line rule `Mover` uses, seen
|
|
194
|
+
# from the other side.
|
|
195
|
+
#
|
|
196
|
+
# A visit is one circle arriving, and it is counted by adding one in the handler,
|
|
197
|
+
# because `on_hit` is the arrival rather than the overlap. Two circles standing
|
|
198
|
+
# in this crate at once is two visits: the edges are per pair, so each of them
|
|
199
|
+
# announced itself.
|
|
200
|
+
class Crate < RGame::Engine::Node2D
|
|
201
|
+
BODY = RGame::Util::Color.new(88, 104, 136)
|
|
202
|
+
HIT = RGame::Util::Color.new(150, 196, 255)
|
|
203
|
+
INK = RGame::Util::Color.new(180, 190, 210)
|
|
204
|
+
|
|
205
|
+
def initialize(width:, height:, **)
|
|
206
|
+
super
|
|
207
|
+
@flash = 0.0
|
|
208
|
+
@touches = 0
|
|
209
|
+
@label = RGame::Engine::Text.new('hud.visits', :count)
|
|
210
|
+
# The box is offset to sit around the node's origin, which is where a circle's
|
|
211
|
+
# centre already is — so both shapes here are drawn and collide about the
|
|
212
|
+
# same point. A sprite that wants a small box at its feet moves the offset
|
|
213
|
+
# instead; nothing says the box has to be centred.
|
|
214
|
+
collider = add_component(RGame::Engine::Components::BoxCollider.new(
|
|
215
|
+
width: width, height: height,
|
|
216
|
+
offset_x: -width / 2, offset_y: -height / 2, layer: :wall
|
|
217
|
+
))
|
|
218
|
+
collider.on_hit do |other|
|
|
219
|
+
next if other.layer == :wall
|
|
220
|
+
|
|
221
|
+
@touches += 1
|
|
222
|
+
@flash = FLASH_TIME
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Only the flash, which is a cosmetic timer and the one thing here that has to
|
|
227
|
+
# be advanced rather than announced. The counter is not touched: it moves on the
|
|
228
|
+
# edge, in the handler above.
|
|
229
|
+
def on_update(dt) = @flash -= dt
|
|
230
|
+
|
|
231
|
+
def on_draw(renderer, _view)
|
|
232
|
+
renderer.rect(-width / 2, -height / 2, width, height, color: @flash.positive? ? HIT : BODY)
|
|
233
|
+
renderer.text(@label.with(count: @touches), -width / 2, (-height / 2) - 22, color: INK)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class Scene < RGame::Engine::Node2D
|
|
238
|
+
BACKDROP = RGame::Util::Color.new(26, 30, 38)
|
|
239
|
+
GRID = RGame::Util::Color.new(38, 44, 56)
|
|
240
|
+
|
|
241
|
+
# Centre x, centre y, width, height. The first two overlap.
|
|
242
|
+
CRATES = [
|
|
243
|
+
[300, 200, 96, 40],
|
|
244
|
+
[336, 244, 40, 96],
|
|
245
|
+
[520, 140, 72, 56],
|
|
246
|
+
[150, 340, 64, 64]
|
|
247
|
+
].freeze
|
|
248
|
+
|
|
249
|
+
# Centre x, centre y, vx, vy, spin in degrees per second. Fixed rather than
|
|
250
|
+
# random, so two runs of this example can be compared without a seed.
|
|
251
|
+
DRIFTERS = [
|
|
252
|
+
[40, 200, 120.0, 0.0, 90.0],
|
|
253
|
+
[600, 140, -95.0, 0.0, -140.0],
|
|
254
|
+
[150, 104, 0.0, 105.0, 0.0]
|
|
255
|
+
].freeze
|
|
256
|
+
|
|
257
|
+
# Both systems are mounted here rather than in `on_add`, so they exist before
|
|
258
|
+
# any child's collider attaches and goes looking for them.
|
|
259
|
+
def initialize
|
|
260
|
+
super
|
|
261
|
+
@help_walk = RGame::Engine::Text.new('help.walk')
|
|
262
|
+
@help_crate = RGame::Engine::Text.new('help.crate')
|
|
263
|
+
@help_circles = RGame::Engine::Text.new('help.circles')
|
|
264
|
+
add_component(RGame::Engine::Components::World.new(width: WIDTH, height: HEIGHT))
|
|
265
|
+
add_component(RGame::Engine::Components::CollisionWorld.new(cell_size: CELL_SIZE))
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def on_add
|
|
269
|
+
CRATES.each { |x, y, w, h| add_node(Crate.new(x: x, y: y, width: w, height: h)) }
|
|
270
|
+
DRIFTERS.each do |x, y, vx, vy, spin|
|
|
271
|
+
add_node(Drifter.new(x: x, y: y, vx: vx, vy: vy, spin: spin))
|
|
272
|
+
end
|
|
273
|
+
add_node(Walker.new(x: 80, y: 430))
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def on_draw(renderer, view)
|
|
277
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
278
|
+
draw_cells(renderer, view)
|
|
279
|
+
|
|
280
|
+
renderer.text(@help_walk, 12, 12)
|
|
281
|
+
renderer.text(@help_crate, 12, 34)
|
|
282
|
+
renderer.text(@help_circles, 12, 56)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# The broadphase's own lattice, so `cell_size` is something you can look at.
|
|
288
|
+
# A while loop rather than a Range: this runs every frame, and a fresh Range
|
|
289
|
+
# every frame is the allocation the hot-path cops exist to refuse.
|
|
290
|
+
#
|
|
291
|
+
# hot-path
|
|
292
|
+
def draw_cells(renderer, view)
|
|
293
|
+
x = CELL_SIZE
|
|
294
|
+
while x < view.width
|
|
295
|
+
renderer.rect(x, 0, 1, view.height, color: GRID)
|
|
296
|
+
x += CELL_SIZE
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
y = CELL_SIZE
|
|
300
|
+
while y < view.height
|
|
301
|
+
renderer.rect(0, y, view.width, 1, color: GRID)
|
|
302
|
+
y += CELL_SIZE
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
game = RGame::Game.new(
|
|
308
|
+
root: Scene.new,
|
|
309
|
+
caption: 'Collision',
|
|
310
|
+
width: WIDTH,
|
|
311
|
+
height: HEIGHT,
|
|
312
|
+
media_root: ASSETS,
|
|
313
|
+
locales: LOCALES
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
game.start
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
walk: Arrows / WASD / gamepad to walk — trees and the fence are solid
|
|
5
|
+
slide: "Hold down and left against the fence: you slide to its one gap"
|
|
6
|
+
box: The red box is what collides. The rest of the sprite is a picture
|
|
7
|
+
ball: "Walk east into the spiky ball: it stops you, and it costs a life"
|
|
8
|
+
hud:
|
|
9
|
+
lives: "Lives: %{lives}"
|