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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Timer — things that happen on a clock, with nobody pressing anything.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/timer/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Watch the race; **Space** sets off the one-shot. It exercises:
|
|
10
|
+
# - Components::Timer — a repeating interval that rides the node's tick;
|
|
11
|
+
# - `repeating: false` — the same component as a one-shot that arms itself;
|
|
12
|
+
# - `as:` — two cadences on one node, because a node holds one per slot;
|
|
13
|
+
# - Engine::Timer — the pure accumulator underneath all of that.
|
|
14
|
+
#
|
|
15
|
+
# ## A spawner is not waiting for a key
|
|
16
|
+
#
|
|
17
|
+
# Every example before this one moved when something was pressed. A wave clock, a
|
|
18
|
+
# turret's rate of fire and an enemy every few seconds are none of them input, and
|
|
19
|
+
# they are what this is for.
|
|
20
|
+
#
|
|
21
|
+
# ## The race is the point, and the loser is the obvious implementation
|
|
22
|
+
#
|
|
23
|
+
# Both bars count beats at the same interval. The top one is a
|
|
24
|
+
# `Components::Timer`; the bottom one is four lines anybody would write:
|
|
25
|
+
#
|
|
26
|
+
# @elapsed += dt
|
|
27
|
+
# if @elapsed >= INTERVAL
|
|
28
|
+
# @elapsed = 0.0 # <- this
|
|
29
|
+
# beat
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# The top bar wins every lap, and that line is why. A frame almost never lands
|
|
33
|
+
# exactly on an interval boundary, so `@elapsed` is a little *past* it when the
|
|
34
|
+
# beat fires — and zeroing throws that overshoot away. Every beat starts late by
|
|
35
|
+
# whatever was discarded, and the error adds up for as long as the program runs.
|
|
36
|
+
#
|
|
37
|
+
# `Engine::Timer#consume` subtracts the interval instead of zeroing, so the
|
|
38
|
+
# remainder carries into the next beat and the cadence stays true to the clock
|
|
39
|
+
# rather than to the frame it happened to be noticed on:
|
|
40
|
+
#
|
|
41
|
+
# def consume
|
|
42
|
+
# @elapsed -= @interval
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# One character of difference, and the gap it opens is on screen. The interval
|
|
46
|
+
# here is deliberately not a whole number of frames — at a fixed sixtieth of a
|
|
47
|
+
# second and an interval of 0.07, a beat is 4.2 frames, and rounding that up to 5
|
|
48
|
+
# costs the naive bar about one lap in six.
|
|
49
|
+
#
|
|
50
|
+
# ## The catch-up half of the same idea
|
|
51
|
+
#
|
|
52
|
+
# `Components::Timer#update` fires `on_timeout` *once per whole interval* that
|
|
53
|
+
# elapsed, in a loop, rather than once per call. A step long enough to cover
|
|
54
|
+
# three intervals emits three times. Nothing in this example produces one — the
|
|
55
|
+
# loop is fixed-timestep — but a game that ever sees a long step wants the
|
|
56
|
+
# spawner to owe it three enemies rather than one.
|
|
57
|
+
#
|
|
58
|
+
# ## One component per slot, which is what `as:` is for
|
|
59
|
+
#
|
|
60
|
+
# A node holds one component per slot, and a slot defaults to the component's
|
|
61
|
+
# class — so a second `Components::Timer` added plainly would be refused. The top
|
|
62
|
+
# runner carries two: a fast `:beat` and a slow `:chime` that flips the marker
|
|
63
|
+
# beside it. Naming them is the whole of making that legal, and it is also how
|
|
64
|
+
# either is found again.
|
|
65
|
+
#
|
|
66
|
+
# ## The one-shot arms itself, which matters more than it sounds
|
|
67
|
+
#
|
|
68
|
+
# `repeating: false` fires once and goes inert, and `reset` re-arms it. The
|
|
69
|
+
# component calls `reset` from `on_attach`, so a node that leaves the tree and
|
|
70
|
+
# comes back gets a fresh countdown rather than inheriting a spent one. That line
|
|
71
|
+
# is what makes a pooled projectile with a lifetime work — see `examples/pooling`,
|
|
72
|
+
# where the same component retires what the pool hands out.
|
|
73
|
+
#
|
|
74
|
+
# ## What it does not solve
|
|
75
|
+
#
|
|
76
|
+
# Pausing a timer on its own. A `Components::Timer` stops when its node stops,
|
|
77
|
+
# because `paused` halts the node's whole update — which is usually what is
|
|
78
|
+
# wanted, and is not the same as a timer with its own switch.
|
|
79
|
+
|
|
80
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
81
|
+
require 'rgame/game'
|
|
82
|
+
|
|
83
|
+
WIDTH = 640
|
|
84
|
+
HEIGHT = 480
|
|
85
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
86
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
87
|
+
|
|
88
|
+
# 4.2 frames at a fixed sixtieth of a second, chosen so that rounding it up to a
|
|
89
|
+
# whole frame is a mistake with a visible size.
|
|
90
|
+
BEAT = 0.07
|
|
91
|
+
CHIME = 1.5
|
|
92
|
+
FUSE = 1.2 # how long the one-shot banner stays
|
|
93
|
+
|
|
94
|
+
TRACK_X = 60
|
|
95
|
+
TRACK_W = 480
|
|
96
|
+
LAP = 70 # beats to the end of the track
|
|
97
|
+
|
|
98
|
+
# The two runners share this, so the only difference between them is how each
|
|
99
|
+
# decides a beat has happened.
|
|
100
|
+
module Runner
|
|
101
|
+
BAR_H = 26
|
|
102
|
+
TRACK = RGame::Util::Color.new(44, 50, 64)
|
|
103
|
+
|
|
104
|
+
def beats = @beats ||= 0
|
|
105
|
+
|
|
106
|
+
def draw_track(renderer, color)
|
|
107
|
+
renderer.rect(0, 0, TRACK_W, BAR_H, color: TRACK)
|
|
108
|
+
renderer.rect(0, 0, TRACK_W * (beats.to_f / LAP), BAR_H, color: color)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Beats on a Components::Timer, which carries the remainder forward.
|
|
113
|
+
class TrueRunner < RGame::Engine::Node2D
|
|
114
|
+
include Runner
|
|
115
|
+
|
|
116
|
+
BAR = RGame::Util::Color.new(120, 210, 150)
|
|
117
|
+
MARK_ON = RGame::Util::Color.new(255, 226, 130)
|
|
118
|
+
MARK_OFF = RGame::Util::Color.new(70, 78, 96)
|
|
119
|
+
MARK = 18
|
|
120
|
+
|
|
121
|
+
def initialize(**)
|
|
122
|
+
super
|
|
123
|
+
@beats = 0
|
|
124
|
+
@chimed = false
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Two timers, two slots. Without the names the second would be refused, and
|
|
128
|
+
# `get_component(Components::Timer)` would have no single answer to give.
|
|
129
|
+
def on_add
|
|
130
|
+
add_component(RGame::Engine::Components::Timer.new(BEAT), as: :beat)
|
|
131
|
+
.on_timeout { @beats += 1 }
|
|
132
|
+
add_component(RGame::Engine::Components::Timer.new(CHIME), as: :chime)
|
|
133
|
+
.on_timeout { @chimed = !@chimed }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Resetting the lap is this node's business rather than the timer's: the timer
|
|
137
|
+
# counts time and the owner decides what a count means.
|
|
138
|
+
def lap? = @beats >= LAP
|
|
139
|
+
|
|
140
|
+
def restart = @beats = 0
|
|
141
|
+
|
|
142
|
+
def on_draw(renderer, _view)
|
|
143
|
+
draw_track(renderer, BAR)
|
|
144
|
+
renderer.rect(TRACK_W + 14, 4, MARK, MARK, color: @chimed ? MARK_ON : MARK_OFF)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# The same cadence, written the way it comes out first. It is slower, and the
|
|
149
|
+
# only reason is the line that throws the overshoot away.
|
|
150
|
+
class NaiveRunner < RGame::Engine::Node2D
|
|
151
|
+
include Runner
|
|
152
|
+
|
|
153
|
+
BAR = RGame::Util::Color.new(235, 120, 100)
|
|
154
|
+
MARK = RGame::Util::Color.new(255, 255, 255)
|
|
155
|
+
MARK_W = 3
|
|
156
|
+
|
|
157
|
+
def initialize(**)
|
|
158
|
+
super
|
|
159
|
+
@beats = 0
|
|
160
|
+
@elapsed = 0.0
|
|
161
|
+
@shortfall = 0
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def on_update(dt)
|
|
165
|
+
@elapsed += dt
|
|
166
|
+
return if @elapsed < BEAT
|
|
167
|
+
|
|
168
|
+
# Discarding the overshoot is the bug. `@elapsed -= BEAT` here and the two
|
|
169
|
+
# bars finish level.
|
|
170
|
+
@elapsed = 0.0
|
|
171
|
+
@beats += 1
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Remembered before it is cleared, because how far this bar had got when the
|
|
175
|
+
# other one finished *is* the size of the error. The marker lands in the same
|
|
176
|
+
# place every lap, which is the other half of the point: the gap is a steady
|
|
177
|
+
# rate, not a one-off stumble.
|
|
178
|
+
def restart
|
|
179
|
+
@shortfall = @beats
|
|
180
|
+
@beats = 0
|
|
181
|
+
@elapsed = 0.0
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def on_draw(renderer, _view)
|
|
185
|
+
draw_track(renderer, BAR)
|
|
186
|
+
return if @shortfall.zero?
|
|
187
|
+
|
|
188
|
+
renderer.rect(TRACK_W * (@shortfall.to_f / LAP), 0, MARK_W, BAR_H, color: MARK)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# A banner that takes itself away. Nothing removes it from outside.
|
|
193
|
+
class Fuse < RGame::Engine::Node2D
|
|
194
|
+
BANNER = RGame::Util::Color.new(120, 200, 255)
|
|
195
|
+
INK = RGame::Util::Color.new(20, 26, 34)
|
|
196
|
+
BANNER_W = 260
|
|
197
|
+
BANNER_H = 30
|
|
198
|
+
|
|
199
|
+
def initialize(**)
|
|
200
|
+
super
|
|
201
|
+
@label = RGame::Engine::Text.new('fuse.banner')
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def on_add
|
|
205
|
+
add_component(RGame::Engine::Components::Timer.new(FUSE, repeating: false))
|
|
206
|
+
.on_timeout { queue_free }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def on_draw(renderer, _view)
|
|
210
|
+
renderer.rect(0, 0, BANNER_W, BANNER_H, color: BANNER)
|
|
211
|
+
renderer.text(@label, 10, 8, z: 1, color: INK)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
class Scene < RGame::Engine::Node2D
|
|
216
|
+
BACKDROP = RGame::Util::Color.new(28, 32, 42)
|
|
217
|
+
FUSE_X = 60
|
|
218
|
+
FUSE_Y = 370
|
|
219
|
+
|
|
220
|
+
def initialize
|
|
221
|
+
super
|
|
222
|
+
@help = RGame::Engine::Text.new('help.beat')
|
|
223
|
+
@true_caption = RGame::Engine::Text.new('captions.timer')
|
|
224
|
+
@naive_caption = RGame::Engine::Text.new('captions.naive')
|
|
225
|
+
@mark_caption = RGame::Engine::Text.new('captions.mark')
|
|
226
|
+
@blink_caption = RGame::Engine::Text.new('captions.blink')
|
|
227
|
+
@fuse_caption = RGame::Engine::Text.new('captions.fuse')
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def on_add
|
|
231
|
+
@truth = add_node(TrueRunner.new(x: TRACK_X, y: 150))
|
|
232
|
+
@naive = add_node(NaiveRunner.new(x: TRACK_X, y: 210))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The lap is settled here, where both runners are visible, so that neither of
|
|
236
|
+
# them needs to know the other exists.
|
|
237
|
+
def on_update(_dt)
|
|
238
|
+
return unless @truth.lap?
|
|
239
|
+
|
|
240
|
+
@truth.restart
|
|
241
|
+
@naive.restart
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def on_control(actions)
|
|
245
|
+
return unless actions.pressed?(:fire)
|
|
246
|
+
# One at a time: a second banner would sit exactly on top of the first.
|
|
247
|
+
return if @fuse&.in_tree?
|
|
248
|
+
|
|
249
|
+
@fuse = add_node(Fuse.new(x: FUSE_X, y: FUSE_Y))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def on_draw(renderer, view)
|
|
253
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
254
|
+
|
|
255
|
+
renderer.text(@help, 12, 12)
|
|
256
|
+
renderer.text(@true_caption, TRACK_X, 126)
|
|
257
|
+
renderer.text(@naive_caption, TRACK_X, 244)
|
|
258
|
+
renderer.text(@mark_caption, TRACK_X, 264)
|
|
259
|
+
renderer.text(@blink_caption, TRACK_X, 296)
|
|
260
|
+
renderer.text(@fuse_caption, TRACK_X, 336)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
game = RGame::Game.new(
|
|
265
|
+
root: Scene.new,
|
|
266
|
+
caption: 'Timer',
|
|
267
|
+
width: WIDTH,
|
|
268
|
+
height: HEIGHT,
|
|
269
|
+
media_root: ASSETS,
|
|
270
|
+
locales: LOCALES
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
game.start
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# The English table: every string this example draws.
|
|
2
|
+
en:
|
|
3
|
+
help:
|
|
4
|
+
walk: Arrows / WASD walk the pale circle — the rectangles need nobody
|
|
5
|
+
wrap: They wrap at the edge of the world, which stops above this text
|
|
6
|
+
spin: "Two of them spin: one component adding to angle, no shape the wiser"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Velocity — movement with nobody driving.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/velocity/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys, WASD or a stick walk the pale circle. The rectangles are not
|
|
10
|
+
# waiting for you. It exercises:
|
|
11
|
+
# - Components::Velocity — a velocity integrated into the node each step;
|
|
12
|
+
# - Components::ScreenWrap — leaving one edge and arriving at the opposite one;
|
|
13
|
+
# - Components::World — how big the world is, as a service the scene mounts;
|
|
14
|
+
# - Components::WorldBounds — the contract that lets one wrap fit two worlds.
|
|
15
|
+
#
|
|
16
|
+
# ## Two ways to move, and the difference is who decides
|
|
17
|
+
#
|
|
18
|
+
# Every example before this one moved things with `CharacterBody`, which takes an
|
|
19
|
+
# *intent* — each axis in -1..1 — and turns it into a step at a fixed speed. Let
|
|
20
|
+
# go of the key and the intent is zero and the node stops. That is what a walking
|
|
21
|
+
# character wants, and it is why the circle here behaves the way you expect.
|
|
22
|
+
#
|
|
23
|
+
# A rock has no intent. It has a velocity, and something integrates it:
|
|
24
|
+
#
|
|
25
|
+
# node.x += vx * dt
|
|
26
|
+
# node.y += vy * dt
|
|
27
|
+
# node.angle += spin * dt
|
|
28
|
+
#
|
|
29
|
+
# That is the whole of Components::Velocity. Nothing writes to it after the
|
|
30
|
+
# rectangles are built, nothing reads input, and they drift for as long as the
|
|
31
|
+
# program runs. The two components are siblings rather than rivals —
|
|
32
|
+
# `ThrustController` is the third of the family, and it *accelerates* a velocity
|
|
33
|
+
# rather than setting one, which is how a ship differs from both.
|
|
34
|
+
#
|
|
35
|
+
# ## `spin` is an angle, and the shape does not know
|
|
36
|
+
#
|
|
37
|
+
# Two of the rectangles turn as they drift, and the drawing code is the same
|
|
38
|
+
# single `renderer.rect` the still ones use, at the same coordinates. `Node2D#draw`
|
|
39
|
+
# has already pushed the node's rotation, so a component that adds to `angle` is
|
|
40
|
+
# enough to make a shape spin — the same reason the sprite in `examples/sprite`
|
|
41
|
+
# turns without Components::Sprite knowing that angles exist.
|
|
42
|
+
#
|
|
43
|
+
# ## The world is not the window
|
|
44
|
+
#
|
|
45
|
+
# The world stops short of the bottom of the window, where the text is, and the
|
|
46
|
+
# rectangles wrap at its edge rather than the screen's. Ask the `view` a draw is
|
|
47
|
+
# handed how big the **window** is, and `node.system(WorldBounds)` how big the
|
|
48
|
+
# **world** is. `examples/scroll_map` is where the two differ in earnest.
|
|
49
|
+
#
|
|
50
|
+
# ## Why the wrap asks for a contract rather than a class
|
|
51
|
+
#
|
|
52
|
+
# `ScreenWrap` never mentions `World`. It calls `WorldBounds.resolve`, and what
|
|
53
|
+
# answers is whichever system the scene mounted — the plain rectangle here, or a
|
|
54
|
+
# `TileWorld` that works its bounds out from a parsed map. `Node2D#get_component`
|
|
55
|
+
# matches with `is_a?`, which matches an included module as readily as a class,
|
|
56
|
+
# and that is the whole mechanism.
|
|
57
|
+
#
|
|
58
|
+
# It resolves in `on_attach` rather than `initialize`, because a node has no
|
|
59
|
+
# scene to ask until it is in the tree — and it resolves again on every entry, so
|
|
60
|
+
# an entity pooled out of one scene and into a smaller one wraps against the
|
|
61
|
+
# smaller one.
|
|
62
|
+
#
|
|
63
|
+
# ## What it does not solve
|
|
64
|
+
#
|
|
65
|
+
# Collision. These pass straight through each other, because a velocity is only
|
|
66
|
+
# a position changing over time and nothing here is watching for overlaps.
|
|
67
|
+
# `examples/collision` is where that is the subject.
|
|
68
|
+
|
|
69
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
70
|
+
require 'rgame/game'
|
|
71
|
+
|
|
72
|
+
WIDTH = 640
|
|
73
|
+
HEIGHT = 480
|
|
74
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
75
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
76
|
+
|
|
77
|
+
# The world is the window minus a band along the bottom for the text. It is
|
|
78
|
+
# shorter by more than the wrap margin, so a shape leaving through the bottom edge
|
|
79
|
+
# is put back before it reaches the text.
|
|
80
|
+
WORLD_W = WIDTH
|
|
81
|
+
WORLD_H = 360
|
|
82
|
+
TEXT_Y = 410
|
|
83
|
+
|
|
84
|
+
# How far past the edge a shape travels before it is put back on the other side.
|
|
85
|
+
# Without it a rectangle jumps while half of it is still showing.
|
|
86
|
+
WRAP_MARGIN = 40.0
|
|
87
|
+
|
|
88
|
+
WALK_SPEED = 140.0
|
|
89
|
+
|
|
90
|
+
# A rectangle that moves because it was given a velocity, and for no other
|
|
91
|
+
# reason. Everything that makes it move is a component; this class is a shape.
|
|
92
|
+
class Drifter < RGame::Engine::Node2D
|
|
93
|
+
BOX_W = 38
|
|
94
|
+
BOX_H = 20
|
|
95
|
+
|
|
96
|
+
def initialize(color:, vx:, vy:, spin: 0.0, **)
|
|
97
|
+
super(width: BOX_W, height: BOX_H, **)
|
|
98
|
+
@color = color
|
|
99
|
+
@vx = vx
|
|
100
|
+
@vy = vy
|
|
101
|
+
@spin = spin
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def on_add
|
|
105
|
+
add_component(RGame::Engine::Components::Velocity.new(vx: @vx, vy: @vy, spin: @spin))
|
|
106
|
+
add_component(RGame::Engine::Components::ScreenWrap.new(margin: WRAP_MARGIN))
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Centred on the node's own origin, which is where the traversal has already
|
|
110
|
+
# put the renderer — so a spinning node turns this rectangle about its middle
|
|
111
|
+
# and this method never learns of it.
|
|
112
|
+
def on_draw(renderer, _view)
|
|
113
|
+
renderer.rect(-BOX_W / 2, -BOX_H / 2, BOX_W, BOX_H, color: @color)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# The one with an intent. Same wrap, same world, different reason to move.
|
|
118
|
+
class Walker < RGame::Engine::Node2D
|
|
119
|
+
BODY = RGame::Util::Color.new(236, 233, 220)
|
|
120
|
+
RADIUS = 13
|
|
121
|
+
|
|
122
|
+
def initialize(**)
|
|
123
|
+
super(width: RADIUS * 2, height: RADIUS * 2, **)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# CharacterBody first: PlayerController drives a sibling body and asks for it
|
|
127
|
+
# by class when it attaches, and a component added from `on_add` can only see
|
|
128
|
+
# the ones already there.
|
|
129
|
+
def on_add
|
|
130
|
+
add_component(RGame::Engine::Components::CharacterBody.new(speed: WALK_SPEED))
|
|
131
|
+
add_component(RGame::Engine::Components::PlayerController.new)
|
|
132
|
+
add_component(RGame::Engine::Components::ScreenWrap.new(margin: RADIUS))
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def on_draw(renderer, _view) = renderer.circle(0, 0, RADIUS, color: BODY)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class Scene < RGame::Engine::Node2D
|
|
139
|
+
BACKDROP = RGame::Util::Color.new(28, 32, 42)
|
|
140
|
+
FLOOR = RGame::Util::Color.new(44, 50, 64)
|
|
141
|
+
EDGE = RGame::Util::Color.new(120, 200, 255)
|
|
142
|
+
THICK = 2
|
|
143
|
+
|
|
144
|
+
# vx, vy, spin (degrees per second), colour. Fixed rather than random: two runs
|
|
145
|
+
# of this example should be able to be compared without a seed.
|
|
146
|
+
DRIFTERS = [
|
|
147
|
+
[90.0, 34.0, 0.0, [235, 145, 90]],
|
|
148
|
+
[-70.0, 58.0, 110.0, [130, 210, 150]],
|
|
149
|
+
[48.0, -76.0, 0.0, [225, 205, 110]],
|
|
150
|
+
[-104.0, -26.0, -150.0, [190, 150, 235]],
|
|
151
|
+
[62.0, 90.0, 0.0, [120, 200, 255]]
|
|
152
|
+
].freeze
|
|
153
|
+
|
|
154
|
+
# Mounted here rather than in `on_add` for a reason worth knowing: a node
|
|
155
|
+
# assembled outside the tree collects every component before any of them
|
|
156
|
+
# attaches, so nothing depends on the order. Added from `on_add` the node is
|
|
157
|
+
# already live, each component attaches as it arrives, and the wraps below
|
|
158
|
+
# would resolve their bounds before this existed.
|
|
159
|
+
def initialize
|
|
160
|
+
super
|
|
161
|
+
@help_walk = RGame::Engine::Text.new('help.walk')
|
|
162
|
+
@help_wrap = RGame::Engine::Text.new('help.wrap')
|
|
163
|
+
@help_spin = RGame::Engine::Text.new('help.spin')
|
|
164
|
+
add_component(RGame::Engine::Components::World.new(width: WORLD_W, height: WORLD_H))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def on_add
|
|
168
|
+
DRIFTERS.each_with_index do |(vx, vy, spin, rgb), index|
|
|
169
|
+
add_node(Drifter.new(color: RGame::Util::Color.new(*rgb), vx: vx, vy: vy, spin: spin,
|
|
170
|
+
x: 60 + (index * 120), y: 40 + (index * 60)))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
add_node(Walker.new(x: WORLD_W / 2, y: WORLD_H / 2))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def on_draw(renderer, view)
|
|
177
|
+
renderer.rect(0, 0, view.width, view.height, color: BACKDROP)
|
|
178
|
+
renderer.rect(0, 0, WORLD_W, WORLD_H, color: FLOOR)
|
|
179
|
+
renderer.rect(0, WORLD_H - THICK, WORLD_W, THICK, color: EDGE)
|
|
180
|
+
|
|
181
|
+
renderer.text(@help_walk, 12, TEXT_Y)
|
|
182
|
+
renderer.text(@help_wrap, 12, TEXT_Y + 22)
|
|
183
|
+
renderer.text(@help_spin, 12, TEXT_Y + 44)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
game = RGame::Game.new(
|
|
188
|
+
root: Scene.new,
|
|
189
|
+
caption: 'Velocity',
|
|
190
|
+
width: WIDTH,
|
|
191
|
+
height: HEIGHT,
|
|
192
|
+
media_root: ASSETS,
|
|
193
|
+
locales: LOCALES
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
game.start
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Walk — a player-controlled sprite, and the smallest complete game there is.
|
|
4
|
+
#
|
|
5
|
+
# Run it:
|
|
6
|
+
#
|
|
7
|
+
# ruby examples/walk/main.rb
|
|
8
|
+
#
|
|
9
|
+
# Arrow keys, WASD, a d-pad or a left stick; they all work and none of them are
|
|
10
|
+
# mentioned below. It exercises:
|
|
11
|
+
# - Node2D — the scene tree, and `on_update`/`on_draw` as blank hooks;
|
|
12
|
+
# - Components::AnimatedSprite — directional animation off a sprite sheet;
|
|
13
|
+
# - Components::CharacterBody — an intent in -1..1 becomes a step at a speed;
|
|
14
|
+
# - Components::PlayerController — input axes become that intent;
|
|
15
|
+
# - InputMap.default — physical keys become the :move_x / :move_y actions.
|
|
16
|
+
#
|
|
17
|
+
# ## The shape to take away
|
|
18
|
+
#
|
|
19
|
+
# The hero is a plain Node2D with three components on it. Nothing subclasses a
|
|
20
|
+
# "player" class, and the three components do not know about each other: the
|
|
21
|
+
# controller writes a movement intent, the body turns intent into a step, and
|
|
22
|
+
# the sprite reads the same intent back to pick which way to face. Swapping the
|
|
23
|
+
# controller for Components::WanderController is the whole of turning this into
|
|
24
|
+
# an NPC.
|
|
25
|
+
#
|
|
26
|
+
# The one subclass here is Hero, and it exists only to keep the walker on
|
|
27
|
+
# screen — see the comment on `on_update`.
|
|
28
|
+
|
|
29
|
+
# lib/ on the load path, so `require 'rgame/game'` resolves the same way it
|
|
30
|
+
# would from an installed gem. examples/ and lib/ are siblings in both, so this
|
|
31
|
+
# line is correct either way.
|
|
32
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __dir__)
|
|
33
|
+
require 'rgame/game'
|
|
34
|
+
|
|
35
|
+
WIDTH = 640
|
|
36
|
+
HEIGHT = 480
|
|
37
|
+
ASSETS = File.expand_path('../assets', __dir__)
|
|
38
|
+
LOCALES = File.expand_path('locales', __dir__) # the text on screen: locales/en.yml
|
|
39
|
+
|
|
40
|
+
SPEED = 90.0 # pixels per second
|
|
41
|
+
|
|
42
|
+
# A Node2D that stays inside the window.
|
|
43
|
+
#
|
|
44
|
+
# Clamping is not something the engine does for you here: a plain CharacterBody
|
|
45
|
+
# moves the node wherever the intent points, because it is meant for an actor in
|
|
46
|
+
# a world with nothing to bump into. Giving it edges is one `on_update` — and in
|
|
47
|
+
# a real game it is usually Components::TileWorld doing it instead, which is
|
|
48
|
+
# what `examples/scroll_map` shows.
|
|
49
|
+
class Hero < RGame::Engine::Node2D
|
|
50
|
+
# `on_update` is the blank hook: Node2D#update does the bookkeeping and calls
|
|
51
|
+
# this, so there is no `super` to forget.
|
|
52
|
+
def on_update(_dt)
|
|
53
|
+
self.x = x.clamp(0, WIDTH - width)
|
|
54
|
+
self.y = y.clamp(0, HEIGHT - height)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# The root. It builds the hero in `initialize`, before anything is in the tree.
|
|
59
|
+
#
|
|
60
|
+
# That matters and is easy to get wrong the other way round: `add_component`
|
|
61
|
+
# attaches immediately once its node is live, and AnimatedSprite's attach looks
|
|
62
|
+
# for a CharacterBody sibling. Build the node whole, then add it — then the
|
|
63
|
+
# order components go on in cannot matter.
|
|
64
|
+
class Root < RGame::Engine::Node2D
|
|
65
|
+
def initialize
|
|
66
|
+
super
|
|
67
|
+
@help = RGame::Engine::Text.new('help.walk')
|
|
68
|
+
add_node(build_hero)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def on_draw(renderer, _view)
|
|
72
|
+
renderer.text(@help, 12, 12)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def build_hero
|
|
78
|
+
hero = Hero.new(x: (WIDTH - 16) / 2, y: (HEIGHT - 22) / 2)
|
|
79
|
+
# The sheet is a path relative to the asset manager's root, resolved on
|
|
80
|
+
# attach — nothing is loaded or registered by hand. hero.json names its own
|
|
81
|
+
# image and its animations; AnimatedSprite picks between them by reading the
|
|
82
|
+
# body's intent, so :walk_left and friends are looked up by name.
|
|
83
|
+
hero.add_component(RGame::Engine::Components::AnimatedSprite.new(sheet: 'hero.json'))
|
|
84
|
+
hero.add_component(RGame::Engine::Components::CharacterBody.new(speed: SPEED))
|
|
85
|
+
hero.add_component(RGame::Engine::Components::PlayerController.new)
|
|
86
|
+
hero
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
game = RGame::Game.new(
|
|
91
|
+
root: Root.new,
|
|
92
|
+
caption: 'Walk',
|
|
93
|
+
width: WIDTH,
|
|
94
|
+
height: HEIGHT,
|
|
95
|
+
media_root: ASSETS,
|
|
96
|
+
locales: LOCALES
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
game.start
|
data/exe/rgame
ADDED
data/ext/rgame_core/app/app.c
CHANGED
|
@@ -95,7 +95,11 @@ struct rgame_app {
|
|
|
95
95
|
int drawing;
|
|
96
96
|
};
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
/* Desktop fullscreen, never a mode change — see the header for why. Named once
|
|
99
|
+
* so the creation flags and the runtime toggle cannot disagree. */
|
|
100
|
+
#define RGAME_FULLSCREEN_FLAG SDL_WINDOW_FULLSCREEN_DESKTOP
|
|
101
|
+
|
|
102
|
+
rgame_app *rgame_app_create(int width, int height, const char *title, int fullscreen) {
|
|
99
103
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
|
|
100
104
|
SDL_Log("SDL_Init failed: %s", SDL_GetError());
|
|
101
105
|
return NULL;
|
|
@@ -114,7 +118,8 @@ rgame_app *rgame_app_create(int width, int height, const char *title) {
|
|
|
114
118
|
title,
|
|
115
119
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
116
120
|
width, height,
|
|
117
|
-
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN
|
|
121
|
+
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN |
|
|
122
|
+
(fullscreen ? RGAME_FULLSCREEN_FLAG : 0u));
|
|
118
123
|
if (!app->window) {
|
|
119
124
|
SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
|
|
120
125
|
free(app);
|
|
@@ -467,7 +472,13 @@ static void rgame_app_poll_events(rgame_app *app, const rgame_app_callbacks *cb)
|
|
|
467
472
|
break;
|
|
468
473
|
}
|
|
469
474
|
case SDL_WINDOWEVENT:
|
|
470
|
-
|
|
475
|
+
/* SIZE_CHANGED rather than RESIZED, and the difference matters:
|
|
476
|
+
* SDL sends RESIZED only when something *outside* the program
|
|
477
|
+
* resized the window, and SIZE_CHANGED for that case as well as for
|
|
478
|
+
* a size this program asked for. Toggling fullscreen is the second
|
|
479
|
+
* kind, so a listener on RESIZED alone never hears about it and the
|
|
480
|
+
* game carries on laying out for the old size. */
|
|
481
|
+
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
|
471
482
|
/* The viewport is set from the current size every frame, in the
|
|
472
483
|
* backend's begin_frame, so there is nothing to update here. */
|
|
473
484
|
if (cb->resize) {
|
|
@@ -865,6 +876,25 @@ void rgame_app_set_title(rgame_app *app, const char *title) {
|
|
|
865
876
|
SDL_SetWindowTitle(app->window, title);
|
|
866
877
|
}
|
|
867
878
|
|
|
879
|
+
void rgame_app_set_fullscreen(rgame_app *app, int fullscreen) {
|
|
880
|
+
if (!app) {
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
SDL_SetWindowFullscreen(app->window, fullscreen ? RGAME_FULLSCREEN_FLAG : 0u);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
int rgame_app_fullscreen(const rgame_app *app) {
|
|
887
|
+
if (!app) {
|
|
888
|
+
return 0;
|
|
889
|
+
}
|
|
890
|
+
/* Tested against the whole mask rather than for any bit in it:
|
|
891
|
+
* SDL_WINDOW_FULLSCREEN_DESKTOP is SDL_WINDOW_FULLSCREEN plus one more,
|
|
892
|
+
* so `flags & FLAG` is also true of an exclusive-fullscreen window — which
|
|
893
|
+
* this engine never asks for, but a caller could have set behind our back. */
|
|
894
|
+
Uint32 flags = SDL_GetWindowFlags(app->window);
|
|
895
|
+
return (flags & RGAME_FULLSCREEN_FLAG) == RGAME_FULLSCREEN_FLAG;
|
|
896
|
+
}
|
|
897
|
+
|
|
868
898
|
int rgame_app_input_down(const rgame_app *app, int device, int button_id) {
|
|
869
899
|
return rgame_input_state_down(&app->input, device, button_id);
|
|
870
900
|
}
|