box2d-ruby 1.0.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.txt +21 -0
- data/README.md +336 -0
- data/examples/contact_events.rb +22 -0
- data/examples/debug_draw.rb +15 -0
- data/examples/falling_ball.rb +19 -0
- data/examples/rugl_debug_draw.rb +68 -0
- data/examples/stagecraft_debug_draw.rb +64 -0
- data/examples/support/box2d_debug_lines.rb +125 -0
- data/ext/box2d/CMakeLists.txt +35 -0
- data/ext/box2d/extconf.rb +42 -0
- data/ext/box2d/native.c +9 -0
- data/ext/box2d/vendor/box2d/LICENSE +21 -0
- data/ext/box2d/vendor/box2d/VERSION +1 -0
- data/ext/box2d/vendor/box2d/include/box2d/base.h +131 -0
- data/ext/box2d/vendor/box2d/include/box2d/box2d.h +1222 -0
- data/ext/box2d/vendor/box2d/include/box2d/collision.h +830 -0
- data/ext/box2d/vendor/box2d/include/box2d/id.h +144 -0
- data/ext/box2d/vendor/box2d/include/box2d/math_functions.h +761 -0
- data/ext/box2d/vendor/box2d/include/box2d/types.h +1457 -0
- data/ext/box2d/vendor/box2d/src/CMakeLists.txt +223 -0
- data/ext/box2d/vendor/box2d/src/aabb.c +132 -0
- data/ext/box2d/vendor/box2d/src/aabb.h +56 -0
- data/ext/box2d/vendor/box2d/src/arena_allocator.c +112 -0
- data/ext/box2d/vendor/box2d/src/arena_allocator.h +48 -0
- data/ext/box2d/vendor/box2d/src/array.c +8 -0
- data/ext/box2d/vendor/box2d/src/array.h +179 -0
- data/ext/box2d/vendor/box2d/src/atomic.h +79 -0
- data/ext/box2d/vendor/box2d/src/bitset.c +67 -0
- data/ext/box2d/vendor/box2d/src/bitset.h +65 -0
- data/ext/box2d/vendor/box2d/src/body.c +1884 -0
- data/ext/box2d/vendor/box2d/src/body.h +194 -0
- data/ext/box2d/vendor/box2d/src/box2d.natvis +41 -0
- data/ext/box2d/vendor/box2d/src/broad_phase.c +524 -0
- data/ext/box2d/vendor/box2d/src/broad_phase.h +83 -0
- data/ext/box2d/vendor/box2d/src/constants.h +54 -0
- data/ext/box2d/vendor/box2d/src/constraint_graph.c +322 -0
- data/ext/box2d/vendor/box2d/src/constraint_graph.h +58 -0
- data/ext/box2d/vendor/box2d/src/contact.c +650 -0
- data/ext/box2d/vendor/box2d/src/contact.h +148 -0
- data/ext/box2d/vendor/box2d/src/contact_solver.c +2120 -0
- data/ext/box2d/vendor/box2d/src/contact_solver.h +54 -0
- data/ext/box2d/vendor/box2d/src/core.c +178 -0
- data/ext/box2d/vendor/box2d/src/core.h +149 -0
- data/ext/box2d/vendor/box2d/src/ctz.h +112 -0
- data/ext/box2d/vendor/box2d/src/distance.c +1415 -0
- data/ext/box2d/vendor/box2d/src/distance_joint.c +556 -0
- data/ext/box2d/vendor/box2d/src/dynamic_tree.c +1989 -0
- data/ext/box2d/vendor/box2d/src/geometry.c +1028 -0
- data/ext/box2d/vendor/box2d/src/hull.c +328 -0
- data/ext/box2d/vendor/box2d/src/id_pool.c +79 -0
- data/ext/box2d/vendor/box2d/src/id_pool.h +35 -0
- data/ext/box2d/vendor/box2d/src/island.c +977 -0
- data/ext/box2d/vendor/box2d/src/island.h +89 -0
- data/ext/box2d/vendor/box2d/src/joint.c +1272 -0
- data/ext/box2d/vendor/box2d/src/joint.h +335 -0
- data/ext/box2d/vendor/box2d/src/manifold.c +1726 -0
- data/ext/box2d/vendor/box2d/src/math_functions.c +159 -0
- data/ext/box2d/vendor/box2d/src/motor_joint.c +283 -0
- data/ext/box2d/vendor/box2d/src/mouse_joint.c +214 -0
- data/ext/box2d/vendor/box2d/src/mover.c +73 -0
- data/ext/box2d/vendor/box2d/src/prismatic_joint.c +656 -0
- data/ext/box2d/vendor/box2d/src/revolute_joint.c +534 -0
- data/ext/box2d/vendor/box2d/src/sensor.c +389 -0
- data/ext/box2d/vendor/box2d/src/sensor.h +36 -0
- data/ext/box2d/vendor/box2d/src/shape.c +1714 -0
- data/ext/box2d/vendor/box2d/src/shape.h +123 -0
- data/ext/box2d/vendor/box2d/src/solver.c +2038 -0
- data/ext/box2d/vendor/box2d/src/solver.h +155 -0
- data/ext/box2d/vendor/box2d/src/solver_set.c +613 -0
- data/ext/box2d/vendor/box2d/src/solver_set.h +57 -0
- data/ext/box2d/vendor/box2d/src/table.c +238 -0
- data/ext/box2d/vendor/box2d/src/table.h +37 -0
- data/ext/box2d/vendor/box2d/src/timer.c +185 -0
- data/ext/box2d/vendor/box2d/src/types.c +151 -0
- data/ext/box2d/vendor/box2d/src/weld_joint.c +310 -0
- data/ext/box2d/vendor/box2d/src/wheel_joint.c +551 -0
- data/ext/box2d/vendor/box2d/src/world.c +3301 -0
- data/ext/box2d/vendor/box2d/src/world.h +192 -0
- data/generator/generate.rb +316 -0
- data/generator/layout_probe.c +507 -0
- data/generator/verify_layouts.rb +32 -0
- data/lib/box2d/body.rb +172 -0
- data/lib/box2d/body_definition.rb +38 -0
- data/lib/box2d/body_shapes.rb +135 -0
- data/lib/box2d/chain.rb +61 -0
- data/lib/box2d/debug_draw.rb +118 -0
- data/lib/box2d/events.rb +103 -0
- data/lib/box2d/fixed_stepper.rb +39 -0
- data/lib/box2d/handle.rb +46 -0
- data/lib/box2d/hit.rb +5 -0
- data/lib/box2d/joint.rb +197 -0
- data/lib/box2d/native.rb +1462 -0
- data/lib/box2d/native_loader.rb +45 -0
- data/lib/box2d/pixel_scale.rb +29 -0
- data/lib/box2d/shape.rb +109 -0
- data/lib/box2d/shape_definition.rb +44 -0
- data/lib/box2d/value_conversion.rb +80 -0
- data/lib/box2d/version.rb +7 -0
- data/lib/box2d/world.rb +135 -0
- data/lib/box2d/world_joints.rb +156 -0
- data/lib/box2d/world_queries.rb +122 -0
- data/lib/box2d/world_registry.rb +95 -0
- data/lib/box2d.rb +31 -0
- data/script/build_platform_gem.rb +24 -0
- data/script/deterministic_scene.rb +56 -0
- data/script/update_deterministic_snapshot.rb +11 -0
- data/script/verify_platform_gem.rb +24 -0
- metadata +164 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b27be6541a6fc5670786520d3679496edadc41237328ff6f3d5f7c561d19a4d8
|
|
4
|
+
data.tar.gz: 8f5162a1673fc3c797507b33ef51b48004e50bbd546c8e7b44377e6bf33ebbf5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1d29cb822f55e9ac3d222d3745c3e12ae323287f8098eb80752576d7cf090c0da8c4340d731491c66114d45bbfd8f0cff36223bf62b8dc2c84eb32e3c21f4439
|
|
7
|
+
data.tar.gz: d2972da3d6bd61af13f41619d3f6f9c2144718c53b1a04a1beac8233e76b8effe98fe49be0bf3514908a40d44b898d0361f5b561911655c36ae3a421d5c4f4f6
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ydah
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# box2d-ruby
|
|
2
|
+
|
|
3
|
+
`box2d-ruby` is an idiomatic Ruby binding for the Box2D 3 C API. It ships a
|
|
4
|
+
complete generated FFI layer and a memory-safe Ruby API for worlds, bodies,
|
|
5
|
+
shapes, events, queries, joints, and debug drawing.
|
|
6
|
+
|
|
7
|
+
The gem currently vendors Box2D 3.1.0.
|
|
8
|
+
|
|
9
|
+
## API stability
|
|
10
|
+
|
|
11
|
+
Version 1.0 freezes the documented Ruby API. Releases in the 1.x series follow
|
|
12
|
+
semantic versioning: existing public methods and their documented behavior will
|
|
13
|
+
remain compatible, and breaking changes require a new major version. The
|
|
14
|
+
generated `Box2D::Native` layer mirrors the vendored Box2D headers exactly and
|
|
15
|
+
is regenerated when that native dependency is intentionally upgraded.
|
|
16
|
+
|
|
17
|
+
## Units and coordinates
|
|
18
|
+
|
|
19
|
+
Box2D uses SI units: meters, kilograms, seconds, and radians. It has no built-in
|
|
20
|
+
pixel scale and this binding uses a y-up coordinate system.
|
|
21
|
+
|
|
22
|
+
Do not pass screen pixels directly to physics APIs. A common game scale is
|
|
23
|
+
32–64 pixels per meter:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
scale = Box2D::PixelScale.new(64)
|
|
27
|
+
physics_position = scale.vector_to_meters([320, 128])
|
|
28
|
+
screen_position = scale.vector_to_pixels(physics_position)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Add the gem to your bundle:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
bundle add box2d-ruby
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Or install it directly:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
gem install box2d-ruby
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Platform gems contain a precompiled Box2D library. The source gem falls back to
|
|
46
|
+
building the vendored C17 source with CMake and requires CMake 3.16 or newer, a
|
|
47
|
+
C compiler, `make` (or another CMake build backend), and Ruby development
|
|
48
|
+
headers.
|
|
49
|
+
|
|
50
|
+
The runtime dependency is `ffi`. `larb` is optional: if it is already loaded,
|
|
51
|
+
vector-returning APIs produce `Larb::Vec2`; otherwise they return two-element
|
|
52
|
+
arrays.
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require "box2d"
|
|
58
|
+
|
|
59
|
+
world = Box2D::World.new(gravity: [0, -9.8]) do |configuration|
|
|
60
|
+
configuration.substeps = 4
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
world.create_body(type: :static, position: [0, -10]) do |body|
|
|
64
|
+
body.box(50, 10)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
ball = world.create_body(
|
|
68
|
+
type: :dynamic,
|
|
69
|
+
position: [0, 4],
|
|
70
|
+
user_data: {kind: :ball}
|
|
71
|
+
) do |body|
|
|
72
|
+
body.circle(
|
|
73
|
+
radius: 0.5,
|
|
74
|
+
density: 1.0,
|
|
75
|
+
friction: 0.3,
|
|
76
|
+
restitution: 0.6
|
|
77
|
+
)
|
|
78
|
+
body.bullet = true
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
60.times { world.step(1.0 / 60) }
|
|
82
|
+
|
|
83
|
+
p ball.position
|
|
84
|
+
ball.linear_velocity = [3, 0]
|
|
85
|
+
ball.apply_impulse([0, 5])
|
|
86
|
+
|
|
87
|
+
world.destroy
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`World#step` releases Ruby's GVL while Box2D is solving. Accessing the same
|
|
91
|
+
world from another Ruby thread during a step is rejected with
|
|
92
|
+
`Box2D::ReentrantStepError`.
|
|
93
|
+
|
|
94
|
+
## Fixed timestep
|
|
95
|
+
|
|
96
|
+
`FixedStepper` implements an accumulator with a substep cap:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
stepper = Box2D::FixedStepper.new(hz: 60, max_substeps: 5)
|
|
100
|
+
|
|
101
|
+
app.run do |delta_time|
|
|
102
|
+
alpha = stepper.advance(delta_time) { |step| world.step(step) }
|
|
103
|
+
render(alpha)
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`advance` returns interpolation alpha in `[0, 1)`. Excess accumulated time is
|
|
108
|
+
discarded at `max_substeps` to prevent a spiral of death.
|
|
109
|
+
|
|
110
|
+
## Shapes
|
|
111
|
+
|
|
112
|
+
A body block is a shape builder:
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
body = world.create_body(type: :dynamic, position: [0, 3]) do |builder|
|
|
116
|
+
builder.box(half_width, half_height, center: [0, 0], angle: 0)
|
|
117
|
+
builder.circle(radius: 0.5, center: [0, 0])
|
|
118
|
+
builder.capsule([-1, 0], [1, 0], radius: 0.25)
|
|
119
|
+
builder.polygon([[0, 0], [1, 0], [0, 1]])
|
|
120
|
+
builder.segment([-1, 0], [1, 0])
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Every shape builder accepts:
|
|
125
|
+
|
|
126
|
+
- `density`, `friction`, `restitution`, and `rolling_resistance`
|
|
127
|
+
- `sensor: true`
|
|
128
|
+
- `filter: {category:, mask:, group:}`
|
|
129
|
+
- `sensor_events`, `contact_events`, and `hit_events`
|
|
130
|
+
- Ruby-owned `user_data`
|
|
131
|
+
|
|
132
|
+
Chains are separate native objects:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
chain = ground.chain(
|
|
136
|
+
[[-4, 0], [-2, 1], [2, 1], [4, 0]],
|
|
137
|
+
loop: false,
|
|
138
|
+
friction: 0.8
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Box2D chains require at least four vertices.
|
|
143
|
+
|
|
144
|
+
## Events
|
|
145
|
+
|
|
146
|
+
Contact and sensor event buffers are transient in Box2D. `World#step` copies
|
|
147
|
+
them into immutable Ruby values before returning:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
world.step(1.0 / 60)
|
|
151
|
+
|
|
152
|
+
world.events.begin_contacts.each do |contact|
|
|
153
|
+
a = contact.shape_a.body.user_data
|
|
154
|
+
b = contact.shape_b.body.user_data
|
|
155
|
+
p [a, b, contact.manifold]
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
world.events.end_contacts.each { |contact| p contact }
|
|
159
|
+
world.events.hits.each { |hit| p hit.approach_speed }
|
|
160
|
+
world.events.sensor_begins.each { |event| p [event.sensor, event.visitor] }
|
|
161
|
+
world.events.sensor_ends.each { |event| p [event.sensor, event.visitor] }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Queries
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
hit = world.raycast([0, 10], [0, -10])
|
|
168
|
+
if hit
|
|
169
|
+
p [hit.shape, hit.point, hit.normal, hit.fraction]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
world.overlap_aabb([-2, -2], [2, 2]) do |shape|
|
|
173
|
+
p shape
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
world.overlap_circle([0, 1], radius: 0.5) { |shape| p shape }
|
|
177
|
+
world.overlap_capsule([-1, 0], [1, 0], radius: 0.25) { |shape| p shape }
|
|
178
|
+
world.overlap_polygon(
|
|
179
|
+
[[-1, -1], [1, -1], [1, 1], [-1, 1]],
|
|
180
|
+
position: [4, 0],
|
|
181
|
+
angle: Math::PI / 4
|
|
182
|
+
) { |shape| p shape }
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
All query methods accept `filter: {category:, mask:}`. Without a block, overlap
|
|
186
|
+
queries return an `Enumerator`. Returning `false` from the block stops the
|
|
187
|
+
query.
|
|
188
|
+
|
|
189
|
+
## Joints
|
|
190
|
+
|
|
191
|
+
The high-level API supports revolute, prismatic, distance, mouse, and weld
|
|
192
|
+
joints:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
joint = world.create_revolute_joint(
|
|
196
|
+
body_a: arm,
|
|
197
|
+
body_b: hand,
|
|
198
|
+
anchor: [1, 2],
|
|
199
|
+
limits: (-0.5..0.5),
|
|
200
|
+
motor: {speed: 2.0, max_torque: 10.0}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
joint.motor_speed = -2.0
|
|
204
|
+
joint.destroy
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Equivalent constructors are:
|
|
208
|
+
|
|
209
|
+
- `create_prismatic_joint`
|
|
210
|
+
- `create_distance_joint`
|
|
211
|
+
- `create_mouse_joint`
|
|
212
|
+
- `create_weld_joint`
|
|
213
|
+
|
|
214
|
+
All anchors supplied to constructors are world coordinates and are converted to
|
|
215
|
+
body-local coordinates internally.
|
|
216
|
+
|
|
217
|
+
## Debug drawing
|
|
218
|
+
|
|
219
|
+
Debug drawing converts native callbacks into arrays suitable for Ruby pattern
|
|
220
|
+
matching:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
world.debug_draw(flags: [:shapes, :joints, :aabbs]) do |command|
|
|
224
|
+
case command
|
|
225
|
+
in [:polygon, points, color]
|
|
226
|
+
draw_outline(points, color)
|
|
227
|
+
in [:solid_circle, transform, radius, color]
|
|
228
|
+
draw_circle(transform[:position], radius, color)
|
|
229
|
+
in [:segment, point1, point2, color]
|
|
230
|
+
draw_line(point1, point2, color)
|
|
231
|
+
else
|
|
232
|
+
# Other commands include solid_polygon, solid_capsule, transform,
|
|
233
|
+
# point, circle, and string.
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The owning world retains every `FFI::Function` used by the draw session, so GC
|
|
239
|
+
cannot collect a callback while native code is using it.
|
|
240
|
+
|
|
241
|
+
Runnable line-renderer examples are included for both supported rendering
|
|
242
|
+
styles:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
gem install rugl glfw
|
|
246
|
+
ruby examples/rugl_debug_draw.rb
|
|
247
|
+
|
|
248
|
+
gem install stagecraft
|
|
249
|
+
ruby examples/stagecraft_debug_draw.rb
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Both adapters consume the same `Box2DDebugLines` converter in
|
|
253
|
+
`examples/support`, which turns debug callbacks into colored line-list
|
|
254
|
+
vertices.
|
|
255
|
+
|
|
256
|
+
## Verification
|
|
257
|
+
|
|
258
|
+
`bundle exec rake` builds the CMake extension, checks all generated FFI struct
|
|
259
|
+
layouts, and runs the complete spec suite. Deterministic 100-body fixtures live
|
|
260
|
+
under `spec/snapshots/100_boxes` for every supported native-gem target. Update
|
|
261
|
+
the fixture for the current platform after an intentional solver change with:
|
|
262
|
+
|
|
263
|
+
```sh
|
|
264
|
+
bundle exec rake snapshots:update
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`bundle exec rake native:verify_package` additionally builds a platform gem,
|
|
268
|
+
extracts its packaged library, and reruns the full suite against that exact
|
|
269
|
+
artifact. Release CI performs this check for all five supported targets before
|
|
270
|
+
uploading any gem.
|
|
271
|
+
|
|
272
|
+
## Ownership and destruction
|
|
273
|
+
|
|
274
|
+
`World` owns every native body, shape, chain, and joint. Ruby wrappers are
|
|
275
|
+
non-owning ID values and do not use GC finalizers.
|
|
276
|
+
|
|
277
|
+
Destroy resources explicitly:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
shape.destroy
|
|
281
|
+
body.destroy
|
|
282
|
+
joint.destroy
|
|
283
|
+
world.destroy # destroys all remaining children
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Access through a destroyed handle raises `Box2D::UseAfterDestroyError`. Ruby
|
|
287
|
+
objects assigned as `user_data` are held in world-side registries; Ruby object
|
|
288
|
+
pointers are never written to Box2D's native `void*` fields.
|
|
289
|
+
|
|
290
|
+
## Native API
|
|
291
|
+
|
|
292
|
+
All 409 exported Box2D 3.1.0 functions are available under `Box2D::Native`.
|
|
293
|
+
Structs, enums, and function signatures are generated from the vendored headers:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
version = Box2D::Native.b2GetVersion
|
|
297
|
+
p [version[:major], version[:minor], version[:revision]]
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Set `BOX2D_LIBRARY_PATH` to load a compatible external Box2D shared library
|
|
301
|
+
instead of the bundled extension.
|
|
302
|
+
|
|
303
|
+
## Development
|
|
304
|
+
|
|
305
|
+
Install dependencies and run the complete quality gate:
|
|
306
|
+
|
|
307
|
+
```sh
|
|
308
|
+
bundle install
|
|
309
|
+
bundle exec rake
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The default task:
|
|
313
|
+
|
|
314
|
+
1. builds the vendored native library;
|
|
315
|
+
2. checks that generated FFI declarations match the headers;
|
|
316
|
+
3. compiles a C layout probe and checks all 71 struct sizes, alignments, and
|
|
317
|
+
field offsets;
|
|
318
|
+
4. runs the RSpec suite, including 1000 steps under `GC.stress`.
|
|
319
|
+
|
|
320
|
+
Useful tasks:
|
|
321
|
+
|
|
322
|
+
```sh
|
|
323
|
+
bundle exec rake bindings:generate
|
|
324
|
+
bundle exec rake bindings:check
|
|
325
|
+
bundle exec rake native:compile
|
|
326
|
+
bundle exec rake native:package
|
|
327
|
+
bundle exec rake build
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
See `examples/` for complete runnable scripts.
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
The Ruby binding is available under the MIT License. Vendored Box2D source is
|
|
335
|
+
also MIT licensed; its license is included at
|
|
336
|
+
`ext/box2d/vendor/box2d/LICENSE`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "box2d"
|
|
4
|
+
|
|
5
|
+
world = nil
|
|
6
|
+
begin
|
|
7
|
+
world = Box2D::World.new
|
|
8
|
+
world.create_body(position: [0, -1], user_data: :ground) { |body| body.box(10, 1) }
|
|
9
|
+
world.create_body(type: :dynamic, position: [0, 4], user_data: :ball) do |body|
|
|
10
|
+
body.circle(radius: 0.5)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
120.times do
|
|
14
|
+
world.step(1.0 / 60)
|
|
15
|
+
world.events.begin_contacts.each do |contact|
|
|
16
|
+
names = [contact.shape_a.body.user_data, contact.shape_b.body.user_data]
|
|
17
|
+
puts "contact began: #{names.join(" <-> ")}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
ensure
|
|
21
|
+
world&.destroy if world&.valid?
|
|
22
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "box2d"
|
|
4
|
+
|
|
5
|
+
world = nil
|
|
6
|
+
begin
|
|
7
|
+
world = Box2D::World.new
|
|
8
|
+
world.create_body { |body| body.box(2, 1) }
|
|
9
|
+
|
|
10
|
+
world.debug_draw(flags: [:shapes, :aabbs]) do |command|
|
|
11
|
+
p command
|
|
12
|
+
end
|
|
13
|
+
ensure
|
|
14
|
+
world&.destroy if world&.valid?
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "box2d"
|
|
4
|
+
|
|
5
|
+
world = nil
|
|
6
|
+
begin
|
|
7
|
+
world = Box2D::World.new
|
|
8
|
+
world.create_body(position: [0, -1]) { |body| body.box(10, 1) }
|
|
9
|
+
ball = world.create_body(type: :dynamic, position: [0, 4]) do |body|
|
|
10
|
+
body.circle(radius: 0.5, restitution: 0.6)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
120.times do |frame|
|
|
14
|
+
world.step(1.0 / 60)
|
|
15
|
+
puts format("%3d: x=% .3f y=% .3f", frame, *ball.position)
|
|
16
|
+
end
|
|
17
|
+
ensure
|
|
18
|
+
world&.destroy if world&.valid?
|
|
19
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "box2d"
|
|
4
|
+
require "rugl"
|
|
5
|
+
require_relative "support/box2d_debug_lines"
|
|
6
|
+
|
|
7
|
+
world = nil
|
|
8
|
+
rugl = nil
|
|
9
|
+
at_exit do
|
|
10
|
+
world&.destroy if world&.valid?
|
|
11
|
+
rugl&.destroy unless rugl&.destroyed?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
world = Box2D::World.new
|
|
15
|
+
world.create_body(position: [0, -1]) { |body| body.box(8, 1) }
|
|
16
|
+
10.times do |index|
|
|
17
|
+
world.create_body(type: :dynamic, position: [(index % 5) - 2, 1 + index / 5.0]) do |body|
|
|
18
|
+
body.box(0.4, 0.4)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
rugl = Rugl.create(width: 960, height: 720, title: "Box2D debug draw")
|
|
23
|
+
positions = rugl.buffer(data: [], usage: :stream)
|
|
24
|
+
colors = rugl.buffer(data: [], usage: :stream)
|
|
25
|
+
draw_lines = rugl.command(
|
|
26
|
+
vert: <<~GLSL,
|
|
27
|
+
#version 410 core
|
|
28
|
+
layout(location = 0) in vec2 position;
|
|
29
|
+
layout(location = 1) in vec4 color;
|
|
30
|
+
out vec4 vertexColor;
|
|
31
|
+
void main() {
|
|
32
|
+
gl_Position = vec4(position * vec2(0.1, 0.12) + vec2(0.0, -0.65), 0.0, 1.0);
|
|
33
|
+
vertexColor = color;
|
|
34
|
+
}
|
|
35
|
+
GLSL
|
|
36
|
+
frag: <<~GLSL,
|
|
37
|
+
#version 410 core
|
|
38
|
+
in vec4 vertexColor;
|
|
39
|
+
out vec4 fragColor;
|
|
40
|
+
void main() {
|
|
41
|
+
fragColor = vertexColor;
|
|
42
|
+
}
|
|
43
|
+
GLSL
|
|
44
|
+
attributes: {
|
|
45
|
+
position: {buffer: positions, size: 2},
|
|
46
|
+
color: {buffer: colors, size: 4}
|
|
47
|
+
},
|
|
48
|
+
count: Rugl.prop(:count),
|
|
49
|
+
primitive: :lines,
|
|
50
|
+
line_width: 1.5
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
lines = Box2DDebugLines.new
|
|
54
|
+
stepper = Box2D::FixedStepper.new(hz: 60)
|
|
55
|
+
previous_time = 0.0
|
|
56
|
+
rugl.frame do |context|
|
|
57
|
+
delta_time = context[:time] - previous_time
|
|
58
|
+
previous_time = context[:time]
|
|
59
|
+
stepper.advance(delta_time) { |step| world.step(step) }
|
|
60
|
+
|
|
61
|
+
lines.clear
|
|
62
|
+
world.debug_draw(flags: [:shapes, :joints]) { |command| lines << command }
|
|
63
|
+
positions.update(data: lines.positions)
|
|
64
|
+
colors.update(data: lines.colors)
|
|
65
|
+
|
|
66
|
+
rugl.clear(color: [0.04, 0.05, 0.08, 1.0])
|
|
67
|
+
draw_lines.call(count: lines.positions.length)
|
|
68
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "box2d"
|
|
4
|
+
require "stagecraft"
|
|
5
|
+
require_relative "support/box2d_debug_lines"
|
|
6
|
+
|
|
7
|
+
world = nil
|
|
8
|
+
app = nil
|
|
9
|
+
at_exit do
|
|
10
|
+
world&.destroy if world&.valid?
|
|
11
|
+
if app && !app.renderer.disposed?
|
|
12
|
+
app.renderer.dispose
|
|
13
|
+
app.window.close
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
world = Box2D::World.new
|
|
18
|
+
world.create_body(position: [0, -1]) { |body| body.box(8, 1) }
|
|
19
|
+
10.times do |index|
|
|
20
|
+
world.create_body(type: :dynamic, position: [(index % 5) - 2, 1 + index / 5.0]) do |body|
|
|
21
|
+
body.circle(radius: 0.4)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
app = Stagecraft::App.new(title: "Box2D debug draw", width: 960, height: 720)
|
|
26
|
+
scene = Stagecraft::Scene.new
|
|
27
|
+
scene.background = "#0a0d14"
|
|
28
|
+
camera = Stagecraft::Cameras::Orthographic.new(
|
|
29
|
+
left: -10, right: 10, top: 12, bottom: -3, near: 0.1, far: 20
|
|
30
|
+
)
|
|
31
|
+
camera.position.z = 10
|
|
32
|
+
|
|
33
|
+
geometry = Stagecraft::Geometry.new(topology: :line_list)
|
|
34
|
+
geometry.set_attribute(:position, data: "".b, format: :float32x3, count: 0)
|
|
35
|
+
geometry.set_attribute(:color, data: "".b, format: :float32x4, count: 0)
|
|
36
|
+
mesh = Stagecraft::Mesh.new(geometry, Stagecraft::Materials::Unlit.new)
|
|
37
|
+
mesh.frustum_culled = false
|
|
38
|
+
scene.add(mesh)
|
|
39
|
+
|
|
40
|
+
lines = Box2DDebugLines.new
|
|
41
|
+
stepper = Box2D::FixedStepper.new(hz: 60)
|
|
42
|
+
app.run do |delta_time|
|
|
43
|
+
stepper.advance(delta_time) { |step| world.step(step) }
|
|
44
|
+
lines.clear
|
|
45
|
+
world.debug_draw(flags: [:shapes, :joints]) { |command| lines << command }
|
|
46
|
+
|
|
47
|
+
next_geometry = Stagecraft::Geometry.new(topology: :line_list)
|
|
48
|
+
next_geometry.set_attribute(
|
|
49
|
+
:position,
|
|
50
|
+
data: lines.positions.flat_map { |x, y| [x, y, 0.0] }.pack("e*"),
|
|
51
|
+
format: :float32x3,
|
|
52
|
+
count: lines.positions.length
|
|
53
|
+
)
|
|
54
|
+
next_geometry.set_attribute(
|
|
55
|
+
:color,
|
|
56
|
+
data: lines.colors.flatten.pack("e*"),
|
|
57
|
+
format: :float32x4,
|
|
58
|
+
count: lines.colors.length
|
|
59
|
+
)
|
|
60
|
+
mesh.geometry = next_geometry
|
|
61
|
+
geometry.dispose
|
|
62
|
+
geometry = next_geometry
|
|
63
|
+
app.renderer.render(scene, camera)
|
|
64
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Box2DDebugLines
|
|
4
|
+
CIRCLE_SEGMENTS = 24
|
|
5
|
+
|
|
6
|
+
attr_reader :positions, :colors
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
clear
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clear
|
|
13
|
+
@positions = []
|
|
14
|
+
@colors = []
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def <<(command)
|
|
19
|
+
type, *arguments = command
|
|
20
|
+
case type
|
|
21
|
+
when :polygon
|
|
22
|
+
points, color = arguments
|
|
23
|
+
add_loop(points, color)
|
|
24
|
+
when :solid_polygon
|
|
25
|
+
transform, points, _radius, color = arguments
|
|
26
|
+
add_loop(transform_points(points, transform), color)
|
|
27
|
+
when :circle
|
|
28
|
+
center, radius, color = arguments
|
|
29
|
+
add_circle(center, radius, color)
|
|
30
|
+
when :solid_circle
|
|
31
|
+
transform, radius, color = arguments
|
|
32
|
+
add_circle(transform.fetch(:position), radius, color)
|
|
33
|
+
when :solid_capsule
|
|
34
|
+
point1, point2, radius, color = arguments
|
|
35
|
+
add_capsule(point1, point2, radius, color)
|
|
36
|
+
when :segment
|
|
37
|
+
point1, point2, color = arguments
|
|
38
|
+
add_segment(point1, point2, color)
|
|
39
|
+
when :transform
|
|
40
|
+
add_transform(arguments.fetch(0))
|
|
41
|
+
when :point
|
|
42
|
+
point, _size, color = arguments
|
|
43
|
+
add_point(point, color)
|
|
44
|
+
end
|
|
45
|
+
self
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def add_loop(points, color)
|
|
51
|
+
return if points.empty?
|
|
52
|
+
|
|
53
|
+
points.each_with_index do |point, index|
|
|
54
|
+
add_segment(point, points[(index + 1) % points.length], color)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def add_circle(center, radius, color)
|
|
59
|
+
points = CIRCLE_SEGMENTS.times.map do |index|
|
|
60
|
+
angle = index * Math::PI * 2 / CIRCLE_SEGMENTS
|
|
61
|
+
[center[0] + Math.cos(angle) * radius, center[1] + Math.sin(angle) * radius]
|
|
62
|
+
end
|
|
63
|
+
add_loop(points, color)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def add_capsule(point1, point2, radius, color)
|
|
67
|
+
add_circle(point1, radius, color)
|
|
68
|
+
add_circle(point2, radius, color)
|
|
69
|
+
dx = point2[0] - point1[0]
|
|
70
|
+
dy = point2[1] - point1[1]
|
|
71
|
+
length = Math.hypot(dx, dy)
|
|
72
|
+
return if length.zero?
|
|
73
|
+
|
|
74
|
+
normal = [-dy * radius / length, dx * radius / length]
|
|
75
|
+
add_segment(offset(point1, normal), offset(point2, normal), color)
|
|
76
|
+
add_segment(offset(point1, normal, -1), offset(point2, normal, -1), color)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def add_transform(transform)
|
|
80
|
+
origin = transform.fetch(:position)
|
|
81
|
+
angle = transform.fetch(:angle)
|
|
82
|
+
x_axis = [origin[0] + Math.cos(angle) * 0.4, origin[1] + Math.sin(angle) * 0.4]
|
|
83
|
+
y_axis = [origin[0] - Math.sin(angle) * 0.4, origin[1] + Math.cos(angle) * 0.4]
|
|
84
|
+
add_segment(origin, x_axis, 0xFF0000)
|
|
85
|
+
add_segment(origin, y_axis, 0x00FF00)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def add_point(point, color)
|
|
89
|
+
add_segment([point[0] - 0.05, point[1]], [point[0] + 0.05, point[1]], color)
|
|
90
|
+
add_segment([point[0], point[1] - 0.05], [point[0], point[1] + 0.05], color)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_segment(point1, point2, color)
|
|
94
|
+
rgba = hex_color(color)
|
|
95
|
+
@positions.push(point1.to_a, point2.to_a)
|
|
96
|
+
@colors.push(rgba, rgba)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def transform_points(points, transform)
|
|
100
|
+
position = transform.fetch(:position)
|
|
101
|
+
angle = transform.fetch(:angle)
|
|
102
|
+
cosine = Math.cos(angle)
|
|
103
|
+
sine = Math.sin(angle)
|
|
104
|
+
points.map do |x, y|
|
|
105
|
+
[
|
|
106
|
+
position[0] + x * cosine - y * sine,
|
|
107
|
+
position[1] + x * sine + y * cosine
|
|
108
|
+
]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def offset(point, normal, direction = 1)
|
|
113
|
+
[point[0] + normal[0] * direction, point[1] + normal[1] * direction]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def hex_color(value)
|
|
117
|
+
color = Integer(value) & 0xFFFFFF
|
|
118
|
+
[
|
|
119
|
+
((color >> 16) & 0xFF) / 255.0,
|
|
120
|
+
((color >> 8) & 0xFF) / 255.0,
|
|
121
|
+
(color & 0xFF) / 255.0,
|
|
122
|
+
1.0
|
|
123
|
+
]
|
|
124
|
+
end
|
|
125
|
+
end
|