rgame 0.1.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 +7 -0
- data/LICENSE +26 -0
- data/README.md +406 -0
- data/docs/api/README.md +167 -0
- data/docs/api/app.md +192 -0
- data/docs/api/assets.md +426 -0
- data/docs/api/audio.md +208 -0
- data/docs/api/components.md +321 -0
- data/docs/api/drawing.md +330 -0
- data/docs/api/game.md +99 -0
- data/docs/api/images.md +118 -0
- data/docs/api/input.md +179 -0
- data/docs/api/internals.md +110 -0
- data/docs/api/scene_graph.md +159 -0
- data/docs/api/signals.md +142 -0
- data/docs/api/systems.md +98 -0
- data/docs/api/text.md +116 -0
- data/docs/api/toolbox.md +240 -0
- data/docs/api/values.md +101 -0
- data/ext/README.md +225 -0
- data/ext/rgame_core/app/app.c +721 -0
- data/ext/rgame_core/app/app_gl.h +64 -0
- data/ext/rgame_core/app/frame_loop.c +42 -0
- data/ext/rgame_core/app/frame_loop.h +54 -0
- data/ext/rgame_core/audio/audio.c +466 -0
- data/ext/rgame_core/audio/audio_internal.h +45 -0
- data/ext/rgame_core/audio/vorbis_decoder.c +282 -0
- data/ext/rgame_core/audio/vorbis_decoder.h +45 -0
- data/ext/rgame_core/example.rb +188 -0
- data/ext/rgame_core/extconf.rb +167 -0
- data/ext/rgame_core/graphics/backend.c +52 -0
- data/ext/rgame_core/graphics/backend.h +64 -0
- data/ext/rgame_core/graphics/canvas.c +247 -0
- data/ext/rgame_core/graphics/canvas.h +143 -0
- data/ext/rgame_core/graphics/clip.c +87 -0
- data/ext/rgame_core/graphics/clip.h +89 -0
- data/ext/rgame_core/graphics/draw_queue.c +216 -0
- data/ext/rgame_core/graphics/draw_queue.h +174 -0
- data/ext/rgame_core/graphics/gl_backend.c +122 -0
- data/ext/rgame_core/graphics/gl_backend.h +43 -0
- data/ext/rgame_core/graphics/image.c +304 -0
- data/ext/rgame_core/graphics/image_internal.h +30 -0
- data/ext/rgame_core/graphics/primitives.c +189 -0
- data/ext/rgame_core/graphics/primitives.h +111 -0
- data/ext/rgame_core/graphics/recording.c +119 -0
- data/ext/rgame_core/graphics/recording.h +88 -0
- data/ext/rgame_core/graphics/texture.c +181 -0
- data/ext/rgame_core/graphics/texture.h +165 -0
- data/ext/rgame_core/graphics/transform.c +128 -0
- data/ext/rgame_core/graphics/transform.h +106 -0
- data/ext/rgame_core/include/rgame/core.h +577 -0
- data/ext/rgame_core/input/device_slots.c +103 -0
- data/ext/rgame_core/input/device_slots.h +93 -0
- data/ext/rgame_core/input/gamepad.c +145 -0
- data/ext/rgame_core/input/gamepad.h +63 -0
- data/ext/rgame_core/input/input.c +109 -0
- data/ext/rgame_core/input/input.h +99 -0
- data/ext/rgame_core/ruby/audio_ext.c +321 -0
- data/ext/rgame_core/ruby/core_ext.c +513 -0
- data/ext/rgame_core/ruby/core_ext.h +51 -0
- data/ext/rgame_core/ruby/font_ext.c +168 -0
- data/ext/rgame_core/ruby/image_ext.c +230 -0
- data/ext/rgame_core/ruby/recording_ext.c +186 -0
- data/ext/rgame_core/ruby/renderer_ext.c +376 -0
- data/ext/rgame_core/text/atlas.c +59 -0
- data/ext/rgame_core/text/atlas.h +85 -0
- data/ext/rgame_core/text/font.c +281 -0
- data/ext/rgame_core/text/font.h +139 -0
- data/ext/rgame_core/text/font_atlas.c +385 -0
- data/ext/rgame_core/text/font_internal.h +47 -0
- data/ext/rgame_core/text/glyph_cache.c +142 -0
- data/ext/rgame_core/text/glyph_cache.h +89 -0
- data/ext/rgame_core/vendor/README.md +159 -0
- data/ext/rgame_core/vendor/miniaudio.h +95864 -0
- data/ext/rgame_core/vendor/miniaudio_impl.c +62 -0
- data/ext/rgame_core/vendor/stb_image.h +7988 -0
- data/ext/rgame_core/vendor/stb_image_impl.c +31 -0
- data/ext/rgame_core/vendor/stb_truetype.h +5079 -0
- data/ext/rgame_core/vendor/stb_truetype_impl.c +23 -0
- data/ext/rgame_core/vendor/stb_vorbis.c +5584 -0
- data/ext/rgame_core/vendor/stb_vorbis_impl.c +29 -0
- data/ext/rgame_util/color.c +19 -0
- data/ext/rgame_util/color.h +60 -0
- data/ext/rgame_util/color_ext.c +156 -0
- data/ext/rgame_util/extconf.rb +27 -0
- data/ext/rgame_util/tensor.c +186 -0
- data/ext/rgame_util/util_ext.c +27 -0
- data/ext/rgame_util/util_ext.h +16 -0
- data/lib/rgame/boot.rb +13 -0
- data/lib/rgame/core/app.rb +82 -0
- data/lib/rgame/core/asset_manager.rb +224 -0
- data/lib/rgame/core/audio.rb +124 -0
- data/lib/rgame/core/font.rb +49 -0
- data/lib/rgame/core/gamepad.rb +55 -0
- data/lib/rgame/core/image.rb +55 -0
- data/lib/rgame/core/input.rb +77 -0
- data/lib/rgame/core/nine_slice.rb +163 -0
- data/lib/rgame/core/recording.rb +52 -0
- data/lib/rgame/core/renderer.rb +363 -0
- data/lib/rgame/core/sprite_sheet.rb +108 -0
- data/lib/rgame/core/tile_map_renderer.rb +160 -0
- data/lib/rgame/core/ui_atlas.rb +86 -0
- data/lib/rgame/core.rb +24 -0
- data/lib/rgame/engine/actor.rb +53 -0
- data/lib/rgame/engine/animation_set.rb +49 -0
- data/lib/rgame/engine/animator.rb +44 -0
- data/lib/rgame/engine/audio_bus.rb +24 -0
- data/lib/rgame/engine/audio_director.rb +29 -0
- data/lib/rgame/engine/body.rb +49 -0
- data/lib/rgame/engine/cached_label.rb +33 -0
- data/lib/rgame/engine/camera.rb +33 -0
- data/lib/rgame/engine/camera_view.rb +28 -0
- data/lib/rgame/engine/circle_collider.rb +32 -0
- data/lib/rgame/engine/collision_box.rb +34 -0
- data/lib/rgame/engine/collision_system.rb +44 -0
- data/lib/rgame/engine/component.rb +30 -0
- data/lib/rgame/engine/components/action_trigger.rb +41 -0
- data/lib/rgame/engine/components/animated_sprite.rb +63 -0
- data/lib/rgame/engine/components/character_body.rb +70 -0
- data/lib/rgame/engine/components/circle_collider.rb +44 -0
- data/lib/rgame/engine/components/collision_world.rb +103 -0
- data/lib/rgame/engine/components/despawn_offscreen.rb +26 -0
- data/lib/rgame/engine/components/path_follow.rb +84 -0
- data/lib/rgame/engine/components/player_controller.rb +24 -0
- data/lib/rgame/engine/components/pool.rb +53 -0
- data/lib/rgame/engine/components/screen_wrap.rb +27 -0
- data/lib/rgame/engine/components/sprite.rb +31 -0
- data/lib/rgame/engine/components/targeting.rb +54 -0
- data/lib/rgame/engine/components/thrust_controller.rb +65 -0
- data/lib/rgame/engine/components/tile_world.rb +68 -0
- data/lib/rgame/engine/components/timer.rb +75 -0
- data/lib/rgame/engine/components/velocity.rb +27 -0
- data/lib/rgame/engine/components/wander_controller.rb +60 -0
- data/lib/rgame/engine/debug_overlay.rb +106 -0
- data/lib/rgame/engine/i18n.rb +97 -0
- data/lib/rgame/engine/input/action_mapper.rb +46 -0
- data/lib/rgame/engine/input/actions.rb +41 -0
- data/lib/rgame/engine/input/player_controller.rb +14 -0
- data/lib/rgame/engine/matrix.rb +32 -0
- data/lib/rgame/engine/node2d.rb +271 -0
- data/lib/rgame/engine/path.rb +78 -0
- data/lib/rgame/engine/pool.rb +51 -0
- data/lib/rgame/engine/resettable.rb +67 -0
- data/lib/rgame/engine/scene/scene_stack.rb +65 -0
- data/lib/rgame/engine/signal.rb +75 -0
- data/lib/rgame/engine/spatial_hash.rb +71 -0
- data/lib/rgame/engine/tile_collision.rb +78 -0
- data/lib/rgame/engine/tile_map.rb +149 -0
- data/lib/rgame/engine/tileset.rb +101 -0
- data/lib/rgame/engine/timer.rb +51 -0
- data/lib/rgame/engine.rb +68 -0
- data/lib/rgame/fonts/LiberationSans-Regular.ttf +0 -0
- data/lib/rgame/fonts/OFL.txt +102 -0
- data/lib/rgame/game.rb +129 -0
- data/lib/rgame/util/color.rb +27 -0
- data/lib/rgame/util/controls.rb +107 -0
- data/lib/rgame/util/tensor.rb +12 -0
- data/lib/rgame/util.rb +8 -0
- data/lib/rgame/version.rb +12 -0
- data/lib/rgame.rb +20 -0
- metadata +215 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* app.c — the engine itself: SDL window + OpenGL context, and the main loop.
|
|
3
|
+
*
|
|
4
|
+
* This is the implementation behind the opaque `rgame_app` handle declared in
|
|
5
|
+
* include/rgame/core.h. It owns the loop and calls back out to the caller's
|
|
6
|
+
* update/draw functions, which is what lets the same engine be driven from a C
|
|
7
|
+
* main() (src/main.c) and from Ruby (core_ext.c) without either of them
|
|
8
|
+
* knowing about SDL or GL.
|
|
9
|
+
*
|
|
10
|
+
* Three files in this directory have "core"-ish names and it's worth being
|
|
11
|
+
* clear which is which:
|
|
12
|
+
*
|
|
13
|
+
* include/rgame/core.h the public API — no SDL/GL types, safe to include
|
|
14
|
+
* anywhere (this file implements it)
|
|
15
|
+
* app.c this file: the real SDL/GL engine behind that API
|
|
16
|
+
* core_ext.c the Ruby binding, which only ever calls core.h
|
|
17
|
+
*
|
|
18
|
+
* The pure timing logic (accumulator, FPS counter) deliberately lives in
|
|
19
|
+
* frame_loop.{c,h} instead of here, so it can be unit-tested without a window.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
#include "rgame/core.h"
|
|
23
|
+
#include "app/app_gl.h"
|
|
24
|
+
#include "graphics/canvas.h"
|
|
25
|
+
#include "text/font_internal.h"
|
|
26
|
+
#include "app/frame_loop.h"
|
|
27
|
+
#include "input/gamepad.h"
|
|
28
|
+
#include "graphics/gl_backend.h"
|
|
29
|
+
#include "graphics/image_internal.h"
|
|
30
|
+
#include "input/input.h"
|
|
31
|
+
#include "graphics/primitives.h"
|
|
32
|
+
#include "graphics/recording.h"
|
|
33
|
+
|
|
34
|
+
#include <SDL2/SDL.h>
|
|
35
|
+
#include <SDL2/SDL_opengl.h>
|
|
36
|
+
#include <stdlib.h>
|
|
37
|
+
|
|
38
|
+
/* Fixed simulation step and the cap on catch-up steps per rendered frame.
|
|
39
|
+
* 1/60 s tick; at most 5 sim steps before we drop backlog (see frame_loop.h). */
|
|
40
|
+
#define RGAME_TICK_SECONDS (1.0 / 60.0)
|
|
41
|
+
#define RGAME_MAX_TICKS_PER_FRAME 5
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* How many apps are alive. SDL_Init is safe to call repeatedly, but SDL_Quit
|
|
45
|
+
* is not paired with it — it tears down *everything* regardless of how many
|
|
46
|
+
* callers still need SDL. Destroying one app while another lived therefore
|
|
47
|
+
* pulled the video subsystem out from under it and segfaulted inside GC, which
|
|
48
|
+
* is exactly the shape a spec suite produces (many short-lived Apps, collected
|
|
49
|
+
* at unpredictable times). So SDL is shut down only when the last app goes.
|
|
50
|
+
*
|
|
51
|
+
* Single-threaded by assumption, like the rest of the engine.
|
|
52
|
+
*/
|
|
53
|
+
static int rgame_live_apps = 0;
|
|
54
|
+
|
|
55
|
+
struct rgame_app {
|
|
56
|
+
SDL_Window *window;
|
|
57
|
+
SDL_GLContext gl_context;
|
|
58
|
+
int running;
|
|
59
|
+
/*
|
|
60
|
+
* How many things still hold this pointer: the creator, plus every image
|
|
61
|
+
* uploaded into this app's context. The window and context are torn down
|
|
62
|
+
* the moment rgame_app_destroy is called — this refcount only governs when
|
|
63
|
+
* the *struct* is freed, so that an image outliving its app finds a valid
|
|
64
|
+
* pointer saying "the context is gone" rather than freed memory.
|
|
65
|
+
*
|
|
66
|
+
* This matters because Ruby's collector may sweep an app and its images in
|
|
67
|
+
* the same pass, in an order nothing guarantees. Nothing leaks when the app
|
|
68
|
+
* goes first: destroying a GL context frees every texture in it, which is
|
|
69
|
+
* why the image side can simply skip its glDeleteTextures.
|
|
70
|
+
*/
|
|
71
|
+
int refs;
|
|
72
|
+
rgame_frame_loop frame_loop;
|
|
73
|
+
rgame_fps_counter fps_counter;
|
|
74
|
+
rgame_input_state input;
|
|
75
|
+
rgame_gamepads gamepads;
|
|
76
|
+
|
|
77
|
+
/* The drawing surface, and the GL backend it is submitted to. Both live
|
|
78
|
+
* for the app's lifetime; the canvas reuses its buffers across frames
|
|
79
|
+
* rather than reallocating (see draw_queue.h). */
|
|
80
|
+
rgame_canvas canvas;
|
|
81
|
+
rgame_gl_backend gl;
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* Where draws go while a recording is open. Kept for the app's lifetime
|
|
85
|
+
* like the frame canvas, so baking a layer between levels does not
|
|
86
|
+
* allocate a queue from scratch each time.
|
|
87
|
+
*/
|
|
88
|
+
rgame_canvas record_canvas;
|
|
89
|
+
int recording;
|
|
90
|
+
/*
|
|
91
|
+
* Whether a frame is currently open. Drawing outside `draw` would append to
|
|
92
|
+
* a queue that is about to be reset — silently drawing nothing — so every
|
|
93
|
+
* primitive checks this, and the Ruby binding turns it into an error.
|
|
94
|
+
*/
|
|
95
|
+
int drawing;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
rgame_app *rgame_app_create(int width, int height, const char *title) {
|
|
99
|
+
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) != 0) {
|
|
100
|
+
SDL_Log("SDL_Init failed: %s", SDL_GetError());
|
|
101
|
+
return NULL;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
|
105
|
+
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
|
|
106
|
+
|
|
107
|
+
rgame_app *app = calloc(1, sizeof(rgame_app));
|
|
108
|
+
if (!app) {
|
|
109
|
+
SDL_Quit();
|
|
110
|
+
return NULL;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
app->window = SDL_CreateWindow(
|
|
114
|
+
title,
|
|
115
|
+
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
|
116
|
+
width, height,
|
|
117
|
+
SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN);
|
|
118
|
+
if (!app->window) {
|
|
119
|
+
SDL_Log("SDL_CreateWindow failed: %s", SDL_GetError());
|
|
120
|
+
free(app);
|
|
121
|
+
SDL_Quit();
|
|
122
|
+
return NULL;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
app->gl_context = SDL_GL_CreateContext(app->window);
|
|
126
|
+
if (!app->gl_context) {
|
|
127
|
+
SDL_Log("SDL_GL_CreateContext failed: %s", SDL_GetError());
|
|
128
|
+
SDL_DestroyWindow(app->window);
|
|
129
|
+
free(app);
|
|
130
|
+
SDL_Quit();
|
|
131
|
+
return NULL;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
SDL_GL_SetSwapInterval(1); /* vsync */
|
|
135
|
+
|
|
136
|
+
rgame_live_apps++;
|
|
137
|
+
|
|
138
|
+
app->running = 1;
|
|
139
|
+
app->refs = 1;
|
|
140
|
+
app->drawing = 0;
|
|
141
|
+
app->recording = 0;
|
|
142
|
+
rgame_canvas_init(&app->canvas);
|
|
143
|
+
rgame_canvas_init(&app->record_canvas);
|
|
144
|
+
rgame_input_state_clear(&app->input);
|
|
145
|
+
rgame_gamepads_init(&app->gamepads);
|
|
146
|
+
rgame_frame_loop_init(&app->frame_loop);
|
|
147
|
+
rgame_fps_counter_init(&app->fps_counter);
|
|
148
|
+
return app;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* Frees the struct once nothing points at it any more. The window and context
|
|
152
|
+
* are already gone by this point; see the refs comment above. */
|
|
153
|
+
static void app_unref(rgame_app *app) {
|
|
154
|
+
if (--app->refs <= 0) {
|
|
155
|
+
free(app);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
void rgame_app_destroy(rgame_app *app) {
|
|
160
|
+
if (!app) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Idempotent: the window is NULLed below, so a second call falls out here
|
|
165
|
+
* rather than shutting SDL down twice or dropping a reference twice. */
|
|
166
|
+
if (!app->window && !app->gl_context) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
rgame_gamepads_shutdown(&app->gamepads);
|
|
171
|
+
rgame_canvas_destroy(&app->canvas);
|
|
172
|
+
rgame_canvas_destroy(&app->record_canvas);
|
|
173
|
+
if (app->gl_context) {
|
|
174
|
+
SDL_GL_DeleteContext(app->gl_context);
|
|
175
|
+
app->gl_context = NULL;
|
|
176
|
+
}
|
|
177
|
+
if (app->window) {
|
|
178
|
+
SDL_DestroyWindow(app->window);
|
|
179
|
+
app->window = NULL;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (--rgame_live_apps <= 0) {
|
|
183
|
+
rgame_live_apps = 0;
|
|
184
|
+
SDL_Quit();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
app_unref(app);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
void rgame_app_gl_retain(rgame_app *app) {
|
|
191
|
+
if (app) {
|
|
192
|
+
app->refs++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
void rgame_app_gl_release(rgame_app *app) {
|
|
197
|
+
if (app) {
|
|
198
|
+
app_unref(app);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
int rgame_app_gl_make_current(rgame_app *app, rgame_gl_context_save *saved) {
|
|
203
|
+
if (saved) {
|
|
204
|
+
/* Captured before the switch, and captured even if the switch then
|
|
205
|
+
* fails — restoring a no-op is free, and forgetting to capture is not
|
|
206
|
+
* recoverable. */
|
|
207
|
+
saved->window = SDL_GL_GetCurrentWindow();
|
|
208
|
+
saved->context = SDL_GL_GetCurrentContext();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* Both are NULLed by rgame_app_destroy, so this also answers "is there
|
|
212
|
+
* still a context to draw into?" for anything holding a retained app. */
|
|
213
|
+
if (!app || !app->window || !app->gl_context) {
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
return SDL_GL_MakeCurrent(app->window, app->gl_context) == 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
void rgame_app_gl_restore(const rgame_gl_context_save *saved) {
|
|
220
|
+
if (saved && saved->context) {
|
|
221
|
+
SDL_GL_MakeCurrent((SDL_Window *)saved->window, (SDL_GLContext)saved->context);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/*
|
|
226
|
+
* The button ids in core.h are SDL scancodes, but core.h can't say so in a way
|
|
227
|
+
* the compiler checks — it must not include SDL. These assertions close that
|
|
228
|
+
* gap at compile time, so the two can never drift apart silently.
|
|
229
|
+
*/
|
|
230
|
+
_Static_assert(RGAME_KEY_RETURN == SDL_SCANCODE_RETURN, "key id must match SDL scancode");
|
|
231
|
+
_Static_assert(RGAME_KEY_ESCAPE == SDL_SCANCODE_ESCAPE, "key id must match SDL scancode");
|
|
232
|
+
_Static_assert(RGAME_KEY_SPACE == SDL_SCANCODE_SPACE, "key id must match SDL scancode");
|
|
233
|
+
_Static_assert(RGAME_KEY_F1 == SDL_SCANCODE_F1, "key id must match SDL scancode");
|
|
234
|
+
_Static_assert(RGAME_KEY_RIGHT == SDL_SCANCODE_RIGHT, "key id must match SDL scancode");
|
|
235
|
+
_Static_assert(RGAME_KEY_LEFT == SDL_SCANCODE_LEFT, "key id must match SDL scancode");
|
|
236
|
+
_Static_assert(RGAME_KEY_DOWN == SDL_SCANCODE_DOWN, "key id must match SDL scancode");
|
|
237
|
+
_Static_assert(RGAME_KEY_UP == SDL_SCANCODE_UP, "key id must match SDL scancode");
|
|
238
|
+
|
|
239
|
+
/* The snapshot array must be exactly as long as SDL's keyboard state array,
|
|
240
|
+
* since filling it is a straight copy. */
|
|
241
|
+
_Static_assert(RGAME_KEYBOARD_KEY_COUNT == SDL_NUM_SCANCODES,
|
|
242
|
+
"RGAME_KEYBOARD_KEY_COUNT must match SDL_NUM_SCANCODES");
|
|
243
|
+
|
|
244
|
+
/* Every keyboard id must land inside the keyboard range of the flat id space,
|
|
245
|
+
* or rgame_input_state_down would reject it as belonging to another device. */
|
|
246
|
+
_Static_assert(SDL_NUM_SCANCODES - 1 <= RGAME_BUTTON_KEYBOARD_LAST,
|
|
247
|
+
"SDL scancodes must fit the keyboard button-id range");
|
|
248
|
+
|
|
249
|
+
/*
|
|
250
|
+
* Copies SDL's live keyboard state into the app's snapshot. Called once per
|
|
251
|
+
* frame, right after the event queue is drained, so every simulation tick in
|
|
252
|
+
* that frame sees identical input — see input.h for why that matters.
|
|
253
|
+
*
|
|
254
|
+
* SDL_GetKeyboardState returns a pointer to SDL's own internal array, which
|
|
255
|
+
* SDL updates as events are pumped; it must not be freed, and holding onto it
|
|
256
|
+
* across frames would defeat the whole point, so it is copied here and then
|
|
257
|
+
* forgotten.
|
|
258
|
+
*/
|
|
259
|
+
static void rgame_app_snapshot_input(rgame_app *app) {
|
|
260
|
+
int count = 0;
|
|
261
|
+
const Uint8 *keys = SDL_GetKeyboardState(&count);
|
|
262
|
+
if (keys && count == RGAME_KEYBOARD_KEY_COUNT) {
|
|
263
|
+
rgame_input_state_set_keys(&app->input, keys);
|
|
264
|
+
}
|
|
265
|
+
rgame_gamepads_snapshot(&app->gamepads, &app->input);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/*
|
|
269
|
+
* Drains the SDL event queue, dispatching to the caller's event callbacks.
|
|
270
|
+
*
|
|
271
|
+
* Closing the window still stops the loop here — that really is the platform's
|
|
272
|
+
* decision. Quitting on a *key*, though, is a game decision and deliberately
|
|
273
|
+
* isn't handled: the caller gets the button_down and closes if it wants to.
|
|
274
|
+
*/
|
|
275
|
+
static void rgame_app_poll_events(rgame_app *app, const rgame_app_callbacks *cb) {
|
|
276
|
+
SDL_Event event;
|
|
277
|
+
while (SDL_PollEvent(&event)) {
|
|
278
|
+
switch (event.type) {
|
|
279
|
+
case SDL_QUIT:
|
|
280
|
+
app->running = 0;
|
|
281
|
+
break;
|
|
282
|
+
case SDL_KEYDOWN:
|
|
283
|
+
/* event.key.repeat is non-zero for auto-repeats while a key is
|
|
284
|
+
* held. A discrete press must fire exactly once, so drop those. */
|
|
285
|
+
if (!event.key.repeat && cb->button_down) {
|
|
286
|
+
cb->button_down(cb->userdata, (int)event.key.keysym.scancode);
|
|
287
|
+
}
|
|
288
|
+
break;
|
|
289
|
+
case SDL_KEYUP:
|
|
290
|
+
if (cb->button_up) {
|
|
291
|
+
cb->button_up(cb->userdata, (int)event.key.keysym.scancode);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
case SDL_CONTROLLERDEVICEADDED: {
|
|
295
|
+
/* For ADDED, `which` is a device *index*. */
|
|
296
|
+
int slot = rgame_gamepads_add(&app->gamepads, event.cdevice.which);
|
|
297
|
+
if (slot != RGAME_DEVICE_SLOT_NONE && cb->gamepad_connected) {
|
|
298
|
+
cb->gamepad_connected(cb->userdata, slot);
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
case SDL_CONTROLLERDEVICEREMOVED: {
|
|
303
|
+
/* ...but for REMOVED, the same field is an *instance id*. Passing
|
|
304
|
+
* one where the other is expected is silent and wrong. */
|
|
305
|
+
int slot = rgame_gamepads_remove(&app->gamepads, event.cdevice.which);
|
|
306
|
+
if (slot != RGAME_DEVICE_SLOT_NONE) {
|
|
307
|
+
/* Drop any buttons that were held at the moment it vanished. */
|
|
308
|
+
rgame_input_state_clear_pad(&app->input, slot);
|
|
309
|
+
if (cb->gamepad_disconnected) {
|
|
310
|
+
cb->gamepad_disconnected(cb->userdata, slot);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
case SDL_WINDOWEVENT:
|
|
316
|
+
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
|
317
|
+
/* The viewport is set from the current size every frame, in the
|
|
318
|
+
* backend's begin_frame, so there is nothing to update here. */
|
|
319
|
+
if (cb->resize) {
|
|
320
|
+
cb->resize(cb->userdata, event.window.data1, event.window.data2);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
default:
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
void rgame_app_run(rgame_app *app, const rgame_app_callbacks *cb) {
|
|
331
|
+
if (!app || !cb) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
Uint32 prev_ms = SDL_GetTicks();
|
|
336
|
+
|
|
337
|
+
/* Every step re-checks `running`, because any callback may have asked to
|
|
338
|
+
* close — including a Ruby callback that raised and unwound into one. */
|
|
339
|
+
while (app->running) {
|
|
340
|
+
rgame_app_poll_events(app, cb);
|
|
341
|
+
if (!app->running) {
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* One sample per frame, after the events that produced it. */
|
|
346
|
+
rgame_app_snapshot_input(app);
|
|
347
|
+
|
|
348
|
+
Uint32 now_ms = SDL_GetTicks();
|
|
349
|
+
double elapsed_seconds = (now_ms - prev_ms) / 1000.0;
|
|
350
|
+
prev_ms = now_ms;
|
|
351
|
+
|
|
352
|
+
/* Once per frame, before the tick batch: the caller's chance to sample
|
|
353
|
+
* input once and reuse it across every catch-up tick below. */
|
|
354
|
+
if (cb->frame_begin) {
|
|
355
|
+
cb->frame_begin(cb->userdata);
|
|
356
|
+
}
|
|
357
|
+
if (!app->running) {
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Fixed-timestep simulation: run whole ticks the accumulator has banked. */
|
|
362
|
+
int ticks = rgame_frame_loop_advance(&app->frame_loop, elapsed_seconds,
|
|
363
|
+
RGAME_TICK_SECONDS, RGAME_MAX_TICKS_PER_FRAME);
|
|
364
|
+
for (int i = 0; i < ticks && app->running; i++) {
|
|
365
|
+
if (cb->update) {
|
|
366
|
+
cb->update(cb->userdata, RGAME_TICK_SECONDS);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!app->running) {
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/* Count every loop iteration toward FPS. With the current always-draw
|
|
374
|
+
* behavior this equals the render rate; if needs_redraw skipping is
|
|
375
|
+
* added later, revisit whether FPS should track loop vs. render rate. */
|
|
376
|
+
rgame_fps_counter_tick(&app->fps_counter, elapsed_seconds);
|
|
377
|
+
|
|
378
|
+
int redraw = cb->needs_redraw ? cb->needs_redraw(cb->userdata) : 1;
|
|
379
|
+
if (!app->running) {
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (redraw) {
|
|
384
|
+
/*
|
|
385
|
+
* Open the frame, let the caller draw into it, then sort, batch and
|
|
386
|
+
* submit. Re-stating the window size every frame is also how a
|
|
387
|
+
* resize takes effect, so there is no separate path to forget.
|
|
388
|
+
*
|
|
389
|
+
* The caller's draw callback only ever appends to the queue; the
|
|
390
|
+
* bracketing is the engine's job. That is deliberate — a hook the
|
|
391
|
+
* user must remember to open and close is a hook someone forgets.
|
|
392
|
+
*/
|
|
393
|
+
rgame_canvas_begin_frame(&app->canvas, rgame_app_width(app), rgame_app_height(app));
|
|
394
|
+
app->drawing = 1;
|
|
395
|
+
if (cb->draw) {
|
|
396
|
+
cb->draw(cb->userdata);
|
|
397
|
+
}
|
|
398
|
+
app->drawing = 0;
|
|
399
|
+
rgame_canvas_end_frame(&app->canvas);
|
|
400
|
+
|
|
401
|
+
rgame_draw_backend backend = rgame_gl_backend_table(&app->gl);
|
|
402
|
+
rgame_canvas_submit(&app->canvas, &backend);
|
|
403
|
+
SDL_GL_SwapWindow(app->window);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* ------------------------------------------------------------------------- *
|
|
409
|
+
* Drawing
|
|
410
|
+
*
|
|
411
|
+
* Each of these is a guard plus one call into primitives.c, which is pure and
|
|
412
|
+
* Check-tested. The guard is the interesting part: outside a frame the canvas
|
|
413
|
+
* would happily accept the vertices and throw them away at the next
|
|
414
|
+
* begin_frame, so refusing here is what turns "my draw call does nothing" into
|
|
415
|
+
* something a caller can be told about.
|
|
416
|
+
* ------------------------------------------------------------------------- */
|
|
417
|
+
|
|
418
|
+
int rgame_app_is_drawing(const rgame_app *app) {
|
|
419
|
+
return app && app->drawing;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/*
|
|
423
|
+
* The canvas to draw into, or NULL if no frame is open.
|
|
424
|
+
*
|
|
425
|
+
* While a recording is open this is the recording's own canvas, which is the
|
|
426
|
+
* entire mechanism: every draw function below is unchanged, and capture is a
|
|
427
|
+
* matter of where their vertices land.
|
|
428
|
+
*/
|
|
429
|
+
static rgame_canvas *drawing_canvas(rgame_app *app) {
|
|
430
|
+
if (!rgame_app_is_drawing(app)) {
|
|
431
|
+
return NULL;
|
|
432
|
+
}
|
|
433
|
+
return app->recording ? &app->record_canvas : &app->canvas;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
void rgame_app_draw_rect(rgame_app *app, float x, float y, float width, float height,
|
|
437
|
+
unsigned int color, double z) {
|
|
438
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
439
|
+
if (canvas) {
|
|
440
|
+
rgame_prim_rect(canvas, x, y, width, height, color, z);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
void rgame_app_draw_quad(rgame_app *app, const float *xy8, unsigned int color, double z) {
|
|
445
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
446
|
+
if (canvas && xy8) {
|
|
447
|
+
rgame_canvas_quad(canvas, xy8, color, z);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
void rgame_app_draw_triangle(rgame_app *app, const float *xy6, unsigned int color, double z) {
|
|
452
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
453
|
+
if (canvas && xy6) {
|
|
454
|
+
rgame_canvas_triangle(canvas, xy6, color, z);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
void rgame_app_draw_line(rgame_app *app, float x1, float y1, float x2, float y2,
|
|
459
|
+
float thickness, unsigned int color, double z) {
|
|
460
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
461
|
+
if (canvas) {
|
|
462
|
+
rgame_prim_line(canvas, x1, y1, x2, y2, thickness, color, z);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
void rgame_app_draw_circle(rgame_app *app, float cx, float cy, float radius, int segments,
|
|
467
|
+
unsigned int color, double z) {
|
|
468
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
469
|
+
if (canvas) {
|
|
470
|
+
rgame_prim_circle(canvas, cx, cy, radius, segments, color, z);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* An image may only be drawn by the app that uploaded it; see core.h. A NULL
|
|
475
|
+
* image is "nothing to draw", not a mismatch, and simply draws nothing. */
|
|
476
|
+
static int image_belongs_here(const rgame_app *app, const rgame_image *image) {
|
|
477
|
+
return !image || rgame_image_owner(image) == app;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
int rgame_app_draw_image(rgame_app *app, const rgame_image *image, float x, float y,
|
|
481
|
+
unsigned int color, double z) {
|
|
482
|
+
if (!image_belongs_here(app, image)) {
|
|
483
|
+
return 0;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
487
|
+
if (canvas) {
|
|
488
|
+
rgame_prim_image(canvas, rgame_image_view(image), x, y, color, z);
|
|
489
|
+
}
|
|
490
|
+
return 1;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
int rgame_app_draw_image_scaled(rgame_app *app, const rgame_image *image, float x, float y,
|
|
494
|
+
float scale_x, float scale_y, unsigned int color, double z) {
|
|
495
|
+
if (!image_belongs_here(app, image)) {
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
500
|
+
if (canvas) {
|
|
501
|
+
rgame_prim_image_scaled(canvas, rgame_image_view(image), x, y, scale_x, scale_y, color, z);
|
|
502
|
+
}
|
|
503
|
+
return 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
int rgame_app_draw_image_rot(rgame_app *app, const rgame_image *image, float cx, float cy,
|
|
507
|
+
float angle_degrees, float scale, unsigned int color, double z) {
|
|
508
|
+
if (!image_belongs_here(app, image)) {
|
|
509
|
+
return 0;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
513
|
+
if (canvas) {
|
|
514
|
+
rgame_prim_image_rot(canvas, rgame_image_view(image), cx, cy, angle_degrees, scale, color,
|
|
515
|
+
z);
|
|
516
|
+
}
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* A font's atlas pages live in one GL context, exactly like an image's
|
|
521
|
+
* texture; see image_belongs_here. */
|
|
522
|
+
static int font_belongs_here(const rgame_app *app, const rgame_font *font) {
|
|
523
|
+
return !font || rgame_font_owner(font) == app;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
int rgame_app_draw_text(rgame_app *app, rgame_font *font, const char *text, size_t length,
|
|
527
|
+
float x, float y, unsigned int color, double z) {
|
|
528
|
+
if (!font_belongs_here(app, font)) {
|
|
529
|
+
return 0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
533
|
+
if (!canvas || !font || !text) {
|
|
534
|
+
return 1;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/*
|
|
538
|
+
* The same cursor `rgame_font_measure` uses — not the same arithmetic, the
|
|
539
|
+
* same code. Two loops that both sum advances drift the moment one gains a
|
|
540
|
+
* rounding rule, and then every centred label in the game sits a pixel off
|
|
541
|
+
* with nothing to point at.
|
|
542
|
+
*/
|
|
543
|
+
rgame_text_cursor cursor;
|
|
544
|
+
rgame_text_cursor_init(&cursor, text, length);
|
|
545
|
+
|
|
546
|
+
int codepoint = 0;
|
|
547
|
+
float pen_x = 0.0f;
|
|
548
|
+
while (rgame_text_cursor_next(&cursor, rgame_font_typeface(font), &codepoint, &pen_x)) {
|
|
549
|
+
rgame_glyph glyph;
|
|
550
|
+
unsigned int texture = 0;
|
|
551
|
+
int page_width = 0, page_height = 0;
|
|
552
|
+
if (!rgame_font_glyph(font, codepoint, &glyph, &texture, &page_width, &page_height)) {
|
|
553
|
+
continue; /* skip the one glyph, draw the rest of the string */
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* The bearings turn a pen position into where the ink goes: x from the
|
|
557
|
+
* pen, y from the top of the line box. A space has no rect and
|
|
558
|
+
* rgame_prim_glyph draws nothing for it. */
|
|
559
|
+
rgame_prim_glyph(canvas, texture, glyph.rect, page_width, page_height,
|
|
560
|
+
x + pen_x + glyph.bearing_x, y + glyph.bearing_y, color, z);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return 1;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
void rgame_app_push_translate(rgame_app *app, float dx, float dy) {
|
|
567
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
568
|
+
if (canvas) {
|
|
569
|
+
rgame_canvas_push_translate(canvas, dx, dy);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
void rgame_app_push_rotate(rgame_app *app, float degrees, float pivot_x, float pivot_y) {
|
|
574
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
575
|
+
if (canvas) {
|
|
576
|
+
rgame_canvas_push_rotate(canvas, degrees, pivot_x, pivot_y);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
void rgame_app_push_scale(rgame_app *app, float sx, float sy) {
|
|
581
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
582
|
+
if (canvas) {
|
|
583
|
+
rgame_canvas_push_scale(canvas, sx, sy);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
int rgame_app_push_clip(rgame_app *app, int x, int y, int width, int height) {
|
|
588
|
+
/* A clip cannot be baked: see the recording section of core.h. Refusing
|
|
589
|
+
* keeps the push/pop pairing honest — nothing was pushed, so the caller's
|
|
590
|
+
* matching pop has nothing to undo either. */
|
|
591
|
+
if (app && app->recording) {
|
|
592
|
+
return 0;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
596
|
+
if (canvas) {
|
|
597
|
+
rgame_canvas_push_clip(canvas, rgame_rect_make(x, y, width, height));
|
|
598
|
+
}
|
|
599
|
+
return 1;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
void rgame_app_pop(rgame_app *app) {
|
|
603
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
604
|
+
if (canvas) {
|
|
605
|
+
rgame_canvas_pop(canvas);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* ------------------------------------------------------------------------- *
|
|
610
|
+
* Recordings
|
|
611
|
+
* ------------------------------------------------------------------------- */
|
|
612
|
+
|
|
613
|
+
int rgame_app_begin_record(rgame_app *app) {
|
|
614
|
+
if (!rgame_app_is_drawing(app) || app->recording) {
|
|
615
|
+
return 0;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/* The size only decides the recording canvas's base clip. Nothing inside
|
|
619
|
+
* may narrow it and the recorded clips are discarded at capture, so it
|
|
620
|
+
* simply has to be big enough not to drop anything — the window is. */
|
|
621
|
+
rgame_canvas_begin_frame(&app->record_canvas, rgame_app_width(app), rgame_app_height(app));
|
|
622
|
+
app->recording = 1;
|
|
623
|
+
return 1;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
rgame_recording *rgame_app_end_record(rgame_app *app) {
|
|
627
|
+
if (!app || !app->recording) {
|
|
628
|
+
return NULL;
|
|
629
|
+
}
|
|
630
|
+
app->recording = 0;
|
|
631
|
+
|
|
632
|
+
/* Sorting and grouping is exactly the work a recording exists to do once,
|
|
633
|
+
* so it happens here rather than on every replay. */
|
|
634
|
+
rgame_canvas_end_frame(&app->record_canvas);
|
|
635
|
+
|
|
636
|
+
rgame_recording *recording = calloc(1, sizeof(rgame_recording));
|
|
637
|
+
if (!recording) {
|
|
638
|
+
return NULL;
|
|
639
|
+
}
|
|
640
|
+
if (!rgame_recording_capture(recording, rgame_canvas_queue(&app->record_canvas))) {
|
|
641
|
+
free(recording);
|
|
642
|
+
return NULL;
|
|
643
|
+
}
|
|
644
|
+
return recording;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
void rgame_app_cancel_record(rgame_app *app) {
|
|
648
|
+
if (app) {
|
|
649
|
+
app->recording = 0;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
void rgame_recording_free(rgame_recording *recording) {
|
|
654
|
+
if (recording) {
|
|
655
|
+
rgame_recording_destroy(recording);
|
|
656
|
+
free(recording);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
void rgame_app_draw_recording(rgame_app *app, const rgame_recording *recording, float x,
|
|
661
|
+
float y, unsigned int color, double z) {
|
|
662
|
+
rgame_canvas *canvas = drawing_canvas(app);
|
|
663
|
+
if (canvas) {
|
|
664
|
+
rgame_canvas_replay(canvas, recording, x, y, color, z);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
void rgame_app_close(rgame_app *app) {
|
|
669
|
+
if (app) {
|
|
670
|
+
app->running = 0;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
int rgame_app_width(const rgame_app *app) {
|
|
675
|
+
int width = 0;
|
|
676
|
+
SDL_GetWindowSize(app->window, &width, NULL);
|
|
677
|
+
return width;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
int rgame_app_height(const rgame_app *app) {
|
|
681
|
+
int height = 0;
|
|
682
|
+
SDL_GetWindowSize(app->window, NULL, &height);
|
|
683
|
+
return height;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
const char *rgame_app_title(const rgame_app *app) {
|
|
687
|
+
return SDL_GetWindowTitle(app->window);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
void rgame_app_set_title(rgame_app *app, const char *title) {
|
|
691
|
+
SDL_SetWindowTitle(app->window, title);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
int rgame_app_input_down(const rgame_app *app, int device, int button_id) {
|
|
695
|
+
return rgame_input_state_down(&app->input, device, button_id);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
float rgame_app_input_axis(const rgame_app *app, int device, int axis_id) {
|
|
699
|
+
return rgame_input_state_axis(&app->input, device, axis_id);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
int rgame_app_gamepad_connected(const rgame_app *app, int slot) {
|
|
703
|
+
return rgame_gamepads_connected(&app->gamepads, slot);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const char *rgame_app_gamepad_name(const rgame_app *app, int slot) {
|
|
707
|
+
return rgame_gamepads_name(&app->gamepads, slot);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
int rgame_app_gamepad_count(const rgame_app *app) {
|
|
711
|
+
return rgame_gamepads_count(&app->gamepads);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
unsigned int rgame_app_ticks_ms(const rgame_app *app) {
|
|
715
|
+
(void)app; /* SDL's clock is global; app is taken for API consistency. */
|
|
716
|
+
return SDL_GetTicks();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
double rgame_app_fps(const rgame_app *app) {
|
|
720
|
+
return app->fps_counter.fps;
|
|
721
|
+
}
|