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,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A uniform-grid spatial hash for broadphase collision: bucket colliders into
|
|
6
|
+
# fixed-size cells, then test only candidates that share a cell instead of every
|
|
7
|
+
# pair. Pure logic; no graphics.
|
|
8
|
+
#
|
|
9
|
+
# Typical per-frame use: `clear`, `insert` every collider of the static set
|
|
10
|
+
# (here: rocks), then `query` around each moving collider (bullets, the ship).
|
|
11
|
+
#
|
|
12
|
+
# hash.clear
|
|
13
|
+
# rocks.each { |r| hash.insert(r, *r.aabb) }
|
|
14
|
+
# hash.query(*bullet.aabb) { |rock| ...narrowphase... }
|
|
15
|
+
class SpatialHash
|
|
16
|
+
# Cell (col, row) → one integer key. The offset keeps negative cells (objects
|
|
17
|
+
# off-screen / mid-wrap) non-negative; the stride keeps pairs unique. Both fit
|
|
18
|
+
# in a tagged Fixnum, so keying allocates nothing.
|
|
19
|
+
OFFSET = 1 << 20
|
|
20
|
+
STRIDE = 1 << 21
|
|
21
|
+
|
|
22
|
+
def initialize(cell_size:)
|
|
23
|
+
@cell_size = cell_size
|
|
24
|
+
@buckets = Hash.new { |h, key| h[key] = [] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Reuse the bucket arrays across frames (clear contents, keep capacity).
|
|
28
|
+
def clear
|
|
29
|
+
@buckets.each_value(&:clear)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def insert(item, x, y, w, h)
|
|
33
|
+
each_cell(x, y, w, h) { |key| @buckets[key] << item }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Yield every item whose buckets overlap the region. Dedup contract: an item
|
|
37
|
+
# spanning several cells may be yielded more than once. Narrowphase callers
|
|
38
|
+
# must already guard with `next if a.dead? || b.dead?` to make hits idempotent,
|
|
39
|
+
# so we skip a per-query visited set and stay allocation-free.
|
|
40
|
+
def query(x, y, w, h, &)
|
|
41
|
+
each_cell(x, y, w, h) do |key|
|
|
42
|
+
bucket = @buckets[key]
|
|
43
|
+
bucket.each(&) unless bucket.empty?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Broadphase a circle: yield every item bucketed in a cell the circle's bounding
|
|
48
|
+
# box covers — the radial counterpart to #query, for range/nearest lookups. Same
|
|
49
|
+
# dedup contract (an item may be yielded more than once; the narrowphase caller
|
|
50
|
+
# refines by true distance). Allocation-free.
|
|
51
|
+
def query_circle(cx, cy, r, &)
|
|
52
|
+
d = r * 2
|
|
53
|
+
query(cx - r, cy - r, d, d, &)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def each_cell(x, y, w, h)
|
|
59
|
+
col0 = (x / @cell_size).floor
|
|
60
|
+
row0 = (y / @cell_size).floor
|
|
61
|
+
col1 = ((x + w) / @cell_size).floor
|
|
62
|
+
row1 = ((y + h) / @cell_size).floor
|
|
63
|
+
row0.upto(row1) do |row|
|
|
64
|
+
col0.upto(col1) do |col|
|
|
65
|
+
yield (col + OFFSET) * STRIDE + (row + OFFSET)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# Axis-separated AABB-vs-tile collision resolution (pure). `solid` is a callable
|
|
6
|
+
# `solid.call(col, row) -> bool`. Resolve X then Y (with the X result) to get
|
|
7
|
+
# wall-sliding. Assumes per-step movement smaller than a tile (no tunneling),
|
|
8
|
+
# which holds for our speeds.
|
|
9
|
+
class TileCollision
|
|
10
|
+
EPS = 1e-9
|
|
11
|
+
|
|
12
|
+
def initialize(tile_width:, tile_height:, solid:)
|
|
13
|
+
@tile_width = tile_width
|
|
14
|
+
@tile_height = tile_height
|
|
15
|
+
@solid = solid
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Move an AABB (top-left x, y; size w, h) by dx, snapping flush against solids.
|
|
19
|
+
def resolve_x(x, y, w, h, dx)
|
|
20
|
+
nx = x + dx
|
|
21
|
+
return nx if dx.zero?
|
|
22
|
+
|
|
23
|
+
first_row = (y / @tile_height).floor
|
|
24
|
+
last_row = ((y + h - EPS) / @tile_height).floor
|
|
25
|
+
|
|
26
|
+
if dx.positive?
|
|
27
|
+
col = ((nx + w - EPS) / @tile_width).floor
|
|
28
|
+
return col * @tile_width - w if solid_in_rows?(col, first_row, last_row)
|
|
29
|
+
else
|
|
30
|
+
col = (nx / @tile_width).floor
|
|
31
|
+
return (col + 1) * @tile_width if solid_in_rows?(col, first_row, last_row)
|
|
32
|
+
end
|
|
33
|
+
nx
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def resolve_y(x, y, w, h, dy)
|
|
37
|
+
ny = y + dy
|
|
38
|
+
return ny if dy.zero?
|
|
39
|
+
|
|
40
|
+
first_col = (x / @tile_width).floor
|
|
41
|
+
last_col = ((x + w - EPS) / @tile_width).floor
|
|
42
|
+
|
|
43
|
+
if dy.positive?
|
|
44
|
+
row = ((ny + h - EPS) / @tile_height).floor
|
|
45
|
+
return row * @tile_height - h if solid_in_cols?(row, first_col, last_col)
|
|
46
|
+
else
|
|
47
|
+
row = (ny / @tile_height).floor
|
|
48
|
+
return (row + 1) * @tile_height if solid_in_cols?(row, first_col, last_col)
|
|
49
|
+
end
|
|
50
|
+
ny
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Plain integer loops rather than (a..b).any? { ... }: the Range literal would
|
|
56
|
+
# allocate on this per-frame collision path.
|
|
57
|
+
def solid_in_rows?(col, first_row, last_row)
|
|
58
|
+
row = first_row
|
|
59
|
+
while row <= last_row
|
|
60
|
+
return true if @solid.call(col, row)
|
|
61
|
+
|
|
62
|
+
row += 1
|
|
63
|
+
end
|
|
64
|
+
false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def solid_in_cols?(row, first_col, last_col)
|
|
68
|
+
col = first_col
|
|
69
|
+
while col <= last_col
|
|
70
|
+
return true if @solid.call(col, row)
|
|
71
|
+
|
|
72
|
+
col += 1
|
|
73
|
+
end
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
require 'base64'
|
|
5
|
+
require 'zlib'
|
|
6
|
+
|
|
7
|
+
require_relative '../util'
|
|
8
|
+
require_relative 'tileset'
|
|
9
|
+
|
|
10
|
+
module RGame
|
|
11
|
+
module Engine
|
|
12
|
+
# An orthogonal tile map parsed from a Tiled .tmx. Holds the per-layer gid
|
|
13
|
+
# arrays and geometry, and answers collision queries via its Tileset.
|
|
14
|
+
#
|
|
15
|
+
# `parse` takes a *string*, so the parsing itself needs no filesystem at all;
|
|
16
|
+
# `load` adds the file plumbing on top — reading the .tmx, following it to the
|
|
17
|
+
# .tsx it names, and working out where the tileset image sits relative to
|
|
18
|
+
# that. Both belong here. What does *not* is the image: a texture is a GPU
|
|
19
|
+
# handle, and the renderer that owns one lives a layer below and may not name
|
|
20
|
+
# this class (see CLAUDE.md, "The rule points both ways"). So `load` hands
|
|
21
|
+
# back a path and stops there.
|
|
22
|
+
class TileMap
|
|
23
|
+
FLIP_MASK = 0x1FFFFFFF # strip Tiled's flip/rotation flags from a gid
|
|
24
|
+
|
|
25
|
+
attr_reader :width, :height, :tile_width, :tile_height,
|
|
26
|
+
:pixel_width, :pixel_height, :tileset_source, :firstgid
|
|
27
|
+
attr_accessor :tileset
|
|
28
|
+
|
|
29
|
+
# Reads a .tmx and everything it points at, returning `[map, image_path]`.
|
|
30
|
+
#
|
|
31
|
+
# Two values rather than one because they are two kinds of thing: the map is
|
|
32
|
+
# the grid, and the path is where its pixels happen to live. Keeping the
|
|
33
|
+
# second off the map means a stand-in map in a spec has one less method to
|
|
34
|
+
# answer, and the renderer's protocol stays "things about the grid".
|
|
35
|
+
#
|
|
36
|
+
# Every path is resolved relative to the file that named it — the .tsx
|
|
37
|
+
# relative to the .tmx, the image relative to the .tsx — which is what Tiled
|
|
38
|
+
# itself writes and what lets a map be moved as a set.
|
|
39
|
+
def self.load(tmx_path)
|
|
40
|
+
map = parse(File.read(tmx_path))
|
|
41
|
+
|
|
42
|
+
tsx_path = File.join(File.dirname(tmx_path), map.tileset_source)
|
|
43
|
+
map.tileset = Tileset.parse(File.read(tsx_path), firstgid: map.firstgid)
|
|
44
|
+
|
|
45
|
+
[map, File.join(File.dirname(tsx_path), map.tileset.image_source)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.parse(tmx_string)
|
|
49
|
+
root = REXML::Document.new(tmx_string).root
|
|
50
|
+
tileset_el = root.elements['tileset']
|
|
51
|
+
|
|
52
|
+
layers = []
|
|
53
|
+
above = []
|
|
54
|
+
root.each_element('layer') do |layer_el|
|
|
55
|
+
raw = Base64.decode64(layer_el.elements['data'].text.strip)
|
|
56
|
+
gids = Zlib::Inflate.inflate(raw).unpack('V*')
|
|
57
|
+
gids.map! { |g| g & FLIP_MASK }
|
|
58
|
+
layers << gids
|
|
59
|
+
above << layer_flag?(layer_el, 'above')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
new(
|
|
63
|
+
width: root.attributes['width'].to_i,
|
|
64
|
+
height: root.attributes['height'].to_i,
|
|
65
|
+
tile_width: root.attributes['tilewidth'].to_i,
|
|
66
|
+
tile_height: root.attributes['tileheight'].to_i,
|
|
67
|
+
tileset_source: tileset_el.attributes['source'],
|
|
68
|
+
firstgid: tileset_el.attributes['firstgid'].to_i,
|
|
69
|
+
layers: layers,
|
|
70
|
+
above: above
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# True if a layer carries a Tiled bool custom property `name` set to "true".
|
|
75
|
+
def self.layer_flag?(layer_el, name)
|
|
76
|
+
props = layer_el.elements['properties']
|
|
77
|
+
return false unless props
|
|
78
|
+
|
|
79
|
+
props.each_element('property') do |property|
|
|
80
|
+
return property.attributes['value'] == 'true' if property.attributes['name'] == name
|
|
81
|
+
end
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def initialize(width:, height:, tile_width:, tile_height:, tileset_source:, firstgid:, layers:, above: [])
|
|
86
|
+
@width = width
|
|
87
|
+
@height = height
|
|
88
|
+
@tile_width = tile_width
|
|
89
|
+
@tile_height = tile_height
|
|
90
|
+
@pixel_width = width * tile_width
|
|
91
|
+
@pixel_height = height * tile_height
|
|
92
|
+
@tileset_source = tileset_source
|
|
93
|
+
@firstgid = firstgid
|
|
94
|
+
@above = above
|
|
95
|
+
@tileset = nil
|
|
96
|
+
@tiles = build_tiles(layers)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def layer_count
|
|
100
|
+
@tiles.depth
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Whether a layer is drawn *above* the actors (a tree canopy, roof, etc.) rather
|
|
104
|
+
# than beneath them. Marked in Tiled with a bool layer property `above`; layers
|
|
105
|
+
# without it default to below. Purely a rendering distinction (collision still
|
|
106
|
+
# considers every layer).
|
|
107
|
+
def above_layer?(index)
|
|
108
|
+
@above[index] || false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def in_bounds?(col, row)
|
|
112
|
+
col >= 0 && row >= 0 && col < @width && row < @height
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def gid(layer_index, col, row)
|
|
116
|
+
return 0 unless in_bounds?(col, row)
|
|
117
|
+
|
|
118
|
+
@tiles[col, row, layer_index]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Solid if any layer has a solid tile at (col, row). Out of bounds is not solid
|
|
122
|
+
# — the camera/bounds clamp keeps the player inside the map.
|
|
123
|
+
def solid_tile?(col, row)
|
|
124
|
+
return false unless in_bounds?(col, row)
|
|
125
|
+
|
|
126
|
+
@tiles.depth.times { |layer| return true if @tileset.solid?(@tiles[col, row, layer]) }
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def solid_at?(world_x, world_y)
|
|
131
|
+
solid_tile?((world_x / @tile_width).floor, (world_y / @tile_height).floor)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
# Pack the parsed per-layer gid rows into one flat Tensor ([col, row, layer]).
|
|
137
|
+
def build_tiles(layers)
|
|
138
|
+
tiles = Util::Tensor.new(@width, @height, layers.length, initial: 0)
|
|
139
|
+
layers.each_with_index do |gids, layer|
|
|
140
|
+
@height.times do |row|
|
|
141
|
+
base = row * @width
|
|
142
|
+
@width.times { |col| tiles[col, row, layer] = gids[base + col] }
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
tiles
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rexml/document'
|
|
4
|
+
|
|
5
|
+
module RGame
|
|
6
|
+
module Engine
|
|
7
|
+
# Tile-type data parsed from a Tiled .tsx (pure; no graphics). Knows tile geometry,
|
|
8
|
+
# sheet columns, per-tile animations, and which tiles are solid. Solidity is
|
|
9
|
+
# *baked into the asset*: a tile is solid when it carries a Tiled collision shape
|
|
10
|
+
# (a per-tile `<objectgroup>` with at least one object), authored in Tiled's
|
|
11
|
+
# collision editor — so the map owns its collision, not the engine. `solid_ids`
|
|
12
|
+
# stays writable for games that want to override or augment it.
|
|
13
|
+
# Animation frames resolve from elapsed time so the renderer stays a thin layer.
|
|
14
|
+
class Tileset
|
|
15
|
+
Frame = Struct.new(:tile_id, :duration)
|
|
16
|
+
|
|
17
|
+
attr_reader :firstgid, :columns, :tile_width, :tile_height, :image_source, :animations
|
|
18
|
+
attr_accessor :solid_ids # Set of *local* tile ids treated as solid
|
|
19
|
+
|
|
20
|
+
def self.parse(tsx_string, firstgid:)
|
|
21
|
+
root = REXML::Document.new(tsx_string).root
|
|
22
|
+
|
|
23
|
+
animations = {}
|
|
24
|
+
solid = Set.new
|
|
25
|
+
root.each_element('tile') do |tile_el|
|
|
26
|
+
id = tile_el.attributes['id'].to_i
|
|
27
|
+
animations[id] = parse_animation(tile_el)
|
|
28
|
+
solid << id if collision_shape?(tile_el)
|
|
29
|
+
end
|
|
30
|
+
animations.compact!
|
|
31
|
+
|
|
32
|
+
new(
|
|
33
|
+
firstgid: firstgid,
|
|
34
|
+
columns: root.attributes['columns'].to_i,
|
|
35
|
+
tile_width: root.attributes['tilewidth'].to_i,
|
|
36
|
+
tile_height: root.attributes['tileheight'].to_i,
|
|
37
|
+
image_source: root.elements['image'].attributes['source'],
|
|
38
|
+
animations: animations,
|
|
39
|
+
solid_ids: solid
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Frames for an animated tile, or nil (compacted away) for a static one.
|
|
44
|
+
def self.parse_animation(tile_el)
|
|
45
|
+
anim = tile_el.elements['animation']
|
|
46
|
+
return unless anim
|
|
47
|
+
|
|
48
|
+
anim.get_elements('frame').map do |f|
|
|
49
|
+
Frame.new(f.attributes['tileid'].to_i, f.attributes['duration'].to_i)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# A tile is solid if it carries a Tiled collision shape — an `<objectgroup>`
|
|
54
|
+
# with at least one object (the per-tile collision editor's output).
|
|
55
|
+
def self.collision_shape?(tile_el)
|
|
56
|
+
group = tile_el.elements['objectgroup']
|
|
57
|
+
!group.nil? && !group.elements['object'].nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def initialize(firstgid:, columns:, tile_width:, tile_height:, image_source:, animations:, solid_ids: Set.new)
|
|
61
|
+
@firstgid = firstgid
|
|
62
|
+
@columns = columns
|
|
63
|
+
@tile_width = tile_width
|
|
64
|
+
@tile_height = tile_height
|
|
65
|
+
@image_source = image_source
|
|
66
|
+
@animations = animations
|
|
67
|
+
@solid_ids = solid_ids
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def animated_ids
|
|
71
|
+
@animations.keys
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def local_id(gid)
|
|
75
|
+
gid - @firstgid
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def solid?(gid)
|
|
79
|
+
return false if gid.zero?
|
|
80
|
+
|
|
81
|
+
@solid_ids.include?(local_id(gid))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# The local tile id to draw for `local` at time `ms`, following its animation
|
|
85
|
+
# (or `local` itself if the tile isn't animated).
|
|
86
|
+
def frame_local_id(local, ms)
|
|
87
|
+
frames = @animations[local]
|
|
88
|
+
return local unless frames
|
|
89
|
+
|
|
90
|
+
total = frames.sum(&:duration)
|
|
91
|
+
t = ms % total
|
|
92
|
+
frames.each do |frame|
|
|
93
|
+
return frame.tile_id if t < frame.duration
|
|
94
|
+
|
|
95
|
+
t -= frame.duration
|
|
96
|
+
end
|
|
97
|
+
frames.last.tile_id
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RGame
|
|
4
|
+
module Engine
|
|
5
|
+
# A repeating interval timer for periodic events that aren't driven by input — a
|
|
6
|
+
# spawner emitting an enemy every N seconds, a tower's fire rate, a wave clock.
|
|
7
|
+
#
|
|
8
|
+
# It only accumulates time; the owner decides what each elapsed interval means. That
|
|
9
|
+
# split lets one primitive serve both styles: "act automatically" (consume every ready
|
|
10
|
+
# interval) and "stay loaded until conditions allow" (check `ready?`, but `consume`
|
|
11
|
+
# only when actually acting — so a tower with no target keeps its shot ready instead of
|
|
12
|
+
# wasting it). Pure and allocation-free, so it ticks on the per-frame path.
|
|
13
|
+
#
|
|
14
|
+
# timer = Engine::Timer.new(0.8)
|
|
15
|
+
# timer.update(dt)
|
|
16
|
+
# if timer.ready? # at least one whole interval has passed
|
|
17
|
+
# timer.consume # deduct it (remainder carries forward, so timing won't drift)
|
|
18
|
+
# spawn_enemy
|
|
19
|
+
# end
|
|
20
|
+
class Timer
|
|
21
|
+
attr_accessor :interval
|
|
22
|
+
|
|
23
|
+
def initialize(interval)
|
|
24
|
+
@interval = interval
|
|
25
|
+
@elapsed = 0.0
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Advance by one timestep. Returns self so callers can chain.
|
|
29
|
+
def update(dt)
|
|
30
|
+
@elapsed += dt
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Has at least one whole interval accumulated since the last consume?
|
|
35
|
+
def ready? = @elapsed >= @interval
|
|
36
|
+
|
|
37
|
+
# Deduct one interval after acting on a ready timer. Carries the remainder forward
|
|
38
|
+
# (rather than zeroing) so a long-running cadence doesn't drift. Returns self.
|
|
39
|
+
def consume
|
|
40
|
+
@elapsed -= @interval
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Drop any accumulated time — e.g. after retuning the interval. Returns self.
|
|
45
|
+
def reset
|
|
46
|
+
@elapsed = 0.0
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/rgame/engine.rb
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# RGame::Engine — the scene graph a game is written against: nodes, components,
|
|
4
|
+
# signals, scenes, collision, tile maps, pathing, pooling.
|
|
5
|
+
#
|
|
6
|
+
# Pure Ruby, and it links nothing. It may hold `RGame::Util` values but may not
|
|
7
|
+
# name `RGame::Core` at all, reaching the platform only through objects handed
|
|
8
|
+
# to it — a `renderer` passed into `draw`, an audio server behind
|
|
9
|
+
# `AudioDirector`. See CLAUDE.md, "The three layers, and who may talk to whom".
|
|
10
|
+
#
|
|
11
|
+
# That is what lets the whole layer be specified with no window, no GPU and no
|
|
12
|
+
# clock: `spec/` drives `update(dt)` directly and can run a simulated hour in
|
|
13
|
+
# milliseconds.
|
|
14
|
+
#
|
|
15
|
+
# Loaded by `require "rgame"` along with Util, since between them they are
|
|
16
|
+
# everything that runs without a window. Requirable on its own too — hence the
|
|
17
|
+
# `util` require below rather than relying on `rgame.rb` to have got there
|
|
18
|
+
# first: `TileMap` holds a `Util::Tensor`, which is exactly the dependency the
|
|
19
|
+
# three-layer rule allows and this file therefore has to declare.
|
|
20
|
+
require_relative 'util'
|
|
21
|
+
|
|
22
|
+
require_relative 'engine/matrix'
|
|
23
|
+
require_relative 'engine/signal'
|
|
24
|
+
require_relative 'engine/node2d'
|
|
25
|
+
require_relative 'engine/component'
|
|
26
|
+
require_relative 'engine/animation_set'
|
|
27
|
+
require_relative 'engine/animator'
|
|
28
|
+
require_relative 'engine/camera'
|
|
29
|
+
require_relative 'engine/camera_view'
|
|
30
|
+
require_relative 'engine/path'
|
|
31
|
+
require_relative 'engine/timer'
|
|
32
|
+
require_relative 'engine/tileset'
|
|
33
|
+
require_relative 'engine/tile_map'
|
|
34
|
+
require_relative 'engine/tile_collision'
|
|
35
|
+
require_relative 'engine/collision_box'
|
|
36
|
+
require_relative 'engine/collision_system'
|
|
37
|
+
require_relative 'engine/circle_collider'
|
|
38
|
+
require_relative 'engine/spatial_hash'
|
|
39
|
+
require_relative 'engine/pool'
|
|
40
|
+
require_relative 'engine/resettable'
|
|
41
|
+
require_relative 'engine/audio_bus'
|
|
42
|
+
require_relative 'engine/audio_director'
|
|
43
|
+
require_relative 'engine/i18n'
|
|
44
|
+
require_relative 'engine/cached_label'
|
|
45
|
+
require_relative 'engine/debug_overlay'
|
|
46
|
+
require_relative 'engine/body'
|
|
47
|
+
require_relative 'engine/actor'
|
|
48
|
+
require_relative 'engine/input/actions'
|
|
49
|
+
require_relative 'engine/input/action_mapper'
|
|
50
|
+
require_relative 'engine/input/player_controller'
|
|
51
|
+
require_relative 'engine/scene/scene_stack'
|
|
52
|
+
require_relative 'engine/components/velocity'
|
|
53
|
+
require_relative 'engine/components/pool'
|
|
54
|
+
require_relative 'engine/components/path_follow'
|
|
55
|
+
require_relative 'engine/components/timer'
|
|
56
|
+
require_relative 'engine/components/screen_wrap'
|
|
57
|
+
require_relative 'engine/components/despawn_offscreen'
|
|
58
|
+
require_relative 'engine/components/circle_collider'
|
|
59
|
+
require_relative 'engine/components/collision_world'
|
|
60
|
+
require_relative 'engine/components/targeting'
|
|
61
|
+
require_relative 'engine/components/sprite'
|
|
62
|
+
require_relative 'engine/components/thrust_controller'
|
|
63
|
+
require_relative 'engine/components/action_trigger'
|
|
64
|
+
require_relative 'engine/components/tile_world'
|
|
65
|
+
require_relative 'engine/components/character_body'
|
|
66
|
+
require_relative 'engine/components/player_controller'
|
|
67
|
+
require_relative 'engine/components/wander_controller'
|
|
68
|
+
require_relative 'engine/components/animated_sprite'
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Digitized data copyright (c) 2010 Google Corporation
|
|
2
|
+
with Reserved Font Arimo, Tinos and Cousine.
|
|
3
|
+
Copyright (c) 2012 Red Hat, Inc.
|
|
4
|
+
with Reserved Font Name Liberation.
|
|
5
|
+
|
|
6
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
7
|
+
Version 1.1.
|
|
8
|
+
|
|
9
|
+
This license is copied below, and is also available with a FAQ at:
|
|
10
|
+
http://scripts.sil.org/OFL
|
|
11
|
+
|
|
12
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
13
|
+
|
|
14
|
+
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
|
|
15
|
+
worldwide development of collaborative font projects, to support the font
|
|
16
|
+
creation efforts of academic and linguistic communities, and to provide
|
|
17
|
+
a free and open framework in which fonts may be shared and improved in
|
|
18
|
+
partnership with others.
|
|
19
|
+
|
|
20
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
21
|
+
redistributed freely as long as they are not sold by themselves.
|
|
22
|
+
The fonts, including any derivative works, can be bundled, embedded,
|
|
23
|
+
redistributed and/or sold with any software provided that any reserved
|
|
24
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
25
|
+
however, cannot be released under any other type of license. The
|
|
26
|
+
requirement for fonts to remain under this license does not apply to
|
|
27
|
+
any document created using the fonts or their derivatives.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
DEFINITIONS
|
|
32
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
33
|
+
Holder(s) under this license and clearly marked as such.
|
|
34
|
+
This may include source files, build scripts and documentation.
|
|
35
|
+
|
|
36
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
37
|
+
copyright statement(s).
|
|
38
|
+
|
|
39
|
+
"Original Version" refers to the collection of Font Software components
|
|
40
|
+
as distributed by the Copyright Holder(s).
|
|
41
|
+
|
|
42
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
43
|
+
or substituting ? in part or in whole ?
|
|
44
|
+
any of the components of the Original Version, by changing formats or
|
|
45
|
+
by porting the Font Software to a new environment.
|
|
46
|
+
|
|
47
|
+
"Author" refers to any designer, engineer, programmer, technical writer
|
|
48
|
+
or other person who contributed to the Font Software.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
PERMISSION & CONDITIONS
|
|
52
|
+
|
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
54
|
+
copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
55
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
56
|
+
Software, subject to the following conditions:
|
|
57
|
+
|
|
58
|
+
1) Neither the Font Software nor any of its individual components,in
|
|
59
|
+
Original or Modified Versions, may be sold by itself.
|
|
60
|
+
|
|
61
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
62
|
+
redistributed and/or sold with any software, provided that each copy
|
|
63
|
+
contains the above copyright notice and this license. These can be
|
|
64
|
+
included either as stand-alone text files, human-readable headers or
|
|
65
|
+
in the appropriate machine-readable metadata fields within text or
|
|
66
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
67
|
+
|
|
68
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
69
|
+
Name(s) unless explicit written permission is granted by the
|
|
70
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
71
|
+
primary font name as presented to the users.
|
|
72
|
+
|
|
73
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
74
|
+
Software shall not be used to promote, endorse or advertise any
|
|
75
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
76
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
77
|
+
permission.
|
|
78
|
+
|
|
79
|
+
5) The Font Software, modified or unmodified, in part or in whole, must
|
|
80
|
+
be distributed entirely under this license, and must not be distributed
|
|
81
|
+
under any other license. The requirement for fonts to remain under
|
|
82
|
+
this license does not apply to any document created using the Font
|
|
83
|
+
Software.
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
TERMINATION
|
|
88
|
+
This license becomes null and void if any of the above conditions are not met.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
DISCLAIMER
|
|
93
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
94
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
95
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
96
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
97
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
98
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
99
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
100
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
|
101
|
+
DEALINGS IN THE FONT SOFTWARE.
|
|
102
|
+
|