Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Saturday, 1 November 2014

Creating a gem wrapper for jbox2d in ruby-processing

The mechanism for including Daniel Shiffmans PBox2D for processing into ruby-processing is unnecessarily complicated, and I had always thought there should be an easier way. So here it is jbox2d wrapped as a gem using some jruby sugar (and avoiding the nasty reflection nonsense that Dan used, although under the hood jruby may do something similar).
The ruby-processing sketch
require 'pbox2d'
require_relative 'lib/particle_system'
attr_reader :box2d, :boundaries, :systems

def setup
  size(400,300)
  @box2d = Box2D.new(self)
  box2d.create_world
  # We are setting a custom gravity
  box2d.gravity(0, -20)

  # Create Arrays
  @systems = []
  @boundaries = []
  # Add a bunch of fixed boundaries
  boundaries << Boundary.new(box2d, 50, 100, 300, 5, -0.3)
  boundaries << Boundary.new(box2d, 250, 175, 300, 5, 0.5)
end

def draw
  background(255)
  # Run all the particle systems
  if systems.size > 0
    systems.each do |system|
      system.run
      system.add_particles(box2d, rand(0..2))
    end
  end
  # Display all the boundaries
  boundaries.each(&:display)
end

def mouse_pressed
  # Add a new Particle System whenever the mouse is clicked
  systems << ParticleSystem.new(box2d, 0, mouse_x, mouse_y)
end

The associated (local) library
require 'forwardable'

module Runnable
  def run
    reject! { |item| item.done }
    each { |item| item.display }
  end
end

class ParticleSystem
  include Enumerable, Runnable
  extend Forwardable
  def_delegators(:@particles, :each, :reject!, :<<, :empty?)
  def_delegator(:@particles, :empty?, :dead?)

  attr_reader :x, :y

  def initialize(bd, num, x, y)
    @particles = []          # Initialize the Array
    @x, @y = x, y            # Store the origin point  
    num.times do
      self << Particle.new(bd, x, y)
    end
  end

  def add_particles(bd, n)
    n.times do
      self << Particle.new(bd, x, y)
    end
  end
end

# A Particle
require 'pbox2d'

class Particle
  include Processing::Proxy, PB
  TRAIL_SIZE = 6
  # We need to keep track of a Body

  attr_reader :trail, :body, :box2d

  # Constructor
  def initialize(b2d, x, y)
    @box2d = b2d
    @trail = Array.new(TRAIL_SIZE, [x, y])
    # Add the box to the box2d world
    # Here's a little trick, let's make a tiny tiny radius
    # This way we have collisions, but they don't overwhelm the system
    make_body(PB::Vec2.new(x, y), 0.2)
  end

  # This function removes the particle from the box2d world
  def kill_body
    box2d.destroy_body(body)
  end

  # Is the particle ready for deletion?
  def done
    # Let's find the screen position of the particle
    pos = box2d.body_coord(body)
    # Is it off the bottom of the screen?
    return false unless (pos.y > $app.height + 20)
    kill_body
    true
  end

  # Drawing the box
  def display
    # We look at each body and get its screen position
    pos = box2d.body_coord(body)
    # Keep track of a history of screen positions in an array
    (TRAIL_SIZE - 1).times do |i|
      trail[i] = trail[i + 1]
    end
    trail[TRAIL_SIZE - 1] = [pos.x, pos.y]
    # Draw particle as a trail
    begin_shape
    no_fill
    stroke_weight(2)
    stroke(0, 150)
    trail.each do |v|
      vertex(v[0], v[1])
    end
    end_shape
  end

  # This function adds the rectangle to the box2d world
  def make_body(center, r)
    # Define and create the body
    bd = PB::BodyDef.new
    bd.type = PB::BodyType::DYNAMIC
    bd.position.set(box2d.processing_to_world(center))
    @body = box2d.create_body(bd)
    # Give it some initial random velocity
    body.set_linear_velocity(PB::Vec2.new(rand(-1.0..1), rand(-1.0..1)))
    # Make the body's shape a circle
    cs = PB::CircleShape.new
    cs.m_radius = box2d.scale_to_world(r)
    fd = PB::FixtureDef.new
    fd.shape = cs
    fd.density = 1
    fd.friction = 0  # Slippery when wet!
    fd.restitution = 0.5
    # We could use this if we want to turn collisions off
    # cd.filter.groupIndex = -10
    # Attach fixture to body
    body.create_fixture(fd)
  end
end

class Boundary
  include Processing::Proxy, PB
  attr_reader :box2d, :b, :x, :y, :w, :h

  def initialize(b2d, x, y, w, h, a)
    @box2d, @x, @y, @w, @h = b2d, x, y, w, h
    # Define the polygon
    sd = PB::PolygonShape.new
    # Figure out the box2d coordinates
    box2d_w = box2d.scale_to_world(w / 2)
    box2d_h = box2d.scale_to_world(h / 2)
    # We're just a box
    sd.set_as_box(box2d_w, box2d_h)
    # Create the body
    bd = PB::BodyDef.new
    bd.type = PB::BodyType::STATIC
    bd.angle = a
    bd.position.set(box2d.processing_to_world(x, y))
    @b = box2d.create_body(bd)
    # Attached the shape to the body using a Fixture
    b.create_fixture(sd, 1)
  end

  # Draw the boundary, it doesn't move so we don't have to ask the Body for location
  def display
    fill(0)
    stroke(0)
    stroke_weight(1)
    rect_mode(CENTER)
    a = b.get_angle
    push_matrix
    translate(x, y)
    rotate(-a)
    rect(0, 0, w, h)
    pop_matrix
  end
end

1 comment:

  1. This comment has been removed by a blog administrator.

    ReplyDelete

Followers

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2