Class: RGame::Engine::Components::CollisionWorld

Inherits:
RGame::Engine::Component show all
Defined in:
lib/rgame/engine/components/collision_world.rb

Overview

Scene-scoped broadphase collision system: a Component that lives on the scene node (so it is born and torn down with the scene, and rides the normal update traversal). CircleColliders register/unregister with it via their tree lifecycle; each update it buckets them in a SpatialHash and fires on_hit on every overlapping pair. It is layer-agnostic — it reports contacts and lets the colliders' owners decide meaning. See docs/api/systems.md.

Instance Attribute Summary

Attributes inherited from RGame::Engine::Component

#node

Instance Method Summary collapse

Methods inherited from RGame::Engine::Component

#context, #control, #draw, #on_attach, #on_detach, #sweep_freed

Methods included from Signal::DSL

#signal

Constructor Details

#initialize(cell_size:) ⇒ CollisionWorld

Returns a new instance of CollisionWorld.



13
14
15
16
17
# File 'lib/rgame/engine/components/collision_world.rb', line 13

def initialize(cell_size:)
  super()
  @hash = Engine::SpatialHash.new(cell_size: cell_size)
  @colliders = []
end

Instance Method Details

#nearest(x, y, r, layer: nil) ⇒ Object

The registered collider nearest to (x, y) within range r, or nil when none qualifies. Restrict to a single layer: (the common case: a tower targeting only :enemy). Dup-safe — it keeps the running minimum, so #query_circle's possible multi-cell repeats don't matter. Allocation-free.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/rgame/engine/components/collision_world.rb', line 45

def nearest(x, y, r, layer: nil)
  best = nil
  best_d2 = nil
  query_circle(x, y, r) do |collider|
    next if layer && collider.layer != layer

    dx = collider.cx - x
    dy = collider.cy - y
    d2 = (dx * dx) + (dy * dy)
    next unless best_d2.nil? || d2 < best_d2

    best = collider
    best_d2 = d2
  end
  best
end

#query_circle(x, y, r) ⇒ Object

Yield every registered collider whose centre lies within r of (x, y), using the spatial index built by the most recent #update. The narrowphase is a centre-distance test — the query is a point + range (a tower's range ring), so the collider's own radius isn't added in. Colliders whose node is queued for removal are skipped. As with SpatialHash#query a collider spanning several cells may be yielded more than once, so callers that select (e.g. #nearest) are written dup-insensitively. Layer-agnostic — filter by collider.layer in the block. Allocation-free.



30
31
32
33
34
35
36
37
38
39
# File 'lib/rgame/engine/components/collision_world.rb', line 30

def query_circle(x, y, r)
  r2 = r * r
  @hash.query_circle(x, y, r) do |collider|
    next if collider.node.freed?

    dx = collider.cx - x
    dy = collider.cy - y
    yield collider if (dx * dx) + (dy * dy) <= r2
  end
end

#register(collider) ⇒ Object



19
# File 'lib/rgame/engine/components/collision_world.rb', line 19

def register(collider) = @colliders << collider

#unregister(collider) ⇒ Object



20
# File 'lib/rgame/engine/components/collision_world.rb', line 20

def unregister(collider) = @colliders.delete(collider)

#update(_dt) ⇒ Object



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/rgame/engine/components/collision_world.rb', line 62

def update(_dt)
  @hash.clear
  @colliders.each { |c| insert(c) }

  # Index-bounded over the count at frame start: an on_hit handler may spawn
  # entities (a rock splitting), which `register`s new colliders mid-loop; those
  # appended ones are skipped this frame (processed next) rather than mutating
  # the array being iterated. The hash was built before the loop, so they're
  # absent from queries too — consistent.
  count = @colliders.size
  i = 0
  while i < count
    a = @colliders[i]
    i += 1
    next if a.node.freed?

    r = a.radius
    d = r * 2
    @hash.query(a.cx - r, a.cy - r, d, d) do |b|
      # object_id ordering visits each unordered pair once (and skips self);
      # the freed? guards skip nodes already queued for removal, so a dead
      # entity stops colliding and duplicate (multi-cell) yields are ignored.
      next if a.node.freed? || b.node.freed? || a.object_id >= b.object_id
      next unless a.overlap?(b)

      a.emit_hit(b)
      b.emit_hit(a)
    end
  end
end